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:
parent
d2ccc80e59
commit
427332acaa
3 changed files with 96 additions and 34 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue