632 lines
23 KiB
C#
632 lines
23 KiB
C#
using System.Buffers;
|
||
using System.Buffers.Binary;
|
||
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 PixelType ImagePixelType { get; set; } = PixelType.Bgra8888;
|
||
|
||
public ReplayDescriptor Descriptor { get; } = new();
|
||
public ReplayModel Replay { get; set; }
|
||
public int CurrentTick { get; set; }
|
||
|
||
public ReplayModel ReadFull(bool readOrdinaryTicks = true)
|
||
{
|
||
ReadInfo();
|
||
|
||
ReadTicks(readOrdinaryTicks);
|
||
|
||
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"u8)
|
||
.SkipInt32(3);
|
||
|
||
Replay.TotalTicks = _binaryReader.ReadInt32(); // Полное игровое время (1 сек = 8 тиков)
|
||
|
||
_binaryReader // relic chunky
|
||
.Skip(24)
|
||
.NextAsciiStringMustEqual("FOLDINFO"u8)
|
||
.SkipInt32();
|
||
|
||
Descriptor.FoldInfoStart = _binaryReader.Position; // 153
|
||
Descriptor.FoldInfoSize = _binaryReader.ReadInt32();
|
||
|
||
_binaryReader
|
||
.SkipInt32()
|
||
.NextAsciiStringMustEqual("GAMEINFO\0FOLDWMAN"u8)
|
||
.SkipInt32();
|
||
|
||
Descriptor.FoldWmanStart = _binaryReader.Position; // 182
|
||
Descriptor.FoldWmanSize = _binaryReader.ReadInt32();
|
||
|
||
_binaryReader
|
||
.SkipInt32()
|
||
.NextAsciiStringMustEqual("DATASDSC"u8)
|
||
.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"u8)
|
||
.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"u8))
|
||
{
|
||
_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"u8)
|
||
.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"u8))
|
||
{
|
||
_binaryReader
|
||
.Skip(12) // int32 version 1; int32 size; int32 strLen 0;
|
||
.NextAsciiStringMustEqual("DATALCIN"u8)
|
||
.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"u8) // 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"u8)
|
||
.Skip(8); // int32 version 1; int32 size;
|
||
|
||
playerImage.Name = _binaryReader.ReadNextAsciiString();
|
||
|
||
_binaryReader
|
||
.NextAsciiStringMustEqual("DATAATTR"u8)
|
||
.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"u8)
|
||
.Skip(12); // int32 version 2; int32 size; int32 (?) 0;
|
||
|
||
playerImage.Init(width, height);
|
||
for (var y = height - 1; y >= 0; y--)
|
||
{
|
||
var line = playerImage.GetLine(y);
|
||
_binaryReader.ReadBytes(MemoryMarshal.AsBytes(line));
|
||
|
||
if (ImagePixelType == PixelType.Argb8888)
|
||
{
|
||
var uintLine = MemoryMarshal.Cast<ReplayColor, uint>(line);
|
||
|
||
for (var x = 0; x < uintLine.Length; x++)
|
||
uintLine[x] = BinaryPrimitives.ReverseEndianness(uintLine[x]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else _binaryReader.Skip(-8);
|
||
|
||
return player;
|
||
}
|
||
|
||
private void ReadTicks(bool readOrdinaryTicks)
|
||
{
|
||
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(readOrdinaryTicks);
|
||
|
||
#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(bool readOrdinaryTicks)
|
||
{
|
||
var tickType = (TickType)_binaryReader.ReadInt32();
|
||
var tickSize = _binaryReader.ReadInt32();
|
||
|
||
if (tickType == TickType.Normal)
|
||
{
|
||
if (tickSize == 17 || !readOrdinaryTicks)
|
||
{
|
||
_binaryReader.Skip(tickSize); // Всегда 17 байт
|
||
}
|
||
else
|
||
{
|
||
byte[] rentBytes = null;
|
||
var bytes = tickSize switch
|
||
{
|
||
> 0 and < 0xFF => stackalloc byte[tickSize],
|
||
_ => rentBytes = ArrayPool<byte>.Shared.Rent(tickSize)
|
||
};
|
||
|
||
try
|
||
{
|
||
bytes = _binaryReader.ReadBytes(bytes[..tickSize]);
|
||
ParseOrdinaryTick(bytes);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine(ex);
|
||
}
|
||
finally
|
||
{
|
||
if (rentBytes is not null)
|
||
ArrayPool<byte>.Shared.Return(rentBytes);
|
||
}
|
||
}
|
||
|
||
CurrentTick++;
|
||
}
|
||
else if (tickType == TickType.Extra)
|
||
ReadExtraTick();
|
||
else
|
||
ReadWierdTick((int)tickType, tickSize);
|
||
}
|
||
|
||
private void ParseOrdinaryTick(Span<byte> bytes)
|
||
{
|
||
var spanReader = new SpanReader(bytes);
|
||
|
||
// -- TICK HEADER -- size: 17
|
||
spanReader.Skip(1); // 1 byte = 0x50
|
||
var tickCount = spanReader.ReadInt32();
|
||
spanReader.Skip(8); // 8 bytes = int32 "Номер действия игрока" ??? + int32 Random
|
||
var playerChunksCount = spanReader.ReadInt32();
|
||
|
||
// -- TICK BODY --
|
||
for (var i = 0; i < playerChunksCount; i++) // для каждого игрока предусмотрен свой чанк со своим размером
|
||
{
|
||
// -- PLAYER CHUNK HEADER -- size: 13 + пока размер чанка != 0
|
||
spanReader.Skip(8); // 8 bytes = ???
|
||
var playerChunkSize = spanReader.ReadInt32();
|
||
spanReader.Skip(1); // 1 byte = playerChunkSize, но байт
|
||
|
||
// -- PLAYER CHUNK BODY --
|
||
while (playerChunkSize != 0)
|
||
{
|
||
var actionSize = spanReader.ReadInt16(); // размер захватывает 2 байта (размера) следующего экшена
|
||
|
||
spanReader.SliceToOffset();
|
||
var action = ParsePlayerAction(ref spanReader, actionSize - 2);
|
||
action.Tick = tickCount;
|
||
|
||
Replay.Actions.Add(action);
|
||
playerChunkSize -= actionSize;
|
||
}
|
||
}
|
||
}
|
||
|
||
private GameActionModel ParsePlayerAction(ref SpanReader spanReader, int actionSize)
|
||
{
|
||
// -- ACTION HEADER --
|
||
spanReader.Skip(4); // 4 bytes = какое то время (свое для каждого игрока)
|
||
// (начинается с рандомного значения и увеличивается по ходу игры)
|
||
// Скорее всего является global timer
|
||
|
||
var cmd = spanReader.ReadInt32();
|
||
var arg = spanReader.ReadByte(); // int32 in game
|
||
var subCmd = spanReader.ReadInt16();
|
||
var someNum = spanReader.ReadByte();
|
||
var shiftPressed = spanReader.ReadByte() == 1;
|
||
// 2 bytes = player id
|
||
var playerId = spanReader.Skip(2).ReadInt16() % 10;
|
||
var playerActionCount = spanReader.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
|
||
{
|
||
Cmd = cmd,
|
||
Arg = arg,
|
||
SubCmd = subCmd,
|
||
SomeByte = someNum,
|
||
ShiftPressed = shiftPressed,
|
||
|
||
PlayerId = playerId,
|
||
PlayerActionCount = playerActionCount
|
||
};
|
||
|
||
|
||
if (CheckForBugs)
|
||
Replay.Players[playerId].BugChecker?.Check(curAction);
|
||
|
||
spanReader.Skip((uint)(actionSize - spanReader.offset));
|
||
|
||
|
||
#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();
|
||
}
|
||
} |