fix(motion): heading + jump bugs in InterpolationManager path (L.3.1)
Visual verification (Task 7) revealed two bugs in the new env-var gated path: 1. Heading locked at login direction. Cause: AdjustOffset returns position delta only; the dist≤96 enqueue branch never updated body.Orientation. Fix: apply orientation unconditionally on every UpdatePosition (snap-on-receipt). Position lerps via queue. 2. Endless jumping. Cause: (a) body.Velocity persisted forever after arc landed because apply_current_movement no longer ran; (b) UpdatePositions during the arc were enqueued, fighting the gravity sim. Fix: skip enqueue when rm.Airborne (mirrors retail MoveOrTeleport has_contact=false → no-op); zero non-airborne body.Velocity each tick (mirrors legacy apply_current_movement); detect landed when receiving UpdatePosition while airborne with no/zero velocity. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
e08accf7c2
commit
5154a3eae1
1 changed files with 57 additions and 3 deletions
|
|
@ -3261,6 +3261,48 @@ public sealed class GameWindow : IDisposable
|
||||||
// - has_contact && distance ≤ 96 → InterpolationManager.Enqueue (queue)
|
// - has_contact && distance ≤ 96 → InterpolationManager.Enqueue (queue)
|
||||||
// - has_contact && distance > 96 → SetPositionSimple (slide-snap)
|
// - has_contact && distance > 96 → SetPositionSimple (slide-snap)
|
||||||
|
|
||||||
|
// Bug 1 fix (L.3.1 visual verification): apply orientation unconditionally
|
||||||
|
// on every UpdatePosition, regardless of the routing branch below.
|
||||||
|
// InterpolationManager.AdjustOffset returns a position delta only — it
|
||||||
|
// never updates Orientation. Without this, the dist≤96 enqueue branch
|
||||||
|
// never touched Body.Orientation, so remote heading was locked at whatever
|
||||||
|
// it was at login. Position lerps via the queue; heading snaps on receipt,
|
||||||
|
// which is both perceptually correct and mirrors retail's set_frame behavior
|
||||||
|
// (FUN_00514b90 @ chunk_00510000.c:5637 — direct assignment).
|
||||||
|
rmState.Body.Orientation = rot;
|
||||||
|
|
||||||
|
// Bug 2b fix (L.3.1 visual verification): if the remote is currently
|
||||||
|
// airborne (body.Velocity set by VectorUpdate, gravity integrating), skip
|
||||||
|
// enqueueing position waypoints. The queue and the gravity sim would
|
||||||
|
// double-step position. Mirrors retail MoveOrTeleport returning false when
|
||||||
|
// has_contact == false (acclient @ 0x00516330). The landing UpdatePosition
|
||||||
|
// (received after arc completes with no/zero velocity) will arrive with
|
||||||
|
// rmState.Airborne == false and proceed normally.
|
||||||
|
//
|
||||||
|
// Bug 2c fix: detect "just landed" — if Airborne was true but this
|
||||||
|
// UpdatePosition carries no non-trivial velocity, treat it as ground
|
||||||
|
// 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).
|
||||||
|
if (rmState.Airborne)
|
||||||
|
{
|
||||||
|
bool velocityIsNegligible = update.Velocity is null
|
||||||
|
|| update.Velocity.Value.LengthSquared() < 0.04f;
|
||||||
|
if (velocityIsNegligible)
|
||||||
|
{
|
||||||
|
// Landed: snap to server position, re-ground the body.
|
||||||
|
rmState.Airborne = false;
|
||||||
|
rmState.Body.Velocity = System.Numerics.Vector3.Zero;
|
||||||
|
rmState.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
|
||||||
|
rmState.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
||||||
|
| AcDream.Core.Physics.TransientStateFlags.OnWalkable;
|
||||||
|
rmState.Body.Position = worldPos;
|
||||||
|
rmState.Interp.Clear();
|
||||||
|
}
|
||||||
|
// Still airborne: don't enqueue — let gravity arc continue.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const float MaxPhysicsDistance = 96f;
|
const float MaxPhysicsDistance = 96f;
|
||||||
System.Numerics.Vector3 localPlayerPos =
|
System.Numerics.Vector3 localPlayerPos =
|
||||||
_playerController?.Position ?? System.Numerics.Vector3.Zero;
|
_playerController?.Position ?? System.Numerics.Vector3.Zero;
|
||||||
|
|
@ -3273,22 +3315,23 @@ public sealed class GameWindow : IDisposable
|
||||||
|
|
||||||
if (teleportFlag)
|
if (teleportFlag)
|
||||||
{
|
{
|
||||||
// SetPosition equivalent: hard-snap position + orientation, clear interp queue.
|
// SetPosition equivalent: hard-snap position, clear interp queue.
|
||||||
|
// Orientation already applied unconditionally above.
|
||||||
rmState.Body.Position = worldPos;
|
rmState.Body.Position = worldPos;
|
||||||
rmState.Body.Orientation = rot;
|
|
||||||
rmState.Interp.Clear();
|
rmState.Interp.Clear();
|
||||||
}
|
}
|
||||||
else if (dist > MaxPhysicsDistance)
|
else if (dist > MaxPhysicsDistance)
|
||||||
{
|
{
|
||||||
// SetPositionSimple equivalent: slide-snap (clear queue, then hard-snap).
|
// SetPositionSimple equivalent: slide-snap (clear queue, then hard-snap).
|
||||||
|
// Orientation already applied unconditionally above.
|
||||||
rmState.Interp.Clear();
|
rmState.Interp.Clear();
|
||||||
rmState.Body.Position = worldPos;
|
rmState.Body.Position = worldPos;
|
||||||
rmState.Body.Orientation = rot;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// InterpolationManager.Enqueue equivalent: queue for adjust_offset to walk to.
|
// InterpolationManager.Enqueue equivalent: queue for adjust_offset to walk to.
|
||||||
// NOTE: do NOT touch rmState.Body.Position here — adjust_offset (Task 5) owns it.
|
// NOTE: do NOT touch rmState.Body.Position here — adjust_offset (Task 5) owns it.
|
||||||
|
// Orientation already applied unconditionally above.
|
||||||
float headingFromQuat = ExtractYawFromQuaternion(rot);
|
float headingFromQuat = ExtractYawFromQuaternion(rot);
|
||||||
rmState.Interp.Enqueue(worldPos, headingFromQuat, isMovingTo: false);
|
rmState.Interp.Enqueue(worldPos, headingFromQuat, isMovingTo: false);
|
||||||
}
|
}
|
||||||
|
|
@ -5797,6 +5840,17 @@ public sealed class GameWindow : IDisposable
|
||||||
rm.Body.Position += delta;
|
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
|
// Gravity integration: retail's UpdatePhysicsInternal still
|
||||||
// fires every frame regardless of the interpolation path.
|
// fires every frame regardless of the interpolation path.
|
||||||
// For grounded remotes body.Velocity == 0 so this is a no-op;
|
// For grounded remotes body.Velocity == 0 so this is a no-op;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue