# 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.
