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>
133 lines
5.8 KiB
Markdown
133 lines
5.8 KiB
Markdown
# 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.
|
|
```
|