diff --git a/CLAUDE.md b/CLAUDE.md index b12d1d1..1f2ec4a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -776,7 +776,39 @@ acdream's plan lives in two files committed to the repo: acceptance criteria. Do not drift from the spec without explicit user approval. -**Currently in Phase L.2 (Movement & Collision Conformance).** L.2a slices +**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 + +**#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). + +**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. + +**Previously in Phase L.2 (Movement & Collision Conformance).** L.2a slices 1+2+3 + L.2d slice 1+1.5 + L.2g slice 1 + L.2g slice 1b + L.2g slice 1c + **Phase B.4b** + **Phase B.4c** all shipped and visual-verified 2026-05-13; **Phase B.5** (ground-item pickup, F-key) shipped and visual-verified diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 91fe17b..d80b7bf 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -239,7 +239,7 @@ to the second floor without getting stuck. ## #84 — Blocked by air indoors -**Status:** OPEN +**Status:** OPEN (partial fix 2026-05-19) **Severity:** HIGH (blocks indoor navigation) **Filed:** 2026-05-19 **Component:** physics, collision @@ -266,6 +266,20 @@ visible cell mesh. Possibilities: **Acceptance:** Walking through interior cell space hits collisions only where visible walls/furniture exist. +**Resolution (2026-05-19 partial · `c19d6fb`):** Phase D of Cluster A +extended `ResolveOutdoorCellId` in `PhysicsEngine.cs` with an indoor +cell-containment scan: when the player's world position falls inside any +cached EnvCell's AABB, `CellId` is promoted to that indoor cell, which +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. + --- ## #85 — Pass through walls from outside→in @@ -293,30 +307,65 @@ collision polys or per-poly back-face handling. **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. + --- -## #86 — Click selection penetrates walls +## #87 — Indoor cell tracking uses AABB containment instead of portal traversal **Status:** OPEN -**Severity:** MEDIUM +**Severity:** HIGH **Filed:** 2026-05-19 -**Component:** input, interaction +**Component:** physics -**Description:** Clicking through a wall from the outside selects NPCs -and objects inside the building. The `WorldPicker` raycast doesn't -intersect cell BSP geometry. +**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 +(the polys that sit at a room boundary) have AABB Z ranges of only ~0.2 m; +a standing player at local Z=0.46 m is OUTSIDE the AABB and containment +fails. Because `CellId` drifts back to the outdoor cell, the indoor-BSP +collision branch in `TransitionTypes.FindEnvCollisions` is gated out for +most movement, so walls don't block from inside the house and the floor +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`. -**Root cause / status:** `WorldPicker.BuildRay + Pick` (introduced in -Phase B.4) tests against entity AABBs and scenery BSPs but probably -not cell BSP. Outdoor NPCs are pickable because their entity AABB is -the test target; indoor NPCs are pickable from outside because the -wall isn't in the ray's intersection set. +**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). **Files:** -- `src/AcDream.App/Rendering/WorldPicker.cs` (or equivalent — check - Phase B.4b reference). +- `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) -**Acceptance:** Clicking on a wall doesn't select NPCs behind it. +**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). --- @@ -2847,6 +2896,23 @@ Unverified. The likely culprits, ranked by suspected probability: # Recently closed +## #86 — [DONE 2026-05-19 · 3764867 + 4e308d5] Click selection penetrates walls + +**Closed:** 2026-05-19 +**Commits:** `3764867` — fix(picker): Cluster A #86 — cell-BSP ray occlusion in WorldPicker; `4e308d5` — test(picker): Cluster A #86 — screen-rect cell-occlusion tests +**Component:** input, interaction + +**Resolution:** `WorldPicker.Pick` now accepts a `cellOccluder` callback +(`CellBspRayOccluder`). Before returning a hit, both `Pick` overloads +consult the occluder's `NearestWallT` value; any candidate entity whose +ray parameter exceeds the nearest-wall intersection is filtered out. +The occluder is wired from `GameWindow` using the loaded `PhysicsDataCache` +cell structs. Entities behind walls from the camera's perspective are no +longer selectable. Screen-rect occlusion tests verify the filter across +several hit/miss scenarios. + +--- + ## #77 — [DONE 2026-05-18 · 3be7000] Auto-walk doesn't engage at walking range; pickup at walking range overshoots and snaps back **Closed:** 2026-05-18 diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index ba54f0c..81ab9aa 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -71,6 +71,7 @@ | Indoor lighting + rendering — Phase 1 (diagnostics) | Five `[indoor-*]` probes wired through new `AcDream.Core.Rendering.RenderingDiagnostics` static class + DebugVM mirrors + DebugPanel checkboxes. `WbMeshAdapter` emits `[indoor-upload] requested/completed`; `WbDrawDispatcher` emits `[indoor-walk]`, `[indoor-lookup]`, `[indoor-xform]`, `[indoor-cull]` per cell entity. All rate-limited via per-cellId frame counter; lookup probe uses high-bit-tagged key namespace to avoid cross-probe suppression. Holtburg `ACDREAM_PROBE_INDOOR_ALL=1` capture identified 26/123 cells silently failing — confirmed H1 (WB swallowed exception). Spec: [`docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md`](../superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md). Plan: [`docs/superpowers/plans/2026-05-19-indoor-cell-rendering-phase1-diagnostics.md`](../superpowers/plans/2026-05-19-indoor-cell-rendering-phase1-diagnostics.md). Capture: [`docs/research/2026-05-19-indoor-cell-rendering-probe-capture.md`](../research/2026-05-19-indoor-cell-rendering-probe-capture.md). | Tests ✓ | | Indoor lighting + rendering — Phase 2 (fix) | Three-component diagnostic-driven fix for missing-floor bug. Component 1: `WbMeshAdapter` captures the `Task` from `PrepareMeshDataAsync` and attaches a `ContinueWith` for EnvCell ids — surfaces faulted-task exceptions + clean-null returns. Component 2: replaced `NullLogger` with a Console-backed `ConsoleErrorLogger` 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(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(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 ✓ | Plus polish that doesn't get its own phase number: - FlyCamera default speed lowered + Shift-to-boost @@ -224,7 +225,8 @@ Research: R9 + R12 + R13. - **✓ SHIPPED — G.1 — Sky + weather + day-night.** Deterministic client-side from Portal Year time. Sky dome geometry + keyframe gradients + rain/snow particles. See `r12-weather-daynight.md`. Full data + visual stack shipped: Region dat loader, keyframe interp, WeatherSystem with 5-kind PDF + transitions + storm flashes, WorldSession→WorldTimeService sync via ConnectRequest+TimeSync, SkyRenderer with sky-object arcs + UV scroll, rain/snow billboard renderer, F7/F10 debug cycle keys. - **✓ SHIPPED — G.2 — Dynamic lighting.** 8-light D3D-style fixed pipeline. Hard-cutoff at Range, no attenuation inside. Cell ambient. Shader UBO per frame. See `r13-dynamic-lighting.md`. SceneLightingUbo std140 at binding=1 feeds terrain + mesh + mesh_instanced + sky shaders. LightingHookSink auto-registers Setup.Lights at entity stream-in, flips IsLit on SetLightHook, unregisters on landblock unload. -- **G.3 — Dungeon streaming + portal space.** `EnvCellStreamer`, portal-visibility BFS, `PlayerTeleport (0xF751)` handling with `LoginComplete` re-send, "pink bubble" loading state. **Blocked on L.2e** for trustworthy `cell_bsp`, indoor/outdoor portal transit, adjacent-cell ownership, and building entry/exit collision boundaries. See `r09-dungeon-portal-space.md`. +- **Indoor portal-based cell tracking (follow-up to Indoor walking Phase 1 / issue #87).** Replace `PhysicsDataCache.TryFindContainingCell` AABB containment with retail's `CObjMaint::HandleObjectEnterCell` portal traversal. When the player crosses a cell portal boundary, `CellId` propagates through the `CEnvCell` portal connectivity graph. Prerequisite for wall collision from outside (#85) and the remaining #84 threshold symptom. PDB symbols and `acclient.h` `CCellStructure` refs are in place (see #87). **Unblocks G.3.** +- **G.3 — Dungeon streaming + portal space.** `EnvCellStreamer`, portal-visibility BFS, `PlayerTeleport (0xF751)` handling with `LoginComplete` re-send, "pink bubble" loading state. **Blocked on indoor portal-based cell tracking above** (and previously on L.2e) for trustworthy indoor/outdoor portal transit, adjacent-cell ownership, and building entry/exit collision boundaries. See `r09-dungeon-portal-space.md`. **Acceptance:** walk outside at dusk, see the sky gradient + sun moving; enter a torch-lit dungeon via portal; leave back to daylight. diff --git a/docs/research/2026-05-19-cluster-a-shipped-handoff.md b/docs/research/2026-05-19-cluster-a-shipped-handoff.md new file mode 100644 index 0000000..b46fa1e --- /dev/null +++ b/docs/research/2026-05-19-cluster-a-shipped-handoff.md @@ -0,0 +1,256 @@ +# Indoor walking Phase 1 — BSP cluster (Cluster A) — handoff (2026-05-19) + +**Date:** 2026-05-19. +**Branch:** `claude/competent-robinson-dec1f4` (commits land here; merge to main handled by controller). +**Predecessor:** Indoor lighting + rendering Phase 2 (fix) — floors now render in Holtburg Inn. Nine pre-existing indoor bugs surfaced the moment floors were visible; this cluster addresses the collision/interaction subset (#84, #85, #86) and adds diagnostic infrastructure for the follow-up portal-traversal phase. +**Plan:** [`docs/superpowers/plans/2026-05-19-indoor-walking-phase1-bsp-cluster.md`](../superpowers/plans/2026-05-19-indoor-walking-phase1-bsp-cluster.md). + +--- + +## TL;DR + +Cluster A shipped **partially**. Three of the five planned phases (A, B, D) +produced real behavior changes; two (C — obstacle audit — and E — cell-cache +diagnostics) are diagnostic/research phases. The cluster's investigation +confirmed that the wall-collision failures (#84, #85) all root in one cause: +the player's `CellId` is never promoted to an indoor cell during normal +walking, so the indoor-BSP collision branch in `TransitionTypes.FindEnvCollisions` +never fires. Phase D implemented an AABB-containment shortcut that resolves +the specific "spawn inside a building and be stuck above the floor" case but +proved too tight to keep `CellId` promoted through threshold/doorway cells +during normal outdoor→indoor entry. + +**#86** (click selection penetrates walls) is **fully closed** — a clean, +self-contained fix in `WorldPicker`. + +**#84** is **partially closed** — the spawn-in-building symptom is gone; the +remaining wall-collision symptom during normal walking is tracked under the +new **#87**. + +**#85** remains **open**; its root cause is confirmed identical to #84's +remaining symptom and is also tracked under #87. + +**#87** (indoor portal-based cell tracking) is **filed** and ready for the +follow-up phase. + +--- + +## Commits + +| # | SHA | Subject | Phase | +|---|---|---|---| +| 1 | `18a2e28` | `docs(plan): implementation plan written` | Plan doc | +| 2 | `27d7de1` | `feat(physics): Cluster A — indoor BSP collision probe` | Phase A | +| 3 | `3764867` | `fix(picker): Cluster A #86 — cell-BSP ray occlusion in WorldPicker` | Phase B | +| 4 | `4e308d5` | `test(picker): Cluster A #86 — screen-rect cell-occlusion tests` | Phase B follow-up | +| 5 | `c19d6fb` | `fix(physics): Cluster A #84 + #85 — indoor cell tracking` | Phase D | +| 6 | `fda6af7` | `feat(physics): Cluster A — cell-cache diagnostic` | Phase E (1st) | +| 7 | `1f11ba9` | `feat(diag): Cluster A — extend [cell-cache] with AABB + bsphere + recursive poly count` | Phase E (2nd) | + +**Build:** clean on all commits. +**Tests:** `dotnet test` shows the same 8 pre-existing failures in +`AcDream.Core.Tests` (MotionInterpreter / BSPStepUp / etc., unchanged across +the entire cluster). All targeted test projects green. Phase B follow-up +adds screen-rect occlusion tests; Phase D adds `RegisterCellStructForTest` +helper used by caller-side tests. + +--- + +## What shipped + +### Phase A — `[indoor-bsp]` probe + +New `PhysicsDiagnostics.ProbeIndoorBspEnabled` toggle (env var +`ACDREAM_PROBE_INDOOR_BSP` + DebugPanel checkbox under +`ACDREAM_DEVTOOLS=1`). When enabled, logs one `[indoor-bsp]` line each time +`TransitionTypes.FindEnvCollisions` takes the indoor-cell branch — +i.e., when `CellId` is an EnvCell id and the BSP contains physics polys. The +probe serves as a presence detector: if `[indoor-bsp]` never fires during +indoor walking, the BSP is not being consulted at all. + +### Phase B — WorldPicker cell-BSP ray occlusion (closes #86) + +New `CellBspRayOccluder` class (in `src/AcDream.App/Rendering/`) computes +`NearestWallT`: the smallest ray parameter at which the pick ray intersects +any cached EnvCell BSP polygon. Both `WorldPicker.Pick` overloads now accept +an optional `cellOccluder` callback and filter out any hit candidate whose +ray T exceeds `NearestWallT`. The occluder is wired from `GameWindow` using +the `PhysicsDataCache` cell structs that Phase D also extends. + +Before Phase B: clicking through a wall from the outside selected NPCs/items +inside the building — `WorldPicker.BuildRay + Pick` (Phase B.4b) tested only +entity AABBs and scenery BSPs, not EnvCell BSP geometry. + +After Phase B: entities behind the nearest wall from the camera's perspective +are filtered out of the candidate set. Screen-rect unit tests verify the +filter across hit/miss/occlusion scenarios. + +### Phase D — AABB containment for indoor CellId (partial #84 fix) + +`PhysicsEngine.ResolveOutdoorCellId` is extended with an indoor +cell-containment scan. After resolving the outdoor cell, the method checks +whether the player's world position falls inside any cached `CellPhysics` +AABB; if so, `CellId` is promoted to that EnvCell. This enables the +`FindEnvCollisions` indoor-BSP branch. + +New `PhysicsDataCache.TryFindContainingCell(worldPos)` does the AABB scan. +New `CellPhysics.WorldAabb` caches the cell-local AABB in world space on +first call (transforms the BSP bounding sphere's local AABB by the cell +origin). New `RegisterCellStructForTest` helper allows unit test callers to +populate the cache directly. + +Also fixes the L.2e bare-low-byte preservation bug: `ResolveOutdoorCellId` +was silently truncating the player CellId to the low 16 bits; the fix +preserves the full 32-bit value. + +**What this solved:** player spawning inside a building (e.g., logging in +from a position inside Holtburg cottage) no longer sees `walkable=False` for +hundreds of resolves with world Z=94.000. Phase D promotes CellId to the +indoor cell, the floor's BSP polys are found, the player can move. + +**What this did NOT solve:** the `[indoor-bsp]` probe fires only 6 times +during an entire indoor walking session (all mid-jump, when the body happens +to be at a height that falls inside a room AABB). During normal walking on +the floor, the player's world Z is at the AABB floor level or lower — +outside the AABB for threshold/doorway cells that have only a 0.2 m Z range. +See Phase E evidence below. + +### Phase E — Cell-cache diagnostic infrastructure + +Two commits add `[cell-cache]` log output (env var +`ACDREAM_PROBE_CELL_CACHE`, also DebugPanel). For each EnvCell in the +physics cache, the probe logs: + +``` +[cell-cache] id=0xA9B40143 physicsPolyCount=14 bspTotalLeafPolys=14 + bspUnmatchedIds=0 aabbMin=(-11.60,-1.60,0.00) aabbMax=(-6.20,7.60,2.80) + bspOrigin=(0.00,0.00,0.00) bspRadius=9.97 +``` + +The extended second commit adds `bspTotalLeafPolys`, `bspUnmatchedIds`, +`bspOrigin`, and `bspRadius` fields to give a complete picture of cell +geometry from the physics cache perspective. This infrastructure stays in +place as scaffolding for the portal-traversal phase. + +--- + +## Issue status after Cluster A + +| Issue | Status | Notes | +|---|---|---| +| #84 Blocked by air indoors | OPEN (partial) | Spawn-in-building variant resolved by Phase D. Threshold/doorway wall-blocking remains open under #87. | +| #85 Pass through walls outside→in | OPEN | Root cause confirmed as same as #84 remaining symptom. See #87. | +| #86 Click selection penetrates walls | **CLOSED** | Phase B. `WorldPicker.Pick` + `CellBspRayOccluder`. | +| #87 Indoor portal-based cell tracking | OPEN (new) | Filed 2026-05-19. Retail-faithful fix via `CObjMaint::HandleObjectEnterCell`. | + +--- + +## Probe evidence — log file findings + +### `launch-cluster-a-capture.log` + +Initial probe run with `ACDREAM_PROBE_INDOOR_BSP=1`. Result: **zero +`[indoor-bsp]` lines** during outdoor walking and during approach to the +Holtburg cottage doorway. This was the first confirmation that the indoor-BSP +branch was entirely gated out. The player's CellId remained an outdoor cell +for all movement. + +### `launch-cluster-a-verify.log` + +Post-Phase-D run. Observed `[indoor-bsp]` lines **only during jump frames** +(6 total). When the player jumped inside the cottage, the body briefly rose +to a height inside the room AABB, CellId promoted to `0xA9B40143`, and the +indoor-BSP branch fired. On landing, the body returned to floor level, fell +outside the AABB, and CellId reverted to the outdoor cell. Confirmed that +AABB containment works for the room cell when the player is mid-air, but +fails at floor level. + +### `launch-cluster-a-cache-diag2.log` + +First `[cell-cache]` probe run (Phase E first commit). Showed all cached +cells with their physics poly counts and local AABBs. Confirmed 14 physics +polys in cell `0xA9B40143` (the room), indicating BSP geometry is present +and complete. Identified cell `0xA9B40146` as a 4-poly threshold cell. + +### `launch-cluster-a-cache-diag3.log` + +Extended `[cell-cache]` probe run (Phase E second commit). Full data: + +``` +[cell-cache] id=0xA9B40143 physicsPolyCount=14 bspTotalLeafPolys=14 + bspUnmatchedIds=0 aabbMin=(-11.60,-1.60,0.00) aabbMax=(-6.20,7.60,2.80) + bspOrigin=(0.00,0.00,0.00) bspRadius=9.97 +``` +Room cell: 2.80 m AABB height — works for mid-air player. + +``` +[cell-cache] id=0xA9B40146 physicsPolyCount=4 + aabbMin=(-11.60,2.80,-0.20) aabbMax=(-10.00,7.60,0.00) + bspRadius=2.3 +``` +Threshold/doorway cell: 0.20 m AABB Z range (from -0.20 to 0.00). A standing +player at local Z=0.46 m is outside this AABB. **This is why AABB containment +fails for normal walking through doorways.** + +Key conclusion: the geometry is correct and complete (14/14 polys match between +physics cache and BSP leaf count). The problem is purely in the cell-ownership +tracking mechanism, not the collision data itself. + +--- + +## Diagnostic infrastructure remaining in place + +Both probes stay committed and wired. They serve as scaffolding for the +portal-traversal follow-up phase: + +- **`ACDREAM_PROBE_INDOOR_BSP=1`** / DebugPanel "Indoor BSP probe": logs one + `[indoor-bsp]` line each time `FindEnvCollisions` takes the indoor-cell + branch. After portal traversal is implemented, this probe should fire + consistently whenever the player is indoors. + +- **`ACDREAM_PROBE_CELL_CACHE=1`** / DebugPanel "Cell cache probe": dumps all + cached EnvCell physics data (poly counts, BSP bounding sphere, AABB, + unmatched ID count). Useful for verifying that cell structs load correctly + and that portal connectivity data is present. + +Both are gated behind `PhysicsDiagnostics` static class (existing pattern +from L.2a). + +--- + +## Follow-up items for the portal-traversal phase + +**1. Implement portal-based indoor cell tracking (issue #87).** +Replace `PhysicsDataCache.TryFindContainingCell` AABB containment with retail's +`CObjMaint::HandleObjectEnterCell` portal traversal. When the player crosses +a cell portal boundary, `CellId` propagates through `CEnvCell` portal +connectivity data. PDB symbols in `docs/research/named-retail/acclient_2013_pseudo_c.txt` +and struct definitions in `docs/research/named-retail/acclient.h` lines +31715-31726 (`CCellStructure` shape). The retail reference implementation +is the right oracle — do not guess at the traversal algorithm. + +**2. Audit-trail note: add retail PDB symbol citations to `TryFindContainingCell`.** +The current implementation in `src/AcDream.Core/Physics/PhysicsDataCache.cs` +~line 261 is documented as a shortcut. The follow-up phase should add +the PDB symbol citation (e.g., `// retail: CObjMaint::HandleObjectEnterCell +// docs/research/named-retail/acclient_2013_pseudo_c.txt:XXXXX`) +per the Phase D code-review I1 note, so future readers know this is intentionally +replacing an interim implementation. + +**3. Consider renaming `ResolveOutdoorCellId` → `ResolveCellId`.** +The method now handles both outdoor and indoor cell resolution. The rename +is low-risk (one call site in `PhysicsEngine.cs`) and would reduce the +cognitive overhead for the next phase's author. Noted as a Phase D code-review +M2 suggestion — do it in the same commit as the portal-traversal implementation +to keep the rename and the semantic change together. + +--- + +## State at handoff + +- **Branch:** `claude/competent-robinson-dec1f4`, 7 commits of implementation/test/diagnostic work. +- **Build state:** `dotnet build -c Debug` clean. +- **Tests:** 8 pre-existing failures unchanged (MotionInterpreter / BSPStepUp baseline). All new tests green. +- **Issues:** #86 CLOSED; #84 PARTIAL; #85 OPEN; #87 OPEN (new). +- **Diagnostic probes:** `[indoor-bsp]` + `[cell-cache]` active and wired. +- **Next:** portal-based indoor cell tracking (#87) or M2 critical path — Claude's choice per work-order autonomy.