acdream/docs/research/2026-07-02-r1-csequence/r1-acdream-sequencer.md
Erik 1371c2a14c feat(R1-P0/P1): CSequence research base + verbatim AnimSequenceNode
P0 — research + pins: full CSequence-family verbatim extraction (1756
lines, per-function raw pseudo-C + cleaned flow, decomp line anchors),
ACE cross-reference (9 ranked divergences; headline: retail frame_number
is x87 long double — ACE's float is the worst case, our double the best
available; ACE's frame-boundary epsilon is an ACE fabrication, NOT
retail), current-sequencer map, and the R1 gap map (20 gaps, 13 keeps,
P0-P6 port order). Pinned the one decomp ambiguity (leftover-time carry
after advance_to_next_animation — ACE reading adopted; cdb confirmation
protocol recorded, non-blocking).

P1 — AnimSequenceNode verbatim (gap G1/G2/G16/G18):
- direction-aware BARE-INT boundary pair (get_starting_frame 0x00525c80 /
  get_ending_frame 0x00525cb0): reverse starts at high+1, ends at low —
  NO epsilon;
- multiply_framerate (0x00525be0) swaps low/high on negative factor;
- set_animation_id (0x00525d60) retail clamp order (high<0 -> num-1;
  low>=num -> num-1; high>=num -> num-1; low>high -> high=low);
- ctors with retail defaults (30f/-1/-1; AnimData copy + clamp);
- get_pos_frame null out-of-range (retail; ACE returns identity),
  floor double overload; get_part_frame same discipline;
- NO per-node IsLooping/Velocity/Omega — loop membership is list
  structure, physics accumulators live on the sequence (G16).

22 conformance tests (clamp table, boundary mirror table, swap
round-trip, bounds/floor semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:45:56 +02:00

36 KiB
Raw Blame History

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<Animation>(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<AnimNode> _queue, _currNode, _firstCyclic pointers Doubly-linked list mirrored with .NET LinkedList<T>. 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)
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=64acdream-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<PartTransform> 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<AnimationHook> 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 IAnimationHookSinks).
  • (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::UpdatePositionManager::adjust_offsetFrame::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<Animation>), 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=0x20000000Modifier; ActionMask=0x10000000Action; SubStateMask=0x40000000SubState; 0None; 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/ChatEmotesequencer.PlayAction(fullCommand, speedMod); SubStatesequencer.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<int, MotionData> keyed by (style<<16)|(adjustedMotion&0xFFFFFF) — the looping tail source, read in SetCycle/HasCycle.
    • Links : Dictionary<int, MotionCommandData> where MotionCommandData.MotionData : Dictionary<int, MotionData> — the transition-frame source, read in GetLink (both forward and reversed-key branches) and in PlayAction's Action-mask lookup.
    • Modifiers : Dictionary<int, MotionData> — the overlay/action source for Modifier-mask commands in PlayAction, tried with a styled key first then a plain (unstyled) key.
    • StyleDefaults : Dictionary<MotionCommand, MotionCommand> — used only inside GetLink's reversed-direction fallback branch.
  • MotionData (per Cycles/Links/Modifiers entry) — Anims : List<AnimData>; Velocity/Omega : Vector3; Flags : MotionDataFlags (HasVelocity=0x01, HasOmega=0x02) gates whether Velocity/Omega are applied or zeroed in EnqueueMotionData and PlayAction.
  • AnimDataAnimId : QualifiedDataId<Animation> (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<AnimationFrame> (drives numFrames bounds-clamping in LoadAnimNode and the per-part lookup in BuildBlendedFrame); PosFrames : List<Frame> (root motion, gated by Flags.HasFlag(PosFrames) AND PosFrames.Count >= numFrames); Flags : AnimationFlags.
  • AnimationFrameFrames : List<Frame> (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<AnimationHook> (read in ExecuteHooks).
  • FrameOrigin : 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<Setup>(e.SourceGfxObjOrSetupId), then MotionTable via dats.Get<MotionTable>(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.
  4. 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-428adjust_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.