using AnotherReplayReader.ReplayFile; using AnotherReplayReader.Utils; using OxyPlot; using OxyPlot.Axes; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Windows; using System.Windows.Threading; namespace AnotherReplayReader.Apm { public class ApmPlotterFilterOptions { public bool CountUnknowns { get; } public bool CountAutos { get; } public bool CountClicks { get; } public ApmPlotterFilterOptions(bool countUnknowns, bool countAutos, bool countClicks) { CountUnknowns = countUnknowns; CountAutos = countAutos; CountClicks = countClicks; } public bool ShouldSkip(int command) { if (!CountUnknowns && ApmPlotter.IsUnknown(command)) { return true; } if (!CountAutos && ApmPlotter.IsAuto(command)) { return true; } if (!CountClicks && ApmPlotter.IsClick(command)) { return true; } return false; } } internal class ApmPlotter { private static readonly ConcurrentDictionary UnknownCommands; private static readonly ConcurrentDictionary AutoCommands; private readonly ImmutableArray<(TimeSpan, ImmutableArray)> _commands; public Replay Replay { get; } public ImmutableSortedDictionary PlayersMap { get; } public ImmutableArray PlayersArray { get; } public ImmutableArray PlayerLifes { get; } public ImmutableArray StricterPlayerLifes { get; } public TimeSpan ReplayLength { get; } public ImmutableArray<(TimeSpan, ImmutableArray)> Commands => _commands; static ApmPlotter() { static KeyValuePair Create(int x) => new(x, default); UnknownCommands = new(RA3Commands.UnknownCommands.Select(Create)); AutoCommands = new(RA3Commands.AutoCommands.Select(Create)); } public ApmPlotter(Replay replay) { if (replay.Body is not { } body) { throw new InvalidOperationException("Replay must be fully parsed!"); } Replay = replay; var queryFull = from chunk in body where chunk.Type is 1 from command in CommandChunk.Parse(chunk) select command; var playersIndices = queryFull .Select(x => x.PlayerIndex) .Distinct() .OrderBy(x => x) .ToArray(); // if length mismatch and last is post commentator, remove post commentator from replay players var replayPlayers = replay.Players; var postCommentatorIndex = replayPlayers.FindIndex(p => p.PlayerName == Replay.PostCommentator); if (playersIndices.Length + 1 == replayPlayers.Length && postCommentatorIndex is int index) { replayPlayers = replayPlayers.RemoveAt(index); } if (playersIndices.Length != replayPlayers.Length) { throw new InvalidOperationException( $"Player count mismatch. Indices={playersIndices.Length}, Players={replayPlayers.Length}"); } PlayersMap = playersIndices .Zip(replayPlayers, (index, player) => new KeyValuePair(index, player)) .ToImmutableSortedDictionary(); PlayersArray = PlayersMap.Values.ToImmutableArray(); //var queryFull = from chunk in body // where chunk.Type is 1 // from command in CommandChunk.Parse(chunk) // select (chunk.Time, command); //var sb = new System.Text.StringBuilder(); //var stringHashes = System.IO.File.ReadAllText(@"C:\Apps\RA3-MODSDK-X\builtmods\StringHashes.xml"); //XDocument doc = XDocument.Parse(stringHashes); //XNamespace ns = "uri:ea.com:eala:asset"; //var table = doc // .Descendants(ns + "StringHashTable") // .FirstOrDefault(x => (string?)x.Attribute("id") == "StringHashBin_INSTANCEID"); //if (table == null) // throw new InvalidOperationException("StringHashBin_INSTANCEID not found."); //Dictionary hashTable = table // .Elements(ns + "StringAndHash") // .ToDictionary( // x => uint.Parse(x.Attribute("Hash")!.Value), // x => x.Attribute("Text")!.Value); //foreach (var (time, command) in queryFull) //{ // if (IsUnknown(command.CommandId) || IsAuto(command.CommandId)) // { // continue; // } // var name = RA3Commands.GetCommandName(command.CommandId); // sb.Append($"[{time}] 玩家 {command.PlayerIndex},{name}\r\n"); // foreach (var kv in command.Data) // { // // try to convert int32 or uint32 to hashes and retrieve text // var textFromHash = string.Empty; // uint hashValue = kv.Key switch // { // CommandArgumentType.Int32 => (uint)(int)kv.Value, // CommandArgumentType.UInt32 => (uint)kv.Value, // CommandArgumentType.UInt32_2 => (uint)kv.Value, // _ => 0 // }; // if (hashTable.TryGetValue(hashValue, out var text)) // { // sb.Append($" {text}\r\n"); // } // else // { // sb.Append($" {kv.Key}: {kv.Value}\r\n"); // } // } //} //流水账 = sb.ToString(); //System.IO.File.WriteAllText("流水账.txt", 流水账); // get all commands var query = from chunk in body where chunk.Type is 1 select (chunk.Time, CommandChunk.Parse(chunk).ToImmutableArray()); _commands = query.ToImmutableArray(); var playerLifes = new SortedDictionary(PlayersMap.Keys.ToDictionary(x => x, _ => TimeSpan.Zero)); var threeSeconds = TimeSpan.FromSeconds(3); var stricterLifes = new SortedDictionary(PlayersMap.Keys.ToDictionary(x => x, _ => TimeSpan.Zero)); foreach (var (time, commands) in _commands) { var estimatedTime = time + threeSeconds; foreach (var command in commands) { var commandId = command.CommandId; var playerIndex = command.PlayerIndex; if (playerIndex != -1) { if (commandId == 0x221) { if (playerLifes[playerIndex] < estimatedTime) { playerLifes[playerIndex] = estimatedTime; } } else if (!IsUnknown(commandId) && !IsAuto(commandId)) { if (stricterLifes[playerIndex] < estimatedTime) { stricterLifes[playerIndex] = estimatedTime; } } } } } PlayerLifes = playerLifes.Values.ToImmutableArray(); StricterPlayerLifes = stricterLifes.Values.ToImmutableArray(); ReplayLength = replay.Footer?.ReplayLength ?? PlayerLifes.Max(); } public DataPoint[][] GetPoints(TimeSpan resolution, ApmPlotterFilterOptions options) { var lists = new SortedDictionary>(PlayersMap.Keys.ToDictionary(x => x, _ => new List())); var currentTime = TimeSpan.Zero; var currentIndex = 0; foreach (var (time, commands) in _commands) { while (time >= currentTime + resolution) { currentTime += resolution; ++currentIndex; } foreach (var command in commands) { if (options.ShouldSkip(command.CommandId)) { continue; } var list = lists[command.PlayerIndex]; var gap = currentIndex - list.Count; if (gap >= 0) { list.AddRange(Enumerable.Repeat(0, gap + 1)); } ++lists[command.PlayerIndex][currentIndex]; } } var apmMultiplier = 1 / resolution.TotalMinutes; DataPoint Create(int v, int i) => new(TimeSpanAxis.ToDouble(resolution) * i, v * apmMultiplier); return [.. lists.Select(l => l.Value.Select(Create).ToArray())]; } public double[] CalculateAverageApm(ApmPlotterFilterOptions options) { var apm = new SortedDictionary(PlayersMap.Keys.ToDictionary(x => x, _ => 0.0)); foreach (var (_, commands) in _commands) { foreach (var command in commands) { if (options.ShouldSkip(command.CommandId)) { continue; } ++apm[command.PlayerIndex]; } } return [.. apm.Select((kv, i) => kv.Value / PlayerLifes[i].TotalMinutes)]; } public static double[] CalculateInstantApm(DataPoint[][] data, TimeSpan begin, TimeSpan end) { return [.. data.Select(playerData => { return playerData .SkipWhile(p => TimeSpanAxis.ToTimeSpan(p.X) < begin) .TakeWhile(p => TimeSpanAxis.ToTimeSpan(p.X) < end) .Average(p => new double?(p.Y)) ?? double.NaN; })]; } public Dictionary GetCommandCounts(TimeSpan begin, TimeSpan end) { var playerCommands = new Dictionary>(); foreach (var (time, commands) in _commands) { if (time < begin) { continue; } if (time >= end) { break; } foreach (var command in commands) { if (!playerCommands.TryGetValue(command.CommandId, out var commandCount)) { commandCount = playerCommands[command.CommandId] = new(PlayersMap.Keys.ToDictionary(x => x, _ => 0)); } commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1; } } return playerCommands.ToDictionary(kv => kv.Key, kv => kv.Value.Values.ToArray()); } public static bool IsUnknown(int commandId) { if (UnknownCommands.ContainsKey(commandId)) { return true; } if (RA3Commands.IsUnknownCommand(commandId)) { UnknownCommands.TryAdd(commandId, default); return true; } return false; } public static bool IsAuto(int commandId) => AutoCommands.ContainsKey(commandId); public static bool IsClick(int commandId) => commandId is 0x1F8 or 0x1F5; } }