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
|
|
@ -375,8 +375,14 @@ public sealed class GameWindow : IDisposable
|
|||
/// </summary>
|
||||
public System.Numerics.Vector3 PrevServerPos;
|
||||
public double PrevServerPosTime;
|
||||
public double LastVelDiagLogTime;
|
||||
public double LastOmegaDiagLogTime;
|
||||
/// <summary>
|
||||
/// Diagnostic-only: max |sequencer.CurrentVelocity| observed across
|
||||
/// all per-tick samples since the last UpdatePosition arrival. The
|
||||
/// next UP compares this against (LastServerPos - PrevServerPos) /
|
||||
/// dtServer to compute the overshoot ratio. Reset on each UP.
|
||||
/// </summary>
|
||||
public float MaxSeqSpeedSinceLastUP;
|
||||
|
||||
public RemoteMotion()
|
||||
{
|
||||
|
|
@ -2782,6 +2788,15 @@ public sealed class GameWindow : IDisposable
|
|||
// to the Attack/Twitch/etc command, and
|
||||
// get_state_velocity returns 0 because the gate is
|
||||
// RunForward||WalkForward — body stops moving forward.
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1"
|
||||
&& remoteMot.Motion.InterpretedState.ForwardCommand != fullMotion)
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[FWD_WIRE] guid={update.Guid:X8} "
|
||||
+ $"oldCmd=0x{remoteMot.Motion.InterpretedState.ForwardCommand:X8} "
|
||||
+ $"newCmd=0x{fullMotion:X8} "
|
||||
+ $"newLow=0x{fullMotion & 0xFFu:X2} speed={speedMod:F3}");
|
||||
}
|
||||
remoteMot.Motion.InterpretedState.ForwardCommand = fullMotion;
|
||||
// Pass speedMod through verbatim — preserve sign so retail's
|
||||
// adjust_motion'd backward walk (cmd=WalkForward, spd<0)
|
||||
|
|
@ -3312,19 +3327,46 @@ public sealed class GameWindow : IDisposable
|
|||
// position only; heading would otherwise lag the queue.
|
||||
rmState.Body.Orientation = rot;
|
||||
|
||||
// Track the most recent server-broadcast Z on EVERY UP — including
|
||||
// mid-arc airborne ones. Read by the per-tick landing-fallback in
|
||||
// TickAnimations: if gravity drags the body below this floor while
|
||||
// still airborne, we force-land locally even when the server never
|
||||
// Track the most recent GROUNDED server-broadcast Z. Read by
|
||||
// the per-tick landing-fallback in TickAnimations: if gravity
|
||||
// drags the body more than 0.5 m below this floor while still
|
||||
// airborne, we force-land locally even when the server never
|
||||
// sent an IsGrounded=true UP for the actual landing frame.
|
||||
rmState.LastServerZ = worldPos.Z;
|
||||
//
|
||||
// Only updated for grounded UPs — mid-arc airborne UPs would
|
||||
// raise this value to the player's peak Z, then the body's
|
||||
// descent would cross (peak - 0.5) and trigger a force-land
|
||||
// mid-air, producing the user-reported "small landing in the
|
||||
// air before landing on the ground" when jumping while moving.
|
||||
if (update.IsGrounded)
|
||||
rmState.LastServerZ = worldPos.Z;
|
||||
|
||||
// Diagnostic-only (ACDREAM_REMOTE_VEL_DIAG=1): roll the previous
|
||||
// server-pos snapshot forward so the per-tick comparison has a
|
||||
// delta to work with. Cheap (struct copy + double write); not
|
||||
// gated here because the read side gates the actual print.
|
||||
// Diagnostic (ACDREAM_REMOTE_VEL_DIAG=1): roll the previous
|
||||
// server-pos snapshot forward AND print the per-UP comparison
|
||||
// between the max sequencer speed observed since last UP and
|
||||
// the actual server broadcast pace. Both sides are now sampled
|
||||
// over the same window so the ratio reflects real overshoot.
|
||||
{
|
||||
double nowSecDiag = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1"
|
||||
&& rmState.LastServerPosTime > 0.0)
|
||||
{
|
||||
double dtServer = nowSecDiag - rmState.LastServerPosTime;
|
||||
if (dtServer > 0.001)
|
||||
{
|
||||
var serverDelta = worldPos - rmState.LastServerPos;
|
||||
float serverSpeed = (float)(serverDelta.Length() / dtServer);
|
||||
float seqSpeed = rmState.MaxSeqSpeedSinceLastUP;
|
||||
if (serverSpeed > 0.1f || seqSpeed > 0.1f)
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[VEL_DIAG] guid={update.Guid:X8} maxSeqSpeed={seqSpeed:F3} m/s "
|
||||
+ $"serverSpeed={serverSpeed:F3} m/s dtServer={dtServer:F3}s "
|
||||
+ $"ratio={(serverSpeed > 1e-3f ? seqSpeed / serverSpeed : 0f):F3}");
|
||||
}
|
||||
}
|
||||
}
|
||||
rmState.MaxSeqSpeedSinceLastUP = 0f;
|
||||
rmState.PrevServerPos = rmState.LastServerPos;
|
||||
rmState.PrevServerPosTime = rmState.LastServerPosTime;
|
||||
rmState.LastServerPos = worldPos;
|
||||
|
|
@ -3353,6 +3395,22 @@ public sealed class GameWindow : IDisposable
|
|||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable;
|
||||
rmState.Interp.Clear();
|
||||
rmState.Body.Position = worldPos;
|
||||
|
||||
// Reset the sequencer out of Falling — see matching block in
|
||||
// TickAnimations Step 5 (env-var path) for rationale.
|
||||
if (_animatedEntities.TryGetValue(entity.Id, out var aeForLand)
|
||||
&& aeForLand.Sequencer is not null)
|
||||
{
|
||||
uint style = aeForLand.Sequencer.CurrentStyle != 0
|
||||
? aeForLand.Sequencer.CurrentStyle
|
||||
: 0x8000003Du;
|
||||
uint landingCmd = rmState.Motion.InterpretedState.ForwardCommand;
|
||||
if (landingCmd == 0)
|
||||
landingCmd = AcDream.Core.Physics.MotionCommand.Ready;
|
||||
float landingSpeed = rmState.Motion.InterpretedState.ForwardSpeed;
|
||||
if (landingSpeed <= 0f) landingSpeed = 1f;
|
||||
aeForLand.Sequencer.SetCycle(style, landingCmd, landingSpeed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -5853,43 +5911,85 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_INTERP_MANAGER") == "1")
|
||||
{
|
||||
// ── NEW PATH: PositionManager (animation root motion + InterpolationManager) ──
|
||||
// ── NEW PATH: retail-faithful per-frame remote tick ──
|
||||
// (L.3.1+L.3.2 Task 3/follow-up — ACDREAM_INTERP_MANAGER=1 gates this path)
|
||||
//
|
||||
// Always-run-all-steps per retail CPhysicsObj::UpdateObjectInternal
|
||||
// (acclient @ 0x00513730):
|
||||
// 1+2. animation root motion + interpolation correction (combined)
|
||||
// 2.5 sequencer omega → body orientation (TurnRight/TurnLeft angular velocity)
|
||||
// 3. calc_acceleration (gravity flag → body.Acceleration)
|
||||
// 4. physics integration (gravity for airborne; no-op for grounded)
|
||||
// Mirrors retail CPhysicsObj::UpdateObjectInternal
|
||||
// (acclient @ 0x005156b0) → UpdatePositionInternal (@ 0x00512c30):
|
||||
//
|
||||
// 1. Force grounded transient flags (matches the legacy path
|
||||
// and the gate inside MotionInterpreter.apply_current_movement
|
||||
// which only writes velocity when OnWalkable is set).
|
||||
// 2. apply_current_movement → body.set_local_velocity(get_state_velocity())
|
||||
// Refreshes body.Velocity from the current InterpretedState
|
||||
// every tick. Matches the legacy path that has been working
|
||||
// for player remotes since pre-L.3.
|
||||
// 3. PositionManager.ComputeOffset returns ONLY the
|
||||
// InterpolationManager catch-up correction (with seqVel=0).
|
||||
// Retail's CPartArray::Update writes a tiny per-anim-frame
|
||||
// stride into the offset frame; PositionManager::adjust_offset
|
||||
// either lets it through or REPLACES it with catch-up. Our
|
||||
// AnimationSequencer.CurrentVelocity is the SYNTHESIZED
|
||||
// RunAnimSpeed × speedMod (matches body.Velocity), NOT a
|
||||
// per-anim-frame stride — passing it as root motion
|
||||
// double-counts the bulk translation that body.Velocity
|
||||
// already provides via UpdatePhysicsInternal. Pass zero
|
||||
// so only the queue-correction reaches the body.
|
||||
// 4. Apply correction to body.Position.
|
||||
// 5. Sequencer omega → body orientation (turn cycles).
|
||||
// 6. calc_acceleration + UpdatePhysicsInternal — Euler-
|
||||
// integrates body.Position += body.Velocity × dt.
|
||||
|
||||
// Sequencer-driven motion sources: linear velocity (root motion)
|
||||
// AND angular velocity (turn-cycle omega).
|
||||
System.Numerics.Vector3 seqVel = ae.Sequencer?.CurrentVelocity
|
||||
?? System.Numerics.Vector3.Zero;
|
||||
System.Numerics.Vector3 seqOmega = ae.Sequencer?.CurrentOmega
|
||||
?? System.Numerics.Vector3.Zero;
|
||||
|
||||
// Step 1+2: animation root motion + Interp correction (combined via PositionManager).
|
||||
// Step 1: grounded flags so apply_current_movement writes velocity.
|
||||
if (!rm.Airborne)
|
||||
{
|
||||
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
||||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable
|
||||
| AcDream.Core.Physics.TransientStateFlags.Active;
|
||||
|
||||
// Step 2: refresh body.Velocity from current motion state.
|
||||
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Active;
|
||||
}
|
||||
|
||||
// Step 3+4: queue catch-up correction only (no double-count of seqVel).
|
||||
float maxSpeed = rm.Motion.GetMaxSpeed();
|
||||
System.Numerics.Vector3 offset = rm.Position.ComputeOffset(
|
||||
dt: (double)dt,
|
||||
currentBodyPosition: rm.Body.Position,
|
||||
seqVel: seqVel,
|
||||
seqVel: System.Numerics.Vector3.Zero,
|
||||
ori: rm.Body.Orientation,
|
||||
interp: rm.Interp,
|
||||
maxSpeed: maxSpeed);
|
||||
rm.Body.Position += offset;
|
||||
|
||||
// Step 2.5: animation-driven rotation. Retail's sequencer bakes Omega
|
||||
// for TurnRight/TurnLeft cycles; we apply it as a per-frame quaternion
|
||||
// rotation. seqOmega is body-local angular velocity (axis-angle: axis
|
||||
// is omega.Normalized, magnitude is rad/sec). For Z-axis turns it's
|
||||
// (0, 0, ±π/2 × turnSpeed) typically.
|
||||
if (seqOmega.LengthSquared() > 1e-9f)
|
||||
// Step 2.5: angular velocity → body orientation. Prefer
|
||||
// ObservedOmega (set explicitly in OnLiveMotionUpdated from
|
||||
// the wire's TurnCommand + signed TurnSpeed) over the
|
||||
// sequencer's synthesized omega: when the player runs in
|
||||
// a circle ACE broadcasts ForwardCommand=RunForward AND
|
||||
// TurnCommand=TurnLeft on the same UpdateMotion. The
|
||||
// sequencer's animCycle picker chooses RunForward (legs
|
||||
// running), whose synthesized CurrentOmega is zero. Body
|
||||
// would not rotate between UPs and body.Velocity stays in
|
||||
// an out-of-date world direction, producing the
|
||||
// user-reported "rectangle when running circles" effect.
|
||||
// ObservedOmega has the correct turn rate even when the
|
||||
// visible cycle is RunForward.
|
||||
System.Numerics.Vector3 omegaToApply =
|
||||
rm.ObservedOmega.LengthSquared() > 1e-9f
|
||||
? rm.ObservedOmega
|
||||
: seqOmega;
|
||||
if (omegaToApply.LengthSquared() > 1e-9f)
|
||||
{
|
||||
float angleDelta = seqOmega.Length() * (float)dt;
|
||||
System.Numerics.Vector3 axis = System.Numerics.Vector3.Normalize(seqOmega);
|
||||
float angleDelta = omegaToApply.Length() * (float)dt;
|
||||
System.Numerics.Vector3 axis = System.Numerics.Vector3.Normalize(omegaToApply);
|
||||
var rot = System.Numerics.Quaternion.CreateFromAxisAngle(axis, angleDelta);
|
||||
rm.Body.Orientation = System.Numerics.Quaternion.Normalize(
|
||||
System.Numerics.Quaternion.Concatenate(rm.Body.Orientation, rot));
|
||||
|
|
@ -5906,7 +6006,9 @@ public sealed class GameWindow : IDisposable
|
|||
uint seqMotion = ae.Sequencer?.CurrentMotion ?? 0;
|
||||
System.Console.WriteLine(
|
||||
$"[OMEGA_DIAG] guid={serverGuid:X8} motion=0x{seqMotion:X8} "
|
||||
+ $"seqOmega.Z={seqOmega.Z:F3} (Z>0=CCW=TurnLeft, Z<0=CW=TurnRight)");
|
||||
+ $"omegaApplied.Z={omegaToApply.Z:F3} "
|
||||
+ $"(seq.Z={seqOmega.Z:F3} obs.Z={rm.ObservedOmega.Z:F3}) "
|
||||
+ $"(Z>0=CCW=TurnLeft, Z<0=CW=TurnRight)");
|
||||
rm.LastOmegaDiagLogTime = nowSec;
|
||||
}
|
||||
}
|
||||
|
|
@ -5945,39 +6047,48 @@ public sealed class GameWindow : IDisposable
|
|||
rm.Interp.Clear();
|
||||
rm.Body.Position = new System.Numerics.Vector3(
|
||||
rm.Body.Position.X, rm.Body.Position.Y, rm.LastServerZ);
|
||||
|
||||
// Swap the sequencer out of Falling — without this the
|
||||
// legs stay folded in the airborne pose forever even
|
||||
// though the body is now planted on the ground. Mirrors
|
||||
// the legacy K-fix17 path at the bottom of TickAnimations
|
||||
// (line ~6284): pick the cycle from the last-known
|
||||
// InterpretedState.ForwardCommand, falling back to Ready
|
||||
// when nothing is held. The next UpdateMotion the server
|
||||
// sends will refine if the player was strafing/turning
|
||||
// mid-jump; this just gets them out of Falling now.
|
||||
if (ae.Sequencer is not null)
|
||||
{
|
||||
uint style = ae.Sequencer.CurrentStyle != 0
|
||||
? ae.Sequencer.CurrentStyle
|
||||
: 0x8000003Du;
|
||||
uint landingCmd = rm.Motion.InterpretedState.ForwardCommand;
|
||||
if (landingCmd == 0)
|
||||
landingCmd = AcDream.Core.Physics.MotionCommand.Ready;
|
||||
float landingSpeed = rm.Motion.InterpretedState.ForwardSpeed;
|
||||
if (landingSpeed <= 0f) landingSpeed = 1f;
|
||||
ae.Sequencer.SetCycle(style, landingCmd, landingSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: speed-overshoot diagnostic (ACDREAM_REMOTE_VEL_DIAG=1).
|
||||
// Compare the sequencer's body-local CurrentVelocity (root motion
|
||||
// we're applying per tick) against the server's effective
|
||||
// broadcast pace ((LastServerPos - PrevServerPos) / Δt). If
|
||||
// |seqVel| significantly exceeds |serverVel|, the body
|
||||
// overshoots between UPs and the InterpolationManager has to
|
||||
// walk it backward each waypoint — visible as 1-Hz blips.
|
||||
// The ratio prints once per remote per ~2 seconds so a moving
|
||||
// remote shows up without flooding the console.
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1"
|
||||
&& rm.PrevServerPosTime > 0.0
|
||||
&& rm.LastServerPosTime > rm.PrevServerPosTime)
|
||||
// Track the maximum sequencer velocity magnitude seen since
|
||||
// the last UpdatePosition arrival (carried on the
|
||||
// RemoteMotion struct), then on each UP arrival the
|
||||
// OnLivePositionUpdated path prints the comparison against
|
||||
// the server's actual broadcast pace
|
||||
// ((LastServerPos - PrevServerPos) / Δt). This guarantees
|
||||
// both sides are sampled during the same window and the
|
||||
// ratio reflects the real overshoot.
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||
{
|
||||
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
if (nowSec - rm.LastVelDiagLogTime > 2.0)
|
||||
{
|
||||
double dtServer = rm.LastServerPosTime - rm.PrevServerPosTime;
|
||||
var serverDelta = rm.LastServerPos - rm.PrevServerPos;
|
||||
float serverSpeed = (float)(serverDelta.Length() / dtServer);
|
||||
float seqSpeed = seqVel.Length();
|
||||
// Only log when the entity is actually moving — skip
|
||||
// idle remotes where both speeds are ~0.
|
||||
if (serverSpeed > 0.1f || seqSpeed > 0.1f)
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[VEL_DIAG] guid={serverGuid:X8} seqSpeed={seqSpeed:F3} m/s "
|
||||
+ $"serverSpeed={serverSpeed:F3} m/s "
|
||||
+ $"ratio={(serverSpeed > 1e-3f ? seqSpeed / serverSpeed : 0f):F3}");
|
||||
rm.LastVelDiagLogTime = nowSec;
|
||||
}
|
||||
}
|
||||
// body.Velocity is now the source of bulk translation
|
||||
// (set above by apply_current_movement). Track its
|
||||
// magnitude so VEL_DIAG can compare against the actual
|
||||
// server broadcast pace.
|
||||
float seqSpeedNow = rm.Body.Velocity.Length();
|
||||
if (seqSpeedNow > rm.MaxSeqSpeedSinceLastUP)
|
||||
rm.MaxSeqSpeedSinceLastUP = seqSpeedNow;
|
||||
}
|
||||
|
||||
ae.Entity.Position = rm.Body.Position;
|
||||
|
|
|
|||
|
|
@ -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