Add ACDREAM_PROBE_PLACEMENT_FAIL gate + LogPlacementFail emitter + side-channel polygon attribution in PhysicsDiagnostics. Wire into BSPQuery.FindCollisions Path 1 (Placement/Ethereal) on Collided returns; wire into Transition.DoStepDown after the placement_insert TransitionalInsert(1) call; wire into Transition.FindObjCollisions to emit per-static-object [place-fail-obj] lines. Run scen4 cellar-up with the probe → 168 [place-fail] events. 80 of 81 BSPQuery Path 1 placement rejections cite polygon 0x0020 in cellar cell 0xA9B40147's BSP: n=(0,0,-1) d=-0.2, world Z=93.82 — the cellar ceiling (underside of cottage main floor thickness layer). 0 [place-fail-obj] lines, confirming the failure source is the cell BSP not a static object. The probe-driven evidence INVALIDATES the 2026-05-22 morning handoff's "Path 5 vs Path 6 in BSPQuery.FindCollisions" diagnosis. Retail's BP4 trace shows every find_collisions hit has collide=0 — retail enters the same Contact branch we do, no outer-dispatcher divergence. Retail's BP5 fires 17+ times on the cellar ramp polygon, not "30 hits all on flat planes" as morning claimed. The actual divergence is downstream in cell-promotion: retail's check_cell transitions to cottage cell 0xA9B40146 during the ascent (BP7 sets ContactPlane to the cottage main floor poly, which lives in cottage cell's BSP not cellar's). Ours stays at cellar 0xA9B40147, where the ceiling poly 0x0020 correctly rejects the lifted sphere. No fix attempted this session per CLAUDE.md discipline check (3+ failed fixes = handoff). Full slice 5 evidence + concrete next-session pickup steps at docs/research/2026-05-22-a6-p3-slice5-handoff.md. ISSUES.md #98 updated with the corrected diagnosis. Test baseline: 1148 + 8 pre-existing fail. Maintained. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
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:
- Retail's [BP4] dispatcher trace shows every hit has
collide=0. Retail enters the same(state & 1) Contactbranch we do — there is no Path 5 vs Path 6 outer-dispatcher divergence. Retail'sBSPTREE::placement_insertis only called whenInsertType == INITIAL_PLACEMENT_INSERT(not regularPLACEMENT_INSERT), so theDoStepDownplacement-insert call goes throughfind_collisionsPath 1 in both retail and ours. - 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. - The actual blocker is polygon
0x0020in 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 polygon0x0004quad form, or the ramp0x0008), the sphere center ends up at world Z=93.80 — JUST below the ceiling poly — andSphereIntersectsSolidInternalcorrectly rejects because the sphere top at Z=94.28 overlaps the ceiling polygon. - 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'sfind_walkableat 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) 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:
-
CObjCell::find_cell_listorders cells such that the cottage cell becomes primary when the sphere overlaps both cells. OurPhysicsEngine.ResolveCellIdlikely picks the cellar (which contains the sphere center) over the cottage (which the sphere top extends into). -
Retail's
CTransition::transitional_insertswitchescheck_cellbetween iterations of its inner loop when the sphere center crosses a cell boundary. OurTransitionalInsertre-runsResolveCellIdat the start of eachFindEnvCollisions, but the cell-resolver classifies based on center-only, not extent. -
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
-
Capture retail at the cell-transition moment. Add a cdb breakpoint on
CObjCell::find_cell_listthat 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. -
Compare to our
PhysicsEngine.ResolveCellIdbehavior 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. -
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).
-
DO NOT attempt:
- Modifying
BSPQuery.FindCollisionspath-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)
- Modifying
-
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— addedProbePlacementFailEnabled+ side-channel +LogPlacementFail.src/AcDream.Core/Physics/BSPQuery.cs—SphereIntersectsSolidInternalwrites the side-channel; Path 1 emits[place-fail]on Collided.src/AcDream.Core/Physics/TransitionTypes.cs—DoStepDownemits[place-fail] source=DoStepDownon placement_insert failure;FindObjCollisionsemits[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.