docs(handoff): door collision per-part BSP session handoff (2026-05-24)

Long session that shipped A6.P4 infrastructure (Tasks 1-6 of the
per-part BSP plan) but discovered the specific shapes we register
from door setup 0x020019FF don't catch the player. Per-part BSP at
0x010044B5 produced ZERO collision attributions in 188K+ resolve
lines despite player walking at doors. Cylinder still blocks
center-only.

Task 7 (refactor RegisterLiveEntityCollision) was implemented and
visually tested, but reverted because the new per-part BSP shape
didn't actually fix the door bug. The infrastructure stays — it's
correctly modeling retail's CPhysicsObj-with-parts model — but the
shapes we feed it need to be re-investigated apparatus-first.

Three hypotheses ranked: (A) part BSPs are visual-only, no collision
polys; (B) building BSP has a wide doorway gap our tiny cylinder
doesn't fill; (C) retail uses Setup.Radius/Height directly. Next-
session move: dump GfxObj 0x010044B5's PhysicsBSP first, then cdb
retail at a doorway.

Recommends: stop speculation, build apparatus, decide fix from
evidence.

#99 stays OPEN. Slice 1's "Closes #99" claim was premature; the real
close requires the per-part BSP work + correct shape identification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-24 18:16:02 +02:00
parent 1498697bc5
commit c89df8e4c0

View file

@ -0,0 +1,265 @@
# Door collision per-part BSP session — handoff
**Date:** 2026-05-24 (long session, multiple phases)
**Branch:** `claude/strange-albattani-3fc83c`
**Worktree:** `C:\Users\erikn\source\repos\acdream\.claude\worktrees\strange-albattani-3fc83c`
This handoff documents an A6.P4-driven session that:
1. Shipped A6.P4 slice 1 (real cleanup, didn't close #99)
2. Investigated why doors don't block (apparatus-first)
3. Brainstormed + speced a per-part BSP collision design
4. Shipped most of the implementation (Tasks 1-6 of 10)
5. Discovered Task 7's per-part BSP doesn't actually fix the door bug
6. Reverted Task 7 and paused for further investigation
---
## TL;DR
**Shipped (real commits):**
- `b49ed90` — A6.P4 slice 1: drop the `< 0x0100u` filter in
`ShadowObjectRegistry.GetNearbyObjects`'s portalReachableCells loop,
rename `indoorCellIds``portalReachableCells`. Real cleanup; the
`FindCellSet`-already-includes-outdoor-cells discovery means doors at
building thresholds should be reachable from indoor primary spheres
via the exit-portal logic. But the user-visible #99 close was wrongly
claimed in the commit message — see below.
- `d71ceab` — design spec: per-part BSP for server-spawned entities.
- `8d4f14c` — 10-task implementation plan.
- `ab4278c` — Task 1: ShadowShape record.
- `7f5c287` — Task 2: ShadowShapeBuilder.FromSetup + 7 unit tests.
- `1454eab` — Task 3: ShadowEntry adds LocalPosition + LocalRotation.
- `fca0a13` — Task 4: ShadowObjectRegistry.RegisterMultiPart + 6 tests +
Deregister clears `_entityShapes` (Task 6 folded in).
- `d5ffb03` — Task 5: UpdatePosition recomposes multi-part transforms
via `_entityShapes`.
- `3e5dc8c` — Task 6 regression test: stray UpdatePosition after
Deregister is no-op.
- `1498697``[cyl-test]` diagnostic probe (broadly useful).
**Reverted (Task 7 staged, then `git restore`):** the
`RegisterLiveEntityCollision` refactor at `GameWindow.cs:3076`. Reverted
because visual verification showed the per-part BSP shape didn't actually
block the door — only the small Cylinder did, and even that only at
dead-center approach.
**Still pending:** Tasks 7-10 in the plan + the real fix for door
collision.
---
## What we learned (apparatus-first findings)
### Door Setup 0x020019FF shape inventory (live dump captured 2026-05-24)
```
[door-setup-dump] setupId=0x020019FF setupRadius=0.141 setupHeight=0.200
cylSpheres=0 spheres=1 parts=3 placementFrames=1
stepUp=0.090 stepDown=0.090
[door-setup-dump] sphere[0] r=0.100 origin=(0.000,0.000,0.018)
[door-setup-dump] part[0] gfxObj=0x010044B5
[door-setup-dump] part[1] gfxObj=0x010044B6
[door-setup-dump] part[2] gfxObj=0x010044B6
```
### Per-shape registration (post-Task-7-experiment)
With `ShadowShapeBuilder.FromSetup` running over Setup 0x020019FF in the
live launch, doors registered 2 shadows each:
1. `type=Cylinder radius=0.100 height=0.200 localPos=(0,0,0.018)` — from
the Sphere converted to short Cylinder.
2. `type=BSP gfxObj=0x010044B5 radius=2.000 localPos=(-0.006,0.125,1.275)`
from part 0 (the frame). The other two parts (`0x010044B6` x2) have
`BSP=null` → skipped.
### Collision behavior (visual verified by user, 2026-05-24)
| Scenario | Result |
|---|---|
| Cellar climb (#98 regression check) | ✅ Works |
| Door from outside, dead center | ⚠️ Partial — only the small Cylinder blocks; player stops at the center |
| Door from outside, ~50 cm off-center | ❌ Pass through |
| Door from outside (Use → swing) | ✅ Swing animation works, door opens |
| Indoor furniture (#91 regression check) | ✅ Works |
| Outdoor exterior wall (regression check) | ✅ Works |
| Door from inside walking out | ❌ Pass through |
### Diagnostic evidence
In 188K+ resolve lines from the launch:
- `Door 0xF4249 : 85 cyl-tests, 13 resolve hits attributed`
- `Door 0xF424F : 227 cyl-tests, 16 resolve hits attributed`
- **Zero `[resolve-bldg]` attributions for any door**
Conclusion: the per-part BSP at `0x010044B5` produces NO collision hits.
Either:
1. The PhysicsBSP at that GfxObj has no collision-bearing polygons
(only visual polys), OR
2. Our world-to-part-local sphere transform is wrong, OR
3. The broadphase rejects it (unlikely with radius=2.0 default).
---
## Why this differs from M1 visual verification on 2026-05-13
The user remembers doors blocking on the M1 demo verification. That
demo was "open the inn door" — clicking + watching the swing animation.
The walking-through-an-open-door part was not deliberately tested. The
closed-door blocking was probably observed accidentally when the user
walked directly at a center-of-doorway cylinder; the 14 cm cylinder is
just wide enough to catch a sphere at exactly the centerline. Today's
careful off-center test exposed the gap.
So nothing regressed since 2026-05-13. The bug has been latent. Our
investigation just exposed it.
---
## Investigation gap to close before the next implementation attempt
The per-part BSP design IS retail-faithful in shape (matches
`CPhysicsObj::FindObjCollisions``CPartArray::FindObjCollisions`
`CPhysicsPart::find_obj_collisions``CGfxObj::find_obj_collisions`).
But it didn't surface a working blocker for the cottage doors. Three
hypotheses, ranked by likelihood:
### Hypothesis A (most likely): Part 0x010044B5 has no collision-bearing PhysicsBSP polygons
The Setup defines visual parts. Some parts (especially decorative
hardware) may have a PhysicsBSP that's just the visual mesh's bounding
volume, with no walls or threshold polygons. The door's collision might
genuinely be just the small Cylinder by retail design, and retail
gets full doorway blocking from the **building's BSP** having a narrow
gap exactly the size of the door's Cylinder (~28 cm × 28 cm).
**How to verify:** Dump `0x010044B5`'s PhysicsBSP polygons via
`ACDREAM_DUMP_GFXOBJS=0x010044B5`. Inspect the polygons. If they're
just an axis-aligned bounding box matching the visual mesh, no useful
collision data exists at the part level.
### Hypothesis B: Building BSP has a wide doorway gap that retail's tiny cylinder doesn't fill
A retail building (e.g., cottage interior 0x020XXXXX) has its walls as
BSP polygons. The doorway is a gap. If the gap is ~2 m wide (visual
opening), the 28 cm cylinder doesn't span it — even retail wouldn't
block.
**How to verify:** Open RenderDoc on retail (or our client) and inspect
the cottage interior GfxObj BSP at the doorway. Measure the gap. If
it's narrow (~30 cm), the small cylinder fills it. If wide (~2 m), the
cylinder is decorative and the actual blocker must come from elsewhere.
### Hypothesis C: Retail uses a different collision mechanism entirely
Doors might use Setup.Radius / Setup.Height (the bounding cylinder
dimensions, 0.141 × 0.200 — slightly larger than our Sphere-derived
0.100 × 0.200) AS THE PRIMARY BLOCKER, not the Sphere. Or retail
overrides shape selection for `ItemType==Door` specifically.
**How to verify:** Attach cdb to a live retail client at a cottage
doorway, set a breakpoint on `CPhysicsObj::FindObjCollisions` for the
door's PhysicsObj, observe which shape branch fires.
---
## Recommended next-session approach
Per the project's "apparatus-first for physics divergences" rule
(`feedback_apparatus_for_physics_bugs.md`):
1. **Stop coding.** Don't try another fix without evidence.
2. **Dump 0x010044B5's PhysicsBSP** via `ACDREAM_DUMP_GFXOBJS=0x010044B5`.
If it has zero floor-touching polygons → Hypothesis A confirmed.
3. **Attach cdb to retail** at a cottage doorway. Trace which shapes
block the player. See `project_retail_debugger.md` for the toolchain.
4. **Cross-reference ACE source** for Door collision (if any) — search
`references/ACE/Source/ACE.Server/Physics/` for door handling.
5. **Re-brainstorm** with the new evidence. The Task 1-6 infrastructure
stays (it's correctly modeling retail's CPhysicsObj-per-entity
with parts iterated for collision). Only the SHAPES we register
need to change.
The infrastructure investment was not wasted. The architecture is right.
We just registered the wrong shapes from the door setup.
---
## What's in the tree right now
```
$ git log --oneline -15
1498697 diag(phys): [cyl-test] probe — log every Cylinder shadow collision test
3e5dc8c test(phys): Task 6 regression — Deregister clears _entityShapes cache
d5ffb03 feat(phys): UpdatePosition handles multi-part entities
fca0a13 feat(phys): ShadowObjectRegistry.RegisterMultiPart
1454eab feat(phys): ShadowEntry adds LocalPosition + LocalRotation
7f5c287 feat(phys): ShadowShapeBuilder.FromSetup
ab4278c feat(phys): add ShadowShape record (no callers yet)
8d4f14c docs(phys): implementation plan — per-part BSP for server-spawned entities
d71ceab docs(phys): design spec — per-part BSP collision for server-spawned entities
b49ed90 feat(phys): A6.P4 slice 1 — portal-reachable cellSet includes outdoor cells
b3ce505 fix(phys): A6.P3 #98 — gate outdoor shadow radial sweep on indoor primary cell
b55ae83 docs: A6.P3 #98 resolution + A6.P4 design + #99/#100 filed
3e3cd77 docs(handoff): A6.P4 pickup handoff — full session-resume artifact
```
All 49+ tests pass:
- 24 ShadowObjectRegistryTests
- 7 ShadowShapeBuilderTests
- 8 ShadowObjectRegistryMultiPartTests
- 11 CellarUpTrajectoryReplayTests
Pre-existing 6-8 baseline static-state-leakage failures in the broader
Physics suite are unchanged from prior sessions.
**No-commit state:** working tree is clean. `git status --short`
shows only untracked investigation logs (`a6-issue98-*.log`,
`launch-task7-*.log`, etc. — these accumulate from launches and don't
get committed).
---
## #99 status: still open
The A6.P4 slice 1 commit message claimed "Closes #99" but the visual
verification today proves that's premature. Slice 1 did a real cleanup
(removed a misleading filter) but didn't fully address the user-visible
door-block bug. Update `docs/ISSUES.md` accordingly (issue #99 remains
OPEN; the per-part BSP architecture is NEW infrastructure built today
that will support the eventual fix once we identify the right shapes).
---
## Pickup prompt for next session
```
Door collision still doesn't fully block in M1.5 Holtburg. Per-part BSP
infrastructure shipped 2026-05-24 (Tasks 1-6 of A6.P4 plan), but the
specific shapes we register from door setup 0x020019FF don't catch the
player. Need apparatus-first investigation:
Read docs/research/2026-05-24-door-collision-session-handoff.md
(this doc — recent session handoff)
State both altitudes:
Currently working toward: M1.5 — Indoor world feels right
Current phase: A6.P4 — investigation phase to find the right door
collision shapes; per-part BSP infrastructure
already shipped; need to verify Hypothesis A/B/C
before any more implementation
First moves (in order):
1. Dump GfxObj 0x010044B5's PhysicsBSP via ACDREAM_DUMP_GFXOBJS.
Does it have collision-bearing polygons or just visual?
2. If yes → debug the per-part transform (likely Hypothesis B/C
wrong); if no → confirm Hypothesis A and pivot strategy.
3. Either way, attach cdb to retail at a cottage doorway to see
what retail actually blocks with.
DO NOT speculate-and-fix again. The session 2026-05-24 already
burned a Task 7 attempt on a hypothesis that turned out wrong. The
6 committed implementation tasks (Tasks 1-6) are correct and stay.
Only Tasks 7-10 of the plan need to change once we know the right
shapes.
```