fix(motion): SetCycle forces _currNode onto first newly-enqueued node;
skip SubState commands in UM Commands list iteration
Two related fixes for the "remote-driven character animation cycle
does not visibly switch" bug:
1. AnimationSequencer.SetCycle now snapshots _queue.Last BEFORE
appending the new link/cycle nodes, then forces _currNode onto
preEnqueueTail.Next (= first newly-added node). Without this,
_currNode could stay pointing into stale non-cyclic head frames
left over from the previous cycle (typically a Walk_link or
Ready_link's tail), and the visible animation continues playing
those stale frames before the queue advances naturally to the
new cycle. Local player avoided the bug because
PlayerMovementController fires SetCycle in a tight per-input loop
that keeps the queue clean; remote player accumulates stale
link drains across many bundled UMs.
2. OnLiveMotionUpdated's UM Commands list iteration now skips
SubState class commands (high byte 0x40-0x4F like Ready
0x41000003). The router's SetCycle call for those would silently
override the animCycle picker's own SetCycle a few lines above
in the same UM packet — verified via SETCYCLE diag captures
showing run/walk being immediately re-cycled to Ready. Only
Action / Modifier / ChatEmote class commands (overlays that
interleave with the cycle) belong in this list iteration.
This fixed the landing-from-jump animation issue (user-confirmed:
"landing now works"). Walk↔run direct transitions still don't
visibly switch the leg cycle for observed retail-driven characters
even though ae.Sequencer.CurrentMotion correctly transitions
(per-tick SEQSTATE diag added — proves the sequencer's logical
state holds the right motion). Bug is somewhere downstream of
SetCycle's queue/state setup, possibly in Advance/BuildBlendedFrame
or in how seqFrames are applied to MeshRefs for remote entities.
Filed for next investigation.
Adds env-var-gated diagnostics (ACDREAM_REMOTE_VEL_DIAG=1):
CMD_LIST — what's in the UM's Commands list at receive time
HASCYCLE — whether the requested cycle exists in the dat
SEQSTATE — per-tick sequencer.CurrentMotion + CurrentSpeedMod
for the observed retail char (1Hz throttled)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a2ae2aefcc
commit
357dcc0547
2 changed files with 84 additions and 2 deletions
|
|
@ -3113,10 +3113,46 @@ public sealed class GameWindow : IDisposable
|
|||
// InterpretedMotionState.Commands[]; the router reconstructs the
|
||||
// class byte and chooses PlayAction for actions/modifiers/emotes
|
||||
// or SetCycle for persistent substates.
|
||||
//
|
||||
// 2026-05-03: SKIP SubState class commands (high-byte 0x40-0x4F).
|
||||
// The animCycle picker above already chose the correct SubState
|
||||
// cycle based on Forward/Sidestep/Turn command priority and just
|
||||
// called SetCycle for it. Letting the Commands list also call
|
||||
// SetCycle(SubState) would OVERRIDE our chosen cycle — e.g. ACE
|
||||
// bundles Ready (0x41000003) into the Commands list of a
|
||||
// RunForward UpdateMotion (cdb trace 2026-05-03 confirmed retail
|
||||
// does the same), and our router would silently re-cycle the
|
||||
// sequencer back to Ready right after we set RunForward. That's
|
||||
// why observed retail-driven characters never visibly switched
|
||||
// their leg cycle even though SETCYCLE diags fired correctly:
|
||||
// a second SetCycle call wiped the first within the same UM
|
||||
// packet processing. Only Actions/Modifiers/ChatEmotes (overlays
|
||||
// that interleave with the cycle) belong in the list iteration.
|
||||
if (update.MotionState.Commands is { Count: > 0 } cmds)
|
||||
{
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1")
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.Append($"[CMD_LIST] guid={update.Guid:X8} fwd=0x{fullMotion:X8} cmds=[");
|
||||
for (int i = 0; i < cmds.Count; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(", ");
|
||||
uint fc = AcDream.Core.Physics.MotionCommandResolver
|
||||
.ReconstructFullCommand(cmds[i].Command);
|
||||
var rt = AcDream.Core.Physics.AnimationCommandRouter.Classify(fc);
|
||||
sb.Append($"0x{fc:X8}({rt})");
|
||||
}
|
||||
sb.Append("]");
|
||||
System.Console.WriteLine(sb.ToString());
|
||||
}
|
||||
foreach (var item in cmds)
|
||||
{
|
||||
uint fullItemCommand = AcDream.Core.Physics.MotionCommandResolver
|
||||
.ReconstructFullCommand(item.Command);
|
||||
var itemRoute = AcDream.Core.Physics.AnimationCommandRouter
|
||||
.Classify(fullItemCommand);
|
||||
if (itemRoute == AcDream.Core.Physics.AnimationCommandRouteKind.SubState)
|
||||
continue;
|
||||
AcDream.Core.Physics.AnimationCommandRouter.RouteWireCommand(
|
||||
ae.Sequencer,
|
||||
fullStyle,
|
||||
|
|
@ -6475,6 +6511,25 @@ public sealed class GameWindow : IDisposable
|
|||
IReadOnlyList<AcDream.Core.Physics.PartTransform>? seqFrames = null;
|
||||
if (ae.Sequencer is not null)
|
||||
{
|
||||
// Per-tick sequencer-state diag: prove whether the sequencer
|
||||
// for the observed retail char actually holds the latest
|
||||
// motion (= SetCycle landed) OR is stuck on an old motion
|
||||
// (= something elsewhere is reverting). Throttled to once
|
||||
// per second per remote.
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_REMOTE_VEL_DIAG") == "1"
|
||||
&& serverGuid != 0
|
||||
&& serverGuid != _playerServerGuid)
|
||||
{
|
||||
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
if (_remoteDeadReckon.TryGetValue(serverGuid, out var rmDiag)
|
||||
&& nowSec - rmDiag.LastOmegaDiagLogTime > 1.0)
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[SEQSTATE] guid={serverGuid:X8} CurrentMotion=0x{ae.Sequencer.CurrentMotion:X8} "
|
||||
+ $"CurrentSpeedMod={ae.Sequencer.CurrentSpeedMod:F3}");
|
||||
rmDiag.LastOmegaDiagLogTime = nowSec;
|
||||
}
|
||||
}
|
||||
seqFrames = ae.Sequencer.Advance(dt);
|
||||
|
||||
// Phase E.1: drain animation hooks (footstep sounds, attack
|
||||
|
|
|
|||
|
|
@ -447,6 +447,21 @@ public sealed class AnimationSequencer
|
|||
// add_motion chain (MotionTable.cs L100-L101, L152-L153).
|
||||
ClearPhysics();
|
||||
|
||||
// Snapshot the queue tail BEFORE appending new motion data so we
|
||||
// can locate the first newly-added node afterward and force
|
||||
// _currNode onto it. Without this, _currNode can stay pointing
|
||||
// into stale non-cyclic head frames left over from the previous
|
||||
// cycle (typically a Walk_link or Ready_link's tail), and the
|
||||
// visible animation continues playing those stale frames before
|
||||
// the queue advances naturally to the new cycle. For remote
|
||||
// entities receiving many bundled UMs over time, this stale-head
|
||||
// build-up was the root cause of "transitions between cycles
|
||||
// don't visibly switch the leg pose" even though SetCycle's
|
||||
// CurrentMotion/CurrentSpeedMod were updated correctly. Local
|
||||
// player avoided the bug because PlayerMovementController fires
|
||||
// SetCycle in a tight per-input loop that keeps the queue clean.
|
||||
var preEnqueueTail = _queue.Last;
|
||||
|
||||
// Enqueue link frames (with adjusted speed for left→right remapping).
|
||||
if (linkData is { Anims.Count: > 0 })
|
||||
EnqueueMotionData(linkData, adjustedSpeed, isLooping: false);
|
||||
|
|
@ -478,9 +493,21 @@ public sealed class AnimationSequencer
|
|||
}
|
||||
}
|
||||
|
||||
// If we have no current anim, start at the beginning of the queue.
|
||||
if (_currNode == null)
|
||||
// Force _currNode onto the FIRST NEWLY-ENQUEUED node so the
|
||||
// visible animation switches to the new cycle/link immediately
|
||||
// instead of finishing whatever stale head frames were sitting
|
||||
// at the front of the queue. preEnqueueTail.Next is the first
|
||||
// newly-added node; if preEnqueueTail was null (queue was empty
|
||||
// before enqueue), the first new node is _queue.First.
|
||||
var firstNew = preEnqueueTail is null ? _queue.First : preEnqueueTail.Next;
|
||||
if (firstNew is not null)
|
||||
{
|
||||
_currNode = firstNew;
|
||||
_framePosition = _currNode.Value.GetStartFramePosition();
|
||||
}
|
||||
else if (_currNode == null)
|
||||
{
|
||||
// Defensive fallback: nothing newly added AND no current node.
|
||||
_currNode = _queue.First;
|
||||
_framePosition = _currNode?.Value.GetStartFramePosition() ?? 0.0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue