docs(research): animation-pipeline decompile audit — no real gaps
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>
This commit is contained in:
parent
f562215e6c
commit
7007758293
1 changed files with 220 additions and 0 deletions
220
docs/research/2026-04-21-animation-audit.md
Normal file
220
docs/research/2026-04-21-animation-audit.md
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
# 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.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue