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