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

171 lines
8.2 KiB
Markdown

# 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](../../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](../../tests/AcDream.Core.Tests/Physics/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](../../tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs)):
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.cs](../../src/AcDream.Core/Physics/CellTransit.cs)
`AddAllOutsideCells` 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](../../tests/AcDream.Core.Tests/Physics/CellTransitAddAllOutsideCellsTests.cs)
and
[CellTransitFindCellSetTests.cs](../../tests/AcDream.Core.Tests/Physics/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
```