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

14 KiB
Raw Blame History

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):

// 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:

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:

// 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