Adds a ref ushort hitPolyId parameter to FindWalkableInternal so callers
can identify which polygon was hit. The leaf branch already iterates
foreach (ushort polyId in node.Polygons); this surfaces it.
No behavior change. Existing callers (StepSphereDown, Path 4 Collide)
pass a discard local. The new BSPQuery.FindWalkableSphere wrapper
(next commit) will consume it.
Prep for indoor walkable-plane BSP port — see spec
docs/superpowers/specs/2026-05-19-indoor-walkable-plane-bsp-port-design.md
Brainstormed spec for resolving the cellar-descent / 2nd-floor /
invisible-obstacle indoor collision regressions reported post-Phase 2.
Root cause: Phase 2 commit eb0f772 introduced TryFindIndoorWalkablePlane
as a stop-gap walkable-plane synthesis when the indoor BSP returns OK.
Its body does a linear first-match XY scan over cellPhysics.Resolved with
no Z-proximity test, so multi-Z indoor geometry (cellars, 2nd floors,
balconies) collapses to wrong-floor selection. Walking UP stairs works
because step_up routes through DoStepDown → TransitionalInsert(5) →
BSPQuery.FindCollisions Path 3 (StepSphereDown) which already uses
FindWalkableInternal — the retail-faithful BSP walkable-finder. The
linear scan only fires in the OK-no-wall branch.
Fix: route TryFindIndoorWalkablePlane through the existing
FindWalkableInternal via a thin new BSPQuery.FindWalkableSphere wrapper.
Extends FindWalkableInternal's signature to expose the hit polyId
(dictionary key, since ResolvedPolygon doesn't carry its own id). Threads
the foot-sphere radius through TryFindIndoorWalkablePlane's signature
(was hardcoded to nothing — used the localFootCenter alone). Deletes
the now-dead PointInPolygonXY helper.
Awaiting user spec review before plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User explicitly redirected the next-phase track: no M2 (kill-a-drudge), stay
on indoor walking issues, collision, physics, and dungeons. Update the
pickup prompt to reflect this:
- Drop M2 from the candidate list entirely.
- Add #83 (walking up stairs) as the recommended next phase — pure
indoor/physics, unblocks both multi-floor cottages AND dungeons.
- Add a "dungeon stress test" candidate (Path B) — verify Phase 2's
portal traversal works on multi-cell indoor spaces.
- Move indoor lighting from "recommended" to Path E with a note that it
depends on stairs (#83) landing first to be testable.
- Update the helper section with concrete file pointers per path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After merging Phase 1 + Phase 2 to main at 1af49b7, file a pickup prompt
that orients a fresh Claude Code session: what shipped, what's open, the
four ranked candidate next phases (indoor lighting / M2 / #88 vibration /
triage), and key file pointers for whichever path is chosen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
When the indoor cell-BSP query returns OK (no wall collision), the player
is standing on a floor poly inside the cell. Previously the code fell
through to outdoor terrain (SampleTerrainWalkable + ValidateWalkable),
which used the OUTDOOR terrain plane — below the indoor floor due to the
+0.02f Z-bump applied for render z-fight prevention. ValidateWalkable
saw the player 0.5m above the outdoor plane → marked them as airborne
→ walkable=False → falling animation, never recovers.
Adds TryFindIndoorWalkablePlane (internal static for testability): scans
the cell's resolved physics polys for a walkable floor poly (normal.Z >=
0.6664, walkable-slope threshold matching retail) under the player's XY,
transforms its plane + vertices to world space via WorldTransform, and
calls ValidateWalkable with the indoor plane. Adds PointInPolygonXY
(ray-casting even-odd rule, ignores Z). Both are wired just after the
BSP OK branch in FindEnvCollisions; outdoor terrain remains a defensive
backstop if no floor poly is found under the player indoors (rare).
Matches retail's CEnvCell::find_env_collisions behavior: no fall-through
to terrain when the cell BSP successfully completes a query.
Evidence: launch-phase2-verify5.log captured 12,141 walkable=False
events during an indoor session where the player never managed to walk
back outdoor through a door — they got stuck against the indoor wall
and the resolver never re-established a walkable contact plane.
Adds 13 unit tests in IndoorWalkablePlaneTests.cs covering:
- player over floor poly (returns true, plane normal up, plane at correct Z)
- player outside poly XY (returns false)
- no walkable polys (returns false)
- empty Resolved dict (returns false)
- cell with world translation (plane + vertices in world space)
- PointInPolygonXY cases (centre, near corner, on boundary, outside, Z ignored)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual test of Phase 2 portal traversal showed walls still didn't
block from inside buildings. Diagnosis: ResolveCellId was being
called with sp.CheckPos (entity reference, at the feet — world
Z=terrain) instead of sp.GlobalSphere[0].Origin (foot sphere center,
~0.5m above terrain). Combined with the +0.02f Z-bump on cached
cell origins (for render z-fight prevention), the test position
landed at cell-local Z=-0.02 — just below the cell floor — and
PointInsideCellBsp correctly reported "outside" for every cell.
CheckBuildingTransit never added candidates; player CellId stayed
outdoor; indoor cell-BSP collision branch never fired; walls didn't
block.
Retail's check_building_transit uses sphere.Center (the sphere CENTER,
not the entity reference) per the pseudocode at
docs/research/acclient_indoor_transitions_pseudocode.md:222-238.
Three call sites updated (PhysicsEngine x2 inside ResolveWithTransition;
TransitionTypes inside Transition.FindEnvCollisions).
Also adds a [check-bldg] diagnostic line to CheckBuildingTransit (gated
on the existing ACDREAM_PROBE_INDOOR_BSP flag) so future verification
captures show per-portal inside/outside results without needing
another diagnostic flag.
Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five reviewer-flagged items addressed:
- Fix#1: GameWindow building-loop now reuses TerrainSurface.ComputeOutdoorCellId
instead of re-deriving the row-major cell-index formula. DRY win; no risk
of the two formulas drifting.
- Fix#2: BuildingPhysics.ExactMatch decoder now references
DatReaderWriter.Enums.PortalFlags.ExactMatch instead of magic 0x0001.
- Fix#3: ExactMatch XML doc clarified as "reserved per retail's
CBldPortal::exact_match; not currently consumed by CheckBuildingTransit".
- Fix#4: CheckBuildingTransit docstring now explicitly documents the
retail divergence — retail's sphere_intersects_cell (radius-aware) vs.
our PointInsideCellBsp (radius-less). The sphereRadius parameter is
reserved for the future sphere_intersects_cell port. Practical effect
noted: entry fires ~sphereRadius (~0.48m) deeper than retail.
- Fix#5: Test method `SphereInsideBuildingPortalDestination_AddsInteriorCell`
renamed to `BuildingPortalWithUnloadedCellBSP_NoCandidateAdded` — the
test asserts Empty(candidates), not that the cell is added. Comment
updated.
Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-portal-cell-tracking.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the outdoor→indoor entry path. New BuildingPhysics type holds
the per-SortCell BldPortal list + building world transform; PhysicsDataCache
caches it (CacheBuilding + GetBuilding); CellTransit.CheckBuildingTransit
tests each portal's destination cell via PointInsideCellBsp.
PhysicsEngine.ResolveCellId's outdoor branch now hooks CheckBuildingTransit
after the terrain-grid lookup: if the matched landcell has a cached
building stab, check whether the sphere has crossed into one of its
interior EnvCells before returning.
GameWindow at landblock-load time iterates LandBlockInfo.Buildings and
caches each via PhysicsDataCache.CacheBuilding. The landcell-id derivation
uses retail's row-major cell-index formula (gridX * 8 + gridY + 1).
Polish items from Subagent B/C reviews folded in:
- visited HashSet in FindCellList's BFS (avoids O(N^2) re-enqueue)
- ResolveCellId_NoDataCache_ReturnsFallback test (closes coverage gap)
- DataCache-asymmetry comment in PhysicsEngine.ResolveCellId
- Replaced misleading FindCellList outdoor-branch TODO with explicit
note that ResolveCellId bypasses this branch — wired in ResolveCellId
directly.
- Removed unused 'using DatReaderWriter.Types;' from CellTransit.cs
- 2 new CellTransitFindCellListTests integration tests
- 1 new CellTransitCheckBuildingTransitTests test (null-CellBSP guard
case; happy path deferred to visual verification).
Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-portal-cell-tracking.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New CellTransit static class ports retail's portal-graph cell traversal:
- FindTransitCellsSphere — indoor portal-neighbour walk
- AddAllOutsideCells — outdoor 24m grid expansion
- FindCellList — top-level driver (BFS through portals;
PointInsideCellBsp for final containment)
PhysicsEngine.ResolveOutdoorCellId renamed to ResolveCellId. Body
rewritten: indoor seeds delegate to CellTransit.FindCellList (portal-
graph BFS + BSP containment test); outdoor seeds keep the landblock
terrain grid lookup from the original implementation (preserving the
L.2e prefix-preservation fix). Signature extended with sphereRadius
parameter (needed by the sphere-vs-portal-plane test). Three call
sites updated (PhysicsEngine x2, TransitionTypes x1).
BSPQuery.PointInsideCellBsp retyped from PhysicsBSPNode? to CellBSPNode?
— the function operates on the cell-BSP tree (CellPhysics.CellBSP.Root
is a CellBSPNode). The previous PhysicsBSPNode typing was dead code, so
retype is safe.
Deletes the Phase D ResolveOutdoorCellIdTests.cs file. New ResolveCellIdTests
covers the equivalent contracts (fallback zero, outdoor seed with no
landblock).
Outdoor->indoor entry (check_building_transit) is stubbed pending the
BuildingPhysics infrastructure landing in the next commit.
Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-portal-cell-tracking.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds PortalInfo struct and extends CellPhysics with CellBSP (third BSP
for point-in-cell tests, typed CellBSPTree from DatReaderWriter),
Portals (from envCell.CellPortals), PortalPolygons (resolved
cellStruct.Polygons — portals reference visible polys, not
PhysicsPolygons), and VisibleCellIds (populated for future use;
envCell.VisibleCells is List<UInt16>, not Dictionary).
Deletes CellPhysics.LocalAabbMin/Max and PhysicsDataCache.TryFindContainingCell
— Phase D's AABB shortcut is gone. CacheCellStruct's AABB compute
removed; the [cell-cache] diagnostic updated with portal/visible counts
instead.
CacheCellStruct signature gains an EnvCell parameter (one call site in
GameWindow.cs:5384 updated). ResolveOutdoorCellId drops the
TryFindContainingCell call; portal-graph CellTransit replaces it next.
ResolveOutdoorCellIdTests object initializers had the deleted AABB
properties stripped temporarily so the build stays green; the file gets
replaced wholesale in the next commit (CellTransit integration). Those
2 AABB-containment tests continue to fail (they were pre-broken on this
branch); no new failures introduced.
Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-portal-cell-tracking.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
11-task plan: data wiring (PortalInfo + extend CellPhysics + extend
CacheCellStruct) → CellTransit port (FindTransitCellsSphere +
AddAllOutsideCells + FindCellList) → ResolveCellId integration (rename +
plumb sphereRadius + delete AABB containment) → BuildingPhysics for
outdoor→indoor → capture + docs.
Task 0 verifies DatReaderWriter exposes CellStruct.CellBSP and
LandBlockInfo.Buildings before any code touches them. The CellBSP
property name is the one known unknown.
Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brainstormed spec for the follow-up to Cluster A: port retail's portal-graph
cell traversal to replace Phase D's AABB containment shortcut. Closes
ISSUES.md #87 and the remaining wall-collision parts of #84 + #85 — indoor
walking with walls that block from inside, walking through doors that
updates CellId.
Scope: all three transition types (indoor↔indoor, indoor↔outdoor,
outdoor→indoor). AABB containment deleted entirely; portal traversal is the
only path.
Key data references: docs/research/acclient_indoor_transitions_pseudocode.md
(2026-04-13) has the entire algorithm already documented from ACE source
cross-referenced against the retail header. BSPQuery.PointInsideCellBsp is
already wired (just unused).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cluster A's investigation pinned #86 (picker) as structural and closed
it (Phase B). #84 and #85 both pinned on missing indoor cell tracking;
Phase D promoted CellId via AABB containment which un-stuck the
spawn-in-building case (closes#84 partially) but proved too tight for
threshold/doorway cells to keep CellId indoor during normal walking.
The proper fix is retail's portal-based cell traversal; filed as a
new ISSUES.md issue (see body) for the follow-up phase. Phase E
diagnostic infrastructure ([cell-cache] + extended [indoor-bsp]) stays
in place as scaffolding for that work.
ISSUES.md: #86 → Recently closed. #84 status updated to PARTIAL with
resolution paragraph. #85 status update note added. New issue #87 filed
for portal-based indoor cell tracking.
Roadmap: Cluster A added to Recently shipped with partial-ship note.
Forward entry added for the portal-traversal follow-up under Phase G.
CLAUDE.md: current-phase paragraph updated to reflect Cluster A partial
ship. Next phase deferred to Claude's choice in a future session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original Phase E [cell-cache] probe (fda6af7) only showed the BSP root
node's direct poly count, which was always 0 for non-trivial trees (internal
node root). Extending the probe to:
- Recursively walk the BSP tree and count total leaf polys
- Detect unmatched poly IDs (BSP leaves referencing IDs not in our resolved dict)
- Dump the BSP root bounding sphere (center + radius)
- Dump the cell's local AABB (min/max from poly vertices)
- Dump the cell's world origin (cellTransform * (0,0,0))
The extended data made the route-δ diagnosis definitive: Holtburg cells DO
have full physics polygons in their BSPs (e.g. 0xA9B40143 has 14 polys all
resolved, full Z range 0-2.8 m). The bug is upstream — AABB-based cell
containment is too tight to capture a standing player at most thresholds
between rooms, so the indoor cell-BSP branch fires only intermittently.
Retail uses portal traversal (CObjMaint::HandleObjectEnterCell + cell-side
portal data) which propagates CellId at door crossings. Our AABB-containment
shortcut is partial. This diagnostic stays in place as infrastructure for
the follow-up "Indoor portal-based cell tracking" phase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every [indoor-bsp] probe line reports result=OK poly=n/a, meaning
BSPQuery.FindCollisions never records a hit polygon. Four hypotheses:
(a) PhysicsPolygons.Count == 0 for all cached EnvCells (empty data),
(b) BSP leaf Polygons IDs don't match PhysicsPolygons dict keys,
(c) ResolvePolygons filters out all polygons (vertex lookups fail or
degenerate normals), or (d) sphere is too far from BSP leaf bounds.
Format analysis rules out (b): retail BSPLEAF::PackLeaf writes
poly_id (not array index) into the BSP leaf ushort list; CPolygon::Pack
writes poly_id as first field; DatReaderWriter reads it as dictionary
key. ACE DatLoader does the same. Keys are consistent end-to-end.
Add ProbeCellCacheEnabled (ACDREAM_PROBE_CELL_CACHE=1) to
PhysicsDiagnostics and a [cell-cache] log line at the end of
CacheCellStruct. One line per cached EnvCell:
[cell-cache] envCellId=0x... physicsPolyCount=N resolvedCount=M
bspRootPolyCount=K bspRootHasChildren=true|false
physicsPolyCount=0 -> hypothesis (a).
resolvedCount < physicsPolyCount -> hypothesis (c).
Non-zero counts + bspRootPolyCount=0 + bspRootHasChildren=true ->
expected (internal node, leaves hold poly refs); then investigate (d).
Non-zero counts + bspRootPolyCount=0 + bspRootHasChildren=false ->
leaf with empty Polygons list, deeper investigation needed.
Cross-referencing cell-cache lines with indoor-bsp lines (same
envCellId) will pin the root cause in the next launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ResolveOutdoorCellId only resolved outdoor terrain landcells. A player
geometrically inside an EnvCell stayed in outdoor-landcell range, so
FindEnvCollisions' indoor cell-BSP branch (gated on cellLow >= 0x0100)
never fired. Both #84 (blocked by air indoors) and #85 (pass through
walls outside→in) are downstream of this — without indoor cell-BSP
collision the player gets stuck against outdoor-stab back-faces of the
building shell, and walls only block from one side.
Adds an indoor-cell-containment check via PhysicsDataCache: at
CacheCellStruct time, compute each cell's local AABB from its resolved
polygon vertices; at ResolveOutdoorCellId time, transform the world
position into each cached cell's local space and return the matched
cell's full id when contained. Falls through to the existing outdoor
terrain logic when no EnvCell contains the position.
Also fixes a pre-existing prefix-preservation bug in the outdoor branch:
the function now always applies the matched landblock's high-16 prefix
even when the input fallbackCellId arrived bare-low-byte (the L.2e
finding from CLAUDE.md). Updated two existing PhysicsEngineTests that
encoded the old bare-low-byte output.
Evidence: launch-cluster-a-capture.log @ 2026-05-19 — player at
worldPos (155.376, 14.010, 94.000) geometrically inside cottage cell
0xA9B40172, but sp.CheckCellId stuck at 0x00000031 (outdoor landcell)
across 454 [resolve] lines; zero [indoor-bsp] lines because the gate
never opened.
Closes#84.
Closes#85.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B's WorldPicker change added cellOccluder to both Pick overloads,
but the integration test suite only covered the legacy ray-sphere
overload. The production code path (GameWindow.PickAndStoreSelection)
uses the screen-rect overload, and its clip.W depth-conversion math
had no direct test. Adds two integration tests mirroring the existing
ray-sphere variants:
- Pick_ScreenRect_EntityBehindWall_OccludedByCellBsp — entity dead-
ahead, wall between, with cellOccluder → null.
- Pick_ScreenRect_NoWall_HitsEntity — same scene, null occluder → hit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WorldPicker.Pick previously had no occlusion test — any entity along
the click ray within maxDistance was a candidate, including ones
behind walls. Adds the CellBspRayOccluder static helper that
Möller-Trumbore-tests the click ray against every polygon in every
currently-cached EnvCell BSP, returning the nearest wall-hit `t`.
Both Pick overloads gate candidate selection by that wall-t (legacy
ray-sphere via world-space `t`, screen-rect via camera-space clip.W
depth — matching ScreenProjection.TryProjectSphereToScreenRect's
convention).
PhysicsDataCache exposes a new CellStructIds snapshot accessor so the
caller can iterate without needing the private cache dictionary.
CellPhysics.BSP/PhysicsPolygons/Vertices relaxed from required to
nullable so test fixtures can construct a CellPhysics from Resolved
alone without a real DAT BSP object. GameWindow snapshots the loaded
cell physics on each Pick call and passes the occluder callback.
Closes#86.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2's one-line WB patch (Setup-prefix guard at ObjectMeshManager.cs:1230)
fixed the symptom but is structurally a band-aid. CLAUDE.md's
no-workarounds rule says we should retire it.
The proper fix is switching our EnvCell rendering from the
general-purpose PrepareMeshDataAsync entry point (which iterates
static-object parts + emitters we don't need + triggers the buggy
TryGet<Setup> call) to WB's narrower PrepareEnvCellGeomMeshDataAsync
API at ObjectMeshManager.cs:386. That function only builds cell
room mesh — which is the only thing we use WB for at the cell
level. Static objects are already hydrated separately, particle
scripts already run via our own EntityScriptActivator.
#87 is the issue tracking that refactor. When it lands the WB fork
returns to pristine state (no acdream-specific commits on the
acdream branch for this file).
Handoff doc updated to flag the patch as a known band-aid pending #87.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the [indoor-bsp] probe + ProbeIndoorBspEnabled toggle for the
Indoor walking Phase 1 BSP-cluster investigation. Mirrors the existing
[resolve] / [cell-transit] / [indoor-*] pattern: one log line per
BSPQuery.FindCollisions call from FindEnvCollisions' cell branch,
capturing cell id, sphere local-pos, result TransitionState, and the
hit poly's normal + side-type via the LastBspHitPoly side-channel
(already wired for ProbeBuildingEnabled, now also fires for the indoor
flag).
Toggle via ACDREAM_PROBE_INDOOR_BSP=1 env var or DebugPanel checkbox.
Zero-cost when off.
Predecessor for the three fix commits that will close ISSUES.md
#84/#85/#86 after the capture session.
Spec: docs/superpowers/specs/2026-05-19-indoor-walking-phase1-bsp-cluster-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-walking-phase1-bsp-cluster.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14-task plan covering the diagnostic-driven phase: probe + capture +
three fix commits + docs. Tasks 1-6 land the [indoor-bsp] probe in
one feature commit. Task 7 is the user-run capture gate. Tasks 8-11
do post-capture diagnosis + fix for #84 and #85 (with a route-δ
escape hatch if #85's fix turns out to be a large cross-cell port).
Tasks 12-13 ship the WorldPicker cell-BSP occlusion fix for #86
(no capture dependency — pinned by code-reading). Task 14 closes
out ISSUES.md + roadmap + ships the post-phase handoff doc.
Spec: docs/superpowers/specs/2026-05-19-indoor-walking-phase1-bsp-cluster-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brainstormed spec for the next indoor follow-up phase: surfacing root
causes for ISSUES.md #84 (blocked by air) + #85 (pass through walls
outside→in) + #86 (click selection penetrates walls). Diagnostic-first
single capture pass; one [indoor-bsp] probe in FindEnvCollisions, then
surgical fixes (one commit per issue). Mirrors the indoor cell rendering
Phase 1+2 pattern that landed earlier today.
#86's root cause is already pinned by code reading (WorldPicker has no
cell-BSP test) — its fix is structural and doesn't need capture data.
#78 (outdoor stabs through floor) is in the same handoff cluster but
defers to a separate phase — different code path (render visibility).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After Phase 1+2 (indoor cell rendering — missing floors fixed),
9 follow-up issues (#78-#86 in docs/ISSUES.md) need their own
phases. This handoff doc gives the next session everything it needs
to start cold: probe infrastructure status, issue cluster groupings,
suggested phase order, and the verification approach that worked
for Phase 1+2.
Companion prompt file is a self-contained kickoff that can be pasted
into a fresh Claude Code session to start work on the cluster.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps the references/WorldBuilder submodule pointer from 167788b
to 34460c4 — picks up the Setup-prefix guard at PrepareEnvCellMeshData
line 1223 that lands Phase 2 indoor cell rendering fix.
WB branch acdream-fix-floor-rendering pushed to
git@github.com:eriknihlen/WorldBuilder.git earlier in this session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Roadmap shipped-table: two new rows for Phase 1 (diagnostics) +
Phase 2 (fix). Header status block updated to 2026-05-19 with the
Phase 2 cause + fix one-liner and pointer to the 9 surfaced issues.
- ISSUES.md: filed nine new issues (#78-#86) covering the indoor
bugs the user observed once the floor rendered. Grouped under an
"Indoor walking issue cluster" header. Cross-references the Phase 1
+ Phase 2 work that surfaced them. Hypotheses + suspected root
causes documented for each.
The 9 issues split into two probable shared-cause groups:
- Cell BSP / portal cull (#78, #84, #85, #86) — likely fixable in
one phase.
- Indoor lighting plumbing (#79, #80, #81, #82) — needs separate
investigation per-symptom.
Plus #83 (stairs) which probably needs its own physics phase work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User visually confirmed floors render in Holtburg Inn after the WB
TryGet<Setup> guard. Probe re-capture: 0 [wb-error] lines (was 385),
0 NULL_RESULT (was 55), Holtburg 123/123 cells complete (was 97/123).
Documents the nine pre-existing indoor bugs the user observed during
verification (see-through floor, indoor collision, stairs, walls,
clicking, indoor lighting artifacts, stabs-don't-react-to-atmospheric-
lighting, slope terrain lighting). All pre-existing; filed for follow-up
phases via docs/ISSUES.md.
Phase 2 complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 diagnostic chain identified the EXACT cause of 26/123 Holtburg
cells silently failing in WB's PrepareEnvCellMeshData:
ArgumentOutOfRangeException thrown from Setup.Unpack inside
DatReaderWriter when WB calls TryGet<Setup>(stab.Id, ...) on a stab id
whose prefix is GfxObj (0x01xxxxxx), not Setup (0x02xxxxxx).
DatReaderWriter finds the file in Portal's tree (GfxObjs and Setups
share tree-lookups), attempts to parse GfxObj bytes as Setup format,
throws OOR. Exception bubbles to PrepareMeshData's outer try/catch
which silently swallows + returns null. Entire cell fails to upload.
This commit lands the diagnostic infrastructure that surfaced the bug:
- WbMeshAdapter: replaced NullLogger<ObjectMeshManager> with a small
Console-backed ConsoleErrorLogger<T> private class. Filters to
LogLevel.Error+. WB's existing _logger.LogError(ex, ...) at the
swallow site now writes [wb-error] lines with type + message + top 5
stack frames. Bridges WB's intentional log point to acdream's console.
- WbMeshAdapter: extended [indoor-upload] NULL_RESULT probe with
reader-divergence diagnostic (ourCellDb.TryGet, wbResolveId.Count,
wbSelectedType, wbDbIsPortal, wbDbTryGet<EnvCell>, hadRenderData).
Made it possible to rule out cache-hits and reader-divergence as
causes before identifying the real one.
- Cause report at docs/research/2026-05-19-indoor-cell-rendering-cause.md
documents the full chain: 55 ArgumentOutOfRangeException stack traces
captured in one launch, all from PrepareEnvCellMeshData line 1223.
The fix itself (1-line guard at WB's TryGet<Setup> call site) is applied
to references/WorldBuilder/.../ObjectMeshManager.cs — which is a git
submodule. Will be committed separately to the WB submodule after
visual verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 Task 1's continuation logged [indoor-upload] NULL_RESULT
when WB's PrepareMeshDataAsync returned null. Extend the line to
include two cross-checks:
ourCellDb.TryGet=<bool> — acdream's DatCollection.Cell.TryGet<EnvCell>
wbResolveId.Count=<int> — WB's DefaultDatReaderWriter.ResolveId().Count
This narrows the cause among WB's null-return paths (ResolveId empty
vs TryGet<EnvCell> failure vs wrong type). Best-effort: both calls
wrapped in try/catch so diagnostic failures don't propagate.
Capture: 55 NULL_RESULTs across multiple landblocks ALL show
ourCellDb.TryGet=True + wbResolveId.Count=1. Both readers find the
cells in their indices, but WB's downstream PrepareMeshData logic
still returns null. Divergence is downstream of ResolveId.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 confirmed 26/123 Holtburg cells silently fail in WB's
PrepareEnvCellMeshData / PrepareMeshData. WB's catch block at
ObjectMeshManager.cs:589 calls _logger.LogError(ex, ...) — but we
construct ObjectMeshManager with NullLogger, so the log is dropped.
Capture the Task from PrepareMeshDataAsync (previously fire-and-forget)
and attach a ContinueWith that, for EnvCell ids only when the probe
is on, logs:
[indoor-upload] FAILED cellId=0x... exception=<Type>: <Message>
stack=[<top 3 frames>]
[indoor-upload] NULL_RESULT cellId=0x...
Runs on ThreadPool — non-blocking. Zero cost when ProbeIndoorUploadEnabled
is off. AggregateException is unwrapped to InnerException for readability.
Stack truncated to top 3 frames.
Next: capture procedure, identify cause, target the fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six tasks:
1. Add exception-surfacing ContinueWith in WbMeshAdapter.IncrementRefCount
for EnvCell ids when ProbeIndoorUploadEnabled is on. Logs
[indoor-upload] FAILED + [indoor-upload] NULL_RESULT.
2. Capture procedure: user walks Holtburg with the probe on; analyze log.
3. Write cause report documenting the captured exception type(s).
4. Apply targeted fix (4a/4b/4c/4d sub-shapes for the 4 most-likely causes
— choice driven by Task 2's data). Or 4d: re-design if cause is none
of the above.
5. Verification: re-capture confirms completed lines, user visually
confirms floor in Holtburg Inn.
6. Roadmap update.
Tasks 2 and 5 are user-driven (must walk the client). Tasks 1, 3, 4, 6
can be subagent-dispatched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three components:
1. WbMeshAdapter wraps the PrepareMeshDataAsync task with a continuation
that surfaces faulted-task exceptions + null-result cases for EnvCell
IDs only (gated by ProbeIndoorUploadEnabled). Two new log shapes:
[indoor-upload] FAILED cellId=0x... exception=<TypeName>: <Message>
stack=[<top 3 frames>]
[indoor-upload] NULL_RESULT cellId=0x...
2. Capture procedure: re-launch at Holtburg with the probe on, grep for
FAILED/NULL_RESULT lines, get definitive per-cell cause for the 26
missing-completion cells from Phase 1's capture.
3. Targeted fix: code change matching whichever exception type / null
pattern dominates. Fix shape is data-driven — see the contingency
table in the spec.
WB's catch at ObjectMeshManager.cs:589 already calls _logger.LogError,
but WbMeshAdapter constructs the manager with NullLogger.Instance, so
the log is dropped. Our continuation surfaces the same data scoped to
EnvCells only (avoids the thousands of GfxObj/Setup log lines a real
logger would emit during landblock streaming).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final code review of Phase 1 flagged that the three flag-mutating
tests leaked static state across test boundaries. Wrap each in
try/finally that snapshots IndoorAll on entry and restores it on
exit, matching the PhysicsDiagnosticsTests pattern at line 30-49.
Tests now safe under parallel test runs + future additions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captured at Holtburg landblock 0xA9B4 with ACDREAM_PROBE_INDOOR_ALL=1.
Result: 123 EnvCells in Holtburg get [indoor-upload] requested but
ONLY 97 get a matching [indoor-upload] completed. 26 cells silently
fail in WB's PrepareEnvCellMeshData / PrepareMeshData. The first
interior cell 0xA9B40100 — likely the inn entry or another major
building anchor — is among the failures, exactly matching the
user's "floor missing" symptom.
Other hypotheses ruled out:
- H2 (empty batches): completed cells have cellGeomVerts=14-86.
- H3 (cull bug): walk probe confirms cells pass all visibility filters.
- H4 (double-spawn): partCount values match expected SetupParts.
- H5 (transform double-apply): xform probe shows composedT==meshRefT;
no double-apply.
- H6 (MeshRefs structure): lookup probe shows isSetup=True and
partsHit≈partCount for uploaded cells.
Phase 2 plan: wrap PrepareMeshDataAsync with our own catch-and-log
in WbMeshAdapter so the swallowed exception (most likely cause of
the 26 silent failures, per WB ObjectMeshManager.cs:589) becomes
visible. Once we know the actual failure reason, target the fix.
Also flags IsEnvCellId false-positives on GfxObj IDs whose lower 24
bits ≥ 0x0100 — tightening recommended in Phase 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Instruments the per-MeshRef draw loop in WbDrawDispatcher:
- [indoor-lookup]: per cell entity, dumps render-data hit/miss,
IsSetup, parts count, and a partsHit/partsMiss tally over the
SetupParts. Disambiguates hypothesis H2 (WB produces empty
ObjectRenderData with zero parts) and H6 (dispatcher fails to
traverse Setup).
- [indoor-xform]: only fires for the cell's synthetic geometry part
(the SetupPart whose GfxObjId has bit 32 set, per WB's
PrepareEnvCellMeshData cellGeomId convention). Logs the three
composed transform translations: entityWorld, meshRef.PartTransform,
partTransform, and the final composed matrix translation. Disambiguates
hypothesis H5 (transform double-apply — composedT lands at 2 ×
cellOrigin).
Rate-limited via the ShouldEmitIndoorProbe instance helper added in
Task 6 (now consumed — no longer dead code).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Instruments WalkVisibleEntities to identify whether cell entities (first
MeshRef.GfxObjId low-16-bits >= 0x0100) pass all visibility filters or
get culled. Three emission paths:
- [indoor-cull] reason=visibleCellIds-miss -- when the ParentCellId
filter rejects the entity.
- [indoor-cull] reason=frustum -- when AABB frustum cull rejects.
- [indoor-walk] -- when the entity passes all filters and reaches the
draw list.
Rate-limited to once per cellId per ~1 sec (30 frames at 30 Hz) via
IndoorProbeState, a nested class wrapping _lastIndoorProbeFrame dictionary
and _indoorProbeFrameCounter (bumped at top of Draw()). WalkEntitiesInto
accepts a new optional IndoorProbeState? parameter (null = probes off,
default) so the test-friendly WalkEntities overload is unaffected. The
ShouldEmitIndoorProbe instance helper is also retained for Task 7 use.
Disambiguates hypothesis H3 (cull bug -- cell entity dropped before draw).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Instruments WbMeshAdapter at two sites:
- IncrementRefCount: on first call for an EnvCell id (low 16 bits >=
0x0100), tag the id in _pendingEnvCellRequests and log
[indoor-upload] requested.
- Tick: when WB's StagedMeshData drains an ObjectMeshData whose
ObjectId matches a pending EnvCell, log [indoor-upload] completed
with parts count, EnvCellGeometry vertex count, and upload result.
Missing "completed" lines after "requested" identify hypothesis H1
(WB silently returns null from PrepareEnvCellMeshData).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six checkboxes (ALL master + five individual probes) in the existing
DrawDiagnostics block. Toggling flips the corresponding
RenderingDiagnostics.Probe* flag live via DebugVM forwarding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Live-toggle wrappers for the five indoor-rendering probe flags plus the
ProbeIndoorAll master cascade. Pattern matches existing ProbeResolve /
ProbeCell / ProbeBuilding / ProbeAutoWalk mirrors so a checkbox flip in
the DebugPanel takes effect on the next frame.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the master IndoorAll cascade (both directions) and the IsEnvCellId
helper's 0x0100 boundary check across outdoor cells, indoor cells, and
landblock-prefixed forms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: "add the probes you need. Better info, better code."
Original spec had a single ACDREAM_PROBE_INDOOR=1 with vague "log
lookup results" guidance. Replaced with five individually-toggleable
probes, each with:
- Specific env var name + DebugPanel checkbox name.
- Concrete log-line format.
- Exact code site to instrument.
- The hypothesis it disambiguates.
Probe set:
- ACDREAM_PROBE_INDOOR_WALK — dispatcher entity walk per cell
- ACDREAM_PROBE_INDOOR_LOOKUP — render-data lookup hit/miss + SetupParts
- ACDREAM_PROBE_INDOOR_UPLOAD — WB upload result (requested + completed)
- ACDREAM_PROBE_INDOOR_XFORM — composed world transform for cell geom
- ACDREAM_PROBE_INDOOR_CULL — visibility/frustum filter decisions
Plus ACDREAM_PROBE_INDOOR_ALL master toggle.
Implementation outline added: new RenderingDiagnostics static class
(mirrors L.2a's PhysicsDiagnostics pattern), DebugPanel subsection,
edits to WbDrawDispatcher + WbMeshAdapter.
Acceptance criteria refreshed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial brainstorm assumed N.5 retirement broke EnvCell rendering by
leaving _pendingCellMeshes unconsumed. Pivoted mid-brainstorm:
- WB's PrepareMeshData routes EnvCell dat-record types to
PrepareEnvCellMeshData (ObjectMeshManager.cs:557) which produces an
IsSetup=true ObjectMeshData with the floor mesh as EnvCellGeometry.
- WbDrawDispatcher correctly handles IsSetup=true (line 607-621) by
iterating SetupParts and drawing each.
- DefaultDatReaderWriter loads region cell dats; ResolveId resolves
envCellId correctly.
- LandblockSpawnAdapter calls IncrementRefCount on every entity's
GfxObjId, including envCellId for cell entities. ServerGuid==0 passes
the atlas-tier filter.
Chain is structurally intact. The bug is somewhere subtler.
Spec pivots to a diagnostics-first phase: ACDREAM_PROBE_INDOOR=1
captures per-frame cell-entity walk + render-data lookup + SetupParts
traversal + composed-transform values. Six hypotheses (WB silently
returns null, empty batches, cull bug, double-spawn, transform
double-apply, dispatcher MeshRefs mismatch) match six concrete fix
shapes. Phase 2 design follows the probe data.
This is more honest than the original "build a new upload path"
design, which would have hidden the actual bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: third-person chase camera enters interiors before the
player body does, so the camera-based cameraInsideCell flag was
flipping the scene to indoor lighting prematurely (ambient drops to
0.2 white before the player has actually crossed the doorway).
Retail keys lighting off the PLAYER's cell. CellManager::ChangePosition
@ 0x004559B0 reads CObjCell::seen_outside on the player's current
cell — never on the camera. Match that semantics.
- CellVisibility.IsInsideAnyCell(Vector3): new non-caching brute-force
scan that's safe to call alongside ComputeVisibility(cameraPos)
without thrashing the camera cell cache.
- GameWindow render loop: derive playerInsideCell from the player's
Position when in player mode, otherwise fall back to cameraInsideCell
(orbit/fly debug camera).
- UpdateSunFromSky now takes playerInsideCell. The sky-render and
depth-buffer-clear decisions still use cameraInsideCell — those are
legitimately camera-POV concerns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Indoor cells rendered "almost black" because the hardcoded ambient at
GameWindow.cs:8342-8345 was an early-2026 guess (0.10, 0.09, 0.08 — half
retail brightness, warm-tinted) rather than the retail value. The named
retail decomp (acclient.pdb, Sept 2013 EoR build) shows
CellManager::ChangePosition @ 0x004559B0 calls
SmartBox::SetWorldAmbientLight(0.2f, 0xFFFFFFFF) whenever the player's
CObjCell::seen_outside flag is 0 — a flat 0.20 white floor, not a
dungeon-tone warm color.
Investigation also confirmed:
- EnvCell.dat does NOT carry inline lights — CEnvCell::UnPack reads
numVisibleCells where Binary Ninja's heuristic decomp inferred
"num_lights". Retail's CObjCell.light_list is populated at runtime via
add_light() calls from neighbouring cell light registrations + per-cell
static-object Setup.Lights, NOT from the dat byte stream.
- Setup.Lights from indoor static objects (entity.SourceGfxObjOrSetupId
prefix 0x02xxxxxx) DO flow through LightInfoLoader.Load (line 5765)
and reach LightManager via LightingHookSink. The wire is intact; the
per-frame Tick + UBO upload chain (line 6865-6867) is intact.
- Retail's particle system does NOT emit lights from particles themselves.
The light comes from the owning Setup's LightInfo records.
Pre-existing failures in DispatcherToMovementIntegrationTests, BSPStepUpTests,
and MotionInterpreterTests are on the branch already and unrelated to this
change (verified by stashing + retesting).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After visual verification 2026-05-18 (turn lag, coast-and-settle,
slope-tilt, jump tracking with contact-plane projection all working),
make the retail chase camera the default. Legacy ChaseCamera stays
available via the DebugPanel toggle (ACDREAM_RETAIL_CHASE=0 or the
checkbox) pending a follow-up deletion commit.
Env var polarity now matches AlignToSlope: default-on if unset, off
only when explicitly "0".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>