diff --git a/docs/research/2026-06-03-p0-conformance-apparatus-notes.md b/docs/research/2026-06-03-p0-conformance-apparatus-notes.md
index e09b55f..6a50a79 100644
--- a/docs/research/2026-06-03-p0-conformance-apparatus-notes.md
+++ b/docs/research/2026-06-03-p0-conformance-apparatus-notes.md
@@ -167,7 +167,21 @@ evidence. **Do NOT design or code a P1 membership "fix" before the production-pa
RED/GREEN is read.** The P0 `..._DivergesFromRetail_PendingP1` test is a UNIT-level pin only, NOT
evidence of a production divergence.
-## ✅ RESOLVED — the production path DIVERGES (the "probe artifact" hypothesis is FALSIFIED)
+## ⚠ SUPERSEDED 2026-06-03 — the "production path DIVERGES" conclusion below was a CAPTURE ARTIFACT
+
+The section below concluded the production path genuinely diverges (0/11 → "port the swept curr_cell
+advance"). **That is wrong.** The 0/11 came from a one-frame skew in the cdb golden:
+`CPhysicsObj::SetPositionInternal` calls `change_cell` (`acclient_2013_pseudo_c.txt:283456`) BEFORE
+`set_frame` writes `m_position` (`:283458`), so the original capture paired each frame's NEW cell with
+the PREVIOUS frame's position (`golden_picked[i] == geom(golden_position[i+1])`, all 22 rows). An
+aligned re-capture (`tools/cdb/find-cell-list-capture-aligned.cdb`, position read from the following
+`set_frame`) makes the production gate read **9/9 with NO code change** — acdream's center-only
+`point_in_cell` pick already IS retail's true per-frame membership. Canonical corrected finding:
+`memory/project_retail_membership_criterion.md` + the RESOLVED banner in
+`docs/research/2026-06-03-p1-membership-swept-advance-handoff.md`. The text below is retained as the
+investigation trail.
+
+## ✅ (HISTORICAL) RESOLVED — the production path DIVERGES (the "probe artifact" hypothesis is FALSIFIED)
Built `ThresholdPortalCrossingReplayTests.ProductionPath_IndoorCrossings_DivergeFromRetail_PendingP1`
(replays the golden indoor `0170↔0171` segments through the REAL `ResolveWithTransition` — engine
diff --git a/docs/research/2026-06-03-p1-membership-swept-advance-handoff.md b/docs/research/2026-06-03-p1-membership-swept-advance-handoff.md
index 17ef0ed..77d328e 100644
--- a/docs/research/2026-06-03-p1-membership-swept-advance-handoff.md
+++ b/docs/research/2026-06-03-p1-membership-swept-advance-handoff.md
@@ -4,6 +4,34 @@
> (do NOT branch/worktree; do NOT push without asking; NEVER `git stash`/`gc`). PowerShell on
> Windows; launch logs are UTF-16. Read this FIRST, then the linked docs as needed.
+## ⚠ RESOLVED 2026-06-03 — premise REVERSED (read this first)
+
+The thesis of this doc ("port retail's swept `curr_cell` advance") is **FALSIFIED**. acdream's doorway
+membership already matches retail. The "0/11 production divergence" was a **cdb CAPTURE ARTIFACT**:
+`CPhysicsObj::SetPositionInternal` calls `change_cell` (`acclient_2013_pseudo_c.txt:283456`) BEFORE
+`set_frame` writes `m_position` (`:283458`), so the original golden paired each frame's NEW cell with
+the PREVIOUS frame's position — a one-frame skew (`golden_picked[i] == geom(golden_position[i+1])`,
+verified on all 22 rows; and acdream's static pick `== golden_picked[i-1]` on all rows). Re-capturing
+the COMMITTED position from the following `set_frame` (`tools/cdb/find-cell-list-capture-aligned.cdb`)
+aligns cell+position, and the production gate `ProductionPath_IndoorCrossings_MatchRetail` reads
+**9/9 with NO code change**. Both retail and acdream pick with **center-only `point_in_cell` on
+`global_sphere[0]`** (the foot sphere does not lead the foot; `cache_global_sphere` @ pc:274196);
+`curr_cell` commits via `CTransition::validate_transition` (@ pc:272608, `curr_cell = check_cell`) where
+`check_cell` is the `find_cell_list` pick — **structurally identical to acdream's
+`RunCheckOtherCellsAndAdvance` → `FindCellSet` → `SetCheckPos`**. There is nothing to port; porting a
+swept advance would make membership LEAD the foot by a frame (a bug to satisfy a mis-aligned golden).
+
+Corrected finding + current state: `memory/project_retail_membership_criterion.md` and
+`tests/AcDream.Core.Tests/Conformance/Fixtures/README.md`. The two decomp questions below ARE answered
+(Q1: no XY lead; Q2: commit is the `find_cell_list` pick via `validate_transition`) — the text below is
+retained as the investigation trail. **Still open** (separate from indoor membership, which is DONE):
+outdoor→indoor (`0031↔0170`) building-entry conformance (the building-only gate cache can't promote an
+outdoor seed — needs the landcell + building stab loaded); the master-plan cleanups (delete
+`CheckBuildingTransit`, unify `find_env_collisions`, demote `ResolveCellId`) refactor WORKING
+retail-faithful code → need explicit user approval.
+
+---
+
## State both altitudes
- **Milestone:** M1.5 — Indoor world feels right.
diff --git a/tests/AcDream.Core.Tests/Conformance/FindCellListConformanceTests.cs b/tests/AcDream.Core.Tests/Conformance/FindCellListConformanceTests.cs
index 90390df..7e617e3 100644
--- a/tests/AcDream.Core.Tests/Conformance/FindCellListConformanceTests.cs
+++ b/tests/AcDream.Core.Tests/Conformance/FindCellListConformanceTests.cs
@@ -106,37 +106,41 @@ public class FindCellListConformanceTests
}
///
- /// THE retail-trace-backed golden (the P0 gate for P1). Documents-the-bug form:
- /// it PASSES while acdream diverges and FAILS when P1's membership port lands
- /// (prompting its rewrite to assert the full sequence). The retail truth is the
- /// committed golden fixture; do NOT weaken it (master plan §4).
+ /// Retail-trace-backed conformance for the INDOOR (0170↔0171) membership picks: for every
+ /// captured pick whose seed AND committed cell are interior, acdream's center-only
+ /// point_in_cell returns retail's committed cell.
///
- /// ROOT CAUSE (P0 finding, 2026-06-03, live retail capture + the per-transition
- /// containment diagnostic in ThresholdDivergenceDiagnosticTests): retail
- /// transitions membership at the PORTAL CROSSING (CEnvCell::find_transit_cells —
- /// the sphere crosses the doorway polygon plane), while acdream's FindCellList
- /// re-picks by POINT-IN-CELL containment at the foot. So retail commits the
- /// neighbour cell BEFORE the foot point is geometrically inside it (it enters
- /// room 0171 while the foot is still inside vestibule 0170's BSP, in_0171=0), and
- /// acdream lags. ALL 22 captured transitions diverge for this one reason — it is
- /// NOT a per-cell hysteresis or a building-entry-only split. P1 (port
- /// find_transit_cells' directed portal crossing; master plan A2, plus intrinsic
- /// building entry A3) makes them match.
+ /// HISTORY (2026-06-03): with the FIRST capture (find-cell-list-capture.cdb) this read
+ /// all-diverge and was believed to prove a "membership lags retail" bug needing a
+ /// find_transit_cells portal-crossing / swept curr_cell-advance port. That was a CAPTURE
+ /// ARTIFACT, not a real divergence: CPhysicsObj::SetPositionInternal calls
+ /// change_cell (acclient_2013_pseudo_c.txt:283456) BEFORE set_frame updates
+ /// m_position (:283458), so the golden paired THIS frame's new cell with the PREVIOUS
+ /// frame's position (golden_picked[i] == geom(position[i+1])). The aligned re-capture
+ /// (tools/cdb/find-cell-list-capture-aligned.cdb) removes the skew, and acdream matches retail
+ /// on every indoor pick — acdream's pick already IS retail's true per-frame membership
+ /// (both use center-only point_in_cell on global_sphere[0]; the foot sphere does not lead the
+ /// foot). See docs/research/2026-06-03-p1-membership-swept-advance-handoff.md.
+ ///
+ /// Outdoor-involving (0031↔0170) picks are NOT asserted here: the building-only cache cannot
+ /// promote an outdoor seed into the interior (no landcell/building stab loaded) — that is the
+ /// separate outdoor-entry validation.
///
[Fact]
- public void FindCellList_DoorwayThreshold_DivergesFromRetail_PendingP1()
+ public void FindCellList_DoorwayThreshold_IndoorPicks_MatchRetail()
{
var (cache, picks) = LoadThresholdGolden();
if (cache is null) return;
Assert.NotEmpty(picks!);
- int matches = picks!.Count(p =>
- CellTransit.FindCellList(cache, p.Position, FootRadius, p.SeedCellId) == p.PickedCellId);
+ var indoor = picks!.Where(p =>
+ (p.SeedCellId & 0xFFFFu) >= 0x100u && (p.PickedCellId & 0xFFFFu) >= 0x100u).ToList();
+ Assert.NotEmpty(indoor);
- Assert.True(matches < picks.Count,
- $"acdream now reproduces {matches}/{picks.Count} retail doorway transitions. " +
- "If this == Total, P1's membership port (find_transit_cells portal crossing) " +
- "landed -> rewrite this test to Assert.Equal(pick.PickedCellId, FindCellList(...)) " +
- "for every captured pick. The retail truth is in the golden fixture.");
+ foreach (var p in indoor)
+ {
+ uint got = CellTransit.FindCellList(cache, p.Position, FootRadius, p.SeedCellId);
+ Assert.Equal(p.PickedCellId, got);
+ }
}
}
diff --git a/tests/AcDream.Core.Tests/Conformance/Fixtures/README.md b/tests/AcDream.Core.Tests/Conformance/Fixtures/README.md
index 3afeca8..0de668d 100644
--- a/tests/AcDream.Core.Tests/Conformance/Fixtures/README.md
+++ b/tests/AcDream.Core.Tests/Conformance/Fixtures/README.md
@@ -2,10 +2,22 @@
Captured retail golden data for the P0 conformance tests.
-- `find-cell-list-threshold.log` — **pending capture.** Retail `find_cell_list` picks at the
- Holtburg cottage doorway, in the `[fcl] seed=0x.. px=.. py=.. pz=.. picked=0x..` format read by
- `RetailTrace.ParseFindCellList`. Produced by `tools/cdb/find-cell-list-capture.cdb` (see its
- README). Until it lands, `FindCellList_DoorwayThreshold_MatchesRetailTrace` skips.
+- `find-cell-list-threshold.log` — retail `find_cell_list` picks at the Holtburg "Agent of
+ Arcanum" doorway (the `0031↔0170↔0171` building), in the
+ `[fcl] seed=0x.. px=.. py=.. pz=.. picked=0x..` format read by `RetailTrace.ParseFindCellList`.
+ **Captured ALIGNED** by `tools/cdb/find-cell-list-capture-aligned.cdb`: each row's position is
+ the COMMITTED position read from the `set_frame` call that follows `change_cell`, so cell and
+ position are from the same instant.
+
+ History (2026-06-03): the first capture (`tools/cdb/find-cell-list-capture.cdb`) read position
+ from `m_position` AT the `change_cell` breakpoint — but `SetPositionInternal` calls `change_cell`
+ (`acclient_2013_pseudo_c.txt:283456`) BEFORE `set_frame` writes `m_position` (`:283458`), so each
+ row paired THIS frame's new cell with the PREVIOUS frame's position (a one-frame skew,
+ `picked[i] == geom(position[i+1])`). That skew made the conformance read 0/11 and was
+ briefly misdiagnosed as a "membership lags retail" bug. The aligned golden removes the skew;
+ acdream's center-only `point_in_cell` pick matches retail 9/9 on the indoor crossings with **no
+ code change** — acdream's membership already IS retail's true per-frame behaviour. See
+ `docs/research/2026-06-03-p1-membership-swept-advance-handoff.md`.
Fixtures are read from this SOURCE directory (via `ConformanceDats.FixturesDir`), not copied to
the build output — matching the existing `Fixtures/issue98/**` pattern.
diff --git a/tests/AcDream.Core.Tests/Conformance/Fixtures/find-cell-list-threshold.log b/tests/AcDream.Core.Tests/Conformance/Fixtures/find-cell-list-threshold.log
index 669cffd..c14ba6f 100644
--- a/tests/AcDream.Core.Tests/Conformance/Fixtures/find-cell-list-threshold.log
+++ b/tests/AcDream.Core.Tests/Conformance/Fixtures/find-cell-list-threshold.log
@@ -1,22 +1,19 @@
-[fcl] seed=0xA9B40031 px=155.4350 py=16.1483 pz=94.0050 picked=0xA9B40170
-[fcl] seed=0xA9B40170 px=155.4210 py=15.4818 pz=94.0050 picked=0xA9B40171
-[fcl] seed=0xA9B40171 px=155.4130 py=15.1004 pz=94.0050 picked=0xA9B40170
-[fcl] seed=0xA9B40170 px=155.4302 py=15.9169 pz=94.0050 picked=0xA9B40031
-[fcl] seed=0xA9B40031 px=155.4360 py=16.1878 pz=94.0050 picked=0xA9B40170
-[fcl] seed=0xA9B40170 px=155.4192 py=15.3880 pz=94.0050 picked=0xA9B40171
-[fcl] seed=0xA9B40171 px=155.4126 py=15.0677 pz=94.0050 picked=0xA9B40170
-[fcl] seed=0xA9B40170 px=155.4298 py=15.8842 pz=94.0050 picked=0xA9B40031
-[fcl] seed=0xA9B40031 px=155.4341 py=16.0885 pz=94.0050 picked=0xA9B40170
-[fcl] seed=0xA9B40170 px=155.4173 py=15.2887 pz=94.0050 picked=0xA9B40171
-[fcl] seed=0xA9B40171 px=155.4139 py=15.1239 pz=94.0050 picked=0xA9B40170
-[fcl] seed=0xA9B40170 px=155.4311 py=15.9404 pz=94.0050 picked=0xA9B40031
-[fcl] seed=0xA9B40031 px=155.4369 py=16.2114 pz=94.0050 picked=0xA9B40170
-[fcl] seed=0xA9B40170 px=155.4201 py=15.4115 pz=94.0050 picked=0xA9B40171
-[fcl] seed=0xA9B40171 px=155.4139 py=15.1135 pz=94.0050 picked=0xA9B40170
-[fcl] seed=0xA9B40170 px=155.4311 py=15.9299 pz=94.0050 picked=0xA9B40031
-[fcl] seed=0xA9B40031 px=155.4388 py=16.2898 pz=94.0050 picked=0xA9B40170
-[fcl] seed=0xA9B40170 px=155.4219 py=15.4899 pz=94.0050 picked=0xA9B40171
-[fcl] seed=0xA9B40171 px=155.4135 py=15.0863 pz=94.0050 picked=0xA9B40170
-[fcl] seed=0xA9B40170 px=155.4340 py=16.0583 pz=94.0050 picked=0xA9B40031
-[fcl] seed=0xA9B40031 px=155.4365 py=16.1738 pz=94.0050 picked=0xA9B40170
-[fcl] seed=0xA9B40170 px=155.4196 py=15.3739 pz=94.0050 picked=0xA9B40171
+[fcl] seed=0xA9B40031 px=155.3717 py=16.0283 pz=94.0050 picked=0xA9B40170
+[fcl] seed=0xA9B40170 px=155.3438 py=15.1343 pz=94.0050 picked=0xA9B40171
+[fcl] seed=0xA9B40171 px=155.3463 py=15.2186 pz=94.0050 picked=0xA9B40170
+[fcl] seed=0xA9B40170 px=155.3742 py=16.1126 pz=94.0050 picked=0xA9B40031
+[fcl] seed=0xA9B40031 px=155.3723 py=16.0516 pz=94.0050 picked=0xA9B40170
+[fcl] seed=0xA9B40170 px=155.3431 py=15.1187 pz=94.0050 picked=0xA9B40171
+[fcl] seed=0xA9B40171 px=155.3445 py=15.1641 pz=94.0050 picked=0xA9B40170
+[fcl] seed=0xA9B40170 px=155.3748 py=16.1358 pz=94.0050 picked=0xA9B40031
+[fcl] seed=0xA9B40031 px=155.3692 py=15.9583 pz=94.0050 picked=0xA9B40170
+[fcl] seed=0xA9B40170 px=155.3437 py=15.1420 pz=94.0050 picked=0xA9B40171
+[fcl] seed=0xA9B40171 px=155.3451 py=15.1874 pz=94.0050 picked=0xA9B40170
+[fcl] seed=0xA9B40170 px=155.3730 py=16.0814 pz=94.0050 picked=0xA9B40031
+[fcl] seed=0xA9B40031 px=155.3721 py=16.0571 pz=94.0050 picked=0xA9B40170
+[fcl] seed=0xA9B40170 px=155.3418 py=15.0854 pz=94.0050 picked=0xA9B40171
+[fcl] seed=0xA9B40171 px=155.3456 py=15.2085 pz=94.0050 picked=0xA9B40170
+[fcl] seed=0xA9B40170 px=155.3735 py=16.1025 pz=94.0050 picked=0xA9B40031
+[fcl] seed=0xA9B40170 px=155.3435 py=15.1454 pz=94.0050 picked=0xA9B40171
+[fcl] seed=0xA9B40171 px=155.3461 py=15.2296 pz=94.0050 picked=0xA9B40170
+[fcl] seed=0xA9B40170 px=155.3740 py=16.1236 pz=94.0050 picked=0xA9B40031
diff --git a/tests/AcDream.Core.Tests/Conformance/ThresholdPortalCrossingReplayTests.cs b/tests/AcDream.Core.Tests/Conformance/ThresholdPortalCrossingReplayTests.cs
index e6651b0..356f439 100644
--- a/tests/AcDream.Core.Tests/Conformance/ThresholdPortalCrossingReplayTests.cs
+++ b/tests/AcDream.Core.Tests/Conformance/ThresholdPortalCrossingReplayTests.cs
@@ -78,18 +78,33 @@ public class ThresholdPortalCrossingReplayTests
private static uint Low(uint id) => id & 0xFFFFu;
///
- /// Documents-the-bug (GREEN while acdream diverges; FAILS when P1 lands → rewrite to
- /// per-segment Assert.Equal). The swept production path DIVERGES from retail on the
- /// indoor doorway crossings: ResolveWithTransition completes the move
- /// (restPos == target) but leaves CellId on the SOURCE cell — it never
- /// advances curr_cell across the portal the way retail's change_cell golden
- /// does. So the P0 finding is NOT a probe artifact: production membership genuinely lags.
- /// P1 must port retail's swept-crossing curr_cell advance (how the sphere crossing
- /// the doorway polygon / the leading sphere point promotes the neighbour to the membership
- /// answer mid-sweep), then this flips to all-match.
+ /// P1 conformance gate. Replays the captured retail doorway golden's INDOOR
+ /// (0170↔0171) crossings through the REAL
+ /// and asserts acdream's swept
+ /// CellId equals retail's committed cell on every crossing.
+ ///
+ /// HISTORY (2026-06-03). With the FIRST capture (find-cell-list-capture.cdb) this read
+ /// 0/11 and was believed to prove a "membership lags retail" bug needing a swept
+ /// curr_cell-advance port. That was a CAPTURE ARTIFACT, not a real divergence:
+ /// CPhysicsObj::SetPositionInternal calls change_cell
+ /// (acclient_2013_pseudo_c.txt:283456) BEFORE set_frame updates m_position
+ /// (:283458), so the cdb golden paired THIS frame's new cell with the PREVIOUS frame's
+ /// position — a deterministic one-frame skew (golden_picked[i] == geom(position[i+1])).
+ /// Re-capturing with the committed position read from the following set_frame
+ /// (tools/cdb/find-cell-list-capture-aligned.cdb) makes cell+position align at the same
+ /// instant, and acdream then matches retail 9/9 with NO code change: acdream's center-only
+ /// point_in_cell pick at the swept rest position IS retail's true per-frame
+ /// membership. Both pick with center-only point_in_cell on global_sphere[0]
+ /// (find_cell_list @ pc:308788-308825); the foot sphere does not lead the foot in XY
+ /// (cache_global_sphere @ pc:274196). See
+ /// docs/research/2026-06-03-p1-membership-swept-advance-handoff.md.
+ ///
+ /// Scope = INDOOR segments (the building-cell cache models these fully). The
+ /// outdoor-involving 0031↔0170 segments need the landcell + building stab loaded
+ /// (separate fixture work — verifies the outdoor→indoor entry path).
///
[Fact]
- public void ProductionPath_IndoorCrossings_DivergeFromRetail_PendingP1()
+ public void ProductionPath_IndoorCrossings_MatchRetail()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
@@ -101,6 +116,7 @@ public class ThresholdPortalCrossingReplayTests
var picks = RetailTrace.ParseAll(File.ReadAllLines(fixturePath));
int match = 0, total = 0;
+ var failures = new System.Collections.Generic.List();
for (int i = 0; i + 1 < picks.Count; i++)
{
uint fromCell = picks[i].PickedCellId;
@@ -123,6 +139,8 @@ public class ThresholdPortalCrossingReplayTests
bool ok = result.CellId == toCell;
if (ok) match++;
+ else failures.Add(System.FormattableString.Invariant(
+ $"0x{Low(fromCell):X4}->0x{Low(toCell):X4}@({picks[i + 1].Position.X:F2},{picks[i + 1].Position.Y:F2}): acdream=0x{Low(result.CellId):X4}"));
total++;
_out.WriteLine(
$"seg 0x{Low(fromCell):X4}->0x{Low(toCell):X4} " +
@@ -133,9 +151,8 @@ public class ThresholdPortalCrossingReplayTests
_out.WriteLine($"=== production-path indoor crossings: {match}/{total} match retail ===");
Assert.True(total > 0, "no indoor doorway segments found in the golden");
- Assert.True(match < total,
- $"acdream's swept ResolveWithTransition now reproduces {match}/{total} retail indoor " +
- "doorway crossings. If match == total, P1's curr_cell swept-advance port landed -> " +
- "rewrite this to Assert.Equal(toCell, result.CellId) per segment. (Retail truth = the golden.)");
+ Assert.True(failures.Count == 0,
+ $"acdream's swept membership diverged on {failures.Count}/{total} indoor doorway " +
+ $"crossings (retail truth = the aligned golden): {string.Join("; ", failures)}");
}
}
diff --git a/tools/cdb/find-cell-list-capture-aligned.cdb b/tools/cdb/find-cell-list-capture-aligned.cdb
new file mode 100644
index 0000000..4ab523f
--- /dev/null
+++ b/tools/cdb/find-cell-list-capture-aligned.cdb
@@ -0,0 +1,61 @@
+$$ ============================================================================
+$$ find-cell-list-capture-aligned.cdb — P1 ALIGNED retail golden capture
+$$ ----------------------------------------------------------------------------
+$$ SUPERSEDES find-cell-list-capture.cdb for the P1 gate. The original logged
+$$ this->m_position AT the change_cell breakpoint. But CPhysicsObj::SetPositionInternal
+$$ (acclient_2013_pseudo_c.txt) calls:
+$$ change_cell(this, curr_cell) @ line 283456 <- breakpoint fired here
+$$ set_frame(this, &curr_pos.frame) @ line 283458 <- m_position updated HERE, AFTER
+$$ so the original paired THIS frame's NEW cell with the PREVIOUS frame's position
+$$ — a deterministic one-frame offset (proven: golden_picked[i] == geom(golden_position[i+1])
+$$ for all 22 rows of the original capture; see
+$$ docs/research/2026-06-03-p1-membership-swept-advance-handoff.md follow-up).
+$$
+$$ This script reads the NEW (committed) position from the set_frame call that
+$$ IMMEDIATELY follows change_cell within the same SetPositionInternal, so the cell
+$$ and position are from the SAME instant. The resulting golden lets the P1 gate be a
+$$ true integration test (no position/cell skew).
+$$
+$$ Why the correlation is safe:
+$$ - Physics is single-threaded; SetPositionInternal(obj) runs change_cell(obj) then
+$$ set_frame(obj) back-to-back for the SAME object (no reentrancy).
+$$ - enter_cell (called by change_cell) does NOT call set_frame (verified
+$$ acclient_2013_pseudo_c.txt:278928). So the FIRST set_frame after a change_cell
+$$ is that object's committed frame.
+$$ - $t3 is the pending flag: change_cell arms it, the next set_frame consumes it.
+$$
+$$ Offsets (verified live via discover-types.cdb; reused from the original script):
+$$ change_cell: this=@ecx ; seed = this->cell->id = poi(poi(@ecx+0x90)+0x58)
+$$ new_cell = poi(@esp+4) ; new_cell->id = poi(poi(@esp+4)+0x58)
+$$ set_frame: arg2 (Frame*) = poi(@esp+4) ; Frame.m_fOrigin @ +0x34
+$$ new px/py/pz = poi(poi(@esp+4)+0x34 / +0x38 / +0x3c) (raw IEEE hex)
+$$
+$$ Emits the golden format read by RetailTrace.ParseFindCellList; decode the hex with
+$$ tools/cdb/decode_fcl_capture.py (then filter to the doorway cells 0031/0170/0171):
+$$ [fcl] seed=0x px=0x py=0x pz=0x picked=0x
+$$
+$$ Capture in a spot with NO other moving objects (NPCs crossing cells would also be
+$$ logged); the user paces the PLAYER alone in/out of the doorway. Stray rows are
+$$ filtered post-hoc to seed/picked in {0xA9B40031, 0xA9B40170, 0xA9B40171}.
+$$ ============================================================================
+
+.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\thirsty-goldberg-51bb9b\find-cell-list-capture-aligned.log
+.sympath C:\Users\erikn\source\repos\acdream\refs
+.symopt+ 0x40
+.reload /f acclient.exe
+
+r $t0 = 0
+r $t3 = 0
+r $t4 = 0
+r $t2 = 0
+
+$$ BP1: change_cell — capture seed (old cell) + picked (new cell), arm pending.
+$$ Do NOT read position here: m_position is still the PREVIOUS frame's value.
+bp acclient!CPhysicsObj::change_cell "r $t4 = 0; .if (poi(@ecx+0x90) != 0) { r $t4 = poi(poi(@ecx+0x90)+0x58) }; r $t2 = poi(poi(@esp+4)+0x58); r $t3 = 1; gc"
+
+$$ BP2: set_frame — the FIRST set_frame after a change_cell carries the NEW
+$$ (committed, aligned) position. arg2 = &curr_pos.frame ; m_fOrigin @ +0x34.
+bp acclient!CPhysicsObj::set_frame ".if (@$t3 == 1) { r $t0 = @$t0 + 1; .printf /D \"[fcl] seed=0x%08x px=0x%08x py=0x%08x pz=0x%08x picked=0x%08x\\n\", @$t4, poi(poi(@esp+4)+0x34), poi(poi(@esp+4)+0x38), poi(poi(@esp+4)+0x3c), @$t2; r $t3 = 0; .if (@$t0 >= 16) { .printf /D \"=== DETACH after %d aligned cell changes ===\\n\", @$t0; qd } }; gc"
+
+.printf \"aligned find-cell-list capture armed (change_cell + set_frame). Walk SLOWLY in/out of the doorway now.\\n\"
+g