acdream/docs/research/acclient_animation_map.md
Erik 67b51a3e6f fix(anim): implement adjust_motion — TurnLeft/SideStepLeft play backward
ROOT CAUSE FIX for missing left-side animations.

The AC client's MotionTable has NO cycles for TurnLeft (0x000E),
SideStepLeft (0x0010), or WalkBackward (0x0006). The real client
calls adjust_motion() which remaps these to their right-side
equivalents with NEGATIVE speed before looking up the cycle. Then
multiply_framerate() swaps LowFrame↔HighFrame so the animation
plays backward.

Source: ACE MotionInterp.cs:394-428, decompiled FUN_005267E0.

Changes:
- AnimationSequencer.SetCycle: adds adjust_motion block that remaps
  left→right with speed *= -1 (TurnLeft, SideStepLeft) or
  speed *= -0.65 (WalkBackward = BackwardsFactor)
- LoadAnimNode: when framerate < 0, swaps Low↔High (matching the
  decompiled multiply_framerate)
- GameWindow.UpdatePlayerAnimation: passes original animCommand to
  SetCycle (sequencer handles remapping internally), keeps legacy
  fallback for non-sequencer entities

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:17:26 +02:00

279 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# acclient.exe Animation System Function Map
Mapped from decompiled C chunks against ACE's C# animation port.
Cross-referenced with `references/ACE/Source/ACE.Server/Physics/`.
---
## CPartArray (PhysicsObj+0x10 ptr)
PartArray is heap-allocated and accessed via a pointer stored at `PhysicsObj+0x10`.
The Sequence is embedded inside PartArray at `PartArray+0x08`.
### Struct Layout
| Offset | Field | Type | Notes |
|--------|-------|------|-------|
| +0x00 | State | uint | Flags (0x10000 = HasPhysicsBSP) |
| +0x04 | Owner | ptr | PhysicsObj* |
| +0x08 | Sequence | embedded | 68 bytes of Sequence struct |
| +0x50 | MotionTableManager | ptr | null if no motion table |
| +0x54 | Setup | ptr | CSetup dat object |
| +0x58 | NumParts | uint | Part count |
| +0x5c | Parts | ptr | Array of PhysicsPart* |
| +0x60 | Scale | float[3] | XYZ scale (default 1,1,1) |
| +0x70 | ShadowParts | ptr | Shadow part array |
### Functions (chunk_00510000.c)
| Address | FUN name | ACE method | Description |
|---------|----------|-----------|-------------|
| 0x510140 | FUN_00510140 | PartArray::UpdateParts | Calls SetFrame when anim flag set |
| 0x512DE0 | FUN_00512de0 | AFrame::Combine | Quaternion compose + position transform |
| 0x513730 | FUN_00513730 | PhysicsObj::UpdatePositionInternal | Advances physics: calls PartArray::Update then SetFrame |
| 0x514B90 | FUN_00514b90 | PhysicsObj::UpdateObjectInternal | Calls PartArray::Update (SetFrame) |
| 0x518790 | FUN_00518790 | PartArray::CopyShadowParts | Copies frame data to shadow parts array (stride 0x48/part) |
| 0x5188E0 | FUN_005188e0 | PartArray::Update | Thin wrapper: calls Sequence::Update (FUN_00526780) |
| 0x519B00 | FUN_00519b00 | PartArray::SetPartFrame | Per-part transform: matrix3×3 + quaternion multiply → part position |
| 0x519C20 | FUN_00519c20 | PartArray::UpdateParts | Loops over NumParts (stride 0x1c per anim frame part), calls SetPartFrame |
| 0x519E40 | FUN_00519e40 | PartArray::SetFrame | Top-level frame apply: calls UpdateParts + CopyShadowParts |
**Key pattern in FUN_00519b00** (SetPartFrame):
Computes `pos = matrix3x3 * scale + parentPos`, then calls `FUN_00535dc0` (quaternion normalize) to compose rotation. The matrix at `animFrame[partIdx]` has 16 floats:
`[0..3]`=quaternion, `[4..0xc]`=rotation matrix (computed from quat), `[0xd..0xf]`=XYZ position.
---
## CSequence (embedded at PartArray+0x08)
### Struct Layout
| Offset | Field | Type | Notes |
|--------|-------|------|-------|
| +0x00 | vtable | ptr | Points to &PTR_FUN_007c92a0 |
| +0x04 | AnimList.Head | ptr | First node in linked list |
| +0x08 | AnimList.Tail | ptr | Last node sentinel |
| +0x0c | FirstCyclic | ptr | Node before first cyclic anim |
| +0x10 | Velocity.X | float | |
| +0x14 | Velocity.Y | float | |
| +0x18 | Velocity.Z | float | |
| +0x1c | Omega.X | float | Angular velocity |
| +0x20 | Omega.Y | float | |
| +0x24 | Omega.Z | float | |
| +0x28 | HookObj | ptr | PhysicsObj for animation hooks |
| +0x30 | FrameNumber | double | Current frame (fractional) |
| +0x38 | CurrAnim | ptr | Current AnimSequenceNode* |
| +0x3c | PlacementFrame | ptr | AnimationFrame* (used when no anims) |
| +0x40 | PlacementFrameID | int | Placement ID (e.g. 0x65) |
Total size: ~0x44 = 68 bytes.
### Functions (chunk_00520000.c)
| Address | FUN name | ACE method | Description |
|---------|----------|-----------|-------------|
| 0x5254C0 | FUN_005254c0 | Sequence::CombinePhysics | Adds velocity+omega to sequence totals |
| 0x525500 | FUN_00525500 | Sequence::subtract_physics | Subtracts velocity+omega |
| 0x525540 | FUN_00525540 | Sequence::remove_all_link_animations | Removes all pre-cyclic nodes from list |
| 0x525570 | FUN_00525570 | Sequence::GetCurrAnimFrame | If CurrAnim≠null: floor(FrameNumber)→get_part_frame; else PlacementFrame |
| 0x5255B0 | FUN_005255b0 | Sequence::SetPlacementFrame | Sets PlacementFrame ptr (+0x3c) and ID (+0x40) |
| 0x5255D0 | FUN_005255d0 | Sequence::get_curr_frame_number | floor(FrameNumber) |
| 0x5255F0 | FUN_005255f0 | Sequence::Init | Zero-initializes all fields, sets vtable |
| 0x525630 | FUN_00525630 | Sequence::clear_animations | Walks linked list, releases all nodes |
| 0x5256B0 | FUN_005256b0 | Sequence::apply_physics | frame.Origin += Velocity\*quantum; frame.Rotate(Omega\*quantum) |
| 0x525740 | FUN_00525740 | Sequence::apricot | Removes consumed nodes up to CurrAnim |
| 0x525EB0 | FUN_00525eb0 | Sequence::advance_to_next_animation | Moves CurrAnim to next/prev node or wraps to FirstCyclic |
| 0x526110 | FUN_00526110 | Sequence::append_animation | Allocates AnimSequenceNode (0x1c bytes), links at tail, sets FirstCyclic and CurrAnim if first |
| 0x5261B0 | FUN_005261b0 | Sequence::Clear | Calls clear_animations + clear_physics, zeroes PlacementFrame |
| 0x5261D0 | FUN_005261d0 | Sequence::update_internal | **Core update loop**: `FrameNumber += Framerate * elapsed`; handles forward/backward; fires hooks; calls advance_to_next_animation when done |
| 0x526780 | FUN_00526780 | Sequence::Update | Wrapper: if AnimList not empty → update_internal + apricot; else apply_physics only |
**Key pattern in FUN_005261d0** (update_internal):
```c
// Forward direction (frametime > 0):
while (floor(frameNum) > lastFrame) {
apply_physics + execute_hooks(lastFrame);
lastFrame++;
}
if (floor(frameNum) > get_high_frame()) {
frameTimeElapsed = (frameNum - get_high_frame() - 1.0) / framerate;
frameNum = get_high_frame();
advance_to_next_animation(timeElapsed, ...);
recurse with remaining time
}
```
---
## CAnimSequenceNode (linked list node, 0x1c bytes)
### Struct Layout
| Offset | Field | Type | Notes |
|--------|-------|------|-------|
| +0x00 | prev | ptr | Previous linked list node |
| +0x04 | next | ptr | Next linked list node |
| +0x08 | (sentinel) | ptr | |
| +0x0c | Anim | ptr | Animation* dat object |
| +0x10 | Framerate | float | Frames per second (default 30.0) |
| +0x14 | LowFrame | int | Start frame |
| +0x18 | HighFrame | int | End frame (-1 = use all) |
Total size: 0x1c = 28 bytes (heap allocated via `FUN_005df0f5(0x1c)`).
### Functions (chunk_00520000.c)
| Address | FUN name | ACE method | Description |
|---------|----------|-----------|-------------|
| 0x526810 | FUN_00526810 | AnimSequenceNode::get_pos_frame | Returns PosFrames[frameIdx] at ptr+frame\*0x1c |
| 0x526840 | FUN_00526840 | AnimSequenceNode::get_part_frame | Returns PartFrames[frameIdx] at ptr+frame\*0x10 |
| 0x526870 | FUN_00526870 | AnimSequenceNode::has_anim | Returns Anim != null |
| 0x526880 | FUN_00526880 | AnimSequenceNode::get_starting_frame | LowFrame if Framerate≥0, else HighFrame+1-ε |
| 0x5268B0 | FUN_005268b0 | AnimSequenceNode::get_ending_frame | HighFrame+1-ε if Framerate≥0, else LowFrame |
| 0x5267E0 | FUN_005267e0 | AnimSequenceNode::multiply_framerate | Swaps Low/High if multiplier<0; Framerate \*= multiplier |
**Animation dat object layout** (referenced from AnimSequenceNode+0x0c):
- `Anim+0x38`: PosFrames array ptr (AFrame per frame, stride 0x1c)
- `Anim+0x3c`: PartFrames array ptr (AnimationFrame per frame, stride 0x10 per part)
- `Anim+0x48`: NumFrames (int)
---
## MotionTable / MotionTableManager (chunk_00520000.c)
### Cycle Key Construction
The cycle key is built as: `(motionCategory << 16) | (command & 0xFFFFFF)`
Confirmed at lines 2469, 2474, 2491, 2551, 2597, 2660, 2699 in chunk_00520000.c:
```c
key = param_4 & 0xffffff | param_1 << 0x10;
// or equivalently:
key = (int)*param_3 << 0x10 | (uint)param_2 & 0xffffff;
```
### Hash Map Structure (used for all MotionTable lookups)
The hash map has:
- `+0x04`: hash mask (capacity - 1)
- `+0x08`: hash shift
- `+0x0c`: bucket array ptr
- Each bucket is a linked list of nodes; each node has key at `+0x08`, value at `+0x0c`, next at `+0x04`
Hash formula: `bucketIdx = (key >> hashShift ^ key) & hashMask`
### Functions
| Address | FUN name | ACE method | Description |
|---------|----------|-----------|-------------|
| 0x520A00 | FUN_00520a00 | MotionTableManager::Init | Initializes all MTM fields (large constructor) |
| 0x520BB0 | FUN_00520bb0 | MotionTableManager::Create | Factory: allocates 0xd8 bytes, calls Init |
| 0x521770 | FUN_00521770 | MotionTable::unpack | Deserializes MotionTable from dat stream |
| 0x521F10 | FUN_00521f10 | MotionTable::find_motion | Hash map lookup by motionID |
| 0x521F60 | FUN_00521f60 | MotionTable::destructor | Frees all MotionTable allocations |
| 0x522E30 | FUN_00522e30 | MotionTableManager::SetCurrentMotion | Sets current state node |
| 0x523000 | FUN_00523000 | MotionTable::add_animations_to_sequence | Applies velocity/omega + appends AnimData to Sequence |
| 0x5230D0 | FUN_005230d0 | MotionTable::set_physics | Sets velocity+omega on Sequence only |
| 0x523150 | FUN_00523150 | MotionTable::subtract_physics | Subtracts velocity+omega from Sequence |
| 0x5231D0 | FUN_005231d0 | MotionTable::FindObjectAndMotionData | Hash lookup returning node ptr-4 |
| 0x523210 | FUN_00523210 | MotionTable::FindObjectAndCycle | Hash lookup returning cycle data at node+0xc |
| 0x523260 | FUN_00523260 | MotionTable::CheckAlreadySet | Checks if motion already current at same speed |
| 0x5232B0 | FUN_005232b0 | MotionTable::GetObjectAnimData | Builds cycle key (`src<<16 \| dst&0xFFFFFF`), calls FindObjectAndMotionData |
| 0x523400 | FUN_00523400 | MotionTableManager::PerformMovement | **Core motion dispatch**: transitions between states, populates Sequence |
**FUN_00523400 (PerformMovement) key pattern**:
```c
// Transition key for cycle lookup:
key = (int)*param_3 << 0x10 | (uint)local_8 & 0xffffff;
anim_data = FUN_005231d0(key); // FindObjectAndMotionData
// Then:
FUN_00523000(sequence, link_anim, speed); // add link animations
FUN_00523000(sequence, from_anim, speed); // add departure animation
FUN_00523000(sequence, to_anim, speed); // add transition animation
FUN_00523000(sequence, cycle_anim, speed); // add cyclic animation
```
---
## AFrame (chunk_00530000.c)
AFrame is a combined rotation+position: quaternion (4 floats) + 3×3 matrix (derived) + XYZ (3 floats).
### Struct Layout (0x1c = 28 bytes total)
| Offset | Field | Type |
|--------|-------|------|
| +0x00..+0x0c | Quaternion W,X,Y,Z | float[4] |
| +0x10..+0x30 | Matrix 3×3 (derived) | float[9] |
| +0x34..+0x3c | Origin XYZ | float[3] |
Note: In Ghidra decompiled code the struct appears as offsets in uint32s:
`[0..3]` = quaternion, `[4..0xc]` = matrix, `[0xd..0xf]` = origin.
### Functions
| Address | FUN name | ACE method | Description |
|---------|----------|-----------|-------------|
| 0x535B30 | FUN_00535b30 | AFrame::update_matrix | Quaternion 3×3 rotation matrix (stores in [4..0xc]) |
| 0x535C10 | FUN_00535c10 | AFrame::is_valid | Checks NaN in origin [0xd..0xf] and quaternion [0..3] |
| 0x535DC0 | FUN_00535dc0 | AFrame::normalize_quaternion | Normalizes quat: each *= 1/sqrt(dot); validates with is_valid |
| 0x535E70 | FUN_00535e70 | AFrame::pack_quaternion | Serializes quaternion [0..3] + origin [0xd..0xf] to byte stream |
---
## PhysicsObj Animation-Relevant Offsets (chunk_00510000.c)
| Offset | Field | Notes |
|--------|-------|-------|
| +0x10 | PartArray ptr | null = no animation; checked before every anim call |
| +0x50 | Position.Frame | AFrame (embedded); updated by FUN_00512de0 (AFrame::Combine) |
| +0xA8 | State | bit 0x4000 = no-animate flag; bit 0x1000 = invisible |
| +0xD8 | LastUpdateTime | double; used to compute dt for Sequence::Update |
| +0x114 | RunRate | float; velocity scale applied to omega after anim update |
---
## Call Chain: Per-Frame Animation Update
```
PhysicsEngine::update (FUN_00452A10)
→ PhysicsObj::update_object (FUN_00515020)
→ PhysicsObj::UpdatePositionInternal (FUN_00513730)
→ PartArray::Update (FUN_005188E0) [if PhysicsObj+0x10 != null]
→ Sequence::Update (FUN_00526780)
→ Sequence::update_internal (FUN_005261D0)
→ AnimSequenceNode::get_part_frame (FUN_00526840)
→ Sequence::execute_hooks (via FUN_00525430)
→ Sequence::advance_to_next_animation (FUN_00525EB0)
→ AnimSequenceNode::get_starting_frame (FUN_00526880)
→ Sequence::apply_physics (FUN_005256B0)
→ Sequence::apricot (FUN_00525740)
→ AFrame::Combine (FUN_00512DE0) [combines offsetFrame into PhysicsObj.Position]
→ PartArray::SetFrame (FUN_00519E40) [if PhysicsObj+0x10 != null]
→ PartArray::UpdateParts (FUN_00519C20)
→ Sequence::GetCurrAnimFrame (FUN_00525570)
→ PartArray::SetPartFrame (FUN_00519B00) [per-part matrix*scale+quaternion]
→ PartArray::CopyShadowParts (FUN_00518790) [if shadow parts exist]
```
---
## Key Constants and Identifiers
| Constant | Value | Meaning |
|----------|-------|---------|
| AnimSequenceNode size | 0x1c (28) | heap alloc size |
| AnimationFrame stride (PartFrames) | 0x10 per part | 4 floats: quaternion per part |
| AFrame size | 0x1c (28) | quaternion+matrix+pos |
| AnimData stride in MotionTable nodes | 0x1c | 7 uint32s per entry |
| PosFrames stride | 0x1c per frame | AFrame per position keyframe |
| PhysicsObj.PartArray offset | +0x10 | pointer field |
| PhysicsObj.Position.Frame offset | +0x50 | AFrame embedded |
| PartArray.NumParts offset | +0x58 (relative to PartArray) | uint |
| PartArray.Scale offset | +0x60 (relative to PartArray) | float[3] |
| Cycle key formula | `(category << 16) \| (cmd & 0xFFFFFF)` | MotionTable lookup |
| Default framerate | 30.0 (0x41F00000) | AnimSequenceNode default |
| PhysicsGlobals.EPSILON | from _DAT_007c92b4 | ~1e-5 |
| No-animate state flag | 0x6500000d | Walk/Run forward (left) |
| No-animate state flag | 0x6500000f | Walk/Run backward |