304 lines
12 KiB
C#
304 lines
12 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace AnotherReplayReader
|
||
{
|
||
internal static class RA3Commands
|
||
{
|
||
//public static IReadOnlyDictionary<byte, Func<BinaryReader, string>> CommandParser { get; private set; }
|
||
|
||
public static IReadOnlyDictionary<byte, Action<BinaryReader>> CommandParser => _commandParser;
|
||
public static IReadOnlyDictionary<byte, string> CommandNames => _commandNames;
|
||
|
||
private static Dictionary<byte, Action<BinaryReader>> _commandParser;
|
||
private static 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 UnknownCommandParser(BinaryReader current, byte commandID)
|
||
{
|
||
while(true)
|
||
{
|
||
var value = current.ReadByte();
|
||
if (value == 0xFF)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
//return $"(未知指令 0x{commandID:2X})";
|
||
}
|
||
|
||
public static string GetCommandName(byte commandID)
|
||
{
|
||
return CommandNames.TryGetValue(commandID, out var storedName) ? storedName : $"(未知指令 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)
|
||
{
|
||
if (chunk.Type != 1)
|
||
{
|
||
throw new NotImplementedException();
|
||
}
|
||
|
||
int ManglePlayerID(byte ID)
|
||
{
|
||
return ID / 8 - 2;
|
||
}
|
||
|
||
using (var stream = new MemoryStream(chunk.Data))
|
||
using (var reader = new BinaryReader(stream))
|
||
{
|
||
if(reader.ReadByte() != 1)
|
||
{
|
||
throw new InvalidDataException("Payload first byte not 1");
|
||
}
|
||
|
||
var list = new List<CommandChunk>();
|
||
var numberOfCommands = reader.ReadInt32();
|
||
for(var i = 0; i < numberOfCommands; ++i)
|
||
{
|
||
var commandID = reader.ReadByte();
|
||
var playerID = reader.ReadByte();
|
||
if (RA3Commands.CommandParser.TryGetValue(commandID, out var parser))
|
||
{
|
||
RA3Commands.CommandParser[commandID](reader);
|
||
}
|
||
else
|
||
{
|
||
RA3Commands.UnknownCommandParser(reader, 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;
|
||
}
|
||
}
|
||
}
|
||
}
|