acdream/docs/research/2026-07-02-r3-motioninterp/r3-motioninterp-decomp.md
Erik cd0289bea2 docs(R3-W0): ambiguity pins A1-A10 — all textually resolved, adversarially verified
Workflow-produced pin pass over the R3 decomp extraction (3 independent
raw re-readers + adversarial refuters on the two load-bearing pins +
synthesis). No pin was refuted; none blocks on cdb.

Headlines:
- A1: motion_allows_jump 0x005279e0 is a BLOCKLIST (0 = pass, 0x48 =
  blocked) — the BN extraction's whitelist annotation was inverted
  (corrected in-place in §3a + §10). Retail blocks FALLEN 0x40000008
  and PASSES Falling 0x40000015; ACE mis-transcribed the exact-id term
  as Falling (one-off slip — ACE's own charge gates use Fallen).
  Definitive literal-uint blocklist table recorded.
- A3: the raw-vs-interpreted dual-dispatch gate is IsThePlayer (vtable
  slot +0x14, bound via the ACCWeenieObject vftable dump @0x007e3ea0),
  NOT ACE's IsCreature — in all four functions. Anti-artifact proof:
  HitGround/LeaveGround nearby call the +0x2c IsCreature slot, so BN
  distinguishes the slots locally. Copying ACE's gate would send
  remotes down apply_raw_movement against an empty raw state.
- A4: MovementParameters absolute-mask table pinned from acclient.h's
  own bitfield struct; retail ctor default 0x1EE0F has can_charge
  CLEAR (ACE sets it true) and walk_run_threshold 15.0 (ACE 1.0).
- A5/A6: both jump-velocity epsilons are 0.000199999995f (acdream's
  0.001 must change); get_leave_ground_velocity's fallback matrix is
  GLOBAL→LOCAL (index-pattern match against Frame::globaltolocalvec).
- A10: definitive error-code table from an exhaustive return-site
  sweep, incl. a second 0x24 site (DoInterpretedMotion action-class
  contact block) absent from the plan row.
- Adjacent: move_to_interpreted_state's apply_current_movement arg2 is
  a garbled allowJump = (motion_allows_jump(old fwd) == 0) — polarity
  trap for W-commits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:24:46 +02:00

1705 lines
85 KiB
Markdown

# 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<CMotionInterp::MotionNode> 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.