acdream/docs/research/2026-05-24-door-collision-session-handoff.md
Erik c89df8e4c0 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>
2026-05-24 18:16:02 +02:00

11 KiB
Raw Blame History

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 indoorCellIdsportalReachableCells. 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::FindObjCollisionsCPartArray::FindObjCollisionsCPhysicsPart::find_obj_collisionsCGfxObj::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.


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.