acdream/docs/superpowers/plans/2026-05-21-a6-p3-slice1-cp-retention.md
Erik ba9655f6f7 plan(phys): A6.P3 slice 1 — indoor ContactPlane retention (Finding 2 fix)
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) <noreply@anthropic.com>
2026-05-21 21:27:38 +02:00

35 KiB
Raw Blame History

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 §1.2 (hypothesis), §5 (A6.P3 fix surface).

Findings: docs/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:

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:

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:

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:

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
# 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
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) <noreply@anthropic.com>"

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

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
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:

/// <summary>
/// Test-only counter for ContactPlane writes. Incremented by every
/// call to <see cref="SetContactPlane"/>. Used by
/// IndoorContactPlaneRetentionTests to assert that CP retention is
/// working (A6.P3 slice 1, 2026-05-21).
/// </summary>
internal int ContactPlaneWriteCount { get; private set; }

Then modify SetContactPlane (the existing method around line 251) to increment the counter. Find the existing line:

public void SetContactPlane(Plane plane, uint cellId, bool isWater = false)
{
    ContactPlane = plane;

Insert before ContactPlane = plane;:

    ContactPlaneWriteCount++;
    ContactPlane = plane;
  • Step 4: Build and verify
dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug

Expected: Build succeeded. 0 Warning(s). 0 Error(s).

  • Step 5: Commit
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) <noreply@anthropic.com>"

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

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).

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:

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;

/// <summary>
/// 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.
/// </summary>
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
dotnet build tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug

Expected: build succeeds.

  • Step 4: Run the test — expect FAIL
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
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) <noreply@anthropic.com>"

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

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 <INSERTION_POINT> with the exact location from Step 2):

// ── 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
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
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
git add src/AcDream.Core/Physics/<file_modified>
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) <noreply@anthropic.com>"

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

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:

                // ── 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 <function> (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
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
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
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

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) <noreply@anthropic.com>"

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

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)
.\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
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
$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
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

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) <noreply@anthropic.com>"

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
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) <noreply@anthropic.com>"

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:

## 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
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) <noreply@anthropic.com>"

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.