acdream/docs/research/2026-07-02-r1-csequence/r1-csequence-decomp.md
Erik 1371c2a14c feat(R1-P0/P1): CSequence research base + verbatim AnimSequenceNode
P0 — research + pins: full CSequence-family verbatim extraction (1756
lines, per-function raw pseudo-C + cleaned flow, decomp line anchors),
ACE cross-reference (9 ranked divergences; headline: retail frame_number
is x87 long double — ACE's float is the worst case, our double the best
available; ACE's frame-boundary epsilon is an ACE fabrication, NOT
retail), current-sequencer map, and the R1 gap map (20 gaps, 13 keeps,
P0-P6 port order). Pinned the one decomp ambiguity (leftover-time carry
after advance_to_next_animation — ACE reading adopted; cdb confirmation
protocol recorded, non-blocking).

P1 — AnimSequenceNode verbatim (gap G1/G2/G16/G18):
- direction-aware BARE-INT boundary pair (get_starting_frame 0x00525c80 /
  get_ending_frame 0x00525cb0): reverse starts at high+1, ends at low —
  NO epsilon;
- multiply_framerate (0x00525be0) swaps low/high on negative factor;
- set_animation_id (0x00525d60) retail clamp order (high<0 -> num-1;
  low>=num -> num-1; high>=num -> num-1; low>high -> high=low);
- ctors with retail defaults (30f/-1/-1; AnimData copy + clamp);
- get_pos_frame null out-of-range (retail; ACE returns identity),
  floor double overload; get_part_frame same discipline;
- NO per-node IsLooping/Velocity/Omega — loop membership is list
  structure, physics accumulators live on the sequence (G16).

22 conformance tests (clamp table, boundary mirror table, swap
round-trip, bounds/floor semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:45:56 +02:00

1756 lines
70 KiB
Markdown
Raw 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.

# 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<AnimSequenceNode> 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<CAnimHook *>::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(<per-frame quantum>) >= 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(<per-frame quantum>) >= 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.