This commit is contained in:
2026-06-21 14:42:04 +02:00
parent 22cfb1f0a9
commit a3b4cc530d
15 changed files with 2437 additions and 403 deletions

View File

@@ -1,4 +1,5 @@
using AnotherReplayReader.ReplayFile;
using AnotherReplayReader.Utils;
using OxyPlot;
using OxyPlot.Axes;
using System;
@@ -6,6 +7,8 @@ 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
{
@@ -22,7 +25,7 @@ namespace AnotherReplayReader.Apm
CountClicks = countClicks;
}
public bool ShouldSkip(byte command)
public bool ShouldSkip(int command)
{
if (!CountUnknowns && ApmPlotter.IsUnknown(command))
{
@@ -42,19 +45,21 @@ namespace AnotherReplayReader.Apm
internal class ApmPlotter
{
private static readonly ConcurrentDictionary<byte, byte> UnknownCommands;
private static readonly ConcurrentDictionary<byte, byte> AutoCommands;
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 ImmutableArray<Player> Players { 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<byte, byte> Create(byte x) => new(x, default);
static KeyValuePair<int, byte> Create(int x) => new(x, default);
UnknownCommands = new(RA3Commands.UnknownCommands.Select(Create));
AutoCommands = new(RA3Commands.AutoCommands.Select(Create));
}
@@ -68,32 +73,99 @@ namespace AnotherReplayReader.Apm
Replay = replay;
// player list without post Commentator
Players = (replay.Players.Last().PlayerName == Replay.PostCommentator
? replay.Players.Take(replay.Players.Length - 1)
: replay.Players).ToImmutableArray();
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
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());
select (chunk.Time, CommandChunk.Parse(chunk).ToImmutableArray());
_commands = query.ToImmutableArray();
var playerLifes = new TimeSpan[Players.Length];
var playerLifes = new SortedDictionary<int, TimeSpan>(PlayersMap.Keys.ToDictionary(x => x, _ => TimeSpan.Zero));
var threeSeconds = TimeSpan.FromSeconds(3);
var stricterLifes = new TimeSpan[Players.Length];
var stricterLifes = new SortedDictionary<int, TimeSpan>(PlayersMap.Keys.ToDictionary(x => x, _ => TimeSpan.Zero));
foreach (var (time, commands) in _commands)
{
var estimatedTime = time + threeSeconds;
@@ -101,32 +173,34 @@ namespace AnotherReplayReader.Apm
{
var commandId = command.CommandId;
var playerIndex = command.PlayerIndex;
if (commandId == 0x21)
if (playerIndex != -1)
{
if (playerLifes[playerIndex] < estimatedTime)
if (commandId == 0x221)
{
playerLifes[playerIndex] = estimatedTime;
if (playerLifes[playerIndex] < estimatedTime)
{
playerLifes[playerIndex] = estimatedTime;
}
}
}
else if (!IsUnknown(commandId) && !IsAuto(commandId) && playerIndex >= 0)
{
if (stricterLifes[playerIndex] < estimatedTime)
else if (!IsUnknown(commandId) && !IsAuto(commandId))
{
stricterLifes[playerIndex] = estimatedTime;
if (stricterLifes[playerIndex] < estimatedTime)
{
stricterLifes[playerIndex] = estimatedTime;
}
}
}
}
}
PlayerLifes = playerLifes.ToImmutableArray();
StricterPlayerLifes = stricterLifes.ToImmutableArray();
PlayerLifes = playerLifes.Values.ToImmutableArray();
StricterPlayerLifes = stricterLifes.Values.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 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)
@@ -138,7 +212,7 @@ namespace AnotherReplayReader.Apm
}
foreach (var command in commands)
{
if (options.ShouldSkip(command.CommandId) || command.PlayerIndex < 0)
if (options.ShouldSkip(command.CommandId))
{
continue;
}
@@ -154,44 +228,40 @@ namespace AnotherReplayReader.Apm
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();
return [.. lists.Select(l => l.Value.Select(Create).ToArray())];
}
public double[] CalculateAverageApm(ApmPlotterFilterOptions options)
{
var apm = new double[Players.Length];
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) || command.PlayerIndex < 0)
if (options.ShouldSkip(command.CommandId))
{
continue;
}
++apm[command.PlayerIndex];
}
}
for (var i = 0; i < apm.Length; ++i)
{
apm[i] /= PlayerLifes[i].TotalMinutes;
}
return apm;
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 [.. 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)
public Dictionary<int, int[]> GetCommandCounts(TimeSpan begin, TimeSpan end)
{
var playerCommands = new Dictionary<byte, int[]>();
var playerCommands = new Dictionary<int, SortedDictionary<int, int>>();
foreach (var (time, commands) in _commands)
{
@@ -207,18 +277,15 @@ namespace AnotherReplayReader.Apm
{
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;
commandCount = playerCommands[command.CommandId] = new(PlayersMap.Keys.ToDictionary(x => x, _ => 0));
}
commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1;
}
}
return playerCommands;
return playerCommands.ToDictionary(kv => kv.Key, kv => kv.Value.Values.ToArray());
}
public static bool IsUnknown(byte commandId)
public static bool IsUnknown(int commandId)
{
if (UnknownCommands.ContainsKey(commandId))
{
@@ -232,8 +299,8 @@ namespace AnotherReplayReader.Apm
return false;
}
public static bool IsAuto(byte commandId) => AutoCommands.ContainsKey(commandId);
public static bool IsAuto(int commandId) => AutoCommands.ContainsKey(commandId);
public static bool IsClick(byte commandId) => commandId is 0xF8 or 0xF5;
public static bool IsClick(int commandId) => commandId is 0x1F8 or 0x1F5;
}
}