- 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>
75 KiB
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):
0x40000000— cycle class: substate IDs that name a cyclic/looping animation state directly addressable viathis->cycles. GetObjectSequence'sebx_1 < 0branch (i.e. sign bit / top bit set — note0x40000000alone 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& 0x40000000cycle-lookup path lower down) and theStopSequenceMotionearly-out both special-case this bit: "this IS the current substate, no transition — reissue via GetDefaultStyle equivalent (style_defaults default) witharg7=1(stop flag)."0x20000000— modifier 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 viaMotionState::modifier_headlinked list (add_modifier/add_modifier_no_check/remove_modifier).StopSequenceMotionwalksmodifier_headto find and subtract a matching modifier when this bit is set.0x10000000— action class: one-shot, non-cycle-replacing actions (e.g. attacks, emotes) queued ontoMotionState::action_head/action_tailviaadd_action, and popped in FIFO viaremove_action_headwhen their queued AnimNode completes (CheckForCompletedMotions/AnimationDonetestnum_anims_node_flags & 0x10000000to 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.000199999995f ≈ 0.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.
4. CMotionTable::get_link — 0x00522710 (@298552)
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
- Guard:
esi->style == 0oresi->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. - 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 1–2 hop link chain from the current style's default substate to the target style's default substate — with a fallback double-hop throughthis->default_style(the table's global fallback style, e.g. standing-idle) when no direct style-to-style link exists. Commitsesi->style/esi->substate/esi->substate_mod, replays modifiers viare_modify, computes*arg6(outTicks) as the sum of all appended link/cycle animation tick counts minus 1. - Cycle-class target (
ebx_1 & 0x40000000): looks up (or falls back todefault_style's) base cyclic MotionData for the target substate; gates viais_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. - 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. - 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_modifieris idempotent-guarded; if it reports "already present," the code stops the existing modifier viaStopSequenceMotionfirst, then re-adds — this is the toggle/restart semantics. On success,combine_motionadds the modifier's velocity/omega into the sequence (animation frames untouched — modifiers are physics-only overlays). - Falls through to
return 0if 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
}
remove_redundant_links — 0x0051bf20 (@290771)
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 DoObjectMotion → GetObjectSequence(...,arg7=0).
StopInterpretedCommand (type 4) drives StopObjectMotion → StopSequenceMotion.
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 viaadd_modifier_no_check, arbitrary removal viaremove_modifiergiven a node+predecessor pointer). Represents currently-active modifier overlays (0x20000000-class motions).action_head/action_tail— a FIFO queue (append-tail viaadd_action, pop-front viaremove_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::GetObjectSequence0x00522860 @298636 — COMPLETE, all 4 class branches + fast pathsCMotionTable::get_link0x00522710 @298552 — COMPLETECMotionTable::is_allowed0x005226c0 @298526 — COMPLETECMotionTable::GetDefaultStyle/GetDefaultMotion— NOT PRESENT as named symbols in this PDB build (greppedCMotionTable::exhaustively — 22 members total, see full grep output below). The task's "GetDefaultStyle/GetDefaultMotion" likely refer to thestyle_defaultshash lookups (LongNIValHash<unsigned long>::lookup(&this->style_defaults, style, &out)) that appear inline throughoutGetObjectSequence/get_link/SetDefaultState/is_allowed— there is no separate named accessor method; it's always an inline hash lookup.CMotionTable::DoObjectMotion0x00523e90 @300045 — COMPLETE (thin wrapper)CMotionTable::StopObjectMotion0x00523ec0 @300053 — COMPLETE (thin wrapper)CMotionTable::StopSequenceMotion0x00522fc0 @298954 — COMPLETECMotionTable::re_modify0x005222e0 @298300 — COMPLETECMotionTable::StopObjectCompletely0x00523ed0 @300062 — COMPLETE (bonus, referenced by PerformMovement)CMotionTable::SetDefaultState0x005230a0 @299004 — COMPLETE (bonus, referenced by initialize_state)add_motion0x005224b0 @298437 — COMPLETEcombine_motion0x00522580 @298472 — COMPLETEsubtract_motion0x00522600 @298492 — COMPLETEchange_cycle_speed0x00522290 @298276 — COMPLETEsame_sign0x00522260 @298253 — COMPLETE (BN x87-flag noise cleaned)MotionTableManagerctor/Create — COMPLETE (0x0051bc50 @290510)MotionTableManager::SetPhysicsObject— COMPLETE (0x0051bbc0 @290464)MotionTableManager::PerformMovement— COMPLETE, all 3 handled MovementTypes + default (0x0051c0b0 @290906)MotionTableManager::add_to_queue0x0051bfe0 @290854 — COMPLETEMotionTableManager::remove_redundant_links0x0051bf20 @290771 — COMPLETEMotionTableManager::truncate_animation_list— COMPLETE (0x0051bca0 @290533)MotionTableManager::CheckForCompletedMotions0x0051be00 @290645(~290645) — COMPLETEMotionTableManager::AnimationDone— COMPLETE (0x0051bce0 @290558)MotionTableManager::UseTime— COMPLETE (0x0051bfd0 @290845)MotionTableManager::HandleEnterWorld/HandleExitWorld— COMPLETE (0x0051bdd0/0x0051bda0)MotionTableManager::GetMotion_PersistentStates— NOT PRESENT in this PDB build (confirmed via exhaustive grep ofMotionTableManager::— 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)