acdream/docs/superpowers/plans/2026-05-20-indoor-walkable-synthesis-removal.md
Erik 686f27f227 docs(plan): remove per-frame indoor walkable-plane synthesis (Bug A)
Six-task plan for Bug A slice (spec 2026-05-20):
1. Replace synthesis call site with return TransitionState.OK
2. Delete Transition.TryFindIndoorWalkablePlane method + constant
3. Delete IndoorWalkablePlaneTests.cs + TransitionTypesTests.cs
4. Run physics suite, confirm baseline holds
5. Single commit per spec
6. User visual verification (5 scenarios)

Net delta: ~-480 lines. BSPQuery.FindWalkableSphere + its 5 unit tests
retained as the underlying retail-faithful walkable-finder API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:53:58 +02:00

22 KiB

Indoor Walkable-Plane Synthesis Removal 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: Delete the per-frame Transition.TryFindIndoorWalkablePlane synthesis + outdoor-terrain fallthrough from the indoor branch of FindEnvCollisions, restoring retail's "ContactPlane retained on OK" behavior.

Architecture: Pure deletion. Replace the ~50-line synthesis block in FindEnvCollisions with return TransitionState.OK;. Delete the now-orphan helper method + constant + 2 test files (9 tests total). Net delta about -480 lines.

Tech Stack: C# .NET 10, xUnit, existing BSPQuery / Transition / CellPhysics types.

Spec: docs/superpowers/specs/2026-05-20-indoor-walkable-synthesis-removal-design.md

Predecessor: docs/superpowers/specs/2026-05-20-indoor-bsp-worldorigin-fix-design.md (Bug B, shipped de8ffde).


File Structure

Files modified:

  • src/AcDream.Core/Physics/TransitionTypes.cs — three deletions in this one file:
    1. The synthesis block in FindEnvCollisions (about lines 1506-1558), replaced with return TransitionState.OK;.
    2. The TryFindIndoorWalkablePlane method itself (about lines 1269-1372, including doc-comment).
    3. The INDOOR_WALKABLE_PROBE_DISTANCE constant (about lines 1374-1381).

Files deleted:

  • tests/AcDream.Core.Tests/Physics/IndoorWalkablePlaneTests.cs (291 lines, 8 tests).
  • tests/AcDream.Core.Tests/Physics/TransitionTypesTests.cs (111 lines, 1 test).

Files intentionally preserved:

  • src/AcDream.Core/Physics/BSPQuery.csFindWalkableSphere + FindWalkableInternal's hitPolyId ref param stay.
  • tests/AcDream.Core.Tests/Physics/BSPQueryTests.cs — all 5 FindWalkableSphere_* tests + the Bug B regression test stay.
  • src/AcDream.Core/Physics/PhysicsDiagnostics.cs[cp-write] probe stays.

Task 1: Replace per-frame synthesis call site with return OK

The synthesis block sits inside the indoor branch of FindEnvCollisions, immediately after the cellState != TransitionState.OK early-return.

Files:

  • Modify: src/AcDream.Core/Physics/TransitionTypes.cs (about lines 1506-1558).

  • Step 1: Read the lines to confirm exact whitespace

Run Read on src/AcDream.Core/Physics/TransitionTypes.cs with offset=1498 and limit=65. Confirm the block from the if (cellState != TransitionState.OK) early-return through the trailing } closing the indoor BSP branch matches what the Edit will operate on.

  • Step 2: Apply the Edit to TransitionTypes.cs

Use the Edit tool with the following parameters. The old_string is the existing synthesis block; the new_string is the bare return TransitionState.OK;.

OLD_STRING (paste into Edit's old_string exactly, including leading whitespace):

                if (cellState != TransitionState.OK)
                {
                    if (!ObjectInfo.State.HasFlag(ObjectInfoState.Contact))
                        ci.CollidedWithEnvironment = true;
                    return cellState;
                }

                // ── Synthesize indoor walkable contact plane ──────────────
                // Indoor walking Phase 2 follow-up (2026-05-19). When the BSP
                // returns OK (no wall collision), the player is standing on a
                // floor poly inside the cell. We must NOT fall through to
                // outdoor terrain (SampleTerrainWalkable) — the outdoor terrain
                // Z is below the indoor floor due to the +0.02f Z-bump applied
                // for render z-fight prevention. ValidateWalkable would then see
                // the player 0.5m above the outdoor plane → marks them as
                // airborne → walkable=False → falling animation, never recovers.
                //
                // Retail: CEnvCell::find_env_collisions returns from the cell
                // branch with the cell's walkable plane set — no fall-through
                // to terrain.
                bool walkableHit = TryFindIndoorWalkablePlane(
                    cellPhysics, localCenter, sphereRadius,
                    out var indoorPlane,
                    out var indoorVertices,
                    out uint hitPolyId);

                if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
                {
                    if (walkableHit)
                    {
                        // dz = signed gap between foot and synthesized plane.
                        // Plane: N·p + D = 0  ⇒  pZ_on_plane = -D/N.z (for upward-facing planes)
                        // gap   = foot.Z - pZ_on_plane = foot.Z - (-D/N.z) = foot.Z + D/N.z
                        float dz = footCenter.Z + indoorPlane.D / indoorPlane.Normal.Z;
                        Console.WriteLine(System.FormattableString.Invariant(
                            $"[indoor-walkable] cell=0x{sp.CheckCellId:X8} wpos=({footCenter.X:F3},{footCenter.Y:F3},{footCenter.Z:F3}) probe={INDOOR_WALKABLE_PROBE_DISTANCE:F2} result=HIT poly=0x{hitPolyId:X4} wn=({indoorPlane.Normal.X:F3},{indoorPlane.Normal.Y:F3},{indoorPlane.Normal.Z:F3}) wD={indoorPlane.D:F3} dz={dz:+0.00;-0.00;+0.00}"));
                    }
                    else
                    {
                        Console.WriteLine(System.FormattableString.Invariant(
                            $"[indoor-walkable] cell=0x{sp.CheckCellId:X8} wpos=({footCenter.X:F3},{footCenter.Y:F3},{footCenter.Z:F3}) probe={INDOOR_WALKABLE_PROBE_DISTANCE:F2} result=MISS"));
                    }
                }

                if (walkableHit)
                {
                    return ValidateWalkable(
                        footCenter,
                        sphereRadius,
                        indoorPlane,
                        isWater: false,
                        waterDepth: 0f,
                        cellId: sp.CheckCellId,
                        walkableVertices: indoorVertices);
                }
                // If no walkable floor was found under the player indoors
                // (rare — cell with only walls/ceiling), fall through to
                // outdoor terrain as a defensive backstop. Indoor walking
                // will report walkable=False until the player moves over a
                // cell with a proper floor poly.
            }
        }

NEW_STRING (paste into Edit's new_string):

                if (cellState != TransitionState.OK)
                {
                    if (!ObjectInfo.State.HasFlag(ObjectInfoState.Contact))
                        ci.CollidedWithEnvironment = true;
                    return cellState;
                }

                // Indoor BSP returned OK — no wall collision. ContactPlane
                // is RETAINED from the prior tick's seed
                // (PhysicsEngine.ResolveWithTransition:583, the
                // init_contact_plane equivalent) OR refreshed by Path 3
                // step-down / Path 4 land if those fired this tick. Either
                // way, no synthesis is needed here — matches retail's
                // BSPTREE::find_collisions OK path
                // (acclient_2013_pseudo_c.txt:323938).
                //
                // Do NOT fall through to outdoor terrain backstop: the
                // player is in an indoor cell, and the outdoor terrain
                // Z is below the indoor floor by ~0.02m (the render Z-bump),
                // which would mark the player as airborne and trigger the
                // falling-animation stuck symptom (the original Bug A).
                // 2026-05-20 slice 2 of indoor ContactPlane retention.
                return TransitionState.OK;
            }
        }
  • Step 3: Build to verify the source still compiles

Run:

dotnet build -c Debug 2>&1 | tail -5

Expected: Build succeeded. with 0 errors. (The TryFindIndoorWalkablePlane method is still defined; we haven't removed it yet. It just has no callers now, which is a warning at most.)


Task 2: Delete the orphan TryFindIndoorWalkablePlane method + constant

After Task 1, TryFindIndoorWalkablePlane has no production callers. Tests in IndoorWalkablePlaneTests.cs and TransitionTypesTests.cs still reference it — they will be deleted in Task 3. For now, the build will go RED after this task and recover in Task 3.

Files:

  • Modify: src/AcDream.Core/Physics/TransitionTypes.cs (about lines 1269-1381).

  • Step 1: Read the method + constant block

Run Read on src/AcDream.Core/Physics/TransitionTypes.cs with offset=1268 and limit=115. Confirm the block from the /// <summary> doc-comment opening TryFindIndoorWalkablePlane, through the method body, through the INDOOR_WALKABLE_PROBE_DISTANCE constant + its doc-comment, ends at the blank line before the /// <summary> doc-comment for FindEnvCollisions.

  • Step 2: Apply the Edit

Use the Edit tool. The old_string is the entire method + constant block (lines about 1269-1381). The new_string is empty (just a single blank line to keep the file structure clean between the Fmt helper above and the Environment collision section header).

OLD_STRING starts with this line:

    /// <summary>
    /// Synthesize the indoor walkable contact plane for the player's current

and ends with this line:

    private const float INDOOR_WALKABLE_PROBE_DISTANCE = 0.5f;

Read the file FIRST (Step 1 above) to capture the exact bytes between those two markers, then paste them as old_string.

NEW_STRING:

(empty — just delete the block entirely. The // Environment collision — outdoor terrain section header at about line 1265 sits directly above the deleted region; after deletion it will sit directly above the FindEnvCollisions doc-comment, which is the correct placement.)

  • Step 3: Build to confirm source-only orphaned helper is gone

Run:

dotnet build -c Debug src/AcDream.Core/AcDream.Core.csproj 2>&1 | tail -5

Expected: Build succeeded. with 0 errors in AcDream.Core.csproj. (AcDream.Core.Tests.csproj will fail to build at this point — the test files still reference TryFindIndoorWalkablePlane. That's intentional; Task 3 fixes it.)


Task 3: Delete the obsolete test files

The two test files in tests/AcDream.Core.Tests/Physics/ exist solely to cover Transition.TryFindIndoorWalkablePlane. With the method deleted, the tests are dead code. Delete both files outright.

Files:

  • Delete: tests/AcDream.Core.Tests/Physics/IndoorWalkablePlaneTests.cs

  • Delete: tests/AcDream.Core.Tests/Physics/TransitionTypesTests.cs

  • Step 1: Confirm the file contents are TryFindIndoorWalkablePlane-only

Run:

grep -c 'TryFindIndoorWalkablePlane' tests/AcDream.Core.Tests/Physics/IndoorWalkablePlaneTests.cs tests/AcDream.Core.Tests/Physics/TransitionTypesTests.cs

Expected: both files have non-zero matches (confirming they touch the deleted method). If either has 0 matches, STOP and investigate — that file is not what the spec assumed.

  • Step 2: Delete the files

Run:

git rm tests/AcDream.Core.Tests/Physics/IndoorWalkablePlaneTests.cs tests/AcDream.Core.Tests/Physics/TransitionTypesTests.cs

Expected:

rm 'tests/AcDream.Core.Tests/Physics/IndoorWalkablePlaneTests.cs'
rm 'tests/AcDream.Core.Tests/Physics/TransitionTypesTests.cs'
  • Step 3: Build to confirm both projects compile

Run:

dotnet build -c Debug 2>&1 | tail -5

Expected: Build succeeded. with 0 errors across all projects. If there's still a build failure pointing at TryFindIndoorWalkablePlane, search the codebase for any other reference:

grep -rn 'TryFindIndoorWalkablePlane' src tests

(should return 0 results after Tasks 1-3 are complete).


Task 4: Run the physics test suite, confirm baseline holds

After deletion, the test count drops by 9 (the 9 deleted tests). The 6 pre-existing physics failures (3 MotionInterpreter + 2 BSPStepUp + 1 PositionManager) should still be the only failures.

  • Step 1: Run physics tests

Run:

dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug --no-build --nologo --filter "FullyQualifiedName~Physics"

Expected (the failing count should match the pre-existing baseline of 6):

Failed!  - Failed:     6, Passed:   ...(around 398-402), Skipped:     0, Total:   ...

If you see 7+ failures or any failure name NOT in this set, STOP and investigate:

AcDream.Core.Tests.Physics.MotionInterpreterTests.GetMaxSpeed_WalkForward_ReturnsWalkAnimSpeed
AcDream.Core.Tests.Physics.MotionInterpreterTests.GetMaxSpeed_Idle_ReturnsZero
AcDream.Core.Tests.Physics.MotionInterpreterTests.GetMaxSpeed_WalkBackward_ReturnsWalkAnimSpeedTimesBackwardsFactor
AcDream.Core.Tests.Physics.BSPStepUpTests.C3_Path6_AirborneMoverHitsSteepSlope_SetsCollide
AcDream.Core.Tests.Physics.BSPStepUpTests.D4_AirborneMover_TallWall_PersistsSlidingNormalAcrossFrames
AcDream.Core.Tests.Physics.PositionManagerTests.ComputeOffset_BothActive_Combined
  • Step 2: Run the full Core test suite for good measure

Run:

dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug --no-build --nologo

Expected: 8 failed total (the 6 physics above + the 2 non-physics ones from the broader 1126-test suite). If the broader suite shows new failures, investigate.


Task 5: Commit the change (single commit per spec)

The spec calls for a single commit covering the synthesis removal + method deletion + test deletion.

  • Step 1: Stage all changes

Run:

git add src/AcDream.Core/Physics/TransitionTypes.cs
git add -u tests/AcDream.Core.Tests/Physics/IndoorWalkablePlaneTests.cs tests/AcDream.Core.Tests/Physics/TransitionTypesTests.cs
git status

Expected git status:

Changes to be committed:
  modified:   src/AcDream.Core/Physics/TransitionTypes.cs
  deleted:    tests/AcDream.Core.Tests/Physics/IndoorWalkablePlaneTests.cs
  deleted:    tests/AcDream.Core.Tests/Physics/TransitionTypesTests.cs

If the test files were removed via git rm in Task 3 they should already be staged; git add -u just ensures any straggler is captured.

  • Step 2: Commit with the spec's commit message

Run:

git commit -m "fix(physics): remove per-frame indoor walkable-plane synthesis

The indoor branch of FindEnvCollisions called Transition.TryFindIndoorWalkablePlane
every frame to re-synthesize the ContactPlane after BSP returned OK.
The synthesis routed through BSPQuery.FindWalkableSphere -> walkable_hits_sphere,
which correctly rejects tangent contact via |dist| > radius - epsilon. For a
grounded player standing on or brushing a floor, the foot sphere is
tangent: 99.87% MISS rate per the 2026-05-20 [cp-write] probe.
Each MISS fell through to outdoor terrain backstop, writing a
ContactPlane that's below the indoor floor by ~0.02m, marking the
player airborne and triggering the falling-animation stuck symptom.

Fix: delete the synthesis + outdoor-fallthrough from the indoor OK
path. ContactPlane is retained from the prior tick's seed
(PhysicsEngine.ResolveWithTransition:583, init_contact_plane equivalent)
or refreshed by BSP Path 3 / Path 4 during the same tick. Matches
retail's BSPTREE::find_collisions OK path
(acclient_2013_pseudo_c.txt:323938).

Also deletes:
- Transition.TryFindIndoorWalkablePlane (~80 lines)
- INDOOR_WALKABLE_PROBE_DISTANCE constant
- [indoor-walkable] probe log line
- IndoorWalkablePlaneTests.cs (8 tests, the helper's coverage)
- TransitionTypesTests.cs (1 test, also tested the helper)

Net: ~-480 lines. BSPQuery.FindWalkableSphere + its 5 tests retained
as the underlying retail-faithful walkable-finder API.

Closes Bug A in the indoor ContactPlane retention phase.
Spec: docs/superpowers/specs/2026-05-20-indoor-walkable-synthesis-removal-design.md.
Predecessor: de8ffde (Bug B, BSP world-origin fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
  • Step 3: Verify commit landed

Run:

git log --oneline -7

Expected: top of log shows the new fix commit, followed by 3bec18f (Bug A spec), de8ffde (Bug B fix), 39d4e65 (Bug B regression test), 56816fc (Bug B plan), 865634f (Bug B spec), 66de00d ([cp-write] probe).


Task 6: Hand off to user for visual verification

The fix's primary acceptance test is visual: the user walks the 5 scenarios with the probes enabled and reports whether the stuck-falling symptom is gone.

  • Step 1: Close any stale acdream client process

Run:

$proc = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue
if ($proc) {
    $proc.CloseMainWindow() | Out-Null
    $proc.WaitForExit(5000) | Out-Null
}
Start-Sleep -Seconds 3
  • Step 2: Launch the probed build in the background

Run as a PowerShell command with run_in_background: true:

$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_INDOOR_BSP    = "1"
$env:ACDREAM_PROBE_CONTACT_PLANE = "1"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug *>&1 | Tee-Object -FilePath "launch-buga-postfix.log"
  • Step 3: Tell the user to walk the 5 scenarios + close

Tell the user:

Bug A fix landed. Launch is in the background. Please walk these 5 scenarios (about 30 seconds each):

  1. Cottage entry (outdoor to indoor).
  2. Indoor standstill about 10 seconds. Acceptance: stable, no flicker.
  3. 2nd-floor walking (the one that was broken). Acceptance: no stuck-falling when brushing upper floor edges.
  4. Cellar descent.
  5. Single-floor cottage walk (M1 regression check).

Close the window when done.

  • Step 4: Read + analyze the post-fix log

When the user reports the window is closed (or the background command notification arrives), convert and grep the log:

Get-Content launch-buga-postfix.log -Encoding Unicode | Out-File launch-buga-postfix.utf8.log -Encoding utf8

Then in Bash:

echo "=== [cp-write] caller distribution (post-Bug-A) ==="
grep -oE 'caller=[A-Za-z_.:0-9]+' launch-buga-postfix.utf8.log | sort | uniq -c | sort -rn | head -15
echo ""
echo "=== [indoor-walkable] lines (expect ZERO) ==="
grep -c '\[indoor-walkable\]' launch-buga-postfix.utf8.log
echo ""
echo "=== Transition.ValidateWalkable cp-writes (expect dramatic drop) ==="
grep -c 'caller=Transition\.ValidateWalkable' launch-buga-postfix.utf8.log
echo ""
echo "=== indoor-bsp result distribution ==="
grep -oE '\[indoor-bsp\].*result=[A-Z]+' launch-buga-postfix.utf8.log | grep -oE 'result=[A-Z]+' | sort | uniq -c

Expected:

  • [indoor-walkable] count: 0 (the probe line was deleted).

  • Transition.ValidateWalkable cp-write count: dramatic drop from the pre-fix 370 (pre-Bug-B saw 224+146 from indoor synthesis HIT path + outdoor fallthrough). Post-Bug-A should see only the outdoor terrain calls that legitimately fire when the player IS outdoors, perhaps tens of calls.

  • BSP FindCollisions:1615 + StepSphereDown:1123 (Path 3 + 4 BSP-internal writes): unchanged or similar to post-Bug-B counts. These are the legitimate retail-path CP writers.

  • PhysicsEngine.ResolveWithTransition:583: unchanged (per-tick seed still fires).

  • Step 5: Deliver the assessment to the user

Tell the user one of the following based on their visual report:

Success path:

Visual verification clean. [indoor-walkable] probe gone (expected — deleted). Transition.ValidateWalkable cp-write count dropped from 370 to N. Stuck-falling symptom resolved per user report. Bug A closed. Indoor ContactPlane retention phase complete (2 slices: Bug B [world-origin], Bug A [synthesis removal]).

Partial success (Bug A resolves stuck-falling, but a new "one-frame flicker" appears on outdoor->indoor transitions or after teleport):

Visual verification mostly clean, but a one-frame flicker on outdoor->indoor transitions. This is R2 (spec section) — acceptable for now. File as follow-up issue if it becomes annoying.

Failure path (stuck-falling persists):

Visual verification shows stuck-falling unchanged. Hypothesis was wrong — the per-frame synthesis was not the dominant cause, or there's a deeper issue with the OK-path retention. Investigate by adding a [ci-cp-final] probe at the bottom of FindTransitionalPosition to capture what ci.CP actually IS when the resolver finishes the OK case. Hand off as a separate session.


Self-Review

Run after writing this plan:

1. Spec coverage:

  • Spec section "Fix > Code changes" → Tasks 1 (synthesis call) + 2 (method + constant deletion). Covered.
  • Spec section "Fix > Test changes" → Task 3. Covered.
  • Spec section "Acceptance criteria > Probe-equivalence" → Task 6 step 4. Covered.
  • Spec section "Acceptance criteria > Visual verification" → Task 6 steps 2-3. Covered.
  • Spec section "Acceptance criteria > M1-baseline regression check" → Task 6 step 3 scenario 5. Covered.
  • Spec section "Risks" → R1/R2/R3 mitigations live as falsification language in Task 6 step 5. R4 is "decision noted in spec, no action needed."
  • Spec section "Out of scope" → not implemented. Acceptable.

2. Placeholder scan: No TBD/TODO. Every step has concrete commands + expected output.

3. Type consistency: No new types introduced; everything is a deletion. Existing names (TryFindIndoorWalkablePlane, INDOOR_WALKABLE_PROBE_DISTANCE, FindEnvCollisions, BSPQuery.FindWalkableSphere) are spelled consistently.

4. Build-order discipline: Task 1 leaves source-only code compiling (helper has no callers but still defined). Task 2 deletes the helper, breaking the test build. Task 3 deletes the tests, restoring build green. Task 4 confirms baseline. The temporary RED state between Tasks 2 and 3 is intentional and noted in Task 2 Step 3.