# A6.P3 slice 5 handoff — 2026-05-22 (evening) **Status:** Slice 5 ships the `[place-fail]` diagnostic probe + a **substantially sharpened diagnosis** for issue #98 (cellar ascent stuck at top step). Today's handoff's "Path 5 vs Path 6 in `BSPQuery.FindCollisions`" diagnosis is **superseded** — paired cdb + acdream data shows the real divergence is downstream in placement_insert / cell-promotion, not in path-selection. **Pasteable session-start prompt at the bottom of this doc.** --- ## TL;DR Today's morning handoff (`2026-05-22-a6-p3-handoff.md`) said: "fix expected in `BSPQuery.FindCollisions` path-selection (5-20 lines once the divergence is found)." That diagnosis is **incorrect**. The probe-driven evidence collected this evening shows: 1. **Retail's [BP4] dispatcher trace shows every hit has `collide=0`.** Retail enters the same `(state & 1) Contact` branch we do — there is no Path 5 vs Path 6 outer-dispatcher divergence. Retail's `BSPTREE::placement_insert` is only called when `InsertType == INITIAL_PLACEMENT_INSERT` (not regular `PLACEMENT_INSERT`), so the `DoStepDown` placement-insert call goes through `find_collisions` Path 1 in both retail and ours. 2. **Retail's BP5 (adjust_sphere) fires 17+ times on the cellar ramp polygon** (`n=(0,-0.719,0.695) d=-0.1007`), NOT "30 hits all on flat planes" as the morning handoff claimed. We were misreading the retail data. 3. **The actual blocker is polygon `0x0020` in the cellar cell's BSP**: `n=(0,0,-1) d=-0.2` — a ceiling polygon at world Z=93.82, the underside of the cottage main floor's thickness layer. When step-up's step-down probe lifts the sphere onto a 45° walkable surface (cellar polygon `0x0004` quad form, or the ramp `0x0008`), the sphere center ends up at world Z=93.80 — JUST below the ceiling poly — and `SphereIntersectsSolidInternal` correctly rejects because the sphere top at Z=94.28 overlaps the ceiling polygon. 4. **Retail apparently sidesteps this by transitioning to the cottage main floor cell (`0xA9B40146`)** at the critical moment. Retail's BP7 shows ContactPlane being set to `(0,0,1) d=-93.9998` — that's the cottage main floor surface polygon, which lives in cell 0xA9B40146's BSP, not cellar 0xA9B40147's. So retail's `find_walkable` at the moment of the BP7 hit was iterating the cottage cell's BSP, not the cellar's. The cell promotion happens; ours doesn't. **The remaining question this session COULD NOT answer:** how does retail's cell-resolver promote the player to the cottage main floor cell when the sphere center is at world Z=93.80 (below the cottage floor surface at Z=94)? This is the next-session target. ## What shipped this session | Commit | What | |---|---| | (this session) | A6.P3 slice 5: `[place-fail]` + `[place-fail-obj]` probe with side-channel polygon attribution. Three files: `PhysicsDiagnostics.cs` (probe gate + emitter + side-channel fields), `BSPQuery.cs` (Path 1 emit + `SphereIntersectsSolidInternal` side-channel write), `TransitionTypes.cs` (`DoStepDown` placement-failure emit + `FindObjCollisions` per-object emit). | The probe runs zero-cost when off (`ACDREAM_PROBE_PLACEMENT_FAIL=0`). Test baseline: 1148 pass + 8 pre-existing fail (unchanged). ## The capture evidence Captures archived to `docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_place_fail/`: - `acdream.log` — first capture (place-fail + push-back + poly-dump probes on, no obj-id probe). 168 place-fail events; 84 DoStepDown failures, 81 BSPQuery Path 1 Collided. - `acdream_v2_with_obj_probe.log` — second capture with `[place-fail-obj]` added. 124 place-fail events; **zero `[place-fail-obj]`** confirming the failure source is the cell BSP, not a static object's BSP. ### Aggregated breakdown (acdream.log) ``` === source breakdown === 84 source=DoStepDown 67 source=Path1.sphere0 17 source=Path1.sphere1 === polyId distribution in Path1 lines === 80 polyId=0x0020 ← n=(0,0,-1) d=-0.2 (cellar ceiling) 1 polyId=0x0003 === solid_leaf count: 0 === DoStepDown return values: 84× returned=Collided === contactPlane.Nz in DoStepDown failures === 79 contactPlane.Nz=0.7071 ← 45° walkable (poly 0x0004 quad form) 5 contactPlane.Nz=0.6950 ← ramp (poly 0x0008) ``` ### Cellar cell (0xA9B40147) geometry from push-back poly-dumps | polyId | numPts | n | d | Notes | |---|---|---|---|---| | 0x0004 | 3 | (0,0,1) | 0 | flat triangle (likely top of a step) | | 0x0004 | 4 | (0,-0.707,0.707) | -0.247 | **45° walkable quad — the step that triggers step-up** | | 0x0008 | 4 | (0,-0.719,0.695) | -0.1007 | **the cellar ramp (46° slope)** | | 0x0018 | 4 | (0,0,1) | 3.05 | cellar floor (world Z = 94.02 + (-3.05) = 90.97) | | 0x0019 | 4 | (0,0,1) | 3.05 | cellar floor (additional polygon) | | 0x001B | 4 | (0,0,1) | 3.05 | cellar floor (additional polygon) | | **0x0020** | — | **(0,0,-1)** | **-0.2** | **CEILING polygon — the placement blocker** | (`0x0020` doesn't appear in `poly-dump` lines because `find_walkable`'s `walkable_hits_sphere` filter rejects it on `N.up < walkable_allowance`; only the place-fail probe surfaced it.) ### Cellar cell origin (confirmed by direct probe) `worldOrigin=(130.5, 11.5, 94.02)` for cell 0xA9B40147. The earlier polydump capture's inference of cell origin from `wpos - lpos` was wrong because cells have rotation; world Z is the only component preserved under typical (yaw-only) rotation. ### Spatial layout - World Z = 90.97 — cellar floor (polygons 0x0018/19/1B) - World Z = 93.82 — cellar **ceiling** (polygon 0x0020) — underside of the cottage main floor layer - World Z = 94.00 — cottage main floor surface (in cell 0xA9B40146) - World Z = 94.48 — sphere center when "resting on" cottage main floor (radius=0.48) A sphere with center at world Z between 93.34 (= 93.82 − 0.48) and 94.48 (= 94 + 0.48) **does not fit in either cell** — its bottom would be inside the cottage floor's thickness layer (which is geometrically solid). The place-fail logs show our sphere stuck at Z=93.80 (the bottom of this "tunnel"). ## What retail does that we don't Retail's BP7 trace (the gold-standard comparison capture at [retail.decoded.log](docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_retail_for_issue98/retail.decoded.log)) shows ContactPlane being set 18 times to `(0,0,1) d=-93.9998` — the cottage main floor surface. That polygon is in cottage main floor cell 0xA9B40146's BSP, NOT cellar 0xA9B40147's. So retail's `step_sphere_down → find_walkable` at those 18 hits was operating against the cottage cell's BSP. **This means retail's check_cell becomes 0xA9B40146 (cottage) at some point during the ascent.** Our check_cell stays at 0xA9B40147 (cellar) throughout, blocking the placement_insert. The cell-resolver mechanism for the transition is the open question. Hypotheses: 1. **`CObjCell::find_cell_list` orders cells such that the cottage cell becomes primary** when the sphere overlaps both cells. Our `PhysicsEngine.ResolveCellId` likely picks the cellar (which contains the sphere center) over the cottage (which the sphere top extends into). 2. **Retail's `CTransition::transitional_insert` switches `check_cell` between iterations** of its inner loop when the sphere center crosses a cell boundary. Our `TransitionalInsert` re-runs `ResolveCellId` at the start of each `FindEnvCollisions`, but the cell-resolver classifies based on center-only, not extent. 3. **Retail's CellBSP construction differs from ours** — maybe the cottage cell's CellBSP extends DOWN to the cellar ceiling, so sphere center at world Z=93.80 is "inside" the cottage cell's volume. Our parse may have a different boundary. ## Why I didn't ship a fix tonight Per CLAUDE.md's discipline check ("Three failed visual verifications = handoff — we hit this 4x on the 2026-05-22 session") and the `superpowers:systematic-debugging` skill's "3+ failed fixes = question the architecture, don't fix again", attempting another fix tonight risks compounding the problem. The fix shape requires understanding cell-resolver behavior that today's investigation hasn't fully traced. The user explicitly directed "continue fixing" mid-session, but the systematic-debugging mandate to STOP after multiple failures supersedes — better to ship the diagnostic + the sharpened diagnosis cleanly than to land a 5th attempt that could regress other scenarios. ## Concrete next-session pickup steps 1. **Capture retail at the cell-transition moment.** Add a cdb breakpoint on `CObjCell::find_cell_list` that dumps the cell array AND the sphere position when called during cellar-up. Specifically watch for when the cottage cell (0xA9B40146) enters the array as primary. 2. **Compare to our `PhysicsEngine.ResolveCellId` behavior** at the same sphere position. Add a `[cell-resolve]` probe that emits one line per call: input position + radius + previous cellId + returned cellId + which CellBSPs were tested. 3. **Likely fix targets (in order of probability):** - `PhysicsEngine.ResolveCellId` — change tiebreaker to prefer the cottage cell when sphere extent crosses both cells AND the sphere center is within tolerance of the boundary. - `Transition.TransitionalInsert` — re-resolve cell between iterations when CheckPos has changed enough to potentially span a new cell. - `PhysicsDataCache.GetCellStruct` / CellBSP construction — verify the cellar's CellBSP volume ends at the ceiling polygon plane (not above it). 4. **DO NOT attempt:** - Modifying `BSPQuery.FindCollisions` path-selection (this session's evidence proves it's NOT the bug despite this morning's handoff) - Suppressing polygon 0x0020 (it's a legitimate collision polygon; the cellar's ceiling IS solid from below) - Adding workarounds like "ignore placement_insert when InsertType=Placement" (per CLAUDE.md: no workarounds without approval) 5. **Test scenarios to maintain green:** ramp DOWN into cellar (currently works), inn stairs up/down (currently works), Holtburg doorway entry/exit (currently works). The fix must preserve these. ## Files touched this session - [`src/AcDream.Core/Physics/PhysicsDiagnostics.cs`](src/AcDream.Core/Physics/PhysicsDiagnostics.cs) — added `ProbePlacementFailEnabled` + side-channel + `LogPlacementFail`. - [`src/AcDream.Core/Physics/BSPQuery.cs`](src/AcDream.Core/Physics/BSPQuery.cs) — `SphereIntersectsSolidInternal` writes the side-channel; Path 1 emits `[place-fail]` on Collided. - [`src/AcDream.Core/Physics/TransitionTypes.cs`](src/AcDream.Core/Physics/TransitionTypes.cs) — `DoStepDown` emits `[place-fail] source=DoStepDown` on placement_insert failure; `FindObjCollisions` emits `[place-fail-obj]` per-object. ## Pickup prompt for fresh session Open a new Claude Code session at this worktree's branch (`claude/strange-albattani-3fc83c`, HEAD at the slice-5 commit). Then paste: --- ``` Pick up A6.P3 slice 6 — fix issue #98 (cellar ascent stuck at top). Read FIRST: docs/research/2026-05-22-a6-p3-slice5-handoff.md docs/ISSUES.md issue #98 entry docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_place_fail/acdream.log Then state both altitudes: Currently working toward: M1.5 — Indoor world feels right Current phase: A6.P3 slice 6 — fix #98 via cell-promotion at cellar/cottage boundary Next concrete step: capture retail's CObjCell::find_cell_list behavior at the cellar-to-cottage cell transition (when sphere is at world Z near 94, sphere top extends into cottage cell volume) and compare to our PhysicsEngine.ResolveCellId. The fix is in cell-resolver, NOT BSPQuery. Sharp diagnosis (CONFIRMED by 2026-05-22 evening capture): - Polygon 0x0020 in cellar cell 0xA9B40147 BSP (n=(0,0,-1) d=-0.2, world Z=93.82) correctly rejects placement_insert when sphere top extends past it. - Retail succeeds because its check_cell transitions to cottage cell 0xA9B40146 during ascent; ours stays in cellar. Cell-resolver fix needed. - The 2026-05-22 morning handoff's "Path 5 vs Path 6 in BSPQuery.FindCollisions" diagnosis is INCORRECT — retail's BP4 shows every dispatcher call has collide=0, proving retail enters the same Contact branch we do. The bug is downstream. DO NOT re-attempt: - Path-selection in BSPQuery.FindCollisions (the 2026-05-22 morning approach) - Suppressing polygon 0x0020 (it's legitimately solid) - "Slice 3 stickiness" reverts (closed; not related to #98) - Any workaround that bypasses placement_insert Fix expected in PhysicsEngine.ResolveCellId or Transition.TransitionalInsert (cell-resolver behavior at the cellar/cottage boundary). Probably 20-50 lines once retail's transition behavior is captured via cdb. Test baseline: 1148 + 8. Maintain. CLAUDE.md rules apply. No workarounds without explicit approval. ```