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

85 KiB

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)

/* 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_queue00527b80 @ line 305032

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_pending00527fe0 @ line 305322

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):

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::MotionDone00527ec0 @ line 305238 (FULL BODY)

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::MotionDoneMovementManager::MotionDoneCMotionInterp::MotionDone, called once per completed animation node.

1d. CMotionInterp::HandleExitWorld00527f30 (drains the whole queue, same action-class handling, looped until empty)

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

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_jump005279e0 @ line 304908 (__pure)

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_allowed00527a50 @ line 304935

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_z00527aa0 @ line 304953

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_velocity005280c0 @ line 305404

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_jump005281c0 @ line 305448

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::jump00528780 @ line 305792

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_allowedJumpStaminaCost). 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_ground00527b20 @ line 304996 (context, referenced by 3e)

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_allowed005282b0 (context — feeds jump(), §3f)

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::HitGround00528ac0 @ line 305996

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::LeaveGround00529710... 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.)

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::ReportExhaustion005288d0 @ line 305861

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_state00528c80 @ line 306124

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::StopCompletely00527e40 @ line 305208

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::StopMotion00528530 @ line 305674

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_run00528b70 @ line 306053

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::SetHoldKey00528bb0 @ line 306072

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::Create00524050 @ line 300150

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/SetWeenieObjectCreate 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::PerformMovement005240d0 @ line 300194 (the dispatch)

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

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::MotionDoneMovementManager::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

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).

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.

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).

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.

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.

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::CancelMoveTo005241b0 @ line 300277

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::EnterDefaultState005241c0 @ line 300292

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

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  }
00524230  class InterpretedMotionState const* __fastcall MovementManager::InqInterpretedMotionState(class MovementManager* this)
00524230  { /* identical lazy-create pattern */ return &this->motion_interpreter->interpreted_state; }
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  }
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::Destroy005243f0 @ line 300538

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. HandleUpdateTargetnot 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_autonomous0050eb30 @ line 276443

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::MotionDone0050fdb0 @ line 277856 (FULL BODY — pure tailcall relay)

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):

/* 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::UpdateCSequence::update, and by CPartArray::CheckForCompletedMotions/AnimationDone tailcalls from CPhysicsObj, §7d/7e) that fires the completion callback back UP into CMotionInterp.

7c. CPhysicsObj::InqRawMotionState0050fde0 @ line 277883

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::CheckForCompletedMotions0050fe30 @ line 277925 (pure relay, tailcall)

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_AnimDone0050fda0 @ line 277845 (the animation-hook entry point, distinct from MotionDone)

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::AnimationDoneMotionTableManager::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 / IsMovingOrAnimatingnot 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).
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.
  • 0x8000003dMotionStance 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.