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

1603 lines
75 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)
```c
/* 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 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)."
- `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 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.
- `0x10000000`**action 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)
```c
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.
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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-forwardrun-forward vs walk-backwardrun-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
```c
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)
```c
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)
```c
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)
```c
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`
```c
/* 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)
```c
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)
```c
void __thiscall MotionTableManager::SetPhysicsObject(class MotionTableManager* this, class CPhysicsObj* arg2)
{
this->physics_obj = arg2;
}
```
### `SetMotionTableID` — 0x0051bbd0 (@290472)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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
```c
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)
```c
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)
```c
void __thiscall MotionState::MotionState(class MotionState* this, class MotionState const* arg2)
```
### `MotionState::add_modifier_no_check` — 0x00525ff0 (@302772)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
```c
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)
- [x] `CMotionTable::GetObjectSequence` 0x00522860 @298636 COMPLETE, all 4 class branches + fast paths
- [x] `CMotionTable::get_link` 0x00522710 @298552 COMPLETE
- [x] `CMotionTable::is_allowed` 0x005226c0 @298526 COMPLETE
- [x] `CMotionTable::GetDefaultStyle`/`GetDefaultMotion` **NOT 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.
- [x] `CMotionTable::DoObjectMotion` 0x00523e90 @300045 COMPLETE (thin wrapper)
- [x] `CMotionTable::StopObjectMotion` 0x00523ec0 @300053 COMPLETE (thin wrapper)
- [x] `CMotionTable::StopSequenceMotion` 0x00522fc0 @298954 COMPLETE
- [x] `CMotionTable::re_modify` 0x005222e0 @298300 COMPLETE
- [x] `CMotionTable::StopObjectCompletely` 0x00523ed0 @300062 COMPLETE (bonus, referenced by PerformMovement)
- [x] `CMotionTable::SetDefaultState` 0x005230a0 @299004 COMPLETE (bonus, referenced by initialize_state)
- [x] `add_motion` 0x005224b0 @298437 COMPLETE
- [x] `combine_motion` 0x00522580 @298472 COMPLETE
- [x] `subtract_motion` 0x00522600 @298492 COMPLETE
- [x] `change_cycle_speed` 0x00522290 @298276 COMPLETE
- [x] `same_sign` 0x00522260 @298253 COMPLETE (BN x87-flag noise cleaned)
- [x] `MotionTableManager` ctor/Create COMPLETE (0x0051bc50 @290510)
- [x] `MotionTableManager::SetPhysicsObject` COMPLETE (0x0051bbc0 @290464)
- [x] `MotionTableManager::PerformMovement` COMPLETE, all 3 handled MovementTypes + default (0x0051c0b0 @290906)
- [x] `MotionTableManager::add_to_queue` 0x0051bfe0 @290854 COMPLETE
- [x] `MotionTableManager::remove_redundant_links` 0x0051bf20 @290771 COMPLETE
- [x] `MotionTableManager::truncate_animation_list` COMPLETE (0x0051bca0 @290533)
- [x] `MotionTableManager::CheckForCompletedMotions` 0x0051be00 @290645(~290645) COMPLETE
- [x] `MotionTableManager::AnimationDone` COMPLETE (0x0051bce0 @290558)
- [x] `MotionTableManager::UseTime` COMPLETE (0x0051bfd0 @290845)
- [x] `MotionTableManager::HandleEnterWorld`/`HandleExitWorld` COMPLETE (0x0051bdd0/0x0051bda0)
- [x] `MotionTableManager::GetMotion_PersistentStates` **NOT PRESENT** in this PDB build (confirmed
via exhaustive grep of `MotionTableManager::` 16 total members, listed in section 11)
- [x] `MotionStruct`/state types (`MotionState`, `MotionList`, `MotionData`, `AnimNode`,
`MovementStruct`, `MovementTypes::Type`) struct layouts + all reader/writer member
functions (section 13) COMPLETE
- [x] 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)
- [x] Grep of full `MotionTableManager::` member list performed (16 total members, all covered above)