focus on intervals, and fix provider
This commit is contained in:
+110
-14
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user