using AnotherReplayReader.ReplayFile; using OxyPlot; using OxyPlot.Axes; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; 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(byte 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 ImmutableArray Players { get; } public ImmutableArray PlayerLifes { get; } public ImmutableArray StricterPlayerLifes { get; } public TimeSpan ReplayLength { get; } static ApmPlotter() { static KeyValuePair Create(byte 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; // player list without post Commentator Players = (replay.Players.Last().PlayerName == Replay.PostCommentator ? replay.Players.Take(replay.Players.Length - 1) : replay.Players).ToImmutableArray(); // get all commands IEnumerable Filter(ReplayChunk chunk) { foreach (var command in CommandChunk.Parse(chunk)) { if (command.PlayerIndex >= Players.Length) { Debug.Instance.DebugMessage += $"Unknown command with invalid player index: {command}\r\n"; continue; } yield return command; } } var query = from chunk in body where chunk.Type is 1 select (chunk.Time, Filter(chunk).ToImmutableArray()); _commands = query.ToImmutableArray(); var playerLifes = new TimeSpan[Players.Length]; var threeSeconds = TimeSpan.FromSeconds(3); var stricterLifes = new TimeSpan[Players.Length]; foreach (var (time, commands) in _commands) { var estimatedTime = time + threeSeconds; foreach (var command in commands) { var commandId = command.CommandId; var playerIndex = command.PlayerIndex; if (commandId == 0x21) { if (playerLifes[playerIndex] < estimatedTime) { playerLifes[playerIndex] = estimatedTime; } } else if (!IsUnknown(commandId) && !IsAuto(commandId) && playerIndex >= 0) { if (stricterLifes[playerIndex] < estimatedTime) { stricterLifes[playerIndex] = estimatedTime; } } } } PlayerLifes = playerLifes.ToImmutableArray(); StricterPlayerLifes = stricterLifes.ToImmutableArray(); ReplayLength = replay.Footer?.ReplayLength ?? PlayerLifes.Max(); } public DataPoint[][] GetPoints(TimeSpan resolution, ApmPlotterFilterOptions options) { var lists = Players .Select(x => new List { 0 }) .ToArray(); 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) || command.PlayerIndex < 0) { 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.Select(Create).ToArray()).ToArray(); } public double[] CalculateAverageApm(ApmPlotterFilterOptions options) { var apm = new double[Players.Length]; foreach (var (_, commands) in _commands) { foreach (var command in commands) { if (options.ShouldSkip(command.CommandId) || command.PlayerIndex < 0) { continue; } ++apm[command.PlayerIndex]; } } for (var i = 0; i < apm.Length; ++i) { apm[i] /= PlayerLifes[i].TotalMinutes; } return apm; } 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; }).ToArray(); } 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 int[Players.Length]; } if (command.PlayerIndex >= 0 && command.PlayerIndex < Players.Length) { commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1; } } } return playerCommands; } public static bool IsUnknown(byte commandId) { if (UnknownCommands.ContainsKey(commandId)) { return true; } if (RA3Commands.IsUnknownCommand(commandId)) { UnknownCommands.TryAdd(commandId, default); return true; } return false; } public static bool IsAuto(byte commandId) => AutoCommands.ContainsKey(commandId); public static bool IsClick(byte commandId) => commandId is 0xF8 or 0xF5; } }