APM!
This commit is contained in:
236
Apm/ApmPlotter.cs
Normal file
236
Apm/ApmPlotter.cs
Normal file
@@ -0,0 +1,236 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
146
Apm/DataRow.cs
Normal file
146
Apm/DataRow.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using AnotherReplayReader.ReplayFile;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AnotherReplayReader.Apm
|
||||
{
|
||||
internal class DataRow
|
||||
{
|
||||
public const string AverageApmRow = "APM(整局游戏)";
|
||||
public const string PartialApmRow = "APM(当前时间段)";
|
||||
|
||||
public string Name { get; }
|
||||
public bool IsDisabled { get; }
|
||||
public DataValue Player1Value => _values[0];
|
||||
public DataValue Player2Value => _values[1];
|
||||
public DataValue Player3Value => _values[2];
|
||||
public DataValue Player4Value => _values[3];
|
||||
public DataValue Player5Value => _values[4];
|
||||
public DataValue Player6Value => _values[5];
|
||||
|
||||
private readonly IReadOnlyList<DataValue> _values;
|
||||
|
||||
public DataRow(string name,
|
||||
IEnumerable<string> values,
|
||||
bool isDisabled = false)
|
||||
{
|
||||
Name = name;
|
||||
IsDisabled = isDisabled;
|
||||
|
||||
if (values.Count() < 6)
|
||||
{
|
||||
values = values.Concat(new string[6 - values.Count()]);
|
||||
}
|
||||
_values = values.Select(x => new DataValue(x)).ToArray();
|
||||
}
|
||||
|
||||
public static List<DataRow> GetList(ApmPlotter plotter,
|
||||
ApmPlotterFilterOptions options,
|
||||
PlayerIdentity identity,
|
||||
TimeSpan begin,
|
||||
TimeSpan end,
|
||||
out int apmRowIndex)
|
||||
{
|
||||
// these commands should appear in this order by default
|
||||
var orderedCommands = new List<byte>();
|
||||
orderedCommands.AddRange(RA3Commands.UnknownCommands);
|
||||
orderedCommands.AddRange(RA3Commands.AutoCommands);
|
||||
orderedCommands.AddRange(new byte[]
|
||||
{
|
||||
0xF5,
|
||||
0xF8,
|
||||
0x2A,
|
||||
0xFA,
|
||||
0xFB,
|
||||
0x07,
|
||||
0x08,
|
||||
0x05,
|
||||
0x06,
|
||||
0x09,
|
||||
0x00,
|
||||
0x0A,
|
||||
0x03,
|
||||
0x04,
|
||||
0x28,
|
||||
0x29,
|
||||
0x0D,
|
||||
0x0E,
|
||||
0x15,
|
||||
0x14,
|
||||
0x36,
|
||||
0x16,
|
||||
0x2C,
|
||||
0x1A,
|
||||
0x4E,
|
||||
0xFE,
|
||||
0xFF,
|
||||
0x32,
|
||||
0x2E,
|
||||
0x2F,
|
||||
0x4B,
|
||||
0x4C,
|
||||
0x02,
|
||||
0x0C,
|
||||
0x10,
|
||||
});
|
||||
|
||||
var dataList = new List<DataRow>
|
||||
{
|
||||
new("ID", plotter.Players.Select(x => x.PlayerName))
|
||||
};
|
||||
var isPartial = begin > TimeSpan.Zero || end <= plotter.ReplayLength;
|
||||
if (isPartial)
|
||||
{
|
||||
string GetStatus(Player player, int i)
|
||||
{
|
||||
if (player.IsComputer)
|
||||
{
|
||||
return "这是 AI";
|
||||
}
|
||||
if (begin < plotter.StricterPlayerLifes[i])
|
||||
{
|
||||
return "存活";
|
||||
}
|
||||
return begin < plotter.PlayerLifes[i]
|
||||
? "变身天眼帝国,或双手离开键盘"
|
||||
: "可能已离开房间";
|
||||
}
|
||||
dataList.Add(new("存活状态(推测)", plotter.Players.Select(GetStatus)));
|
||||
}
|
||||
if (plotter.Replay.Type is ReplayType.Lan && identity.IsUsable)
|
||||
{
|
||||
string GetIpAndName(Player player) => player.IsComputer
|
||||
? "这是 AI"
|
||||
: identity.QueryRealNameAndIP(player.PlayerIp);
|
||||
dataList.Add(new("局域网 IP", plotter.Players.Select(GetIpAndName)));
|
||||
}
|
||||
apmRowIndex = dataList.Count;
|
||||
// get commands
|
||||
var commandCounts = plotter.GetCommandCounts(begin, end);
|
||||
// add commands in the order specified by the list
|
||||
foreach (var command in orderedCommands)
|
||||
{
|
||||
var counts = commandCounts.TryGetValue(command, out var stored)
|
||||
? stored
|
||||
: new int[plotter.Players.Length];
|
||||
if (!isPartial || counts.Any(x => x > 0))
|
||||
{
|
||||
dataList.Add(new(RA3Commands.GetCommandName(command),
|
||||
counts.Select(x => $"{x}"),
|
||||
options.ShouldSkip(command)));
|
||||
}
|
||||
commandCounts.Remove(command);
|
||||
}
|
||||
// add other commands
|
||||
foreach (var commandCount in commandCounts)
|
||||
{
|
||||
dataList.Add(new(RA3Commands.GetCommandName(commandCount.Key),
|
||||
commandCount.Value.Select(x => $"{x}"),
|
||||
options.ShouldSkip(commandCount.Key)));
|
||||
}
|
||||
|
||||
return dataList;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Apm/DataValue.cs
Normal file
43
Apm/DataValue.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
|
||||
namespace AnotherReplayReader.Apm
|
||||
{
|
||||
internal class DataValue : IComparable<DataValue>, IComparable
|
||||
{
|
||||
public int? NumberValue { get; }
|
||||
public string Value { get; }
|
||||
|
||||
public DataValue(string value)
|
||||
{
|
||||
Value = value;
|
||||
if (int.TryParse(value, out var numberValue))
|
||||
{
|
||||
NumberValue = numberValue;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => Value;
|
||||
|
||||
public int CompareTo(DataValue other)
|
||||
{
|
||||
if (NumberValue.HasValue == other.NumberValue.HasValue)
|
||||
{
|
||||
if (!NumberValue.HasValue)
|
||||
{
|
||||
return Value.CompareTo(other.Value);
|
||||
}
|
||||
return NumberValue.Value.CompareTo(other.NumberValue!.Value);
|
||||
}
|
||||
return NumberValue.HasValue ? 1 : -1;
|
||||
}
|
||||
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
if (obj is DataValue other)
|
||||
{
|
||||
return CompareTo(other);
|
||||
}
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user