using System.Globalization;
using System.Text.RegularExpressions;
namespace AcDream.App.Tests.Rendering.Walk;
///
/// S4-c1 fix round 1, F3: parser for the OH alpha/depth captures
/// (docs/research/2026-09-01-overhaul/oh-capture/*.alphadepth.log —
/// tools/walk-oracle/oh/oh-capture-alpha-depth.cdb.template documents the
/// line formats). Reads the two lines this round's gate cares about:
///
///
/// - PM poly=<ptr> mode=<0|1> counterBefore=<hex> —
/// one per D3DPolyRender::DrawPortalPolyInternal @0x0059bc90 ENTRY
/// (mode 1 = far-Z building punch, mode 0 = true-depth exit seal). The
/// breakpoint is at function ENTRY, before the boundary guard runs inside
/// the function — a PM line is emitted for every ATTEMPT, guard-rejected
/// or not; counterBefore is the persistent
/// portalsDrawnCount global sampled before THIS call's own possible
/// increment.
/// - PC ov=<n> counter=<hex> fc=<0|1> — one per
/// PView::DrawCells @0x005a4840 ENTRY (root turn AND every building
/// look-in's own re-entrant call alike — the breakpoint doesn't
/// distinguish). counter is portalsDrawnCount sampled at
/// THIS call's entry, before its own possible read-then-zero.
///
///
/// Frame delimiting mirrors exactly (same
/// F <n> marker, same "events between F_n and F_(n+1) belong to
/// frame n" rule, same final-frame drop) so a caller can parse the SAME
/// capture file with both parsers and get position-consistent frame
/// numbering — for the pose, this type for
/// the PM/PC sequences.
///
public static class WalkAlphaDepthTrace
{
public static IReadOnlyList Parse(IEnumerable lines)
{
var frames = new List();
List<(int Mode, int CounterBefore)>? currentPm = null;
List<(int Ov, int Counter, int ForceClear)>? currentPc = null;
int currentNumber = 0;
foreach (string line in lines)
{
Match frameMatch = FramePattern.Match(line);
if (frameMatch.Success)
{
if (currentPm is not null)
{
frames.Add(new WalkAlphaDepthFrame(currentNumber, currentPm, currentPc!));
}
currentNumber = int.Parse(
frameMatch.Groups[1].Value, CultureInfo.InvariantCulture);
currentPm = new List<(int, int)>();
currentPc = new List<(int, int, int)>();
continue;
}
if (currentPm is null)
continue;
Match pm = PmPattern.Match(line);
if (pm.Success)
{
currentPm.Add((
int.Parse(pm.Groups[1].Value, CultureInfo.InvariantCulture),
int.Parse(pm.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture)));
continue;
}
Match pc = PcPattern.Match(line);
if (pc.Success)
{
currentPc!.Add((
int.Parse(pc.Groups[1].Value, CultureInfo.InvariantCulture),
int.Parse(pc.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture),
int.Parse(pc.Groups[3].Value, CultureInfo.InvariantCulture)));
continue;
}
// Anything else (FL/AM lines, cdb chrome) is out of this
// parser's scope — ignored, matching WalkOracleTrace's own
// catch-all.
}
// The last STARTED frame is deliberately never appended — the
// truncated detach frame, same rule as WalkOracleTrace.
return frames;
}
public static IReadOnlyList Load(string root, string fixtureName)
{
string repoRoot = FindRepositoryRoot();
string path = Path.Combine(
repoRoot, Path.Combine(root.Split('/')), 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 readonly Regex FramePattern = new(@"^F (\d+)\s*$", RegexOptions.Compiled);
private static readonly Regex PmPattern = new(
@"^PM poly=[0-9a-f]+ mode=([01]) counterBefore=([0-9a-f]{4})\s*$", RegexOptions.Compiled);
private static readonly Regex PcPattern = new(
@"^PC ov=(\d+) counter=([0-9a-f]{4}) fc=([01])\s*$", RegexOptions.Compiled);
}
/// One capture frame's PM (mode, counterBefore) and PC (ov,
/// counter, forceClear) sequences, in the exact order the retail trace
/// recorded them. Pointers are intentionally not carried — S4-c1 F3's own
/// comparison ignores them.
public sealed record WalkAlphaDepthFrame(
int Number,
IReadOnlyList<(int Mode, int CounterBefore)> PmEvents,
IReadOnlyList<(int Ov, int Counter, int ForceClear)> PcEvents);