From e94e7913fb60ba4c2461570420852dca10827576 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 3 May 2026 10:18:24 +0200 Subject: [PATCH] feat(motion): retail-faithful per-frame remote tick (L.3.1+L.3.2 Task 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combines PositionManager (Task 1, commit 08fbbef) + IsGrounded plumbing (Task 2, commit 5d71731) into the per-frame remote motion path. Three changes in GameWindow.cs, all gated behind ACDREAM_INTERP_MANAGER=1: 1. RemoteMotion gains Position field (PositionManager instance). 2. OnLivePositionUpdated env-var branch rewritten to mirror retail CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330): - orientation snap-on-receipt (PositionManager handles position only) - airborne (!IsGrounded) → no-op (server is authoritative for arc; body.Velocity from VectorUpdate integrates gravity locally) - landing transition (first IsGrounded=true after Airborne) → clear airborne flags, hard-snap to landing pos, clear queue - grounded routing: dist > 96m → slide-snap; dist ≤ 96m → enqueue 3. TickAnimations env-var branch rewritten to use PositionManager: body.Position += PositionManager.ComputeOffset(dt, pos, seqVel, ori, interp, maxSpeed); body.UpdatePhysicsInternal(dt) for gravity. Replaces the L.3.1-only AdjustOffset-only path. Legacy (env-var off) path unchanged. Cleanup commit (next sub-task) deletes the env-var dual paths after visual verification. Co-Authored-By: Claude Opus 4.7 --- src/AcDream.App/Rendering/GameWindow.cs | 117 ++++++++++++------------ 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 0866e84..59a2a28 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -342,6 +342,14 @@ public sealed class GameWindow : IDisposable public AcDream.Core.Physics.InterpolationManager Interp { get; } = new AcDream.Core.Physics.InterpolationManager(); + /// + /// Per-frame combiner for animation root motion + InterpolationManager + /// correction (Phase L.3.2). Consumed in TickAnimations to compute the + /// per-frame body.Position delta. + /// + public AcDream.Core.Physics.PositionManager Position { get; } = + new AcDream.Core.Physics.PositionManager(); + public RemoteMotion() { Body = new AcDream.Core.Physics.PhysicsBody @@ -3254,46 +3262,55 @@ public sealed class GameWindow : IDisposable // identical to before this commit. Legacy hard-snap path remains below. if (System.Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1") { - // CPhysicsObj::MoveOrTeleport router (acclient @ 0x00516330): - // - stale instance/position seq → ignore (TODO: IsStaleSequence not yet plumbed) - // - teleport-seq newer or no-cell → SetPosition (hard-snap) - // - has_contact false → no-op (TODO: HasContact not on wire — default true for L.3.1) - // - has_contact && distance ≤ 96 → InterpolationManager.Enqueue (queue) - // - has_contact && distance > 96 → SetPositionSimple (slide-snap) + // Orientation always snaps on receipt — InterpolationManager walks + // position only; heading would otherwise lag the queue. + rmState.Body.Orientation = rot; + // ── AIRBORNE NO-OP ──────────────────────────────────────────── + // Mirrors retail CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330): + // when has_contact==0, return false (don't touch body, don't queue). + // body.Velocity (set once by OnLiveVectorUpdated at jump start) keeps + // integrating gravity via per-frame UpdatePhysicsInternal. Server is + // authoritative for the arc; we don't predict it locally. + if (!update.IsGrounded) + return; + + // ── LANDING TRANSITION ──────────────────────────────────────── + // First IsGrounded=true UP after rmState.Airborne signals landed. + // Clear airborne flags, hard-snap to authoritative landing position, + // clear interpolation queue (any pre-jump waypoints are stale). + if (rmState.Airborne) + { + 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.Interp.Clear(); + rmState.Body.Position = worldPos; + return; + } + + // ── GROUNDED ROUTING (CPhysicsObj::MoveOrTeleport) ──────────── const float MaxPhysicsDistance = 96f; - System.Numerics.Vector3 localPlayerPos = - _playerController?.Position ?? System.Numerics.Vector3.Zero; + var localPlayerPos = _playerController?.Position ?? System.Numerics.Vector3.Zero; float dist = System.Numerics.Vector3.Distance(worldPos, localPlayerPos); - // Default-false: teleport flag not plumbed until sequence comparison lands (Task 5+). - bool teleportFlag = false; - // Default-true: HasContact not on wire yet (CreateObject.ServerPosition gap). - // bool hasContact = true; (implicit — only the teleport and distance branches below) - - if (teleportFlag) + if (dist > MaxPhysicsDistance) { - // SetPosition equivalent: hard-snap position + orientation, clear interp queue. - rmState.Body.Position = worldPos; - rmState.Body.Orientation = rot; - rmState.Interp.Clear(); - } - else if (dist > MaxPhysicsDistance) - { - // SetPositionSimple equivalent: slide-snap (clear queue, then hard-snap). + // Beyond view bubble: SetPositionSimple slide-snap. Clear queue. rmState.Interp.Clear(); rmState.Body.Position = worldPos; - rmState.Body.Orientation = rot; } 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. + // Within view bubble: enqueue waypoint for adjust_offset to walk to. + // PositionManager (called per-frame in TickAnimations) handles the + // actual body advancement — mix of animation root motion + queue + // correction. float headingFromQuat = ExtractYawFromQuaternion(rot); rmState.Interp.Enqueue(worldPos, headingFromQuat, isMovingTo: false); } - - // Skip the legacy hard-snap path below. return; } @@ -5771,36 +5788,24 @@ public sealed class GameWindow : IDisposable { 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) + // ── NEW PATH: PositionManager (animation root motion + InterpolationManager) ── + // (L.3.1+L.3.2 Task 3 — 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. + // Always-run-all-steps per retail CPhysicsObj::UpdateObjectInternal + // (acclient @ 0x00513730): + // 1+2. animation root motion + interpolation correction (combined) + // 3. physics integration (gravity for airborne; no-op for grounded) + System.Numerics.Vector3 seqVel = ae.Sequencer?.CurrentVelocity + ?? System.Numerics.Vector3.Zero; + float maxSpeed = rm.Motion.GetMaxSpeed(); + System.Numerics.Vector3 offset = rm.Position.ComputeOffset( + dt: (double)dt, + currentBodyPosition: rm.Body.Position, + seqVel: seqVel, + ori: rm.Body.Orientation, + interp: rm.Interp, + maxSpeed: maxSpeed); + rm.Body.Position += offset; rm.Body.UpdatePhysicsInternal(dt); ae.Entity.Position = rm.Body.Position;