From a3b4cc530d6e568d40344cd48a384d5af24462dd Mon Sep 17 00:00:00 2001 From: lanyizi Date: Sun, 21 Jun 2026 14:42:04 +0200 Subject: [PATCH] wip --- Apm/ApmPlotter.cs | 185 ++++-- Apm/DataRow.cs | 102 ++-- ApmWindow.xaml | 90 +-- ApmWindow.xaml.cs | 13 +- EventDump.xaml | 127 ++++ EventDump.xaml.cs | 575 +++++++++++++++++++ MinimapReader.cs | 65 ++- ReplayFile/CommandChunk.cs | 48 +- ReplayFile/Player.cs | 2 +- ReplayFile/RA3Commands.cs | 412 ++++++------- ReplayFile/ReplayChunk.cs | 4 +- Utils/AIAnalyze.cs | 1115 ++++++++++++++++++++++++++++++++++++ Utils/AIAnalyzeUI.cs | 89 +++ Utils/CompilerServices.cs | 6 + Window1.xaml.cs | 7 +- 15 files changed, 2437 insertions(+), 403 deletions(-) create mode 100644 EventDump.xaml create mode 100644 EventDump.xaml.cs create mode 100644 Utils/AIAnalyze.cs create mode 100644 Utils/AIAnalyzeUI.cs create mode 100644 Utils/CompilerServices.cs diff --git a/Apm/ApmPlotter.cs b/Apm/ApmPlotter.cs index 4438217..63fe9e9 100644 --- a/Apm/ApmPlotter.cs +++ b/Apm/ApmPlotter.cs @@ -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 UnknownCommands; - private static readonly ConcurrentDictionary AutoCommands; + private static readonly ConcurrentDictionary UnknownCommands; + private static readonly ConcurrentDictionary AutoCommands; private readonly ImmutableArray<(TimeSpan, ImmutableArray)> _commands; public Replay Replay { get; } - public ImmutableArray Players { get; } + public ImmutableSortedDictionary PlayersMap { get; } + public ImmutableArray PlayersArray { get; } public ImmutableArray PlayerLifes { get; } public ImmutableArray StricterPlayerLifes { get; } public TimeSpan ReplayLength { get; } + public ImmutableArray<(TimeSpan, ImmutableArray)> Commands => _commands; static ApmPlotter() { - static KeyValuePair Create(byte x) => new(x, default); + static KeyValuePair 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(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 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 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(PlayersMap.Keys.ToDictionary(x => x, _ => TimeSpan.Zero)); var threeSeconds = TimeSpan.FromSeconds(3); - var stricterLifes = new TimeSpan[Players.Length]; + var stricterLifes = new SortedDictionary(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 { 0 }) - .ToArray(); + var lists = new SortedDictionary>(PlayersMap.Keys.ToDictionary(x => x, _ => new List())); + 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(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 GetCommandCounts(TimeSpan begin, TimeSpan end) + public Dictionary GetCommandCounts(TimeSpan begin, TimeSpan end) { - var playerCommands = new Dictionary(); + var playerCommands = new Dictionary>(); 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; } } diff --git a/Apm/DataRow.cs b/Apm/DataRow.cs index 6ee84e2..ad74307 100644 --- a/Apm/DataRow.cs +++ b/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(); + var orderedCommands = new List(); 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 { - 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), diff --git a/ApmWindow.xaml b/ApmWindow.xaml index 6dfe615..e58607f 100644 --- a/ApmWindow.xaml +++ b/ApmWindow.xaml @@ -26,41 +26,61 @@ BorderThickness="1" /> - - + + + + + + + + + + + +