init
This commit is contained in:
commit
f2db04033d
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
13
.idea/.idea.SoulstormReplayReader/.idea/.gitignore
generated
vendored
Normal file
13
.idea/.idea.SoulstormReplayReader/.idea/.gitignore
generated
vendored
Normal 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
|
||||||
7
.idea/.idea.SoulstormReplayReader/.idea/misc.xml
generated
Normal file
7
.idea/.idea.SoulstormReplayReader/.idea/misc.xml
generated
Normal 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>
|
||||||
13
SoulstormReplayReader.Core/.idea/.idea.SoulstormReplayReader.dir/.idea/.gitignore
generated
vendored
Normal file
13
SoulstormReplayReader.Core/.idea/.idea.SoulstormReplayReader.dir/.idea/.gitignore
generated
vendored
Normal 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
|
||||||
7
SoulstormReplayReader.Core/.idea/.idea.SoulstormReplayReader.dir/.idea/misc.xml
generated
Normal file
7
SoulstormReplayReader.Core/.idea/.idea.SoulstormReplayReader.dir/.idea/misc.xml
generated
Normal 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>
|
||||||
6
SoulstormReplayReader.Core/Const.cs
Normal file
6
SoulstormReplayReader.Core/Const.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace SoulstormReplayReader.Core;
|
||||||
|
|
||||||
|
public static class Consts
|
||||||
|
{
|
||||||
|
public const int PlayersMaxCount = 8;
|
||||||
|
}
|
||||||
86
SoulstormReplayReader.Core/Domain/Action/GameActionModel.cs
Normal file
86
SoulstormReplayReader.Core/Domain/Action/GameActionModel.cs
Normal 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
|
||||||
|
*/
|
||||||
15
SoulstormReplayReader.Core/Domain/Action/IGameAction.cs
Normal file
15
SoulstormReplayReader.Core/Domain/Action/IGameAction.cs
Normal 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; }
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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}";
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
namespace SoulstormReplayReader.Core.Domain.ChatMessage;
|
||||||
|
|
||||||
|
public enum ReceiverType : byte
|
||||||
|
{
|
||||||
|
All = 0,
|
||||||
|
Team,
|
||||||
|
System
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
namespace SoulstormReplayReader.Core.Domain.ChatMessage;
|
||||||
|
|
||||||
|
public enum SenderType : byte
|
||||||
|
{
|
||||||
|
Player = 0,
|
||||||
|
Observer,
|
||||||
|
System
|
||||||
|
}
|
||||||
17
SoulstormReplayReader.Core/Domain/GameSettings.cs
Normal file
17
SoulstormReplayReader.Core/Domain/GameSettings.cs
Normal 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;
|
||||||
|
}
|
||||||
14
SoulstormReplayReader.Core/Domain/Player/ColorScheme.cs
Normal file
14
SoulstormReplayReader.Core/Domain/Player/ColorScheme.cs
Normal 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; }
|
||||||
|
}
|
||||||
40
SoulstormReplayReader.Core/Domain/Player/PlayerBugChecker.cs
Normal file
40
SoulstormReplayReader.Core/Domain/Player/PlayerBugChecker.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
SoulstormReplayReader.Core/Domain/Player/PlayerModel.cs
Normal file
57
SoulstormReplayReader.Core/Domain/Player/PlayerModel.cs
Normal 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;
|
||||||
|
}
|
||||||
15
SoulstormReplayReader.Core/Domain/RaceEnum.cs
Normal file
15
SoulstormReplayReader.Core/Domain/RaceEnum.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
namespace SoulstormReplayReader.Core.Domain;
|
||||||
|
|
||||||
|
public enum RaceEnum
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
SpaceMarines,
|
||||||
|
Orks,
|
||||||
|
Eldar,
|
||||||
|
ChaosSpaceMarines,
|
||||||
|
DarkEldar,
|
||||||
|
Necrons,
|
||||||
|
SistersOfBattle,
|
||||||
|
Tau,
|
||||||
|
ImperialGuard
|
||||||
|
}
|
||||||
8
SoulstormReplayReader.Core/Domain/Replay/Map.cs
Normal file
8
SoulstormReplayReader.Core/Domain/Replay/Map.cs
Normal 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; }
|
||||||
|
}
|
||||||
34
SoulstormReplayReader.Core/Domain/Replay/ReplayModel.cs
Normal file
34
SoulstormReplayReader.Core/Domain/Replay/ReplayModel.cs
Normal 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();
|
||||||
|
}
|
||||||
7
SoulstormReplayReader.Core/Domain/Tick/TickType.cs
Normal file
7
SoulstormReplayReader.Core/Domain/Tick/TickType.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace SoulstormReplayReader.Core.Domain.Tick;
|
||||||
|
|
||||||
|
public enum TickType: byte
|
||||||
|
{
|
||||||
|
Normal = 0,
|
||||||
|
Extra = 1
|
||||||
|
}
|
||||||
75
SoulstormReplayReader.Core/Domain/WinCondition.cs
Normal file
75
SoulstormReplayReader.Core/Domain/WinCondition.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
SoulstormReplayReader.Core/Enums/ReplayPlayerType.cs
Normal file
10
SoulstormReplayReader.Core/Enums/ReplayPlayerType.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace SoulstormReplayReader.Core.Enums;
|
||||||
|
|
||||||
|
public enum ReplayPlayerType
|
||||||
|
{
|
||||||
|
EmptySlot = 0,
|
||||||
|
Host,
|
||||||
|
Computer,
|
||||||
|
Spectator,
|
||||||
|
Player
|
||||||
|
}
|
||||||
7
SoulstormReplayReader.Core/Enums/ReplayVersion.cs
Normal file
7
SoulstormReplayReader.Core/Enums/ReplayVersion.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace SoulstormReplayReader.Core.Enums;
|
||||||
|
|
||||||
|
public enum ReplayVersion : byte
|
||||||
|
{
|
||||||
|
_1_2 = 9,
|
||||||
|
Steam = 10
|
||||||
|
}
|
||||||
87
SoulstormReplayReader.Core/Extensions/ExBinaryReader.cs
Normal file
87
SoulstormReplayReader.Core/Extensions/ExBinaryReader.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
93
SoulstormReplayReader.Core/Extensions/Extensions.cs
Normal file
93
SoulstormReplayReader.Core/Extensions/Extensions.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
SoulstormReplayReader.Core/Models/ReplayColor.cs
Normal file
23
SoulstormReplayReader.Core/Models/ReplayColor.cs
Normal 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}";
|
||||||
|
}
|
||||||
24
SoulstormReplayReader.Core/Models/ReplayImage.cs
Normal file
24
SoulstormReplayReader.Core/Models/ReplayImage.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
SoulstormReplayReader.Core/ReplayDescriptor.cs
Normal file
24
SoulstormReplayReader.Core/ReplayDescriptor.cs
Normal 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; }
|
||||||
|
}
|
||||||
42
SoulstormReplayReader.Core/Services/ReplayService.cs
Normal file
42
SoulstormReplayReader.Core/Services/ReplayService.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
11
SoulstormReplayReader.Core/SoulstormReplayReader.Core.csproj
Normal file
11
SoulstormReplayReader.Core/SoulstormReplayReader.Core.csproj
Normal 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>
|
||||||
614
SoulstormReplayReader.Core/SsReplayReader.cs
Normal file
614
SoulstormReplayReader.Core/SsReplayReader.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
81
SoulstormReplayReader.Core/Utils/NativeDowRandom.cs
Normal file
81
SoulstormReplayReader.Core/Utils/NativeDowRandom.cs
Normal 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
|
||||||
|
}
|
||||||
128
SoulstormReplayReader.Core/Utils/PlayersRandomizer.cs
Normal file
128
SoulstormReplayReader.Core/Utils/PlayersRandomizer.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
SoulstormReplayReader.Core/Utils/TicksFormatter.cs
Normal file
38
SoulstormReplayReader.Core/Utils/TicksFormatter.cs
Normal 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
16
SoulstormReplayReader.sln
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user