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>
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:
-
Live capture (
door-walkthrough.jsonlfrom previous session; not committed): 24,310 records ofPhysicsEngine.ResolveWithTransitioncalls including PhysicsBody snapshots before/after. -
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.
-
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 (AddAllOutsideCellsreturned empty when given landblock-local sphere coords). Now passes after fix.
-
Dat-direct cell-portal inspector (DoorSetupGfxObjInspectionTests.HoltburgCottage_CellPortals_DatInspection): reads
EnvCell+Environment.Cells+ portalPolygon.Planefrom the real dat for cells0xA9B40150(doorway alcove),0xA9B4013F(cottage interior),0xA9B40029(outdoor — confirmed NOT EnvCell). Output: cell0xA9B40150HAS a 0xFFFF exit portal at poly0x0005with planen_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.29within±rad=0.5→ straddles) confirmedexitOutsideSHOULD fire — butAddAllOutsideCellsthen silently dropped the outdoor cell.
The fix
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 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_polyfamily.
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