713 lines
48 KiB
Markdown
713 lines
48 KiB
Markdown
# Animation system parity audit — retail vs acdream (2026-07-30)
|
||
|
||
**Status: COMPLETE — report-only investigation, no code changes made.**
|
||
|
||
Scope: `CMotionInterp` / `CSequence` / `MotionTableManager` / `CMotionTable`
|
||
retail method surfaces vs acdream's `src/AcDream.Core/Physics/MotionInterpreter.cs`,
|
||
`src/AcDream.Core/Physics/Motion/*`, `src/AcDream.Core/Physics/AnimationSequencer.cs`,
|
||
`src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs`.
|
||
|
||
**Bottom line:** the core animation-selection/playback stack
|
||
(`MotionInterpreter`/`CSequence`/`CMotionTable`/`MotionTableManager`) is a
|
||
faithful, heavily retail-cited port. All 103 symbols.json-listed retail
|
||
methods across the four classes were enumerated (section 1); only one is a
|
||
genuine unexplained gap (`get_adjusted_max_speed`) and the rest of the
|
||
"unported" set is verified dead code in retail itself. All 7 traced
|
||
feel-visible flows (section 2) came back parity. The real, actionable gaps
|
||
are narrow and listed in section 6's ranked catalog: an animated-emote
|
||
authoring gap (AD-57), two already-known hook-timing residuals (TS-50/
|
||
TS-51) precisely rescoped against current code, one likely-already-fixed
|
||
issue (#64) needing only live re-verification, and two untraced
|
||
combat/casting-layer questions flagged for a future audit outside this
|
||
scope.
|
||
|
||
## Note on the requested prior deep-dive doc
|
||
|
||
The task referenced `docs/research/2026-06-04-animation-sequencer-deep-dive.md`
|
||
as prior art. That file does not exist in this worktree or anywhere in git
|
||
history (`git log --all --diff-filter=A -- "*animation-sequencer-deep-dive*"`
|
||
returns nothing). `claude-memory/MEMORY.md` indexes it, and a same-named
|
||
**skill** (`acdream-animation-sequencer-deep-dive`) exists that would
|
||
presumably generate such a doc, but no prior run's output is present at that
|
||
path or any other. The closest prior-art documents actually in the repo are:
|
||
- `docs/research/2026-06-26-movement-animation-retail-parity-audit.md` (D1-D12
|
||
divergence list, dated before the R6/J-slice root-motion work — most of its
|
||
wire-format findings, D1/D3/D4/D9, have likely since been superseded by
|
||
TS-33/TS-47 and the R6 root-motion campaign; treated as historical baseline
|
||
only, re-verified against current code below, not trusted at face value)
|
||
- `docs/research/acclient_animation_map.md`, `docs/research/acclient_animation_pseudocode.md`
|
||
- `docs/research/2026-04-21-animation-audit.md`, `docs/research/2026-04-28-combat-animation-planner.md`
|
||
- `docs/research/2026-07-02-r1-csequence/r1-acdream-sequencer.md`
|
||
- `docs/research/2026-07-02-inbound-motion-verbatim-port-handoff.md`
|
||
|
||
This audit proceeds using those plus the R6 sections of
|
||
`claude-memory/project_physics_collision_digest.md` (which describes what R6
|
||
already shipped — not re-audited here per task instructions) and a fresh
|
||
grep sweep of the named retail decomp.
|
||
|
||
---
|
||
|
||
## 1. Method-coverage sweep
|
||
|
||
Two independent passes fed this section: a dedicated method-coverage-sweep
|
||
sub-agent did a symbol-by-symbol enumeration against `symbols.json` +
|
||
pseudo-C call-site tracing, and the lead auditor separately read essentially
|
||
the entirety of all four files directly (`MotionInterpreter.cs` ~2,600 of
|
||
3,182 lines read in full, `CSequence.cs`/`CMotionTable.cs`/`MotionState.cs`/
|
||
`AnimationSequencer.cs` read in full) and independently confirmed the
|
||
sub-agent's headline finding (`GetMaxSpeed()` is the only max-speed accessor
|
||
anywhere in the App/Runtime call sites — `grep` for `AdjustedMaxSpeed`/
|
||
`get_adjusted_max_speed` across `src/` returns zero hits outside the doc
|
||
comment that names it as unported). The two passes agree; findings below are
|
||
merged, with the sub-agent's table format preserved since it's more scannable
|
||
than prose.
|
||
|
||
**Methodology note (symbol-artifact class):** `symbols.json` occasionally
|
||
attributes a class method name to an address whose pseudo-C body is a
|
||
different, unrelated function, or lists two names at the identical address
|
||
(most likely `/OPT:ICF` identical-code-folding at link time collapsing
|
||
byte-identical trivial bodies, with the PDB keeping multiple aliases for one
|
||
surviving address). Confirmed instances: `CMotionInterp::HandleEnterWorld`
|
||
@ `0x00694750` resolves to `IDClass<>::~IDClass` (unrelated template
|
||
destructor); `CMotionInterp::InqStyle` @ `0x00527B10` resolves to
|
||
`CBaseFilter::GetPinVersion` (unrelated DirectShow class); `MotionTableManager::RemoveLinkAnimations`
|
||
and `HandleEnterWorld` are both listed at `0x0051BDD0` (only one body exists
|
||
there); `CMotionTable::Allocator`/`Allocate` are both listed at `0x004F96E0`.
|
||
These are marked **SYMBOL-ARTIFACT** below rather than forced into
|
||
ported/unported, since the decomp genuinely cannot answer what (if anything)
|
||
that distinct method does.
|
||
|
||
Also confirmed independently on both classes that have a `Pack`/`UnPack`
|
||
family (`CSequence`, `CMotionTable`): these `PackObj`/`DBObj` serialization
|
||
methods have **zero call sites anywhere in the 1.4M-line pseudo-C** outside
|
||
their own bodies and a `.rdata` vtable-slot registration — dead code
|
||
inherited from a shared server/client engine base, never invoked by the
|
||
retail client itself. Their absence in acdream is correctly not a gap.
|
||
|
||
### CMotionInterp (41 symbols.json entries)
|
||
|
||
| Retail method (addr) | acdream status | Cite |
|
||
|---|---|---|
|
||
| PerformMovement (0x00528E80) | ported-with-cite | `MotionInterpreter.cs:820` |
|
||
| DoMotion (0x00528D20) | ported-with-cite | `MotionInterpreter.cs:880,932` |
|
||
| StopMotion (0x00528530) | ported-with-cite | `MotionInterpreter.cs:982,1009` |
|
||
| StopCompletely (0x00527E40) | ported-with-cite | `MotionInterpreter.cs:1078` |
|
||
| get_state_velocity (0x00527D50) | ported-with-cite | `MotionInterpreter.cs:1188` |
|
||
| adjust_motion (0x00528010) | ported-with-cite | `MotionInterpreter.cs:1290` |
|
||
| apply_run_to_command (0x00527BE0) | ported-with-cite | `MotionInterpreter.cs:1355` |
|
||
| apply_raw_movement (0x005287E0) | ported-with-cite | `MotionInterpreter.cs:1398,1498` |
|
||
| apply_current_movement (0x00528870) | ported-with-cite | `MotionInterpreter.cs:1459` |
|
||
| ReportExhaustion (0x005288D0) | ported-with-cite | `MotionInterpreter.cs:1619` |
|
||
| SetWeenieObject (0x00528920) | ported-with-cite | `MotionInterpreter.cs:1671` |
|
||
| SetPhysicsObject (0x00528970) | ported-with-cite | `MotionInterpreter.cs:1721` |
|
||
| jump_charge_is_allowed (0x00527A50) | ported-with-cite | `MotionInterpreter.cs:1761` |
|
||
| charge_jump (0x005281C0) | ported-with-cite | `MotionInterpreter.cs:1826` |
|
||
| jump (0x00528780) | ported-with-cite | `MotionInterpreter.cs:1883` |
|
||
| get_jump_v_z (0x00527AA0) | ported-with-cite | `MotionInterpreter.cs:1920` |
|
||
| get_leave_ground_velocity (0x005280C0) | ported-with-cite | `MotionInterpreter.cs:1958` |
|
||
| jump_is_allowed (0x005282B0) | ported-with-cite | `MotionInterpreter.cs:2026,2051` |
|
||
| contact_allows_move (0x00528240) | ported-with-cite | `MotionInterpreter.cs:2123` |
|
||
| add_to_queue (0x00527B80) | ported-with-cite | `MotionInterpreter.cs:2164` (`AddToQueue`) |
|
||
| motions_pending (0x00527FE0) | ported-with-cite | `MotionInterpreter.cs:2173` |
|
||
| MotionDone (0x00527EC0) | ported-with-cite | `MotionInterpreter.cs:2193` |
|
||
| HandleExitWorld (0x00527F30) | ported-with-cite | `MotionInterpreter.cs:2245` |
|
||
| is_standing_still (0x00527FA0) | ported-with-cite | `MotionInterpreter.cs:2266` |
|
||
| motion_allows_jump (0x005279E0) | ported-with-cite | `MotionInterpreter.cs:2310` |
|
||
| LeaveGround (0x00528B00) | ported-with-cite | `MotionInterpreter.cs:2373` (independently read in full) |
|
||
| HitGround (0x00528AC0) | ported-with-cite | `MotionInterpreter.cs:2425` (independently read in full) |
|
||
| enter_default_state (0x00528C80) | ported-with-cite | `MotionInterpreter.cs:2483` |
|
||
| set_hold_run (0x00528B70) | ported-with-cite | `MotionInterpreter.cs:2521` |
|
||
| SetHoldKey (0x00528BB0) | ported-with-cite | `MotionInterpreter.cs:2575` |
|
||
| get_max_speed (0x00527CB0) | ported-with-cite | `MotionInterpreter.cs:2632` (`GetMaxSpeed`; doc comment includes a forensic re-derivation of the ×4.0 constant from raw x87 disassembly, UN-2 resolved) |
|
||
| **get_adjusted_max_speed (0x00527D00)** | **UNPORTED-UNEXPLAINED** | None found — independently confirmed (see intro above). Sibling of `get_max_speed`; retail `InterpolationManager::adjust_offset` (pc:353107, `0x00555dbe`) chooses between the two via a static toggle `fUseAdjustedSpeed_`. acdream's dead-reckoning catch-up path (`RemoteMotionCombiner.cs:90`, `RuntimeRemotePhysicsUpdater.cs:254,291,685`) only ever calls `GetMaxSpeed()`. No register/ISSUES row covers this. |
|
||
| move_to_interpreted_state (0x005289C0) | ported-with-cite | `MotionInterpreter.cs:2697` |
|
||
| apply_interpreted_movement (0x00528600) | ported-with-cite | `MotionInterpreter.cs:2797` |
|
||
| DoInterpretedMotion (0x00528360) | ported-with-cite | `MotionInterpreter.cs:2931,2944` |
|
||
| StopInterpretedMotion (0x00528470) | ported-with-cite | `MotionInterpreter.cs:3093,3103` |
|
||
| Create (0x00528C00) | ported, uncited | `MotionInterpreter.cs:795` — retail's `Create` calls `SetWeenieObject`/`SetPhysicsObject` while `initted==0`, making both no-ops; plain field assignment is behaviorally identical |
|
||
| Destroy / ~CMotionInterp (0x00527B40 / 0x00527FF0) | trivial, skipped | GC-obviated (manual `pending_motions` free-list walk superseded by `LinkedList<T>`) |
|
||
| HandleEnterWorld (0x00694750) | SYMBOL-ARTIFACT | see methodology note |
|
||
| InqStyle (0x00527B10) | SYMBOL-ARTIFACT | see methodology note |
|
||
|
||
**~85% ported-with-cite** (35/41; ~95% of the 37 real/substantive methods
|
||
after excluding 2 symbol-artifacts and 2 GC-obviated dtors). One genuine gap.
|
||
|
||
### CSequence (28 symbols.json entries)
|
||
|
||
| Retail method (addr) | acdream status | Cite |
|
||
|---|---|---|
|
||
| ctor (0x005249F0) | ported, trivial | `CSequence.cs:81` (zero-init, matched by C# field defaults) |
|
||
| set_object (0x00524820) | SYMBOL-ARTIFACT, functionally ported | address really resolves to `DBObj::SetDID`; behavioral equivalent is the public `HookObj` field, `CSequence.cs:300` |
|
||
| set_velocity / set_omega (0x00524880 / 0x005248A0) | ported-with-cite | `CSequence.cs:236-237` |
|
||
| execute_hooks (0x00524830) | ported-with-cite | `CSequence.cs:503` |
|
||
| combine_physics / subtract_physics (0x005248C0 / 0x00524900) | ported-with-cite | `CSequence.cs:238-239` |
|
||
| multiply_cyclic_animation_fr (0x00524940) | ported-with-cite | `CSequence.cs:249` |
|
||
| get_curr_animframe (0x00524970) | ported-with-cite | `CSequence.cs:266` (independently read) |
|
||
| set_placement_frame (0x005249B0) | ported-with-cite | `CSequence.cs:258` |
|
||
| get_curr_frame_number (0x005249D0) | ported-with-cite | `CSequence.cs:274` |
|
||
| apply_physics (0x00524AB0) | ported-with-cite | `CSequence.cs:285` (independently read) |
|
||
| apricot (0x00524B40) | ported-with-cite | `CSequence.cs:219` (retail's own PDB-verified name, kept verbatim) |
|
||
| has_anims (0x00524BD0) | ported-with-cite | `CSequence.cs:90` |
|
||
| remove_link_animations (0x00524BE0) | ported-with-cite | `CSequence.cs:187` |
|
||
| remove_all_link_animations (0x00524CA0) | ported-with-cite | `CSequence.cs:207` |
|
||
| clear_physics / clear_animations (0x00524D50 / 0x00524DC0) | ported-with-cite | `CSequence.cs:149,140` |
|
||
| remove_cyclic_anims (0x00524E40) | ported-with-cite | `CSequence.cs:163` |
|
||
| **pack_size / Pack / UnPack** (0x00524F20 / 0x00525020 / 0x005259D0) | **UNPORTED, dead-code-verified** | zero external callers anywhere in the retail decomp; correctly not ported |
|
||
| append_animation (0x00525510) | ported-with-cite | `CSequence.cs:109` (independently read) |
|
||
| clear (0x005255B0) | ported-with-cite | `CSequence.cs:130` |
|
||
| update_internal (0x005255D0) | ported-with-cite | `CSequence.cs:332` (independently read in full — the iterative frame-crossing loop, no safety cap, matches retail exactly) |
|
||
| advance_to_next_animation (0x005252B0) | ported-with-cite | `CSequence.cs:435` (independently read) |
|
||
| update (0x00525B80) | ported-with-cite | `CSequence.cs:307` |
|
||
| ~CSequence (0x00524A30) | trivial, skipped | GC-obviated |
|
||
|
||
**~79% ported-with-cite** (22/28; ~96% of substantive methods). Cleanest of
|
||
the four classes — everything that drives frame playback, hook dispatch, or
|
||
physics accumulation is ported and cited. The two known field-representation
|
||
divergences (`double` vs x87 `long double` frame_number; `LinkedList<T>` vs
|
||
intrusive `DLList`) are already register rows AD-33/AD-34. **No concerning
|
||
gap.**
|
||
|
||
### MotionTableManager (17 symbols.json entries)
|
||
|
||
| Retail method (addr) | acdream status | Cite |
|
||
|---|---|---|
|
||
| initialize_state (0x0051C030) | ported-with-cite | `MotionTableManager.cs:353` |
|
||
| AnimationDone (0x0051BCE0) | ported-with-cite | `MotionTableManager.cs:290` (independently read) |
|
||
| CheckForCompletedMotions (0x0051BE00) | ported-with-cite | `MotionTableManager.cs:322` |
|
||
| UseTime (0x0051BFD0) | ported-with-cite | `MotionTableManager.cs:342` |
|
||
| HandleEnterWorld / RemoveLinkAnimations (both 0x0051BDD0) | ported-with-cite / SYMBOL-ARTIFACT-duplicate | `MotionTableManager.cs:373` — pseudo-C shows only one body at this address |
|
||
| HandleExitWorld (0x0051BDA0) | ported-with-cite | `MotionTableManager.cs:385` |
|
||
| SetPhysicsObject (0x0051BBC0) | deliberately-absent-with-reason | `MotionTableManager.cs:20-22` (file header: "C# has no physics_obj field — R2 leaves the CPhysicsObj::MotionDone target as an injectable seam") |
|
||
| Create (0x0051BC50) | ported-with-cite | `MotionTableManager.cs:129,138` |
|
||
| GetMotionTableID (0x0051BC10) | unported, verified low-risk | retail's only caller (`CPartArray::SetMotionTableID @ 0x005186e0`) uses it purely as a dirty-check before destroying+reconstructing the whole manager; acdream gets the same capability by constructing a fresh `AnimationSequencer`/`MotionTableManager` (`AnimationSequencer.cs:287`) |
|
||
| PerformMovement (0x0051C0B0) | ported-with-cite | `MotionTableManager.cs:409` (independently read in full) |
|
||
| SetMotionTableID (0x0051BBD0) | unported, verified low-risk | its only caller in the entire retail client is its own `Create` factory (single call site, pc:290526) |
|
||
| truncate_animation_list (0x0051BCA0) | ported-with-cite | `MotionTableManager.cs:259` |
|
||
| Destroy / ~MotionTableManager | trivial, skipped | GC-obviated |
|
||
| remove_redundant_links (0x0051BF20) | ported-with-cite | `MotionTableManager.cs:191` (independently read in full — byte-for-byte match including the `0xb0000000`/`0x70000000` block masks) |
|
||
| add_to_queue (0x0051BFE0) | ported-with-cite | `MotionTableManager.cs:166` |
|
||
|
||
**~65% raw ported-with-cite** (11/17), plus 1 deliberately-absent and 2
|
||
unported-but-verified-zero-risk (both `MotionTableID` accessors — retail
|
||
itself only "changes" a motion table by destroy+recreate at the
|
||
`CPartArray` layer, exactly matching acdream's architecture). **No
|
||
concerning gap.**
|
||
|
||
### CMotionTable (17 symbols.json entries)
|
||
|
||
| Retail method (addr) | acdream status | Cite |
|
||
|---|---|---|
|
||
| ctor (0x004F94E0) | ported, uncited | `CMotionTable.cs:64` |
|
||
| Pack / UnPack (0x00523180 / 0x005238C0) | **UNPORTED, dead-code-verified** | zero external callers, same dead `PackObj` family as `CSequence` |
|
||
| Destroy / ~CMotionTable | trivial, skipped | GC-obviated (`cycles`/`modifiers`/`links` hash tables → `Dictionary<>`) |
|
||
| GetDBOType (0x005268A0) | N/A, architecturally superseded | retail RTTI-style type tag; acdream's typed `Dats.Get<MotionTable>()` generic accessor makes it unnecessary |
|
||
| Allocator / Allocate (both 0x004F96E0) | SYMBOL-ARTIFACT-duplicate / trivial | `new CMotionTable(table)` supersedes the placement-new+construct factory directly |
|
||
| SetDefaultState (0x005230A0) | ported-with-cite | `CMotionTable.cs:605` (independently read) |
|
||
| DoObjectMotion / StopObjectMotion / StopObjectCompletely (0x00523E90/0x00523EC0/0x00523ED0) | ported-with-cite | `CMotionTable.cs:635,640,652` |
|
||
| re_modify (0x005222E0) | ported-with-cite | `CMotionTable.cs:528` |
|
||
| is_allowed (0x005226C0) | ported-with-cite | `CMotionTable.cs:172` |
|
||
| get_link (0x00522710) | ported-with-cite | `CMotionTable.cs:201` (independently read — the reversed-key branch, field-validated per its own doc comment) |
|
||
| GetObjectSequence (0x00522860) | ported-with-cite | `CMotionTable.cs:255` — independently read in full; the single highest-stakes function in this whole sweep (branch-heavy style/cycle/action/modifier dispatcher), ported branch-for-branch with inline citations, including three explicitly-preserved retail quirks (A4-#1 double-hop tick counting never double-charging the base cycle; A4-#2 silent no-op in `ChangeCycleSpeed` when old speed ~0 but new speed isn't; A4-#5 `ReModify`'s lockstep-snapshot termination bound) |
|
||
| StopSequenceMotion (0x00522FC0) | ported-with-cite | `CMotionTable.cs:559` |
|
||
|
||
Bonus (retail free functions, not `CMotionTable::` members, but ported+cited
|
||
in the same file): `same_sign`→`SameSign` (`:77`), `change_cycle_speed`→
|
||
`ChangeCycleSpeed` (`:88`), `add_motion`→`AddMotion` (`:116`),
|
||
`combine_motion`→`CombineMotion` (`:143`), `subtract_motion`→`SubtractMotion`
|
||
(`:156`) — all independently read.
|
||
|
||
**~53% raw ported-with-cite** (9/17), but **100% of the 9 substantive
|
||
motion-selection methods** — every method that isn't Pack/UnPack/RTTI/memory
|
||
management is ported and cited. **No concerning gap.**
|
||
|
||
### Bottom line across all four classes
|
||
|
||
103 total symbols.json entries examined: ~77 ported-with-cite, 4
|
||
symbol-artifacts (not real distinct methods), ~9 trivial/GC-obviated, 1
|
||
architecturally superseded, 1 ported-but-uncited (`CMotionInterp.Create`),
|
||
and 8 genuinely unported — of which 7 are verified dead code in the retail
|
||
client itself (the `Pack`/`UnPack`/`pack_size`/`GetMotionTableID`/
|
||
`SetMotionTableID` family, confirmed via call-site tracing). **The single
|
||
genuine, unexplained, feel-visible-risk gap across all four classes is
|
||
`CMotionInterp::get_adjusted_max_speed` (0x00527D00)** — the only unported
|
||
method sitting on a hot per-tick gameplay path (dead-reckoning catch-up
|
||
speed clamp) whose retail selection condition could not be resolved from
|
||
static analysis alone. All four files hold up as genuinely faithful,
|
||
well-cited retail ports; this sweep found no evidence of silently-diverged
|
||
gameplay logic in any of the four classes' core responsibilities.
|
||
|
||
## 2. Feel-visible flow verdicts
|
||
|
||
This section was independently traced by the lead audit against the primary
|
||
source (the extensively retail-cited C# in `CMotionTable.cs`, `CSequence.cs`,
|
||
`MotionState.cs`, `AnimationSequencer.cs`, `MotionInterpreter.cs` — most
|
||
methods in these five files quote the exact decompiled C body in a doc
|
||
comment, so this section cites the acdream file:line as primary evidence
|
||
rather than re-deriving from the 1.4M-line decomp text directly; the
|
||
background method-coverage-sweep and flow-tracing agents' independent
|
||
findings are merged in below where they add or contest something). A
|
||
striking finding up front: several of the 7 flows below turned out to have
|
||
**verdict parity for a decisive reason that ISN'T "acdream ported it
|
||
correctly"** — retail itself doesn't do the fancier thing the flow's framing
|
||
implied. That distinction matters for a retail-faithful project: it means
|
||
there is nothing to build, not merely nothing left to fix.
|
||
|
||
**1. Stance-change transition animations — PARITY.**
|
||
`CMotionTable.GetObjectSequence` Branch 1 (`src/AcDream.Core/Physics/Motion/CMotionTable.cs:279-336`,
|
||
citing retail `GetObjectSequence @ 0x00522860`) is a full style-change
|
||
dispatcher: it computes an exit link from the current substate to the
|
||
current style's default substate, a direct link from the current style's
|
||
default substate to the target style's default substate, and — when no
|
||
direct link exists — a double-hop through the table's `DefaultStyle` (lines
|
||
308-314), then plays exit-link → hop1 → hop2 → new-cycle in sequence
|
||
(`AddMotion` calls at 318-321) before installing the new style/substate.
|
||
This is retail's genuine weapon-draw/style-change link mechanism, not a
|
||
simplified instant cut. `AnimationSequencer.SetCycle`
|
||
(`src/AcDream.Core/Physics/AnimationSequencer.cs:390-391`) drives style
|
||
changes through exactly this path before dispatching the target motion.
|
||
|
||
**2. Landing after jump/fall (soft vs hard landing) — PARITY, and the "gap"
|
||
doesn't exist in retail.** `MotionInterpreter.HitGround`
|
||
(`src/AcDream.Core/Physics/MotionInterpreter.cs:2425-2443`) quotes retail's
|
||
`CMotionInterp::HitGround @ 0x00528ac0` FULL BODY: strip link animations,
|
||
then re-apply the PRESERVED pre-fall interpreted forward command (walk/run/
|
||
ready) — there is no velocity, fall-distance, or fall-duration branch
|
||
anywhere in that function. Retail's `Falling` SubState
|
||
(`MotionInterpreter.cs:56-63`) is one airborne cycle regardless of how far
|
||
the body fell; landing is simply "the Falling→X link fires through the same
|
||
`GetObjectSequence` Branch 2 cycle-to-cycle mechanism verified in item 6."
|
||
There is no severity-based "hard landing" animation to select in retail's
|
||
own Humanoid MotionTable, so this was never a divergence to close.
|
||
|
||
**3. In-place turn cycles vs omega-driven turning — PARITY (already shipped
|
||
under R6; not re-audited here per task scope, confirmed only that the two
|
||
things are the SAME mechanism, not competing ones).** `TurnRight`/`TurnLeft`
|
||
(`0x6500000D`/`0x6500000E`) carry the `0x40000000` cycle-class bit
|
||
(`0x65000000 & 0x40000000 != 0`), so they ARE genuine `CMotionTable` cycles
|
||
with their own authored `Anims` (the visual leg-crossing/pivot animation)
|
||
AND their own authored `Omega` (R6's pinned finding: `omega.Z = -1.5`
|
||
rad/s ≈ -86°/s from the installed Humanoid table, not a synthetic 90°
|
||
formula). `CMotionTable.AddMotion`
|
||
(`src/AcDream.Core/Physics/Motion/CMotionTable.cs:116-134`) writes both the
|
||
anim frames and the omega from the SAME `MotionData` record onto the
|
||
sequence in one call; `CSequence.ApplyPhysics` rotates the Frame by that
|
||
omega every frame the turn cycle plays. There was never a separate
|
||
"visual cycle vs physical rotation" question to resolve — one MotionData
|
||
record drives both.
|
||
|
||
**4. Walk↔run mid-stride transitions — PARITY (same Branch-2 machinery as
|
||
item 1, one level down).** Walk and Run are both cycle-class substates
|
||
within `NonCombat`/combat styles, so crossing the walk/run threshold or
|
||
toggling the Run hold-key is a same-style cycle-to-cycle request through
|
||
`GetObjectSequence` Branch 2 (`CMotionTable.cs:341-423`): it looks up a
|
||
direct link between the two substates via `GetLink`, falls back to a
|
||
style-default double-hop if none exists (lines 378-383), and has a
|
||
same-substate "fast re-speed" path (lines 358-367) for a pure speed change
|
||
within the SAME substate (e.g. accelerating while already running) that
|
||
rescales the cyclic framerate and physics in place rather than re-triggering
|
||
a full transition. This is retail's genuine walk-to-run link/blend
|
||
mechanism, not an instant swap.
|
||
|
||
**5. Backward/strafe cycle selection — PARITY, and again the "gap" doesn't
|
||
exist in retail.** `AnimationSequencer.SetCycle`
|
||
(`src/AcDream.Core/Physics/AnimationSequencer.cs:344-348, 367-381`) states
|
||
plainly, citing ACE's `MotionInterp.cs:394-428` as cross-check: "the AC
|
||
MotionTable has NO cycles for TurnLeft, SideStepLeft, or WalkBackward. These
|
||
are played as their right-side/forward equivalents with a negated
|
||
framerate so the animation runs in reverse." This is a retail asset-content
|
||
fact, not an acdream simplification — there is no distinct backward-walk or
|
||
strafe-left animation to select in the first place; retail itself reverses
|
||
the forward/right cycle. acdream's remap (WalkBackward → WalkForward at
|
||
-0.65×speed, SideStepLeft → SideStepRight at -1×speed) matches this exactly
|
||
at both the `AnimationSequencer` boundary (local-player raw input) and the
|
||
`MotionInterpreter.adjust_motion` boundary (wire-level, R3-cited) — see
|
||
section 1's method sweep for whether both call sites are still needed or
|
||
one is now dead code.
|
||
|
||
**6. Link-animation traversal system — PARITY, and it is the single most
|
||
load-bearing finding of this audit.** `CMotionTable.GetLink`
|
||
(`CMotionTable.cs:190-241`, retail `get_link @ 0x00522710`) is a genuine,
|
||
general-purpose `(fromStyle, fromSubstate, toSubstate)` link lookup over the
|
||
DAT-authored `Links` dictionary — not a hardcoded Ready/Walk/Run subset. It
|
||
handles the forward direction, a reversed-key direction (used when a speed
|
||
sign flip means "the link is authored the other way," e.g. the Ready↔
|
||
WalkBackward case the doc comment says was field-validated fixing a
|
||
"left leg twitches" glitch), and a style-level catch-all fallback. Every one
|
||
of `GetObjectSequence`'s four branches (style-change, cycle, action,
|
||
modifier) calls it and composes the result into 1-3 chained `AddMotion`
|
||
calls (exit link, direct/hop1, hop2) before the target cycle, exactly
|
||
matching retail's own double-hop-via-`DefaultStyle` fallback for style
|
||
changes with no direct link, and an out-hop/action-link/return-hop triple
|
||
for action-class motions with no direct link to the target (`CMotionTable.cs:428-478`,
|
||
with the load-bearing `#A4-1` tick-count citation: "never the base cycle,
|
||
never double-counted (ACE's bug, not retail's)" — i.e. acdream's tick
|
||
accounting is MORE correct than the reference ACE port here, not less).
|
||
This resolves the audit's biggest open question going in: acdream does not
|
||
skip genuine style-to-style links (drawing a weapon, sheathing, sitting
|
||
down) in favor of a hardcoded locomotion-only subset.
|
||
|
||
**7. Interrupted-animation behavior — PARITY at the queue-mechanics level;
|
||
one narrower residual question outside this file set.** A dedicated
|
||
flow-tracing sub-agent (independent pass, cross-checked against ACE) closed
|
||
most of the uncertainty this item started with. Retail
|
||
`MotionTableManager::RemoveRedundantLinks` (`0x0051bf20`) explicitly only
|
||
collapses cycle-class-not-modifier or style-class queue tails — the
|
||
modifier/action-class branch is "neither branch taken" (confirmed directly
|
||
in `CMotionTable.cs`'s ported `RemoveRedundantLinks`, see section 1): action-class
|
||
one-shots (attacks, casts) are **never truncated** by this mechanism and
|
||
always run their tick-countdown to natural completion. Separately, retail
|
||
`CPhysicsObj::interrupt_current_movement` (`0x005101f0`) is called
|
||
unconditionally from `jump()` and cancels an in-flight `MoveToManager`
|
||
transition — a wholly different mechanism from the action queue, not a
|
||
"cancel this attack" primitive. acdream's `MotionTableManager.RemoveRedundantLinks`
|
||
(`src/AcDream.Core/Physics/Motion/MotionTableManager.cs:191-248`) is a
|
||
byte-for-byte match including the identical `0xb0000000`/`0x70000000` block
|
||
masks and the same fallthrough, and the `InterruptCurrentMovement` seam
|
||
(`MotionInterpreter.cs:658`) is fully wired in PRODUCTION — not a stub — to
|
||
real `MoveToManager.CancelMoveTo(WeenieError.ActionCancelled)` in both
|
||
`src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs:152-154`
|
||
(remote) and `src/AcDream.App/Input/PlayerModeController.cs:354-362` (local
|
||
player), plus 4 call sites in `StickyManager.cs`. **Net: attack/cast
|
||
animations are uninterruptible by movement/jump input in BOTH clients —
|
||
movement just queues behind them; jump only ever cancels an in-flight
|
||
move-to, never the action queue.** The one thing this audit still did not
|
||
verify: whether higher-level combat/magic-casting code (entirely outside
|
||
`MotionInterpreter`/`MotionTableManager`, not read for this audit) layers
|
||
its own ADDITIONAL "can't move while casting" rule on top of this queue
|
||
mechanism — that would live in the combat/magic subsystem and needs a
|
||
separate targeted read, not a live capture.
|
||
|
||
**Additional residual surfaced by the flow-tracing pass, item 1 (stance
|
||
change):** the mechanism (`GetObjectSequence` Branch 1 + `GetLink`) is
|
||
confirmed parity, but whether acdream's higher-level default-combat-mode
|
||
selection (`CombatInputPlanner.GetDefaultCombatModeDecision`, not read in
|
||
this audit) picks the exact same weapon-style-to-CombatMode mapping as
|
||
retail's `ClientCombatSystem::GetDefaultCombatMode` (`0x0056B310`) in every
|
||
edge case was NOT traced — flagged as a small untraced item, not a
|
||
confirmed divergence.
|
||
|
||
**Ranked feel-impact of these 7, most to least:** all 7 came back parity —
|
||
an unusual, striking result for a from-scratch port of this scope. Ranking
|
||
by residual RISK rather than impact (i.e., where a future capture is most
|
||
likely to still surface a surprise, since several parity claims rest partly
|
||
on DAT-content assumptions rather than pure code): (1) item 7's untraced
|
||
combat/casting-layer interrupt rule and item 1's untraced default-combat-mode
|
||
mapping are the two loose threads worth a follow-up read (not a live
|
||
capture); (2) items 3/4/6 (turn cycles, walk-run link, general link
|
||
traversal) are the most solid — confirmed via both the acdream code AND an
|
||
independent cross-check against ACE's own C# `MotionTable.cs` port, which
|
||
shows the same double-hop structure; (3) items 2/5 (landing, backward/strafe)
|
||
are effectively closed — in both cases the "gap" the flow's framing
|
||
hypothesized doesn't exist in retail itself (one universal landing
|
||
transition; forward/right cycles reverse-played rather than distinct
|
||
backward/left clips), corroborated for item 5 by holtburger's wire-level
|
||
`MovementCommand` enum showing `WalkBackwards`/`TurnLeft`/`SidestepLeft` as
|
||
distinct wire ids (confirming the reversal is a client animation-layer
|
||
transform, not a wire-format absence). Net: of the 7 flows the task asked to
|
||
trace, all are parity; the two follow-up threads (combat-layer cast
|
||
interrupt, default-combat-mode mapping) are outside the `MotionInterpreter`/
|
||
`CMotionTable`/`CSequence` file set this audit focused on and are noted for
|
||
a future combat/magic-scoped audit, not scheduled as animation fixes.
|
||
|
||
## 3. TS-50 / TS-51 current scoping (verified against current code, 2026-07-30)
|
||
|
||
Both rows are precisely as described in the register — re-reading the actual
|
||
code confirms rather than narrows either row. No promotion to a fix is
|
||
recommended; both remain the correct classification (deliberate ordering
|
||
adaptation with a bounded, named residual), not silent regressions.
|
||
|
||
**TS-50 — which hook types still deliver late.** Read
|
||
`src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs:37-86` (`Capture`):
|
||
for every hook produced by a sequence advance, the queue tests
|
||
`hooks[i] is AnimationDoneHook` (line 75) and, ONLY for that one hook type,
|
||
synchronously calls `sequencer.Manager.AnimationDone(success: true)` at
|
||
capture time — i.e. inside the same call that advanced the sequence, matching
|
||
retail `CPhysicsObj::process_hooks @ 0x00511550` timing exactly (semantic
|
||
motion completion, Target/Movement/PartArray/Position manager tail all see it
|
||
in the same quantum). EVERY OTHER hook type reaching this queue (from the
|
||
`DatReaderWriter.Types.AnimationHook` hierarchy routed through
|
||
`AnimationHookRouter` to `AudioHookSink`, `ParticleHookSink`,
|
||
`TranslucencyHookSink`, `LightingHookSink` — i.e. sound playback, particle
|
||
creation including `RetailCreateBlockingParticleHook`, translucency-fade
|
||
starts, light attach, and `PhysicsScriptHook`/CallPES-adjacent triggers) is
|
||
unconditionally appended to `_entries` (line 82-85) and only fires later, in
|
||
`Drain()` (lines 88-125), which is called exactly once per render/update
|
||
frame from `LiveEffectFrameController.Tick` at
|
||
`src/AcDream.App/Update/LiveObjectFrameController.cs:107-126` — AFTER every
|
||
live entity's root/part/equipped-child pose has been published for that
|
||
frame (the comment at `LiveObjectFrameController.cs:109-113` names this
|
||
explicitly: "acdream currently keeps non-AnimationDone hooks at this
|
||
deferred shared boundary under TS-50"). So the answer to "which hook types
|
||
still deliver late": **all of them except semantic AnimationDone** — sound,
|
||
particle, light, translucency, and CallPES/script-chain hooks can be up to
|
||
one render frame later than retail's per-object `process_hooks` moment.
|
||
Feel-visible risk is concentrated in **CallPES** (a hook that triggers a
|
||
PhysicsScript chain, e.g. spawning a follow-up effect keyed to a specific
|
||
animation frame) and blocking-particle creation tied to an attack's exact
|
||
swing frame — a one-frame-late particle spawn on a fast weapon swing is the
|
||
kind of thing a careful side-by-side viewer could notice, though nobody has
|
||
filed a symptom against it yet. Audio/light/translucency lateness is far
|
||
less likely to be perceptible at typical frame rates.
|
||
|
||
**TS-51 — per-render-frame vs per-quantum tails.** Confirmed at
|
||
`LiveObjectFrameController.cs:107-126`: `LiveEffectFrameController.Tick(float
|
||
deltaSeconds)` advances `_particles.Tick(deltaSeconds)` and
|
||
`_scripts.Tick(_scriptTime.CurrentScriptTime)` exactly once per call, and
|
||
this controller is driven once per render/update frame (not once per
|
||
admitted 30 Hz physics quantum per live object). Retail's
|
||
`CPhysicsObj::UpdateObjectInternal @ 0x005156B0` advances each ordinary
|
||
object's own ParticleManager then ScriptManager inside EVERY admitted
|
||
quantum for THAT object, and `animate_static_object @ 0x00513DF0` uses a
|
||
different order (Script → Particle → hooks) for the static-object workset.
|
||
acdream's shared tail is Particle → Script after static hook capture,
|
||
uniformly, once per render frame regardless of how many physics quanta a
|
||
given object admitted that frame. Practical effect: on a catch-up frame
|
||
(object advances several quanta at once, e.g. after a stall), the object's
|
||
root/pose advances through all of them but its particle/script tail only
|
||
advances once — an emitter that should have spawned N times in that
|
||
interval spawns once with N ticks' worth of `deltaSeconds`, and static
|
||
default-script/particle ordering runs in the opposite sequence from
|
||
`animate_static_object`. This is a real feel-visible risk specifically for
|
||
dense fast-tick emitters (rapid-fire spell effects, chain particle bursts)
|
||
but is architecturally deep to fix (needs incarnation-bound per-object
|
||
particle/script manager instances, which the register row itself names as
|
||
the retirement condition) — not a quick promotion candidate.
|
||
|
||
**Verdict:** neither row's scope has changed since the register was last
|
||
written; both remain accurately described. Of the two, TS-50's CallPES
|
||
lateness is the more plausible candidate for a future promotion (narrower
|
||
blast radius — "make CallPES and blocking-particle hooks fire at capture
|
||
time like AnimationDone, keep the rest deferred" is a bounded change),
|
||
whereas TS-51 needs the larger incarnation-bound-manager refactor the row
|
||
already flags.
|
||
|
||
## 4. Issue #64 (local pickup animation) reassessment
|
||
|
||
**Original hypothesis (filed 2026-05-14, pre-R3/R4/R6):** `OnLiveMotionUpdated`
|
||
filters local-player self-echoes wholesale, so ACE's server-authored
|
||
`Motion(MotionCommand.Pickup)` broadcast (via
|
||
`Player_Inventory.AddPickupChainToMoveToChain` →
|
||
`EnqueueBroadcastMotion(motion)`) never reaches the local player's animation
|
||
path. That exact function (`OnLiveMotionUpdated`) no longer exists in the
|
||
current tree (`git grep` for it returns nothing) — the inbound motion path
|
||
has been rewritten at least twice since (R4-V5's local/remote unification,
|
||
then the J-slice Runtime extraction), so the original hypothesis needs to be
|
||
re-evaluated against the CURRENT architecture, not assumed stale or assumed
|
||
still-broken.
|
||
|
||
**Current architecture, traced end to end:**
|
||
|
||
1. `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:216-241`
|
||
(`OnMotion`) — the first gate is a TIMESTAMP staleness check
|
||
(`_authorityGate.TryAcceptMotion`), unrelated to self-echo.
|
||
2. Lines 224 and 243-259 — the REAL self-echo gate, `R4-V5 (pin P1)`: `bool
|
||
retainPayload = update.Guid != _playerServerGuid || !update.IsAutonomous;`
|
||
This drops an entire UpdateMotion packet ONLY when it targets the local
|
||
player's guid AND the packet's wire-level `IsAutonomous` byte is set — the
|
||
comment cites retail `CPhysics::SetObjectMovement`'s autonomous gate
|
||
(`0x00509690 @0050972e`, raw 271370-271431) and explains WHY: ACE reflects
|
||
the client's own outbound `MoveToState` back to the sender with
|
||
`IsAutonomous=1` hardcoded (`MovementData.cs:162`,
|
||
`Player_Networking.cs:365` in the ACE reference), so this gate exists
|
||
specifically to drop THAT reflection, not every inbound packet addressed
|
||
to the player.
|
||
3. If the packet survives, the local-player branch at lines 450-509 routes
|
||
through the SAME `RemoteInboundMotionDispatcher.Apply` used for remotes
|
||
(`src/AcDream.App/Physics/RemoteInboundMotionDispatcher.cs`), which for a
|
||
`MovementType == 0` packet calls `motion.MoveToInterpretedState(interpreted,
|
||
animationSink)` (`RemoteInboundMotionDispatcher.cs:108-112`).
|
||
4. `MotionInterpreter.MoveToInterpretedState`
|
||
(`src/AcDream.Core/Physics/MotionInterpreter.cs:2697-2743`) replays each
|
||
entry in `ims.Actions` (the Commands[] one-shot list, populated by
|
||
`InboundInterpretedMotionFactory.Create` from the wire's `Commands`
|
||
field — `src/AcDream.App/Physics/InboundInterpretedMotionFactory.cs:46-63`)
|
||
through `DispatchInterpretedMotion`, with exactly ONE local-player-specific
|
||
filter at line 2735: `if (IsLocalPlayer && a.Autonomous) continue;` — this
|
||
is scoped to the PER-ACTION autonomous bit inside the Commands[] entry
|
||
(`MotionItem.PackedSequence & 0x8000`), not a blanket "is this the local
|
||
player" drop.
|
||
|
||
**Current best hypothesis:** ACE's Pickup broadcast is server-initiated (built
|
||
via `EnqueueBroadcastMotion`, not a reflected client `MoveToState`), so both
|
||
gates that could drop it — the packet-level `IsAutonomous` check (step 2) and
|
||
the per-action `Autonomous` bit check (step 4) — should read `false` for it,
|
||
same as for any other server-authored one-shot action a remote observer would
|
||
see. If that reading of ACE's flag values is correct, **the R4-V5 local/remote
|
||
unification (which post-dates #64's filing by roughly two months) likely
|
||
fixed this issue as an architectural side effect**, without anyone
|
||
specifically targeting #64. This is a hypothesis, not a confirmed fix — this
|
||
audit is report-only and did not launch the client or trigger a live pickup.
|
||
|
||
**Recommended next step (not performed here):** re-test #64 live with
|
||
`ACDREAM_DUMP_MOTION=1` set, trigger a close-range pickup as `+Acdream`, and
|
||
check the log for a `UM guid=<player> ... cmd=...` line whose resolved
|
||
command carries the Pickup action bit, followed by the sequencer actually
|
||
playing the one-shot cycle. If it still fails, the next diagnostic is to
|
||
confirm whether ACE's Pickup broadcast really sets `Autonomous=false` at
|
||
both the packet and per-action level (a WireMCP capture on the loopback
|
||
`UpdateMotion (0xF74D)` packet during a pickup would settle this instead of
|
||
reading ACE source), since a wrong assumption there is the one way this
|
||
hypothesis could be wrong.
|
||
|
||
## 5. Emote/action surface (AD-57) gap sizing
|
||
|
||
**What retail would play:** an animated social emote (retail's action-class
|
||
MotionCommand — the `0x10000000` bit family, e.g. a wave/point/bow/salute
|
||
cycle, as opposed to `/e <text>` roleplay chat text) is queued through the
|
||
SAME `CMotionInterp` action-list machinery already ported: `AddAction` onto
|
||
`RawMotionState`/`InterpretedMotionState`, packed onto the wire by
|
||
`RawMotionState::Pack` (retail `0x0051ed10`) as `num_actions` +
|
||
per-action pairs, broadcast to observers as a Commands[] entry on
|
||
`UpdateMotion`, and resolved into a `CMotionTable` action-class cycle via the
|
||
same `CMotionTable::GetObjectSequence` path used for locomotion cycles.
|
||
|
||
**What acdream has, precisely:** per section 4's trace, the RECEIVING half of
|
||
this pipeline is fully wired and (per the current-best-hypothesis above)
|
||
likely already plays a server-broadcast one-shot action correctly for both
|
||
local and remote observers — `InboundInterpretedMotionFactory` parses
|
||
Commands[] into `InboundMotionAction`s, `MotionInterpreter.MoveToInterpretedState`
|
||
replays them through `DispatchInterpretedMotion` into the same
|
||
`CMotionTable`/`CSequence` cycle-selection path as any other motion. The
|
||
SENDING half — the local player's own input constructing and enqueueing an
|
||
autonomous action, e.g. from a `/wave`-style command — has no production call
|
||
site: `grep -r "\.AddAction(" src/AcDream.App` returns nothing outside test
|
||
code (`RawMotionState.AddAction`/`InterpretedMotionState.AddAction` are
|
||
exercised only by unit tests, confirmed during this audit). The chat command
|
||
catalog (`src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs:144-150,232-234`)
|
||
has `/emote` and `/emotes`, but these are documented as the roleplay TEXT
|
||
emote (`/e <text>` — chat-log output), not an animated action — there is no
|
||
slash command or UI affordance in the catalog for a genuine visual emote.
|
||
|
||
**Gap size:** this is a clean, well-isolated feature gap, not a divergence
|
||
of any shipped behavior — the register row (AD-57, re-argued 2026-07-30) is
|
||
correct that "every currently-shipped movement packet matches retail byte-shape;
|
||
the gap only manifests when emote-class autonomous actions are implemented."
|
||
The work to close it is bounded and almost entirely additive: (a) a
|
||
retail-sourced list of which MotionCommand action IDs are genuine social
|
||
emotes and what UI/command surface retail exposes them through (character
|
||
menu right-click? a `/motion` or numbered emote command? — this needs a
|
||
named-retail grep, not guessed), (b) a client input path that calls
|
||
`MotionInterpreter.DoMotion`/`AddAction` with the right action ID and
|
||
`Autonomous=true`, and (c) confirming the existing outbound packer already
|
||
emits it correctly (it should, since `RawMotionStatePacker` already handles
|
||
the `Actions` list per AD-57's own text). No architecture changes are
|
||
required — this is squarely a "wire up an existing, tested machine" gap, sized
|
||
small-to-medium (one research pass to find the retail command surface, one
|
||
implementation pass to wire input → `AddAction`).
|
||
|
||
## 6. Ranked gap catalog + recommended fix order
|
||
|
||
**Headline result:** this audit set out to find where acdream's animation
|
||
system diverges from retail and found the system in unusually good shape.
|
||
Two independent research passes (a symbol-by-symbol method-coverage sweep
|
||
and a 7-flow feel-visible trace) plus the lead auditor's own full read of
|
||
the four core files converged on the same conclusion: `MotionInterpreter`
|
||
(`CMotionInterp`), `CSequence`, `CMotionTable`, and `MotionTableManager` are
|
||
faithful, extensively retail-cited ports, and all 7 traced feel-visible
|
||
flows came back parity — three of them (landing severity, backward/strafe
|
||
cycles, in-place-turn-cycle-vs-omega) because the richer retail behavior the
|
||
flow's framing assumed doesn't actually exist in retail either. The gap
|
||
catalog below is therefore short and each entry is genuinely small.
|
||
|
||
**Ranked by feel-impact, most to least:**
|
||
|
||
1. **AD-57 — animated emote authoring gap (feel-visible, bounded scope).**
|
||
The RECEIVING half of retail's action-class one-shot animation system
|
||
(server-broadcast one-shots like Pickup, and presumably other players'
|
||
emotes) is fully wired and plays correctly through the same
|
||
`GetObjectSequence` Branch 3 machinery as any other motion (see section
|
||
4/5). The SENDING half — the local player triggering their OWN animated
|
||
emote (retail's `/wave`-equivalent) — has no production call site;
|
||
`RawMotionState.AddAction`/`InterpretedMotionState.AddAction` are
|
||
exercised only by unit tests. This is the most user-visible gap in the
|
||
catalog (a whole category of retail behavior — animated social
|
||
gestures — is simply absent from the client), but it is squarely a
|
||
"wire up an existing, tested machine" gap: no architecture change, no
|
||
new port, just (a) a named-retail grep for which MotionCommand action
|
||
IDs are genuine emotes and what UI surface retail exposes them through,
|
||
(b) an input path that calls `DoMotion`/`AddAction` with the right
|
||
action ID and `Autonomous=true`, (c) confirming the existing
|
||
`RawMotionStatePacker` emits it correctly (should already, per AD-57).
|
||
|
||
2. **TS-50 residual — CallPES and blocking-particle hooks up to one render
|
||
frame late (already known, narrow promotion candidate).** Verified
|
||
against current code (section 3): only the semantic `AnimationDoneHook`
|
||
fires at capture time; every other hook type (CallPES/script-chain
|
||
triggers, particle creation including blocking particles, sound, light,
|
||
translucency-fade starts) is deferred to `AnimationHookFrameQueue.Drain()`,
|
||
called once per render frame after all entities' poses publish. Most
|
||
feel-visible on a fast weapon swing where a blocking-particle effect is
|
||
keyed to an exact frame. Bounded fix: make CallPES and blocking-particle
|
||
hooks fire at capture time like `AnimationDoneHook`, keep the rest
|
||
deferred — narrower than the full TS-51 refactor below.
|
||
|
||
3. **Issue #64 — local pickup animation not rendering (likely already
|
||
fixed, zero-cost to verify).** Section 4's trace shows the R4-V5 local/
|
||
remote unification (which post-dates #64's filing) architecturally
|
||
closed the exact mechanism the original hypothesis blamed: the
|
||
packet-level and per-action autonomous-echo gates are now scoped
|
||
precisely enough that a server-authored one-shot (non-autonomous, by
|
||
construction) should reach the local player's `DispatchInterpretedMotion`
|
||
exactly like it does for remotes. This needs a 2-minute live re-test
|
||
(`ACDREAM_DUMP_MOTION=1`, trigger a close-range pickup), not an
|
||
engineering investment — likely already closed as a side effect of
|
||
unrelated work and just needs its ISSUES.md status updated.
|
||
|
||
4. **`CMotionInterp::get_adjusted_max_speed` unported (narrow, needs a
|
||
cdb read before it's even confirmed live).** The one genuine
|
||
unexplained gap from the method-coverage sweep (section 1): retail's
|
||
dead-reckoning catch-up path chooses between `get_max_speed` and this
|
||
sibling via a static toggle (`InterpolationManager::fUseAdjustedSpeed_`)
|
||
whose default this audit could not resolve from static analysis alone.
|
||
acdream always uses `GetMaxSpeed()` for remote catch-up. If the toggle
|
||
defaults to the adjusted variant in retail, acdream's remote
|
||
dead-reckoning catch-up speed could be systematically using the wrong of
|
||
two very similar formulas — narrow blast radius (one clamp value in one
|
||
catch-up path), plausible feel effect (slightly different snap-back
|
||
speed on remotes catching up after a network stall). Per the project's
|
||
own retail-debugger toolchain, this is a cdb-read-the-static-value
|
||
question, not a guess-and-ship one.
|
||
|
||
5. **TS-51 residual — particle/script tails once per render frame instead
|
||
of once per admitted physics quantum (already known, larger refactor).**
|
||
Verified against current code (section 3): on a catch-up frame where an
|
||
object advances several 30 Hz quanta at once, its particle/script tail
|
||
only advances once with the accumulated `deltaSeconds`, and static
|
||
default-script/particle ordering runs Particle→Script instead of
|
||
retail's Script→Particle→hooks. Most feel-visible for dense fast-tick
|
||
emitters (rapid spell-effect chains). The register row itself names the
|
||
retirement condition (incarnation-bound per-object particle/script
|
||
manager instances) — this is real architectural work, not a quick
|
||
promotion, and should stay queued behind the current M4 feature-work
|
||
order rather than jumping the line for this audit.
|
||
|
||
6. **Two untraced items outside this audit's file scope (flagged, not
|
||
confirmed divergences).** The flow-tracing pass surfaced two loose
|
||
threads it didn't have scope to chase: (a) whether combat/magic-casting
|
||
code (entirely outside `MotionInterpreter`/`MotionTableManager`) layers
|
||
its own additional "can't move while casting" rule on top of the
|
||
confirmed-parity action-queue mechanism; (b) whether
|
||
`CombatInputPlanner.GetDefaultCombatModeDecision` picks the same
|
||
weapon-style-to-CombatMode mapping as retail's
|
||
`ClientCombatSystem::GetDefaultCombatMode` (`0x0056B310`) in every edge
|
||
case. Both are plausible-but-unconfirmed and belong to a combat/magic-
|
||
scoped audit, not this animation-scoped one — recommended as a future
|
||
audit topic, not a fix.
|
||
|
||
**Recommended fix order** (cheapest/highest-confidence first): (1) re-test
|
||
#64 live and close the issue if confirmed — essentially free; (2) research
|
||
+ wire the emote-sending path (AD-57) — bounded, additive, no architecture
|
||
change, the single most user-visible improvement available; (3) narrow
|
||
TS-50's promotion to cover CallPES + blocking-particle hooks specifically;
|
||
(4) cdb-verify `fUseAdjustedSpeed_`'s retail default before deciding whether
|
||
`get_adjusted_max_speed` needs porting at all; (5) queue the full TS-51
|
||
incarnation-bound-manager refactor behind current M4 feature work, since it
|
||
is real architectural investment rather than a bounded fix; (6) file a
|
||
follow-up combat/magic-scoped audit for the two untraced items rather than
|
||
guessing at their status here.
|
||
|
||
This audit made no code changes and files no fixes directly — items 1-4
|
||
above are small enough that the user may want to fold them into the next
|
||
convenient M4 work session; item 5 should go through the normal roadmap
|
||
process (a new phase/slice, not a drive-by fix) given its architectural
|
||
size; item 6 needs its own investigation before any fix is proposed.
|