feat(motion): retail-faithful per-frame remote tick (L.3.1+L.3.2 Task 3)
Combines PositionManager (Task 1, commit08fbbef) + IsGrounded plumbing (Task 2, commit5d71731) 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 <noreply@anthropic.com>
This commit is contained in:
parent
5d717312cc
commit
e94e7913fb
1 changed files with 61 additions and 56 deletions
|
|
@ -342,6 +342,14 @@ public sealed class GameWindow : IDisposable
|
|||
public AcDream.Core.Physics.InterpolationManager Interp { get; } =
|
||||
new AcDream.Core.Physics.InterpolationManager();
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame combiner for animation root motion + InterpolationManager
|
||||
/// correction (Phase L.3.2). Consumed in TickAnimations to compute the
|
||||
/// per-frame body.Position delta.
|
||||
/// </summary>
|
||||
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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue