# R4 — MoveToManager: verbatim decomp extraction Source: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (line numbers = file line numbers, not addresses). Structs: `docs/research/named-retail/acclient.h`. Style/pin conventions follow `docs/research/2026-07-02-r3-motioninterp/r3-motioninterp-decomp.md`; the MovementParameters bit masks are the R3 **A4 pin** (`W0-pins.md`) and are used as-is here. ACE cross-ref: `references/ACE/Source/ACE.Server/Physics/Managers/MoveToManager.cs` (interpretation aid only; decomp wins). StickyManager / ConstraintManager bodies are **out of scope (R5)** — only their call shapes from MoveToManager appear here. Coverage: every `MoveToManager::` member in the raw (grep enumerated 33 members, all extracted below), the MovementManager relay/dispatch, the MovementParameters command-selection family, and the CPhysicsObj-side entry points. --- ## 0. Key structs (acclient.h) ```c /* acclient.h:31473 — struct #3462 */ struct __cppobj __declspec(align(8)) MoveToManager { MovementTypes::Type movement_type; // +0x00 Position sought_position; // +0x04 (Position = vtable+objcell_id+Frame = 0x48) Position current_target_position; // +0x4C Position starting_position; // +0x94 MovementParameters movement_params; // +0xDC (bitfield +0xE0, distance_to_object +0xE4, // min_distance +0xE8, desired_heading +0xEC, // speed +0xF0, fail_distance +0xF4, // walk_run_threshhold +0xF8) float previous_heading; // +0x108 float previous_distance; // +0x10C long double previous_distance_time; // +0x110 float original_distance; // +0x118 long double original_distance_time; // +0x120 unsigned int fail_progress_count; // +0x128 unsigned int sought_object_id; // +0x12C unsigned int top_level_object_id; // +0x130 float sought_object_radius; // +0x134 float sought_object_height; // +0x138 unsigned int current_command; // +0x13C unsigned int aux_command; // +0x140 int moving_away; // +0x144 int initialized; // +0x148 DLList pending_actions; // +0x14C head, +0x150 tail CPhysicsObj *physics_obj; // +0x154 (confirmed: Create writes +0x154) CWeenieObject *weenie_obj; // +0x158 (confirmed: Create writes +0x158) }; // sizeof = 0x160 (operator new(0x160) in Create) /* acclient.h:57702 — struct #6350; alloc sites use operator new(0x10) */ struct __cppobj MoveToManager::MovementNode : DLListData { // +0 dllist_next, +4 dllist_prev (DLListData) MovementTypes::Type type; // +8 — only 7 (MoveToPosition) and 9 (TurnToHeading) ever queued float heading; // +0xc — only meaningful for type 9 }; /* acclient.h:2856 — enum #229 */ enum MovementTypes::Type { Invalid=0, RawCommand=1, InterpretedCommand=2, StopRawCommand=3, StopInterpretedCommand=4, StopCompletely=5, MoveToObject=6, MoveToPosition=7, TurnToObject=8, TurnToHeading=9 }; /* acclient.h:38069 — struct #4067 */ struct __cppobj MovementStruct { MovementTypes::Type type; unsigned int motion; // types 1-4 only unsigned int object_id; // types 6, 8 unsigned int top_level_id; // types 6, 8 Position pos; // type 7 float radius; // type 6 float height; // type 6 MovementParameters *params; // types 1-4, 6-9 }; /* acclient.h:31453 + bitfield 31423-31443 — the A4 pin masks apply: 0x1 can_walk / 0x2 can_run / 0x4 can_sidestep / 0x8 can_walk_backwards / 0x10 can_charge / 0x20 fail_walk / 0x40 use_final_heading / 0x80 sticky / 0x100 move_away / 0x200 move_towards / 0x400 use_spheres / 0x800 set_hold_key / 0x1000 autonomous / 0x2000 modify_raw_state / 0x4000 modify_interpreted_state / 0x8000 cancel_moveto / 0x10000 stop_completely / 0x20000 disable_jump_during_link. Ctor default 0x1EE0F; distance_to_object=0.6, min_distance=0, fail_distance=FLT_MAX, desired_heading=0, speed=1, walk_run_threshhold=15, hold_key=HoldKey_Invalid. */ /* acclient.h:31591 — struct #3482 */ struct __cppobj TargetInfo { unsigned int context_id; unsigned int object_id; float radius; long double quantum; Position target_position; Position interpolated_position; AC1Legacy::Vector3 interpolated_heading; AC1Legacy::Vector3 velocity; TargetStatus status; long double last_update_time; }; /* acclient.h:7264 */ enum TargetStatus { Undef=0, Ok_TargetStatus=1, ExitWorld=2, Teleported=3, Contained=4, Parented=5, TimedOut=6 }; /* acclient.h:3394 */ enum HoldKey { HoldKey_Invalid=0, HoldKey_None=1, HoldKey_Run=2 }; ``` Motion-command literals used throughout (cross-ref ACE `MotionCommand`): `0x45000005` WalkForward, `0x45000006` WalkBackwards, `0x44000007` RunForward, `0x6500000d` TurnRight, `0x6500000e` TurnLeft. --- ## 1. Creation / lifecycle ### 1a. `MoveToManager::MoveToManager` — `005293b0` @ 306554-306593 Verbatim (member-init noise elided): default-constructs `sought_position`, `current_target_position`, `starting_position` (identity Frame, objcell_id 0, `Frame::cache` each), `MovementParameters::MovementParameters(&this->movement_params)`, `pending_actions.head_ = 0; tail_ = 0; physics_obj = 0; weenie_obj = 0;` then `MoveToManager::InitializeLocalVariables(this)` (@306592). ### 1b. `MoveToManager::Create` — `00529470` @ 306597-306614 ```c 00529475 void* eax = operator new(0x160); 0052947f if (eax == 0) return 0; 00529483 void* result = MoveToManager::MoveToManager(eax); 0052948a if (result != 0) { 00529494 *(uint32_t*)((char*)result + 0x154) = arg1; // physics_obj 0052949a *(uint32_t*)((char*)result + 0x158) = arg2; // weenie_obj 0052948a } 005294a0 return result; ``` Note: unlike `MovementManager::Create`, this writes the pointers DIRECTLY (no `SetPhysicsObject`/`SetWeenieObject` virtual-ish path). ### 1c. `MoveToManager::InitializeLocalVariables` — `00529250` @ 306490-306534 ```c 00529259 this->movement_type = 0; // Invalid 0052925b this->movement_params.__inner0 = 0; // bitfield CLEARED (not re-defaulted!) 00529261 this->movement_params.context_id = 0; 0052926c this->previous_distance = 3.40282347e+38f; // FLT_MAX 0052927e this->previous_distance_time = Timer::cur_time; // [BN renders the 8-byte double copy // as two 32-bit halves @306497-306498] 0052928a this->original_distance = 3.40282347e+38f; 005292a5 this->original_distance_time = Timer::cur_time; 005292ab this->previous_heading = 0f; 005292b1 this->fail_progress_count = 0; 005292b7 this->current_command = 0; 005292bd this->aux_command = 0; 005292c3 this->moving_away = 0; 005292c9 this->initialized = 0; /* @306508-306529: sought_position and current_target_position reset to objcell_id 0 + identity Frame (starting_position NOT reset) */ 0052935d this->sought_object_id = 0; 00529363 this->top_level_object_id = 0; 00529369 this->sought_object_radius = 0f; 0052936f this->sought_object_height = 0f; ``` Cleaned intent: full soft-reset. Only `movement_params.bitfield` + `context_id` are zeroed of the params (the float fields keep stale values — every entry point copies all ten fields anyway). `pending_actions` is NOT drained here (callers drain first). ### 1d. `MoveToManager::Destroy` — `005294b0` @ 306618-306663 Drains `pending_actions` with the inlined DLList unlink+`operator delete` loop (@306621-306659, byte-identical in shape to the drain in `CancelMoveTo` §7c), then tailcalls `InitializeLocalVariables` (@306662). ### 1e. `MoveToManager::~MoveToManager` — `005299d0` @ 306945-306953 `Destroy(this)` then repoints the four Position/PackObj vtables (`0x79285c`) — pure dtor bookkeeping. ### 1f. `SetWeenieObject` `00529230` @ 306474-306478 / `SetPhysicsObject` `00529240` @ 306482-306486 Plain field stores. **No side effects** (unlike CMotionInterp's versions, which re-dispatch movement — R3 A3). ### 1g. `MoveToManager::is_moving_to` — `00529220` @ 306464-306470 ```c 00529226 result = this->movement_type != Invalid; ``` --- ## 2. MovementManager relay layer ### 2a. `MovementManager::MakeMoveToManager` — `00524000` @ 300124-300129 ```c 00524008 if (this->moveto_manager == 0) 0052401a this->moveto_manager = MoveToManager::Create(this->physics_obj, this->weenie_obj); ``` ### 2b. `MovementManager::PerformMovement` — `005240d0` @ 300194-300237 ```c 005240d9 CPhysicsObj::set_active(this->physics_obj, 1); 005240e4 void* eax_1 = (arg2->type - 1); 005240e8 if (eax_1 > 8) return 0x47; 005240f1 switch (eax_1) { case 0..4: // RawCommand, InterpretedCommand, StopRawCommand, // StopInterpretedCommand, StopCompletely 005240fb if (this->motion_interpreter == 0) { 00524105 eax_3 = CMotionInterp::Create(this->physics_obj, this->weenie_obj); 00524112 this->motion_interpreter = eax_3; 00524114 if (physics_obj != 0) CMotionInterp::enter_default_state(eax_3); 005240fb } 00524127 return CMotionInterp::PerformMovement(this->motion_interpreter, arg2); case 5..8: // MoveToObject(6), MoveToPosition(7), TurnToObject(8), TurnToHeading(9) 0052412f if (this->moveto_manager == 0) 00524141 this->moveto_manager = MoveToManager::Create(this->physics_obj, this->weenie_obj); 00524148 MoveToManager::PerformMovement(this->moveto_manager, arg2); 0052414f return 0; } ``` The lookup table @300244-300255 confirms the 1-5 → interp / 6-9 → moveto split. Type 0 (Invalid) and >9 return `0x47`. **Moveto types always return 0** — errors surface via `CancelMoveTo`, not the return value. ### 2c. `MovementManager::CancelMoveTo` — `005241b0` @ 300277-300288 ```c 005241b0 class MoveToManager* moveto_manager = this->moveto_manager; 005241b5 if (moveto_manager == 0) return; 005241b7 uint32_t edx; 005241b7 return MoveToManager::CancelMoveTo(moveto_manager, edx); // [BN uninit-edx relay // artifact — A7 family; // arg2 (WeenieError) is // passed through] ``` ### 2d. Per-tick / event forwarders - `MovementManager::UseTime` `005242f0` @ 300411-300421: `if (moveto_manager) tailcall MoveToManager::UseTime`. **CMotionInterp is NOT ticked here** — UseTime is moveto-only. - `MovementManager::HitGround` `00524300` @ 300425-300440: `minterp->HitGround()` then `tailcall MoveToManager::HitGround(moveto_manager)`. - `MovementManager::HandleUpdateTarget` `00524790` @ 300723-300730: `if (moveto_manager) MoveToManager::HandleUpdateTarget(moveto_manager, &arg2)`. **Confirms the R3 negative result: HandleUpdateTarget lives on MoveToManager** (§6 below). - `MovementManager::IsMovingTo` `00524260` @ 300352-300361: `moveto_manager && MoveToManager::is_moving_to(...)`. - `MovementManager::Destroy` `005243f0` @ 300538-300559: deletes both minterp (`~CMotionInterp` + `operator delete`) and moveto (`~MoveToManager` + `operator delete`). - `MovementManager::SetWeenieObject` `00524020` @ 300133-300146: forwards to both members. ### 2e. ⚠ COMDAT-folded no-ops (BN symbol-collision mislabels) Three forwarders tailcall `IDClass<_tagDataID,32,0>::~IDClass` — the TRIVIAL EMPTY function; the linker's identical-COMDAT folding collapsed several empty methods into one body and the PDB picked the IDClass dtor as the canonical name: - `MovementManager::LeaveGround` `00524320` @ 300444-300459: `minterp->LeaveGround()` then "tailcall IDClass dtor(moveto_manager)" → **`MoveToManager::LeaveGround` is a no-op in this build.** - `MovementManager::ReportExhaustion` `00524360` @ 300491-300506: `minterp->ReportExhaustion()` then the same folded no-op on moveto_manager → **`MoveToManager::ReportExhaustion` is a no-op.** - `MovementManager::HandleEnterWorld` `00524340` @ 300463-300473: folded no-op on motion_interpreter → **`CMotionInterp::HandleEnterWorld` is a no-op** (HandleExitWorld @300477-300487 calls the real `CMotionInterp::HandleExitWorld`). Port: MoveToManager needs no LeaveGround/ReportExhaustion members; do not invent behavior for them. ### 2f. `MovementManager::unpack_movement` — `00524440` @ 300563-300719 (the wire entry) The client-side parse of the server movement event. Head (@300566-300598): requires `motion_interpreter != 0 && physics_obj != 0`; then `CPhysicsObj::interrupt_current_movement` + `CPhysicsObj::unstick_from_object`; reads uint16 `type` header + uint16 style index; `ecx_5 = command_ids[style]`; if the minterp's current style differs (@300597 — `CBaseFilter::GetPinVersion(minterp)` is a **mislabeled trivial getter** of the interp's current style, symbol-collision artifact) → `CMotionInterp::DoMotion(minterp, style, &defaultParams)`. Then per type: ```c case 0: // full InterpretedMotionState 00524551 InterpretedMotionState::UnPack(&var_28, arg2, arg3); 0052455d if (header & 0x100) sticky_object_guid = read_dword(); // [flag bit via var_a4 byte1&1] 0052457c MovementManager::move_to_interpreted_state(this, &var_28); 00524583 if (sticky_object_guid) CPhysicsObj::stick_to_object(this->physics_obj, guid); 0052458e this->motion_interpreter->standing_longjump = (header & 0x200); case 6: // MoveToObject 005245b3 MovementManager::MakeMoveToManager(this); 005245ba object_id = read_dword(); 005245ce Position::UnPackOrigin(&var_70, ...); // target origin 005245db MovementParameters::UnPackNet(&var_9c, MoveToObject, ...); 005245e9 this->motion_interpreter->my_run_rate = read_float(); 005245f9 if (CPhysicsObj::GetObjectA(object_id) == 0) goto label_524668; // object not // resolvable → 00524604 CPhysicsObj::MoveToObject(this->physics_obj, object_id, &var_9c); // else full path case 7: // MoveToPosition 00524629 MakeMoveToManager; UnPackOrigin(&var_70); UnPackNet(MoveToPosition); my_run_rate = read_float(); 00524668 label_524668: 00524668 MoveToManager::MoveToPosition(this->moveto_manager, &var_70, &var_9c); // ^ the case-6 fallback DEGRADES MoveToObject to MoveToPosition(wire origin) case 8: // TurnToObject 0052468d MakeMoveToManager; object_id = read_dword(); wire_heading = read_dword(); 005246b5 UnPackNet(&var_9c, TurnToObject, ...); 005246c5 if (GetObjectA(object_id) == 0) { var_9c.desired_heading = wire_heading; // @300676: goto label_524725; } // fallback → TurnToHeading 005246d0 CPhysicsObj::TurnToObject(this->physics_obj, object_id, &var_9c); case 9: // TurnToHeading 00524704 MakeMoveToManager; UnPackNet(&var_9c, TurnToHeading, ...); 00524725 label_524725: 00524725 MoveToManager::TurnToHeading(this->moveto_manager, &var_9c); ``` (Return 1 on every handled case, 0 otherwise; jump table @300707-300719.) ### 2g. `MovementParameters::UnPackNet` — `0052ac50` @ 308118-308205 (wire field order) - MoveToObject / MoveToPosition (0x1c bytes, 7 dwords): `bitfield, distance_to_object, min_distance, fail_distance, speed, walk_run_threshhold, desired_heading`. - TurnToObject / TurnToHeading (0xc bytes, 3 dwords): `bitfield, speed, desired_heading`. - Types outside 6-9 → return 0. **The wire bitfield carries can_charge 0x10** — this is the walk-vs-run answer consumed in §5c (cross-ref `feedback_autowalk_cancharge_bit`). (`Pack`/`UnPack` @308037-308114 are the full 0x28-byte 10-field forms — dat/persist serialization, order: bitfield, distance_to_object, min_distance, fail_distance, desired_heading, speed, walk_run_threshhold, context_id, hold_key, action_stamp.) --- ## 3. Entry points (movement_type setters) ### 3a. `MoveToManager::PerformMovement` — `0052a900` @ 307871-307904 ```c 0052a901 int32_t var_8 = 0x36; // WeenieError.ActionCancelled arg setup 0052a905 MoveToManager::CancelMoveTo(this, edx); // [uninit-edx artifact; 0x36 is the arg] 0052a910 CPhysicsObj::unstick_from_object(this->physics_obj); // → PositionManager::UnStick 0052a923 switch (arg2->type - 6) { 0052a940 case 0: MoveToManager::MoveToObject(this, arg2->object_id, arg2->top_level_id, arg2->radius, arg2->height, arg2->params); break; 0052a955 case 1: MoveToManager::MoveToPosition(this, &arg2->pos, arg2->params); break; 0052a96e case 2: MoveToManager::TurnToObject(this, arg2->object_id, arg2->top_level_id, arg2->params); break; 0052a97f case 3: MoveToManager::TurnToHeading(this, arg2->params); break; } 0052a987 return 0; ``` Every new moveto CANCELS the previous one (0x36) and UNSTICKS first. ### 3b. `MoveToManager::MoveToObject` — `00529680` @ 306756-306817 ```c 0052968e if (physics_obj != 0) { 005296cc CPhysicsObj::StopCompletely(physics_obj, edx); 005296e0 this->starting_position = physics_obj->m_position; // objcell + Frame copy 005296fe this->sought_object_id = arg2; 00529708 this->sought_object_radius = arg4; 00529712 this->sought_object_height = arg5; 00529718 this->movement_type = 6; // MoveToObject 0052971e this->top_level_object_id = arg3; 00529727-306783 this->movement_params = *arg6; // all 10 fields copied individually 00529784 this->initialized = 0; 00529791 if (arg3 == physics_obj->id) { // targeting SELF 00529795 MoveToManager::CleanUp(this); 0052979a if (physics_obj) CPhysicsObj::StopCompletely(physics_obj, edx); // label_52979a 00529791 } else { 005297c4 CPhysicsObj::set_target(physics_obj, 0, this->top_level_object_id, 0.5f, 0f); // (context_id=0, target=top_level, radius=0.5, quantum=0.0) } } // physics_obj == 0 path (@306798-306807): dead frame-cache noise, falls into // label_52979a (StopCompletely if obj appeared) — effectively a no-op guard. ``` **No nodes are queued yet** — object moves are deferred until the first `HandleUpdateTarget` callback delivers the target's position (§6). Note the wire's `MovementParameters.sticky` (0x80) is preserved here (unlike position moves, §3c). ### 3c. `MoveToManager::MoveToPosition` — `0052a240` @ 307521-307593 ```c 0052a24e if (physics_obj_1 != 0) { 0052a259 CPhysicsObj::StopCompletely(physics_obj_1, edx); 0052a26c this->current_target_position = *arg2; 0052a276 this->sought_object_radius = 0f; 0052a280 MoveToManager::GetCurrentDistance(this); // → curr_distance (x87) 0052a292 heading_diff = Position::heading(&myPos, arg2) - CPhysicsObj::get_heading(); 0052a2ba if (fabs(heading_diff) < 0.000199999995f) heading_diff = 0; // @307547 0052a2d5 if (heading_diff < -0.000199999995f) heading_diff += 360f; // @307554 0052a304 MovementParameters::get_command(arg3, curr_distance, heading_diff, &cmd, &holdKey, &movingAway); 0052a30f if (cmd != 0) { 0052a31b MoveToManager::AddTurnToHeadingNode(this, Position::heading(&myPos, arg2)); 0052a332 MoveToManager::AddMoveToPositionNode(this); 0052a30f } 0052a33b if ((arg3->bitfield & 0x40) != 0) // use_final_heading 0052a343 MoveToManager::AddTurnToHeadingNode(this, arg3->desired_heading); 0052a34f this->sought_position = *arg2; 0052a366 this->starting_position = physics_obj->m_position; 0052a378 this->movement_type = 7; // MoveToPosition 0052a381-307589 this->movement_params = *arg3; // 10-field copy 0052a3e5 this->movement_params.__inner0 &= 0xffffff7f; // CLEAR sticky (0x80) 0052a3eb MoveToManager::BeginNextNode(this); ``` Node plan: `[TurnToHeading(face target)] → [MoveToPosition] → [TurnToHeading(final)]?`. If `get_command` says no motion needed (already inside distance_to_object), NO move nodes are queued — only the optional final-heading turn. Position moves and heading turns **force sticky off**; only object moves can stick (§4c). ### 3d. `MoveToManager::TurnToObject` — `005297d0` @ 306820-306882 ```c 005297de if (physics_obj != 0) { 0052984c if ((arg4->bitfield & 0x10000) != 0) // stop_completely 00529850 CPhysicsObj::StopCompletely(physics_obj, edx); 00529859 this->movement_type = 8; // TurnToObject 0052985f this->sought_object_id = arg2; 0052986c Frame::set_heading(&this->current_target_position.frame, arg4->desired_heading); // ^ seeds CURRENT_TARGET's heading — but TurnToObject_Internal later // OVERWRITES current_target_position wholesale and reads // SOUGHT_position's heading (§6c). ACE MoveToManager.cs:246 matches // this verbatim (also writes CurrentTargetPosition) — retail quirk, // NOT a BN artifact: desired_heading is effectively DISCARDED for // TurnToObject; the final heading = heading-to-target + sought.frame // heading (0 after InitializeLocalVariables). 00529875 this->top_level_object_id = arg3; 0052987e-306847 this->movement_params = *arg4; // 10-field copy 005298d5 if (arg3 == physics_obj->id) { CleanUp; StopCompletely; } // self-target else { 0052990c this->initialized = 0; 00529916 CPhysicsObj::set_target(physics_obj_1, 0, arg3, 0.5f, 0f); } } // physics_obj == 0 path @306862-306872: stores context_id only + dead // frame noise, then StopCompletely-if-obj (label_5298e8). ``` Deferred like MoveToObject; `stop_completely` (0x10000) is honored HERE (conditional) where MoveToObject/MoveToPosition stop unconditionally. ### 3e. `MoveToManager::TurnToHeading` — `0052a630` @ 307706-307772 ```c 0052a641 if (physics_obj != 0) { 0052a6aa if ((arg2->bitfield & 0x10000) != 0) // stop_completely 0052a6ad CPhysicsObj::StopCompletely(physics_obj, edx); 0052a6b5-307729 this->movement_params = *arg2; // 10-field copy 0052a70c this->movement_params.__inner0 &= 0xffffff7f; // CLEAR sticky 0052a71d Frame::set_heading(&this->sought_position.frame, arg2->desired_heading); 0052a722 this->movement_type = 9; // TurnToHeading 0052a731 node = operator new(0x10); node->type(+8) = 9; node->heading(+0xc) = arg2->desired_heading; 0052a766 DLListBase::InsertAfter(&this->pending_actions, node, tail_); 0052a76d MoveToManager::BeginNextNode(this); 0052a641 } else { /* @307753-307770: context_id store + StopCompletely-if-obj noise */ } ``` Note `initialized` is NOT set for TurnToHeading/MoveToPosition — the UseTime gate (§6a) passes anyway because `top_level_object_id == 0` for non-object moves. --- ## 4. Node stepping ### 4a. Node factories `AddTurnToHeadingNode` `00529530` @ 306667-306685: `new(0x10)`, `+0xc = heading`, `+8 = 9`, `DLListBase::InsertAfter(&pending_actions, node, tail_)` (tail-append). `AddMoveToPositionNode` `00529580` @ 306689-306706: same with `+8 = 7`, no heading. `RemovePendingActionsHead` `00529380` @ 306538-306550: `DLListBase::Remove(head)` + `operator delete(head)` (several sites inline this same pattern instead of calling it). ### 4b. `MoveToManager::BeginNextNode` — `00529cb0` @ 307123-307171 ```c 00529cb6 head_ = this->pending_actions.head_; 00529cbe if (head_ != 0) { 00529cc0 type = *(int*)((char*)head_ + 8); 00529cc6 if (type == 7) return MoveToManager::BeginMoveForward(this); // tailcall 00529ccb if (type == 9) return MoveToManager::BeginTurnToHeading(this); // tailcall 00529ccb return; // unknown node type: stall (defensive) 00529cbe } // queue EMPTY → moveto complete: 00529ce5 if ((int8_t)(this->movement_params.bitfield & 0xff) < 0) { // byte0 sign = bit7 = STICKY 00529cef height = this->sought_object_height; radius = this->sought_object_radius; 00529d00 tlid = this->top_level_object_id; 00529d0c MoveToManager::CleanUp(this); 00529d19 if (physics_obj) CPhysicsObj::StopCompletely(physics_obj, edx); 00529d3a PositionManager::StickTo(CPhysicsObj::get_position_manager(this->physics_obj), tlid, radius, height); // ← R5 seam (call shape) 00529d44 return; 00529ced } 00529d47 MoveToManager::CleanUp(this); 00529d54 if (physics_obj) CPhysicsObj::StopCompletely(physics_obj, edx); ``` **This is the sticky handoff**: arrival at a sticky object move transfers control to StickyManager via `PositionManager::StickTo(top_level_object_id, sought_radius, sought_height)`. The radius/height/tlid are read BEFORE CleanUp zeroes them. ### 4c. `MoveToManager::BeginMoveForward` — `00529a00` @ 306957-307042 ```c 00529a0e if (physics_obj == 0) { CancelMoveTo(this, 8); return; } // [8 = NoPhysicsObject // via __saved_edi store] 00529a1d dist = MoveToManager::GetCurrentDistance(this); // [x87 return] 00529a3e heading = Position::heading(&myPos, ¤t_target_position) - get_heading(); 00529a5a if (fabs(heading) < 0.000199999995f) heading = 0; 00529a75 if (heading < -0.000199999995f) heading += 360f; // normalize [0,360) 00529aa4 MovementParameters::get_command(&this->movement_params, dist, heading, &cmd, &holdKey, &movingAway); 00529aaf if (cmd == 0) { // nothing to do (already in range) 00529ab1 pop+delete head node; // inlined RemovePendingActionsHead 00529ad6 MoveToManager::BeginNextNode(this); 00529ae0 return; 00529aaf } 00529ae5 MovementParameters localParams; // default-ctor 00529af4 localParams.hold_key_to_apply = holdKey; // [BN dead-store garble: the 00529b07 localParams.bitfield &= 0xffff7fff; // var_8/var_28 writes are stores 00529b0e localParams.speed = this->movement_params.speed; // into the stack params — // hoisted-register artifact. // 0xffff7fff clears 0x8000 // cancel_moveto so _DoMotion // doesn't cancel THIS moveto] 00529b12 err = MoveToManager::_DoMotion(this, cmd, &localParams); 00529b19 if (err != 0) { MoveToManager::CancelMoveTo(this, err); return; } 00529b35 this->current_command = cmd; 00529b3f this->moving_away = movingAway; 00529b45 this->movement_params.hold_key_to_apply = holdKey; // chosen key WRITTEN BACK 00529b4b this->previous_distance = dist; 00529b5d this->previous_distance_time = Timer::cur_time; 00529b69 this->original_distance = dist; 00529b7c this->original_distance_time = Timer::cur_time; ``` ### 4d. `MoveToManager::BeginTurnToHeading` — `00529b90` @ 307046-307120 ```c 00529bad if (head_ == 0 || physics_obj == 0) { CancelMoveTo(this, 8); return; } 00529bba if (CPhysicsObj::motions_pending(physics_obj) != 0) return; // wait for anims to drain 00529bc0 target_heading = node->heading; // head +0xc 00529be0 diff = heading_diff(target_heading, get_heading(), 0x6500000d /*TurnRight*/); // pick direction / early-out: 00529bee if (diff > 180f) { // [flag-test rendering @307074] 00529c19 if (diff + 0.000199999995f >= 360f) // effectively already there 00529c8f { RemovePendingActionsHead; BeginNextNode; return; } // label_529c8f 00529c28 turn = 0x6500000e; // TurnLeft 00529bf7 } else { 00529bf9 if (diff <= 0.000199999995f) // @307094: & 0x41 → ≤ eps 00529c04 goto label_529c8f; // done 00529c0a turn = 0x6500000d; // TurnRight 00529bf7 } 00529c31 MovementParameters localParams; // default; & 0xffff7fff (clear // cancel_moveto); speed + 00529c58 localParams.speed = movement_params.speed; // hold_key copied from 00529c5c localParams.hold_key = movement_params.hold_key_to_apply; // this->movement_params 00529c60 err = MoveToManager::_DoMotion(this, turn, &localParams); 00529c67 if (err != 0) { CancelMoveTo(this, err); return; } 00529c7b this->current_command = turn; 00529c82 this->previous_heading = diff; // NOTE: stores the REMAINING DIFF, not a heading // (HandleTurnToHeading's progress test then // compares heading against this seed on its // first tick — retail quirk, keep verbatim; // ACE matches: PreviousHeading = diff) ``` --- ## 5. Distance / progress / command-selection helpers ### 5a. `MoveToManager::GetCurrentDistance` — `005291b0` @ 306435-306460 ```c 005291bb if (physics_obj == 0) return; // [x87 return garbled to void] 005291ce if ((movement_params.bitfield & 0x400) == 0) // use_spheres NOT set 0052920f return Position::distance(&physics_obj->m_position, ¤t_target_position); // use_spheres: 005291d0 return Position::cylinder_distance(GetRadius(physics_obj), GetHeight(physics_obj), &physics_obj->m_position, this->sought_object_radius, this->sought_object_height, &this->current_target_position); // [BN garbles the x87 float plumbing; arg order per the PDB signature — // own radius/height from CPhysicsObj::GetRadius/GetHeight, target's from // the stored sought_object_* fields] ``` Object moves (which set sought radius/height and get `use_spheres` on the wire) use edge-to-edge cylinder distance; position moves use center distance. ### 5b. `MoveToManager::CheckProgressMade` — `005290f0` @ 306385-306431 ```c 005290f7 elapsed = Timer::cur_time - this->previous_distance_time; 00529102 if (elapsed <= 1.0) return 1; // [@306392 & 0x41: window < 1s → OK] 00529113 progress = moving_away ? (curr - previous_distance) : (previous_distance - curr); // @306397-306400 00529135 if (progress / elapsed >= 0.25f) { // [@306402-306406 flag test] 0052914a previous_distance = curr; previous_distance_time = Timer::cur_time; 00529175 total = moving_away ? (curr - original_distance) : (original_distance - curr); 0052918d total_rate = total / (Timer::cur_time - original_distance_time); 0052918f if (total_rate >= 0.25f) return 1; // @306419-306423 00529142 } 005291a1 return 0; // no progress ``` Cleaned intent (adjudicated with ACE `CheckProgressMade`, which matches): progress is only evaluated after a **1-second** window; requires BOTH the incremental rate (since last checkpoint) AND the overall rate (since the move began) to be **≥ 0.25 units/second** toward (or away from, when moving_away) the target. The incremental checkpoint only advances when the incremental rate passes. ### 5c. `MovementParameters::get_command` — `0052aa00` @ 307946-308012 — **walk-vs-run + command pick** ```c 0052aa03 flags = (int16_t)this->bitfield; // --- motion command + moving_away --- 0052aa09 if (flags & 0x200) { // move_towards 0052aa0e if ((flags & 0x100) == 0) goto label_52aa66; // not move_away → plain towards 0052aa26 MovementParameters::towards_and_away(this, dist, heading, &cmd, &movingAway); 0052aa09 } else if ((flags & 0x100) == 0) { // neither towards nor away 0052aa66 label_52aa66: // plain TOWARDS: 0052aa66 if (dist > this->distance_to_object) // [@307962 test ah,0x41 — BN's if-sense 0052aa84 { cmd = 0x45000005; movingAway = 0; } // is INVERTED in the text; adjudicated 0052aa74 else cmd = 0; // via towards_and_away's explicit twin // @307924 + ACE GetCommand] 0052aa30 } else { // move_away only: 0052aa32 if (dist < this->min_distance) // @307978 & 1 = C0 (explicit, reliable) 0052aa54 { cmd = 0x45000005; movingAway = 1; } // WalkForward, away (heading flips 0052aa44 else cmd = 0; // via get_desired_heading +180) 0052aa30 } // --- HoldKey (walk vs run) --- 0052aa90 flags = (int16_t)this->bitfield; 0052aa95 if ((flags & 0x10) == 0) { // can_charge NOT set: 0052aa99 if ((flags & 2) == 0) // can_run not set → 0052aac2 { *holdKey = HoldKey_None; return; }// always walk 0052aa9d if ((flags & 1) != 0) { // can_walk set: 0052aaa3 if ((dist - distance_to_object) <= walk_run_threshhold) // @308003 & 0x41 ≤ 0052aac2 { *holdKey = HoldKey_None; return; } // close → WALK 0052aa9d } 0052aa95 } 0052aab4 *holdKey = HoldKey_Run; // can_charge, or far, or walk-incapable ``` **THE walk-vs-run rule** (confirms `feedback_autowalk_cancharge_bit`): `HoldKey_Run` ⇐ `can_charge (0x10)` set, OR (`can_run` set AND (`can_walk` clear OR `dist - distance_to_object > walk_run_threshhold` [default 15 units])). `HoldKey_None` (walk) ⇐ no `can_run`, or walking-capable within threshold of target. The chosen key rides into `_DoMotion` via `localParams.hold_key_to_apply` and is written back to `movement_params.hold_key_to_apply` (§4c) — MoveToManager never touches `set_hold_run` directly. ### 5d. `MovementParameters::towards_and_away` — `0052a9a0` @ 307917-307942 ```c 0052a9a4 if (dist > this->distance_to_object) // @307924 & 0x41 == 0 → greater (explicit) 0052a9b6 { *cmd = 0x45000005; *movingAway = 0; return; } // WalkForward, towards 0052a9cc if ((dist - this->min_distance) < 0.000199999995f) // inside min band 0052a9e1 { *cmd = 0x45000006; *movingAway = 1; } // WalkBackwards, away 0052a9d7 else *cmd = 0; // inside [min, dto] — idle // [@307932-307937: BN's if(p)-sense inverted in the text; the cleaned // branch assignment above is adjudicated by ACE TowardsAndAway + the // physical meaning (back away only when too close)] ``` Note the asymmetry vs §5c: pure-away uses WalkForward+turn-around; towards_and_away backs up with WalkBackwards (no turn). ### 5e. `MovementParameters::get_desired_heading` — `0052aad0` @ 308016-308033 — ⚠ BN garble ```c 0052aae9 if ((arg2 == 0x44000007 || arg2 == 0x45000005)) // RunForward | WalkForward 0052ab00 result = arg3; 0052aaec else if (arg2 != 0x45000006) return (arg2 - 0x45000006); // ← x87-return garble 0052aaee else result = arg3; // WalkBackwards ``` The BN text is an x87 setcc-garbled float return (the `arg2 - 0x45000006` "return" is flag-synthesis nonsense — the R3 A5 artifact class). Cleaned intent (ACE `get_desired_heading`, matching the command grouping the BN text preserves): ``` WalkForward | RunForward : return moving_away ? 180.0f : 0.0f; WalkBackwards : return moving_away ? 0.0f : 180.0f; default : return 0.0f; ``` i.e. the heading OFFSET added to raw heading-to-target so an "away" walk faces away and an "away" backstep faces the target. **Port verify:** worth one Ghidra MCP decompile of `0x0052aad0` when a CodeBrowser is open (server was down this session) to confirm the two 180/0 constants — confidence high but the BN text alone doesn't show them. ### 5f. `heading_greater` — `00528f60` @ 306281-306323 (free function) ```c 00528f68 if (fabs(arg1 - arg2) > 180.0) // wrapped case: compare flipped 00528f81 greater = (arg2 > arg1); // [@306291 & 0x41 → not-≤ = >] 00528f77 else greater = (arg1 > arg2); 00528fa1 if (arg3 == 0x6500000d) return greater; // TurnRight: heading increases 00528fa7 return greater == 0; // TurnLeft: inverted ``` "Has the turn passed the target heading" — direction-aware, 360°-wrap-aware (cleaned reading; the two fcom branches @306291-306308 are explicit `& 0x41` patterns, reliable). ### 5g. `heading_diff` — `00528fb0` @ 306327-306347 (free function) ```c 00528fb4 d = arg1 - arg2; 00528fc9 if (fabs(d) <= 0.000199999995f) d = 0; // [@306335 & 0x41] 00528fe1 if (d < -0.000199999995f) d += 360f; // [@306338-306342 flag test] 00529009 return d; // [BN renders the x87 return as flag // synthesis vs eps — garble; callers // (§4d, §6d) consume it as the // normalized [0,360) diff] ``` `arg3` (the turn command) is UNUSED in this build's body — keep the parameter for signature parity (ACE keeps it too). --- ## 6. Per-tick drivers + target updates ### 6a. `MoveToManager::UseTime` — `0052a780` @ 307776-307798 — **the tick gate** ```c 0052a791 if (physics_obj != 0 && (physics_obj->transient_state & 1) != 0) { // CONTACT bit 0052a793 head_ = this->pending_actions.head_; 0052a7b4 if (head_ != 0 && ((this->top_level_object_id == 0 || this->movement_type == Invalid) || this->initialized != 0)) { 0052a7b6 type = *(int*)((char*)head_ + 8); 0052a7bc if (type == 7) return MoveToManager::HandleMoveToPosition(this); 0052a7c1 if (type == 9) return MoveToManager::HandleTurnToHeading(this); 0052a7b4 } 0052a791 } ``` Three gates: (1) grounded (`transient_state & 1` = CONTACT — no moveto progress mid-air; `HitGround` §6e resumes), (2) a node exists, (3) object-moves (`top_level_object_id != 0 && movement_type != Invalid`) must be `initialized` (i.e. the first target update arrived). ### 6b. `MoveToManager::HandleMoveToPosition` — `00529d80` @ 307187-307438 — the big per-tick driver ```c 00529d94 if (physics_obj == 0) { CancelMoveTo(this, 8); return; } 00529db5 curPos = physics_obj->m_position; // objcell + Frame stack copy 00529dc7 MovementParameters localParams; // default; speed = // movement_params.speed; // bitfield &= ~0x8000; // hold_key = movement_params. // hold_key_to_apply [same // dead-store garble as §4c] // ---- PHASE 1: aux turn steering (only when not animating) ---- 00529df5 if (CPhysicsObj::motions_pending(this->physics_obj) != 0) { 00529ef6 if (aux_command) { _StopMotion(aux_command, &localParams); aux_command = 0; } 00529df5 } else { 00529e03 heading = get_desired_heading(&movement_params, current_command, moving_away) + Position::heading(&curPos, ¤t_target_position); // @307225-307227 00529e2d if (heading >= 360f) heading -= 360f; 00529e4e diff = heading - get_heading(); 00529e68 if (fabs(diff) < 0.000199999995f) diff = 0; 00529e80 if (diff < -0.000199999995f) diff += 360f; 00529e8b if (diff <= 20f || diff >= 340f) { // deadband [@307255-307261] 00529ee3 if (aux_command) { _StopMotion(aux); aux_command = 0; } 00529ead } else { 00529eaf turn = (diff >= 180f) ? 0x6500000e /*TurnLeft*/ : 0x6500000d /*TurnRight*/; // [@307274-307280: BN default TurnRight + `if(p_3)` flip — the // test-sense is ambiguous in the text; adjudicated via ACE // HandleMoveToPosition (diff >= 180 → TurnLeft) + §4d's twin] 00529ecc if (turn != aux_command) { _DoMotion(turn, &localParams); aux_command = turn; } 00529ead } 00529df5 } // ---- PHASE 2: arrival / progress ---- 00529f15 dist = GetCurrentDistance(); 00529f2c if (CheckProgressMade(this, dist) == 0) { 0052a003 if (!CPhysicsObj::IsInterpolating(physics_obj) && !CPhysicsObj::motions_pending(physics_obj)) 0052a014 this->fail_progress_count += 1; // counter is WRITE-ONLY (§8) 00529f2c } else { 00529f3e this->fail_progress_count = 0; // arrival predicate [@307306-307331: fcomp garbles; away-branch @307310 // `& 1`=C0 is explicit; towards-branch if-sense inverted in BN text — // adjudicated via ACE + the away branch]: // arrived = moving_away ? (dist >= min_distance) // fcomp [esi+0xe8] // : (dist <= distance_to_object) // fcomp [esi+0xe4] if (!arrived) { 00529f63 startDist = Position::distance(&starting_position, &physics_obj->m_position); 00529f68 if (startDist > movement_params.fail_distance) // fcomp [esi+0xf4] 00529f7d MoveToManager::CancelMoveTo(this, 0x3d); // YouChargedTooFar } else { // label_529f94 00529f94 pop+delete head node; 00529fc5 _StopMotion(this->current_command, &localParams); 00529fd2 this->current_command = 0; 00529fd8 if (aux_command) { _StopMotion(aux); aux_command = 0; } 00529fef MoveToManager::BeginNextNode(this); } 00529f2c } // ---- PHASE 3: TargetManager quantum retune (object moves only) ---- 0052a029 if (this->top_level_object_id != 0 && this->movement_type != Invalid) { 0052a03a v = CPhysicsObj::get_velocity(this->physics_obj); speed = sqrt(v.x² + v.y² + v.z²); // @307380-307393 raw x87 0052a05f if (speed > 0.10000000000000001) { // double @data_7a6db0 0052a06c eta = dist / speed; // @307413-307415 0052a07e if (fabs(eta - CPhysicsObj::get_target_quantum(physics_obj)) >= 1.0) 0052a0a6 CPhysicsObj::set_target_quantum(this->physics_obj, eta); 0052a05f } 0052a029 } ``` Phase-3 cleaned intent (the x87 block @307378-307436 is heavily garbled; structure adjudicated against ACE, which matches operation-for-operation): while chasing an object, retune the TargetManager update quantum to the estimated time-to-arrival (distance/speed) whenever it drifts ≥1 s from the current quantum — faster target updates as you close in. **ACE-divergence trap:** ACE lines 444-446 add a `\ custom: sync for server ticrate` `set_heading` in the stop-aux path — retail has NO set_heading there; do not copy. ### 6c. `MoveToManager::HandleTurnToHeading` — `0052a0c0` @ 307442-307517 ```c 0052a0ce if (physics_obj == 0) { CancelMoveTo(this, 8); return; } 0052a0de cmd = this->current_command; 0052a0f0 if (cmd != 0x6500000e && cmd != 0x6500000d) // not currently turning 0052a0f4 { MoveToManager::BeginTurnToHeading(this); return; } 0052a0ff head_ = pending_actions.head_; 0052a10d heading = CPhysicsObj::get_heading(physics_obj); 0052a130 if (heading_greater(heading, head_->heading, cmd) != 0) { // PASSED the target 0052a13a this->fail_progress_count = 0; 0052a146 CPhysicsObj::set_heading(physics_obj, head_->heading, 1); // snap + send(1) 0052a14b pop+delete head node; 0052a16a MovementParameters localParams; // default; &= ~0x8000; hold_key copied 0052a195 _StopMotion(this->current_command, &localParams); 0052a19c this->current_command = 0; 0052a1a2 MoveToManager::BeginNextNode(this); 0052a1ae return; 0052a130 } 0052a1bc diff = heading_diff(heading, this->previous_heading, cmd); 0052a1cf if (diff < 180f && diff > 0.000199999995f) { // made rotational progress 0052a1e5 this->fail_progress_count = 0; 0052a1ef this->previous_heading = heading; 0052a1f9 return; 0052a1dc } 0052a206 this->previous_heading = heading; // no progress: 0052a213 if (!CPhysicsObj::IsInterpolating(physics_obj) && !CPhysicsObj::motions_pending(this->physics_obj)) 0052a224 this->fail_progress_count += 1; ``` ### 6d. `MoveToManager::HandleUpdateTarget` — `0052a7d0` @ 307802-307867 — **CONFIRMED here** (R3 open item closed) ```c 0052a7db if (physics_obj == 0) { CancelMoveTo(this, 8); return; } 0052a7f6 if (this->top_level_object_id != arg2->object_id) return; // not our target 0052a804 if (this->initialized == 0) { // ---- first callback: build the node plan ---- 0052a886 if (top_level_object_id == physics_obj->id) { // self-target 0052a88f sought_position = physics_obj->m_position; 0052a8aa current_target_position = physics_obj->m_position; 0052a8aa MoveToManager::CleanUpAndCallWeenie(this, 0); // instant success 0052a8b1 return; 0052a886 } 0052a8bb if (arg2->status != Ok_TargetStatus) 0052a8c1 { CancelMoveTo(this, 0x38); return; } // NoObject 0052a8d0 if (movement_type == MoveToObject) // 6 0052a8f1 MoveToManager::MoveToObject_Internal(this, &arg2->target_position, &arg2->interpolated_position); 0052a8d0 else if (movement_type == 8) // TurnToObject 0052a8dd MoveToManager::TurnToObject_Internal(this, &arg2->target_position); 0052a804 } else { // ---- retarget while running ---- 0052a80d if (arg2->status != Ok_TargetStatus) 0052a813 { CancelMoveTo(this, 0x37); return; } // ObjectGone 0052a820 if (this->movement_type == MoveToObject) { 0052a82d sought_position = arg2->interpolated_position; 0052a839 current_target_position = arg2->target_position; 0052a843 previous_distance = 3.40282347e+38f; // FLT_MAX 0052a855 previous_distance_time = Timer::cur_time; 0052a861 original_distance = 3.40282347e+38f; 0052a873 original_distance_time = Timer::cur_time; // progress reset 0052a820 } 0052a804 } ``` Note the retarget path updates positions + resets the progress clock but does NOT requeue nodes — the running MoveToPosition node keeps steering toward the moved `current_target_position` each tick (§6b Phase 1). TurnToObject gets NO retarget handling (heading was frozen at the initial callback). ### 6e. `MoveToManager::HitGround` — `00529d70` @ 307175-307183 ```c 00529d73 if (this->movement_type == Invalid) return; 00529d75 return MoveToManager::BeginNextNode(this); // tailcall — re-arm after landing ``` ### 6f. `MoveToManager::MoveToObject_Internal` — `0052a400` @ 307597-307663 ```c 0052a40e if (physics_obj == 0) { CancelMoveTo(this, 8); return; } 0052a42d this->sought_position = *arg3; // interpolated_position 0052a443 this->current_target_position = *arg2; // target_position 0052a455 iHeading = Position::heading(&physics_obj->m_position, arg3); // toward INTERPOLATED 0052a460 dist = GetCurrentDistance(); // vs current_target (cylinder if spheres) 0052a46f diff = iHeading - get_heading(); 0052a48c if (fabs(diff) < 0.000199999995f) diff = 0; 0052a4a7 if (diff < -0.000199999995f) diff += 360f; 0052a4d6 MovementParameters::get_command(&this->movement_params, dist, diff, &cmd, &holdKey, &movingAway); 0052a4e1 if (cmd != 0) { 0052a4ea MoveToManager::AddTurnToHeadingNode(this, iHeading); 0052a4f1 MoveToManager::AddMoveToPositionNode(this); 0052a4e1 } 0052a4fd if ((this->movement_params.bitfield & 0x40) != 0) { // use_final_heading 0052a503 final = iHeading + this->movement_params.desired_heading; 0052a513 if (final >= 360f) final -= 360f; // [@307653-307656] 0052a52f MoveToManager::AddTurnToHeadingNode(this, final); 0052a4fd } 0052a536 this->initialized = 1; 0052a540 MoveToManager::BeginNextNode(this); ``` Same node plan as MoveToPosition (§3c) but heading aims at the INTERPOLATED position and the final heading is RELATIVE (`heading-to-target + desired_heading`) instead of absolute. ### 6g. `MoveToManager::TurnToObject_Internal` — `0052a550` @ 307667-307702 ```c 0052a55b if (physics_obj == 0) { CancelMoveTo(this, 8); return; } 0052a571 this->current_target_position = *arg2; // target_position 0052a58d soughtHeading = Frame::get_heading(&this->sought_position.frame); 0052a59b targetHeading = Position::heading(&physics_obj->m_position, ¤t_target_position); 0052a5aa final = fmod(targetHeading + soughtHeading, 360.0); // j__CIfmod @307682; the BN // float plumbing @307680-307684 // is garbled — structure per // ACE TurnToObject_Internal // (matches call-for-call) 0052a5ba Frame::set_heading(&this->sought_position.frame, final); 0052a5c1 node = new(0x10); node->type = 9; node->heading = final; // inlined AddTurnToHeadingNode 0052a607 DLListBase::InsertAfter(&pending_actions, node, tail_); 0052a60e this->initialized = 1; 0052a618 MoveToManager::BeginNextNode(this); ``` With §3d's quirk, `soughtHeading` is 0 for a fresh TurnToObject (sought was reset by `InitializeLocalVariables`; `desired_heading` went to current_target's frame and was overwritten here) → the final heading is simply "face the object". ACE matches verbatim (MoveToManager.cs:246 + 271-276). --- ## 7. Motion issuing + cleanup family ### 7a. `MoveToManager::_DoMotion` — `00529010` @ 306351-306364 ```c 0052901b if (physics_obj == 0) return 8; // NoPhysicsObject 0052902d if (CPhysicsObj::get_minterp(physics_obj) == 0) return 0xb; // NoMotionInterpreter 00529057 CMotionInterp::adjust_motion(get_minterp(physics_obj), &arg2, &arg3->speed, arg3->hold_key_to_apply); 00529076 return CMotionInterp::DoInterpretedMotion(get_minterp(physics_obj), arg2, arg3); ``` ### 7b. `MoveToManager::_StopMotion` — `00529080` @ 306368-306381 Identical shape; tail is `CMotionInterp::StopInterpretedMotion(minterp, arg2, arg3)`. **This is the entire CMotionInterp seam**: MoveToManager only ever issues `adjust_motion` → `DoInterpretedMotion` / `StopInterpretedMotion`. `adjust_motion` (already ported, R3) applies the hold key: WalkForward + HoldKey_Run → RunForward + speed scale. It never calls `DoMotion`, `set_hold_run`, or raw-state APIs directly. ### 7c. `MoveToManager::CancelMoveTo` — `00529930` @ 306886-306940 ```c 0052993a if (this->movement_type != Invalid) { 00529946 while (pending_actions.head_ != 0) { /* inlined unlink + operator delete, @306891-306929 */ } 005299b5 MoveToManager::CleanUp(this); 005299c2 if (physics_obj) CPhysicsObj::StopCompletely(physics_obj, edx); 0052993a } ``` Signature is `(this, uint32_t arg2)` — **arg2 (the WeenieError) is NEVER READ in the body**. Every call site stores a code into a stack slot right before the call (`8`, `0x36`, `0x37`, `0x38`, `0x3d`) which BN disconnects into dead stores + uninit-edx (A7 artifact family). In this client build the code is dropped (the weenie-callback plumbing of the original source is compiled out). Port: keep the `WeenieError` parameter for parity/logging; the retail-visible behavior is error-independent. Codes seen (ACE names): `8` NoPhysicsObject, `0x36` ActionCancelled, `0x37` ObjectGone, `0x38` NoObject, `0x3d` YouChargedTooFar. ### 7d. `MoveToManager::CleanUp` — `005295c0` @ 306710-306736 ```c 005295ca MovementParameters localParams; // default ctor 005295d9 localParams.hold_key_to_apply = this->movement_params.hold_key_to_apply; 005295eb localParams.bitfield &= 0xffff7fff; // clear cancel_moveto [dead-store garble] 005295ef if (this->physics_obj != 0) { 005295f9 if (current_command) _StopMotion(this, current_command, &localParams); 00529610 if (aux_command) _StopMotion(this, aux_command, &localParams); 0052962c if (this->top_level_object_id != 0 && this->movement_type != Invalid) 00529634 CPhysicsObj::clear_target(this->physics_obj); // → TargetManager::ClearTarget 005295ef } 0052963b MoveToManager::InitializeLocalVariables(this); ``` Does NOT drain `pending_actions` (CancelMoveTo/Destroy do that first). ### 7e. `MoveToManager::CleanUpAndCallWeenie` — `00529650` @ 306740-306752 ```c 00529653 MoveToManager::CleanUp(this); 00529661 if (physics_obj == 0) return; 0052966b return CPhysicsObj::StopCompletely(physics_obj, edx); // tailcall ``` Despite the name, **no weenie call survives in this build** (same compiled-out callback as §7c; the `arg2 = 0` @306749 is argument-setup residue). Body ≡ CleanUp + StopCompletely. --- ## 8. `fail_progress_count` — write-only (negative result with teeth) Exhaustive grep (`fail_progress_count`, 6 hits total @306503/307297/307303/307472/ 307506/307516): initialized to 0, reset to 0 at three progress sites, incremented at two stall sites (§6b/§6c) — **never compared, never read**. The stall counter is vestigial in the 2013 client; there is NO give-up-after-N-ticks path (the only abort conditions are fail_distance §6b and target-status §6d). Port it as a field (cheap parity + diagnostics) but wire no behavior to it. --- ## 9. CPhysicsObj-side entry points (MovementSystem-facing) ### 9a. `CPhysicsObj::MoveToObject` — `00512860` @ 280598-280655 ```c 0051286c if (movement_manager == 0) { MovementManager::Create + EnterDefaultState; set update_time; transient_state |= 0x80; } // lazy-make 005128c9 if (CPhysicsObj::obj_maint != 0) { 005128d2 target = CObjectMaint::GetObjectA(obj_maint, arg2); 005128db if (target != 0) { 005128e9 height = target->part_array ? CPartArray::GetHeight(part_array) : 0; 00512903 radius = target->part_array ? CPartArray::GetRadius(part_array_1) : 0; 00512913 top = target->parent ? target->parent : target; // TOP-LEVEL resolve 00512932 CPhysicsObj::MoveToObject_Internal(this, arg2, top->id, radius, height, arg3); } } ``` `CPhysicsObj::MoveToObject_Internal` `005102e0` @ 278254-278296: lazy-make again, then builds a stack `MovementStruct { type=6 (@278293), object_id=arg2 (@278290), top_level_id=arg3 (@278288), radius=arg4 (@278289), height=arg5 (@278294), params=arg6 (@278291) }` → `MovementManager::PerformMovement(movement_manager, &ms)`. ### 9b. `CPhysicsObj::TurnToObject` — `00512940` @ 280659-280679 Same GetObjectA + parent-resolve shape (NO radius/height fetch) → `CPhysicsObj::TurnToObject_Internal(this, arg2, top->id, arg3)`; `_Internal` `005103f0` @ 278300-278340 builds `MovementStruct { type=8, object_id, top_level_id, params }` → PerformMovement. ### 9c. `CPhysicsObj::TurnToHeading` — `00512980` @ 280683-280721 Lazy-make; `MovementStruct { type=9 (@280718), params=arg2 (@280719) }` → PerformMovement. ### 9d. `CPhysicsObj::StopCompletely` — `00510180` @ 278165-278185 `if (movement_manager)` build `MovementStruct { type=5 }` → PerformMovement. The `arg2` in its `__fastcall(this, int32_t arg2)` signature is unused (BN phantom-edx at every call site). ### 9e. `CPhysicsObj::interrupt_current_movement` — `005101f0` @ 278189-278200 — the cancel entry ```c 005101f8 if (movement_manager != 0) { 005101fa int32_t var_4_1 = 0x36; // ActionCancelled 005101fc MovementManager::CancelMoveTo(movement_manager, edx); // [uninit-edx artifact] 005101f8 } ``` **There is no `CPhysicsObj::cancel_moveto` symbol** (grepped, §10) — this is the public cancel path (called from unpack_movement heads, StopCompletely-family, etc.). ### 9f. Target plumbing (TargetManager seam — call shapes) - `CPhysicsObj::set_target` `0050ed30` @ 276571-276589: lazy-`new TargetManager(0x18)`, `TargetManager::SetTarget(tm, context_id, object_id, radius, quantum)`. MoveToManager always passes `(0, top_level_object_id, 0.5f, 0.0)`. - `CPhysicsObj::clear_target` `0050ed90` @ 276593-276603: `TargetManager::ClearTarget`. - `CPhysicsObj::get_target_quantum` `0050edc0` @ 276620-276634: returns `target_manager->target_info` quantum — the BN text reads `last_update_time` and returns a pointer; **field-attribution garble** (x87 double-return through a TargetInfo field; ACE: returns `TargetInfo.Quantum`, default 0). - `CPhysicsObj::set_target_quantum` `0050eda0` @ 276606-276616: `TargetManager::SetTargetQuantum(tm, quantum)`. - `CPhysicsObj::receive_target_update` `0050ede0` @ 276638-276648: `TargetManager::ReceiveUpdate(tm, TargetInfo*)` — the inbound side that eventually fans back out through `CPhysicsObj::HandleUpdateTarget`. - `CPhysicsObj::HandleUpdateTarget` `00512bc0` @ 280794-280813: ```c 00512bc9 if (arg2.context_id == 0) { // context 0 = the movement context 00512bd3 if (movement_manager) MovementManager::HandleUpdateTarget(mm, copy(TargetInfo)); 00512bfd if (position_manager) PositionManager::HandleUpdateTarget(pm, copy(TargetInfo)); 00512bc9 } ``` (Sticky/interp side of PositionManager::HandleUpdateTarget is R5.) ### 9g. Sticky call shapes referenced from this extraction (bodies = R5) - `PositionManager::StickTo(pm, object_id, radius, height)` — from §4b (arrival with sticky bit) and from `CPhysicsObj::stick_to_object` `005127e0` @ 280559-280595 (parent-resolved id + CPartArray radius/height, unpack case-0 path). - `PositionManager::UnStick(pm)` — via `CPhysicsObj::unstick_from_object` `0050eae0` @ 276403-276413 (called at the head of every `MoveToManager::PerformMovement` and `unpack_movement`). - `CPhysicsObj::IsInterpolating` `0050eb50` @ 276464-276474 → `PositionManager::IsInterpolating` (consumed by the fail-progress stall tests). --- ## 10. Negative results (grepped-not-found) - **`MoveToManager::add_listener` / `remove_listener` / `AddListener` / `RemoveListener` — DO NOT EXIST** (grep over the full raw for both managers: zero hits). No listener/observer machinery on MoveToManager or MovementManager in this build. - **`CPhysicsObj::cancel_moveto` — does not exist**; the cancel entry is `interrupt_current_movement` (§9e). **`CPhysicsObj::MoveToPosition` — does not exist** either; position moves enter only via `unpack_movement` case 7 or a direct `MoveToManager::MoveToPosition` call. - **`MoveToManager::LeaveGround` / `ReportExhaustion` — no bodies**; COMDAT-folded no-ops (§2e). `CMotionInterp::HandleEnterWorld` likewise. - **`fail_progress_count` has no consumer** (§8) — no give-up threshold exists. - **`heading_diff`'s third arg (turn command) is unused** in the body (§5g). - **R3 open item CLOSED:** `HandleUpdateTarget` does live on MoveToManager (`0x0052a7d0`), reached via `CPhysicsObj::HandleUpdateTarget` → `MovementManager::HandleUpdateTarget` → here, fed by `TargetManager::ReceiveUpdate`. - Older Ghidra chunk (`docs/research/decompiled/chunk_00520000.c`) is from a DIFFERENT build for this region — function boundaries don't align (`FUN_005297c0` there ≠ `MoveToManager::TurnToObject` 0x005297d0 here); unusable for adjudication. Ghidra MCP was down this session (both ports probed) — the one soft item worth a live decompile later is §5e's 180/0 constants. ## 11. BN artifact ledger (this extraction) | Site | Artifact class | Adjudication | |---|---|---| | `get_command` towards branch @307962, `towards_and_away` @307932, HandleMoveToPosition arrival @307329 + turn pick @307277 | flag-test `if(p)` sense inversion | explicit `& 0x41`/`& 1` twins in the same bodies + ACE (matches retail line-for-line in this class) | | `get_desired_heading` @308027 | x87 setcc garbled return | ACE shape (180/0 offsets); verify via Ghidra when up | | `heading_diff` return @306346, `GetCurrentDistance` void-return @306448/306459, HandleMoveToPosition Phase-3 @307378-307436, `TurnToObject_Internal` fmod plumbing @307682 | x87 float plumbing lost | structure per ACE, constants verbatim from the raw | | local MovementParameters writes rendered as dead `var_28`/`var_8` stores (§4c/§4d/§6b/§7d) | hoisted-register / lost store destination | field offsets (+4 bitfield, +0x24 hold_key, +0x14 speed) + ACE | | `CancelMoveTo(this, edx)` at 7 sites; `StopCompletely(obj, edx)` everywhere | uninit-edx arg relay (A7 family) | stack store adjacent to each call is the real arg; both callees ignore it | | `MovementManager::LeaveGround`/`ReportExhaustion`/`HandleEnterWorld` tails → `IDClass::~IDClass` | COMDAT-fold symbol collision | trivial-body fold; treat as no-ops | | `CBaseFilter::GetPinVersion(minterp)` @300597 | symbol-collision mislabel | trivial getter of the interp's current style | | `CPhysicsObj::get_target_quantum` reading `last_update_time` @276630 | field-attribution on double return | ACE: returns TargetInfo.Quantum | ## 12. Constants inventory | Constant | Meaning | Sites | |---|---|---| | `0.000199999995f` | universal heading/distance epsilon (same literal as R3's A5/A6) | §3c/§4c/§4d/§5d/§5g/§6b/§6f | | `20f` / `340f` (=360-20) | aux-turn deadband while move-walking | HandleMoveToPosition @307251-307257 | | `180f` | turn-direction pick + wrap tests | @306285/307069/307274/307496 | | `360f` | heading normalization | throughout | | `1.0` (double) | min progress-check window (s); quantum-retune delta (s) | CheckProgressMade @306389; @307423 | | `0.25f` | min progress rate (units/s), incremental AND overall | CheckProgressMade @306402/306419 | | `0.10000000000000001` (double) | min speed for quantum retune | @307400 | | `0.5f` / `0.0` | set_target radius / initial quantum | @306794/306858 | | `3.40282347e+38f` | FLT_MAX distance reset | @306496/306499/307858/307861 | | `0.6f` / `15f` / FLT_MAX / `1f` | params defaults: distance_to_object / walk_run_threshhold / fail_distance / speed | ctor @300513-300519 | | `0x1EE0F` | params default bitfield (A4 pin; can_charge CLEAR) | @300531 | | `0x45000005/6`, `0x44000007`, `0x6500000d/e` | WalkForward/WalkBackwards, RunForward, TurnRight/TurnLeft | §5c/§5d/§5e/§4d/§6b | | `transient_state & 1` | CONTACT gate for UseTime | @307781 | | `transient_state \|= 0x80` | ACTIVE flag on lazy manager creation | §9a-9c | | `8, 0xb, 0x36, 0x37, 0x38, 0x3d, 0x47` | WeenieError codes (NoPhysicsObject, NoMotionInterpreter, ActionCancelled, ObjectGone, NoObject, YouChargedTooFar, InvalidMovementType) | §7a/§7c/§2b | | `0x1c` / `0xc` / `0x28` | UnPackNet sizes (move/turn) / full Pack size | §2g | | `0x160` / `0x10` | sizeof MoveToManager / MovementNode | §1b/§4a | ## 13. Summary table (function → address → raw lines → completeness) | Function | Address | Raw lines | Completeness | |---|---|---|---| | MoveToManager::MoveToManager (ctor) | 005293b0 | 306554-306593 | full | | MoveToManager::Create | 00529470 | 306597-306614 | full | | MoveToManager::InitializeLocalVariables | 00529250 | 306490-306534 | full | | MoveToManager::Destroy | 005294b0 | 306618-306663 | full | | MoveToManager::~MoveToManager | 005299d0 | 306945-306953 | full | | MoveToManager::SetWeenieObject / SetPhysicsObject | 00529230 / 00529240 | 306474-306486 | full | | MoveToManager::is_moving_to | 00529220 | 306464-306470 | full | | MoveToManager::PerformMovement | 0052a900 | 307871-307904 | full | | MoveToManager::MoveToObject | 00529680 | 306756-306817 | full | | MoveToManager::MoveToPosition | 0052a240 | 307521-307593 | full | | MoveToManager::TurnToObject | 005297d0 | 306820-306882 | full | | MoveToManager::TurnToHeading | 0052a630 | 307706-307772 | full | | MoveToManager::MoveToObject_Internal | 0052a400 | 307597-307663 | full | | MoveToManager::TurnToObject_Internal | 0052a550 | 307667-307702 | full (fmod plumbing via ACE) | | MoveToManager::HandleUpdateTarget | 0052a7d0 | 307802-307867 | full | | MoveToManager::UseTime | 0052a780 | 307776-307798 | full | | MoveToManager::HandleMoveToPosition | 00529d80 | 307187-307438 | full (Phase-3 x87 via ACE) | | MoveToManager::HandleTurnToHeading | 0052a0c0 | 307442-307517 | full | | MoveToManager::BeginNextNode | 00529cb0 | 307123-307171 | full | | MoveToManager::BeginMoveForward | 00529a00 | 306957-307042 | full | | MoveToManager::BeginTurnToHeading | 00529b90 | 307046-307120 | full | | MoveToManager::HitGround | 00529d70 | 307175-307183 | full | | MoveToManager::AddTurnToHeadingNode / AddMoveToPositionNode | 00529530 / 00529580 | 306667-306706 | full | | MoveToManager::RemovePendingActionsHead | 00529380 | 306538-306550 | full | | MoveToManager::CleanUp | 005295c0 | 306710-306736 | full | | MoveToManager::CleanUpAndCallWeenie | 00529650 | 306740-306752 | full | | MoveToManager::CancelMoveTo | 00529930 | 306886-306940 | full | | MoveToManager::_DoMotion / _StopMotion | 00529010 / 00529080 | 306351-306381 | full | | MoveToManager::CheckProgressMade | 005290f0 | 306385-306431 | full | | MoveToManager::GetCurrentDistance | 005291b0 | 306435-306460 | full (x87 args via signature) | | MovementParameters::get_command | 0052aa00 | 307946-308012 | full | | MovementParameters::towards_and_away | 0052a9a0 | 307917-307942 | full | | MovementParameters::get_desired_heading | 0052aad0 | 308016-308033 | shape (constants via ACE; garbled) | | MovementParameters::Pack / UnPack / UnPackNet | 0052ab20 / 0052abc0 / 0052ac50 | 308037-308205 | full | | heading_greater / heading_diff | 00528f60 / 00528fb0 | 306281-306347 | full | | MovementManager::MakeMoveToManager | 00524000 | 300124-300129 | full | | MovementManager::PerformMovement | 005240d0 | 300194-300255 | full | | MovementManager::CancelMoveTo | 005241b0 | 300277-300288 | full | | MovementManager::UseTime / HitGround / IsMovingTo / HandleUpdateTarget | 005242f0 / 00524300 / 00524260 / 00524790 | 300352-300440, 300723-300730 | full | | MovementManager::LeaveGround / ReportExhaustion / HandleEnterWorld | 00524320 / 00524360 / 00524340 | 300444-300506 | full (folded no-ops) | | MovementManager::Destroy | 005243f0 | 300538-300559 | full | | MovementManager::unpack_movement | 00524440 | 300563-300719 | full | | CPhysicsObj::MoveToObject (+_Internal) | 00512860 / 005102e0 | 280598-280655, 278254-278296 | full | | CPhysicsObj::TurnToObject (+_Internal) | 00512940 / 005103f0 | 280659-280679, 278300-278340 | full | | CPhysicsObj::TurnToHeading | 00512980 | 280683-280721 | full | | CPhysicsObj::StopCompletely | 00510180 | 278165-278185 | full | | CPhysicsObj::interrupt_current_movement | 005101f0 | 278189-278200 | full | | CPhysicsObj::MakeMovementManager / MakePositionManager | 00510270 / 00510210 | 278204-278250 | full | | CPhysicsObj::set_target / clear_target / set_target_quantum / get_target_quantum / receive_target_update / HandleUpdateTarget | 0050ed30-0050ede0, 00512bc0 | 276571-276648, 280794-280813 | full (quantum getter garbled, noted) | | CPhysicsObj::stick_to_object / unstick_from_object / IsInterpolating | 005127e0 / 0050eae0 / 0050eb50 | 280559-280595, 276403-276413, 276464-276474 | call shapes (R5 owns internals) |