feat(motion): per-frame Interp.AdjustOffset in remote tick (L.3.1 Task 5)

Wraps the existing legacy per-frame remote tick (apply_current_movement
+ force-OnWalkable + Euler-extrapolate) in ACDREAM_INTERP_MANAGER=1
env-var guard. When set:
- if Interp.IsActive: rm.Body.Position += Interp.AdjustOffset(dt, pos, maxSpeed)
- still call body.UpdatePhysicsInternal so airborne arcs (gravity)
  continue to integrate via the OnLiveVectorUpdated-set velocity.

When env-var unset (default), legacy path runs unchanged.

Mirrors retail's per-tick CPhysicsObj::UpdateObjectInternal (acclient
@ 0x00513730) which calls InterpolationManager::adjust_offset
(@ 0x00555D30) every frame.

Old legacy path will be removed in Task 8 cleanup commit after visual
verification.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-02 19:31:03 +02:00
parent 062e19f463
commit ae79e34a6d

View file

@ -5763,6 +5763,47 @@ public sealed class GameWindow : IDisposable
&& serverGuid != _playerServerGuid && serverGuid != _playerServerGuid
&& _remoteDeadReckon.TryGetValue(serverGuid, out var rm)) && _remoteDeadReckon.TryGetValue(serverGuid, out var rm))
{ {
if (System.Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
{
// ── NEW PATH: queued position-chase via InterpolationManager ──
// (L.3.1 Task 5 — ACDREAM_INTERP_MANAGER=1 gates this path)
//
// Walking remotes have m_velocityVector == 0 in retail; all
// visible horizontal motion comes from
// InterpolationManager::adjust_offset (acclient @ 0x00555D30)
// walking the body toward the head of the waypoint queue at
// 2 × motion_max_speed × dt (clamped, 7.5 m/s fallback).
//
// Mirrors retail CPhysicsObj::UpdateObjectInternal
// (acclient @ 0x00513730) which calls adjust_offset every frame
// before UpdatePhysicsInternal integrates gravity.
//
// For airborne remotes, OnLiveVectorUpdated has set
// body.Velocity (launch arc); we still call
// UpdatePhysicsInternal below so gravity applies each frame and
// produces the parabolic arc. The IsActive gate prevents
// AdjustOffset from pulling against an in-flight arc when no
// waypoints are queued for a jumping remote.
if (rm.Interp.IsActive)
{
float maxSpeed = rm.Motion.GetMaxSpeed();
System.Numerics.Vector3 delta = rm.Interp.AdjustOffset((double)dt, rm.Body.Position, maxSpeed);
rm.Body.Position += delta;
}
// 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.
rm.Body.UpdatePhysicsInternal(dt);
ae.Entity.Position = rm.Body.Position;
ae.Entity.Rotation = rm.Body.Orientation;
}
else
{
// ── LEGACY PATH (UNCHANGED — kept until Task 8 cleanup) ──
//
// Stop detection is handled explicitly on packet receipt: // Stop detection is handled explicitly on packet receipt:
// - UpdateMotion with ForwardCommand flag CLEARED → Ready. // - UpdateMotion with ForwardCommand flag CLEARED → Ready.
// - UpdatePosition with HasVelocity flag CLEARED → StopCompletely. // - UpdatePosition with HasVelocity flag CLEARED → StopCompletely.
@ -6078,6 +6119,7 @@ public sealed class GameWindow : IDisposable
ae.Entity.Position = rm.Body.Position; ae.Entity.Position = rm.Body.Position;
ae.Entity.Rotation = rm.Body.Orientation; ae.Entity.Rotation = rm.Body.Orientation;
} }
}
// ── Get per-part (origin, orientation) from either sequencer or legacy ── // ── Get per-part (origin, orientation) from either sequencer or legacy ──
IReadOnlyList<AcDream.Core.Physics.PartTransform>? seqFrames = null; IReadOnlyList<AcDream.Core.Physics.PartTransform>? seqFrames = null;