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>
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_queue — 00527b80 @ 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)whereeax_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)whereeax_5is the jump-error-code computed just above (see §3motion_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
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::MotionDone — 00527ec0 @ 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:
- If
physics_obj == null, no-op (motion interpreter isn't attached). - If
pending_motionsis empty, no-op. - Peek the head node. If
head.motion & 0x10000000(the action-class flag, i.e. this pending motion was queued via the0x10000000-flagged multi-action path — seeDoMotion'sInterpretedMotionState::GetNumActions(...) >= 6gate andmove_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'sactionsqueue.RawMotionState::RemoveAction(&raw_state)— same for raw state.
- Unconditionally pop the queue head (dequeue +
deletetheLListDatanode), updatingtail_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)
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:
- No-op (return
8= "no physics object") ifphysics_objis null. - Snapshot every
MovementParametersfield onto the stack (locals), then rebuild a fresh localMovementParameters(var_2c) — this isDoMotiondefaulting: the incomingarg3is copied field-by-field but the call intoDoInterpretedMotionbelow passes the freshly-defaulted local, not the caller'sarg3, except for the mutatedarg2/speed/hold_key_to_applytriple thatadjust_motionwrites in place. In other words:DoMotionre-derives a canonicalMovementParametersfrom 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). - If the incoming bitfield's sign bit is set →
interrupt_current_movement(cancel any physics-level transition in progress before applying the new motion). - If bit
0x800(SetHoldKeyflag) is set → callSetHoldKey(hold_key_to_apply, bit0xf)BEFOREadjust_motionruns, so the new hold-key affects the walk/run reinterpretation below. adjust_motion(&arg2, &speed, hold_key_to_apply)— this is the D6-ported function; mutatesarg2(the motion id) andspeedin place per the retail walk/run/sidestep reinterpretation table.- Combat-stance gate: if
interpreted_state.current_style != 0x8000003d(i.e. the creature is in ANY combat/special stance, not the neutralMotionStance_NonCombat), then jump-charge motions (0x41000012/13/14) and any motion with bit0x2000000set are rejected outright with distinct error codes0x3f/0x40/0x41/0x42— you cannot charge or release a jump while in combat stance, non-combat with bow drawn, etc. - Action-queue depth gate: if the motion is an "action-class" motion (bit
0x10000000set) ANDInterpretedMotionState::GetNumActions(...) >= 6, reject with0x45("too many pending actions" — 6 is the hard cap on queued interpreted actions). - Otherwise delegate to
DoInterpretedMotion(this, arg2, &var_2c)(the already-ported function) using the locally reconstructedMovementParameters. - If that succeeded AND bit
0x2000of the ORIGINALarg3->__inner0is set (mirror- to-raw flag), replay the motion intoraw_stateviaRawMotionState::ApplyMotion. - Return whatever
DoInterpretedMotionreturned (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)
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 == 0x40000016through0x40000018inclusive (Reload/Unload/Pickup) — BLOCKED (falls through theelse ifsince neither branch passes it; note< 0x40000016is the else-if guard, so0x40000016..0x40000018skip both inner branches and hitreturn 0x48directly).arg2 in [0x4000001e, 0x40000039](AimLevel..MagicPray) — BLOCKED.arg2 in [0x41000012, 0x41000014](Crouch/Sitting/Sleeping) — BLOCKED.- everything else →
0(pass), including Falling0x40000015, Ready0x41000003, Dead0x40000011, 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
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
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
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.
- Start with
get_state_velocity(esi)— the horizontal (x/y) velocity implied by the current interpreted forward/sidestep speed (already ported in D6). - Set
esi.z = get_jump_v_z(this)— the vertical jump-charge velocity. - 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), overwriteesientirely with the physics object's current local velocity transformed into global space via them_position.frame.m_fl2gv3x3 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
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
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)
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)
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
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.)
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:
- Compute the launch velocity via
get_leave_ground_velocity(§3d). 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).- Reset
standing_longjump = 0andjump_extent = 0.0— the charge is consumed the instant you actually leave the ground. 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
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
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
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
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
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)— explicitarg3=1(interrupt flag) from the player input/command layer.
5d. CMotionInterp::SetHoldKey — 00528bb0 @ 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::Create — 00524050 @ 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/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)
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::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
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::CancelMoveTo — 005241b0 @ 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::EnterDefaultState — 005241c0 @ 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::Destroy — 005243f0 @ 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. 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
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)
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:
- If the node's
motion & 0x10000000(action-class flag — same bitCMotionInterp:: MotionDonetests), callMotionState::remove_action_head(&this->state)— pop the animation-table's own parallel action-tracking state (a THIRD action-queue, local toMotionTableManager::state, distinct fromInterpretedMotionState::actionsandRawMotionState's equivalent — all three are kept in lockstep by the0x10000000bit). - Fire
CPhysicsObj::MotionDone(physics_obj, node.motion, ...)— which is the whole chain that eventually reachesCMotionInterp::MotionDone(§1c) to poppending_motions. - Unlink+delete the
pending_animationsnode 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
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)
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)
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) — tailcallsCSequence::has_anims(&sequence).CMotionInterp::is_standing_still(00527fa0, full body already shown inline below since it's small and directly relevant) — testson_groundbits ANDinterpreted_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 inCMotionInterp::MotionDone,CMotionInterp::HandleExitWorld,DoMotion's action-depth gate,MotionTableManager::AnimationDone/CheckForCompletedMotions.0x8000003d—MotionStanceid for "non-combat / unrestricted" (the ONLY stance from which jump-charge motions and bit-0x2000000motions are allowed throughDoMotion's gate).0x2000000— bit tested against the raw motion id inDoMotion's combat-stance gate (rejected with0x42outside non-combat stance).0x41000012/0x41000013/0x41000014— the three jump-charge motion ids, rejected in combat stance with distinct codes0x3f/0x40/0x41respectively.- Error codes seen:
8(no physics_obj),0x24(not grounded, physicsstatebit0x400set —jump_is_allowed),0x3f/0x40/0x41/0x42(combat-stance jump-charge rejections,DoMotion),0x45(action queue depth ≥ 6,DoMotion),0x47(badMovementStruct.type, ORIsFullyConstrained, ORJumpStaminaCostfailure),0x48(jump BLOCKED by the current motion/position — the blocklist reject frommotion_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(CanJumpvirtual 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_velocityx3).HoldKeyencoding confirmed by direct assignment:HoldKey_None = 1,HoldKey_Run = 2(derived fromset_hold_run's(bool)+1arithmetic cross-checked againstSetHoldKey's literalHoldKey_None/HoldKey_Runassignments).MovementTypes::Typeenum (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->statebit0x400(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, gatesHitGround/LeaveGround's body ANDjump_is_allowed's ground-check branch.transient_statebits0x1and0x2(both required) = "on ground" —on_ground,charge_jump,contact_allows_move,is_standing_stillall inline this exact test.