diff --git a/docs/research/2026-05-24-door-collision-session-handoff.md b/docs/research/2026-05-24-door-collision-session-handoff.md new file mode 100644 index 0000000..604dba8 --- /dev/null +++ b/docs/research/2026-05-24-door-collision-session-handoff.md @@ -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. +```