This commit is contained in:
2021-10-20 22:22:19 +02:00
parent 78a7310a3b
commit 0863b2fd71
12 changed files with 1206 additions and 508 deletions

View File

@@ -1,264 +1,15 @@
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:X2}";
}
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)
public static List<CommandChunk> Parse(ReplayChunk chunk)
{
if (chunk.Type != 1)
{
@@ -297,5 +48,10 @@ namespace AnotherReplayReader.ReplayFile
return list;
}
public override string ToString()
{
return $"[玩家 {PlayerIndex}{RA3Commands.GetCommandName(CommandId)}]";
}
}
}

View File

@@ -12,6 +12,7 @@ namespace AnotherReplayReader.ReplayFile
{ "B", "凶残" },
};
public bool IsComputer { get; }
public string PlayerName { get; }
public uint PlayerIp { get; }
public int FactionId { get; }
@@ -19,11 +20,11 @@ namespace AnotherReplayReader.ReplayFile
public Player(string[] playerEntry)
{
var isComputer = playerEntry[0][0] == 'C';
IsComputer = playerEntry[0][0] == 'C';
PlayerName = playerEntry[0].Substring(1);
if (isComputer)
if (IsComputer)
{
PlayerName = ComputerNames[PlayerName];
PlayerIp = 0;

288
ReplayFile/RA3Commands.cs Normal file
View File

@@ -0,0 +1,288 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
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;
public static ImmutableArray<byte> UnknownCommands { get; } = new byte[]
{
0x0F, // unk
0x5F, // unk
0x12, // unk
0x1B, // unk
0x48, // unk
0x52, // unk
0xFC, // unk
0xFD, // unk
}.ToImmutableArray();
public static ImmutableArray<byte> AutoCommands { get; } = new byte[]
{
0x01, // auto gen
0x21, // 3 seconds heartbeat
0x33, // uuid
0x34, // uuid
0x35, // player info
0x37, // indeterminate autogen
0x47, // 5th frame auto gen
0xF6, // auto, maybe from barrack deploy
0xF9, // auto, maybe unit from structures
}.ToImmutableArray();
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, "(未知指令 0x0F"),
(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, "(未知指令 0x5F"),
(0x0A, VariableSizeParser, 2, "出售建筑"),
(0x0D, VariableSizeParser, 2, "右键攻击"),
(0x0E, VariableSizeParser, 2, "强制攻击Ctrl"),
(0x12, VariableSizeParser, 2, "(未知指令 0x12"),
(0x1A, VariableSizeParser, 2, "停止S"),
(0x1B, VariableSizeParser, 2, "(未知指令 0x1B"),
(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, "(未知指令 0x48"),
(0x4C, VariableSizeParser, 2, "删除信标(或 F9"),
(0x4E, VariableSizeParser, 2, "选择协议"),
(0x52, VariableSizeParser, 2, "(未知指令 0x52"),
(0xF5, VariableSizeParser, 5, "选择单位"),
(0xF6, VariableSizeParser, 5, "[未知指令,貌似会在展开兵营核心时自动产生?]"),
(0xF8, VariableSizeParser, 4, "鼠标左键单击/取消选择"),
(0xF9, VariableSizeParser, 2, "[可能是步兵自行从进驻的建筑撤出]"),
(0xFA, VariableSizeParser, 7, "创建编队"),
(0xFB, VariableSizeParser, 2, "选择编队"),
(0xFC, VariableSizeParser, 2, "(未知指令 0xFC"),
(0xFD, VariableSizeParser, 7, "(未知指令 0xFD"),
(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 bool IsUnknownCommand(byte commandId)
{
return !_commandNames.ContainsKey(commandId);
}
public static string GetCommandName(byte commandId)
{
return _commandNames.TryGetValue(commandId, out var storedName)
? storedName
: $"(未知指令 0x{commandId:X2}";
}
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();
}
}
}
}

View File

@@ -1,6 +1,7 @@
using AnotherReplayReader.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
@@ -23,6 +24,8 @@ namespace AnotherReplayReader.ReplayFile
{ ReplayType.Lan, "局域网录像" },
{ ReplayType.Online, "官网录像" },
};
public const double FrameRate = 15.0;
public const string PostCommentator = "post Commentator";
private readonly byte _replaySaverIndex;
private readonly byte[]? _rawHeader;
@@ -31,7 +34,7 @@ namespace AnotherReplayReader.ReplayFile
public DateTime Date { get; }
public string MapName { get; }
public string MapPath { get; }
public IReadOnlyList<Player> Players { get; }
public ImmutableArray<Player> Players { get; }
public int NumberOfPlayingPlayers { get; }
public Mod Mod { get; }
public ReplayType Type { get; }
@@ -39,7 +42,7 @@ namespace AnotherReplayReader.ReplayFile
public long Size { get; }
public ReplayFooter? Footer { get; }
public IReadOnlyList<ReplayChunk>? Body { get; }
public ImmutableArray<ReplayChunk>? Body { get; }
public string FileName => System.IO.Path.GetFileNameWithoutExtension(Path);
public Player ReplaySaver => Players[_replaySaverIndex];
@@ -119,7 +122,7 @@ namespace AnotherReplayReader.ReplayFile
Players = entries["S"].Split(':')
.TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X')
.Select(x => new Player(x.Split(',')))
.ToList();
.ToImmutableArray();
}
catch (Exception exception)
{
@@ -219,40 +222,7 @@ namespace AnotherReplayReader.ReplayFile
}
_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;
Body = body.ToImmutableArray();
}
public Replay CloneHeader()
@@ -283,7 +253,7 @@ namespace AnotherReplayReader.ReplayFile
string size = GetSizeString(Size);
string length = Length?.ToString() ?? "录像已损坏,请先修复录像";
var replaySaver = _replaySaverIndex < Players.Count
var replaySaver = _replaySaverIndex < Players.Length
? ReplaySaver.PlayerName
: "[无法获取保存录像的玩家]";
@@ -298,7 +268,7 @@ namespace AnotherReplayReader.ReplayFile
writer.WriteLine("玩家列表:");
foreach (var player in Players)
{
if (player == Players.Last() && player.PlayerName.Equals("post Commentator"))
if (player == Players.Last() && player.PlayerName.Equals(PostCommentator))
{
break;
}

View File

@@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
namespace AnotherReplayReader.ReplayFile
{
@@ -9,6 +10,8 @@ namespace AnotherReplayReader.ReplayFile
public uint TimeCode { get; }
public byte Type { get; }
public TimeSpan Time => TimeSpan.FromSeconds(TimeCode / Replay.FrameRate);
public ReplayChunk(uint timeCode, BinaryReader reader)
{
TimeCode = timeCode; // reader.ReadUInt32();

View File

@@ -20,7 +20,7 @@ namespace AnotherReplayReader.ReplayFile
private readonly byte[] _data;
public uint FinalTimeCode { get; }
public TimeSpan ReplayLength => TimeSpan.FromSeconds(FinalTimeCode / 15.0);
public TimeSpan ReplayLength => TimeSpan.FromSeconds(FinalTimeCode / Replay.FrameRate);
public ReplayFooter(BinaryReader reader, ReplayFooterOption option)
{