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>
36 KiB
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 realDatCollection.DatCollectionLoader : IAnimationLoader— production impl, wrapsDatCollection.Get<Animation>(id).PartTransform(readonly struct) —Vector3 Origin,Quaternion Orientation. Output unit ofAdvance.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=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 callingSetCycleto avoid the "torso on ground" missing-cycle collapse. Called fromGameWindow.cs:3723/3728/3732/3824(initial spawn cycle selection with Run→Walk→Ready fallback chain) andRemoteMotionSink.Commit()(same fallback pattern for remote UM-driven cycles, plus anACDREAM_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 withskipTransitionLink: true(K-fix18).GameWindow.cs:4936(ApplyServerControlledVelocityCycle) — NPC/monster remotes still on the legacyServerControlledLocomotion.PlanFromVelocitypath (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 ownSetCyclecall (with sidestep speed-scaling correction factorWalkAnimSpeed/SidestepAnimSpeed*0.5documented against ACE'sMovementData.cs:124-131wire formula, #45).RemoteMotionSink.Commit()(src/AcDream.App/Rendering/RemoteMotionSink.cs:215) — remote player entities via the L.2g S2bCMotionInterpfunnel path (see below); this is the modern replacement for the legacy path above for player remotes specifically.AnimationCommandRouter.RouteFullCommand— routesSubState-class commands here (see router section below).
void MultiplyCyclicFramerate(float factor)— not called directly outsideAnimationSequencer.csin the searched consumer set (invoked internally bySetCycle's fast-path re-speed branch); no external call sites found insrc/.IReadOnlyList<PartTransform> Advance(float dt)— per-frame tick. Sole call site:GameWindow.cs:9876inside the animated-entity tick loop (seqFrames = ae.Sequencer.Advance(dt)), guarded by anae.Sequencer is not nullbranch; entities without a sequencer fall to a "legacy path" (ae.CurrFrame += dt * ae.Frameratemanual slerp, line ~9896).IReadOnlyList<AnimationHook> ConsumePendingHooks()— drained immediately afterAdvanceatGameWindow.cs:9882, fanned out per-hook to_hookRouter.OnHook(ae.Entity.Id, worldPos, hook)(AnimationHookRouter, which further fans out to registeredIAnimationHookSinks).(Vector3 Position, Quaternion Rotation) ConsumeRootMotionDelta()— no call sites found in the searchedsrc/consumer grep; root motion (PosFrames) accumulation exists in the sequencer but nothing currently drains/consumes it in production code (search ofConsumeRootMotionDeltaand\.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 fromAnimationCommandRouter.RouteFullCommandforAction/Modifier/ChatEmoteroute kinds, and directly fromRemoteMotionSink.ApplyMotionfor 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 byGameWindow.cs(style fallback defaults0x8000003DuNonCombat at lines 3723/4827/4919 etc.),RemoteMotionSinkctor (seeds_stylefromsequencer.CurrentStyle),RemoteMotionSink.Commit()(_sequencer.CurrentMotion/CurrentSpeedModfor diagnostic comparison),GameWindow.cs:4915(ApplyServerControlledVelocityCycle'sIsRemoteLocomotion(currentMotion)gate).CurrentVelocity/CurrentOmega(Vector3, private-set) — the most cross-cutting output surface:GameWindow.cs:9331-9334— remote player per-tick body translation (seqVel/seqOmegafeedPositionManager-style catch-up/anim composition; extensive comment block on lines 9310-9330 walks through retail'sCPartArray::Update→PositionManager::adjust_offset→Frame::combineper-tick pipeline this is meant to mirror).PlayerMovementController.AttachCycleVelocityAccessor(() => playerSeq.CurrentVelocity)(GameWindow.cs:12917) — wires the local player's sequencer velocity intoMotionInterpreter.GetCycleVelocityas an override of the decompiled constant path (RunAnimSpeed * ForwardSpeed), used because arbitrary creatures' MotionTables don't all bakeVelocity=4.0on RunForward the way Humanoid does.MotionInterpreter.cs— comments describeCurrentVelocityas "alreadyMotionData.Velocity * speedMod" (body-local) and reference it as the accessor payload described above; also cross-checksAnimationSequencer.CurrentVelocity/CurrentOmegadoc pointers at lines 347/376/632/662.
QueueCount(int) — diagnostic only (CurrentNodeDiagtuple'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 byGameWindow.cs:9863-9871inside anACDREAM_REMOTE_VEL_DIAG-gated[CURRNODE]log block that comparesAnimRefHashagainstFirstCyclicAnimRefHashto 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 byDatCollectionLoader(wrapsDatCollection.Get<Animation>), constructed once inRenderBootstrap.cs:138(animLoader) and passed by reference into everySequencerFactory-built sequencer; aNullAnimLoader(referenced but not shown in this file — defined elsewhere) is the last-resort fallback inRenderBootstrap'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/0x13000000class →ChatEmote;ModifierMask=0x20000000→Modifier;ActionMask=0x10000000→Action;SubStateMask=0x40000000→SubState;0→None; elseIgnored. (Note: checks Modifier before Action, meaning any command with both bits set classifies as Modifier — matches thePlayActioncomment about Jump 0x2500003B having both bits but ACE treating it as Modifier.)RouteWireCommand(sequencer, currentStyle, ushort wireCommand, speedMod)— reconstructs the full 32-bit command viaMotionCommandResolver.ReconstructFullCommand(wireCommand)then delegates toRouteFullCommand.RouteFullCommand(sequencer, currentStyle, fullCommand, speedMod)— dispatch table:Action/Modifier/ChatEmote→sequencer.PlayAction(fullCommand, speedMod);SubState→sequencer.SetCycle(currentStyle, fullCommand, speedMod). Returns the classifiedAnimationCommandRouteKindto 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.AnimationHookand 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 fromDatReaderWriter.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 + aNullAnimationHookSinkno-op singleton for tests/headless.AnimationHookRouter : IAnimationHookSink— composite/fan-out implementation.Register(sink)/Unregister(sink)are lock-protected (_gate), copy-on-write into aIAnimationHookSink[]array (idempotent register — checksReferenceEqualsbefore adding).OnHookiterates a snapshot array without locking (explicitly commented "no lock in the hot path (render thread)") and wraps each sink'sOnHookcall in atry/catchthat 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'sfeedback_logger_injection_for_silent_catches.mdmemory note: libs that silently catch+return null usually should inject anILogger; this router has no logger injection point at all).Sinksproperty 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) — suppliesParts.Count(used byAdvance/BuildBlendedFrame/BuildIdentityFrameto size thePartTransform[]output) andDefaultMotionTableid (used byRenderBootstrap.SequencerFactoryto look up the pairedMotionTable). - MotionTable (
DatReaderWriter.DBObjs.MotionTable) — three dictionaries consumed directly:Cycles : Dictionary<int, MotionData>keyed by(style<<16)|(adjustedMotion&0xFFFFFF)— the looping tail source, read inSetCycle/HasCycle.Links : Dictionary<int, MotionCommandData>whereMotionCommandData.MotionData : Dictionary<int, MotionData>— the transition-frame source, read inGetLink(both forward and reversed-key branches) and inPlayAction's Action-mask lookup.Modifiers : Dictionary<int, MotionData>— the overlay/action source for Modifier-mask commands inPlayAction, tried with a styled key first then a plain (unstyled) key.StyleDefaults : Dictionary<MotionCommand, MotionCommand>— used only insideGetLink's reversed-direction fallback branch.
- MotionData (per Cycles/Links/Modifiers entry) —
Anims : List<AnimData>;Velocity/Omega : Vector3;Flags : MotionDataFlags(HasVelocity=0x01,HasOmega=0x02) gates whetherVelocity/Omegaare applied or zeroed inEnqueueMotionDataandPlayAction. - AnimData —
AnimId : QualifiedDataId<Animation>(cast touintforIAnimationLoader.LoadAnimation),LowFrame/HighFrame(sentinelHighFrame == -1meaning "all frames", resolved inLoadAnimNode),Framerate(float, multiplied byspeedModto produce thedouble Frameratestored onAnimNode). - Animation (loaded via
IAnimationLoader.LoadAnimation(animId)) —PartFrames : List<AnimationFrame>(drivesnumFramesbounds-clamping inLoadAnimNodeand the per-part lookup inBuildBlendedFrame);PosFrames : List<Frame>(root motion, gated byFlags.HasFlag(PosFrames)ANDPosFrames.Count >= numFrames);Flags : AnimationFlags. - AnimationFrame —
Frames : List<Frame>(oneFrameper Setup part — indexed by part index inBuildBlendedFrame'sf0Parts/f1Partslookups, defaulting to identity for parts beyond the frame's list length),Hooks : List<AnimationHook>(read inExecuteHooks). - Frame —
Origin : Vector3,Orientation : Quaternion— the raw per-part or per-posframe pose sample, lerped/slerped inBuildBlendedFrameor combined inApplyPosFrame.
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_frameratesemantics (modulo the documented Start/EndFrame-swap simplification),update_internal's frame-boundary walk + hook firing + root-motion accumulation,advance_to_next_animationwrap,execute_hooksdirection matching, the retail slerp,clear_physics/add_motionreplace-semantics for Velocity/Omega, PlayAction's Action/Modifier dual-path lookup.
acdream-invented (no retail citation, or explicitly-flagged deviation/workaround):
- K-fix18
skipTransitionLink— product decision to skip the retail transition-link pose for Falling-on-jump-start; not retail behavior. - 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
_currNodeonto_firstCyclic) is acdream's own structural patch, not a literal port of a retail function. - Stale-head
_currNodeforce-relocation (preEnqueueTail/firstNewtracking) — acdream bug-fix for a structural mismatch versus retail's apparent behavior; no retail citation for the mechanism itself. - Stop-anim fallback (direction-agnostic settle-link retry) — acdream workaround for a null-linkData gap.
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 ofget_link, cross-checked against ACE line ranges rather than a single retail function decompile.- CurrentVelocity/CurrentOmega synthesis for locomotion/turn cycles
(
WalkAnimSpeed=3.12f,RunAnimSpeed=4.0f,SidestepAnimSpeed=1.25fconstants, turn±π/2synthesis) — 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). - 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.
safety = 64loop cap inAdvance— defensive engineering, no retail citation.- 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). AnimationDoneSentinelas a single shared static instance — works for "some link just finished" notifications but can't carry per-fire identity; not verified against how retail'sAnimDoneHookactually carries context.
Unwired / apparently dead API surface found during this pass:
ConsumeRootMotionDelta()— no call site found insrc/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 bySetCycle'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.mdsection 3 — cited byAnimationCommandRouter.csfor the class-mask routing scheme.references/ACE/Source/ACE.Server/Physics/Animation/Sequence.csandMotionTable.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_motionsource.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 anypending_motionsconcept (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.