acdream/docs/research/2026-05-19-indoor-walking-phase2-shipped-handoff.md
Erik a9c74d153a 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>
2026-05-19 19:31:22 +02:00

14 KiB

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.


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.
  • PortalsIReadOnlyList<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.