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