fix(physics): #32 L.2c wire edge-slide movement flag

This commit is contained in:
Erik 2026-04-30 07:40:43 +02:00
parent 9fea9b13ad
commit 1ec40f2a4f
6 changed files with 75 additions and 14 deletions

View file

@ -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;

View file

@ -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)

View file

@ -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
// -----------------------------------------------------------------------