deepseek wip
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
using AnotherReplayReader.ReplayFile;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace AnotherReplayReader.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Index of replay facts extracted from CommandChunk data.
|
||||
/// Used by AIAnalysisValidation to cross-reference LLM claims against
|
||||
/// actual replay operations.
|
||||
/// </summary>
|
||||
internal sealed class ReplayFactIndex
|
||||
{
|
||||
/// <summary>First time a UnitId was observed in any command.</summary>
|
||||
public ImmutableDictionary<uint, TimeSpan> UnitIdFirstObservedTime { get; }
|
||||
|
||||
/// <summary>Special powers used by each UnitId.</summary>
|
||||
public ImmutableDictionary<uint, ImmutableHashSet<string>> UnitIdSpecialPowers { get; }
|
||||
|
||||
/// <summary>UnitIds that appeared as builder ("建造者") in construction commands.</summary>
|
||||
public ImmutableHashSet<uint> BuilderUnitIds { get; }
|
||||
|
||||
/// <summary>UnitIds that appeared as production structures ("出兵建筑").</summary>
|
||||
public ImmutableHashSet<uint> ProducerUnitIds { get; }
|
||||
|
||||
/// <summary>Per player, per unit asset name, first production start time.</summary>
|
||||
public ImmutableDictionary<int, ImmutableDictionary<string, TimeSpan>> PlayerFirstProductionTime { get; }
|
||||
|
||||
/// <summary>Per player, which UnitIds they have selected.</summary>
|
||||
public ImmutableDictionary<int, ImmutableHashSet<uint>> PlayerSelectedUnitIds { get; }
|
||||
|
||||
public ReplayFactIndex(
|
||||
ImmutableDictionary<uint, TimeSpan> unitIdFirstObservedTime,
|
||||
ImmutableDictionary<uint, ImmutableHashSet<string>> unitIdSpecialPowers,
|
||||
ImmutableHashSet<uint> builderUnitIds,
|
||||
ImmutableHashSet<uint> producerUnitIds,
|
||||
ImmutableDictionary<int, ImmutableDictionary<string, TimeSpan>> playerFirstProductionTime,
|
||||
ImmutableDictionary<int, ImmutableHashSet<uint>> playerSelectedUnitIds)
|
||||
{
|
||||
UnitIdFirstObservedTime = unitIdFirstObservedTime;
|
||||
UnitIdSpecialPowers = unitIdSpecialPowers;
|
||||
BuilderUnitIds = builderUnitIds;
|
||||
ProducerUnitIds = producerUnitIds;
|
||||
PlayerFirstProductionTime = playerFirstProductionTime;
|
||||
PlayerSelectedUnitIds = playerSelectedUnitIds;
|
||||
}
|
||||
|
||||
public static ReplayFactIndex Build(
|
||||
ImmutableArray<(TimeSpan Time, ImmutableArray<CommandChunk> Commands)> timeline,
|
||||
IReadOnlyDictionary<uint, string> stringHashTable)
|
||||
{
|
||||
var unitFirstObserved = new Dictionary<uint, TimeSpan>();
|
||||
var unitSpecialPowers = new Dictionary<uint, HashSet<string>>();
|
||||
var builderUnits = new HashSet<uint>();
|
||||
var producerUnits = new HashSet<uint>();
|
||||
var playerFirstProduction = new Dictionary<int, Dictionary<string, TimeSpan>>();
|
||||
var playerSelected = new Dictionary<int, HashSet<uint>>();
|
||||
|
||||
foreach (var (time, commands) in timeline)
|
||||
{
|
||||
foreach (var command in commands)
|
||||
{
|
||||
ProcessCommand(time, command, stringHashTable,
|
||||
unitFirstObserved, unitSpecialPowers,
|
||||
builderUnits, producerUnits,
|
||||
playerFirstProduction, playerSelected);
|
||||
}
|
||||
}
|
||||
|
||||
return new ReplayFactIndex(
|
||||
unitFirstObserved.ToImmutableDictionary(),
|
||||
unitSpecialPowers.ToImmutableDictionary(
|
||||
kv => kv.Key, kv => kv.Value.ToImmutableHashSet()),
|
||||
builderUnits.ToImmutableHashSet(),
|
||||
producerUnits.ToImmutableHashSet(),
|
||||
playerFirstProduction.ToImmutableDictionary(
|
||||
kv => kv.Key, kv => kv.Value.ToImmutableDictionary()),
|
||||
playerSelected.ToImmutableDictionary(
|
||||
kv => kv.Key, kv => kv.Value.ToImmutableHashSet()));
|
||||
}
|
||||
|
||||
private static void ProcessCommand(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
IReadOnlyDictionary<uint, string> stringHashTable,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
Dictionary<uint, HashSet<string>> unitSpecialPowers,
|
||||
HashSet<uint> builderUnits,
|
||||
HashSet<uint> producerUnits,
|
||||
Dictionary<int, Dictionary<string, TimeSpan>> playerFirstProduction,
|
||||
Dictionary<int, HashSet<uint>> playerSelected)
|
||||
{
|
||||
var player = command.PlayerIndex;
|
||||
var cmdId = command.CommandId;
|
||||
|
||||
switch (cmdId)
|
||||
{
|
||||
// select unit(s): 0x1F5
|
||||
case 0x1F5:
|
||||
RecordSelectUnit(time, command, player, unitFirstObserved, playerSelected);
|
||||
break;
|
||||
|
||||
// special power (no target): 0x1FE
|
||||
case 0x1FE:
|
||||
RecordSpecialPower(time, command, stringHashTable, unitFirstObserved, unitSpecialPowers);
|
||||
break;
|
||||
|
||||
// special power (target position): 0x1FF
|
||||
case 0x1FF:
|
||||
RecordSpecialPower(time, command, stringHashTable, unitFirstObserved, unitSpecialPowers);
|
||||
break;
|
||||
|
||||
// special power (target position and angle): 0x200
|
||||
case 0x200:
|
||||
RecordSpecialPower(time, command, stringHashTable, unitFirstObserved, unitSpecialPowers);
|
||||
break;
|
||||
|
||||
// special power (target unit): 0x201
|
||||
case 0x201:
|
||||
RecordSpecialPower(time, command, stringHashTable, unitFirstObserved, unitSpecialPowers);
|
||||
break;
|
||||
|
||||
// special power (one or more targets): 0x232
|
||||
case 0x232:
|
||||
RecordSpecialPower(time, command, stringHashTable, unitFirstObserved, unitSpecialPowers);
|
||||
break;
|
||||
|
||||
// start production: 0x205
|
||||
case 0x205:
|
||||
RecordProduction(time, command, player, unitFirstObserved, producerUnits, playerFirstProduction);
|
||||
break;
|
||||
|
||||
// start construction: 0x207
|
||||
case 0x207:
|
||||
RecordConstruction(time, command, unitFirstObserved, builderUnits);
|
||||
break;
|
||||
|
||||
// place building: 0x209
|
||||
case 0x209:
|
||||
RecordPlaceBuilding(time, command, unitFirstObserved, builderUnits);
|
||||
break;
|
||||
|
||||
// sell building: 0x20A
|
||||
case 0x20A:
|
||||
RecordObjectReference(time, command, unitFirstObserved);
|
||||
break;
|
||||
|
||||
// move: 0x214
|
||||
case 0x214:
|
||||
// attack move: 0x215
|
||||
case 0x215:
|
||||
// These commands operate on currently selected units.
|
||||
// The target is a position, not a UnitId.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecordSelectUnit(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
int player,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
Dictionary<int, HashSet<uint>> playerSelected)
|
||||
{
|
||||
// Data layout for 0x1F5:
|
||||
// Data[0]: Bool (isReplace), if count > 0 the rest are ObjectIds
|
||||
// Data[1..]: ObjectIds of selected units
|
||||
foreach (var entry in command.Data)
|
||||
{
|
||||
if (entry.Type is CommandArgumentType.ObjectId or CommandArgumentType.ObjectId_2)
|
||||
{
|
||||
if (entry.Count == 1)
|
||||
{
|
||||
var unitId = (uint)entry.Value;
|
||||
TryRecordFirstObserved(unitId, time, unitFirstObserved);
|
||||
RecordPlayerSelection(player, unitId, playerSelected);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var id in (uint[])entry.Value)
|
||||
{
|
||||
TryRecordFirstObserved(id, time, unitFirstObserved);
|
||||
RecordPlayerSelection(player, id, playerSelected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecordSpecialPower(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
IReadOnlyDictionary<uint, string> stringHashTable,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
Dictionary<uint, HashSet<string>> unitSpecialPowers)
|
||||
{
|
||||
string? powerName = null;
|
||||
var unitIds = new List<uint>();
|
||||
|
||||
foreach (var entry in command.Data)
|
||||
{
|
||||
switch (entry.Type)
|
||||
{
|
||||
case CommandArgumentType.Int32 when powerName is null:
|
||||
{
|
||||
// First Int32 is the special power hash ID
|
||||
var hash = unchecked((uint)(int)entry.Value);
|
||||
powerName = stringHashTable.TryGetValue(hash, out var name)
|
||||
? name
|
||||
: $"Hash_{hash:X8}";
|
||||
break;
|
||||
}
|
||||
case CommandArgumentType.ObjectId or CommandArgumentType.ObjectId_2:
|
||||
{
|
||||
if (entry.Count == 1)
|
||||
{
|
||||
var id = (uint)entry.Value;
|
||||
if (id != 0) unitIds.Add(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var id in (uint[])entry.Value)
|
||||
{
|
||||
if (id != 0) unitIds.Add(id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (powerName is null || unitIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var unitId in unitIds)
|
||||
{
|
||||
TryRecordFirstObserved(unitId, time, unitFirstObserved);
|
||||
if (!unitSpecialPowers.TryGetValue(unitId, out var powers))
|
||||
{
|
||||
powers = new HashSet<string>();
|
||||
unitSpecialPowers[unitId] = powers;
|
||||
}
|
||||
powers.Add(powerName);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecordProduction(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
int player,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
HashSet<uint> producerUnits,
|
||||
Dictionary<int, Dictionary<string, TimeSpan>> playerFirstProduction)
|
||||
{
|
||||
uint? producerId = null;
|
||||
string? unitName = null;
|
||||
|
||||
foreach (var entry in command.Data)
|
||||
{
|
||||
switch (entry.Type)
|
||||
{
|
||||
case CommandArgumentType.ObjectId or CommandArgumentType.ObjectId_2
|
||||
when entry.Count == 1 && producerId is null:
|
||||
producerId = (uint)entry.Value;
|
||||
break;
|
||||
case CommandArgumentType.AsciiString or CommandArgumentType.UnicodeString
|
||||
when unitName is null:
|
||||
unitName = entry.Value.ToString() ?? string.Empty;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (producerId.HasValue)
|
||||
{
|
||||
TryRecordFirstObserved(producerId.Value, time, unitFirstObserved);
|
||||
producerUnits.Add(producerId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(unitName))
|
||||
{
|
||||
if (!playerFirstProduction.TryGetValue(player, out var perPlayer))
|
||||
{
|
||||
perPlayer = new Dictionary<string, TimeSpan>();
|
||||
playerFirstProduction[player] = perPlayer;
|
||||
}
|
||||
if (!perPlayer.ContainsKey(unitName))
|
||||
{
|
||||
perPlayer[unitName] = time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecordConstruction(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
HashSet<uint> builderUnits)
|
||||
{
|
||||
// Data[0]: ObjectId (builder)
|
||||
// Data[1]: AsciiString (building name)
|
||||
RecordBuilder(time, command, unitFirstObserved, builderUnits);
|
||||
}
|
||||
|
||||
private static void RecordPlaceBuilding(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
HashSet<uint> builderUnits)
|
||||
{
|
||||
// Data[0]: ObjectId (builder)
|
||||
// Data[1]: AsciiString (building name)
|
||||
// Data[2]: Int32 (count)
|
||||
// Data[3]: Vector3 (position)
|
||||
// Data[4]: Float32 (angle)
|
||||
RecordBuilder(time, command, unitFirstObserved, builderUnits);
|
||||
}
|
||||
|
||||
private static void RecordBuilder(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved,
|
||||
HashSet<uint> builderUnits)
|
||||
{
|
||||
foreach (var entry in command.Data)
|
||||
{
|
||||
if (entry.Type is CommandArgumentType.ObjectId or CommandArgumentType.ObjectId_2
|
||||
&& entry.Count == 1)
|
||||
{
|
||||
var id = (uint)entry.Value;
|
||||
if (id != 0)
|
||||
{
|
||||
TryRecordFirstObserved(id, time, unitFirstObserved);
|
||||
builderUnits.Add(id);
|
||||
}
|
||||
return; // only first ObjectId is the builder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecordObjectReference(
|
||||
TimeSpan time,
|
||||
CommandChunk command,
|
||||
Dictionary<uint, TimeSpan> unitFirstObserved)
|
||||
{
|
||||
foreach (var entry in command.Data)
|
||||
{
|
||||
if (entry.Type is CommandArgumentType.ObjectId or CommandArgumentType.ObjectId_2)
|
||||
{
|
||||
if (entry.Count == 1)
|
||||
{
|
||||
TryRecordFirstObserved((uint)entry.Value, time, unitFirstObserved);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var id in (uint[])entry.Value)
|
||||
{
|
||||
TryRecordFirstObserved(id, time, unitFirstObserved);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecordPlayerSelection(int player, uint unitId, Dictionary<int, HashSet<uint>> playerSelected)
|
||||
{
|
||||
if (!playerSelected.TryGetValue(player, out var set))
|
||||
{
|
||||
set = new HashSet<uint>();
|
||||
playerSelected[player] = set;
|
||||
}
|
||||
set.Add(unitId);
|
||||
}
|
||||
|
||||
private static void TryRecordFirstObserved(uint unitId, TimeSpan time, Dictionary<uint, TimeSpan> unitFirstObserved)
|
||||
{
|
||||
if (unitId == 0) return;
|
||||
if (unitFirstObserved.ContainsKey(unitId)) return;
|
||||
unitFirstObserved[unitId] = time;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user