using AnotherReplayReader.Utils; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace AnotherReplayReader.ReplayFile { internal enum ReplayType { Skirmish, Lan, Online } internal sealed class Replay { public static readonly byte[] HeaderMagic = Encoding.ASCII.GetBytes("RA3 REPLAY HEADER"); public static readonly Dictionary TypeStrings = new() { { ReplayType.Skirmish, "遭遇战录像" }, { ReplayType.Lan, "局域网录像" }, { ReplayType.Online, "官网录像" }, }; private readonly byte _replaySaverIndex; private readonly byte[]? _rawHeader; public string Path { get; } public DateTime Date { get; } public string MapName { get; } public string MapPath { get; } public IReadOnlyList Players { get; } public int NumberOfPlayingPlayers { get; } public Mod Mod { get; } public ReplayType Type { get; } public bool HasCommentator { get; } public long Size { get; } public ReplayFooter? Footer { get; } public IReadOnlyList? Body { get; } public string FileName => System.IO.Path.GetFileNameWithoutExtension(Path); public Player ReplaySaver => Players[_replaySaverIndex]; public string TypeString => TypeStrings[Type]; public bool HasFooter => Footer != null; public ShortTimeSpan? Length => Footer?.ReplayLength; public Replay(string path, bool parseBody = false) : this(path, new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), parseBody) { } public Replay(string path, Stream stream, bool parseBody = false) { 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(); var headerSize = checked((int)(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; 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 e) { throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{e}"); } 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) { Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (acutally {reader.BaseStream.Position})\r\n"; return; } if (!parseBody) { // jump to footer directly reader.BaseStream.Seek(-4, SeekOrigin.End); try { Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter); } catch (Exception e) { Debug.Instance.DebugMessage += $"Failed to parse replay footer, replay might be corrupt: {e}\r\n"; Footer = null; } return; } var body = new List(); try { while (true) { var timeCode = reader.ReadUInt32(); if (timeCode == ReplayFooter.Terminator) { Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter); break; } body.Add(new ReplayChunk(timeCode, reader)); } } catch (Exception e) { Debug.Instance.DebugMessage += $"Failed to parse replay body, replay might be corrupt: {e}\r\n"; } byte[]? rawHeader = null; try { // 重新读取原来的整个录像头 reader.BaseStream.Seek(0, SeekOrigin.Begin); rawHeader = reader.ReadBytes(headerSize); } catch (Exception e) { Debug.Instance.DebugMessage += $"Warning: failed to read raw header: {e}\r\n"; return; } if (rawHeader.Length != headerSize) { Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (raw header length = {rawHeader.Length})\r\n"; return; } _rawHeader = rawHeader; Body = body; } public Dictionary GetCommandCounts() { if (Body is null) { throw new InvalidOperationException("Replay body must be parsed before retrieving command chunks"); } var playerCommands = new Dictionary(); 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; } public Replay CloneHeader() { using var stream = new MemoryStream(); using (var writer = new BinaryWriter(stream, Encoding.UTF8, true)) { WriteTo(writer); } stream.Position = 0; return new Replay(Path, stream); } public bool PathEquals(Replay replay) => PathEquals(replay.Path); public bool PathEquals(string path) => Path.Equals(path, StringComparison.OrdinalIgnoreCase); public string GetDetails(PlayerIdentity playerIdentity) { static string GetSizeString(double size) { if (size > 1024 * 1024) { return $"{Math.Round(size / (1024 * 1024), 2)}MB"; } return $"{Math.Round(size / 1024)}KB"; } string size = GetSizeString(Size); string length = Length?.ToString() ?? "录像已损坏,请先修复录像"; var replaySaver = _replaySaverIndex < Players.Count ? ReplaySaver.PlayerName : "[无法获取保存录像的玩家]"; using var writer = new StringWriter(); writer.WriteLine("文件名:{0}", FileName); writer.WriteLine("大小:{0}", size); writer.WriteLine("地图:{0}", MapName); writer.WriteLine("日期:{0}", Date); writer.WriteLine("长度:{0}", length); writer.WriteLine("录像类别:{0}", TypeString); writer.WriteLine("这个文件是{0}保存的", replaySaver); writer.WriteLine("玩家列表:"); foreach (var player in Players) { if (player == Players.Last() && player.PlayerName.Equals("post Commentator")) { break; } var factionName = ModData.GetFaction(Mod, player.FactionId).Name; var realName = Type == ReplayType.Lan ? playerIdentity.FormatRealName(player.PlayerIp) : string.Empty; writer.WriteLine($"{player.PlayerName + realName},{factionName}"); } return writer.ToString(); } public void WriteTo(BinaryWriter writer) => WriteTo(writer, false); private void WriteTo(BinaryWriter writer, bool skipBody) { if ((_rawHeader is null || Body is null) && !skipBody) { throw new InvalidOperationException("Replay body must be parsed before writing replay"); } writer.Write(_rawHeader); var lastTimeCode = Footer?.FinalTimeCode; if (Body is not null) { foreach (var chunk in Body) { lastTimeCode = chunk.TimeCode; writer.Write(chunk); } } if (Footer is not null) { writer.Write(Footer); } else if (lastTimeCode is uint lastTimeCodeValue) { writer.Write(new ReplayFooter(lastTimeCodeValue)); } } } }