diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 8c97087..7112f33 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -179,7 +179,7 @@ missing is the plugin-API surface. ## #32 — Retail edge-slide / cliff-slide / precipice-slide incomplete -**Status:** OPEN +**Status:** IN-PROGRESS **Severity:** HIGH **Filed:** 2026-04-29 **Component:** physics / collision @@ -188,7 +188,12 @@ missing is the plugin-API surface. step-down boundaries, retail often slides along the boundary. acdream still hard-blocks or accepts too much in several of these cases. -**Root cause / status:** Tracked under Phase L.2c. Named retail anchors include +**Root cause / status:** Tracked under Phase L.2c. Wall-adjacent +`step_up_slide` now feels acceptable in live testing. L.2c plumbing now passes +the retail-default `EdgeSlide` flag into local and remote movement and logs +failed step-down edge cases behind `ACDREAM_DUMP_EDGE_SLIDE=1`. Remaining gap: +preserve walkable polygon context for `precipice_slide` and finish +`cliff_slide` / `NegPolyHit` dispatch. Named retail anchors include `CTransition::edge_slide`, `CTransition::cliff_slide`, `SPHEREPATH::precipice_slide`, and `SPHEREPATH::step_up_slide`. diff --git a/memory/project_movement_collision_conformance.md b/memory/project_movement_collision_conformance.md index 66cbfdb..1fa54b8 100644 --- a/memory/project_movement_collision_conformance.md +++ b/memory/project_movement_collision_conformance.md @@ -59,3 +59,8 @@ InputDispatcher / PlayerMovementController outdoor cell ownership from world position during the sphere sweep, so 24m outdoor seams update low cell ids and full-cell callers crossing landblock seams get the destination landblock prefix plus the correct outdoor low cell. +- 2026-04-30: L.2c edge-slide plumbing. User live-tested wall-adjacent slide as + acceptable. Local player and remote dead-reckoning now pass retail-default + `ObjectInfoState.EdgeSlide`; `ACDREAM_DUMP_EDGE_SLIDE=1` logs failed + step-down edge cases so the next slice can distinguish missing walkable + polygon context from cliff-slide/NegPolyHit gaps. diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index 2a832d0..35daf3f 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -428,12 +428,18 @@ public sealed class PlayerMovementController stepDownHeight: StepDownHeight, // L.2.3a: from Setup.StepDownHeight isOnGround: _body.OnWalkable, body: _body, // persist ContactPlane across frames for slope tracking + // L.2c 2026-04-30: retail PhysicsGlobals.DefaultState includes + // EdgeSlide, and PhysicsObj.get_object_info copies that bit into + // OBJECTINFO. Keep it explicit here so edge/cliff handling runs + // under the same flag profile as retail player movement. + // // Commit C 2026-04-29 — local player is always IsPlayer. // The PK/PKLite/Impenetrable bits come from PlayerDescription's // PlayerKillerStatus property; not yet parsed (non-PK pair → walks // through other non-PK players, which is retail's default for // ACE's character creation defaults too). - moverFlags: AcDream.Core.Physics.ObjectInfoState.IsPlayer); + moverFlags: AcDream.Core.Physics.ObjectInfoState.IsPlayer + | AcDream.Core.Physics.ObjectInfoState.EdgeSlide); // Apply resolved position. _body.Position = resolveResult.Position; diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 7a343a7..771c37d 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -5856,7 +5856,11 @@ public sealed class GameWindow : IDisposable // branch zeroes the +Z offset every step (same bug // we hit on the local jump). isOnGround: !rm.Airborne, - body: rm.Body); // persist ContactPlane across frames for slope tracking + body: rm.Body, // persist ContactPlane across frames for slope tracking + // Retail default physics state includes EdgeSlide. + // Remote dead-reckoning should exercise the same + // edge/cliff branch as local movement. + moverFlags: AcDream.Core.Physics.ObjectInfoState.EdgeSlide); rm.Body.Position = resolveResult.Position; if (resolveResult.CellId != 0) diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index 5b11b10..8bdaef6 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -345,6 +345,9 @@ public sealed class Transition public SpherePath SpherePath = new(); public CollisionInfo CollisionInfo = new(); + private static bool DumpEdgeSlideEnabled => + Environment.GetEnvironmentVariable("ACDREAM_DUMP_EDGE_SLIDE") == "1"; + // ----------------------------------------------------------------------- // Public entry point // ----------------------------------------------------------------------- @@ -665,18 +668,19 @@ public sealed class Transition } } - // L.2.3e (2026-04-29): step-down failed — the move would put + // L.2c (2026-04-30): 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. + // Retail's EdgeSlide path then needs either: + // - a steep contact plane for CliffSlide, or + // - SpherePath.Walkable polygon context for PrecipiceSlide. // - // 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). + // acdream does not yet preserve the full walkable polygon + // context from terrain/BSP step-down, so this is still the + // conservative stop-at-edge fallback. The diagnostic below is + // intentionally narrow: it tells the next L.2c slice whether + // we are missing precipice context, a steep contact plane, or + // merely the EdgeSlide flag. + DumpEdgeSlideStepDownFailed(stepDownHeight, zVal); sp.RestoreCheckPos(); return TransitionState.Collided; } @@ -689,6 +693,22 @@ public sealed class Transition return TransitionState.Slid; } + private void DumpEdgeSlideStepDownFailed(float stepDownHeight, float zVal) + { + if (!DumpEdgeSlideEnabled) return; + + var sp = SpherePath; + var ci = CollisionInfo; + var oi = ObjectInfo; + + Console.WriteLine( + System.FormattableString.Invariant( + $"edge-slide: stepdown-failed cur={Fmt(sp.CurPos)} check={Fmt(sp.CheckPos)} cell=0x{sp.CheckCellId:X8} edgeFlag={oi.EdgeSlide} contactFlag={oi.Contact} onWalkable={oi.OnWalkable} contactPlane={ci.ContactPlaneValid} lastPlane={ci.LastKnownContactPlaneValid} walkableValid={sp.WalkableValid} stepDown={stepDownHeight:F3} zVal={zVal:F3}")); + } + + private static string Fmt(Vector3 value) => + System.FormattableString.Invariant($"({value.X:F3},{value.Y:F3},{value.Z:F3})"); + // ----------------------------------------------------------------------- // Environment collision — outdoor terrain // ----------------------------------------------------------------------- diff --git a/tests/AcDream.Core.Tests/Physics/PhysicsEngineTests.cs b/tests/AcDream.Core.Tests/Physics/PhysicsEngineTests.cs index 0f212fa..d6f08cf 100644 --- a/tests/AcDream.Core.Tests/Physics/PhysicsEngineTests.cs +++ b/tests/AcDream.Core.Tests/Physics/PhysicsEngineTests.cs @@ -210,6 +210,27 @@ public class PhysicsEngineTests Assert.Equal(0x0009u, result.CellId); } + [Fact] + public void ResolveWithTransition_EdgeSlideFlag_AllowsNormalFlatMovement() + { + var engine = MakeFlatEngine(terrainZ: 50f); + + var result = engine.ResolveWithTransition( + currentPos: new Vector3(96f, 96f, 50f), + targetPos: new Vector3(98f, 96f, 50f), + cellId: 0x0025u, + sphereRadius: 0.5f, + sphereHeight: 1.2f, + stepUpHeight: 0.4f, + stepDownHeight: 0.4f, + isOnGround: true, + moverFlags: ObjectInfoState.EdgeSlide); + + Assert.True(result.IsOnGround); + Assert.InRange(result.Position.X, 97.9f, 98.1f); + Assert.Equal(0x0025u, result.CellId); + } + [Fact] public void ResolveWithTransition_LandblockBoundary_UpdatesFullOutdoorCellId() {