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

785 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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) <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**
```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
/// <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:
```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) <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**
```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;
/// <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**
```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) <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**
```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 `<INSERTION_POINT>` 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/<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**
```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 <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**
```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) <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**
```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) <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**
```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) <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:
```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) <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.