acdream/docs/research/2026-07-03-r4-moveto/r4-moveto-decomp.md
Erik 988304e13b docs(R4-V-1): MoveToManager research base — decomp extraction + ACE cross-ref + port work-list
Workflow-produced R4 research (3 docs, 2,541 lines):
- r4-moveto-decomp.md: all 33 MoveToManager members verbatim
  (0x00529010-0x0052a987) + the MovementManager type-6/7/8/9 relay +
  MovementParameters::get_command. HandleUpdateTarget CONFIRMED on
  MoveToManager (0x0052a7d0 — closes the R3 negative result; object
  moves are DEFERRED until the first target-update callback). Walk-vs-
  run = the CanCharge rule exactly (can_charge OR can_run && dist-gap >
  walk_run_threshhold, riding hold_key_to_apply into DoInterpretedMotion
  — MoveToManager never calls set_hold_run). Sticky handoff located
  (empty queue + 0x80 → StickTo). fail_progress_count is WRITE-ONLY in
  retail (no give-up threshold — do not invent one). 8-class BN
  artifact ledger.
- r4-ace-moveto.md: 16 flagged ACE-isms, all retail-verified — incl. a
  stale-member read (MoveToPosition checks the PREVIOUS move's
  UseFinalHeading), a field transposition (InitializeLocalVars zeroes
  DistanceToObject where retail zeroes the flags word), a dropped
  BeginNextNode (ACE turns start one tick late), a UseTime gate
  inversion, and the canceling CanCharge default+fast-path pair.
  Blast radius: acdream has THREE independent approximations of this
  one mechanism (DriveServerAutoWalk / RemoteMoveToDriver /
  ServerControlledLocomotion) + an outdated chase/flee claim in
  RemoteMoveToDriver's class doc.
- r4-port-plan.md: 7 pins (P1 blocker: retail interrupts current
  movement on EVERY UM but ACE sends a companion mt-0 echo after each
  MoveTo — needs a discriminator pin before V5; P3: heading_diff's
  TurnLeft mirror needs one Ghidra decompile of 0x00528fb0; P4:
  type-6/8 moves need a minimal TargetTracker adapter until R5),
  17 gaps M1-M17, commits V0-V6. Retires AD-8/AD-9/AP-8/AP-9 + TS-36.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:31:12 +02:00

70 KiB

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)

/* 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<MoveToManager::MovementNode> 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::MoveToManager005293b0 @ 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::Create00529470 @ 306597-306614

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::InitializeLocalVariables00529250 @ 306490-306534

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::Destroy005294b0 @ 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::~MoveToManager005299d0 @ 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_to00529220 @ 306464-306470

00529226      result = this->movement_type != Invalid;

2. MovementManager relay layer

2a. MovementManager::MakeMoveToManager00524000 @ 300124-300129

00524008      if (this->moveto_manager == 0)
0052401a          this->moveto_manager = MoveToManager::Create(this->physics_obj, this->weenie_obj);

2b. MovementManager::PerformMovement005240d0 @ 300194-300237

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::CancelMoveTo005241b0 @ 300277-300288

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_movement00524440 @ 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:

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::UnPackNet0052ac50 @ 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::PerformMovement0052a900 @ 307871-307904

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::MoveToObject00529680 @ 306756-306817

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::MoveToPosition0052a240 @ 307521-307593

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::TurnToObject005297d0 @ 306820-306882

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::TurnToHeading0052a630 @ 307706-307772

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::BeginNextNode00529cb0 @ 307123-307171

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::BeginMoveForward00529a00 @ 306957-307042

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, &current_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::BeginTurnToHeading00529b90 @ 307046-307120

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::GetCurrentDistance005291b0 @ 306435-306460

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, &current_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::CheckProgressMade005290f0 @ 306385-306431

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_command0052aa00 @ 307946-308012 — walk-vs-run + command pick

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_Runcan_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_away0052a9a0 @ 307917-307942

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_heading0052aad0 @ 308016-308033 — ⚠ BN garble

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_greater00528f60 @ 306281-306323 (free function)

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_diff00528fb0 @ 306327-306347 (free function)

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::UseTime0052a780 @ 307776-307798 — the tick gate

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::HandleMoveToPosition00529d80 @ 307187-307438 — the big per-tick driver

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, &current_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::HandleTurnToHeading0052a0c0 @ 307442-307517

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::HandleUpdateTarget0052a7d0 @ 307802-307867 — CONFIRMED here (R3 open item closed)

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::HitGround00529d70 @ 307175-307183

00529d73      if (this->movement_type == Invalid) return;
00529d75      return MoveToManager::BeginNextNode(this);     // tailcall — re-arm after landing

6f. MoveToManager::MoveToObject_Internal0052a400 @ 307597-307663

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_Internal0052a550 @ 307667-307702

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, &current_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::_DoMotion00529010 @ 306351-306364

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::_StopMotion00529080 @ 306368-306381

Identical shape; tail is CMotionInterp::StopInterpretedMotion(minterp, arg2, arg3).

This is the entire CMotionInterp seam: MoveToManager only ever issues adjust_motionDoInterpretedMotion / 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::CancelMoveTo00529930 @ 306886-306940

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::CleanUp005295c0 @ 306710-306736

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::CleanUpAndCallWeenie00529650 @ 306740-306752

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::MoveToObject00512860 @ 280598-280655

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::TurnToObject00512940 @ 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::TurnToHeading00512980 @ 280683-280721

Lazy-make; MovementStruct { type=9 (@280718), params=arg2 (@280719) } → PerformMovement.

9d. CPhysicsObj::StopCompletely00510180 @ 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_movement005101f0 @ 278189-278200 — the cancel entry

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:
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::HandleUpdateTargetMovementManager::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)