feat(L.2g-S5): delete player pace-inference layer; close #39 (DEV-2)

S0 wire probe (live capture, retail actor via ACE) refuted the premise
the #39 machinery was built on: walk<->run Shift toggles arrive as
EXPLICIT UMs (0x0005 <-> 0x0007@runRate) because retail's default-
difference packing baselines forward_command against Ready — W-held is
always packed, and ACE re-emits it unconditionally with the holdKey
upgrade (MovementData.cs:104-119). The frequent flags=0 autonomous UMs
are genuine keys-released / heading-only states; retail applies them as
full stops (InterpretedMotionState ctor 0x0051e8d0 defaults Ready +
move_to_interpreted_state 0x005289c0 flat copy).

Retail has NO pace->animation adaptation anywhere in its inbound
pipeline (two decomp dives + ACE cross-check, deviation map DEV-2), and
the refinement layer's 0.2s-grace re-promotion after legitimate Ready
UMs was itself the observed Ready<->Run thrash / rubber-band component.

Deleted: ApplyPlayerLocomotionRefinement, UmGraceSeconds,
PlayerRunPromoteSpeed/PlayerRunDemoteSpeed, RemoteMotion.LastUMTime,
the synth-player refinement call in OnLivePositionUpdated. Player-remote
cycles are now UM-driven only, exactly like retail. NPC PlanFromVelocity
path untouched (S6 unifies).

S0 findings + S1 live validation (0 false UM_STALE drops across 280 UMs)
recorded in docs/research/2026-07-02-inbound-motion-deviation-map.md.

fix #39: closed — root-cause narrative corrected, machinery removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-02 16:10:28 +02:00
parent cb74e64343
commit a2f8104cbf
3 changed files with 87 additions and 274 deletions

View file

@ -3108,7 +3108,20 @@ regression on the species that already render correctly.
## #39 — Run↔Walk cycle transition not visible on observed player remotes (acdream-as-observer) ## #39 — Run↔Walk cycle transition not visible on observed player remotes (acdream-as-observer)
**Status:** OPEN — VERIFY-PENDING (cases #1/#2/#4/#5 user-verified working 2026-05-06; cases #3/#6/#7 unverified in live test) **Status:** CLOSED 2026-07-02 (superseded by L.2g; refinement machinery DELETED in the S5 commit)
**Closure note:** the root-cause narrative below is WRONG — the 2026-05-06
"wire goes silent on Shift toggle" finding was refuted at three oracles + a
fresh live capture (`docs/research/2026-07-02-inbound-motion-deviation-map.md`,
S0 section): retail sends a fresh MoveToState on HoldRun toggle while moving
(`CommandInterpreter` 0x006b37a8 → `SendMovementEvent`), ACE rebroadcasts every
MoveToState (`GameActionMoveToState.cs:36`), and the S0 capture shows explicit
`0x0005↔0x0007` UMs on each toggle. Retail has NO pace→cycle adaptation
anywhere (DEV-2). The `ApplyPlayerLocomotionRefinement` layer this issue added
was itself causing Ready↔Run thrash against legitimate flags=0 stop UMs and
was deleted; player-remote cycles are UM-driven only. Remaining transition
polish (funnel, stop path, link pose) is tracked as roadmap L.2g S2S4.
**Original status:** OPEN — VERIFY-PENDING (cases #1/#2/#4/#5 user-verified working 2026-05-06; cases #3/#6/#7 unverified in live test)
**Severity:** LOW (most cases now visibly correct after the 2026-05-06 fix sequence; remaining unverified cases are direction-flip — believed to work via direct UM but not explicitly exercised) **Severity:** LOW (most cases now visibly correct after the 2026-05-06 fix sequence; remaining unverified cases are direction-flip — believed to work via direct UM but not explicitly exercised)
**Filed:** 2026-05-03 **Filed:** 2026-05-03
**Component:** physics / motion / animation **Component:** physics / motion / animation

View file

@ -258,6 +258,46 @@ bookkeeping (DEV-7), and stop path (DEV-3) can be ported around it surgically;
and it is NOT a dead-reckoning-tuning problem — the chase is already retail, and it is NOT a dead-reckoning-tuning problem — the chase is already retail,
it's the inputs (cycle + velocity source + stop signal) that deviate. it's the inputs (cycle + velocity source + stop signal) that deviate.
## S0 wire-probe RESULTS (2026-07-02, post-S1 live capture)
Observer acdream (+Acdream), actor retail-driven (guid 0x5000000B), structured
protocol incl. Shift toggles. 280 UMs captured (`launch-s0-wireprobe.log`).
1. **The wire is NOT silent — DEV-2's premise is dead.** Walk↔run toggles
arrive explicitly: `fwd=0x0005 fwdSpd=null(=1.0)` ↔ `fwd=0x0007
fwdSpd=2.85`. Root cause of the 2026-05-06 misreading: retail's
default-difference packing baselines `forward_command` against **Ready**
(`RawMotionState.Default`, our own D6.2b verbatim port) — W-held is
non-default and always packed, so ACE (`MovementData.cs:104-119`) always
re-emits it with the holdKey upgrade + run-skill speed.
2. **flags=0 "empty" autonomous UMs are frequent (127/209) and LEGITIMATE**
— they are genuine keys-released / heading-only MoveToState relays
(all-default raw state packs nothing; ACE `RawMotionState.Read` leaves
absent = Invalid; `MovementData` emits nothing). Retail semantics
(verified: `InterpretedMotionState` ctor 0x0051e8d0 defaults
`forward_command=Ready`; `MovementManager::unpack_movement` case 0
UnPacks into a default-constructed state;
`CMotionInterp::move_to_interpreted_state` 0x005289c0 does a FLAT
`copy_movement_from` + `apply_current_movement`): an empty UM = a real
full stop. acdream already maps it to Ready — but the DEV-2 refinement
then re-promotes the cycle from UP pace after the 0.2 s grace, producing
a Ready↔Run thrash against legitimate stop signals. That thrash is the
observed flicker/rubber-band component.
3. **S1 gate validated live**: 0 `[UM_STALE]` drops across 280 UMs;
movementSeq advances +1 per UM; byte-level layout confirmed
(`instanceSeq|movementSeq|serverControlSeq|isAutonomous` exactly where
the parser reads them).
4. Also confirmed in `unpack_movement` (0x00524440) for the S2 port: the
outer style u16 routes through `command_ids[]` and applies via
`DoMotion` ONLY when it differs from the current style; motionFlags
carries sticky-object (0x100 → `stick_to_object`) and standing_longjump
(0x200); the actions list is consumed under a 15-bit
`server_action_stamp` wraparound compare
(`move_to_interpreted_state` 0x005289c0, lines 305953-305989).
**Disposition: S5 = delete the player pace-inference layer** (executed in the
same session; NPC `PlanFromVelocity` path untouched — its unification is S6).
## Provenance ## Provenance
- Workflow `wf_b92a5670-283`: 4 read-only mappers (retail decomp / ACE port / - Workflow `wf_b92a5670-283`: 4 read-only mappers (retail decomp / ACE port /

View file

@ -570,19 +570,6 @@ public sealed class GameWindow : IDisposable
/// </summary> /// </summary>
public float MaxSeqSpeedSinceLastUP; public float MaxSeqSpeedSinceLastUP;
/// <summary>
/// Seconds-since-epoch timestamp of the most recent UpdateMotion (UM)
/// for this remote. Used by the player-remote velocity-fallback cycle
/// refinement to skip refinement while a fresh UM is authoritative —
/// retail's outbound MoveToState gives us direction-explicit cycles
/// on direction-key changes (W press, W release, W↔S flip), and we
/// only want UP-derived velocity to refine the speed bucket within
/// a direction when no UM has arrived recently. Defaults to 0
/// (epoch) so the first UP after spawn is allowed to refine
/// immediately if velocity already differs from the spawn cycle.
/// </summary>
public double LastUMTime;
public RemoteMotion() public RemoteMotion()
{ {
Body = new AcDream.Core.Physics.PhysicsBody Body = new AcDream.Core.Physics.PhysicsBody
@ -4288,19 +4275,6 @@ public sealed class GameWindow : IDisposable
return; return;
} }
// #39 (2026-05-06): stamp the per-remote LastUMTime so the
// UP-velocity fallback path in ApplyServerControlledVelocityCycle
// can skip refinement while a UM is fresh. UMs are authoritative
// for direction-key changes (W press / release / W↔S flip);
// velocity refinement only helps for HoldKey-only changes (Shift
// toggle while a direction key is held — retail does NOT broadcast
// a fresh MoveToState in that case).
if (_remoteDeadReckon.TryGetValue(update.Guid, out var rmStateForUm))
{
rmStateForUm.LastUMTime =
(System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
}
// Re-resolve using the new stance/command. Keep the setup and // Re-resolve using the new stance/command. Keep the setup and
// motion-table we already know about — the server's motion // motion-table we already know about — the server's motion
// updates override state within the same table, not swap tables. // updates override state within the same table, not swap tables.
@ -5135,35 +5109,6 @@ public sealed class GameWindow : IDisposable
return low is 0x05 or 0x06 or 0x07 or 0x0F or 0x10; return low is 0x05 or 0x06 or 0x07 or 0x0F or 0x10;
} }
/// <summary>
/// Grace window in seconds after a UM arrives during which UP-derived
/// velocity refinement is suppressed for a player remote. UMs are
/// authoritative; the velocity fallback only fills the gap when retail
/// does not send a fresh MoveToState (Shift toggle while direction key
/// held). 200 ms covers the worst-case UM/UP race — UMs arrive on
/// direction-key events and UPs at 510 Hz, so the first UP after a
/// fresh UM lands ~100200 ms behind. Tightened from 500 ms (commit
/// 8fa04af original) per user observation that the Shift-toggle
/// transition was visibly slower than retail; with 0.2 s the worst-case
/// added latency is just the UP cadence below it.
/// </summary>
private const double UmGraceSeconds = 0.2;
/// <summary>
/// Speed (m/s) above which a player-remote currently in WalkForward
/// is promoted to RunForward by velocity refinement. Tuned to player
/// speeds: walk ≈ 3.12 m/s (WalkAnimSpeed × 1.0), run ≈ 812 m/s
/// (RunAnimSpeed × runRate ≈ 4.0 × 2.03.0). Hysteresis with
/// <see cref="PlayerRunDemoteSpeed"/> avoids thrashing at the boundary.
/// </summary>
private const float PlayerRunPromoteSpeed = 5.5f;
/// <summary>
/// Speed (m/s) below which a player-remote currently in RunForward
/// is demoted to WalkForward by velocity refinement.
/// </summary>
private const float PlayerRunDemoteSpeed = 4.5f;
private void ApplyServerControlledVelocityCycle( private void ApplyServerControlledVelocityCycle(
uint serverGuid, uint serverGuid,
AnimatedEntity ae, AnimatedEntity ae,
@ -5180,27 +5125,24 @@ public sealed class GameWindow : IDisposable
if (IsPlayerGuid(serverGuid)) if (IsPlayerGuid(serverGuid))
{ {
// #39 (2026-05-06): player-remote forward-direction speed-bucket // L.2g S5 (2026-07-02, DEV-2 DELETED): player remotes get NO
// refinement. The bug case: actor toggles Shift while holding W // pace-derived cycle refinement — retail has no such mechanism
// (or releases Shift). Retail's outbound apparently does NOT // anywhere in its inbound pipeline (deviation map DEV-2, two
// broadcast a fresh MoveToState for HoldKey-only changes // independent decomp dives + ACE cross-check). The #39-era
// (verified via static analysis of CommandInterpreter::SendMovementEvent // premise ("retail's outbound goes silent on Shift toggle") was
// call sites; needs cdb confirmation). ACE has nothing to // refuted at all three oracles + the S0 live capture: retail
// broadcast → no UM arrives at the observer → cycle stays at // sends a fresh MoveToState on HoldRun toggle while moving
// whichever direction-bucket was last set. Velocity DOES change // (CommandInterpreter 0x006b37a8 → SendMovementEvent), ACE
// (UP carries new pace), so this code path uses UP-derived // rebroadcasts every MoveToState (GameActionMoveToState.cs:36),
// velocity to refine the speed bucket within the same direction. // and the wire shows explicit 0x0005↔0x0007 UMs on each toggle
// (launch-s0-wireprobe.log). The refinement layer's re-promote
// after legitimate flags=0 Ready UMs was itself the observed
// Ready↔Run thrash. Cycle changes for player remotes come from
// UpdateMotion ONLY, exactly like retail; position error is the
// InterpolationManager chase's job.
// //
// Conservative scope: // NPC/monster remotes below keep PlanFromVelocity until S6
// - Forward direction only (low byte 0x05 or 0x07). Sidestep // unifies all entity classes onto the CMotionInterp funnel.
// and backward HoldKey toggles are deferred until the TTD
// trace described in
// docs/research/2026-05-06-locomotion-cycle-transitions/
// confirms retail's exact algorithm.
// - Hysteresis (4.5 m/s demote / 5.5 m/s promote) prevents
// thrashing at the boundary.
// - 500 ms UM grace window — a fresh UM is always authoritative.
ApplyPlayerLocomotionRefinement(serverGuid, ae, rm, velocity);
return; return;
} }
@ -5230,162 +5172,6 @@ public sealed class GameWindow : IDisposable
ae.Sequencer.SetCycle(style, plan.Motion, plan.SpeedMod); ae.Sequencer.SetCycle(style, plan.Motion, plan.SpeedMod);
} }
private void ApplyPlayerLocomotionRefinement(
uint serverGuid,
AnimatedEntity ae,
RemoteMotion rm,
System.Numerics.Vector3 velocity)
{
// UM grace: a fresh UM is authoritative.
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
double sinceUm = nowSec - rm.LastUMTime;
if (sinceUm < UmGraceSeconds) return;
uint currentMotion = ae.Sequencer!.CurrentMotion;
uint lowByte = currentMotion & 0xFFu;
float currentSign = MathF.Sign(ae.Sequencer.CurrentSpeedMod);
if (currentSign == 0f) currentSign = 1f;
// Recognised locomotion directions:
// 0x05 (WalkForward) — also encodes WalkBackward via negative speed
// (ACE convention: SidestepCommand= cancel, ForwardCommand=
// WalkForward, ForwardSpeed *= -0.65)
// 0x07 (RunForward)
// 0x0F (SideStepRight)
// 0x10 (SideStepLeft)
// Other motions (Ready, Turn, emotes, attacks) are left to UM-driven SetCycle.
const uint LowWalkForward = 0x05u;
const uint LowRunForward = 0x07u;
const uint LowSideStepRight = 0x0Fu;
const uint LowSideStepLeft = 0x10u;
bool isForwardClass = lowByte == LowWalkForward || lowByte == LowRunForward;
bool isSidestep = lowByte == LowSideStepRight || lowByte == LowSideStepLeft;
if (!isForwardClass && !isSidestep) return;
float horizSpeed = MathF.Sqrt(velocity.X * velocity.X + velocity.Y * velocity.Y);
// Hysteresis: stay in current bucket unless we cross the appropriate
// threshold. Below StopSpeed → don't refine (let UM Ready stop signal
// handle the stop transition; we don't want UP momentary 0-velocity
// to drop the cycle to Ready while the actor is mid-stride).
if (horizSpeed < AcDream.Core.Physics.ServerControlledLocomotion.StopSpeed)
return;
uint targetMotion;
float speedMod;
if (isSidestep)
{
// Sidestep: motion ID stays the same (SideStepLeft / SideStepRight).
// Retail's wire encoding for sidestep speed buckets uses the same
// motion ID with different SidestepSpeed (slow ≈ 1.25 multiplier,
// fast ≈ 3.0 clamp per ACE MovementData.cs:124-131). On Shift
// toggle while a strafe key is held, retail does NOT broadcast a
// fresh MoveToState (same wire-silence rule as the forward case),
// so observer-side cycle refinement must come from UP-derived
// velocity here. Preserve the sign — SideStepLeft is sometimes
// emitted with negative speedMod by the adjust_motion path.
//
// Magnitude: horizSpeed / SidestepAnimSpeed maps the observed
// speed back to a SideStepSpeed the sequencer can apply as a
// framerate multiplier. Retail's get_state_velocity for
// sidestep cycles is `velocity.X = SidestepAnimSpeed *
// SideStepSpeed` (MotionInterpreter.cs:592 — constant 1.25
// m/s). Dividing by WalkAnimSpeed (3.12) here was wrong by
// 2.5× and made slow strafe play visibly slower than retail.
float sideMag = horizSpeed / AcDream.Core.Physics.MotionInterpreter.SidestepAnimSpeed;
sideMag = MathF.Min(MathF.Max(
sideMag,
AcDream.Core.Physics.ServerControlledLocomotion.MinSpeedMod),
AcDream.Core.Physics.ServerControlledLocomotion.MaxSpeedMod);
targetMotion = currentMotion;
speedMod = sideMag * currentSign;
}
else if (currentSign < 0f)
{
// BACKWARD walk: ACE encodes WalkBackward as `WalkForward` motion
// with NEGATIVE speedMod (MovementData.cs:115 `interpState.ForwardSpeed *= -0.65f`).
// No "RunBackward" motion exists — Shift toggle on backward
// changes only the magnitude of speedMod (slow back ≈ -0.65,
// fast back ≈ -1.91 = -runRate × 0.65). Keep WalkForward motion,
// refine magnitude, preserve negative sign.
//
// Without this branch (the original fix #1), backward refinement
// computed a positive speedMod from horizSpeed and overwrote the
// negative sign, making the legs play forward-walk while the body
// continued moving backward (the rubber-banding the user reported).
float backMag = horizSpeed / AcDream.Core.Physics.MotionInterpreter.WalkAnimSpeed;
backMag = MathF.Min(MathF.Max(
backMag,
AcDream.Core.Physics.ServerControlledLocomotion.MinSpeedMod),
AcDream.Core.Physics.ServerControlledLocomotion.MaxSpeedMod);
targetMotion = AcDream.Core.Physics.MotionCommand.WalkForward;
speedMod = -backMag;
}
else if (lowByte == LowRunForward)
{
if (horizSpeed < PlayerRunDemoteSpeed)
{
targetMotion = AcDream.Core.Physics.MotionCommand.WalkForward;
speedMod = MathF.Min(MathF.Max(
horizSpeed / AcDream.Core.Physics.MotionInterpreter.WalkAnimSpeed,
AcDream.Core.Physics.ServerControlledLocomotion.MinSpeedMod),
AcDream.Core.Physics.ServerControlledLocomotion.MaxSpeedMod);
}
else
{
targetMotion = AcDream.Core.Physics.MotionCommand.RunForward;
speedMod = MathF.Min(MathF.Max(
horizSpeed / AcDream.Core.Physics.MotionInterpreter.RunAnimSpeed,
AcDream.Core.Physics.ServerControlledLocomotion.MinSpeedMod),
AcDream.Core.Physics.ServerControlledLocomotion.MaxSpeedMod);
}
}
else
{
// currently WalkForward (0x05) with positive speedMod = walking forward.
if (horizSpeed > PlayerRunPromoteSpeed)
{
targetMotion = AcDream.Core.Physics.MotionCommand.RunForward;
speedMod = MathF.Min(MathF.Max(
horizSpeed / AcDream.Core.Physics.MotionInterpreter.RunAnimSpeed,
AcDream.Core.Physics.ServerControlledLocomotion.MinSpeedMod),
AcDream.Core.Physics.ServerControlledLocomotion.MaxSpeedMod);
}
else
{
targetMotion = AcDream.Core.Physics.MotionCommand.WalkForward;
speedMod = MathF.Min(MathF.Max(
horizSpeed / AcDream.Core.Physics.MotionInterpreter.WalkAnimSpeed,
AcDream.Core.Physics.ServerControlledLocomotion.MinSpeedMod),
AcDream.Core.Physics.ServerControlledLocomotion.MaxSpeedMod);
}
}
// Skip the SetCycle if neither motion nor speedMod changed
// meaningfully — avoids replaying transition links every UP.
bool motionChanged = currentMotion != targetMotion
&& (currentMotion & 0xFFu) != (targetMotion & 0xFFu);
bool speedChanged = MathF.Abs(ae.Sequencer.CurrentSpeedMod - speedMod) > 0.05f;
if (!motionChanged && !speedChanged)
return;
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
{
System.Console.WriteLine(
$"[UPCYCLE_PLAYER] guid={serverGuid:X8} "
+ $"|v|={horizSpeed:F2} cur=0x{currentMotion:X8} "
+ $"-> motion=0x{targetMotion:X8} speedMod={speedMod:F2} "
+ $"sinceUM={sinceUm:F2}s "
+ $"motionChg={motionChanged} speedChg={speedChanged}");
}
uint style = ae.Sequencer.CurrentStyle != 0
? ae.Sequencer.CurrentStyle
: 0x8000003Du;
ae.Sequencer.SetCycle(style, targetMotion, speedMod);
}
private void OnLivePositionUpdated(AcDream.Core.Net.WorldSession.EntityPositionUpdate update) private void OnLivePositionUpdated(AcDream.Core.Net.WorldSession.EntityPositionUpdate update)
{ {
// Phase A.1 / #135: track the PLAYER's last server-known landblock so the // Phase A.1 / #135: track the PLAYER's last server-known landblock so the
@ -5634,24 +5420,14 @@ public sealed class GameWindow : IDisposable
isMovingTo: false, isMovingTo: false,
currentBodyPosition: rmState.Body.Position); currentBodyPosition: rmState.Body.Position);
} }
// #39 fix-3 (2026-05-06): velocity-fallback cycle refinement // Track the UP-derived synth velocity for diagnostics
// for player remotes. Wire-level evidence (`launch-39-diag2.log`): // ([VEL_DIAG] pace comparison). L.2g S5 (2026-07-02): the
// when retail's actor toggles Shift while a direction key // #39-era cycle-refinement call that used to live here is
// is held, retail's outbound MoveToState logic does NOT // DELETED — retail never adapts a remote's animation from
// emit a fresh packet (only Ready ↔ Run UMs visible in // observed pace (deviation map DEV-2; premise refuted at
// `[FWD_WIRE]`, despite a clear walk-pace ↔ run-pace // decomp + ACE source + the S0 live capture, which shows
// velocity transition in `[VEL_DIAG]`). ACE has nothing // explicit 0x0005↔0x0007 UMs on every Shift toggle).
// to broadcast → no UM arrives at the observer → cycle // Player-remote cycles are UM-driven only.
// sticks at whatever the last UM set. Compute the
// synth-velocity here in the player-remote path AND
// call into ApplyServerControlledVelocityCycle, which
// routes through the direction-preserving + UM-grace
// ApplyPlayerLocomotionRefinement helper (added in
// commit 8fa04af).
//
// The legacy non-player block below (3759+) covers NPCs
// and is gated `!IsPlayerGuid`; this block fills the
// matching gap for players.
if (rmState.PrevServerPosTime > 0.0) if (rmState.PrevServerPosTime > 0.0)
{ {
double nowSecVel = rmState.LastServerPosTime; double nowSecVel = rmState.LastServerPosTime;
@ -5661,21 +5437,6 @@ public sealed class GameWindow : IDisposable
var synthVel = (worldPos - rmState.PrevServerPos) / (float)dtPos; var synthVel = (worldPos - rmState.PrevServerPos) / (float)dtPos;
rmState.ServerVelocity = synthVel; rmState.ServerVelocity = synthVel;
rmState.HasServerVelocity = true; rmState.HasServerVelocity = true;
if (_animatedEntities.TryGetValue(entity.Id, out var aeForVel)
&& aeForVel.Sequencer is not null)
{
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
{
System.Console.WriteLine(
$"[UPCYCLE_SRC] guid={update.Guid:X8} src=synth-player");
}
ApplyServerControlledVelocityCycle(
update.Guid,
aeForVel,
rmState,
synthVel);
}
} }
} }
@ -5793,17 +5554,16 @@ public sealed class GameWindow : IDisposable
if (rmState.HasServerVelocity if (rmState.HasServerVelocity
&& _animatedEntities.TryGetValue(entity.Id, out var aeForVelocity)) && _animatedEntities.TryGetValue(entity.Id, out var aeForVelocity))
{ {
// #39 (2026-05-06): un-gated for player remotes — the // NPC/monster remotes: PlanFromVelocity cycle selection from
// function itself routes player remotes into the dedicated // UP-derived velocity (ACE broadcasts NPC motion patterns the
// ApplyPlayerLocomotionRefinement path (forward-direction // UM stream alone doesn't cover). Player remotes return early
// speed bucket only, with UM grace + hysteresis). Non-player // inside — their cycles are UM-driven only per retail (L.2g
// remotes use the existing PlanFromVelocity path. // S5; DEV-2 deleted). Unification of NPCs onto the
// CMotionInterp funnel is S6.
// //
// D2 (Commit A 2026-05-03): tag whether the velocity feeding // D2 (Commit A 2026-05-03): tag whether the velocity feeding
// ApplyServerControlledVelocityCycle is wire-explicit (rare for // ApplyServerControlledVelocityCycle is wire-explicit or
// player remotes — ACE almost never sets HasVelocity on player // synthesized from position deltas (the common case).
// UPs) or synthesized from position deltas (the common case).
// Pairs with the [UPCYCLE]/[UPCYCLE_PLAYER] line printed inside.
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1") if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
{ {
string velSrc = update.Velocity is null ? "synth" : "wire"; string velSrc = update.Velocity is null ? "synth" : "wire";