# Indoor walking Phase 1 — BSP cluster (Cluster A) — handoff (2026-05-19)

**Date:** 2026-05-19.
**Branch:** `claude/competent-robinson-dec1f4` (commits land here; merge to main handled by controller).
**Predecessor:** Indoor lighting + rendering Phase 2 (fix) — floors now render in Holtburg Inn. Nine pre-existing indoor bugs surfaced the moment floors were visible; this cluster addresses the collision/interaction subset (#84, #85, #86) and adds diagnostic infrastructure for the follow-up portal-traversal phase.
**Plan:** [`docs/superpowers/plans/2026-05-19-indoor-walking-phase1-bsp-cluster.md`](../superpowers/plans/2026-05-19-indoor-walking-phase1-bsp-cluster.md).

---

## TL;DR

Cluster A shipped **partially**. Three of the five planned phases (A, B, D)
produced real behavior changes; two (C — obstacle audit — and E — cell-cache
diagnostics) are diagnostic/research phases. The cluster's investigation
confirmed that the wall-collision failures (#84, #85) all root in one cause:
the player's `CellId` is never promoted to an indoor cell during normal
walking, so the indoor-BSP collision branch in `TransitionTypes.FindEnvCollisions`
never fires. Phase D implemented an AABB-containment shortcut that resolves
the specific "spawn inside a building and be stuck above the floor" case but
proved too tight to keep `CellId` promoted through threshold/doorway cells
during normal outdoor→indoor entry.

**#86** (click selection penetrates walls) is **fully closed** — a clean,
self-contained fix in `WorldPicker`.

**#84** is **partially closed** — the spawn-in-building symptom is gone; the
remaining wall-collision symptom during normal walking is tracked under the
new **#87**.

**#85** remains **open**; its root cause is confirmed identical to #84's
remaining symptom and is also tracked under #87.

**#87** (indoor portal-based cell tracking) is **filed** and ready for the
follow-up phase.

---

## Commits

| # | SHA | Subject | Phase |
|---|---|---|---|
| 1 | `18a2e28` | `docs(plan): implementation plan written` | Plan doc |
| 2 | `27d7de1` | `feat(physics): Cluster A — indoor BSP collision probe` | Phase A |
| 3 | `3764867` | `fix(picker): Cluster A #86 — cell-BSP ray occlusion in WorldPicker` | Phase B |
| 4 | `4e308d5` | `test(picker): Cluster A #86 — screen-rect cell-occlusion tests` | Phase B follow-up |
| 5 | `c19d6fb` | `fix(physics): Cluster A #84 + #85 — indoor cell tracking` | Phase D |
| 6 | `fda6af7` | `feat(physics): Cluster A — cell-cache diagnostic` | Phase E (1st) |
| 7 | `1f11ba9` | `feat(diag): Cluster A — extend [cell-cache] with AABB + bsphere + recursive poly count` | Phase E (2nd) |

**Build:** clean on all commits.
**Tests:** `dotnet test` shows the same 8 pre-existing failures in
`AcDream.Core.Tests` (MotionInterpreter / BSPStepUp / etc., unchanged across
the entire cluster). All targeted test projects green. Phase B follow-up
adds screen-rect occlusion tests; Phase D adds `RegisterCellStructForTest`
helper used by caller-side tests.

---

## What shipped

### Phase A — `[indoor-bsp]` probe

New `PhysicsDiagnostics.ProbeIndoorBspEnabled` toggle (env var
`ACDREAM_PROBE_INDOOR_BSP` + DebugPanel checkbox under
`ACDREAM_DEVTOOLS=1`). When enabled, logs one `[indoor-bsp]` line each time
`TransitionTypes.FindEnvCollisions` takes the indoor-cell branch —
i.e., when `CellId` is an EnvCell id and the BSP contains physics polys. The
probe serves as a presence detector: if `[indoor-bsp]` never fires during
indoor walking, the BSP is not being consulted at all.

### Phase B — WorldPicker cell-BSP ray occlusion (closes #86)

New `CellBspRayOccluder` class (in `src/AcDream.App/Rendering/`) computes
`NearestWallT`: the smallest ray parameter at which the pick ray intersects
any cached EnvCell BSP polygon. Both `WorldPicker.Pick` overloads now accept
an optional `cellOccluder` callback and filter out any hit candidate whose
ray T exceeds `NearestWallT`. The occluder is wired from `GameWindow` using
the `PhysicsDataCache` cell structs that Phase D also extends.

Before Phase B: clicking through a wall from the outside selected NPCs/items
inside the building — `WorldPicker.BuildRay + Pick` (Phase B.4b) tested only
entity AABBs and scenery BSPs, not EnvCell BSP geometry.

After Phase B: entities behind the nearest wall from the camera's perspective
are filtered out of the candidate set. Screen-rect unit tests verify the
filter across hit/miss/occlusion scenarios.

### Phase D — AABB containment for indoor CellId (partial #84 fix)

`PhysicsEngine.ResolveOutdoorCellId` is extended with an indoor
cell-containment scan. After resolving the outdoor cell, the method checks
whether the player's world position falls inside any cached `CellPhysics`
AABB; if so, `CellId` is promoted to that EnvCell. This enables the
`FindEnvCollisions` indoor-BSP branch.

New `PhysicsDataCache.TryFindContainingCell(worldPos)` does the AABB scan.
New `CellPhysics.WorldAabb` caches the cell-local AABB in world space on
first call (transforms the BSP bounding sphere's local AABB by the cell
origin). New `RegisterCellStructForTest` helper allows unit test callers to
populate the cache directly.

Also fixes the L.2e bare-low-byte preservation bug: `ResolveOutdoorCellId`
was silently truncating the player CellId to the low 16 bits; the fix
preserves the full 32-bit value.

**What this solved:** player spawning inside a building (e.g., logging in
from a position inside Holtburg cottage) no longer sees `walkable=False` for
hundreds of resolves with world Z=94.000. Phase D promotes CellId to the
indoor cell, the floor's BSP polys are found, the player can move.

**What this did NOT solve:** the `[indoor-bsp]` probe fires only 6 times
during an entire indoor walking session (all mid-jump, when the body happens
to be at a height that falls inside a room AABB). During normal walking on
the floor, the player's world Z is at the AABB floor level or lower —
outside the AABB for threshold/doorway cells that have only a 0.2 m Z range.
See Phase E evidence below.

### Phase E — Cell-cache diagnostic infrastructure

Two commits add `[cell-cache]` log output (env var
`ACDREAM_PROBE_CELL_CACHE`, also DebugPanel). For each EnvCell in the
physics cache, the probe logs:

```
[cell-cache] id=0xA9B40143 physicsPolyCount=14 bspTotalLeafPolys=14
  bspUnmatchedIds=0 aabbMin=(-11.60,-1.60,0.00) aabbMax=(-6.20,7.60,2.80)
  bspOrigin=(0.00,0.00,0.00) bspRadius=9.97
```

The extended second commit adds `bspTotalLeafPolys`, `bspUnmatchedIds`,
`bspOrigin`, and `bspRadius` fields to give a complete picture of cell
geometry from the physics cache perspective. This infrastructure stays in
place as scaffolding for the portal-traversal phase.

---

## Issue status after Cluster A

| Issue | Status | Notes |
|---|---|---|
| #84 Blocked by air indoors | OPEN (partial) | Spawn-in-building variant resolved by Phase D. Threshold/doorway wall-blocking remains open under #87. |
| #85 Pass through walls outside→in | OPEN | Root cause confirmed as same as #84 remaining symptom. See #87. |
| #86 Click selection penetrates walls | **CLOSED** | Phase B. `WorldPicker.Pick` + `CellBspRayOccluder`. |
| #87 Indoor portal-based cell tracking | OPEN (new) | Filed 2026-05-19. Retail-faithful fix via `CObjMaint::HandleObjectEnterCell`. |

---

## Probe evidence — log file findings

### `launch-cluster-a-capture.log`

Initial probe run with `ACDREAM_PROBE_INDOOR_BSP=1`. Result: **zero
`[indoor-bsp]` lines** during outdoor walking and during approach to the
Holtburg cottage doorway. This was the first confirmation that the indoor-BSP
branch was entirely gated out. The player's CellId remained an outdoor cell
for all movement.

### `launch-cluster-a-verify.log`

Post-Phase-D run. Observed `[indoor-bsp]` lines **only during jump frames**
(6 total). When the player jumped inside the cottage, the body briefly rose
to a height inside the room AABB, CellId promoted to `0xA9B40143`, and the
indoor-BSP branch fired. On landing, the body returned to floor level, fell
outside the AABB, and CellId reverted to the outdoor cell. Confirmed that
AABB containment works for the room cell when the player is mid-air, but
fails at floor level.

### `launch-cluster-a-cache-diag2.log`

First `[cell-cache]` probe run (Phase E first commit). Showed all cached
cells with their physics poly counts and local AABBs. Confirmed 14 physics
polys in cell `0xA9B40143` (the room), indicating BSP geometry is present
and complete. Identified cell `0xA9B40146` as a 4-poly threshold cell.

### `launch-cluster-a-cache-diag3.log`

Extended `[cell-cache]` probe run (Phase E second commit). Full data:

```
[cell-cache] id=0xA9B40143 physicsPolyCount=14 bspTotalLeafPolys=14
  bspUnmatchedIds=0 aabbMin=(-11.60,-1.60,0.00) aabbMax=(-6.20,7.60,2.80)
  bspOrigin=(0.00,0.00,0.00) bspRadius=9.97
```
Room cell: 2.80 m AABB height — works for mid-air player.

```
[cell-cache] id=0xA9B40146 physicsPolyCount=4
  aabbMin=(-11.60,2.80,-0.20) aabbMax=(-10.00,7.60,0.00)
  bspRadius=2.3
```
Threshold/doorway cell: 0.20 m AABB Z range (from -0.20 to 0.00). A standing
player at local Z=0.46 m is outside this AABB. **This is why AABB containment
fails for normal walking through doorways.**

Key conclusion: the geometry is correct and complete (14/14 polys match between
physics cache and BSP leaf count). The problem is purely in the cell-ownership
tracking mechanism, not the collision data itself.

---

## Diagnostic infrastructure remaining in place

Both probes stay committed and wired. They serve as scaffolding for the
portal-traversal follow-up phase:

- **`ACDREAM_PROBE_INDOOR_BSP=1`** / DebugPanel "Indoor BSP probe": logs one
  `[indoor-bsp]` line each time `FindEnvCollisions` takes the indoor-cell
  branch. After portal traversal is implemented, this probe should fire
  consistently whenever the player is indoors.

- **`ACDREAM_PROBE_CELL_CACHE=1`** / DebugPanel "Cell cache probe": dumps all
  cached EnvCell physics data (poly counts, BSP bounding sphere, AABB,
  unmatched ID count). Useful for verifying that cell structs load correctly
  and that portal connectivity data is present.

Both are gated behind `PhysicsDiagnostics` static class (existing pattern
from L.2a).

---

## Follow-up items for the portal-traversal phase

**1. Implement portal-based indoor cell tracking (issue #87).**
Replace `PhysicsDataCache.TryFindContainingCell` AABB containment with retail's
`CObjMaint::HandleObjectEnterCell` portal traversal. When the player crosses
a cell portal boundary, `CellId` propagates through `CEnvCell` portal
connectivity data. PDB symbols in `docs/research/named-retail/acclient_2013_pseudo_c.txt`
and struct definitions in `docs/research/named-retail/acclient.h` lines
31715-31726 (`CCellStructure` shape). The retail reference implementation
is the right oracle — do not guess at the traversal algorithm.

**2. Audit-trail note: add retail PDB symbol citations to `TryFindContainingCell`.**
The current implementation in `src/AcDream.Core/Physics/PhysicsDataCache.cs`
~line 261 is documented as a shortcut. The follow-up phase should add
the PDB symbol citation (e.g., `// retail: CObjMaint::HandleObjectEnterCell
// docs/research/named-retail/acclient_2013_pseudo_c.txt:XXXXX`)
per the Phase D code-review I1 note, so future readers know this is intentionally
replacing an interim implementation.

**3. Consider renaming `ResolveOutdoorCellId` → `ResolveCellId`.**
The method now handles both outdoor and indoor cell resolution. The rename
is low-risk (one call site in `PhysicsEngine.cs`) and would reduce the
cognitive overhead for the next phase's author. Noted as a Phase D code-review
M2 suggestion — do it in the same commit as the portal-traversal implementation
to keep the rename and the semantic change together.

---

## State at handoff

- **Branch:** `claude/competent-robinson-dec1f4`, 7 commits of implementation/test/diagnostic work.
- **Build state:** `dotnet build -c Debug` clean.
- **Tests:** 8 pre-existing failures unchanged (MotionInterpreter / BSPStepUp baseline). All new tests green.
- **Issues:** #86 CLOSED; #84 PARTIAL; #85 OPEN; #87 OPEN (new).
- **Diagnostic probes:** `[indoor-bsp]` + `[cell-cache]` active and wired.
- **Next:** portal-based indoor cell tracking (#87) or M2 critical path — Claude's choice per work-order autonomy.
