acdream/docs/plans/animation-system-audit.md

22 KiB

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.