The retail movement-manager family the R4 MoveToManager port left as do-not-invent seams (decomp §9f/§9g). Faithful C# ports of retail's PositionManager facade + StickyManager + ConstraintManager + the TargetManager voyeur system, with full conformance tests. NO wiring yet — purely additive, no behavior change. Wiring (retiring TS-39 sticky + AP-79 target adapter) is R5-V2/V3. New Core classes (src/AcDream.Core/Physics/Motion/): - StickyManager (0x00555400): follow-a-target steering. adjust_offset's dense x87 mush decoded via ACE (StickyRadius 0.3, StickyTime 1.0, follow speed ×5 / fallback 15) — speed-clamped signed-distance steer + bounded turn-to-face; 1 s watchdog; Ok→initialized / non-Ok→teardown. - ConstraintManager (0x00556090): the server-position rubber-band leash. 90% IsFullyConstrained jump gate + grounded linear brake taper. Structural only — acdream never ARMS it (retail arms from SmartBox::HandleReceivedPosition, which acdream lacks, with two x87 constants BN elided). IsFullyConstrained stays false = TS-35 behavior; leash-arming + the unknown constants are a deferred issue. - PositionManager facade (0x00555160): lazy Sticky/Constraint + fan-out. - TargetManager (0x0051a370) + TargettedVoyeurInfo: the peer-to-peer voyeur subscription system (0.5 s throttle, 10 s staleness, send-on-drift-past-radius, dead-reckon GetInterpolatedPosition). A faithful superset of the AP-79 adapter — SetTarget subscribes ON the target; the target's HandleTargetting pushes updates back. - IPhysicsObjHost: the CPhysicsObj back-pointer seam (position/velocity/ radius/contact/GetObjectA + target-tracking fan-out) the App wires per entity in V2/V3. MotionDeltaFrame: mutable retail-Frame delta accumulator. Supporting: - TargetInfo extended to the full retail 10-field struct (additive defaults keep the R4 4-arg call sites compiling). - MoveToMath: signed CylinderDistanceNoZ, NormalizeCheckSmall, GlobalToLocalVec. - Rename: the misnamed AcDream.Core.Physics.PositionManager (a remote anim+interp per-frame combiner, NOT the retail facade) → RemoteMotion Combiner, freeing the name and removing the ambiguity that breaks every file importing both Physics + Physics.Motion (GameWindow will in V2/V3). Tests: 42 new conformance cases (Sticky/Constraint/Position facade + TargetManager incl. the full cross-entity voyeur round-trip). Full suite 4006 green (+2 skipped), no regressions. Decomp + ACE cross-ref + port plan: docs/research/2026-07-03-r5-managers/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
70 KiB
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)
/* 3468 */
struct __cppobj PositionManager
{
InterpolationManager *interpolation_manager;
StickyManager *sticky_manager;
ConstraintManager *constraint_manager;
CPhysicsObj *physics_obj;
};
struct StickyManager (acclient.h:31518, type id 3466)
/* 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)
/* 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
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
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
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)
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)
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
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)
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)
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)
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
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)
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)
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)
005553a0 void __fastcall PositionManager::~PositionManager(class PositionManager* this)
005553a0 {
005553a0 /* tailcall */
005553a0 return PositionManager::Destroy(this);
005553a0 }
PositionManager::IsInterpolating — 0x005553b0 (context)
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
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
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
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:
- Early-out unless
target_id != 0 && initialized != 0(mirrorsUnStick's guard). - Resolve the live target object via
CPhysicsObj::GetObjectA(target_id); if it's gone, fall back to the cachedthis->target_position(the last known position fromHandleUpdateTarget). - Compute
Position::get_offset(my_position, &out, target_position)— the offset vector from self to target — store intoarg2->m_fOrigin(the outputFrame's origin, i.e. this becomes the STICK MOVEMENT DELTA for the frame). - 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). - Compute
target_radius(cached) +CPhysicsObj::GetRadius(physics_obj)(own radius) feedingPosition::cylinder_distance_no_z(...)minus a0.3fconstant — 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). 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).- The x87 mush (
005554d5–005555f9) computes a clamped speed-scaled offset: readsCMotionInterp::get_max_speedfrom the owning object'sCMotionInterp(or falls back to aMAX_VELOCITYconstant if no motion interpreter), clamps the per-tick offset magnitude by that speed, applies it tox/y(stillz = 0), then separately computes a target HEADING viaPosition::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 viaFrame::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
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
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
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
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
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)
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)
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 Frames 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
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
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
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
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
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):
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):
CPhysicsObj::UpdatePositionInternal(this, arg2, &var_40)— builds the delta frame (part-array animation +PositionManager::adjust_offsetsticky/interp/constraint contributions), returns a heading-ish scalar inst0_1.- If the object has sphere-collision parts (
part_array->GetNumSphere() != 0): compute a facing/heading update, runCPhysicsObj::transition(...)(collision/movement resolution against the delta frame), then eitherset_framedirectly (transition failed/no-op) orSetPositionInternalwith the transition's resolved sphere-path position (transition succeeded) — this is where the sticky-computed delta actually gets validated against collision before being committed. - Else (no collision parts): just
set_framedirectly with the delta frame — no collision check. - THEN:
DetectionManager::CheckDetection,TargetManager::HandleTargetting,MovementManager::UseTime,CPartArray::HandleMovement, and finallyPositionManager::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
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
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
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):
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
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
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
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.txtlines 352066–352644 (PositionManager + StickyManager method bodies, contiguous block)docs/research/named-retail/acclient_2013_pseudo_c.txtlines 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.txtlines 300563–300621 (MovementManager::unpack_movement)docs/research/named-retail/acclient_2013_pseudo_c.txtlines 305238–305271 (CMotionInterp::MotionDone)docs/research/named-retail/acclient_2013_pseudo_c.txtlines 307123–307161 (MoveToManager::BeginNextNode)docs/research/named-retail/acclient_2013_pseudo_c.txtlines 307871–307878 (MoveToManager::PerformMovement)docs/research/named-retail/acclient.hlines 30952–30958 (struct PositionManager)docs/research/named-retail/acclient.hlines 31518–31526 (struct StickyManager)docs/research/named-retail/acclient.hlines 31529–31537 (struct ConstraintManager, context)