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>
785 lines
35 KiB
Markdown
785 lines
35 KiB
Markdown
# 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.
|