# CSequence family — verbatim retail decomp extraction Source: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (PDB-named, Sept 2013 EoR build, x87 32-bit) + `docs/research/named-retail/acclient.h` (verbatim retail struct layouts). Line numbers below are `grep -n` line anchors into `acclient_2013_pseudo_c.txt` as of this extraction. All code quoted is the tool's literal x87-mangled pseudo-C output (FCMP_UO / floor / `unimplemented {fld ...}` comments and all) — this is what the decompiler actually emitted; the "Cleaned control flow" sections underneath translate it into readable pseudocode without changing any comparison direction, boundary constant, or branch order. --- ## 0. Struct layouts (verbatim from acclient.h) ### `CSequence` (acclient.h:30747, PDB record 3267) ```cpp struct __cppobj CSequence : PackObj { DLList anim_list; // +0x00 head_, +0x04 tail_ (DLListBase) AnimSequenceNode *first_cyclic; // +0x08 (per line 30750; see note below) AC1Legacy::Vector3 velocity; // linear velocity accumulator AC1Legacy::Vector3 omega; // angular velocity accumulator CPhysicsObj *hook_obj; // owning physics object (fires hooks through this) long double frame_number; // x87 80-bit extended — CURRENT FRACTIONAL FRAME AnimSequenceNode *curr_anim; // node actively playing AnimFrame *placement_frame; // static override frame when anim_list is empty unsigned int placement_frame_id; int bIsTrivial; }; ``` `frame_number` is declared `long double` (x87 extended, 80-bit) — NOT a `double`. This is why every comparison against it in the decomp goes through the raw FPU compare/FCMP_UO sequence rather than a SSE `ucomisd`. ### `CPartArray` (acclient.h:30762) — where CSequence lives ```cpp struct __cppobj CPartArray { unsigned int pa_state; CPhysicsObj *owner; CSequence sequence; // EMBEDDED, not a pointer MotionTableManager *motion_table_manager; CSetup *setup; unsigned int num_parts; CPhysicsPart **parts; AC1Legacy::Vector3 scale; Palette **pals; LIGHTLIST *lights; AnimFrame *last_animframe; }; ``` ### `AnimSequenceNode` (acclient.h:31063, PDB record 3266) ```cpp struct __cppobj AnimSequenceNode : PackObj, DLListData { CAnimation *anim; float framerate; // signed — negative = playing this node in reverse int low_frame; // inclusive start frame index (forward-orientation) int high_frame; // inclusive end frame index (forward-orientation) }; ``` `DLListData` (base) supplies `dllist_next` / `dllist_prev`. The node object itself is allocated at `dllist_data_ptr - 4` (the "ADJ" / `- 4` adjustment seen throughout the list-splice code is converting between a `DLListData*` and the owning `AnimSequenceNode*`, i.e. `DLListData` is the FIRST base and the node is 4 bytes past it — actually the compiler emits `((char*)head_ - 4)` meaning the AnimSequenceNode base starts 4 bytes BEFORE the DLListData sub-object, so `DLListData` is inherited second / offset +4 inside the node). ### `AnimFrame` (acclient.h:31072, PDB record 3264) ```cpp struct __cppobj AnimFrame { AFrame *frame; unsigned int num_frame_hooks; CAnimHook *hooks; // singly-linked hook chain for THIS frame unsigned int num_parts; }; ``` ### `CAnimHook` (acclient.h:30973, PDB record 3262) ```cpp struct __cppobj CAnimHook { CAnimHookVtbl *vfptr; CAnimHook *next_hook; int direction_; // 0 = fire on either direction; else must match exactly }; ``` `AnimDoneHook : CAnimHook` (acclient.h:57557) is an empty subclass — pure dispatch marker, `Execute()` calls `CPhysicsObj::Hook_AnimDone`. ### Constants (verbatim, multiple identical copies in the data segment) ``` F_EPSILON = 0.000199999995f (acclient.h data; ~0.0002f) data_794610 (double) = 0x0000000000000000 (0.0) data_7928c0 (double) = 0x3ff0000000000000 (1.0) ``` --- ## 1. `CSequence::CSequence` — ctor (line 300891, addr `0x005249f0`) ``` 00524a30... wait — ctor is 005249f0 ``` ```c 00524a30 // actually printed at 300891: 005249f0 void __fastcall CSequence::CSequence(class CSequence* this) { this->vtable = 0x7c84d8; __builtin_memset(&this->anim_list, 0, 0x28); __builtin_memset(&this->frame_number, 0, 0x18); } ``` Cleaned: zero-fills `anim_list` (0x28 = 40 bytes, covers `anim_list` + `first_cyclic` + `velocity` + `omega` + `hook_obj`), then zero-fills the tail 0x18 = 24 bytes starting at `frame_number` (covers `frame_number` [10 bytes as long double, padded] + `curr_anim` + `placement_frame` + `placement_frame_id`). `frame_number` starts at exactly `0.0`. ## 2. `CSequence::~CSequence` — dtor (line 300901, addr `0x00524a30`) ```c 00524a30 void __fastcall CSequence::~CSequence(class CSequence* this) { bool cond:0 = this->anim_list.head_ == 0; this->vtable = 0x7c84d8; if (!(cond:0)) { do { class DLListData* head_ = this->anim_list.head_; if (head_ != 0) { class DLListData* dllist_prev = head_->dllist_prev; if (dllist_prev == 0) { class DLListData* dllist_next = head_->dllist_next; this->anim_list.head_ = dllist_next; if (dllist_next != 0) dllist_next->dllist_prev = nullptr; } else dllist_prev->dllist_next = head_->dllist_next; class DLListData* dllist_next_1 = head_->dllist_next; if (dllist_next_1 == 0) { class DLListData* dllist_prev_1 = this->anim_list.tail_->dllist_prev; this->anim_list.tail_ = dllist_prev_1; if (dllist_prev_1 != 0) dllist_prev_1->dllist_next = 0; } else dllist_next_1->dllist_prev = head_->dllist_prev; head_->dllist_next = 0; head_->dllist_prev = nullptr; if ((head_ != 0 && head_ != 4)) *(uint32_t*)head_->dllist_next(1); // AnimSequenceNode::`scalar deleting destructor`(1) } } while (this->anim_list.head_ != 0); } this->vtable = 0x79285c; // PackObj vtable (base slice) } ``` Cleaned: pop-and-delete every node from `anim_list.head_` until empty (unlink from doubly-linked list, then call the node's scalar deleting destructor with `delete`-flag=1). ## 3. `CSequence::clear` (line 301828, addr `0x005255b0`) ```c 005255b0 void __fastcall CSequence::clear(class CSequence* this) { CSequence::clear_animations(this); CSequence::clear_physics(this); this->placement_frame = nullptr; this->placement_frame_id = 0; } ``` ## 4. `CSequence::clear_physics` (line 301194, addr `0x00524d50`) ```c 00524d50 void __fastcall CSequence::clear_physics(class CSequence* this) { this->velocity.x = 0; this->velocity.y = 0f; this->velocity.z = 0f; this->omega.x = 0; this->omega.y = 0f; this->omega.z = 0f; } ``` ## 5. `CSequence::clear_animations` (line 301207, addr `0x00524dc0`) ```c 00524dc0 void __fastcall CSequence::clear_animations(class CSequence* this) { while (this->anim_list.head_ != 0) { class DLListData* head_ = this->anim_list.head_; if (head_ != 0) { class DLListData* dllist_prev = head_->dllist_prev; if (dllist_prev == 0) { class DLListData* dllist_next = head_->dllist_next; this->anim_list.head_ = dllist_next; if (dllist_next != 0) dllist_next->dllist_prev = nullptr; } else dllist_prev->dllist_next = head_->dllist_next; class DLListData* dllist_next_1 = head_->dllist_next; if (dllist_next_1 == 0) { class DLListData* dllist_prev_1 = this->anim_list.tail_->dllist_prev; this->anim_list.tail_ = dllist_prev_1; if (dllist_prev_1 != 0) dllist_prev_1->dllist_next = 0; } else dllist_next_1->dllist_prev = head_->dllist_prev; head_->dllist_next = 0; head_->dllist_prev = nullptr; if ((head_ != 0 && head_ != 4)) *(uint32_t*)head_->dllist_next(1); // node dtor+delete } } this->first_cyclic = nullptr; this->frame_number = 0f; *(uint32_t*)((char*)this->frame_number)[4] = 0; // (high dword of the long double, zeroed too) this->curr_anim = nullptr; } ``` Cleaned: identical unlink-and-delete loop over the WHOLE list (same pattern as the dtor), then resets `first_cyclic = null`, `frame_number = 0.0`, `curr_anim = null`. This is the full "wipe the sequence back to empty" operation, distinct from `remove_cyclic_anims` below which only removes the cyclic tail. ## 6. `CSequence::remove_cyclic_anims` (line 301258, addr `0x00524e40`) ```c 00524e40 void __fastcall CSequence::remove_cyclic_anims(class CSequence* this) { class CSequence* this_1 = this; class AnimSequenceNode* first_cyclic_1; for (class AnimSequenceNode* first_cyclic = this->first_cyclic; first_cyclic != 0; first_cyclic = first_cyclic_1) { if (this->curr_anim == first_cyclic) { class AnimSequenceNode* eax_1 = AnimSequenceNode::GetPrev(first_cyclic); this->curr_anim = eax_1; if (eax_1 == 0) { this->frame_number = 0f; *(uint32_t*)((char*)this->frame_number)[4] = 0; } else this->frame_number = ((double)AnimSequenceNode::get_ending_frame(eax_1)); } first_cyclic_1 = AnimSequenceNode::GetNext(first_cyclic); class DLListData** eax_2; if (first_cyclic == 0) eax_2 = nullptr; else eax_2 = &first_cyclic->dllist_next; class DLListData* dllist_prev = ADJ(eax_2)->dllist_prev; if (dllist_prev == 0) { class DLListData* dllist_next = this->anim_list.head_->dllist_next; this->anim_list.head_ = dllist_next; if (dllist_next != 0) dllist_next->dllist_prev = nullptr; } else dllist_prev->dllist_next = ADJ(eax_2)->dllist_next; class DLListData* dllist_next_1 = ADJ(eax_2)->dllist_next; if (dllist_next_1 == 0) { class DLListData* dllist_prev_1 = this->anim_list.tail_->dllist_prev; this->anim_list.tail_ = dllist_prev_1; if (dllist_prev_1 != 0) dllist_prev_1->dllist_next = 0; } else dllist_next_1->dllist_prev = ADJ(eax_2)->dllist_prev; ADJ(eax_2)->dllist_next = nullptr; ADJ(eax_2)->dllist_prev = nullptr; if (first_cyclic != 0) first_cyclic->vtable->__vecDelDtor(1); } class DLListData* tail_ = this->anim_list.tail_; if (tail_ != 0) { this->first_cyclic = ((char*)tail_ - 4); return; } this->first_cyclic = nullptr; } ``` Cleaned: walks from `first_cyclic` to the END of the list, unlinking and deleting EVERY node from `first_cyclic` onward (the "cyclic" tail — i.e. the looping animation(s) queued after the one-shot transition animations). If `curr_anim` was one of the removed nodes, `curr_anim` snaps back to the PREVIOUS node (the last non-cyclic node) and `frame_number` is set to that previous node's `get_ending_frame()` (or `0.0` if there is no previous node at all). After the sweep, `first_cyclic` is reset to point at the new tail of the list (`tail_ - 4`), or `null` if the list is now empty. ## 7. `CSequence::remove_link_animations` (line 301060, addr `0x00524be0`) ```c 00524be0 void __thiscall CSequence::remove_link_animations(class CSequence* this, uint32_t arg2) { int32_t ebp = 0; if (arg2 > 0) { do { if (AnimSequenceNode::GetPrev(this->first_cyclic) == 0) break; if (AnimSequenceNode::GetPrev(this->first_cyclic) == this->curr_anim) { class AnimSequenceNode* first_cyclic = this->first_cyclic; this->curr_anim = first_cyclic; if (first_cyclic != 0) this->frame_number = ((double)AnimSequenceNode::get_starting_frame(first_cyclic)); } class AnimSequenceNode* eax_2 = AnimSequenceNode::GetPrev(this->first_cyclic); class DLListData** edx_1; if (eax_2 == 0) edx_1 = nullptr; else edx_1 = &eax_2->dllist_next; // unlink eax_2 (the node immediately before first_cyclic) from anim_list class DLListData* dllist_prev = ADJ(edx_1)->dllist_prev; if (dllist_prev == 0) { class DLListData* dllist_next = this->anim_list.head_->dllist_next; this->anim_list.head_ = dllist_next; if (dllist_next != 0) dllist_next->dllist_prev = nullptr; } else dllist_prev->dllist_next = ADJ(edx_1)->dllist_next; class DLListData* dllist_next_1 = ADJ(edx_1)->dllist_next; if (dllist_next_1 == 0) { class DLListData* dllist_prev_1 = this->anim_list.tail_->dllist_prev; this->anim_list.tail_ = dllist_prev_1; if (dllist_prev_1 != 0) dllist_prev_1->dllist_next = 0; } else dllist_next_1->dllist_prev = ADJ(edx_1)->dllist_prev; ADJ(edx_1)->dllist_next = nullptr; ADJ(edx_1)->dllist_prev = nullptr; if (eax_2 != 0) eax_2->vtable->__vecDelDtor(1); ebp += 1; } while (ebp < arg2); } } ``` Cleaned: removes `arg2` (count) "link" nodes — nodes chained IMMEDIATELY BEFORE `first_cyclic` (i.e. the linked/one-shot animations queued ahead of the cyclic tail) — one at a time, working backward from `first_cyclic`'s predecessor. If the removed node was `curr_anim`, `curr_anim` is force-advanced to `first_cyclic` and `frame_number` reset to that node's `get_starting_frame()`. Stops early if there's no predecessor left (`GetPrev(first_cyclic) == 0`). ## 8. `CSequence::remove_all_link_animations` (line 301128, addr `0x00524ca0`) ```c 00524ca0 void __fastcall CSequence::remove_all_link_animations(class CSequence* this) { class AnimSequenceNode* first_cyclic = this->first_cyclic; if ((first_cyclic != 0 && AnimSequenceNode::GetPrev(first_cyclic) != 0)) { class AnimSequenceNode* i; do { if (AnimSequenceNode::GetPrev(this->first_cyclic) == this->curr_anim) { class AnimSequenceNode* first_cyclic_1 = this->first_cyclic; this->curr_anim = first_cyclic_1; if (first_cyclic_1 != 0) this->frame_number = ((double)AnimSequenceNode::get_starting_frame(first_cyclic_1)); } class AnimSequenceNode* eax_3 = AnimSequenceNode::GetPrev(this->first_cyclic); // ... identical unlink-and-delete of eax_3 as remove_link_animations ... if (eax_3 != 0) eax_3->vtable->__vecDelDtor(1); i = AnimSequenceNode::GetPrev(this->first_cyclic); } while (i != 0); } } ``` Cleaned: identical to `remove_link_animations` but loops until `GetPrev(first_cyclic) == 0` (i.e. removes ALL link nodes, not a fixed count). ## 9. `CSequence::has_anims` (line 301050, addr `0x00524bd0`) ```c 00524bd0 int32_t __fastcall CSequence::has_anims(class CSequence const* this) { int32_t result; result = this->anim_list.head_ != 0; return result; } ``` ## 10. `CSequence::set_velocity` (line 300798, addr `0x00524880`) ```c 00524880 void __thiscall CSequence::set_velocity(class CSequence* this, class AC1Legacy::Vector3 const* arg2) { this->velocity.x = arg2->x; this->velocity.y = arg2->y; this->velocity.z = arg2->z; } ``` ## 11. `CSequence::set_omega` (line 300808, addr `0x005248a0`) ```c 005248a0 void __thiscall CSequence::set_omega(class CSequence* this, class AC1Legacy::Vector3 const* arg2) { this->omega.x = arg2->x; this->omega.y = arg2->y; this->omega.z = arg2->z; } ``` ## 12. `CSequence::combine_physics` (line 300818, addr `0x005248c0`) ```c 005248c0 void __thiscall CSequence::combine_physics(class CSequence* this, class AC1Legacy::Vector3 const* arg2, class AC1Legacy::Vector3 const* arg3) { float* eax = arg2; this->velocity.x = ((float)(((long double)this->velocity.x) + ((long double)*(uint32_t*)eax))); this->velocity.y = ((float)(((long double)eax[1]) + ((long double)this->velocity.y))); this->velocity.z = ((float)(((long double)eax[2]) + ((long double)this->velocity.z))); this->omega.x = ((float)(((long double)this->omega.x) + ((long double)arg3->x))); this->omega.y = ((float)(((long double)arg3->y) + ((long double)this->omega.y))); this->omega.z = ((float)(((long double)arg3->z) + ((long double)this->omega.z))); } ``` Cleaned: `velocity += arg2; omega += arg3;` (element-wise), all math promoted through x87 `long double` and truncated back to `float` per component (retail's usual FP-widen-then-narrow pattern; NOT a bit-identical no-op — matters for exact conformance tests). ## 13. `CSequence::subtract_physics` (line 300832, addr `0x00524900`) ```c 00524900 void __thiscall CSequence::subtract_physics(class CSequence* this, class AC1Legacy::Vector3 const* arg2, class AC1Legacy::Vector3 const* arg3) { float* eax = arg2; this->velocity.x = ((float)(((long double)this->velocity.x) - ((long double)*(uint32_t*)eax))); this->velocity.y = ((float)(((long double)this->velocity.y) - ((long double)eax[1]))); this->velocity.z = ((float)(((long double)this->velocity.z) - ((long double)eax[2]))); this->omega.x = ((float)(((long double)this->omega.x) - ((long double)arg3->x))); this->omega.y = ((float)(((long double)this->omega.y) - ((long double)arg3->y))); this->omega.z = ((float)(((long double)this->omega.z) - ((long double)arg3->z))); } ``` `velocity -= arg2; omega -= arg3;` — mirror of `combine_physics`. ## 14. `CSequence::multiply_cyclic_animation_fr` (line 300846, addr `0x00524940`) Note: the task prompt's "multiply_cyclic_animation_framerate" is this function; the PDB-recovered name is truncated to `multiply_cyclic_animation_fr` (28-char Windows debug-symbol truncation on some builds). ```c 00524940 void __thiscall CSequence::multiply_cyclic_animation_fr(class CSequence* this, float arg2) { for (class AnimSequenceNode* first_cyclic = this->first_cyclic; first_cyclic != 0; first_cyclic = AnimSequenceNode::GetNext(first_cyclic)) AnimSequenceNode::multiply_framerate(first_cyclic, arg2); } ``` Walks from `first_cyclic` to the tail (the cyclic/looping portion of the sequence) and multiplies EVERY node's framerate by `arg2`. Used elsewhere (line 298286/298295 call sites) to retarget a cyclic motion's playback rate — e.g. scaling Walk_Forward's framerate to match the character's actual movement speed: ``` 298286 CSequence::multiply_cyclic_animation_fr(arg1, ((float)(((long double)arg4) / ((long double)arg3)))); 298295 CSequence::multiply_cyclic_animation_fr(arg1, 0f); ``` ### 14a. `AnimSequenceNode::multiply_framerate` (line 302425, addr `0x00525be0`) ```c 00525be0 void __thiscall AnimSequenceNode::multiply_framerate(class AnimSequenceNode* this, float arg2) { long double x87_r7 = ((long double)arg2); long double temp1 = ((long double)0f); (x87_r7 - temp1); int32_t eax; eax = ((((x87_r7 < temp1) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | (((((FCMP_UO(x87_r7, temp1))) ? 1 : 0) << 0xa) | ((((x87_r7 == temp1) ? 1 : 0) << 0xe) | 0)))); bool p = /* test ah, 0x5 -> "arg2 < 0.0" (unordered-safe less-than) */; if (!(p)) { int32_t low_frame = this->low_frame; this->low_frame = this->high_frame; this->high_frame = low_frame; } this->framerate = ((float)(((long double)arg2) * ((long double)this->framerate))); } ``` Cleaned: ``` if (arg2 < 0.0f) swap(low_frame, high_frame); framerate *= arg2; ``` (NOTE the branch condition polarity: the raw x87 test is `test ah,0x5` on a compare of `arg2` vs `0.0`; the `!(p)` wrapping the swap makes the swap fire when `arg2 < 0.0` is TRUE — i.e. multiplying by a NEGATIVE factor swaps low/high frame, consistent with `get_starting_frame`/ `get_ending_frame` below which key off `framerate < 0` to decide playback direction.) ## 15. `CSequence::get_curr_animframe` (line 300855, addr `0x00524970`) (This is retail's "get current animation frame" accessor — the closest named function to the task's "get_curr_animation"; there is no separate `CSequence::get_curr_animation` symbol in the PDB.) ```c 00524970 class AnimFrame const* __fastcall CSequence::get_curr_animframe(class CSequence const* this) { class AnimSequenceNode* curr_anim = this->curr_anim; if (curr_anim == 0) return this->placement_frame; int32_t eax = this->frame_number; int32_t ecx = *(uint32_t*)((char*)this->frame_number)[4]; int32_t var_8 = eax; int32_t var_4 = ecx; floor(eax, ecx); return AnimSequenceNode::get_part_frame(curr_anim, _ftol2()); } ``` Cleaned: ``` if (curr_anim == null) return placement_frame; return curr_anim.get_part_frame((int)floor(frame_number)); ``` If there is no active animation node, the sequence renders whatever static `placement_frame` was set (see `set_placement_frame` below). Otherwise it floors the fractional `frame_number` to an integer frame index and asks the current node for that frame's part-transform data. ## 16. `CSequence::set_placement_frame` (line 300872, addr `0x005249b0`) ```c 005249b0 void __thiscall CSequence::set_placement_frame(class CSequence* this, class AnimFrame const* arg2, uint32_t arg3) { this->placement_frame = arg2; this->placement_frame_id = arg3; } ``` Trivial setter for the two placement fields; called elsewhere (line 287517, 287550) with `nullptr, 0` to clear placement, or a live `AnimFrame*` + id to set a static pose (e.g. for objects with no animation, "placement_frame_id" ~ `0x65` seen in the older function map's field notes). ## 17. `CSequence::get_curr_frame_number` (line 300881, addr `0x005249d0`) ```c 005249d0 uint32_t __fastcall CSequence::get_curr_frame_number(class CSequence const* this) { floor(this->frame_number, *(uint32_t*)((char*)this->frame_number)[4]); return _ftol2(); } ``` `return (uint32_t)floor(frame_number);` — integer frame index for the current fractional position (used by `CPartArray::get_curr_frame_number` callers at line 300779/300781 in `CPartArray`). ## 18. `CSequence::execute_hooks` — hook dispatch (line 300780, addr `0x00524830`) ```c 00524830 void __thiscall CSequence::execute_hooks(class CSequence const* this, class AnimFrame const* arg2, int32_t arg3) { if (this->hook_obj != 0) { for (class CAnimHook* i = arg2->hooks; i != 0; i = i->next_hook) { int32_t direction_ = i->direction_; if ((direction_ == 0 || arg3 == direction_)) CPhysicsObj::add_anim_hook(this->hook_obj, i); } } } ``` **Hook-dispatch mechanics (verbatim):** `execute_hooks(frame, direction)` walks the singly-linked `hooks` chain attached to a SPECIFIC `AnimFrame` (a frame within the current animation's `part_frames` array — see `AnimFrame` struct, field `hooks`). Each `CAnimHook` node carries a `direction_` filter: - `direction_ == 0` → fires regardless of playback direction (both forward and reverse frame-crossings queue it). - `direction_ != 0` → only fires when the CALLER'S `arg3` direction matches exactly. The actual call sites in `update_internal` (see §21 below) pass: - `arg3 = 1` when crossing a frame FORWARD (line 302189: `CSequence::execute_hooks(this, AnimSequenceNode::get_part_frame(*arg3, ebx_2), 1)`) - `arg3 = 0xffffffff` (i.e. `-1`) when crossing a frame BACKWARD (line 302039: `CSequence::execute_hooks(this, AnimSequenceNode::get_part_frame(*arg3, ebx_1), 0xffffffff)`) So a hook registered with `direction_ = 1` fires only on forward frame crossings, one with `direction_ = -1` (`0xffffffff`) fires only on backward crossings, and `direction_ = 0` fires on both. Matched hooks are NOT executed immediately — they are appended (`CPhysicsObj::add_anim_hook`, line 282906, `0x00514c20`) to the owning `CPhysicsObj`'s `anim_hooks` SmartArray, which is drained once per physics tick by `CPhysicsObj::process_hooks` (line 279431, `0x00511550`): ```c 00511550 void __fastcall CPhysicsObj::process_hooks(class CPhysicsObj* this) { // ... first drains a SEPARATE linked list `this->hooks` (PhysicsObjHook*) // — one-shot script/PES/transparency hooks, unrelated to CAnimHook ... uint32_t m_num = this->anim_hooks.m_num; if (m_num > 0) { int32_t i = 0; if (m_num > 0) { do { this->anim_hooks.m_data[i]->vtable->Execute(this); i += 1; } while (i < this->anim_hooks.m_num); } AC1Legacy::SmartArray::shrink(&this->anim_hooks); this->anim_hooks.m_num = 0; } } ``` So the full pipeline per crossed frame is: `execute_hooks` queues matching `CAnimHook*` pointers into `anim_hooks` (append-only, growable SmartArray, doubles capacity from a base of 8 via `grow()`) → `process_hooks` later executes EVERY queued hook via its vtable `Execute(CPhysicsObj*)` and then resets `m_num = 0` (queue is drained completely every call, `shrink()` just trims capacity bookkeeping). ### 18a. `AnimDoneHook` — the animation-complete hook (line 302227/302223 call site + 303832 Execute) ```c 00526c20 void __stdcall AnimDoneHook::Execute(class AnimDoneHook const* this @ ecx, class CPhysicsObj* arg2) { CPhysicsObj::Hook_AnimDone(arg2); } ``` ```c 0050fda0 void __fastcall CPhysicsObj::Hook_AnimDone(class CPhysicsObj* this) { class CPartArray* part_array = this->part_array; if (part_array != 0) CPartArray::AnimationDone(part_array, 1); } ``` `AnimDoneHook` is a GLOBAL singleton instance (`class AnimDoneHook anim_done_hook` at data address `0x0081d9fc`, vtable installed at line 901343 `0x007681f0`) — NOT allocated per-node. It has no per-instance `direction_`/frame association; it's queued directly by `update_internal` (not via `execute_hooks`/per-frame `hooks` chain) when a node transition consumes the LAST node in the list (see §21, the "leading edge" check). This is retail's `MotionDone` signal path: `AnimDoneHook::Execute` → `CPhysicsObj::Hook_AnimDone` → `CPartArray::AnimationDone(part_array, 1)`. ## 19. `CSequence::apply_physics` (line 300955, addr `0x00524ab0`) ```c 00524ab0 void __thiscall CSequence::apply_physics(class CSequence const* this, class Frame* arg2, double arg3, double arg4) { long double x87_r7 = ((long double)arg4); long double temp1 = ((long double)0.0); (x87_r7 - temp1); long double x87_r7_2 = fabsl(((long double)arg3)); if ((*(uint8_t*)((char*)((((x87_r7 < temp1) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | (((((FCMP_UO(x87_r7, temp1))) ? 1 : 0) << 0xa) | ((((x87_r7 == temp1) ? 1 : 0) << 0xe) | 0x3800)))))[1] & 1) != 0) x87_r7_2 = -(x87_r7_2); arg2->m_fOrigin.x = ((float)((x87_r7_2 * ((long double)this->velocity.x)) + ((long double)arg2->m_fOrigin.x))); arg2->m_fOrigin.y = ((float)(((long double)((float)(x87_r7_2 * ((long double)this->velocity.y)))) + ((long double)arg2->m_fOrigin.y))); arg2->m_fOrigin.z = ((float)(((long double)((float)(x87_r7_2 * ((long double)this->velocity.z)))) + ((long double)arg2->m_fOrigin.z))); long double x87_r5_5 = (x87_r7_2 * ((long double)this->omega.y)); float var_10_1 = ((float)(x87_r7_2 * ((long double)this->omega.z))); float var_18 = ((float)(x87_r7_2 * ((long double)this->omega.x))); float var_14_1 = ((float)x87_r5_5); Frame::rotate(arg2, &var_18); } ``` Cleaned: ``` CSequence::apply_physics(this, Frame* frame, double quantum, double sign_source): signed_quantum = fabs(quantum) if (sign_source < 0.0) // sign copied from arg4, magnitude from arg3 signed_quantum = -signed_quantum frame.m_fOrigin += velocity * signed_quantum // per-component frame.rotate( omega * signed_quantum ) // Vector3(omega.x,omega.y,omega.z) * signed_quantum ``` This is `copysign(fabs(quantum_magnitude), sign_source)` — i.e. the function takes the MAGNITUDE from `arg3` and the SIGN from `arg4` (this matches every call site passing `1.0/framerate` for the magnitude and the raw signed `frameRate`/`arg2` (elapsed-time-with-direction) as the sign source — see §21). The result scales BOTH the accumulated linear `velocity` (added into the frame's origin) and the accumulated angular `omega` (fed into `Frame::rotate`) by the same signed quantum. `Frame::rotate` signature (line 91477, `0x004525b0`): `void Frame::rotate(Frame* this, AC1Legacy::Vector3 const* arg2)` — takes the omega*quantum vector and applies it as an incremental rotation to the frame's orientation. ## 20. `CSequence::apricot` — trim consumed nodes (line 300978, addr `0x00524b40`) (Retail's actual internal name for this function IS `apricot` — verified via the PDB symbol table, not a placeholder.) ```c 00524b40 void __fastcall CSequence::apricot(class CSequence* this) { class DLListData* head_ = this->anim_list.head_; void* __offset(DLListData, -0x4) i; if (head_ == 0) i = nullptr; else i = ((char*)head_ - 4); if (i != this->curr_anim) { int32_t ebx; int32_t var_c_1 = ebx; while (i != this->first_cyclic) { class DLListData* eax; if (i == 0) eax = nullptr; else eax = ((char*)i + 4); // unlink `eax` (the head node) from anim_list class DLListData* dllist_prev = eax->dllist_prev; if (dllist_prev == 0) { class DLListData* dllist_next = this->anim_list.head_->dllist_next; this->anim_list.head_ = dllist_next; if (dllist_next != 0) dllist_next->dllist_prev = nullptr; } else dllist_prev->dllist_next = eax->dllist_next; class DLListData* dllist_next_1 = eax->dllist_next; if (dllist_next_1 == 0) { class DLListData* dllist_prev_1 = this->anim_list.tail_->dllist_prev; this->anim_list.tail_ = dllist_prev_1; if (dllist_prev_1 != 0) dllist_prev_1->dllist_next = 0; } else dllist_next_1->dllist_prev = eax->dllist_prev; eax->dllist_next = 0; eax->dllist_prev = nullptr; if (i != 0) *(uint32_t*)ADJ(i)->dllist_next(1); // delete head node class DLListData* head__1 = this->anim_list.head_; if (head__1 == 0) i = nullptr; else i = ((char*)head__1 - 4); if (i == this->curr_anim) break; } } } ``` Cleaned: ``` CSequence::apricot(): head = anim_list.head (as AnimSequenceNode*) if (head == curr_anim) return // nothing consumed yet, no trim needed while (head != first_cyclic): // unlink+delete `head` from anim_list delete_and_unlink(head) head = anim_list.head (new head, as AnimSequenceNode*) if (head == curr_anim) break ``` Called at the end of every `CSequence::update` (see §22), immediately after `update_internal` advances `curr_anim`. Its job is to free every node BEFORE `curr_anim` that has already fully played and been popped — it walks from the OLD list head forward, deleting nodes one at a time, until it reaches either `curr_anim` (stop — that node is still live) or `first_cyclic` (stop — do not delete into the cyclic tail even if `curr_anim` has somehow moved past it, a defensive bound). ## 21. `CSequence::update_internal` — the core per-frame advance loop (line 301839, addr `0x005255d0`) **Signature:** `CSequence::update_internal(CSequence const* this, double arg2, AnimSequenceNode** arg3, double* arg4, Frame* arg5)` - `this` — the sequence - `arg2` — SIGNED elapsed time this tick (positive = forward playback request, negative = reverse) - `arg3` = `&this->curr_anim` (in/out — current node pointer) - `arg4` = `&this->frame_number` (in/out — fractional frame position, passed as a `double*` even though the underlying field is `long double`; the field is read/written through `floor(lo,hi)` pairs which address it as two 32-bit halves — the x87 extended representation) - `arg5` = destination `Frame*` to accumulate physics into (or `null` if the caller only wants the frame counter advanced, no motion applied — see `CSequence::update`'s no-arg5-branch fallback) The raw pseudo-C for this function is almost entirely FPU compare soup (each conditional branch lowers to an `fcom`/`fcomp`/`fcompp` against `data_794610` [0.0] or `F_EPSILON` [0.0002f], captured as a software-emulated FPU status word test `((c0<<8)|(c2<<10)|(c3<<14))` then `test ah, 0x41`/`0x5`). Two call-site name mismatches from vtable-slot devirtualization noise are corrected below: - Every call site the decompiler labeled `MD_Data_Fade::GetDuration(node)` (lines 301633, 301647, 301657, 301680, 301696, 301706, 301725, 301735, 301745, 301759, 301769) is a virtual call through `AnimSequenceNode`'s vtable at the `framerate`-comparison slot — cross-checked against `AnimSequenceNode::get_starting_frame`/`get_ending_frame` (§26/27 below), whose boolean logic (`framerate < 0.0` branch, then return `high_frame+1` or `low_frame`) is EXACTLY what every one of these call sites' surrounding code does immediately afterward. `MD_Data_Fade` is an unrelated `MediaDesc`-family class (acclient.h:34176) whose real `GetDuration` takes incompatible arguments — a decompiler address-collision mislabel, not a real call target. Read these sites as `node->framerate` (raw field access via a trivial inline getter), not a call to media-fade duration. - Similarly `EffectInfoRegion::GetStat(node)` (lines 301917, 301931, 301979) and `Attribute2ndInfoRegion::GetStat(node)` (lines 302064, 302079, 302129) are mislabeled — by call-site position and use (assigned into `var_28_1` then immediately combined with `AnimSequenceNode::get_pos_frame`/`get_part_frame` and frame-index arithmetic) these are `AnimSequenceNode::get_starting_frame(node)` / `AnimSequenceNode::get_ending_frame(node)` respectively — same address-collision artifact as above (`EffectInfoRegion`/ `Attribute2ndInfoRegion::GetStat` are real functions elsewhere in the binary, lines 244494/245331, unrelated chat/stat classes). ### Cleaned control flow (semantics-preserving translation) ``` CSequence::update_internal(seq, elapsed, &curr_anim, &frame_number, frame): loop: node_framerate = curr_anim.framerate // AnimSequenceNode::framerate (signed) delta = elapsed * node_framerate // signed frame-step for this tick old_frame_idx = floor(frame_number) // integer frame BEFORE advancing new_pos = frame_number + delta frame_number = new_pos remaining = 0.0 // leftover elapsed time after a boundary hit hit_boundary = false // "var_30_1" — did we cross curr_anim's end? if (delta < 0.0): // ── REVERSE playback ── // boundary = curr_anim.get_starting_frame() (mislabeled EffectInfoRegion::GetStat call) boundary = curr_anim.get_starting_frame() if (frame_number < boundary): // crossed/undershot the start if (frame != null): if (curr_anim.anim.pos_frames != 0) Frame::subtract1(frame, frame, curr_anim.get_pos_frame((int)frame_number)) if (fabs(node_framerate) >= F_EPSILON) // 0.000199999995f CSequence::apply_physics(this, frame, 1.0 / node_framerate, elapsed) // (return early here — the "p_5"/"return" branch, line 301911: // if the compare says frame_number was not < 0.0 relative to F_EPSILON // test, execute_hooks/advance is skipped this call — degenerate // micro-step case) return // fire per-frame REVERSE hooks/velocity from old_frame_idx down to // (but not below) floor(frame_number), highest frame first: idx = old_frame_idx do: if (frame != null): if (curr_anim.anim.pos_frames != 0) Frame::subtract1(frame, frame, curr_anim.get_pos_frame(idx)) if (fabs() >= F_EPSILON): CSequence::apply_physics(this, frame, 1.0/node_framerate, elapsed) CSequence::execute_hooks(this, curr_anim.get_part_frame(idx), 0xffffffff) // direction = -1 (backward) idx -= 1 while (idx > floor(frame_number)) // Node exhausted going backward → boundary crossed // (var_30_1 = 1 set here; falls through to the "advance" tail) hit_boundary = true else if (delta > 0.0): // ── FORWARD playback ── // boundary = curr_anim.get_ending_frame() (mislabeled Attribute2ndInfoRegion::GetStat call) boundary = curr_anim.get_ending_frame() if (frame_number > boundary): // crossed/overshot the end // same early-apply-then-return shape as the reverse case, // using Frame::combine instead of Frame::subtract1: if (frame != null): if (curr_anim.anim.pos_frames != 0) Frame::combine(frame, frame, curr_anim.get_pos_frame((int)frame_number)) if (fabs(node_framerate) >= F_EPSILON) CSequence::apply_physics(this, frame, 1.0 / node_framerate, elapsed) return // fire per-frame FORWARD hooks/velocity from old_frame_idx up to // (but not past) floor(frame_number), lowest frame first: idx = old_frame_idx do: if (frame != null): if (curr_anim.anim.pos_frames != 0) Frame::combine(frame, frame, curr_anim.get_pos_frame(idx)) if (fabs() >= F_EPSILON): CSequence::apply_physics(this, frame, 1.0/node_framerate, elapsed) CSequence::execute_hooks(this, curr_anim.get_part_frame(idx), 1) // direction = +1 (forward) idx += 1 while (idx < floor(frame_number)) hit_boundary = true else: // delta == 0.0 (node_framerate == 0 or elapsed == 0) return // nothing to do this tick // ── boundary crossed: advance to the next queued animation ── if (hit_boundary == false): return hook_obj = this->hook_obj if (hook_obj != null): // if the OLD list head has already been fully consumed and we are // NOT still inside the "cyclic tail" region, queue AnimDoneHook — // signals MotionDone to the owning weenie/entity: list_head_node = (anim_list.head_ != null) ? (anim_list.head_ - 4) : null if (list_head_node != this->first_cyclic): CPhysicsObj::add_anim_hook(hook_obj, &anim_done_hook) CSequence::advance_to_next_animation(this, elapsed, &curr_anim, &frame_number, frame) elapsed = remaining // carry the leftover time (past the boundary) into the next loop pass goto loop ``` **Verbatim structural notes preserved from the raw decomp:** - The **degenerate-boundary early-return** (reverse case around line 301885-301911, forward mirror around line 302008-301912-ish) is a REAL early exit distinct from the main per-frame loop — it only fires when the SINGLE-STEP position already lands past the boundary on the FIRST comparison (i.e. `delta` alone overshoots in one step); in that branch `apply_physics`/position-combine happens ONCE using the raw `1.0/node_framerate` quantum and the function returns WITHOUT calling `advance_to_next_animation` or firing `execute_hooks` at all for that tick — because `arg5`(frame)==null guards the whole inner block, so if no destination Frame was supplied, this early-return path does literally nothing but return. - The **per-frame do/while loops** (lines 302006-302056 forward; the reverse mirror is symmetric) always execute the position-combine + `apply_physics` + `execute_hooks` triple for EVERY whole frame index crossed this tick, not just the final one — multi-frame skips (large `elapsed`/`delta` values, e.g. a lag spike) fire ALL intermediate frame hooks in order, matching the task's requirement to "note which hooks fire on which frame crossings": every crossed integer frame fires its `AnimFrame.hooks` chain filtered by direction (`1` forward / `-1` (`0xffffffff`) reverse) via `execute_hooks`, in strict ascending (forward) or descending (reverse) frame order. - `Frame::combine` (forward) vs `Frame::subtract1` (reverse) — these are NOT symmetric operations; `combine` composes the animation's stored per-frame `AFrame` (quaternion + origin) INTO the destination frame (i.e. applies the pose), `subtract1` un-applies (backs out) that same pose. This is retail's mechanism for incremental per-frame pose application driven purely by boundary-crossing bookkeeping — the actual skeletal pose comes from `curr_anim.get_pos_frame(idx)` (root/`AFrame`-level) and `get_part_frame(idx)` (per-part `AnimFrame`) tables baked into the `CAnimation` dat resource, NOT from interpolating `frame_number`'s fractional part at render time inside this function (rendering interpolation happens elsewhere, in `PartArray::SetFrame`/`UpdateParts`, off `get_curr_animframe`'s floored index). - `CSequence::execute_hooks` is called with the **PART frame** ( `AnimSequenceNode::get_part_frame(idx)`, an `AnimFrame const*`), NOT the pos frame — hooks live on `AnimFrame.hooks`, keyed per animation part-frame index, exactly matching the `AnimFrame` struct's `hooks` field. - Boundary detection uses `>` / `<` (strict) against `get_ending_frame()` / `get_starting_frame()`, meaning `frame_number` is allowed to sit EXACTLY AT the boundary value without triggering an advance — the advance only fires once the position strictly exceeds it. Combined with `get_ending_frame()` returning `high_frame + 1 - ε`-ish (see §27, actually `high_frame+1` verbatim, no explicit epsilon subtraction visible in the recovered code — the epsilon appears in the F_EPSILON velocity-magnitude gates, not the frame boundary values themselves) this is how retail avoids double-firing the last frame's hooks. ## 22. `CSequence::update` — public per-tick entry point (line 302402, addr `0x00525b80`) ```c 00525b80 void __thiscall CSequence::update(class CSequence* this, double arg2, class Frame* arg3) { if (this->anim_list.head_ != 0) { int32_t var_14_1 = *(uint32_t*)((char*)arg2)[4]; CSequence::update_internal(this, arg2, &this->curr_anim, &this->frame_number, arg3); CSequence::apricot(this); return; } if (arg3 != 0) { int32_t eax_3 = *(uint32_t*)((char*)arg2)[4]; int32_t ecx_4 = arg2; int32_t var_8_2 = eax_3; int32_t var_10_2 = eax_3; CSequence::apply_physics(this, arg3, ecx_4, ecx_4); } } ``` Cleaned: ``` CSequence::update(this, double elapsed, Frame* frame): if (anim_list.head_ != null): update_internal(this, elapsed, &curr_anim, &frame_number, frame) apricot(this) // trim already-consumed leading nodes return // no queued animations at all — pure physics-only motion (e.g. free-fall, // knockback velocity with no animation playing) if (frame != null): apply_physics(this, frame, elapsed, elapsed) // magnitude == sign source == elapsed ``` This is THE per-tick call site (`PartArray::Update` in the older function-map cross-reference, `FUN_005188e0`/named `CSequence::update` wrapper) — every physics tick that has an active animation list goes through `update_internal` then immediately `apricot`s the consumed leading nodes; a sequence with an EMPTY animation list (e.g. between transitions, or an object with no motion table) falls through to a bare `apply_physics` call so accumulated `velocity`/`omega` (e.g. from `combine_physics` calls, jump/knockback) still moves the frame even with nothing animating. ## 23. `CSequence::advance_to_next_animation` (line 301622, addr `0x005252b0`) **Signature:** `advance_to_next_animation(CSequence const* this, double arg2, AnimSequenceNode const** arg3, double* arg4, Frame* arg5)` — `arg2` is the signed elapsed/rate value carried over from `update_internal` (same sign convention: negative = reverse). ```c 005252b0 void __thiscall CSequence::advance_to_next_animation(class CSequence const* this, double arg2, class AnimSequenceNode const** arg3, double* arg4, class Frame* arg5) { class CSequence* this_1 = this; long double x87_r7 = ((long double)arg2); long double temp1 = ((long double)0.0); (x87_r7 - temp1); class AnimSequenceNode* ecx = *(uint32_t*)arg3; if (/* arg2 < 0.0 */) { // ── REVERSE: step to the PREVIOUS node ── if (!(/* node.get_ending_frame's framerate<0 branch NOT taken, i.e. degenerate small-duration guard */) && arg5 != 0) { class AnimSequenceNode* ecx_16 = *(uint32_t*)arg3; if (ecx_16->anim->pos_frames != 0) Frame::subtract1(arg5, arg5, AnimSequenceNode::get_pos_frame(ecx_16, *(uint32_t*)arg4)); if (fabs(node.framerate) >= F_EPSILON) // 0.000199999995f CSequence::apply_physics(this, arg5, ((double)(((long double)1.0) / node.framerate)), arg2); } // move to predecessor, or wrap to the list tail if there is none class AnimSequenceNode* eax_17; if (AnimSequenceNode::GetPrev(*(uint32_t*)arg3) == 0) { class DLListData* tail_ = this->anim_list.tail_; eax_17 = (tail_ == 0) ? nullptr : ((char*)tail_ - 4); } else eax_17 = AnimSequenceNode::GetPrev(*(uint32_t*)arg3); *(uint32_t*)arg3 = eax_17; // curr_anim = eax_17 *(uint64_t*)arg4 = ((double)AnimSequenceNode::get_ending_frame(eax_17)); // frame_number = new node's ending frame if (!(/* degenerate small-duration guard */) && arg5 != 0) { class AnimSequenceNode* ecx_26 = *(uint32_t*)arg3; if (ecx_26->anim->pos_frames != 0) Frame::combine(arg5, arg5, AnimSequenceNode::get_pos_frame(ecx_26, *(uint32_t*)arg4)); if (fabs(node.framerate) >= F_EPSILON) CSequence::apply_physics(this, arg5, ((double)(((long double)1.0) / node.framerate)), arg2); } } else { // ── FORWARD: step to the NEXT node, wrapping to first_cyclic at the tail ── if (!(/* degenerate small-duration guard */) && arg5 != 0) { class AnimSequenceNode* ecx_1 = *(uint32_t*)arg3; if (ecx_1->anim->pos_frames != 0) Frame::subtract1(arg5, arg5, AnimSequenceNode::get_pos_frame(ecx_1, *(uint32_t*)arg4)); if (fabs(node.framerate) >= F_EPSILON) CSequence::apply_physics(this, arg5, ((double)(((long double)1.0) / node.framerate)), arg2); } if (AnimSequenceNode::GetNext(*(uint32_t*)arg3) == 0) *(uint32_t*)arg3 = this->first_cyclic; // wrap to first_cyclic when list exhausted else *(uint32_t*)arg3 = AnimSequenceNode::GetNext(*(uint32_t*)arg3); *(uint64_t*)arg4 = ((double)AnimSequenceNode::get_starting_frame(*(uint32_t*)arg3)); // frame_number = new node's starting frame if (/* extra 0x41-mask condition — degenerate guard with an extra bit vs the mirrored branches above */ && arg5 != 0) { class AnimSequenceNode* ecx_12 = *(uint32_t*)arg3; if (ecx_12->anim->pos_frames != 0) Frame::combine(arg5, arg5, AnimSequenceNode::get_pos_frame(ecx_12, *(uint32_t*)arg4)); if (fabs(node.framerate) >= F_EPSILON) CSequence::apply_physics(this, arg5, ((double)(((long double)1.0) / node.framerate)), arg2); } } } ``` Cleaned: ``` CSequence::advance_to_next_animation(this, elapsed, &curr_anim, &frame_number, frame): if (elapsed < 0.0): // REVERSE // (a) un-apply the outgoing node's pose at its current frame_number, // and roll its residual velocity/omega into `frame`, UNLESS this // is a zero-duration/degenerate node if (frame != null && duration(curr_anim) != 0.0): if (curr_anim.anim.pos_frames != 0) Frame::subtract1(frame, frame, curr_anim.get_pos_frame((int)frame_number)) if (fabs(curr_anim.framerate) >= F_EPSILON) CSequence::apply_physics(this, frame, 1.0 / curr_anim.framerate, elapsed) // (b) step to the PREVIOUS node in the list; if there is no // predecessor, wrap around to the LIST TAIL prev = GetPrev(curr_anim) curr_anim = (prev != null) ? prev : (anim_list.tail_ != null ? tail_node : null) frame_number = curr_anim.get_ending_frame() // (c) apply the INCOMING node's pose at its new frame_number if (frame != null && duration(curr_anim) != 0.0): if (curr_anim.anim.pos_frames != 0) Frame::combine(frame, frame, curr_anim.get_pos_frame((int)frame_number)) if (fabs(curr_anim.framerate) >= F_EPSILON) CSequence::apply_physics(this, frame, 1.0 / curr_anim.framerate, elapsed) else: // FORWARD (elapsed >= 0.0) // (a) un-apply the outgoing node's pose if (frame != null && duration(curr_anim) != 0.0): if (curr_anim.anim.pos_frames != 0) Frame::subtract1(frame, frame, curr_anim.get_pos_frame((int)frame_number)) if (fabs(curr_anim.framerate) >= F_EPSILON) CSequence::apply_physics(this, frame, 1.0 / curr_anim.framerate, elapsed) // (b) step to the NEXT node; if there is none, WRAP TO first_cyclic // (this is the "loop the cyclic tail forever" mechanism) next = GetNext(curr_anim) curr_anim = (next != null) ? next : first_cyclic frame_number = curr_anim.get_starting_frame() // (c) apply the incoming node's pose if (frame != null && duration(curr_anim) != 0.0): // + one extra mask bit vs (a)/reverse-(c) — same epsilon-style guard if (curr_anim.anim.pos_frames != 0) Frame::combine(frame, frame, curr_anim.get_pos_frame((int)frame_number)) if (fabs(curr_anim.framerate) >= F_EPSILON) CSequence::apply_physics(this, frame, 1.0 / curr_anim.framerate, elapsed) ``` **Key retail-faithful details:** - **Forward wrap target is `first_cyclic`, NOT the list head.** When the last node in the forward chain is exhausted, playback loops back to `first_cyclic` — the boundary marker between one-shot "link" animations (queued ahead of the loop, consumed once and freed via `apricot`) and the actual repeating cycle. This is THE mechanism that makes e.g. Walk_Forward loop forever while a one-shot Jump transition plays once and falls through. - **Reverse wrap target is the LIST TAIL**, not `first_cyclic` — reverse playback (used for backing out of a motion, e.g. an interrupted transition) wraps to the very end of the queued list, not to the cyclic boundary. Asymmetric by design. - Every node transition does FOUR pose operations in sequence: un-apply-outgoing (`subtract1`/reverse-mirror), select-new-node, apply-incoming (`combine`), interleaved with `apply_physics` calls using `1.0 / node.framerate` as the physics quantum's magnitude and the ORIGINAL caller's `elapsed`/`arg2` as the sign source (matching `apply_physics`'s `copysign` semantics from §19). - The three inline "degenerate guard" conditions (reverse-out, reverse-in, forward-out, forward-in) are the SAME F_EPSILON-style FPU compare pattern seen throughout — a duration/framerate near-zero check that skips the pos-frame combine/subtract entirely when the node's timing is degenerate (e.g. a 1-frame or 0-duration transition node), a guard against divide-by-near-zero when computing `1.0/node.framerate`. ## 24. `CSequence::append_animation` (line 301777, addr `0x00525510`) ```c 00525510 void __thiscall CSequence::append_animation(class CSequence* this, class AnimData const* arg2) { void* eax = operator new(0x1c); int32_t* esi; if (eax == 0) esi = nullptr; else esi = AnimSequenceNode::AnimSequenceNode(eax, arg2); if (AnimSequenceNode::has_anim(esi) != 0) { void* eax_3; if (esi == 0) eax_3 = nullptr; else eax_3 = &esi[1]; DLListBase::InsertAfter(&this->anim_list, eax_3, this->anim_list.tail_); class DLListData* tail_ = this->anim_list.tail_; void* __offset(DLListData, -0x4) eax_4; if (tail_ == 0) eax_4 = nullptr; else eax_4 = ((char*)tail_ - 4); this->first_cyclic = eax_4; if (this->curr_anim == 0) { void* head_ = this->anim_list.head_; if (head_ != 0) { this->curr_anim = ((char*)head_ - 4); this->frame_number = ((double)AnimSequenceNode::get_starting_frame(((char*)head_ - 4))); return; } this->curr_anim = nullptr; this->frame_number = ((double)AnimSequenceNode::get_starting_frame(nullptr)); } } else if (esi != 0) **(uint32_t**)esi(1); // node had no anim data — self-destruct (scalar deleting dtor, delete=1) } ``` Cleaned: ``` CSequence::append_animation(this, AnimData const* data): node = new AnimSequenceNode(data) // heap alloc 0x1c bytes; ctor copies framerate/low_frame/high_frame/anim_id from `data` if (node.has_anim()): // node.anim != null (DBObj::Get succeeded) anim_list.InsertAfter(node, anim_list.tail_) // append at the tail first_cyclic = anim_list.tail_ // ALWAYS repoints first_cyclic to the JUST-APPENDED node if (curr_anim == null): if (anim_list.head_ != null): curr_anim = anim_list.head_ frame_number = curr_anim.get_starting_frame() return curr_anim = null frame_number = AnimSequenceNode::get_starting_frame(null) // degenerate/default-framerate(30) starting-frame value else: delete node // failed to resolve the anim dat resource — discard ``` **Critical retail-faithful detail:** `first_cyclic` is updated to the NEWLY APPENDED node on EVERY successful `append_animation` call, not just the first. This means the "cyclic tail" boundary always tracks the LAST node appended so far — i.e. calling `append_animation` multiple times in sequence (as a transition chain builder does — e.g. "exit-substate, transition, new-stance" in sequence) keeps sliding `first_cyclic` forward to the newest node, so only the FINAL `append_animation` call in a chain-build actually establishes the node(s) that will be treated as the looping cycle once earlier one-shot nodes are trimmed by `remove_cyclic_anims`/consumed by `advance_to_next_animation`'s forward-wrap. ## 25. `AnimSequenceNode::AnimSequenceNode` ctors (lines 302547 & 302744) ### Default ctor (line 302547, addr `0x00525d30`) ```c 00525d30 void __fastcall AnimSequenceNode::AnimSequenceNode(class AnimSequenceNode* this) { this->dllist_next = nullptr; this->dllist_prev = nullptr; this->anim = nullptr; this->vtable = 0x7c8504; this->framerate = 30f; this->low_frame = 0xffffffff; this->high_frame = 0xffffffff; } ``` ### From `AnimData` (line 302744, addr `0x00525f90`) ```c 00525f90 void __thiscall AnimSequenceNode::AnimSequenceNode(class AnimSequenceNode* this, class AnimData const* arg2) { this->dllist_next = nullptr; this->dllist_prev = nullptr; this->anim = nullptr; this->vtable = 0x7c8504; this->framerate = arg2->framerate; this->low_frame = arg2->low_frame; this->high_frame = arg2->high_frame; AnimSequenceNode::set_animation_id(this, arg2->anim_id.id); } ``` ### `AnimSequenceNode::set_animation_id` (line 302561, addr `0x00525d60`) ```c 00525d60 void __thiscall AnimSequenceNode::set_animation_id(class AnimSequenceNode* this, class IDClass<_tagDataID,32,0> arg2) { class CAnimation* anim_1 = this->anim; if (anim_1 != 0) anim_1->vtable->Release(); void var_8; if (arg2 == 0) this->anim = nullptr; else this->anim = DBObj::Get(QualifiedDataID::QualifiedDataID(&var_8, arg2, 8)); class CAnimation* anim = this->anim; if (anim != 0) { if (this->high_frame < 0) this->high_frame = (anim->num_frames - 1); uint32_t num_frames_1 = anim->num_frames; if (this->low_frame >= num_frames_1) this->low_frame = (num_frames_1 - 1); uint32_t num_frames = anim->num_frames; if (this->high_frame >= num_frames) this->high_frame = (num_frames - 1); int32_t low_frame = this->low_frame; if (low_frame > this->high_frame) this->high_frame = low_frame; } } ``` Cleaned: resolves the dat animation resource (type-8 qualified DBObj lookup) by `anim_id`, then CLAMPS `low_frame`/`high_frame` into `[0, anim.num_frames-1]`: - `high_frame < 0` (the ctor default `0xffffffff` == `-1` when interpreted signed) → clamp to `num_frames - 1` (i.e. "play to the end"). - `low_frame >= num_frames` → clamp to `num_frames - 1`. - `high_frame >= num_frames` → clamp to `num_frames - 1`. - if after clamping `low_frame > high_frame`, force `high_frame = low_frame` (degenerate single-frame range, never inverted). `AnimData::AnimData` default ctor (line 302519, addr `0x00525ce0`) confirms the DEFAULT values a fresh `AnimData` (before per-transition override) carries: `anim_id = 0`, `low_frame = 0`, `high_frame = 0xffffffff` (i.e. -1, "use full anim"), `framerate = 30.0f`. ## 26. `AnimSequenceNode::get_starting_frame` (line 302483, addr `0x00525c80`) ```c 00525c80 int32_t __fastcall AnimSequenceNode::get_starting_frame(class AnimSequenceNode const* this) { class AnimSequenceNode* this_1 = this; long double x87_r7 = ((long double)this->framerate); long double temp0 = ((long double)0f); (x87_r7 - temp0); int16_t result = ((((x87_r7 < temp0) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | (((((FCMP_UO(x87_r7, temp0))) ? 1 : 0) << 0xa) | ((((x87_r7 == temp0) ? 1 : 0) << 0xe) | 0)))); if ((*(uint8_t*)((char*)result)[1] & 1) != 0) return (this->high_frame + 1); this->low_frame; return result; } ``` Cleaned: ``` AnimSequenceNode::get_starting_frame(): if (framerate < 0.0f) return high_frame + 1 return low_frame ``` (the `test ah,1` bit pulled from the FPU status word after `fcomp` is the raw "less-than" flag — `framerate < 0.0`.) ## 27. `AnimSequenceNode::get_ending_frame` (line 302501, addr `0x00525cb0`) ```c 00525cb0 int32_t __fastcall AnimSequenceNode::get_ending_frame(class AnimSequenceNode const* this) { class AnimSequenceNode* this_1 = this; long double x87_r7 = ((long double)this->framerate); long double temp0 = ((long double)0f); (x87_r7 - temp0); int16_t result = ((((x87_r7 < temp0) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | (((((FCMP_UO(x87_r7, temp0))) ? 1 : 0) << 0xa) | ((((x87_r7 == temp0) ? 1 : 0) << 0xe) | 0)))); if ((*(uint8_t*)((char*)result)[1] & 1) == 0) return (this->high_frame + 1); this->low_frame; return result; } ``` Cleaned: ``` AnimSequenceNode::get_ending_frame(): if (framerate < 0.0f) return low_frame return high_frame + 1 ``` **These two are exact mirrors of each other, keyed off the SAME sign test** (`framerate < 0.0`), which is retail's playback-direction flag: - Forward playback (`framerate >= 0`): starts at `low_frame`, ends at `high_frame + 1`. - Reverse playback (`framerate < 0`): starts at `high_frame + 1`, ends at `low_frame`. So "starting frame" and "ending frame" are DIRECTION-AWARE — for a reverse node, `get_starting_frame()` returns the numerically HIGHER value (`high_frame + 1`) and `get_ending_frame()` returns the numerically LOWER value (`low_frame`), because playback is counting DOWN. This is exactly what feeds `frame_number` at every `advance_to_next_animation` transition (§23) and is why `update_internal`'s forward/reverse branches both correctly detect "exhausted" via a single `frame_number > boundary` / `frame_number < boundary` test regardless of which physical direction the node's `framerate` sign implies. ## 28. `AnimSequenceNode::get_pos_frame` (two overloads, lines 300734 & 302447) ### `double`-index overload (line 300734, addr `0x005247b0`) ```c 005247b0 class AFrame* __thiscall AnimSequenceNode::get_pos_frame(class AnimSequenceNode const* this, double arg2) { floor(arg2, *(uint32_t*)((char*)arg2)[4]); return AnimSequenceNode::get_pos_frame(this, _ftol2()); } ``` Truncates `arg2` (a `double` frame position) to `int` via `floor` then tailcalls the `int`-index overload. ### `int`-index overload (line 302447, addr `0x00525c10`) ```c 00525c10 class AFrame* __thiscall AnimSequenceNode::get_pos_frame(class AnimSequenceNode const* this, int32_t arg2) { class CAnimation* anim = this->anim; if ((anim != 0 && (arg2 >= 0 && arg2 < anim->num_frames))) return ((arg2 * 0x1c) + anim->pos_frames); return 0; } ``` Cleaned: ``` AnimSequenceNode::get_pos_frame(int frame_index): if (anim != null && 0 <= frame_index < anim.num_frames) return &anim.pos_frames[frame_index] // AFrame, stride 0x1c (28 bytes) return null ``` Bounds-checked lookup into `CAnimation::pos_frames` (root/whole-object position+orientation per frame — an `AFrame`, stride 28 bytes). Returns `null` (not a fallback frame) out of range or when the node has no resolved `anim`. ## 29. `AnimSequenceNode::get_part_frame` (line 302460, addr `0x00525c40`) ```c 00525c40 class AnimFrame const* __thiscall AnimSequenceNode::get_part_frame(class AnimSequenceNode const* this, int32_t arg2) { class CAnimation* anim = this->anim; if ((anim != 0 && (arg2 >= 0 && arg2 < anim->num_frames))) return &anim->part_frames[arg2]; return 0; } ``` Cleaned: identical shape to `get_pos_frame` but indexes `CAnimation::part_frames` (the PER-PART `AnimFrame` array — this is what carries `.hooks` for the frame, consumed by `CSequence::execute_hooks`). Same bounds check, same `null` on out-of-range/no-anim. ## 30. `AnimSequenceNode::has_anim` (line 302473, addr `0x00525c70`) ```c 00525c70 int32_t __fastcall AnimSequenceNode::has_anim(class AnimSequenceNode const* this) { int32_t result; result = this->anim != 0; return result; } ``` ## 31. `AnimSequenceNode::GetNext` / `GetPrev` (lines 302601, 302614) ```c 00525de0 class AnimSequenceNode const* __fastcall AnimSequenceNode::GetNext(class AnimSequenceNode const* this) { class DLListData* dllist_next = this->dllist_next; if (dllist_next == 0) return 0; return ((char*)dllist_next - 4); } 00525df0 class AnimSequenceNode* __fastcall AnimSequenceNode::GetPrev(class AnimSequenceNode* this) { class DLListData* dllist_prev = this->dllist_prev; if (dllist_prev == 0) return 0; return ((char*)dllist_prev - 4); } ``` Both convert the raw `DLListData*` link pointer to the owning `AnimSequenceNode*` via the same `-4` byte adjustment seen throughout the list-splice code (§0 struct-layout note). ## 32. `CSequence::pack_size` / `Pack` / `UnPack` (lines 301334, 301458, 302235) — wire serialization Included for completeness (not part of the per-frame hot path, but documents the `CSequence` wire format used by `PackObj::Pack`/`UnPack` dispatch, relevant if acdream ever needs to interoperate with a serialized retail sequence snapshot): ```c 00524f20 uint32_t __thiscall CSequence::pack_size(class CSequence* this, uint32_t* arg2, uint32_t* arg3) { *(uint32_t*)arg2 = 0; // flags accumulator (bit0 = has non-zero velocity, bit1 = has non-zero omega) *(uint32_t*)arg3 = 0; // node count accumulator // count nodes in anim_list, summing each node's Pack() size (0x10 each) into `edi` // edi starts at 4 (header dword), += 4 more if list non-empty (node-count field), // += 0x10 if node count != 0 vs += 4 if empty (mismatched header sizing between // "has nodes" vs "no nodes" cases) result = edi_1 + 4; // + placement_frame_id / frame_number header dword // velocity: if fabs(x) < F_EPSILON check EACH axis (short-circuit: first axis that // fails the epsilon test forces the WHOLE vector to be packed as 0xc bytes + flag bit 0) if (fabs(velocity.x) >= F_EPSILON) { result += 0xc; flags |= 1; } else if (fabs(velocity.y) >= F_EPSILON) { result += 0xc; flags |= 1; } else if (fabs(velocity.z) >= F_EPSILON) { result += 0xc; flags |= 1; } // omega: same pattern, flag bit 1 if (fabs(omega.x) >= F_EPSILON) { result += 0xc; flags |= 2; } else if (fabs(omega.y) >= F_EPSILON) { result += 0xc; flags |= 2; } else if (fabs(omega.z) >= F_EPSILON) { result += 0xc; flags |= 2; } return result; } ``` `CSequence::Pack` writes: flags-dword, [per-node `Pack()` blob × count], then EITHER `placement_frame_id` (if no nodes) OR `frame_number` (8 bytes) + `first_cyclic`-index (distance from head to `first_cyclic` walking `GetNext`) + `curr_anim`-index (distance from head to `curr_anim`), then conditionally `velocity` (if flags&1) and `omega` (if flags&2) as 3 floats each (12 bytes, `0xc`) gated by `arg3 >= 0xc` (buffer-size guard). `AnimSequenceNode::Pack`/`UnPack` (lines 302692/302721, `0x00525ee0`/ `0x00525f40`) pack as `[DID (or INVALID_DID if anim==null), low_frame, high_frame, framerate]` = 0x10 (16) bytes fixed. ## 33. `CSequence::UnPack` (line 302235, addr `0x005259d0`) — tail (velocity/omega restore) ```c 00525b3f if (((ecx_11 & 2) != 0 && arg3 >= 0xc)) { this->omega.x = *(uint32_t*)eax_14; void* edx_5 = (*(uint32_t*)esi + 4); *(uint32_t*)esi = edx_5; this->omega.y = *(uint32_t*)edx_5; void* ecx_14 = (*(uint32_t*)esi + 4); *(uint32_t*)esi = ecx_14; this->omega.z = *(uint32_t*)ecx_14; *(uint32_t*)esi += 4; } return 1; } ``` Symmetric restore of `velocity` (flag bit 0) then `omega` (flag bit 1), matching the `Pack`/`pack_size` bit layout above. `UnPack` begins (line 302239-302247) by calling `clear_animations()` + `clear_physics()` and resetting `placement_frame`/`placement_frame_id` to null/0 before reading the wire data — a full state wipe before deserializing. --- ## Summary: hook-firing timeline per `CSequence::update` tick 1. `CSequence::update(elapsed, frame)` called once per physics tick from `PartArray::Update`. 2. If `anim_list` is non-empty: `update_internal` runs, possibly LOOPING internally (the `goto loop` in §21) if `elapsed` overshoots the current node's frame range — each loop iteration can itself fire MULTIPLE per-frame hook batches (the inner do/while over crossed integer frame indices) before even considering a node transition. 3. For EVERY whole integer frame index crossed within a single node (forward or reverse), in strict frame order: a. The node's stored pose delta at that frame index is combined/subtracted into the destination `Frame*` (`Frame::combine` forward, `Frame::subtract1` reverse) — but ONLY if the node's `CAnimation.pos_frames != null`. b. `apply_physics` folds in the sequence's accumulated `velocity`/`omega` scaled by `1.0/framerate`, signed by the caller's original elapsed/rate — but ONLY if `fabs(framerate) >= F_EPSILON`. c. `execute_hooks(part_frame_at_index, direction)` queues every matching `CAnimHook` (direction 0 = both, else must match caller's `1`=forward / `-1`=reverse) onto the owning `CPhysicsObj.anim_hooks` array — NOT executed inline. 4. When a node's frame range is exhausted (`frame_number` strictly past `get_ending_frame()`/`get_starting_frame()`), BEFORE calling `advance_to_next_animation`: if the OLD list head has already been fully consumed (`head != first_cyclic`), `AnimDoneHook` (the global singleton) is queued directly onto `anim_hooks` — this is the signal that eventually calls `CPartArray::AnimationDone(1)` / `MovementManager::MotionDone` once drained. 5. `advance_to_next_animation` performs the un-apply/select-next/apply pose sequence (§23), wrapping forward exhaustion to `first_cyclic` (loop the cycle) or reverse exhaustion to the list tail. 6. Control returns to `update_internal`'s outer loop with the LEFTOVER elapsed time (`remaining`, the fractional overshoot past the old boundary converted back to a time delta via `/ node_framerate`), which may immediately trigger ANOTHER frame-crossing pass against the NEW `curr_anim` in the same `update_internal` call — i.e. a single `CSequence::update()` call can transition through several queued animation nodes in one tick if `elapsed` is large enough (a slow frame / lag spike can legitimately fast-forward through multiple short one-shot transition nodes in one physics step). 7. After `update_internal` returns (list still non-empty), `apricot()` frees every node strictly BEFORE the (possibly new) `curr_anim`, bounded so it never deletes into `first_cyclic`'s cyclic tail. 8. All queued `anim_hooks` (frame hooks + any `AnimDoneHook`) are actually EXECUTED later, once per physics tick, by `CPhysicsObj::process_hooks` — a separate call NOT inside `CSequence` at all, invoked by the owning `CPhysicsObj`'s update path — and the `anim_hooks` array is fully drained (`m_num = 0`) after every execute pass.