This commit is contained in:
Tretiner 2024-07-26 20:49:10 +05:00
commit f2db04033d
37 changed files with 1782 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/

13
.idea/.idea.SoulstormReplayReader/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/projectSettingsUpdater.xml
/modules.xml
/contentModel.xml
/.idea.SoulstormReplayReader.iml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
</project>

View File

@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/modules.xml
/projectSettingsUpdater.xml
/.idea.SoulstormReplayReader.iml
/contentModel.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
</project>

View File

@ -0,0 +1,6 @@
namespace SoulstormReplayReader.Core;
public static class Consts
{
public const int PlayersMaxCount = 8;
}

View File

@ -0,0 +1,86 @@
namespace SoulstormReplayReader.Core.Domain.Action;
public sealed class GameActionModel(int tick) : IGameAction
{
public int Tick { get; } = tick;
public int PlayerId { get; set; }
public int PlayerActionCount { get; set; }
public int Cmd { get; set; }
public byte Arg { get; set; }
public short SubCmd { get; set; }
public byte SomeByte { get; set; }
public bool ShiftPressed { get; set; }
public string Name => GetName(Cmd);
public override string ToString() => $"{Cmd,3} {Arg,3} {SubCmd:x} {SomeByte:x} {ShiftPressed,5}";
private static string GetName(int playerCommand) => playerCommand switch
{
0 => "Moved unit",
2 => "Destroyed building",
3 => "Train unit",
4 => "Canceled research",
6 => "Research started",
7 => "Building is upgrading",
10 => "UI building attack",
14 => "Point of gathering",
18 => "Changed building behavior",
20 => "Building abils?? (laud hailer)",
24 => "Auto train",
26 => "Get from building",
28 => "[eldar] Teleport building",
56 => "UI move",
57 => "UI stop",
58 => "Delete unit",
62 => "Clicked to reinforce",
63 => "Training squad leader",
64 => "Weapon upgrade row:1 col:$Arg",
65 => "Weapon reinforce (->64)",
66 => "UI attack ground",
67 => "UI attack melee",
68 => "Changed troops behavior",
69 => "UI melee/range",
70 => "UI attack move", // 0x09EB1DB8=attackmove_modal
71 => "Jump/Teleport",
73 => "Turn on ability",
74 => "Connect leader",
75 => "Disconnect leader",
77 => "Squad leader auto reinforce",
78 => "Auto reinforce",
79 => "Auto reinforce weapon (->64)",
80 => "Building repair",
82 => "Land troops",
84 => "[ork] Skwiggott stomps",
85 => "Cargo unit button",
89 => "[necr] Start collecting bodies",
90 => "[necr] Stop collecting bodies",
91 => "[necr] Summon nw from spider",
94 => "[necr] Summon attack scarabs",
97 => "[necr] Possess the tech",
107 => "[eld] Dance of death",
100 => "[necr] Summon lie monolith",
103 => "[necr] Cast stasis field",
110 => "[necr] Deceived",
112 => "[necr] Turn in nightbringer",
113 => "[necr] Turn in deceiver",
117 => "Building started",
126 => "All overwatch",
127 => "Cancel all overwatch",
_ => "UNKNOWN"
};
}
/*
auto reinforce flames
auto reinforce
cancel auto reinforce
cancel auto reinforce flames
auto reinforce flames
auto reinforce
cancel auto reinforce
click reinforce
auto reinforce
*/

View File

@ -0,0 +1,15 @@
namespace SoulstormReplayReader.Core.Domain.Action;
public interface IGameAction
{
public int Tick { get; }
public int PlayerId { get; set; }
public int PlayerActionCount { get; set; }
public int Cmd { get; set; }
public byte Arg { get; set; }
public short SubCmd { get; set; }
public byte SomeByte { get; set; }
public bool ShiftPressed { get; set; }
}

View File

@ -0,0 +1,15 @@
using SoulstormReplayReader.Core.Domain.Action;
namespace SoulstormReplayReader.Core.Domain.BugCheckers;
public abstract class BugCheckerBase(int playerId, RaceEnum playerRace)
{
public readonly int PlayerId = playerId;
public readonly RaceEnum PlayerRace = playerRace;
public abstract void Check(IGameAction action);
public bool IsCompleted { get; set; }
public string Accusation { get; set; }
public bool HasAccusation => !string.IsNullOrEmpty(Accusation);
}

View File

@ -0,0 +1,61 @@
using SoulstormReplayReader.Core.Domain.Action;
using SoulstormReplayReader.Core.Utils;
namespace SoulstormReplayReader.Core.Domain.BugCheckers;
public sealed class FastT2Checker(int playerId, RaceEnum playerRace) : BugCheckerBase(playerId, playerRace)
{
private byte _gensBuilt = 0;
private bool _isAspectPortalBuilt = false;
private bool _isSoulShrineBuilt = false;
private bool _isListenPostBuilt = false;
private bool _isMachineryBuilt = false;
public override void Check(IGameAction action)
{
if (IsCompleted)
return;
if (PlayerId != action.PlayerId)
return;
if (action.Cmd != 117)
return;
switch (action.Arg)
{
case 24:
_gensBuilt++;
break;
case 14:
_isListenPostBuilt = true;
break;
case 7:
_isAspectPortalBuilt = true;
break;
case 16:
_isSoulShrineBuilt = true;
break;
case 22:
_isMachineryBuilt = true;
break;
}
if (_isMachineryBuilt)
{
if (action.Tick <= (2 * 60 + 30) * 8)
{
IsCompleted = true;
Accusation = "FastT2: machinery build started in under 2:30 minutes";
}
else if (_gensBuilt >= 3 && _isSoulShrineBuilt && !_isAspectPortalBuilt)
{
IsCompleted = true;
var time = TicksFormatter.Format(action.Tick, TicksFormatter.Pattern.Hms);
Accusation = $"FastT2: machinery build started in {time} but there is no aspect portal";
if (!_isListenPostBuilt)
Accusation += " and listening posts";
}
}
}
}

View File

@ -0,0 +1,37 @@
using SoulstormReplayReader.Core.Domain.Action;
namespace SoulstormReplayReader.Core.Domain.BugCheckers;
public sealed class GenBugChecker(int playerId, RaceEnum playerRace) : BugCheckerBase(playerId, playerRace)
{
private const int TicksRange = 4;
private const int GensToTrigger = 3;
private int _startTick;
private int _gensBuilt;
public override void Check(IGameAction action)
{
if (IsCompleted)
return;
if (PlayerId != action.PlayerId)
return;
if (action.Cmd != 117 || action.Arg != 141)
return;
if (_startTick == 0 || action.Tick - _startTick > TicksRange)
{
_startTick = action.Tick;
_gensBuilt = 0;
return;
}
if (++_gensBuilt == GensToTrigger)
{
Accusation = $"GenBug: {GensToTrigger}+ gens added in under {TicksRange / 8.0} sec";
IsCompleted = true;
}
}
}

View File

@ -0,0 +1,36 @@
using SoulstormReplayReader.Core.Utils;
namespace SoulstormReplayReader.Core.Domain.ChatMessage;
public readonly struct ChatMessageModel
{
public readonly int Ticks;
public readonly string SenderName;
public readonly int SenderId;
public readonly string Text;
public readonly SenderType From;
public readonly ReceiverType To;
public ChatMessageModel(
int ticks,
string senderName,
int senderId,
string text,
SenderType from,
ReceiverType to
)
{
Ticks = ticks;
SenderName = senderName;
SenderId = senderId;
Text = text;
From = from;
To = to;
}
public string FormattedTime => TicksFormatter.ShortFormat(Ticks);
public string FromTo => From == SenderType.System ? "(All) System:" : $"({To}) {SenderName}:";
public override string ToString() => $"[{FormattedTime}] {FromTo} {Text}";
}

View File

@ -0,0 +1,8 @@
namespace SoulstormReplayReader.Core.Domain.ChatMessage;
public enum ReceiverType : byte
{
All = 0,
Team,
System
}

View File

@ -0,0 +1,8 @@
namespace SoulstormReplayReader.Core.Domain.ChatMessage;
public enum SenderType : byte
{
Player = 0,
Observer,
System
}

View File

@ -0,0 +1,17 @@
namespace SoulstormReplayReader.Core.Domain;
public sealed class GameSettings
{
public byte AiDifficulty { get; set; }
public byte StartResources { get; set; }
public byte LockTeams { get; set; }
public byte CheatsOn { get; set; }
public byte StartingLocation { get; set; }
public byte GameSpeed { get; set; }
public byte ResourceSharing { get; set; }
public byte ResourceRate { get; set; }
public bool IsQuickStart => StartResources == 1;
public bool AreCheatsOn => CheatsOn == 1;
public bool ArePositionsRandomized => StartingLocation == 0;
}

View File

@ -0,0 +1,14 @@
using SoulstormReplayReader.Core.Models;
namespace SoulstormReplayReader.Core.Domain.Player;
public sealed class ColorScheme
{
public string Name { get; set; }
public ReplayColor Primary { get; set; }
public ReplayColor Secondary { get; set; }
public ReplayColor Trim { get; set; }
public ReplayColor Weapons { get; set; }
public ReplayColor Eyes { get; set; }
}

View File

@ -0,0 +1,40 @@
using System.Text;
using SoulstormReplayReader.Core.Domain.Action;
using SoulstormReplayReader.Core.Domain.BugCheckers;
namespace SoulstormReplayReader.Core.Domain.Player;
public class PlayerBugChecker
{
private readonly List<BugCheckerBase> _bugCheckers = new();
public PlayerBugChecker Add(BugCheckerBase checker)
{
_bugCheckers.Add(checker);
return this;
}
public void Check(IGameAction action)
{
_bugCheckers.ForEach(bugChecker => bugChecker.Check(action));
}
public bool HasAccusations() => _bugCheckers.Any(checker => checker.HasAccusation);
public string GetAccusationsList()
{
var sb = new StringBuilder();
foreach (var checker in _bugCheckers.Where(checker => checker.HasAccusation))
{
sb.AppendLine(checker.Accusation);
}
var accusationsList = sb.ToString();
if (string.IsNullOrEmpty(accusationsList))
return null;
return accusationsList;
}
}

View File

@ -0,0 +1,57 @@
using SoulstormReplayReader.Core.Enums;
using SoulstormReplayReader.Core.Models;
namespace SoulstormReplayReader.Core.Domain.Player;
public sealed class PlayerModel
{
public static readonly PlayerModel Empty = new();
public PlayerBugChecker BugChecker = null;
public int Id { get; set; }
public string Name { get; set; }
public int RawType { get; set; }
public string RawRace { get; set; }
public int Team { get; set; }
public ColorScheme ColorScheme { get; set; }
public ReplayImage Banner { get; set; }
public ReplayImage Badge { get; set; }
public ReplayPlayerType ResolvedType => RawType switch
{
_ when Name is null && RawRace is null => ReplayPlayerType.EmptySlot,
0 => ReplayPlayerType.Host,
1 => ReplayPlayerType.Computer,
2 => ReplayPlayerType.Player,
3 => ReplayPlayerType.Computer,
4 => ReplayPlayerType.Spectator,
7 => ReplayPlayerType.EmptySlot,
11 => ReplayPlayerType.Computer,
_ => ReplayPlayerType.EmptySlot
};
public RaceEnum Race => RawRace?.ToLowerInvariant() switch
{
"space_marine_race" => RaceEnum.SpaceMarines,
"ork_race" => RaceEnum.Orks,
"eldar_race" => RaceEnum.Eldar,
"chaos_marine_race" => RaceEnum.ChaosSpaceMarines,
"dark_eldar_race" => RaceEnum.DarkEldar,
"necron_race" => RaceEnum.Necrons,
"sisters_race" => RaceEnum.SistersOfBattle,
"tau_race" => RaceEnum.Tau,
"guard_race" => RaceEnum.ImperialGuard,
_ => RaceEnum.Unknown
};
public string RaceName => RawRace[..^5].Replace('_', ' ');
public string NameTrimNewLines => Name.Replace("\t", "").Split('\r', '\n')[0];
public bool IsHost => ResolvedType is ReplayPlayerType.Host;
public bool IsActive => ResolvedType is ReplayPlayerType.Player or ReplayPlayerType.Host or ReplayPlayerType.Computer;
public bool IsSpectator => ResolvedType is ReplayPlayerType.Spectator;
public bool IsEmpty => string.IsNullOrEmpty(Name) || ResolvedType is ReplayPlayerType.EmptySlot;
}

View File

@ -0,0 +1,15 @@
namespace SoulstormReplayReader.Core.Domain;
public enum RaceEnum
{
Unknown = 0,
SpaceMarines,
Orks,
Eldar,
ChaosSpaceMarines,
DarkEldar,
Necrons,
SistersOfBattle,
Tau,
ImperialGuard
}

View File

@ -0,0 +1,8 @@
namespace SoulstormReplayReader.Core.Domain.Replay;
public sealed class Map
{
public string Name { get; set; }
public int MaxPlayersCount { get; set; }
public int Size { get; set; }
}

View File

@ -0,0 +1,34 @@
using SoulstormReplayReader.Core.Domain.Action;
using SoulstormReplayReader.Core.Domain.ChatMessage;
using SoulstormReplayReader.Core.Domain.Player;
using SoulstormReplayReader.Core.Enums;
namespace SoulstormReplayReader.Core.Domain.Replay;
public sealed class ReplayModel
{
public ReplayVersion Version { get; set; }
public string IngameName { get; set; }
public string ModName { get; set; }
public string EngineName { get; set; }
public string EngineAddon { get; set; } // Не очень понятно что это
public uint WorldSeed { get; set; }
public int TotalTicks { get; set; }
public Map Map { get; } = new();
public List<PlayerModel> Players { get; set; }
public List<IGameAction> Actions { get; set; }
public List<ChatMessageModel> ChatMessages { get; set; }
public GameSettings GameSettings { get; } = new();
public WinConditions WinConditions { get; } = new();
public Exception Exception { get; set; }
public IEnumerable<PlayerModel> ActivePlayers => Players.Where(x => x.IsActive);
public int ActivePlayersCount => Players.Count(p => p.IsActive);
public int TeamsCount => Players.Select(player => player.Team).Max();
}

View File

@ -0,0 +1,7 @@
namespace SoulstormReplayReader.Core.Domain.Tick;
public enum TickType: byte
{
Normal = 0,
Extra = 1
}

View File

@ -0,0 +1,75 @@
using System.Text;
namespace SoulstormReplayReader.Core.Domain;
public sealed class WinConditions
{
public enum ConditionValues
{
Annihilate = 1003066394,
SuddenDeath = -1826760460,
Assassinate = 200405640,
EconomicVictory = -242444938,
ControlArea = 735076042,
DestroyHQ = 1509920563,
TakeAndHold = 1959084950,
GameTimer = 69421273
}
// [DWORD] [WIN_CONDITION]
// 767227721 Annihilate
// -1826760460 SuddenDeath
// 200405640 Assassinate
// -242444938 EconomicVictory
// 735076042 ControlArea
// 1509920563 DestroyHQ
// 1959084950 TakeAndHold
// 69421273 GameTime
public bool TakeAndHold { get; set; }
public bool DestroyHQ { get; set; }
public bool ControlArea { get; set; }
public bool EconomicVictory { get; set; }
public bool SuddenDeath { get; set; }
public bool Annihilate { get; set; }
public bool Assassinate { get; set; }
public bool GameTimer { get; set; }
public override string ToString()
{
var sb = new StringBuilder();
if (Annihilate)
sb.Append("Annihilate ");
if (Assassinate)
sb.Append(nameof(Assassinate) + ' ');
if (ControlArea)
sb.Append("ControlArea ");
if (DestroyHQ)
sb.Append("DestroyHQ ");
if (EconomicVictory)
sb.Append("EconomicVictory ");
if (GameTimer)
sb.Append("GameTimer ");
if (SuddenDeath)
sb.Append("SuddenDeath ");
if (TakeAndHold)
sb.Append("TakeAndHold ");
return sb.ToString();
}
}

View File

@ -0,0 +1,10 @@
namespace SoulstormReplayReader.Core.Enums;
public enum ReplayPlayerType
{
EmptySlot = 0,
Host,
Computer,
Spectator,
Player
}

View File

@ -0,0 +1,7 @@
namespace SoulstormReplayReader.Core.Enums;
public enum ReplayVersion : byte
{
_1_2 = 9,
Steam = 10
}

View File

@ -0,0 +1,87 @@
using System.Text;
namespace SoulstormReplayReader.Core.Extensions;
public class ExBinaryReader : BinaryReader
{
public long Position => BaseStream.Position;
public long Length => BaseStream.Length;
public long BytesLeft => Length - Position;
public bool HasBytes => Position < Length;
public ExBinaryReader(Stream stream) : base(stream) { }
public ExBinaryReader(byte[] byteArray) : base(new MemoryStream(byteArray)) { }
public ExBinaryReader SkipInt32(int count = 1) => Skip(count * 4);
public ExBinaryReader Skip(int bytesCount)
{
BaseStream.Seek(bytesCount, SeekOrigin.Current);
return this;
}
public ExBinaryReader Seek(int offset, SeekOrigin seekOrigin = SeekOrigin.Begin)
{
BaseStream.Seek(offset, seekOrigin);
return this;
}
public ExBinaryReader Seek(uint offset, SeekOrigin seekOrigin = SeekOrigin.Begin)
{
BaseStream.Seek(offset, seekOrigin);
return this;
}
public ExBinaryReader Seek(long offset, SeekOrigin seekOrigin = SeekOrigin.Begin)
{
BaseStream.Seek(offset, seekOrigin);
return this;
}
public string ReadNextUnicodeString() => ReadUnicodeString(ReadInt32());
public string ReadUnicodeString(int length)
{
var doubleLength = length * 2;
var bytes = doubleLength switch
{
> 0 and < 0xFF => ReadBytes(stackalloc byte[doubleLength]),
_ => ReadBytes(doubleLength)
};
return Encoding.UTF8.GetString(bytes);
}
public string ReadNextAsciiString() => ReadAsciiString(ReadInt32());
public string ReadAsciiString(int length)
{
var bytes = length switch
{
> 0 and < 0xFF => ReadBytes(stackalloc byte[length]),
_ => ReadBytes(length)
};
return Encoding.ASCII.GetString(bytes);
}
public string ReadCString(int length = 150)
{
var bytes = length switch
{
> 0 and < 0xFF => ReadBytes(stackalloc byte[length]),
_ => ReadBytes(length)
};
bytes = bytes[..bytes.IndexOf(byte.MinValue)];
return Encoding.ASCII.GetString(bytes);
}
public Span<byte> ReadBytes(Span<byte> buffer)
{
BaseStream.ReadExactly(buffer);
return buffer;
}
}

View File

@ -0,0 +1,93 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using SoulstormReplayReader.Core.Domain.Player;
using SoulstormReplayReader.Core.Models;
namespace SoulstormReplayReader.Core.Extensions;
public static class Extensions
{
public static void RequireInRange(this int num, int min, int max, string errorMsg)
{
if (num < min || num > max)
throw new ArgumentOutOfRangeException(errorMsg);
}
public static void Swap<T>(this IList<T> list, int i1, int i2)
{
(list[i1], list[i2]) = (list[i2], list[i1]);
}
public static Span<byte> TrimEnd(this Span<byte> span, byte val = 0)
{
var i = span.Length - 1;
while (i > 0 && span[i--] == val) ;
return span;
}
public static ExBinaryReader NextAsciiStringMustEqual(this ExBinaryReader binReader, string text)
{
if (!binReader.IsNextAsciiStringEquals(text))
{
throw new Exception(text + "Нарушение структуры файла. ");
}
return binReader;
}
public static bool IsNextAsciiStringEquals(this ExBinaryReader binReader, string text)
{
Span<byte> textSpan = Encoding.ASCII.GetBytes(text);
var nextTextSpan = binReader.ReadBytes(stackalloc byte[text.Length]);
return textSpan.Length == nextTextSpan.Length && SpansEqualInvariant(textSpan, nextTextSpan);
static bool SpansEqualInvariant(ReadOnlySpan<byte> span1, ReadOnlySpan<byte> span2)
{
for (var i = 0; i < span1.Length; i++)
{
if (span1[i] == span2[i])
continue;
var char1 = char.ToLower((char)span1[i]);
var char2 = char.ToLower((char)span2[i]);
if (char1 != char2)
{
return false;
}
}
return true;
}
}
public static ReplayColor ReadBgraToArgb(this ExBinaryReader binReader)
{
var bgra = binReader.ReadUInt32();
var argb = ReverseBytes(bgra);
return Unsafe.As<uint, ReplayColor>(ref argb);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint ReverseBytes(uint value) =>
((value & 0x000000FFU) << 24) | ((value & 0x0000FF00U) << 8) |
((value & 0x00FF0000U) >> 8) | ((value & 0xFF000000U) >> 24);
public static string ToContentString(this IEnumerable<byte> bytes) =>
string.Join(' ',
bytes.Select(b => $"{b:x}".PadLeft(2, '0'))
);
[Conditional("DEBUGLOGGING")]
public static void LogPlayers(this List<PlayerModel> players)
{
Console.WriteLine("NAME TEAM TYPE RACE");
foreach (var pl in players.Where(p => p != null))
{
Console.WriteLine($"{pl.Name ?? "",-15} {pl.Team,-8} {pl.RawType} {pl.ResolvedType,-10} {pl.Race}");
}
}
}

View File

@ -0,0 +1,23 @@
namespace SoulstormReplayReader.Core.Models;
public readonly struct ReplayColor
{
public readonly byte A;
public readonly byte R;
public readonly byte G;
public readonly byte B;
private ReplayColor(byte a, byte r, byte g, byte b)
{
A = a;
R = r;
G = g;
B = b;
}
public static ReplayColor FromArgb(params byte[] argb) => new(argb[0], argb[1], argb[2], argb[3]);
public static ReplayColor FromBgra(params byte[] bgra) => new(bgra[3], bgra[2], bgra[1], bgra[0]);
public readonly override string ToString() => $"{A} {R} {G} {B}";
}

View File

@ -0,0 +1,24 @@
namespace SoulstormReplayReader.Core.Models;
/// <summary>
/// Native dow rgd image consists of bgra pixels
/// </summary>
public sealed class ReplayImage
{
public string Name { get; set; }
public int Width { get; private set; }
public int Height { get; private set; }
public ReplayColor[] Bitmap { get; private set; }
public void Init(int width, int height)
{
Width = width;
Height = height;
Bitmap = new ReplayColor[height * width];
}
public void SetPixel(int x, int y, ReplayColor color)
{
Bitmap[y * Width + x] = color;
}
}

View File

@ -0,0 +1,24 @@
namespace SoulstormReplayReader.Core;
public sealed class ReplayDescriptor
{
public int FileNameStart { get; set; }
public long FoldInfoStart { get; set; }
public int FoldInfoSize { get; set; }
public int DataBaseChunkSize { get; set; }
public long BeginDataBaseChunkSize { get; set; }
public long DataBaseStart { get; set; }
public int DataBaseSize { get; set; }
public long FoldWmanStart { get; set; }
public int FoldWmanSize { get; set; }
public long PlayersChunkStart { get; set; }
public List<long> PlayerStartPoses { get; set; }
public long ActionsChunkStart { get; set; }
public long ActionsChunkSize { get; set; }
}

View File

@ -0,0 +1,42 @@
using System.Text;
using SoulstormReplayReader.Core.Extensions;
namespace SoulstormReplayReader.Core.Services;
public static class ReplayService
{
public static void Rename(Stream inStream, Stream outStream, string newName)
{
var binaryReader = new ExBinaryReader(inStream);
var binaryWriter = new BinaryWriter(outStream);
// Костыльное нахождение длины старого имени игры
binaryReader
.Skip(182) // Перемещаюсь на позицию после (GameInfo.FOLDWMAN....)
.Skip(binaryReader.ReadInt32() + 20) // DataSdscSize + 24
.Skip(85);
var bytesLengthDifference = (newName.Length - binaryReader.ReadInt32()) * 2;
binaryReader.Seek(0);
binaryWriter.Write(binaryReader.ReadBytes(149 + 4)); // Перемещаюсь к позиции перед размером FOLDINFO
binaryWriter.Write(binaryReader.ReadInt32() + bytesLengthDifference); // Переписываю, добавляя разность длинн имен
binaryWriter.Write(binaryReader.ReadBytes(21 + 4)); // Иду к позиции перед размером FOLDWMAN
var foldWmanSize = binaryReader.ReadInt32();
binaryWriter.Write(foldWmanSize);
binaryWriter.Write(binaryReader.ReadBytes(foldWmanSize + 16)); // Телепортируюсь к позиции перед размером DataBase
binaryWriter.Write(binaryReader.ReadInt32() + bytesLengthDifference); // Переписываю, добавляя разность длинн имен
binaryWriter.Write(binaryReader.ReadBytes(85)); // Первые 85 байт DataBase статичные
binaryWriter.Write(newName.Length); // После них идет длина имени игры
binaryReader.Skip(binaryReader.ReadInt32() * 2);
binaryWriter.Write(Encoding.Unicode.GetBytes(newName)); // И ее имя
while (binaryReader.HasBytes)
binaryWriter.Write(binaryReader.ReadByte());
binaryWriter.Flush();
}
}

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Configurations>Debug;Release;DebugLogging</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,614 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using SoulstormReplayReader.Core.Domain;
using SoulstormReplayReader.Core.Domain.Action;
using SoulstormReplayReader.Core.Domain.BugCheckers;
using SoulstormReplayReader.Core.Domain.ChatMessage;
using SoulstormReplayReader.Core.Domain.Player;
using SoulstormReplayReader.Core.Domain.Replay;
using SoulstormReplayReader.Core.Domain.Tick;
using SoulstormReplayReader.Core.Enums;
using SoulstormReplayReader.Core.Extensions;
using SoulstormReplayReader.Core.Models;
using SoulstormReplayReader.Core.Utils;
namespace SoulstormReplayReader.Core;
// Существует благодаря
// http://forums.warforge.ru/index.php?showtopic=101573&hl=replay
// а также реплей менеджеру эламаунта :0
/*
* Relic chunk struct:
* - int32 version
* - int32 chunkSize
* - int32 variableStringLength or else
* - ...other data...
*/
public sealed class SsReplayReader(Stream stream) : IDisposable
{
private readonly ExBinaryReader _binaryReader = new(stream);
/// <summary>
/// Если параметр активен, картинки баннера и бейджа игроков не парсятся
/// <para/>
/// Player.Banner.Bitmap = null
/// <para/>
/// Player.Badge.Bitmap = null
/// </summary>
public bool SkipImages { get; set; }
/// <summary>
/// Если параметр активен, на игроков будут повешены BugChecker'ы в зависимости от их расы
/// </summary>
public bool CheckForBugs { get; set; }
public ReplayDescriptor Descriptor { get; } = new();
public ReplayModel Replay { get; set; }
public int CurrentTick { get; set; }
public ReplayModel ReadFull()
{
ReadInfo();
ReadTicks();
return Replay;
}
public ReplayModel ReadInfo()
{
ReadHeader();
ReadPlayers();
return Replay;
}
public ReplayModel ReadHeader()
{
Reset();
ReadStaticHeader(); // 226 - Последняя статичная позиция
ReadDynamicHeader();
return Replay;
}
public void Reset()
{
_binaryReader.Seek(0);
Replay = new ReplayModel();
CurrentTick = 0;
}
private void ReadStaticHeader()
{
Replay.Version = (ReplayVersion)_binaryReader.ReadInt32();
var modNameSpan = _binaryReader.ReadBytes(stackalloc byte[32]);
Replay.ModName = Encoding.ASCII.GetString(modNameSpan.TrimEnd());
_binaryReader // relic chunky
.Skip(44)
.NextAsciiStringMustEqual("POSTGAMEINFO\0DATADATA")
.SkipInt32(3);
Replay.TotalTicks = _binaryReader.ReadInt32(); // Полное игровое время (1 сек = 8 тиков)
_binaryReader // relic chunky
.Skip(24)
.NextAsciiStringMustEqual("FOLDINFO")
.SkipInt32();
Descriptor.FoldInfoStart = _binaryReader.Position; // 153
Descriptor.FoldInfoSize = _binaryReader.ReadInt32();
_binaryReader
.SkipInt32()
.NextAsciiStringMustEqual("GAMEINFO\0FOLDWMAN")
.SkipInt32();
Descriptor.FoldWmanStart = _binaryReader.Position; // 182
Descriptor.FoldWmanSize = _binaryReader.ReadInt32();
_binaryReader
.SkipInt32()
.NextAsciiStringMustEqual("DATASDSC")
.Skip(20);
Replay.Map.MaxPlayersCount = _binaryReader.ReadInt32();
Replay.Map.Size = _binaryReader.ReadInt32();
}
private void ReadDynamicHeader()
{
var modNameLength = _binaryReader.ReadInt32();
modNameLength.RequireInRange(1, 1000, "Не удалось считать название мода");
Replay.EngineAddon = _binaryReader.ReadAsciiString(modNameLength);
// Название движка (?)
var engineNameLength = _binaryReader.ReadInt32();
engineNameLength.RequireInRange(1, 1000, "Не удалось считать название движка");
Replay.EngineName = _binaryReader.ReadUnicodeString(engineNameLength);
var mapNameLength = _binaryReader.ReadInt32();
mapNameLength.RequireInRange(1, 1000, "Не удалось считать название карты");
// DATA:Scenarios\MP\2P_FATA_MORGANA
// Пропускаем DATA:Scenarios\MP\
Replay.Map.Name = _binaryReader.Skip(18).ReadAsciiString(mapNameLength - 18);
_binaryReader
.SkipInt32(4)
.NextAsciiStringMustEqual("DATABASE")
.SkipInt32();
Descriptor.BeginDataBaseChunkSize = _binaryReader.Position;
Descriptor.DataBaseChunkSize = _binaryReader.ReadInt32();
_binaryReader.SkipInt32(3);
Replay.WorldSeed = _binaryReader.ReadUInt32();
_binaryReader.SkipInt32(); // Размер слотов в игре. Всегда 8
Descriptor.DataBaseStart = _binaryReader.Position;
Descriptor.DataBaseSize = Descriptor.DataBaseChunkSize - 20;
ReadGameSettings();
Descriptor.FileNameStart = (int)_binaryReader.Position;
var ingameNameLength = _binaryReader.ReadInt32();
ingameNameLength.RequireInRange(0, 300, "Не удалось считать имя реплея");
Replay.IngameName = _binaryReader.ReadUnicodeString(ingameNameLength);
_binaryReader.SkipInt32(); // 0
var winConditionCount = _binaryReader.ReadInt32();
winConditionCount.RequireInRange(0, 1000, "Не удалось считать условия победы");
var winConditions = MemoryMarshal.Cast<byte, int>(_binaryReader.ReadBytes(stackalloc byte[winConditionCount * 4]));
Replay.WinConditions.Annihilate = winConditions.Contains((int)WinConditions.ConditionValues.Annihilate);
Replay.WinConditions.SuddenDeath = winConditions.Contains((int)WinConditions.ConditionValues.SuddenDeath);
Replay.WinConditions.Assassinate = winConditions.Contains((int)WinConditions.ConditionValues.Assassinate);
Replay.WinConditions.EconomicVictory = winConditions.Contains((int)WinConditions.ConditionValues.EconomicVictory);
Replay.WinConditions.ControlArea = winConditions.Contains((int)WinConditions.ConditionValues.ControlArea);
Replay.WinConditions.DestroyHQ = winConditions.Contains((int)WinConditions.ConditionValues.DestroyHQ);
Replay.WinConditions.TakeAndHold = winConditions.Contains((int)WinConditions.ConditionValues.TakeAndHold);
Replay.WinConditions.GameTimer = winConditions.Contains((int)WinConditions.ConditionValues.GameTimer);
_binaryReader.Skip(5);
Descriptor.PlayersChunkStart = _binaryReader.Position;
}
// Game setting:
// * 0000 - value
// * AAAA - compact name
// res: 0000COLS = (0000)random (COLS=SLOC)Starting locations
private void ReadGameSettings()
{
Replay.GameSettings.AiDifficulty = _binaryReader.ReadByte(); // FDIA = AIDF = AI Difficulty
_binaryReader.Skip(7);
Replay.GameSettings.StartResources = _binaryReader.ReadByte(); // TSSR = RSST = Starting Resources
_binaryReader.Skip(7);
Replay.GameSettings.LockTeams = _binaryReader.ReadByte(); // MTKL = LKTM = Lock Teams
_binaryReader.Skip(7);
Replay.GameSettings.CheatsOn = _binaryReader.ReadByte(); // AEHC = CHEA = Cheats Enabled
_binaryReader.Skip(7);
Replay.GameSettings.StartingLocation = _binaryReader.ReadByte(); // COLS = SLOC = Starting Location
_binaryReader.Skip(7);
Replay.GameSettings.GameSpeed = _binaryReader.ReadByte(); // DPSG = GSPD = Game Speed
_binaryReader.Skip(7);
Replay.GameSettings.ResourceSharing = _binaryReader.ReadByte(); // HSSR = RSSH = Resource Sharing
_binaryReader.Skip(7);
Replay.GameSettings.ResourceRate = _binaryReader.ReadByte(); // TRSR = RSRT = Resource Rate
_binaryReader.Skip(7);
#if DEBUGLOGGING
Console.WriteLine($"{Replay.GameSettings.AiDifficulty} " +
$"{Replay.GameSettings.StartResources} " +
$"{Replay.GameSettings.LockTeams} " +
$"{Replay.GameSettings.CheatsOn} " +
$"{Replay.GameSettings.StartingLocation} " +
$"{Replay.GameSettings.GameSpeed} " +
$"{Replay.GameSettings.ResourceSharing} " +
$"{Replay.GameSettings.ResourceRate}"
);
#endif
_binaryReader.Skip(1); // 0
}
public void ReadPlayers()
{
Descriptor.PlayerStartPoses = new List<long>();
Replay.Players = new List<PlayerModel>(Consts.PlayersMaxCount);
for (var players = 0; players < Consts.PlayersMaxCount; players++)
Replay.Players.Add(ReadPlayer());
if (Replay.GameSettings.ArePositionsRandomized)
{
var randomizer = new PlayersRandomizer(Replay.WorldSeed);
Replay.Players = randomizer.Randomize(Replay.Players, Replay.Map.MaxPlayersCount);
}
Descriptor.ActionsChunkStart = _binaryReader.Position;
Descriptor.ActionsChunkSize = _binaryReader.BytesLeft;
}
private PlayerModel ReadPlayer()
{
// В реплеях c ботами нет пустых мест
if (!_binaryReader.IsNextAsciiStringEquals("FOLDGPLY"))
{
_binaryReader.Skip(-8);
return PlayerModel.Empty;
}
Descriptor.PlayerStartPoses.Add(_binaryReader.Position);
var player = new PlayerModel();
_binaryReader
.Skip(12) // int32 version 2; int32 size; int32 strLen 0;
.NextAsciiStringMustEqual("DATAINFO")
.Skip(12); // int32 version 1; int32 size; int32 strLen 0;
player.Name = _binaryReader.ReadNextUnicodeString();
player.RawType = _binaryReader.ReadInt32(); // Тип игрока: 0 Host/2 player/4 specc/7 empty/1,3,11 computer
player.Team = _binaryReader.ReadInt32() + 1; // Команда игрока (тут начинается с 0 (в игре с 1))
player.RawRace = _binaryReader.ReadNextAsciiString(); // Раса игрока: necron_race
_binaryReader
.Skip(Replay.Version == ReplayVersion.Steam ? 21 : 9) // byte; int32 0; int32 0xFFFFFFFF;
.Skip(_binaryReader.ReadInt32()) // непонятно зачем и почему
.Skip(36);
if (!_binaryReader.HasBytes)
return player;
if (_binaryReader.IsNextAsciiStringEquals("FOLDTCUC"))
{
_binaryReader
.Skip(12) // int32 version 1; int32 size; int32 strLen 0;
.NextAsciiStringMustEqual("DATALCIN")
.Skip(4); // int32 version 2;
_binaryReader.ReadNextAsciiString(); // описание раскрасок отправителя реплея?
// ◄ chaos_marine_race↔ chaos_marine_race/AlphaLegion↔ chaos_marine_race/AlphaLe
// ← inquisition_daemonhunt_race0 inquisition_daemonhunt_race/lodge_magister_badge1 inquisition_daemonhunt_race/lodge_magister_ba
_binaryReader
.Skip(4) // int32 strLen 0;
.NextAsciiStringMustEqual("DATAUNCU") // 42192
.Skip(12); // int32 version 1; int32 size; int32 strLen 0;
player.ColorScheme = new ColorScheme
{
Name = _binaryReader.ReadNextUnicodeString(),
Primary = _binaryReader.ReadBgraToArgb(),
Secondary = _binaryReader.ReadBgraToArgb(),
Trim = _binaryReader.ReadBgraToArgb(),
Weapons = _binaryReader.ReadBgraToArgb(),
Eyes = _binaryReader.ReadBgraToArgb()
};
// Бейдж и\или баннер (могут не существовать или быть перепутаны)
for (var imgNum = 0; imgNum < 2; imgNum++)
{
var imgTypeName = _binaryReader.ReadAsciiString(8);
if (!imgTypeName.StartsWith("FOLDTC")) // fold team color
{
_binaryReader.Skip(-8);
break;
}
if (SkipImages)
{
_binaryReader
.Skip(4)
.Skip(_binaryReader.ReadInt32() + 4);
continue;
}
_binaryReader.Skip(12); // int32 version 1; int32 size; int32 strLen 0;
var playerImage = new ReplayImage();
if (imgTypeName.EndsWith("BN"))
{
player.Banner = playerImage;
}
else
{
player.Badge = playerImage;
}
_binaryReader
.NextAsciiStringMustEqual("FOLDIMAG")
.Skip(8); // int32 version 1; int32 size;
playerImage.Name = _binaryReader.ReadNextAsciiString();
_binaryReader
.NextAsciiStringMustEqual("DATAATTR")
.Skip(16); // int32 version 2; int32 size; 2 * int32 (?) 0;
var width = _binaryReader.ReadInt32();
var height = _binaryReader.ReadInt32();
_binaryReader
.Skip(4) // int32 (?) 1;
.NextAsciiStringMustEqual("DATADATA")
.Skip(12); // int32 version 2; int32 size; int32 (?) 0;
playerImage.Init(width, height);
for (var y = height - 1; y >= 0; y--)
{
for (var x = 0; x < width; x++)
{
playerImage.SetPixel(x, y, _binaryReader.ReadBgraToArgb());
}
}
}
}
else _binaryReader.Skip(-8);
return player;
}
private void ReadTicks()
{
if (Replay.Version < ReplayVersion._1_2) // Действия в старых версиях читаются не правильно
{
Console.WriteLine("SS version must be at least 1.2 to read actions");
return;
}
if (!Replay.GameSettings.IsQuickStart && !Replay.GameSettings.AreCheatsOn && CheckForBugs)
AttachBugCheckers();
Replay.Actions = new List<IGameAction>(Replay.TotalTicks / 2);
Replay.ChatMessages = new List<ChatMessageModel>();
// Иногда в конце реплея появляются остаточные байты
while (_binaryReader.BytesLeft >= 17)
ReadTick();
#if DEBUGLOGGING
if (_binaryReader.HasBytes)
Console.WriteLine($"bytes left: {_binaryReader.BytesLeft} at pos: {_binaryReader.Position}");
#endif
}
private void AttachBugCheckers()
{
for (var playerId = 0; playerId < Replay.Players.Count; playerId++)
{
var player = Replay.Players[playerId];
if (!player.IsActive) continue;
player.Id = playerId;
player.BugChecker = player.Race switch
{
RaceEnum.Necrons => new PlayerBugChecker()
.Add(new GenBugChecker(player.Id, player.Race)),
RaceEnum.Eldar => new PlayerBugChecker()
.Add(new FastT2Checker(player.Id, player.Race)),
_ => null
};
}
}
// структура тика:
// tick:
// - header
// - player chunk 1:
// - - action 1
// - - action 2
// - player chunk 2:
// - - action 1
private void ReadTick()
{
var tickType = (TickType)_binaryReader.ReadInt32();
var tickSize = _binaryReader.ReadInt32();
if (tickType == TickType.Normal)
{
if (tickSize == 17)
SkipEmptyTick();
else
ReadOrdinaryTick();
CurrentTick++;
}
else if (tickType == TickType.Extra)
ReadExtraTick();
else
ReadWierdTick((int)tickType, tickSize);
}
// Всегда 17 байт
private void SkipEmptyTick()
{
_binaryReader.Skip(17);
}
private void ReadOrdinaryTick()
{
// -- TICK HEADER --
_binaryReader.Skip(1); // 0x50
var tickCount = _binaryReader.ReadInt32();
_binaryReader.Skip(8); // int32 "Номер действия игрока" ??? + int32 Random
var playerChunksCount = _binaryReader.ReadInt32();
// -- TICK BODY --
for (var i = 0; i < playerChunksCount; i++) // для каждого игрока предусмотрен свой чанк со своим размером
{
// -- PLAYER CHUNK HEADER --
_binaryReader.Skip(8); // ???
var playerChunkSize = _binaryReader.ReadInt32();
_binaryReader.Skip(1); // то же самое
// -- PLAYER CHUNK BODY --
while (playerChunkSize != 0)
{
var actionSize = _binaryReader.ReadInt16(); // размер захватывает 2 байта след чанка
var action = ReadPlayerAction(tickCount, actionSize);
Replay.Actions.Add(action);
playerChunkSize -= actionSize;
}
}
}
private GameActionModel ReadPlayerAction(int tickCount, int actionSize)
{
var actionEndPos = _binaryReader.Position + actionSize - 2;
// -- ACTION HEADER --
_binaryReader.SkipInt32(); // какое то время (свое для каждого игрока)
// (начинается с рандомного значения и увеличивается по ходу игры)
// Скорее всего является global timer
// var s_cmd = _binaryReader.ReadInt32();
// var s_param = _binaryReader.ReadInt32();
// var s_shiftPressed = _binaryReader.ReadByte() == 1;
// var s_playerId = _binaryReader.Skip(2).ReadInt16() % 10;
//
//
// var leftOverBytes2 = actionEndPos - _binaryReader.Position;
// Console.WriteLine(_binaryReader.ReadBytes((int)leftOverBytes2).ToContentString());
// return new GameActionModel(1);
var cmd = _binaryReader.ReadInt32();
var arg = _binaryReader.ReadByte(); // int32 in game
var subCmd = _binaryReader.ReadInt16();
var someNum = _binaryReader.ReadByte();
var shiftPressed = _binaryReader.ReadByte() == 1;
var playerId = _binaryReader.Skip(2).ReadInt16() % 10;
var playerActionCount = _binaryReader.ReadInt16();
#if DEBUGLOGGING
var selectedCount = _binaryReader.ReadByte();
var actionTypeNum = _binaryReader.ReadByte();
var actionType = actionTypeNum switch
{
1 => "1.Builder",
2 => "2.Building",
3 => "3.Army",
_ => throw new ArgumentOutOfRangeException($"Action type num: {actionTypeNum} was not recognized")
};
var primaryId = _binaryReader.ReadInt32();
var actionBodyDataType = _binaryReader.ReadByte();
#else
_binaryReader.Skip(7);
#endif
var curAction = new GameActionModel(tickCount)
{
Cmd = cmd,
Arg = arg,
SubCmd = subCmd,
SomeByte = someNum,
ShiftPressed = shiftPressed,
PlayerId = playerId,
PlayerActionCount = playerActionCount
};
if (CheckForBugs)
Replay.Players[playerId].BugChecker?.Check(curAction);
#if DEBUGLOGGING
// -- ACTION BODY --
var leftOverBytes = actionEndPos - _binaryReader.Position;
var bytes = _binaryReader.ReadBytes((int)leftOverBytes);
var player = Replay.Players[curAction.PlayerId];
var playerRace = player.Race == RaceEnum.Unknown ? player.RawRace : player.Race.ToString();
// curAction.PlayerId == 0 && pCommandNum == 117 && Replay.EngineAddon == "dxp2" dataLeftType == 1 && !nn.Contains(f) && !player.Name.StartsWith("Computer")
// if (Replay.EngineAddon == "dxp2")
// {
var playname = player.Name;
var playnameFormatted = player.Name.PadRight(50 - player.Name.Length, '#');
Console.WriteLine($"{playname.Length} {playname} \n{playnameFormatted.Length} {playnameFormatted}");
// Actions.Add(curAction.Cmd);
Console.Write($"[{TicksFormatter.Format(curAction.Tick)}] | "); // base p:{startPos,-7}
Console.Write($"{playnameFormatted} | {playerRace,-18} | "); // player info
Console.Write($"{curAction.Name,-30} | {curAction} | {selectedCount} units | ");
Console.Write($"{actionType,-10} | {primaryId,-5} | ");
Console.WriteLine($"{actionBodyDataType} | {bytes.ToContentString()}");
// }
#else
// -- ACTION BODY --
var leftOverBytes = actionEndPos - _binaryReader.Position;
_binaryReader.Skip((int)leftOverBytes);
#endif
return curAction;
}
private void ReadExtraTick()
{
var tickType = _binaryReader.ReadInt32();
_binaryReader.Skip(4); // Размер тика
_binaryReader.Skip(1); // То же самое
if (tickType == 0)
ReadPlayerQuit();
else if (tickType == 1)
Replay.ChatMessages.Add(ReadChatMessage());
}
private void ReadPlayerQuit() // Событие выхода игрока. (может отсутствовать)
{
// ED 03 00 00 00 00 00 00 => 03 ED = int16 player id + 1000;
_binaryReader.Skip(8);
}
private ChatMessageModel ReadChatMessage() => new(
ticks: CurrentTick,
senderName: _binaryReader.ReadNextUnicodeString(),
senderId: _binaryReader.ReadInt32() % 10,
from: (SenderType)_binaryReader.ReadInt32(), // 0 player | 1 observer | 2 system
to: (ReceiverType)_binaryReader.ReadInt32(), // 0 all | 1 team | 2 system
text: _binaryReader.ReadNextUnicodeString()
);
private void ReadWierdTick(int type, int size)
{
Replay.Exception = new Exception($"Wierd tick at {_binaryReader.Position} of type:{type} and size:{size}");
// Console.WriteLine(Replay.Exception.Message);
_binaryReader.Skip(size);
}
public void Dispose()
{
_binaryReader.Dispose();
}
}

View File

@ -0,0 +1,81 @@
using System.Diagnostics;
namespace SoulstormReplayReader.Core.Utils;
internal sealed class NativeDowRandom
{
public uint Seed { get; set; }
public uint Index { get; set; }
public uint IterLimit { get; set; }
public int Lock { get; set; }
public NativeDowRandom(uint iterLimit = 4u)
{
IterLimit = iterLimit;
}
public NativeDowRandom SetSeed(uint seed)
{
Seed = seed;
Index = 0;
return this;
}
public int GetMax(uint num)
{
if (num == 0) return 0;
var res = (int)(HashRandom() % num);
Debug.WriteLine($"{num} {res}");
return res;
}
public int GetMax(int num) => GetMax((uint)num);
#region Dow decompiled code
private uint HashRandom()
{
var result = Index;
Index++;
var seed = Seed;
if (IterLimit == 0) return result;
for (var ind = 0; ind < IterLimit; ind++)
{
var s4 = result ^ s_arr1[ind];
var s5 = result;
var s6 = (ushort)s4;
s4 >>= 16;
var s7 = s_arr2[ind] ^ RotateLeft4((uint)(s6 * s6 + ~(s4 * s4)), '\x10');
result = (seed ^ (s6 * s4 + s7));
seed = s5;
}
return result;
}
private static uint RotateLeft4(uint x, char n) => (x << n) | (x >> (32 - n));
private static readonly uint[] s_arr1 =
{
3131664519, 504877868, 62708796, 255054258, 1990609181,
3312506973, 3816993251, 2027635554, 1949936084, 2632616645,
2303949916, 629732701, 1096506909, 560872853, 2244371175,
1586148200, 1051053196, 3530166132, 4087947819, 1219155940,
0, 0, 0, 0
};
private static readonly uint[] s_arr2 =
{
1259289432, 3899977923, 1767228838, 1437059654, 1301978502,
4264075669, 2982836325, 1797478225, 2593749601, 2859374024,
2863238881, 1526273356, 4237173347, 957308556, 1493161986,
2886360762, 2890429686, 2849966355, 2260618356, 3979088443,
0, 0, 0, 0
};
#endregion
}

View File

@ -0,0 +1,128 @@
using System.Diagnostics;
using SoulstormReplayReader.Core.Extensions;
using SoulstormReplayReader.Core.Domain.Player;
namespace SoulstormReplayReader.Core.Utils;
// TODO: Посмотреть что происходит с наблюдателями
internal sealed class PlayersRandomizer
{
private readonly NativeDowRandom _rand = new();
public uint Seed
{
get => _rand.Seed;
set => _rand.SetSeed(value);
}
public PlayersRandomizer(uint seed = 0)
{
Seed = seed;
Debug.WriteLine($"The seed is: {Seed}");
}
public List<PlayerModel> Randomize(List<PlayerModel> players, int mapPlayersCount)
{
var isFfa = GetIsFfa(players);
Debug.WriteLine(isFfa ? "FFA" : "SET TEAMS");
Debug.WriteLine("RANDOMIZING TEAMS");
var newPlayers = RandomizeTeams(players, isFfa);
Debug.WriteLine("RANDOMIZING HOLES");
RandomizeHoles(newPlayers, mapPlayersCount);
if (!isFfa)
{
Debug.WriteLine("RANDOMIZING PLAYERS IN TEAMS");
RandomizePlayersInTeams(newPlayers);
}
return newPlayers;
}
private static bool GetIsFfa(List<PlayerModel> players)
{
var lastTeam = 0;
foreach (var player in players.OrderBy(static p => p.Team))
{
if (player.Team - lastTeam > 1)
return true;
lastTeam = player.Team;
}
return false;
}
private List<PlayerModel> RandomizeTeams(List<PlayerModel> players, bool isFfa)
{
if (isFfa)
{
var nPlayers = players.ToList();
var activePlayersCount = players.Count(static p => p.IsActive);
for (var i = 1; i < 8 && activePlayersCount > 1; i++)
{
if (nPlayers[i].IsActive) activePlayersCount--;
nPlayers.Swap(i, _rand.GetMax(i + 1));
}
return nPlayers.Where(static p => p.IsActive).ToList();
}
var teams = players
.Where(static p => p.IsActive)
.OrderBy(static p => p.Team)
.GroupBy(static pl => pl.Team)
.Select(static team => team.ToList()).ToList();
for (var i = 1; i < teams.Count; i++)
teams.Swap(i, _rand.GetMax(i + 1));
return teams.SelectMany(static p => p).ToList();
}
private void RandomizeHoles(List<PlayerModel> players, int mapPlayersCount)
{
var holesCount = mapPlayersCount - players.Count;
for (var i = 1; i < players.Count && holesCount != 0; i++)
{
var playerHolesCount = _rand.GetMax(holesCount + 1);
if (playerHolesCount == 0) continue;
holesCount -= playerHolesCount;
for (var j = 0; j < playerHolesCount; j++)
{
players.Insert(i, PlayerModel.Empty);
i++;
}
}
if (holesCount == 0) return;
Debug.WriteLine($"{holesCount + 1} {holesCount}");
while (holesCount-- != 0)
players.Add(PlayerModel.Empty);
}
private void RandomizePlayersInTeams(List<PlayerModel> players)
{
var indexTeams = players
.Select(static (p, i) => new KeyValuePair<int, int>(p.Team, i))
.Where(static kv => kv.Key != 0)
.GroupBy(static kv => kv.Key)
.Select(static teamIndexes => teamIndexes.ToList())
.ToList();
foreach (var indexTeam in indexTeams)
for (var i = 1; i < indexTeam.Count; i++)
{
var n = _rand.GetMax(i + 1);
indexTeam.Swap(i, n);
players.Swap(indexTeam[i].Value, indexTeam[n].Value);
}
}
}

View File

@ -0,0 +1,38 @@
namespace SoulstormReplayReader.Core.Utils;
public static class TicksFormatter
{
public static class Pattern
{
/// <summary> hours : minutes : seconds . milliseconds </summary>
public const string Hmsf = @"hh\:mm\:ss\.fff";
/// <summary> hours : minutes : seconds </summary>
public const string Hms = @"hh\:mm\:ss";
/// <summary> minutes : seconds </summary>
public const string Ms = @"mm\:ss";
/// <summary> minutes : seconds . milliseconds </summary>
public const string Msf = @"mm\:ss\.fff";
/// <summary> minutes : seconds . milliseconds </summary>
public const string Sf = @"ss\.fff";
}
public static string Format(int ticks, string pattern = Pattern.Hmsf) =>
TimeSpan.FromMilliseconds(ticks * 125).ToString(pattern);
public static string ShortFormat(int ticks)
{
var tickMillis = TimeSpan.FromMilliseconds(ticks * 125);
var formatPattern = tickMillis switch
{
{ Hours: 0 } => Pattern.Ms,
_ => Pattern.Hms
};
return tickMillis.ToString(formatPattern);
}
}

16
SoulstormReplayReader.sln Normal file
View File

@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SoulstormReplayReader.Core", "SoulstormReplayReader.Core\SoulstormReplayReader.Core.csproj", "{5DAD7ED6-C5A1-4489-9D17-57C752A5296A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5DAD7ED6-C5A1-4489-9D17-57C752A5296A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5DAD7ED6-C5A1-4489-9D17-57C752A5296A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5DAD7ED6-C5A1-4489-9D17-57C752A5296A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5DAD7ED6-C5A1-4489-9D17-57C752A5296A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal