acdream/docs/research/2026-07-03-r5-managers/r5-positionmanager-sticky-decomp.md
Erik 3d89446d98 feat(physics): R5-V1 — port PositionManager/Sticky/Constraint + TargetManager (Core, unwired)
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>
2026-07-03 19:34:49 +02:00

1533 lines
70 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 352066352644
(PositionManager + StickyManager method bodies, contiguous block)
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines 276403276426, 278204278222,
278344278364, 280236280241, 280559280595, 280794280866, 283079283151, 283611283757
(CPhysicsObj seams + UpdatePositionInternal/UpdateObjectInternal per-tick chain)
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines 300563300621 (MovementManager::unpack_movement)
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines 305238305271 (CMotionInterp::MotionDone)
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines 307123307161 (MoveToManager::BeginNextNode)
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines 307871307878 (MoveToManager::PerformMovement)
- `docs/research/named-retail/acclient.h` lines 3095230958 (struct PositionManager)
- `docs/research/named-retail/acclient.h` lines 3151831526 (struct StickyManager)
- `docs/research/named-retail/acclient.h` lines 3152931537 (struct ConstraintManager, context)