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

25 KiB
Raw Permalink Blame History

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

// 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

// 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

// 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)

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

// 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.

// 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:

// 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 0x00525D00new_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:

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):

// 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).