acdream/docs/research/2026-05-25-door-bug-partial-fix-shipped.md
Erik 28cd97be62 fix(phys): A6.P4 door bug — AddAllOutsideCells coord convention + replay apparatus
CellTransit.AddAllOutsideCells assumed sphere coords were absolute world
coords (subtracting lbXf = 0xA9 * 192 = 32448 from the sphere position).
Production has used landblock-local coords since Phase A.1
(streaming-center landblock at world origin), so the subtraction
produced localX = -32316, gridX = -1346 → out-of-range → early return
→ ZERO outdoor cells added.

For outdoor primary cells the bug was masked by GetNearbyObjects's
radial sweep. For indoor primary cells (where #98 gates the outdoor
sweep), the door's outdoor cell 0xA9B40029 never reached
portalReachableCells, the door's BSP was never queried, and the player
walked through Holtburg cottage doors unimpeded.

Fix: AddAllOutsideCells treats worldSphereCenter as landblock-local
directly. Matches retail CLandCell::add_all_outside_cells which uses
the per-cell 6-byte landblock-relative position struct.

Existing CellTransitAddAllOutsideCellsTests + CellTransitFindCellSetTests
updated to use landblock-local sphere coords (they were the only callers
using the world-coord convention; production never did).

Apparatus shipped:
- DoorBugTrajectoryReplayTests — live-capture-driven replay harness
  that pinpointed the bug per-field at unit-test speed (<500ms iteration)
- AddAllOutsideCells_LandblockLocalSphere_AddsDoorOutdoorCell — direct
  unit test that demonstrates the fix
- FindTransitCellsSphere_IndoorExitPortal_AddsOutsideForCapturedSpherePos
  — verifies cell-portal traversal at the captured sphere position
- DoorSetupGfxObjInspectionTests.HoltburgCottage_CellPortals_DatInspection
  — dat-direct EnvCell + Environment.Cells + portal-poly inspector
- Fixture: tests/AcDream.Core.Tests/Fixtures/door-bug/live-capture.jsonl
  (tick 13558 walkthrough + tick 22760 outdoor block)

Visual verification (user-driven at Holtburg cottage door, ~50cm off-center):
- outside→inside RUN: now BLOCKS (was: walks through)
- outside→inside WALK: presumed blocks (not retested)
- inside→outside RUN: PARTIAL — body intersects door, sphere slides through
- inside→outside WALK: same partial behavior

The remaining inside→outside asymmetry is a SEPARATE bug in BSP
collision response for two-sided polygons. The [bsp-test] probe now
fires 245 times for the door entity from indoor (was 0 pre-fix) —
door IS being queried; the BSP polygon-level collision response is
the new bug. Handoff at
docs/research/2026-05-25-door-bug-partial-fix-shipped.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:53:34 +02:00

8.2 KiB

Door bug — partial fix shipped (cell visibility), inside-out asymmetric collision remains

2026-05-25

TL;DR

Major root cause closed. CellTransit.AddAllOutsideCells was silently failing for every production caller because it assumed sphere positions were in absolute world coordinates (subtracting the landblock's "absolute" world origin lbXf = 0xA9 * 192 = 32448), while production has used landblock-local coordinates since Phase A.1 (streaming-center landblock at world origin → lbOffset = (0, 0)). For outdoor primary cells the bug was masked by GetNearbyObjects's radial sweep. For indoor primary cells (where issue #98's gate skips the outdoor sweep), it meant outdoor cells were never added to portalReachableCells → cottage door's outdoor cell 0xA9B40029 invisible from indoor cell 0xA9B40150 → door's BSP never queried → player walked through.

Outside→inside now blocks correctly. Inside→outside REMAINS BROKEN asymmetrically. Body partially intersects the door, slides through visibly. Not retail-faithful. This is a SEPARATE bug in BSP-collision-response for two-sided polygons — to investigate next session.

Apparatus shipped

Full trajectory-replay harness:

  1. Live capture (door-walkthrough.jsonl from previous session; not committed): 24,310 records of PhysicsEngine.ResolveWithTransition calls including PhysicsBody snapshots before/after.

  2. Fixture extraction (tests/AcDream.Core.Tests/Fixtures/door-bug/live-capture.jsonl, 4 KB): tick 13558 (the walkthrough) + tick 22760 (the working outdoor block) as representative records.

  3. Replay harness (DoorBugTrajectoryReplayTests.cs):

    • LiveCompare_* tests load the failing tick + replay through the harness + diff result fields vs captured live values.
    • FindTransitCellsSphere_IndoorExitPortal_AddsOutsideForCapturedSpherePos — direct unit test for cell-portal traversal at the captured sphere position. PASSES (cell graph is correct).
    • AddAllOutsideCells_LandblockLocalSphere_AddsDoorOutdoorCell — direct unit test that pinpointed the root cause. Initially failed (AddAllOutsideCells returned empty when given landblock-local sphere coords). Now passes after fix.
  4. Dat-direct cell-portal inspector (DoorSetupGfxObjInspectionTests.HoltburgCottage_CellPortals_DatInspection): reads EnvCell + Environment.Cells + portal Polygon.Plane from the real dat for cells 0xA9B40150 (doorway alcove), 0xA9B4013F (cottage interior), 0xA9B40029 (outdoor — confirmed NOT EnvCell). Output: cell 0xA9B40150 HAS a 0xFFFF exit portal at poly 0x0005 with plane n_local=(0, +1, 0), d_local=+5.6. The sphere-vs-plane math (sphere world (132.36, 16.81, 94) → local (-1.86, -5.31, 0) via 180° Z rotation → dist = +0.29 within ±rad=0.5 → straddles) confirmed exitOutside SHOULD fire — but AddAllOutsideCells then silently dropped the outdoor cell.

The fix

src/AcDream.Core/Physics/CellTransit.csAddAllOutsideCells no longer subtracts the landblock's "absolute" world origin from the sphere position. Treats worldSphereCenter as landblock-local directly (matching retail's CLandCell::add_all_outside_cells which uses the per-cell 6-byte position struct, and matching production's universal convention since Phase A.1).

Existing tests in CellTransitAddAllOutsideCellsTests.cs and CellTransitFindCellSetTests.cs updated to use landblock-local sphere coords (they were the only callers using the world-coord convention; production never did).

Visual verification

User tested all four combinations at a closed Holtburg cottage door, ~50cm off-center:

Direction Speed Pre-fix Post-fix
outside → inside RUN walks through BLOCKS
outside → inside WALK walks through (presumed BLOCKS — not retested)
inside → outside RUN walks through PARTIAL ⚠️ body intersects door, sometimes through
inside → outside WALK walks through PARTIAL ⚠️ same as run

User quote: "We have partial blocking from inside out. Can get through some times. However, char is blocked a bit through the door. So for example if I'm running towards this from the inside, I can see parts of the body getting blocked a bit in to the door. This is not per retail behavior and this is not how it looks when its block from the outside".

The asymmetry is the new diagnostic: outside-in produces a clean block (no body-into-door intersection visible); inside-out produces a partial block with visible body intersection. This is the signature of an asymmetric collision response to the door slab's two-sided polygons (SidesType=Landblock), or a BSP query that handles sphere-already-overlapping-slab differently from sphere-approaching-slab.

The [bsp-test] probe fires 245 times for the door entity during the post-fix inside-out attempts — door IS being queried. The collision-detection mechanics produce the wrong response.

What's next (separate bug)

Investigate BSPQuery.FindCollisions's response for two-sided polygons when the sphere is already overlapping the slab. Retail's CBSPTree::find_collisions family handles this specifically — the sphere's path through the slab faces gets traced and the FIRST face crossed in motion direction is the collision. With two-sided polygons, both faces are collidable; the front-vs-back determination is by sphere-velocity vs face-normal dot product.

Likely files:

  • src/AcDream.Core/Physics/BSPQuery.cs — the BSP traversal + sphere-poly intersection logic.
  • Retail decomp anchors: acclient_2013_pseudo_c.txt:BSPTREE::find_collisions + SPHEREPATH::sphere_intersects_poly family.

Apparatus to write next: a focused test that registers the door at its actual production world transform (entity origin + partFrame offset from the dat, with correct rotation) and replays a sphere passing through it from EACH side at various speeds. Compare collision normal

  • position-resolution per side. The asymmetric response will be reproducible at unit-test speed.

Commits

[List the commit SHAs of the apparatus + fix once landed.]

Pickup prompt for the next session

Door bug — major root cause closed (CellTransit.AddAllOutsideCells
landblock-local coord convention). Outside→inside now blocks. But
inside→outside has asymmetric BSP collision response: body partially
intersects the door slab, sphere slides through. Same behavior at run
+ walk speed. Bug is in BSP collision response for two-sided polygons
or sphere-already-overlapping-slab handling.

  Read docs/research/2026-05-25-door-bug-partial-fix-shipped.md

  State both altitudes:
    Currently working toward: M1.5 — Indoor world feels right
    Current phase: A6.P4 door bug — inside-out asymmetric BSP collision
                   response. Apparatus is shipped (DoorBugTrajectoryReplayTests).
                   First major root cause closed. Remaining bug is in
                   BSP-collision-response mechanics, not cell visibility.

  First move: extend the existing DoorBug apparatus with a more
  faithful door registration (entity at the actual production world
  pos + correct rotation; use the partFrame from the dat). Then write
  TWO directional tests: sphere approaching the slab from the south
  (outside-in) and sphere approaching from the north (inside-out).
  Compare cn normal + resolution for each. The asymmetric response
  will reproduce at unit-test speed. From there, inspect
  BSPQuery.FindCollisions's handling of two-sided polygons and
  sphere-already-overlapping cases. Retail oracle:
  CBSPTree::find_collisions family at acclient_2013_pseudo_c.txt.

  DO NOT:
    - Re-investigate cell visibility (closed by AddAllOutsideCells fix)
    - Re-do the registration shape (multi-part registration is correct)
    - Speculate on the BSP fix without apparatus