wip
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
102
Apm/DataRow.cs
102
Apm/DataRow.cs
@@ -42,51 +42,73 @@ namespace AnotherReplayReader.Apm
|
||||
out int apmRowIndex)
|
||||
{
|
||||
// these commands should appear in this order by default
|
||||
var orderedCommands = new List<byte>();
|
||||
var orderedCommands = new List<int>();
|
||||
orderedCommands.AddRange(
|
||||
[
|
||||
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,
|
||||
0x1F5, // 选择
|
||||
0x1F6,
|
||||
0x1F8,
|
||||
0x1F9,
|
||||
0x22A,
|
||||
0x1FA, // 编队
|
||||
0x1FB,
|
||||
0x1FC,
|
||||
|
||||
0x207, // 生产/建造
|
||||
0x208,
|
||||
0x205,
|
||||
0x206,
|
||||
0x209, // 摆放
|
||||
|
||||
0x20A, // 出售
|
||||
|
||||
0x203, // 升级
|
||||
0x204,
|
||||
|
||||
0x20D, // 右键攻击
|
||||
0x20E, // 强A
|
||||
0x20F, // 强A
|
||||
0x215, // 移动攻击
|
||||
0x214, // 移动
|
||||
0x236, // 倒车
|
||||
0x216, // 碾压
|
||||
0x22C, // 队形
|
||||
0x21A, // 停止
|
||||
0x21B, // 散开
|
||||
|
||||
0x24E, // 选择协议
|
||||
|
||||
0x1FE, // 释放特殊能力
|
||||
0x1FF,
|
||||
0x200,
|
||||
0x201,
|
||||
0x232,
|
||||
|
||||
0x22E, // 姿态
|
||||
0x22F, // 计划模式
|
||||
|
||||
0x228, // 维修
|
||||
0x229,
|
||||
|
||||
0x202, // 集结点
|
||||
0x210, // 进驻建筑
|
||||
0x20C, // 从建筑中撤出
|
||||
|
||||
0x248, // 采矿交矿
|
||||
0x212,
|
||||
|
||||
0x24B, // 信标
|
||||
0x24C,
|
||||
0x24D,
|
||||
|
||||
0x1, // 退出地图
|
||||
]);
|
||||
orderedCommands.AddRange(RA3Commands.AutoCommands);
|
||||
orderedCommands.AddRange(RA3Commands.UnknownCommands);
|
||||
|
||||
var dataList = new List<DataRow>
|
||||
{
|
||||
new("ID", plotter.Players.Select(x => x.PlayerName))
|
||||
new("ID", plotter.PlayersArray.Select(x => x.PlayerName))
|
||||
};
|
||||
var isPartial = begin > TimeSpan.Zero || end <= plotter.ReplayLength;
|
||||
if (isPartial)
|
||||
@@ -105,14 +127,14 @@ namespace AnotherReplayReader.Apm
|
||||
? "变身天眼帝国,或双手离开键盘"
|
||||
: "可能已离开房间";
|
||||
}
|
||||
dataList.Add(new("存活状态(推测)", plotter.Players.Select(GetStatus)));
|
||||
dataList.Add(new("存活状态(推测)", plotter.PlayersArray.Select(GetStatus)));
|
||||
}
|
||||
apmRowIndex = dataList.Count;
|
||||
// kill-death ratio
|
||||
if (plotter.Replay.Footer?.TryGetKillDeathRatios() is { } kdRatios)
|
||||
{
|
||||
var texts = kdRatios
|
||||
.Take(plotter.Players.Length)
|
||||
.Take(plotter.PlayersArray.Length)
|
||||
.Select(x => $"{x:0.##}");
|
||||
dataList.Add(new("击杀阵亡比(存疑)", texts));
|
||||
}
|
||||
@@ -123,7 +145,7 @@ namespace AnotherReplayReader.Apm
|
||||
{
|
||||
var counts = commandCounts.TryGetValue(command, out var stored)
|
||||
? stored
|
||||
: new int[plotter.Players.Length];
|
||||
: new int[plotter.PlayersArray.Length];
|
||||
if (!isPartial || counts.Any(x => x > 0))
|
||||
{
|
||||
dataList.Add(new(RA3Commands.GetCommandName(command),
|
||||
|
||||
Reference in New Issue
Block a user