# acdream AnimationSequencer — current-state map (R1) File: `src/AcDream.Core/Physics/AnimationSequencer.cs` (1584 lines, read whole). Companions: `AnimationCommandRouter.cs` (98 lines), `AnimationHookRouter.cs` (95 lines), `IAnimationHookSink.cs` (89 lines) — all read whole. ## 0. Type inventory in AnimationSequencer.cs - `IAnimationLoader` — abstraction (`LoadAnimation(uint id) : Animation?`) so the sequencer can be unit-tested without a real `DatCollection`. - `DatCollectionLoader : IAnimationLoader` — production impl, wraps `DatCollection.Get(id)`. - `PartTransform` (readonly struct) — `Vector3 Origin`, `Quaternion Orientation`. Output unit of `Advance`. - `AnimNode` (internal sealed class) — one queue entry: `Animation Anim; double Framerate; int StartFrame; int EndFrame; bool IsLooping; bool HasPosFrames; Vector3 Velocity; Vector3 Omega`. Methods: `MultiplyFramerate(factor)`, `GetStartFramePosition()`, `GetEndFramePosition()`. `FrameEpsilon = 1e-5` (mirrors retail `_DAT_007c92b4`). - `AnimationSequencer` (public sealed class) — the engine itself, one instance per entity. ## (a) Feature matrix — AnimationSequencer vs retail CSequence concepts | Retail concept | Retail anchor cited in acdream | acdream implementation | Notes / fidelity | |---|---|---|---| | Node list (AnimSequenceNode queue) | FUN_00525EB0 `advance_to_next_animation`; ACE `Sequence.cs` | `LinkedList _queue`, `_currNode`, `_firstCyclic` pointers | Doubly-linked list mirrored with .NET `LinkedList`. Non-cyclic head (link frames) + looping tail (`_firstCyclic..end`) invariant maintained explicitly rather than via a node flag walked at runtime. | | Link resolution (`get_link`) | ACE `MotionTable.cs:395-426`; retail `CMotionTable::GetObjectSequence` 0x00522860 | `GetLink(style, substate, substateSpeed, motion, speed)` private method | Forward-direction path: `Links[(style<<16)|substate][motion]`, then style-level catch-all `Links[style<<16][motion]`. **Reversed-direction branch** added (comment cites "K-fix6"): when either speed is negative, looks up `Links[(style<<16)|motion][substate]` instead (link FROM motion TO substate, played reversed) — handles WalkBackward/SideStepLeft/TurnLeft transitions. Also a `StyleDefaults` fallback under the reversed branch. This 2-branch structure is **acdream's own generalization**, not a literal 1:1 decompile citation (no direct FUN_xxx cited for the branch split itself, only for its constituent parts via ACE line numbers). | | `adjust_motion` (TurnLeft/SideStepLeft/WalkBackward → mirror) | ACE `MotionInterp.cs:394-428` | Inlined at top of `SetCycle` + duplicated in `HasCycle`: 0x000E→0x000D (negate speed), 0x0010→0x000F (negate speed), 0x0006→0x0005 (negate speed × 0.65 `BackwardsFactor`) | Faithful port; the `0x65` low-byte switch is applied to `motion & 0xFFFFu`, i.e. matched purely on the low 16 bits regardless of class byte. | | Fast-path re-speed (no restart on same motion) | ACE `MotionTable.cs:132-139` | `SetCycle` early-return branch: if `CurrentStyle==style && CurrentMotion==motion && sign(speedMod)==sign(CurrentSpeedMod)` → `MultiplyCyclicFramerate` instead of rebuild | Faithful. Explicit **sign-flip exception** documented: when adjust_motion flips speedMod's sign while `motion` value itself stays the same (WalkForward with negative speed = backward), the fast path is bypassed by the sign check so a full restart occurs — comment marks this as an acdream-observed necessity (2026-05-02), not literally cited to a retail address. | | `multiply_cyclic_animation_framerate` | FUN_00525CE0; ACE `Sequence.cs L277-L287` | `MultiplyCyclicFramerate(float factor)` — walks `_firstCyclic..end`, calls `AnimNode.MultiplyFramerate` on each, and also scales `CurrentVelocity *= factor; CurrentOmega *= factor` | Faithful for node framerates. The velocity/omega scaling is justified by algebraic equivalence to ACE's `subtract_motion(old)+combine_motion(new)` (`MotionTable.change_cycle_speed`, `MotionTable.cs L372-L379`) rather than being separately decompiled — an acdream derivation, not a citation of a specific retail scaling line. | | `GetStartFramePosition` / `GetEndFramePosition` | FUN_00526880 / FUN_005268B0 | `AnimNode.GetStartFramePosition()/GetEndFramePosition()` | Faithful 1:1 including the `EPSILON = _DAT_007c92b4` (hardcoded here as `1e-5` — the retail exact float constant was **not** independently verified against the binary in this file's comments; it's asserted equal). | | `multiply_framerate` (StartFrame↔EndFrame swap for negative speed) | FUN_005267E0 | `AnimNode.MultiplyFramerate(factor)` | **Explicitly documented divergence**: retail swaps StartFrame↔EndFrame for negative factor; acdream keeps `StartFrame ≤ EndFrame` as an invariant and encodes direction purely via `Framerate`'s sign, compensating in the `Advance` loop's boundary checks instead. Comment states this is valid "because the callers we care about... only ever pass positive factors" — i.e. an acknowledged simplification with a stated (unverified beyond code review) precondition. | | `update_internal` (per-frame advance loop) | FUN_005261D0; ACE `Sequence.cs:351-443` | `Advance(float dt)` | Faithful structurally: `while (timeRemaining>0 && _currNode!=null)` loop (capped `safety=64` — **acdream-invented safety valve**, no retail citation, guards against a "degenerate motion table" infinite loop). Computes `delta = rate*timeRemaining`; forward vs reverse branches each: (1) detect boundary overflow, (2) clamp `_framePosition` to boundary-epsilon, (3) walk every integer frame crossed applying `ApplyPosFrame` + `ExecuteHooks`, (4) on wrap, call `advance_to_next_animation` (`AdvanceToNextAnimation()`) and continue with leftover `overflow` time. | | `advance_to_next_animation` (node wrap) | FUN_00525EB0 | `AdvanceToNextAnimation()` — moves `_currNode` to `.Next`, or wraps to `_firstCyclic` if null, else holds on last node if no cyclic tail exists | Faithful. Resets `_framePosition` via `GetStartFramePosition()` on transition. | | `execute_hooks` | ACE `Sequence.cs:262-270` | `ExecuteHooks(node, frameIndex, playbackDir)` — fires hook if `hook.Direction == Both \|\| hook.Direction == playbackDir` | Faithful 1:1 port of the direction-match condition. | | Root motion (`AFrame.Combine` / `frame.Subtract`) | ACE (`AFrame.Combine`/`Subtract`, cited generically, no line #) | `ApplyPosFrame(node, frameIndex, reverse)` — forward: `_rootMotionPos += Rotate(pf.Origin, _rootMotionRot); _rootMotionRot = Normalize(_rootMotionRot * pf.Orientation)`; reverse: conjugate-then-subtract | Faithful port of the two composition directions. Accumulated into `_rootMotionPos`/`_rootMotionRot`, drained via `ConsumeRootMotionDelta()`. | | AnimationDone hook on link drain | ACE `PhysicsObj.add_anim_hook(AnimationHook.AnimDoneHook)` (cited generically) | `AnimationDoneSentinel` (static `AnimationDoneHook{Direction=Both}`) pushed to `_pendingHooks` in `Advance` just before `AdvanceToNextAnimation()`, gated on `!_currNode.Value.IsLooping` | Faithful concept; the sentinel object is a single shared static instance (not per-fire), meaning downstream code cannot distinguish *which* motion completed purely from hook identity — must correlate via entity + timing. Not flagged as a bug in the file; simply a design note for consumers. | | Quaternion slerp | FUN_005360d0 (chunk_00530000.c:4799-4846) | `SlerpRetailClient(q1, q2, t)` static method | Faithful port including retail's odd step-5 validation-then-linear-fallback quirk (`SlerpEpsilon = 1e-4f` near-parallel check, then acos/sin slerp, then re-validate blend weights ∈[0,1] before trusting the slerp result — falls back to linear otherwise). Explicitly documented as differing from "the standard formula" only in this validation step. | | Frame-boundary blend (`BuildBlendedFrame`) | Not directly cited to a single retail FUN — described as producing "the current blended keyframe" | `BuildBlendedFrame()` — clamps `frameIdx` to `[rangeLo,rangeHi]`; computes `nextIdx` stepping in playback direction; **wraps only if `curr.IsLooping`**, else holds boundary frame | **Explicitly acdream-motivated fix, not retail-cited**: comment for issue **#61** ("door swing-open flap; run-stop twitch") states holding the boundary frame instead of wrapping-blending into frame 0 for one-shot (non-looping) nodes avoids "a brief flash through the anim's starting pose at the link→cycle boundary." This is the **frame-swap / link→cycle boundary flash class of bug** the deep-dive skill targets — currently patched empirically here, not derived from a decompiled retail boundary-hold mechanism. | | Placement / root frames | `AnimData.AnimId`, `Animation.PartFrames`, `Animation.PosFrames`, `Frame.Origin/Orientation` (DatReaderWriter types) | `LoadAnimNode` reads `anim.PartFrames.Count` for frame bounds; `hasPosFrames = anim.Flags.HasFlag(PosFrames) && anim.PosFrames.Count >= numFrames` | Sentinel resolution for `HighFrame == -1` ("all frames") ported from `MotionResolver.GetIdleCycle` (cited by name, not address). `if (low > high) high = low` guards a degenerate AnimData. | | "Fix B" (#39, 2026-05-06) — cyclic→cyclic direct transition skips link | cdb live trace 2026-05-03 of a Walk→Run transition: `add_to_queue(45000005, looping=1)` then `add_to_queue(44000007, looping=1)` with `truncate_animation_list` never firing | In `SetCycle`: `IsLocomotionCycleLowByte` check on both `CurrentMotion` and new `motion`'s low byte (0x05/0x06/0x07/0x0F/0x10); if both are locomotion AND `_firstCyclic` exists, the just-enqueued link node is **removed from the queue** and `_currNode` is forced directly onto `_firstCyclic` | This is the most heavily-commented divergence-turned-fix in the file. Explicit warning left in comments: "Commit c06b6c5 (reverted in a2ae2ae) demonstrated that unconditionally skipping the link breaks all of these [Idle→cycle, Falling→Ready, pose-change links, combat substates]" — i.e. the fix is deliberately **scoped only to the locomotion-cycle subset**, confirmed retail-faithful via a live cdb capture (grep-named-first / attach-cdb workflow), not guessed. | | K-fix18 — `skipTransitionLink` | Not a retail citation — an acdream product decision, explicitly labeled "K-fix18" in the doc comment | `SetCycle(..., bool skipTransitionLink = false)` param. When true: `linkData` forced null, AND (separately) the entire `_queue` is cleared / `_currNode`/`_firstCyclic` reset to null before rebuild | **Confirmed acdream-invented deviation** (not retail behavior) — used only for Falling-on-jump-start (`GameWindow.cs:4830`, `10201`) to avoid a ~100ms "stop running" pose delay before the fall animation engages. This should have (or needs to be checked against) a `retail-divergence-register.md` row per CLAUDE.md's mandatory bookkeeping rule — the file itself doesn't reference the register. | | Stop-anim fallback (direction-agnostic settle) | Not retail-cited; acdream reasoning: "settle anim is direction-agnostic" | In `SetCycle`, if `linkData is null` after the primary lookup, retries `GetLink` with `CurrentMotion`'s low byte remapped 0x06→0x05, 0x10→0x0F, 0x0E→0x0D (peer forward/right substate) | Acknowledged as an acdream workaround for a `null linkData` gap when stopping from WalkBackward/SideStepLeft/TurnLeft — motivated by an observed visible glitch ("left leg twitches forward two times") rather than by a decompiled retail fallback path. | | Stale-head handling | Comment: "For remote entities receiving many bundled UMs over time, this stale-head build-up was the root cause of..." | `preEnqueueTail` snapshot before `EnqueueMotionData` calls; after enqueue, `firstNew = preEnqueueTail?.Next ?? _queue.First`; `_currNode` is **force-set** onto `firstNew` (or `_firstCyclic` under the Fix-B branch) rather than left wherever it was | Acdream-diagnosed and acdream-authored fix (no retail citation) — addresses a structural bug where `_currNode` could remain parked on old non-cyclic head frames left over from a previous `SetCycle` call, which visually manifested as "transitions between cycles don't visibly switch the leg pose" for remote entities. Local player was unaffected because `PlayerMovementController` calls `SetCycle` frequently enough to keep the queue clean — this is called out explicitly as a per-entity-class asymmetry. | | Velocity/Omega synthesis for locomotion & turn cycles | `CMotionInterp::get_state_velocity` (FUN_00528960 cited by address); `MotionInterpreter.RunAnimSpeed` etc (decompiled from `_DAT_007c96e0/e4/e8`); ACE's `omega.z = ±(π/2)×turnSpeed` (cross-checked against holtburger `motion_resolution.rs`) | Post-`EnqueueMotionData` block in `SetCycle`: switches on `motion & 0xFFu` for 0x05/0x06/0x07/0x0F/0x10, sets `CurrentVelocity` from `WalkAnimSpeed=3.12f` / `RunAnimSpeed=4.0f` / `SidestepAnimSpeed=1.25f` constants × `adjustedSpeed`; separately, if `CurrentOmega` is ~zero, synthesizes turn omega `±(π/2)*adjustedSpeed` for 0x0D/0x0E | **Explicitly justified divergence from literal dat content**: "the Humanoid motion table ships every locomotion MotionData with Flags=0x00 (no HasVelocity)" — i.e. the dat itself is silent on velocity for these cycles, and retail's *actual* body-physics source (`get_state_velocity`) is a separate C++ function not expressed as sequencer/dat state at all. acdream folds that separate retail mechanism into `AnimationSequencer.CurrentVelocity` as a pragmatic single surface for consumers (dead-reckoning, `get_state_velocity` Option-B). Comment explicitly documents an earlier bug: a gate of `if (CurrentVelocity.LengthSquared() < 1e-9f)` previously let dat-baked link velocity "win" over synthesis, breaking walk→run transitions — now **unconditionally overwritten** for known locomotion low-bytes. | | `clear_physics` before rebuild | ACE `Sequence.cs L256-L260` | `ClearPhysics()` — zeroes `CurrentVelocity`/`CurrentOmega`, called from `SetCycle` before the enqueue chain | Faithful; matches retail's `sequence.clear_physics()` call before each `add_motion` chain (`MotionTable.cs L100-L101, L152-L153`). | | `add_motion` velocity/omega replace-not-accumulate semantics | ACE `MotionTable.add_motion` (`MotionTable.cs L358-L370`) | `EnqueueMotionData`: `if (motionData.Flags.HasFlag(HasVelocity)) CurrentVelocity = vel;` (replace, not add) similarly for Omega | Faithful — comment explicitly notes this REPLACES not accumulates, and that the final value after link+cycle enqueue is the cycle's (last write wins), which dead-reckoning consumers rely on. | | PlayAction / Modifier & Action-class overlay | ACE `MotionTable.GetObjectSequence` line 189-207 (Action, mask 0x10) and line 234-242 (Modifier, mask 0x20) | `PlayAction(uint motionCommand, float speedMod=1f)` — tries `GetLink` (Links dict) first for Action-mask commands, then falls back to `_mtable.Modifiers` dict (styled key then plain key) for Modifier-mask commands | Faithful dual-path lookup. Notes Jump (`0x2500003B`) has both Action+Modifier-looking bits but ACE treats it via the Modifier path specifically — cited as a cross-check against ACE, not an independent retail decompile of Jump's own class byte. Inserts new non-looping nodes via `_queue.AddBefore(_firstCyclic, n)` (or `AddLast` if no cyclic tail), and force-jumps `_currNode` to the first inserted node if the cursor was currently on the cyclic tail (so overlay actions play immediately instead of waiting for a cycle wrap). | | `Sequence.Velocity`/`Sequence.Omega` sequence-wide mirror | ACE `Sequence.cs L127-130`; `MotionTable.add_motion` L358-370 | `CurrentVelocity`/`CurrentOmega` public properties, described in the doc comment as "not per-node" — reflects the **most recently added** MotionData's velocity×speedMod, even while an earlier link node is still playing visually | Faithful semantic port — explicitly documented gotcha: "while a link animation plays, the surfaced velocity is still the cycle's velocity." | | `HasCycle` (retail cycle-existence probe) | Not a literal retail function name — acdream utility mirroring the head of `SetCycle`'s adjust_motion + cycle-key lookup | `HasCycle(uint style, uint motion) : bool` — duplicates the adjust_motion switch (0x000E/0x0010/0x0006) then checks `_mtable.Cycles.ContainsKey(cycleKey)` | Acdream-invented consumer helper (doc comment explains it exists so callers can fall back to a known-good motion instead of hitting `SetCycle`'s unconditional `ClearCyclicTail` on a missing cycle, which "leaves the body without any animation tail" — visible as "torso on the ground"). Not retail-cited as its own function. | | Reset | Not retail-cited | `Reset()` — clears queue, hooks, root motion, resets Current* fields to defaults | Utility method for entity despawn/respawn recycling; no retail anchor given. | | Diagnostics: `CurrentNodeDiag`, `FirstCyclicAnimRefHash`, `[SCFAST]/[SCFULL]/[SCNULLFALLBACK]` logging | N/A — acdream-only | `CurrentNodeDiag` tuple property (AnimRefHash via `RuntimeHelpers.GetHashCode`, IsLooping, Framerate, StartFrame, EndFrame, FramePosition, QueueCount); `FirstCyclicAnimRefHash`; three `Console.WriteLine` blocks gated on `ACDREAM_REMOTE_VEL_DIAG=1`, throttled to 0.5s via `_lastSetCycleDiagTime` | Entirely acdream-invented instrumentation ("Commit A 2026-05-03" / D2/D3/D4 diagnostic labels) for the remote-motion debugging campaign. Per CLAUDE.md rule 5 (diagnostic owner classes), this is a per-call-site `Environment.GetEnvironmentVariable` read pattern flagged in CLAUDE.md itself as "tech debt; do not add more" — these predate that rule and haven't been migrated to a diagnostic-owner class. | ## (b) Public API surface — what consumers depend on ### Constructor ``` AnimationSequencer(Setup setup, MotionTable motionTable, IAnimationLoader loader) ``` All three args are `ArgumentNullException.ThrowIfNull`'d. Consumers must supply a non-null `Setup`/`MotionTable`/loader — `RenderBootstrap.cs`'s `SequencerFactory` local function has a 3-tier fallback chain (real setup+mtable → real setup + empty `MotionTable()` → fully-empty `Setup()`+`MotionTable()`+`NullAnimLoader()`) precisely because the ctor won't accept nulls. ### Methods - `bool HasCycle(uint style, uint motion)` — probe before calling `SetCycle` to avoid the "torso on ground" missing-cycle collapse. Called from `GameWindow.cs:3723/3728/3732/3824` (initial spawn cycle selection with Run→Walk→Ready fallback chain) and `RemoteMotionSink.Commit()` (same fallback pattern for remote UM-driven cycles, plus an `ACDREAM_REMOTE_VEL_DIAG` `[HASCYCLE]` diagnostic). - `void SetCycle(uint style, uint motion, float speedMod = 1f, bool skipTransitionLink = false)` — the primary state-transition entry point. Callers across the codebase: - `GameWindow.cs:3751` — initial spawn cycle. - `GameWindow.cs:3825` — initial cycle after a fallback check. - `GameWindow.cs:4830` — Falling-on-jump-start with `skipTransitionLink: true` (K-fix18). - `GameWindow.cs:4936` (`ApplyServerControlledVelocityCycle`) — NPC/monster remotes still on the legacy `ServerControlledLocomotion.PlanFromVelocity` path (pre-S6 unification; comment says this path stays until "S6 unifies all entity classes onto the CMotionInterp funnel"). - `GameWindow.cs:5155/5309/9817` — landing cycle / stop-to-ready transitions. - `GameWindow.cs:10223` — local player's own `SetCycle` call (with sidestep speed-scaling correction factor `WalkAnimSpeed/SidestepAnimSpeed*0.5` documented against ACE's `MovementData.cs:124-131` wire formula, #45). - `RemoteMotionSink.Commit()` (`src/AcDream.App/Rendering/RemoteMotionSink.cs:215`) — remote **player** entities via the L.2g S2b `CMotionInterp` funnel path (see below); this is the modern replacement for the legacy path above for player remotes specifically. - `AnimationCommandRouter.RouteFullCommand` — routes `SubState`-class commands here (see router section below). - `void MultiplyCyclicFramerate(float factor)` — not called directly outside `AnimationSequencer.cs` in the searched consumer set (invoked internally by `SetCycle`'s fast-path re-speed branch); no external call sites found in `src/`. - `IReadOnlyList Advance(float dt)` — per-frame tick. Sole call site: `GameWindow.cs:9876` inside the animated-entity tick loop (`seqFrames = ae.Sequencer.Advance(dt)`), guarded by an `ae.Sequencer is not null` branch; entities without a sequencer fall to a "legacy path" (`ae.CurrFrame += dt * ae.Framerate` manual slerp, line ~9896). - `IReadOnlyList ConsumePendingHooks()` — drained immediately after `Advance` at `GameWindow.cs:9882`, fanned out per-hook to `_hookRouter.OnHook(ae.Entity.Id, worldPos, hook)` (`AnimationHookRouter`, which further fans out to registered `IAnimationHookSink`s). - `(Vector3 Position, Quaternion Rotation) ConsumeRootMotionDelta()` — no call sites found in the searched `src/` consumer grep; root motion (PosFrames) accumulation exists in the sequencer but nothing currently drains/consumes it in production code (search of `ConsumeRootMotionDelta` and `\.PlayAction\(` etc. above did not surface a caller — **this looks like dead/unwired API surface**, worth flagging for the deep-dive). - `void PlayAction(uint motionCommand, float speedMod = 1f)` — called from `AnimationCommandRouter.RouteFullCommand` for `Action`/`Modifier`/ `ChatEmote` route kinds, and directly from `RemoteMotionSink.ApplyMotion` for the same route kinds (overlay dispatch for remote entities). - `void Reset()` — no external call site found in the searched consumer set. ### Properties (read by consumers) - `CurrentStyle`, `CurrentMotion`, `CurrentSpeedMod` (uint/uint/float, private-set) — read by `GameWindow.cs` (style fallback defaults `0x8000003Du` NonCombat at lines 3723/4827/4919 etc.), `RemoteMotionSink` ctor (seeds `_style` from `sequencer.CurrentStyle`), `RemoteMotionSink.Commit()` (`_sequencer.CurrentMotion`/`CurrentSpeedMod` for diagnostic comparison), `GameWindow.cs:4915` (`ApplyServerControlledVelocityCycle`'s `IsRemoteLocomotion(currentMotion)` gate). - `CurrentVelocity` / `CurrentOmega` (`Vector3`, private-set) — the most cross-cutting output surface: - `GameWindow.cs:9331-9334` — remote player per-tick body translation (`seqVel`/`seqOmega` feed `PositionManager`-style catch-up/anim composition; extensive comment block on lines 9310-9330 walks through retail's `CPartArray::Update` → `PositionManager::adjust_offset` → `Frame::combine` per-tick pipeline this is meant to mirror). - `PlayerMovementController.AttachCycleVelocityAccessor(() => playerSeq.CurrentVelocity)` (`GameWindow.cs:12917`) — wires the local player's sequencer velocity into `MotionInterpreter.GetCycleVelocity` as an override of the decompiled constant path (`RunAnimSpeed * ForwardSpeed`), used because arbitrary creatures' MotionTables don't all bake `Velocity=4.0` on RunForward the way Humanoid does. - `MotionInterpreter.cs` — comments describe `CurrentVelocity` as "already `MotionData.Velocity * speedMod`" (body-local) and reference it as the accessor payload described above; also cross-checks `AnimationSequencer.CurrentVelocity`/`CurrentOmega` doc pointers at lines 347/376/632/662. - `QueueCount` (int) — diagnostic only (`CurrentNodeDiag` tuple's last field also duplicates it). - `HasCurrentNode` (bool) — diagnostic; no external call site found in the searched set beyond the type itself. - `CurrentNodeDiag` / `FirstCyclicAnimRefHash` — consumed by `GameWindow.cs:9863-9871` inside an `ACDREAM_REMOTE_VEL_DIAG`-gated `[CURRNODE]` log block that compares `AnimRefHash` against `FirstCyclicAnimRefHash` to detect whether the visible animation is actually on the intended cyclic tail. ### Nested/companion API — `IAnimationLoader` / `DatCollectionLoader` - `IAnimationLoader.LoadAnimation(uint id) : Animation?` — implemented in production by `DatCollectionLoader` (wraps `DatCollection.Get`), constructed once in `RenderBootstrap.cs:138` (`animLoader`) and passed by reference into every `SequencerFactory`-built sequencer; a `NullAnimLoader` (referenced but not shown in this file — defined elsewhere) is the last-resort fallback in `RenderBootstrap`'s factory. ## AnimationCommandRouter.cs — companion routing layer Static class, no state. Cited retail anchors: `CMotionTable::GetObjectSequence` 0x00522860, `CMotionInterp::DoInterpretedMotion` 0x00528360, plus `docs/research/deepdives/r03-motion-animation.md` section 3. - `Classify(uint fullCommand) : AnimationCommandRouteKind` — classifies by class-mask bits: `0x12000000`/`0x13000000` class → `ChatEmote`; `ModifierMask=0x20000000` → `Modifier`; `ActionMask=0x10000000` → `Action`; `SubStateMask=0x40000000` → `SubState`; `0` → `None`; else `Ignored`. (Note: checks Modifier before Action, meaning any command with both bits set classifies as Modifier — matches the `PlayAction` comment about Jump 0x2500003B having both bits but ACE treating it as Modifier.) - `RouteWireCommand(sequencer, currentStyle, ushort wireCommand, speedMod)` — reconstructs the full 32-bit command via `MotionCommandResolver.ReconstructFullCommand(wireCommand)` then delegates to `RouteFullCommand`. - `RouteFullCommand(sequencer, currentStyle, fullCommand, speedMod)` — dispatch table: `Action`/`Modifier`/`ChatEmote` → `sequencer.PlayAction(fullCommand, speedMod)`; `SubState` → `sequencer.SetCycle(currentStyle, fullCommand, speedMod)`. Returns the classified `AnimationCommandRouteKind` to the caller for logging/branching. `AnimationCommandRouteKind` enum: `None, Action, Modifier, ChatEmote, SubState, Ignored`. Consumers: `RemoteMotionSink.ApplyMotion` calls `AnimationCommandRouter.Classify` directly (to special-case Turn/Sidestep before falling through) and `RouteFullCommand` for the overlay branch. No other call sites found in the searched consumer set for `AnimationCommandRouter` itself (GameWindow.cs appears to call `Sequencer.SetCycle`/`PlayAction` directly rather than through the router in most of the enumerated call sites above — the router is primarily the remote-entity/funnel-driven path's dispatch layer). ## AnimationHookRouter.cs / IAnimationHookSink.cs — hook fan-out layer - `IAnimationHookSink.OnHook(uint entityId, Vector3 entityWorldPosition, AnimationHook hook)` — single-method interface. `AnimationHook` and its subclasses (`SoundHook`, `SoundTableHook`, `SoundTweakedHook`, `CreateParticleHook`, `DestroyParticleHook`, `StopParticleHook`, `CallPESHook`, `DefaultScriptHook`, `DefaultScriptPartHook`, `AttackHook`, `ReplaceObjectHook`, `TransparentHook`, `LuminousHook`, `DiffuseHook`, `ScaleHook`, `NoDrawHook`, `SetOmegaHook`, `TextureVelocityHook`, `SetLightHook`, `AnimationDoneHook`) are all **external types from `DatReaderWriter.Types`**, not defined in acdream's own tree — they're dat-format hook payload classes. The doc comment enumerates intended downstream routing (Phase E.2 audio / E.3 particles / E.4 combat dispatcher / renderer state mutation / UI notifications) but this file itself contains no subsystem logic — it's purely the interface contract + a `NullAnimationHookSink` no-op singleton for tests/headless. - `AnimationHookRouter : IAnimationHookSink` — composite/fan-out implementation. `Register(sink)` / `Unregister(sink)` are lock-protected (`_gate`), copy-on-write into a `IAnimationHookSink[]` array (idempotent register — checks `ReferenceEquals` before adding). `OnHook` iterates a snapshot array **without locking** (explicitly commented "no lock in the hot path (render thread)") and wraps each sink's `OnHook` call in a `try/catch` that **silently swallows all exceptions** ("one misbehaving sink must not take down the entire animation tick... individual subsystems can log their own errors internally") — this is a blanket catch-and-discard, worth flagging against CLAUDE.md's "no workarounds" spirit if a sink is silently failing (per the user's `feedback_logger_injection_for_silent_catches.md` memory note: libs that silently catch+return null usually should inject an `ILogger`; this router has no logger injection point at all). `Sinks` property exposes a read-only snapshot for diagnostics/tests. Only one production sink registration path was located via the router's own file (none) — sink registration call sites (`_hookRouter.Register(...)`) were not enumerated in this pass; the single confirmed dispatch call site is `GameWindow.cs:9890` (`_hookRouter.OnHook(ae.Entity.Id, worldPos, hook)`). ## How MotionData/anim dat structures reach the sequencer - **Setup** (`DatReaderWriter.DBObjs.Setup`) — supplies `Parts.Count` (used by `Advance`/`BuildBlendedFrame`/`BuildIdentityFrame` to size the `PartTransform[]` output) and `DefaultMotionTable` id (used by `RenderBootstrap.SequencerFactory` to look up the paired `MotionTable`). - **MotionTable** (`DatReaderWriter.DBObjs.MotionTable`) — three dictionaries consumed directly: - `Cycles : Dictionary` keyed by `(style<<16)|(adjustedMotion&0xFFFFFF)` — the looping tail source, read in `SetCycle`/`HasCycle`. - `Links : Dictionary` where `MotionCommandData.MotionData : Dictionary` — the transition-frame source, read in `GetLink` (both forward and reversed-key branches) and in `PlayAction`'s Action-mask lookup. - `Modifiers : Dictionary` — the overlay/action source for Modifier-mask commands in `PlayAction`, tried with a styled key first then a plain (unstyled) key. - `StyleDefaults : Dictionary` — used only inside `GetLink`'s reversed-direction fallback branch. - **MotionData** (per Cycles/Links/Modifiers entry) — `Anims : List`; `Velocity`/`Omega : Vector3`; `Flags : MotionDataFlags` (`HasVelocity=0x01`, `HasOmega=0x02`) gates whether `Velocity`/`Omega` are applied or zeroed in `EnqueueMotionData` and `PlayAction`. - **AnimData** — `AnimId : QualifiedDataId` (cast to `uint` for `IAnimationLoader.LoadAnimation`), `LowFrame`/`HighFrame` (sentinel `HighFrame == -1` meaning "all frames", resolved in `LoadAnimNode`), `Framerate` (float, multiplied by `speedMod` to produce the `double Framerate` stored on `AnimNode`). - **Animation** (loaded via `IAnimationLoader.LoadAnimation(animId)`) — `PartFrames : List` (drives `numFrames` bounds-clamping in `LoadAnimNode` and the per-part lookup in `BuildBlendedFrame`); `PosFrames : List` (root motion, gated by `Flags.HasFlag(PosFrames)` AND `PosFrames.Count >= numFrames`); `Flags : AnimationFlags`. - **AnimationFrame** — `Frames : List` (one `Frame` per Setup part — indexed by part index in `BuildBlendedFrame`'s `f0Parts`/`f1Parts` lookups, defaulting to identity for parts beyond the frame's list length), `Hooks : List` (read in `ExecuteHooks`). - **Frame** — `Origin : Vector3`, `Orientation : Quaternion` — the raw per-part or per-posframe pose sample, lerped/slerped in `BuildBlendedFrame` or combined in `ApplyPosFrame`. Entry point that ties Setup+MotionTable+Loader together for a live entity: `RenderBootstrap.cs`'s `SequencerFactory(WorldEntity e)` local function (lines ~147-174, moved verbatim from the pre-extraction `GameWindow.cs` ~2306-2334 per its own comment) — looks up `Setup` via `dats.Get(e.SourceGfxObjOrSetupId)`, then `MotionTable` via `dats.Get(setup.DefaultMotionTable)`, falling back through three tiers (real/real → real-setup/empty-mtable → empty/empty) so the constructor's null-guards are never tripped for any spawned entity, even ones missing dat data. ## (c) acdream-invented vs dat-driven — summary classification **Purely dat-driven (faithful mechanical port of retail's node/frame model):** - Node list structure, link resolution forward-path, `GetStartFramePosition`/ `GetEndFramePosition`, `multiply_framerate` semantics (modulo the documented Start/EndFrame-swap simplification), `update_internal`'s frame-boundary walk + hook firing + root-motion accumulation, `advance_to_next_animation` wrap, `execute_hooks` direction matching, the retail slerp, `clear_physics`/`add_motion` replace-semantics for Velocity/Omega, PlayAction's Action/Modifier dual-path lookup. **acdream-invented (no retail citation, or explicitly-flagged deviation/workaround):** 1. **K-fix18 `skipTransitionLink`** — product decision to skip the retail transition-link pose for Falling-on-jump-start; not retail behavior. 2. **Fix B locomotion cyclic→cyclic link-skip** — scoped fix verified via live cdb trace (so it IS retail-faithful for that subset), but the *mechanism* (removing the enqueued link node, forcing `_currNode` onto `_firstCyclic`) is acdream's own structural patch, not a literal port of a retail function. 3. **Stale-head `_currNode` force-relocation** (`preEnqueueTail`/`firstNew` tracking) — acdream bug-fix for a structural mismatch versus retail's apparent behavior; no retail citation for the mechanism itself. 2. **Stop-anim fallback** (direction-agnostic settle-link retry) — acdream workaround for a null-linkData gap. 5. **`GetLink`'s reversed-direction branch** — while individually justified against an *observed* bug (X-key twitch), it's presented as acdream's own two-branch generalization of `get_link`, cross-checked against ACE line ranges rather than a single retail function decompile. 6. **CurrentVelocity/CurrentOmega synthesis for locomotion/turn cycles** (`WalkAnimSpeed=3.12f`, `RunAnimSpeed=4.0f`, `SidestepAnimSpeed=1.25f` constants, turn `±π/2` synthesis) — folds a *separate* retail C++ mechanism (`CMotionInterp::get_state_velocity`, a physics-side function) into the *sequencer's* velocity surface because the Humanoid dat itself carries no baked velocity for these MotionData entries. Functionally retail-faithful in intent but architecturally a deviation (retail keeps this computation outside the Sequence object entirely). 7. **BuildBlendedFrame's non-looping boundary-hold** (issue #61 fix) — an empirically-motivated patch for the link→cycle boundary flash, without a cited retail mechanism describing how the real client avoids this flash. **This is exactly the class of bug the animation-sequencer deep-dive skill was built to root-cause** — the current fix may be masking rather than replicating retail's actual boundary behavior. 8. **`safety = 64` loop cap in `Advance`** — defensive engineering, no retail citation. 9. **All `[SCFAST]/[SCFULL]/[SCNULLFALLBACK]/[CURRNODE]` diagnostics** — acdream-only instrumentation, per-call-site env var reads (flagged by CLAUDE.md itself as a tech-debt pattern that shouldn't be extended further without promotion to a diagnostic-owner class per Code Structure Rule 5). 10. **`AnimationDoneSentinel` as a single shared static instance** — works for "some link just finished" notifications but can't carry per-fire identity; not verified against how retail's `AnimDoneHook` actually carries context. **Unwired / apparently dead API surface found during this pass:** - `ConsumeRootMotionDelta()` — no call site found in `src/` outside the sequencer itself. Root motion (PosFrames) is accumulated every tick but nothing drains it into entity placement. - `Reset()` — no external call site found in the searched consumer set. - `MultiplyCyclicFramerate` — only self-invoked by `SetCycle`'s fast path; no direct external callers. - `HasCurrentNode` — no external call site found beyond its own declaration. ## Cross-references worth pulling into the deep-dive - `docs/research/acclient_animation_pseudocode.md` — cited pseudocode doc for FUN_005267E0/FUN_00525EB0/FUN_00526880/FUN_005268B0/FUN_005360d0/ FUN_005261D0 (sections 5-7). - `docs/research/deepdives/r03-motion-animation.md` section 3 — cited by `AnimationCommandRouter.cs` for the class-mask routing scheme. - `references/ACE/Source/ACE.Server/Physics/Animation/Sequence.cs` and `MotionTable.cs` — the ACE C# port cross-referenced throughout (`Sequence.cs:127-130, 256-260, 262-270, 277-287, 351-443`; `MotionTable.cs:100-101, 132-139, 152-153, 358-370, 372-379, 395-426`). - `references/ACE/Source/ACE.Server/Physics/Animation/MotionInterp.cs:394-428` — `adjust_motion` source. - `docs/research/2026-06-04-animation-sequencer-deep-dive.md` (per MEMORY.md) — prior deep-dive already exists; ranked 8 divergences including "missing pending_motions/MotionDone HIGH" — **this current file does not implement any `pending_motions` concept** (no field named that, no queueing of *future* motions ahead of the current queue beyond the single link+cycle model) — worth checking whether that HIGH-ranked gap from the prior research drop is still open against this current implementation.