fix(physics): L.2.3b — Path 5 step-up recursion guard

Path 5 (Contact mover hits BSP polygon) calls DoStepUp → DoStepDown →
TransitionalInsert(5) → FindObjCollisions → which can hit the same wall
again → Path 5 fires AGAIN → recursive DoStepUp.

Bounded by the inner numAttempts=5 budget, but with significant per-step
churn — every recursion clears and re-establishes the contact plane,
finishing in an inconsistent state when the ranges decay. Also produced
gratuitous slowdown against tall walls.

Retail (acclient_2013_pseudo_c.txt:272954) gates step_sphere_up on
`if (sp.step_up == 0 && sp.step_down == 0)`. acdream's port was
missing this guard. Mid-recursion we now fall back to the wall-slide
response that already exists for the no-engine path.

Files:
- BSPQuery.cs Path 5 (foot sphere): added `&& !path.StepUp && !path.StepDown`
- BSPQuery.cs Path 5 (head sphere): same guard

Live-test bug: walking into building walls intermittently locked the
player in falling animation, hard to recover. After the guard, the
single-shot wall-slide produces clean blocking + horizontal slide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-29 17:24:12 +02:00
parent b2aaac4e52
commit 3789491394

View file

@ -1469,11 +1469,18 @@ public static class BSPQuery
if (hit0 || hitPoly0 is not null)
{
var worldNormal = L2W(hitPoly0!.Plane.Normal);
if (engine is not null)
// 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 (engine is not null && !path.StepUp && !path.StepDown)
return StepSphereUp(transition, worldNormal, engine);
// No engine available (env-cell path without engine param) —
// fall back to wall-slide so existing indoor geometry still blocks.
// No engine OR step-up/step-down already in progress — fall
// back to wall-slide so the inner sphere doesn't recurse.
collisions.SetCollisionNormal(worldNormal);
collisions.SetSlidingNormal(worldNormal);
return TransitionState.Slid;
@ -1490,7 +1497,8 @@ public static class BSPQuery
if (hit1 || hitPoly1 is not null)
{
var worldNormal = L2W(hitPoly1!.Plane.Normal);
if (engine is not null)
// 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);