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

1015 lines
44 KiB
Markdown

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