Complete ground-up rewrite of AnimationSequencer.cs using the retail AC client
pseudocode (docs/research/acclient_animation_pseudocode.md) as the direct
translation guide. Every key algorithmic difference from the previous patched
implementation is addressed:
1. _framePosition is now double (64-bit), matching Sequence+0x30 in the retail
client binary. Previously float, which accumulated rounding error over long
sessions.
2. FUN_005267E0 (multiply_framerate) is now correctly applied at node load time:
negative speedScale swaps startFrame↔endFrame so the advance loop counts DOWN
from (EndFrame+1)-epsilon toward EndFrame, exactly matching the retail layout.
3. update_internal (FUN_005261D0) is faithfully ported: one loop handles both
forward and reverse; boundary detection uses EndFrame as the lower bound for
reverse playback (matching the post-swap field semantics); remainder time
propagates correctly across node boundaries for large dt values.
4. GetStartFramePosition (FUN_00526880) and GetEndFramePosition (FUN_005268B0)
formulas are now correct: negative speed starts at (EndFrame+1)-epsilon,
ends at StartFrame; positive speed starts at StartFrame, ends at (EndFrame+1)-epsilon.
5. advance_to_next_animation (FUN_00525EB0) wraps to _firstCyclic when the
linked list is exhausted, matching the retail loop-forever semantics.
6. adjust_motion (ACE MotionInterp.cs:394-428) remapping is unchanged and
correct: TurnLeft→TurnRight, SideStepLeft→SideStepRight (negate speed),
WalkBackward→WalkForward (negate×0.65 BackwardsFactor).
7. SlerpRetailClient (FUN_005360d0) is unchanged — the pseudocode confirms the
existing implementation is correct.
AnimationSequencerTests grows from 9 to 17 tests:
- Negative-speed playback: TurnLeft remaps and cursor initializes near EndFrame+1
- Reverse frame position decreases (not increases) over time
- Reverse wrap at start boundary recovers and loops
- advance_to_next_animation: link node drains then enters cycle
- Cycle loops repeatedly without crash or position drift
All 431 tests green (109 net + 322 core).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port the animation playback engine from the decompiled retail client
into AcDream.Core.Physics.AnimationSequencer.
## What this adds
**AnimationSequencer** (src/AcDream.Core/Physics/AnimationSequencer.cs):
- Frame advancer: `frameNum += framerate * dt`, bounds-checks against
AnimData.HighFrame/LowFrame (with sentinel resolution for HighFrame=-1),
wraps at cycle boundaries. Matches ACE's `Sequence.update_internal`.
- Quaternion slerp (`SlerpRetailClient`): ported from decompiled
`FUN_005360d0` (chunk_00530000.c:4799-4846):
1. dot-product sign-flip to take the shorter arc
2. fallback to linear blend when 1-dot <= 1e-4 (near-parallel)
3. sin-based slerp for all other cases
4. validate weights lie in [0,1] before using sin result (retail
client validation step that guards degenerate inputs)
- Transition link resolution: `GetLink(style, fromMotion, toMotion)`
mirrors ACE's `MotionTable.get_link` positive-speed path.
DatReaderWriter layout: `Links[style<<16|(from&0xFFFFFF)]` is a
`MotionCommandData` whose `.MotionData[toMotion]` is the transition
`MotionData`. Link frames are prepended before the cyclic tail, so
idle->walk plays the short transition clip then loops the walk cycle.
- `IAnimationLoader` / `DatCollectionLoader`: thin abstraction so the
sequencer is testable offline without opening dat files.
- Public API: `SetCycle(style, motion, speedMod)` + `Advance(dt)`
returning `IReadOnlyList<PartTransform>` (Origin+Orientation per part).
**AnimationSequencerTests** (tests/...Physics/AnimationSequencerTests.cs):
14 tests, all offline, covering slerp math, frame wrap, transition link
prepend, no-link direct switch, same-motion fast path, reset.
317 tests green, 0 warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>