feat(motion): MoveOrTeleport routing in OnLivePositionUpdated (L.3.1 Task 4)

Wraps the legacy hard-snap path in ACDREAM_INTERP_MANAGER=1 env-var
guard. When set, runs retail-faithful routing (acclient!CPhysicsObj::
MoveOrTeleport @ 0x00516330):
- distance > 96m → hard-snap (SetPositionSimple equivalent)
- distance ≤ 96m → Interp.Enqueue (queue for adjust_offset to walk to)
- teleport flag → hard-snap (default false until sequence plumbing)
- has_contact false → no-op (default true until parser plumbing)

Existing hard-snap behavior preserved when flag unset (default).
Old path will be removed in cleanup commit (Task 8) after visual
verification.

Helper: ExtractYawFromQuaternion (inverse of GameWindow.YawToAcQuaternion).

TODO followups (filed as plan known-limitations):
- IsStaleSequence (uint16 wrap-aware compare on 4 sequence counters)
- HasContact wire field (CreateObject.ServerPosition gap)
- Teleport-sequence comparison

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

View file

@ -3241,6 +3241,56 @@ public sealed class GameWindow : IDisposable
// slerp doesn't visibly rotate from Identity to truth.
rmState.Body.Orientation = rot;
}
// L.3.1 Task 4: env-var gated retail-faithful MoveOrTeleport routing.
// Mirrors CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330).
// Enabled only when ACDREAM_INTERP_MANAGER=1 to keep default behavior
// 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)
const float MaxPhysicsDistance = 96f;
System.Numerics.Vector3 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)
{
// 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).
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.
float headingFromQuat = ExtractYawFromQuaternion(rot);
rmState.Interp.Enqueue(worldPos, headingFromQuat, isMovingTo: false);
}
// Skip the legacy hard-snap path below.
return;
}
double nowSec = (now - System.DateTime.UnixEpoch).TotalSeconds;
System.Numerics.Vector3? serverVelocity = update.Velocity;
if (serverVelocity is null
@ -5131,6 +5181,20 @@ public sealed class GameWindow : IDisposable
return new System.Numerics.Quaternion(0f, 0f, z, w);
}
/// <summary>
/// Inverse of <see cref="YawToAcQuaternion"/>: extracts the local yaw (rotation
/// about the Z axis, in radians) from an AC wire quaternion.
/// Yaw=0 faces +X (East). Used by the L.3.1 InterpolationManager routing to
/// convert server orientation into the heading expected by <c>InterpolationManager.Enqueue</c>.
/// Standard formula: atan2( 2(wz + xy), 1 2(y² + z²) ).
/// </summary>
private static float ExtractYawFromQuaternion(System.Numerics.Quaternion q)
{
return MathF.Atan2(
2f * (q.W * q.Z + q.X * q.Y),
1f - 2f * (q.Y * q.Y + q.Z * q.Z));
}
private void OnCameraModeChanged(bool _modeBool)
{
if (_input is null) return;