fix(#170): stop per-frame interpreted re-dispatch flooding pending_motions (partial — flood fixed, run install not yet sustained)

Live retail cdb tracing + acdream probes root-caused the creature chase "slide"
end-to-end, SUPERSEDING the earlier MovementManager-coexistence hypothesis
(eb423fb7):

  Chase run cycle is manufactured client-side from mt-6 MoveToObject:
    HandleUpdateTarget -> MoveToObject_Internal -> (TurnToHeading node completes
    via UseTime) -> BeginMoveForward -> _DoMotion(RunForward) sets substate.
  BeginTurnToHeading (MoveToManager 0x00529b90) bails while
  CMotionInterp.motions_pending is non-empty (retail-faithful).
  acdream's pending_motions EXPLODED to ~1.3M entries (live: add=1.37M vs
  done=5.7K) because the NPC per-tick called rm.Motion.apply_current_movement
  EVERY FRAME, which for a remote (has a DefaultSink) re-ran
  ApplyInterpretedMovement and re-dispatched the WHOLE interpreted state (stance
  0x8000003C + forward=attack + sidestep/turn stops), each appending a
  pending_motions node that barely drains. => MotionsPending() permanently true
  => the chase turn never started => BeginMoveForward/RunForward ~never fired =>
  the creature slid in an idle+attack pose. Retail dispatches per MOTION EVENT
  (per UM), never per frame — cdb drain trace: retail add_to_queue == MotionDone.

FIX: delete the per-frame apply_current_movement call in the grounded remote-NPC
dead-reckon path (GameWindow ~9992). The motion is already dispatched per UM by
the funnel (MoveToInterpretedState) + by the MoveToManager per node; body
velocity is refreshed directly by the existing get_state_velocity line.

RESULT (verified live): pending_motions depth 1.3M -> ~1 (add~=done); "stuck in
attack animation" GONE (user-confirmed); run cycle now installs (BeginMoveForward
1->10, RunForward held 0->7). PARTIAL: still not fully sustained — BeginTurnToHeading
still blocked motionsPending=True 256/272 (94%) because a residual Ready
(0x41000003) stop-node backlog keeps the queue from fully emptying between swings
(retail hits add==done exactly). acdream gets ~10 run-starts vs retail's ~21, so
it now twitches+glides instead of a clean run-then-stop. Residual = the R2/R3
Ready-stop drain (next session; see docs/research/2026-07-04-*-handoff.md).

Also includes the env-gated #170 diagnostic probes used to find this
(ACDREAM_MVTO_DIAG=1: [mvto]/[npc-tick]/[drain]/[drainq]) — TEMPORARY, strip when
#170 closes. No effect on normal runs. Core suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-04 20:29:10 +02:00
parent d2ccc80e59
commit 427332acaa
3 changed files with 96 additions and 34 deletions

View file

@ -9959,6 +9959,9 @@ public sealed class GameWindow : IDisposable
| AcDream.Core.Physics.TransientStateFlags.Active;
if (!IsPlayerGuid(serverGuid) && rm.HasServerVelocity)
{
if (Environment.GetEnvironmentVariable("ACDREAM_MVTO_DIAG") == "1")
Console.WriteLine(System.FormattableString.Invariant(
$"[npc-tick] guid={serverGuid:X8} branch=SERVERVEL (skips UseTime) mtState={rm.MoveTo?.MovementTypeState}"));
double velocityAge = nowSec - rm.LastServerPosTime;
if (velocityAge > ServerControlledVelocityStaleSeconds)
{
@ -9979,42 +9982,45 @@ public sealed class GameWindow : IDisposable
else
{
// R4-V4: the retail MoveToManager drives
// server-directed movement per tick — the P4
// TargetTracker adapter feeds HandleUpdateTarget
// from the live entity table (full TargetManager
// port is R5; register row), then UseTime runs
// HandleMoveToPosition/HandleTurnToHeading
// (steering + arrival + fail-distance), and
// apply_current_movement recomputes velocity from
// whatever the manager dispatched — the same
// per-tick shape ordinary locomotion uses.
// server-directed movement per tick — UseTime runs
// HandleMoveToPosition/HandleTurnToHeading (steering
// + arrival + fail-distance), dispatching its OWN
// per-node locomotion (turn / RunForward) through the
// sink. That per-node dispatch is the only motion the
// per-tick update should issue for a remote.
if (Environment.GetEnvironmentVariable("ACDREAM_MVTO_DIAG") == "1")
Console.WriteLine(System.FormattableString.Invariant(
$"[npc-tick] guid={serverGuid:X8} branch=MOVETO (UseTime runs) mtState={rm.MoveTo?.MovementTypeState}"));
TickRemoteMoveTo(rm);
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
// #170 glide fix (2026-07-04): refresh the body's
// translation velocity from the current interpreted
// state, matching retail. apply_current_movement's
// SINK path (all remotes have a DefaultSink) only
// dispatches the ANIMATION — the
// get_state_velocity → set_local_velocity write
// that retail's apply_current_movement performs
// lives ONLY in acdream's no-sink fallback
// (MotionInterpreter.ApplyCurrentMovementInterpreted,
// ~1702), so a remote's body.Velocity is never
// recomputed and goes STALE. get_state_velocity
// (0x00527d50, already a verbatim port) returns
// WalkForward→3.12×spd, RunForward→4.0×spd, and 0
// for every other forward command — so when a
// chasing creature's ForwardCommand switches to an
// attack (0x1000006x, action-class), the body
// stops instead of coasting at the last run
// velocity while playing an idle+attack pose (the
// user-reported "glides after me"). Grounded only
// (this branch already asserted Contact+OnWalkable);
// airborne velocity is owned by the jump arc.
// Confirmed vs a live ACDREAM_DUMP_MOTION capture of
// Mite Scamp 0x80000244 (66 mt-0 attack UMs carrying
// ForwardCommand=0x62/63/64 vs 2 mt-6 chase UMs).
// #170 ROOT FIX (2026-07-04): the per-frame
// rm.Motion.apply_current_movement(...) call that used
// to be here is DELETED. For a remote (which has a
// DefaultSink) it re-ran ApplyInterpretedMovement EVERY
// FRAME, re-dispatching the whole interpreted state —
// stance (0x8000003C), forward=attack, sidestep/turn
// stops — and each successful DoInterpretedMotion
// appends a CMotionInterp.pending_motions node. Those
// nodes barely drain for a remote, so pending_motions
// EXPLODED to ~1.3M entries (live capture: add=1.37M vs
// done=5.7K; ~671K of them the unchanged stance).
// MotionsPending() then stayed permanently true, so
// MoveToManager.BeginTurnToHeading (0x00529b90,
// `if (motions_pending) return`) could never start the
// chase turn → the chase never reached BeginMoveForward
// → RunForward, so the creature SLID in an idle+attack
// pose instead of running. Retail dispatches per MOTION
// EVENT (per UM), never per frame — a live cdb drain
// trace showed retail add_to_queue == MotionDone (queue
// stays shallow). The motion is already dispatched per
// UM by the funnel (MoveToInterpretedState) and by the
// MoveToManager per node above; the only thing the
// deleted call still provided was body velocity, which
// the get_state_velocity refresh below performs
// directly. get_state_velocity (0x00527d50, verbatim)
// returns WalkForward→3.12×spd, RunForward→4.0×spd, 0
// otherwise; grounded-only (this branch already asserted
// Contact+OnWalkable), airborne velocity owns the jump.
if (rm.Body.OnWalkable)
rm.Body.set_local_velocity(
rm.Motion.get_state_velocity(),