# 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 766–846): 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 614–650) 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 570–650 synth-velocity,
  766–846 per-keyframe loop, 1236–1330 advance/posFrame application).
