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

@ -179,7 +179,7 @@ missing is the plugin-API surface.
## #32 — Retail edge-slide / cliff-slide / precipice-slide incomplete ## #32 — Retail edge-slide / cliff-slide / precipice-slide incomplete
**Status:** OPEN **Status:** IN-PROGRESS
**Severity:** HIGH **Severity:** HIGH
**Filed:** 2026-04-29 **Filed:** 2026-04-29
**Component:** physics / collision **Component:** physics / collision
@ -188,7 +188,12 @@ missing is the plugin-API surface.
step-down boundaries, retail often slides along the boundary. acdream still step-down boundaries, retail often slides along the boundary. acdream still
hard-blocks or accepts too much in several of these cases. 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`, `CTransition::edge_slide`, `CTransition::cliff_slide`,
`SPHEREPATH::precipice_slide`, and `SPHEREPATH::step_up_slide`. `SPHEREPATH::precipice_slide`, and `SPHEREPATH::step_up_slide`.

View file

@ -59,3 +59,8 @@ InputDispatcher / PlayerMovementController
outdoor cell ownership from world position during the sphere sweep, so 24m outdoor cell ownership from world position during the sphere sweep, so 24m
outdoor seams update low cell ids and full-cell callers crossing landblock outdoor seams update low cell ids and full-cell callers crossing landblock
seams get the destination landblock prefix plus the correct outdoor low cell. 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.

View file

@ -428,12 +428,18 @@ public sealed class PlayerMovementController
stepDownHeight: StepDownHeight, // L.2.3a: from Setup.StepDownHeight stepDownHeight: StepDownHeight, // L.2.3a: from Setup.StepDownHeight
isOnGround: _body.OnWalkable, isOnGround: _body.OnWalkable,
body: _body, // persist ContactPlane across frames for slope tracking 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. // Commit C 2026-04-29 — local player is always IsPlayer.
// The PK/PKLite/Impenetrable bits come from PlayerDescription's // The PK/PKLite/Impenetrable bits come from PlayerDescription's
// PlayerKillerStatus property; not yet parsed (non-PK pair → walks // PlayerKillerStatus property; not yet parsed (non-PK pair → walks
// through other non-PK players, which is retail's default for // through other non-PK players, which is retail's default for
// ACE's character creation defaults too). // 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. // Apply resolved position.
_body.Position = resolveResult.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 // branch zeroes the +Z offset every step (same bug
// we hit on the local jump). // we hit on the local jump).
isOnGround: !rm.Airborne, 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; rm.Body.Position = resolveResult.Position;
if (resolveResult.CellId != 0) if (resolveResult.CellId != 0)

View file

@ -345,6 +345,9 @@ public sealed class Transition
public SpherePath SpherePath = new(); public SpherePath SpherePath = new();
public CollisionInfo CollisionInfo = new(); public CollisionInfo CollisionInfo = new();
private static bool DumpEdgeSlideEnabled =>
Environment.GetEnvironmentVariable("ACDREAM_DUMP_EDGE_SLIDE") == "1";
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Public entry point // 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. // the player off an edge with no walkable surface within reach.
// Retail's EdgeSlide (ACE Transition.cs:268-320) maps this to // Retail's EdgeSlide path then needs either:
// SetEdgeSlide(true, true, OK) which restores CheckPos to the // - a steep contact plane for CliffSlide, or
// saved (post-step) position — but our outer ValidateTransition // - SpherePath.Walkable polygon context for PrecipiceSlide.
// accepts CheckPos as the new CurPos, defeating the intent.
// //
// Returning Collided here makes ValidateTransition revert to // acdream does not yet preserve the full walkable polygon
// CurPos (pre-step) — the always-on retail "stop at edge" // context from terrain/BSP step-down, so this is still the
// behavior. Confirmed against ACE Transition.cs:317 where // conservative stop-at-edge fallback. The diagnostic below is
// EdgeSlide returns Collided when no walkable surface is // intentionally narrow: it tells the next L.2c slice whether
// found and the EdgeSlide flag is unset (player default). // we are missing precipice context, a steep contact plane, or
// merely the EdgeSlide flag.
DumpEdgeSlideStepDownFailed(stepDownHeight, zVal);
sp.RestoreCheckPos(); sp.RestoreCheckPos();
return TransitionState.Collided; return TransitionState.Collided;
} }
@ -689,6 +693,22 @@ public sealed class Transition
return TransitionState.Slid; 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 // Environment collision — outdoor terrain
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------

View file

@ -210,6 +210,27 @@ public class PhysicsEngineTests
Assert.Equal(0x0009u, result.CellId); 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] [Fact]
public void ResolveWithTransition_LandblockBoundary_UpdatesFullOutdoorCellId() public void ResolveWithTransition_LandblockBoundary_UpdatesFullOutdoorCellId()
{ {