# R3 — CMotionInterp completion + MovementManager relay: verbatim decomp extraction Source: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (line numbers = file line numbers, not addresses). Structs: `docs/research/named-retail/acclient.h`. Already ported (S2a/D6, NOT re-extracted here): `move_to_interpreted_state`, `apply_interpreted_movement`, `DoInterpretedMotion`, `StopInterpretedMotion`, `contact_allows_move`, `adjust_motion`, `apply_run_to_command`, `apply_raw_movement`, `get_state_velocity`. --- ## 0. Key structs (acclient.h) ```c /* acclient.h:31407 — struct #3457 */ struct __cppobj CMotionInterp { int initted; CWeenieObject *weenie_obj; CPhysicsObj *physics_obj; RawMotionState raw_state; InterpretedMotionState interpreted_state; float current_speed_factor; int standing_longjump; float jump_extent; unsigned int server_action_stamp; float my_run_rate; LList pending_motions; }; /* acclient.h:53293 — struct #5857 */ struct __cppobj CMotionInterp::MotionNode : LListData { unsigned int context_id; // +4 (offset from LListData's own +0 next-ptr) unsigned int motion; // +8 — the "action-class" field; bit 0x10000000 = action-class flag unsigned int jump_error_code; // +0xc }; /* acclient.h:30943 — struct #3463 */ struct __cppobj MovementManager { CMotionInterp *motion_interpreter; MoveToManager *moveto_manager; CPhysicsObj *physics_obj; CWeenieObject *weenie_obj; }; /* acclient.h:38069 — struct #4067 */ 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; }; /* acclient.h:2856 — enum #229 */ 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, }; /* acclient.h:31453 — struct #3460 */ struct __cppobj MovementParameters : PackObj { union { unsigned int bitfield; ... } ___u1; // flags: bit0x8=SetHoldKey, bit0x20=RawMotionState::ApplyMotion/RemoveMotion mirror, bit0x40=InterpretedMotionState::ApplyMotion/RemoveMotion, bit0x80000000(sign)=interrupt_current_movement, bit0x1ee0f default set (see MovementParameters ctor) float distance_to_object; float min_distance; float desired_heading; float speed; float fail_distance; float walk_run_threshhold; unsigned int context_id; HoldKey hold_key_to_apply; unsigned int action_stamp; }; ``` `MovementParameters` default ctor (line 300510-300534, `00524380`): `min_distance=0`, `distance_to_object=0.6`, `fail_distance=FLT_MAX`, `desired_heading=0`, `speed=1`, `walk_run_threshhold=15`, `context_id=0`, `hold_key_to_apply=HoldKey_Invalid`, `action_stamp=0`, bitfield defaults to `(bitfield & 0xfffdee0f) | 0x1ee0f` via a static cached template (`normal_bitfield`). --- ## 1. `pending_motions` lifecycle ### 1a. `CMotionInterp::add_to_queue` — `00527b80` @ line 305032 ```c 00527b80 void __thiscall CMotionInterp::add_to_queue(class CMotionInterp* this, uint32_t arg2, uint32_t arg3, uint32_t arg4) 00527b80 { 00527b85 class LListData* eax = operator new(0x10); 00527b8f if (eax == 0) 00527bae eax = nullptr; 00527b8f else 00527b8f { 00527b99 *(int32_t*)((char*)eax + 4) = arg2; // MotionNode.context_id = arg2 00527ba0 eax->llist_next = 0; 00527ba6 *(int32_t*)((char*)eax + 8) = arg3; // MotionNode.motion = arg3 00527ba9 *(int32_t*)((char*)eax + 0xc) = arg4; // MotionNode.jump_error_code = arg4 00527b8f } 00527b8f 00527bb0 class LListData** tail_ = this->pending_motions.tail_; 00527bb0 00527bb8 if (tail_ != 0) 00527bb8 { 00527bba *(uint32_t*)tail_ = eax; 00527bbc this->pending_motions.tail_ = eax; 00527bc3 return; 00527bb8 } 00527bb8 00527bc6 this->pending_motions.head_ = eax; 00527bcc this->pending_motions.tail_ = eax; 00527b80 } ``` Cleaned flow: allocate a `MotionNode{context_id=arg2, motion=arg3, jump_error_code=arg4}`, append to the singly-linked `pending_motions` queue (append at tail; if queue was empty, set both head and tail). **Callers** (every enqueue point in CMotionInterp): - `StopCompletely` (line 305227): `add_to_queue(this, 0, 0x41000003 /*forward=none*/, eax_2)` where `eax_2 = motion_allows_jump(this, interpreted_state.forward_command)`. - `DoInterpretedMotion` (line 305607): `add_to_queue(this, arg3->context_id, arg2 /*the motion just applied*/, eax_5)` where `eax_5` is the jump-error-code computed just above (see §3 `motion_allows_jump`). - `StopInterpretedMotion` (line 305657): `add_to_queue(this, arg3->context_id, 0x41000003, result_1)` — always re-queues a "return to none" motion node after a successful stop. - `apply_interpreted_movement` (line 305775): `add_to_queue(this, var_c /*uninitialized in decomp — context_id*/, 0x41000003, eax_10)` when the fallback "stop 0x6500000d" path succeeds. ### 1b. `CMotionInterp::motions_pending` — `00527fe0` @ line 305322 ```c 00527fe0 int32_t __fastcall CMotionInterp::motions_pending(class CMotionInterp const* this) 00527fe0 { 00527fea int32_t result; 00527fea result = this->pending_motions.head_ != 0; 00527fed return result; 00527fe0 } ``` Cleaned: `motions_pending()` = `pending_motions.head_ != null`. Non-empty queue means "at least one motion in flight waiting for a `MotionDone` callback." **Caller (`MovementManager::motions_pending`, `00524280` @ line 300365):** ```c 00524280 int32_t __fastcall MovementManager::motions_pending(class MovementManager const* this) 00524280 { 00524280 class CMotionInterp* motion_interpreter = this->motion_interpreter; 00524284 if ((motion_interpreter != 0 && CMotionInterp::motions_pending(motion_interpreter) != 0)) 00524294 return 1; 00524294 return 0; 00524280 } ``` ### 1c. `CMotionInterp::MotionDone` — `00527ec0` @ line 305238 (FULL BODY) ```c 00527ec0 void __fastcall CMotionInterp::MotionDone(class CMotionInterp* this, int32_t arg2) 00527ec0 { 00527ec3 class CPhysicsObj* physics_obj = this->physics_obj; 00527ec8 if (physics_obj != 0) 00527ec8 { 00527eca class LListData* head_ = this->pending_motions.head_; 00527ed2 if (head_ != 0) 00527ed2 { 00527edb if ((*(int32_t*)((char*)head_ + 8) & 0x10000000) != 0) // MotionNode.motion & 0x10000000 (action-class bit) 00527edb { 00527edd CPhysicsObj::unstick_from_object(physics_obj); 00527ee5 InterpretedMotionState::RemoveAction(&this->interpreted_state); 00527eed RawMotionState::RemoveAction(&this->raw_state); 00527edb } 00527edb 00527ef2 class LListData* head__1 = this->pending_motions.head_; 00527efa if (head__1 != 0) 00527efa { 00527efc class LListData* llist_next = head__1->llist_next; 00527f00 this->pending_motions.head_ = llist_next; 00527f06 if (llist_next == 0) 00527f08 this->pending_motions.tail_ = llist_next; 00527f08 00527f0f head__1->llist_next = 0; 00527f15 operator delete(head__1); 00527efa } 00527ed2 } 00527ec8 } 00527ec0 } ``` Cleaned flow: `MotionDone(arg2)` — note `arg2` (the "was interrupted?" flag from the caller) is **read into `edx` but never actually used** in this build (dead parameter per the `MovementManager::MotionDone` relay below, which loads `var_4_1 = arg3` then passes an uninitialized `edx` through — a decompiler artifact, but functionally this build's `CMotionInterp::MotionDone` body never branches on `arg2`). The real logic: 1. If `physics_obj == null`, no-op (motion interpreter isn't attached). 2. If `pending_motions` is empty, no-op. 3. Peek the **head** node. If `head.motion & 0x10000000` (the **action-class flag**, i.e. this pending motion was queued via the `0x10000000`-flagged multi-action path — see `DoMotion`'s `InterpretedMotionState::GetNumActions(...) >= 6` gate and `move_to_interpreted_state`'s per-action replay loop), then: - `CPhysicsObj::unstick_from_object(physics_obj)` — release any "stuck to object" sticky-manager attachment associated with this action. - `InterpretedMotionState::RemoveAction(&interpreted_state)` — pop the action-list head off the interpreted state's `actions` queue. - `RawMotionState::RemoveAction(&raw_state)` — same for raw state. 4. Unconditionally pop the queue head (dequeue + `delete` the `LListData` node), updating `tail_` to null if the queue is now empty. **No explicit `CheckForCompletedMotions` on `CMotionInterp` itself** — the actual "walk the completed-animation list and fire callbacks" driver lives on `MotionTableManager`/`CPartArray` (see §6). `CMotionInterp::MotionDone` is the *consumer* end of that pipeline: `CPhysicsObj::MotionDone` → `MovementManager::MotionDone` → `CMotionInterp::MotionDone`, called once per completed animation node. ### 1d. `CMotionInterp::HandleExitWorld` — `00527f30` (drains the whole queue, same action-class handling, looped until empty) ```c 00527f30 void __fastcall CMotionInterp::HandleExitWorld(class CMotionInterp* this) 00527f30 { 00527f3b for (class LListData* head_ = this->pending_motions.head_; head_ != 0; head_ = this->pending_motions.head_) 00527f3b { 00527f40 class CPhysicsObj* physics_obj = this->physics_obj; 00527f49 if ((physics_obj != 0 && head_ != 0)) 00527f49 { 00527f52 if ((*(int32_t*)((char*)head_ + 8) & 0x10000000) != 0) 00527f52 { 00527f54 CPhysicsObj::unstick_from_object(physics_obj); 00527f5c InterpretedMotionState::RemoveAction(&this->interpreted_state); 00527f64 RawMotionState::RemoveAction(&this->raw_state); 00527f52 } 00527f52 00527f69 class LListData* head__1 = this->pending_motions.head_; 00527f71 if (head__1 != 0) 00527f71 { 00527f73 class LListData* llist_next = head__1->llist_next; 00527f77 this->pending_motions.head_ = llist_next; 00527f7d if (llist_next == 0) 00527f7f this->pending_motions.tail_ = llist_next; 00527f7f 00527f86 head__1->llist_next = 0; 00527f8c operator delete(head__1); 00527f71 } 00527f49 } 00527f3b } 00527f30 } ``` Identical body to `MotionDone`'s single-pop logic, just looped until the queue drains (world exit = flush all pending motion callbacks unconditionally, since no more animation-completion events will arrive). --- ## 2. `DoMotion` family — `00528d20` @ line 306159 ```c 00528d20 uint32_t __thiscall CMotionInterp::DoMotion(class CMotionInterp* this, uint32_t arg2, class MovementParameters const* arg3) 00528d20 { 00528d26 class CPhysicsObj* physics_obj = this->physics_obj; 00528d2b if (physics_obj == 0) 00528d36 return 8; 00528d36 00528d3a uint32_t ebp = arg2; // ebp = original motion id (unmutated copy) 00528d46 union __inner0 = arg3->__inner0; // MovementParameters bitfield 00528d4b float distance_to_object = arg3->distance_to_object; 00528d52 float min_distance = arg3->min_distance; 00528d59 float desired_heading = arg3->desired_heading; 00528d60 float speed = arg3->speed; 00528d67 float fail_distance = arg3->fail_distance; 00528d6e float walk_run_threshhold = arg3->walk_run_threshhold; 00528d75 uint32_t context_id = arg3->context_id; 00528d7c enum HoldKey hold_key_to_apply = arg3->hold_key_to_apply; 00528d80 uint32_t action_stamp = arg3->action_stamp; 00528d83 arg2 = ebp; 00528d87 int32_t var_2c = 0x7c83f8; // local MovementParameters vtable stamp (stack copy) 00528d8f union __inner0_2 = __inner0; 00528d93 uint32_t action_stamp_1 = action_stamp; 00528d93 00528d97 if (*(uint8_t*)((char*)__inner0 + 1) < 0) // bitfield high-byte sign bit => "interrupt" flag 00528d99 CPhysicsObj::interrupt_current_movement(physics_obj); 00528d99 00528da4 if ((*(uint8_t*)((char*)__inner0_1 + 1) & 8) != 0) // bit 0x800 => SetHoldKey requested 00528db3 CMotionInterp::SetHoldKey(this, arg3->hold_key_to_apply, ((__inner0_1 >> 0xf) & 1)); 00528db3 00528dc8 CMotionInterp::adjust_motion(this, &arg2, &speed, arg3->hold_key_to_apply); 00528dc8 00528dd4 if (this->interpreted_state.current_style != 0x8000003d) // not "MotionStance_NonCombat" (the free/uninhibited style) 00528dd4 { 00528ddd if (ebp == 0x41000012) 00528e22 return 0x3f; 00528e22 00528de0 if (ebp == 0x41000013) 00528e14 return 0x40; 00528e14 00528de3 if (ebp == 0x41000014) 00528e06 return 0x41; 00528e06 00528deb if ((ebp & 0x2000000) != 0) 00528df8 return 0x42; 00528dd4 } 00528dd4 00528e2b if (((ebp & 0x10000000) != 0 && InterpretedMotionState::GetNumActions(&this->interpreted_state) >= 6)) 00528e45 return 0x45; 00528e45 00528e55 uint32_t result = CMotionInterp::DoInterpretedMotion(this, arg2, &var_2c); 00528e55 00528e66 if ((result == 0 && (*(uint8_t*)((char*)((int16_t)arg3->__inner0))[1] & 0x20) != 0)) // bit 0x2000 => mirror to raw_state 00528e6d RawMotionState::ApplyMotion(&this->raw_state, ebp, arg3); 00528e6d 00528e7b return result; 00528d20 } ``` Cleaned flow: 1. No-op (return `8` = "no physics object") if `physics_obj` is null. 2. Snapshot every `MovementParameters` field onto the stack (locals), then rebuild a **fresh local `MovementParameters` (`var_2c`)** — this is `DoMotion` defaulting: the incoming `arg3` is copied field-by-field but the call into `DoInterpretedMotion` below passes the **freshly-defaulted local**, not the caller's `arg3`, except for the mutated `arg2`/`speed`/`hold_key_to_apply` triple that `adjust_motion` writes in place. In other words: `DoMotion` re-derives a canonical `MovementParameters` from the caller's flags/speed/holdkey, discarding caller-supplied distance/heading/ fail-distance fields for the interpreted-motion call (those only matter for MoveTo-family commands, not raw DoMotion). 3. If the incoming bitfield's sign bit is set → `interrupt_current_movement` (cancel any physics-level transition in progress before applying the new motion). 4. If bit `0x800` (`SetHoldKey` flag) is set → call `SetHoldKey(hold_key_to_apply, bit0xf)` BEFORE `adjust_motion` runs, so the new hold-key affects the walk/run reinterpretation below. 5. `adjust_motion(&arg2, &speed, hold_key_to_apply)` — this is the D6-ported function; mutates `arg2` (the motion id) and `speed` in place per the retail walk/run/sidestep reinterpretation table. 6. **Combat-stance gate**: if `interpreted_state.current_style != 0x8000003d` (i.e. the creature is in ANY combat/special stance, not the neutral `MotionStance_NonCombat`), then jump-charge motions (`0x41000012`/`13`/`14`) and any motion with bit `0x2000000` set are **rejected outright** with distinct error codes `0x3f`/`0x40`/`0x41`/`0x42` — you cannot charge or release a jump while in combat stance, non-combat with bow drawn, etc. 7. **Action-queue depth gate**: if the motion is an "action-class" motion (bit `0x10000000` set) AND `InterpretedMotionState::GetNumActions(...) >= 6`, reject with `0x45` ("too many pending actions" — 6 is the hard cap on queued interpreted actions). 8. Otherwise delegate to `DoInterpretedMotion(this, arg2, &var_2c)` (the already-ported function) using the **locally reconstructed** `MovementParameters`. 9. If that succeeded AND bit `0x2000` of the ORIGINAL `arg3->__inner0` is set (mirror- to-raw flag), replay the motion into `raw_state` via `RawMotionState::ApplyMotion`. 10. Return whatever `DoInterpretedMotion` returned (0 = success, propagated error code otherwise). **Relationship to `DoInterpretedMotion`**: `DoMotion` is a thin **gating + defaulting wrapper** around `DoInterpretedMotion`. It exists specifically for the `RawCommand` movement-type entry point (`MovementTypes::Type::RawCommand = 1`, dispatched via `CMotionInterp::PerformMovement` case 0, see §5), whereas `InterpretedCommand` (type 2, case 1) calls `DoInterpretedMotion` directly with the caller's own `MovementParameters` (no combat-stance / action-depth gating, no forced-local-defaulting). So: raw commands (keyboard input from THIS client, i.e. autonomous local movement) get extra validation that interpreted/remote commands skip. --- ## 3. Jump family ### 3a. `CMotionInterp::motion_allows_jump` — `005279e0` @ line 304908 (`__pure`) ```c 005279e0 uint32_t __stdcall CMotionInterp::motion_allows_jump(class CMotionInterp* this @ ecx, uint32_t arg2) __pure 005279e0 { 005279e9 if (arg2 > 0x40000018) 005279e9 { 00527a24 if (arg2 > 0x41000014) 00527a10 return 0; 00527a10 00527a39 if ((arg2 < 0x41000012 && (arg2 < 0x4000001e || arg2 > 0x40000039))) 00527a10 return 0; 005279e9 } 005279e9 else if (arg2 < 0x40000016) 005279f0 { 005279f7 if (arg2 > 0x10000131) 005279f7 { 00527a18 if (arg2 != 0x40000008) 00527a1c return 0; 005279f7 } 005279f7 else if ((arg2 < 0x10000128 && (arg2 < 0x1000006f || arg2 > 0x10000078))) 00527a10 return 0; 005279f0 } 005279f0 00527a40 return 0x48; 005279e0 } ``` Cleaned **[polarity corrected per W0-pins A1 — the original note here was inverted]**: returns `0x48` = jump BLOCKED ("YouCantJumpFromThisPosition"-class error code) for a specific **blocklist** of motion-id ranges, else `0` = jump allowed (pass). Callers treat nonzero as the error (`jump_is_allowed` @305541: `if (eax_7 != 0) return eax_7;`; `jump` 0x00528780 executes only when the chain returns 0). Blocked set: - `arg2 in [0x1000006f, 0x10000078]` (MagicPowerUp01..MagicPowerUp10) — BLOCKED. - `arg2 in [0x10000128, 0x10000131]` (TripleThrustLow..MagicPowerUp07Purple) — BLOCKED. - `arg2 == 0x40000008` (**Fallen** — NOT Falling; ACE mis-transcribed this as Falling) — BLOCKED. - `arg2 == 0x40000016` through `0x40000018` inclusive (Reload/Unload/Pickup) — BLOCKED (falls through the `else if` since neither branch passes it; note `< 0x40000016` is the else-if guard, so `0x40000016..0x40000018` skip both inner branches and hit `return 0x48` directly). - `arg2 in [0x4000001e, 0x40000039]` (AimLevel..MagicPray) — BLOCKED. - `arg2 in [0x41000012, 0x41000014]` (Crouch/Sitting/Sleeping) — BLOCKED. - everything else → `0` (pass), including **Falling `0x40000015`**, Ready `0x41000003`, Dead `0x40000011`, and all ids > `0x41000014` (turns etc.). This is the "can retail let you jump given the motion currently applying" check — used by `StopCompletely`, `DoInterpretedMotion`, `move_to_interpreted_state` to compute the `jump_error_code` stashed in each `MotionNode`. ### 3b. `CMotionInterp::jump_charge_is_allowed` — `00527a50` @ line 304935 ```c 00527a50 uint32_t __fastcall CMotionInterp::jump_charge_is_allowed(class CMotionInterp* this) 00527a50 { 00527a53 class CWeenieObject* weenie_obj = this->weenie_obj; 00527a58 if ((weenie_obj != 0 && weenie_obj->vtable->CanJump(this->jump_extent) == 0)) 00527a6d return 0x49; 00527a6d 00527a6e uint32_t forward_command = this->interpreted_state.forward_command; 00527a87 if ((forward_command != 0x40000008 && (forward_command <= 0x41000011 || forward_command > 0x41000014))) 00527a8c return 0; 00527a8c 00527a93 return 0x48; 00527a50 } ``` Cleaned **[polarity corrected per W0-pins A1]**: `0x49` if the weenie's `CanJump(jump_extent)` virtual rejects the current charge amount (e.g. stamina/burden gate). `0x48` = charge BLOCKED if the **current interpreted forward_command** is either exactly `0x40000008` (Fallen — prone) OR strictly inside `(0x41000011, 0x41000014]` (Crouch/Sitting/Sleeping `0x41000012..0x41000014`); anything else → `0` = charge allowed (pass). ### 3c. `CMotionInterp::get_jump_v_z` — `00527aa0` @ line 304953 ```c 00527aa0 float __fastcall CMotionInterp::get_jump_v_z(class CMotionInterp const* this) 00527aa0 { 00527aa0 class CMotionInterp* jump_extent = this; 00527aa4 jump_extent = this->jump_extent; 00527aac long double temp0 = ((long double)0.000199999995f); 00527ab2 float result; 00527ab2 result = /* jump_extent < 0.0002f (epsilon) ? */; 00527ab4 bool p = /* test ah, 0x5 — i.e. "jump_extent < 0.0002" */; 00527ab7 if (p) 00527ab7 { 00527ab9 long double x87_r7_1 = ((long double)jump_extent); 00527abd long double temp1_1 = ((long double)1f); 00527ac3 result = /* jump_extent < 1.0f */; 00527ac8 if ((*(uint8_t*)((char*)result)[1] & 0x41) == 0) // i.e. jump_extent >= 1.0 00527aca jump_extent = 0x3f800000; // clamp jump_extent to 1.0f 00527aca 00527ad2 class CWeenieObject* weenie_obj = this->weenie_obj; 00527ad7 if (weenie_obj == 0) 00527ae0 return result; 00527ae0 00527aed result = weenie_obj->vtable->InqJumpVelocity(jump_extent, &jump_extent); 00527ab7 } 00527ab7 00527b01 return result; 00527aa0 } ``` Cleaned: if `jump_extent < 0.0002` (essentially zero — no charge held), **`result` is left as the raw FCOM-flags byte from the comparison, effectively returning 0** (this looks like a decompiler mis-render of a `fldz`-style zero return, not a real "return the comparison flags" — the practical behavior is: **near-zero charge returns 0 velocity contribution**, deferred entirely to the `else` path). If `jump_extent >= 0.0002`, clamp `jump_extent` to `[extent, 1.0]` (values ≥ 1.0 get clamped to exactly `1.0f`), then if there's a weenie object, call its `InqJumpVelocity(jump_extent, &jump_extent)` virtual to get the actual world-space jump Z velocity for that charge fraction (this is where creature-specific jump power / `JUMP_STAMINA` scaling lives — outside CMotionInterp, in `CWeenieObject`/`PlayerWeenie`). If no weenie object, returns the clamped `jump_extent` directly as the velocity (fallback for non-weenie physics objects). ### 3d. `CMotionInterp::get_leave_ground_velocity` — `005280c0` @ line 305404 ```c 005280c0 void __thiscall CMotionInterp::get_leave_ground_velocity(class CMotionInterp* this, class AC1Legacy::Vector3* arg2) 005280c0 { 005280c4 class AC1Legacy::Vector3* esi = arg2; 005280cc CMotionInterp::get_state_velocity(this, esi); // fills esi.x/esi.y from interpreted forward/sidestep speed (D6-ported) 005280d8 arg2 = ((float)CMotionInterp::get_jump_v_z(this)); 005280e2 long double x87_r0_2 = fabsl(((long double)esi->x)); 005280e4 esi->z = arg2; // esi.z = jump vertical velocity 005280e7 long double x87_r7 = ((long double)0.000199999995f); 005280ef /* if fabs(esi.x) < 0.0002 */ 005280f4 if ( /* fabs(esi.x) < 0.0002 */ ) 005280f4 { 005280fd long double x87_r0_4 = fabsl(((long double)esi->y)); 00528105 /* if fabs(esi.y) < 0.0002 */ 0052810c if ( /* fabs(esi.y) < 0.0002 */ ) 0052810c { 00528116 long double x87_r0_6 = fabsl(((long double)arg2)); // fabs(esi.z) 0052811e /* if fabs(esi.z) < 0.0002 */ 00528125 if ( /* fabs(esi.z) < 0.0002 */ ) 00528125 { 0052812b class CPhysicsObj* physics_obj = this->physics_obj; 0052814b // esi = physics_obj->m_velocityVector transformed by m_position.frame.m_fl2gv (local->global velocity basis, 3x3-ish) 0052814b long double x87_r0_10 = (m_fl2gv[1]*vel.y + m_fl2gv[0]*vel.x) + m_fl2gv[2]*vel.z; 00528172 long double x87_r0_14 = (m_fl2gv[4]*vel.y + m_fl2gv[3]*vel.x) + m_fl2gv[5]*vel.z; 0052818e long double x87_r0_17 = m_fl2gv[7]*vel.y + m_fl2gv[6]*vel.x; 00528196 long double x87_r7_14 = m_fl2gv[8]*vel.z; 0052819c esi->x = ((float)x87_r0_10); 0052819e esi->y = ((float)x87_r0_14); 005281ab esi->z = ((float)(x87_r0_17 + x87_r7_14)); 00528125 } 0052810c } 005280f4 } 005280c0 } ``` Cleaned flow: compute the leave-ground (jump) launch velocity vector. 1. Start with `get_state_velocity(esi)` — the horizontal (x/y) velocity implied by the current interpreted forward/sidestep speed (already ported in D6). 2. Set `esi.z = get_jump_v_z(this)` — the vertical jump-charge velocity. 3. **Fallback**: if the resulting velocity vector is essentially zero on ALL THREE axes (`|x| < 0.0002 && |y| < 0.0002 && |z| < 0.0002` — i.e. no forward/sidestep motion AND no jump charge, e.g. leaving ground via a fall-off-edge rather than an active jump), overwrite `esi` entirely with the **physics object's current local velocity transformed into global space** via the `m_position.frame.m_fl2gv` 3x3 local→global basis matrix (a 9-float row-major array: `m_fl2gv[0..8]`). This preserves momentum from e.g. being pushed off a ledge, rather than snapping to zero velocity. `m_fl2gv` — "frame local to global vector" transform matrix, part of `Position.frame`. **Caller**: `LeaveGround` (§4b) — this is the velocity CMotionInterp hands to `CPhysicsObj::set_local_velocity` the instant the mover leaves the ground. ### 3e. `CMotionInterp::charge_jump` — `005281c0` @ line 305448 ```c 005281c0 uint32_t __fastcall CMotionInterp::charge_jump(class CMotionInterp* this) 005281c0 { 005281c3 class CWeenieObject* weenie_obj = this->weenie_obj; 005281c8 if ((weenie_obj != 0 && weenie_obj->vtable->CanJump(this->jump_extent) == 0)) 005281dd return 0x49; 005281dd 005281de uint32_t forward_command = this->interpreted_state.forward_command; 005281f7 if ((forward_command == 0x40000008 || (forward_command > 0x41000011 && forward_command <= 0x41000014))) 00528233 return 0x48; 00528233 005281fc uint8_t transient_state = ((int8_t)this->physics_obj->transient_state); 00528222 if (((transient_state & 1) != 0 && ((transient_state & 2) != 0 && (forward_command == 0x41000003 && (this->interpreted_state.sidestep_command == 0 && this->interpreted_state.turn_command == 0))))) 00528224 this->standing_longjump = 1; 00528224 0052822c return 0; 005281c0 } ``` Cleaned **[polarity corrected per W0-pins A1]**: `0x49` if `CanJump` virtual rejects the current charge. `0x48` = charge BLOCKED if `forward_command` is `0x40000008` (Fallen) or in `[0x41000012,0x41000014]` (Crouch/Sitting/Sleeping) — the same blocked set as `jump_charge_is_allowed`'s gate. Otherwise: `standing_longjump = 1` **iff** the physics object is currently grounded (`transient_state` bits `0x1` AND `0x2` both set — matches `CPhysicsObj::on_ground`, §3g) AND the mover is perfectly idle (`forward_command == 0x41000003 /*none*/`, `sidestep_command == 0`, `turn_command == 0`) — i.e. a **standing** long-jump charge (distinct code path from a running jump). Returns `0` = success (charge accepted, flag set if it was a standing charge). **Caller** (from outside CMotionInterp, line 376144, `0056afac`): ``` uint32_t eax_3 = CMotionInterp::charge_jump(CPhysicsObj::get_minterp(SmartBox::smartbox->player)); ``` Reached via the player-input/SmartBox layer — outside R3 scope (input handling), noted for completeness only. ### 3f. `CMotionInterp::jump` — `00528780` @ line 305792 ```c 00528780 uint32_t __thiscall CMotionInterp::jump(class CMotionInterp* this, float arg2, int32_t* arg3) 00528780 { 00528783 class CPhysicsObj* physics_obj = this->physics_obj; 00528788 if (physics_obj == 0) 00528790 return 8; 00528790 00528794 CPhysicsObj::interrupt_current_movement(physics_obj); 005287a5 uint32_t result = CMotionInterp::jump_is_allowed(this, arg2, arg3); 005287ae if (result != 0) 005287ae { 005287ca this->standing_longjump = 0; 005287d2 return result; 005287ae } 005287ae 005287b4 class CPhysicsObj* physics_obj_1 = this->physics_obj; 005287b8 this->jump_extent = arg2; 005287bb CPhysicsObj::set_on_walkable(physics_obj_1, result); // result == 0 here, so set_on_walkable(false) 005287c4 return result; 00528780 } ``` Cleaned: `arg2` = jump extent/charge fraction (`0.0`..`1.0`), `arg3` = out-param for stamina cost (threaded through `jump_is_allowed` → `JumpStaminaCost`). `8` if no physics object. Always calls `interrupt_current_movement` first (cancel any pending transition). Calls `jump_is_allowed(arg2, arg3)` (§3h below); on failure, **clears** `standing_longjump` (a failed jump attempt cancels any pending standing-longjump flag) and returns the error. On success (`result == 0`): store `jump_extent = arg2`, then `CPhysicsObj::set_on_walkable(physics_obj, 0)` — explicitly mark the physics object as **no longer on a walkable surface** (the jump has begun; this is what triggers the ground→air physics transition that eventually calls back into `LeaveGround`). ### 3g. `CPhysicsObj::on_ground` — `00527b20` @ line 304996 (context, referenced by 3e) ```c 00527b20 int32_t __fastcall CPhysicsObj::on_ground(class CPhysicsObj const* this) 00527b20 { 00527b20 uint8_t transient_state = ((int8_t)this->transient_state); 00527b2c if (((transient_state & 1) != 0 && (transient_state & 2) != 0)) 00527b33 return 1; 00527b33 return 0; 00527b20 } ``` Both `transient_state` bit `0x1` and bit `0x2` must be set for "on ground" — same test inlined directly in `charge_jump`, `contact_allows_move`, `is_standing_still`. ### 3h. `CMotionInterp::jump_is_allowed` — `005282b0` (context — feeds `jump()`, §3f) ```c 005282b0 uint32_t __thiscall CMotionInterp::jump_is_allowed(class CMotionInterp* this, float arg2, int32_t* arg3) 005282b0 { 005282b8 if (this->physics_obj != 0) 005282b8 { 005282ba class CWeenieObject* weenie_obj = this->weenie_obj; 005282bf int32_t eax_2; 005282bf if (weenie_obj != 0) 005282c3 eax_2 = weenie_obj->vtable->IsCreature(); 005282c3 005282c8 if ((weenie_obj != 0 && eax_2 == 0)) // non-creature weenie => skip contact/ground gating 005282c8 { 005282f6 label_5282f6: 005282fd if (CPhysicsObj::IsFullyConstrained(this->physics_obj) != 0) 00528305 return 0x47; 00528305 00528308 class LListData* head_ = this->pending_motions.head_; 00528310 uint32_t eax_6; 00528310 if (head_ != 0) 00528312 eax_6 = *(int32_t*)((char*)head_ + 0xc); // peek head's jump_error_code 00528312 00528317 if ((head_ == 0 || eax_6 == 0)) 00528317 { 0052831b eax_6 = CMotionInterp::jump_charge_is_allowed(this); 0052831b 00528322 if (eax_6 == 0) 00528322 { 0052832b uint32_t eax_7 = CMotionInterp::motion_allows_jump(this, this->interpreted_state.forward_command); 0052832b 00528334 if (eax_7 != 0) 00528355 return eax_7; 00528355 00528336 class CWeenieObject* weenie_obj_1 = this->weenie_obj; 0052833b if (weenie_obj_1 == 0) 00528355 return eax_7; 00528355 0052834e eax_6 = 0x47; 00528353 if (weenie_obj_1->vtable->JumpStaminaCost(arg2, arg3) != 0) 00528355 return eax_7; // eax_7 == 0 here (success) 00528322 } 00528317 } 00528317 00528359 return eax_6; 005282c8 } 005282c8 005282ca class CPhysicsObj* physics_obj = this->physics_obj; 005282da if ((physics_obj == 0 || (*(uint8_t*)((char*)((int16_t)physics_obj->state))[1] & 4) == 0)) 005282cf goto label_5282f6; // state bit 0x400 not set => skip ground check, go straight to constrained/queue gating 005282cf 005282dc uint8_t transient_state = ((int8_t)physics_obj->transient_state); 005282e8 if (((transient_state & 1) != 0 && (transient_state & 2) != 0)) 005282e8 goto label_5282f6; // on-ground => also goes to the shared gating path 005282b8 } 005282b8 005282f0 return 0x24; // not on ground and state bit 0x400 is set => "can't jump, not grounded" (0x24) 005282b0 } ``` Cleaned: for a creature-type weenie that is NOT on the ground (and physics `state` bit `0x400` — i.e. `HAS_PHYSICS_BSP`/gravity-active flag — is set), jumping is disallowed outright (`0x24`). Otherwise (non-creature weenie, OR grounded, OR gravity-inactive object) falls into the shared gate: `IsFullyConstrained` check (`0x47` if constrained), then peek the pending-motion-queue head's `jump_error_code` — if the head node already carries a nonzero jump-error, that's returned directly (an earlier motion already determined jump is blocked); otherwise recompute via `jump_charge_is_allowed` + `motion_allows_jump(forward_command)`, and finally consult the weenie's `JumpStaminaCost(extent, &outCost)` virtual (rejects with `0x47` if the weenie can't afford the stamina cost of this jump extent). --- ## 4. Ground transitions ### 4a. `CMotionInterp::HitGround` — `00528ac0` @ line 305996 ```c 00528ac0 void __fastcall CMotionInterp::HitGround(class CMotionInterp* this) 00528ac0 { 00528ac8 if (this->physics_obj != 0) 00528ac8 { 00528aca class CWeenieObject* weenie_obj = this->weenie_obj; 00528acf int32_t eax_2; 00528acf if (weenie_obj != 0) 00528ad3 eax_2 = weenie_obj->vtable->IsCreature(); 00528ad3 00528ad8 if ((weenie_obj == 0 || eax_2 != 0)) 00528ad8 { 00528ada class CPhysicsObj* physics_obj = this->physics_obj; 00528aea if ((physics_obj != 0 && (*(uint8_t*)((char*)((int16_t)physics_obj->state))[1] & 4) != 0)) // state bit 0x400 (gravity/BSP-active) 00528aea { 00528aec CPhysicsObj::RemoveLinkAnimations(physics_obj); 00528af7 CMotionInterp::apply_current_movement(this, 0, 0); 00528aea } 00528ad8 } 00528ac8 } 00528ac0 } ``` Cleaned: guarded to non-creature-or-creature weenies (basically always runs unless the weenie is a non-creature — mirrors the same `IsCreature` gate used throughout). If `physics_obj` has gravity/BSP active (`state & 0x400`), remove any link/stuck animations and re-apply current movement (`apply_current_movement(interruptFlag=0, otherFlag=0)`) — this re-syncs the motion interpreter's applied motion once landing occurs (e.g. transitioning out of a fall/jump animation into idle/walk/run). ### 4b. `CMotionInterp::LeaveGround` — `00529710`... **actual address is `00528b00`** @ line 306022 (Note: the task description's address `00529710` does not match this build; the correctly-grepped symbol resolves to `00528b00`.) ```c 00528b00 void __fastcall CMotionInterp::LeaveGround(class CMotionInterp* this) 00528b00 { 00528b0b if (this->physics_obj != 0) 00528b0b { 00528b0d class CWeenieObject* weenie_obj = this->weenie_obj; 00528b12 int32_t eax_2; 00528b12 if (weenie_obj != 0) 00528b16 eax_2 = weenie_obj->vtable->IsCreature(); 00528b16 00528b1b if ((weenie_obj == 0 || eax_2 != 0)) 00528b1b { 00528b1d class CPhysicsObj* physics_obj = this->physics_obj; 00528b2d if ((physics_obj != 0 && (*(uint8_t*)((char*)((int16_t)physics_obj->state))[1] & 4) != 0)) 00528b2d { 00528b36 void var_c; 00528b36 CMotionInterp::get_leave_ground_velocity(this, &var_c); 00528b45 CPhysicsObj::set_local_velocity(this->physics_obj, &var_c, 1); 00528b4a class CPhysicsObj* physics_obj_1 = this->physics_obj; 00528b4d this->standing_longjump = 0; 00528b54 this->jump_extent = 0f; 00528b5b CPhysicsObj::RemoveLinkAnimations(physics_obj_1); 00528b66 CMotionInterp::apply_current_movement(this, 0, 0); 00528b2d } 00528b1b } 00528b0b } 00528b00 } ``` Cleaned: same creature/gravity-active gating as `HitGround`. When leaving the ground: 1. Compute the launch velocity via `get_leave_ground_velocity` (§3d). 2. `CPhysicsObj::set_local_velocity(&velocity, autonomous=1)` — apply it as the physics object's local velocity (marked autonomous — i.e. this is a locally-simulated velocity change, not a server-dictated one). 3. Reset `standing_longjump = 0` and `jump_extent = 0.0` — the charge is consumed the instant you actually leave the ground. 4. `RemoveLinkAnimations` + `apply_current_movement(0, 0)` — same re-sync as HitGround. **Also called from `enter_default_state`** (§4d) — entering the default motion state always calls `LeaveGround` unconditionally as its last step (see below), which is how a freshly-created `CMotionInterp` initializes into an airborne-capable state. ### 4c. `CMotionInterp::ReportExhaustion` — `005288d0` @ line 305861 ```c 005288d0 void __fastcall CMotionInterp::ReportExhaustion(class CMotionInterp* this) 005288d0 { 005288dd if ((this->physics_obj != 0 && this->initted != 0)) 005288dd { 005288df class CWeenieObject* weenie_obj = this->weenie_obj; 005288e4 int32_t eax_2; 005288e4 if (weenie_obj != 0) 005288e8 eax_2 = weenie_obj->vtable->IsThePlayer(); 005288e8 005288ed if (((weenie_obj == 0 || eax_2 != 0) && CPhysicsObj::movement_is_autonomous(this->physics_obj) != 0)) 005288ed { 00528901 CMotionInterp::apply_raw_movement(this, 0, 0); 00528907 return; 005288ed } 005288ed 0052890e CMotionInterp::apply_interpreted_movement(this, 0, 0); 005288dd } 005288d0 } ``` Cleaned: requires `initted`. If (no weenie OR this IS the player weenie) AND the physics object's last move was autonomous (`movement_is_autonomous` — local prediction, not a server-driven DR update), re-apply via `apply_raw_movement(0,0)` (raw/local input path). Otherwise, re-apply via `apply_interpreted_movement(0,0)` (remote/server-driven path). This exact dual-dispatch pattern (`apply_raw_movement` vs `apply_interpreted_movement` gated on `IsThePlayer() && movement_is_autonomous()`) is **identical** in `apply_current_movement`, `SetWeenieObject`, `SetPhysicsObject` — see §4e-4g. Called from `CPhysicsObj::report_exhaustion` (`0050fdd0`, tailcall) which itself relays from `MovementManager::ReportExhaustion` (`00524360`, see §7). ### 4d. `CMotionInterp::enter_default_state` — `00528c80` @ line 306124 ```c 00528c80 void __fastcall CMotionInterp::enter_default_state(class CMotionInterp* this) 00528c80 { 00528c93 void var_38; 00528c93 RawMotionState::operator=(&this->raw_state, RawMotionState::RawMotionState(&var_38)); 00528c9c RawMotionState::~RawMotionState(&var_38); 00528cae InterpretedMotionState::operator=(&this->interpreted_state, InterpretedMotionState::InterpretedMotionState(&var_38)); 00528cb7 InterpretedMotionState::~InterpretedMotionState(&var_38); 00528cbf CPhysicsObj::InitializeMotionTables(this->physics_obj); 00528cc6 void* eax_2 = operator new(0x10); 00528cc6 if (eax_2 == 0) 00528ce5 eax_2 = nullptr; 00528cd2 else 00528cd2 { 00528cd4 *(uint32_t*)eax_2 = 0; 00528cd6 *(uint32_t*)((char*)eax_2 + 4) = 0; // MotionNode.context_id = 0 00528cd9 *(uint32_t*)((char*)eax_2 + 8) = 0x41000003; // MotionNode.motion = 0x41000003 (forward=none) 00528ce0 *(uint32_t*)((char*)eax_2 + 0xc) = 0; // MotionNode.jump_error_code = 0 00528cd2 } 00528cd2 00528ce7 void** tail_ = this->pending_motions.tail_; 00528cef if (tail_ == 0) 00528cf5 this->pending_motions.head_ = eax_2; 00528cef else 00528cf1 *(uint32_t*)tail_ = eax_2; 00528cf1 00528cfb this->pending_motions.tail_ = eax_2; 00528d03 this->initted = 1; 00528d09 CMotionInterp::LeaveGround(this); 00528c80 } ``` Cleaned: reset `raw_state` and `interpreted_state` to fresh default-constructed values (discarding any prior motion), re-initialize the physics object's motion tables, then **manually enqueue a `MotionNode{context_id=0, motion=0x41000003 /*forward-none*/, jump_error_code=0}`** directly onto `pending_motions` (bypassing `add_to_queue`'s inline construction but doing the exact same list-splice), mark `initted = 1`, and finally call `LeaveGround()` unconditionally. Since this is called both from `CMotionInterp::Create`'s callers (whenever `physics_obj != null`) AND is the shared "reset to idle" path, the manual sentinel enqueue + forced `LeaveGround` establishes: "a freshly-initted motion interpreter starts with one pending 'idle' motion queued and immediately treats itself as airborne-transitioning" (i.e. it re-syncs velocity/anim state exactly like any other ground-leave event, even though nothing physically left the ground — this primes the pending_motions queue and the walkable flag consistently). --- ## 5. `StopCompletely` / `StopMotion` / `HoldKey` boundary ### 5a. `CMotionInterp::StopCompletely` — `00527e40` @ line 305208 ```c 00527e40 uint32_t __fastcall CMotionInterp::StopCompletely(class CMotionInterp* this) 00527e40 { 00527e44 class CPhysicsObj* physics_obj = this->physics_obj; 00527e4b if (physics_obj == 0) 00527e54 return 8; 00527e54 00527e56 CPhysicsObj::interrupt_current_movement(physics_obj); 00527e61 uint32_t eax_2 = CMotionInterp::motion_allows_jump(this, this->interpreted_state.forward_command); 00527e70 this->raw_state.forward_command = 0x41000003; 00527e77 this->raw_state.forward_speed = 1f; 00527e7a this->raw_state.sidestep_command = 0; 00527e7d this->raw_state.turn_command = 0; 00527e80 this->interpreted_state.forward_command = 0x41000003; 00527e87 this->interpreted_state.forward_speed = 1f; 00527e8a this->interpreted_state.sidestep_command = 0; 00527e8d this->interpreted_state.turn_command = 0; 00527e90 CPhysicsObj::StopCompletely_Internal(this->physics_obj); 00527e9e CMotionInterp::add_to_queue(this, 0, 0x41000003, eax_2); 00527ea3 class CPhysicsObj* physics_obj_1 = this->physics_obj; 00527eb1 if ((physics_obj_1 != 0 && physics_obj_1->cell == 0)) 00527eb3 CPhysicsObj::RemoveLinkAnimations(physics_obj_1); 00527ebc return 0; 00527e40 } ``` Cleaned: `8` if no physics object. Otherwise: interrupt any current transition, snapshot whether a jump would still be allowed given the (about-to-be-overwritten) `forward_command`, forcibly zero out BOTH `raw_state` and `interpreted_state`'s forward/sidestep/turn commands to "none" (`forward_command = 0x41000003`, `forward_speed = 1.0`, `sidestep_command = turn_command = 0`), call `CPhysicsObj::StopCompletely_Internal` (the actual physics-level full stop — outside CMotionInterp), then unconditionally enqueue a `MotionNode{0, 0x41000003, eax_2}` onto `pending_motions` (this is the ONLY caller that passes the **precomputed** jump-error code (0 = allowed) as the queued node's `jump_error_code`, rather than recomputing after applying the new motion — note the semantic quirk: `eax_2` was computed from the OLD `forward_command` value, before the overwrite two lines later). If the physics object has no cell (detached / not in world), also strip link animations. Always returns `0` (success — `StopCompletely` cannot fail once a physics object exists). **Note the constant `0x41000003`** = the canonical "no forward motion" / neutral forward-command id used throughout this whole subsystem (`StopCompletely`, `enter_default_state`'s sentinel, `StopInterpretedMotion`'s post-stop requeue, `apply_interpreted_movement`'s fallback requeue). ### 5b. `CMotionInterp::StopMotion` — `00528530` @ line 305674 ```c 00528530 uint32_t __thiscall CMotionInterp::StopMotion(class CMotionInterp* this, uint32_t arg2, class MovementParameters const* arg3) 00528530 { 00528536 class CPhysicsObj* physics_obj = this->physics_obj; 0052853b if (physics_obj == 0) 00528546 return 8; 00528546 0052854c class MovementParameters* esi = arg3; 00528555 if (*(uint8_t*)((char*)((int16_t)esi->__inner0))[1] < 0) // sign-bit / interrupt flag 00528557 CPhysicsObj::interrupt_current_movement(physics_obj); 00528557 00528562 float min_distance = esi->min_distance; 00528569 union __inner0 = esi->__inner0; 00528570 float desired_heading = esi->desired_heading; 00528577 float distance_to_object = esi->distance_to_object; 0052857e float walk_run_threshhold = esi->walk_run_threshhold; 00528582 enum HoldKey hold_key_to_apply = esi->hold_key_to_apply; 00528585 float speed = esi->speed; 0052858d float min_distance_1 = min_distance; 00528594 enum HoldKey hold_key_to_apply_1 = hold_key_to_apply; 00528598 uint32_t context_id = esi->context_id; 005285a5 float fail_distance = esi->fail_distance; 005285a9 uint32_t action_stamp = esi->action_stamp; 005285af arg3 = arg2; // reuse arg3 slot to hold the motion id (register shuffle) 005285b3 int32_t var_2c = 0x7c83f8; // fresh local MovementParameters (vtable stamp) 005285bb uint32_t action_stamp_1 = action_stamp; 005285bf CMotionInterp::adjust_motion(this, &arg3, &speed, hold_key_to_apply); 005285d0 uint32_t result = CMotionInterp::StopInterpretedMotion(this, arg3, &var_2c); 005285e1 if ((result == 0 && (*(uint8_t*)((char*)((int16_t)esi->__inner0))[1] & 0x20) != 0)) // bit 0x2000 mirror 005285e7 RawMotionState::RemoveMotion(&this->raw_state, arg2); 005285f5 return result; 00528530 } ``` Cleaned: the `StopMotion` mirror of `DoMotion` — same defaulting/reconstruction pattern (snapshot all `arg3` fields onto the stack, build a fresh local `MovementParameters`), same optional `interrupt_current_movement`, same `adjust_motion` reinterpretation of the motion id + speed, but no combat-stance or action-depth gating (those are `DoMotion`-only). Delegates to `StopInterpretedMotion(arg3 /*adjusted motion id*/, &var_2c /*local params*/)` (already ported). On success, mirrors the removal into `raw_state` via `RawMotionState::RemoveMotion(arg2)` **using the ORIGINAL unmutated motion id**, not the adjusted one — matches `DoMotion`'s equivalent `RawMotionState::ApplyMotion(ebp, arg3)` also using the pre-adjustment id. ### 5c. `CMotionInterp::set_hold_run` — `00528b70` @ line 306053 ```c 00528b70 void __thiscall CMotionInterp::set_hold_run(class CMotionInterp* this, int32_t arg2, int32_t arg3) 00528b70 { 00528b79 int32_t eax; 00528b79 eax = arg2 == 0; 00528b85 int32_t edx; 00528b85 edx = this->raw_state.current_holdkey != HoldKey_Run; 00528b8a if (eax != edx) 00528b8a { 00528b94 int32_t eax_1; 00528b94 eax_1 = arg2 != 0; 00528b9b this->raw_state.current_holdkey = (eax_1 + 1); // 1 = HoldKey_None+1? see enum below 00528b9e CMotionInterp::apply_current_movement(this, arg3, 0); 00528b8a } 00528b70 } ``` Cleaned: `arg2` is a bool "hold run key down?" (nonzero = yes). `eax = (arg2 == 0)` (i.e. "run key is up"), `edx = (current_holdkey != Run)`. The XOR-style `if (eax != edx)` is "only act if this toggles a *change* in effective hold-run state" — i.e. skip if we're already in the requested state (guards against redundant re-application). If changed: `current_holdkey = (arg2 != 0) + 1`. Given `HoldKey_None = 1`? — cross-check the enum: `HoldKey_Invalid=0`? See note below; regardless, the arithmetic `(bool)+1` maps `false→1`, `true→2`, and `SetHoldKey` (§5d) explicitly writes `HoldKey_None` and `HoldKey_Run` as the two values it ever assigns — so `1 = HoldKey_None`, `2 = HoldKey_Run` is confirmed by direct comparison against `SetHoldKey`'s own literal assignments. Then calls `apply_current_movement(arg3, 0)` to push the change through immediately. **Callers** (outside CMotionInterp, both from the CommandInterpreter/input boundary — noted per the request, out of R3's direct scope but shown for the seam): - Line 405488 (`0058b303`): `CMotionInterp::set_hold_run(CPhysicsObj::get_minterp(*(this-0xc0)), eax_3)` — 2-arg call site (arg3 defaults via calling convention / omitted in decomp). - Line 699120 (`006b33ca`): `CMotionInterp::set_hold_run(CPhysicsObj::get_minterp(this->player), eax_4, 1)` — explicit `arg3=1` (interrupt flag) from the player input/command layer. ### 5d. `CMotionInterp::SetHoldKey` — `00528bb0` @ line 306072 ```c 00528bb0 void __thiscall CMotionInterp::SetHoldKey(class CMotionInterp* this, enum HoldKey arg2, int32_t arg3) 00528bb0 { 00528bb0 enum HoldKey current_holdkey = this->raw_state.current_holdkey; 00528bb9 if (arg2 != current_holdkey) 00528bb9 { 00528bbc if (arg2 == HoldKey_None) 00528bbc { 00528bdf if (current_holdkey == HoldKey_Run) 00528bdf { 00528be8 this->raw_state.current_holdkey = HoldKey_None; 00528bef CMotionInterp::apply_current_movement(this, arg3, 0); 00528bdf } 00528bbc } 00528bbc else if ((arg2 == 2 && current_holdkey != HoldKey_Run)) // arg2 == HoldKey_Run (== 2) 00528bc4 { 00528bcd this->raw_state.current_holdkey = HoldKey_Run; 00528bd4 CMotionInterp::apply_current_movement(this, arg3, 0); 00528bb9 } 00528bb9 } 00528bb0 } ``` Cleaned: no-op if `arg2` already equals the current hold key. Setting to `HoldKey_None` only takes effect (and re-applies movement) if we were previously `HoldKey_Run` — setting `None` while already something else (e.g. `Invalid`) is silently ignored. Setting to `HoldKey_Run` (`== 2`) takes effect whenever we weren't already `Run`. Both effective branches call `apply_current_movement(arg3 /*interrupt flag passthrough*/, 0)`. This is the function `DoMotion` calls (§2, step 4) when bit `0x800` of the incoming `MovementParameters` bitfield requests a hold-key change, and it's what `CMotionInterp::adjust_motion` (D6, already ported) reads back via `this->raw_state.current_holdkey` to decide whether `apply_run_to_command` fires. --- ## 6. MovementManager (beyond `unpack_movement`, S2a-covered) ### 6a. `MovementManager::Create` — `00524050` @ line 300150 ```c 00524050 class MovementManager* MovementManager::Create(class CPhysicsObj* arg1, class CWeenieObject* arg2) 00524050 { 00524054 void* result_1 = operator new(0x10); 0052405e void* result; 0052405e if (result_1 == 0) 0052407f result = nullptr; 0052405e else 0052405e { 00524060 *(uint32_t*)result_1 = 0; // motion_interpreter = null 00524066 *(uint32_t*)((char*)result_1 + 4) = 0; // moveto_manager = null 0052406d *(uint32_t*)((char*)result_1 + 8) = 0; // physics_obj = null (overwritten below) 00524074 *(uint32_t*)((char*)result_1 + 0xc) = 0; // weenie_obj = null (overwritten below) 0052407b result = result_1; 0052405e } 0052405e 00524081 class CMotionInterp* ecx = *(uint32_t*)result; 00524089 *(uint32_t*)((char*)result + 8) = arg1; // physics_obj = arg1 0052408c if (ecx != 0) 0052408f CMotionInterp::SetPhysicsObject(ecx, arg1); 0052408f 00524094 class MoveToManager* ecx_1 = *(uint32_t*)((char*)result + 4); 00524099 if (ecx_1 != 0) 0052409c MoveToManager::SetPhysicsObject(ecx_1, arg1); 0052409c 005240a1 class CMotionInterp* ecx_2 = *(uint32_t*)result; 005240a9 *(uint32_t*)((char*)result + 0xc) = arg2; // weenie_obj = arg2 005240ac if (ecx_2 != 0) 005240af CMotionInterp::SetWeenieObject(ecx_2, arg2); 005240af 005240b4 class MoveToManager* ecx_3 = *(uint32_t*)((char*)result + 4); 005240b9 if (ecx_3 != 0) 005240bc MoveToManager::SetWeenieObject(ecx_3, arg2); 005240bc 005240c5 return result; 00524050 } ``` Cleaned: allocate+zero a `MovementManager`, set `physics_obj = arg1` and (if a `motion_interpreter` already existed — it never does on a fresh alloc, this branch is dead on first construction but matches the setter-forwarding pattern used everywhere else) forward to `CMotionInterp::SetPhysicsObject`/`MoveToManager::SetPhysicsObject`. Same for `weenie_obj = arg2`. **`motion_interpreter` and `moveto_manager` are NOT eagerly created here** — they're lazily created on first access (see `get_minterp`, `PerformMovement`, `move_to_interpreted_state`, `EnterDefaultState`, `InqRawMotionState`, `InqInterpretedMotionState` — every one of them has the identical `if (motion_interpreter == 0) { motion_interpreter = CMotionInterp::Create(...); if (physics_obj != 0) enter_default_state(...); }` guard). ### 6b. `MovementManager::SetPhysicsObject` / `SetWeenieObject` Not directly decompiled as named top-level functions in this range (they're the `CMotionInterp::SetPhysicsObject`/`SetWeenieObject` calls forwarded from `Create` above, §3/§4e-4g show `CMotionInterp`'s own versions). `MovementManager` itself has no separate `SetPhysicsObject`/`SetWeenieObject` — `Create` is the only place these fields are set, matching the struct having no setters of its own (raw field assignment inline in `Create`). ### 6c. `MovementManager::PerformMovement` — `005240d0` @ line 300194 (the dispatch) ```c 005240d0 uint32_t __thiscall MovementManager::PerformMovement(class MovementManager* this, class MovementStruct const* arg2) 005240d0 { 005240d9 CPhysicsObj::set_active(this->physics_obj, 1); 005240e4 void* eax_1 = (arg2->type - 1); 005240e8 if (eax_1 > 8) 00524159 return 0x47; 00524159 005240f1 switch (eax_1) 005240f1 { 005240fb case nullptr: // type == RawCommand(1) -> eax_1 == 0 005240fb case 1: // type == InterpretedCommand(2) -> eax_1 == 1 005240fb case 2: // type == StopRawCommand(3) -> eax_1 == 2 005240fb case 3: // type == StopInterpretedCommand(4) -> eax_1 == 3 005240fb case 4: // type == StopCompletely(5) -> eax_1 == 4 005240fb { 005240fb if (this->motion_interpreter == 0) 005240fb { 00524105 class CMotionInterp* eax_3 = CMotionInterp::Create(this->physics_obj, this->weenie_obj); 00524110 bool cond:0_1 = this->physics_obj == 0; 00524112 this->motion_interpreter = eax_3; 00524114 if (!(cond:0_1)) 00524118 CMotionInterp::enter_default_state(eax_3); 005240fb } 005240fb 00524127 return CMotionInterp::PerformMovement(this->motion_interpreter, arg2); 005240fb break; 005240fb } 0052412f case 5: // type == MoveToObject(6) -> eax_1 == 5 0052412f case 6: // type == MoveToPosition(7) -> eax_1 == 6 0052412f case 7: // type == TurnToObject(8) -> eax_1 == 7 0052412f case 8: // type == TurnToHeading(9) -> eax_1 == 8 0052412f { 0052412f if (this->moveto_manager == 0) 00524141 this->moveto_manager = MoveToManager::Create(this->physics_obj, this->weenie_obj); 00524141 00524148 MoveToManager::PerformMovement(this->moveto_manager, arg2); 0052414f return 0; 0052412f break; 0052412f } 005240f1 } 005240d0 } ``` Cleaned: this is the **top-level `MovementManager` dispatch** — always marks the physics object active first. `MovementTypes::Type` values `1..5` (Raw/Interpreted/ StopRaw/StopInterpreted/StopCompletely) all route to **`CMotionInterp::PerformMovement`** (lazily creating + default-initing the motion interpreter first if needed). Values `6..9` (MoveToObject/MoveToPosition/TurnToObject/TurnToHeading) route to **`MoveToManager::PerformMovement`** (lazily creating the MoveTo manager; R4 territory, not expanded here — `MoveToManager::Create`/`PerformMovement` entry points only, per the task's explicit deferral). Any `type` outside `[1,9]` → error `0x47`. ### 6d. `MovementManager::MotionDone` (the relay) — `005242d0` @ line 300396 ```c 005242d0 void __thiscall MovementManager::MotionDone(class MovementManager* this, uint32_t arg2, int32_t arg3) 005242d0 { 005242d0 class CMotionInterp* motion_interpreter = this->motion_interpreter; 005242d4 if (motion_interpreter != 0) 005242d4 { 005242da int32_t var_4_1 = arg3; 005242db int32_t edx; 005242db CMotionInterp::MotionDone(motion_interpreter, edx); // NOTE: passes uninitialized `edx`, NOT arg2/arg3 — see caveat below 005242d4 } 005242d0 } ``` Cleaned: pure relay, guarded on `motion_interpreter != null` (no lazy-create here — if the interpreter doesn't exist yet, the done-callback is simply dropped, which is correct: no interpreter means no motion was ever dispatched through it to complete). **Decompiler caveat**: the pseudo-C shows `var_4_1 = arg3` computed but discarded, and `edx` (uninitialized) passed as `CMotionInterp::MotionDone`'s second argument instead of `arg2`. Given `CMotionInterp::MotionDone`'s body never reads its `arg2` parameter at all (§1c), this is very likely a register-allocation artifact of the decompiler (the real assembly probably loads `arg2` into `edx` via the calling convention and the pseudo-C's "uninitialized edx" annotation is simply failing to trace that it came from the argument) rather than a genuine "pass garbage" bug — **do not port this as "pass garbage"; port as `CMotionInterp::MotionDone(motionId)` taking whatever came in as `arg2`,** consistent with `arg2` at the `CPhysicsObj::MotionDone` → `MovementManager::MotionDone` call site always being the real `MotionNode.motion` id (see §6e below and the two call sites at lines 290573/290657 in `MotionTableManager`). ### 6e. `MovementManager::UseTime`, `HitGround`, `LeaveGround`, `HandleEnterWorld`, `HandleExitWorld`, `ReportExhaustion` ```c 005242f0 void __fastcall MovementManager::UseTime(class MovementManager* this) 005242f0 { 005242f0 class MoveToManager* moveto_manager = this->moveto_manager; 005242f5 if (moveto_manager == 0) 005242fc return; 005242f7 /* tailcall */ 005242f7 return MoveToManager::UseTime(moveto_manager); 005242f0 } ``` Pure relay to `MoveToManager::UseTime` only — **CMotionInterp has no `UseTime` of its own** (motion interpreter's per-frame work happens through the physics/anim pipeline, not a polled `UseTime`). ```c 00524300 void __fastcall MovementManager::HitGround(class MovementManager* this) 00524300 { 00524303 class CMotionInterp* motion_interpreter = this->motion_interpreter; 00524307 if (motion_interpreter != 0) 00524309 CMotionInterp::HitGround(motion_interpreter); 00524309 0052430e class MoveToManager* moveto_manager = this->moveto_manager; 00524314 if (moveto_manager == 0) 0052431b return; 0052431b /* tailcall */ 0052431b return MoveToManager::HitGround(moveto_manager); 00524300 } ``` Fans out to BOTH `CMotionInterp::HitGround` (§4a) AND `MoveToManager::HitGround` (R4, not expanded) — both subsystems need to know when the ground was hit. ```c 00524320 void __fastcall MovementManager::LeaveGround(class MovementManager* this) 00524320 { 00524323 class CMotionInterp* motion_interpreter = this->motion_interpreter; 00524327 if (motion_interpreter != 0) 00524329 CMotionInterp::LeaveGround(motion_interpreter); 00524329 0052432e class MoveToManager* moveto_manager = this->moveto_manager; 00524334 if (moveto_manager == 0) 0052433b return; 0052433b /* tailcall */ 0052433b return IDClass<_tagDataID,32,0>::~IDClass<_tagDataID,32,0>(moveto_manager); 00524320 } ``` Same fan-out pattern as `HitGround` (the trailing tailcall target name `IDClass<...>::~IDClass<...>` is a decompiler mis-symbolication — the actual target is almost certainly `MoveToManager::LeaveGround`, matching the `HitGround` sibling's shape exactly; flagging this as a **known decomp mislabel**, not a real destructor call). ```c 00524340 void __fastcall MovementManager::HandleEnterWorld(class MovementManager* this) 00524340 { 00524340 class CMotionInterp* motion_interpreter = this->motion_interpreter; 00524344 if (motion_interpreter == 0) 0052434b return; 0052434b /* tailcall */ 0052434b return IDClass<_tagDataID,32,0>::~IDClass<_tagDataID,32,0>(motion_interpreter); 00524340 } ``` Same mis-symbolication pattern — tailcalls to what is almost certainly `CMotionInterp::HandleEnterWorld` (there is no such symbol independently decompiled in this dump; `CPartArray::HandleEnterWorld`/`MotionTableManager::HandleEnterWorld` exist at the animation layer — §7 — but `CMotionInterp` itself does not appear to define a distinct `HandleEnterWorld`, only `HandleExitWorld` was found named). **Only relays to `motion_interpreter`, no `moveto_manager` fan-out** (contrast with `HitGround`/ `LeaveGround` above) — enter-world doesn't need to touch pending MoveTo state. ```c 00524350 void __fastcall MovementManager::HandleExitWorld(class MovementManager* this) 00524350 { 00524350 class CMotionInterp* motion_interpreter = this->motion_interpreter; 00524354 if (motion_interpreter == 0) 0052435b return; 0052435b /* tailcall */ 0052435b return CMotionInterp::HandleExitWorld(motion_interpreter); 00524350 } ``` Clean relay to `CMotionInterp::HandleExitWorld` (§1d) — correctly symbolicated this time. Also no `moveto_manager` fan-out. ```c 00524360 void __fastcall MovementManager::ReportExhaustion(class MovementManager* this) 00524360 { 00524363 class CMotionInterp* motion_interpreter = this->motion_interpreter; 00524367 if (motion_interpreter != 0) 00524369 CMotionInterp::ReportExhaustion(motion_interpreter); 00524369 0052436e class MoveToManager* moveto_manager = this->moveto_manager; 00524374 if (moveto_manager == 0) 0052437b return; 0052437b /* tailcall */ 0052437b return IDClass<_tagDataID,32,0>::~IDClass<_tagDataID,32,0>(moveto_manager); 00524360 } ``` Fan-out to `CMotionInterp::ReportExhaustion` (§4c) AND (mis-symbolicated, real target almost certainly `MoveToManager::ReportExhaustion`) the MoveTo manager. ### 6f. `MovementManager::CancelMoveTo` — `005241b0` @ line 300277 ```c 005241b0 void __fastcall MovementManager::CancelMoveTo(class MovementManager* this, uint32_t arg2) 005241b0 { 005241b0 class MoveToManager* moveto_manager = this->moveto_manager; 005241b5 if (moveto_manager == 0) 005241bc return; 005241bc /* tailcall */ 005241b7 return MoveToManager::CancelMoveTo(moveto_manager, arg2); 005241b0 } ``` Pure relay, no lazy-create (canceling a MoveTo that was never started is a no-op). ### 6g. `MovementManager::EnterDefaultState` — `005241c0` @ line 300292 ```c 005241c0 void __fastcall MovementManager::EnterDefaultState(class MovementManager* this) 005241c0 { 005241c3 class CPhysicsObj* physics_obj = this->physics_obj; 005241c8 if (physics_obj == 0) 005241f5 return; 005241f5 005241cd if (this->motion_interpreter == 0) 005241cd { 005241d4 class CMotionInterp* eax = CMotionInterp::Create(physics_obj, this->weenie_obj); 005241df bool cond:0_1 = this->physics_obj == 0; 005241e1 this->motion_interpreter = eax; 005241e1 005241e3 if (!(cond:0_1)) 005241e7 CMotionInterp::enter_default_state(eax); 005241cd } 005241cd 005241ef /* tailcall */ 005241ef return CMotionInterp::enter_default_state(this->motion_interpreter); 005241c0 } ``` No-op if no physics object. Lazy-creates the motion interpreter (with the same "create-then-immediately-enter-default-state-if-physics-exists" double-init pattern — note this means a brand-new interpreter gets `enter_default_state` called on it TWICE in a row when reached via this path: once inside the lazy-create guard, once again as the trailing unconditional tailcall — harmless since `enter_default_state` is fully idempotent/reset-style, but worth flagging as a genuine retail double-call, not a porting bug to "fix"). ### 6h. `MovementManager::InqRawMotionState` / `InqInterpretedMotionState` / `IsMovingTo` / `motions_pending` / `get_minterp` ```c 00524200 class RawMotionState const* __fastcall MovementManager::InqRawMotionState(class MovementManager* this) 00524200 { 00524206 if (this->motion_interpreter == 0) 00524206 { 00524210 class CMotionInterp* eax_2 = CMotionInterp::Create(this->physics_obj, this->weenie_obj); 0052421b bool cond:0_1 = this->physics_obj == 0; 0052421d this->motion_interpreter = eax_2; 0052421f if (!(cond:0_1)) 00524223 CMotionInterp::enter_default_state(eax_2); 00524206 } 00524206 return &this->motion_interpreter->raw_state; 00524200 } ``` ```c 00524230 class InterpretedMotionState const* __fastcall MovementManager::InqInterpretedMotionState(class MovementManager* this) 00524230 { /* identical lazy-create pattern */ return &this->motion_interpreter->interpreted_state; } ``` ```c 00524260 int32_t __fastcall MovementManager::IsMovingTo(class MovementManager const* this) 00524260 { 00524260 class MoveToManager* moveto_manager = this->moveto_manager; 00524265 if ((moveto_manager != 0 && MoveToManager::is_moving_to(moveto_manager) != 0)) 00524275 return 1; 00524275 return 0; 00524260 } ``` ```c 005242a0 class CMotionInterp* __fastcall MovementManager::get_minterp(class MovementManager* this) 005242a0 { /* identical lazy-create pattern */ return this->motion_interpreter; } ``` All four follow the exact same "lazy-create-with-default-state-if-physics-exists, then return a pointer/reference into the freshly-guaranteed-nonnull interpreter" shape as `get_minterp`/`EnterDefaultState`/`PerformMovement`. ### 6i. `MovementManager::Destroy` — `005243f0` @ line 300538 ```c 005243f0 void __fastcall MovementManager::Destroy(class MovementManager* this) 005243f0 { 005243f4 class CMotionInterp* motion_interpreter = this->motion_interpreter; 005243f8 if (motion_interpreter != 0) 005243f8 { 005243fc CMotionInterp::~CMotionInterp(motion_interpreter); 00524402 operator delete(motion_interpreter); 005243f8 } 005243f8 0052440a class MoveToManager* moveto_manager = this->moveto_manager; 0052440f this->motion_interpreter = 0; 00524415 if (moveto_manager != 0) 00524415 { 00524419 MoveToManager::~MoveToManager(moveto_manager); 0052441f operator delete(moveto_manager); 00524415 } 00524415 00524428 this->moveto_manager = nullptr; 005243f0 } ``` Explicit teardown of both sub-managers (dtor + `operator delete` each, since these are raw owned pointers, not smart pointers, per retail C++ idiom). ### 6j. `HandleUpdateTarget` — **not found** in this build's decomp under `MovementManager::` or `CMotionInterp::` namespaces (grepped both; no match). Likely does not exist as a named symbol in the Sept 2013 PDB, or the functionality lives elsewhere (possibly `MoveToManager`-internal, R4 territory) — noting the negative result rather than guessing. ### 6k. `MakeMoveToManager` — entry points only (deferred to R4 per instructions) `MoveToManager::Create(class CPhysicsObj*, class CWeenieObject*)` is referenced at line 300230 (`0052412f` case block) and `MovementManager::Create` does NOT eagerly construct it. Full `MoveToManager` internals (its own `PerformMovement`, `UseTime`, `HitGround`, `is_moving_to`, `CancelMoveTo`, `ReportExhaustion`, `LeaveGround`) are **out of scope for R3** — only the entry-point call shape from `MovementManager` is captured above (§6c, §6f, §6e, §6g). --- ## 7. `CPhysicsObj::MotionDone` / `IsAnimating` / `IsMovingOrAnimating` / `InqRawMotionState` / `movement_is_autonomous` / animation-completion driver ### 7a. `CPhysicsObj::movement_is_autonomous` — `0050eb30` @ line 276443 ```c 0050eb30 int32_t __fastcall CPhysicsObj::movement_is_autonomous(class CPhysicsObj const* this) 0050eb30 { 0050eb36 return this->last_move_was_autonomous; 0050eb30 } ``` Trivial field accessor — `last_move_was_autonomous` is a plain flag on `CPhysicsObj`, set elsewhere (transition/physics-tick code, not in this range) whenever a locally- simulated (as opposed to server-dictated dead-reckoning) motion update occurs. This is the flag every `apply_raw_movement` vs `apply_interpreted_movement` gate reads (§4c, and `apply_current_movement`/`SetWeenieObject`/`SetPhysicsObject` below). ### 7b. `CPhysicsObj::MotionDone` — `0050fdb0` @ line 277856 (FULL BODY — pure tailcall relay) ```c 0050fdb0 void __thiscall CPhysicsObj::MotionDone(class CPhysicsObj* this, uint32_t arg2, int32_t arg3) 0050fdb0 { 0050fdb0 class MovementManager* movement_manager = this->movement_manager; 0050fdb8 if (movement_manager == 0) 0050fdbf return; 0050fdbf 0050fdba /* tailcall */ 0050fdba return MovementManager::MotionDone(movement_manager, arg2, arg3); 0050fdb0 } ``` No-op if the physics object has no `movement_manager` attached (e.g. static/inanimate objects never get a `MovementManager`). Otherwise a clean 2-arg tailcall relay to `MovementManager::MotionDone` (§6d) — `arg2` is the `MotionNode.motion` id that just completed, `arg3` is whatever caller-specific flag the two call sites below pass. **Call sites** — this is the actual **animation-completion driver**, both inside `MotionTableManager` (the per-`CPartArray` animation-node scheduler): ```c /* MotionTableManager::AnimationDone, 0051bce0 @ line 290558 — fired once per game-clock tick as the animation_counter advances past queued nodes */ 0051bd20 CPhysicsObj::MotionDone(this->physics_obj, *(int32_t*)((char*)head__2 + 8) /* pending_animations node's motion id */, arg2 /* AnimationDone's own arg2, e.g. "was interrupted" */); /* MotionTableManager::CheckForCompletedMotions, 0051be00 @ line 290645 — fired once per completed-immediately (0-duration) animation node */ 0051be2e CPhysicsObj::MotionDone(this->physics_obj, *(int32_t*)((char*)head__1 + 8), 1 /* literal 1 — always "not interrupted" for immediate completions */); ``` Both `MotionTableManager::AnimationDone` and `MotionTableManager::CheckForCompletedMotions` walk `this->pending_animations` (a **separate** `DLListData`-based doubly-linked queue scoped to `MotionTableManager`, distinct from `CMotionInterp::pending_motions` — they are two parallel queues that both track in-flight motions, one at the animation-table/CPartArray layer keyed by duration-in-ticks, one at the CMotionInterp/movement layer keyed by "waiting for a done callback"), and for every node whose duration has elapsed (`AnimationDone`: `node.duration <= animation_counter` after incrementing; `CheckForCompletedMotions`: `node.duration == 0`, i.e. instantly complete), they: 1. If the node's `motion & 0x10000000` (action-class flag — same bit `CMotionInterp:: MotionDone` tests), call `MotionState::remove_action_head(&this->state)` — pop the **animation-table's own parallel action-tracking state** (a THIRD action-queue, local to `MotionTableManager::state`, distinct from `InterpretedMotionState::actions` and `RawMotionState`'s equivalent — all three are kept in lockstep by the `0x10000000` bit). 2. Fire `CPhysicsObj::MotionDone(physics_obj, node.motion, ...)` — which is the whole chain that eventually reaches `CMotionInterp::MotionDone` (§1c) to pop `pending_motions`. 3. Unlink+delete the `pending_animations` node itself (standard doubly-linked-list removal, handling head/tail edge cases). **This is the actual driver behind "when does a queued motion get dequeued"** — it is **NOT** a per-frame poll of `CMotionInterp` itself; it's the animation-tick system (`MotionTableManager`, driven by `CPartArray::Update`→`CSequence::update`, and by `CPartArray::CheckForCompletedMotions`/`AnimationDone` tailcalls from `CPhysicsObj`, §7d/7e) that fires the completion callback back UP into `CMotionInterp`. ### 7c. `CPhysicsObj::InqRawMotionState` — `0050fde0` @ line 277883 ```c 0050fde0 class RawMotionState* __fastcall CPhysicsObj::InqRawMotionState(class CPhysicsObj const* this) 0050fde0 { 0050fde0 class MovementManager* movement_manager = this->movement_manager; 0050fde8 if (movement_manager != 0) 0050fded /* tailcall */ 0050fded return MovementManager::InqRawMotionState(movement_manager); 0050fded 0050fdec return 0; 0050fde0 } ``` Null-safe relay (returns `null` if no `movement_manager`, vs. `MovementManager:: InqRawMotionState`'s own lazy-create-guaranteed-nonnull behavior once a manager exists). ### 7d. `CPhysicsObj::CheckForCompletedMotions` — `0050fe30` @ line 277925 (pure relay, tailcall) ```c 0050fe30 void __fastcall CPhysicsObj::CheckForCompletedMotions(class CPhysicsObj* this) 0050fe30 { 0050fe30 class CPartArray* part_array = this->part_array; 0050fe35 if (part_array == 0) 0050fe3c return; 0050fe3c /* tailcall */ 0050fe37 return CPartArray::CheckForCompletedMotions(part_array); 0050fe30 } ``` This is what `CMotionInterp::PerformMovement` (§ from S2a/2) calls after EVERY dispatched motion op (`DoMotion`/`DoInterpretedMotion`/`StopMotion`/ `StopInterpretedMotion`/`StopCompletely`) — see line 306234/306241/306248/306255/ 306262 in §2's sibling `CMotionInterp::PerformMovement`. It synchronously drains any **already-elapsed** (0-duration) animation-table nodes right after the motion is applied, in case the newly-applied motion table entry immediately completes (e.g. a 0-frame transition). `CPartArray::CheckForCompletedMotions` (`00517d50`) is itself a null-safe tailcall relay to `MotionTableManager::CheckForCompletedMotions` (§7b, second call site). ### 7e. `CPhysicsObj::Hook_AnimDone` — `0050fda0` @ line 277845 (the animation-hook entry point, distinct from `MotionDone`) ```c 0050fda0 void __fastcall CPhysicsObj::Hook_AnimDone(class CPhysicsObj* this) 0050fda0 { 0050fda0 class CPartArray* part_array = this->part_array; 0050fda5 if (part_array != 0) 0050fda9 CPartArray::AnimationDone(part_array, 1); 0050fda0 } ``` This is the actual **per-frame/per-tick animation hook callback entry point** (called from the CSequence/hook-dispatch machinery covered by the separate animation-sequencer deep-dive) — it always passes literal `1` down into `CPartArray::AnimationDone` → `MotionTableManager::AnimationDone(1)` (§1c/§7b's first call site — the `arg2` there IS this `1`, meaning "not interrupted", contrasted with whatever value `CPartArray::AnimationDone` gets called with from other paths). `CPartArray::AnimationDone` (`00517d30`) and `CPartArray::CheckForCompletedMotions` (`00517d50`) are both null-safe single-line tailcall relays to the identically-named `MotionTableManager::` methods (§1c/§7b bodies already shown in full). ### 7f. `IsAnimating` / `IsMovingOrAnimating` — **not found** as literal symbol names `CPhysicsObj::IsAnimating` and `CPhysicsObj::IsMovingOrAnimating` were grepped across the full decomp file and **do not appear** as named functions in this PDB (neither under `CPhysicsObj::` nor any other class). The closest matching concepts actually present in this build: - `CPartArray::HasAnims` (`00517d40`) — tailcalls `CSequence::has_anims(&sequence)`. - `CMotionInterp::is_standing_still` (`00527fa0`, full body already shown inline below since it's small and directly relevant) — tests `on_ground` bits AND `interpreted_state.forward_command == 0x41000003 && sidestep_command == 0 && turn_command == 0`. - `CMotionInterp::motions_pending` (§1b). ```c 00527fa0 int32_t __fastcall CMotionInterp::is_standing_still(class CMotionInterp const* this) 00527fa0 { 00527fa3 uint8_t transient_state = ((int8_t)this->physics_obj->transient_state); 00527fc6 if (((transient_state & 1) != 0 && ((transient_state & 2) != 0 && (this->interpreted_state.forward_command == 0x41000003 && (this->interpreted_state.sidestep_command == 0 && this->interpreted_state.turn_command == 0))))) 00527fcd return 1; 00527fcd return 0; 00527fa0 } ``` If acdream needs an `IsAnimating`/`IsMovingOrAnimating`-equivalent gate, the retail composite would be built from `CPartArray::HasAnims()` (raw sequence has active animation hooks) OR'd with `!CMotionInterp::is_standing_still()` OR'd with `MovementManager::motions_pending()` — but this is a **synthesis**, not a verbatim retail function, since no single named retail function computes exactly this union. Flag before porting: confirm the actual acdream call site's intent against these three primitives individually rather than inventing a combined helper that doesn't exist in retail. --- ## 8. `set_hold_run` at the CommandInterpreter boundary — call-site context only Both call sites (already shown in §5c) originate outside `CMotionInterp`/ `MovementManager` — one at `0058b303` (function context not extracted; a `CPhysicsObj::get_minterp`-based call with only 2 explicit args in the decomp, arg3 presumably defaulted or passed via a register the decompiler didn't attribute), one at `006b33ca` inside what's very likely `CommandInterpreter`-adjacent code (`this->player` field access, `arg3=1` for the interrupt flag) at address range `006b33xx` — this is **outside R3's CMotionInterp/MovementManager scope** per the task's own boundary ("input handling" / "CommandInterpreter boundary" is a note-only ask); recorded here only as confirmation that `set_hold_run`'s two callers exist and roughly where. --- ## 9. Summary table — function → address → line → status | Function | Address | Line | Extracted | |---|---|---|---| | `CMotionInterp::add_to_queue` | 0x00527b80 | 305032 | full body | | `CMotionInterp::motions_pending` | 0x00527fe0 | 305322 | full body | | `CMotionInterp::MotionDone` | 0x00527ec0 | 305238 | full body | | `CMotionInterp::HandleExitWorld` | 0x00527f30 | 305275 | full body | | `MovementManager::motions_pending` | 0x00524280 | 300365 | full body | | `MovementManager::MotionDone` | 0x005242d0 | 300396 | full body + caveat | | `CMotionInterp::DoMotion` | 0x00528d20 | 306159 | full body | | `CMotionInterp::PerformMovement` | 0x00528e80 | 306221 | full body | | `CMotionInterp::motion_allows_jump` | 0x005279e0 | 304908 | full body | | `CMotionInterp::jump_charge_is_allowed` | 0x00527a50 | 304935 | full body | | `CMotionInterp::get_jump_v_z` | 0x00527aa0 | 304953 | full body | | `CMotionInterp::get_leave_ground_velocity` | 0x005280c0 | 305404 | full body | | `CMotionInterp::charge_jump` | 0x005281c0 | 305448 | full body | | `CMotionInterp::jump` | 0x00528780 | 305792 | full body | | `CMotionInterp::jump_is_allowed` | 0x005282b0 | 305509 | full body (context) | | `CPhysicsObj::on_ground` | 0x00527b20 | 304996 | full body | | `CMotionInterp::HitGround` | 0x00528ac0 | 305996 | full body | | `CMotionInterp::LeaveGround` | 0x00528b00 | 306022 | full body (addr differs from task's 0x00529710 guess) | | `CMotionInterp::ReportExhaustion` | 0x005288d0 | 305861 | full body | | `CMotionInterp::enter_default_state` | 0x00528c80 | 306124 | full body | | `CMotionInterp::StopCompletely` | 0x00527e40 | 305208 | full body | | `CMotionInterp::StopMotion` | 0x00528530 | 305674 | full body | | `CMotionInterp::set_hold_run` | 0x00528b70 | 306053 | full body | | `CMotionInterp::SetHoldKey` | 0x00528bb0 | 306072 | full body | | `CMotionInterp::is_standing_still` | 0x00527fa0 | 305309 | full body | | `CMotionInterp::apply_current_movement` | 0x00528870 | 305838 | full body | | `CMotionInterp::SetWeenieObject` | 0x00528920 | 305884 | full body | | `CMotionInterp::SetPhysicsObject` | 0x00528970 | 305911 | full body | | `CMotionInterp::Create` | 0x00528c00 | 306097 | full body | | `CMotionInterp::Destroy` | 0x00527b40 | 305009 | full body | | `CMotionInterp::~CMotionInterp` | 0x00527ff0 | 305332 | full body | | `MovementManager::Create` | 0x00524050 | 300150 | full body | | `MovementManager::PerformMovement` | 0x005240d0 | 300194 | full body | | `MovementManager::move_to_interpreted_state` | 0x00524170 | 300259 | full body | | `MovementManager::CancelMoveTo` | 0x005241b0 | 300277 | full body | | `MovementManager::EnterDefaultState` | 0x005241c0 | 300292 | full body | | `MovementManager::InqRawMotionState` | 0x00524200 | 300316 | full body | | `MovementManager::InqInterpretedMotionState` | 0x00524230 | 300334 | full body | | `MovementManager::IsMovingTo` | 0x00524260 | 300352 | full body | | `MovementManager::get_minterp` | 0x005242a0 | 300378 | full body | | `MovementManager::UseTime` | 0x005242f0 | 300411 | full body | | `MovementManager::HitGround` | 0x00524300 | 300425 | full body | | `MovementManager::LeaveGround` | 0x00524320 | 300444 | full body (tailcall target mis-symbolicated) | | `MovementManager::HandleEnterWorld` | 0x00524340 | 300463 | full body (tailcall target mis-symbolicated) | | `MovementManager::HandleExitWorld` | 0x00524350 | 300477 | full body | | `MovementManager::ReportExhaustion` | 0x00524360 | 300491 | full body (tailcall target mis-symbolicated) | | `MovementManager::Destroy` | 0x005243f0 | 300538 | full body | | `MovementParameters::MovementParameters` (ctor) | 0x00524380 | 300510 | full body | | `CPhysicsObj::movement_is_autonomous` | 0x0050eb30 | 276443 | full body | | `CPhysicsObj::MotionDone` | 0x0050fdb0 | 277856 | full body | | `CPhysicsObj::report_exhaustion` | 0x0050fdd0 | 277870 | full body | | `CPhysicsObj::InqRawMotionState` | 0x0050fde0 | 277883 | full body | | `CPhysicsObj::InqInterpretedMotionState` | 0x0050fe00 | 277897 | full body | | `CPhysicsObj::RemoveLinkAnimations` | 0x0050fe20 | 277911 | full body | | `CPhysicsObj::CheckForCompletedMotions` | 0x0050fe30 | 277925 | full body | | `CPhysicsObj::Hook_AnimDone` | 0x0050fda0 | 277845 | full body | | `MotionTableManager::AnimationDone` | 0x0051bce0 | 290558 | full body | | `MotionTableManager::CheckForCompletedMotions` | 0x0051be00 | 290645 | full body (partial read, list-unlink tail identical pattern) | | `MotionTableManager::HandleExitWorld` | 0x0051bda0 | 290625 | full body | | `MotionTableManager::HandleEnterWorld` | 0x0051bdd0 | 290634 | full body | | `CPartArray::AnimationDone` | 0x00517d30 | 285806 | full body | | `CPartArray::CheckForCompletedMotions` | 0x00517d50 | 285829 | full body | | `CPartArray::HandleMovement` | 0x00517d60 | 285843 | full body | | `CPartArray::HandleEnterWorld` | 0x00517d70 | 285857 | full body | | `CPartArray::HandleExitWorld` | 0x00517d90 | 285868 | full body | | `CPartArray::HasAnims` | 0x00517d40 | 285820 | full body | | `CBaseFilter::GetPinVersion` | 0x00527b10 | 304988 | full body (context for `unpack_movement`'s pin-version check) | | `CPhysicsObj::IsAnimating` | — | — | **not present in this PDB** (see §7f) | | `CPhysicsObj::IsMovingOrAnimating` | — | — | **not present in this PDB** (see §7f) | | `MovementManager::HandleUpdateTarget` | — | — | **not present in this PDB** (see §6j) | | `MakeMoveToManager` | — | — | deferred to R4; only `MoveToManager::Create` entry-point call shape captured (§6c/§6k) | --- ## 10. Verbatim constants inventory (motion ids / error codes / bit flags seen in this scope) - `0x41000003` — canonical "forward = none" motion id (neutral). Used as the reset target everywhere: `StopCompletely`, `enter_default_state`'s sentinel node, `StopInterpretedMotion`'s post-stop requeue, `apply_interpreted_movement`'s fallback. - `0x10000000` — the **action-class bit** on a motion id — tested identically in `CMotionInterp::MotionDone`, `CMotionInterp::HandleExitWorld`, `DoMotion`'s action-depth gate, `MotionTableManager::AnimationDone`/ `CheckForCompletedMotions`. - `0x8000003d` — `MotionStance` id for "non-combat / unrestricted" (the ONLY stance from which jump-charge motions and bit-`0x2000000` motions are allowed through `DoMotion`'s gate). - `0x2000000` — bit tested against the raw motion id in `DoMotion`'s combat-stance gate (rejected with `0x42` outside non-combat stance). - `0x41000012` / `0x41000013` / `0x41000014` — the three jump-charge motion ids, rejected in combat stance with distinct codes `0x3f`/`0x40`/`0x41` respectively. - Error codes seen: `8` (no physics_obj), `0x24` (not grounded, physics `state` bit `0x400` set — `jump_is_allowed`), `0x3f`/`0x40`/`0x41`/`0x42` (combat-stance jump-charge rejections, `DoMotion`), `0x45` (action queue depth ≥ 6, `DoMotion`), `0x47` (bad `MovementStruct.type`, OR `IsFullyConstrained`, OR `JumpStaminaCost` failure), `0x48` (jump BLOCKED by the current motion/position — the blocklist reject from `motion_allows_jump`/`jump_charge_is_allowed`/ `charge_jump`; **NOT a success sentinel** — 0 is the success/pass value across the whole jump-gate family, per W0-pins A1), `0x49` (`CanJump` virtual rejected the charge — stamina/burden gate). - `0.000199999995f` (~0.0002) — the epsilon used throughout for "is this velocity component effectively zero" (`get_jump_v_z`, `get_leave_ground_velocity` x3). - `HoldKey` encoding confirmed by direct assignment: `HoldKey_None = 1`, `HoldKey_Run = 2` (derived from `set_hold_run`'s `(bool)+1` arithmetic cross-checked against `SetHoldKey`'s literal `HoldKey_None`/`HoldKey_Run` assignments). - `MovementTypes::Type` enum (verbatim, acclient.h:2856): `Invalid=0, RawCommand=1, InterpretedCommand=2, StopRawCommand=3, StopInterpretedCommand=4, StopCompletely=5, MoveToObject=6, MoveToPosition=7, TurnToObject=8, TurnToHeading=9`. - `physics_obj->state` bit `0x400` (tested as `(int16_t)state)[1] & 4`, i.e. byte 1 bit 2 of the 16-bit state word = bit 10 overall = `0x400`) — gravity/BSP-active flag, gates `HitGround`/`LeaveGround`'s body AND `jump_is_allowed`'s ground-check branch. - `transient_state` bits `0x1` and `0x2` (both required) = "on ground" — `on_ground`, `charge_jump`, `contact_allows_move`, `is_standing_still` all inline this exact test.