# Collision fixes — session 2026-05-21 shipped handoff **Status:** All 9 commits merged to **`main`** via fast-forward (HEAD `56d2b5e`). Local main is ahead of `origin/main` (`7034be9`); not yet pushed. The original session worktree (`claude/lucid-goldberg-1ba520`) is still on disk but its branch is now identical to main and can be removed. **Next session should branch from main into a fresh worktree.** ## TL;DR User reported the world feeling buggy — collision in thin air inside and outside buildings, walls walk-through-able in spots. A two-step investigation surfaced a foundation-level math bug (`PolygonHitsSpherePrecise` inverted vs retail) and four discrete registration / cell-tracking bugs. **Four surgical fixes landed this session** (A1, A1.5, A1.6, A1.7) plus a `[walk-miss]` / `[floor-polys]` diagnostic probe set that quantified the bug rates. **What's left is one architectural change (A4: multi-cell BSP iteration) and three smaller code-correctness items.** Visual verification at the end of each phase confirmed forward progress; remaining wall-walkthroughs in vestibule cells are the A4 gap. ## What shipped this session ### Probe spike (3 commits) | SHA | What | Why | |---|---|---| | `27c7284` | `ProbeWalkMissEnabled` flag + roundtrip test | Diagnostic gate for ISSUES #83 H-disambiguation | | `31da57c` | `WalkMissDiagnostic` aggregator + 2 logic tests | Pure-function aggregator over `CellPhysics.Resolved` | | `a2e7a87` | `[walk-miss]` + `[floor-polys]` emission sites | Wire flag + aggregator into `Transition.FindEnvCollisions` MISS branch + `PhysicsDataCache.CacheCellStruct` | | `bb1e919` | Spec + plan + findings docs | The doc artifacts for the spike | The walk-miss probe produced the **smoking-gun analysis** in [`docs/research/2026-05-21-walk-miss-capture-findings.md`](2026-05-21-walk-miss-capture-findings.md): 0.38 % synthesis HIT rate, with a 2 cm boundary between HIT (`dz≈0.46 m`) and MISS (`dz≈0.48 m`) at sphere radius 0.480 m. This proved **`PolygonHitsSpherePrecise` is inverted vs retail's `polygon_hits_sphere_slow_but_sure`** (BSPQuery.cs:117 vs acclient_2013_pseudo_c.txt:322509-322517). That's Phase A2, still pending. ### Collision fixes (4 commits) | Phase | SHA | Fix | |---|---|---| | **A1** | `5f2b545` | **Skip mesh-AABB-fallback cylinder for landblock stabs.** Stabs (`entity.Id 0xC0XXYY00+n`) had their per-part BSP shadow correctly registered AND a redundant 1.5 m-clamped invisible cylinder at the mesh origin. The cylinder was the "thin air" collision inside cottages. Gate: `_isLandblockStab = (entity.Id & 0xFF000000u) == 0xC0000000u`. | | **A1.5** | `4d3bf6f` | **Scope interior cell shadows to ParentCellId.** `ShadowObjectRegistry.Register` assigned every entity to outdoor landcells based on XY. Interior statics (fireplace, furniture in cell `0xA9B40121`) got stamped into the outdoor landcell whose XY they overlapped (e.g., `0xA9B40029`), firing collisions for players walking OUTSIDE the building. New optional `cellScope` parameter, passed `entity.ParentCellId ?? 0u` from all 5 entity-loop call sites. | | **A1.6** | `700abad` | **Skip Setup CylSphere/Sphere shadows for landblock stabs.** A1 only gated the mesh-AABB-fallback path. Setup-derived registrations (lines 5910-6005 in GameWindow) still fired for stabs whose source is a Setup with CylSpheres. Same `_isLandblockStab` gate, extended to the outer `if (setup is not null)` block. | | **A1.7** | `4679134` | **Fall through to outdoor cell when indoor BSP doesn't contain player.** `CellTransit.FindCellList` returns `currentCellId` when no candidate cell's `CellBSP` contains the sphere — but this also fired when the player walked OUTSIDE the entire portal-connected indoor graph. The player's CellId was stuck on an old indoor cell whose BSP was geometrically far away; every indoor-bsp query returned OK at the BSP root; no walls blocked. Fix: after `FindCellList`, verify with `PointInsideCellBsp`; if not inside, fall through to the existing outdoor resolution branch. | ### Visual verification at each phase Each fix was visually verified by walking the same buildings before/after: - **A1**: "thin air" inside cottage GONE. - **A1.5**: "thin air" outside buildings → 71/97 interior-static-leak hits down to 0. - **A1.6**: Setup-CylSphere bleed around buildings cleared. - **A1.7**: cell-id correctly transitions between indoor doorway cell and adjacent outdoor cell on building exit. ## What's still broken Per end-of-session user testing: 1. **Walls walk-through-able in "vestibule" cells.** Some interior cells (e.g., the Holtburg cell `0xA9B40164`) have very few physics polygons — only 4 polys, BSP bounding sphere of 2 m radius. When the player walks past the doorway, they're geometrically inside a *neighboring* cell's actual walls — but the collision check only queries the cell the player's center is "in." That cell (the vestibule) has no walls there. The neighboring cell's walls (e.g., `0xA9B40157` with 23 polys, 38 % hit rate when the player IS there) are never queried. 2. **Stairs walk-through.** Likely the same multi-cell iteration gap — stairs span cell boundaries. 3. **Lighting indoors broken.** Separate rendering concern; M7 polish. 4. **Items projecting spotlight on walls.** Per-entity light direction bug; M7 polish. 5. **PHSP inversion (A2).** Still pending. The `[walk-miss]` data proved this bug exists but fixing it alone doesn't fix walkable synthesis at the tangent boundary — needs to pair with synthesis removal (A3). 6. **Synthesis architecturally wrong (A3).** Retail's grounded path never re-synthesizes `ContactPlane`; it retains via Mechanisms A/B/C. Our `TryFindIndoorWalkablePlane` runs every frame and is the wrong shape. Removing it is Bug A from the 2026-05-20 session — was tried + reverted because retention had its own gaps. A1.7 closed one of those gaps; A2 + A4 close the others. ## The architectural picture (plain-English) acdream's world is divided into invisible chunks called **cells**. There are two flavors: - **Outdoor cells**: the world is gridded into 24 m × 24 m squares. Each landblock (the 192 m × 192 m unit of streaming) has 64 such cells in an 8 × 8 grid. They get cell IDs like `0xA9B40029`. - **Indoor cells**: each room (or section of room) inside a building gets its own cell. They're not grid-aligned — they follow the building's interior partitioning. Cell IDs have the high bit of the low-16 set, e.g. `0xA9B40157`. Each cell carries: - A **CellBSP** — defines the volume the cell occupies in space (used for "is this point inside this cell?" lookups during cell-id resolution). - A **PhysicsBSP** — the collision geometry (walls, floors, stairs) the player can hit. - **Portals** — connections to adjacent indoor cells (think doorways). - **Static objects** — furniture, decoration meshes hydrated as entities. The collision system asks two things per frame: 1. **What cell is the player in?** Driven by `PhysicsEngine.ResolveCellId` → `CellTransit.FindCellList`. Walks the portal graph from the current cell, picks the cell whose `CellBSP` contains the sphere center. With **A1.7**, when no indoor cell claims the player, falls through to outdoor landcell resolution. 2. **Does the player hit anything?** Drives `Transition.FindEnvCollisions`. Queries the **one cell** the player is "in" — its `PhysicsBSP` for walls/floor and its shadow-registered statics for furniture. **The architectural gap** is step 2 only queries one cell. Retail queries the **cell_array** — the sphere center's cell plus every other cell the sphere geometrically overlaps. So if you're in a vestibule cell with no real walls but your shoulder pokes into the next room's wall, retail's collision sees the wall. acdream doesn't. ## Phase A4 — multi-cell iteration (the next big fix) This is the gap. Implementation sketch: ### What to port from retail `CTransition::check_other_cells` at `acclient_2013_pseudo_c.txt:272717-272798`. After the primary cell's `find_collisions` runs, it iterates every other cell in `this->cell_array` (built from `CObjCell::find_cell_list` which fills via interior portals + `add_all_outside_cells` for outdoor neighbors). For each cell: - Calls the cell's vtable `find_collisions`. - On Slid (4): clears `contact_plane_valid`, returns. - On Collided (2) or Adjusted (3): returns immediately. - On OK: continues to the next cell. If the sphere is geometrically outside the original cell, the fallback (line 272761-272797) sets `check_cell = var_4c` (the cell containing the final position) and adjusts `check_pos.objcell_id`. ### What we already have Phase 2 portal cell-tracking is shipped (commits `1969c55` → `eb0f772`, 2026-05-19). It gives us: - `CellTransit.FindCellList` (sphere variant) — top-level driver. - `CellTransit.FindTransitCellsSphere` — interior portal neighbour expansion. - `CellTransit.AddAllOutsideCells` — outdoor landcell neighbour expansion. - `CellPhysics.VisibleCellIds` — pre-computed visible-cell set per cell. These currently feed **cell-id resolution** (step 1 above). They are NOT yet used to drive **collision iteration** (step 2). A4's job is to wire them into `Transition.FindEnvCollisions`. ### Implementation outline for A4 1. **In `Transition.FindEnvCollisions`** (`src/AcDream.Core/Physics/TransitionTypes.cs:1407-1559`): - Currently: queries one cell (`engine.DataCache.GetCellStruct(sp.CheckCellId)`) and runs `BSPQuery.FindCollisions` against its BSP. - Change to: build the cell_array from the current cell using `CellTransit.FindCellList` (or a new variant that returns the full set), then iterate each cell and run BSP collision against each. Combine results. 2. **Combine semantics** match retail's `check_other_cells`: - Any cell returning `Collided` (2) or `Adjusted` (3) → return that immediately (halt iteration). - Any cell returning `Slid` (4) → record but continue (in case another cell collides harder). After all cells: return Slid. - All cells OK → return OK. 3. **Outdoor case**: if the resolved cell is outdoor, iterate adjacent outdoor landcells via `AddAllOutsideCells` and any indoor cells accessible via building portals (`CheckBuildingTransit`). Both already exist as helpers. 4. **Shadow objects (the L.2d `[resolve-bldg]` path)** likely also need multi-cell awareness — `FindObjCollisions` only checks shadows keyed to the player's current cell. After A1.5, interior shadows are scoped to their `ParentCellId`, so multi-cell iteration automatically picks them up too. 5. **Testing strategy**: - Unit tests: synthetic two-cell fixture where wall lives in cell B and player is in cell A's vestibule. Assert collision fires. - Live capture: walk the Holtburg inn vestibule (`0xA9B40164`) and verify walls in `0xA9B40157` now block. 6. **Performance**: each cell query is ~50 µs. Multi-cell iteration visits ~3-7 cells in worst case. ~200-350 µs extra per resolve. At 30 Hz that's ~10 ms/sec. Acceptable. ### Risks - **R1**: shadow objects in cells visible from multiple positions may get tested multiple times in one frame. Need dedup via the existing `_entityToCells` map. - **R2**: cells in `cell_array` may have stale `CellPhysics` (loaded for rendering but not for physics). Guard with `cellPhysics?.BSP?.Root is not null`. - **R3**: the existing `BSPQuery.FindCollisions` mutates `Transition` state (SpherePath.CheckPos, CollisionInfo). Running it multiple times per frame requires either save/restore between cells or letting the first-hit's mutations stand (matching retail). ## Other pending items ### Phase A2 — PHSP inversion fix `BSPQuery.PolygonHitsSpherePrecise` at `BSPQuery.cs:117` has its early-return condition inverted vs retail's `polygon_hits_sphere_slow_but_sure` at `acclient_2013_pseudo_c.txt:322509-322517`. Ours bails when sphere is FAR from plane; retail bails when sphere is OVERLAPPING plane. The actual fix is one line, but it doesn't fix walkable synthesis on its own (because `AdjustSphereToPlane` still rejects tangent). It DOES affect wall-collision precision at the tangent boundary. Pair with A3 (synthesis removal) for the full benefit. ### Phase A3 — synthesis removal Delete `TryFindIndoorWalkablePlane` (TransitionTypes.cs:1294) and rely on the three retail CP retention mechanisms (Mechanisms A/B/C). The previous session (2026-05-20) tried this and reverted because multi-cell iteration was missing, so doorway transitions caused free-fall. With A1.7 + A4 in place, A3 should work. ### Lighting bugs - **Indoor lighting broken**: probably cell-light association or visibility culling for lights inside cells. - **Spotlight projection**: per-entity light direction transform. These are M7 polish, separate phase. Not blocking M2 ("kill a drudge"). ## How to start a fresh session Open a new Claude Code session **in the main acdream worktree** (`C:\Users\erikn\source\repos\acdream`, branch `main` at SHA `56d2b5e` or later). Then paste the block below: --- ``` Pick up the acdream collision-fix work from the 2026-05-21 session. 1. Read docs/research/2026-05-21-collision-fixes-shipped-handoff.md FIRST. It captures everything that shipped (4 fixes A1/A1.5/A1.6/A1.7 + a probe spike) and what's left (Phase A4 multi-cell iteration is the next major user-visible win). 2. All 9 commits from the previous session are merged into main (HEAD 56d2b5e). Build green, no regressions in the 1129-test baseline, four user-visible visual improvements verified live. Local main is ahead of origin/main (origin at 7034be9, an older commit); push only if explicitly desired. 3. **Set up isolation FIRST.** Use the superpowers:using-git-worktrees skill to create a fresh worktree branched from main for the A4 work. Do NOT work directly on main in the parent worktree. The previous session's worktree (claude/lucid-goldberg-1ba520) can be removed — its branch is identical to main now. 4. The next phase to design + ship is **A4 (multi-cell BSP iteration)**. Sketch in §"Phase A4" of the handoff. Reads retail's CTransition::check_other_cells (acclient_2013_pseudo_c.txt:272717-272798). Wires the existing CellTransit helpers (FindCellList, FindTransitCellsSphere, AddAllOutsideCells) into Transition.FindEnvCollisions so collision is queried against ALL cells the sphere overlaps, not just the one cell the player's center is in. 5. CLAUDE.md rules apply: - No workarounds. Retail-faithful. - Probe-first, design-second. Already have [indoor-bsp] + [cell-transit] + [cell-cache] probes available. - Use the superpowers:brainstorming skill before writing code. A4 is a real architectural change deserving its own spec. - Visual verification at the Holtburg inn (cell 0xA9B40164 vestibule) is the acceptance test — walls in cell 0xA9B40157 should block when the player is "in" 0xA9B40164 but their sphere extends into 0xA9B40157. 6. M2 ("kill a drudge") is the active milestone. Indoor walking robustness is on the M2 critical path because dungeons have drudges. A4 is the last big collision fix needed for M2's "walkable indoor space" demo target. 7. Launch command (same as last session): $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_CELL = "1" $env:ACDREAM_PROBE_CELL_CACHE = "1" dotnet build -c Debug dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "launch-a4.log" DO NOT set ACDREAM_PROBE_RESOLVE — it lagged the client last session (400k+ log lines at 30 Hz). State the milestone + chosen phase in your first action. ``` --- ## Anti-patterns from this session 1. **Don't enable `ACDREAM_PROBE_RESOLVE` for live captures.** It emits one line per resolve call at 30 Hz, producing 400k+ lines per session and making the client laggy enough that the user couldn't move. Use the lighter `[indoor-bsp]` + `[cell-transit]` probes instead. 2. **Don't assume "walk through wall" means PHSP inversion.** This session walked through that misconception twice. The actual cause was different bugs each time (doubled cylinders, interior shadow bleed, cell-id stuck, missing physics polys in vestibule cells). Always capture probe data before designing fixes. 3. **Don't merge A1.5's pattern (`cellScope: entity.ParentCellId`) without understanding that interior shadows might need MULTI-cell scope, not just their parent cell.** A1.5 fixed the obvious leak but introduced "stairs span cells" gaps. The real fix needs A4. 4. **Don't skip visual verification between fixes.** Each of A1, A1.5, A1.6, A1.7 was visually confirmed before moving to the next. The user reported what was still broken at each step, which guided the next fix. Without that loop, we'd have shipped a "fix" that broke something else. 5. **Don't try to fix lighting bugs in the same session as collision bugs.** Different domain (rendering, not physics). Defer to its own session. ## Code anchors ### This session's fixes (in commit order) - [`src/AcDream.Core/Physics/PhysicsDiagnostics.cs:246-277`](src/AcDream.Core/Physics/PhysicsDiagnostics.cs:246) — `ProbeWalkMissEnabled` flag. - [`src/AcDream.Core/Physics/WalkMissDiagnostic.cs`](src/AcDream.Core/Physics/WalkMissDiagnostic.cs) — pure-function aggregator (full file). - [`src/AcDream.Core/Physics/TransitionTypes.cs:1543-1586`](src/AcDream.Core/Physics/TransitionTypes.cs:1543) — `[walk-miss]` emission. - [`src/AcDream.Core/Physics/PhysicsDataCache.cs:222-238`](src/AcDream.Core/Physics/PhysicsDataCache.cs:222) — `[floor-polys]` emission. - [`src/AcDream.App/Rendering/GameWindow.cs:5830-5839`](src/AcDream.App/Rendering/GameWindow.cs:5830) — `_isLandblockStab` flag (A1). - [`src/AcDream.App/Rendering/GameWindow.cs:6062-6064`](src/AcDream.App/Rendering/GameWindow.cs:6062) — mesh-AABB-fallback gate (A1). - [`src/AcDream.Core/Physics/ShadowObjectRegistry.cs:34-92`](src/AcDream.Core/Physics/ShadowObjectRegistry.cs:34) — `cellScope` parameter (A1.5). - [`src/AcDream.App/Rendering/GameWindow.cs`](src/AcDream.App/Rendering/GameWindow.cs) — 5 call sites pass `entity.ParentCellId ?? 0u` (A1.5). - [`src/AcDream.App/Rendering/GameWindow.cs:5922-5933`](src/AcDream.App/Rendering/GameWindow.cs:5922) — `setup is not null && !_isLandblockStab` gate (A1.6). - [`src/AcDream.Core/Physics/PhysicsEngine.cs:259-289`](src/AcDream.Core/Physics/PhysicsEngine.cs:259) — `PointInsideCellBsp` fall-through (A1.7). ### What A4 will touch - [`src/AcDream.Core/Physics/TransitionTypes.cs:1407-1559`](src/AcDream.Core/Physics/TransitionTypes.cs:1407) — `FindEnvCollisions` (extend to iterate cell_array). - [`src/AcDream.Core/Physics/CellTransit.cs`](src/AcDream.Core/Physics/CellTransit.cs) — already has the helpers; may need a new `EnumerateCells` variant that returns the set rather than picking one. - [`src/AcDream.Core/Physics/PhysicsEngine.cs`](src/AcDream.Core/Physics/PhysicsEngine.cs) — `FindObjCollisions` may need similar treatment for shadow objects. ## Retail decomp anchors - `acclient_2013_pseudo_c.txt:272717-272798` — `CTransition::check_other_cells` (A4 oracle). - `:272565-272582` — `validate_transition` Mechanism B (LKCP proximity). - `:273242-273340` — `transitional_insert` Mechanism C (step-down probe). - `:322032-322077` — `CPolygon::adjust_sphere_to_plane`. - `:322403-322500` — `CPolygon::polygon_hits_sphere`. - `:322504-322593` — `CPolygon::polygon_hits_sphere_slow_but_sure` (A2 oracle — inversion). - `:322974-322993` — `CPolygon::pos_hits_sphere` (front-face culling). - `:323725-323939` — `BSPTREE::find_collisions` (full 6-path dispatcher). - `:326211-326242` — `BSPNODE::find_walkable`. - `:326706-326727` — `BSPLEAF::sphere_intersects_poly`. - `:326793-326816` — `BSPLEAF::find_walkable`. ## Probe + diagnostic reference | Env var | Volume | When to use | |---|---|---| | `ACDREAM_PROBE_INDOOR_BSP` | Low (indoor cells only) | Wall walk-through investigations. Logs `cell`, `wpos`, `lpos`, `result`, hit poly. | | `ACDREAM_PROBE_CELL` | Very low (cell change events) | Cell-tracking issues. Logs old → new cell + position. | | `ACDREAM_PROBE_CELL_CACHE` | One-shot per cell load | When you need cell BSP poly counts + bsphere. Identifies "vestibule" cells with sparse geometry. | | `ACDREAM_PROBE_WALK_MISS` | High (per-frame MISS) | Walkable synthesis investigations (Phase A2/A3 work). | | `ACDREAM_PROBE_BUILDING` | Medium | Building-shadow attribution. Multi-line `[resolve-bldg]` per hit. | | `ACDREAM_PROBE_RESOLVE` | **VERY HIGH — DO NOT USE FOR LIVE PLAY** | Per-resolve attribution. 30 Hz × per-entity = 400k+ lines/session. Lagged the client this session. | | `ACDREAM_PROBE_CONTACT_PLANE` | Medium | CP retention investigations. Bug B from 2026-05-20 era. | ### Log analysis recipe ```powershell # 1. Convert UTF-16LE to UTF-8 for grep: Get-Content launch.log -Encoding Unicode | Out-File launch.utf8.log -Encoding utf8 # 2. Quick counts: grep -c '\[indoor-bsp\]' launch.utf8.log grep -c '\[cell-transit\]' launch.utf8.log # 3. Per-cell hit rate: grep '\[indoor-bsp\] cell=0xA9B40164' launch.utf8.log | grep -oE 'result=[A-Za-z]+' | sort | uniq -c ``` ## What this is NOT This is **NOT** a complete fix for indoor walking. Walls walk-through-able remain in cells where the PhysicsBSP has sparse coverage (vestibule cells). A4 closes that gap by querying multiple cells per frame — which is exactly what retail does. This is **NOT** related to the PHSP inversion (A2). A2 fixes per-poly overlap math precision at the tangent boundary. A4 fixes which cells get queried. They're orthogonal. This is **NOT** related to the lighting bugs the user reported. Those are rendering-side; ignore in any collision work. ## References - [`docs/research/2026-05-21-walk-miss-capture-findings.md`](2026-05-21-walk-miss-capture-findings.md) — probe spike findings. - [`docs/superpowers/specs/2026-05-21-indoor-walk-miss-probe-design.md`](../superpowers/specs/2026-05-21-indoor-walk-miss-probe-design.md) — probe spec. - [`docs/superpowers/specs/2026-05-21-cylinder-fallback-dedup-design.md`](../superpowers/specs/2026-05-21-cylinder-fallback-dedup-design.md) — A1 spec. - [`docs/research/2026-05-20-indoor-walking-bug-a-handoff.md`](2026-05-20-indoor-walking-bug-a-handoff.md) — previous-session handoff (Bug B shipped, Bug A reverted). - [`docs/research/2026-05-21-indoor-walking-doorway-investigation-prompt.md`](2026-05-21-indoor-walking-doorway-investigation-prompt.md) — the prompt that started this session.