316 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AnotherReplayReader.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
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<ReplayType, string> TypeStrings = new()
{
{ ReplayType.Skirmish, "遭遇战录像" },
{ ReplayType.Lan, "局域网录像" },
{ ReplayType.Online, "官网录像" },
};
public const double FrameRate = 15.0;
public const string PostCommentator = "post Commentator";
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 ImmutableArray<Player> 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 ImmutableArray<ReplayChunk>? 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<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 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(',')))
.ToImmutableArray();
}
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<ReplayChunk>();
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.ToImmutableArray();
}
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()
{
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.Length
? 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(PostCommentator))
{
break;
}
var factionName = ModData.GetFaction(Mod, player.FactionId).Name;
writer.WriteLine($"{player.PlayerName}{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));
}
}
}
}