wip
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user