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>
This commit is contained in:
Erik 2026-05-25 08:14:49 +02:00
parent c27fded61e
commit 85a164f4a8
2 changed files with 125 additions and 129 deletions

View file

@ -106,90 +106,90 @@ 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 — REFRAMED 2026-05-25 evening)
## What's next (separate bug)
**Initial hypothesis was wrong.** Two new directional tests built
post-fix (`Directional_OutsideIn_*`, `Directional_InsideOut_*`) BOTH
PASS — the BSP collision response is symmetric at unit-test level.
The asymmetric production bug must come from something the unit tests
weren't capturing.
**Investigation status (corrected 2026-05-25 late evening).** Two new
directional tests + a geometric pin test all PASS:
A geometric pin test (`Geometric_DoorSlabZRange_AbovePlayerSphereTop`)
reveals the real story:
- `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):
CylSphere[0]: r=0.10, h=0.20, origin=(0, 0, 0.018)
→ world Z [94.118, 94.318] when entity at Z=94.1
Part 0 (GfxObj 0x010044B5, the BSP "slab"):
placement frame [Default][0].Origin = (-0.006, 0.125, 1.275)
→ BSP world Z [95.375, 97.865] when entity at Z=94.1
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)
Player at floor Z=94:
sphere height = 1.20, sphere top = 95.20
partFrame[0].Origin = (-0.006, 0.125, 1.275) → lifts slab origin
1.275 m above entity Z
BSP slab BOTTOM (95.375) is ABOVE sphere TOP (95.20) by 0.175 m.
The slab NEVER collides with the player's body sphere.
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 a LINTEL (the door frame above the doorway), not a leaf.
The door's only collision against a player at floor level is the
0.10 m radius foot cylinder. Sphere radius 0.48 + cyl 0.10 = 0.58 m
collision reach. Any sphere center > 0.58 m from cylinder center
(132.6, 17.1) passes freely.
**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:
The user-reported "inside-out walkthrough at ~50 cm off-center" is
the sphere walking AROUND the cylinder, at X = 132.6 ± 0.6+ m where
collision misses entirely. "Body partially intersects door" is the
character model occupying the visual door's volume while the collision
sphere passes beside the foot cylinder.
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.
The "outside-in works" case is also the foot cylinder doing the
blocking — when the user approaches more centered or hits the cylinder
head-on. The cell-visibility fix made the cylinder visible from indoor
cells (it wasn't before), which is why outside-in went from "walks
through" to "blocks" — but the cylinder is the actual collider.
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.
**This is a door-geometry interpretation question, NOT a BSP query bug.**
Three candidate next-step investigations:
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.
1. **Retail-faithfulness audit on door collision.** Read retail's
`CPhysicsObj::set_setup_for_door` (or similar) to determine what
shapes retail loads for a closed cottage door. If retail uses the
SAME setup data + finds the same shapes we do, the door geometry
in the dat IS the spec. The "blocking" the user sees in retail
might similarly be the foot cylinder + perhaps a different default
sphere from setup.Radius/setup.Height that we're not registering.
Three candidate investigations, ranked by ROI:
2. **Inspect door parts 1+2 (GfxObj 0x010044B6).** Per prior session's
handoff they are visual-only (HasPhysics=false). Verify by direct
dat read — maybe the PhysicsBSP is null but the cylinder/sphere
list on the GfxObj itself has collision data we're missing.
`DoorSetupGfxObjInspectionTests` already prints this; re-read.
**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.
3. **The cottage cell BSP encloses the doorway.** Cell 0x0150
(the doorway alcove) is bounded by cottage walls. The DOOR opens
through a gap in those walls. When the door is closed, the gap
is filled by the door geometry. Maybe retail's collision relies
on the cottage walls (cell BSP) for the "doorway side walls" and
the door's slab covers ONLY the opening's leaf area. The asymmetric
block we see could be the cottage cell walls catching outside-in
approach (cell BSP collision via FindEnvCollisions, before the
door's shadow ever fires) but NOT catching inside-out at the same
alcove geometry.
**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.
The 50cm off-center approach probably exits the cottage walls' BSP
through the doorway gap, walks past the foot cylinder (which is small),
and never has anything to collide against in the world's collision
representation. Retail might block this via a `setup.Radius=0.1414`
cylinder we're missing (Setup.Radius isn't currently registered by
`ShadowShapeBuilder.FromSetup` for entities WITH a CylSphere).
**C. Audit GetNearbyObjects radial sweep + AddAllOutsideCells coverage**
for east-neighbor cell when sphere XY is at primary cell boundary.
Apparatus to write next: a test that registers the door using
ShadowShapeBuilder.FromSetup AND verifies setup.Radius is reflected
in the shape list. If it isn't, that's a candidate fix — the door's
0.14 m radius + 0.20 m height SETUP sphere is wider than the 0.10 m
CylSphere and would catch the off-center approach.
Recommendation: **A first** (cdb), then **B** to validate the fix at
unit-test speed.
## Commits