From ba9655f6f71211de8001f3a8c37b4d82b7edd5ab Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 21 May 2026 21:27:38 +0200 Subject: [PATCH] =?UTF-8?q?plan(phys):=20A6.P3=20slice=201=20=E2=80=94=20i?= =?UTF-8?q?ndoor=20ContactPlane=20retention=20(Finding=202=20fix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight-task plan to close A6.P2 Finding 2 (ContactPlane resynthesis blowup, ~1,470x more CP writes than retail). Strategy: strip the synthesis path inside Transition.FindEnvCollisions indoor branch + add per-transition Mechanism B (LKCP restore) so cross-frame CP retention flows via the existing retail mechanisms instead of per-frame TryFindIndoorWalkablePlane synthesis. Plan structure: T1 — Research note (retail Mechanism B oracle) — mandatory before code. T2 — Add ContactPlaneWriteCount probe (test instrumentation). T3 — Write failing IndoorContactPlaneRetentionTests regression. T4 — Add Mechanism B (LKCP restore) per-transition. T5 — Strip indoor walkable synthesis from FindEnvCollisions. T6 — Re-capture scen3 + verify cp-write ratio drops to ≤200. T7 — Re-capture scen1 + scen5 for full slice 1 sign-off. T8 — Bookkeeping (findings doc, roadmap, CLAUDE.md). Out of scope (deferred to slice 2 or A6.P4): - Mechanism C (frames_stationary_fall flat-CP synthesis); add only if slice 1 visual verification shows first-frame fall-through. - Finding 3 (cell-resolver sling-out); independent fix surface. - TryFindIndoorWalkablePlane definition deletion (A6.P4). - Issue #95 (visibility blowup; outside A6 scope). Acceptance: scen3 cp-write ≤ 200 (vs current 86,748); scen1/5 ratio ≤ 10x; visual verification at Holtburg inn 2nd floor passes; 1147+8 baseline maintained. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-21-a6-p3-slice1-cp-retention.md | 785 ++++++++++++++++++ 1 file changed, 785 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-21-a6-p3-slice1-cp-retention.md diff --git a/docs/superpowers/plans/2026-05-21-a6-p3-slice1-cp-retention.md b/docs/superpowers/plans/2026-05-21-a6-p3-slice1-cp-retention.md new file mode 100644 index 0000000..b2b4263 --- /dev/null +++ b/docs/superpowers/plans/2026-05-21-a6-p3-slice1-cp-retention.md @@ -0,0 +1,785 @@ +# A6.P3 Slice 1 — Indoor ContactPlane retention (Finding 2 fix) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Stop acdream's indoor-physics ContactPlane resynthesis blowup (A6.P2 Finding 2 — ~1,470× more CP writes than retail). Strip the per-frame synthesis path inside `Transition.FindEnvCollisions` indoor branch and rely on the existing Mechanism A (Path-6 land write) + Mechanism B (LKCP restore) for ContactPlane state. + +**Architecture:** Make our indoor branch of `FindEnvCollisions` match retail's tiny `CEnvCell::find_env_collisions` (10 lines: just call `BSPTREE::find_collisions` and return state). The current branch calls `TryFindIndoorWalkablePlane` (a synthesis workaround) + `ValidateWalkable` (which writes CP) EVERY frame — that's the blowup. The body-level LKCP-restore already exists in `PhysicsEngine.RunTransitionResolve` (lines 668-674) and handles the cross-frame retention. We add per-transition Mechanism B (LKCP restore into `ci.ContactPlane` inside the transition resolver) so the indoor branch can return OK without writing CP and downstream consumers still see a valid CP. + +**Tech Stack:** C# .NET 10, existing physics code in `src/AcDream.Core/Physics/`. Unit tests use xUnit (already wired). Integration verification uses the A6.P1 cdb probe infrastructure (already shipped). + +**Spec:** [`docs/superpowers/specs/2026-05-21-phase-a6-indoor-physics-fidelity-design.md`](../specs/2026-05-21-phase-a6-indoor-physics-fidelity-design.md) §1.2 (hypothesis), §5 (A6.P3 fix surface). + +**Findings:** [`docs/research/2026-05-21-a6-cdb-capture-findings.md`](../../research/2026-05-21-a6-cdb-capture-findings.md) Finding 2. + +**Retail oracle:** `docs/research/named-retail/acclient_2013_pseudo_c.txt`: +- `CEnvCell::find_env_collisions` line 309573 — the 10-line indoor branch shape. +- `COLLISIONINFO::set_contact_plane` line 271925 — the CP setter. +- LKCP restore inside `validate_transition` family — line 272565-272582 (restore CP from `last_known_contact_plane` when sphere is geometrically close). +- `frames_stationary_fall` flat-CP synthesis — line 272622+ (Mechanism C; deferred to A6.P3 slice 2). + +**Out of scope (deferred to A6.P3 slice 2):** +- Mechanism C (`frames_stationary_fall` counter + flat CP synthesis after 2+ stationary-falling frames). Add only if slice 1 visual verification shows first-frame fall-through after teleport / cell entry. +- Finding 3 (cell-resolver sling-out). Independent fix surface. Separate plan. +- Issue #95 (visibility blowup). Outside A6 scope. + +**Acceptance for slice 1:** +- scen3 re-capture: acdream cp-write count drops from 86,748 to ≤ 200 (≤ retail BP7 + small idle buffer). +- scen1, scen5 re-captures: CP-write ratio drops from 1,000+× to ≤ 10×. +- Visual verification at Holtburg inn 2nd floor: walking feels solid (no falling, no jitter, no fall-through to outdoor terrain). +- `dotnet build` + `dotnet test` green (1147+8 baseline maintained). + +--- + +## File Structure + +| Path | Purpose | Change | +|---|---|---| +| `src/AcDream.Core/Physics/TransitionTypes.cs` | `Transition` class — indoor BSP branch of `FindEnvCollisions` is the blowup site (lines 1514-1777). `TryFindIndoorWalkablePlane` (lines 1294-1380) is the synthesis workaround. | Modify — strip synthesis from `FindEnvCollisions` indoor branch; leave `TryFindIndoorWalkablePlane` definition in place (deleted in A6.P4) | +| `src/AcDream.Core/Physics/PhysicsEngine.cs` | `RunTransitionResolve` already has cross-frame LKCP restore (lines 668-674). Verify per-tick Mechanism B (LKCP restore into `ci.ContactPlane`) is wired or add it. | Read; modify if needed | +| `tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs` | New regression test asserting CP-write count stays low across multiple `FindTransitionalPosition` calls on the same flat plane. | Create | +| `docs/research/2026-05-21-a6-p3-slice1-retail-mech-b-research.md` | Short research note grounding the fix in retail's exact LKCP-restore pattern. Mandatory before code changes. | Create | +| `docs/research/2026-05-21-a6-captures/scen1-recap/` etc | Re-capture directories for verification. | Create dirs (already part of capture protocol) | + +--- + +## Task Decomposition + +### Task 1: Research note — retail Mechanism B + how `FindEnvCollisions` returns OK without writing CP + +This is non-negotiable. The current synthesis path was added to fix a real bug (fall-through to outdoor terrain at the inn doorway). Removing it without understanding retail's equivalent retention pattern will re-introduce that bug. Read retail's flow and document the exact path before changing code. + +**Files:** +- Create: `docs/research/2026-05-21-a6-p3-slice1-retail-mech-b-research.md` + +- [ ] **Step 1: Read `CEnvCell::find_env_collisions` in retail decomp** + +Run: +```bash +sed -n '309570,309600p' docs/research/named-retail/acclient_2013_pseudo_c.txt +``` + +Expected: see the 10-line function (already inspected in plan-write phase). Confirm: returns OK after `BSPTREE::find_collisions` returns OK; no `set_contact_plane` call inside `find_env_collisions`. + +- [ ] **Step 2: Read retail's `validate_transition` LKCP-restore block** + +Run: +```bash +sed -n '272540,272620p' docs/research/named-retail/acclient_2013_pseudo_c.txt +``` + +Expected: see the block where `last_known_contact_plane_valid != 0` triggers `COLLISIONINFO::set_contact_plane(&this->collision_info, &last_known_contact_plane, ...)`. This is the per-transition restore that closes the gap. + +- [ ] **Step 3: Find which retail function contains the LKCP-restore** + +Run: +```bash +awk 'NR<=272540 && /void __thiscall.*::/ {f=$0; ln=NR} NR>272540 {print ln, f; exit}' docs/research/named-retail/acclient_2013_pseudo_c.txt +``` + +Expected: print the most recent function header before line 272540. This identifies whether the restore is inside `validate_transition`, `find_obj_collisions`, `transitional_insert`, or another function. + +- [ ] **Step 4: Find the equivalent in our code** + +Search for our equivalent of that retail function name. Try `grep -n` for the C++ method name without the class prefix: + +Run: +```bash +grep -rn "TransitionalInsert\|TransitionalInsertGround\|FindTransitionalPosition\|ValidateTransition" src/AcDream.Core/Physics/ +``` + +Expected: identifies the C# method that should contain Mechanism B. Likely `Transition.FindTransitionalPosition` or `Transition.CheckTransition`. + +- [ ] **Step 5: Write the research note** + +```bash +# Open and write the file with the answers +``` + +Write `docs/research/2026-05-21-a6-p3-slice1-retail-mech-b-research.md` with these sections: + +1. **`CEnvCell::find_env_collisions` shape** (paste the 10-line function with our line annotations). +2. **Retail Mechanism B location** (the function name + line number from Step 3). +3. **Retail Mechanism B trigger condition** (the geometric proximity check at line 272569 — `|dot(global_curr_center, LKCP.N) + LKCP.d| <= radius + 0.0002f`). +4. **Our equivalent function** (from Step 4). +5. **Decision: where to add Mechanism B in our code** — either inside `Transition.FindEnvCollisions` itself (if our equivalent isn't called per-transition) OR inside the per-transition resolver (if it is). Note the choice. +6. **Risk: first-frame fall-through** — what happens when LKCP is invalid AND BSP returns OK on the first frame in a cell (post-teleport or post-cell-cross). Either accept the risk (slice 2 adds Mechanism C) or document a slice-1 mitigation. + +- [ ] **Step 6: Commit the research note** + +```bash +git add docs/research/2026-05-21-a6-p3-slice1-retail-mech-b-research.md +git commit -m "docs(research): A6.P3 slice 1 — retail Mechanism B oracle for CP retention + +Pre-fix research note grounding the indoor CP-retention refactor in +retail's exact LKCP-restore pattern (acclient_2013_pseudo_c.txt:272565-272582) +and CEnvCell::find_env_collisions tiny shape (line 309573). + +Output of this note drives the per-transition Mechanism B insertion +point selection in Task 4 + the slice-1 acceptance shape. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 2: Add CP-write count probe assertion infrastructure + +We need a deterministic way to count CP writes from a unit test. Look for an existing counter; if none, add one inside `CollisionInfo.SetContactPlane`. + +**Files:** +- Read: `src/AcDream.Core/Physics/TransitionTypes.cs:251-270` (the `SetContactPlane` setter) +- Modify (if needed): `src/AcDream.Core/Physics/TransitionTypes.cs` (add a static counter for tests) +- Test: `tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs` (created in Task 3) + +- [ ] **Step 1: Read the current `SetContactPlane` setter** + +```bash +sed -n '245,275p' src/AcDream.Core/Physics/TransitionTypes.cs +``` + +Expected: see the method signature `public void SetContactPlane(Plane plane, uint cellId, bool isWater = false)` setting `ContactPlane`, `ContactPlaneCellId`, `ContactPlaneIsWater`, plus updating LKCP fields. + +- [ ] **Step 2: Check if a CP-write counter already exists** + +```bash +grep -rn "ContactPlaneWriteCount\|CpWriteCount\|TotalContactPlaneWrites" src/AcDream.Core/Physics/ +``` + +Expected: probably no result (counter doesn't exist). If a result appears, use that counter and skip step 3. + +- [ ] **Step 3: Add a test-only counter to `CollisionInfo`** + +In `src/AcDream.Core/Physics/TransitionTypes.cs`, inside the `CollisionInfo` class (around line 245), add: + +```csharp +/// +/// Test-only counter for ContactPlane writes. Incremented by every +/// call to . Used by +/// IndoorContactPlaneRetentionTests to assert that CP retention is +/// working (A6.P3 slice 1, 2026-05-21). +/// +internal int ContactPlaneWriteCount { get; private set; } +``` + +Then modify `SetContactPlane` (the existing method around line 251) to increment the counter. Find the existing line: + +```csharp +public void SetContactPlane(Plane plane, uint cellId, bool isWater = false) +{ + ContactPlane = plane; +``` + +Insert before `ContactPlane = plane;`: + +```csharp + ContactPlaneWriteCount++; + ContactPlane = plane; +``` + +- [ ] **Step 4: Build and verify** + +```bash +dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug +``` + +Expected: `Build succeeded. 0 Warning(s). 0 Error(s).` + +- [ ] **Step 5: Commit** + +```bash +git add src/AcDream.Core/Physics/TransitionTypes.cs +git commit -m "test(phys): A6.P3 slice 1 — add CollisionInfo.ContactPlaneWriteCount + +Internal test-only counter incremented by SetContactPlane. Required +by IndoorContactPlaneRetentionTests to assert CP retention works +post-Finding-2 fix (A6.P2). + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 3: Write the failing regression test + +TDD step. Encode the expected post-fix behavior as a test that FAILS today and will PASS after the fix. + +**Files:** +- Create: `tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs` + +- [ ] **Step 1: Identify the test fixture pattern in the existing test project** + +```bash +ls tests/AcDream.Core.Tests/Physics/ | head -20 +``` + +Look for an existing test file using `Transition` directly — likely a `TransitionTests.cs` or `BSPQueryTests.cs`. Read it to learn the setup pattern (how to construct `Transition`, `SpherePath`, `CollisionInfo`, mock `PhysicsEngine`). + +```bash +grep -l "new Transition\|Transition.*FindTransitionalPosition\|Transition.*FindEnvCollisions" tests/AcDream.Core.Tests/Physics/ +``` + +Expected: one or more files; pick the most-similar test file as the template. + +- [ ] **Step 2: Write the test file** + +Create `tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs`: + +```csharp +using System.Numerics; +using AcDream.Core.Physics; +using Xunit; +// Plus any using statements identified from the template file in Step 1. + +namespace AcDream.Core.Tests.Physics; + +/// +/// A6.P3 slice 1 (2026-05-21). Regression tests for Finding 2: +/// ContactPlane resynthesis blowup. Asserts that running multiple +/// FindEnvCollisions calls on the same indoor flat-floor configuration +/// does NOT cause an unbounded CP-write count. +/// +public class IndoorContactPlaneRetentionTests +{ + [Fact] + public void IndoorFlatFloorWalk_DoesNotResynthesizeContactPlanePerFrame() + { + // Arrange: build a minimal indoor scenario. + // - Create a Transition with a SpherePath positioned inside an + // indoor cell (CellId low 16 bits >= 0x0100). + // - Mock a cell with a flat floor poly at Z=0. + // - Seed the CollisionInfo's ContactPlane + LastKnownContactPlane + // with the floor plane (simulating "we've already touched the floor"). + // - Reset ContactPlaneWriteCount to 0 just before the test loop. + // + // Setup the Transition + SpherePath + CollisionInfo per the + // template-test pattern identified in Step 1. + + var transition = /* ... build per template ... */; + var ci = transition.CollisionInfo; + + // Seed: simulate "already on the floor" + var floorPlane = new System.Numerics.Plane(new Vector3(0, 0, 1), 0f); + ci.SetContactPlane(floorPlane, cellId: 0xA9B40166, isWater: false); + + // Reset write counter after seeding. + int seededWrites = ci.ContactPlaneWriteCount; + Assert.Equal(1, seededWrites); + + // Act: simulate 60 frames of flat-floor walking by calling + // FindEnvCollisions 60 times with positions that stay on the same + // flat plane (small horizontal deltas, identical Z). + var engine = /* ... mock PhysicsEngine ... */; + for (int frame = 0; frame < 60; frame++) + { + transition.SpherePath.SetCheckPos( + new Position(/* same Z, small XY delta */), + cellId: 0xA9B40166); + + var state = transition.FindEnvCollisions(engine); + Assert.Equal(TransitionState.OK, state); + } + + // Assert: after 60 frames of identical flat-floor walking, the + // ContactPlane should have been written AT MOST a small constant + // number of additional times (ideally 0; allow a small budget for + // legitimate Mechanism A re-lands by BSP path-6). + int totalWrites = ci.ContactPlaneWriteCount; + int additionalWrites = totalWrites - seededWrites; + + // Threshold: 60 frames should produce at most ~5 additional + // CP writes (ratio ≤ 0.1 writes/frame). Today's broken code + // produces ~60-180 (1-3 writes/frame from per-frame synthesis). + Assert.True(additionalWrites <= 5, + $"Expected ≤5 additional CP writes across 60 flat-floor frames, " + + $"got {additionalWrites}. Finding 2 fix not complete."); + } +} +``` + +**Note:** The exact `Transition` construction depends on the template test from Step 1. If the template requires concrete `CellPhysics` + `DataCache`, replicate that setup. If the construction is too painful (requires mocking many fields), simplify the test by directly calling `Transition.FindEnvCollisions` and verifying ContactPlaneWriteCount stays low — even if the test setup is artificial, the assertion's job is to prove "we don't write CP on every frame." + +- [ ] **Step 3: Build the test project** + +```bash +dotnet build tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug +``` + +Expected: build succeeds. + +- [ ] **Step 4: Run the test — expect FAIL** + +```bash +dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~IndoorContactPlaneRetentionTests" --no-build +``` + +Expected: test FAILS with message like "Expected ≤5 additional CP writes ... got 60+." This proves the test correctly captures the bug. + +If the test PASSES today (no extra writes), the test setup doesn't exercise the indoor synthesis path. Revisit: either the SpherePath cell isn't in the indoor range (low 16 bits must be ≥ 0x0100), or the `engine.DataCache` mock isn't returning a cell with a BSP — meaning the indoor branch isn't entered. + +- [ ] **Step 5: Commit the failing test** + +```bash +git add tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs +git commit -m "test(phys): A6.P3 slice 1 — failing regression for Finding 2 CP blowup + +Test asserts 60 frames of indoor flat-floor walking should produce +≤5 ContactPlane writes. Fails today (broken code: ~60-180 writes). +Will pass after Task 4 + Task 5 strip the synthesis path. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 4: Add Mechanism B (LKCP restore) per-transition + +Based on Task 1's research note, add the LKCP-restore step in the correct location (per Step 5 of Task 1's research note). The shape should match retail's `validate_transition` lines 272565-272582 — when LKCP is valid AND the sphere is close to the LKCP plane, restore CP from LKCP. + +**Files:** +- Modify: `src/AcDream.Core/Physics/TransitionTypes.cs` OR `src/AcDream.Core/Physics/PhysicsEngine.cs` (location determined by Task 1 Step 5) + +- [ ] **Step 1: Re-read Task 1's research note** + +```bash +cat docs/research/2026-05-21-a6-p3-slice1-retail-mech-b-research.md +``` + +Locate Section 5: "Decision: where to add Mechanism B in our code." + +- [ ] **Step 2: Read the target function (the file + line range identified in Task 1 Step 5)** + +Read the function in full to identify the correct insertion point. Mechanism B should fire AFTER the transition's sub-step loop completes with OK_TS but BEFORE the body persist. + +- [ ] **Step 3: Write the Mechanism B insertion** + +Add at the determined insertion point (replace `` with the exact location from Step 2): + +```csharp +// ── Mechanism B — restore CP from LKCP when geometrically close ── +// A6.P3 slice 1 (2026-05-21). Retail oracle: +// acclient_2013_pseudo_c.txt:272565-272582 (validate_transition's +// LKCP-restore block). When the player is moving across a flat +// indoor floor and FindEnvCollisions returns OK without writing +// a fresh CP (per the now-stripped synthesis path), restore CP +// from LastKnownContactPlane if the sphere is close to the LKCP +// plane geometrically. +if (ci.LastKnownContactPlaneValid && !ci.ContactPlaneValid) +{ + var sphereCenter = SpherePath.GlobalCurrCenter[0].Origin; + var lkcp = ci.LastKnownContactPlane; + float distToLKCP = MathF.Abs( + Vector3.Dot(sphereCenter, lkcp.Normal) + lkcp.D); + float threshold = SpherePath.GlobalSphere[0].Radius + 0.0002f; + if (distToLKCP <= threshold) + { + ci.SetContactPlane( + lkcp, + ci.LastKnownContactPlaneCellId, + ci.LastKnownContactPlaneIsWater); + } +} +``` + +- [ ] **Step 4: Build** + +```bash +dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug +``` + +Expected: build succeeds. + +- [ ] **Step 5: Run the full Core test suite — should be 1147+ green** + +```bash +dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build +``` + +Expected: still 1147+ pass. Mechanism B by itself should not break anything (it only fires when ContactPlane is invalid but LKCP is valid — a state that's rare in the current code, but harmless if it does happen). + +If a previously-green test now fails: the Mechanism B logic is firing where it shouldn't. Inspect the failing test's setup to understand what state assumption it makes, then either narrow the Mechanism B condition or fix the test. + +- [ ] **Step 6: Commit Mechanism B alone** + +```bash +git add src/AcDream.Core/Physics/ +git commit -m "feat(phys): A6.P3 slice 1 step 1 — add Mechanism B (LKCP restore) + +Restores CollisionInfo.ContactPlane from LastKnownContactPlane when: + - LKCP is valid + - ContactPlane is currently invalid + - sphere is geometrically close to the LKCP plane + (|dot(center, N) + d| <= radius + 0.0002) + +Matches retail's validate_transition LKCP-restore at +acclient_2013_pseudo_c.txt:272565-272582. Slice 1 step 1 of the +A6.P3 indoor CP retention fix. Step 2 (Task 5) strips the +TryFindIndoorWalkablePlane synthesis from FindEnvCollisions. + +Tests pass: 1147+ green. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 5: Strip the synthesis path from `FindEnvCollisions` indoor branch + +The main fix. Match retail's `CEnvCell::find_env_collisions` tiny shape. + +**Files:** +- Modify: `src/AcDream.Core/Physics/TransitionTypes.cs:1623-1737` (the synthesis block after BSP returns OK) + +- [ ] **Step 1: Re-read the current indoor branch** + +```bash +sed -n '1623,1745p' src/AcDream.Core/Physics/TransitionTypes.cs +``` + +Expected: see the block that calls `CheckOtherCells`, then `TryFindIndoorWalkablePlane`, then `ValidateWalkable` on the synthesized plane. + +- [ ] **Step 2: Identify what to KEEP vs DELETE** + +KEEP: +- The `cellState != TransitionState.OK` early return (lines 1623-1628). Retail's tiny version has this. +- The Phase A4 `CheckOtherCells` call (lines 1638-1642). This is multi-cell collision iteration — separate concern from CP retention; keep. +- All probe diagnostics (`[indoor-bsp]`, `[indoor-walkable]` lines). These print only when the env var is set; keep for A6.P3 verification re-captures. + +DELETE: +- The `TryFindIndoorWalkablePlane` call (lines 1658-1662). +- The `if (walkableHit)` block calling `ValidateWalkable` (lines 1727-1737). +- The defensive comment about "fall through to outdoor terrain" — replaced by Mechanism B. +- The `[walk-miss]` diagnostic block (lines 1682-1725). This diagnostic was tied to the synthesis MISS case; with synthesis stripped, the diagnostic is meaningless. Move out of scope OR delete. + +REPLACE with: `return TransitionState.OK;` immediately after `CheckOtherCells` returns OK. + +- [ ] **Step 3: Apply the edit** + +Replace the block (lines 1645-1743 approximately) with: + +```csharp + // ── Indoor walkable handling — A6.P3 slice 1 (2026-05-21) ─ + // Retail's CEnvCell::find_env_collisions (decomp + // acclient_2013_pseudo_c.txt:309573) returns OK after + // BSPTREE::find_collisions returns OK — NO call to + // set_contact_plane or any synthesis. ContactPlane is + // either: + // - Already valid from a previous frame's Path-6 land + // write inside BSPQuery.FindCollisions (Mechanism A). + // - Restored from LKCP by the per-transition Mechanism B + // in (see Task 4). + // + // The old TryFindIndoorWalkablePlane synthesis path is + // removed here; the function definition is retained for + // now and is deleted in A6.P4 along with the #90 + // workaround. + // + // If subsequent visual verification shows first-frame + // fall-through (LKCP invalid AND no Path-6 land happens + // for a flat-walk-only scenario), A6.P3 slice 2 adds + // Mechanism C (retail's frames_stationary_fall flat-CP + // synthesis at acclient_2013_pseudo_c.txt:272622+). + return TransitionState.OK; + } + } + + // ── Outdoor terrain collision ──────────────────────────────────── +``` + +(Keep everything BEFORE `// ── Synthesize indoor walkable contact plane ──` and everything AFTER `// ── Outdoor terrain collision ──`.) + +- [ ] **Step 4: Build** + +```bash +dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug +``` + +Expected: build succeeds. If `walkableHit`, `indoorPlane`, `indoorVertices`, `hitPolyId`, `INDOOR_WALKABLE_PROBE_DISTANCE`, `WalkMissDiagnostic` are referenced by other code OUTSIDE this block, build will fail with "unused variable" warnings — those are now unused and the code outside this block should also not reference them; reach for `git grep`. + +- [ ] **Step 5: Run the IndoorContactPlaneRetentionTests test — expect PASS now** + +```bash +dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~IndoorContactPlaneRetentionTests" --no-build +``` + +Expected: test PASSES. The 60-frame flat-walk no longer writes CP per-frame because synthesis is gone, and Mechanism B (Task 4) keeps `ci.ContactPlane` valid via LKCP restore. + +If test still fails: Mechanism B isn't firing in the test scenario. Debug by adding a `Console.WriteLine` inside the Mechanism B block to print whether it fires. Re-run test, inspect output. + +- [ ] **Step 6: Run the FULL test suite — should be 1147+8 green** + +```bash +dotnet test --no-build +``` + +Expected: full pass. Tests that touched the old indoor-walkable path (if any) may need updating; investigate failures individually. Common causes: +- A test that expected `[walk-miss]` diagnostic to fire — re-scope or remove. +- A test that called `TryFindIndoorWalkablePlane` directly — those tests are testing the deleted-in-A6.P4 workaround; remove them or skip. + +- [ ] **Step 7: Commit the strip** + +```bash +git add src/AcDream.Core/Physics/TransitionTypes.cs +# Any other modified test files from Step 6 +git commit -m "fix(phys): A6.P3 slice 1 step 2 — strip indoor walkable synthesis + +Closes A6.P2 Finding 2 (ContactPlane resynthesis blowup, 250x to ∞x +more CP writes than retail). Indoor branch of Transition.FindEnvCollisions +now matches retail's CEnvCell::find_env_collisions tiny shape (decomp +line 309573): call BSPTREE::find_collisions, return state. No +synthesis, no per-frame ValidateWalkable call, no per-frame +ContactPlane write. + +Cross-frame CP retention now flows via: + - Mechanism A: BSPQuery.FindCollisions Path-6 land write (already + present, unchanged). + - Mechanism B: per-transition LKCP restore (added in prior commit). + - PhysicsEngine.RunTransitionResolve body persist (unchanged). + +TryFindIndoorWalkablePlane definition retained for now; deleted in +A6.P4 alongside the #90 sphere-overlap workaround. + +Verification: +- IndoorContactPlaneRetentionTests now passes. +- Full suite 1147+8 green maintained. +- Re-capture verification deferred to Task 6. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 6: Re-capture scen3 and verify CP-write ratio drops + +Integration verification. Re-run the A6.P1 capture protocol for scen3 (flat 2nd-floor walk, the cleanest CP-write-blowup signal) and confirm the ratio drops from ~86,748 to ≤200. + +**Files:** +- Create: `docs/research/2026-05-21-a6-captures/scen3_inn_2nd_floor_postfix/` directory +- The acdream.log is the artifact; no code changes in this task. + +- [ ] **Step 1: Confirm build is green** + +```bash +dotnet build -c Debug 2>&1 | tail -5 +``` + +Expected: `Build succeeded. 0 Warning(s). 0 Error(s).` + +- [ ] **Step 2: User confirms retail is running, character on Holtburg inn 2nd floor (via stairs in retail), ready to walk** + +Interactive coordination — wait for the user to confirm position. + +- [ ] **Step 3: Run cdb capture for retail-postfix (optional — gives a fresh paired baseline)** + +```powershell +.\tools\cdb\a6-probe-runner.ps1 -ScenarioTag "scen3_inn_2nd_floor_postfix" +``` + +Wait for "a6-probe v4 armed:" in `docs/research/2026-05-21-a6-captures/scen3_inn_2nd_floor_postfix/retail.log`. + +User walks: forward 3 m, sidestep 1 m, walk back. Shuffle a bit at the end. Then close retail to release cdb. + +- [ ] **Step 4: Decode retail.log** + +```bash +py tools/cdb/decode_retail_hex.py docs/research/2026-05-21-a6-captures/scen3_inn_2nd_floor_postfix/retail.log +``` + +- [ ] **Step 5: Launch acdream with probe env vars + walk same scenario** + +```powershell +$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call" +$env:ACDREAM_LIVE = "1" +$env:ACDREAM_TEST_HOST = "127.0.0.1" +$env:ACDREAM_TEST_PORT = "9000" +$env:ACDREAM_TEST_USER = "testaccount" +$env:ACDREAM_TEST_PASS = "testpassword" +$env:ACDREAM_DEVTOOLS = "1" +$env:ACDREAM_PROBE_PUSH_BACK = "1" +$env:ACDREAM_PROBE_INDOOR_BSP = "1" +$env:ACDREAM_PROBE_CELL = "1" +$env:ACDREAM_PROBE_CELL_CACHE = "1" +$env:ACDREAM_PROBE_CONTACT_PLANE = "1" +dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | + Out-File -FilePath "docs\research\2026-05-21-a6-captures\scen3_inn_2nd_floor_postfix\acdream.log" -Encoding ASCII +``` + +User teleports +Acdream to the inn 2nd floor (via `@teleport` per the original scen3 capture protocol), walks the same scenario, closes gracefully. + +- [ ] **Step 6: Compare CP-write counts** + +```bash +D="docs/research/2026-05-21-a6-captures/scen3_inn_2nd_floor_postfix" +echo "--- retail BP7 (set_contact_plane) ---" +grep -c "^\[BP7\]" "$D/retail.decoded.log" +echo "--- acdream cp-write ---" +grep -c "^\[cp-write\]" "$D/acdream.log" +echo "--- prefix vs postfix acdream ---" +echo "scen3 prefix: 86,748 cp-writes (committed at 4b5aebc)" +echo "scen3 postfix: $(grep -c "^\[cp-write\]" "$D/acdream.log") cp-writes" +``` + +Expected: acdream cp-write count ≤ 200 (the success threshold). If still in the thousands, Finding 2 fix is INCOMPLETE — diagnose: +- Is Mechanism B firing? Add a probe inside the Mechanism B `if` block, re-launch, check. +- Is some OTHER write site still firing? Grep `git log -p src/AcDream.Core/Physics/` for `SetContactPlane` calls not yet audited. + +- [ ] **Step 7: Visual verification with user** + +User walks +Acdream on the inn 2nd floor for ~30 seconds in various patterns: +- Walk back and forth. +- Stand still for a few seconds. +- Walk into a wall. +- Walk into furniture. + +User reports: does the player feel solid (no falling, no jitter, no fall-through to outdoor terrain)? + +If user reports a regression: ROLLBACK the commit from Task 5 and either: +- Add Mechanism C (frames_stationary_fall synthesis) per slice 2, then retry; OR +- Re-scope: keep TryFindIndoorWalkablePlane gated on `!ci.LastKnownContactPlaneValid` (synthesis only on first frame in cell), keep the rest of the fix. + +- [ ] **Step 8: Commit the verification capture** + +```bash +git add docs/research/2026-05-21-a6-captures/scen3_inn_2nd_floor_postfix/ +git commit -m "capture(research): A6.P3 slice 1 — scen3 post-fix verification + +Re-capture of scen3 (Holtburg inn 2nd floor, flat-floor walk) after +the A6.P3 slice 1 fix. CP-write ratio: + + scen3 pre-fix (4b5aebc): retail BP7 = 0, acdream cp-write = 86,748 + scen3 post-fix: retail BP7 = N, acdream cp-write = M + +[Fill in N + M from Step 6 output.] + +Visual verification with user at the inn 2nd floor: [PASS/FAIL/notes]. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 7: Re-capture scen1 and scen5 for full slice 1 sign-off + +Two more re-captures to ensure the fix generalizes (no regressions in scenarios where retail DID write CP). scen2 stair-fail + scen4 sling-out are intentionally OUT of slice 1 scope (those are Finding 3 territory). + +**Files:** +- Create: `docs/research/2026-05-21-a6-captures/scen1_inn_doorway_postfix/` +- Create: `docs/research/2026-05-21-a6-captures/scen5_sewer_entry_postfix/` + +- [ ] **Step 1: Re-capture scen1 (doorway walk-through)** + +Apply the same protocol as Task 6 Steps 2-6, with `-ScenarioTag "scen1_inn_doorway_postfix"`. Walk script: walk forward through inn front door, stop just inside. + +Expected: cp-write count drops from 73,304 to a small multiple of retail's BP7 (18 hits) — target ≤ ~200. + +- [ ] **Step 2: Re-capture scen5 (Town Network portal entry)** + +Apply the same protocol with `-ScenarioTag "scen5_sewer_entry_postfix"`. Walk script: walk to Town Network Portal, enter, walk 2m inside. + +Expected: cp-write count drops from 20,956 to a small multiple of retail's BP7 (65 hits) — target ≤ ~500 (portal threshold + indoor hub walking is naturally more CP-active than flat 2nd-floor walking). + +- [ ] **Step 3: Commit both verifications** + +```bash +git add docs/research/2026-05-21-a6-captures/scen1_inn_doorway_postfix/ +git add docs/research/2026-05-21-a6-captures/scen5_sewer_entry_postfix/ +git commit -m "capture(research): A6.P3 slice 1 — scen1 + scen5 post-fix verification + +scen1 pre-fix vs post-fix CP-write ratio: + retail BP7: 18 + acdream cp-write: 73,304 -> N (ratio ~4072x -> ~Nx) + +scen5 pre-fix vs post-fix CP-write ratio: + retail BP7: 65 + acdream cp-write: 20,956 -> M (ratio ~322x -> ~Mx) + +[Fill in N + M from re-capture decodes.] + +A6.P3 slice 1 acceptance threshold (CP-write ratio ≤ ~10x) met +across all three flat-floor + portal-walk scenarios. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +### Task 8: Update findings doc + roadmap; mark A6.P3 slice 1 SHIPPED + +Bookkeeping. Updates the A6.P2 findings doc with the post-fix data and the roadmap with the slice-1 ship. + +**Files:** +- Modify: `docs/research/2026-05-21-a6-cdb-capture-findings.md` +- Modify: `docs/plans/2026-04-11-roadmap.md` +- Modify: `CLAUDE.md` (Currently-working-toward block) + +- [ ] **Step 1: Append a "post-fix" section to the findings doc** + +In `docs/research/2026-05-21-a6-cdb-capture-findings.md`, add a new section after the existing "Findings" section: + +```markdown +## A6.P3 slice 1 — Finding 2 closed (2026-MM-DD) + +[Update date when this lands.] + +Strip + Mechanism B fix shipped at commits [list]. Re-capture verification: + +| Scenario | Pre-fix cp-write | Post-fix cp-write | Pre-fix ratio | Post-fix ratio | +|---|---:|---:|---:|---:| +| scen1 inn doorway | 73,304 | N | 4,072x | Nx | +| scen3 inn 2nd floor | 86,748 | M | ∞ | Mx | +| scen5 town network | 20,956 | K | 322x | Kx | + +[Fill in N, M, K from Task 6 + Task 7 outputs.] + +Finding 2 closed. Finding 1 dispatcher entry frequency mismatch +[status — closed as side effect / still wide / TBD per re-capture]. + +Next: Finding 3 (cell-resolver sling-out from scen4). Separate plan +when A6.P3 slice 2 is scoped. +``` + +- [ ] **Step 2: Update the roadmap A6.P3 entry** + +In `docs/plans/2026-04-11-roadmap.md` find the `- **A6.P3 — Fix the BSP correction paths**` line and update to show slice 1 SHIPPED with the commits. + +- [ ] **Step 3: Update CLAUDE.md Currently-working-toward block** + +In `CLAUDE.md` find the M1.5 + A6.P3 block. Update the "Current phase" line to reflect slice 1 ship + next slice (slice 2 = Finding 3 OR slice 2 = Mechanism C if needed). + +- [ ] **Step 4: Commit the bookkeeping** + +```bash +git add docs/research/2026-05-21-a6-cdb-capture-findings.md +git add docs/plans/2026-04-11-roadmap.md +git add CLAUDE.md +git commit -m "docs(roadmap+findings): A6.P3 slice 1 — SHIPPED + +CP-write resynthesis blowup (Finding 2) closed. scen1/3/5 re-captures +confirm ratio drop from 250-∞x to ≤10x. Strip-synthesis + Mechanism B +land. TryFindIndoorWalkablePlane retained pending A6.P4 deletion. + +Next: assess Finding 1 (dispatcher entry frequency) post-fix; if +still wide, scope as A6.P3 slice 2. Otherwise proceed to Finding 3 +(cell-resolver sling-out) as slice 2. + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +--- + +## Self-review checklist (for the implementer, before declaring slice 1 done) + +- [ ] Task 1's research note exists and is committed. +- [ ] CollisionInfo.ContactPlaneWriteCount counter exists and is incremented by SetContactPlane. +- [ ] IndoorContactPlaneRetentionTests exists, fails pre-fix, passes post-fix. +- [ ] Mechanism B (LKCP restore) is wired at the correct per-transition site. +- [ ] FindEnvCollisions indoor branch is stripped to match retail's CEnvCell::find_env_collisions shape. +- [ ] TryFindIndoorWalkablePlane definition is NOT deleted (deferred to A6.P4). +- [ ] Build green; full test suite 1147+8 green. +- [ ] scen3 re-capture cp-write count ≤ 200. +- [ ] scen1 + scen5 re-capture cp-write ratios ≤ 10x. +- [ ] Visual verification at Holtburg inn 2nd floor passes (no fall-through, no jitter). +- [ ] Findings doc + roadmap + CLAUDE.md updated. +- [ ] If visual verification revealed a regression: rolled back and either added Mechanism C (slice 2 promotion) or applied a narrower gating fix. + +If all 12 items check ✅, slice 1 is shipped. Next: either Finding 3 (separate plan) or Mechanism C if first-frame fall-through was reported.