fix(anim): Phase L.1c sequencer cycle fallback for missing MoveTo motion
User-observed regression on commit37de771: monsters in combat with another client appear as "just a torso on the ground" until they move. User correctly identified this as a regression I introduced. Cause traced to the SEQUENCER side, not the InterpretedState side. AnimationSequencer.SetCycle (AnimationSequencer.cs:392-396) unconditionally calls ClearCyclicTail() BEFORE looking up the requested cycle in the MotionTable. If the cycle is missing (_mtable.Cycles.TryGetValue returns false), the body is left without ANY cyclic tail at all — and every part snaps to its setup-default offset on the next Advance(). Most creatures' setup-defaults put all limbs at the torso origin, so the visual collapses to "just a torso on the ground" until a different (working) cycle arrives. This is specifically a regression from commit186a584(Phase L.1c port). Pre-fix, MoveTo packets fell through to fullMotion=Ready (every MotionTable contains a Ready cycle). Post-fix, MoveTo packets seed fullMotion=RunForward via PlanMoveToStart. Some combat-stance creatures (e.g. monsters in HandCombat 0x003C) have no (combat, RunForward) cycle in their MotionTable — they're meant to walk in combat, with retail's apply_run_to_command upgrading WalkForward → RunForward at the velocity layer rather than the animation-cycle layer. Fix: add `AnimationSequencer.HasCycle(style, motion)` query and gate the SetCycle call site in GameWindow.OnLiveMotionUpdated behind it. Fall back chain: requested motion → WalkForward → Ready → no-op-don't-clear. The InterpretedState.ForwardCommand bulk-copy (commit37de771) is unchanged — body still gets RunForward velocity even when the visible animation falls back to WalkForward or Ready. Tests: 1420 → 1422. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
37de771778
commit
34d7f4def2
3 changed files with 114 additions and 1 deletions
|
|
@ -2578,7 +2578,53 @@ public sealed class GameWindow : IDisposable
|
|||
// whatever the interpreted state says when the body
|
||||
// lands.
|
||||
if (!remoteIsAirborne)
|
||||
ae.Sequencer.SetCycle(fullStyle, animCycle, animSpeed);
|
||||
{
|
||||
// Fallback chain for missing cycles in the MotionTable.
|
||||
// SetCycle unconditionally calls ClearCyclicTail() before
|
||||
// looking up the cycle; if the cycle is absent, the body
|
||||
// ends up with no cyclic tail at all and every part snaps
|
||||
// to its setup-default offset — visible as "torso on the
|
||||
// ground" because most creatures' setup-default puts all
|
||||
// limbs at the torso origin.
|
||||
//
|
||||
// This is specifically a regression from commit 186a584
|
||||
// (Phase L.1c port): pre-fix, MoveTo packets fell through
|
||||
// to fullMotion=Ready (which always exists in every
|
||||
// MotionTable). Post-fix, MoveTo packets seed
|
||||
// fullMotion=RunForward, but some creatures (especially
|
||||
// when stance=HandCombat) lack a (combat, RunForward)
|
||||
// cycle. Fall through RunForward → WalkForward → Ready
|
||||
// until we find one the table actually contains.
|
||||
//
|
||||
// Note: this fallback is for the SEQUENCER (visible
|
||||
// animation) only. InterpretedState.ForwardCommand still
|
||||
// gets the wire's (or seeded) ForwardCommand verbatim
|
||||
// so apply_current_movement produces correct velocity.
|
||||
uint cycleToPlay = animCycle;
|
||||
if (!ae.Sequencer.HasCycle(fullStyle, cycleToPlay))
|
||||
{
|
||||
// RunForward (0x44000007) → WalkForward (0x45000005)
|
||||
if ((cycleToPlay & 0xFFu) == 0x07
|
||||
&& ae.Sequencer.HasCycle(fullStyle, 0x45000005u))
|
||||
{
|
||||
cycleToPlay = 0x45000005u;
|
||||
}
|
||||
// WalkForward → Ready (0x41000003)
|
||||
else if (ae.Sequencer.HasCycle(fullStyle, 0x41000003u))
|
||||
{
|
||||
cycleToPlay = 0x41000003u;
|
||||
}
|
||||
// Ready missing too — leave the existing cycle alone
|
||||
// by not calling SetCycle at all (avoids the
|
||||
// ClearCyclicTail wipe).
|
||||
else
|
||||
{
|
||||
cycleToPlay = 0;
|
||||
}
|
||||
}
|
||||
if (cycleToPlay != 0)
|
||||
ae.Sequencer.SetCycle(fullStyle, cycleToPlay, animSpeed);
|
||||
}
|
||||
|
||||
// Retail runs the full MotionInterp state machine on every
|
||||
// remote. Route each wire command (forward, sidestep, turn)
|
||||
|
|
|
|||
|
|
@ -330,6 +330,33 @@ public sealed class AnimationSequencer
|
|||
/// makes the jump look delayed (legs stand still for ~100 ms while
|
||||
/// the link drains, then fold into Falling). Defaults to false to
|
||||
/// preserve normal smooth transitions for everything else.</param>
|
||||
/// <summary>
|
||||
/// Check whether the underlying MotionTable contains a cycle for the
|
||||
/// given (style, motion) pair. Useful for callers that want to fall
|
||||
/// back to a known-good motion (e.g. <c>WalkForward</c> →
|
||||
/// <c>Ready</c>) instead of triggering <see cref="SetCycle"/>'s
|
||||
/// unconditional <c>ClearCyclicTail</c> path on a missing cycle —
|
||||
/// which leaves the body without any animation tail and snaps every
|
||||
/// part to the setup-default offset (visible as "torso on the
|
||||
/// ground" since most creatures' setup-default has limbs at the
|
||||
/// torso origin).
|
||||
/// </summary>
|
||||
public bool HasCycle(uint style, uint motion)
|
||||
{
|
||||
// adjust_motion remapping (mirrors the head of SetCycle):
|
||||
// TurnLeft, SideStepLeft, WalkBackward map to their right/forward
|
||||
// mirror cycles.
|
||||
uint adjustedMotion = motion;
|
||||
switch (motion & 0xFFFFu)
|
||||
{
|
||||
case 0x000E: adjustedMotion = (motion & 0xFFFF0000u) | 0x000Du; break;
|
||||
case 0x0010: adjustedMotion = (motion & 0xFFFF0000u) | 0x000Fu; break;
|
||||
case 0x0006: adjustedMotion = (motion & 0xFFFF0000u) | 0x0005u; break;
|
||||
}
|
||||
int cycleKey = (int)(((style & 0xFFFFu) << 16) | (adjustedMotion & 0xFFFFFFu));
|
||||
return _mtable.Cycles.ContainsKey(cycleKey);
|
||||
}
|
||||
|
||||
public void SetCycle(uint style, uint motion, float speedMod = 1f, bool skipTransitionLink = false)
|
||||
{
|
||||
// ── adjust_motion: remap left→right / backward→forward variants ───
|
||||
|
|
|
|||
|
|
@ -223,6 +223,46 @@ public sealed class AnimationSequencerTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasCycle_PresentInTable_ReturnsTrue()
|
||||
{
|
||||
// Phase L.1c followup (2026-04-28): regression guard for
|
||||
// "torso on the ground" — caller (GameWindow MoveTo path) needs
|
||||
// to query the table before SetCycle to avoid the
|
||||
// ClearCyclicTail wipe on a missing cycle.
|
||||
const uint Style = 0x003Cu; // HandCombat
|
||||
const uint Motion = 0x0003u; // Ready
|
||||
const uint AnimId = 0x03000001u;
|
||||
|
||||
var setup = Fixtures.MakeSetup(2);
|
||||
var mt = Fixtures.MakeMtable(Style, Motion, AnimId);
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(AnimId, Fixtures.MakeTwoFrameAnim(2, Vector3.Zero, Quaternion.Identity, Vector3.Zero, Quaternion.Identity));
|
||||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
|
||||
// Caller passes the SAME shape SetCycle expects: full style with
|
||||
// class byte (0x80000000) and full motion (0x40000000 / 0x10000000).
|
||||
Assert.True(seq.HasCycle(0x8000003Cu, 0x41000003u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasCycle_MissingFromTable_ReturnsFalse()
|
||||
{
|
||||
const uint Style = 0x003Cu;
|
||||
const uint ReadyMotion = 0x0003u;
|
||||
const uint AnimId = 0x03000001u;
|
||||
|
||||
var setup = Fixtures.MakeSetup(2);
|
||||
var mt = Fixtures.MakeMtable(Style, ReadyMotion, AnimId);
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(AnimId, Fixtures.MakeTwoFrameAnim(2, Vector3.Zero, Quaternion.Identity, Vector3.Zero, Quaternion.Identity));
|
||||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
|
||||
// RunForward (0x44000007) is NOT in the table — caller should
|
||||
// see false and fall back to a known motion (WalkForward / Ready).
|
||||
Assert.False(seq.HasCycle(0x8000003Cu, 0x44000007u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCycle_LoadsAnimation_AdvanceReturnsBoundedTransforms()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue