# Animation Pipeline Audit — 2026-04-21 Decompile-first audit of acdream's animation pipeline against retail. Previous audit on this topic relied on ACE and got wiring claims wrong; this one cites `chunk_*.c:LINE FUN_XXXXXXXX` for every claim and cross-references our code line-for-line. ## Bottom line **Our animation pipeline is complete and faithful to retail for every packet-driven path verified.** The agent flagged one "gap" (hit-react autonomous trigger) that turned out to be a misinterpretation of `FUN_0048d760` — the user confirmed retail does not play a body animation on hit, so whatever `FUN_0048d760` does, it isn't animation. Most likely on-hit particle / damage-number / sound effect — that's a separate "combat feedback" feature, not an animation gap. ## Section A — PerformMovement dispatcher **Retail source**: `chunk_00520000.c:7627 FUN_00529a90` (PerformMovement) ```c switch(*param_1) { case 1: FUN_00529930(...); // DoMotion case 2: FUN_00528f70(...); // DoInterpretedMotion case 3: FUN_00529140(...); // StopMotion case 4: FUN_00529080(...); // StopInterpretedMotion case 5: FUN_00528a50(); // StopCompletely } ``` **Our code**: `src/AcDream.Core/Physics/MotionInterpreter.cs:10-99` `MovementType` enum (RawCommand / InterpretedCommand / StopRawCommand / StopInterpretedCommand / StopCompletely) mirrors the 5 cases exactly. `GameWindow.OnLiveMotionUpdated` routes inbound motion through `DoInterpretedMotion`. **Verdict**: MATCHES. ## Section B — Core animation dispatcher (apply_current_movement) **Retail source**: `chunk_00520000.c:7134 FUN_00529210` Resolves the InterpretedMotionState into an active animation cycle. Priority: forward if valid substate, else sidestep, else turn, else `0x40000015 Falling` as the default fallback. **Our code**: `src/AcDream.App/Rendering/GameWindow.cs:1758-1832` (OnLiveMotionUpdated sequencer path) Same priority: WalkForward/RunForward/WalkBackward → sidestep → turn → Ready. Uses `MotionCommandResolver.ReconstructFullCommand` to lift the wire's 16-bit command to the full 32-bit MotionCommand. Calls `AnimationSequencer.SetCycle` with the resolved cycle. **Verdict**: MATCHES semantically. Different syntax (we check low-byte vs. retail's `is_motion_substate`) but equivalent output. ## Section C — Commands list (one-shot emotes / attacks / actions) **Retail source**: `chunk_00510000.c:13957 FUN_0051F260` — bulk field copy for `InterpretedMotionState`, consumed downstream into the animation sequencer's action queue. **Our code**: `src/AcDream.App/Rendering/GameWindow.cs:1879-1917` ```csharp foreach (var item in cmds) { uint fullCmd = MotionCommandResolver.ReconstructFullCommand(item.Command); uint cls = fullCmd & 0xFF000000u; if ((cls & 0x10000000u) != 0 || (cls & 0x20000000u) != 0 || cls == 0x12000000u || cls == 0x13000000u) { ae.Sequencer.PlayAction(fullCmd, item.Speed); } else if ((cls & 0x40000000u) != 0) { ae.Sequencer.SetCycle(fullStyle, fullCmd, item.Speed); } } ``` `AnimationSequencer.PlayAction` (lines 744+) resolves via Links dict for Action-class, Modifiers dict for Modifier-class, inserts the resulting nodes before `_firstCyclic` so the one-shot plays before returning to the active cycle. Cursor-on-cyclic rewind logic keeps actions from being delayed behind a cycle wrap. **Verdict**: MATCHES. Previous audit's claim that this path is "orphaned" was wrong. ## Section D — Hit-react on damage (claimed gap, not real) **Agent claim**: `chunk_00480000.c:8202 FUN_0048d760` calls vtable `+0x9c` with `0x10000055` on damage → "retail triggers StaggerBackward locally". **Verification of the claim**: ```c piVar2 = (int *)FUN_00463c00(0x1000051a); // table lookup if (piVar2 != 0) { piVar2 = (**(code **)(*piVar2 + 0x94))(0xc); // vtable +0x94 if (piVar2 != 0) { FUN_0046a740(param_1); // setup FUN_00460530(0x10000085, param_1); // register ID on entity if (param_3 != 0) { (**(code **)(*piVar2 + 0x9c))(0x10000054); return 1; } (**(code **)(*piVar2 + 0x9c))(0x10000055); return 1; } } ``` **Why the claim is unverified**: 1. `0x10000054` and `0x10000055` are Action-class-*shaped* 32-bit IDs. The agent assumed they're `MotionCommand.StaggerForward/StaggerBackward` because ACE's enum labels them that way. That's ACE's labeling, not retail's — and ACE's enum is itself partly guesswork for values not in shipping motion tables. 2. Vtable slot `+0x9c` was never identified. Could be play-animation; could just as easily be emit-effect, play-sound, set-state, notify-listeners, register-target. The decompile name is unknown. 3. `FUN_00463c00(0x1000051a)` is a table lookup by 32-bit key — again not necessarily the motion-table. Could be a particle-effect table, sound-effect table, or generic resource table. 4. **User confirms retail shows NO body animation on hit.** If `FUN_0048d760` plays animation, this contradicts the observation. Therefore it plays something else — most likely on-hit particle, damage flash, sound, or damage-number UI spawn. **Verdict**: Agent's claim is WRONG. `FUN_0048d760` does something on damage — likely non-animation (particles / feedback UI) — but it is NOT an animation gap in our port. ## Section E — Jump / Falling / Death substates **Retail source**: defined in `MotionCommand` enum (per `docs/research/deepdives/r03-motion-animation.md §3.2`): - `Jumpup = 0x1000004B` (Action — not used by humanoid player) - `FallDown = 0x10000050` (Action — same story) - `Falling = 0x40000015` (SubState — the airborne cycle) - `Dead = 0x40000011` (SubState) - `Jump = 0x2500003B` (Modifier — jump root-motion overlay) `FUN_00529210` auto-falls back to Falling when `is_motion_substate` rejects the current command. **Our code**: `src/AcDream.Core/Physics/MotionInterpreter.cs:30-81` defines the same constants. `SetCycle` path in OnLiveMotionUpdated handles SubState routing. **Verdict**: MATCHES. ## Section F — Frame timing + playback quirks **Retail source**: `_DAT_007c92b4 = 1e-5` used in `FUN_00526880 / FUN_005268B0` (frame-position helpers) to place the cursor "just before" a frame boundary. **Our code**: `src/AcDream.Core/Physics/AnimationSequencer.cs:171` `FrameEpsilon = 1e-5`. Same value; same logic in `GetStartFramePosition / GetEndFramePosition` including negative-speed start/end swap. **Verdict**: MATCHES. ## Overall summary ### Verified correct (matches retail per decompile) - `PerformMovement` 5-case dispatcher (RawCommand / InterpretedCommand / StopRawCommand / StopInterpretedCommand / StopCompletely). - `DoInterpretedMotion` routing. - `apply_current_movement` cycle-selection priority (forward → sidestep → turn → Ready, with Falling as invalid-substate fallback). - Commands list → PlayAction (Link/Modifier lookup, insert-before-cyclic). - Class-byte reconstruction from 16-bit wire commands. - SubState routing for Falling / Jump / Dead. - Frame-timing epsilon and negative-speed playback. ### Verified divergent - None found. ### Verified missing - None found (agent's hit-react claim does not survive scrutiny). ### Couldn't pin down - **`FUN_0048d760` purpose**: called from damage-application code, invokes `vtable +0x9c` with `0x10000054 / 0x10000055`. Probably on-hit feedback (particle, sound, damage number). Confirming its exact purpose requires identifying the vtable class — not done in this audit. Not an animation pipeline concern. ### Open follow-ups (not gaps) - **On-hit feedback**: whatever `FUN_0048d760` really does, retail clients get some visible feedback on damage (damage numbers, particles) that we don't have. Particle infrastructure is partly scaffolded (session notes mention E.3 data layer) but not wired to GL. Separate feature, medium-scope project. - **`CreateObject` initial Commands list**: parsed by `CreateObject.cs:619` but not replayed when the entity spawns. Minor edge case — rare that an entity is spawned mid-action. Flag but don't prioritize. ## Conclusion Animation pipeline for acdream is **complete and retail-faithful** as of this session. The historical "whack-a-mole animation bugs" frustration was genuine bugs in previous sessions (remote-motion wire-format, MovementStateFlag bit layout, stop-detection via absent ForwardCommand, etc.) — all fixed in commits before 2026-04-21. No further animation work is required at the pipeline level; next visible-animation improvements are particles / combat feedback (separate feature) or specific bug reports with a reproduction.