Ground-truth audit of acdream's animation pipeline against retail decompile (chunk_*.c), cross-referenced line-by-line with our code. Previous audit relied on ACE and got wiring claims wrong (said our PlayAction path was orphaned when it's wired via OnLiveMotionUpdated). Findings: - PerformMovement dispatcher (FUN_00529a90) matches our MotionInterpreter. - apply_current_movement cycle priority (FUN_00529210) matches our OnLiveMotionUpdated sequencer path. - Commands list → PlayAction wiring matches retail. - Falling / Jump / Dead substate routing matches. - Frame-timing epsilon + negative-speed playback matches. The agent's "hit-react missing" claim turned out to be wrong: the referenced FUN_0048d760 call passes 32-bit IDs shaped like MotionCommand values but user-confirmed retail shows NO body animation on damage, so vtable +0x9c is almost certainly emit-effect / play-sound / spawn- particle — not a motion play. Not an animation gap. Open follow-up: CreateObject initial Commands list is parsed but not replayed when the entity hydrates (minor; rare case). Not a follow-up: on-hit combat feedback (particles, damage numbers). That's a separate feature, not an animation pipeline concern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.5 KiB
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)
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
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:
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:
0x10000054and0x10000055are Action-class-shaped 32-bit IDs. The agent assumed they'reMotionCommand.StaggerForward/StaggerBackwardbecause 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.- Vtable slot
+0x9cwas 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. 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.- User confirms retail shows NO body animation on hit. If
FUN_0048d760plays 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)
PerformMovement5-case dispatcher (RawCommand / InterpretedCommand / StopRawCommand / StopInterpretedCommand / StopCompletely).DoInterpretedMotionrouting.apply_current_movementcycle-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_0048d760purpose: called from damage-application code, invokesvtable +0x9cwith0x10000054 / 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_0048d760really 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. -
CreateObjectinitial Commands list: parsed byCreateObject.cs:619but 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.