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>
1015 lines
44 KiB
Markdown
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.
|