acdream/docs/research/2026-07-03-r5-managers/r5-movementmanager-decomp.md
Erik 3d89446d98 feat(physics): R5-V1 — port PositionManager/Sticky/Constraint + TargetManager (Core, unwired)
The retail movement-manager family the R4 MoveToManager port left as
do-not-invent seams (decomp §9f/§9g). Faithful C# ports of retail's
PositionManager facade + StickyManager + ConstraintManager + the
TargetManager voyeur system, with full conformance tests. NO wiring yet
— purely additive, no behavior change. Wiring (retiring TS-39 sticky +
AP-79 target adapter) is R5-V2/V3.

New Core classes (src/AcDream.Core/Physics/Motion/):
- StickyManager (0x00555400): follow-a-target steering. adjust_offset's
  dense x87 mush decoded via ACE (StickyRadius 0.3, StickyTime 1.0,
  follow speed ×5 / fallback 15) — speed-clamped signed-distance steer +
  bounded turn-to-face; 1 s watchdog; Ok→initialized / non-Ok→teardown.
- ConstraintManager (0x00556090): the server-position rubber-band leash.
  90% IsFullyConstrained jump gate + grounded linear brake taper.
  Structural only — acdream never ARMS it (retail arms from
  SmartBox::HandleReceivedPosition, which acdream lacks, with two x87
  constants BN elided). IsFullyConstrained stays false = TS-35 behavior;
  leash-arming + the unknown constants are a deferred issue.
- PositionManager facade (0x00555160): lazy Sticky/Constraint + fan-out.
- TargetManager (0x0051a370) + TargettedVoyeurInfo: the peer-to-peer
  voyeur subscription system (0.5 s throttle, 10 s staleness,
  send-on-drift-past-radius, dead-reckon GetInterpolatedPosition). A
  faithful superset of the AP-79 adapter — SetTarget subscribes ON the
  target; the target's HandleTargetting pushes updates back.
- IPhysicsObjHost: the CPhysicsObj back-pointer seam (position/velocity/
  radius/contact/GetObjectA + target-tracking fan-out) the App wires per
  entity in V2/V3. MotionDeltaFrame: mutable retail-Frame delta accumulator.

Supporting:
- TargetInfo extended to the full retail 10-field struct (additive
  defaults keep the R4 4-arg call sites compiling).
- MoveToMath: signed CylinderDistanceNoZ, NormalizeCheckSmall,
  GlobalToLocalVec.
- Rename: the misnamed AcDream.Core.Physics.PositionManager (a remote
  anim+interp per-frame combiner, NOT the retail facade) → RemoteMotion
  Combiner, freeing the name and removing the ambiguity that breaks every
  file importing both Physics + Physics.Motion (GameWindow will in V2/V3).

Tests: 42 new conformance cases (Sticky/Constraint/Position facade +
TargetManager incl. the full cross-entity voyeur round-trip). Full suite
4006 green (+2 skipped), no regressions.

Decomp + ACE cross-ref + port plan: docs/research/2026-07-03-r5-managers/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:34:49 +02:00

44 KiB

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 */)

/* 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)

/* 3468 */
struct __cppobj PositionManager
{
  InterpolationManager *interpolation_manager;
  StickyManager *sticky_manager;
  ConstraintManager *constraint_manager;
  CPhysicsObj *physics_obj;
};

Ownership: CPhysicsObj fields (acclient.h ~line 30715-30717)

  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

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

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.

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

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

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

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

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

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

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)

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

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 CancelMoveToarg2 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

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

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

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

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

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

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)

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)

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

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

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)

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.