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