revert: L.3.1 band-aid fixes (5154a3e + f199a6a)

Round 1 (5154a3e) tried to fix:
- heading locked → orientation snap-on-receipt (good idea)
- endless jump → landing detector via UP-with-zero-velocity (didn't work; ACE sends non-zero velocity through arc)

Round 2 (f199a6a) tried to fix:
- chop at 1 Hz → seed body.Velocity from update.Velocity for between-UP extrapolation (didn't help)
- endless jump → reported-Z-near-body-Z + falling-velocity heuristic (didn't catch reliably)

The actual problem was scoping: L.3.1's "InterpolationManager only" cannot
produce smooth motion. Retail combines animation root motion (L.3.2 /
PositionManager) + InterpolationManager corrections. Both halves are
required for "remotes look smooth".

Reverting to e08accf (Task 6 — VectorUpdate.Omega). The next commits
will properly port PositionManager + plumb IsGrounded through the wire
parser, replacing L.3.1-only with L.3.1+L.3.2 combined per the
revised spec.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-03 09:51:26 +02:00
parent f199a6a075
commit 1641d6ea1b

View file

@ -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;