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

@ -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.