2021-10-13 20:17:32 +02:00

434 lines
15 KiB
C#

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AnotherReplayReader
{
internal sealed class Player
{
public static readonly IReadOnlyDictionary<string, string> ComputerNames = new Dictionary<string, string>
{
{ "E", "简单" },
{ "M", "中等" },
{ "H", "困难" },
{ "B", "凶残" },
};
public string PlayerName { get; private set; }
public uint PlayerIP { get; private set; }
public int FactionID { get; private set; }
public int Team { get; private set; }
public Player(string[] playerEntry)
{
var isComputer = playerEntry[0][0] == 'C';
PlayerName = playerEntry[0].Substring(1);
if (isComputer)
{
PlayerName = ComputerNames[PlayerName];
PlayerIP = 0;
FactionID = int.Parse(playerEntry[2]);
Team = int.Parse(playerEntry[4]);
}
else
{
PlayerIP = uint.Parse(playerEntry[1], System.Globalization.NumberStyles.HexNumber);
FactionID = int.Parse(playerEntry[5]);
Team = int.Parse(playerEntry[7]);
}
}
}
internal enum ReplayType
{
Skirmish,
Lan,
Online
}
internal sealed class ReplayChunk
{
public uint TimeCode { get; private set; }
public byte Type { get; private set; }
public byte[] Data { get; private set; }
public ReplayChunk(uint timeCode, BinaryReader reader)
{
TimeCode = timeCode; // reader.ReadUInt32();
Type = reader.ReadByte();
var chunkSize = reader.ReadInt32();
Data = reader.ReadBytes(chunkSize);
if(reader.ReadInt32() != 0)
{
throw new InvalidDataException("Replay Chunk not ended with zero");
}
}
}
internal enum ReplayFooterOption
{
SeekToFooter,
CurrentlyAtFooter,
}
internal sealed class ReplayFooter
{
public const uint Terminator = 0x7FFFFFFF;
public static readonly byte[] FooterString = Encoding.ASCII.GetBytes("RA3 REPLAY FOOTER");
public uint FinalTimeCode { get; private set; }
public byte[] Data { get; private set; }
public ReplayFooter(BinaryReader reader, ReplayFooterOption option)
{
var currentPosition = reader.BaseStream.Position;
reader.BaseStream.Seek(-4, SeekOrigin.End);
var footerLength = reader.ReadInt32();
if (option == ReplayFooterOption.SeekToFooter)
{
currentPosition = reader.BaseStream.Length - footerLength;
}
reader.BaseStream.Seek(currentPosition, SeekOrigin.Begin);
var footer = reader.ReadBytes(footerLength);
if(footer.Length != footerLength || reader.BaseStream.Position != reader.BaseStream.Length)
{
throw new InvalidDataException("Invalid footer");
}
using (var footerStream = new MemoryStream(footer))
using (var footerReader = new BinaryReader(footerStream))
{
var footerString = footerReader.ReadBytes(17);
if(!footerString.SequenceEqual(FooterString))
{
throw new InvalidDataException("Invalid footer, no footer string");
}
FinalTimeCode = footerReader.ReadUInt32();
Data = footerReader.ReadBytes(footer.Length - 25);
if(footerReader.ReadInt32() != footerLength)
{
throw new InvalidDataException();
}
}
}
public ReplayFooter(uint finalTimeCode)
{
FinalTimeCode = finalTimeCode;
Data = new byte[] { 0x02, 0x1A, 0x00, 0x00, 0x00 };
}
List<float> TryGetKillDeathRatio()
{
if(Data.Length < 24)
{
return null;
}
var ratios = new List<float>();
using (var stream = new MemoryStream(Data, Data.Length - 24, 24))
using (var reader = new BinaryReader(stream))
{
ratios.Add(reader.ReadSingle());
}
return ratios;
}
}
internal static class ReplayExtensions
{
public static string ReadUTF16String(this BinaryReader reader)
{
var currentBytes = new List<byte>();
byte[] lastTwoBytes = null;
while(true)
{
lastTwoBytes = reader.ReadBytes(2);
if (lastTwoBytes.Length != 2)
{
throw new InvalidDataException();
}
if(lastTwoBytes.All(x => x == 0))
{
break;
}
currentBytes.AddRange(lastTwoBytes);
}
return Encoding.Unicode.GetString(currentBytes.ToArray());
}
public static void WriteChunk(this BinaryWriter writer, ReplayChunk chunk)
{
writer.Write(chunk.TimeCode);
writer.Write(chunk.Type);
writer.Write(chunk.Data.Length);
writer.Write(chunk.Data);
writer.Write(0);
}
public static void WriteFooter(this BinaryWriter writer, ReplayFooter footer)
{
writer.Write(ReplayFooter.Terminator);
writer.Write(ReplayFooter.FooterString);
writer.Write(footer.FinalTimeCode);
writer.Write(footer.Data);
writer.Write(ReplayFooter.FooterString.Length + footer.Data.Length + 8);
}
public static void WriteReplay(this BinaryWriter writer, Replay replay)
{
writer.Write(replay.RawHeader);
var lastTimeCode = (uint)0;
foreach(var chunk in replay.Body)
{
lastTimeCode = chunk.TimeCode;
writer.WriteChunk(chunk);
}
if (replay.HasFooter)
{
writer.WriteFooter(replay.Footer);
}
else
{
writer.WriteFooter(new ReplayFooter(lastTimeCode));
}
}
}
internal sealed class Replay
{
public static readonly byte[] HeaderMagic = Encoding.ASCII.GetBytes("RA3 REPLAY HEADER");
public static readonly Dictionary<ReplayType, string> TypeStrings = new Dictionary<ReplayType, string>()
{
{ ReplayType.Skirmish, "遭遇战录像" },
{ ReplayType.Lan, "局域网录像" },
{ ReplayType.Online, "官网录像" },
};
public string Path { get; private set; }
public string FileName => System.IO.Path.GetFileNameWithoutExtension(Path);
public DateTime Date { get; private set; }
public bool HasFooter => Footer != null;
public TimeSpan? Length { get; private set; }
public string MapName { get; private set; }
public string MapPath { get; private set; }
public IReadOnlyList<Player> Players => _players;
public int NumberOfPlayingPlayers { get; private set; }
public long Size { get; private set; }
public Mod Mod { get; private set; }
public ReplayType Type { get; private set; }
public string TypeString => TypeStrings[Type];
public bool HasCommentator { get; private set; }
public Player ReplaySaver => Players[_replaySaverIndex];
public byte[] RawHeader { get; private set; }
public IReadOnlyList<ReplayChunk> Body => _body;
public ReplayFooter Footer { get; private set; }
private List<Player> _players;
private byte _replaySaverIndex;
private long _headerSize;
private List<ReplayChunk> _body;
public Replay(string path)
{
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
Parse(path, stream);
}
}
public Replay(string path, Stream stream)
{
Parse(path, stream);
}
private void Parse(string path, Stream stream)
{
Path = path;
using (var reader = new BinaryReader(stream))
{
Size = reader.BaseStream.Length;
var headerMagic = reader.ReadBytes(HeaderMagic.Length);
if(!headerMagic.SequenceEqual(HeaderMagic))
{
throw new InvalidDataException($"{Path} is not a replay, header is {BitConverter.ToString(headerMagic)}");
}
var isSkirmish = reader.ReadByte() == 0x04;
reader.ReadBytes(4 * 4); // version and builds
reader.ReadBytes(2); // commentary flag, and padding zero byte
reader.ReadUTF16String(); // title
reader.ReadUTF16String(); // description
MapName = reader.ReadUTF16String(); // map name
reader.ReadUTF16String(); // map id
NumberOfPlayingPlayers = reader.ReadByte();
for(var i = 0; i <= NumberOfPlayingPlayers; ++i)
{
reader.ReadUInt32();
reader.ReadUTF16String(); // utf16 player name
if(!isSkirmish)
{
reader.ReadByte(); // team
}
}
var offset = reader.ReadInt32();
var cnc3MagicLength = reader.ReadInt32();
_headerSize = reader.BaseStream.Position + offset;
reader.ReadBytes(cnc3MagicLength);
var modInfo = reader.ReadBytes(22);
Mod = new Mod(Encoding.UTF8.GetString(modInfo));
var timeStamp = reader.ReadUInt32();
Date = DateTimeOffset.FromUnixTimeSeconds(timeStamp).DateTime;
reader.ReadBytes(31);
var descriptionsLength = reader.ReadInt32();
var description = Encoding.UTF8.GetString(reader.ReadBytes(descriptionsLength));
var entries = null as Dictionary<string, string>;
try
{
var query = from splitted in description.Split(';')
where !string.IsNullOrWhiteSpace(splitted)
select splitted.Split(new[] { '=' }, 2);
entries = query.ToDictionary(x => x[0], x => x[1]);
}
catch(Exception exception)
{
throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{exception}");
}
try
{
_players = entries["S"].Split(':')
.TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X')
.Select(x => new Player(x.Split(',')))
.ToList();
}
catch (Exception exception)
{
throw new InvalidDataException($"Failed to parse playerdata from string header of replay {Path}: \r\n{exception}");
}
MapPath = entries["M"].Substring(3);
HasCommentator = !entries["PC"].Equals("-1");
var lanFlag = int.Parse(entries["GT"]) == 0;
if (lanFlag)
{
if(_players.First().PlayerIP == 0)
{
Type = ReplayType.Skirmish;
}
else
{
Type = ReplayType.Lan;
}
}
else
{
Type = ReplayType.Online;
}
_replaySaverIndex = reader.ReadByte();
reader.ReadBytes(8); // 8 bit paddings
var fileNameLength = reader.ReadInt32();
reader.ReadBytes(fileNameLength * 2);
reader.ReadBytes(16);
var verMagicLength = reader.ReadInt32();
reader.ReadBytes(verMagicLength);
reader.ReadBytes(85);
if(reader.BaseStream.Position != _headerSize)
{
throw new InvalidDataException();
}
reader.BaseStream.Seek(-4, SeekOrigin.End);
try
{
Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter);
Length = TimeSpan.FromSeconds(Math.Round(Footer.FinalTimeCode / 15.0));
}
catch(Exception)
{
Length = null;
Footer = null;
}
}
}
public void ParseBody()
{
_body = new List<ReplayChunk>();
using (var stream = new FileStream(Path, FileMode.Open))
using (var reader = new BinaryReader(stream))
{
RawHeader = reader.ReadBytes((int)_headerSize);
while (true)
{
var timeCode = reader.ReadUInt32();
if (timeCode == ReplayFooter.Terminator)
{
Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter);
break;
}
_body.Add(new ReplayChunk(timeCode, reader));
}
}
}
public Dictionary<byte, int[]> GetCommandCounts()
{
var playerCommands = new Dictionary<byte, int[]>();
foreach(var chunk in _body)
{
if(chunk.Type != 1)
{
continue;
}
foreach(var command in CommandChunk.Parse(chunk))
{
var commandCount = playerCommands.TryGetValue(command.CommandID, out var current) ? current : new int[Players.Count];
if(command.PlayerIndex >= commandCount.Length) // unknown or unparsable command?
{
commandCount = commandCount
.Concat(new int[command.PlayerIndex - commandCount.Length + 1])
.ToArray();
}
commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1;
playerCommands[command.CommandID] = commandCount;
}
}
return playerCommands;
}
}
}