From 8fe178ee5c4d46c9b217f4678abcdd5ddefe115f Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 29 Apr 2026 17:56:22 +0200 Subject: [PATCH] =?UTF-8?q?fix(physics):=20L.2.3d/e/f=20=E2=80=94=20wall?= =?UTF-8?q?=20slide,=20edge=20block,=20step-up=20diagnostic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three follow-up fixes from live testing of the L.2.3 step-height pass. L.2.3d — StepUpSlide actually applies the slide Previously SpherePath.StepUpSlide only set ci.SlidingNormal as a flag and returned Slid; the CURRENT step's CheckPos was never adjusted, so the sphere stopped dead at the wall. ValidateTransition's "default to UnitZ" branch then propagated UnitZ into SlidingNormal, overwriting the wall normal entirely. Net effect: stop-at-wall, no horizontal slide. ACE's StepUpSlide (SpherePath.cs:309-317) calls Sphere.SlideSphere which computes the actual slide offset against the contact-plane / wall-normal crease and applies it to CheckPos. acdream already had the same logic in Transition.SlideSphere as a private helper. Exposed as internal SlideSphereInternal; routed StepUpSlide through it. L.2.3e — step-down failure returns Collided (always-on edge block) When walking forward off a balcony / cliff, the step-down probe in TransitionalInsert searches stepDownHeight below CheckPos for a walkable surface. On failure the previous code returned OK, which ValidateTransition accepted — the player walked off the edge anyway, with `RestoreCheckPos` reverting only to the position right after the outer step's offset (still post-edge). Per ACE Transition.cs:268-320 (EdgeSlide), retail's always-on default for OnWalkable + !EdgeSlide-flag movers is to reject the move. Returning Collided here makes ValidateTransition revert CheckPos to CurPos (pre-step), giving the retail-faithful "stop at edge" behavior — both on terrain cliffs and on building/balcony edges. L.2.3f — diagnostic instrumentation for steep-roof investigation GameWindow logs the player's actual StepUpHeight + StepDownHeight at world-entry (along with the raw Setup.* values for comparison) so we can confirm whether the dat-derived value matches retail's spec (~0.4m) or is overriding to something larger. Transition.DoStepUp logs the polygon's collision-normal Z (gated on ACDREAM_DUMP_STEPUP=1 to keep cold-path noise low) so we can tell whether step-up is being triggered against truly-walkable polygons (Z >= FloorZ ≈ 0.66) or whether something steeper is sneaking through. Tests: 825/825 still pass. The L.2 conformance fixtures cover the slide path; D1 + D2 regression tests still pass with the StepUpSlide port. Live verification needed for: - #2 Wall slide: running close to a wall should slide along it. - #4 Edge block: running off a balcony should stop at the edge. - #3 Steep roof: launch with ACDREAM_DUMP_STEPUP=1 and report the "stepup: normal=..." log lines when climbing the offending roof. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 11 ++++ src/AcDream.Core/Physics/BSPQuery.cs | 2 +- src/AcDream.Core/Physics/TransitionTypes.cs | 68 ++++++++++++++++++--- 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index d5ee444..b2bd967 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -7034,11 +7034,22 @@ public sealed class GameWindow : IDisposable _playerController.StepDownHeight = (playerSetup is not null && playerSetup.StepDownHeight > 0f) ? playerSetup.StepDownHeight : 0.4f; + // L.2.3f (2026-04-29): diagnostic — confirm what the actual + // values from the player's Setup dat are. Retail's spec says ~0.4 m + // for humans, but we want to verify rather than guess. If the + // dat-derived value is large (e.g. 1.5 m+) it explains why the + // player can mount steep roofs via the step-up scan reach. + Console.WriteLine( + $"physics: player step heights — StepUp={_playerController.StepUpHeight:F3} m " + + $"(Setup.StepUpHeight={(playerSetup?.StepUpHeight ?? 0f):F3}), " + + $"StepDown={_playerController.StepDownHeight:F3} m " + + $"(Setup.StepDownHeight={(playerSetup?.StepDownHeight ?? 0f):F3})"); } else { _playerController.StepUpHeight = 0.4f; _playerController.StepDownHeight = 0.4f; + Console.WriteLine($"physics: player step heights — defaulting to 0.4 m (no setup dat)"); } int plbX = _liveCenterX + (int)MathF.Floor(playerEntity.Position.X / 192f); int plbY = _liveCenterY + (int)MathF.Floor(playerEntity.Position.Y / 192f); diff --git a/src/AcDream.Core/Physics/BSPQuery.cs b/src/AcDream.Core/Physics/BSPQuery.cs index eff0222..a0947d0 100644 --- a/src/AcDream.Core/Physics/BSPQuery.cs +++ b/src/AcDream.Core/Physics/BSPQuery.cs @@ -1106,7 +1106,7 @@ public static class BSPQuery if (transition.DoStepUp(collisionNormal, engine!)) return TransitionState.OK; - return transition.SpherePath.StepUpSlide(transition.CollisionInfo); + return transition.SpherePath.StepUpSlide(transition); } // ------------------------------------------------------------------------- diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index bd34857..2b5834c 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -248,15 +248,29 @@ public sealed class SpherePath /// /// Slide fallback when step-up fails. Clears the contact-plane state that - /// caused the step-up attempt and issues a slide along StepUpNormal. - /// ACE: SpherePath.StepUpSlide (ACE SpherePath.cs:309-317). + /// caused the step-up attempt and runs the full sphere-slide computation + /// to actually move the sphere along the wall. + /// + /// + /// L.2.3d (2026-04-29): the previous version only set + /// as a flag; it never applied a slide offset. The user observed "running + /// close to the wall now I stop" — the sphere stayed pinned at the wall + /// and the slide normal got overwritten by ValidateTransition's + /// default-to-UnitZ branch. ACE actually computes the slide offset and + /// applies it to via Sphere.SlideSphere; + /// we delegate to which does + /// the same thing. + /// + /// + /// ACE: SpherePath.StepUpSlide + Sphere.SlideSphere + /// (SpherePath.cs:309-317, Sphere.cs:558-604). /// - public TransitionState StepUpSlide(CollisionInfo collisions) + public TransitionState StepUpSlide(Transition transition) { - collisions.ContactPlaneValid = false; - collisions.ContactPlaneIsWater = false; - collisions.SetSlidingNormal(StepUpNormal); - return TransitionState.Slid; + var ci = transition.CollisionInfo; + ci.ContactPlaneValid = false; + ci.ContactPlaneIsWater = false; + return transition.SlideSphereInternal(StepUpNormal, GlobalCurrCenter[0].Origin); } /// @@ -635,9 +649,20 @@ public sealed class Transition } } - // Step-down failed: stay at current position. + // L.2.3e (2026-04-29): step-down failed — the move would put + // the player off an edge with no walkable surface within reach. + // Retail's EdgeSlide (ACE Transition.cs:268-320) maps this to + // SetEdgeSlide(true, true, OK) which restores CheckPos to the + // saved (post-step) position — but our outer ValidateTransition + // accepts CheckPos as the new CurPos, defeating the intent. + // + // Returning Collided here makes ValidateTransition revert to + // CurPos (pre-step) — the always-on retail "stop at edge" + // behavior. Confirmed against ACE Transition.cs:317 where + // EdgeSlide returns Collided when no walkable surface is + // found and the EdgeSlide flag is unset (player default). sp.RestoreCheckPos(); - return TransitionState.OK; + return TransitionState.Collided; } return TransitionState.OK; @@ -1082,6 +1107,14 @@ public sealed class Transition /// normal variant). ACE: Sphere.SlideSphere(Transition, ref Vector3, Vector3). /// Decompiled: FUN_00538180. /// + /// + /// L.2.3d: exposed as internal so + /// can apply the same slide computation ACE's Sphere.SlideSphere uses + /// for failed step-up. Mirror of ACE Sphere.cs:558-604 (Plane variant). + /// + internal TransitionState SlideSphereInternal(Vector3 collisionNormal, Vector3 currPos) + => SlideSphere(collisionNormal, currPos); + private TransitionState SlideSphere(Vector3 collisionNormal, Vector3 currPos) { var sp = SpherePath; @@ -1356,6 +1389,23 @@ public sealed class Transition var ci = CollisionInfo; var oi = ObjectInfo; + // L.2.3f (2026-04-29): diagnostic for steep-roof bug. Log the + // collision normal Z so we can confirm whether the polygon being + // stepped up onto is "walkable" (normal.Z >= FloorZ ≈ 0.66) or + // not (≈ 0.087 LandingZ when not OnWalkable). If we see steep + // normals being accepted, the issue is in the find_walkable + // threshold rather than the StepUpHeight reach. + if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_STEPUP") == "1") + { + float floor = PhysicsGlobals.FloorZ; + string verdict = collisionNormal.Z >= floor ? "WALKABLE" : "STEEP"; + Console.WriteLine( + $"stepup: normal=({collisionNormal.X:F3},{collisionNormal.Y:F3},{collisionNormal.Z:F3}) " + + $"|Z|={collisionNormal.Z:F3} vs FloorZ={floor:F3} → {verdict}, " + + $"OnWalkable={oi.State.HasFlag(ObjectInfoState.OnWalkable)}, " + + $"StepUpHeight={oi.StepUpHeight:F3}"); + } + // L.2.3c (2026-04-29): capture the existing contact plane BEFORE // clearing it. On step-up failure (too-tall wall) we restore it so // the mover stays grounded — without this, walking into a wall