From 7007758293ed2504e0e7bdbdbad869cfe3ef9cdf Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Apr 2026 21:18:45 +0200 Subject: [PATCH] =?UTF-8?q?docs(research):=20animation-pipeline=20decompil?= =?UTF-8?q?e=20audit=20=E2=80=94=20no=20real=20gaps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/research/2026-04-21-animation-audit.md | 220 ++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 docs/research/2026-04-21-animation-audit.md diff --git a/docs/research/2026-04-21-animation-audit.md b/docs/research/2026-04-21-animation-audit.md new file mode 100644 index 0000000..b69b767 --- /dev/null +++ b/docs/research/2026-04-21-animation-audit.md @@ -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.