This commit is contained in:
2026-06-21 14:42:04 +02:00
parent 22cfb1f0a9
commit a3b4cc530d
15 changed files with 2437 additions and 403 deletions

View File

@@ -2,288 +2,220 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
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[]
private interface IArgumentReader
{
0x0F, // unk
0x5F, // unk
0x12, // unk
0x1B, // unk
0x48, // unk
0x52, // unk
0xFC, // unk
0xFD, // unk
object Read(BinaryReader reader, int count);
}
private sealed class ArgumentReader<T> : IArgumentReader
{
private readonly Func<BinaryReader, T> _reader;
public ArgumentReader(Func<BinaryReader, T> 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<CommandArgumentType, IArgumentReader> _readers = new()
{
[CommandArgumentType.Int32] = new ArgumentReader<int>(r => r.ReadInt32()),
[CommandArgumentType.Float32] = new ArgumentReader<float>(r => r.ReadSingle()),
[CommandArgumentType.Bool] = new ArgumentReader<bool>(r => r.ReadByte() != 0),
[CommandArgumentType.UInt16] = new ArgumentReader<ushort>(r => r.ReadUInt16()),
[CommandArgumentType.UInt32] = new ArgumentReader<uint>(r => r.ReadUInt32()),
[CommandArgumentType.UInt32_2] = new ArgumentReader<uint>(r => r.ReadUInt32()),
[CommandArgumentType.ObjectId] = new ArgumentReader<uint>(r => r.ReadUInt32()),
[CommandArgumentType.ObjectId_2] = new ArgumentReader<uint>(r => r.ReadUInt32()),
[CommandArgumentType.Vector3] = new ArgumentReader<Vector3>(ReadVector3),
[CommandArgumentType.AssetId] = new ArgumentReader<AssetId>(ReadAssetId),
[CommandArgumentType.AsciiString] = new ArgumentReader<string>(r => ReadString(r, CommandArgumentType.AsciiString)),
[CommandArgumentType.UnicodeString] = new ArgumentReader<string>(r => ReadString(r, CommandArgumentType.UnicodeString)),
};
private static readonly Dictionary<int, string> _commandNames;
public static ImmutableArray<int> UnknownCommands { get; } = new[]
{
0x1FD, // unk
0x25F, // unk
}.ToImmutableArray();
public static ImmutableArray<byte> AutoCommands { get; } = new byte[]
public static ImmutableArray<int> AutoCommands { get; } = new[]
{
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
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()
{
Action<BinaryReader> FixedSizeParser(byte command, int size)
_commandNames = new()
{
return (BinaryReader current) =>
{
var lastByte = current.ReadBytes(size - 2).Last();
if (lastByte != 0xFF)
{
Debug.Instance.DebugMessage += $"Failed to parse command {command:X}, last byte is {lastByte:X}";
// throw new InvalidDataException($"Failed to parse command {command:X}, last byte is {lastByte:X}");
}
};
}
[0x1] = "[游戏结束]", // 1
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;
[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
var size = ((x >> 4) + 1) * 4;
totalBytes += current.ReadBytes(size).Length;
}
totalBytes += 1;
var chk = totalBytes;
};
// 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
};
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)
public static List<CommandArgumentEntry> ReadCommandData(this BinaryReader reader)
{
if (_commandParser.TryGetValue(commandId, out var parser))
var list = new List<CommandArgumentEntry>();
while (true)
{
_commandParser[commandId](reader);
}
else
{
UnknownCommandParser(reader, commandId);
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(byte commandId)
public static bool IsUnknownCommand(int commandId)
{
return !_commandNames.ContainsKey(commandId);
}
public static string GetCommandName(byte commandId)
public static string GetCommandName(int commandId)
{
return _commandNames.TryGetValue(commandId, out var storedName)
? storedName
return _commandNames.TryGetValue(commandId, out var storedName)
? storedName
: $"(未知指令 0x{commandId:X2}";
}
public static void UnknownCommandParser(BinaryReader current, byte commandId)
private static AssetId ReadAssetId(BinaryReader reader)
{
while (true)
{
var value = current.ReadByte();
if (value == 0xFF)
{
break;
}
}
//return $"(未知指令 0x{commandID:2X}";
var version = reader.ReadByte();
var typeId = reader.ReadUInt32();
var instanceId = reader.ReadUInt32();
return new(TypeId: typeId, InstanceId: instanceId);
}
private static void ParseSpecialChunk0x01(BinaryReader current)
private static Vector3 ReadVector3(BinaryReader reader)
{
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;
var x = reader.ReadSingle();
var y = reader.ReadSingle();
var z = reader.ReadSingle();
return new(X: x, Y: y, Z: z);
}
private static void ParseSetRallyPoint0x02(BinaryReader current)
private static string ReadString(BinaryReader current, CommandArgumentType type)
{
var size = (current.ReadBytes(23).Last() + 1) * 2 + 1;
var lastByte = current.ReadBytes(size).Last();
if (lastByte != 0xFF)
var length = (int)current.ReadByte();
if (length == 0xFF)
{
throw new InvalidDataException();
length = current.ReadInt32();
}
}
private static void ParseUngarrison0x0C(BinaryReader current)
{
current.ReadByte();
var size = (current.ReadByte() + 1) * 4 + 1;
var lastByte = current.ReadBytes(size).Last();
if (lastByte != 0xFF)
// read byte string or wchar_t string based on T type
switch (type)
{
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();
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}");
}
}
}