focus on intervals, and fix provider

This commit is contained in:
2026-09-08 14:44:34 +02:00
parent 579cd8e4b3
commit acad70aaac
11 changed files with 1163 additions and 286 deletions
+110 -14
View File
@@ -6,6 +6,13 @@ using System.Linq;
namespace AnotherReplayReader.Utils
{
/// <summary>一次特殊能力事件(含真实发生时间与玩家),用于生成可读的打包/展开时间线。</summary>
internal sealed record SpecialPowerEvent(
TimeSpan Time,
int PlayerIndex,
uint UnitId,
string PowerName);
/// <summary>
/// Index of replay facts extracted from CommandChunk data.
/// Used by AIAnalysisValidation to cross-reference LLM claims against
@@ -19,6 +26,9 @@ namespace AnotherReplayReader.Utils
/// <summary>Special powers used by each UnitId.</summary>
public ImmutableDictionary<uint, ImmutableHashSet<string>> UnitIdSpecialPowers { get; }
/// <summary>按时间排序的特殊能力事件列表。</summary>
public ImmutableArray<SpecialPowerEvent> SpecialPowerEvents { get; }
/// <summary>UnitIds that appeared as builder ("建造者") in construction commands.</summary>
public ImmutableHashSet<uint> BuilderUnitIds { get; }
@@ -43,6 +53,7 @@ namespace AnotherReplayReader.Utils
public ReplayFactIndex(
ImmutableDictionary<uint, TimeSpan> unitIdFirstObservedTime,
ImmutableDictionary<uint, ImmutableHashSet<string>> unitIdSpecialPowers,
ImmutableArray<SpecialPowerEvent> specialPowerEvents,
ImmutableHashSet<uint> builderUnitIds,
ImmutableHashSet<uint> producerUnitIds,
ImmutableDictionary<int, ImmutableDictionary<string, TimeSpan>> playerFirstProductionTime,
@@ -53,6 +64,7 @@ namespace AnotherReplayReader.Utils
{
UnitIdFirstObservedTime = unitIdFirstObservedTime;
UnitIdSpecialPowers = unitIdSpecialPowers;
SpecialPowerEvents = specialPowerEvents;
BuilderUnitIds = builderUnitIds;
ProducerUnitIds = producerUnitIds;
PlayerFirstProductionTime = playerFirstProductionTime;
@@ -68,6 +80,7 @@ namespace AnotherReplayReader.Utils
{
var unitFirstObserved = new Dictionary<uint, TimeSpan>();
var unitSpecialPowers = new Dictionary<uint, HashSet<string>>();
var specialPowerEvents = new List<SpecialPowerEvent>();
var builderUnits = new HashSet<uint>();
var producerUnits = new HashSet<uint>();
var playerFirstProduction = new Dictionary<int, Dictionary<string, TimeSpan>>();
@@ -83,7 +96,7 @@ namespace AnotherReplayReader.Utils
foreach (var command in commands)
{
ProcessCommand(time, command, stringHashTable,
unitFirstObserved, unitSpecialPowers,
unitFirstObserved, unitSpecialPowers, specialPowerEvents,
builderUnits, producerUnits,
playerFirstProduction, playerSelected,
playerStrongOwnership, playerWeakOwnership,
@@ -95,6 +108,11 @@ namespace AnotherReplayReader.Utils
unitFirstObserved.ToImmutableDictionary(),
unitSpecialPowers.ToImmutableDictionary(
kv => kv.Key, kv => kv.Value.ToImmutableHashSet()),
specialPowerEvents
.OrderBy(e => e.Time)
.ThenBy(e => e.PlayerIndex)
.ThenBy(e => e.UnitId)
.ToImmutableArray(),
builderUnits.ToImmutableHashSet(),
producerUnits.ToImmutableHashSet(),
playerFirstProduction.ToImmutableDictionary(
@@ -115,6 +133,7 @@ namespace AnotherReplayReader.Utils
IReadOnlyDictionary<uint, string> stringHashTable,
Dictionary<uint, TimeSpan> unitFirstObserved,
Dictionary<uint, HashSet<string>> unitSpecialPowers,
List<SpecialPowerEvent> specialPowerEvents,
HashSet<uint> builderUnits,
HashSet<uint> producerUnits,
Dictionary<int, Dictionary<string, TimeSpan>> playerFirstProduction,
@@ -148,7 +167,8 @@ namespace AnotherReplayReader.Utils
// special power (target position and angle): 0x200 —— 布局确凿,ObjectId 是施法者
case 0x200:
RecordSpecialPower(time, command, player, stringHashTable,
unitFirstObserved, unitSpecialPowers, playerStrongOwnership);
unitFirstObserved, unitSpecialPowers, playerStrongOwnership,
specialPowerEvents);
break;
// special power (target position): 0x1FF —— ObjectId 语义待核实,只记录"出现过"
@@ -163,7 +183,7 @@ namespace AnotherReplayReader.Utils
// start production: 0x205
case 0x205:
RecordProduction(time, command, player, unitFirstObserved,
producerUnits, playerFirstProduction, playerStrongOwnership);
stringHashTable, producerUnits, playerFirstProduction, playerStrongOwnership);
break;
// start construction: 0x207
@@ -208,7 +228,7 @@ namespace AnotherReplayReader.Utils
// 选择协议:全局生效,无 UnitId
case 0x24E:
RecordTechChoice(time, command, player, playerTechChoices);
RecordTechChoice(time, command, player, stringHashTable, playerTechChoices);
break;
// move: 0x214
@@ -263,7 +283,8 @@ namespace AnotherReplayReader.Utils
IReadOnlyDictionary<uint, string> stringHashTable,
Dictionary<uint, TimeSpan> unitFirstObserved,
Dictionary<uint, HashSet<string>> unitSpecialPowers,
Dictionary<int, HashSet<uint>> playerStrongOwnership)
Dictionary<int, HashSet<uint>> playerStrongOwnership,
List<SpecialPowerEvent> specialPowerEvents)
{
string? powerName = null;
var unitIds = new List<uint>();
@@ -323,6 +344,8 @@ namespace AnotherReplayReader.Utils
}
powers.Add(powerName);
RecordPlayerOwnership(player, unitId, playerStrongOwnership);
specialPowerEvents.Add(new SpecialPowerEvent(
time, player, unitId, powerName));
}
}
@@ -331,6 +354,7 @@ namespace AnotherReplayReader.Utils
CommandChunk command,
int player,
Dictionary<uint, TimeSpan> unitFirstObserved,
IReadOnlyDictionary<uint, string> stringHashTable,
HashSet<uint> producerUnits,
Dictionary<int, Dictionary<string, TimeSpan>> playerFirstProduction,
Dictionary<int, HashSet<uint>> playerStrongOwnership)
@@ -362,8 +386,14 @@ namespace AnotherReplayReader.Utils
}
break;
case CommandArgumentType.AsciiString or CommandArgumentType.UnicodeString
or CommandArgumentType.Int32
or CommandArgumentType.UInt32
or CommandArgumentType.UInt32_2
when unitName is null:
unitName = entry.Value.ToString() ?? string.Empty;
if (TryReadCommandName(entry, stringHashTable, out var resolvedName))
{
unitName = resolvedName;
}
break;
}
}
@@ -382,7 +412,7 @@ namespace AnotherReplayReader.Utils
perPlayer = new Dictionary<string, TimeSpan>();
playerFirstProduction[player] = perPlayer;
}
if (!perPlayer.ContainsKey(unitName))
if (unitName is not null && !perPlayer.ContainsKey(unitName))
{
perPlayer[unitName] = time;
}
@@ -552,18 +582,18 @@ namespace AnotherReplayReader.Utils
TimeSpan time,
CommandChunk command,
int player,
IReadOnlyDictionary<uint, string> stringHashTable,
Dictionary<int, HashSet<string>> playerTechChoices)
{
foreach (var entry in command.Data)
{
if (entry.Type is CommandArgumentType.AsciiString or CommandArgumentType.UnicodeString)
if (entry.Type is CommandArgumentType.AsciiString
or CommandArgumentType.UnicodeString
or CommandArgumentType.Int32
or CommandArgumentType.UInt32
or CommandArgumentType.UInt32_2)
{
var tech = entry.Count == 1
? entry.Value.ToString()
: entry.Value is string[] strings
? strings.FirstOrDefault(s => !string.IsNullOrWhiteSpace(s))
: null;
if (string.IsNullOrWhiteSpace(tech))
if (!TryReadCommandName(entry, stringHashTable, out var tech))
{
continue;
}
@@ -578,6 +608,72 @@ namespace AnotherReplayReader.Utils
}
}
private static bool TryReadCommandName(
CommandArgumentEntry entry,
IReadOnlyDictionary<uint, string> stringHashTable,
out string name)
{
name = string.Empty;
if (entry.Type is CommandArgumentType.AsciiString or CommandArgumentType.UnicodeString)
{
if (entry.Count == 1 && entry.Value is string single)
{
if (!string.IsNullOrWhiteSpace(single))
{
name = single;
return true;
}
}
else if (entry.Value is string[] values)
{
foreach (var value in values)
{
if (!string.IsNullOrWhiteSpace(value))
{
name = value;
return true;
}
}
}
return false;
}
return TryReadCommandNameAsHash(entry, stringHashTable, out name);
}
private static bool TryReadCommandNameAsHash(
CommandArgumentEntry entry,
IReadOnlyDictionary<uint, string> stringHashTable,
out string name)
{
name = string.Empty;
IEnumerable<uint> hashes = entry.Type switch
{
CommandArgumentType.Int32 when entry.Count == 1 && entry.Value is int singleInt =>
new[] { unchecked((uint)singleInt) },
CommandArgumentType.Int32 when entry.Value is int[] ints =>
ints.Select(x => unchecked((uint)x)),
CommandArgumentType.UInt32 or CommandArgumentType.UInt32_2
when entry.Count == 1 && entry.Value is uint singleUint =>
new[] { singleUint },
CommandArgumentType.UInt32 or CommandArgumentType.UInt32_2
when entry.Value is uint[] uints =>
uints,
_ => Array.Empty<uint>(),
};
foreach (var hash in hashes)
{
if (stringHashTable.TryGetValue(hash, out var resolved)
&& !string.IsNullOrWhiteSpace(resolved))
{
name = resolved;
return true;
}
}
return false;
}
private static void RecordObjectReferenceWithOwnership(
TimeSpan time,
CommandChunk command,