acdream/docs/research/2026-05-25-door-bug-partial-fix-shipped.md
Erik 85a164f4a8 fix(test): correct geometric pin test for door slab Z math
The Geometric_DoorSlabZRange_AbovePlayerSphereTop test was computing
slabWorldZBottom as (entity.Z + partFrame.Z) — assuming the slab's
local Z=0 was its bottom. Actually checking the dat shows the slab's
PhysicsPolygons local AABB is min=(-0.954, -0.134, -1.236) max=(0.971,
0.127, 1.255) — the slab's local origin is at its GEOMETRIC CENTER,
not the bottom. With partFrame.Z=1.275 lifting the origin, the slab
world Z is actually [94.139, 96.630], not [95.375, 97.865].

Corrected test now computes both slabLocalZMin and slabLocalZMax from
the polygon vertices and asserts the opposite (correct) geometric fact:
the slab IS at sphere height — overlap from Z=94.139 to Z=95.20 (1.061
m of vertical overlap with the player's sphere). The slab is NOT a
lintel that misses the sphere; it should collide.

Test renamed: Geometric_DoorSlabZRange_AbovePlayerSphereTop →
Geometric_DoorSlabAtSphereHeight_OverlapsInZ.

Handoff doc 2026-05-25-door-bug-partial-fix-shipped.md updated with
the corrected analysis. The "next investigation candidates" list now
points toward cdb attach to retail as the highest-ROI option, since
the BSP collision IS active at sphere height but production still
shows asymmetric walkthrough behavior. The bug is in either the
GetNearbyObjects coverage at primary-cell boundaries, the BSP
polygon partial-overlap handling, or missing cell-BSP collision for
cottage doorway walls.

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

11 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)

Investigation status (corrected 2026-05-25 late evening). Two new directional tests + a geometric pin test all PASS:

  • Directional_OutsideIn_SouthApproach_BlocksAtSlabSouthFace PASSES.
  • Directional_InsideOut_NorthApproach_BlocksAtSlabNorthFace PASSES.
  • Geometric_DoorSlabAtSphereHeight_OverlapsInZ PASSES.

The geometric test reveals (correctly computed this time):

Setup 0x020019FF (cottage door) PhysicsPolygons local AABB:
  min=(-0.954, -0.134, -1.236) max=(0.971, 0.127, 1.255)
  (slab origin at GEOMETRIC CENTER, not the bottom)

partFrame[0].Origin = (-0.006, 0.125, 1.275) → lifts slab origin
                                                 1.275 m above entity Z

With entity at world (132.6, 17.1, 94.1) + 180° entity rotation:
  partWorldPos = (132.606, 16.975, 95.375)

Slab WORLD AABB:
  X: [131.635, 133.560] (1.925 m wide)
  Y: [16.848, 17.109]   (0.261 m thick)
  Z: [94.139, 96.630]   (2.491 m tall, bottom JUST above floor)

Player sphere at foot Z=94:
  Z: [94, 95.20]

Slab DOES overlap sphere in Z (overlap Z=[94.139, 95.20] = 1.061 m).

The slab IS at sphere height — it should collide. Both directional tests prove BSP collision response is symmetric for sphere-to-slab approach. Yet production shows asymmetric inside-out walkthrough at off-center positions. The bug must be in one of:

  1. The portal-reachable cells from indoor cell 0x0150 still miss the door's shadow at certain sphere positions, despite the AddAllOutsideCells fix. The user's walkthrough at X=133.655 (1.05 m east of door center) puts the sphere mostly east of slab X range [131.635, 133.560]. The sphere's WEST edge (X=133.175) is barely inside the slab. If GetNearbyObjects's outdoor radial sweep uses sphere center XY for cell lookup, it computes gridX = (int)(133.655 / 24) = 5 → cell 0xA9B40029. But AddAllOutsideCells only adds cells based on the sphere's PRIMARY position. The east-cell neighbor might not be added if the sphere is wholly within the primary cell's grid XY. Worth verifying.

  2. The BSP polygon-level test for partial-overlap geometry. Sphere half-east-of-slab, sphere south edge at slab north edge, moving +Y: sphere is on the verge of leaving the slab volume. BSPQuery's polygon intersection might consider this a "leaving collision" with no response, even though the sphere body still partially occupies the slab volume. Retail might handle this as "depenetration push" to resolve the overlap.

  3. Cell BSP (cell 0x0150's PhysicsPolygons) is missing. The doorway alcove cell has 4 physics polygons — likely walls + floor. If retail relies on the cell's walls to catch sphere-vs-doorway-side-wall collisions (in addition to the door slab), and we're not loading / testing the cell BSP correctly for the player's foot at sphere height, the side walls would miss.

Three candidate investigations, ranked by ROI:

A. cdb attach to retail at a Holtburg cottage doorway. Break on CTransition::FindObjCollisions for the door entity. Inspect what shapes retail actually tests against. THIS IS DEFINITIVE — answers "what should we be doing differently" in 15-30 min. CLAUDE.md has the toolchain ready.

B. Reproduce inside-out walkthrough at unit-test speed. Load real cell 0x0150 BSP into the harness (via CacheCellStruct from dat) + register door at faithful transform + replay captured tick 3262. If walkthrough reproduces at unit speed, can iterate on the fix in <500 ms.

C. Audit GetNearbyObjects radial sweep + AddAllOutsideCells coverage for east-neighbor cell when sphere XY is at primary cell boundary.

Recommendation: A first (cdb), then B to validate the fix 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