# Animation System Audit Phase A audit for `feature/animation-system-complete`. Date: 2026-04-28. ## Summary The animation core is much stronger than the feature surface around it. `AnimationSequencer` already handles cyclic state changes, transition links, negative-speed retail remaps, mid-cycle speed changes, frame hooks, and PosFrame root-motion accumulation. `GameWindow.OnLiveMotionUpdated` already routes `InterpretedMotionState.Commands[]` through `PlayAction`, which means server-broadcast NPC/monster/emote/action overlays are likely to animate when the server emits motion commands. The remaining gap is mostly orchestration: - local combat/spell/item-use commands build wire packets but do not yet drive the local visible action immediately; - several combat/spell/emote packet surfaces need conformance fixes before animation triggers can be trusted: combat mode enum values, split melee/missile attack builders, `CombatCommenceAttack`, `AttackDone`, damage/death notification parsers, `MagicSchool` enum order, and outbound emote/soul-emote builders; - combat/spell/item-use game events populate state/chat but do not yet map to animation overlays for attacker/defender/caster; - style changes are handled as simple `SetCycle(style, motion)` swaps, not the full ACE `MotionTable.GetObjectSequence` multi-link style transition chain; - held posture/emote commands need a small command resolver and tests around one-shot-vs-persistent routing; - death needs explicit `Sanctuary` action -> `Dead/Fallen` persistence rather than relying on chat/health side effects. ## Evidence Sources Named retail decomp: - `CMotionTable::is_allowed` at `0x005226C0` - `CMotionTable::get_link` at `0x00522710` - `CSequence::update_internal` at `0x005255D0` - `CMotionInterp::adjust_motion` at `0x00528010` - `CMotionInterp::charge_jump` at `0x005281C0` - `CMotionInterp::get_jump_v_z` at `0x00527AA0` - `CMotionInterp::jump` at `0x00528780` - `CMotionInterp::apply_current_movement` at `0x00528870` - `CMotionInterp::HitGround` at `0x00528AC0` - `CMotionInterp::LeaveGround` at `0x00528B00` - `CMotionInterp::DoMotion` at `0x00528D20` - `CMotionInterp::DoInterpretedMotion` at `0x00528360` - `ClientCombatSystem::HandleCommenceAttackEvent` at `0x0056AD20` - `ClientCombatSystem::SetCombatMode` at `0x0056BE30` - `ClientCombatSystem::StartAttackRequest` at `0x0056C040` - `ClientCombatSystem::EndAttackRequest` at `0x0056C0E0` - `ClientCombatSystem::StartPowerBarBuild` at `0x0056ADB0` - `ClientCombatSystem::GetPowerBarLevel` at `0x0056ADE0` - `ClientCombatSystem::ExecuteAttack` at `0x0056BB70` - `ClientCombatSystem::HandleDefenderNotificationEvent` at `0x0056C920` - `ClientCombatSystem::HandleEvasionDefenderNotificationEvent` at `0x0056C620` - `ClientCombatSystem::HandlePlayerDeathEvent` at `0x0056C320` - `ClientCombatSystem::HandleAttackerNotificationEvent` at `0x0056B420` - `ClientCombatSystem::HandleAttackDoneEvent` at `0x0056C500` - `CM_Combat::Event_ChangeCombatMode` at `0x006A9A70` - `CM_Combat::Event_TargetedMeleeAttack` at `0x006A9C10` - `CM_Combat::Event_TargetedMissileAttack` at `0x006A9D60` - `AttackHook::Execute` at `0x00526B70` - `gmSpellcastingUI::Cast` at `0x004C6050` - `ClientMagicSystem::CastSpell` at `0x00568040` - `ClientMagicSystem::FreeHandsAndCastSpell` at `0x00566EF0` - `ClientMagicSystem::GetAppropriateSpellFormula` at `0x00567D50` - `CM_Magic::Event_CastUntargetedSpell` at `0x006A3150` - `CM_Magic::Event_CastTargetedSpell` at `0x006A3040` - `ItemHolder::UseObject` at `0x00588A80` - `CM_Inventory::Event_UseEvent` at `0x006AC3B0` - `CM_Inventory::Event_UseWithTargetEvent` at `0x006AC480` - `CM_Item::DispatchUI_UseDone` at `0x006A8510` - `CommandInterpreter::PlayerIsDead` at `0x006B3D70` - `SmartBox::HandlePlayScriptID` at `0x00452020` - `CM_Physics::DispatchSB_PlayScriptID` at `0x006ACC40` - `CM_Physics::DispatchSB_PlayScriptType` at `0x006AC6E0` - `ClientCommunicationSystem::DoEmote` at `0x00578AD0` - `ClientCommunicationSystem::Pose` at `0x00580480` - `ClientCommunicationSystem::Handle_Communication__HearEmote` at `0x0057CBE0` - `ClientCommunicationSystem::Handle_Communication__HearSoulEmote` at `0x0057D020` - `ChatPoseTable::InqChatPoseCommand` at `0x00570AD0` - `ChatEmoteData::Pack` at `0x004FCE80` Cross-reference material: - `docs/research/deepdives/r03-motion-animation.md` - `C:\Users\erikn\source\repos\acdream\references\ACE\Source\ACE.Server\Physics\Animation\MotionTable.cs` - `C:\Users\erikn\source\repos\acdream\references\ACE\Source\ACE.Server\Physics\Animation\MotionInterp.cs` - `C:\Users\erikn\source\repos\acdream\references\ACE\Source\ACE.Entity\Enum\MotionCommand.cs` - `C:\Users\erikn\source\repos\acdream\references\holtburger\apps\holtburger-cli\src\pages\game\combat.rs` - `C:\Users\erikn\source\repos\acdream\references\holtburger\crates\holtburger-core\src\client\messages.rs` - `C:\Users\erikn\source\repos\acdream\references\holtburger\crates\holtburger-protocol\src\messages\movement\types.rs` The clean worktree intentionally does not contain `references/`; it was read read-only from the original checkout path above. ## Current Code Surface Core animation: - `src/AcDream.Core/Physics/AnimationSequencer.cs` - `SetCycle(style, motion, speedMod, skipTransitionLink)` handles cyclic state changes and transition links. - `PlayAction(motionCommand, speedMod)` handles Action, Modifier, and ChatEmote one-shots through Links/Modifiers lookup. - `Advance(dt)` emits pending hooks and accumulates PosFrame deltas. - Missing: full style-transition chain, durable modifier list, action queue accounting, and a public command-resolution facade that callers can test without `GameWindow`. - `src/AcDream.Core/Physics/MotionInterpreter.cs` - Handles locomotion, jump, leave-ground/hit-ground, and basic contact guards. - Missing: full retail `MotionState`, action list, modifier list, hold-key run application, combat-state guards, and `move_to_interpreted_state`. - `src/AcDream.Core/Physics/MotionCommandResolver.cs` - Reconstructs full 32-bit commands from 16-bit wire values. App integration: - `src/AcDream.App/Input/PlayerMovementController.cs` - Local walk/run/strafe/turn/jump driver. It does not own combat/spell/item action animation. - `src/AcDream.App/Rendering/GameWindow.cs` - `OnLiveMotionUpdated` is the main inbound motion/action router. - `OnLiveVectorUpdated` seeds airborne jump arcs and Falling cycles. - `OnLivePositionUpdated` snaps positions and lands airborne remotes. - `TickAnimations` advances sequencers and drains hooks. - `UpdatePlayerAnimation` drives the local movement cycle. - Missing: typed animation coordinator for combat/spell/use/death/emote events; too much command mapping still lives inline. Wire/state: - `src/AcDream.Core.Net/Messages/AttackTargetRequest.cs`: outbound attack request exists, but currently combines melee and missile into one layout; retail/ACE/holtburger use distinct `0x0008` melee and `0x000A` missile payloads. - `src/AcDream.Core.Net/Messages/CastSpellRequest.cs`: outbound spell request exists. - `src/AcDream.Core.Net/Messages/CharacterActions.cs`: combat mode request exists, but the combat-mode enum must be corrected to retail values `NonCombat=1`, `Melee=2`, `Missile=4`, `Magic=8`. - `src/AcDream.Core.Net/Messages/InteractRequests.cs`: use/use-with-target request exists. - `src/AcDream.Core.Net/GameEventWiring.cs`: combat, spell, item, chat events route into state classes. - Missing: public `WorldSession.SendAttack/SendCast/SendUse/ChangeCombatMode` wrappers and animation-side subscriptions. ## Retail Command Catalogue To Use From ACE `MotionCommand.cs` + `r03-motion-animation.md`: - Locomotion substates: Ready `0x41000003`, WalkForward `0x45000005`, WalkBackward `0x45000006`, RunForward `0x44000007`, TurnRight `0x6500000D`, TurnLeft `0x6500000E`, SideStepRight `0x6500000F`, SideStepLeft `0x65000010`, Falling `0x40000015`. - Held/posture substates: Crouch `0x41000012`, Sitting `0x41000013`, Sleeping `0x41000014`, Dead `0x40000011`, Fallen `0x40000008`. - Item/use substates: Reload `0x40000016`, Unload `0x40000017`, Pickup `0x40000018`, StoreInBackpack `0x40000019`, Eat `0x4000001A`, Drink `0x4000001B`, Reading `0x4000001C`. - Spell substates/actions: CastSpell `0x400000D3`, MagicBlast `0x4000002B`, MagicSelfHead `0x4000002C`, MagicSelfHeart `0x4000002D`, MagicBonus..MagicPenalty `0x4000002E..0x40000034`, MagicTransfer `0x40000035`, MagicEnchantItem `0x40000037`, MagicPortal `0x40000038`, MagicPray `0x40000039`, MagicPowerUp01..10 `0x1000006F..0x10000078`, MagicPowerUp01Purple..10Purple `0x1000012B..0x10000134`. - Combat actions: Sanctuary `0x10000057`, ThrustMed/Low/High `0x10000058..0x1000005A`, SlashHigh/Med/Low `0x1000005B..0x1000005D`, BackhandHigh/Med/Low `0x1000005E..0x10000060`, Shoot `0x10000061`, AttackHigh/Med/Low1..6 `0x10000062..0x1000006A` and `0x10000186..0x1000018E`, MissileAttack1..3 `0x100000D0..0x100000D2`, SpecialAttack1..3 `0x100000CD..0x100000CF`, dual-wield/offhand ranges `0x10000173..0x1000019A`. - ChatEmote actions: Wave `0x13000087`, BowDeep `0x1300007D`, Laugh `0x13000080`, Point `0x13000084`, Salute `0x1300008A`, Kneel `0x13000092`, HaveASeat `0x13000152`, DrudgeDance `0x13000151`, plus the full `0x1200/0x1300` ranges in `r03`. - Persistent emote states: `0x430000EA..0x430000FD`, SnowAngelState `0x43000118`, CurtseyState `0x4300011A`, AFKState `0x4300011B`, MeditateState `0x4300011C`, SitState `0x4300013A`, SitCrossleggedState `0x4300013B`, SitBackState `0x4300013C`, PossumState `0x43000142`, HaveASeatState `0x43000145`. ACE's enum is a useful alias catalog but has a shifted range for some late chat-emote states; named-retail values win when hard-coding constants. ## Category Audit ### 1. Own Player Movement Status: mostly working. Evidence: retail `CMotionInterp` jump/grounding symbols listed above; ACE `MotionInterp.cs` for `adjust_motion`, `apply_current_movement`, `HitGround`, and `LeaveGround`. acdream locations: `PlayerMovementController`, `MotionInterpreter`, `UpdatePlayerAnimation`, `OnLiveVectorUpdated`, `OnLivePositionUpdated`. Gaps: - held postures exist as retail commands but are not driven by a general posture/action API; - `MotionInterpreter` does not yet own full `MotionState`, so non-locomotion commands cannot be uniformly tested there; - mounted/swimming need dat/retail verification before any implementation. Tests to add: - posture state `SetCycle` tests for Crouch/Sitting/Sleeping; - `motion_allows_jump` conformance for item/spell/aim/posture ranges; - local action does not stomp Falling while airborne. ### 2. Other Players' Movement Status: partially working after the K-fix series. Evidence: `UpdateMotion` handling in `OnLiveMotionUpdated`; retail `CMotionInterp::DoInterpretedMotion` and `apply_current_movement`; ACE `MotionInterp.apply_current_movement`. acdream locations: `RemoteMotion`, `OnLiveMotionUpdated`, `OnLiveVectorUpdated`, `OnLivePositionUpdated`, `TickAnimations`. Gaps: - remote action overlays only happen when the server includes `InterpretedMotionState.Commands[]`; combat/spell game events do not yet synthesize overlays when the wire omits motion commands; - no test fixture exercises `OnLiveMotionUpdated` command-list routing outside `GameWindow`; - root-motion deltas are accumulated but not applied to remote body transforms. Tests to add: - command-list `Wave` -> `PlayAction` routing through a new coordinator; - airborne remote ignores mid-arc locomotion cycle swaps but still updates interpreted movement; - landing swaps Falling back to current interpreted command. ### 3. NPC Movement Status: likely works for UpdateMotion-driven locomotion and simple gestures; not verified. Evidence: retail MotionTable/InterpretedMotionState path; ACE `MotionTable.GetObjectSequence` and `MotionInterp.move_to_interpreted_state`. acdream locations: `CreateObject.ParseServerMotionState`, `OnLiveMotionUpdated`, `TickAnimations`. Gaps: - no NPC-specific live test checklist; - no retained action/modifier list, so repeated scripted gestures are fire-and-forget overlays only; - no head-look/threat-pose state beyond whatever arrives as motion commands. Tests to add: - synthetic NPC `UpdateMotion` with `Commands=[Wave, Ready]` plays one-shot then returns to Ready; - style-default fallback for creature motion tables. ### 4. Monster Movement Status: locomotion probably works when `MotionTableId` and UpdateMotion are present; special attacks are unknown. Evidence: ACE MotionTable supports monster actions such as HeadThrow, FistSlam, BreatheFlame, SpinAttack, Bite, SpecialAttack1..3. acdream locations: same as NPC movement; `AnimationHookRouter` for VFX/audio side effects. Gaps: - attack action overlays for monsters depend on server motion command lists; - no mapping from combat events to visible monster attack/hit reactions; - no exotic creature spot-checks. Tests to add: - `PlayAction(BreatheFlame)` resolves from Links/Modifiers when synthetic data provides it; - Attack hooks fire exactly once for a synthetic monster action. ### 5. Combat Actions Status: wire codecs and combat state exist; visual action orchestration is missing for local and event-driven paths. Evidence: - retail `ClientCombatSystem::StartPowerBarBuild`, `ClientCombatSystem::GetPowerBarLevel`, `ClientCombatSystem::ExecuteAttack`, `HandleCommenceAttackEvent`, `HandleAttackerNotificationEvent`, `HandleAttackDoneEvent`; - ACE `MotionTable.GetAttackFrames` scans Attack hooks and is the canonical hit-frame source; - holtburger combat UI tracks `AttackCommenced`, `AttackDone`, victim, attacker, defender, evasion, and killed feedback as runtime state. acdream locations: - `AttackTargetRequest` exists but no `WorldSession.SendAttack` wrapper was found; - `CombatState` emits `DamageTaken`, `DamageDealtAccepted`, evasion, `AttackDone`, and `KillLanded`; - `GameEventWiring` registers combat event parsers; - `AnimationSequencer.PlayAction` can play the swing once the command is known. Gaps: - combat-mode enum values are currently non-retail for missile/magic; - melee/missile attack request builders need to be split to retail layouts: `0x0008 targetGuid, attackHeight, power` and `0x000A targetGuid, attackHeight, accuracy`; - `CombatCommenceAttack (0x01B8)` is enumerated but not parsed/wired; - `AttackDone (0x01A7)` and attacker/defender/death notification parsers need ACE/holtburger fixtures before downstream animation can trust them; - `CombatState` has no `CurrentMode`, no attack sequence active flag, no selected target, and no power-bar state; - no local predictive swing on attack request; - hit reactions (Twitch/Stagger/Tipped/FallDown) are not mapped from defender notifications; - style changes for draw/sheath do not run the full style-transition chain. Tests to add: - parse/wire `CombatCommenceAttack`; - `CombatAnimationCoordinator` maps height/power/style to attack command; - defender hit quadrant maps to a stable flinch command; - `AttackHook` dispatch is one-shot. ### 6. Spell Casting Status: outbound cast packets and spellbook/enchantment state exist; visible cast-stage animation is missing. Evidence: - retail `ClientMagicSystem::CastSpell` and `FreeHandsAndCastSpell`; - `gmSpellcastingUI::Cast` calls `ClientMagicSystem::CastSpell`; - outbound cast actions are `0x0048` untargeted (`spellId`) and `0x004A` targeted (`targetGuid`, `spellId`); - retail/ACE school order is `War=1`, `Life=2`, `Item=3`, `Creature=4`, `Void=5`; - MotionCommand spell catalogue above; - `GameEventWiring` wires spellbook/enchantment updates but not casting animation. acdream locations: - `CastSpellRequest` targeted/untargeted builders; - `Spellbook`, `SpellTable`, `GameEventWiring` spell handlers; - `AnimationHookRouter` already routes hooks to audio/VFX sinks. Gaps: - no cast coordinator reading server `UpdateMotion`, spellcasting chat, `PlayScript.Fizzle`, `UseDone`, and errors into one local cast timeline; - no fizzle/interruption animation mapping from `PlayScript.Fizzle = 0x51` (ACE sends speed `0.5`) and `WeenieError`; - no recoil/release state; - no local immediate cast animation on request. - `MagicSchool` enum currently needs conformance against the retail/ACE order. Tests to add: - spell school/effect classifier maps to MagicBlast/MagicSelf/MagicPortal; - fizzle error maps to a one-shot action or recovery state once retail command is confirmed; - cast request triggers local action overlay without waiting for enchantment. ### 7. Emotes Status: inbound text parsers and chat display exist; motion command-list emotes likely animate if server emits them. Slash-command-to-emote wire and text-event-to-animation are missing. Evidence: - retail `ClientCommunicationSystem::DoEmote`, `HelpEmote`, `DoEmoteList`, `InitializeEmoteInputActionHash`; - retail `ClientCommunicationSystem::Pose` looks up a token in `ChatPoseTable`, issues the motion command locally, then sends SoulEmote; - `ChatEmoteData::Pack`; - ACE MotionCommand ChatEmote range. acdream locations: - `EmoteText` and `SoulEmote` top-level parsers; - `ChatLog.OnEmote` / `OnSoulEmote`; - `GameWindow.OnLiveMotionUpdated` command-list `PlayAction` route. Gaps: - no outbound `Communication_Emote (0x01DF)` or `Communication_SoulEmote (0x01E1)` GameAction builder found; - `MoveToState` currently writes zero command-list entries, so the client cannot yet send pose/emote commands in the retail motion-state path; - `ChatInputParser` has no `/em`, `/emote`, `/me`, `/sit`, `/kneel`, `/sleep`, or `/lie` parsing; - `EmoteText`/`SoulEmote` text events do not carry an emote id, so they should not be used as the primary animation source unless retail proves a deterministic text -> command mapping; - held postures need `SetCycle`, not `PlayAction`. Tests to add: - `MotionCommandResolver` reconstructs representative ChatEmotes; - command-list Wave routes to `PlayAction`; - persistent Sit/Meditate routes to `SetCycle`. ### 8. Death Animations Status: death chat and killer notifications exist; pose transition is missing. Evidence: - retail `CommandInterpreter::PlayerIsDead` checks forward command `0x40000011`; - MotionCommand `Sanctuary = 0x10000057` is an action and must not be used as the persistent death state; - MotionCommand `Dead = 0x40000011` and `Fallen = 0x40000008` are persistent states; - `PlayerKilled` top-level message and `KillerNotification (0x01AD)` are parsed/wired. acdream locations: - `PlayerKilled`, `ChatLog.OnPlayerKilled`; - `CombatState.OnKillerNotification`; - `MotionCommand.Dead` currently incorrectly comments `0x10000057` in `MotionInterpreter`; this should be split into `Sanctuary` action and `Dead` substate before death work. Gaps: - no explicit death animation coordinator; - no hit-direction-aware fall; - no dead-pose persistence or respawn reset. Tests to add: - death event plays Sanctuary then persists Dead/Fallen; - movement is blocked while Dead/Fallen; - respawn/reset returns to Ready. ### 9. Item-Use Animations Status: outbound use builders exist; local visible use animations are missing. Evidence: - retail `ItemHolder::UseObject`; - MotionCommand item/use states: Pickup, StoreInBackpack, Eat, Drink, Reading, HouseRecall, LifestoneRecall. acdream locations: - `InteractRequests.BuildUse` / `BuildUseWithTarget`; - `ItemRepository`, appraise/use-done event enum. Gaps: - no `WorldSession.SendUse` wrapper found; - `UseDone (0x01C7)` is enumerated but not parsed/wired; - `0xF754 PlayScriptId` is wired, but target anchoring and speed handling need audit; `0xF755 PlayScriptType` is not wired; - no item-class-to-motion mapping. Tests to add: - potion use maps to Drink; - food maps to Eat; - scroll/book maps to Reading; - recall spell/item maps to recall command once retail source is confirmed. ### 10. Mounting / Dismounting Status: not implemented; likely not relevant to retail AC character movement. Evidence: `r03` lists `Graze` as a monster-only/mount-like stance, but no player mount feature has been verified in retail references in this audit. Action: defer until a server/content feature requires it. Do not invent mounting behavior. ### 11. Floating-Point / Polish Status: partially implemented. Evidence: - `AnimationSequencer.MultiplyCyclicFramerate` exists and is tested; - `LocalAnimationSpeed` exists in `MovementResult`; - PosFrame deltas are accumulated in `AnimationSequencer`. Gaps: - root-motion deltas are not composed into entity/body transforms; - remote animation speed scaling is tied to ForwardSpeed/SidestepSpeed/TurnSpeed only when UpdateMotion carries them; - style-transition and modifier physics combination are incomplete. Tests to add: - same-motion/different-speed rescale remains green; - root-motion delta is consumed by an integration coordinator; - modifiers combine velocity/omega instead of replacing base cycle physics. ## Implementation Order 1. Extract an `AnimationCommandRouter` in Core/App-adjacent code that owns `SetCycle` vs `PlayAction` routing for full 32-bit commands. Move the command-list logic out of `GameWindow.OnLiveMotionUpdated` into tests. 2. Add missing MotionCommand constants and fix the `Dead`/`Sanctuary` distinction. 3. Fix combat wire conformance first: combat-mode enum, split attack builders, `CombatCommenceAttack`, `AttackDone`, damage/evasion/death notification parsers, and fixtures from ACE/holtburger. 4. Wire `CombatState.CurrentMode` and `WorldSession.SendAttack/ChangeCombatMode`; trigger local and remote swing overlays through the router. 5. Add spell-cast event/state wiring: `WorldSession.SendCast`, school enum conformance, `UpdateMotion` cast actions, spellcasting chat, `PlayScript.Fizzle`, `UseDone`, and errors. 6. Add outbound emote/soul-emote builders, MoveToState command-list emission, chat parser aliases, and posture routing. 7. Add item-use wrappers, `UseDone`, script target anchoring, and item-class-to-motion mapping. 8. Add death coordinator and respawn reset. 9. Port full ACE style-transition/modifier/action queue semantics into a `MotionTableWalker` or equivalent, replacing `SetCycle` special cases only after the category tests cover current behavior. 10. Apply/consume root motion where retail expects it; leave purely decorative PosFrames un-applied when decomp/ACE proves they should not move the body. ## Visual Sign-Off Points The agent can build, test, and live-launch autonomously, but these require user visual confirmation before claiming complete: - local attack swing + defender flinch; - local spell windup -> release/fizzle; - local `/wave` and persistent sit/lie/kneel/sleep; - local death pose and respawn recovery; - potion drink/eat/read animations; - remote observer view for all of the above.