Merge feature/animation-system-complete — Phase L.1c animation MVP
21 commits porting retail's MoveToManager-equivalent client-side behavior for server-controlled creature locomotion and combat engagement. Shipped as MVP after live visual verification across multiple iteration rounds with the user. Highlights: -186a584— initial Phase L.1c port: extracts Origin / target guid / MovementParameters block from MoveTo packets (movementType 6/7), adds RemoteMoveToDriver per-tick body-orientation steering with ±20° aux-turn-equivalent snap tolerance. -d247aef— corrected arrival predicate semantics + 1.5 s stale-destination timeout for entities leaving the streaming view. -f794832— root-caused "creature won't stop to attack" via two research subagents converging on retail CMotionInterp::move_to_interpreted_state's unconditional forward_command bulk-copy. Lifted ServerMoveToActive flag clearing + InterpretedState bulk-copy out of substate-only branch so Action-class swing UMs (mt=0 ForwardCommand=AttackHigh1) clear stale MoveTo state and zero forward velocity. -ff6d3d0— RemoteMoveToDriver.ClampApproachVelocity caps horizontal velocity at the final-approach tick so body lands EXACTLY at DistanceToObject instead of overshooting through the player. -37de771— bulk-copy ForwardCommand for MoveTo packets too (closed the regression where MoveTo creatures stayed at default ForwardCommand=Ready in InterpretedState and only translated via UpdatePosition snaps). -34d7f4d+e71ed73— AnimationSequencer.HasCycle query + fallback chain (requested → WalkForward → Ready → no-op) at BOTH the OnLiveMotionUpdated path AND the spawn handler. Prevents ClearCyclicTail from wiping the body's cyclic tail when ACE CreateObject carries CurrentMotionState.ForwardCommand pointing to an Action-class motion (e.g. AttackHigh1 from a mid-swing creature) which has no cyclic-table entry — was the "torso on the ground" symptom for monsters seen in combat by a fresh observer. Cross-references: docs/research/named-retail/acclient_2013_pseudo_c.txt (MoveToManager 0x00529680 + 0x0052a240 + 0x00529d80, CMotionInterp::move_to_interpreted_state 0x00528xxx, MovementParameters::UnPackNet 0x0052ac50), references/ACE/Source/ ACE.Server/Physics/Animation/MoveToManager.cs (port aid), references/holtburger/ (cross-check on snapshot-only client behavior), docs/research/2026-04-28-remote-moveto-pseudocode.md (the Phase L.1c pseudocode doc). Tests: 1404 → 1422 (parser type-7 path retention, type-6 target guid retention, driver arrival semantics, retail-faithful chase/flee branches, approach-velocity clamp scenarios, HasCycle present/missing, AttackHigh1 wire layout). Pending follow-ups (filed for future): target-guid live resolution for type 6 packets (residual chase lag), StickToObject sticky-target guid trailing field, full MoveToManager state machine port (CheckProgressMade stall detector, Sticky/StickTo, use_final_heading, pending_actions queue). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
b93dfe95d8
44 changed files with 4580 additions and 301 deletions
|
|
@ -306,6 +306,49 @@ with retail's MMB-hold mouse-look.
|
|||
|
||||
---
|
||||
|
||||
### Phase L.1 — Animation system completion
|
||||
|
||||
**Status:** IN PROGRESS on `feature/animation-system-complete`.
|
||||
|
||||
**Goal:** complete the retail-faithful animation surface beyond the
|
||||
locomotion/jump K-fix series: combat swings, spell casting, emotes, death,
|
||||
item-use, NPC/monster special actions, remote observer parity, and the
|
||||
remaining floating-point polish around style transitions, modifiers, action
|
||||
queues, speed scaling, and PosFrame root motion.
|
||||
|
||||
**Plan of record:** `docs/plans/animation-system-audit.md`.
|
||||
|
||||
**Sub-pieces:**
|
||||
- **L.1a — Audit & inventory.** Map retail named-decomp evidence, ACE
|
||||
cross-references, existing acdream hook points, and current gaps for each
|
||||
animation category. Output: `docs/plans/animation-system-audit.md`.
|
||||
- **L.1b — Command router + motion-state cleanup.** Extract tested
|
||||
`SetCycle` vs `PlayAction` routing, add missing `MotionCommand` constants,
|
||||
and split death `Sanctuary` action from persistent `Dead` substate.
|
||||
- **L.1c — Combat animation wiring.** Combat mode tracking, draw/sheath
|
||||
style transitions, attack swings by stance/power/height, hit reactions,
|
||||
evades/blocks/parries, and death handoff.
|
||||
- **L.1d — Spell casting wiring.** Cast command classification, windup,
|
||||
release, fizzle/interruption, recoil, and school/effect distinctions.
|
||||
- **L.1e — Emotes + postures.** Outbound slash emotes, inbound
|
||||
command-list emotes, and persistent sit/lie/kneel/sleep states.
|
||||
- **L.1f — NPC/monster + item-use coverage.** Scripted gestures, monster
|
||||
special actions, potion/food/scroll/recall cycles, and remote parity.
|
||||
- **L.1g — Polish + conformance.** Style-transition chain, durable
|
||||
modifiers/action queues, root-motion handling, speed scaling, and broad
|
||||
synthetic MotionTable tests.
|
||||
|
||||
**Acceptance:**
|
||||
- `dotnet build` and `dotnet test` green at each commit.
|
||||
- Test count grows by at least 30 with one representative cycle/action test
|
||||
per major animation category.
|
||||
- Every AC-specific behavior cites named retail decomp or ACE/holtburger
|
||||
cross-reference evidence in code comments, tests, or commit notes.
|
||||
- User visual sign-off for local and remote attack, spell, emote, death, and
|
||||
item-use animation parity before marking shipped.
|
||||
|
||||
---
|
||||
|
||||
### Phase J — Long-tail (deferred / low-priority)
|
||||
|
||||
Not detailed here; each gets its own brainstorm when it becomes relevant.
|
||||
|
|
|
|||
557
docs/plans/animation-system-audit.md
Normal file
557
docs/plans/animation-system-audit.md
Normal file
|
|
@ -0,0 +1,557 @@
|
|||
# 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.
|
||||
113
docs/research/2026-04-28-combat-animation-planner.md
Normal file
113
docs/research/2026-04-28-combat-animation-planner.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Combat Animation Planner Pseudocode
|
||||
|
||||
## Sources
|
||||
|
||||
- Retail `ClientCombatSystem::ExecuteAttack` (`0x0056BB70`): sends
|
||||
targeted melee or missile attack intent and records pending response state.
|
||||
It does not choose a local swing animation.
|
||||
- Retail `ClientCombatSystem::HandleCommenceAttackEvent` (`0x0056AD20`):
|
||||
starts/updates power-bar and busy UI state. The event carries no
|
||||
`MotionCommand`.
|
||||
- Retail command-name table around `0x00803F34`: combat commands include
|
||||
`Twitch1..4`, `StaggerBackward`, `StaggerForward`, `ThrustMed`,
|
||||
`SlashHigh`, `Shoot`, `AttackHigh1`, and later offhand/multistrike
|
||||
commands.
|
||||
- ACE `Player_Melee.DoSwingMotion` and `GetSwingAnimation`: server chooses
|
||||
a swing from `CombatManeuverTable.GetMotion(...)` and broadcasts the
|
||||
selected `MotionCommand` with `UpdateMotion`.
|
||||
- ACE `CombatManeuverTable.GetMotion`: indexes `(stance, attack height,
|
||||
attack type)` to one or more motion commands; power level chooses between
|
||||
multiple entries.
|
||||
|
||||
## Retail Rule
|
||||
|
||||
Combat GameEvents are state/UI notifications. Motion state is the animation
|
||||
authority.
|
||||
|
||||
## Pseudocode
|
||||
|
||||
```text
|
||||
PlanForEvent(event):
|
||||
return None
|
||||
|
||||
PlanFromWireCommand(wireCommand, speed):
|
||||
fullCommand = MotionCommandResolver.ReconstructFullCommand(wireCommand)
|
||||
return PlanFromFullCommand(fullCommand, speed)
|
||||
|
||||
PlanFromFullCommand(fullCommand, speed):
|
||||
kind = ClassifyMotionCommand(fullCommand)
|
||||
if kind is None:
|
||||
return None
|
||||
|
||||
routeKind = AnimationCommandRouter.Classify(fullCommand)
|
||||
return Plan(kind, routeKind, fullCommand, speed)
|
||||
|
||||
ClassifyMotionCommand(fullCommand):
|
||||
if command is a combat stance:
|
||||
return CombatStance
|
||||
if command is a thrust/slash/backhand/offhand/multistrike motion:
|
||||
return MeleeSwing
|
||||
if command is Shoot, MissileAttack*, or Reload:
|
||||
return MissileAttack
|
||||
if command is AttackHigh/Med/Low 1..6:
|
||||
return CreatureAttack
|
||||
if command is CastSpell, UseMagicStaff, or UseMagicWand:
|
||||
return SpellCast
|
||||
if command is Twitch*, Stagger*, FallDown, or Sanctuary:
|
||||
return HitReaction
|
||||
if command is Dead:
|
||||
return Death
|
||||
return None
|
||||
```
|
||||
|
||||
## Maneuver Selection Pseudocode
|
||||
|
||||
```text
|
||||
SelectMotion(table, stance, attackHeight, attackType, powerLevel,
|
||||
isThrustSlashWeapon):
|
||||
candidates = []
|
||||
for maneuver in table.CombatManeuvers:
|
||||
if maneuver.Style == stance
|
||||
and maneuver.AttackHeight == attackHeight
|
||||
and maneuver.AttackType == attackType:
|
||||
candidates.append(maneuver.Motion)
|
||||
|
||||
if candidates is empty:
|
||||
return None
|
||||
|
||||
subdivision = isThrustSlashWeapon ? 0.66 : 0.33
|
||||
|
||||
if candidates.Count > 1 and powerLevel < subdivision:
|
||||
motion = candidates[1]
|
||||
else:
|
||||
motion = candidates[0]
|
||||
|
||||
return motion
|
||||
```
|
||||
|
||||
This matches ACE `CombatManeuverTable.GetMotion` plus
|
||||
`Player_Melee.GetSwingAnimation`. The `prevMotion` parameter is present in
|
||||
ACE's table API but the current ACE implementation does not use it; the
|
||||
power threshold chooses between multiple entries.
|
||||
|
||||
## Named Retail Motion IDs
|
||||
|
||||
`DatReaderWriter.Enums.MotionCommand` is shifted by three entries starting
|
||||
at `AllegianceHometownRecall`. Named retail command tables are:
|
||||
|
||||
- `command_ids` table lines 1017626-1017658:
|
||||
`0x016E..0x0197 -> 0x1000016E..0x10000197`.
|
||||
- command-name table lines 1068272-1068313:
|
||||
`OffhandSlashHigh = 0x10000170`, `AttackLow6 = 0x1000018B`,
|
||||
`PunchFastLow = 0x1000018E`, etc.
|
||||
|
||||
`MotionCommandResolver` therefore overrides that range after building the
|
||||
DRW reflection table, otherwise offhand and late unarmed attack actions
|
||||
resolve as UI/mappable commands and never reach `PlayAction`.
|
||||
|
||||
## Implementation Note
|
||||
|
||||
The next table-driven layer can use `DatReaderWriter.DBObjs.CombatTable`
|
||||
and `DatReaderWriter.Types.CombatManeuver` directly. acdream already
|
||||
references `Chorizite.DatReaderWriter`; the missing live-state piece is a
|
||||
named `CombatTable` data-id on player/creature state.
|
||||
285
docs/research/2026-04-28-remote-moveto-pseudocode.md
Normal file
285
docs/research/2026-04-28-remote-moveto-pseudocode.md
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
# Phase L.1c — Remote MoveTo body-driver pseudocode
|
||||
|
||||
**Date**: 2026-04-28
|
||||
**Goal**: Port the minimum viable subset of retail `MoveToManager` so the body
|
||||
position of server-controlled chasing creatures (movementType 6/7) tracks the
|
||||
server-supplied destination smoothly, instead of freezing at zero velocity
|
||||
between sparse `UpdatePosition` snaps.
|
||||
|
||||
## Problem (root cause from systematic-debugging Phase 1)
|
||||
|
||||
The 882a07c stabilizer holds `rm.Body.Velocity = 0` while `ServerMoveToActive`
|
||||
is true, on the principle "do not let `apply_current_movement` free-run with
|
||||
incomplete MoveTo state." The state IS incomplete: our parser at
|
||||
[`UpdateMotion.cs:280-290`](../../src/AcDream.Core.Net/Messages/UpdateMotion.cs)
|
||||
keeps only `speed`/`runRate`/flags from the 7-DWORD `MovementParameters`
|
||||
block and the `runRate` trailer, **discarding** `Origin (destination)`,
|
||||
`targetGuid` (type 6 only), `distance_to_object`, `min_distance`,
|
||||
`fail_distance`, `walk_run_threshhold`, and `desired_heading`.
|
||||
|
||||
Symptoms (live log + user observation 2026-04-28):
|
||||
- **Disappearing**: body frozen at `Velocity=0` while RunForward animation
|
||||
plays; next UpdatePosition teleports body to actual server pose. If the
|
||||
teleport target is outside the visible window, observer sees disappear/reappear.
|
||||
- **Jitter**: when a stale UP-derived velocity exists, body extrapolates along
|
||||
the OLD heading; meanwhile the server is steering the creature on a curve.
|
||||
Each new UP snap-corrects → visible stutter.
|
||||
|
||||
The fresh MoveTo packet stream (~1 Hz, seq 0x01FE→0x0204 in the live log) IS
|
||||
sending fresh target positions and headings each tick — we're throwing them
|
||||
away.
|
||||
|
||||
## Retail behavior (named decomp + ACE port)
|
||||
|
||||
Sources:
|
||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` — citations below
|
||||
- `docs/research/named-retail/acclient.h` — struct definitions
|
||||
- `references/ACE/Source/ACE.Server/Physics/Animation/MoveToManager.cs`
|
||||
- `references/ACE/Source/ACE.Server/Physics/Animation/MovementParameters.cs`
|
||||
|
||||
### Wire layout (`MovementParameters::UnPackNet` @ `0x0052ac50`, type 6/7)
|
||||
|
||||
```
|
||||
[uint targetGuid] // type 6 only (MoveToObject)
|
||||
Origin: uint cellId // then 3 floats local x/y/z
|
||||
float x, y, z // destination position
|
||||
MovementParameters (28 bytes, exact retail order):
|
||||
uint flags // bitfield (see below)
|
||||
float distance_to_object // arrival far-bound (ACE default 0.6)
|
||||
float min_distance // arrival near-bound
|
||||
float fail_distance // abort when starting→current >= this
|
||||
float speed // base speed multiplier
|
||||
float walk_run_threshhold // (sic, two h's) — wire default 15.0
|
||||
float desired_heading // final orientation (radians or degrees)
|
||||
float runRate // CMotionInterp::my_run_rate copy
|
||||
```
|
||||
|
||||
### MovementParameters bit-flags (declaration order, acclient.h:31423-31443)
|
||||
|
||||
| Bit | Mask | Name | Meaning |
|
||||
|----:|---------|------|---------|
|
||||
| 0 | 0x00001 | can_walk | gait permission |
|
||||
| 1 | 0x00002 | can_run | gait permission (we already use this for `MoveToCanRun`) |
|
||||
| 2 | 0x00004 | can_sidestep | enables strafe path |
|
||||
| 3 | 0x00008 | can_walk_backwards | gait permission |
|
||||
| 4 | 0x00010 | can_charge | force HoldKey_Run |
|
||||
| 5 | 0x00020 | fail_walk | fail if only walk possible |
|
||||
| 6 | 0x00040 | use_final_heading | append final TurnToHeading after arrival |
|
||||
| 7 | 0x00080 | sticky | MoveToObject only — StickTo on completion |
|
||||
| 8 | 0x00100 | move_away | flee target |
|
||||
| 9 | 0x00200 | move_towards | chase target (chase creatures set this) |
|
||||
| 10 | 0x00400 | use_spheres | use cylinder distance vs straight-line |
|
||||
| 11 | 0x00800 | set_hold_key | apply HoldKeyToApply |
|
||||
| ... | ... | ... | (autonomous, modify_*_state, cancel_moveto, stop_completely, disable_jump) |
|
||||
|
||||
### MoveToManager::HandleMoveToPosition (per-tick, `0x00529d80` lines 307187-307440)
|
||||
|
||||
```
|
||||
if physics.motions_pending:
|
||||
cancel any aux turn cmd (let the queued motion complete)
|
||||
else:
|
||||
targetWorld = currentTargetPosition // last server-supplied destination
|
||||
desiredHeading = atan2(targetWorld - body.position) + get_desired_heading(currentCmd)
|
||||
headingDelta = normalize(desiredHeading - body.heading)
|
||||
if |headingDelta| <= 20°: // retail tolerance
|
||||
// ACE adds set_heading(target, true) here (server-tic-rate fudge)
|
||||
cancel any aux turn cmd
|
||||
else:
|
||||
edi = (headingDelta < 180°) ? TurnLeft : TurnRight
|
||||
if edi != auxCommand:
|
||||
_DoMotion(edi) // -> CMotionInterp
|
||||
auxCommand = edi
|
||||
dist = GetCurrentDistance()
|
||||
if CheckProgressMade(dist):
|
||||
if !movingAway and dist <= min_distance: // arrived
|
||||
popHeadNode(); _StopMotion(currentCmd); _StopMotion(auxCommand); BeginNextNode()
|
||||
if movingAway and dist >= distance_to_object:
|
||||
popHeadNode(); ...
|
||||
if !movingAway and Position.distance(starting, current) >= fail_distance:
|
||||
CancelMoveTo(0x3d) // YouChargedTooFar
|
||||
```
|
||||
|
||||
### Key insight: MoveToManager does NOT touch the body directly
|
||||
|
||||
Every motion start/stop is dispatched through `CMotionInterp::DoInterpretedMotion`
|
||||
(via `_DoMotion`/`_StopMotion`). The body's actual position evolves via the
|
||||
ordinary physics tick (`PhysicsBody::UpdatePhysicsInternal`). MoveToManager is
|
||||
purely a *planner* sitting above CMotionInterp, deciding *which command* (and
|
||||
which auxiliary turn) the body should be running at any given tick.
|
||||
|
||||
## Acdream port — minimum viable subset
|
||||
|
||||
The server re-emits MoveTo packets ~1 Hz with fresh destinations, so we can
|
||||
skip:
|
||||
- `MoveToObject_Internal` target-tracking (`HandleUpdateTarget`) — server does it
|
||||
- Sticky / `PositionManager::StickTo`
|
||||
- `CheckProgressMade` stall detection — server cancels the move
|
||||
- `fail_distance` / `WeenieError.YouChargedTooFar` — server-side concern
|
||||
- `WeenieObj::OnMoveComplete` callback
|
||||
- Pending-actions queue (only ever 1-2 nodes; we treat each MoveTo packet as
|
||||
a fresh single-step plan)
|
||||
|
||||
We DO need:
|
||||
1. **Parser**: extract the discarded fields into `ServerMotionState`.
|
||||
2. **Per-tick steer**: compute heading-to-destination, turn body orientation
|
||||
toward it (snap when within ±20° per ACE's tic-rate fudge), then *allow*
|
||||
`apply_current_movement` to run — which sets `Body.Velocity` from the
|
||||
active RunForward cycle, oriented along the now-correct heading.
|
||||
3. **Arrival**: when `dist <= distance_to_object`, switch animation to Ready
|
||||
and clear `ServerMoveToActive`. Server's next MoveTo packet will resume.
|
||||
|
||||
## Pseudocode — acdream port
|
||||
|
||||
### Parser change (`UpdateMotion.TryParseMoveToPayload`)
|
||||
|
||||
```
|
||||
TryParseMoveToPayload(body, pos, mt, out parsed):
|
||||
if mt == 6:
|
||||
if rem < 4: return false
|
||||
parsed.TargetGuid = ReadU32; pos += 4
|
||||
|
||||
if rem < 16: return false
|
||||
parsed.OriginCellId = ReadU32; pos += 4
|
||||
parsed.OriginX = ReadF32; pos += 4
|
||||
parsed.OriginY = ReadF32; pos += 4
|
||||
parsed.OriginZ = ReadF32; pos += 4
|
||||
|
||||
if rem < 28: return false
|
||||
parsed.Flags = ReadU32; pos += 4
|
||||
parsed.DistanceToObject = ReadF32; pos += 4
|
||||
parsed.MinDistance = ReadF32; pos += 4
|
||||
parsed.FailDistance = ReadF32; pos += 4
|
||||
parsed.Speed = ReadF32; pos += 4
|
||||
parsed.WalkRunThreshold = ReadF32; pos += 4
|
||||
parsed.DesiredHeading = ReadF32; pos += 4
|
||||
|
||||
if rem < 4: return false
|
||||
parsed.RunRate = ReadF32
|
||||
return true
|
||||
```
|
||||
|
||||
### Per-tick driver (new `RemoteMoveToDriver` in `AcDream.Core.Physics`)
|
||||
|
||||
```
|
||||
DriveOneTick(rm, dt):
|
||||
if not rm.HasMoveToDestination: return ApplyDefault
|
||||
|
||||
targetWorld = rm.MoveToDestinationWorld // pre-converted at packet time
|
||||
bodyPos = rm.Body.Position
|
||||
|
||||
// Distance check first — arrival short-circuits before any heading work
|
||||
dist = horizontalDistance(targetWorld, bodyPos)
|
||||
if dist <= rm.MoveToMinDistance + 0.05 (epsilon for float wobble):
|
||||
rm.HasMoveToDestination = false
|
||||
// animation cycle moves to Ready via the existing
|
||||
// ApplyServerControlledVelocityCycle path on next zero-velocity sample
|
||||
rm.Body.Velocity = Vector3.Zero
|
||||
return Arrived
|
||||
|
||||
// Heading compute (XY plane; Z untouched — server owns Z)
|
||||
deltaXY = (targetWorld.XY - bodyPos.XY).Normalized
|
||||
desiredHeading = atan2(deltaXY) // radians
|
||||
currentHeading = QuaternionToYaw(rm.Body.Orientation)
|
||||
headingDelta = wrapPi(desiredHeading - currentHeading)
|
||||
|
||||
// Snap orientation toward target — match ACE's set_heading(target, true)
|
||||
// when within tolerance, otherwise rotate at retail-faithful turn rate.
|
||||
const float tolerance = 20° (in radians)
|
||||
if |headingDelta| <= tolerance:
|
||||
rm.Body.Orientation = QuaternionFromYaw(desiredHeading)
|
||||
else:
|
||||
// retail TurnSpeed default ≈ π/2 rad/s for monsters; clamp by dt
|
||||
float maxStep = TurnRateRadPerSec * dt
|
||||
float step = clamp(headingDelta, -maxStep, +maxStep)
|
||||
rm.Body.Orientation = QuaternionFromYaw(currentHeading + step)
|
||||
|
||||
// Allow apply_current_movement to set Velocity from RunForward cycle.
|
||||
// The cycle was already seeded by PlanMoveToStart at packet receipt
|
||||
// and is being played by the AnimationSequencer. CMotionInterp's
|
||||
// apply_current_movement reads InterpretedState.ForwardCommand and
|
||||
// sets Body.Velocity = (forward axis of orientation) * RunAnimSpeed * speedMod.
|
||||
return DriveActive // caller now invokes apply_current_movement
|
||||
```
|
||||
|
||||
### Integration in `GameWindow.OnUpdateMotion` (movementType 6/7 branch)
|
||||
|
||||
```
|
||||
on receipt of MoveTo packet:
|
||||
// existing code already seeds the animation cycle via PlanMoveToStart
|
||||
// NEW: store world-converted destination + thresholds on rmState
|
||||
lbX = (originCellId >> 24) & 0xFF
|
||||
lbY = (originCellId >> 16) & 0xFF
|
||||
origin = ((lbX - liveCenterX) * 192, (lbY - liveCenterY) * 192, 0)
|
||||
rmState.MoveToDestinationWorld = (originX, originY, originZ) + origin
|
||||
rmState.MoveToMinDistance = parsed.MinDistance
|
||||
rmState.MoveToDistanceToObject = parsed.DistanceToObject
|
||||
rmState.HasMoveToDestination = true
|
||||
// ServerMoveToActive remains set; existing
|
||||
```
|
||||
|
||||
### Integration in per-tick remote update (`GameWindow.cs` ~line 5045)
|
||||
|
||||
```
|
||||
// Replace the current Velocity = Zero hold with:
|
||||
else if (!IsPlayerGuid(serverGuid) && rm.ServerMoveToActive && rm.HasMoveToDestination)
|
||||
{
|
||||
var driveResult = RemoteMoveToDriver.DriveOneTick(rm, dt);
|
||||
if driveResult == Arrived:
|
||||
// signal cycle update to Ready via existing path
|
||||
ApplyServerControlledVelocityCycle(serverGuid, ae, rm, Vector3.Zero);
|
||||
else:
|
||||
rm.Body.TransientState |= Contact | OnWalkable | Active
|
||||
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
||||
}
|
||||
else if (!IsPlayerGuid(serverGuid) && rm.ServerMoveToActive)
|
||||
{
|
||||
// No destination yet (very early frame, packet hasn't fully landed)
|
||||
rm.Body.Velocity = Vector3.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
||||
}
|
||||
```
|
||||
|
||||
## Conformance test cases
|
||||
|
||||
1. **Parser round-trip — type 7 (MoveToPosition)**
|
||||
- Synthesize a 68-byte body with known origin + 7 params + runRate.
|
||||
- Assert all 9 new fields decode correctly.
|
||||
|
||||
2. **Parser round-trip — type 6 (MoveToObject)**
|
||||
- Synthesize a 72-byte body with target guid + origin + params + runRate.
|
||||
- Assert TargetGuid populated and shifts subsequent fields by 4 bytes.
|
||||
|
||||
3. **DriveOneTick — heading snap within tolerance**
|
||||
- body at (0,0,0) facing east, destination (10,0,0).
|
||||
- DesiredHeading=0; current=0; |delta|=0 ≤ 20° → snap.
|
||||
- assert orientation unchanged (already correct).
|
||||
|
||||
4. **DriveOneTick — heading turn beyond tolerance**
|
||||
- body at (0,0,0) facing east, destination (0,10,0).
|
||||
- desiredHeading=π/2; current=0; |delta|=π/2 > 20°.
|
||||
- dt=0.1s, TurnRate=π/2 → step = π/4 toward target.
|
||||
- assert orientation rotated by π/4 (not full snap).
|
||||
|
||||
5. **DriveOneTick — arrival**
|
||||
- body at (0,0,0), destination (0.4,0,0), MinDistance=0.6.
|
||||
- assert HasMoveToDestination cleared and Velocity zeroed.
|
||||
|
||||
6. **Bit-flag mapping** (already partially tested via `MoveToCanRun`)
|
||||
- assert flag 0x00200 (move_towards) is detected as `MoveTowards=true`.
|
||||
|
||||
## Out of scope (future Phase L.1d if needed)
|
||||
|
||||
- Sticky / StickTo for MoveToObject completion
|
||||
- `use_final_heading` (post-arrival turn-to-heading)
|
||||
- `fail_distance` early-cancel (server already does this; we just don't flag it)
|
||||
- `CheckProgressMade` stall detector
|
||||
- Strafe / move_away / move_towards-and-away combo (`towards_and_away` helper)
|
||||
- Sphere-cylinder distance (`use_spheres` bit)
|
||||
- `MoveToObject` target-guid resolution — currently we only honor the Origin,
|
||||
which works because the server re-emits with refreshed Origin each tick.
|
||||
If the target is moving fast and the server's emit cadence falls behind,
|
||||
we'd see lag; a future enhancement is to look up the target entity by
|
||||
guid and use its current world position when fresher than Origin.
|
||||
Loading…
Add table
Add a link
Reference in a new issue