diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 8cb2351..0866e84 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -3261,72 +3261,6 @@ public sealed class GameWindow : IDisposable // - has_contact && distance ≤ 96 → InterpolationManager.Enqueue (queue) // - has_contact && distance > 96 → SetPositionSimple (slide-snap) - // Bug 1 fix (L.3.1 visual verification): apply orientation unconditionally - // on every UpdatePosition, regardless of the routing branch below. - // InterpolationManager.AdjustOffset returns a position delta only — it - // never updates Orientation. Without this, the dist≤96 enqueue branch - // never touched Body.Orientation, so remote heading was locked at whatever - // it was at login. Position lerps via the queue; heading snaps on receipt, - // which is both perceptually correct and mirrors retail's set_frame behavior - // (FUN_00514b90 @ chunk_00510000.c:5637 — direct assignment). - rmState.Body.Orientation = rot; - - // Bug 2b fix (L.3.1 visual verification): if the remote is currently - // airborne (body.Velocity set by VectorUpdate, gravity integrating), skip - // enqueueing position waypoints. The queue and the gravity sim would - // double-step position. Mirrors retail MoveOrTeleport returning false when - // has_contact == false (acclient @ 0x00516330). The landing UpdatePosition - // (received after arc completes with no/zero velocity) will arrive with - // rmState.Airborne == false and proceed normally. - // - // Bug 2c fix: detect "just landed" — if Airborne was true but this - // UpdatePosition carries no non-trivial velocity, treat it as ground - // contact: clear Airborne, zero body.Velocity, restore contact flags. - // This is the signal ACE uses (VectorUpdate only fires on jump start; - // no corresponding "landed" packet — the next plain UpdatePosition is it). - // ── AIRBORNE ──────────────────────────────────────────────────────────── - // Server is authoritative for the arc. Hard-snap position from every UP - // while airborne; body.Velocity (set by OnLiveVectorUpdated at jump start, - // or unchanged) continues to integrate via UpdatePhysicsInternal/gravity - // between UPs. Don't enqueue — the queue is for grounded motion only. - // - // Landing heuristic (L.3.1): ACE doesn't send an explicit "landed" packet. - // Instead we detect landing by two conditions simultaneously: - // 1. The server-reported Z is within 0.5m of the body's current Z - // (server has snapped to ground level — close to where we are). - // 2. Body's vertical velocity is falling or settled (vz <= 0.5 m/s). - // Both together mean the arc is complete. We do NOT use "velocity == 0" - // because ACE sends non-zero velocity through the entire arc (Bug 2 root - // cause in Round 1). - if (rmState.Airborne) - { - bool reportedNearBodyZ = - MathF.Abs(worldPos.Z - rmState.Body.Position.Z) < 0.5f; - bool velocityFallingOrSettled = - rmState.Body.Velocity.Z <= 0.5f; - - if (reportedNearBodyZ && velocityFallingOrSettled) - { - // LANDED: snap to ground, re-ground the body. - 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.Body.Position = worldPos; - rmState.Interp.Clear(); - return; - } - - // Still airborne: hard-snap so server is authoritative for the arc. - // body.Velocity preserved from VectorUpdate; UpdatePhysicsInternal - // integrates gravity between UPs. - rmState.Body.Position = worldPos; - return; - } - - // ── GROUNDED ──────────────────────────────────────────────────────────── - // Routing mirrors CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330). const float MaxPhysicsDistance = 96f; System.Numerics.Vector3 localPlayerPos = _playerController?.Position ?? System.Numerics.Vector3.Zero; @@ -3337,37 +3271,26 @@ public sealed class GameWindow : IDisposable // Default-true: HasContact not on wire yet (CreateObject.ServerPosition gap). // bool hasContact = true; (implicit — only the teleport and distance branches below) - if (teleportFlag || dist > MaxPhysicsDistance) + if (teleportFlag) { - // SetPosition / SetPositionSimple equivalent: hard-snap, clear queue. - // Orientation already applied unconditionally above. - // Zero velocity so UpdatePhysicsInternal doesn't extrapolate from - // a prior walk-direction after a teleport or distant slide-snap. + // 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.Velocity = System.Numerics.Vector3.Zero; + rmState.Body.Orientation = rot; } else { - // InterpolationManager.Enqueue equivalent: queue for AdjustOffset to walk to. - // NOTE: do NOT touch rmState.Body.Position here — AdjustOffset owns it. - // Orientation already applied unconditionally above. - // - // L.3.1 WORKAROUND — velocity-extrapolation between UPs: - // Retail achieves smooth 60 fps motion via animation root motion feeding - // PositionManager (Phase L.3.2 / PositionManager port). Until that lands, - // AdjustOffset alone catches up in ~150 ms after each 1-Hz UP then sits - // idle the remaining 850 ms — visible as "updates every 1 second" stepping. - // Workaround: seed body.Velocity from the UP's velocity field so - // UpdatePhysicsInternal integrates position += vel*dt between UPs; - // AdjustOffset provides corrective patches when drift accumulates. - // When update.Velocity is null the entity is stationary on this UP → - // zero velocity → only queue-walking applies. This deviates from the - // retail decomp finding that walking remotes have m_velocityVector == 0, - // but is the best approximation available without root motion. + // 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); - rmState.Body.Velocity = update.Velocity ?? System.Numerics.Vector3.Zero; } // Skip the legacy hard-snap path below. @@ -5874,13 +5797,10 @@ public sealed class GameWindow : IDisposable rm.Body.Position += delta; } - // Velocity policy is owned by OnLivePositionUpdated (grounded) and - // OnLiveVectorUpdated (airborne jump start). Do NOT clamp body.Velocity - // here — doing so stomped the velocity-extrapolation workaround seeded - // on grounded UPs (Bug 1 regression from Round 1). UpdatePhysicsInternal - // integrates whatever velocity is set: zero for stationary remotes, - // update.Velocity for moving remotes (L.3.1 workaround), or the launch - // arc velocity for airborne remotes. Gravity is applied by the same call. + // 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;