- 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>
1603 lines
75 KiB
Markdown
1603 lines
75 KiB
Markdown
# 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-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
|
||
|
||
```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 **1–2 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)
|