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