When the server broadcasts a mid-run UpdateMotion with a different
ForwardSpeed (e.g. the player's RunRate changes due to stamina / skill
update), acdream must NOT restart the cycle — that would reset the
footstep cursor and look like a visible twitch. Retail handles this via
Sequence.multiply_cyclic_animation_framerate (ACE
references/ACE/Source/ACE.Server/Physics/Animation/Sequence.cs L277-L287),
which walks the cyclic tail of the queue and scales each node's
framerate by newSpeed / oldSpeed. MotionTable.change_cycle_speed
(MotionTable.cs L372-L379) is the caller from the same-motion path in
GetObjectSequence (L132-L139).
This commit:
1. Adds AnimNode.MultiplyFramerate(factor) — scales a single node's
framerate. Retail also swapped StartFrame↔EndFrame for negative
factors; acdream keeps StartFrame ≤ EndFrame as an invariant and
encodes direction via Framerate sign (see existing comment in
LoadAnimNode), so we only scale. Valid because callers only ever
pass positive factors from UpdateMotion ForwardSpeed.
2. Adds AnimationSequencer.MultiplyCyclicFramerate(factor) — walks
_firstCyclic through the tail and calls node.MultiplyFramerate(factor).
Also scales each node's Velocity and Omega by the same factor so
CurrentVelocity / CurrentOmega stay aligned with playback — matches
ACE's subtract_motion + combine_motion pair in change_cycle_speed.
3. Adds AnimationSequencer.CurrentSpeedMod public property — starts at
1.0, updated by SetCycle on both restart and mid-cycle rescale.
4. Adds a speed-change fast-path to SetCycle: when the (style, motion)
pair matches the current cycle and signs agree,
MultiplyCyclicFramerate(newSpeed/oldSpeed) is called instead of
rebuilding the queue — the cursor stays where it is and the animation
continues at the new rate.
5. Wires InterpretedMotionState.ForwardSpeed from UpdateMotion through
to SetCycle in OnLiveMotionUpdated. ACE omits the ForwardSpeed flag
when speed == 1.0 (InterpretedMotionState.cs:101-103), so we default
missing/zero values to 1.0.
Tests: 4 new sequencer tests covering MultiplyCyclicFramerate,
cursor preservation across speed changes, the same-motion-different-speed
fast-path, and the same-motion-same-speed no-op guard. 632 tests green
(was 628).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>