fix(p2): Path 5 near-miss = retail num_sphere>1 gate (fixes B1 step-up wedge)
The 2026-06-03 handoff localized the failing Core tests to the BSP Path 5
step-up CLIMB (find_walkable/step_sphere_down). An ITestOutputHelper capture
of B1 disproved that: the climb code is correct (matches ACE
Polygon.adjust_sphere_to_plane / BSPTree.step_sphere_down exactly). The real
bug is the A6.P4 near-miss dispatch in FindCollisions' Path 5 (Contact
branch), which diverged from retail three ways:
1. Recorded a near-miss NegPolyHit UNCONDITIONALLY. Retail gates both
set_neg_poly_hit calls behind `if (num_sphere > 1)`
(acclient_2013_pseudo_c.txt:323852).
2. Checked the foot sphere's near-miss before the head's. Retail checks
the head (sphere1) first.
3. Mapped foot->neg_step_up=false / head->true. Retail maps head(index 0)
->false (slide), foot(index 1)->true (step-up), per
SPHEREPATH::set_neg_poly_hit (:323279, neg_step_up = arg2).
For B1's single foot sphere, the spurious near-miss -> outer loop
`!NegStepUp -> SetCollisionNormal + Collided` -> revert: the grounded mover
wedged at x=0.1 and never advanced to the wall to step up. With the verbatim
gate, a single-sphere near-miss records nothing, the sphere advances,
full-hits the wall, and step_sphere_up climbs the 0.25 m step (verified via
probe capture: foot ends at (0.6, 0, 0.25)).
The Holtburg cottage door still blocks faithfully (door slab (0,-1,0) normal,
stops in front of the door) when the scenario has a real floor — confirmed
this change does not regress the door.
The two BSPQueryTests Path5 near-miss tests used a single sphere (the very
non-retail assumption that caused this wedge); converted to the production
2-sphere shape where the head sphere records the near-miss, matching retail.
Core 1312 pass / 4 fail (the 4 pre-existing: 3 door documents-the-bug + D4
airborne, none regressed here); App 177 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
82045805fd
commit
abbd7615ee
2 changed files with 70 additions and 35 deletions
|
|
@ -1855,21 +1855,38 @@ public static class BSPQuery
|
|||
return TransitionState.Slid;
|
||||
}
|
||||
|
||||
// Sphere 0 didn't fully hit. Test sphere 1 (head sphere).
|
||||
ResolvedPolygon? hitPoly1 = null;
|
||||
bool hit1 = false;
|
||||
|
||||
// Sphere 0 didn't fully hit. Per retail, the head-sphere test AND
|
||||
// both near-miss dispatches are gated behind num_sphere > 1 (a head
|
||||
// sphere exists). A single-sphere mover that only near-misses — e.g.
|
||||
// a grounded foot sphere brushing a walkable step's top face on a
|
||||
// parallel (movement ⟂ normal, front-face-culled) move — records NO
|
||||
// neg-poly hit and is allowed to advance. It full-hits the
|
||||
// obstacle's vertical face on a later sub-step and step_sphere_up's
|
||||
// there. Recording a spurious foot near-miss here is what wedged a
|
||||
// grounded mover against a walkable low step (it never advanced far
|
||||
// enough to full-hit the wall and step up). See B1.
|
||||
//
|
||||
// Retail BSPTREE::find_collisions Contact branch
|
||||
// (acclient_2013_pseudo_c.txt:323838-323881, @0x53a630-0x53a6fb):
|
||||
// if (num_sphere > 1) {
|
||||
// sphere1 full hit -> slide_sphere (Slid)
|
||||
// else sphere1 near-miss -> set_neg_poly_hit(0, n1) (neg_step_up=0 -> outer slide)
|
||||
// else sphere0 near-miss -> set_neg_poly_hit(1, n0) (neg_step_up=1 -> outer step_up)
|
||||
// }
|
||||
// SPHEREPATH::set_neg_poly_hit (acclient_2013_pseudo_c.txt:323279)
|
||||
// assigns neg_step_up = arg2, so the HEAD near-miss (index 0) slides
|
||||
// and the FOOT near-miss (index 1) steps up. The head test wins when
|
||||
// both spheres near-miss (sphere1 is checked first).
|
||||
if (sphere1 is not null)
|
||||
{
|
||||
Vector3 contact1 = Vector3.Zero;
|
||||
hit1 = SphereIntersectsPolyInternal(root, resolved, sphere1, movement,
|
||||
ref hitPoly1, ref contact1);
|
||||
ResolvedPolygon? hitPoly1 = null;
|
||||
Vector3 contact1 = Vector3.Zero;
|
||||
bool hit1 = SphereIntersectsPolyInternal(root, resolved, sphere1, movement,
|
||||
ref hitPoly1, ref contact1);
|
||||
|
||||
if (hit1)
|
||||
{
|
||||
// Sphere 1 full hit while sphere 0 had only near-miss
|
||||
// (hitPoly0) — retail calls slide_sphere here. Record
|
||||
// collision + slide.
|
||||
// Sphere 1 (head) full hit → slide_sphere.
|
||||
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
||||
PhysicsDiagnostics.LastBspHitPoly = hitPoly1;
|
||||
|
||||
|
|
@ -1878,26 +1895,24 @@ public static class BSPQuery
|
|||
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;
|
||||
// Sphere 1 (head) near-miss → neg_poly_hit, neg_step_up = false → outer slide.
|
||||
if (hitPoly1 is not null)
|
||||
{
|
||||
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
||||
PhysicsDiagnostics.LastBspHitPoly = hitPoly1;
|
||||
NegPolyHitDispatch(path, hitPoly1, stepUp: false, localToWorld);
|
||||
return TransitionState.OK;
|
||||
}
|
||||
|
||||
// Sphere 0 (foot) near-miss → neg_poly_hit, neg_step_up = true → outer step_up.
|
||||
if (hitPoly0 is not null)
|
||||
{
|
||||
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
||||
PhysicsDiagnostics.LastBspHitPoly = hitPoly0;
|
||||
NegPolyHitDispatch(path, hitPoly0, stepUp: true, localToWorld);
|
||||
return TransitionState.OK;
|
||||
}
|
||||
}
|
||||
|
||||
return TransitionState.OK;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue