using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace AnotherReplayReader.ReplayFile { internal static class RA3Commands { private static readonly Dictionary> _commandParser; private static readonly Dictionary _commandNames; static RA3Commands() { Action 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 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>, 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, string)> { (0x01, ParseSpecialChunk0x01, "[游戏自动生成的指令]"), (0x02, ParseSetRallyPoint0x02, "设计集结点"), (0x0C, ParseUngarrison0x0C, "从进驻的建筑撤出(?)"), (0x10, ParseGarrison0x10, "进驻建筑"), (0x33, ParseUuid0x33, "[游戏自动生成的UUID指令]"), (0x4B, ParsePlaceBeacon0x4B, "信标") }; _commandParser = new Dictionary>(); _commandNames = new Dictionary(); 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 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(); 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; } } }