From 37894913943f83d09d75049f6bc87edd2cddadd9 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 29 Apr 2026 17:24:12 +0200 Subject: [PATCH] =?UTF-8?q?fix(physics):=20L.2.3b=20=E2=80=94=20Path=205?= =?UTF-8?q?=20step-up=20recursion=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.Core/Physics/BSPQuery.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/AcDream.Core/Physics/BSPQuery.cs b/src/AcDream.Core/Physics/BSPQuery.cs index 2895b20..eff0222 100644 --- a/src/AcDream.Core/Physics/BSPQuery.cs +++ b/src/AcDream.Core/Physics/BSPQuery.cs @@ -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);