fix(physics): #32 L.2c wire edge-slide movement flag
This commit is contained in:
parent
9fea9b13ad
commit
1ec40f2a4f
6 changed files with 75 additions and 14 deletions
|
|
@ -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`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue