docs(phase): Indoor walking Phase 2 — Portal-based cell tracking shipped

Closes ISSUES.md #87 + #85 + the remaining wall-pass-through portion of
#84 (fully closes #84). Portal-graph cell traversal replaces Phase D's
AABB containment. Walking through doors promotes/demotes CellId correctly
via portal traversal; walls block from inside indoor cells; indoor walkable
plane is synthesized from the cell's floor poly so the resolver tracks
walkability correctly during indoor movement.

Files two new issues: #88 (indoor static objects vibrate — pre-existing,
spotted during Phase 2 testing) and #89 (BSPQuery.SphereIntersectsCellBsp
— follow-up to make CheckBuildingTransit retail-faithful; currently uses
radius-less PointInsideCellBsp as a documented approximation).

ISSUES.md: #87, #85, #84 moved to DONE. #88 + #89 filed.
Roadmap: Indoor walking Phase 2 added to shipped table.
CLAUDE.md: recent-phase paragraph updated to reflect Phase 2 shipped.
New handoff: docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-19 19:31:22 +02:00
parent eb0f772f0f
commit a9c74d153a
4 changed files with 391 additions and 88 deletions

View file

@ -776,35 +776,32 @@ acdream's plan lives in two files committed to the repo:
acceptance criteria. Do not drift from the spec without explicit user
approval.
**Indoor walking Phase 1 — BSP cluster (Cluster A) partially shipped
2026-05-19.** Seven commits across five phases:
- `18a2e28` — implementation plan
- `27d7de1` — Phase A: `[indoor-bsp]` probe + `ProbeIndoorBspEnabled` toggle
- `3764867` — Phase B: `CellBspRayOccluder` in `WorldPicker.Pick` (**closes #86**)
- `4e308d5` — Phase B follow-up: screen-rect cell-occlusion tests
- `c19d6fb` — Phase D: AABB containment for indoor CellId promotion + L.2e bare-low-byte fix (partial #84 fix)
- `fda6af7` — Phase E first commit: `[cell-cache]` probe
- `1f11ba9` — Phase E second commit: extended `[cell-cache]` with AABB + bsphere + poly counts
**Indoor walking Phase 2 — Portal-based cell tracking shipped
2026-05-19.** Six commits:
- `1969c55` — CellBSP + Portals wired into CellPhysics (`PortalInfo` struct, `VisibleCellIds`)
- `aad6976``CellTransit.FindCellList` + `FindTransitCellsSphere` + `AddAllOutsideCells`; `ResolveCellId` rename
- `069534a``BuildingPhysics` + `CheckBuildingTransit` for outdoor→indoor entry via `BldPortalInfo`
- `702b30a` — code-review polish (DRY cell-id derivation, `PortalFlags.ExactMatch` enum, docs)
- `3ffe1e4` — critical fix: pass foot-sphere center (`GlobalSphere[0].Origin`) not `CheckPos` to `ResolveCellId`
- `eb0f772``TryFindIndoorWalkablePlane` synthesizes indoor walkable plane from cell floor poly
**#86** (click selection penetrates walls) — **CLOSED.** `WorldPicker.Pick`
consults `CellBspRayOccluder.NearestWallT`; entities behind walls are filtered.
**#84** (blocked by air indoors) — **PARTIAL.** The "spawn-in-building stuck
above floor" variant is resolved (Phase D promotes CellId to the indoor cell
on spawn-in). The remaining "walls don't block from inside during normal
walking" symptom is the same root cause as #85 — AABB containment is too
tight for threshold/doorway cells (Z range ~0.2 m, player stands at ~0.46 m)
to keep CellId promoted. Both are tracked under new issue **#87**.
**#85** (pass through walls outside→in) — **OPEN.** Root cause confirmed as
same as #84 remaining symptom — CellId drifts back to outdoor cell, indoor
BSP never fires. See #87.
**#87** (indoor portal-based cell tracking) — **FILED.** Retail-faithful fix
via `CObjMaint::HandleObjectEnterCell` + `CEnvCell` portal connectivity.
Diagnostic infrastructure from Cluster A (`[indoor-bsp]` + `[cell-cache]`
probes, both runtime-toggleable) stays in place as scaffolding for the
follow-up phase. Handoff: [`docs/research/2026-05-19-cluster-a-shipped-handoff.md`](docs/research/2026-05-19-cluster-a-shipped-handoff.md).
**#86** (click selection penetrates walls) — **CLOSED** (Phase 1 Cluster A).
**#84** (blocked by air indoors) — **FULLY CLOSED.** Spawn-in-building variant
closed by Phase 1 (Phase D AABB containment). Wall-block-from-inside variant
closed by Phase 2 (portal-graph traversal).
**#85** (pass through walls outside→in) — **CLOSED** by Phase 2.
`CheckBuildingTransit` promotes CellId via the building-shell portal graph
on outdoor→indoor entry; indoor-BSP collision fires from both sides.
**#87** (indoor portal-based cell tracking) — **CLOSED** by Phase 2.
**#88** (indoor static objects vibrate) — **FILED** (pre-existing, Medium).
**#89** (port `BSPQuery.SphereIntersectsCellBsp`) — **FILED** (Low, documented
approximation in `CheckBuildingTransit`).
Diagnostic infrastructure: `[indoor-bsp]`, `[cell-cache]`, `[cell-transit]`,
`[check-bldg]` probes all stay in place.
Handoff: [`docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md`](docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md).
Phase 1 handoff: [`docs/research/2026-05-19-cluster-a-shipped-handoff.md`](docs/research/2026-05-19-cluster-a-shipped-handoff.md).
**Next phase is Claude's choice** per work-order autonomy. Candidates:
indoor portal-based cell tracking (#87, completes the indoor walking story);
M2 critical path (F.2 / F.3 / F.5a / L.1c / L.1b — kill-a-drudge demo);
or the pre-existing "next phase candidates" list below.

View file

@ -237,9 +237,10 @@ to the second floor without getting stuck.
---
## #84 — Blocked by air indoors
## #84 [DONE 2026-05-19] Blocked by air indoors
**Status:** OPEN (partial fix 2026-05-19)
**Status:** DONE
**Closed:** 2026-05-19
**Severity:** HIGH (blocks indoor navigation)
**Filed:** 2026-05-19
**Component:** physics, collision
@ -274,58 +275,68 @@ enables the `FindEnvCollisions` indoor-BSP branch. This resolved the
"spawn in building and be stuck above the floor" variant of #84
player's CellId now promotes to the interior cell on spawn-in, the floor
is walkable, and the player can move freely. The "invisible air obstacle"
symptom for rooms the player walks INTO from outside is now superseded by
the root cause in #87 (AABB containment is too tight for threshold/
doorway cells to keep CellId promoted during normal walking). That
remaining symptom will be resolved by the portal-based cell tracking
fix.
symptom for rooms the player walks INTO from outside was tracked under #87
and required portal-based cell tracking.
**Resolution (2026-05-19 full · `1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772`):**
Indoor walking Phase 2 replaced AABB containment with portal-graph cell traversal
(`CellTransit.FindCellList` + `CheckBuildingTransit`). CellId now promotes to indoor
cells via portals and remains promoted during normal walking through doorways. Indoor
cell-BSP collision fires consistently. Indoor walkable plane synthesized from floor
poly (`TryFindIndoorWalkablePlane`) so the resolver tracks walkability correctly when
the player is standing on an indoor floor. User visually verified at Holtburg cottage:
walls block from inside, multi-room navigation works, walking outdoors through a door
works. Issue fully closed.
---
## #85 — Pass through walls from outside→in
## #85 [DONE 2026-05-19 · 1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772] Pass through walls from outside→in
**Status:** OPEN
**Severity:** HIGH (gameplay-breaking)
**Status:** DONE
**Closed:** 2026-05-19
**Commits:** `1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772`
**Filed:** 2026-05-19
**Component:** physics, collision
**Description:** Approaching a building from the outside, the player
**Resolution (2026-05-19 · Indoor walking Phase 2):** The root cause (CellId never promoted
to the indoor cell during outdoor→indoor walking) was resolved by portal-graph cell
traversal in `CellTransit.CheckBuildingTransit`. Once `CellId` promotes to the indoor
cell, the indoor-BSP collision branch in `FindEnvCollisions` fires for approaches from
both inside and outside. User visually verified walls block from outside (player must
use the door portal to enter). See #87 and handoff:
[`docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md`](2026-05-19-indoor-walking-phase2-shipped-handoff.md).
**Original description:** Approaching a building from the outside, the player
can walk THROUGH walls into the interior — one-directional wall
collision. From the inside trying to exit, the wall does block.
**Root cause / status:** Cell BSP polygons likely have one-sided
normals (front-facing only). Approach from the inside hits the front;
approach from the outside hits the back which BSP traversal treats as
"behind the plane" → no collision. Retail handles this via two-sided
collision polys or per-poly back-face handling.
**Files:**
- `src/AcDream.Core/Physics/BSPQuery.cs`
- `src/AcDream.Core/Physics/TransitionTypes.cs` (`FindObjCollisions` cell
branch).
**Acceptance:** Walking into an inn wall from outside collides; player
must enter via the door portal.
**Status update (2026-05-19):** The root cause is now pinned as the
same failure as #84's remaining symptom — `CellId` isn't promoted to
the indoor cell during normal outdoor→indoor walking because AABB
containment is too tight for threshold/doorway cells. Without CellId
in the indoor cell, the indoor-BSP collision branch in
`FindEnvCollisions` never fires regardless of approach direction.
See new issue #87 (portal-based indoor cell tracking) for the
retail-faithful fix.
The root cause was pinned (Cluster A 2026-05-19) as the same failure as
#84's remaining symptom — `CellId` wasn't promoted to the indoor cell
during normal outdoor→indoor walking because AABB containment was too
tight for threshold/doorway cells. Without CellId in the indoor cell,
the indoor-BSP collision branch in `FindEnvCollisions` never fired
regardless of approach direction.
---
## #87 — Indoor cell tracking uses AABB containment instead of portal traversal
## #87 — [DONE 2026-05-19 · 1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772] Indoor cell tracking uses AABB containment instead of portal traversal
**Status:** OPEN
**Severity:** HIGH
**Status:** DONE
**Closed:** 2026-05-19
**Commits:** `1969c55, aad6976, 069534a, 702b30a, 3ffe1e4, eb0f772`
**Filed:** 2026-05-19
**Component:** physics
**Description:** `PhysicsDataCache.TryFindContainingCell` promotes the
**Resolution (2026-05-19 · Indoor walking Phase 2):** Portal-graph cell traversal
(`CellTransit.FindCellList` + `CheckBuildingTransit`) replaced the AABB containment
shortcut. Player CellId now correctly promotes to indoor cells via portals;
indoor cell-BSP collision branch fires consistently; walls block from inside.
Outdoor→indoor entry via `BuildingPhysics` + `BldPortalInfo` (`CheckBuildingTransit`)
wires the building-shell portal graph. Indoor walkable plane synthesized from the
cell's floor poly so the resolver tracks walkability during indoor movement (`TryFindIndoorWalkablePlane`).
See handoff: [`docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md`](2026-05-19-indoor-walking-phase2-shipped-handoff.md).
**Original description:** `PhysicsDataCache.TryFindContainingCell` promotes the
player's `CellId` to an indoor EnvCell when their world position falls
inside any cached cell's local AABB. This is too tight to keep `CellId`
promoted to an indoor cell during normal walking. Threshold/doorway cells
@ -338,34 +349,44 @@ physics is unreliable. The retail fix is portal-based cell traversal —
when the player crosses a cell portal boundary, the cell ownership
propagates through portal connectivity data in `CEnvCell`.
**Evidence:** `launch-cluster-a-cache-diag3.log` (Cluster A Phase E
capture). Cell `0xA9B40143` (real room) has
`physicsPolyCount=14 bspTotalLeafPolys=14 bspUnmatchedIds=0
aabbMin=(-11.60,-1.60,0.00) aabbMax=(-6.20,7.60,2.80)` — geometry is
complete and the AABB spans 2.8 m height, which works. Cell `0xA9B40146`
(threshold/doorway) has `physicsPolyCount=4
aabbMin=(-11.60,2.80,-0.20) aabbMax=(-10.00,7.60,0.00)` — Z range is
only 0.2 m; a standing player is always outside it. Only 6 `[indoor-bsp]`
lines fired across an entire indoor walking session (all during mid-jump
frames when the player was briefly inside the room AABB at jump height).
---
## #88 — Indoor static objects vibrate (bookshelves, open furnaces)
**Status:** OPEN
**Severity:** MEDIUM (visual jitter; doesn't block gameplay)
**Filed:** 2026-05-19
**Component:** rendering, animation
**Description:** Static objects inside cells (bookshelves, open furnaces, possibly other interior props) show per-frame transform jitter / vibration. Pre-existing (user noticed before Phase 2 shipped). Likely candidates:
1. `EntityScriptActivator.OnCreate/OnRemove` firing repeatedly as the player's CellId promotes/demotes near cell boundaries (less likely after Phase 2's portal-based tracking — but worth investigating).
2. Per-part transforms for cell-static `WorldEntity` instances getting recomputed each frame with floating-point drift.
3. Particle-emitter offsets accumulating instead of resetting.
**Files to investigate:**
- `src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs` — OnCreate/OnRemove call patterns
- `src/AcDream.App/Rendering/GpuWorldState.cs` — entity transform updates per frame
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` — per-batch transform composition
**Acceptance:** Indoor static objects render stable (no per-frame jitter).
---
## #89 — Port BSPQuery.SphereIntersectsCellBsp for retail-faithful CheckBuildingTransit
**Status:** OPEN
**Severity:** LOW (Phase 2 ships with a documented approximation)
**Filed:** 2026-05-19
**Component:** physics
**Description:** Retail's `CEnvCell::check_building_transit` uses `CCellStruct::sphere_intersects_cell` — a radius-aware sphere-vs-BSP test that returns Inside/Crossing/Outside. Phase 2's `CellTransit.CheckBuildingTransit` uses `BSPQuery.PointInsideCellBsp` (radius-less, tests only the sphere CENTER). Practical effect: outdoor→indoor entry fires ~sphereRadius (~0.48m) deeper into the doorway than retail. The sphereRadius parameter is plumbed through but currently unused.
**Files:**
- `src/AcDream.Core/Physics/PhysicsDataCache.cs` (`TryFindContainingCell`,
approximately line 261)
- `src/AcDream.Core/Physics/PhysicsEngine.cs` (`ResolveOutdoorCellId`,
approximately line 238)
- `src/AcDream.Core/Physics/TransitionTypes.cs` (`FindEnvCollisions` cell
branch, approximately line 1188)
- `src/AcDream.Core/Physics/CellTransit.cs::CheckBuildingTransit` (line ~162)
- `src/AcDream.Core/Physics/BSPQuery.cs::PointInsideCellBsp` (line ~940) — existing point test to model the new sphere variant after
**Retail reference:** PDB symbols `CObjMaint::HandleObjectEnterCell` and
`CEnvCell` portal data. See `docs/research/named-retail/acclient.h` lines
31715-31726 for `CCellStructure` shape; `acclient_2013_pseudo_c.txt` for
the implementations.
**Acceptance:** Player walking from outside the Holtburg cottage into the
interior crosses portals and `CellId` updates accordingly; walls block
from both inside and outside; the `[indoor-bsp]` probe fires consistently
during indoor walking (not just during mid-jump frames).
**Acceptance:** `CellTransit.CheckBuildingTransit` calls a new `BSPQuery.SphereIntersectsCellBsp(node, sphereCenter, sphereRadius)` that returns `Inside`/`Crossing`/`Outside`. Entry timing matches retail visually at the Holtburg cottage door.
---

View file

@ -72,6 +72,7 @@
| Indoor lighting + rendering — Phase 2 (fix) | Three-component diagnostic-driven fix for missing-floor bug. Component 1: `WbMeshAdapter` captures the `Task<ObjectMeshData?>` from `PrepareMeshDataAsync` and attaches a `ContinueWith` for EnvCell ids — surfaces faulted-task exceptions + clean-null returns. Component 2: replaced `NullLogger<ObjectMeshManager>` with a Console-backed `ConsoleErrorLogger<T>` so WB's intentional `_logger.LogError(ex, ...)` at the swallow site at `ObjectMeshManager.cs:589` writes `[wb-error]` lines. **Root cause definitively identified in one capture: `ArgumentOutOfRangeException` from `DatReaderWriter.Setup.Unpack` at WB's `PrepareEnvCellMeshData` line 1223 — `TryGet<Setup>(stab.Id, ...)` was called blindly on every `envCell.StaticObjects` id without checking the Setup-prefix bit. GfxObj-typed stabs (0x01xxxxxx) caused mid-deserialization throws, bubbling up to PrepareMeshData's outer catch which silently returned null. Entire cell upload failed, room mesh never reached `_renderData`.** Component 3 fix: one-line type-check guard `(stab.Id & 0xFF000000u) == 0x02000000u && _dats.Portal.TryGet<Setup>(stab.Id, out var stabSetup)`. Committed to WB submodule on branch `acdream-fix-floor-rendering` at SHA `34460c4` — needs submodule pointer advance at merge time. **Verification: 0 [wb-error] (was 385), 0 NULL_RESULT (was 55), Holtburg 123/123 cells complete (was 97/123). User visually confirmed floors render in Holtburg Inn.** Surfaced 9 pre-existing indoor bugs (see-through floor, indoor collision, stairs, walls, click-thru, indoor lighting artifacts, atmospheric-lighting-on-stabs, slope terrain lighting) — all filed in `docs/ISSUES.md` for follow-up phases. Cause report: [`docs/research/2026-05-19-indoor-cell-rendering-cause.md`](../research/2026-05-19-indoor-cell-rendering-cause.md). Verification: [`docs/research/2026-05-19-indoor-cell-rendering-verification.md`](../research/2026-05-19-indoor-cell-rendering-verification.md). Plan: [`docs/superpowers/plans/2026-05-19-phase2-indoor-cell-rendering-fix.md`](../superpowers/plans/2026-05-19-phase2-indoor-cell-rendering-fix.md). | Live ✓ |
| C.1.5b | Per-part PES transforms + dat-hydrated entity DefaultScript dispatch. Closes issue #56. Shipped 2026-05-12 across 5 commits (`1e3c33b` docs+plan, `f3bc15e` SetupPartTransforms helper, `11521f4` ParticleHookSink applies `CreateParticleHook.PartIndex`, `5ca5827` activator refactor + GameWindow resolver lambda, `8735c39` GpuWorldState 4 new fire-sites). **Slice A** — new [`SetupPartTransforms.Compute(setup)`](../../src/AcDream.Core/Meshing/SetupPartTransforms.cs) walks `PlacementFrames[Resting]``[Default]` → first-available (mirrors `SetupMesh.Flatten` priority) and returns `Matrix4x4` per part; new `ParticleHookSink.SetEntityPartTransforms(entityId, partTransforms)` mirrors the existing `_rotationByEntity` pattern; `SpawnFromHook` now transforms hook offset through `partTransforms[partIndex]` before applying entity rotation. **Slice B** — activator's `ServerGuid==0` guard relaxed: keys by `entity.ServerGuid` when non-zero, else `entity.Id` (collision-free with server guids in the `0x40xxxxxx` interior / `0x80xxxxxx` scenery / `0xC0xxxxxx` ranges). Resolver delegate refactored to return `ScriptActivationInfo(ScriptId, PartTransforms)` so one dat lookup yields both pieces. `GpuWorldState` fires the activator from 4 new sites: `AddLandblock` + `AddEntitiesToExistingLandblock` (Far→Near promotion) for OnCreate, `RemoveLandblock` + `RemoveEntitiesFromLandblock` (Near→Far demotion) for OnRemove. ServerGuid==0 filter on AddLandblock avoids double-firing pending-bucket merges. **Reality discovery folded into spec §3**: EnvCell `StaticObjects` are already hydrated as `WorldEntity` instances by `GameWindow.BuildInteriorEntitiesForStreaming` (with stable `entity.Id` in `0x40xxxxxx`) — no synthetic-ID scheme or separate walker class needed (handoff §4 Q1/Q2 mooted). **Visual verification 2026-05-12**: Holtburg Town network portal swirl distributes across the arch (no ground-burial), Inn fireplace flames render over the firebox, cottage chimney smoke columns render, spell-cast animation-hook particles all match retail. 18 new + 4 updated tests, all Vfx/Meshing/Streaming/Activator green. Spec: [`docs/superpowers/specs/2026-05-13-phase-c1.5b-design.md`](../superpowers/specs/2026-05-13-phase-c1.5b-design.md). Plan: [`docs/superpowers/plans/2026-05-13-phase-c1.5b.md`](../superpowers/plans/2026-05-13-phase-c1.5b.md). | Live ✓ |
| Indoor walking Phase 1 — BSP cluster (partial) | 2026-05-19. Probe + WorldPicker cell-BSP occlusion (#86 closed) + CellId promotion via AABB containment (partial #84 fix). Seven commits across 5 phases: `18a2e28` plan, `27d7de1` Phase A `[indoor-bsp]` probe + toggle, `3764867` Phase B CellBspRayOccluder in WorldPicker, `4e308d5` Phase B screen-rect tests, `c19d6fb` Phase D AABB containment + L.2e bare-low-byte fix, `fda6af7` Phase E `[cell-cache]` diagnostic, `1f11ba9` Phase E extended AABB/bsphere/poly-count fields. **#86 closed** (picker occlusion). **#84 partially closed** (spawn-in-building stuck-above-floor resolved; threshold/doorway walls remain open under #87). **#85 open** (wall pass-through root cause confirmed as same as #84 remaining symptom — CellId doesn't stay promoted during outdoor→indoor walking). **#87 filed** (portal-based indoor cell tracking — retail-faithful follow-up). `[indoor-bsp]` + `[cell-cache]` probes stay in place as scaffolding for the follow-up phase. Handoff: [`docs/research/2026-05-19-cluster-a-shipped-handoff.md`](../research/2026-05-19-cluster-a-shipped-handoff.md). Plan: [`docs/superpowers/plans/2026-05-19-indoor-walking-phase1-bsp-cluster.md`](../superpowers/plans/2026-05-19-indoor-walking-phase1-bsp-cluster.md). | Tests ✓ |
| Indoor walking Phase 2 — Portal-based cell tracking | 2026-05-19. Portal-graph traversal replaces Phase D's AABB containment. Six commits: `1969c55` CellBSP+Portals wired into CellPhysics; `aad6976` CellTransit.FindCellList + FindTransitCellsSphere + AddAllOutsideCells + ResolveCellId rename; `069534a` BuildingPhysics + CheckBuildingTransit for outdoor→indoor entry; `702b30a` code-review polish; `3ffe1e4` pass foot-sphere center to ResolveCellId (critical fix — was passing CheckPos instead of GlobalSphere[0].Origin, causing PointInsideCellBsp to return false at floor level); `eb0f772` TryFindIndoorWalkablePlane synthesizes walkable plane from cell floor poly so the resolver doesn't fall through to outdoor SampleTerrainWalkable. **Closes #87, #85, and the wall-pass-through portion of #84 (fully closes #84).** Files #88 (indoor static object vibration — pre-existing) and #89 (BSPQuery.SphereIntersectsCellBsp — approximation in CheckBuildingTransit). `[cell-transit]`, `[indoor-bsp]`, `[check-bldg]`, `[cell-cache]` probes stay in place. Handoff: [`docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md`](../research/2026-05-19-indoor-walking-phase2-shipped-handoff.md). | Live ✓ |
Plus polish that doesn't get its own phase number:
- FlyCamera default speed lowered + Shift-to-boost

View file

@ -0,0 +1,284 @@
# Indoor walking Phase 2 — Portal-based cell tracking — handoff (2026-05-19)
**Date:** 2026-05-19.
**Branch:** `claude/competent-robinson-dec1f4` (commits land here; merge to main handled by controller).
**Predecessor:** Indoor walking Phase 1 — BSP cluster (Cluster A). Partially shipped 2026-05-19; closed #86 cleanly, filed #87 for the portal-traversal root cause. Diagnostic infrastructure (`[indoor-bsp]` + `[cell-cache]` probes) remained as scaffolding. Handoff: [`docs/research/2026-05-19-cluster-a-shipped-handoff.md`](2026-05-19-cluster-a-shipped-handoff.md).
---
## TL;DR
Phase 2 fully closes the indoor-walking story. Six commits replace Phase D's
AABB-containment shortcut with retail-faithful portal-graph cell traversal.
`CellId` now promotes to indoor cells via portals and remains promoted through
doorways, thresholds, and multi-room navigation. Indoor cell-BSP collision fires
consistently. A critical fix in commit 5 passes the foot-sphere center (not the
entity reference point) to `ResolveCellId`, which was the production failure that
made PointInsideCellBsp return false at floor level. Commit 6 adds
`TryFindIndoorWalkablePlane` so the walkability resolver doesn't fall through to
outdoor terrain when the player is inside.
**Visual verification at Holtburg cottage (2026-05-19, user testing live ACE):**
- Walls block from inside — player cannot walk through cottage walls.
- Multi-room navigation via doorways works — `[cell-transit]` log shows `0xA9B40145 → 0x143 → 0x144 → 0x13F` chains.
- Walking back outdoors through a door works (post-walkable fix in commit 6).
- Cell tracking is robust through multiple indoor sessions.
---
## Commits
| # | SHA | Subject |
|---|---|---|
| 1 | `1969c55` | `feat(physics): Phase 2 — wire CellBSP + Portals into CellPhysics` |
| 2 | `aad6976` | `feat(physics): Phase 2 — port CellTransit + wire into ResolveCellId` |
| 3 | `069534a` | `feat(physics): Phase 2 — BuildingPhysics + CheckBuildingTransit` |
| 4 | `702b30a` | `refactor(physics): Phase 2 — code-review polish on BuildingPhysics commit` |
| 5 | `3ffe1e4` | `fix(physics): Phase 2 — pass foot-sphere center to ResolveCellId` |
| 6 | `eb0f772` | `fix(physics): Phase 2 — synthesize indoor walkable plane from cell floor` |
**Build:** clean on all commits.
**Tests:** `dotnet test` shows the same 8 pre-existing failures in
`AcDream.Core.Tests` (MotionInterpreter / BSPStepUp / etc., unchanged). All
new Phase 2 tests and the walkable-plane tests green.
---
## What shipped
### Commit 1 — CellBSP + Portals wired into CellPhysics
New `PortalInfo` struct holds `PortalId`, `PortalPolygonIndex`, `PortalFlags`,
and `OtherCellId`. `CellPhysics` extended with:
- `CellBSP` — a third BSP tree (alongside `PhysicsBSP` and the render BSP) used
for point-in-cell tests. Retail: `CCellStructure::cell_bsp`.
- `Portals``IReadOnlyList<PortalInfo>` built from `envCell.CellPortals`.
- `PortalPolygons` — the visible polygons that portals reference (`cellStruct.Polygons`,
not `PhysicsPolygons`; portals reference the visible-geometry polygon list).
- `VisibleCellIds` — cells visible from this cell (used by `AddAllOutsideCells`).
Phase D's `LocalAabbMin/Max` + `TryFindContainingCell` are deleted — they are now
superseded by the portal traversal in `CellTransit`.
### Commit 2 — CellTransit + ResolveCellId
New `CellTransit` static class implements the retail portal-neighbour walk.
Three public entry points:
- **`FindTransitCellsSphere(sphereCenter, sphereRadius, startCell, cache)`** —
walks portal connectivity from `startCell` outward. For each portal, tests
whether the sphere overlaps the portal polygon (using `PointInsideCellBsp` on
the sphere center as an approximation — see issue #89 for the retail-faithful
sphere variant). Recurses into neighbour cells up to a depth limit.
- **`AddAllOutsideCells(sphereCenter, blockId, cache, results)`** — for the
outdoor path: populates a 24m grid of outdoor cell ids around the sphere center
using `TerrainSurface.ComputeOutdoorCellId`. Mirrors retail's `add_all_outside_cells`.
- **`FindCellList(sp, startCell, cache)`** — top-level driver. Determines whether
`startCell` is an indoor (EnvCell) or outdoor cell and dispatches accordingly.
Returns a list of candidate cell ids.
`PhysicsEngine.ResolveOutdoorCellId` renamed to `ResolveCellId` (accepts
`sphereRadius` parameter). Body splits on indoor vs outdoor:
- **Indoor:** delegates to `FindCellList` and picks the candidate cell where
`PointInsideCellBsp` returns true for the sphere center.
- **Outdoor:** existing terrain-grid loop (`AddAllOutsideCells`).
`BSPQuery.PointInsideCellBsp` retyped from `PhysicsBSPNode?` to `CellBSPNode?`
(dead code retype — no behavior change). Phase D's test file deleted.
### Commit 3 — BuildingPhysics + CheckBuildingTransit
Outdoor→indoor entry path via building-shell portal graph. New `BuildingPhysics`
class caches per-building portal data (`BldPortalInfo` structs with `PortalId`,
`OtherCellId`, `CellBSP`). `PhysicsDataCache` gains `_buildings` cache keyed by
building entity id. `GameWindow` iterates `lbInfo.Buildings` at landblock load and
populates the cache.
`CellTransit.CheckBuildingTransit(sphereCenter, sphereRadius, blockId, physicsCache)`
ports retail's outdoor→indoor portal-graph entry:
1. For each building in the landblock's physics cache, test whether the sphere
center is inside the building's shell cell BSP (`PointInsideCellBsp`).
2. If inside, walk the building's portal graph to find the indoor EnvCell that
contains the sphere center.
3. Returns the EnvCell id (or 0 if no match).
`PhysicsEngine.ResolveCellId`'s outdoor branch hooks `CheckBuildingTransit` after
the terrain-grid loop, so outdoor→indoor transition is detected during normal walking.
### Commit 4 — Code-review polish
Five items addressed from reviewer:
1. DRY cell-id derivation via existing `TerrainSurface.ComputeOutdoorCellId`
(removed inline duplicate in `CheckBuildingTransit`).
2. Named `PortalFlags.ExactMatch` enum instead of raw `0x01` literal.
3. Comment clarity on `ExactMatch` reserved field.
4. Doc comment on `CheckBuildingTransit` calling out the sphere-vs-point
divergence from retail's `sphere_intersects_cell` (see issue #89).
5. Rename misleading test method name.
### Commit 5 — Critical fix: foot-sphere center to ResolveCellId
**This was the production bug that prevented Phase 2 from working until the last run.**
`ResolveCellId` was being called with `sp.CheckPos` (the entity's reference point
at feet level, world Z = terrain Z after the +0.02f bump) instead of
`sp.GlobalSphere[0].Origin` (the foot sphere CENTER, approximately +0.48m above terrain).
Combined with the +0.02f Z-bump applied to cell origins in `PhysicsDataCache`, the
test point landed at cell-local Z = -0.02 m — just below the cell's floor — and
`PointInsideCellBsp` returned false for every cell. CellId never promoted to indoor
cells during normal walking despite `FindCellList` correctly finding the right
candidate cells.
Passing the foot-sphere center (which sits 0.48m above the floor, well inside any
room cell) made portal-based cell tracking actually work in production.
Also adds the `[check-bldg]` diagnostic line (logged when `CheckBuildingTransit`
returns a non-zero indoor cell id).
### Commit 6 — TryFindIndoorWalkablePlane
**Root cause of the post-Phase-2 falling-stuck bug.**
When indoor cell-BSP returned OK (no wall collision), the code fell through to
outdoor `SampleTerrainWalkable` + `ValidateWalkable`. Outdoor terrain Z is below
the indoor floor (due to the +0.02f Z-bump), so `ValidateWalkable` computed the
player as floating well above terrain → not walkable → player stuck in the falling
animation when blocked by an indoor wall.
New `TryFindIndoorWalkablePlane(worldPos, cellPhysics)`: finds the floor polygon
directly under the player's world position by testing `worldPos` against each
physics polygon's plane normal (upward-facing = floor) and building a `ContactPlane`
from it. Called from the indoor branch of `ResolveWithTransition` before the outdoor
terrain fallback. Returns true when a floor poly is found; the resolver uses the
synthesized plane for walkability.
---
## Issue status after Phase 2
| Issue | Status | Notes |
|---|---|---|
| #84 Blocked by air indoors | **FULLY CLOSED** | Spawn-in-building variant: Phase D (Cluster A). Wall-block-from-inside + falling-stuck variants: Phase 2 commits 2, 5, 6. |
| #85 Pass through walls outside→in | **CLOSED** | `CheckBuildingTransit` + portal traversal. CellId promotes to indoor on outdoor→indoor entry. |
| #86 Click selection penetrates walls | CLOSED (Phase 1) | `WorldPicker.Pick` + `CellBspRayOccluder`. |
| #87 Indoor portal-based cell tracking | **CLOSED** | `CellTransit.FindCellList` + `FindTransitCellsSphere` + `AddAllOutsideCells`. Portal-graph traversal replaces AABB containment. |
| #88 Indoor static objects vibrate | OPEN (new) | Pre-existing visual jitter on bookshelves/furnaces. Filed 2026-05-19. Medium severity. |
| #89 Port BSPQuery.SphereIntersectsCellBsp | OPEN (new) | `CheckBuildingTransit` uses `PointInsideCellBsp` (radius-less approximation) instead of retail's `sphere_intersects_cell`. Filed 2026-05-19. Low severity. |
---
## Probe evidence — log file findings
### `launch-phase2-verify3.log`
First run that showed indoor cell-transits firing. `[cell-transit]` output
confirmed the portal traversal was finding indoor cells. `[indoor-bsp]` probe
fired consistently during indoor walking (not just during mid-jump frames as in
Cluster A). This log is the first evidence that `CellTransit.FindCellList` was
working correctly for room interiors, though outdoor→indoor entry was not yet
exercised.
### `launch-phase2-verify4.log`
Multi-room navigation run. `[cell-transit]` log shows
`0xA9B40145 → 0x143 → 0x144 → 0x13F` chains as the player walked between
rooms in the Holtburg cottage via doorways. Confirmed the `FindTransitCellsSphere`
recursive portal walk was promoting CellId correctly through threshold cells.
Walls blocked from inside in all rooms tested.
### `launch-phase2-verify5.log`
Walkable bug evidence run. After the outdoor→indoor transition was wired
(`CheckBuildingTransit`), the player could walk into the cottage from outside,
but colliding with an indoor wall produced a falling-stuck state (the `[indoor-bsp]`
probe fired for the wall collision, but `ValidateWalkable` returned false because
it was sampling outdoor terrain Z). This log captured the falling-stuck symptom
and the `SampleTerrainWalkable` fallthrough trace, motivating commit 6.
### `launch-phase2-verify6.log`
Post-walkable-fix verification run. After `TryFindIndoorWalkablePlane` was added:
- Outdoor→indoor entry works (player walks through doorway, CellId promotes).
- Indoor wall collision works (walls block, player doesn't pass through).
- Walking back outdoors through the door works (CellId demotes to outdoor cell).
- No falling-stuck state observed. User confirmed all three behaviors.
---
## Diagnostic infrastructure remaining in place
All four probes stay committed and wired. They serve as production diagnostics
and as debugging aids for follow-up issues:
- **`ACDREAM_PROBE_INDOOR_BSP=1`** / DebugPanel "Indoor BSP probe": logs one
`[indoor-bsp]` line each time `FindEnvCollisions` takes the indoor-cell branch.
After Phase 2, this fires consistently whenever the player is indoors. Useful
for confirming the indoor-BSP path is active.
- **`ACDREAM_PROBE_CELL_CACHE=1`** / DebugPanel "Cell cache probe": dumps all
cached EnvCell physics data (poly counts, BSP bounding sphere, AABB, unmatched
ID count, portal count). Useful for verifying cell struct loads and portal
connectivity.
- **`ACDREAM_PROBE_CELL=1`** (existing L.2a slice 1): one `[cell-transit]` line
per `PlayerMovementController.CellId` change (old → new cell, world position,
reason tag). Essential for tracing indoor promotion/demotion sequences.
- **`[check-bldg]`** (commit 5): logged by `ResolveCellId` when
`CheckBuildingTransit` returns a non-zero indoor cell id. Fires once per
outdoor→indoor transition detection.
All gated behind `PhysicsDiagnostics` static class (existing pattern from L.2a).
---
## Visual verification outcomes
**2026-05-19, user testing live against local ACE at Holtburg.**
| Scenario | Result |
|---|---|
| Walk into cottage wall from inside | Blocked ✓ |
| Walk between rooms via doorway | CellId transitions logged, multi-room navigation works ✓ |
| Walk from outside into cottage through door | Outdoor→indoor entry promoted CellId; indoor BSP collision active ✓ |
| Walk back outside through door | CellId demoted to outdoor cell; outdoor physics resumed ✓ |
| No falling-stuck after post-walkable fix | Confirmed ✓ |
| Robust across multiple indoor sessions | Confirmed ✓ |
---
## Known follow-ups
**#88 — Indoor static objects vibrate (bookshelves, open furnaces).** Pre-existing
visual jitter spotted before Phase 2 shipped. Medium severity. Candidates: repeated
`EntityScriptActivator.OnCreate/OnRemove` near cell boundaries, per-part transform
drift, or particle-emitter offset accumulation. Investigate in a follow-up session.
**#89 — Port `BSPQuery.SphereIntersectsCellBsp`.** `CellTransit.CheckBuildingTransit`
currently uses `PointInsideCellBsp` (tests sphere CENTER only). Retail's
`CEnvCell::check_building_transit` uses `CCellStruct::sphere_intersects_cell`
(radius-aware, returns Inside/Crossing/Outside). Practical effect: entry fires
~0.48m deeper into the doorway than retail. Low severity — visually acceptable.
The `sphereRadius` parameter is already plumbed through for when this is ported.
**#80 — Indoor darkness (camera on 2nd floor goes very dark).** Still open.
Not in Phase 2's scope. Lighting / ambient-occlusion issue that predates indoor
rendering Phase 2.
---
## State at handoff
- **Branch:** `claude/competent-robinson-dec1f4`, 6 commits of Phase 2 work
(plus 7 from Phase 1 / Cluster A on the same branch).
- **Build state:** `dotnet build -c Debug` clean.
- **Tests:** 8 pre-existing failures unchanged (MotionInterpreter / BSPStepUp
baseline). All targeted test projects green.
- **Issues:** #84, #85, #87 CLOSED. #86 CLOSED (Phase 1). #88, #89 OPEN (new).
- **Diagnostic probes:** `[indoor-bsp]`, `[cell-cache]`, `[cell-transit]`,
`[check-bldg]` all active and wired.
- **Next:** M2 critical path (F.2 / F.3 / F.5a / L.1c / L.1b — kill-a-drudge
demo) or other candidates per work-order autonomy in CLAUDE.md.