237 lines
8.3 KiB
C#
237 lines
8.3 KiB
C#
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<byte, byte> UnknownCommands;
|
|
private static readonly ConcurrentDictionary<byte, byte> AutoCommands;
|
|
private readonly ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)> _commands;
|
|
|
|
public Replay Replay { get; }
|
|
public ImmutableArray<Player> Players { get; }
|
|
public ImmutableArray<TimeSpan> PlayerLifes { get; }
|
|
public ImmutableArray<TimeSpan> StricterPlayerLifes { get; }
|
|
public TimeSpan ReplayLength { get; }
|
|
|
|
static ApmPlotter()
|
|
{
|
|
static KeyValuePair<byte, byte> 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<CommandChunk> 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))
|
|
{
|
|
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<int> { 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))
|
|
{
|
|
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))
|
|
{
|
|
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<byte, int[]> GetCommandCounts(TimeSpan begin, TimeSpan end)
|
|
{
|
|
var playerCommands = new Dictionary<byte, int[]>();
|
|
|
|
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];
|
|
}
|
|
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;
|
|
}
|
|
}
|