acdream/docs/research/2026-05-04-l3-port/09-cpart-array-cseq-update.md
Erik de129bc164 feat(motion): L.3 M1 — fresh InterpolationManager port + retail spec
Rewrites src/AcDream.Core/Physics/InterpolationManager.cs from the spec
in docs/research/2026-05-04-l3-port/04-interp-manager.md. Public API
preserved (Vector3-returning AdjustOffset, Enqueue, Clear, IsActive,
Count) so PositionManager + GameWindow callers continue to compile;
internals are full retail spec.

Bug fixes vs prior port (audit 04-interp-manager.md § 7):

  #1  progress_quantum accumulates dt (sum of frame deltas), not step
      magnitude. Retail line 353140; the prior port's `+= step` made
      the secondary stall ratio meaningless.

  #3  Far-branch Enqueue (dist > AutonomyBlipDistance = 100m) sets
      _failCount = StallFailCountThreshold + 1 = 4, so the next
      AdjustOffset call's post-stall check fires an immediate blip-to-
      tail snap. Retail line 352944. Prior port silently drifted
      toward far targets at catch-up speed instead of teleporting.

  #4  Secondary stall test ports the retail formula verbatim:
      cumulative / progress_quantum / dt < CREATURE_FAILED_INTERPOLATION_PERCENTAGE.
      Audit notes the units are 1/sec (likely Turbine bug or x87 FPU
      misread by Binary Ninja) — mirrored byte-for-byte regardless.

  #5  Tail-prune is a tail-walking loop, not a single-tail compare.
      Multiple consecutive stale tail entries within DesiredDistance
      (0.05 m) of the new target collapse together. Retail line 352977.

  #6  Cap-eviction at the HEAD when count reaches 20 (already correct
      in the prior port; verified).

New API: Enqueue gains an optional `currentBodyPosition` parameter so
the far-branch detection can reference the body when the queue is
empty. Backward-compatible (default null = pre-far-branch behavior).

UseTime collapsed into AdjustOffset's tail (post-stall blip check)
since acdream has no per-tick UseTime call separate from
adjust_offset; identical semantic outcome.

State fields renamed to retail names with sentinel values:
  _frameCounter, _progressQuantum, _originalDistance (init = 999999f
  sentinel per retail line 0x00555D30 ctor), _failCount.

Tests:
- 17/17 InterpolationManagerTests green.
- New test Enqueue_FarBranch_PrearmsImmediateBlipOnNextAdjustOffset
  pins the bug #3 fix: enqueueing 150 m away triggers a same-tick
  blip (delta length ≈ 150 m), and the queue clears.

Spec tree: 17 research docs (00–14) under docs/research/2026-05-04-l3-port/.
00-master-plan + 00-port-plan describe the 8-phase rollout. 01-per-tick,
03-up-routing, 04-interp-manager, 05-position-manager-and-partarray,
06-acdream-audit, 14-local-player-audit are the L.3 spec used by this
commit and the M2 follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:56:42 +02:00

526 lines
25 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.

# 09 — CPartArray::Update + CSequence::update / update_internal / apply_physics / add_motion
Source: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR
build, Binary Ninja pseudo-C with PDB names applied).
> **PDB symbol-name collisions you will see in the pseudo-C below.** Several
> AnimSequenceNode getters were name-collapsed into mangled stubs. From the
> retail header `acclient.h` the struct is `{ CAnimation* anim; float framerate;
> int low_frame; int high_frame; }`. The Binary-Ninja pseudo-C shows them as:
>
> | Pseudo-C name | Real symbol |
> |-------------------------------------|------------------------------------------|
> | `MD_Data_Fade::GetDuration(node)` | `AnimSequenceNode::get_framerate(node)` |
> | `EffectInfoRegion::GetStat(node)` | `AnimSequenceNode::get_high_frame(node)` |
> | `Attribute2ndInfoRegion::GetStat(node)` | `AnimSequenceNode::get_low_frame(node)` |
>
> Verified by struct layout match (`framerate` is the first float field,
> `low_frame` then `high_frame`) and by the call-site context (these are read
> exactly where the per-keyframe loop needs the framerate, end frame, start
> frame).
---
## 1. CPartArray::Update — `0x00517DB0`
```cpp
// 00517db0
void __thiscall CPartArray::Update(class CPartArray* this, float arg2, class Frame* arg3)
{
// 00517dc2
CSequence::update(&this->sequence, (double)arg2, arg3);
}
```
It is literally a one-line forwarder. **All animation behavior lives in
`CSequence`.** The caller is `CPhysicsObj::UpdatePositionInternal` at
`0x00512c95`, passing `dt` as `arg2` and a `Frame*` to receive root-motion
displacement (`pos_frames` deltas + `velocity*dt`). The same forwarder is also
called from `0x00513e97` inside `set_state_to_starting_frame` for placement.
There is no per-part loop here — `CPartArray` keeps a single shared `CSequence`
that drives the root frame; per-part bone frames come from
`CSequence::get_curr_animframe()` later, applied in
`Frame::combine(this->parts[esi]->pos.frame, parentFrame, anim_frame->frame[i])`
inside `CPartArray::DoLocalToParent` (different code path).
---
## 2. CSequence::update — `0x00525B80`
```cpp
// 00525b80
void __thiscall CSequence::update(class CSequence* this, double arg2 /*dt*/, class Frame* arg3)
{
// 00525b88
if (this->anim_list.head_ != nullptr) {
// 00525ba3
CSequence::update_internal(this, arg2, &this->curr_anim, &this->frame_number, arg3);
// 00525baa
CSequence::apricot(this); // garbage-collect drained link nodes
return;
}
// No animations on the sequence — degenerate path: still integrate the
// raw velocity * dt onto the frame. (Used by physics-only objects with a
// CSequence carrying only velocity/omega and no animation list.)
// 00525bb9
if (arg3 != nullptr) {
// 00525bca
CSequence::apply_physics(this, arg3, /*dt=*/arg2, /*sign=*/arg2);
}
}
```
Two key observations:
1. The "no animations" branch (`anim_list.head_ == 0`) is the only path that
calls `apply_physics` with the **whole `dt`**. With animations present, the
per-keyframe path scales by `1/framerate` (see §4 below).
2. `apricot()` walks the doubly-linked list, deletes any link node prior to
`curr_anim` that has been fully drained, and trims the list down to the
cyclic head. This is how transition links (e.g. WalkForward → RunForward)
"fall off" once consumed.
---
## 3. CSequence::update_internal — `0x005255D0` (the per-keyframe loop)
This is the heart of retail animation. It is the function whose
literal-pseudocode reading is required for L.3 of the motion port. The
disassembly is x87-FPU-heavy with synthesised compare flags; the algorithm
below is the reconstructed control flow.
### Signature
```cpp
// 005255d0
void __thiscall CSequence::update_internal(
class CSequence const* this,
double arg2, // dt (seconds)
class AnimSequenceNode** arg3, // in/out: &this->curr_anim
double* arg4, // in/out: &this->frame_number (fractional)
class Frame* arg5 // out: accumulator for root-motion displacement
);
```
### Algorithm (reconstructed)
```cpp
while (true) { // 005255e8
// -- per-tick framerate scaling ----------------------------------------
framerate = AnimSequenceNode::get_framerate(*arg3); // 005255e8 (PDB-collision: MD_Data_Fade::GetDuration)
delta = framerate * arg2; // signed; negative = reverse play // 005255f1
int start_frame_int = (int)floor(*arg4); // 00525607
bool wrapped = false; // var_30_1 // 005255ff
int ebx = start_frame_int; // 0052561b
*arg4 = *arg4 + delta; // advance fractional frame_number // 00525635 (fadd / fstp)
if (delta >= 0.0) { // forward play branch // 00525646
// ----- FORWARD --------------------------------------------------
end_int = AnimSequenceNode::get_high_frame(*arg3); // 005257f5 (PDB-collision: EffectInfoRegion::GetStat)
if (*arg4 > (double)end_int) { // 00525806
// overflowed past the cycle's end — clamp + record overflow time
float left_in_cycle = (float)end_int - (float)start_frame_int; // 0052580f / 00525817
if (fabs(framerate) >= F_EPSILON) { // 00525841
overflow_time = left_in_cycle / framerate; // 00525843
// var_18_1
} else {
overflow_time = 0.0; // 0052584d
}
*arg4 = (double)AnimSequenceNode::get_high_frame(*arg3); // 00525866 / fild / fstp
wrapped = true; // 0052586e (var_30_1 = 1)
}
// -- per-keyframe loop: walk every integer frame just stepped on --
// ebx is the frame we are LEAVING; it is decremented in the loop
// body BEFORE the iteration cmp. (Retail counts down because a
// forward step crossing N integer frames must apply the deltas in
// reverse: subtract1(prev_frame's posFrame) to back out, then
// combine(curr_frame's posFrame) to step in. See loop body.)
while ((double)ebx > *arg4 - 1.0) { /* equivalent to: while floor(*arg4) > ebx */
if (arg5 != nullptr) {
node = *arg3;
if (node->anim->pos_frames != 0) { // 005258b1
Frame::subtract1(arg5, arg5,
AnimSequenceNode::get_pos_frame(node, ebx)); // 005258be
}
if (fabs(framerate) >= F_EPSILON) { // 005258d3
// apply_physics with dt = (1.0 / framerate) and same sign
// as outer dt — i.e. EXACTLY ONE keyframe duration of
// velocity*dt is integrated per crossed integer.
CSequence::apply_physics(this, arg5,
/*frame_dt =*/ 1.0 / framerate, // 005258d8 / 005258e1
/*sign =*/ arg2); // 005258f8
}
}
CSequence::execute_hooks(this,
AnimSequenceNode::get_part_frame(*arg3, ebx),
/*direction=*/ 0xFFFFFFFF); // 0052590c (-1 = forward)
ebx -= 1; // 00525916
}
} else { // reverse play branch // 00525646 else (negative delta)
// ----- REVERSE --------------------------------------------------
start_int = AnimSequenceNode::get_low_frame(*arg3); // 0052566a (PDB-collision: Attribute2ndInfoRegion::GetStat)
if (*arg4 <= (double)start_int - 1.0) { // 0052567b (fild + fsubr 1.0 + fcom 0.0)
float left_in_cycle = (float)start_int - (float)start_frame_int;
if (fabs(framerate) >= F_EPSILON) {
overflow_time = left_in_cycle / framerate; // 005256be
} else {
overflow_time = 0.0; // 005256c8
}
*arg4 = (double)AnimSequenceNode::get_low_frame(*arg3); // 005256e1
wrapped = true; // 005256e9
}
// -- per-keyframe loop: same idea, opposite direction ------------
int ebx2 = ebx; /* var_2c_1 = ebx (saved at 0052561d) */
while (/* floor(*arg4) < ebx2 */) { // 00525714
if (arg5 != nullptr) {
node = *arg3;
if (node->anim->pos_frames != 0) { // 00525731
Frame::combine(arg5, arg5,
AnimSequenceNode::get_pos_frame(node, ebx2)); // 0052573e
}
if (fabs(framerate) >= F_EPSILON) { // 00525753
CSequence::apply_physics(this, arg5,
/*frame_dt=*/ 1.0 / framerate, // 00525758 / 00525761
/*sign =*/ arg2); // 00525778
}
}
CSequence::execute_hooks(this,
AnimSequenceNode::get_part_frame(*arg3, ebx2),
/*direction=*/ 1); // 0052578c (+1 = backward)
ebx2 += 1; // 00525796
}
}
// -- end-of-tick: did we wrap? ----------------------------------------
if (!wrapped) return; // 00525943 / 005259ca
// We hit the cycle boundary. Notify hook-target if we just consumed a
// non-cyclic link node, then advance to the next animation in the list.
if (this->hook_obj != nullptr) { // 0052594e
anim_list_head = ((char*)this->anim_list.head_) - 4;
if (anim_list_head != this->first_cyclic) {
CPhysicsObj::add_anim_hook(this->hook_obj, &anim_done_hook); // 00525968
}
}
// arg2 (the outer dt) is rewritten here to "overflow_time" so the loop
// reschedules from the new node.
CSequence::advance_to_next_animation(this, arg2 /*now overflow_time*/,
arg3, arg4, arg5); // 0052597d
arg2 = 0; *(arg2 + 4) = 0; /* reset outer dt accumulator */ // 0052598a / 0052598d
// …then `while (true)` again and run another iteration of the chain.
}
```
### What that means in plain English
Per call to `update`:
1. Take the current dt and multiply by `framerate` (signed, decompresses to
the dat's `AnimData::framerate * speedMod` per `operator*` at `00525d00`).
2. Add to `frame_number` (the fractional cursor).
3. Walk every integer keyframe boundary the cursor just crossed:
- **Forward** crossing: subtract the *just-left* keyframe's `pos_frame`
out of the displacement accumulator (it's already been baked in the
animation pose, so don't double-count it as root motion). Run hooks
with direction `-1`.
- **Reverse** crossing: combine (add) the *just-left* keyframe's
`pos_frame` back in. Run hooks with direction `+1`.
- In **both** directions, integrate `velocity * (1 / framerate) * sign(dt)`
onto the displacement frame via `apply_physics`. This is **one
keyframe's worth of velocity per crossed boundary** — not `velocity * dt`.
4. If the cursor went past the cycle's end (forward) or before its start
(reverse), clamp to the boundary, compute the leftover dt, advance to the
next AnimSequenceNode in the linked list, and loop again with the leftover.
---
## 4. CSequence::apply_physics — `0x00524AB0`
```cpp
// 00524ab0
void __thiscall CSequence::apply_physics(class CSequence const* this,
class Frame* arg2 /*frame*/,
double arg3 /*frame_dt = 1.0/framerate*/,
double arg4 /*sign carrier = outer dt*/)
{
// 00524ab7..00524ac1
long double dt = fabs((long double)arg3);
// 00524ac8..00524aca: if (sign(arg4) < 0) dt = -dt;
// I.e. the magnitude comes from arg3 (1/framerate, always positive),
// the sign comes from arg4 (outer dt). Reverse playback flips the sign.
if (arg4 < 0.0) dt = -dt;
// 00524af1..00524b05: integrate world-space velocity onto the frame.
arg2->m_fOrigin.x += (float)(dt * (long double)this->velocity.x);
arg2->m_fOrigin.y += (float)(dt * (long double)this->velocity.y);
arg2->m_fOrigin.z += (float)(dt * (long double)this->velocity.z);
// 00524b0f..00524b29: build (omega.x*dt, omega.y*dt, omega.z*dt) on stack.
Vector3 axis = { (float)(dt * (long double)this->omega.x),
(float)(dt * (long double)this->omega.y),
(float)(dt * (long double)this->omega.z) };
// 00524b2d
Frame::rotate(arg2, &axis);
}
```
So `apply_physics` integrates **the CSequence's stored velocity/omega**, which
were set by `add_motion` (§5 below). The crucial structural detail repeated:
**`arg3` is `1.0 / framerate`, NOT `dt`.** `update_internal` calls
`apply_physics` once per crossed integer keyframe; over a full cycle this
sums to exactly `velocity * (cycle_duration_in_seconds)`.
---
## 5. add_motion — `0x005224B0` (the velocity producer)
`add_motion` is the writer that populates `CSequence::velocity` and
`CSequence::omega`. Called from the motion-table machinery
(`get_seq_animations` chain) when a new `MotionData` is enqueued.
```cpp
// 005224b0
void add_motion(class CSequence* arg1, class MotionData* arg2, float arg3 /*speedMod*/)
{
if (arg2 == nullptr) return;
// Velocity = MotionData.velocity * speedMod (componentwise)
// 005224d1..005224f8
Vector3 v = { arg2->velocity.x * arg3,
arg2->velocity.y * arg3,
arg2->velocity.z * arg3 };
CSequence::set_velocity(arg1, &v);
// Omega = MotionData.omega * speedMod
// 0052250f..0052252f
Vector3 w = { arg2->omega.x * arg3,
arg2->omega.y * arg3,
arg2->omega.z * arg3 };
CSequence::set_omega(arg1, &w);
// For every AnimData in MotionData.anims, append it to the sequence
// with framerate scaled by speedMod (operator*(AnimData, float, AnimData)
// at 00525d00 builds the scaled copy: low/high frame copied verbatim,
// framerate multiplied, anim_id copied).
// 00522537..00522573
for (int i = 0; i < arg2->num_anims; ++i) {
AnimData scaled;
operator*(&scaled, arg3, &arg2->anims[i]);
CSequence::append_animation(arg1, &scaled);
SetPositionStruct::~SetPositionStruct(&scaled);
}
}
```
`set_velocity` (0x00524880) and `set_omega` (0x005248A0) are 3-float
overwrites — **assignment, not accumulation**. So whenever a new MotionData
is added, prior velocity/omega are clobbered.
(`combine_motion` at 0x00522580 / `subtract_motion` at 0x00522600 are the
accumulating variants — they call `combine_physics` / `subtract_physics`
instead — used elsewhere for additive blends.)
### What this produces for Humanoid Walk/Run
This is the dispositive answer to the L.3 mystery:
> *"For Humanoid Walk/Run cycles where dat ships zero baked velocity, what
> does add_motion produce?"*
**Zero.** `MotionData.velocity` for the Humanoid Walk and Run motion-table
entries is `(0,0,0)` (verified by acdream's own
`AnimationSequencer.SetCycle()` comment block at
`src/AcDream.Core/Physics/AnimationSequencer.cs:579-613`). `add_motion`
multiplies that zero by `speedMod` and writes zero into
`CSequence::velocity`. `apply_physics` therefore integrates zero translation
per keyframe step. **The dat-baked `pos_frames` array on each animation
also has zero translation per frame** for the Humanoid run cycle (cycles
in place — root motion is synthesised, not baked).
So `update_internal`'s root-motion accumulator (`arg5`) ends the call
unchanged for a Humanoid run/walk cycle. **Retail does NOT produce body
translation from `CSequence::update`.** Body translation comes from a
SEPARATE source: `CMotionInterp::get_state_velocity` at `0x00528960`,
which returns `RunAnimSpeed × ForwardSpeed` (or `WalkAnimSpeed`,
`SidestepAnimSpeed`) as a hard-coded constant looked up from
`_DAT_007c96e0/e4/e8`. That value is fed into `CPhysicsObj::set_velocity`
upstream, then `CTransition::transitional_insert` integrates it across the
swept-sphere collision pipeline.
For non-locomotion cycles (emotes, attacks, idle, jump): `MotionData.velocity`
may be non-zero (e.g. jump's vertical impulse) AND/OR the animation's
`pos_frames` array may contain baked deltas (e.g. attack lunges). Both
sources flow through the same `update_internal` loop above.
---
## 6. CSequence::velocity / omega / framerate accessors
There are no getter functions; the fields are read directly off the struct
(see `acclient.h` line 30751 / 30752 / 30754). For reference:
```cpp
// 00524880 set_velocity — pure assignment of 3 floats
// 005248a0 set_omega — pure assignment of 3 floats
// 005248c0 combine_physics (additive: velocity += rhs, omega += rhs)
// 00524900 subtract_physics (additive: velocity -= rhs, omega -= rhs)
// 00524940 multiply_cyclic_animation_fr — for-each-cyclic-node node->framerate *= arg2
// 00525be0 AnimSequenceNode::multiply_framerate — node.framerate *= arg2 (single)
```
`framerate` lives on each `AnimSequenceNode`, not on `CSequence`. It is
already pre-multiplied by `speedMod` at the time `add_motion` runs (via
the `operator*(AnimData, float)` constructor at `0x00525D00`
`new_framerate = old_framerate * speedMod`). Negative speedMod produces
negative framerate, which `update_internal` reads as the reverse-play
branch — there is **no separate "play backward" flag**, just the sign of
`framerate`.
`get_starting_frame` (`0x00525C80`) and `get_ending_frame` (`0x00525CB0`)
encode this: when `framerate < 0`, "start" returns `high_frame + 1` and
"end" returns `low_frame`; when `framerate >= 0`, "start" returns
`low_frame` and "end" returns `high_frame + 1`. This is why the
forward-vs-reverse branches in `update_internal` use opposite boundaries.
---
## 7. Frame::combine / Frame::subtract1 — the per-keyframe pos_frame applicator
`AnimSequenceNode::get_pos_frame(node, frame_index)` at `0x00525C10`:
```cpp
class AFrame* get_pos_frame(int frame_index) {
CAnimation* anim = this->anim;
if (anim != nullptr && frame_index >= 0 && frame_index < anim->num_frames)
return ((AFrame*)((char*)anim->pos_frames + 0x1C * frame_index)); // sizeof(AFrame)=0x1C
return nullptr;
}
```
`Frame::combine(Frame* result, Frame const* lhs, AFrame const* rhs)`
at `0x00525180` (3-arg variant) — concatenates two transforms (`result =
lhs ∘ rhs`). `Frame::subtract1` at `0x00535520` is the inverse (`result =
lhs ∘ rhs⁻¹`). Both are pure 4×4-equivalent rigid-body composition; nothing
animation-specific.
---
## 8. AnimSequenceNode::get_pos_frame — `0x00525C10`
(See §7 — same function, used as a getter.)
---
## 9. Per-keyframe loop summary (the answer to the L.3 critical question)
> *"What does the per-keyframe loop look like exactly?"*
Per crossed integer frame boundary (forward branch shown; reverse mirrors):
```cpp
// 005258a5..00525917, per integer keyframe just crossed forward:
if (arg5 /*displacement frame*/) {
if (curr_node->anim->pos_frames) {
// Subtract the LEAVING keyframe's baked offset out of the running
// displacement accumulator. (We already advanced fractional frame
// past it; the pose for this frame is already where it should be in
// local space.)
Frame::subtract1(arg5, arg5,
AnimSequenceNode::get_pos_frame(curr_node, ebx));
}
if (fabs(framerate) >= F_EPSILON) {
// Integrate exactly ONE keyframe-duration of CSequence::velocity
// and CSequence::omega onto arg5.
CSequence::apply_physics(this, arg5, 1.0 / framerate, sign_of(outer_dt));
}
}
CSequence::execute_hooks(this,
AnimSequenceNode::get_part_frame(curr_node, ebx), -1);
ebx -= 1; // walk to next-older crossed frame
```
In plain English: **for every integer frame boundary the fractional cursor
just crossed, apply (velocity × keyframe_period) of "free" displacement and
also stitch baked posframe deltas to keep the cycle's per-frame in-place
loop registered.** Across a full cycle, the velocity contribution is
exactly `velocity * cycle_duration`. The framerate scaling is what keeps
the body moving the same world-space distance per real-time second
regardless of how the cycle is divided into frames.
---
## 10. Cross-reference: acdream's port
`src/AcDream.Core/Physics/AnimationSequencer.cs` (1455 lines):
- **Per-keyframe loop is structurally correct** (lines 766846): it walks
`lastFrame` across crossed integer boundaries, calls `ApplyPosFrame`
forward/reverse, fires hooks. Wrap-and-overflow logic mirrors retail's
`advance_to_next_animation`.
- **Critical gap — apply_physics is missing.** The retail per-keyframe loop
applies `CSequence::velocity * (1/framerate)` *in addition* to the
posFrame delta. acdream's `ApplyPosFrame` (line 1288) only applies the
`posFrames[frameIndex]` and **skips** the `apply_physics(velocity, 1/framerate)`
step. That's harmless for Humanoid run/walk because their dat velocity
is zero, but it is **wrong for any non-locomotion cycle that uses
`MotionData.velocity`** (jump impulse, knock-back, flying creatures).
- acdream adds a synth path (lines 614650) that overwrites
`CSequence.CurrentVelocity` with `RunAnimSpeed * speedMod` for
locomotion cycles. That value is consumed externally by
`CMotionInterp.get_state_velocity` for body translation — which is
retail's actual locomotion path (see §5 closing note). So acdream's
end-to-end behavior matches retail for Humanoid locomotion *despite* the
missing `apply_physics` inside the sequencer, because both paths
bypass it.
- **Summary for L.3:** the sequencer's `apply_physics` integration is dead
code for Humanoid locomotion (dat velocity = 0). Porting it faithfully
is required for jump/emote/flying-creature root motion but does not
affect the run/walk-cycle bug L.3 is investigating. The bug must lie
upstream in `CMotionInterp::get_state_velocity` consumption or in the
per-tick path that re-feeds CSequence's velocity vs. the
`CTransition::transitional_insert` body sweep.
---
## 11. Files touched / line citations
All retail line numbers refer to
`docs/research/named-retail/acclient_2013_pseudo_c.txt`:
| Function | Pseudo-C line | Address |
|-------------------------------------------|---------------|--------------|
| `CPartArray::Update` | 285883 | `0x00517DB0` |
| `CSequence::update` | 302402 | `0x00525B80` |
| `CSequence::update_internal` | 301839 | `0x005255D0` |
| `CSequence::advance_to_next_animation` | 301622 | `0x005252B0` |
| `CSequence::apply_physics` | 300955 | `0x00524AB0` |
| `CSequence::set_velocity` | 300798 | `0x00524880` |
| `CSequence::set_omega` | 300808 | `0x005248A0` |
| `CSequence::combine_physics` | 300818 | `0x005248C0` |
| `CSequence::subtract_physics` | 300832 | `0x00524900` |
| `CSequence::execute_hooks` | 300780 | `0x00524830` |
| `CSequence::apricot` | 300978 | `0x00524B40` |
| `add_motion` | 298437 | `0x005224B0` |
| `combine_motion` | 298472 | `0x00522580` |
| `subtract_motion` | 298492 | `0x00522600` |
| `AnimSequenceNode::get_pos_frame` (int) | 302447 | `0x00525C10` |
| `AnimSequenceNode::get_part_frame` | 302460 | `0x00525C40` |
| `AnimSequenceNode::get_starting_frame` | 302483 | `0x00525C80` |
| `AnimSequenceNode::get_ending_frame` | 302501 | `0x00525CB0` |
| `AnimSequenceNode::multiply_framerate` | 302425 | `0x00525BE0` |
| `operator*(AnimData, float, AnimData)` | 302531 | `0x00525D00` |
| Header struct `CSequence` | acclient.h:30747 | — |
| Header struct `AnimSequenceNode` | acclient.h:31063 | — |
| Header struct `CPartArray` | acclient.h:30762 | — |
acdream files cross-referenced:
- `src/AcDream.Core/Physics/AnimationSequencer.cs` (lines 570650 synth-velocity,
766846 per-keyframe loop, 12361330 advance/posFrame application).