# L.2d slice 1 + 1.5 shipped — handoff **Date:** 2026-05-13 evening, immediately after slice 1.5 + Holtburg verification. **Branch:** `claude/sharp-chatelet-023dda` (ready to merge to main). **Predecessor:** [2026-05-12-l2a-shipped-l2d-handoff.md](2026-05-12-l2a-shipped-l2d-handoff.md). --- ## TL;DR The "I can't walk through Holtburg doorways" symptom is **a closed Door entity blocking the threshold**, not a building-collision-mesh bug. Building BSP collision is healthy. The L.2a handoff's framing ("per-cell walkability missing") was wrong, the L.2d-slice-1 spec's reframe ("BSP shape fidelity, three hypotheses X/Y/Z") was also wrong, and the actual answer fell out of one capture once the probe labeling was fixed (slice 1.5). **L.2d as scoped is essentially closed.** The remaining work is door-state handling — a different sub-phase entirely. --- ## What shipped on this branch | Commit | What | |---|---| | [`92cd723`](.) | `docs(phys L.2d): design spec for slice 1 BSP-hit diagnostic + L.2d reframe` | | [`66dc23e`](.) | `feat(phys L.2d slice 1): BSP-hit diagnostic probe + plan-of-record correction` | | [`8bacef0`](.) | `fix(phys L.2d slice 1.5): probe captures hit poly under StepSphereUp recursion` | What slice 1 + 1.5 give the next agent: - **`ACDREAM_PROBE_BUILDING=1`** env var + DebugPanel checkbox: one multi-line `[resolve-bldg]` entry per attributed BSP shadow-entry hit (partIdx, hasPhys, bspR vs vAabbR, world-space entOrigin_lb, actual hit polygon vertices in both local and world coords). Reliable under `StepSphereUp` recursion after the slice 1.5 fix. - **`[entity-source]`** one-time log line per `ShadowObjects.Register` call, gated on the same flag. Makes `entityId=0xA9B479` in a probe line greppable to its WorldEntity source. - **`PhysicsDiagnostics.LastBspHitPoly`** — diagnostic side-channel for any future "what poly did BSPQuery hit" question. - **The two synthetic tests** in [PhysicsDiagnosticsTests.cs](../../tests/AcDream.Core.Tests/Physics/PhysicsDiagnosticsTests.cs) pin the side-channel API contract. --- ## What the trace actually showed After slice 1.5, walking acdream into a Holtburg town doorway captured 242 real BSP hit polys + 122 cylinder n/a. **Definitive finding:** ``` live: spawn guid=0x7A9B4015 name="Door" setup=0x020019FF pos=(132.6,17.1,94.1)@0xA9B40029 itemType=0x00000080 [entity-source] id=0x000F4244 entityId=0x000F4244 src=0x020019FF gfxObj=0x020019FF lb=0xA9B40029 type=Cylinder note=server-spawn-root ``` The blocker is a **Door entity** — Setup `0x020019FF` named `"Door"` — server-spawned by ACE at the threshold of each Holtburg town building. **Five Doors** appear across Holtburg (landblock cells `0xA9B40029`, `0xA9B40154`, `0xA9B40155`); same Setup DID reused. ItemType `0x00000080` = Misc category in AC's ItemType flags. Each Door's Cylinder collision blocks the player. The building BSP *also* fires (the L.2a evidence the original handoff pointed at), but the BSP hits were the player **already pushed back by the Door cylinder** then grazing the doorframe — they look like wall collision but are a side effect of the Door cylinder push. Slice 1.5's per-tick multi-entity probe revealed this by showing `nObj=3` on every hit resolve: one Door + two sphere checks against the building BSP. The L.2a slice 2 handoff's expectation that doors would be in the `0xCC0Cxxxx` range was wrong; **doors are in `0x000Fxxxx`** (server- spawn-root range) because they're hydrated through the live `CreateObject` stream like NPCs, not the static landblock pipeline. --- ## What this means for L.2d L.2d as originally scoped ("Shape Fidelity: Sphere / CylSphere / Building Objects") is essentially **closed at this site**: - Building BSP is loaded, parsed, queried correctly. `bspR=13.99m` for GfxObj `0x01000A2B`, real triangles in real positions. - `Setup.CylSpheres` for Door (`0x020019FF`) is also loaded correctly — the cylinder is firing the cylinder collision path with sensible world-space radius. - No actual shape-fidelity bug observed at this test site. The remaining work is **door state handling**, which is a different class of problem entirely — it touches network (CreateObject PhysicsState bits), interaction (Use action on door entity), animation (door open/close animation state), and collision-state-toggle (ETHEREAL during open animation). That doesn't fit under L.2d's shape-fidelity umbrella. **Recommend reframing L.2d as "watch-and-wait":** keep the probes for future shape-fidelity work at other sites (dungeon walls, stairs, roofs), but don't plan more slices until a NEW shape-fidelity bug is observed with the probe-armed client. --- ## Side findings (latent bugs to file, not block this slice) ### 1. Building double-registration The trace shows the same WorldEntity registered TWICE in ShadowObjectRegistry: ``` [entity-source] id=0xA9B47900 entityId=0xC0A9B479 ... type=BSP note=partIdx=0 hasPhys=true [entity-source] id=0xC0A9B479 entityId=0xC0A9B479 ... type=Cylinder note=mesh-aabb-fallback ``` [GameWindow.cs:5625](../../src/AcDream.App/Rendering/GameWindow.cs:5625) gates the mesh-AABB-fallback on `entityBsp == 0`, but the BSP registration at [line 5530](../../src/AcDream.App/Rendering/GameWindow.cs:5530) DOES increment `entityBsp`. So the fallback shouldn't fire when BSP parts exist. Either `entityBsp` isn't being checked in the right scope, or there's a second mesh-AABB-fallback site that doesn't gate on `entityBsp`. Worth a short investigation + one-line fix. Filing as ISSUE candidate. Doesn't break anything observable yet (cylinder is too far from player to fire at this Holtburg site), but will cause confusion in any future "why does entity X have two ShadowEntries" trace. ### 2. PhysicsState / EntityCollisionFlags not in entity-source log The slice 1 `[entity-source]` log captures `id, entityId, src, gfxObj, lb, type, note, hasPhys` but **not** `state` (PhysicsState bits) or `flags` (EntityCollisionFlags). For any future ethereal-handling / IGNORE_COLLISIONS work — including the door state handling above — these would be required. Tiny slice 1.6 if the next agent needs them: add `state=0x{...:X8} flags={...}` to the format string. ~5 LOC, gated on the same ProbeBuilding flag. --- ## What the next session probably should NOT do - **Re-investigate Holtburg doorways with the same setup.** The evidence is conclusive; we're not going to find new information by re-running the probe at the same site. - **Port `CBuildingObj` or per-cell walkability infrastructure.** That was based on the original (wrong) hypothesis. ACE's `find_building_collisions` is six lines and doesn't use per-cell walkability; our equivalent is already in place implicitly. - **Start L.2d slice 2 as scoped in the design spec.** Hypotheses X / Y / Z don't apply — the trace ruled them all out. Update or close the spec. --- ## What the next session COULD do (in rough preference order) These are NOT prescribed; they're candidates for the project-level ordering discussion the user wants to have. 1. **Door state handling sub-phase.** New phase (call it L.2g or nest under B.4). Touches: Use action → server door toggle, PhysicsState ETHEREAL bit honor, door open/close animation, collision-shape suppression during open animation. Probably 2-3 commits. 2. **Fix the building double-registration latent bug** (side finding #1). One-liner, no real impact today but cleaner trace later. 3. **Capture slice 1.6** (state + flags in entity-source log) if any future ethereal-related work is on the immediate horizon. Otherwise defer. 4. **Move to a different L.2 sub-phase entirely** — L.2e (cell ownership / `find_cell_list` / outdoor seam updates) or L.2f (real-DAT + retail-observer conformance). Both are scoped in [the L.2 plan-of-record](../plans/2026-04-29-movement-collision-conformance.md). 5. **Triage the 8 pre-existing test failures** that have shadowed the last few sessions. Some are in physics modules that L.2d slice 2 (if it ever happens) would touch — fixing them first gives a cleaner baseline. 6. **Pick from CLAUDE.md's "Next phase candidates"** list — non-L.2 work like Phase C visual fidelity, N.6 slice 2, or perf tiers 2/3. The session-level "I don't know what to do" feeling is often easier to resolve by **shipping something in a different area** for a session. --- ## Reproducibility Same recipe as L.2a + L.2d slice 1: ```powershell $env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call" $env:ACDREAM_LIVE = "1" $env:ACDREAM_TEST_HOST = "127.0.0.1" $env:ACDREAM_TEST_PORT = "9000" $env:ACDREAM_TEST_USER = "testaccount" $env:ACDREAM_TEST_PASS = "testpassword" $env:ACDREAM_DEVTOOLS = "1" $env:ACDREAM_PROBE_CELL = "1" $env:ACDREAM_PROBE_RESOLVE = "1" $env:ACDREAM_PROBE_BUILDING = "1" dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "launch-l2d.log" ``` Walk acdream toward any Holtburg building threshold. Hit `Ctrl+F2` to toggle collision wireframes — you'll see the Door cylinder right at the threshold. The `name="Door"` line appears in the log at startup during the `CreateObject` stream replay. --- ## Open questions / unresolved - **What `PhysicsState` bits is ACE sending for the Door entity?** Not captured in current logs. Slice 1.6 would answer this. - **Are these doors *supposed* to be open by default in retail?** If yes, ACE config issue. If no, retail clients see the same blocker and players had to open them manually. - **What does ACE's door-state state machine look like?** Probably documented in `references/ACE/Source/ACE.Server/Entity/Door.cs` or similar. These are doors-and-ACE-side questions; defer to the door-state sub-phase when (if) it gets scoped. --- ## Worktree state at handoff - All three slice 1 / 1.5 commits ready to merge to main. - WorldBuilder submodule initialized + 6 directory junctions in place for the gitignored peer reference dirs (created during slice 1 prep). Worktree builds clean. - Three test artifacts (`launch-l2d-slice1.log`, `launch-l2d-slice1b.log`, `launch-l2d-slice1c.log`) are in working tree but **not committed** — they're large and ephemeral. Delete or preserve at the merge author's discretion.