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>
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::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
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
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
00529226 result = this->movement_type != Invalid;
2. MovementManager relay layer
2a. MovementManager::MakeMoveToManager — 00524000 @ 300124-300129
00524008 if (this->moveto_manager == 0)
0052401a this->moveto_manager = MoveToManager::Create(this->physics_obj, this->weenie_obj);
2b. MovementManager::PerformMovement — 005240d0 @ 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::CancelMoveTo — 005241b0 @ 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::UseTime005242f0@ 300411-300421:if (moveto_manager) tailcall MoveToManager::UseTime. CMotionInterp is NOT ticked here — UseTime is moveto-only.MovementManager::HitGround00524300@ 300425-300440:minterp->HitGround()thentailcall MoveToManager::HitGround(moveto_manager).MovementManager::HandleUpdateTarget00524790@ 300723-300730:if (moveto_manager) MoveToManager::HandleUpdateTarget(moveto_manager, &arg2). Confirms the R3 negative result: HandleUpdateTarget lives on MoveToManager (§6 below).MovementManager::IsMovingTo00524260@ 300352-300361:moveto_manager && MoveToManager::is_moving_to(...).MovementManager::Destroy005243f0@ 300538-300559: deletes both minterp (~CMotionInterp+operator delete) and moveto (~MoveToManager+operator delete).MovementManager::SetWeenieObject00524020@ 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::LeaveGround00524320@ 300444-300459:minterp->LeaveGround()then "tailcall IDClass dtor(moveto_manager)" →MoveToManager::LeaveGroundis a no-op in this build.MovementManager::ReportExhaustion00524360@ 300491-300506:minterp->ReportExhaustion()then the same folded no-op on moveto_manager →MoveToManager::ReportExhaustionis a no-op.MovementManager::HandleEnterWorld00524340@ 300463-300473: folded no-op on motion_interpreter →CMotionInterp::HandleEnterWorldis a no-op (HandleExitWorld @300477-300487 calls the realCMotionInterp::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:
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
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)
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
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
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
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)
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
00529d73 if (this->movement_type == Invalid) return;
00529d75 return MoveToManager::BeginNextNode(this); // tailcall — re-arm after landing
6f. MoveToManager::MoveToObject_Internal — 0052a400 @ 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_Internal — 0052a550 @ 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, ¤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
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
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
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
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
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
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_target0050ed30@ 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_target0050ed90@ 276593-276603:TargetManager::ClearTarget.CPhysicsObj::get_target_quantum0050edc0@ 276620-276634: returnstarget_manager->target_infoquantum — the BN text readslast_update_timeand returns a pointer; field-attribution garble (x87 double-return through a TargetInfo field; ACE: returnsTargetInfo.Quantum, default 0).CPhysicsObj::set_target_quantum0050eda0@ 276606-276616:TargetManager::SetTargetQuantum(tm, quantum).CPhysicsObj::receive_target_update0050ede0@ 276638-276648:TargetManager::ReceiveUpdate(tm, TargetInfo*)— the inbound side that eventually fans back out throughCPhysicsObj::HandleUpdateTarget.CPhysicsObj::HandleUpdateTarget00512bc0@ 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 fromCPhysicsObj::stick_to_object005127e0@ 280559-280595 (parent-resolved id + CPartArray radius/height, unpack case-0 path).PositionManager::UnStick(pm)— viaCPhysicsObj::unstick_from_object0050eae0@ 276403-276413 (called at the head of everyMoveToManager::PerformMovementandunpack_movement).CPhysicsObj::IsInterpolating0050eb50@ 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 isinterrupt_current_movement(§9e).CPhysicsObj::MoveToPosition— does not exist either; position moves enter only viaunpack_movementcase 7 or a directMoveToManager::MoveToPositioncall.MoveToManager::LeaveGround/ReportExhaustion— no bodies; COMDAT-folded no-ops (§2e).CMotionInterp::HandleEnterWorldlikewise.fail_progress_counthas 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:
HandleUpdateTargetdoes live on MoveToManager (0x0052a7d0), reached viaCPhysicsObj::HandleUpdateTarget→MovementManager::HandleUpdateTarget→ here, fed byTargetManager::ReceiveUpdate. - Older Ghidra chunk (
docs/research/decompiled/chunk_00520000.c) is from a DIFFERENT build for this region — function boundaries don't align (FUN_005297c0there ≠MoveToManager::TurnToObject0x005297d0 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) |