# Retail decomp extract: PositionManager + StickyManager + CPhysicsObj sticky seams Source: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Binary Ninja pseudo-C, Sept 2013 EoR build, PDB-named). Struct defs from `docs/research/named-retail/acclient.h`. All function bodies below are VERBATIM (unedited) except for the surrounding-context excerpts marked as such. --- ## Struct definitions (acclient.h) ### `struct PositionManager` (acclient.h:30952, type id 3468) ```c /* 3468 */ struct __cppobj PositionManager { InterpolationManager *interpolation_manager; StickyManager *sticky_manager; ConstraintManager *constraint_manager; CPhysicsObj *physics_obj; }; ``` ### `struct StickyManager` (acclient.h:31518, type id 3466) ```c /* 3466 */ struct __cppobj StickyManager { unsigned int target_id; float target_radius; Position target_position; CPhysicsObj *physics_obj; int initialized; long double sticky_timeout_time; }; ``` ### `struct ConstraintManager` (acclient.h:31529, type id 3467, for context — sibling manager) ```c /* 3467 */ struct __cppobj ConstraintManager { CPhysicsObj *physics_obj; int is_constrained; float constraint_pos_offset; Position constraint_pos; float constraint_distance_start; float constraint_distance_max; }; ``` NOTE: `PositionManager` is a small 4-pointer facade/dispatcher struct (0x10 bytes — matches `PositionManager::Create`'s `operator new(0x10)`). It owns three independently lazily-created sub-managers: `InterpolationManager` (network position interpolation), `StickyManager` (parent-object stick/follow), `ConstraintManager` (radius-bounded tether). `StickyManager` itself is 0x60 bytes (`StickyManager::Create`'s `operator new(0x60)`) and holds a single sticky target: id, radius, cached target position (a full `Position`, which embeds a `Frame`), back-pointer to owner `CPhysicsObj`, an `initialized` bool gating whether `target_position` is valid yet, and an `x87 long double` timeout timestamp (`sticky_timeout_time`) compared against `Timer::cur_time`. --- ## PositionManager methods ### `PositionManager::UseTime` — 0x00555160 ```c 00555160 void __fastcall PositionManager::UseTime(class PositionManager* this) 00555160 { 00555163 class InterpolationManager* interpolation_manager = this->interpolation_manager; 00555163 00555167 if (interpolation_manager != 0) 00555169 InterpolationManager::UseTime(interpolation_manager); 00555169 0055516e class ConstraintManager* constraint_manager = this->constraint_manager; 0055516e 00555173 if (constraint_manager != 0) 00555175 IDClass<_tagDataID,32,0>::~IDClass<_tagDataID,32,0>(constraint_manager); 00555175 0055517a class StickyManager* sticky_manager = this->sticky_manager; 0055517a 00555180 if (sticky_manager == 0) 00555187 return; 00555187 00555182 /* tailcall */ 00555182 return StickyManager::UseTime(sticky_manager); 00555160 } ``` NOTE: the `constraint_manager != 0` branch calls `IDClass<_tagDataID,32,0>::~IDClass<_tagDataID,32,0>(constraint_manager)` — this is almost certainly a Binary Ninja mis-decompilation / bad-symbol-resolution artifact (the destructor of an unrelated template ID class being called on a `ConstraintManager*` makes no structural sense). The real call is very likely `ConstraintManager::UseTime` (mirroring the `InterpolationManager::UseTime` / `StickyManager::UseTime` pattern on either side of it) — flagging for the lead rather than silently correcting. ### `PositionManager::adjust_offset` — 0x00555190 ```c 00555190 void __thiscall PositionManager::adjust_offset(class PositionManager* this, class Frame* arg2, double arg3) 00555190 { 00555191 int32_t ebx = arg3; 0055519d class InterpolationManager* interpolation_manager = this->interpolation_manager; 005551a2 int32_t edi = *(uint32_t*)((char*)arg3)[4]; 005551a2 005551a6 if (interpolation_manager != 0) 005551a6 { 005551a8 int32_t var_14_1 = edi; 005551ab InterpolationManager::adjust_offset(interpolation_manager, arg2, ebx); 005551a6 } 005551a6 005551b0 class StickyManager* sticky_manager = this->sticky_manager; 005551b0 005551b5 if (sticky_manager != 0) 005551b5 { 005551b7 int32_t var_14_2 = edi; 005551ba StickyManager::adjust_offset(sticky_manager, arg2, ebx); 005551b5 } 005551b5 005551bf class ConstraintManager* constraint_manager = this->constraint_manager; 005551bf 005551c4 if (constraint_manager != 0) 005551c4 { 005551c6 int32_t var_14_3 = edi; 005551c9 ConstraintManager::adjust_offset(constraint_manager, arg2, ebx); 005551c4 } 00555190 } ``` NOTE: the `int32_t edi = *(uint32_t*)((char*)arg3)[4];` line is BN garbling `arg3` (a `double` passed as quantum/elapsed-time) — this looks like a decompiler artifact from the double being passed partly in a register pair; the actual semantic is simply "pass `arg3` (the quantum) through to all three sub-manager `adjust_offset` calls unchanged." Each sub-manager gets the SAME `Frame*` (`arg2`, an in/out accumulator) and the SAME quantum. This mirrors `UseTime`'s dispatch pattern: PositionManager is a pure fan-out facade over its three sub-managers, called once per tick. ### `PositionManager::UnStick` — 0x005551e0 ```c 005551e0 void __fastcall PositionManager::UnStick(class PositionManager* this) 005551e0 { 005551e0 class StickyManager* sticky_manager = this->sticky_manager; 005551e0 005551e5 if (sticky_manager == 0) 005551ec return; 005551ec 005551e7 /* tailcall */ 005551e7 return StickyManager::UnStick(sticky_manager); 005551e0 } ``` ### `PositionManager::InterpolateTo` — 0x005551f0 (context — sibling method) ```c 005551f0 void __thiscall PositionManager::InterpolateTo(class PositionManager* this, class Position const* arg2, int32_t arg3) 005551f0 { 005551f6 if (this->interpolation_manager == 0) 00555204 this->interpolation_manager = InterpolationManager::Create(this->physics_obj); 00555204 00555212 InterpolationManager::InterpolateTo(this->interpolation_manager, arg2, arg3); 005551f0 } ``` ### `PositionManager::StopInterpolating` — 0x00555220 (context) ```c 00555220 void __fastcall PositionManager::StopInterpolating(class PositionManager* this) 00555220 { 00555220 class InterpolationManager* interpolation_manager = this->interpolation_manager; 00555220 00555224 if (interpolation_manager == 0) 0055522b return; 0055522b 00555226 /* tailcall */ 00555226 return InterpolationManager::StopInterpolating(interpolation_manager); 00555220 } ``` ### `PositionManager::StickTo` — 0x00555230 ```c 00555230 void __thiscall PositionManager::StickTo(class PositionManager* this, uint32_t arg2, float arg3, float arg4) 00555230 { 00555238 if (this->sticky_manager == 0) 00555246 this->sticky_manager = StickyManager::Create(this->physics_obj); 00555246 0055525b StickyManager::StickTo(this->sticky_manager, arg2, arg3, arg4); 00555230 } ``` Lazy-creates the `StickyManager` on first stick, then delegates. `arg2` = target object id, `arg3` = radius, `arg4` = height (per callers below). ### `PositionManager::GetStickyObjectID` — 0x00555270 (context) ```c 00555270 uint32_t __fastcall PositionManager::GetStickyObjectID(class PositionManager const* this) 00555270 { 00555270 class StickyManager* sticky_manager = this->sticky_manager; 00555270 00555275 if (sticky_manager == 0) 0055527e return 0; 0055527e 00555277 /* tailcall */ 00555277 return CommandList::GetHead(sticky_manager); 00555270 } ``` NOTE: `CommandList::GetHead(sticky_manager)` is an obviously wrong callee name for a tailcall that's supposed to read `sticky_manager->target_id` (a plain `uint32_t` at offset 0 of `StickyManager`). This looks like a BN symbol-resolution mixup (the real function returns `this->sticky_manager->target_id` directly — likely inlined/no separate symbol, and BN picked a spurious nearby symbol for the tailcall target). Flagging, not correcting. ### `PositionManager::ConstrainTo` — 0x00555280 (context — sibling method) ```c 00555280 void __thiscall PositionManager::ConstrainTo(class PositionManager* this, class Position const* arg2, float arg3, float arg4) 00555280 { 00555288 if (this->constraint_manager == 0) 00555296 this->constraint_manager = ConstraintManager::Create(this->physics_obj); 00555296 00555299 class ConstraintManager* constraint_manager = this->constraint_manager; 00555299 0055529f if (constraint_manager == 0) 005552a6 return; 005552a6 005552a1 /* tailcall */ 005552a1 return ConstraintManager::ConstrainTo(constraint_manager, arg2, arg3, arg4); 00555280 } ``` ### `PositionManager::UnConstrain` — 0x005552b0 (context) ```c 005552b0 void __fastcall PositionManager::UnConstrain(class PositionManager* this) 005552b0 { 005552b0 class ConstraintManager* constraint_manager = this->constraint_manager; 005552b0 005552b5 if (constraint_manager == 0) 005552bc return; 005552bc 005552b7 /* tailcall */ 005552b7 return ConstraintManager::UnConstrain(constraint_manager); 005552b0 } ``` ### `PositionManager::IsFullyConstrained` — 0x005552c0 ```c 005552c0 int32_t __fastcall PositionManager::IsFullyConstrained(class PositionManager const* this) 005552c0 { 005552c0 class ConstraintManager* constraint_manager = this->constraint_manager; 005552c0 005552c5 if (constraint_manager == 0) 005552ce return 0; 005552ce 005552c7 /* tailcall */ 005552c7 return ConstraintManager::IsFullyConstrained(constraint_manager); 005552c0 } ``` ### `PositionManager::Create` — 0x005552d0 (ctor/factory) ```c 005552d0 class PositionManager* PositionManager::Create(class CPhysicsObj* arg1) 005552d0 { 005552d3 void* result = operator new(0x10); 005552d3 005552df if (result == 0) 00555332 return 0; 00555332 005552e1 *(uint32_t*)result = 0; 005552e7 *(uint32_t*)((char*)result + 4) = 0; 005552ee *(uint32_t*)((char*)result + 8) = 0; 005552f5 *(uint32_t*)((char*)result + 0xc) = 0; 005552fc class QuickWindow* ecx = *(uint32_t*)result; 00555305 *(uint32_t*)((char*)result + 0xc) = arg1; 00555305 00555308 if (ecx != 0) 0055530b QuickWindow::SetWindowID(ecx, arg1); 0055530b 00555310 class StickyManager* ecx_1 = *(uint32_t*)((char*)result + 4); 00555310 00555315 if (ecx_1 != 0) 00555318 StickyManager::SetPhysicsObject(ecx_1, arg1); 00555318 0055531d class ConstraintManager* ecx_2 = *(uint32_t*)((char*)result + 8); 0055531d 00555322 if (ecx_2 != 0) 00555325 ConstraintManager::SetPhysicsObject(ecx_2, arg1); 00555325 0055532e return result; 005552d0 } ``` NOTE: `QuickWindow* ecx = *(uint32_t*)result;` / `QuickWindow::SetWindowID(ecx, arg1)` is another BN symbol-mixup artifact — `ecx` here is really the freshly-zeroed `interpolation_manager` field (offset 0, which was just set to 0 two lines above), so this branch is structurally dead at construction time (the null-check always fails right after `operator new` zeroes the block) and the "QuickWindow::SetWindowID" callee name is spurious. Read this constructor as: `operator new(0x10)` → zero all 4 fields (interpolation_manager, sticky_manager, constraint_manager @ offsets 0/4/8) → set `physics_obj` (offset 0xc) = `arg1` → then three no-op "if member != 0, call member::SetPhysicsObject(arg1)" guards that are all unreachable immediately after the zero-fill (they'd only fire if a sub-manager pointer were already non-null, which can't happen on a freshly allocated struct). This is very likely how the retail source literally reads even though the guards are dead code at this call site — the same "if (member) member->SetPhysicsObject(this)" idiom recurs in `StickyManager::Create`. ### `PositionManager::Destroy` — 0x00555340 (dtor helper) ```c 00555340 void __fastcall PositionManager::Destroy(class PositionManager* this) 00555340 { 00555344 class InterpolationManager* interpolation_manager = this->interpolation_manager; 00555344 00555348 if (interpolation_manager != 0) 00555348 { 0055534c InterpolationManager::~InterpolationManager(interpolation_manager); 00555352 operator delete(interpolation_manager); 00555348 } 00555348 0055535a class StickyManager* sticky_manager = this->sticky_manager; 0055535f this->interpolation_manager = 0; 0055535f 00555365 if (sticky_manager != 0) 00555365 { 00555369 StickyManager::~StickyManager(sticky_manager); 0055536f operator delete(sticky_manager); 00555365 } 00555365 00555377 class ConstraintManager* constraint_manager = this->constraint_manager; 0055537c this->sticky_manager = nullptr; 0055537c 00555383 if (constraint_manager != 0) 00555383 { 00555387 ConstraintManager::~ConstraintManager(constraint_manager); 0055538d operator delete(constraint_manager); 00555383 } 00555383 00555396 this->constraint_manager = nullptr; 00555340 } ``` ### `PositionManager::~PositionManager` — 0x005553a0 (dtor, tailcalls Destroy) ```c 005553a0 void __fastcall PositionManager::~PositionManager(class PositionManager* this) 005553a0 { 005553a0 /* tailcall */ 005553a0 return PositionManager::Destroy(this); 005553a0 } ``` ### `PositionManager::IsInterpolating` — 0x005553b0 (context) ```c 005553b0 int32_t __fastcall PositionManager::IsInterpolating(class PositionManager const* this) 005553b0 { 005553b0 class InterpolationManager* interpolation_manager = this->interpolation_manager; 005553b0 005553b4 if (interpolation_manager == 0) 005553c4 return 0; 005553c4 005553bc int32_t result; 005553bc result = interpolation_manager->position_queue.head_ != 0; 005553c1 return result; 005553b0 } ``` ### `PositionManager::HandleUpdateTarget` — 0x005553d0 ```c 005553d0 void __fastcall PositionManager::HandleUpdateTarget(class PositionManager* this, class TargetInfo arg2) 005553d0 { 005553d8 if (this->sticky_manager != 0) 005553d8 { 005553ea void var_d4; 005553ea TargetInfo::TargetInfo(&var_d4, &arg2); 005553f2 StickyManager::HandleUpdateTarget(this->sticky_manager, var_d4); 005553d8 } 005553d0 } ``` Only forwards to `StickyManager` (the `InterpolationManager` / `ConstraintManager` siblings don't care about target-info updates — only sticky-follow does, since it needs the live position of the object it's stuck to). --- ## StickyManager methods ### `StickyManager::UnStick` — 0x00555400 ```c 00555400 void __fastcall StickyManager::UnStick(class StickyManager* this) 00555400 { 00555406 if (this->target_id == 0) 00555427 return; 00555427 00555408 class CPhysicsObj* physics_obj = this->physics_obj; 0055540b this->target_id = 0; 00555411 this->initialized = 0; 00555418 CPhysicsObj::clear_target(physics_obj); 00555421 /* tailcall */ 00555421 return CPhysicsObj::interrupt_current_movement(this->physics_obj); 00555400 } ``` No-op if not currently stuck. Otherwise: clear `target_id` + `initialized`, tell the owning `CPhysicsObj` to clear its target-tracking registration (`CPhysicsObj::clear_target`), then interrupt whatever movement is in flight (`CPhysicsObj::interrupt_current_movement`) — this is the standard "unstick invalidates in-progress movement" pattern repeated in every unstick path below (`UseTime`'s timeout branch, `StickTo`'s re-stick branch, `HandleUpdateTarget`'s failure branch all do the exact same 4-line sequence). ### `StickyManager::adjust_offset` — 0x00555430 ```c 00555430 void __thiscall StickyManager::adjust_offset(class StickyManager* this, class Frame* arg2, double arg3) 00555430 { 00555436 uint32_t target_id = this->target_id; 00555436 00555445 if ((target_id != 0 && this->initialized != 0)) 00555445 { 00555459 class Position* edi_2 = &this->physics_obj->m_position; 0055545c class CPhysicsObj* eax = CPhysicsObj::GetObjectA(target_id); 00555466 class Position* ebp_1 = &eax->m_position; 00555466 00555469 if (eax == 0) 0055546b ebp_1 = &this->target_position; 0055546b 00555476 int32_t __return; 00555476 class AC1Legacy::Vector3* eax_1 = Position::get_offset(edi_2, &__return, ebp_1); 00555456 arg2->m_fOrigin.x = eax_1->x; 00555456 arg2->m_fOrigin.y = eax_1->y; 00555456 arg2->m_fOrigin.z = eax_1->z; 00555495 class AC1Legacy::Vector3* eax_3 = Position::globaltolocalvec(edi_2, &__return, &arg2->m_fOrigin); 00555456 arg2->m_fOrigin.x = eax_3->x; 00555456 arg2->m_fOrigin.y = eax_3->y; 00555456 arg2->m_fOrigin.z = eax_3->z; 00555456 arg2->m_fOrigin.z = 0f; 005554b3 float target_radius = this->target_radius; 005554c1 int32_t var_34_1 = CPhysicsObj::GetRadius(this->physics_obj); 005554d5 long double x87_r0; 005554d5 float var_14_1 = ((float)(Position::cylinder_distance_no_z(((float)x87_r0), edi_2, target_radius, ebp_1) - ((long double)0.300000012f))); 005554d5 int16_t top_1 = 1; 005554d5 005554e0 if (AC1Legacy::Vector3::normalize_check_small(&arg2->m_fOrigin) != 0) 005554e0 { 005554e2 __return = 0; 00555456 arg2->m_fOrigin.x = __return; 00555456 arg2->m_fOrigin.y = 0f; 00555456 arg2->m_fOrigin.z = 0f; 005554e0 } 005554e0 00555522 class CMotionInterp* eax_7; 00555522 long double st0_2; 00555522 00555522 if (CPhysicsObj::get_minterp(this->physics_obj) == 0) 0055553b top_1 = 0; 00555522 else 0055552e eax_7 = CMotionInterp::get_max_speed(CPhysicsObj::get_minterp(this->physics_obj)); 0055553f long double temp1_1 = ((long double)0.000199999995f); 0055553f (/* unimplemented {fcom st0, dword [&F_EPSILON]} f- temp1_1 */ - temp1_1); 0055553f bool c0_1 = /* bool c0_1 = unimplemented {fcom st0, dword [&F_EPSILON]} f< temp1_1 */ < temp1_1; 0055553f bool c2_1 = (FCMP_UO(/* bool c2_1 = is_unordered.t(unimplemented {fcom st0, dword [&F_EPSILON]}, temp1_1) */, temp1_1)); 0055553f bool c3_1 = /* bool c3_1 = unimplemented {fcom st0, dword [&F_EPSILON]} f== temp1_1 */ == temp1_1; 00555545 eax_7 = ((((c0_1) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | ((((c2_1) ? 1 : 0) << 0xa) | ((((c3_1) ? 1 : 0) << 0xe) | ((top_1 & 7) << 0xb))))); 00555547 bool p_1 = /* bool p_1 = unimplemented {test ah, 0x5} */; 00555547 0055554a if (!(p_1)) 0055554a { 0055554c /* unimplemented {fstp st0, st0} */; 0055554c /* unimplemented {fstp st0, st0} */; 0055554e /* unimplemented {fld st0, dword [&MAX_VELOCITY]} */; 0055554a } 0055554a 00555554 /* unimplemented {fmul st0, qword [esp+0x2c]} */; 00555558 /* unimplemented {fld st0, dword [esp+0x10]} */; 0055555c /* unimplemented {fabs } */; 0055555e /* unimplemented {fld st0, st1} */; 00555560 (/* unimplemented {fcompp } f- unimplemented {fcompp } */ - /* unimplemented {fcompp } f- unimplemented {fcompp } */); 00555560 bool c0_2 = /* bool c0_2 = unimplemented {fcompp } f< unimplemented {fcompp } */ < /* bool c0_2 = unimplemented {fcompp } f< unimplemented {fcompp } */; 00555560 bool c2_2 = (FCMP_UO(/* bool c2_2 = is_unordered.t(unimplemented {fcompp }, unimplemented {fcompp }) */, /* bool c2_2 = is_unordered.t(unimplemented {fcompp }, unimplemented {fcompp }) */)); 00555560 bool c3_2 = /* bool c3_2 = unimplemented {fcompp } f== unimplemented {fcompp } */ == /* bool c3_2 = unimplemented {fcompp } f== unimplemented {fcompp } */; 00555560 /* unimplemented {fcompp } */; 00555560 /* unimplemented {fcompp } */; 00555562 eax_7 = ((((c0_2) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | ((((c2_2) ? 1 : 0) << 0xa) | ((((c3_2) ? 1 : 0) << 0xe) | ((top_1 & 7) << 0xb))))); 00555564 bool p_2 = /* bool p_2 = unimplemented {test ah, 0x5} */; 00555567 int16_t top_9; 00555567 00555567 if (p_2) 00555567 { 00555579 /* unimplemented {fstp st0, st0} */; 00555579 /* unimplemented {fstp st0, st0} */; 0055557b /* unimplemented {fld st0, dword [esp+0x10]} */; 0055557f /* unimplemented {fmul st0, dword [esi]} */; 00555456 arg2->m_fOrigin.x = ((float)/* arg2->m_fOrigin.x = fconvert.s(unimplemented {fstp dword [esi], st0}) */); 00555581 /* unimplemented {fstp dword [esi], st0} */; 00555583 /* unimplemented {fld st0, dword [esp+0x10]} */; 00555587 /* unimplemented {fmul st0, dword [esi+0x4]} */; 00555456 arg2->m_fOrigin.y = ((float)/* arg2->m_fOrigin.y = fconvert.s(unimplemented {fstp dword [esi+0x4], st0}) */); 0055558a /* unimplemented {fstp dword [esi+0x4], st0} */; 0055558d top_9 = top_1; 0055558d /* unimplemented {fld st0, dword [esp+0x10]} */; 00555567 } 00555567 else 00555567 { 00555569 /* unimplemented {fld st0, st0} */; 0055556b /* unimplemented {fmul st0, dword [esi]} */; 00555456 arg2->m_fOrigin.x = ((float)/* arg2->m_fOrigin.x = fconvert.s(unimplemented {fstp dword [esi], st0}) */); 0055556d /* unimplemented {fstp dword [esi], st0} */; 0055556f /* unimplemented {fld st0, st0} */; 00555571 /* unimplemented {fmul st0, dword [esi+0x4]} */; 00555456 arg2->m_fOrigin.y = ((float)/* arg2->m_fOrigin.y = fconvert.s(unimplemented {fstp dword [esi+0x4], st0}) */); 00555574 /* unimplemented {fstp dword [esi+0x4], st0} */; 00555574 top_9 = top_1; 00555567 } 00555567 00555591 /* unimplemented {fmul st0, dword [esi+0x8]} */; 00555456 arg2->m_fOrigin.z = ((float)/* arg2->m_fOrigin.z = fconvert.s(unimplemented {fstp dword [esi+0x8], st0}) */); 00555597 /* unimplemented {fstp dword [esi+0x8], st0} */; 0055559a Position::heading(edi_2, ebp_1); 005555a2 arg3 = ((float)/* arg3.d = fconvert.s(unimplemented {fstp dword [esp+0x2c], st0}) */); 005555a2 /* unimplemented {fstp dword [esp+0x2c], st0} */; 005555a6 Frame::get_heading(&edi_2->frame); 005555a6 /* unimplemented {call Frame::get_heading} */; 005555ab /* unimplemented {fsubr st0, dword [esp+0x2c]} */; 005555b1 arg3 = ((float)/* arg3.d = fconvert.s(unimplemented {fst dword [esp+0x24], st0}) */); 005555b6 /* unimplemented {fabs } */; 005555b8 /* unimplemented {fld st0, dword [&F_EPSILON]} */; 005555be (/* unimplemented {fcompp } f- unimplemented {fcompp } */ - /* unimplemented {fcompp } f- unimplemented {fcompp } */); 005555be bool c0_3 = /* bool c0_3 = unimplemented {fcompp } f< unimplemented {fcompp } */ < /* bool c0_3 = unimplemented {fcompp } f< unimplemented {fcompp } */; 005555be bool c2_3 = (FCMP_UO(/* bool c2_3 = is_unordered.t(unimplemented {fcompp }, unimplemented {fcompp }) */, /* bool c2_3 = is_unordered.t(unimplemented {fcompp }, unimplemented {fcompp }) */)); 005555be bool c3_3 = /* bool c3_3 = unimplemented {fcompp } f== unimplemented {fcompp } */ == /* bool c3_3 = unimplemented {fcompp } f== unimplemented {fcompp } */; 005555be /* unimplemented {fcompp } */; 005555be /* unimplemented {fcompp } */; 005555c5 if ((*(uint8_t*)((char*)((((c0_3) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | ((((c2_3) ? 1 : 0) << 0xa) | ((((c3_3) ? 1 : 0) << 0xe) | (((top_9 + 3) & 7) << 0xb))))))[1] & 0x41) == 0) 005555c7 arg3 = 0; 005555c7 005555cf /* unimplemented {fld st0, dword [&F_EPSILON]} */; 005555d5 /* unimplemented {fchs } */; 005555d7 long double temp2_1 = ((long double)arg3); 005555d7 (/* unimplemented {fcomp st0, dword [esp+0x20]} f- temp2_1 */ - temp2_1); 005555d7 bool c0_4 = /* bool c0_4 = unimplemented {fcomp st0, dword [esp+0x20]} f< temp2_1 */ < temp2_1; 005555d7 bool c2_4 = (FCMP_UO(/* bool c2_4 = is_unordered.t(unimplemented {fcomp st0, dword [esp+0x20]}, temp2_1) */, temp2_1)); 005555d7 bool c3_4 = /* bool c3_4 = unimplemented {fcomp st0, dword [esp+0x20]} f== temp2_1 */ == temp2_1; 005555d7 /* unimplemented {fcomp st0, dword [esp+0x20]} */; 005555d7 005555e0 if ((*(uint8_t*)((char*)((((c0_4) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | ((((c2_4) ? 1 : 0) << 0xa) | ((((c3_4) ? 1 : 0) << 0xe) | (((top_9 + 3) & 7) << 0xb))))))[1] & 0x41) == 0) 005555e0 { 005555e2 /* unimplemented {fld st0, dword [esp+0x20]} */; 005555e6 /* unimplemented {fadd dword [&data_79bc60]} */; 005555ec arg3 = ((float)/* arg3.d = fconvert.s(unimplemented {fstp dword [esp+0x20], st0}) */); 005555ec /* unimplemented {fstp dword [esp+0x20], st0} */; 005555e0 } 005555e0 005555f9 Frame::set_heading(arg2, arg3); 00555445 } 00555430 } ``` NOTE (heavy x87 mush, flagging for lead decode — do not treat as porting-ready without decode): the whole back half of this function (from `005554d5` on) is a dense block of `/* unimplemented */` x87 FPU stack operations (fcom/fcomp/fcompp/fabs/fchs/fld/fstp) that BN could not lift to structured expressions, interleaved with FCOM-condition-code decoding boilerplate (`c0`/`c2`/`c3` bit extraction feeding a fake "ah test 0x45" parity check — this is the compiler's expansion of `if (x < y)` / `if (fabs(x) < EPSILON)` style float comparisons via `fcomp` + `fnstsw ax` + `sahf`, which BN failed to fold back into a plain comparison). Mechanically, best-effort reading of what's happening despite the mush: 1. Early-out unless `target_id != 0 && initialized != 0` (mirrors `UnStick`'s guard). 2. Resolve the live target object via `CPhysicsObj::GetObjectA(target_id)`; if it's gone, fall back to the cached `this->target_position` (the last known position from `HandleUpdateTarget`). 3. Compute `Position::get_offset(my_position, &out, target_position)` — the offset vector from self to target — store into `arg2->m_fOrigin` (the output `Frame`'s origin, i.e. this becomes the STICK MOVEMENT DELTA for the frame). 4. Convert that offset to local space via `Position::globaltolocalvec`, then FORCE `.z = 0` — sticky movement is horizontal-only (no auto-adjust of vertical offset; presumably z is handled by normal physics/ground snap). 5. Compute `target_radius` (cached) + `CPhysicsObj::GetRadius(physics_obj)` (own radius) feeding `Position::cylinder_distance_no_z(...)` minus a `0.3f` constant — a horizontal cylinder-distance-to-target minus a 0.3-unit buffer, producing a "how far past the desired follow-distance are we" scalar (`var_14_1`). 6. `AC1Legacy::Vector3::normalize_check_small(&arg2->m_fOrigin)` — normalizes the offset direction in place; if the vector was too small to normalize (near-zero), the offset is zeroed out entirely (no stick movement this tick — already at the target distance). 7. The x87 mush (`005554d5`–`005555f9`) computes a **clamped speed-scaled offset**: reads `CMotionInterp::get_max_speed` from the owning object's `CMotionInterp` (or falls back to a `MAX_VELOCITY` constant if no motion interpreter), clamps the per-tick offset magnitude by that speed, applies it to `x`/`y` (still `z = 0`), then separately computes a target HEADING via `Position::heading(edi_2, ebp_1)` (heading from self to target position) minus the object's current heading (`Frame::get_heading`), applies an epsilon-based snap-to-target-heading (small residual heading errors get zeroed; large ones get consumed additively — likely turning the stuck object to face the target it's following at a bounded turn rate, akin to a max-turn-rate clamp), and finally writes the resulting heading via `Frame::set_heading(arg2, arg3)`. **Net semantic despite the mush**: `StickyManager::adjust_offset` computes, for THIS tick's quantum, a bounded (speed-clamped, distance-buffered) horizontal position delta plus a bounded heading delta that steers the sticking object toward its `target_id`'s position, writing both into the `Frame*` accumulator (`arg2`) that `PositionManager::adjust_offset` shares across all three sub-managers, which `CPhysicsObj::UpdatePositionInternal` then combines into `this->m_position.frame` via `Frame::combine`. This is a follow/leash steering behavior, not a hard position clamp — it moves gradually per-tick toward the target, speed- and turn-rate-limited. ### `StickyManager::UseTime` — 0x00555610 ```c 00555610 void __fastcall StickyManager::UseTime(class StickyManager* this) 00555610 { 00555616 if (this->target_id != 0) 00555616 { 00555618 long double x87_r7_1 = ((long double)Timer::cur_time); 0055561e long double temp0_1 = ((long double)this->sticky_timeout_time); 0055561e (x87_r7_1 - temp0_1); 0055561e 00555626 if ((*(uint8_t*)((char*)((((x87_r7_1 < temp0_1) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | (((((FCMP_UO(x87_r7_1, temp0_1))) ? 1 : 0) << 0xa) | ((((x87_r7_1 == temp0_1) ? 1 : 0) << 0xe) | 0)))))[1] & 0x41) == 0) 00555626 { 00555628 class CPhysicsObj* physics_obj = this->physics_obj; 0055562b this->target_id = 0; 00555631 this->initialized = 0; 00555638 CPhysicsObj::clear_target(physics_obj); 00555641 /* tailcall */ 00555641 return CPhysicsObj::interrupt_current_movement(this->physics_obj); 00555626 } 00555616 } 00555610 } ``` NOTE: same FCOM-condition-code mush pattern as `adjust_offset`, but decodable cleanly this time — the whole block is `if (!(Timer::cur_time < sticky_timeout_time)) { unstick }`, i.e. **`if (Timer::cur_time >= this->sticky_timeout_time) { UnStick-equivalent }`**. This is a per-tick sticky-TIMEOUT check: `StickTo` (below) sets `sticky_timeout_time = Timer::cur_time + 1.0f` at stick time, and every subsequent call to `UseTime` re-checks whether that 1-second deadline has passed; if so it force-clears the stick (same 4-step teardown as `UnStick`: clear `target_id`/`initialized`, `CPhysicsObj::clear_target`, `CPhysicsObj::interrupt_current_movement`). Since `StickTo` is the ONLY writer of `sticky_timeout_time` and it's always `+1.0f` from stick-time, and `initialized` is separately set by `HandleUpdateTarget` on receiving a fresh target position — this reads as **"if we haven't heard a target-position update within 1 second of sticking, give up the stick."** The deadline is NOT refreshed by `HandleUpdateTarget` in the code we pulled — worth double-checking against a wider window, but as extracted, the timeout is a one-shot "1 second grace period to get initialized" rather than a rolling heartbeat. ### `StickyManager::Destroy` — 0x00555650 ```c 00555650 void __fastcall StickyManager::Destroy(class StickyManager* this) 00555650 { 00555659 if (this->target_id != 0) 0055565e CPhysicsObj::clear_target(this->physics_obj); 0055565e 00555667 this->target_id = 0; 0055566d int32_t var_48 = 0x796910; 0055567d int32_t var_40 = 0x3f800000; 00555685 int32_t var_3c = 0; 0055568d int32_t var_38 = 0; 00555695 int32_t var_34 = 0; 0055569d int32_t var_c = 0; 005556a5 int32_t var_8 = 0; 005556ad int32_t var_4 = 0; 005556b5 Frame::cache(&var_40); 005556c6 this->target_position.objcell_id = 0; 005556c9 Frame::operator=(&this->target_position.frame, &var_40); 005556ce this->initialized = 0; 00555650 } ``` Note this does NOT call `interrupt_current_movement` (unlike `UnStick`/`UseTime`'s timeout branch/`StickTo`'s re-stick branch/`HandleUpdateTarget`'s failure branch) — `Destroy` just tears down state (clear target id, reset cached `target_position` to an identity `Frame` via `Frame::cache`, clear `initialized`) without touching movement. It's called from `SetPhysicsObject` (when re-parenting) and the destructor. ### `StickyManager::SetPhysicsObject` — 0x005556e0 ```c 005556e0 void __thiscall StickyManager::SetPhysicsObject(class StickyManager* this, class CPhysicsObj* arg2) 005556e0 { 005556e8 if (this->physics_obj == 0) 005556e8 { 005556fe this->physics_obj = arg2; 00555702 return; 005556e8 } 005556e8 005556ea StickyManager::Destroy(this); 005556f3 this->physics_obj = arg2; 005556e0 } ``` First-time set is a plain assignment; re-set (physics_obj already non-null) tears down any existing stick state first via `Destroy`, then reassigns. ### `StickyManager::StickTo` — 0x00555710 ```c 00555710 void __thiscall StickyManager::StickTo(class StickyManager* this, uint32_t arg2, float arg3, float arg4) 00555710 { 00555716 if (this->target_id != 0) 00555716 { 00555718 class CPhysicsObj* physics_obj = this->physics_obj; 0055571b this->target_id = 0; 00555721 this->initialized = 0; 00555728 CPhysicsObj::clear_target(physics_obj); 00555730 CPhysicsObj::interrupt_current_movement(this->physics_obj); 00555716 } 00555716 00555749 this->target_radius = arg3; 0055574c class CPhysicsObj* physics_obj_1 = this->physics_obj; 0055574f this->target_id = arg2; 00555751 long double x87_r7_1 = (((long double)1f) + ((long double)Timer::cur_time)); 0055575a this->initialized = 0; 00555761 this->sticky_timeout_time = ((double)x87_r7_1); 00555771 CPhysicsObj::set_target(physics_obj_1, 0, arg2, 0.5f, ((double)((long double)0.5f))); 00555710 } ``` NOTE: `arg4` (the 4th parameter, `height`, per callers) is READ NOWHERE in this function body as extracted — only `arg2` (target id) and `arg3` (radius) are used; `target_radius = arg3` and the hardcoded `set_target(..., 0.5f, 0.5)` constants are used instead of `arg4`. Either `height` genuinely goes unused by `StickTo` itself (it's only consumed elsewhere, e.g. by `PositionManager::StickTo`'s caller-side height computation in `CPhysicsObj::stick_to_object`, purely for logging/display), or BN dropped a store. Flagging — worth a targeted look at whether `arg4` should feed the `set_target` call's `0.5f` radius/height-tolerance constants. If already stuck to something, first tears down the existing stick (same 4-step sequence as `UnStick`). Then: cache `target_radius`, set `target_id = arg2`, reset `initialized = 0` (fresh stick — no cached target position yet, must wait for `HandleUpdateTarget`), set `sticky_timeout_time = Timer::cur_time + 1.0` (the 1-second grace window `UseTime` checks), and call `CPhysicsObj::set_target(physics_obj, 0, arg2, 0.5f, 0.5)` — registers with the OWN physics object's target-tracking system (context id 0, target = arg2, with `0.5f`/`0.5` as some pair of radius/height-delta-tolerance constants) so that server/game updates about the target's position get routed back through `HandleUpdateTarget`. ### `StickyManager::HandleUpdateTarget` — 0x00555780 ```c 00555780 void __thiscall StickyManager::HandleUpdateTarget(class StickyManager* this, class TargetInfo arg2) 00555780 { 00555783 uint32_t target_id = this->target_id; 00555783 00555789 if (arg2.object_id == target_id) 00555789 { 00555799 if (arg2.status == Ok_TargetStatus) 00555799 { 0055579b uint32_t objcell_id = arg2.target_position.objcell_id; 0055579f this->initialized = 1; 005557aa this->target_position.objcell_id = objcell_id; 005557ad Frame::operator=(&this->target_position.frame, &arg2.target_position.frame); 005557b3 return; 00555799 } 00555799 005557b8 if (target_id != 0) 005557b8 { 005557ba class CPhysicsObj* physics_obj = this->physics_obj; 005557bd this->target_id = 0; 005557c3 this->initialized = 0; 005557ca CPhysicsObj::clear_target(physics_obj); 005557d2 CPhysicsObj::interrupt_current_movement(this->physics_obj); 005557b8 } 00555789 } 00555780 } ``` Only reacts if the incoming `TargetInfo.object_id` matches our current `target_id` (stale/mismatched updates from a previously-stuck target are ignored). On `Ok_TargetStatus`: cache the target's `objcell_id` + `Frame` into `this->target_position` and set `initialized = 1` — this is what `adjust_offset` reads when the live `CPhysicsObj::GetObjectA(target_id)` lookup fails, and what gates `UseTime`'s "haven't heard back within 1s" cleanup path since `initialized` starts false. On any OTHER status (target lost/out of range/error), tears down the stick with the same 4-step sequence. ### `StickyManager::~StickyManager` — 0x005557e0 (dtor) ```c 005557e0 void __fastcall StickyManager::~StickyManager(class StickyManager* this) 005557e0 { 005557e3 StickyManager::Destroy(this); 005557e8 this->target_position.vtable = 0x79285c; 005557e0 } ``` NOTE: `this->target_position.vtable = 0x79285c` — `target_position` is a `Position` struct which embeds a `Frame`; the `Frame` type apparently starts with (or BN is interpreting an early field as) a vtable-looking pointer that gets reset to a static value at destruction (likely resetting an embedded `Frame`'s internal vtable-like tag back to the base `Frame` type's identity, undoing any polymorphic Frame subtype). Kept verbatim per instructions. ### `StickyManager::Create` — 0x00555800 (ctor/factory) ```c 00555800 class StickyManager* StickyManager::Create(class CPhysicsObj* arg1) 00555800 { 00555804 void* result = operator new(0x60); 00555804 00555812 if (result == 0) 00555866 return 0; 00555866 00555814 *(uint32_t*)result = 0; 00555816 *(uint32_t*)((char*)result + 4) = 0; 0055581c *(uint32_t*)((char*)result + 8) = 0x796910; 00555823 *(uint32_t*)((char*)result + 0xc) = 0; 00555826 *(uint32_t*)((char*)result + 0x10) = 0x3f800000; 0055582c *(uint32_t*)((char*)result + 0x14) = 0; 0055582f *(uint32_t*)((char*)result + 0x18) = 0; 00555832 *(uint32_t*)((char*)result + 0x1c) = 0; 00555835 *(uint32_t*)((char*)result + 0x44) = 0; 00555838 *(uint32_t*)((char*)result + 0x48) = 0; 0055583b *(uint32_t*)((char*)result + 0x4c) = 0; 0055583e Frame::cache(((char*)result + 0x10)); 00555847 *(uint32_t*)((char*)result + 0x50) = 0; 0055584a *(uint32_t*)((char*)result + 0x54) = 0; 0055585a *(uint32_t*)((char*)result + 0x50) = arg1; 00555861 return result; 00555800 } ``` Allocates 0x60 bytes, zero-fills `target_id` (off 0) / `target_radius` (off 4), sets `target_position.objcell_id` (off 8) = `0x796910` — NOTE: this is almost certainly a vtable pointer for the embedded `Frame`/`Position` substructure rather than a literal objcell id (0x796910 recurs as a "vtable"-looking constant elsewhere in this file, e.g. `StickyManager::~StickyManager`'s `target_position.vtable = 0x79285c` uses a DIFFERENT constant for a similarly-named field, and `PositionManager::Destroy`/`MoveToObject_*` functions stack-allocate local `Frame`s with the same `0x796910` / `0x3f800000` pair as identity-transform sentinels). `Frame::cache(result + 0x10)` then initializes the embedded `Frame` (identity transform: the `0x3f800000` = `1.0f` at offset 0x10 lines up with a scale/quaternion-w identity component) at the `target_position.frame` sub-offset. Finally `physics_obj` (off 0x50) = `arg1`, `initialized` (off 0x54, zeroed) stays 0, `sticky_timeout_time` (off 0x58, implicitly zero from the earlier ops covering 0x44-0x4c) stays 0. Net: constructs an unstuck, uninitialized `StickyManager` bound to `arg1`. --- ## CPhysicsObj sticky/position seams ### `CPhysicsObj::MakePositionManager` — 0x00510210 ```c 00510210 void __fastcall CPhysicsObj::MakePositionManager(class CPhysicsObj* this) 00510210 { 0051021b if (this->position_manager == 0) 00510226 this->position_manager = PositionManager::Create(this); 00510226 00510233 if ((this->state & 1) == 0) 00510233 { 00510235 uint32_t transient_state = this->transient_state; 00510235 0051023d if (transient_state >= 0) 0051023d { 0051024b this->update_time = (*(uint32_t*)Timer::cur_time); 00510251 *(uint32_t*)((char*)this->update_time)[4] = *(int32_t*)((char*)Timer::cur_time + 4); 0051023d } 0051023d 0051025c this->transient_state = (transient_state | 0x80); 00510233 } 00510210 } ``` Lazy factory: creates `this->position_manager` only if not already present (idempotent — safe to call unconditionally, which every caller below does). The second half (`state & 1`, `transient_state`, `update_time` stamping, `transient_state |= 0x80`) is NOT specific to PositionManager — it's the same "mark object as needing an update tick / stamp last-update-time" boilerplate that recurs verbatim in `MakeMovementManager`, `MoveToObject_Internal`, `TurnToObject_Internal`, `MoveToObject`, `TurnToHeading` (all seen in the extracted callers below) — i.e. lazily creating ANY manager also flags the object as active/dirty for the next tick's `UpdateObjectInternal` sweep. `state & 1` is presumably an "already in the active/dirty set" bit being checked before re-flagging. ### `CPhysicsObj::get_position_manager` — 0x00512130 ```c 00512130 class PositionManager* __fastcall CPhysicsObj::get_position_manager(class CPhysicsObj* this) 00512130 { 00512133 CPhysicsObj::MakePositionManager(this); 0051213f return this->position_manager; 00512130 } ``` Accessor that guarantees lazy-creation before returning — callers never need their own null check. ### `CPhysicsObj::stick_to_object` — 0x005127e0 ```c 005127e0 void __thiscall CPhysicsObj::stick_to_object(class CPhysicsObj* this, uint32_t arg2) 005127e0 { 005127e0 class CPhysicsObj* this_1 = this; 005127e4 CPhysicsObj::MakePositionManager(this); 005127e9 class CObjectMaint* CPhysicsObj::obj_maint_1 = CPhysicsObj::obj_maint; 005127e9 005127f1 if (CPhysicsObj::obj_maint_1 != 0) 005127f1 { 005127f8 class CPhysicsObj* parent_2 = CObjectMaint::GetObjectA(CPhysicsObj::obj_maint_1, arg2); 005127f8 005127ff if (parent_2 != 0) 005127ff { 00512802 class CPhysicsObj* parent_1 = parent_2; 00512804 class CPhysicsObj* parent = parent_2->parent; 00512804 00512809 if (parent != 0) 0051280b parent_1 = parent; 0051280b 0051280d class CPartArray* part_array = parent_1->part_array; 0051280d 00512812 if (part_array == 0) 0051281f arg2 = 0; 00512812 else 00512819 arg2 = ((float)CPartArray::GetHeight(part_array)); 00512819 00512827 class CPartArray* part_array_1 = parent_1->part_array; 00512827 0051282c if (part_array_1 == 0) 00512839 this_1 = nullptr; 0051282c else 00512833 this_1 = ((float)CPartArray::GetRadius(part_array_1)); 00512833 00512855 PositionManager::StickTo(this->position_manager, parent_1->id, this_1, arg2); 005127ff } 005127f1 } 005127e0 } ``` `arg2` comes in as a target object id, resolved via the global `CObjectMaint` (`CObjectMaint::GetObjectA`); if the resolved object has its OWN `parent` (e.g. it's a sub-part of a multi-part object like a wielded item), the stick target is redirected to the TOP-LEVEL parent instead (`parent_1 = parent`). Radius/height for the stick are pulled from that top-level parent's `CPartArray` (`GetRadius`/`GetHeight`) — this is where `StickTo`'s `arg3`(radius)/`arg4`(height) parameters actually originate: geometry of the object being stuck to, not the sticking object itself. Finally calls `PositionManager::StickTo(this->position_manager, parent_1->id, radius, height)` — note it sticks to `parent_1->id` (the resolved top-level id), NOT the original `arg2` id passed in. ### `CPhysicsObj::unstick_from_object` — 0x0050eae0 ```c 0050eae0 void __fastcall CPhysicsObj::unstick_from_object(class CPhysicsObj* this) 0050eae0 { 0050eae0 class PositionManager* position_manager = this->position_manager; 0050eae0 0050eae8 if (position_manager == 0) 0050eaef return; 0050eaef 0050eaea /* tailcall */ 0050eaea return PositionManager::UnStick(position_manager); 0050eae0 } ``` Thin wrapper: null-safe forward to `PositionManager::UnStick` (which itself forwards to `StickyManager::UnStick`). Unlike `MakePositionManager`, this does NOT lazily create — if there's no `position_manager` yet, there's nothing to unstick. --- ## Callers — where retail invokes these in the per-tick physics chain ### `CPhysicsObj::UpdatePositionInternal` (0x00512c30) — calls `PositionManager::adjust_offset` ```c 00512c30 void __thiscall CPhysicsObj::UpdatePositionInternal(class CPhysicsObj* this, float arg2, class Frame* arg3) 00512c30 { 00512c3c int32_t var_40 = 0x3f800000; 00512c44 int32_t var_3c = 0; 00512c4c int32_t var_38 = 0; 00512c54 int32_t var_34 = 0; 00512c5c float var_c = 0f; 00512c64 float var_8 = 0f; 00512c6c float var_4 = 0f; 00512c74 Frame::cache(&var_40); 00512c74 00512c86 if ((*(uint8_t*)((char*)((int16_t)this->state))[1] & 0x40) == 0) 00512c86 { 00512c88 class CPartArray* part_array = this->part_array; 00512c88 00512c8d if (part_array != 0) 00512c95 CPartArray::Update(part_array, arg2, &var_40); 00512c95 00512ca1 if ((this->transient_state & 2) == 0) 00512ca1 { 00512cd5 float var_c_2 = ((float)(((long double)var_c) * ((long double)0f))); 00512ce3 float var_8_2 = ((float)(((long double)var_8) * ((long double)0f))); 00512cf1 float var_4_2 = ((float)(((long double)var_4) * ((long double)0f))); 00512ca1 } 00512ca1 else 00512ca1 { 00512ca3 long double x87_r7_1 = ((long double)this->m_scale); 00512caf float var_c_1 = ((float)(((long double)var_c) * x87_r7_1)); 00512cb9 float var_8_1 = ((float)(((long double)var_8) * x87_r7_1)); 00512cc3 float var_4_1 = ((float)(((long double)var_4) * x87_r7_1)); 00512ca1 } 00512c86 } 00512c86 00512cf5 class PositionManager* position_manager = this->position_manager; 00512cf5 00512cfd if (position_manager != 0) 00512cfd { 00512d0a float var_54; 00512d0a var_54 = ((double)((long double)arg2)); 00512d0e PositionManager::adjust_offset(position_manager, &var_40, var_54); 00512cfd } 00512cfd 00512d22 Frame::combine(arg3, &this->m_position.frame, &var_40); 00512d22 00512d30 if ((*(uint8_t*)((char*)((int16_t)this->state))[1] & 0x40) == 0) 00512d36 CPhysicsObj::UpdatePhysicsInternal(this, arg2, arg3); 00512d36 00512d3d CPhysicsObj::process_hooks(this); 00512c30 } ``` **This is the per-tick chokepoint.** `arg2` is the frame's elapsed-time quantum. A local identity `Frame` (`var_40`, cached via `Frame::cache`) is built up: first `CPartArray::Update(part_array, quantum, &var_40)` (animation-driven part-array motion contributes to the frame delta), then — if a `position_manager` exists — `PositionManager::adjust_offset(position_manager, &var_40, quantum)` ADDS the sticky/interpolation/constraint contributions into the SAME `var_40` accumulator (this is the call that fans out to `StickyManager::adjust_offset`, `InterpolationManager::adjust_offset`, `ConstraintManager::adjust_offset` in sequence). Finally `Frame::combine(arg3, &this->m_position.frame, &var_40)` composes the accumulated delta frame onto the object's actual position, producing the output frame `arg3` (this is the frame that gets fed into `UpdatePhysicsInternal` next). So sticky steering literally competes/composes with animation-driven part-array movement in the SAME per-tick delta-frame before physics/collision resolves it. ### `CPhysicsObj::UpdateObjectInternal` (0x005156b0) — calls `PositionManager::UseTime`, and transitively `UpdatePositionInternal` Full function extracted (contains the `UpdatePositionInternal` call + the `UseTime` call later in the same tick): ```c 005156b0 void __thiscall CPhysicsObj::UpdateObjectInternal(class CPhysicsObj* this, float arg2) 005156b0 { 005156b6 uint16_t transient_state = ((int16_t)this->transient_state); 005156b6 005156bf if (transient_state >= 0) 005156bf { 005159b8 label_5159b8: 005159b8 class ParticleManager* particle_manager = this->particle_manager; 005159b8 005159c0 if (particle_manager != 0) 005159c2 ParticleManager::UpdateParticles(particle_manager); 005159c2 005159c7 class ScriptManager* script_manager = this->script_manager; 005159c7 005159cc if (script_manager != 0) 005159d0 ScriptManager::UpdateScripts(script_manager); 005156bf } 005156bf else if (this->cell != 0) 005156cf { 005156d8 if ((*(uint8_t*)((char*)transient_state)[1] & 1) != 0) 005156de CPhysicsObj::set_ethereal(this, 0, 0); 005156de 005156e7 this->jumped_this_frame = 0; 005156ed int32_t var_48 = 0x796910; 005156f5 int32_t var_44_1 = 0; 005156f9 int32_t var_40 = 0x3f800000; 00515701 int32_t var_3c_1 = 0; 00515709 int32_t var_38_1 = 0; 00515711 int32_t var_34_1 = 0; 00515719 float x = 0f; 00515721 int32_t var_8_1 = 0; 00515729 int32_t var_4_1 = 0; 00515731 Frame::cache(&var_40); 00515745 uint32_t objcell_id = this->m_position.objcell_id; 00515749 long double st0_1 = CPhysicsObj::UpdatePositionInternal(this, arg2, &var_40); 0051574e class CPartArray* part_array = this->part_array; 00515753 uint32_t eax_1; 00515753 00515753 if (part_array != 0) 00515755 eax_1 = CPartArray::GetNumSphere(part_array); 00515755 0051575c int32_t __return; 0051575c 0051575c if ((part_array != 0 && eax_1 != 0)) 0051575c { 005157ec if (AC1Legacy::Vector3::operator==(&x, &this->m_position.frame.m_fOrigin) == 0) 005157ec { 00515846 uint32_t state = this->state; 00515846 0051584f if ((*(uint8_t*)((char*)state)[1] & 1) != 0) 0051584f { 0051585b AC1Legacy::Vector3::operator-(&x, &__return, &this->m_position.frame.m_fOrigin); 00515864 Vector3::Normalize(&__return); 00515872 Frame::set_vector_heading(&var_40, &__return); 0051584f } 0051584f else if (((state & "activation type (%s) with '%s' b…") != 0 && AC1Legacy::Vector3::is_zero(&this->m_velocityVector) == 0)) 0051587e { 00515898 int32_t var_74_5 = AC1Legacy::Vector3::get_heading(&this->m_velocityVector); 005158a0 Frame::set_heading(&var_40, ((float)st0_1)); 0051587e } 0051587e 005158b2 class CTransition* eax_10 = CPhysicsObj::transition(this, &this->m_position, &var_48, 0); 005158b2 005158bb if (eax_10 == 0) 005158bb { 00515924 x = this->m_position.frame.m_fOrigin.x; 0051592c float z_2 = this->m_position.frame.m_fOrigin.z; 00515933 float y_2 = this->m_position.frame.m_fOrigin.y; 00515937 CPhysicsObj::set_frame(this, &var_40); 0051593c __return = 0; 00515948 this->cached_velocity.x = __return; 00515948 this->cached_velocity.y = 0f; 00515948 this->cached_velocity.z = 0f; 005158bb } 005158bb else 005158bb { 005158cb Position::get_offset(&this->m_position, &__return, &eax_10->sphere_path.curr_pos); 005158de void __return_1; 005158de int32_t* eax_12 = Vector3::operator/(&__return, &__return_1, arg2); 005158e5 float ecx_20 = eax_12[1]; 005158e8 __return = *(uint32_t*)eax_12; 005158ff this->cached_velocity.x = __return; 005158ff this->cached_velocity.y = ecx_20; 005158ff this->cached_velocity.z = eax_12[2]; 00515914 CPhysicsObj::SetPositionInternal(this, eax_10); 005158bb } 005157ec } 005157ec else 005157ec { 005157f7 x = this->m_position.frame.m_fOrigin.x; 00515802 float y_1 = this->m_position.frame.m_fOrigin.y; 00515806 float z_1 = this->m_position.frame.m_fOrigin.z; 0051580a CPhysicsObj::set_frame(this, &var_40); 0051580f __return = 0; 0051581b this->cached_velocity.x = __return; 0051581b this->cached_velocity.y = 0f; 0051581b this->cached_velocity.z = 0f; 005157ec } 0051575c } 0051575c else 0051575c { 00515764 if (this->movement_manager == 0) 00515764 { 00515766 uint32_t transient_state_1 = this->transient_state; 00515766 0051576e if ((transient_state_1 & 2) != 0) 00515775 this->transient_state = (transient_state_1 & 0xffffff7f); 00515764 } 00515764 00515789 x = this->m_position.frame.m_fOrigin.x; 00515794 float y = this->m_position.frame.m_fOrigin.y; 00515798 float z = this->m_position.frame.m_fOrigin.z; 0051579c CPhysicsObj::set_frame(this, &var_40); 005157a1 __return = 0; 005157ad this->cached_velocity.x = __return; 005157ad this->cached_velocity.y = 0f; 005157ad this->cached_velocity.z = 0f; 005157ec } 00515970 class DetectionManager* detection_manager = this->detection_manager; 00515970 00515978 if (detection_manager != 0) 0051597a DetectionManager::CheckDetection(detection_manager); 0051597a 0051597f class TargetManager* target_manager = this->target_manager; 0051597f 00515987 if (target_manager != 0) 00515989 TargetManager::HandleTargetting(target_manager); 00515989 0051598e class MovementManager* movement_manager = this->movement_manager; 0051598e 00515996 if (movement_manager != 0) 00515998 MovementManager::UseTime(movement_manager); 00515998 0051599d class CPartArray* part_array_1 = this->part_array; 0051599d 005159a2 if (part_array_1 != 0) 005159a4 CPartArray::HandleMovement(part_array_1); 005159a4 005159a9 class PositionManager* position_manager = this->position_manager; 005159a9 005159b1 if (position_manager != 0) 005159b3 PositionManager::UseTime(position_manager); 005159b3 00515753 goto label_5159b8; 005156cf } 005156b0 } ``` **Ordering within one tick (the `else if (this->cell != 0)` branch — the "object is in the world" path):** 1. `CPhysicsObj::UpdatePositionInternal(this, arg2, &var_40)` — builds the delta frame (part-array animation + `PositionManager::adjust_offset` sticky/interp/constraint contributions), returns a heading-ish scalar in `st0_1`. 2. If the object has sphere-collision parts (`part_array->GetNumSphere() != 0`): compute a facing/heading update, run `CPhysicsObj::transition(...)` (collision/movement resolution against the delta frame), then either `set_frame` directly (transition failed/no-op) or `SetPositionInternal` with the transition's resolved sphere-path position (transition succeeded) — this is where the sticky-computed delta actually gets validated against collision before being committed. 3. Else (no collision parts): just `set_frame` directly with the delta frame — no collision check. 4. THEN: `DetectionManager::CheckDetection`, `TargetManager::HandleTargetting`, `MovementManager::UseTime`, `CPartArray::HandleMovement`, and finally **`PositionManager::UseTime(position_manager)`** — the sticky-timeout check runs LAST in the tick, AFTER the position has already been moved/collision-resolved for this frame. So a stick that times out this tick still got one more frame of steered movement + collision resolution before being cleared. ### `CPhysicsObj::HandleUpdateTarget` (0x00512bc0) — calls `PositionManager::HandleUpdateTarget` ```c 00512bc0 void __thiscall CPhysicsObj::HandleUpdateTarget(class CPhysicsObj* this, class TargetInfo arg2) 00512bc0 { 00512bc9 if (arg2.context_id == 0) 00512bc9 { 00512bd3 void var_d4; 00512bd3 00512bd3 if (this->movement_manager != 0) 00512bd3 { 00512be5 TargetInfo::TargetInfo(&var_d4, &arg2); 00512bf0 MovementManager::HandleUpdateTarget(this->movement_manager, var_d4); 00512bd3 } 00512bd3 00512bfd if (this->position_manager != 0) 00512bfd { 00512c0f TargetInfo::TargetInfo(&var_d4, &arg2); 00512c1a PositionManager::HandleUpdateTarget(this->position_manager, var_d4); 00512bfd } 00512bc9 } 00512bc0 } ``` Top-level entry point for target-position updates arriving from elsewhere (server messages / target-tracking system), gated on `context_id == 0` (context 0 presumably means "default/self" target tracking vs. some other numbered context). Fans the SAME `TargetInfo` out to BOTH `MovementManager::HandleUpdateTarget` (move-to logic) AND `PositionManager::HandleUpdateTarget` (sticky logic) if each manager exists. This is the producer for the `StickyManager::HandleUpdateTarget` consumer described above. ### `CPhysicsObj::exit_world` (0x00514e60) — calls `PositionManager::UnStick` ```c 00514e60 void __fastcall CPhysicsObj::exit_world(class CPhysicsObj* this) 00514e60 { 00514e63 class CPartArray* part_array = this->part_array; 00514e63 00514e68 if (part_array != 0) 00514e6a CPartArray::HandleExitWorld(part_array); 00514e6a 00514e6f class MovementManager* movement_manager = this->movement_manager; 00514e6f 00514e77 if (movement_manager != 0) 00514e79 MovementManager::HandleExitWorld(movement_manager); 00514e79 00514e7e class PositionManager* position_manager = this->position_manager; 00514e7e 00514e86 if (position_manager != 0) 00514e88 PositionManager::UnStick(position_manager); 00514e88 00514e8d class TargetManager* target_manager = this->target_manager; 00514e8d 00514e95 if (target_manager != 0) 00514e95 { 00514e97 TargetManager::ClearTarget(target_manager); 00514ea4 TargetManager::NotifyVoyeurOfEvent(this->target_manager, ExitWorld_TargetStatus); 00514e95 } 00514e95 00514ea9 class DetectionManager* detection_manager = this->detection_manager; 00514ea9 00514eb1 if (detection_manager != 0) 00514eb5 DetectionManager::DestroyDetectionCylsphere(detection_manager, 0); 00514eb5 00514ebe CPhysicsObj::report_collision_end(this, 1); 00514e60 } ``` Object leaving the world (despawn/logout): unstick unconditionally (only `UnStick`, not full teardown — `position_manager` itself is kept alive). ### `CPhysicsObj::teleport_hook` (0x00514ed0) — calls `PositionManager::UnStick` + `StopInterpolating` + `UnConstrain` ```c 00514ed0 void __fastcall CPhysicsObj::teleport_hook(class CPhysicsObj* this, int32_t arg2) 00514ed0 { 00514ed3 class MovementManager* movement_manager = this->movement_manager; 00514ed3 00514edb if (movement_manager != 0) 00514edb { 00514edd int32_t var_8_1 = 0x3c; 00514edf uint32_t edx; 00514edf MovementManager::CancelMoveTo(movement_manager, edx); 00514edb } 00514edb 00514ee4 class PositionManager* position_manager = this->position_manager; 00514ee4 00514eec if (position_manager != 0) 00514eee PositionManager::UnStick(position_manager); 00514eee 00514ef3 class PositionManager* position_manager_1 = this->position_manager; 00514ef3 00514efb if (position_manager_1 != 0) 00514efd PositionManager::StopInterpolating(position_manager_1); 00514efd 00514f02 class PositionManager* position_manager_2 = this->position_manager; 00514f02 00514f0a if (position_manager_2 != 0) 00514f0c PositionManager::UnConstrain(position_manager_2); 00514f0c 00514f11 class TargetManager* target_manager = this->target_manager; 00514f11 00514f19 if (target_manager != 0) 00514f19 { 00514f1b TargetManager::ClearTarget(target_manager); 00514f28 TargetManager::NotifyVoyeurOfEvent(this->target_manager, Teleported_TargetStatus); 00514f19 } 00514f19 00514f31 CPhysicsObj::report_collision_end(this, 1); 00514ed0 } ``` Teleport is the ONE place all three PositionManager sub-behaviors get explicitly torn down together (cancel any move-to, unstick, stop interpolating, unconstrain) — makes sense: after a teleport, none of the three "gradually approach some reference" behaviors should still be steering toward a pre-teleport reference frame. ### `MovementManager::unpack_movement` (0x00524440) — calls `CPhysicsObj::unstick_from_object` Context excerpt (full function is long; showing the relevant unstick call site inside the inbound-motion-unpacking case-0 branch): ```c 00524440 int32_t __thiscall MovementManager::unpack_movement(class MovementManager* this, void** arg2, uint32_t arg3) 00524440 { 0052444f if (this->motion_interpreter != 0) 0052444f { 00524455 class CPhysicsObj* physics_obj = this->physics_obj; 00524455 0052445a if (physics_obj != 0) 0052445a { 00524460 CPhysicsObj::interrupt_current_movement(physics_obj); 00524468 CPhysicsObj::unstick_from_object(this->physics_obj); ... 00524551 case 0: 00524551 { 00524551 InterpretedMotionState::UnPack(&var_28, arg2, arg3); ... 0052457c MovementManager::move_to_interpreted_state(this, &var_28); 0052457c 00524583 if (ebx_3 != 0) 00524589 CPhysicsObj::stick_to_object(this->physics_obj, ebx_3); 00524589 0052458e this->motion_interpreter->standing_longjump = (ebp_1 & 0x200); ... ``` Two things happen in `unpack_movement`: (1) unconditionally at the top of the whole function, EVERY inbound motion packet interrupts current movement AND unsticks (`unstick_from_object`) before any of the packet's specific motion state is applied — i.e. any new motion command from the network clears a prior stick; (2) later, inside `case 0` (one specific unpacked-motion sub-case), if the unpacked state included a sticky-target object id (`ebx_3`, read conditionally from the packed stream when a state flag bit is set), `stick_to_object` is called to establish a NEW stick to that id. This is the network-driven "server told the client to stick this object to another object" path (e.g. mounting, carrying, or similar attach behaviors). ### `CMotionInterp::MotionDone` (0x00527ec0) — calls `CPhysicsObj::unstick_from_object` ```c 00527ec0 void __fastcall CMotionInterp::MotionDone(class CMotionInterp* this, int32_t arg2) 00527ec0 { 00527ec3 class CPhysicsObj* physics_obj = this->physics_obj; 00527ec3 00527ec8 if (physics_obj != 0) 00527ec8 { 00527eca class LListData* head_ = this->pending_motions.head_; 00527eca 00527ed2 if (head_ != 0) 00527ed2 { 00527edb if ((*(int32_t*)((char*)head_ + 8) & 0x10000000) != 0) 00527edb { 00527edd CPhysicsObj::unstick_from_object(physics_obj); 00527ee5 InterpretedMotionState::RemoveAction(&this->interpreted_state); 00527eed RawMotionState::RemoveAction(&this->raw_state); 00527edb } 00527edb 00527ef2 class LListData* head__1 = this->pending_motions.head_; 00527ef2 00527efa if (head__1 != 0) 00527efa { 00527efc class LListData* llist_next = head__1->llist_next; 00527f00 this->pending_motions.head_ = llist_next; 00527f00 00527f06 if (llist_next == 0) 00527f08 this->pending_motions.tail_ = llist_next; 00527f08 00527f0f head__1->llist_next = 0; 00527f15 operator delete(head__1); 00527efa } 00527ed2 } 00527ec8 } 00527ec0 } ``` And its sibling `CMotionInterp::HandleExitWorld` (0x00527f30) has the identical unstick-on-flag-bit pattern (queue-head motion's flag `0x10000000` set → unstick) when draining `pending_motions` on exit-world. The `0x10000000` bit on a pending motion's flags field marks "this motion action implies/requires a stick, so completing or force-flushing it must unstick." Consistent with `unpack_movement`'s pattern: sticks are tied to the lifecycle of a specific motion action, not held independently. ### `MoveToManager::PerformMovement` (0x0052a900) — calls `CPhysicsObj::unstick_from_object` ```c 0052a900 uint32_t __thiscall MoveToManager::PerformMovement(class MoveToManager* this, class MovementStruct const* arg2) 0052a900 { 0052a901 int32_t var_8 = 0x36; 0052a905 uint32_t edx; 0052a905 MoveToManager::CancelMoveTo(this, edx); 0052a910 CPhysicsObj::unstick_from_object(this->physics_obj); 0052a910 0052a923 switch ((arg2->type - 6)) 0052a923 { 0052a940 case 0: 0052a940 { 0052a940 MoveToManager::MoveToObject(this, arg2->object_id, arg2->top_level_id, arg2->radius, arg2->height, arg2->params); 0052a940 break; 0052a955 case 1: 0052a955 { 0052a955 MoveToManager::MoveToPosition(this, &arg2->pos, arg2->params); 0052a955 break; ... ``` Every new `MoveToManager` movement command starts by cancelling any in-flight move-to AND unsticking — same "new movement intent clears prior stick" pattern as `unpack_movement`. ### `MoveToManager::BeginNextNode` (0x00529cb0) — calls `PositionManager::StickTo` via `get_position_manager` ```c 00529ccb return; 00529cbe } 00529cbe 00529ce5 head_ = *(uint8_t*)((char*)this->movement_params.__inner0 + 0); 00529ce5 00529ced if (head_ < 0) 00529ced { 00529cef float sought_object_height = this->sought_object_height; 00529cf5 float sought_object_radius = this->sought_object_radius; 00529d00 uint32_t top_level_object_id = this->top_level_object_id; 00529d0c int32_t edx_3 = MoveToManager::CleanUp(this); 00529d11 class CPhysicsObj* physics_obj = this->physics_obj; 00529d11 00529d19 if (physics_obj != 0) 00529d19 { 00529d1b int32_t var_14_1 = 0; 00529d1d CPhysicsObj::StopCompletely(physics_obj, edx_3); 00529d19 } 00529d19 00529d3a PositionManager::StickTo(CPhysicsObj::get_position_manager(this->physics_obj), top_level_object_id, sought_object_radius, sought_object_height); 00529d44 return; 00529ced } ``` This is the OTHER stick-establishment path, distinct from `stick_to_object`: when a `MoveToManager` move-to-object node completes and a "sticky" flag is set in `movement_params` (the `head_ < 0` branch — a sign-bit check on a packed byte field, likely a `CanStick`/`Sticky` movement-parameter bit), the mover: cleans up the move-to state, stops movement completely (`CPhysicsObj::StopCompletely`), then sticks to `top_level_object_id` (the object it was moving toward) using the CACHED `sought_object_radius`/`sought_object_height` (computed earlier when the move-to node was set up, not re-derived from `CPartArray` like `stick_to_object` does). This is the "arrived at destination object, now hold position relative to it" transition — e.g. finishing a move-to-object command that has a stick-on-arrival semantic. --- ## Files referenced - `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines 352066–352644 (PositionManager + StickyManager method bodies, contiguous block) - `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines 276403–276426, 278204–278222, 278344–278364, 280236–280241, 280559–280595, 280794–280866, 283079–283151, 283611–283757 (CPhysicsObj seams + UpdatePositionInternal/UpdateObjectInternal per-tick chain) - `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines 300563–300621 (MovementManager::unpack_movement) - `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines 305238–305271 (CMotionInterp::MotionDone) - `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines 307123–307161 (MoveToManager::BeginNextNode) - `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines 307871–307878 (MoveToManager::PerformMovement) - `docs/research/named-retail/acclient.h` lines 30952–30958 (struct PositionManager) - `docs/research/named-retail/acclient.h` lines 31518–31526 (struct StickyManager) - `docs/research/named-retail/acclient.h` lines 31529–31537 (struct ConstraintManager, context)