diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 75857de..af85140 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -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); } + /// + /// Inverse of : 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 InterpolationManager.Enqueue. + /// Standard formula: atan2( 2(wz + xy), 1 − 2(y² + z²) ). + /// + 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;