From b35e491f125fa293b0209bba58fc88498be105be Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 3 Jun 2026 14:26:24 +0200 Subject: [PATCH] test(p0): retail find_cell_list trace parser + cdb value-capture tooling P0 Task 5. RetailTrace parses the [fcl] golden format (seed/pos/picked, RetailCellPick); 4 TDD tests green. find-cell-list-capture.cdb targets CPhysicsObj::change_cell (commit-on-diff) to capture retail's accepted membership sequence at the doorway; README is the operator runbook (dt offset verification + decode_retail_hex float decode). The live run is P0's one user-gated step (Task 6 mines existing traces first). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Conformance/RetailTrace.cs | 52 +++++++++++++ .../Conformance/RetailTraceTests.cs | 56 ++++++++++++++ tools/cdb/README-find-cell-list-capture.md | 75 +++++++++++++++++++ tools/cdb/find-cell-list-capture.cdb | 42 +++++++++++ 4 files changed, 225 insertions(+) create mode 100644 tests/AcDream.Core.Tests/Conformance/RetailTrace.cs create mode 100644 tests/AcDream.Core.Tests/Conformance/RetailTraceTests.cs create mode 100644 tools/cdb/README-find-cell-list-capture.md create mode 100644 tools/cdb/find-cell-list-capture.cdb diff --git a/tests/AcDream.Core.Tests/Conformance/RetailTrace.cs b/tests/AcDream.Core.Tests/Conformance/RetailTrace.cs new file mode 100644 index 0000000..1a940c6 --- /dev/null +++ b/tests/AcDream.Core.Tests/Conformance/RetailTrace.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using System.Text.RegularExpressions; + +namespace AcDream.Core.Tests.Conformance; + +/// +/// A single retail find_cell_list pick captured via cdb (golden oracle). +/// is the seed/current cell at find_cell_list +/// entry; is the chosen containing cell +/// (retail *arg5 in CObjCell::find_cell_list @ 0x52b4e0 pc:308742). +/// +public sealed record RetailCellPick(uint SeedCellId, Vector3 Position, uint PickedCellId); + +/// Parser for the find-cell-list-capture.cdb log format. +public static class RetailTrace +{ + private static readonly Regex Fcl = new( + @"^\[fcl\]\s+seed=0x(?[0-9A-Fa-f]{1,8})\s+" + + @"px=(?-?\d+(\.\d+)?)\s+py=(?-?\d+(\.\d+)?)\s+pz=(?-?\d+(\.\d+)?)\s+" + + @"picked=0x(?[0-9A-Fa-f]{1,8})\s*$", + RegexOptions.Compiled); + + public static RetailCellPick? ParseFindCellList(string line) + { + if (string.IsNullOrEmpty(line)) return null; + var m = Fcl.Match(line); + if (!m.Success) return null; + var ci = CultureInfo.InvariantCulture; + return new RetailCellPick( + SeedCellId: Convert.ToUInt32(m.Groups["seed"].Value, 16), + Position: new Vector3( + float.Parse(m.Groups["px"].Value, ci), + float.Parse(m.Groups["py"].Value, ci), + float.Parse(m.Groups["pz"].Value, ci)), + PickedCellId: Convert.ToUInt32(m.Groups["picked"].Value, 16)); + } + + /// Parse a log, skipping every non-matching line (noise/banner/other BPs). + public static IReadOnlyList ParseAll(IEnumerable lines) + { + var list = new List(); + foreach (var line in lines) + { + var rec = ParseFindCellList(line); + if (rec is not null) list.Add(rec); + } + return list; + } +} diff --git a/tests/AcDream.Core.Tests/Conformance/RetailTraceTests.cs b/tests/AcDream.Core.Tests/Conformance/RetailTraceTests.cs new file mode 100644 index 0000000..420e3ce --- /dev/null +++ b/tests/AcDream.Core.Tests/Conformance/RetailTraceTests.cs @@ -0,0 +1,56 @@ +using System.Numerics; +using Xunit; + +namespace AcDream.Core.Tests.Conformance; + +/// P0 Task 5 — TDD for the retail find_cell_list trace parser. +public class RetailTraceTests +{ + [Fact] + public void Parse_FindCellListLine_YieldsSeedPosAndPicked() + { + const string line = + "[fcl] seed=0xA9B40170 px=141.5000 py=7.2200 pz=92.7400 picked=0xA9B40171"; + var rec = RetailTrace.ParseFindCellList(line); + Assert.NotNull(rec); + Assert.Equal(0xA9B40170u, rec!.SeedCellId); + Assert.Equal(new Vector3(141.5f, 7.22f, 92.74f), rec.Position); + Assert.Equal(0xA9B40171u, rec.PickedCellId); + } + + [Fact] + public void Parse_NegativeCoordinatesAndLowercaseHex_Ok() + { + const string line = + "[fcl] seed=0xa9b40031 px=-12.5 py=0 pz=-3.25 picked=0xa9b40170"; + var rec = RetailTrace.ParseFindCellList(line); + Assert.NotNull(rec); + Assert.Equal(0xA9B40031u, rec!.SeedCellId); + Assert.Equal(new Vector3(-12.5f, 0f, -3.25f), rec.Position); + Assert.Equal(0xA9B40170u, rec.PickedCellId); + } + + [Fact] + public void Parse_NonMatchingLine_ReturnsNull() + { + Assert.Null(RetailTrace.ParseFindCellList("[BP4] find_collisions hit#10170 collide=0")); + Assert.Null(RetailTrace.ParseFindCellList("")); + Assert.Null(RetailTrace.ParseFindCellList("[fcl] seed=0xA9B40170 px=1 py=2")); // truncated + } + + [Fact] + public void ParseFile_SkipsNoiseAndYieldsOnlyPicks() + { + var lines = new[] + { + "armed; walk now", + "[fcl] seed=0xA9B40031 px=160.0 py=10.0 pz=94.0 picked=0xA9B40170", + "[BP] noise", + "[fcl] seed=0xA9B40170 px=158.0 py=12.0 pz=95.0 picked=0xA9B40171", + }; + var picks = RetailTrace.ParseAll(lines); + Assert.Equal(2, picks.Count); + Assert.Equal(0xA9B40170u, picks[0].PickedCellId); + Assert.Equal(0xA9B40171u, picks[1].PickedCellId); + } +} diff --git a/tools/cdb/README-find-cell-list-capture.md b/tools/cdb/README-find-cell-list-capture.md new file mode 100644 index 0000000..88833e8 --- /dev/null +++ b/tools/cdb/README-find-cell-list-capture.md @@ -0,0 +1,75 @@ +# find-cell-list-capture — operator runbook + +Captures retail's **accepted membership sequence** at the cottage doorway so P0 can pin +acdream's `CellTransit.FindCellList` against the real client (P0 Task 6 — the P1 gate). +This is the one user-gated step of P0. Everything else (parser, fixture loader, goldens) is +already headless + green. + +> **Try the autonomous path first.** Before running this, P0 Task 6 greps the *already-committed* +> retail traces under `docs/research/2026-05-21-a6-captures/` for a usable membership pick. Only +> run this capture if those traces don't yield one. + +## What you get + +A log of lines in the golden format the parser (`RetailTrace.ParseFindCellList`) reads: + +``` +[fcl] seed=0xA9B40031 px=0x43200000 py=0x41200000 pz=0x42BC0000 picked=0xA9B40170 +``` + +`seed` = the cell the player was in; `picked` = the cell retail committed; `px/py/pz` = the player +world origin as **raw IEEE-754 hex** (decode with `decode_retail_hex.py`). One line per accepted +cell change, so a doorway walk yields a short clean sequence (e.g. `0031 → 0170 → 0171`). + +## Prerequisites + +1. **PDB matches the binary** (CLAUDE.md retail-debugger toolchain): + ``` + py tools/pdb-extract/check_exe_pdb.py "C:/Turbine/Asheron's Call/acclient.exe" + ``` + Expect `=== MATCH ===`. +2. **Retail in-world at the Holtburg cottage doorway** (the building at world ≈ (161.9, 7.5, 94), + outdoor landcell `0xA9B40031`, vestibule `0xA9B40170`, room `0xA9B40171` — see + `docs/research/2026-06-03-p0-conformance-apparatus-notes.md`). Stand just outside the door. +3. `cdb.exe` (x86) at `C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe`. + +## One-time: verify the struct offsets (the script ships with placeholders) + +Launch cdb attached, let it break once, then dump the layouts and edit `$t4/$t5/$t6` in the +`.cdb`: + +``` +dt acclient!CPhysicsObj @ecx # note m_position (origin x,y,z) offset -> POS_OFF ($t6) + # note the current-cell pointer field -> CELLPTR_OFF ($t5) +dt acclient!CObjCell poi(@esp+4) # note cell_id offset -> CELLID_OFF ($t4) +``` + +`change_cell` is `thiscall`: `this` (CPhysicsObj*) is `@ecx`; the new cell arg is `poi(@esp+4)`. +The defaults in the script (`0x18 / 0x08 / 0x1C`) are best-guesses from `acclient.h` — **confirm +them** before trusting a capture. + +## Run + +```powershell +& "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe" ` + -pn acclient.exe -cf tools\cdb\find-cell-list-capture.cdb *>&1 | + Tee-Object -FilePath "find-cell-list-capture.console.log" +``` + +Then, in retail, **walk slowly in and out of the doorway 5–10 times** (out → vestibule → room → +back). The script auto-detaches (`qd`) after 400 cell changes; retail keeps running. Do **not** +`Stop-Process` cdb — that kills retail (CLAUDE.md watchout). + +## Fold into the golden + +1. Decode the hex floats to decimals: + ``` + py tools/cdb/decode_retail_hex.py find-cell-list-capture.log > find-cell-list-threshold.log + ``` + (or hand-decode; each `0xHHHHHHHH` is a little-endian float). Result lines must read + `[fcl] seed=0xA9B40031 px=160.0 py=10.0 pz=94.0 picked=0xA9B40170`. +2. Place the decoded file at + `tests/AcDream.Core.Tests/Conformance/Fixtures/find-cell-list-threshold.log`. +3. Run `dotnet test … --filter FindCellList_DoorwayThreshold_MatchesRetailTrace`. GREEN = acdream + already matches retail at the threshold. RED = a real divergence → that is the **P1** work, + left as a documents-the-bug conformance test (no weakening the assertion — master plan §4). diff --git a/tools/cdb/find-cell-list-capture.cdb b/tools/cdb/find-cell-list-capture.cdb new file mode 100644 index 0000000..be1f922 --- /dev/null +++ b/tools/cdb/find-cell-list-capture.cdb @@ -0,0 +1,42 @@ +$$ ============================================================================ +$$ find-cell-list-capture.cdb — P0 conformance retail golden capture +$$ ---------------------------------------------------------------------------- +$$ Captures retail's ACCEPTED membership decision at the cottage doorway so we +$$ can pin acdream's CellTransit.FindCellList against it (P0 Task 6, the P1 gate). +$$ +$$ Emits one line per accepted cell change in the golden format the parser reads +$$ (tests/AcDream.Core.Tests/Conformance/RetailTrace.cs): +$$ [fcl] seed=0xHHHHHHHH px=0x py=0x pz=0x picked=0xHHHHHHHH +$$ where seed = the cell the player was in (old), picked = the cell committed (new), +$$ and px/py/pz = the player world origin (raw IEEE-754 hex; decode offline with +$$ tools/cdb/decode_retail_hex.py, then write decimals into the golden fixture). +$$ +$$ Target: CPhysicsObj::change_cell @ 0x00513390 (pc:281192) — commit-on-diff. +$$ It fires ONLY on an accepted cell transition, so the doorway crossing yields a +$$ short, clean sequence (e.g. 0031 -> 0170 -> 0171), not a per-tick flood. +$$ +$$ thiscall: this (CPhysicsObj*) = @ecx ; arg new_cell (CObjCell*) = poi(@esp+4) +$$ +$$ OFFSETS BELOW ARE PLACEHOLDERS — VERIFY LIVE (see README). At the first break: +$$ dt acclient!CPhysicsObj @ecx $$ find m_position (Frame/Position) + cell +$$ dt acclient!CObjCell poi(@esp+4) $$ find cell_id offset +$$ dt acclient!Position $$ find origin (x,y,z) float offsets +$$ then edit CELLID_OFF / CELLPTR_OFF / POS_OFF and re-run. +$$ ============================================================================ + +.logopen C:\Users\erikn\source\repos\acdream\find-cell-list-capture.log +.sympath C:\Users\erikn\source\repos\acdream\refs +.symopt+ 0x40 +.reload /f acclient.exe + +$$ ---- EDIT THESE after the dt dumps (hex byte offsets) ---------------------- +r $t4 = 0x18 $$ CELLID_OFF : CObjCell.cell_id (acclient.h:30938 — verify) +r $t5 = 0x08 $$ CELLPTR_OFF : CPhysicsObj.cell (current cell ptr — verify) +r $t6 = 0x1C $$ POS_OFF : CPhysicsObj.m_position.origin.x (verify; y=+4, z=+8) +$$ --------------------------------------------------------------------------- + +r $t0 = 0 +bp acclient!CPhysicsObj::change_cell "r $t0 = @$t0 + 1; r $t1 = poi(@ecx+@$t5); r $t2 = poi(@esp+4); .printf /D \"[fcl] seed=0x%08x px=0x%08x py=0x%08x pz=0x%08x picked=0x%08x\\n\", poi(@$t1+@$t4), poi(@ecx+@$t6), poi(@ecx+@$t6+4), poi(@ecx+@$t6+8), poi(@$t2+@$t4); .if (@$t0 >= 400) { .printf /D \"=== DETACH after %d cell changes ===\\n\", @$t0; qd } .else { gc }" + +.printf \"find-cell-list capture armed (change_cell). Walk SLOWLY in/out of the cottage doorway now.\\n\" +g