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>
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 shows0xA9B40145 → 0x143 → 0x144 → 0x13Fchains. - 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 (alongsidePhysicsBSPand the render BSP) used for point-in-cell tests. Retail:CCellStructure::cell_bsp.Portals—IReadOnlyList<PortalInfo>built fromenvCell.CellPortals.PortalPolygons— the visible polygons that portals reference (cellStruct.Polygons, notPhysicsPolygons; portals reference the visible-geometry polygon list).VisibleCellIds— cells visible from this cell (used byAddAllOutsideCells).
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 fromstartCelloutward. For each portal, tests whether the sphere overlaps the portal polygon (usingPointInsideCellBspon 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 usingTerrainSurface.ComputeOutdoorCellId. Mirrors retail'sadd_all_outside_cells. -
FindCellList(sp, startCell, cache)— top-level driver. Determines whetherstartCellis 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
FindCellListand picks the candidate cell wherePointInsideCellBspreturns 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:
- For each building in the landblock's physics cache, test whether the sphere
center is inside the building's shell cell BSP (
PointInsideCellBsp). - If inside, walk the building's portal graph to find the indoor EnvCell that contains the sphere center.
- 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:
- DRY cell-id derivation via existing
TerrainSurface.ComputeOutdoorCellId(removed inline duplicate inCheckBuildingTransit). - Named
PortalFlags.ExactMatchenum instead of raw0x01literal. - Comment clarity on
ExactMatchreserved field. - Doc comment on
CheckBuildingTransitcalling out the sphere-vs-point divergence from retail'ssphere_intersects_cell(see issue #89). - 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 timeFindEnvCollisionstakes 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 perPlayerMovementController.CellIdchange (old → new cell, world position, reason tag). Essential for tracing indoor promotion/demotion sequences. -
[check-bldg](commit 5): logged byResolveCellIdwhenCheckBuildingTransitreturns 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 Debugclean. - 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.