改了一亿个东西,修了一亿个 BUG
This commit is contained in:
301
ReplayFile/CommandChunk.cs
Normal file
301
ReplayFile/CommandChunk.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace AnotherReplayReader.ReplayFile
|
||||
{
|
||||
internal static class RA3Commands
|
||||
{
|
||||
private static readonly Dictionary<byte, Action<BinaryReader>> _commandParser;
|
||||
private static readonly Dictionary<byte, string> _commandNames;
|
||||
|
||||
static RA3Commands()
|
||||
{
|
||||
Action<BinaryReader> fixedSizeParser(byte command, int size)
|
||||
{
|
||||
return (BinaryReader current) =>
|
||||
{
|
||||
var lastByte = current.ReadBytes(size - 2).Last();
|
||||
if (lastByte != 0xFF)
|
||||
{
|
||||
throw new InvalidDataException($"Failed to parse command {command:X}, last byte is {lastByte:X}");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Action<BinaryReader> variableSizeParser(byte command, int offset)
|
||||
{
|
||||
return (BinaryReader current) =>
|
||||
{
|
||||
var totalBytes = 2;
|
||||
totalBytes += current.ReadBytes(offset - 2).Length;
|
||||
for (var x = current.ReadByte(); x != 0xFF; x = current.ReadByte())
|
||||
{
|
||||
totalBytes += 1;
|
||||
|
||||
|
||||
var size = ((x >> 4) + 1) * 4;
|
||||
totalBytes += current.ReadBytes(size).Length;
|
||||
}
|
||||
totalBytes += 1;
|
||||
var chk = totalBytes;
|
||||
};
|
||||
};
|
||||
|
||||
var list = new List<(byte, Func<byte, int, Action<BinaryReader>>, int, string)>
|
||||
{
|
||||
(0x00, fixedSizeParser, 45, "展开建筑/建造碉堡(?)"),
|
||||
(0x03, fixedSizeParser, 17, "开始升级"),
|
||||
(0x04, fixedSizeParser, 17, "暂停/中止升级"),
|
||||
(0x05, fixedSizeParser, 20, "开始生产单位或纳米核心"),
|
||||
(0x06, fixedSizeParser, 20, "暂停/取消生产单位或纳米核心"),
|
||||
(0x07, fixedSizeParser, 17, "开始建造建筑"),
|
||||
(0x08, fixedSizeParser, 17, "暂停/取消建造建筑"),
|
||||
(0x09, fixedSizeParser, 35, "摆放建筑"),
|
||||
(0x0F, fixedSizeParser, 16, "(未知指令)"),
|
||||
(0x14, fixedSizeParser, 16, "移动"),
|
||||
(0x15, fixedSizeParser, 16, "移动攻击(A)"),
|
||||
(0x16, fixedSizeParser, 16, "强制移动/碾压(G)"),
|
||||
(0x21, fixedSizeParser, 20, "[游戏每3秒自动产生的指令]"),
|
||||
(0x2C, fixedSizeParser, 29, "队形移动(左右键)"),
|
||||
(0x32, fixedSizeParser, 53, "释放技能或协议(多个目标,如侦察扫描协议)"),
|
||||
(0x34, fixedSizeParser, 45, "[游戏自动产生的UUID]"),
|
||||
(0x35, fixedSizeParser, 1049, "[玩家信息(?)]"),
|
||||
(0x36, fixedSizeParser, 16, "倒车移动(D)"),
|
||||
(0x5F, fixedSizeParser, 11, "(未知指令)"),
|
||||
|
||||
(0x0A, variableSizeParser, 2, "出售建筑"),
|
||||
(0x0D, variableSizeParser, 2, "右键攻击"),
|
||||
(0x0E, variableSizeParser, 2, "强制攻击(Ctrl)"),
|
||||
(0x12, variableSizeParser, 2, "(未知指令)"),
|
||||
(0x1A, variableSizeParser, 2, "停止(S)"),
|
||||
(0x1B, variableSizeParser, 2, "(未知指令)"),
|
||||
(0x28, variableSizeParser, 2, "开始维修建筑"),
|
||||
(0x29, variableSizeParser, 2, "停止维修建筑"),
|
||||
(0x2A, variableSizeParser, 2, "选择所有单位(Q)"),
|
||||
(0x2E, variableSizeParser, 2, "切换警戒/侵略/固守/停火模式"),
|
||||
(0x2F, variableSizeParser, 2, "路径点模式(Alt)(?)"),
|
||||
(0x37, variableSizeParser, 2, "[游戏不定期自动产生的指令]"),
|
||||
(0x47, variableSizeParser, 2, "[游戏在第五帧自动产生的指令]"),
|
||||
(0x48, variableSizeParser, 2, "(未知指令)"),
|
||||
(0x4C, variableSizeParser, 2, "删除信标(或F9?)"),
|
||||
(0x4E, variableSizeParser, 2, "选择协议"),
|
||||
(0x52, variableSizeParser, 2, "(未知指令)"),
|
||||
(0xF5, variableSizeParser, 5, "选择单位"),
|
||||
(0xF6, variableSizeParser, 5, "[未知指令,貌似会在展开兵营核心时自动产生?]"),
|
||||
(0xF8, variableSizeParser, 4, "鼠标左键单击/取消选择"),
|
||||
(0xF9, variableSizeParser, 2, "[可能是步兵自行从进驻的建筑撤出]"),
|
||||
(0xFA, variableSizeParser, 7, "创建编队"),
|
||||
(0xFB, variableSizeParser, 2, "选择编队"),
|
||||
(0xFC, variableSizeParser, 2, "(未知指令)"),
|
||||
(0xFD, variableSizeParser, 7, "(未知指令)"),
|
||||
(0xFE, variableSizeParser, 15, "释放技能或协议(无目标)"),
|
||||
(0xFF, variableSizeParser, 34, "释放技能或协议(单个目标)"),
|
||||
};
|
||||
|
||||
var specialList = new List<(byte, Action<BinaryReader>, string)>
|
||||
{
|
||||
(0x01, ParseSpecialChunk0x01, "[游戏自动生成的指令]"),
|
||||
(0x02, ParseSetRallyPoint0x02, "设计集结点"),
|
||||
(0x0C, ParseUngarrison0x0C, "从进驻的建筑撤出(?)"),
|
||||
(0x10, ParseGarrison0x10, "进驻建筑"),
|
||||
(0x33, ParseUuid0x33, "[游戏自动生成的UUID指令]"),
|
||||
(0x4B, ParsePlaceBeacon0x4B, "信标")
|
||||
};
|
||||
|
||||
_commandParser = new Dictionary<byte, Action<BinaryReader>>();
|
||||
_commandNames = new Dictionary<byte, string>();
|
||||
|
||||
foreach (var (id, maker, size, description) in list)
|
||||
{
|
||||
_commandParser.Add(id, maker(id, size));
|
||||
_commandNames.Add(id, description);
|
||||
}
|
||||
|
||||
foreach (var (id, parser, description) in specialList)
|
||||
{
|
||||
_commandParser.Add(id, parser);
|
||||
_commandNames.Add(id, description);
|
||||
}
|
||||
|
||||
_commandNames.Add(0x4D, "在信标里输入文字");
|
||||
}
|
||||
|
||||
public static void ReadCommandData(this BinaryReader reader, byte commandId)
|
||||
{
|
||||
if (_commandParser.TryGetValue(commandId, out var parser))
|
||||
{
|
||||
_commandParser[commandId](reader);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnknownCommandParser(reader, commandId);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetCommandName(byte commandId)
|
||||
{
|
||||
return _commandNames.TryGetValue(commandId, out var storedName) ? storedName : $"(未知指令 0x{commandId:2X})";
|
||||
}
|
||||
|
||||
public static void UnknownCommandParser(BinaryReader current, byte commandId)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var value = current.ReadByte();
|
||||
if (value == 0xFF)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//return $"(未知指令 0x{commandID:2X})";
|
||||
}
|
||||
|
||||
private static void ParseSpecialChunk0x01(BinaryReader current)
|
||||
{
|
||||
var firstByte = current.ReadByte();
|
||||
if (firstByte == 0xFF)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sixthByte = current.ReadBytes(5).Last();
|
||||
if (sixthByte == 0xFF)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sixteenthByte = current.ReadBytes(10).Last();
|
||||
var size = (int)(sixteenthByte + 1) * 4 + 14;
|
||||
var lastByte = current.ReadBytes(size).Last();
|
||||
if (lastByte != 0xFF)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private static void ParseSetRallyPoint0x02(BinaryReader current)
|
||||
{
|
||||
var size = (current.ReadBytes(23).Last() + 1) * 2 + 1;
|
||||
var lastByte = current.ReadBytes(size).Last();
|
||||
if (lastByte != 0xFF)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseUngarrison0x0C(BinaryReader current)
|
||||
{
|
||||
current.ReadByte();
|
||||
var size = (current.ReadByte() + 1) * 4 + 1;
|
||||
var lastByte = current.ReadBytes(size).Last();
|
||||
if (lastByte != 0xFF)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseGarrison0x10(BinaryReader current)
|
||||
{
|
||||
var type = current.ReadByte();
|
||||
var size = -1;
|
||||
if (type == 0x14)
|
||||
{
|
||||
size = 9;
|
||||
}
|
||||
else if (type == 0x04)
|
||||
{
|
||||
size = 10;
|
||||
}
|
||||
|
||||
var lastByte = current.ReadBytes(size).Last();
|
||||
if (lastByte != 0xFF)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseUuid0x33(BinaryReader current)
|
||||
{
|
||||
current.ReadByte();
|
||||
var firstStringLength = (int)current.ReadByte();
|
||||
current.ReadBytes(firstStringLength + 1);
|
||||
var secondStringLength = current.ReadByte() * 2;
|
||||
var lastByte = current.ReadBytes(secondStringLength + 6).Last();
|
||||
if (lastByte != 0xFF)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParsePlaceBeacon0x4B(BinaryReader current)
|
||||
{
|
||||
var type = current.ReadByte();
|
||||
var size = -1;
|
||||
if (type == 0x04)
|
||||
{
|
||||
size = 5;
|
||||
}
|
||||
else if (type == 0x07)
|
||||
{
|
||||
size = 13;
|
||||
}
|
||||
|
||||
var lastByte = current.ReadBytes(size).Last();
|
||||
if (lastByte != 0xFF)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CommandChunk
|
||||
{
|
||||
public byte CommandId { get; private set; }
|
||||
public int PlayerIndex { get; private set; }
|
||||
|
||||
public static List<CommandChunk> Parse(in ReplayChunk chunk)
|
||||
{
|
||||
if (chunk.Type != 1)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
static int ManglePlayerId(byte id)
|
||||
{
|
||||
return id / 8 - 2;
|
||||
}
|
||||
|
||||
using var reader = chunk.GetReader();
|
||||
if (reader.ReadByte() != 1)
|
||||
{
|
||||
throw new InvalidDataException("Payload first byte not 1");
|
||||
}
|
||||
|
||||
var list = new List<CommandChunk>();
|
||||
var numberOfCommands = reader.ReadInt32();
|
||||
for (var i = 0; i < numberOfCommands; ++i)
|
||||
{
|
||||
var commandId = reader.ReadByte();
|
||||
var playerId = reader.ReadByte();
|
||||
reader.ReadCommandData(commandId);
|
||||
list.Add(new CommandChunk
|
||||
{
|
||||
CommandId = commandId,
|
||||
PlayerIndex = ManglePlayerId(playerId)
|
||||
});
|
||||
}
|
||||
|
||||
if (reader.BaseStream.Position != reader.BaseStream.Length)
|
||||
{
|
||||
throw new InvalidDataException("Payload not fully parsed");
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
ReplayFile/Player.cs
Normal file
41
ReplayFile/Player.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AnotherReplayReader.ReplayFile
|
||||
{
|
||||
internal sealed class Player
|
||||
{
|
||||
public static readonly IReadOnlyDictionary<string, string> ComputerNames = new Dictionary<string, string>
|
||||
{
|
||||
{ "E", "简单" },
|
||||
{ "M", "中等" },
|
||||
{ "H", "困难" },
|
||||
{ "B", "凶残" },
|
||||
};
|
||||
|
||||
public string PlayerName { get; }
|
||||
public uint PlayerIp { get; }
|
||||
public int FactionId { get; }
|
||||
public int Team { get; }
|
||||
|
||||
public Player(string[] playerEntry)
|
||||
{
|
||||
var isComputer = playerEntry[0][0] == 'C';
|
||||
|
||||
PlayerName = playerEntry[0].Substring(1);
|
||||
|
||||
if (isComputer)
|
||||
{
|
||||
PlayerName = ComputerNames[PlayerName];
|
||||
PlayerIp = 0;
|
||||
FactionId = int.Parse(playerEntry[2]);
|
||||
Team = int.Parse(playerEntry[4]);
|
||||
}
|
||||
else
|
||||
{
|
||||
PlayerIp = uint.Parse(playerEntry[1], System.Globalization.NumberStyles.HexNumber);
|
||||
FactionId = int.Parse(playerEntry[5]);
|
||||
Team = int.Parse(playerEntry[7]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
346
ReplayFile/Replay.cs
Normal file
346
ReplayFile/Replay.cs
Normal file
@@ -0,0 +1,346 @@
|
||||
using AnotherReplayReader.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace AnotherReplayReader.ReplayFile
|
||||
{
|
||||
internal enum ReplayType
|
||||
{
|
||||
Skirmish,
|
||||
Lan,
|
||||
Online
|
||||
}
|
||||
|
||||
internal sealed class Replay
|
||||
{
|
||||
public static readonly byte[] HeaderMagic = Encoding.ASCII.GetBytes("RA3 REPLAY HEADER");
|
||||
public static readonly Dictionary<ReplayType, string> TypeStrings = new()
|
||||
{
|
||||
{ ReplayType.Skirmish, "遭遇战录像" },
|
||||
{ ReplayType.Lan, "局域网录像" },
|
||||
{ ReplayType.Online, "官网录像" },
|
||||
};
|
||||
|
||||
private readonly byte _replaySaverIndex;
|
||||
private readonly byte[]? _rawHeader;
|
||||
|
||||
public string Path { get; }
|
||||
public DateTime Date { get; }
|
||||
public string MapName { get; }
|
||||
public string MapPath { get; }
|
||||
public IReadOnlyList<Player> Players { get; }
|
||||
public int NumberOfPlayingPlayers { get; }
|
||||
public Mod Mod { get; }
|
||||
public ReplayType Type { get; }
|
||||
public bool HasCommentator { get; }
|
||||
|
||||
public long Size { get; }
|
||||
public ReplayFooter? Footer { get; }
|
||||
public IReadOnlyList<ReplayChunk>? Body { get; }
|
||||
|
||||
public string FileName => System.IO.Path.GetFileNameWithoutExtension(Path);
|
||||
public Player ReplaySaver => Players[_replaySaverIndex];
|
||||
public string TypeString => TypeStrings[Type];
|
||||
public bool HasFooter => Footer != null;
|
||||
public ShortTimeSpan? Length => Footer?.ReplayLength;
|
||||
|
||||
public Replay(string path, bool parseBody = false) :
|
||||
this(path, new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), parseBody)
|
||||
{
|
||||
}
|
||||
|
||||
public Replay(string path, Stream stream, bool parseBody = false)
|
||||
{
|
||||
Path = path;
|
||||
|
||||
using var reader = new BinaryReader(stream);
|
||||
Size = reader.BaseStream.Length;
|
||||
|
||||
var headerMagic = reader.ReadBytes(HeaderMagic.Length);
|
||||
if (!headerMagic.SequenceEqual(HeaderMagic))
|
||||
{
|
||||
throw new InvalidDataException($"{Path} is not a replay, header is {BitConverter.ToString(headerMagic)}");
|
||||
}
|
||||
|
||||
var isSkirmish = reader.ReadByte() == 0x04;
|
||||
reader.ReadBytes(4 * 4); // version and builds
|
||||
reader.ReadBytes(2); // commentary flag, and padding zero byte
|
||||
|
||||
reader.ReadUTF16String(); // title
|
||||
reader.ReadUTF16String(); // description
|
||||
MapName = reader.ReadUTF16String(); // map name
|
||||
reader.ReadUTF16String(); // map id
|
||||
|
||||
NumberOfPlayingPlayers = reader.ReadByte();
|
||||
|
||||
for (var i = 0; i <= NumberOfPlayingPlayers; ++i)
|
||||
{
|
||||
reader.ReadUInt32();
|
||||
reader.ReadUTF16String(); // utf16 player name
|
||||
if (!isSkirmish)
|
||||
{
|
||||
reader.ReadByte(); // team
|
||||
}
|
||||
}
|
||||
|
||||
var offset = reader.ReadInt32();
|
||||
var cnc3MagicLength = reader.ReadInt32();
|
||||
var headerSize = checked((int)(reader.BaseStream.Position + offset));
|
||||
reader.ReadBytes(cnc3MagicLength);
|
||||
|
||||
var modInfo = reader.ReadBytes(22);
|
||||
Mod = new Mod(Encoding.UTF8.GetString(modInfo));
|
||||
|
||||
var timeStamp = reader.ReadUInt32();
|
||||
Date = DateTimeOffset.FromUnixTimeSeconds(timeStamp).DateTime;
|
||||
|
||||
reader.ReadBytes(31);
|
||||
var descriptionsLength = reader.ReadInt32();
|
||||
var description = Encoding.UTF8.GetString(reader.ReadBytes(descriptionsLength));
|
||||
|
||||
var entries = null as Dictionary<string, string>;
|
||||
try
|
||||
{
|
||||
var query = from splitted in description.Split(';')
|
||||
where !string.IsNullOrWhiteSpace(splitted)
|
||||
select splitted.Split(new[] { '=' }, 2);
|
||||
entries = query.ToDictionary(x => x[0], x => x[1]);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{e}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Players = entries["S"].Split(':')
|
||||
.TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X')
|
||||
.Select(x => new Player(x.Split(',')))
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new InvalidDataException($"Failed to parse playerdata from string header of replay {Path}: \r\n{exception}");
|
||||
}
|
||||
|
||||
MapPath = entries["M"].Substring(3);
|
||||
|
||||
HasCommentator = !entries["PC"].Equals("-1");
|
||||
|
||||
var lanFlag = int.Parse(entries["GT"]) == 0;
|
||||
if (lanFlag)
|
||||
{
|
||||
if (Players.First().PlayerIp == 0)
|
||||
{
|
||||
Type = ReplayType.Skirmish;
|
||||
}
|
||||
else
|
||||
{
|
||||
Type = ReplayType.Lan;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Type = ReplayType.Online;
|
||||
}
|
||||
|
||||
_replaySaverIndex = reader.ReadByte();
|
||||
|
||||
reader.ReadBytes(8); // 8 bit paddings
|
||||
var fileNameLength = reader.ReadInt32();
|
||||
reader.ReadBytes(fileNameLength * 2);
|
||||
reader.ReadBytes(16);
|
||||
var verMagicLength = reader.ReadInt32();
|
||||
reader.ReadBytes(verMagicLength);
|
||||
reader.ReadBytes(85);
|
||||
|
||||
if (reader.BaseStream.Position != headerSize)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (acutally {reader.BaseStream.Position})\r\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parseBody)
|
||||
{
|
||||
// jump to footer directly
|
||||
reader.BaseStream.Seek(-4, SeekOrigin.End);
|
||||
try
|
||||
{
|
||||
Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Failed to parse replay footer, replay might be corrupt: {e}\r\n";
|
||||
Footer = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var body = new List<ReplayChunk>();
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var timeCode = reader.ReadUInt32();
|
||||
if (timeCode == ReplayFooter.Terminator)
|
||||
{
|
||||
Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter);
|
||||
break;
|
||||
}
|
||||
|
||||
body.Add(new ReplayChunk(timeCode, reader));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Failed to parse replay body, replay might be corrupt: {e}\r\n";
|
||||
}
|
||||
|
||||
byte[]? rawHeader = null;
|
||||
try
|
||||
{
|
||||
// 重新读取原来的整个录像头
|
||||
reader.BaseStream.Seek(0, SeekOrigin.Begin);
|
||||
rawHeader = reader.ReadBytes(headerSize);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Warning: failed to read raw header: {e}\r\n";
|
||||
return;
|
||||
}
|
||||
if (rawHeader.Length != headerSize)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (raw header length = {rawHeader.Length})\r\n";
|
||||
return;
|
||||
}
|
||||
|
||||
_rawHeader = rawHeader;
|
||||
Body = body;
|
||||
}
|
||||
|
||||
public Dictionary<byte, int[]> GetCommandCounts()
|
||||
{
|
||||
if (Body is null)
|
||||
{
|
||||
throw new InvalidOperationException("Replay body must be parsed before retrieving command chunks");
|
||||
}
|
||||
|
||||
var playerCommands = new Dictionary<byte, int[]>();
|
||||
|
||||
foreach (var chunk in Body)
|
||||
{
|
||||
if (chunk.Type != 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var command in CommandChunk.Parse(chunk))
|
||||
{
|
||||
var commandCount = playerCommands.TryGetValue(command.CommandId, out var current) ? current : new int[Players.Count];
|
||||
if (command.PlayerIndex >= commandCount.Length) // unknown or unparsable command?
|
||||
{
|
||||
commandCount = commandCount
|
||||
.Concat(new int[command.PlayerIndex - commandCount.Length + 1])
|
||||
.ToArray();
|
||||
}
|
||||
commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1;
|
||||
playerCommands[command.CommandId] = commandCount;
|
||||
}
|
||||
}
|
||||
|
||||
return playerCommands;
|
||||
}
|
||||
|
||||
public Replay CloneHeader()
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using (var writer = new BinaryWriter(stream, Encoding.UTF8, true))
|
||||
{
|
||||
WriteTo(writer);
|
||||
}
|
||||
stream.Position = 0;
|
||||
return new Replay(Path, stream);
|
||||
}
|
||||
|
||||
public bool PathEquals(Replay replay) => PathEquals(replay.Path);
|
||||
public bool PathEquals(string path) => Path.Equals(path, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public string GetDetails(PlayerIdentity playerIdentity)
|
||||
{
|
||||
static string GetSizeString(double size)
|
||||
{
|
||||
if (size > 1024 * 1024)
|
||||
{
|
||||
return $"{Math.Round(size / (1024 * 1024), 2)}MB";
|
||||
}
|
||||
return $"{Math.Round(size / 1024)}KB";
|
||||
}
|
||||
|
||||
string size = GetSizeString(Size);
|
||||
string length = Length?.ToString() ?? "录像已损坏,请先修复录像";
|
||||
|
||||
var replaySaver = _replaySaverIndex < Players.Count
|
||||
? ReplaySaver.PlayerName
|
||||
: "[无法获取保存录像的玩家]";
|
||||
|
||||
using var writer = new StringWriter();
|
||||
writer.WriteLine("文件名:{0}", FileName);
|
||||
writer.WriteLine("大小:{0}", size);
|
||||
writer.WriteLine("地图:{0}", MapName);
|
||||
writer.WriteLine("日期:{0}", Date);
|
||||
writer.WriteLine("长度:{0}", length);
|
||||
writer.WriteLine("录像类别:{0}", TypeString);
|
||||
writer.WriteLine("这个文件是{0}保存的", replaySaver);
|
||||
writer.WriteLine("玩家列表:");
|
||||
foreach (var player in Players)
|
||||
{
|
||||
if (player == Players.Last() && player.PlayerName.Equals("post Commentator"))
|
||||
{
|
||||
break;
|
||||
}
|
||||
var factionName = ModData.GetFaction(Mod, player.FactionId).Name;
|
||||
var realName = Type == ReplayType.Lan ? playerIdentity.FormatRealName(player.PlayerIp) : string.Empty;
|
||||
writer.WriteLine($"{player.PlayerName + realName},{factionName}");
|
||||
}
|
||||
|
||||
return writer.ToString();
|
||||
}
|
||||
|
||||
public void WriteTo(BinaryWriter writer) => WriteTo(writer, false);
|
||||
|
||||
private void WriteTo(BinaryWriter writer, bool skipBody)
|
||||
{
|
||||
if ((_rawHeader is null || Body is null) && !skipBody)
|
||||
{
|
||||
throw new InvalidOperationException("Replay body must be parsed before writing replay");
|
||||
}
|
||||
|
||||
writer.Write(_rawHeader);
|
||||
|
||||
var lastTimeCode = Footer?.FinalTimeCode;
|
||||
if (Body is not null)
|
||||
{
|
||||
foreach (var chunk in Body)
|
||||
{
|
||||
lastTimeCode = chunk.TimeCode;
|
||||
writer.Write(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
if (Footer is not null)
|
||||
{
|
||||
writer.Write(Footer);
|
||||
}
|
||||
else if (lastTimeCode is uint lastTimeCodeValue)
|
||||
{
|
||||
writer.Write(new ReplayFooter(lastTimeCodeValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
37
ReplayFile/ReplayChunk.cs
Normal file
37
ReplayFile/ReplayChunk.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.IO;
|
||||
|
||||
namespace AnotherReplayReader.ReplayFile
|
||||
{
|
||||
internal sealed class ReplayChunk
|
||||
{
|
||||
private readonly byte[] _data;
|
||||
|
||||
public uint TimeCode { get; }
|
||||
public byte Type { get; }
|
||||
|
||||
public ReplayChunk(uint timeCode, BinaryReader reader)
|
||||
{
|
||||
TimeCode = timeCode; // reader.ReadUInt32();
|
||||
Type = reader.ReadByte();
|
||||
var chunkSize = reader.ReadInt32();
|
||||
_data = reader.ReadBytes(chunkSize);
|
||||
if (reader.ReadInt32() != 0)
|
||||
{
|
||||
throw new InvalidDataException("Replay Chunk not ended with zero");
|
||||
}
|
||||
}
|
||||
|
||||
public BinaryReader GetReader() => new(new MemoryStream(_data, false));
|
||||
|
||||
public void WriteTo(BinaryWriter writer)
|
||||
{
|
||||
writer.Write(TimeCode);
|
||||
writer.Write(Type);
|
||||
writer.Write(_data.Length);
|
||||
writer.Write(_data);
|
||||
writer.Write(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
40
ReplayFile/ReplayExtensions.cs
Normal file
40
ReplayFile/ReplayExtensions.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace AnotherReplayReader.ReplayFile
|
||||
{
|
||||
internal static class ReplayExtensions
|
||||
{
|
||||
public static string ReadUTF16String(this BinaryReader reader)
|
||||
{
|
||||
var currentBytes = new List<byte>();
|
||||
var lastTwoBytes = Array.Empty<byte>();
|
||||
while (true)
|
||||
{
|
||||
lastTwoBytes = reader.ReadBytes(2);
|
||||
if (lastTwoBytes.Length != 2)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
if (lastTwoBytes.All(x => x == 0))
|
||||
{
|
||||
break;
|
||||
}
|
||||
currentBytes.AddRange(lastTwoBytes);
|
||||
}
|
||||
|
||||
return Encoding.Unicode.GetString(currentBytes.ToArray());
|
||||
}
|
||||
|
||||
public static void Write(this BinaryWriter writer, ReplayChunk chunk) => chunk.WriteTo(writer);
|
||||
|
||||
public static void Write(this BinaryWriter writer, ReplayFooter footer) => footer.WriteTo(writer);
|
||||
|
||||
public static void Write(this BinaryWriter writer, Replay replay) => replay.WriteTo(writer);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
90
ReplayFile/ReplayFooter.cs
Normal file
90
ReplayFile/ReplayFooter.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace AnotherReplayReader.ReplayFile
|
||||
{
|
||||
internal enum ReplayFooterOption
|
||||
{
|
||||
SeekToFooter,
|
||||
CurrentlyAtFooter,
|
||||
}
|
||||
|
||||
internal sealed class ReplayFooter
|
||||
{
|
||||
public const uint Terminator = 0x7FFFFFFF;
|
||||
public static readonly byte[] FooterString = Encoding.ASCII.GetBytes("RA3 REPLAY FOOTER");
|
||||
|
||||
private readonly byte[] _data;
|
||||
|
||||
public uint FinalTimeCode { get; }
|
||||
public TimeSpan ReplayLength => TimeSpan.FromSeconds(FinalTimeCode / 15.0);
|
||||
|
||||
public ReplayFooter(BinaryReader reader, ReplayFooterOption option)
|
||||
{
|
||||
var currentPosition = reader.BaseStream.Position;
|
||||
reader.BaseStream.Seek(-4, SeekOrigin.End);
|
||||
var footerLength = reader.ReadInt32();
|
||||
|
||||
if (option == ReplayFooterOption.SeekToFooter)
|
||||
{
|
||||
currentPosition = reader.BaseStream.Length - footerLength;
|
||||
}
|
||||
|
||||
reader.BaseStream.Seek(currentPosition, SeekOrigin.Begin);
|
||||
var footer = reader.ReadBytes(footerLength);
|
||||
|
||||
if (footer.Length != footerLength || reader.BaseStream.Position != reader.BaseStream.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid footer");
|
||||
}
|
||||
|
||||
using var footerStream = new MemoryStream(footer);
|
||||
using var footerReader = new BinaryReader(footerStream);
|
||||
var footerString = footerReader.ReadBytes(17);
|
||||
if (!footerString.SequenceEqual(FooterString))
|
||||
{
|
||||
throw new InvalidDataException("Invalid footer, no footer string");
|
||||
}
|
||||
FinalTimeCode = footerReader.ReadUInt32();
|
||||
_data = footerReader.ReadBytes(footer.Length - 25);
|
||||
if (footerReader.ReadInt32() != footerLength)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
|
||||
public ReplayFooter(uint finalTimeCode)
|
||||
{
|
||||
FinalTimeCode = finalTimeCode;
|
||||
_data = new byte[] { 0x02, 0x1A, 0x00, 0x00, 0x00 };
|
||||
}
|
||||
|
||||
List<float>? TryGetKillDeathRatio()
|
||||
{
|
||||
if (_data.Length < 24)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var ratios = new List<float>();
|
||||
using var stream = new MemoryStream(_data, _data.Length - 24, 24);
|
||||
using var reader = new BinaryReader(stream);
|
||||
ratios.Add(reader.ReadSingle());
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void WriteTo(BinaryWriter writer)
|
||||
{
|
||||
writer.Write(Terminator);
|
||||
writer.Write(FooterString);
|
||||
writer.Write(FinalTimeCode);
|
||||
writer.Write(_data);
|
||||
writer.Write(FooterString.Length + _data.Length + 8);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user