# Retail decomp extract: MovementManager facade Source: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (line numbers as of this extraction), struct from `docs/research/named-retail/acclient.h`. ## Struct: `MovementManager` (acclient.h line 30942-30949, comment `/* 3463 */`) ```c /* 3463 */ struct __cppobj MovementManager { CMotionInterp *motion_interpreter; MoveToManager *moveto_manager; CPhysicsObj *physics_obj; CWeenieObject *weenie_obj; }; ``` Field offsets (inferred from `MovementManager::Create` below, all uint32_t/pointer, 0x10 total size): - `+0x0` motion_interpreter - `+0x4` moveto_manager - `+0x8` physics_obj - `+0xc` weenie_obj ## Struct: `PositionManager` (acclient.h line 30951-30958, comment `/* 3468 */`) — adjacent, R5's other target, NOT extracted here (out of scope) ```c /* 3468 */ struct __cppobj PositionManager { InterpolationManager *interpolation_manager; StickyManager *sticky_manager; ConstraintManager *constraint_manager; CPhysicsObj *physics_obj; }; ``` ## Ownership: `CPhysicsObj` fields (acclient.h ~line 30715-30717) ```c MovementManager *movement_manager; PositionManager *position_manager; int last_move_was_autonomous; int jumped_this_frame; ``` `CPhysicsObj` owns exactly one `MovementManager*` and one `PositionManager*`, both lazily constructed. No eager allocation at `CPhysicsObj` construction time was found in this pass. --- ## `MovementManager::MakeMoveToManager` — 00524000 ```c 00524000 void __fastcall MovementManager::MakeMoveToManager(class MovementManager* this) 00524000 { 00524008 if (this->moveto_manager == 0) 0052401a this->moveto_manager = MoveToManager::Create(this->physics_obj, this->weenie_obj); 00524000 } ``` Lazy-construct `moveto_manager` if null, via `MoveToManager::Create(physics_obj, weenie_obj)`. No-op if already present. Called from `unpack_movement` cases 6/7/8/9 before touching `moveto_manager`. --- ## `MovementManager::SetWeenieObject` — 00524020 ```c 00524020 void __thiscall MovementManager::SetWeenieObject(class MovementManager* this, class CWeenieObject* arg2) 00524020 { 00524023 class CMotionInterp* motion_interpreter = this->motion_interpreter; 0052402c this->weenie_obj = arg2; 0052402c 0052402f if (motion_interpreter != 0) 00524032 CMotionInterp::SetWeenieObject(motion_interpreter, arg2); 00524032 00524037 class MoveToManager* moveto_manager = this->moveto_manager; 00524037 0052403c if (moveto_manager != 0) 0052403f MoveToManager::SetWeenieObject(moveto_manager, arg2); 00524020 } ``` Stores `weenie_obj` on `this`, then forwards to both children (`motion_interpreter`, `moveto_manager`) if they exist yet. Pure propagate-to-children setter. --- ## `MovementManager::Create` (factory / constructor seam) — 00524050 Not in the requested list by name but is the actual "constructor" — grep found no `MovementManager::MovementManager` ctor; all construction goes through this static factory. ```c 00524050 class MovementManager* MovementManager::Create(class CPhysicsObj* arg1, class CWeenieObject* arg2) 00524050 { 00524054 void* result_1 = operator new(0x10); 0052405e void* result; 0052405e 0052405e if (result_1 == 0) 0052407f result = nullptr; 0052405e else 0052405e { 00524060 *(uint32_t*)result_1 = 0; 00524066 *(uint32_t*)((char*)result_1 + 4) = 0; 0052406d *(uint32_t*)((char*)result_1 + 8) = 0; 00524074 *(uint32_t*)((char*)result_1 + 0xc) = 0; 0052407b result = result_1; 0052405e } 0052405e 00524081 class CMotionInterp* ecx = *(uint32_t*)result; 00524089 *(uint32_t*)((char*)result + 8) = arg1; 00524089 0052408c if (ecx != 0) 0052408f CMotionInterp::SetPhysicsObject(ecx, arg1); 0052408f 00524094 class MoveToManager* ecx_1 = *(uint32_t*)((char*)result + 4); 00524094 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; 005240a9 005240ac if (ecx_2 != 0) 005240af CMotionInterp::SetWeenieObject(ecx_2, arg2); 005240af 005240b4 class MoveToManager* ecx_3 = *(uint32_t*)((char*)result + 4); 005240b4 005240b9 if (ecx_3 != 0) 005240bc MoveToManager::SetWeenieObject(ecx_3, arg2); 005240bc 005240c5 return result; 00524050 } ``` `operator new(0x10)` (16 bytes, matches 4 pointer fields), zero-fills all 4 fields, then sets `physics_obj = arg1` / `weenie_obj = arg2` and — since `motion_interpreter` and `moveto_manager` are freshly zeroed — the `if (ecx != 0)` / `if (ecx_1 != 0)` branches are dead on a fresh object (always false right after the zero-fill in this call path; they only matter if this same field-setting logic is reused elsewhere). Effectively: **plain-old-data zero-init, no real constructor logic beyond storing the two pointers.** No standalone `MovementManager::SetPhysicsObject` exists — the physics_obj is set once here, at Create time, and never independently. NOTE: this reads like dead/degenerate branches (checking a field it just zeroed two lines earlier) — likely because Binary Ninja inlined a shared "SetPhysicsObject/SetWeenieObject propagate" helper that's also called from non-fresh contexts (matches the pattern seen in `SetWeenieObject` above). Keep verbatim; not garbled bitfield mush, just dead-code-looking symmetry from inlining. --- ## `MovementManager::PerformMovement` — 005240d0 ```c 005240d0 uint32_t __thiscall MovementManager::PerformMovement(class MovementManager* this, class MovementStruct const* arg2) 005240d0 { 005240d9 CPhysicsObj::set_active(this->physics_obj, 1); 005240e4 void* eax_1 = (arg2->type - 1); 005240e4 005240e8 if (eax_1 > 8) 00524159 return 0x47; 00524159 005240f1 switch (eax_1) 005240f1 { 005240fb case nullptr: 005240fb case 1: 005240fb case 2: 005240fb case 3: 005240fb case 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; 00524112 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: 0052412f case 6: 0052412f case 7: 0052412f case 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 } 0052415c uint32_t jump_table_52415c[0x2] = 0052415c { 0052415c [0x0] = 0x005240f8 00524160 [0x1] = 0x0052412a 00524164 } 00524164 uint8_t lookup_table_524164[0x9] = 00524164 { 00524164 [0x0] = 0x00 00524165 [0x1] = 0x00 00524166 [0x2] = 0x00 00524167 [0x3] = 0x00 00524168 [0x4] = 0x00 00524169 [0x5] = 0x01 0052416a [0x6] = 0x01 0052416b [0x7] = 0x01 0052416c [0x8] = 0x01 0052416d } ``` `arg2->type` is 1-based; `eax_1 = type - 1` is the 0-based dispatch index, range-checked against 8 (types 1..9 valid, else return error code `0x47`). Two-way split via `lookup_table_524164`: types 1-5 (index 0-4, i.e. `arg2->type` 1..5) route through `CMotionInterp` (lazy-create + `enter_default_state` if not yet built, then delegate `CMotionInterp::PerformMovement`); types 6-9 (index 5-8) route through `MoveToManager` (lazy-create, delegate `MoveToManager::PerformMovement`, always return 0 — return value of the MoveToManager path is NOT propagated, unlike the CMotionInterp path which returns whatever `CMotionInterp::PerformMovement` returns). Always marks the physics object active first (`CPhysicsObj::set_active(this->physics_obj, 1)`). --- ## `MovementManager::move_to_interpreted_state` — 00524170 ```c 00524170 void __thiscall MovementManager::move_to_interpreted_state(class MovementManager* this, class InterpretedMotionState const* arg2) 00524170 { 00524176 if (this->motion_interpreter == 0) 00524176 { 00524180 class CMotionInterp* eax_2 = CMotionInterp::Create(this->physics_obj, this->weenie_obj); 0052418b bool cond:0_1 = this->physics_obj == 0; 0052418d this->motion_interpreter = eax_2; 0052418d 0052418f if (!(cond:0_1)) 00524193 CMotionInterp::enter_default_state(eax_2); 00524176 } 00524176 0052419f CMotionInterp::move_to_interpreted_state(this->motion_interpreter, arg2); 00524170 } ``` Lazy-create `motion_interpreter` (same idiom as everywhere else: create, then `enter_default_state` ONLY if `physics_obj != 0`), then delegate to `CMotionInterp::move_to_interpreted_state(interp, arg2)`. Called from `unpack_movement` case 0 (the raw/interpreted network unpack path). --- ## `MovementManager::CancelMoveTo` — 005241b0 ```c 005241b0 void __fastcall MovementManager::CancelMoveTo(class MovementManager* this, uint32_t arg2) 005241b0 { 005241b0 class MoveToManager* moveto_manager = this->moveto_manager; 005241b0 005241b5 if (moveto_manager == 0) 005241bc return; 005241bc 005241b7 uint32_t edx; 005241b7 /* tailcall */ 005241b7 return MoveToManager::CancelMoveTo(moveto_manager, edx); 005241b0 } ``` No-op if `moveto_manager` is null; else tailcalls `MoveToManager::CancelMoveTo`. NOTE: `arg2` is loaded but the tailcall passes an **uninitialized** local `edx` instead of `arg2` — decompiler register-tracking artifact (arg2 IS in edx per `__fastcall` ABI, this is BN failing to alias the parameter register to the "edx" pseudo-var name); functionally it's `MoveToManager::CancelMoveTo(moveto_manager, arg2)`. --- ## `MovementManager::EnterDefaultState` — 005241c0 ```c 005241c0 void __fastcall MovementManager::EnterDefaultState(class MovementManager* this) 005241c0 { 005241c3 class CPhysicsObj* physics_obj = this->physics_obj; 005241c3 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 } ``` Early-return no-op if `physics_obj == 0` (i.e. never called meaningfully before the MovementManager is attached to a physics object). Otherwise lazy-create `motion_interpreter` (same idiom), then **unconditionally** tailcalls `CMotionInterp::enter_default_state` again at the end — meaning on the fresh-create path `enter_default_state` runs twice in a row (once inside the lazy-create block, once at the tail). Verbatim as decompiled; flagging as a NOTE since double-invoke looks odd but matches the repeated idiom seen in every other lazy-create call site in this file (all of them gate the inner call on `physics_obj != 0` which is already guaranteed true here since we already early-returned above). --- ## `MovementManager::IsMovingTo` — 00524260 ```c 00524260 int32_t __fastcall MovementManager::IsMovingTo(class MovementManager const* this) 00524260 { 00524260 class MoveToManager* moveto_manager = this->moveto_manager; 00524260 00524265 if ((moveto_manager != 0 && MoveToManager::is_moving_to(moveto_manager) != 0)) 00524275 return 1; 00524275 00524278 return 0; 00524260 } ``` Returns 1 iff `moveto_manager` exists AND `MoveToManager::is_moving_to()` is true, else 0. --- ## `MovementManager::motions_pending` — 00524280 ```c 00524280 int32_t __fastcall MovementManager::motions_pending(class MovementManager const* this) 00524280 { 00524280 class CMotionInterp* motion_interpreter = this->motion_interpreter; 00524280 00524284 if ((motion_interpreter != 0 && CMotionInterp::motions_pending(motion_interpreter) != 0)) 00524294 return 1; 00524294 00524297 return 0; 00524280 } ``` Returns 1 iff `motion_interpreter` exists AND `CMotionInterp::motions_pending()` is true, else 0. Mirror-shape of `IsMovingTo` but checks the interp side instead of the moveto side. --- ## `MovementManager::get_minterp` — 005242a0 (bonus, referenced by callers; not in the original ask but needed for context — CPhysicsObj::get_minterp tailcalls into it) ```c 005242a0 class CMotionInterp* __fastcall MovementManager::get_minterp(class MovementManager* this) 005242a0 { 005242a6 if (this->motion_interpreter == 0) 005242a6 { 005242b0 class CMotionInterp* eax_2 = CMotionInterp::Create(this->physics_obj, this->weenie_obj); 005242bb bool cond:0_1 = this->physics_obj == 0; 005242bd this->motion_interpreter = eax_2; 005242bd 005242bf if (!(cond:0_1)) 005242c3 CMotionInterp::enter_default_state(eax_2); 005242a6 } 005242a6 005242cb return this->motion_interpreter; 005242a0 } ``` Same lazy-create idiom; returns the (possibly freshly created) `motion_interpreter`. --- ## `MovementManager::MotionDone` — 005242d0 ```c 005242d0 void __thiscall MovementManager::MotionDone(class MovementManager* this, uint32_t arg2, int32_t arg3) 005242d0 { 005242d0 class CMotionInterp* motion_interpreter = this->motion_interpreter; 005242d0 005242d4 if (motion_interpreter != 0) 005242d4 { 005242da int32_t var_4_1 = arg3; 005242db int32_t edx; 005242db CMotionInterp::MotionDone(motion_interpreter, edx); 005242d4 } 005242d0 } ``` No-op if `motion_interpreter` is null; else forwards to `CMotionInterp::MotionDone`. NOTE: same register-aliasing artifact as `CancelMoveTo` — `arg2` is stashed but the call passes an uninitialized-looking local `edx` (and `arg3` is stored to `var_4_1` but that local is never read/passed either); functionally this is `CMotionInterp::MotionDone(motion_interpreter, arg2, arg3)` — BN's `__thiscall` register tracking dropped the second/third args' names. Not evidence of a bug in the real function; just decompiler noise on a two/three-arg thiscall forward. --- ## `MovementManager::UseTime` — 005242f0 ```c 005242f0 void __fastcall MovementManager::UseTime(class MovementManager* this) 005242f0 { 005242f0 class MoveToManager* moveto_manager = this->moveto_manager; 005242f0 005242f5 if (moveto_manager == 0) 005242fc return; 005242fc 005242f7 /* tailcall */ 005242f7 return MoveToManager::UseTime(moveto_manager); 005242f0 } ``` No-op if `moveto_manager` null; else tailcalls `MoveToManager::UseTime`. Does NOT touch `motion_interpreter` at all (unlike `HitGround`/`LeaveGround`/`ReportExhaustion` below, which forward to both children). --- ## `MovementManager::HitGround` — 00524300 ```c 00524300 void __fastcall MovementManager::HitGround(class MovementManager* this) 00524300 { 00524303 class CMotionInterp* motion_interpreter = this->motion_interpreter; 00524303 00524307 if (motion_interpreter != 0) 00524309 CMotionInterp::HitGround(motion_interpreter); 00524309 0052430e class MoveToManager* moveto_manager = this->moveto_manager; 0052430e 00524314 if (moveto_manager == 0) 0052431b return; 0052431b 00524316 /* tailcall */ 00524316 return MoveToManager::HitGround(moveto_manager); 00524300 } ``` Fans out to BOTH children unconditionally-if-present: `CMotionInterp::HitGround` first, then tailcalls `MoveToManager::HitGround`. --- ## `MovementManager::LeaveGround` — 00524320 ```c 00524320 void __fastcall MovementManager::LeaveGround(class MovementManager* this) 00524320 { 00524323 class CMotionInterp* motion_interpreter = this->motion_interpreter; 00524323 00524327 if (motion_interpreter != 0) 00524329 CMotionInterp::LeaveGround(motion_interpreter); 00524329 0052432e class MoveToManager* moveto_manager = this->moveto_manager; 0052432e 00524334 if (moveto_manager == 0) 0052433b return; 0052433b 00524336 /* tailcall */ 00524336 return IDClass<_tagDataID,32,0>::~IDClass<_tagDataID,32,0>(moveto_manager); 00524320 } ``` Same shape as `HitGround`: fan out to `CMotionInterp::LeaveGround` then tailcall the moveto-manager equivalent. **NOTE (BN mislabel, HIGH CONFIDENCE):** the tail call target is decompiled as `IDClass<_tagDataID,32,0>::~IDClass<_tagDataID,32,0>(moveto_manager)` — a destructor for an unrelated ID-wrapper template class. This is obviously wrong for the context (nothing is being destroyed here; the pattern is identical to `HitGround`/`UseTime`/`ReportExhaustion` which all call the matching `MoveToManager::XxxMethod`). Binary Ninja's static analysis matched the call target address to the wrong overload/thunk. The real call is almost certainly `MoveToManager::LeaveGround(moveto_manager)`. Keep the raw decompiled text above for the record; the lead should treat the semantic target as `MoveToManager::LeaveGround`. --- ## `MovementManager::HandleEnterWorld` — 00524340 ```c 00524340 void __fastcall MovementManager::HandleEnterWorld(class MovementManager* this) 00524340 { 00524340 class CMotionInterp* motion_interpreter = this->motion_interpreter; 00524340 00524344 if (motion_interpreter == 0) 0052434b return; 0052434b 00524346 /* tailcall */ 00524346 return IDClass<_tagDataID,32,0>::~IDClass<_tagDataID,32,0>(motion_interpreter); 00524340 } ``` No-op if `motion_interpreter` null; else tailcalls what should be `CMotionInterp::HandleEnterWorld(motion_interpreter)`. **NOTE (BN mislabel, HIGH CONFIDENCE):** same spurious `IDClass<...>::~IDClass<...>` destructor mislabel as in `LeaveGround` above. Given the neighboring function `HandleExitWorld` (below) correctly shows `CMotionInterp::HandleExitWorld`, the real target here is almost certainly `CMotionInterp::HandleEnterWorld(motion_interpreter)`. Notably, this function does NOT touch `moveto_manager` at all (unlike HitGround/LeaveGround/ ReportExhaustion) — only the motion interpreter gets the enter-world notification. --- ## `MovementManager::HandleExitWorld` — 00524350 ```c 00524350 void __fastcall MovementManager::HandleExitWorld(class MovementManager* this) 00524350 { 00524350 class CMotionInterp* motion_interpreter = this->motion_interpreter; 00524350 00524354 if (motion_interpreter == 0) 0052435b return; 0052435b 00524356 /* tailcall */ 00524356 return CMotionInterp::HandleExitWorld(motion_interpreter); 00524350 } ``` No-op if `motion_interpreter` null; else tailcalls `CMotionInterp::HandleExitWorld`. This one resolved cleanly (no mislabel) — cross-check anchor confirming the sibling functions' correct semantic targets. Also does NOT touch `moveto_manager`. --- ## `MovementManager::ReportExhaustion` — 00524360 ```c 00524360 void __fastcall MovementManager::ReportExhaustion(class MovementManager* this) 00524360 { 00524363 class CMotionInterp* motion_interpreter = this->motion_interpreter; 00524363 00524367 if (motion_interpreter != 0) 00524369 CMotionInterp::ReportExhaustion(motion_interpreter); 00524369 0052436e class MoveToManager* moveto_manager = this->moveto_manager; 0052436e 00524374 if (moveto_manager == 0) 0052437b return; 0052437b 00524376 /* tailcall */ 00524376 return IDClass<_tagDataID,32,0>::~IDClass<_tagDataID,32,0>(moveto_manager); 00524360 } ``` Fans out to both children like `HitGround`: `CMotionInterp::ReportExhaustion` first (resolved cleanly), then tailcalls the moveto-manager equivalent. **NOTE (BN mislabel, HIGH CONFIDENCE):** same spurious `IDClass<...>::~IDClass<...>` on the `moveto_manager` tail call — real target is almost certainly `MoveToManager::ReportExhaustion(moveto_manager)`, matching the pattern of every other fan-out function (HitGround, LeaveGround) where the CMotionInterp call resolves correctly but the MoveToManager tailcall gets the same wrong destructor label. This looks like a systematic BN issue with one specific MoveToManager vtable slot / thunk rather than three independent misreads. --- ## `MovementManager::Destroy` — 005243f0 (bonus — the matching teardown for `Create`; not originally requested but directly relevant to the facade's lifecycle) ```c 005243f0 void __fastcall MovementManager::Destroy(class MovementManager* this) 005243f0 { 005243f4 class CMotionInterp* motion_interpreter = this->motion_interpreter; 005243f4 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; 0052440f 00524415 if (moveto_manager != 0) 00524415 { 00524419 MoveToManager::~MoveToManager(moveto_manager); 0052441f operator delete(moveto_manager); 00524415 } 00524415 00524428 this->moveto_manager = nullptr; 005243f0 } ``` Destroys+frees both children if present, nulls both pointers. Does NOT free `this` itself (matches — `MovementManager::Create` used `operator new`, but `Destroy` is presumably called before the owning `CPhysicsObj` does its own `operator delete(movement_manager)` elsewhere; that final delete call wasn't in this extraction's scope). --- ## `MovementManager::unpack_movement` — 00524440 (FULL FUNCTION — the movement-type switch) ```c 00524440 int32_t __thiscall MovementManager::unpack_movement(class MovementManager* this, void** arg2, uint32_t arg3) 00524440 { 0052444f if (this->motion_interpreter != 0) 0052444f { 00524455 class CPhysicsObj* physics_obj = this->physics_obj; 00524455 0052445a if (physics_obj != 0) 0052445a { 00524460 CPhysicsObj::interrupt_current_movement(physics_obj); 00524468 CPhysicsObj::unstick_from_object(this->physics_obj); 00524471 int32_t var_70 = 0x796910; 00524479 int32_t var_6c_1 = 0; 00524481 int32_t var_68 = 0x3f800000; 00524489 int32_t var_64_1 = 0; 00524491 int32_t var_60_1 = 0; 00524499 int32_t var_5c_1 = 0; 005244a1 int32_t var_34_1 = 0; 005244ac int32_t var_30_1 = 0; 005244b7 int32_t var_2c_1 = 0; 005244c2 Frame::cache(&var_68); 005244cb void var_9c; 005244cb MovementParameters::MovementParameters(&var_9c); 005244d7 void var_28; 005244d7 InterpretedMotionState::InterpretedMotionState(&var_28); 005244e3 void* eax_1 = *(uint32_t*)arg2; 005244e7 int16_t ecx_4 = *(uint16_t*)eax_1; 005244ed *(uint32_t*)arg2 = ((char*)eax_1 + 2); 005244ef uint32_t ebp_1 = ((uint32_t)ecx_4); 005244f5 int16_t var_a4_1 = ecx_4; 005244f9 ecx_4 = *(uint16_t*)((char*)eax_1 + 2); 005244fd *(uint32_t*)arg2 = ((char*)eax_1 + 4); 00524502 uint32_t ecx_5 = command_ids[((uint32_t)ecx_4)]; 00524502 00524522 if (CBaseFilter::GetPinVersion(this->motion_interpreter) != ecx_5) 0052452c CMotionInterp::DoMotion(this->motion_interpreter, ecx_5, &var_9c); 0052452c 0052453a void* var_b8_15; 0052453a 0052453a switch (((uint32_t)ebp_1)) 0052453a { 00524551 case 0: 00524551 { 00524551 InterpretedMotionState::UnPack(&var_28, arg2, arg3); 0052455d uint32_t ebx_3; 0052455d 0052455d if ((*(uint8_t*)((char*)var_a4_1)[1] & 1) == 0) 0052456a ebx_3 = 0; 0052455d else 0052455d { 0052455f uint32_t* eax_8 = *(uint32_t*)arg2; 00524561 ebx_3 = *(uint32_t*)eax_8; 00524566 *(uint32_t*)arg2 = &eax_8[1]; 0052455d } 0052455d 0052457c MovementManager::move_to_interpreted_state(this, &var_28); 0052457c 00524583 if (ebx_3 != 0) 00524589 CPhysicsObj::stick_to_object(this->physics_obj, ebx_3); 00524589 0052458e this->motion_interpreter->standing_longjump = (ebp_1 & 0x200); 0052459a InterpretedMotionState::~InterpretedMotionState(&var_28); 005245ae return 1; 00524551 break; 00524551 } 005245b3 case 6: 005245b3 { 005245b3 MovementManager::MakeMoveToManager(this); 005245b8 void* eax_11 = *(uint32_t*)arg2; 005245ba uint32_t ebx_4 = *(uint32_t*)eax_11; 005245cc *(uint32_t*)arg2 = ((char*)eax_11 + 4); 005245ce Position::UnPackOrigin(&var_70, arg2, arg3); 005245db MovementParameters::UnPackNet(&var_9c, MoveToObject, arg2, arg3); 005245e0 void* eax_13 = *(uint32_t*)arg2; 005245e2 long double x87_r7 = ((long double)*(uint32_t*)eax_13); 005245e7 *(uint32_t*)arg2 = ((char*)eax_13 + 4); 005245e9 this->motion_interpreter->my_run_rate = ((float)x87_r7); 005245e9 005245f9 if (CPhysicsObj::GetObjectA(ebx_4) == 0) 005245f9 goto label_524668; 005245f9 00524604 CPhysicsObj::MoveToObject(this->physics_obj, ebx_4, &var_9c); 00524610 InterpretedMotionState::~InterpretedMotionState(&var_28); 00524624 return 1; 005245b3 break; 005245b3 } 00524629 case 7: 00524629 { 00524629 MovementManager::MakeMoveToManager(this); 0052463b Position::UnPackOrigin(&var_70, arg2, arg3); 00524648 MovementParameters::UnPackNet(&var_9c, MoveToPosition, arg2, arg3); 0052464d void* eax_18 = *(uint32_t*)arg2; 0052464f long double x87_r7_1 = ((long double)*(uint32_t*)eax_18); 00524654 *(uint32_t*)arg2 = ((char*)eax_18 + 4); 00524656 this->motion_interpreter->my_run_rate = ((float)x87_r7_1); 00524668 label_524668: 00524668 MoveToManager::MoveToPosition(this->moveto_manager, &var_70, &var_9c); 00524674 InterpretedMotionState::~InterpretedMotionState(&var_28); 00524688 return 1; 00524629 break; 00524629 } 0052468d case 8: 0052468d { 0052468d MovementManager::MakeMoveToManager(this); 00524692 void* eax_21 = *(uint32_t*)arg2; 00524694 uint32_t ebx_6 = *(uint32_t*)eax_21; 005246a0 *(uint32_t*)arg2 = ((char*)eax_21 + 4); 005246a2 int32_t ecx_25 = *(uint32_t*)((char*)eax_21 + 4); 005246b3 *(uint32_t*)arg2 = ((char*)eax_21 + 8); 005246b5 MovementParameters::UnPackNet(&var_9c, TurnToObject, arg2, arg3); 005246b5 005246c5 if (CPhysicsObj::GetObjectA(ebx_6) == 0) 005246c5 { 005246fb int32_t var_8c_1 = ecx_25; 005246ff var_b8_15 = &var_9c; 005246c5 goto label_524725; 005246c5 } 005246c5 005246d0 CPhysicsObj::TurnToObject(this->physics_obj, ebx_6, &var_9c); 005246dc InterpretedMotionState::~InterpretedMotionState(&var_28); 005246f0 return 1; 0052468d break; 0052468d } 00524704 case 9: 00524704 { 00524704 MovementManager::MakeMoveToManager(this); 00524718 MovementParameters::UnPackNet(&var_9c, TurnToHeading, arg2, arg3); 00524721 var_b8_15 = &var_9c; 00524725 label_524725: 00524725 MoveToManager::TurnToHeading(this->moveto_manager, var_b8_15); 00524731 InterpretedMotionState::~InterpretedMotionState(&var_28); 00524745 return 1; 00524704 break; 00524704 } 0052453a } 0052453a 0052474f InterpretedMotionState::~InterpretedMotionState(&var_28); 0052445a } 0052444f } 0052444f 00524760 return 0; 00524440 } 00524764 uint32_t jump_table_524764[0xa] = 00524764 { 00524764 [0x0] = 0x00524541 // case 0 00524768 [0x1] = 0x00524748 // (unused index -> falls to default-exit path) 0052476c [0x2] = 0x00524748 00524770 [0x3] = 0x00524748 00524774 [0x4] = 0x00524748 00524778 [0x5] = 0x00524748 0052477c [0x6] = 0x005245b1 // case 6 00524780 [0x7] = 0x00524627 // case 7 00524784 [0x8] = 0x0052468b // case 8 00524788 [0x9] = 0x00524702 // case 9 0052478c } ``` **Function-level structure (top of function, before the type switch):** 1. Entire function is a no-op (falls through to `return 0`) unless `this->motion_interpreter != 0` AND `this->physics_obj != 0`. **This means `unpack_movement` requires the interpreter to already exist** — unlike every OTHER MovementManager method, this one does NOT lazily construct `motion_interpreter` on demand. If the interpreter hasn't been created yet (e.g. via `EnterDefaultState`/`PerformMovement`/`get_minterp`), an inbound network movement packet is silently dropped (returns 0, meaning presumably "0 bytes consumed" or "not handled" to the caller). 2. On the happy path: `CPhysicsObj::interrupt_current_movement(physics_obj)` then `CPhysicsObj::unstick_from_object(physics_obj)` — every unpacked movement command first cancels any in-flight movement and un-sticks the object from whatever it was stuck to (relevant to the R4-era sticky-guid work). 3. Builds a stack `Frame` (`var_70`..`var_2c_1`, `Frame::cache(&var_68)` — identity-ish frame init, `0x3f800000` = 1.0f, rest zeroed) and default-constructs a `MovementParameters` (`var_9c`) and an `InterpretedMotionState` (`var_28`) as scratch locals for the switch below. 4. Reads a 16-bit **header word** `ebp_1` from the wire (`*(uint16_t*)eax_1`, advances `arg2` by 2) — this is the `mt` (movement-type) value the switch dispatches on. A SECOND 16-bit value `ecx_4` is read right after (advances `arg2` by another 2) and used as an index into a `command_ids[]` table to get a motion-command id `ecx_5`; if that differs from the interpreter's current pin/version (`CBaseFilter::GetPinVersion` — **BN mislabel, see NOTE below**), it calls `CMotionInterp::DoMotion(interp, ecx_5, &var_9c)`. This happens **unconditionally before the switch**, for every movement type. 5. `switch (ebp_1)` dispatches on the FULL 16-bit header word, not a masked/shifted sub-field — cases match `0`, `6`, `7`, `8`, `9` literally. **This directly answers the task's ask about 0x100/0x200 header-flag handling: those bits are NOT separate switch cases or pre-switch branches.** They are only consumed in the `case 0` body (see below). No case for 0x100 or 0x200 as a distinct dispatch value exists — the jump table only has 9 accounted-for indices (0,6,7,8,9 real; 1-5 fall through to the same "no case" exit at `0x524748`, which is the shared post-switch cleanup + `return 0`... actually the decompiled text shows those return 0 via falling out of the switch to `InterpretedMotionState::~InterpretedMotionState(&var_28)` then `return 0`, since no case body ran). **`case 0` (mt == 0) — the raw/interpreted-motion unpack path, WITH the header-flag handling:** - `InterpretedMotionState::UnPack(&var_28, arg2, arg3)` — unpacks the actual motion state payload from the wire buffer. - **Sticky-guid extraction (this is where a header-derived flag conditionally reads an extra guid off the wire):** `if ((*(uint8_t*)((char*)var_a4_1)[1] & 1) == 0) ebx_3 = 0; else { read a uint32_t off the wire into ebx_3, advance arg2 by 4 }`. NOTE: the condition reads byte 1 of `var_a4_1` (the ORIGINAL 16-bit header value, stored earlier as `int16_t var_a4_1 = ecx_4` where `ecx_4` was the first 16-bit read == same value as `ebp_1`) and tests bit 0 of that HIGH byte — i.e. bit 8 of the 16-bit header, which is **`0x100`**. This is the "sticky guid" bit the task asked about: `mt & 0x100` gates whether an extra `uint32_t` object-guid is read off the wire right after `InterpretedMotionState::UnPack`. **NOTE (garbled-looking but NOT bitfield mush — it's a byte-address cast):** `*(uint8_t*)((char*)var_a4_1)[1]` looks bizarre (casting a 16-bit local's VALUE to a `char*` and indexing) — this is Binary Ninja's clumsy way of expressing "take the address of the local `var_a4_1`, then read byte offset 1 of it" (i.e. the high byte of the 16-bit `short`, since x86 is little-endian). Read literally it would be UB (treating the int16 VALUE as a pointer), so this is almost certainly BN mis-rendering `*((uint8_t*)&var_a4_1 + 1) & 1` (high byte of the header word, bit 0 of that byte = bit 8 of the word = `0x100`). Keeping the raw text per instructions, but the lead should read this as "bit `0x100` of the mt header word." - `MovementManager::move_to_interpreted_state(this, &var_28)` — feeds the unpacked state into the interpreter (see that function's extract above; itself has a redundant lazy-create guard even though we already know `motion_interpreter != 0` at this point since the whole function is gated on that at the top). - `if (ebx_3 != 0) CPhysicsObj::stick_to_object(this->physics_obj, ebx_3)` — if the sticky bit was set AND the guid we read is non-zero, stick the physics object to that target. This is the **0x100 sticky-guid handling** the task asked about, confirmed. - `this->motion_interpreter->standing_longjump = (ebp_1 & 0x200)` — **the 0x200 standing_longjump bit, confirmed.** Stored directly as a raw masked int (not normalized to 0/1) into `CMotionInterp::standing_longjump` on the (already-guaranteed-non-null) interpreter. This is a plain field write, not mush — the field just stores the raw-masked-bit value (nonzero-but-not-necessarily-1 when set) rather than a boolean 0/1. - destructs the scratch `InterpretedMotionState`, returns 1 (success/consumed). **`case 6` (MoveToObject):** - `MovementManager::MakeMoveToManager(this)` — ensures `moveto_manager` exists. - reads a target-object guid (`ebx_4`) off the wire, then `Position::UnPackOrigin(&var_70, ...)`, then `MovementParameters::UnPackNet(&var_9c, MoveToObject, arg2, arg3)` (the network-unpack overload, taking a `MovementType`/context enum `MoveToObject` as a literal tag), then reads a float `my_run_rate` off the wire (via x87 float-load pattern, `long double` round-trip) and stores it on `motion_interpreter->my_run_rate`. - `if (CPhysicsObj::GetObjectA(ebx_4) == 0) goto label_524668` — if the target object can't be resolved (not currently visible/known?), falls through to the shared `MoveToPosition` tail (reusing the just-unpacked `var_70`/`var_9c` as a position-based fallback) instead of the object-based move. - else `CPhysicsObj::MoveToObject(physics_obj, ebx_4, &var_9c)` — object exists, move directly to it. - returns 1 either way. **`case 7` (MoveToPosition):** - `MakeMoveToManager`, `Position::UnPackOrigin`, `MovementParameters::UnPackNet(..., MoveToPosition, ...)`, reads `my_run_rate` float, falls straight into `label_524668` (shared with case 6's object-not-found fallback): `MoveToManager::MoveToPosition(moveto_manager, &var_70, &var_9c)`. - returns 1. **`case 8` (TurnToObject):** - `MakeMoveToManager`, reads target guid `ebx_6` + an extra dword `ecx_25` (context_id?) off the wire, `MovementParameters::UnPackNet(&var_9c, TurnToObject, arg2, arg3)`. - `if (CPhysicsObj::GetObjectA(ebx_6) == 0)`: object not resolvable — falls through to the shared `label_524725` tail (`var_b8_15 = &var_9c`, i.e. degrades to a TurnToHeading-style call using just the unpacked params) instead of the object-based turn. - else `CPhysicsObj::TurnToObject(physics_obj, ebx_6, &var_9c)` directly. - returns 1 either way. **`case 9` (TurnToHeading):** - `MakeMoveToManager`, `MovementParameters::UnPackNet(&var_9c, TurnToHeading, arg2, arg3)`, falls into shared `label_524725`: `MoveToManager::TurnToHeading(moveto_manager, var_b8_15)`. - returns 1. **Fall-through / no matching case (mt in {1,2,3,4,5} or any other 16-bit value not 0/6/7/8/9):** - switch body produces no case match, control falls to `InterpretedMotionState::~InterpretedMotionState(&var_28)` then `return 0` — i.e. silently treated as "0 bytes handled" / not consumed, same as the "interpreter doesn't exist yet" early-out at the top. **NOTE (BN mislabel, MEDIUM CONFIDENCE):** `CBaseFilter::GetPinVersion(this->motion_interpreter)` at line 300597 — `CBaseFilter` is a DirectShow filter-graph base class, wildly out of place for a `CMotionInterp*` argument. This is almost certainly a mislabeled call to some `CMotionInterp` accessor (a "get current motion id / pin version"-shaped getter, maybe `CMotionInterp::InqPendingMotion` or similar) that BN matched to the wrong vtable-slot symbol. Kept verbatim per instructions; flagging so the lead doesn't chase DirectShow. **NOTE (data table, not extracted in full):** `command_ids[]` (referenced at line 300595, `command_ids[(uint32_t)ecx_4]`) is a lookup table mapping a wire-encoded small integer to a `MotionCommand` enum id. Not dumped here — out of scope for this extraction pass, but the lead may want it if porting the exact `DoMotion` pre-switch call. --- ## `MovementManager::HandleUpdateTarget` — 00524790 ```c 00524790 void __fastcall MovementManager::HandleUpdateTarget(class MovementManager* this, class TargetInfo arg2) 00524790 { 00524790 class MoveToManager* moveto_manager = this->moveto_manager; 00524790 00524795 if (moveto_manager != 0) 0052479c MoveToManager::HandleUpdateTarget(moveto_manager, &arg2); 00524790 } ``` No-op if `moveto_manager` null; else forwards `arg2` (a `TargetInfo`, passed by value into this function but forwarded by address) to `MoveToManager::HandleUpdateTarget`. Does not touch `motion_interpreter`. --- ## `CPhysicsObj::unpack_movement` — 00512040 (the CALLER seam / where `movement_manager` gets lazily constructed from the `CPhysicsObj` side — directly relevant context, not in the original list but requested implicitly via "CPhysicsObj's creation seam") ```c 00512040 void __thiscall CPhysicsObj::unpack_movement(class CPhysicsObj* this, void** arg2, uint32_t arg3) 00512040 { 0051204b if (this->movement_manager == 0) 0051204b { 0051205a this->movement_manager = MovementManager::Create(this, this->weenie_obj); 00512060 class MovementManager* eax_2; 00512060 eax_2 = this->state; 00512060 0051206b if ((eax_2 & 1) == 0) 0051206b { 0051206d uint32_t transient_state = this->transient_state; 0051206d 00512075 if (transient_state >= 0) 00512075 { 00512083 this->update_time = (*(uint32_t*)Timer::cur_time); 00512089 *(uint32_t*)((char*)this->update_time)[4] = *(int32_t*)((char*)Timer::cur_time + 4); 00512075 } 00512075 00512094 this->transient_state = (transient_state | 0x80); 0051206b } 0051204b } 0051204b 005120aa MovementManager::unpack_movement(this->movement_manager, arg2, arg3); 00512040 } ``` Lazy-creates `this->movement_manager` via `MovementManager::Create(this, this->weenie_obj)` if null (note: does NOT call `EnterDefaultState` here, unlike `get_minterp`'s lazy-create below — just `Create` then straight into `unpack_movement`). If bit 0 of `state` is clear (NOT static?), stamps `update_time = Timer::cur_time` (conditionally, if `transient_state >= 0`, i.e. sign bit clear) and ORs `0x80` into `transient_state` — looks like a "wake up / mark dynamic-and-recently-updated" side effect that happens on first-ever movement-manager creation for this physics object, gated behind the same `(state & 1) == 0` check seen again below. Then unconditionally tailcalls `MovementManager::unpack_movement(movement_manager, arg2, arg3)` (the function extracted above) — this is THE entry point that feeds inbound wire movement bytes into the facade. ## `CPhysicsObj::get_minterp` — 005120c0 (sibling lazy-create seam, for contrast) ```c 005120c0 class CMotionInterp* __fastcall CPhysicsObj::get_minterp(class CPhysicsObj* this) 005120c0 { 005120cb if (this->movement_manager == 0) 005120cb { 005120d5 class MovementManager* eax_2 = MovementManager::Create(this, this->weenie_obj); 005120df this->movement_manager = eax_2; 005120e5 MovementManager::EnterDefaultState(eax_2); 005120e5 005120f1 if ((this->state & 1) == 0) 005120f1 { 005120f3 uint32_t transient_state = this->transient_state; 005120f3 005120fb if (transient_state >= 0) 005120fb { 00512109 this->update_time = (*(uint32_t*)Timer::cur_time); 0051210f *(uint32_t*)((char*)this->update_time)[4] = *(int32_t*)((char*)Timer::cur_time + 4); 00512109 } 005120fb 0051211a this->transient_state = (transient_state | 0x80); 005120f1 } 005120cb } 005120cb 00512127 /* tailcall */ 00512127 return MovementManager::get_minterp(this->movement_manager); 005120c0 } ``` Same lazy-create-and-stamp idiom, but this path DOES call `MovementManager::EnterDefaultState` right after `Create` (unlike `unpack_movement`'s seam above, which skips it). Confirms `CPhysicsObj` has (at least) two independent lazy-construction call sites for `movement_manager`, each with a slightly different post-create step — `unpack_movement` skips `EnterDefaultState` (presumably because `unpack_movement` itself, or the subsequent `MovementManager::unpack_movement` call, drives the interpreter into the right state via `move_to_interpreted_state`/`PerformMovement`'s own lazy-create+enter-default-state guards), while `get_minterp` needs the interpreter immediately ready to answer a query and so forces `EnterDefaultState` explicitly. **No standalone `MovementManager::SetPhysicsObject` exists anywhere in the corpus** — grep for the literal symbol returned zero matches. `physics_obj` is set exactly once, inside `MovementManager::Create`, at construction time, and never reassigned.