Files
AnotherReplayReader/Apm/ApmPlotter.cs
2026-06-21 14:42:04 +02:00

307 lines
12 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.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<int, byte> UnknownCommands;
private static readonly ConcurrentDictionary<int, byte> AutoCommands;
private readonly ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)> _commands;
public Replay Replay { get; }
public ImmutableSortedDictionary<int, Player> PlayersMap { get; }
public ImmutableArray<Player> PlayersArray { get; }
public ImmutableArray<TimeSpan> PlayerLifes { get; }
public ImmutableArray<TimeSpan> StricterPlayerLifes { get; }
public TimeSpan ReplayLength { get; }
public ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)> Commands => _commands;
static ApmPlotter()
{
static KeyValuePair<int, byte> 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<int, Player>(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<uint, string> 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<int, TimeSpan>(PlayersMap.Keys.ToDictionary(x => x, _ => TimeSpan.Zero));
var threeSeconds = TimeSpan.FromSeconds(3);
var stricterLifes = new SortedDictionary<int, TimeSpan>(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<int, List<int>>(PlayersMap.Keys.ToDictionary(x => x, _ => new List<int>()));
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<int, double>(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<int, int[]> GetCommandCounts(TimeSpan begin, TimeSpan end)
{
var playerCommands = new Dictionary<int, SortedDictionary<int, 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(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;
}
}