feat(phys): A6.P4 door — cdb-driven NegPolyHit dispatch (incomplete; needs BSP near-miss recording)
cdb attached to retail at a Holtburg cottage door while user walked the inside-out off-center scenario. Three trace iterations identified that retail's collision-recording happens via SPHEREPATH::set_neg_poly_hit (fires hundreds of times during inside-out walk), NOT via the more obvious-named COLLISIONINFO setters (which fire 0 times). Apparatus scripts at tools/cdb/door-inside-out-v[1-3].cdb + symbol-probe.cdb. Our codebase has NegPolyHitDispatch defined but never called. The downstream TransitionalInsert NegPolyHit handler was a stub. Two-part fix landed: 1. BSPQuery.FindCollisions Path 5 (Contact branch) restructured — distinguishes full hit (hit0 == true → StepSphereUp) from near-miss (hit0 == false but hitPoly0 != null → NegPolyHitDispatch). Mirrors retail BSPTREE::find_collisions at acclient_2013_pseudo_c.txt:0053a630-0053a6fb. 2. Transition.TransitionalInsert NegPolyHit handler — dispatches to step_up + step_up_slide (NegStepUp=true) or records collision normal + returns Collided (NegStepUp=false). Mirrors retail CTransition::transitional_insert at acclient_2013_pseudo_c.txt:0050b7af-0050b7e6. Tests: all 11 fix-relevant + regression tests pass including issue #98. VISUAL VERIFICATION (user-driven inside-out off-center): still squeezes through. Diagnostic [neg-poly-dispatch] probe shows ZERO hits in production. The Path 5 restructuring doesn't surface NegPolyHit because our SphereIntersectsPolyInternal only sets hitPoly on FULL hits — retail's sphere_intersects_poly sets var_5c (closest polygon) even on near-misses via BSP-traversal side effect. Remaining fix (next session): add near-miss polygon recording to SphereIntersectsPolyInternal. Once it sets hitPoly on near-miss BSP traversal, the Path 5 NegPolyHit dispatch (this commit) will fire and the TransitionalInsert handler (this commit) will block. Full handoff with cdb trace table + next-step plan: docs/research/2026-05-25-door-bug-cdb-retail-trace-findings.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a657ca946c
commit
fd1548af61
7 changed files with 382 additions and 29 deletions
133
docs/research/2026-05-25-door-bug-cdb-retail-trace-findings.md
Normal file
133
docs/research/2026-05-25-door-bug-cdb-retail-trace-findings.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Door bug — retail cdb trace + NegPolyHit dispatch findings
|
||||
2026-05-25, continuation of door-collision investigation
|
||||
|
||||
## TL;DR
|
||||
|
||||
cdb attached to retail at a Holtburg cottage door while user walked the
|
||||
inside-out off-center scenario. The smoking-gun trace identified the
|
||||
real collision-recording function: **`SPHEREPATH::set_neg_poly_hit`**
|
||||
fired hundreds of times during the walk; `SPHEREPATH::set_collide`,
|
||||
`COLLISIONINFO::set_collision_normal`, `set_sliding_normal`,
|
||||
`add_object` ALL fired zero times.
|
||||
|
||||
In our codebase, `NegPolyHitDispatch` exists but **is never called
|
||||
from any production code path** — it's dead code. The `path.NegPolyHit`
|
||||
flag is therefore never set. The downstream handler in
|
||||
`Transition.TransitionalInsert` was a stub that just cleared the flag.
|
||||
|
||||
Two-part fix attempted this session:
|
||||
|
||||
1. **`BSPQuery.FindCollisions` Path 5** (Contact branch) restructured
|
||||
to call `NegPolyHitDispatch` when sphere 0 had a near-miss polygon
|
||||
set but didn't fully penetrate (mirrors retail's `var_5c != 0` case
|
||||
at `acclient_2013_pseudo_c.txt:0053a6ce-0053a6fb`).
|
||||
|
||||
2. **`Transition.TransitionalInsert` NegPolyHit handler** rewritten
|
||||
to dispatch to `step_up + step_up_slide` (NegStepUp=true) or
|
||||
record collision normal + return `Collided` (NegStepUp=false).
|
||||
|
||||
**Result: fix doesn't fully close the bug.** User still squeezes
|
||||
through. Diagnostic `[neg-poly-dispatch]` probe shows ZERO hits in
|
||||
production — the BSP Path 5 changes don't surface NegPolyHit for this
|
||||
case.
|
||||
|
||||
## Why the fix doesn't fire
|
||||
|
||||
Retail's `BSPTREE::find_collisions` calls
|
||||
`vtable->sphere_intersects_poly(localspace_sphere, var_78_6, var_74_6, var_70_8)`
|
||||
which:
|
||||
- **Returns `eax_10`**: non-zero on full sphere-vs-poly hit
|
||||
- **Writes `var_5c`**: closest polygon pointer, set EVEN ON
|
||||
NEAR-MISS (BSP traversal sets it when entering a leaf containing
|
||||
candidate polys, regardless of intersection)
|
||||
|
||||
So retail records "near miss" polygons during BSP traversal. The
|
||||
caller dispatches `set_neg_poly_hit(1, var_5c + 0x20)` when sphere 0
|
||||
returned `eax_10 == 0` but `var_5c != 0`.
|
||||
|
||||
Our `SphereIntersectsPolyInternal` only sets `hitPoly` on actual
|
||||
hits. Near-miss polygons are NOT recorded. So the Path 5 branch
|
||||
`if (hitPoly0 is not null)` is false → no `NegPolyHitDispatch` call
|
||||
→ no NegPolyHit set → no dispatch in TransitionalInsert.
|
||||
|
||||
## The deeper fix needed
|
||||
|
||||
Implement retail's "BSP traversal records closest near-miss polygon"
|
||||
behavior in `SphereIntersectsPolyInternal` (or a sibling). The
|
||||
function should return TWO outputs:
|
||||
|
||||
- `bool hit` — true if sphere fully penetrates a polygon
|
||||
- `ResolvedPolygon? closestPoly` — set during traversal to the
|
||||
polygon that the sphere came closest to (in the BSP node walk),
|
||||
regardless of whether the full intersection test passed
|
||||
|
||||
This requires modifying the BSP recursion to track the "closest
|
||||
considered" polygon. Retail's sphere_intersects_poly likely tracks
|
||||
this as a side effect of testing each candidate polygon during the
|
||||
traversal.
|
||||
|
||||
Once that's in place, the existing Path 5 changes + TransitionalInsert
|
||||
NegPolyHit dispatch should fire correctly and produce the block.
|
||||
|
||||
## What the cdb trace proved
|
||||
|
||||
| Symbol | v1 hits | v2 hits | v3 hits |
|
||||
|---|---|---|---|
|
||||
| `CPhysicsObj::FindObjCollisions` | 161,081 | 196,608 | 196,608 |
|
||||
| `CCylSphere::collides_with_sphere` | 35,527 | — | — |
|
||||
| `SPHEREPATH::set_collide` | **0** | — | — |
|
||||
| `COLLISIONINFO::set_collision_normal` | — | **0** | — |
|
||||
| `COLLISIONINFO::set_sliding_normal` | — | **0** | — |
|
||||
| `COLLISIONINFO::add_object` | — | **0** | — |
|
||||
| `BSPTREE::slide_sphere` | — | — | **0** |
|
||||
| `CTransition::cliff_slide` | — | — | **0** |
|
||||
| **`SPHEREPATH::set_neg_poly_hit`** | — | — | **303+ (fires)** |
|
||||
| `CTransition::insert_into_cell` | — | — | 3,652 |
|
||||
|
||||
Retail records collisions almost exclusively via
|
||||
`SPHEREPATH::set_neg_poly_hit` during normal-grounded-motion. The
|
||||
COLLISIONINFO normal/sliding setters fire essentially never for
|
||||
walking-into-walls scenarios. Our investigation premise was wrong;
|
||||
the cdb data forced the correction.
|
||||
|
||||
## Apparatus + scripts committed
|
||||
|
||||
- `tools/cdb/door-inside-out.cdb` — v1 (set_collide check)
|
||||
- `tools/cdb/door-inside-out-v2.cdb` — v2 (COLLISIONINFO family)
|
||||
- `tools/cdb/door-inside-out-v3.cdb` — v3 (wide net, found
|
||||
set_neg_poly_hit)
|
||||
- `tools/cdb/symbol-probe.cdb` — verifies symbol resolution
|
||||
|
||||
## Pickup prompt for next session
|
||||
|
||||
```
|
||||
A6.P4 door inside-out: cdb trace + NegPolyHit dispatch landed
|
||||
(BSPQuery.FindCollisions Path 5 + TransitionalInsert NegPolyHit
|
||||
branch) but the fix doesn't fire because our SphereIntersectsPolyInternal
|
||||
doesn't record near-miss polygons. Retail's sphere_intersects_poly
|
||||
sets a "closest polygon" output even on non-hits via BSP traversal
|
||||
side-effect; our equivalent only sets it on full hits.
|
||||
|
||||
Read docs/research/2026-05-25-door-bug-cdb-retail-trace-findings.md
|
||||
|
||||
State both altitudes:
|
||||
Currently working toward: M1.5 — Indoor world feels right
|
||||
Current phase: A6.P4 door bug — implement near-miss polygon
|
||||
recording in SphereIntersectsPolyInternal.
|
||||
|
||||
First move: read SphereIntersectsPolyInternal in
|
||||
src/AcDream.Core/Physics/BSPQuery.cs (the function used at the
|
||||
Path 5 entry). Identify where polygons are tested during BSP
|
||||
traversal. Add a "closestPoly" output param that's set to ANY
|
||||
polygon considered during traversal (not just hit polygons).
|
||||
Then the Path 5 branch `if (hitPoly0 is not null)` will fire on
|
||||
near-miss cases, NegPolyHitDispatch will set NegPolyHit, and the
|
||||
TransitionalInsert dispatch (already landed) will block the sphere.
|
||||
|
||||
Retail oracle: BSPTREE::find_collisions + sphere_intersects_poly
|
||||
vtable call at acclient_2013_pseudo_c.txt:0053a630-0053a6fb.
|
||||
|
||||
Visual verification: same scenario (Holtburg cottage door,
|
||||
inside-out, ~50cm off-center). Should block fully, no squeeze-through.
|
||||
Outside-in should still work. Issue #98 cellar cap must still pass.
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue