using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Text; namespace AnotherReplayReader.ReplayFile { internal static class RA3Commands { private interface IArgumentReader { object Read(BinaryReader reader, int count); } private sealed class ArgumentReader : IArgumentReader { private readonly Func _reader; public ArgumentReader(Func reader) => _reader = reader; public object Read(BinaryReader reader, int count) { if (count == 1) { return _reader(reader)!; } var result = new T[count]; for (int i = 0; i < count; i++) { result[i] = _reader(reader); } return result; } } private static readonly Dictionary _readers = new() { [CommandArgumentType.Int32] = new ArgumentReader(r => r.ReadInt32()), [CommandArgumentType.Float32] = new ArgumentReader(r => r.ReadSingle()), [CommandArgumentType.Bool] = new ArgumentReader(r => r.ReadByte() != 0), [CommandArgumentType.UInt16] = new ArgumentReader(r => r.ReadUInt16()), [CommandArgumentType.UInt32] = new ArgumentReader(r => r.ReadUInt32()), [CommandArgumentType.UInt32_2] = new ArgumentReader(r => r.ReadUInt32()), [CommandArgumentType.ObjectId] = new ArgumentReader(r => r.ReadUInt32()), [CommandArgumentType.ObjectId_2] = new ArgumentReader(r => r.ReadUInt32()), [CommandArgumentType.Vector3] = new ArgumentReader(ReadVector3), [CommandArgumentType.AssetId] = new ArgumentReader(ReadAssetId), [CommandArgumentType.AsciiString] = new ArgumentReader(r => ReadString(r, CommandArgumentType.AsciiString)), [CommandArgumentType.UnicodeString] = new ArgumentReader(r => ReadString(r, CommandArgumentType.UnicodeString)), }; private static readonly Dictionary _commandNames; public static ImmutableArray UnknownCommands { get; } = new[] { 0x1FD, // unk 0x25F, // unk }.ToImmutableArray(); public static ImmutableArray AutoCommands { get; } = new[] { 0x1, 0x221, // 3 seconds heartbeat 0x233, // uuid 0x234, // uuid 0x235, // player info 0x237, // indeterminate autogen 0x247, // 5th frame auto gen 0x252, // another player quits }.ToImmutableArray(); static RA3Commands() { _commandNames = new() { [0x1] = "[游戏结束]", // 1 [0x1F5] = "选择单位", // 501 [0x1F6] = "选择相同单位(W)", // 502 [0x1F8] = "取消选择", // 504 [0x1F9] = "从选择中移除单位", // 505 [0x1FA] = "创建编队", // 506 [0x1FB] = "选择编队", // 507 [0x1FC] = "将编队加入选择", // 508 [0x1FD] = "(未知指令 0x1FD)", // 509 [0x1FE] = "释放特殊能力(无目标)", // 510 [0x1FF] = "释放特殊能力(指定位置)", // 511 [0x200] = "释放特殊能力(指定位置和角度)", // 512 [0x201] = "释放特殊能力(指定目标)", // 513 [0x202] = "设置集结点", // 514 [0x203] = "开始升级", // 515 [0x204] = "暂停/中止升级", // 516 [0x205] = "开始出兵", // 517 [0x206] = "暂停/取消出兵", // 518 [0x207] = "开始建造", // 519 [0x208] = "暂停/取消建造", // 520 [0x209] = "摆放建筑", // 521 [0x20A] = "出售建筑", // 522 // 523 [0x20C] = "从进驻的建筑撤出(?)", // 524 [0x20D] = "集火攻击", // 525 [0x20E] = "强制攻击单位(Ctrl)", // 526 [0x20F] = "强制攻击地板(Ctrl)", // 527 [0x210] = "进驻建筑", // 528 // 529 [0x212] = "命令矿车交矿", // 530 // 531 [0x214] = "移动", // 532 [0x215] = "行进攻击(A)", // 533 [0x216] = "强制移动/碾压(G)", // 534 // 535 // 536 // 537 [0x21A] = "停止(S)", // 538 [0x21B] = "散开(X)", // 539 // 0x21E 542 有可能是 AI 信标相关的? [0x221] = "[游戏每3秒自动产生的检测不同步指令]", // 545 [0x228] = "开始维修建筑", // 552 [0x229] = "停止维修建筑", // 553 [0x22A] = "选择所有单位(Q)", // 554 // 555 [0x22C] = "队形操作(左右键)", // 556 // 557 [0x22E] = "切换姿态(警戒/侵略/固守/停火模式)", // 558 [0x22F] = "路径点模式/计划模式(Alt)", // 559 // 560 // 561 [0x232] = "释放特殊能力(一个或多个目标)", // 562 [0x233] = "[游戏自动生成的UUID指令]", // 563 [0x234] = "[游戏自动产生的UUID]", // 564 [0x235] = "[玩家信息(?)]", // 565 [0x236] = "倒车(D)", // 566 [0x237] = "[游戏不定期自动产生的指令]", // 567 [0x247] = "[游戏在第五帧自动产生的指令]", // 583 [0x248] = "让矿车去采矿", // 584 [0x24B] = "信标", // 587 [0x24C] = "删除信标", // 588 [0x24D] = "在信标里输入文字", // 589 [0x24E] = "选择协议", // 590 [0x252] = "[其他玩家主动退出游戏]", // 594 [0x25F] = "(未知指令 0x25F)", // 607 }; } public static List ReadCommandData(this BinaryReader reader) { var list = new List(); while (true) { byte head = reader.ReadByte(); if (head == 0xFF) break; int count = (head >> 4) + 1; var type = (CommandArgumentType)(head & 0xF); var value = _readers[type].Read(reader, count); list.Add(new CommandArgumentEntry(type, value, count)); } return list; } public static bool IsUnknownCommand(int commandId) { return !_commandNames.ContainsKey(commandId); } public static string GetCommandName(int commandId) { return _commandNames.TryGetValue(commandId, out var storedName) ? storedName : $"(未知指令 0x{commandId:X2})"; } private static AssetId ReadAssetId(BinaryReader reader) { var version = reader.ReadByte(); var typeId = reader.ReadUInt32(); var instanceId = reader.ReadUInt32(); return new(TypeId: typeId, InstanceId: instanceId); } private static Vector3 ReadVector3(BinaryReader reader) { var x = reader.ReadSingle(); var y = reader.ReadSingle(); var z = reader.ReadSingle(); return new(X: x, Y: y, Z: z); } private static string ReadString(BinaryReader current, CommandArgumentType type) { var length = (int)current.ReadByte(); if (length == 0xFF) { length = current.ReadInt32(); } // read byte string or wchar_t string based on T type switch (type) { case CommandArgumentType.AsciiString: return Encoding.UTF8.GetString(current.ReadBytes(length)); case CommandArgumentType.UnicodeString: return Encoding.Unicode.GetString(current.ReadBytes(length * 2)); } throw new InvalidDataException($"Invalid string type {type}"); } } }