fix(motion): airborne hard-snap + velocity-extrapolation (L.3.1)
Round 2 fix for two visual bugs that survived commit 5154a3e:
Bug 1 (chop at 1 Hz UP cadence): Round 1 zeroed body.Velocity each
tick on grounded remotes, leaving AdjustOffset as the sole motion
source. AdjustOffset catches up in ~150 ms then sits idle until the
next UP at 1 Hz, producing visible "updates every 1 second" stepping.
Root cause: retail achieves smoothness via animation root motion +
AdjustOffset *corrections*; we only ported corrections (root motion
is Phase L.3.2 / PositionManager). Workaround for L.3.1: seed
body.Velocity from update.Velocity on every grounded UP so
UpdatePhysicsInternal integrates position += vel*dt between UPs,
with the queue providing corrective patches via AdjustOffset.
Bug 2 (endless jump): Round 1 tried to detect landing via "UP arrives
during airborne with no velocity" but ACE keeps sending non-zero
velocity through the arc, so the detector never fired. Fix: stop
maintaining a local "predicted arc". Server is authoritative for
airborne position too -- hard-snap from each UP during airborne;
body.Velocity (set by OnLiveVectorUpdated) integrates between UPs
for smoothing. Landing detected via reported-Z-near-body-Z + falling/
settled velocity heuristic (more reliable than the velocity-zero
test).
Per-frame tick: removed the !rm.Airborne velocity clamp from Round 1.
OnLivePositionUpdated now owns velocity policy; per-tick just
integrates whatever is set.
Both deviations from retail decomp are documented in source comments
and slated for L.3.2 (PositionManager) cleanup.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
5154a3eae1
commit
f199a6a075
1 changed files with 57 additions and 31 deletions
|
|
@ -3284,13 +3284,30 @@ public sealed class GameWindow : IDisposable
|
|||
// contact: clear Airborne, zero body.Velocity, restore contact flags.
|
||||
// This is the signal ACE uses (VectorUpdate only fires on jump start;
|
||||
// no corresponding "landed" packet — the next plain UpdatePosition is it).
|
||||
// ── AIRBORNE ────────────────────────────────────────────────────────────
|
||||
// Server is authoritative for the arc. Hard-snap position from every UP
|
||||
// while airborne; body.Velocity (set by OnLiveVectorUpdated at jump start,
|
||||
// or unchanged) continues to integrate via UpdatePhysicsInternal/gravity
|
||||
// between UPs. Don't enqueue — the queue is for grounded motion only.
|
||||
//
|
||||
// Landing heuristic (L.3.1): ACE doesn't send an explicit "landed" packet.
|
||||
// Instead we detect landing by two conditions simultaneously:
|
||||
// 1. The server-reported Z is within 0.5m of the body's current Z
|
||||
// (server has snapped to ground level — close to where we are).
|
||||
// 2. Body's vertical velocity is falling or settled (vz <= 0.5 m/s).
|
||||
// Both together mean the arc is complete. We do NOT use "velocity == 0"
|
||||
// because ACE sends non-zero velocity through the entire arc (Bug 2 root
|
||||
// cause in Round 1).
|
||||
if (rmState.Airborne)
|
||||
{
|
||||
bool velocityIsNegligible = update.Velocity is null
|
||||
|| update.Velocity.Value.LengthSquared() < 0.04f;
|
||||
if (velocityIsNegligible)
|
||||
bool reportedNearBodyZ =
|
||||
MathF.Abs(worldPos.Z - rmState.Body.Position.Z) < 0.5f;
|
||||
bool velocityFallingOrSettled =
|
||||
rmState.Body.Velocity.Z <= 0.5f;
|
||||
|
||||
if (reportedNearBodyZ && velocityFallingOrSettled)
|
||||
{
|
||||
// Landed: snap to server position, re-ground the body.
|
||||
// LANDED: snap to ground, re-ground the body.
|
||||
rmState.Airborne = false;
|
||||
rmState.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
rmState.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
|
||||
|
|
@ -3298,11 +3315,18 @@ public sealed class GameWindow : IDisposable
|
|||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable;
|
||||
rmState.Body.Position = worldPos;
|
||||
rmState.Interp.Clear();
|
||||
return;
|
||||
}
|
||||
// Still airborne: don't enqueue — let gravity arc continue.
|
||||
|
||||
// Still airborne: hard-snap so server is authoritative for the arc.
|
||||
// body.Velocity preserved from VectorUpdate; UpdatePhysicsInternal
|
||||
// integrates gravity between UPs.
|
||||
rmState.Body.Position = worldPos;
|
||||
return;
|
||||
}
|
||||
|
||||
// ── GROUNDED ────────────────────────────────────────────────────────────
|
||||
// Routing mirrors CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330).
|
||||
const float MaxPhysicsDistance = 96f;
|
||||
System.Numerics.Vector3 localPlayerPos =
|
||||
_playerController?.Position ?? System.Numerics.Vector3.Zero;
|
||||
|
|
@ -3313,27 +3337,37 @@ public sealed class GameWindow : IDisposable
|
|||
// Default-true: HasContact not on wire yet (CreateObject.ServerPosition gap).
|
||||
// bool hasContact = true; (implicit — only the teleport and distance branches below)
|
||||
|
||||
if (teleportFlag)
|
||||
if (teleportFlag || dist > MaxPhysicsDistance)
|
||||
{
|
||||
// SetPosition equivalent: hard-snap position, clear interp queue.
|
||||
// Orientation already applied unconditionally above.
|
||||
rmState.Body.Position = worldPos;
|
||||
rmState.Interp.Clear();
|
||||
}
|
||||
else if (dist > MaxPhysicsDistance)
|
||||
{
|
||||
// SetPositionSimple equivalent: slide-snap (clear queue, then hard-snap).
|
||||
// SetPosition / SetPositionSimple equivalent: hard-snap, clear queue.
|
||||
// Orientation already applied unconditionally above.
|
||||
// Zero velocity so UpdatePhysicsInternal doesn't extrapolate from
|
||||
// a prior walk-direction after a teleport or distant slide-snap.
|
||||
rmState.Interp.Clear();
|
||||
rmState.Body.Position = worldPos;
|
||||
rmState.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
// InterpolationManager.Enqueue equivalent: queue for adjust_offset to walk to.
|
||||
// NOTE: do NOT touch rmState.Body.Position here — adjust_offset (Task 5) owns it.
|
||||
// InterpolationManager.Enqueue equivalent: queue for AdjustOffset to walk to.
|
||||
// NOTE: do NOT touch rmState.Body.Position here — AdjustOffset owns it.
|
||||
// Orientation already applied unconditionally above.
|
||||
//
|
||||
// L.3.1 WORKAROUND — velocity-extrapolation between UPs:
|
||||
// Retail achieves smooth 60 fps motion via animation root motion feeding
|
||||
// PositionManager (Phase L.3.2 / PositionManager port). Until that lands,
|
||||
// AdjustOffset alone catches up in ~150 ms after each 1-Hz UP then sits
|
||||
// idle the remaining 850 ms — visible as "updates every 1 second" stepping.
|
||||
// Workaround: seed body.Velocity from the UP's velocity field so
|
||||
// UpdatePhysicsInternal integrates position += vel*dt between UPs;
|
||||
// AdjustOffset provides corrective patches when drift accumulates.
|
||||
// When update.Velocity is null the entity is stationary on this UP →
|
||||
// zero velocity → only queue-walking applies. This deviates from the
|
||||
// retail decomp finding that walking remotes have m_velocityVector == 0,
|
||||
// but is the best approximation available without root motion.
|
||||
float headingFromQuat = ExtractYawFromQuaternion(rot);
|
||||
rmState.Interp.Enqueue(worldPos, headingFromQuat, isMovingTo: false);
|
||||
rmState.Body.Velocity = update.Velocity ?? System.Numerics.Vector3.Zero;
|
||||
}
|
||||
|
||||
// Skip the legacy hard-snap path below.
|
||||
|
|
@ -5840,21 +5874,13 @@ public sealed class GameWindow : IDisposable
|
|||
rm.Body.Position += delta;
|
||||
}
|
||||
|
||||
// Bug 2a fix (L.3.1 visual verification): grounded remotes must keep
|
||||
// body.Velocity == 0 so it doesn't fight the queue. In the legacy path
|
||||
// apply_current_movement achieved this by recomputing velocity from
|
||||
// InterpretedState each tick; the new path skips apply_current_movement,
|
||||
// so we explicitly clamp. Airborne remotes keep their VectorUpdate-set
|
||||
// velocity for gravity arc integration (UpdatePhysicsInternal below).
|
||||
if (!rm.Airborne)
|
||||
{
|
||||
rm.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||
}
|
||||
|
||||
// Gravity integration: retail's UpdatePhysicsInternal still
|
||||
// fires every frame regardless of the interpolation path.
|
||||
// For grounded remotes body.Velocity == 0 so this is a no-op;
|
||||
// for airborne remotes it applies gravity to the arc.
|
||||
// Velocity policy is owned by OnLivePositionUpdated (grounded) and
|
||||
// OnLiveVectorUpdated (airborne jump start). Do NOT clamp body.Velocity
|
||||
// here — doing so stomped the velocity-extrapolation workaround seeded
|
||||
// on grounded UPs (Bug 1 regression from Round 1). UpdatePhysicsInternal
|
||||
// integrates whatever velocity is set: zero for stationary remotes,
|
||||
// update.Velocity for moving remotes (L.3.1 workaround), or the launch
|
||||
// arc velocity for airborne remotes. Gravity is applied by the same call.
|
||||
rm.Body.UpdatePhysicsInternal(dt);
|
||||
|
||||
ae.Entity.Position = rm.Body.Position;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue