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>
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):
+0x0motion_interpreter+0x4moveto_manager+0x8physics_obj+0xcweenie_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 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
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):
- Entire function is a no-op (falls through to
return 0) unlessthis->motion_interpreter != 0ANDthis->physics_obj != 0. This meansunpack_movementrequires the interpreter to already exist — unlike every OTHER MovementManager method, this one does NOT lazily constructmotion_interpreteron demand. If the interpreter hasn't been created yet (e.g. viaEnterDefaultState/PerformMovement/get_minterp), an inbound network movement packet is silently dropped (returns 0, meaning presumably "0 bytes consumed" or "not handled" to the caller). - On the happy path:
CPhysicsObj::interrupt_current_movement(physics_obj)thenCPhysicsObj::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). - 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 aMovementParameters(var_9c) and anInterpretedMotionState(var_28) as scratch locals for the switch below. - Reads a 16-bit header word
ebp_1from the wire (*(uint16_t*)eax_1, advancesarg2by 2) — this is themt(movement-type) value the switch dispatches on. A SECOND 16-bit valueecx_4is read right after (advancesarg2by another 2) and used as an index into acommand_ids[]table to get a motion-command idecx_5; if that differs from the interpreter's current pin/version (CBaseFilter::GetPinVersion— BN mislabel, see NOTE below), it callsCMotionInterp::DoMotion(interp, ecx_5, &var_9c). This happens unconditionally before the switch, for every movement type. switch (ebp_1)dispatches on the FULL 16-bit header word, not a masked/shifted sub-field — cases match0,6,7,8,9literally. 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 thecase 0body (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 at0x524748, which is the shared post-switch cleanup +return 0... actually the decompiled text shows those return 0 via falling out of the switch toInterpretedMotionState::~InterpretedMotionState(&var_28)thenreturn 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 ofvar_a4_1(the ORIGINAL 16-bit header value, stored earlier asint16_t var_a4_1 = ecx_4whereecx_4was the first 16-bit read == same value asebp_1) and tests bit 0 of that HIGH byte — i.e. bit 8 of the 16-bit header, which is0x100. This is the "sticky guid" bit the task asked about:mt & 0x100gates whether an extrauint32_tobject-guid is read off the wire right afterInterpretedMotionState::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 achar*and indexing) — this is Binary Ninja's clumsy way of expressing "take the address of the localvar_a4_1, then read byte offset 1 of it" (i.e. the high byte of the 16-bitshort, 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 "bit0x100of 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 knowmotion_interpreter != 0at 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) intoCMotionInterp::standing_longjumpon 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)— ensuresmoveto_managerexists.- reads a target-object guid (
ebx_4) off the wire, thenPosition::UnPackOrigin(&var_70, ...), thenMovementParameters::UnPackNet(&var_9c, MoveToObject, arg2, arg3)(the network-unpack overload, taking aMovementType/context enumMoveToObjectas a literal tag), then reads a floatmy_run_rateoff the wire (via x87 float-load pattern,long doubleround-trip) and stores it onmotion_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 sharedMoveToPositiontail (reusing the just-unpackedvar_70/var_9cas 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, ...), readsmy_run_ratefloat, falls straight intolabel_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 guidebx_6+ an extra dwordecx_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 sharedlabel_524725tail (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 sharedlabel_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)thenreturn 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.