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:
Erik 2026-05-25 10:36:22 +02:00
parent a657ca946c
commit fd1548af61
7 changed files with 382 additions and 29 deletions

View 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.
```

View file

@ -1800,63 +1800,87 @@ public static class BSPQuery
bool hit0 = SphereIntersectsPolyInternal(root, resolved, sphere0, movement,
ref hitPoly0, ref contact0);
if (hit0 || hitPoly0 is not null)
// A6.P4 door inside-out fix (2026-05-25). Retail distinguishes
// FULL HIT (eax_10 != 0 → step_sphere_up early-return) from
// NEAR-MISS (eax_10 == 0 but var_5c != 0 → set_neg_poly_hit).
// Previously this branch conflated both — going to StepSphereUp
// on near-misses too. That meant when sphere 0 had a near-miss
// poly recorded but didn't actually penetrate, we still recursed
// into step_sphere_up, which (because the sphere wasn't actually
// colliding) returned OK and let the sphere walk on. Inside-out
// cottage doors at off-center: sphere 0 has a near-miss with the
// cottage exterior wall east of the doorway, but the full
// sphere_intersects_poly returns false. Pre-fix → walked through.
// Post-fix → set_neg_poly_hit, outer transitional_insert loop
// dispatches to slide_sphere, blocks the sphere properly.
//
// Retail oracle: BSPTREE::find_collisions Contact branch at
// acclient_2013_pseudo_c.txt:0053a630-0053a6fb.
if (hit0)
{
// L.2d slice 1.5 (2026-05-13): record the hit poly EARLY,
// before the StepSphereUp branch can recurse into
// ResolveWithTransition → FindObjCollisions and clobber the
// side-channel via the inner call's per-resolve clear. Path 5
// is the dominant grounded-player path; without this the
// probe's [resolve-bldg] line for every grounded BSP hit was
// mis-labeled as "n/a (cylinder)".
// Full hit — step_sphere_up.
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly0;
var worldNormal = L2W(hitPoly0!.Plane.Normal);
// L.2.3b (2026-04-29): recursion guard. Retail
// (acclient_2013_pseudo_c.txt:272954) gates step_sphere_up on
// `if (sp.step_up == 0 && sp.step_down == 0)`. Without this,
// the inner TransitionalInsert spawned by DoStepDown re-enters
// FindObjCollisions, hits the same wall, and recursively
// re-invokes step-up — churning the contact plane until
// numAttempts decays. Mid-recursion we fall back to wall-slide.
// `if (sp.step_up == 0 && sp.step_down == 0)`.
if (engine is not null && !path.StepUp && !path.StepDown)
return StepSphereUp(transition, worldNormal, engine);
// No engine OR step-up/step-down already in progress — fall
// back to wall-slide so the inner sphere doesn't recurse.
// back to wall-slide.
collisions.SetCollisionNormal(worldNormal);
collisions.SetSlidingNormal(worldNormal);
return TransitionState.Slid;
}
// Sphere 0 didn't fully hit. Test sphere 1 (head sphere).
ResolvedPolygon? hitPoly1 = null;
bool hit1 = false;
if (sphere1 is not null)
{
ResolvedPolygon? hitPoly1 = null;
Vector3 contact1 = Vector3.Zero;
Vector3 contact1 = Vector3.Zero;
hit1 = SphereIntersectsPolyInternal(root, resolved, sphere1, movement,
ref hitPoly1, ref contact1);
bool hit1 = SphereIntersectsPolyInternal(root, resolved, sphere1, movement,
ref hitPoly1, ref contact1);
if (hit1 || hitPoly1 is not null)
if (hit1)
{
// L.2d slice 1.5 (2026-05-13): same early-record as foot
// sphere — head-sphere wall hits also recurse via
// StepSphereUp on the grounded path.
// Sphere 1 full hit while sphere 0 had only near-miss
// (hitPoly0) — retail calls slide_sphere here. Record
// collision + slide.
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly1;
var worldNormal = L2W(hitPoly1!.Plane.Normal);
// L.2.3b: same recursion guard as the foot-sphere branch.
if (engine is not null && !path.StepUp && !path.StepDown)
return StepSphereUp(transition, worldNormal, engine);
collisions.SetCollisionNormal(worldNormal);
collisions.SetSlidingNormal(worldNormal);
return TransitionState.Slid;
}
}
// Neither sphere fully hit. Record neg-poly hit if either had a
// near-miss polygon. Retail's set_neg_poly_hit with stepUp=false
// for sphere 0's near-miss, stepUp=true for sphere 1's near-miss.
// Outer transitional_insert loop then dispatches via slide_sphere
// (stepUp=false) or step_up + step_up_slide (stepUp=true).
if (hitPoly0 is not null)
{
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly0;
NegPolyHitDispatch(path, hitPoly0, stepUp: false, localToWorld);
return TransitionState.OK;
}
if (hitPoly1 is not null)
{
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly1;
NegPolyHitDispatch(path, hitPoly1, stepUp: true, localToWorld);
return TransitionState.OK;
}
return TransitionState.OK;
}

View file

@ -959,11 +959,72 @@ public sealed class Transition
}
// Handle neg-poly hit (backward-facing polygon contact).
//
// A6.P4 door inside-out fix (2026-05-25). Pre-fix this was a
// stub that just cleared the flag — letting the sphere walk
// through closed Holtburg cottage doors at off-center
// positions. cdb trace against retail (door-inside-out-v3.cdb)
// confirmed retail fires SPHEREPATH::set_neg_poly_hit hundreds
// of times during the walk, and the caller dispatches to
// slide_sphere (no neg_step_up) or step_up + step_up_slide
// fallback (neg_step_up). Both paths RECORD the collision
// and stop / slide the sphere.
//
// Retail oracle: CTransition::transitional_insert at
// docs/research/named-retail/acclient_2013_pseudo_c.txt:0050b7af-0050b7e6:
// if (sphere_path.neg_step_up == 0)
// edi = CSphere::slide_sphere(global_sphere, sphere_path,
// collision_info,
// neg_collision_normal, ...);
// else if (CTransition::step_up(this, neg_collision_normal) == 0)
// edi = SPHEREPATH::step_up_slide(sphere_path, this,
// collision_info);
if (sp.NegPolyHit && !sp.StepDown && !sp.StepUp)
{
sp.NegPolyHit = false;
// ACE: dispatch to StepUp or SlideSphere based on NegStepUp flag.
// Simplified: accept current position.
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
{
Console.WriteLine(System.FormattableString.Invariant(
$"[neg-poly-dispatch] stepUp={sp.NegStepUp} n=({sp.NegCollisionNormal.X:F3},{sp.NegCollisionNormal.Y:F3},{sp.NegCollisionNormal.Z:F3})"));
}
if (sp.NegStepUp)
{
// Retail CTransition::step_up; on failure SPHEREPATH::step_up_slide.
if (DoStepUp(sp.NegCollisionNormal, engine))
{
// Step-up succeeded — sphere repositioned by climb.
// Fall through to subsequent step-down logic.
}
else
{
var stepUpSlideRes = sp.StepUpSlide(this);
if (stepUpSlideRes == TransitionState.Slid)
{
ci.ContactPlaneValid = false;
ci.ContactPlaneIsWater = false;
continue;
}
if (stepUpSlideRes != TransitionState.OK)
return stepUpSlideRes;
}
}
else
{
// Retail CSphere::slide_sphere — the full retail version
// adjusts sphere position via add_offset_to_check_pos +
// returns Adjusted (on success) or Collided (degenerate).
// Our simpler response: record the collision normal +
// return Collided. The outer engine sees Collided and
// does NOT advance the sphere position — block achieved.
//
// A6.P4 door inside-out fix (2026-05-25): user-visible
// blocking is the goal (retail behavior); the full
// slide-position adjustment can be a later iteration.
ci.SetCollisionNormal(sp.NegCollisionNormal);
return TransitionState.Collided;
}
}
// Handle step-down when in contact but no ground plane found.

View file

@ -0,0 +1,34 @@
$$
$$ Retail cdb trace v2 — find what records the collision when retail blocks the door.
$$ v1 had SPHEREPATH::set_collide which never fired (0 hits despite retail BLOCKING).
$$ Switching to the COLLISIONINFO family.
$$
$$ BP1: CPhysicsObj::FindObjCollisions (entry count)
$$ BP2: COLLISIONINFO::set_collision_normal (the "I got blocked" record)
$$ BP3: COLLISIONINFO::set_sliding_normal (slide-along response)
$$ BP4: COLLISIONINFO::add_object (which entity caused the collision)
$$
.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\strange-albattani-3fc83c\retail-door-v2.log
.sympath C:\Users\erikn\source\repos\acdream\.claude\worktrees\strange-albattani-3fc83c\refs
.symopt+ 0x40
.reload /f acclient.exe
r $t0 = 0
r $t1 = 0
r $t2 = 0
r $t3 = 0
r $t4 = 0
bp acclient!CPhysicsObj::FindObjCollisions "r $t1 = @$t1 + 1; r $t0 = @$t0 + 1; .if (@$t1 % 5000 == 0) { .printf /D \"[BP1] FindObj=%d setColN=%d setSldN=%d addObj=%d\\n\", @$t1, @$t2, @$t3, @$t4 }; .if (@$t0 >= 30000) { .printf /D \"=== DETACH total=%d findObj=%d setColN=%d setSldN=%d addObj=%d ===\\n\", @$t0, @$t1, @$t2, @$t3, @$t4; qd } .else { gc }"
bp acclient!COLLISIONINFO::set_collision_normal "r $t2 = @$t2 + 1; r $t0 = @$t0 + 1; .printf /D \"[BP2] set_collision_normal hit#%d nx_h=0x%08X ny_h=0x%08X nz_h=0x%08X\\n\", @$t2, dwo(poi(@esp+4)+0), dwo(poi(@esp+4)+4), dwo(poi(@esp+4)+8); .if (@$t0 >= 30000) { qd } .else { gc }"
bp acclient!COLLISIONINFO::set_sliding_normal "r $t3 = @$t3 + 1; r $t0 = @$t0 + 1; .printf /D \"[BP3] set_sliding_normal hit#%d nx_h=0x%08X ny_h=0x%08X nz_h=0x%08X\\n\", @$t3, dwo(poi(@esp+4)+0), dwo(poi(@esp+4)+4), dwo(poi(@esp+4)+8); .if (@$t0 >= 30000) { qd } .else { gc }"
bp acclient!COLLISIONINFO::add_object "r $t4 = @$t4 + 1; r $t0 = @$t0 + 1; .if (@$t0 >= 30000) { qd } .else { gc }"
.printf "v2 trace armed: 4 BPs (COLLISIONINFO family). Walk now.\\n"
g

View file

@ -0,0 +1,43 @@
$$
$$ Retail cdb trace v3 — wider breakpoint net to catch what retail does
$$ when it blocks the sphere. v1 (SPHEREPATH::set_collide) and v2
$$ (COLLISIONINFO::set_collision/sliding/add_object) all came back at 0.
$$
$$ Trying: anything in the BSP slide / step / find_collisions chain.
$$ BP1: CPhysicsObj::FindObjCollisions (counter only)
$$ BP2: BSPTREE::slide_sphere
$$ BP3: CSphere::slide_sphere
$$ BP4: CTransition::cliff_slide
$$ BP5: SPHEREPATH::set_neg_poly_hit (alternate hit-recording)
$$ BP6: CTransition::insert_into_cell (cell transit attempts)
$$
.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\strange-albattani-3fc83c\retail-door-v3.log
.sympath C:\Users\erikn\source\repos\acdream\.claude\worktrees\strange-albattani-3fc83c\refs
.symopt+ 0x40
.reload /f acclient.exe
r $t0 = 0
r $t1 = 0
r $t2 = 0
r $t3 = 0
r $t4 = 0
r $t5 = 0
r $t6 = 0
bp acclient!CPhysicsObj::FindObjCollisions "r $t1 = @$t1 + 1; r $t0 = @$t0 + 1; .if (@$t1 % 5000 == 0) { .printf /D \"[STATS] FindObj=%d bspSld=%d sphSld=%d cliffSld=%d negPoly=%d insertCell=%d\\n\", @$t1, @$t2, @$t3, @$t4, @$t5, @$t6 }; .if (@$t0 >= 40000) { .printf /D \"=== DETACH FindObj=%d bspSld=%d sphSld=%d cliffSld=%d negPoly=%d insertCell=%d ===\\n\", @$t1, @$t2, @$t3, @$t4, @$t5, @$t6; qd } .else { gc }"
bp acclient!BSPTREE::slide_sphere "r $t2 = @$t2 + 1; r $t0 = @$t0 + 1; .printf /D \"[BP2] BSPTREE::slide_sphere hit#%d\\n\", @$t2; .if (@$t0 >= 40000) { qd } .else { gc }"
bp acclient!CSphere::slide_sphere "r $t3 = @$t3 + 1; r $t0 = @$t0 + 1; .printf /D \"[BP3] CSphere::slide_sphere hit#%d\\n\", @$t3; .if (@$t0 >= 40000) { qd } .else { gc }"
bp acclient!CTransition::cliff_slide "r $t4 = @$t4 + 1; r $t0 = @$t0 + 1; .printf /D \"[BP4] cliff_slide hit#%d\\n\", @$t4; .if (@$t0 >= 40000) { qd } .else { gc }"
bp acclient!SPHEREPATH::set_neg_poly_hit "r $t5 = @$t5 + 1; r $t0 = @$t0 + 1; .printf /D \"[BP5] set_neg_poly_hit hit#%d\\n\", @$t5; .if (@$t0 >= 40000) { qd } .else { gc }"
bp acclient!CTransition::insert_into_cell "r $t6 = @$t6 + 1; r $t0 = @$t0 + 1; .if (@$t0 >= 40000) { qd } .else { gc }"
.printf "v3 trace armed: 6 BPs wide net. Walk into door now.\\n"
g

View file

@ -0,0 +1,39 @@
$$
$$ Retail cdb trace — does the cottage door block sphere at off-center inside-out approach?
$$ 2026-05-25
$$
$$ Three breakpoints with counters; auto-detach at 30K total events to avoid
$$ retail-session-lag → ACE timeout. Pattern from a6-probe.cdb (proven).
$$
$$ Question: when player walks from inside cottage to outside, off-center ~50cm,
$$ does retail's collision system FIRE (set_collide) at the corner where
$$ alcove east wall meets cottage north exterior wall? Or does it walk through
$$ silently like acdream does?
$$
$$ BP1: CPhysicsObj::FindObjCollisions — per-object collision test entry.
$$ BP2: CCylSphere::collides_with_sphere — cylinder shape hit test.
$$ BP3: SPHEREPATH::set_collide — collision RECORDED (the smoking gun).
$$ If BP3 fires often during inside-out walk: retail blocks.
$$ If BP3 silent: retail walks through too (= retail-faithful behavior).
$$
.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\strange-albattani-3fc83c\retail-door-inside-out.log
.sympath C:\Users\erikn\source\repos\acdream\.claude\worktrees\strange-albattani-3fc83c\refs
.symopt+ 0x40
.reload /f acclient.exe
r $t0 = 0
r $t1 = 0
r $t2 = 0
r $t3 = 0
bp acclient!CPhysicsObj::FindObjCollisions "r $t1 = @$t1 + 1; r $t0 = @$t0 + 1; .if (@$t1 % 5000 == 0) { .printf /D \"[BP1] FindObjCollisions=%d cyl_collides=%d set_collide=%d\\n\", @$t1, @$t2, @$t3 }; .if (@$t0 >= 30000) { .printf /D \"=== DETACH total=%d FindObj=%d cyl=%d setCollide=%d ===\\n\", @$t0, @$t1, @$t2, @$t3; qd } .else { gc }"
bp acclient!CCylSphere::collides_with_sphere "r $t2 = @$t2 + 1; r $t0 = @$t0 + 1; .if (@$t0 >= 30000) { qd } .else { gc }"
bp acclient!SPHEREPATH::set_collide "r $t3 = @$t3 + 1; r $t0 = @$t0 + 1; .printf /D \"[BP3] set_collide hit#%d nx_h=0x%08X ny_h=0x%08X nz_h=0x%08X\\n\", @$t3, dwo(poi(@esp+4)+0), dwo(poi(@esp+4)+4), dwo(poi(@esp+4)+8); .if (@$t0 >= 30000) { qd } .else { gc }"
.printf "Door inside-out trace armed: 3 BPs, threshold 30K. Walk now.\\n"
g

View file

@ -0,0 +1,19 @@
$$ Quick symbol probe: list what COLLISIONINFO / SPHEREPATH functions exist.
.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\strange-albattani-3fc83c\retail-sym-probe.log
.sympath C:\Users\erikn\source\repos\acdream\.claude\worktrees\strange-albattani-3fc83c\refs
.symopt+ 0x40
.reload /f acclient.exe
.printf "=== COLLISIONINFO symbols ===\n"
x acclient!COLLISIONINFO::*
.printf "\n=== SPHEREPATH symbols ===\n"
x acclient!SPHEREPATH::*
.printf "\n=== CTransition symbols (subset) ===\n"
x acclient!CTransition::*collide*
x acclient!CTransition::insert*
x acclient!CTransition::makeTr*
qd