using System.Globalization;
using System.Text.RegularExpressions;
using AcDream.App.Rendering;
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;
List<(bool IsClip, bool IsNew)>? currentAm = null;
List<(uint ThreshBits, string Ret, int AmCountAtThisPoint)>? currentFl = 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!, currentAm!, currentFl!));
}
currentNumber = int.Parse(
frameMatch.Groups[1].Value, CultureInfo.InvariantCulture);
currentPm = new List<(int, int)>();
currentPc = new List<(int, int, int)>();
currentAm = new List<(bool, bool)>();
currentFl = new List<(uint, string, 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;
}
// S4-c2: AM mesh= surf= csurf= new=<0|1> clip=<0|1>
// listSel=<0|1> — D3DPolyRender::AddMeshToAlphaList @0x0059c230's
// own printf. listSel nonzero selects CLIP
// (alphaedMeshCountClip/List); zero selects ALPHA
// (alphaedMeshCountAlpha/List) — Ghidra-verified 2026-09-04, see
// RetailAlphaList's own doc comment. "new" is retail's per-entry
// first-for-list flag (param_4); "clip" (param_5, unused here)
// is overrideClipmap. S4-c2 fix round 1 (M4/A6): IsNew is parsed
// and RETAINED purely as a captured/parsed FACT (fidelity to the
// capture's own format) but is never compared against acdream's
// own routing — M4 established that flag is trivially true for
// EVERY subset retail appends (one DrawMesh invocation owns one
// subset per list), so a per-subset comparison would be
// meaningless at acdream's coarser per-INSTANCE granularity (the
// same content-volume mismatch M5's KnownFailure count gate
// documents) — no dead-parse-as-evidence claim is made here.
Match am = AmPattern.Match(line);
if (am.Success)
{
currentAm!.Add((
IsClip: am.Groups["listSel"].Value != "0",
IsNew: am.Groups["new"].Value != "0"));
continue;
}
// S4-c2: FL thresh= ret= —
// D3DPolyRender::FlushAlphaList @0x0059d2e0's own printf. `ret`
// is the CALLER's return address (the call instruction's own
// address + 5, x86 CALL rel32 being 5 bytes) — NOT the call
// instruction's address itself.
Match fl = FlPattern.Match(line);
if (fl.Success)
{
currentFl!.Add((
uint.Parse(
fl.Groups["thresh"].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture),
fl.Groups["ret"].Value,
currentAm!.Count));
continue;
}
// Anything else (PM/PC/FL/AM already handled; 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;
}
///
/// S4-c2 gate G-c2: reconstructs retail's own sequence of ACTUAL flush
/// drains (site, threshold, drained-CLIP-count, drained-ALPHA-count)
/// purely from the capture's AM/FL text — no execution needed on this
/// side. Mirrors D3DPolyRender::FlushAlphaList's exact no-op rule
/// (Ghidra-verified 2026-09-04: no-op only when BOTH running counts are
/// strictly below threshold * 3000) so a threshold that DOESN'T
/// drain (the overwhelming majority of 0.75f valve calls at ordinary
/// scene complexity) leaves the running counts untouched for the NEXT
/// flush call to inherit — exactly retail's own accumulation behavior.
///
internal static IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)>
BuildExpectedFlushTranscript(WalkAlphaDepthFrame frame)
{
var result = new List<(RetailAlphaFlushSite, float, int, int)>();
int runningClip = 0;
int runningAlpha = 0;
int lastAmCount = 0;
foreach ((uint threshBits, string ret, int amCountAtThisPoint) in frame.FlEvents)
{
// AM lines recorded (in interleaved parse order) between the
// previous FL and this one accumulate onto the running counts —
// exactly retail's own accumulation, since a no-op FlushAlphaList
// leaves both counters untouched for the next call to inherit.
for (int i = lastAmCount; i < amCountAtThisPoint; i++)
{
if (frame.AmEvents[i].IsClip)
runningClip++;
else
runningAlpha++;
}
lastAmCount = amCountAtThisPoint;
float threshold = BitConverter.UInt32BitsToSingle(threshBits);
bool noOp = runningClip < threshold * 3000f && runningAlpha < threshold * 3000f;
if (!noOp)
{
result.Add((MapSite(ret), threshold, runningClip, runningAlpha));
runningClip = 0;
runningAlpha = 0;
}
}
return result;
}
private static RetailAlphaFlushSite MapSite(string ret) => ret switch
{
"0059f310" => RetailAlphaFlushSite.DrawBuilding,
"005a1a0c" => RetailAlphaFlushSite.SortCellExit,
"005a4877" => RetailAlphaFlushSite.LandscapeFlush,
"00453b90" => RetailAlphaFlushSite.RenderNormalMode,
_ => throw new InvalidOperationException(
$"FL return address 0x{ret} is not one of the four normal-world flush sites "
+ "(OH1 contract §7) — the fifth private CreatureMode::Render caller, or an "
+ "unexpected address, reached this capture."),
};
public static IReadOnlyList Load(string root, string fixtureName)
{
return Parse(File.ReadLines(ResolvePath(root, fixtureName)));
}
/// S4-c1 fix round 2 (R2-2): retail's portalsDrawnCount
/// (wo(008719b4)) is a PERSISTENT session global carried by the running
/// client across every captured frame, so each capture's own FIRST PM or
/// PC line — wherever it falls, including the "F 1" preamble text a cdb
/// session prints before the first parsed frame marker (terrace-edge and
/// holtburg-doorway-still both have their first sample there) — already
/// carries the value a real session would have accumulated before the
/// capture began. Scans 's raw lines,
/// ignoring frame boundaries entirely, for the first line matching
/// either or and returns
/// its counter field.
public static int LoadInitialCounter(string root, string fixtureName)
{
foreach (string line in File.ReadLines(ResolvePath(root, fixtureName)))
{
Match pm = PmPattern.Match(line);
if (pm.Success)
return int.Parse(pm.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
Match pc = PcPattern.Match(line);
if (pc.Success)
return int.Parse(pc.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
}
throw new InvalidOperationException(
$"{fixtureName}: no PM/PC line found to seed PortalsDrawnCount from.");
}
private static string ResolvePath(string root, string fixtureName)
{
string repoRoot = FindRepositoryRoot();
return Path.Combine(repoRoot, Path.Combine(root.Split('/')), fixtureName + ".log");
}
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);
private static readonly Regex AmPattern = new(
@"^AM mesh=[0-9a-f]+ surf=-?\d+ csurf=[0-9a-f]+ new=(?[01]) clip=(?[01]) "
+ @"listSel=(?[01])\s*$",
RegexOptions.Compiled);
private static readonly Regex FlPattern = new(
@"^FL thresh=(?[0-9a-f]{8}) ret=(?[0-9a-f]{8})\s*$", RegexOptions.Compiled);
}
/// One capture frame's PM (mode, counterBefore), PC (ov, counter,
/// forceClear), AM (isClip, isNew), and FL (thresholdBits, returnAddress,
/// cumulativeAmCountAtThisPoint) sequences, in the exact order the retail
/// trace recorded them. Pointers/surface indices are intentionally not
/// carried — every S4 gate'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,
IReadOnlyList<(bool IsClip, bool IsNew)> AmEvents,
IReadOnlyList<(uint ThreshBits, string Ret, int AmCountAtThisPoint)> FlEvents);