fix(motion): retail-faithful per-frame remote tick (L.3.2 follow-up)
Multi-bug fix for the env-var-gated retail-faithful remote tick path (ACDREAM_INTERP_MANAGER=1). Combines four previously-stacked defects into one coherent rewrite: 1. PositionManager.ComputeOffset was additive (rootMotion + correction). Retail's PositionManager::adjust_offset (acclient @ 0x00555190 → InterpolationManager::adjust_offset @ 0x00555d30) REPLACES the offset frame via Frame::operator=(arg2, &__return) when catch-up engages — it does NOT add to the rootOffset that CPartArray::Update wrote. Switched to "correction overrides root motion" semantics. 2. MotionInterpreter.GetMaxSpeed was returning RunAnimSpeed × rate (~11.7 m/s for run skill 200). The retail decomp at acclient_2013_pseudo_c.txt:305127 shows get_max_speed returns the bare run rate (~2.94) — the function's float return rides the x87 FPU stack, which Binary Ninja shows as void. Caller multiplies by 2.0 to get the catch-up speed. With the wrong return our catch-up was 23.5 m/s instead of retail's 5.88 m/s — the queue would walk the body 4× too aggressively. 3. The env-var TickAnimations branch was DOUBLE-COUNTING forward translation: it applied seqVel × dt via PositionManager.ComputeOffset AND let UpdatePhysicsInternal advance body.Position += body.Velocity × dt. Both were ~11.7 m/s for run, so body raced at 23.4 m/s — "way too fast" per the user. Pass seqVel=Vector3.Zero to ComputeOffset; let body.Velocity (refreshed per tick by apply_current_movement) drive the bulk translation alone. 4. Body orientation only applied sequencer.CurrentOmega per tick. For the running-in-circles case ACE broadcasts ForwardCommand=RunForward AND TurnCommand=TurnLeft on the same UpdateMotion; the sequencer picks the RunForward cycle whose synthesized CurrentOmega is zero, so body never rotated between UPs and body.Velocity stayed in an out-of-date world direction — the visible "rectangle when running circles" effect. Prefer ObservedOmega (set explicitly in OnLiveMotionUpdated from the wire's TurnCommand + signed TurnSpeed) when present; fall back to seqOmega for standalone turn cycles. Also adds: - Sequencer-reset call in the env-var landing-fallback so the legs un-fold from Falling on land (mirrors the legacy K-fix17 path). - LastServerZ now only updates on IsGrounded UPs, so the per-tick landing-fallback floor doesn't drift up to the player's airborne peak Z and force-land mid-arc — fixes the user-reported "small landing in the air before landing on the ground" when jumping while moving. - VEL_DIAG now samples at UP arrival with overlapping windows, plus TURN_WIRE / OMEGA_DIAG / FWD_WIRE diagnostics gated on ACDREAM_REMOTE_VEL_DIAG=1 used to trace these bugs to ground truth. Verified via live retail-driven character observation 2026-05-03: turn-left now rotates left (was animating right with snap), running in circles is much smoother, jumping lands on ground (no mid-air pause). Residual ~20% steady-state overshoot for walk remains — WalkAnimSpeed=3.12 (decompiled retail constant) doesn't match ACE's actual broadcast walk pace (~2.6 m/s). Tracked separately. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9960ce3bce
commit
842dfcd092
3 changed files with 228 additions and 99 deletions
|
|
@ -935,34 +935,38 @@ public sealed class MotionInterpreter
|
|||
// ── CMotionInterp::get_max_speed (0x00527cb0) ─────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Return the motion-table-derived max speed (m/s) for the current
|
||||
/// <see cref="InterpretedMotionState.ForwardCommand"/>.
|
||||
/// Return the run rate. Mirrors retail
|
||||
/// <c>CMotionInterp::get_max_speed</c> at <c>0x00527cb0</c>.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Retail reference (named-retail, 0x00527cb0):</b>
|
||||
/// <c>CMotionInterp::get_max_speed</c> fetches the run rate via
|
||||
/// <c>InqRunRate</c> (or falls back to <c>my_run_rate</c>) and returns
|
||||
/// the result as a float from the x87 FPU stack (ST0). The Binary Ninja
|
||||
/// decompiler emits a spurious <c>void</c> return type for x87-returning
|
||||
/// functions — the actual return value is confirmed by the two callers:
|
||||
/// <c>StickyManager::adjust_offset</c> (0x00555430) and
|
||||
/// <c>InterpolationManager::AdjustOffset</c> (0x00555d52), both of which
|
||||
/// multiply the result by 2.0 to produce a catch-up speed in m/s. With a
|
||||
/// run rate of ~1.0 the catch-up would be 2.0 m/s — far too slow — so the
|
||||
/// function must return the actual velocity (m/s), not a bare rate.
|
||||
/// <b>Decomp (named-retail/acclient_2013_pseudo_c.txt:305127):</b>
|
||||
/// <code>
|
||||
/// void get_max_speed(this) {
|
||||
/// weenie_obj = this->weenie_obj;
|
||||
/// this_1 = nullptr;
|
||||
/// if (weenie_obj == 0) return;
|
||||
/// if (weenie_obj->vtable->InqRunRate(&this_1) != 0) return;
|
||||
/// this->my_run_rate; // x87 fld leaves my_run_rate on FPU stack
|
||||
/// }
|
||||
/// </code>
|
||||
/// Binary Ninja shows the return type as <c>void</c> because the float
|
||||
/// return rides the x87 FPU stack rather than EAX. Both branches
|
||||
/// emit an <c>fld</c> of either <c>this_1</c> (the InqRunRate
|
||||
/// out-param value) or <c>my_run_rate</c>, leaving the run rate on
|
||||
/// ST0 as the return value.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The per-command switch mirrors <c>get_state_velocity</c>
|
||||
/// (0x00527d50), which uses the same constants and the same
|
||||
/// RunForward / WalkForward / WalkBackward branches, and the
|
||||
/// <c>adjust_motion</c> (0x00527c0e) <c>BackwardsFactor = 0.65</c>
|
||||
/// scaling confirmed at address 0x00528010.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Used by <c>InterpolationManager.AdjustOffset</c> in L.3 Task 5
|
||||
/// as <c>2 × GetMaxSpeed()</c> catch-up speed.
|
||||
/// <b>Critical:</b> this returns the BARE run rate (typically 1.0 to
|
||||
/// ~3.0), NOT a velocity in m/s. We previously multiplied by
|
||||
/// <c>RunAnimSpeed</c> to get a m/s value, reasoning that
|
||||
/// <c>2 × bare_rate</c> would be too slow a catch-up speed for the
|
||||
/// caller (<c>InterpolationManager::adjust_offset</c>). That was a
|
||||
/// misread of the decomp — retail's catch-up IS that slow on purpose.
|
||||
/// The multi-second 1-Hz blip the user reported when observing retail
|
||||
/// remotes from acdream traced to body racing at the wrong (overshot)
|
||||
/// catch-up speed (~23.5 m/s instead of the retail-correct ~5.9 m/s
|
||||
/// for a run-skill-200 char).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public float GetMaxSpeed()
|
||||
|
|
@ -972,14 +976,7 @@ public sealed class MotionInterpreter
|
|||
float rate = MyRunRate;
|
||||
if (WeenieObj is not null && WeenieObj.InqRunRate(out float queried))
|
||||
rate = queried;
|
||||
|
||||
return InterpretedState.ForwardCommand switch
|
||||
{
|
||||
MotionCommand.RunForward => RunAnimSpeed * rate,
|
||||
MotionCommand.WalkForward => WalkAnimSpeed,
|
||||
MotionCommand.WalkBackward => WalkAnimSpeed * 0.65f, // BackwardsFactor @ adjust_motion 0x00528010
|
||||
_ => 0f, // idle / non-locomotion
|
||||
};
|
||||
return rate;
|
||||
}
|
||||
|
||||
// ── private helper ────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -42,14 +42,35 @@ public sealed class PositionManager
|
|||
InterpolationManager interp,
|
||||
float maxSpeed)
|
||||
{
|
||||
// Step 1: animation root motion (body-local → world).
|
||||
Vector3 rootMotionLocal = seqVel * (float)dt;
|
||||
Vector3 rootMotionWorld = Vector3.Transform(rootMotionLocal, ori);
|
||||
|
||||
// Step 2: interpolation correction (world-space already).
|
||||
// Retail-faithful per-frame combiner. Mirrors
|
||||
// CPhysicsObj::UpdatePositionInternal (acclient @ 0x00512c30) +
|
||||
// InterpolationManager::adjust_offset (@ 0x00555d30):
|
||||
//
|
||||
// 1. CPartArray::Update writes rootOffset (animation root motion)
|
||||
// into the per-tick Frame.
|
||||
// 2. PositionManager::adjust_offset → InterpolationManager::adjust_offset
|
||||
// either:
|
||||
// a) RETURNS EARLY when distance(body, head) < 0.05m
|
||||
// (NodeCompleted; arg2 unmodified) — body uses root motion.
|
||||
// b) OVERWRITES arg2 with `direction × min(catchUpSpeed × dt,
|
||||
// distance)` when body is far from head — catch-up REPLACES
|
||||
// root motion for this frame.
|
||||
//
|
||||
// It is NOT additive. Our prior port added rootMotion + correction
|
||||
// every frame, which stacked the animation push (≈ RunAnimSpeed ×
|
||||
// speedMod, ≈ 11.7 m/s) on top of the queue catch-up (capped at
|
||||
// ≈ 23.5 m/s) so the body advanced at up to ~3× the server's
|
||||
// broadcast pace and the head-behind-body case produced a backward
|
||||
// correction every UP — the visible 1-Hz blip the user reported.
|
||||
//
|
||||
// AdjustOffset returns Vector3.Zero in two cases mapped to retail's
|
||||
// early-return: empty queue OR distance < DesiredDistance (0.05m).
|
||||
// In both, body falls back to animation root motion.
|
||||
Vector3 correction = interp.AdjustOffset(dt, currentBodyPosition, maxSpeed);
|
||||
if (correction.LengthSquared() > 0f)
|
||||
return correction;
|
||||
|
||||
// Step 3: combined delta.
|
||||
return rootMotionWorld + correction;
|
||||
Vector3 rootMotionLocal = seqVel * (float)dt;
|
||||
return Vector3.Transform(rootMotionLocal, ori);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue