acdream/docs/research/2026-07-02-r2-motiontable/r2-motiontable-decomp.md
Erik dc54a3e41f docs(R2-Q0): motion-table research base + ambiguity pins
- r2-motiontable-decomp.md: 1,603-line verbatim extraction — full
  CMotionTable (GetObjectSequence all 4 class branches, get_link,
  is_allowed, re_modify, StopSequenceMotion, SetDefaultState, wrappers),
  the free functions (add/combine/subtract_motion, change_cycle_speed,
  same_sign), all 16 MotionTableManager members (pending_animations,
  add_to_queue, remove_redundant_links with the 0xb0000000/0x70000000
  block masks, truncate, AnimationDone vs CheckForCompletedMotions,
  PerformMovement with the 0x41000003 stop sentinel), MotionState's
  full modifier-stack/action-FIFO cast, verbatim struct layouts +
  constants table. BN mistypings identified (SurfInfo lookups are
  style_defaults/links hashes).
- r2-ace-motiontable.md: ACE cross-ref with the two-tracker headline
  (MotionTableManager UPSTREAM of MotionInterp — never merged) + 5
  flagged ACE oddities.
- r2-port-plan.md: 17 gaps (H1-H17), keep list, Q0-Q6 commit sequence,
  the MotionDone->R3 boundary contract.
- Q0-pins.md: A1/A2 pinned to ACE's reading (three corroborations;
  cdb confirmation folds into the next live session), A3 outTicks
  decode, A4 ACE-oddity adjudications (the Action-branch double-count
  is an ACE bug — do not copy), A5 Bitfield check at Q2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:29:26 +02:00

75 KiB
Raw Blame History

Retail motion-table layer — verbatim decomp extraction

Source: docs/research/named-retail/acclient_2013_pseudo_c.txt (line numbers = file line numbers, left column in raw dumps = virtual address). Structs: docs/research/named-retail/acclient.h. Build: Sept 2013 EoR (acclient.exe v11.4186). All addresses verified via grep -n against the pseudo-C dump on 2026-07-02.

Naming caveat (see feedback_bn_decomp_field_names.md): Binary Ninja's decompiler mis-typed several LongNIValHash<unsigned long>::lookup calls on this->style_defaults as LongNIValHash<SurfInfo*>::lookup. SurfInfo is an unrelated texture struct — every LongNIValHash<SurfInfo*>::lookup(&this->style_defaults.vtable, ...) / lookup(edi, ...) call below is actually operating on CMotionTable::style_defaults (style→default-substate map) or CMotionTable::links (style→substate-link-hash map), not on any SurfInfo data. Verified by field offsets against acclient.h's CMotionTable layout (below). Treat the SurfInfo* type name in the pseudo-C as decompiler noise; the pointee types are unsigned long (style_defaults) or LongHash<MotionData>* (links).


1. Struct layouts (verbatim from acclient.h)

/* acclient.h:31081 */
struct __cppobj MotionState
{
  unsigned int style;
  unsigned int substate;
  float substate_mod;
  MotionList *modifier_head;
  MotionList *action_head;
  MotionList *action_tail;
};

/* acclient.h:31664 — singly-linked node, used for BOTH modifier_head chain and action_head/action_tail chain */
struct __cppobj MotionList
{
  unsigned int motion;
  float speed_mod;
  MotionList *next;
};

/* acclient.h:9720 */
struct __cppobj DLListData
{
  DLListData *dllist_next;
  DLListData *dllist_prev;
};

/* acclient.h:9727 */
struct __cppobj DLListBase
{
  DLListData *head_;
  DLListData *tail_;
};

/* acclient.h:57614 — pending_animations node. Offsets in raw pseudo-C: +0x0 dllist_next,
   +0x4 dllist_prev, +0x8 motion, +0xc num_anims (also used as a raw tick countdown). */
struct __cppobj MotionTableManager::AnimNode : DLListData
{
  unsigned int motion;
  unsigned int num_anims;
};

/* acclient.h:31092 */
struct __cppobj DLList<MotionTableManager::AnimNode> : DLListBase
{
};

/* acclient.h:31097 — offsets: +0x0 physics_obj, +0x4 table, +0x8 state (24 bytes:
   style/substate/substate_mod/modifier_head/action_head/action_tail), +0x20 animation_counter,
   +0x24/+0x28 pending_animations {head_,tail_}. Total sizeof = 0x2c (44 bytes), matches
   `operator new(0x2c)` in Create(). */
struct __cppobj MotionTableManager
{
  CPhysicsObj *physics_obj;
  CMotionTable *table;
  MotionState state;
  int animation_counter;
  DLList<MotionTableManager::AnimNode> pending_animations;
};

/* acclient.h:31654 — style_defaults maps style -> default substate (unsigned long values).
   cycles maps (style<<16 | substate&0xffffff) -> MotionData* (base cyclic animation for a state).
   modifiers maps (style<<16 | modifier&0xffffff) OR bare modifier&0xffffff -> MotionData*.
   links maps (style<<16 | substate&0xffffff) -> LongHash<MotionData>* (a per-(style,substate)
   hash of transition-target-substate -> MotionData* link animation). default_style is the
   fallback style (e.g. "Standing/Idle") used when no direct link/cycle exists. */
const struct __cppobj __declspec(align(8)) CMotionTable : SerializeUsingPackDBObj
{
  LongNIValHash<unsigned long> style_defaults;
  LongHash<MotionData> cycles;
  LongHash<MotionData> modifiers;
  LongNIValHash<LongHash<MotionData> *> links;
  unsigned int default_style;
};

/* acclient.h:57162 */
struct __cppobj __declspec(align(4)) MotionData : PackObj, LongHashData
{
  char num_anims;
  AnimData *anims;
  AC1Legacy::Vector3 velocity;
  AC1Legacy::Vector3 omega;
  char bitfield;      /* bit0 (0x1) = "clear modifiers on apply"; bit1 (0x2) = "substate-checked" (is_allowed gate) */
};

/* acclient.h:38069 */
struct __cppobj MovementStruct
{
  MovementTypes::Type type;
  unsigned int motion;
  unsigned int object_id;
  unsigned int top_level_id;
  Position pos;
  float radius;
  float height;
  MovementParameters *params;   /* params->speed used as substate_mod / speed */
};

/* acclient.h:2856 */
enum MovementTypes::Type
{
  Invalid = 0x0,
  RawCommand = 0x1,
  InterpretedCommand = 0x2,
  StopRawCommand = 0x3,
  StopInterpretedCommand = 0x4,
  StopCompletely = 0x5,
  MoveToObject = 0x6,
  MoveToPosition = 0x7,
  TurnToObject = 0x8,
  TurnToHeading = 0x9,
  FORCE_Type_32_BIT = 0x7FFFFFFF,
};

Motion-id class bits (the 0x?0000000 family used throughout GetObjectSequence/StopSequenceMotion)

Observed usage in the decomp below (no single enum found in acclient.h; these are bit-tested directly as arg2 & 0x40000000, & 0x20000000, & 0x10000000, and substate & 0x20000000):

  • 0x40000000cycle class: substate IDs that name a cyclic/looping animation state directly addressable via this->cycles. GetObjectSequence's ebx_1 < 0 branch (i.e. sign bit / top bit set — note 0x40000000 alone doesn't set the sign bit, so this is really the (uint32_t)ebx_1 & 0x80000000-adjacent negative-as-int32 check on the incoming motion id, handled distinctly from the & 0x40000000 cycle-lookup path lower down) and the StopSequenceMotion early-out both special-case this bit: "this IS the current substate, no transition — reissue via GetDefaultStyle equivalent (style_defaults default) with arg7=1 (stop flag)."
  • 0x20000000modifier class: substate/motion IDs that are modifiers (overlays combined onto the base cycle, e.g. "aiming while running") rather than replacing the base state. Stored/ removed via MotionState::modifier_head linked list (add_modifier/add_modifier_no_check/ remove_modifier). StopSequenceMotion walks modifier_head to find and subtract a matching modifier when this bit is set.
  • 0x10000000action class: one-shot, non-cycle-replacing actions (e.g. attacks, emotes) queued onto MotionState::action_head/action_tail via add_action, and popped in FIFO via remove_action_head when their queued AnimNode completes (CheckForCompletedMotions/ AnimationDone test num_anims_node_flags & 0x10000000 to decide whether to pop the action queue on completion).

2. Free functions (motion arithmetic + comparison helpers)

same_sign — 0x00522260 (@298253)

int32_t same_sign(float arg1, float arg2)

Pseudo-C is x87-flag-comparison noise from BN; the two if/else if legs both collapse to the same conditional. Cleaned flow: returns whether arg1 and arg2 compare on the same side of zero (both >=0 or both <0, roughly — BN's test ah,0x5 pattern after an fcompp-style compare means "ZF|PF clear", i.e. arg2 < arg1 is false in one of the ucomiss orderings). Net effect used by call sites (GetObjectSequence's same_sign(ebp_1, esi->substate_mod) != 0): true when the new speed_mod (arg1) and the current substate_mod (arg2) have the same sign — i.e. direction of motion hasn't flipped (forward vs backward). Used to gate the "just re-speed the existing cycle, don't retransition" fast path.

int32_t same_sign(float arg1, float arg2)
{
    // BN x87 flag noise elided — net semantic:
    // return (arg1 >= 0f) == (arg2 >= 0f);   // both same sign => 1, else 0
    // (mirrors: compares arg1 vs 0, arg2 vs 0, returns 1 if same side, 0 if opposite)
}

change_cycle_speed — 0x00522290 (@298276)

void change_cycle_speed(class CSequence* arg1, class MotionData* arg2, float arg3, float arg4)
{
    // arg1=sequence, arg2=cyclic MotionData (unused directly here besides existence check
    //   upstream), arg3=old substate_mod, arg4=new substate_mod
    if (fabsl(arg3) > 0.0002f)               // guard divide-by-near-zero
    {
        CSequence::multiply_cyclic_animation_fr(arg1, arg4 / arg3);
        return;
    }
    // arg3 ~ 0: fall through to the fabsl(arg4) test
    if (fabsl(arg4) > 0.0002f is FALSE, i.e. arg4 also ~0 -> multiply by 0)
        CSequence::multiply_cyclic_animation_fr(arg1, 0f);
    // (if arg4 is NOT ~0 while arg3 IS ~0, the BN flag test suppresses the call — net:
    //  only rescale when arg3 is non-trivial; when arg3~0 AND arg4~0, zero the playback rate)
}

Constant 0.000199999995f0.0002f — hard epsilon for "is this speed effectively zero." Cleaned intent: rescale the cyclic animation's frame rate by the ratio new_speed/old_speed; guards against div-by-zero when the previous substate_mod was ~0.

add_motion — 0x005224b0 (@298437)

void add_motion(class CSequence* arg1 /*sequence*/, class MotionData* arg2 /*motion*/, float arg3 /*speed_mod*/)
{
    if (arg2 == 0) return;

    // Scale linear + angular velocity by speed_mod and SET on the sequence (overwrite, not add)
    Vector3 v = arg2->velocity * arg3;   // {x,y,z} each independently multiplied
    CSequence::set_velocity(arg1, &v);

    Vector3 w = arg2->omega * arg3;
    CSequence::set_omega(arg1, &w);

    // Append every sub-animation in this MotionData's anims[] array, each scaled by speed_mod
    for (int i = 0; i < arg2->num_anims; i++)
        CSequence::append_animation(arg1, /* AnimData scaled-copy ctor */ operator*(&tmp, arg3, &arg2->anims[i]));
}

num_anims field is char in the struct but compared/looped as unsigned int (BN widens it). Anim stride in the raw dump is 0x14 (20) bytes per AnimData element (ebx_1 += 0x14).

combine_motion — 0x00522580 (@298472)

void combine_motion(class CSequence* arg1, class MotionData* arg2, float arg3)
{
    if (arg2 == 0) return;
    Vector3 w = arg2->omega * arg3;
    Vector3 v = arg2->velocity * arg3;
    CSequence::combine_physics(arg1, &v, &w);   // ADDS to existing sequence velocity/omega (vs add_motion's SET)
}

Note: combine_motion does not touch anims — it only combines the linear/angular physics contribution (used for modifier overlays where the base cycle's animation frames stay, but a modifier's velocity/omega gets blended in).

subtract_motion — 0x00522600 (@298492)

void subtract_motion(class CSequence* arg1, class MotionData* arg2, float arg3)
{
    if (arg2 == 0) return;
    Vector3 w = arg2->omega * arg3;
    Vector3 v = arg2->velocity * arg3;
    CSequence::subtract_physics(arg1, &v, &w);  // inverse of combine_motion; used when REMOVING a modifier
}

3. CMotionTable::is_allowed — 0x005226c0 (@298526)

int32_t __thiscall CMotionTable::is_allowed(class CMotionTable const* this,
                                             uint32_t arg2 /*candidate substate id*/,
                                             class MotionData* arg3 /*candidate cycle MotionData*/,
                                             class MotionState const* arg4 /*current state*/)
{
    class MotionData* eax_4 = arg3;
    if (eax_4 == 0)
        return eax_4;                       // null MotionData -> "not allowed" (returns 0)

    if ((eax_4->bitfield & 2) != 0)          // MotionData.bitfield bit1 = "substate-gated" cycle
    {
        uint32_t substate = arg4->substate;  // arg4 = current MotionState
        if (arg2 != substate)                // candidate substate differs from current substate
        {
            // style_defaults[arg4->style] -> default substate for that style
            uint32_t defaultSubstate;
            LongNIValHash<unsigned long>::lookup(&this->style_defaults, arg4->style, &defaultSubstate);
            return defaultSubstate == substate;   // allowed only if current substate == that style's default
        }
    }
    return 1;   // not gated, or candidate IS the current substate -> allowed
}

Cleaned semantics: a bitfield-bit1 ("gated") cycle is only reusable/continuable when the current substate matches, OR when the current substate already equals the owning style's default substate. This stops e.g. a combat-cycle MotionData from being silently reused while standing in an unrelated substate.


class MotionData* __thiscall CMotionTable::get_link(class CMotionTable const* this,
                                                      uint32_t arg2 /*fromStyle*/,
                                                      uint32_t arg3 /*fromSubstate*/,
                                                      float arg4   /*fromSubstateMod (speed sign)*/,
                                                      uint32_t arg5 /*toSubstate*/,
                                                      float arg6   /*toSubstateMod (speed sign)*/)
{
    bool fromNeg = (arg6 < 0f);   // BN's x87 compare-to-zero noise: p = (arg6<0), computed FIRST
    bool toNeg;
    if (fromNeg)
        toNeg = (arg4 < 0f);
    // net predicate: sameDirection = !(fromNeg) || !(toNeg)  === NOT(fromNeg && toNeg)
    //   i.e. "at least one of the two speed_mods is non-negative" chooses branch A;
    //        "both speed_mods negative" (moving backward on both ends) chooses branch B (swap keys)

    class LongHash<MotionData>* linkHash;
    uint32_t key = (arg2 << 16);   // style in high 16 bits

    if (!(fromNeg && toNeg))   // branch A: NOT both-negative
    {
        // links[(arg2<<16) | (arg5 & 0xffffff)]  — keyed by (fromStyle, toSubstate)
        if (LongNIValHash<LongHash<MotionData>*>::lookup(&this->links, key | (arg5 & 0xffffff), &linkHash) != 0)
        {
            MotionData* found = LongHash<MotionData>::lookup(linkHash, arg3);   // sub-key = fromSubstate
            if (found != 0) return found;
            // fall through with found cached in var_4 for the final fallback return
        }
    }
    else   // branch B: both negative -> swap which value keys the outer hash vs the inner lookup
    {
        // links[(arg2<<16) | (arg3 & 0xffffff)]  — keyed by (fromStyle, FROMsubstate) this time
        if (LongNIValHash<LongHash<MotionData>*>::lookup(&this->links, key | (arg3 & 0xffffff), &linkHash) != 0)
        {
            MotionData* found = LongHash<MotionData>::lookup(linkHash, arg5);   // sub-key = toSubstate
            if (found != 0) return found;
        }
    }

    // ---- Second predicate block: same test repeated on (arg6, arg4) again (BN redundant re-eval) ----
    bool fromNeg2 = (arg6 < 0f);
    bool toNeg2;
    if (fromNeg2) toNeg2 = (arg4 < 0f);

    if (!(fromNeg2 && toNeg2))
    {
        // style_defaults[arg2] -> defaultSubstateForFromStyle (stored into arg5's slot, reused as "arg5")
        // links[(arg2<<16) | (arg3 & 0xffffff)] -> linkHash
        // return linkHash[defaultSubstateForFromStyle]
        uint32_t defaultSubstate;
        if (LongNIValHash<unsigned long>::lookup(&this->style_defaults, arg2, &defaultSubstate) != 0
            && LongNIValHash<LongHash<MotionData>*>::lookup(&this->links, key | (arg3 & 0xffffff), &linkHash) != 0)
            return LongHash<MotionData>::lookup(linkHash, defaultSubstate);
    }
    else if (LongNIValHash<LongHash<MotionData>*>::lookup(&this->links, key /* (arg2<<16)|0 */, &linkHash) != 0)
    {
        return LongHash<MotionData>::lookup(linkHash, arg5);
    }

    return var_4;   // whatever branch A/B's inner lookup found (possibly null) as final fallback
}

Cleaned intent: look up the link (transition) animation between fromSubstate and toSubstate within fromStyle. Direction-of-motion (sign of the two speed mods) picks which of the two IDs keys the outer per-style hash and which keys the inner per-transition hash — this lets forward and backward transitions (e.g. walk-forward→run-forward vs walk-backward→run-backward) be stored as distinct entries without duplicating the whole link table. If no direct link exists, falls back to a link from the style's default substate to the target (the "return via idle" link).


5. CMotionTable::GetObjectSequence — 0x00522860 (@298636) — COMPLETE

int32_t __thiscall CMotionTable::GetObjectSequence(
    class CMotionTable const* this,
    uint32_t arg2,           // requested target substate/motion id ("ebx_1" after copy)
    class MotionState* arg3, // in/out current motion state (style/substate/substate_mod/modifier_head)
    class CSequence* arg4,   // sequence to build/mutate (physics + animation queue)
    float arg5,              // requested speed_mod for the new substate
    uint32_t* arg6,           // OUT: outTicks (frame-tick count of appended animation, minus 1)
    int32_t arg7)             // 0 = normal "do motion", 1 = "stop" call (re-invoked from StopSequenceMotion)
{
    MotionState* esi = arg3;
    *arg6 = 0;                                   // outTicks defaults to 0

    uint32_t style = esi->style;
    if (style == 0)
        return 0;                                // no current style at all -> can't sequence anything

    uint32_t substate = esi->substate;
    if (substate == 0)
        return 0;

    MotionData* var_10_1 = nullptr;   // "toDefault" link data (2nd hop of a style-default double-hop)
    MotionData* var_4_1  = nullptr;   // "fromDefault" link data (1st hop of a style-default double-hop)

    uint32_t var_c;   // default substate for esi->style
    LongNIValHash<unsigned long>::lookup(&this->style_defaults, style, &var_c);

    uint32_t ebx_1 = arg2;   // target motion/substate id, working copy

    // ---- FAST PATH: requesting the SAME substate we're already in, is-a-modifier-class id, not a stop-call ----
    if (ebx_1 == var_c && arg7 == 0 && (substate & 0x20000000) != 0)
        return 1;   // no-op: already at the style default and target is a modifier-class request

    float ebp_1 = arg5;   // working speed_mod

    // ================================================================
    // BRANCH 1: ebx_1 interpreted as NEGATIVE int32 (top bit set) — style-change request
    //   (BN emits `if (ebx_1 < 0)`; ebx_1's raw bit pattern with the top bit set is used
    //    elsewhere as a STYLE id encoded in the upper bits, not a plain substate)
    // ================================================================
    if ((int32_t)ebx_1 < 0)
    {
        uint32_t style_1 = esi->style;
        if (style_1 == ebx_1)
            return 1;   // requesting the style we're already in -> no-op success

        uint32_t eax_1;   // default substate for style_1 (current style)
        LongNIValHash<unsigned long>::lookup(&this->style_defaults, style_1, &eax_1);
        uint32_t substate_3 = substate;   // == esi->substate, aliased

        if (substate_3 != eax_1)
            // current substate isn't already the current style's default -> need a link OUT of it first
            var_4_1 = CMotionTable::get_link(this, esi->style, substate_3, esi->substate_mod, eax_1, ebp_1);

        uint32_t arg7_style_default;   // default substate for the TARGET style (ebx_1)
        if (LongNIValHash<unsigned long>::lookup(&this->style_defaults, ebx_1, &arg7_style_default) != 0)
        {
            // cycles[(arg7_style_default & 0xffffff) | (ebx_1<<16)] — target style's default cycle
            MotionData* eax_5 = LongHash<MotionData>::lookup(&this->cycles, (arg7_style_default & 0xffffff) | (ebx_1 << 16));
            arg3 = eax_5;   // becomes the "new base cycle" MotionData
            if (eax_5 != 0)
            {
                if ((eax_5->bitfield & 1) != 0)
                    MotionState::clear_modifiers(esi);   // new cycle wants a clean modifier slate

                // link FROM current style's default substate TO target style's default substate
                MotionData* eax_7 = CMotionTable::get_link(this, esi->style, var_c, esi->substate_mod, ebx_1, ebp_1);
                arg2 = eax_7;

                if (eax_7 == 0)
                {
                    uint32_t style_2 = esi->style;
                    if (ebx_1 != style_2)
                    {
                        // DOUBLE-HOP VIA this->default_style (the table-wide default style, e.g. "Standing"):
                        //   hop 1: current style's default substate -> default_style's matching substate (speed 1.0)
                        arg2 = CMotionTable::get_link(this, style_2, var_c, 1f, this->default_style, 1f);
                        //   hop 2: default_style's default substate -> target style's default substate (speed 1.0)
                        uint32_t defaultStyleDefaultSubstate;
                        LongNIValHash<unsigned long>::lookup(&this->style_defaults, this->default_style, &defaultStyleDefaultSubstate);
                        var_10_1 = CMotionTable::get_link(this, this->default_style, defaultStyleDefaultSubstate, 1f, ebx_1, 1f);
                    }
                }

                // Rebuild the sequence from scratch: clear physics + strip cyclic anims, then layer:
                //   1) the "exit current substate" link  (var_4_1)
                //   2) the direct or hop-1 link           (arg2)
                //   3) the hop-2 link if double-hop taken (var_10_1)
                //   4) the new base cycle itself           (arg3)
                CSequence::clear_physics(arg4);
                CSequence::remove_cyclic_anims(arg4);
                add_motion(arg4, var_4_1, ebp_1);
                add_motion(arg4, arg2,    ebp_1);
                add_motion(arg4, var_10_1, ebp_1);
                add_motion(arg4, arg3,    ebp_1);

                // Commit new state: substate<-arg7(ORIGINAL passed-in arg7, i.e. the stop-flag param!
                //   at this point arg7 has been OVERWRITTEN above by the style_defaults lookup output —
                //   BN reuses arg7's stack slot for the "arg7_style_default" out-param), style<-ebx_1
                esi->substate     = arg7_style_default;  // == the target style's default substate
                esi->style        = ebx_1;
                esi->substate_mod = arg5;
                CMotionTable::re_modify(this, arg4, esi);   // replay any pending modifiers atop new state

                // outTicks = action_head_ticks(arg3) + var_10_1.num_anims + arg2.num_anims(as byte@+0x10) + var_4_1.num_anims - 1
                uint32_t num_anims_2 = (var_4_1 == 0) ? 0 : var_4_1->num_anims;
                uint32_t ecx_20      = (arg2   == 0) ? 0 : *(uint8_t*)(arg2 + 0x10);   // arg2 here is a MotionData*, +0x10 byte = num_anims field (packed layout)
                uint32_t num_anims_1 = (var_10_1== 0) ? 0 : var_10_1->num_anims;
                *arg6 = (uint32_t)arg3->action_head /* NOTE: arg3 is MotionData* here, ->action_head is BN's
                                                        mis-decoded field name for MotionData's tick-count field
                                                        at that offset, NOT the MotionState action_head pointer */
                        + num_anims_1 + ecx_20 + num_anims_2 - 1;
                return 1;
            }
        }
        // else: fall through to the class-bit blocks below with ebx_1 still negative
    }

    // ================================================================
    // BRANCH 2: target id has the CYCLE-CLASS bit (0x40000000) set
    // ================================================================
    if ((ebx_1 & 0x40000000) != 0)
    {
        uint32_t eax_23 = ebx_1 & 0xffffff;
        MotionData* eax_24 = LongHash<MotionData>::lookup(&this->cycles, (esi->style << 16) | eax_23);
        arg3 = eax_24;

        if (eax_24 == 0)
        {
            // no cycle for THIS style at that id -> retry under the table-wide default_style
            eax_24 = LongHash<MotionData>::lookup(&this->cycles, (this->default_style << 16) | eax_23);
            arg3 = eax_24;
            if (eax_24 != 0)
                goto label_522ae6;   // found one under default_style -> proceed as if found directly
            // else: fall through past this whole branch (arg3 stays whatever eax_24 was = 0)
        }
        else
        {
        label_522ae6:
            if (CMotionTable::is_allowed(this, ebx_1, eax_24, esi) != 0)
            {
                // ---- FAST RE-SPEED PATH: same substate, same direction, already animating ----
                if (ebx_1 == substate /* the ORIGINAL esi->substate captured at entry */
                    && same_sign(ebp_1, esi->substate_mod) != 0
                    && CSequence::has_anims(arg4) != 0)
                {
                    change_cycle_speed(arg4, arg3, esi->substate_mod, ebp_1);
                    subtract_motion(arg4, arg3, esi->substate_mod);   // remove old-speed contribution
                    combine_motion(arg4, arg3, ebp_1);                // add new-speed contribution
                    esi->substate_mod = arg5;
                    return 1;
                }

                if ((arg3->bitfield & 1) != 0)          // MotionData.bitfield: this cycle clears mods on entry
                    MotionState::clear_modifiers(esi);

                // direct link from current (style,substate,substate_mod) to target substate
                MotionData* eax_34 = CMotionTable::get_link(this, esi->style, esi->substate, esi->substate_mod, ebx_1, ebp_1);
                arg2 = eax_34;

                bool sameSign = (eax_34 != 0) ? (same_sign(ebp_1, esi->substate_mod) != 0) : false;

                if (eax_34 == 0 || !sameSign)
                {
                    // direct link missing OR direction flipped -> route via style default substate
                    uint32_t styleDefaultSubstate;
                    LongNIValHash<unsigned long>::lookup(&this->style_defaults, esi->style, &styleDefaultSubstate);
                    arg2     = CMotionTable::get_link(this, esi->style, esi->substate, esi->substate_mod, styleDefaultSubstate, 1f);
                    var_10_1 = CMotionTable::get_link(this, esi->style, styleDefaultSubstate, 1f, ebx_1, ebp_1);
                }

                CSequence::clear_physics(arg4);
                CSequence::remove_cyclic_anims(arg4);

                if (var_10_1 == 0)
                {
                    // No double-hop link needed. Decide sign for arg2's contribution:
                    //   if current substate_mod ~0 use sign of arg5 as-is; else if substate_mod's
                    //   sign bit differs from... (BN x87 flag block collapses to:)
                    float signedSpeed = (esi->substate_mod == 0f || same_sign(esi->substate_mod, arg5))
                                         ? arg5 : -arg5;
                    add_motion(arg4, arg2, signedSpeed);
                }
                else
                {
                    add_motion(arg4, arg2, esi->substate_mod);
                    add_motion(arg4, var_10_1, ebp_1);
                }

                add_motion(arg4, arg3, ebp_1);   // layer the new base cycle itself

                // If we're leaving a modifier-class substate (0x20000000) for something else,
                // and the style's default substate isn't the new target, re-register the OLD
                // substate as a still-active modifier (so it keeps contributing) instead of dropping it.
                uint32_t substate_1 = esi->substate;
                if (substate_1 != ebx_1 && (substate_1 & 0x20000000) != 0)
                {
                    uint32_t styleDefaultSubstate2;
                    LongNIValHash<unsigned long>::lookup(&this->style_defaults, esi->style, &styleDefaultSubstate2);
                    if (styleDefaultSubstate2 != ebx_1)
                        MotionState::add_modifier_no_check(esi, substate_1, esi->substate_mod);
                }

                esi->substate_mod = arg5;
                esi->substate     = ebx_1;
                CMotionTable::re_modify(this, arg4, esi);

                uint32_t ecx_45      = (arg2 == 0) ? 0 : *(uint8_t*)(arg2 + 0x10);       // arg2's packed num_anims byte
                uint32_t num_anims_1 = (var_10_1 == 0) ? 0 : var_10_1->num_anims;
                *arg6 = (uint32_t)arg3->action_head /* MotionData tick-count field, see BRANCH 1 note */
                        + num_anims_1 + ecx_45 - 1;
                return 1;
            }
            // is_allowed() rejected -> fall through past this whole branch
        }
    }

    // ================================================================
    // BRANCH 3: target id has the ACTION-CLASS bit (0x10000000) set
    // ================================================================
    if ((ebx_1 & 0x10000000) != 0)
    {
        uint32_t cycleKey = (esi->style << 16) | (substate & 0xffffff);   // current (style,substate) cycle key
        MotionData* eax_57 = LongHash<MotionData>::lookup(&this->cycles, cycleKey);
        if (eax_57 != 0)
        {
            // direct link from current (style,substate) to the action id
            MotionData* eax_60 = CMotionTable::get_link(this, esi->style, substate, esi->substate_mod, ebx_1, ebp_1);
            arg2 = eax_60;
            if (eax_60 != 0)
            {
                MotionState::add_action(esi, ebx_1, ebp_1);   // queue the action id on action_head/action_tail
                CSequence::clear_physics(arg4);
                CSequence::remove_cyclic_anims(arg4);
                add_motion(arg4, arg2, ebp_1);           // the action's own link animation
                add_motion(arg4, eax_57, esi->substate_mod);  // re-add the still-running base cycle
                CMotionTable::re_modify(this, arg4, esi);
                *arg6 = *(uint8_t*)(arg2 + 0x10);        // outTicks = arg2's packed num_anims byte
                return 1;
            }

            // No direct link -> route the action through the style's default substate (double-hop)
            uint32_t styleDefaultSubstate;
            LongNIValHash<unsigned long>::lookup(&this->style_defaults, esi->style, &styleDefaultSubstate);
            MotionData* eax_66 = CMotionTable::get_link(this, esi->style, substate, esi->substate_mod, styleDefaultSubstate, 1f);
            arg2 = eax_66;
            if (eax_66 != 0)
            {
                MotionData* eax_68 = CMotionTable::get_link(this, esi->style, styleDefaultSubstate, 1f, ebx_1, ebp_1);
                if (eax_68 != 0)
                {
                    MotionData* eax_69 = LongHash<MotionData>::lookup(&this->cycles, cycleKey);   // re-fetch base cycle (same key)
                    if (eax_69 != 0)
                    {
                        // link BACK from styleDefaultSubstate to substate (returning-hop, note args flipped
                        // vs eax_68: (style,defaultSubstate,1.0) -> (substate, esi->substate_mod))
                        MotionData* returningLink = CMotionTable::get_link(this, esi->style, styleDefaultSubstate, 1f, substate, esi->substate_mod);

                        MotionState::add_action(esi, ebx_1, ebp_1);
                        CSequence::clear_physics(arg4);
                        CSequence::remove_cyclic_anims(arg4);
                        add_motion(arg4, arg2, 1f);                      // hop 1: substate -> default (speed 1.0)
                        add_motion(arg4, eax_68, ebp_1);                 // hop 2: default -> action (new speed)
                        add_motion(arg4, returningLink, 1f);             // hop back: default -> substate (speed 1.0)
                        add_motion(arg4, eax_69, esi->substate_mod);     // base cycle continuing at old speed
                        CMotionTable::re_modify(this, arg4, esi);

                        uint32_t outTicks = *(uint8_t*)(arg2 + 0x10) + eax_68->num_anims;
                        *arg6 = outTicks;
                        if (returningLink != 0)
                            *arg6 = *(uint8_t*)(returningLink + 0x10) + outTicks;
                        return 1;
                    }
                }
            }
        }
    }

    // ================================================================
    // BRANCH 4: target id has the MODIFIER-CLASS bit (0x20000000) set
    // ================================================================
    if ((ebx_1 & 0x20000000) != 0)
    {
        uint32_t styleShifted = esi->style << 16;
        MotionData* eax_81 = LongHash<MotionData>::lookup(&this->cycles, (substate & 0xffffff) | styleShifted);

        // must have a base cycle for the current (style,substate) AND that cycle must NOT be
        // the "clear modifiers on entry" kind (bitfield bit0 clear)
        if (eax_81 != 0 && (eax_81->bitfield & 1) == 0)
        {
            uint32_t modKey = ebx_1 & 0xffffff;
            MotionData* eax_85 = LongHash<MotionData>::lookup(&this->modifiers, modKey | styleShifted);   // style-specific modifier
            MotionData* eax_87 = 0;
            if (eax_85 == 0)
                eax_87 = LongHash<MotionData>::lookup(&this->modifiers, modKey);    // style-agnostic (global) modifier fallback
            MotionData* modifierData = (eax_85 != 0) ? eax_85 : eax_87;

            if (eax_85 != 0 || eax_87 != 0)
            {
                int32_t added = MotionState::add_modifier(esi, ebx_1, ebp_1);   // returns 0 if already present or == substate
                if (added == 0)
                {
                    // Already active as a modifier or equals current substate -> STOP it first, then re-add
                    // (this is the "toggle" path: requesting the same modifier again removes+readds it,
                    //  which is how e.g. re-triggering a held key restarts a modifier cleanly)
                    uint32_t dummyTicks;
                    CMotionTable::StopSequenceMotion(this, ebx_1, 1f, esi, arg4, &dummyTicks);
                    added = MotionState::add_modifier(esi, ebx_1, ebp_1);
                }
                if (added != 0)
                {
                    combine_motion(arg4, modifierData, ebp_1);   // ADD the modifier's velocity/omega contribution
                    return 1;
                }
            }
        }
    }

    return 0;   // nothing matched any of the four class branches -> failure
}

Cleaned high-level flow of GetObjectSequence

  1. Guard: esi->style == 0 or esi->substate == 0 → fail (0). No-op fast path: requesting the style's own default substate while it's already a modifier-class substate and this isn't a stop-call → success (1) with no work.
  2. Style-change request ((int32_t)ebx_1 < 0, i.e. target id encodes a STYLE not a substate): no-op if already that style; otherwise builds a 12 hop link chain from the current style's default substate to the target style's default substate — with a fallback double-hop through this->default_style (the table's global fallback style, e.g. standing-idle) when no direct style-to-style link exists. Commits esi->style/esi->substate/esi->substate_mod, replays modifiers via re_modify, computes *arg6 (outTicks) as the sum of all appended link/cycle animation tick counts minus 1.
  3. Cycle-class target (ebx_1 & 0x40000000): looks up (or falls back to default_style's) base cyclic MotionData for the target substate; gates via is_allowed. If already in that exact substate with the same speed sign and animations already running, takes the cheap re-speed path (change_cycle_speed + subtract-old/combine-new). Otherwise builds a direct or double-hop (via style default) transition-link chain, optionally preserving the outgoing substate as a still-active modifier if it was itself modifier-class, commits new substate/substate_mod, replays modifiers, computes outTicks.
  4. Action-class target (ebx_1 & 0x10000000): requires a base cycle to already exist for the current substate. Direct link → queue action + layer link + re-add base cycle. No direct link → 2-hop-out-and-back through the style default substate, queuing four animation layers (out-hop, action, back-hop, base cycle) and summing all their tick counts for outTicks.
  5. Modifier-class target (ebx_1 & 0x20000000): requires a base cycle that does NOT clear modifiers on entry. Looks up a style-specific modifier MotionData, falling back to a style-agnostic one. add_modifier is idempotent-guarded; if it reports "already present," the code stops the existing modifier via StopSequenceMotion first, then re-adds — this is the toggle/restart semantics. On success, combine_motion adds the modifier's velocity/omega into the sequence (animation frames untouched — modifiers are physics-only overlays).
  6. Falls through to return 0 if no branch's preconditions were satisfied.

6. CMotionTable::re_modify — 0x005222e0 (@298300)

void __thiscall CMotionTable::re_modify(class CMotionTable const* this, class CSequence* arg2, class MotionState* arg3)
{
    MotionState* esi = arg3;
    if (esi->modifier_head != 0)
    {
        MotionState snapshot;
        MotionState::MotionState(&snapshot, esi);   // copy-construct a snapshot of the CURRENT modifier list
        CSequence* seq = arg2;

        // Walk esi's modifier list, popping ONE entry per iteration, re-driving it through
        // GetObjectSequence (arg7=0, "do motion") so each modifier gets correctly re-applied
        // on top of whatever base cycle/link chain was just installed.
        do
        {
            MotionList* modifier_head = esi->modifier_head;
            uint32_t motion   = modifier_head->motion;
            float    speedMod = modifier_head->speed_mod;
            MotionState::remove_modifier(esi, modifier_head, nullptr);        // pop from esi's live list
            MotionState::remove_modifier(&snapshot, snapshot_head, nullptr);  // pop matching node from the snapshot copy
            CMotionTable::GetObjectSequence(this, motion, esi, seq, speedMod, &outTicksIgnored, 0);
        } while (snapshot.modifier_head != 0);

        MotionState::~MotionState(&snapshot);
    }
}

Cleaned intent: after installing a new base cycle/substate, replay every previously-active modifier back onto the sequence by re-invoking GetObjectSequence for each one. This is why every substate-changing branch in GetObjectSequence calls re_modify right before returning — modifiers (aiming overlays, etc.) must survive a cycle/style transition.


7. CMotionTable::StopSequenceMotion — 0x00522fc0 (@298954)

int32_t __thiscall CMotionTable::StopSequenceMotion(
    class CMotionTable const* this,
    uint32_t arg2,             // motion/substate id being stopped
    float arg3,                // speed_mod (usually 1.0f from call sites)
    class MotionState* arg4,   // current state
    class CSequence* arg5,     // sequence
    uint32_t* arg6)             // OUT: outTicks
{
    *arg6 = 0;

    // Case A: stopping the CYCLE-class substate we are currently in
    if ((arg2 & 0x40000000) != 0 && arg2 == arg4->substate)
    {
        uint32_t styleDefaultSubstate;
        LongNIValHash<unsigned long>::lookup(&this->style_defaults, arg4->style, &styleDefaultSubstate);
        // Re-invoke GetObjectSequence targeting the STYLE'S DEFAULT SUBSTATE, with arg7=1 ("stop" flag)
        CMotionTable::GetObjectSequence(this, styleDefaultSubstate, arg4, arg5, 1f, arg6, 1);
        return 1;
    }

    // Case B: stopping a MODIFIER-class id — search arg4->modifier_head for a matching node
    if ((arg2 & 0x20000000) != 0)
    {
        MotionList* prev = nullptr;
        for (MotionList* node = arg4->modifier_head; node != 0; node = node->next)
        {
            if (node->motion == arg2)
            {
                uint32_t modKey = arg2 & 0xffffff;
                // style-specific modifier data first
                MotionData* modData = LongHash<MotionData>::lookup(&this->modifiers, (arg4->style << 16) | modKey);
                if (modData != 0)
                {
                found:
                    subtract_motion(arg5, modData, node->speed_mod);     // remove its velocity/omega contribution
                    MotionState::remove_modifier(arg4, node, prev);      // unlink from the modifier_head chain
                    return 1;
                }
                // style-agnostic fallback
                modData = LongHash<MotionData>::lookup(&this->modifiers, modKey);
                if (modData != 0)
                    goto found;
                break;   // matching motion id found but NO MotionData for it anywhere -> give up
            }
            prev = node;
        }
    }

    return 0;   // nothing matched -> stop was a no-op
}

Cleaned intent: two independent stop mechanisms. Stopping the active cycle re-drives GetObjectSequence toward the style's default substate (i.e. "return to idle"). Stopping a modifier just unwinds its physics contribution and removes it from the linked list — no animation re-sequencing needed since modifiers don't touch anims[].

Note: action-class (0x10000000) ids are not handled here at all — actions complete via their own tick-countdown mechanism in MotionTableManager::CheckForCompletedMotions/ AnimationDone (popping action_head), not via StopSequenceMotion.


8. CMotionTable::SetDefaultState — 0x005230a0 (@299004)

int32_t __thiscall CMotionTable::SetDefaultState(class CMotionTable const* this,
                                                   class MotionState* arg2,
                                                   class CSequence* arg3,
                                                   uint32_t* arg4)
{
    uint32_t defaultSubstate;
    if (LongNIValHash<unsigned long>::lookup(&this->style_defaults, this->default_style, &defaultSubstate) == 0)
        return 0;                          // table has no entry for its own default_style -> fail

    MotionState::clear_modifiers(arg2);
    MotionState::clear_actions(arg2);

    uint32_t cycleKey = (defaultSubstate & 0xffffff) | (this->default_style << 16);
    // manual open-addressed hash bucket walk against this->cycles (inlined LongHash lookup)
    HashNode* node = this->cycles.buckets[hash(cycleKey) & this->cycles.table_mask];
    if (node != 0)
    {
        while (cycleKey != node->key)
        {
            node = node->hashNext;
            if (node == 0) return 0;
        }
        if (node != EMPTY_SENTINEL /* raw check: ecx_10 != 4 */)
        {
            arg2->style        = this->default_style;
            arg2->substate      = defaultSubstate;
            arg2->substate_mod  = 1f;
            *arg4 = node->motionData->num_anims - 1;    // outTicks
            CSequence::clear_physics(arg3);
            CSequence::clear_animations(arg3);
            add_motion(arg3, node->motionData, arg2->substate_mod);
            return 1;
        }
    }
    return 0;
}

Cleaned intent: reset a MotionState to the motion table's baseline (default_style + that style's default substate), clearing all modifiers/actions and installing the base cyclic animation for that (style,substate) fresh. Used by MotionTableManager::initialize_state on world-enter.


9. CMotionTable::DoObjectMotion / StopObjectMotion / StopObjectCompletely

/* 0x00523e90 @300045 — thin wrapper: "do" == GetObjectSequence with the stop-flag forced to 0 */
int32_t __thiscall CMotionTable::DoObjectMotion(class CMotionTable const* this, uint32_t arg2,
    class MotionState* arg3, class CSequence* arg4, float arg5, uint32_t* arg6)
{
    return CMotionTable::GetObjectSequence(this, arg2, arg3, arg4, arg5, arg6, 0);
}

/* 0x00523ec0 @300053 — thin wrapper: "stop" == tailcall straight into StopSequenceMotion */
int32_t __thiscall CMotionTable::StopObjectMotion(class CMotionTable const* this, uint32_t arg2,
    float arg3, class MotionState* arg4, class CSequence* arg5, uint32_t* arg6)
{
    return CMotionTable::StopSequenceMotion(this, arg2, arg3, arg4, arg5, arg6);
}

/* 0x00523ed0 @300062 */
int32_t __thiscall CMotionTable::StopObjectCompletely(class CMotionTable const* this,
    class MotionState* arg2, class CSequence* arg3, uint32_t* arg4)
{
    MotionState* esi = arg2;
    int32_t result = 0;

    // Stop EVERY currently-active modifier first, one at a time (list shrinks each iteration
    // because StopSequenceMotion unlinks the node it stops)
    for (MotionList* node = esi->modifier_head; node != 0; node = esi->modifier_head)
    {
        float speedMod = node->speed_mod;
        if (CMotionTable::StopSequenceMotion(this, node->motion, speedMod, esi, arg3, arg4) != 0)
            result = 1;
    }

    // Finally stop the base cycle/substate itself
    if (CMotionTable::StopSequenceMotion(this, esi->substate, esi->substate_mod, esi, arg3, arg4) != 0)
        return 1;

    return result;
}

Cleaned intent: StopObjectCompletely is the "return to idle from everything" call — strips every active modifier first (each one individually going through the same subtract-and-unlink path as a single StopObjectMotion call), then stops the base cycle itself (which re-drives GetObjectSequence toward the style's default substate).


10. CMotionTable::~CMotionTable (destructor) — 0x00523f50 (@300087)

void __fastcall CMotionTable::~CMotionTable(class CMotionTable* this)
{
    this->vtable = &CMotionTable::`vftable';
    CMotionTable::Destroy(this);                 // (Destroy body not requested; frees cycles/modifiers/style_defaults contents)
    LongNIValHash<LongHash<MotionData>*>::destroy_contents(&this->links);
    if (!this->links.fPlacementNew_)
        operator delete[](this->links.buckets);
    if (!this->modifiers.fPlacementNew_)
        operator delete[](this->modifiers.buckets);
    // (truncated in source at capture boundary; remaining teardown is style_defaults + cycles hash frees)
}

11. MotionTableManager — full member list (grep of MotionTableManager:: in the pseudo-C)

Member Address Line
SetPhysicsObject 0x0051bbc0 @290464
SetMotionTableID 0x0051bbd0 @290472
GetMotionTableID 0x0051bc10 @290490
Create (static factory) 0x0051bc50 @290510
truncate_animation_list 0x0051bca0 @290533
AnimationDone 0x0051bce0 @290558
HandleExitWorld 0x0051bda0 @290625
HandleEnterWorld 0x0051bdd0 @290634
CheckForCompletedMotions 0x0051be00 @290645
Destroy 0x0051be90 @290705
~MotionTableManager 0x0051bf00 @290761
remove_redundant_links 0x0051bf20 @290771
UseTime 0x0051bfd0 @290845
add_to_queue 0x0051bfe0 @290854
initialize_state 0x0051c030 @290875
PerformMovement 0x0051c0b0 @290906

(No separately-decompiled GetMotion_PersistentStates — not present as a named symbol under MotionTableManager:: in this dump; the CLAUDE.md task listed it speculatively. Confirmed absent via the exhaustive grep above — 16 members is the complete list in this PDB build.)

SetPhysicsObject — 0x0051bbc0 (@290464)

void __thiscall MotionTableManager::SetPhysicsObject(class MotionTableManager* this, class CPhysicsObj* arg2)
{
    this->physics_obj = arg2;
}

SetMotionTableID — 0x0051bbd0 (@290472)

int32_t __thiscall MotionTableManager::SetMotionTableID(class MotionTableManager* this, class IDClass<_tagDataID,32,0> arg2)
{
    if (this->table != 0)
        this->table->vtable->Release();               // drop existing CMotionTable ref

    class CMotionTable* loaded = DBObj::Get(QualifiedDataID(arg2, /*type tag*/ 0xe));
    int32_t result = (loaded != 0);
    this->table = loaded;
    return result;
}

Type-tag 0xe identifies the DBObj category for motion tables in DBObj::Get's qualified-ID dispatch.

GetMotionTableID — 0x0051bc10 (@290490)

class IDClass<_tagDataID,32,0>* __thiscall MotionTableManager::GetMotionTableID(
    class MotionTableManager const* this, class IDClass<_tagDataID,32,0>* __return)
{
    if (this->table != 0)
    {
        *(uint32_t*)__return = this->table->m_DID.id;
        return __return;
    }
    *(uint32_t*)__return = INVALID_DID.id;
    return __return;
}

Create (static factory) — 0x0051bc50 (@290510)

class MotionTableManager* MotionTableManager::Create(class IDClass<_tagDataID,32,0> arg1)
{
    void* result = operator new(0x2c);      // sizeof(MotionTableManager) == 44 bytes
    if (result == 0) return 0;

    result->physics_obj = 0;
    result->table       = 0;
    MotionState::MotionState((char*)result + 8);   // placement-construct the embedded `state` member
    result->animation_counter    = 0;   // offset +0x20
    result->pending_animations.head_ = 0;  // +0x24
    result->pending_animations.tail_ = 0;  // +0x28

    if (arg1 != INVALID_DID.id)
        MotionTableManager::SetMotionTableID(result, arg1);

    return result;
}

Offset map confirms the struct layout above: physics_obj@0x0, table@0x4, state@0x8 (24 bytes: style/substate/substate_mod/modifier_head/action_head/action_tail = 4+4+4+4+4+4=24, i.e. 0x8..0x1f), animation_counter@0x20, pending_animations@0x24 (head_@0x24, tail_@0x28). Total 0x2c = 44 bytes, matches operator new(0x2c).

truncate_animation_list — 0x0051bca0 (@290533)

void __thiscall MotionTableManager::truncate_animation_list(class MotionTableManager* this,
    class MotionTableManager::AnimNode const* arg2 /* stop-at node, exclusive */, class CSequence* arg3)
{
    if (arg2 == 0) return;

    // Walk pending_animations.tail_ BACKWARD toward head_, zeroing each node's num_anims (tick
    // count) field and accumulating the total ticks removed, UNTIL we reach arg2 (not included).
    DLListData* node = this->pending_animations.tail_;
    uint32_t removedTicks = 0;
    while (node != arg2)
    {
        if (node == 0) return;                 // arg2 wasn't actually in the list -> abort quietly
        removedTicks += ((AnimNode*)node)->num_anims;
        ((AnimNode*)node)->num_anims = 0;       // neuter the tick count (node stays in the list though!)
        node = node->dllist_prev;
    }

    CSequence::remove_link_animations(arg3, removedTicks);   // strip that many ticks' worth of link anims from the sequence
}

Note: this does not unlink the neutered nodes from pending_animations — it only zeroes their num_anims tick-countdown so CheckForCompletedMotions/AnimationDone treat them as "already complete" and pop them without firing CPhysicsObj::MotionDone for their real motion (since the countdown-based completion loop tests num_anims == 0 and immediately dequeues+frees them, but WITHOUT the 0x10000000 action-head-pop / MotionDone callback semantics being meaningfully attached to a zeroed node — effectively "cancel these queued transitions silently").

AnimationDone — 0x0051bce0 (@290558)

void __thiscall MotionTableManager::AnimationDone(class MotionTableManager* this, int32_t arg2 /* success flag passed to CPhysicsObj::MotionDone */)
{
    DLListData* head = this->pending_animations.head_;
    if (head == 0) return;

    this->animation_counter += 1;

    // Pop every head node whose tick-countdown (num_anims) is <= the running animation_counter
    while (((AnimNode*)head)->num_anims <= this->animation_counter)
    {
        if ((((AnimNode*)head)->motion & 0x10000000) != 0)      // action-class node
            MotionState::remove_action_head(&this->state);       // pop the matching queued action

        CPhysicsObj::MotionDone(this->physics_obj, ((AnimNode*)head)->motion, arg2);
        this->animation_counter -= ((AnimNode*)head)->num_anims;

        // Manual DLList unlink-and-free of `head` (standard doubly-linked-list node removal,
        // handling both-null / single-neighbor / has-neighbor cases for prev and next pointers)
        DLListUnlinkAndDelete(&this->pending_animations, head);

        head = this->pending_animations.head_;
        if (head == 0) break;
    }

    if (this->animation_counter != 0 && head == 0)
        this->animation_counter = 0;    // list drained -> reset counter to avoid drift
}

Cleaned intent: advance the animation clock by one tick and fire MotionDone for every queued motion whose duration has elapsed. num_anims on a pending-queue node doubles as a relative tick-duration (not an absolute deadline) — that's why it's subtracted from animation_counter after firing (a decrementing countdown chain, not absolute timestamps).

HandleExitWorld — 0x0051bda0 (@290625)

void __fastcall MotionTableManager::HandleExitWorld(class MotionTableManager* this, class CSequence* arg2)
{
    while (this->pending_animations.head_ != 0)
        MotionTableManager::AnimationDone(this, 0);   // drain the whole queue, signaling "failure/aborted" (arg2=0)
}

HandleEnterWorld — 0x0051bdd0 (@290634)

void __thiscall MotionTableManager::HandleEnterWorld(class MotionTableManager* this, class CSequence* arg2)
{
    CSequence::remove_all_link_animations(arg2);        // strip any stale link anims from the sequence
    while (this->pending_animations.head_ != 0)
        MotionTableManager::AnimationDone(this, 0);      // drain the whole queue (same as ExitWorld)
}

Both handlers fully drain pending_animations — the difference is HandleEnterWorld also scrubs the live CSequence's link animations first (fresh-world-state guarantee).

CheckForCompletedMotions — 0x0051be00 (@290645)

void __fastcall MotionTableManager::CheckForCompletedMotions(class MotionTableManager* this)
{
    DLListData* head = this->pending_animations.head_;
    if (head == 0) return;

    // Pop every head node whose tick-countdown is EXACTLY 0 already (no counter increment here,
    // unlike AnimationDone — this is the "did anything finish with zero remaining ticks" poll)
    while (((AnimNode*)head)->num_anims == 0)
    {
        if ((((AnimNode*)head)->motion & 0x10000000) != 0)
            MotionState::remove_action_head(&this->state);

        CPhysicsObj::MotionDone(this->physics_obj, ((AnimNode*)head)->motion, 1);   // success flag = 1 (hardcoded)

        DLListUnlinkAndDelete(&this->pending_animations, head);

        head = this->pending_animations.head_;
        if (head == 0) break;
    }
}

Distinction from AnimationDone: CheckForCompletedMotions never increments animation_counter and only pops nodes that are ALREADY at num_anims == 0 (e.g. zero-tick entries, or ones neutered by truncate_animation_list), always signaling success (arg2=1 hardcoded). AnimationDone is the real per-tick clock; CheckForCompletedMotions/UseTime is a lightweight "sweep any already-zero entries" pass called every frame via UseTime.

Destroy — 0x0051be90 (@290705)

void __fastcall MotionTableManager::Destroy(class MotionTableManager* this)
{
    // Unconditionally unlink+delete every node in pending_animations (no MotionDone callbacks fired)
    while (this->pending_animations.head_ != 0)
        DLListUnlinkAndDelete(&this->pending_animations, this->pending_animations.head_);

    if (this->table != 0)
    {
        this->table->vtable->Release();
        this->table = nullptr;
    }
}

~MotionTableManager — 0x0051bf00 (@290761)

void __fastcall MotionTableManager::~MotionTableManager(class MotionTableManager* this)
{
    MotionTableManager::Destroy(this);
    /* tailcall */
    return MotionState::~MotionState(&this->state);   // frees modifier_head + action_head/tail chains
}
void __thiscall MotionTableManager::remove_redundant_links(class MotionTableManager* this, class CSequence* arg2)
{
    DLListData* tail = this->pending_animations.tail_;
    if (tail == 0) return;

    // Skip backward over any trailing zero-tick nodes (already-neutered/instant entries)
    while (((AnimNode*)tail)->num_anims == 0)
    {
        tail = tail->dllist_prev;
        if (tail == 0) return;
    }

    uint32_t motion = ((AnimNode*)tail)->motion;

    if ((motion & 0x40000000) != 0 && (motion & 0x20000000) == 0)
    {
        // CYCLE-class (and not also modifier-class) tail node: look backward for an EARLIER
        // node with the SAME motion id that also has a non-zero tick count — if found, every
        // node between them is a now-redundant intermediate link that can be truncated, UNLESS
        // an intervening node has a HIGH-priority class bit (0xb0000000 = cycle|action|... mask)
        // with a non-zero tick count, in which case we bail (can't safely collapse through it).
        DLListData* scan = tail->dllist_prev;
        while (scan != 0)
        {
            uint32_t scanMotion = ((AnimNode*)scan)->motion;
            if (scanMotion == motion && ((AnimNode*)scan)->num_anims != 0)
                break;   // found the earlier matching node -> collapse everything after it
            if (((AnimNode*)scan)->num_anims != 0 && (scanMotion & 0xb0000000) != 0)
                return;  // blocked by an intervening "important" non-zero node -> can't collapse
            scan = scan->dllist_prev;
            if (scan == 0) return;
        }
        if (scan != 0)
            MotionTableManager::truncate_animation_list(this, scan, arg2);
    }
    else if ((int32_t)motion < 0)
    {
        // STYLE-class tail node (top bit set): same backward scan, but the "block" mask is
        // wider (0x70000000 = cycle|action|modifier) and the match test is exact-equality only
        // (no separate not-modifier-class requirement on the match itself).
        DLListData* scan = tail->dllist_prev;
        while (scan != 0)
        {
            uint32_t scanMotion = ((AnimNode*)scan)->motion;
            if (scanMotion == motion)
                break;
            if (((AnimNode*)scan)->num_anims != 0 && (scanMotion & 0x70000000) != 0)
                return;
            scan = scan->dllist_prev;
            if (scan == 0) return;
        }
        if (scan != 0)
            MotionTableManager::truncate_animation_list(this, scan, arg2);
    }
}

Cleaned intent: collapse redundant back-to-back re-entries of the same motion in the pending queue — e.g. if the player rapidly re-issues the same run cycle or the same style change several times before the first one's link animation finishes, everything between the earlier matching entry and the new tail entry is provably superseded and gets truncated (ticks zeroed), UNLESS a higher-priority class of motion (action/modifier for cycle-tails; action/modifier/cycle for style-tails) sits in between with real remaining ticks, in which case the collapse is unsafe and skipped.

UseTime — 0x0051bfd0 (@290845)

void __fastcall MotionTableManager::UseTime(class MotionTableManager* this)
{
    /* tailcall */
    return MotionTableManager::CheckForCompletedMotions(this);
}

Per-frame entry point (called from CSequence's owning object's per-tick update, per the earlier MotionTableManager::UseTime(motion_table_manager) call site at 0x00517d67/@285852) that just sweeps already-complete queue entries.

add_to_queue — 0x0051bfe0 (@290854)

void __thiscall MotionTableManager::add_to_queue(class MotionTableManager* this, uint32_t arg2 /*motion*/, uint32_t arg3 /*ticks*/, class CSequence* arg4)
{
    AnimNode* node = (AnimNode*)operator new(0x10);   // sizeof(AnimNode) == 16: dllist_next+prev (8) + motion+num_anims (8)
    if (node != 0)
    {
        node->dllist_next = 0;
        node->dllist_prev = 0;
        node->motion    = arg2;
        node->num_anims = arg3;     // NOTE: here num_anims holds the TICK COUNT (outTicks from GetObjectSequence), not an anim array length
    }
    DLListBase::InsertAfter(&this->pending_animations, node, this->pending_animations.tail_);
    MotionTableManager::remove_redundant_links(this, arg4);   // opportunistically collapse right after insert
}

initialize_state — 0x0051c030 (@290875)

void __thiscall MotionTableManager::initialize_state(class MotionTableManager* this, class CSequence* arg2)
{
    uint32_t outTicks = 0;
    if (this->table != 0)
        CMotionTable::SetDefaultState(this->table, &this->state, arg2, &outTicks);
    // else outTicks stays 0

    AnimNode* node = (AnimNode*)operator new(0x10);
    if (node != 0)
    {
        node->dllist_next = 0;
        node->dllist_prev = 0;
        node->motion    = 0x41000003;    // sentinel motion id: "initial default state installed" marker
        node->num_anims = outTicks;
    }
    DLListBase::InsertAfter(&this->pending_animations, node, this->pending_animations.tail_);
    MotionTableManager::remove_redundant_links(this, arg2);
}

0x41000003 recurs as the sentinel motion id for "stop completely / reset to default" queue entries — also used verbatim in PerformMovement's StopCompletely and StopInterpretedCommand-failure paths below.

PerformMovement — 0x0051c0b0 (@290906) — COMPLETE

uint32_t __thiscall MotionTableManager::PerformMovement(class MotionTableManager* this,
    class MovementStruct const* arg2, class CSequence* arg3)
{
    if (this->table == 0)
        return 7;                          // error code: no motion table loaded

    uint32_t outTicks = 0;

    switch (arg2->type)
    {
    case MovementTypes::InterpretedCommand:   // 0x2
        if (CMotionTable::DoObjectMotion(this->table, arg2->motion, &this->state, arg3,
                                          arg2->params->speed, &outTicks) != 0)
        {
            MotionTableManager::add_to_queue(this, arg2->motion, outTicks, arg3);
            return 0;                       // success
        }
        // fall through to the trailing `return 0x43` below (BN control flow: no explicit
        // else-branch for the failed-DoObjectMotion case within InterpretedCommand)
        break;

    case 4:   // MovementTypes::StopInterpretedCommand
        if (CMotionTable::StopObjectMotion(this->table, arg2->motion, arg2->params->speed,
                                            &this->state, arg3, &outTicks) != 0)
        {
            MotionTableManager::add_to_queue(this, 0x41000003, outTicks, arg3);   // sentinel motion id on stop-success
            return 0;
        }
        break;   // falls through to 0x43 on failure, same as above

    case 5:   // MovementTypes::StopCompletely
        CMotionTable::StopObjectCompletely(this->table, &this->state, arg3, &outTicks);
        MotionTableManager::add_to_queue(this, 0x41000003, outTicks, arg3);
        return 0;

    default:   // anything else (RawCommand, StopRawCommand, MoveToObject, MoveToPosition,
               // TurnToObject, TurnToHeading, Invalid) is NOT handled by MotionTableManager at all
        return arg3;   // BN artifact: returns the CSequence pointer reinterpreted as uint32_t —
                        // effectively "not my job, caller's arg3 pointer value leaks out as the
                        // return code" for these unhandled types. (Likely dead/unreachable in
                        // practice since callers gate on type before reaching here, or this
                        // return value is never consulted for those types.)
    }

    return 0x43;   // shared failure code: DoObjectMotion / StopObjectMotion returned false
}

Cleaned high-level flow: PerformMovement is the single chokepoint between the wire-level MovementStruct (interpreted command / stop / stop-completely) and the motion-table state machine. InterpretedCommand drives DoObjectMotionGetObjectSequence(...,arg7=0). StopInterpretedCommand (type 4) drives StopObjectMotionStopSequenceMotion. StopCompletely (type 5) drives StopObjectCompletely (strip all modifiers + stop base cycle), unconditionally queuing the 0x41000003 sentinel regardless of return value (unlike the other two cases which only queue on success). Every successful path funnels the resulting tick count into add_to_queue, which itself opportunistically calls remove_redundant_links to collapse superseded queue entries.


12. Where MotionTableManager is driven from CPhysicsObj/CSequence (call-site context, not full functions — for orientation only)

0x00517d1d @285801   MotionTableManager::initialize_state(motion_table_manager, &this->sequence)
0x00517d37 @285815   return MotionTableManager::AnimationDone(motion_table_manager, arg2)
0x00517d57 @285838   return MotionTableManager::CheckForCompletedMotions(motion_table_manager)
0x00517d67 @285852   return MotionTableManager::UseTime(motion_table_manager)
0x00517d7d @285863   MotionTableManager::HandleEnterWorld(motion_table_manager, &this->sequence)
0x00517d9d @285877   MotionTableManager::HandleExitWorld(motion_table_manager, edx)
0x00518722 @286744   class MotionTableManager* eax_2 = MotionTableManager::Create(ebx)
0x00518737 @286750   MotionTableManager::SetPhysicsObject(eax_2, this->owner)
0x005186ed @286753   else if (MotionTableManager::GetMotionTableID(motion_table_manager, &arg2)->id != ebx)
0x00518707 @286759   MotionTableManager::~MotionTableManager(motion_table_manager_1)
0x005187e9 @286793   return MotionTableManager::PerformMovement(motion_table_manager, &var_64, &this->sequence)
0x00518889 @286819   return MotionTableManager::PerformMovement(motion_table_manager, &var_64, &this->sequence)
0x00518917 @286843   return MotionTableManager::PerformMovement(motion_table_manager, &var_64, &this->sequence)
0x005192ad @287430   MotionTableManager::~MotionTableManager(motion_table_manager)

These are all inside a CPhysicsObj-scoped wrapper (the surrounding function at ~0x00517xxx is a CPhysicsObj member that owns motion_table_manager and this->sequence; three separate call sites to PerformMovement at 0x005187e9/0x00518889/0x00518917 correspond to three distinct public CPhysicsObj motion-request entry points that all funnel into the same PerformMovement(this, &localMovementStruct, &this->sequence) pattern). Not expanded further — out of scope for the motion-table layer itself per the task (these are CPhysicsObj, not CMotionTable/MotionTableManager/free-function territory).


13. MotionState member functions (state the motion table reads/writes — full support cast)

MotionState::MotionState (default ctor) — 0x00525fd0 (@302759)

void __fastcall MotionState::MotionState(class MotionState* this)
{
    this->style = 0;
    this->substate = 0;
    this->substate_mod = 1f;
    this->modifier_head = nullptr;
    this->action_head = nullptr;
    this->action_tail = nullptr;
}

MotionState::MotionState (copy ctor) — 0x00526790 (@303432) — signature only (body not required by task scope; used by re_modify's snapshot copy)

void __thiscall MotionState::MotionState(class MotionState* this, class MotionState const* arg2)

MotionState::add_modifier_no_check — 0x00525ff0 (@302772)

void __thiscall MotionState::add_modifier_no_check(class MotionState* this, uint32_t arg2 /*motion*/, float arg3 /*speed_mod*/)
{
    MotionList* node = (MotionList*)operator new(0xc);   // sizeof(MotionList) == 12: motion(4)+speed_mod(4)+next(4)
    if (node != 0)
    {
        node->motion = 0;
        node->speed_mod = 1f;
        node->next = nullptr;
    }
    node->motion    = arg2;
    node->speed_mod = arg3;
    node->next      = this->modifier_head;   // push-front onto modifier_head
    this->modifier_head = node;
}

MotionState::add_modifier — 0x00526340 (@303081)

int32_t __thiscall MotionState::add_modifier(class MotionState* this, uint32_t arg2, float arg3)
{
    for (MotionList* i = this->modifier_head; i != 0; i = i->next)
        if (i->motion == arg2)
            return 0;                       // already present as a modifier -> caller must Stop-then-Add to "refresh"

    if (this->substate == arg2)
        return 0;                            // already IS the current base substate -> refuse (no-op)

    MotionState::add_modifier_no_check(this, arg2, arg3);
    return 1;
}

MotionState::remove_modifier — 0x00526040 (@302794)

void __thiscall MotionState::remove_modifier(class MotionState* this, class MotionList* arg2 /*node to remove*/, class MotionList* arg3 /*predecessor, or null if arg2 is head*/)
{
    if (arg3 != 0)
    {
        arg3->next = arg2->next;
        operator delete(arg2);
        return;
    }
    this->modifier_head = arg2->next;
    operator delete(arg2);
}

MotionState::clear_modifiers — 0x00526070 (@302810)

void __fastcall MotionState::clear_modifiers(class MotionState* this)
{
    for (MotionList* i = this->modifier_head; i != 0; )
    {
        MotionList* next = i->next;
        operator delete(i);
        i = next;
    }
    this->modifier_head = nullptr;
}

MotionState::add_action — 0x005260a0 (@302828)

void __thiscall MotionState::add_action(class MotionState* this, uint32_t arg2, float arg3)
{
    MotionList* node = (MotionList*)operator new(0xc);
    if (node != 0) { node->motion=0; node->speed_mod=1f; node->next=nullptr; }
    node->motion    = arg2;
    node->speed_mod = arg3;
    node->next      = nullptr;

    if (this->action_head == 0)
        this->action_head = node;

    if (this->action_tail != 0)
        this->action_tail->next = node;      // append to tail (FIFO queue, unlike modifier's push-front stack)

    this->action_tail = node;
}

MotionState::clear_actions — 0x005260f0 (@302859)

void __fastcall MotionState::clear_actions(class MotionState* this)
{
    for (MotionList* i = this->action_head; i != 0; )
    {
        MotionList* next = i->next;
        operator delete(i);
        i = next;
    }
    this->action_head = nullptr;
    this->action_tail = nullptr;
}

MotionState::remove_action_head — 0x00526120 (@302877)

uint32_t __fastcall MotionState::remove_action_head(class MotionState* this)
{
    MotionList* head = this->action_head;
    if (head == 0) return 0;

    MotionList* next = head->next;
    uint32_t motion = head->motion;
    this->action_head = next;
    if (next == 0)
        this->action_tail = next;            // list now empty -> clear tail too

    operator delete(head);
    return motion;                            // returns the popped action's motion id (caller mostly ignores it)
}

MotionState::~MotionState — 0x00526330 (@303072) — signature only (frees both chains; body not dumped, trivially clear_modifiers+clear_actions equivalent based on ctor/field symmetry)

void __fastcall MotionState::~MotionState(class MotionState* this)

14. Cross-reference: MotionList — one struct, two independent chains

MotionState owns two separate singly-linked MotionList chains that never intermix:

  • modifier_head — a stack (push-front via add_modifier_no_check, arbitrary removal via remove_modifier given a node+predecessor pointer). Represents currently-active modifier overlays (0x20000000-class motions).
  • action_head/action_tail — a FIFO queue (append-tail via add_action, pop-front via remove_action_head). Represents queued one-shot actions (0x10000000-class motions) waiting for their turn to fire / complete.

Both chains use the identical 12-byte MotionList{motion, speed_mod, next} node — only the insertion/removal discipline differs, matching how the two motion classes behave semantically (modifiers overlay/toggle in any order; actions execute in submission order).


15. Constants summary (verbatim, all confirmed via grep against this dump)

Constant Meaning Source
0x40000000 cycle-class motion id bit GetObjectSequence, StopSequenceMotion, remove_redundant_links
0x20000000 modifier-class motion id bit GetObjectSequence, StopSequenceMotion, remove_redundant_links
0x10000000 action-class motion id bit GetObjectSequence, AnimationDone, CheckForCompletedMotions
0xb0000000 combined cycle|action|(bit29 unset variant) block-mask used in remove_redundant_links's cycle-tail scan remove_redundant_links
0x70000000 combined cycle|action|modifier block-mask used in remove_redundant_links's style-tail scan remove_redundant_links
0x41000003 sentinel "stop-completely / default-state-installed" motion id queued to pending_animations initialize_state, PerformMovement (StopInterpretedCommand success + StopCompletely)
0.000199999995f (~0.0002f) epsilon for "speed is effectively zero" in change_cycle_speed change_cycle_speed
0xe DBObj qualified-type-tag for motion table assets SetMotionTableID → DBObj::Get(QualifiedDataID(id, 0xe))
0x2c (44 bytes) sizeof(MotionTableManager) Create's operator new(0x2c)
0xc (12 bytes) sizeof(MotionList) add_modifier_no_check / add_action's operator new(0xc)
0x10 (16 bytes) sizeof(MotionTableManager::AnimNode) add_to_queue / initialize_state's operator new(0x10)
0x14 (20 bytes) AnimData element stride within MotionData::anims[] add_motion's ebx_1 += 0x14
0x2 (bit1) MotionData.bitfield: "substate-gated" (is_allowed requires exact/default-substate match) is_allowed
0x1 (bit0) MotionData.bitfield: "clear modifiers on entry" GetObjectSequence (both cycle-class and style-change branches)
PerformMovement error codes 7 = no table loaded; 0x43 = DoObjectMotion/StopObjectMotion returned failure; 0 = success PerformMovement
MovementTypes::Type values Invalid=0, RawCommand=1, InterpretedCommand=2, StopRawCommand=3, StopInterpretedCommand=4, StopCompletely=5, MoveToObject=6, MoveToPosition=7, TurnToObject=8, TurnToHeading=9 acclient.h:2856

16. Function inventory checklist (against the task's requested coverage)

  • CMotionTable::GetObjectSequence 0x00522860 @298636 — COMPLETE, all 4 class branches + fast paths
  • CMotionTable::get_link 0x00522710 @298552 — COMPLETE
  • CMotionTable::is_allowed 0x005226c0 @298526 — COMPLETE
  • CMotionTable::GetDefaultStyle/GetDefaultMotionNOT PRESENT as named symbols in this PDB build (grepped CMotionTable:: exhaustively — 22 members total, see full grep output below). The task's "GetDefaultStyle/GetDefaultMotion" likely refer to the style_defaults hash lookups (LongNIValHash<unsigned long>::lookup(&this->style_defaults, style, &out)) that appear inline throughout GetObjectSequence/get_link/SetDefaultState/is_allowed — there is no separate named accessor method; it's always an inline hash lookup.
  • CMotionTable::DoObjectMotion 0x00523e90 @300045 — COMPLETE (thin wrapper)
  • CMotionTable::StopObjectMotion 0x00523ec0 @300053 — COMPLETE (thin wrapper)
  • CMotionTable::StopSequenceMotion 0x00522fc0 @298954 — COMPLETE
  • CMotionTable::re_modify 0x005222e0 @298300 — COMPLETE
  • CMotionTable::StopObjectCompletely 0x00523ed0 @300062 — COMPLETE (bonus, referenced by PerformMovement)
  • CMotionTable::SetDefaultState 0x005230a0 @299004 — COMPLETE (bonus, referenced by initialize_state)
  • add_motion 0x005224b0 @298437 — COMPLETE
  • combine_motion 0x00522580 @298472 — COMPLETE
  • subtract_motion 0x00522600 @298492 — COMPLETE
  • change_cycle_speed 0x00522290 @298276 — COMPLETE
  • same_sign 0x00522260 @298253 — COMPLETE (BN x87-flag noise cleaned)
  • MotionTableManager ctor/Create — COMPLETE (0x0051bc50 @290510)
  • MotionTableManager::SetPhysicsObject — COMPLETE (0x0051bbc0 @290464)
  • MotionTableManager::PerformMovement — COMPLETE, all 3 handled MovementTypes + default (0x0051c0b0 @290906)
  • MotionTableManager::add_to_queue 0x0051bfe0 @290854 — COMPLETE
  • MotionTableManager::remove_redundant_links 0x0051bf20 @290771 — COMPLETE
  • MotionTableManager::truncate_animation_list — COMPLETE (0x0051bca0 @290533)
  • MotionTableManager::CheckForCompletedMotions 0x0051be00 @290645(~290645) — COMPLETE
  • MotionTableManager::AnimationDone — COMPLETE (0x0051bce0 @290558)
  • MotionTableManager::UseTime — COMPLETE (0x0051bfd0 @290845)
  • MotionTableManager::HandleEnterWorld/HandleExitWorld — COMPLETE (0x0051bdd0/0x0051bda0)
  • MotionTableManager::GetMotion_PersistentStatesNOT PRESENT in this PDB build (confirmed via exhaustive grep of MotionTableManager:: — 16 total members, listed in section 11)
  • MotionStruct/state types (MotionState, MotionList, MotionData, AnimNode, MovementStruct, MovementTypes::Type) — struct layouts + all reader/writer member functions (section 13) COMPLETE
  • Grep of full CMotionTable:: member list performed (22 total members: CMotionTable ctor, scalar/vector deleting destructors, Allocate, re_modify, is_allowed, get_link, GetObjectSequence, StopSequenceMotion, SetDefaultState, Pack, UnPack, Destroy, DoObjectMotion, StopObjectMotion, StopObjectCompletely, ~CMotionTable — Pack/UnPack/Destroy bodies not expanded here as out-of-scope serialization plumbing, not motion-selection logic)
  • Grep of full MotionTableManager:: member list performed (16 total members, all covered above)