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>
232 lines
11 KiB
Markdown
232 lines
11 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)
|
|
|
|
**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
|
|
```
|