docs(physics): handoff for 2026-05-21 collision-fix session

Captures everything that shipped in the session — A1, A1.5, A1.6,
A1.7 plus the walk-miss probe spike — and what's still open:

- A4 (multi-cell BSP iteration) — the next big architectural fix,
  closes the "walls walk-through-able in vestibule cells" gap
- A2 (PHSP inversion) — small fix, but only meaningful paired with A3
- A3 (synthesis removal) — needs A4 in place first to avoid
  reverting back to Bug A's free-fall regression
- Lighting bugs (indoor lighting + spotlight projection) — M7 polish,
  separate session

Includes per-fix commit SHAs, code anchors, retail decomp anchors,
probe + launch reference, anti-patterns, and a fresh-session pickup
prompt for boxing into Claude Code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-20 14:42:47 +02:00
parent 4679134d66
commit 56d2b5e4a1

View file

@ -0,0 +1,390 @@
# Collision fixes — session 2026-05-21 shipped handoff
**Branch:** `claude/lucid-goldberg-1ba520`
**Worktree:** `C:\Users\erikn\source\repos\acdream\.claude\worktrees\lucid-goldberg-1ba520`
**Commits ahead of main:** 8 (probe spike + 4 fixes + docs)
## 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
Copy the block below into a new Claude Code session in the acdream worktree:
---
```
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. Current branch state: claude/lucid-goldberg-1ba520 is 8 commits
ahead of main. All 4 fixes have visual verification + no regression
in the 1129-test baseline. Ready to merge or build A4 on top.
3. 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.
4. 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.
5. 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.
6. Launch command (same as this 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 this
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.