# 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.
