WalkOracleTrace decodes the P lines of the pose-stamped capture round (raw IEEE-754 dwords from Render::viewer_pos @0x0081ef00: camera cell, world origin, the four Frame quaternion components in storage order) into WalkOraclePose on each frame. Poseless FW0 fixtures parse as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
194 lines
7.7 KiB
C#
194 lines
7.7 KiB
C#
using System.Globalization;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Walk;
|
|
|
|
/// <summary>
|
|
/// Parser for the FW0 retail walk-oracle traces
|
|
/// (docs/research/2026-08-30-fw-walk-oracle/ — captured live from the
|
|
/// PDB-paired 2013 retail client on 2026-08-30; format documented in that
|
|
/// directory's README). These traces are Campaign FW's conformance
|
|
/// fixtures: FW1's RetailFrameWalk must reproduce each frame's event
|
|
/// sequence position-for-position.
|
|
///
|
|
/// The final frame of every capture is dropped: the harness detaches at
|
|
/// the frame MARKER, so the last frame never records its draws and would
|
|
/// read as a false "outdoor, no landscape" frame.
|
|
/// </summary>
|
|
public static class WalkOracleTrace
|
|
{
|
|
public static IReadOnlyList<WalkOracleFrame> Parse(IEnumerable<string> lines)
|
|
{
|
|
var frames = new List<WalkOracleFrame>();
|
|
List<WalkOracleEvent>? current = null;
|
|
int currentNumber = 0;
|
|
WalkOraclePose? currentPose = null;
|
|
|
|
foreach (string line in lines)
|
|
{
|
|
Match frameMatch = FramePattern.Match(line);
|
|
if (frameMatch.Success)
|
|
{
|
|
if (current is not null)
|
|
frames.Add(new WalkOracleFrame(currentNumber, current, currentPose));
|
|
currentNumber = int.Parse(
|
|
frameMatch.Groups[1].Value, CultureInfo.InvariantCulture);
|
|
current = new List<WalkOracleEvent>();
|
|
currentPose = null;
|
|
continue;
|
|
}
|
|
if (current is null)
|
|
continue;
|
|
|
|
Match poseMatch = PosePattern.Match(line);
|
|
if (poseMatch.Success)
|
|
{
|
|
// P <objcell_id> <origin xyz> <quat q0..q3> — raw IEEE-754 dwords
|
|
// dumped from Render::viewer_pos @0x0081ef00 at the frame marker.
|
|
uint[] raw = new uint[8];
|
|
for (int i = 0; i < 8; i++)
|
|
raw[i] = uint.Parse(
|
|
poseMatch.Groups[i + 1].Value,
|
|
NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
|
currentPose = new WalkOraclePose(
|
|
raw[0],
|
|
new System.Numerics.Vector3(
|
|
BitConverter.Int32BitsToSingle((int)raw[1]),
|
|
BitConverter.Int32BitsToSingle((int)raw[2]),
|
|
BitConverter.Int32BitsToSingle((int)raw[3])),
|
|
BitConverter.Int32BitsToSingle((int)raw[4]),
|
|
BitConverter.Int32BitsToSingle((int)raw[5]),
|
|
BitConverter.Int32BitsToSingle((int)raw[6]),
|
|
BitConverter.Int32BitsToSingle((int)raw[7]));
|
|
continue;
|
|
}
|
|
|
|
if (line == "LS")
|
|
{
|
|
current.Add(WalkOracleEvent.Landscape());
|
|
continue;
|
|
}
|
|
Match buildingMatch = BuildingPattern.Match(line);
|
|
if (buildingMatch.Success)
|
|
{
|
|
current.Add(WalkOracleEvent.Building(ParseId(buildingMatch.Groups[1].Value)));
|
|
continue;
|
|
}
|
|
Match insideMatch = DrawInsidePattern.Match(line);
|
|
if (insideMatch.Success)
|
|
{
|
|
current.Add(WalkOracleEvent.DrawInside(ParseId(insideMatch.Groups[1].Value)));
|
|
continue;
|
|
}
|
|
Match cellsMatch = DrawCellsPattern.Match(line);
|
|
if (cellsMatch.Success)
|
|
{
|
|
uint[] cells = cellsMatch.Groups[3].Value
|
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(ParseId)
|
|
.ToArray();
|
|
current.Add(WalkOracleEvent.DrawCells(
|
|
outsideViewCount: int.Parse(
|
|
cellsMatch.Groups[1].Value, CultureInfo.InvariantCulture),
|
|
declaredCount: int.Parse(
|
|
cellsMatch.Groups[2].Value, CultureInfo.InvariantCulture),
|
|
cells));
|
|
}
|
|
// Anything else is cdb chrome (banner, prompts, symbol notes) — ignored.
|
|
}
|
|
|
|
// The last STARTED frame (still in `current`) is deliberately never
|
|
// appended — that is the truncated detach frame (see class doc).
|
|
return frames;
|
|
}
|
|
|
|
public static IReadOnlyList<WalkOracleFrame> Load(string fixtureName)
|
|
{
|
|
string root = FindRepositoryRoot();
|
|
string path = Path.Combine(
|
|
root, "docs", "research", "2026-08-30-fw-walk-oracle", fixtureName + ".log");
|
|
return Parse(File.ReadLines(path));
|
|
}
|
|
|
|
private static string FindRepositoryRoot()
|
|
{
|
|
DirectoryInfo? dir = new(AppContext.BaseDirectory);
|
|
while (dir is not null)
|
|
{
|
|
if (File.Exists(Path.Combine(dir.FullName, "AcDream.slnx")))
|
|
return dir.FullName;
|
|
dir = dir.Parent;
|
|
}
|
|
throw new InvalidOperationException(
|
|
"AcDream.slnx not found above the test base directory; walk-oracle fixtures unavailable.");
|
|
}
|
|
|
|
private static uint ParseId(string hex)
|
|
=> uint.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
|
|
|
private static readonly Regex FramePattern = new(@"^F (\d+)\s*$", RegexOptions.Compiled);
|
|
private static readonly Regex PosePattern = new(
|
|
@"^P ([0-9a-f]{8}) ([0-9a-f]{8}) ([0-9a-f]{8}) ([0-9a-f]{8}) ([0-9a-f]{8}) ([0-9a-f]{8}) ([0-9a-f]{8}) ([0-9a-f]{8})\s*$",
|
|
RegexOptions.Compiled);
|
|
private static readonly Regex BuildingPattern = new(@"^BLD ([0-9a-f]{8})\s*$", RegexOptions.Compiled);
|
|
private static readonly Regex DrawInsidePattern = new(@"^DI ([0-9a-f]{8})\s*$", RegexOptions.Compiled);
|
|
private static readonly Regex DrawCellsPattern = new(
|
|
@"^DC pv=[0-9a-f]{8} ov=(\d+) n=(\d+):((?: [0-9a-f]{8})*)\s*$", RegexOptions.Compiled);
|
|
}
|
|
|
|
/// <summary>The camera pose dumped at the frame marker (raw dwords from
|
|
/// <c>Render::viewer_pos</c> @0x0081ef00): the camera's cell id, world
|
|
/// origin, and the Frame quaternion's four raw components (q0..q3 in
|
|
/// storage order — axis convention resolved by the replay runner).
|
|
/// Present only in the pose-stamped capture round.</summary>
|
|
public sealed record WalkOraclePose(
|
|
uint CellId,
|
|
System.Numerics.Vector3 Origin,
|
|
float Q0,
|
|
float Q1,
|
|
float Q2,
|
|
float Q3);
|
|
|
|
public sealed record WalkOracleFrame(
|
|
int Number,
|
|
IReadOnlyList<WalkOracleEvent> Events,
|
|
WalkOraclePose? Pose = null)
|
|
{
|
|
/// <summary>The frame's root: the first DrawInside cell, or null when outdoor-rooted.</summary>
|
|
public uint? InteriorRootCell
|
|
=> Events.FirstOrDefault(e => e.Kind == WalkOracleEventKind.DrawInside)?.CellId;
|
|
|
|
public bool HasLandscape => Events.Any(e => e.Kind == WalkOracleEventKind.Landscape);
|
|
|
|
public IEnumerable<uint> Buildings
|
|
=> Events.Where(e => e.Kind == WalkOracleEventKind.Building).Select(e => e.CellId!.Value);
|
|
}
|
|
|
|
public enum WalkOracleEventKind
|
|
{
|
|
Landscape,
|
|
Building,
|
|
DrawInside,
|
|
DrawCells,
|
|
}
|
|
|
|
public sealed record WalkOracleEvent(
|
|
WalkOracleEventKind Kind,
|
|
uint? CellId,
|
|
int OutsideViewCount,
|
|
int DeclaredCellCount,
|
|
IReadOnlyList<uint> Cells)
|
|
{
|
|
public static WalkOracleEvent Landscape()
|
|
=> new(WalkOracleEventKind.Landscape, null, 0, 0, Array.Empty<uint>());
|
|
|
|
public static WalkOracleEvent Building(uint cellId)
|
|
=> new(WalkOracleEventKind.Building, cellId, 0, 0, Array.Empty<uint>());
|
|
|
|
public static WalkOracleEvent DrawInside(uint cellId)
|
|
=> new(WalkOracleEventKind.DrawInside, cellId, 0, 0, Array.Empty<uint>());
|
|
|
|
public static WalkOracleEvent DrawCells(
|
|
int outsideViewCount, int declaredCount, IReadOnlyList<uint> cells)
|
|
=> new(WalkOracleEventKind.DrawCells, null, outsideViewCount, declaredCount, cells);
|
|
}
|