# 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 ```