acdream/docs/research/2026-07-03-r5-managers/r5-constraintmanager-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

679 lines
32 KiB
Markdown

# ConstraintManager — retail decomp extract
Source: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR build, PDB-named).
Struct source: `docs/research/named-retail/acclient.h`.
**Address correction:** the task listed `CPhysicsObj::IsFullyConstrained` at `0x0050f730`.
The actual address in the corpus is **`0x0050ec60`**. Verified by grepping the definition
line (`276520:0050ec60 int32_t __fastcall CPhysicsObj::IsFullyConstrained(...)`)
and cross-checked against its caller in `CMotionInterp::jump_is_allowed` at `0x005282fd`.
---
## struct ConstraintManager (acclient.h, comment `/* 3467 */`)
```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;
};
```
## struct PositionManager (acclient.h, comment `/* 3468 */`) — the owning object
```c
/* 3468 */
struct __cppobj PositionManager
{
InterpolationManager *interpolation_manager;
StickyManager *sticky_manager;
ConstraintManager *constraint_manager;
CPhysicsObj *physics_obj;
};
```
Note the field order in `PositionManager` (`interpolation_manager`, `sticky_manager`,
`constraint_manager`, `physics_obj`) matches the `PositionManager::Create` allocation
writes to offsets `0x0`, `0x4`, `0x8`, `0xc` respectively (see below) and the
`PositionManager::Destroy` teardown order (`interpolation_manager``sticky_manager`
`constraint_manager`).
`ConstraintManager` field layout maps onto `ConstraintManager::Create`'s raw offset
writes: `physics_obj`=`0x0`, `is_constrained`=`0x4`, `constraint_pos_offset`=`0x8` — wait,
per the decompiled writes below the offsets are actually `0x0`/`0x4`/`0x8`/`0xc`
(`vtable` field of `Position constraint_pos` at `0xc`)/`0x10`.../`0x48` is
`constraint_distance_start`, `0x4c` is `constraint_distance_max`. The compiler emits a
`Position` (which itself embeds a `Frame` with its own vtable-looking sentinel field —
see NOTE in `~ConstraintManager` below) between `constraint_pos_offset` and
`constraint_distance_start`, consistent with the struct's declared member order.
---
## ConstraintManager::SetPhysicsObject — `0x00556090`
```c
00556090 void __thiscall ConstraintManager::SetPhysicsObject(class ConstraintManager* this, class CPhysicsObj* arg2)
00556090 {
00556096 if (this->physics_obj == 0)
00556096 {
005560ad this->physics_obj = arg2;
005560af return;
00556096 }
00556096
00556098 this->physics_obj = 0;
0055609a this->is_constrained = 0;
0055609d this->constraint_pos_offset = 0f;
005560a4 this->physics_obj = arg2;
00556090 }
```
## ConstraintManager::UnConstrain — `0x005560c0`
```c
005560c0 void __fastcall ConstraintManager::UnConstrain(class ConstraintManager* this)
005560c0 {
005560c0 this->is_constrained = 0;
005560c0 }
```
## ConstraintManager::IsFullyConstrained — `0x005560d0`
```c
005560d0 int32_t __fastcall ConstraintManager::IsFullyConstrained(class ConstraintManager const* this)
005560d0 {
005560d0 long double x87_r7 = ((long double)this->constraint_pos_offset);
005560d6 long double x87_r6_1 = (((long double)this->constraint_distance_max) * ((long double)0.90000000000000002));
005560dc (x87_r6_1 - x87_r7);
005560de int32_t eax;
005560de eax = ((((x87_r6_1 < x87_r7) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | (((((FCMP_UO(x87_r6_1, x87_r7))) ? 1 : 0) << 0xa) | ((((x87_r6_1 == x87_r7) ? 1 : 0) << 0xe) | 0))));
005560e0 bool p = /* bool p = unimplemented {test ah, 0x5} */;
005560e0
005560e3 if (p)
005560ed return 0;
005560ed
005560ea return 1;
005560d0 }
```
NOTE (garbled bitfield mush / x87 flags mush): the `eax = (...)` line is Binary Ninja's
attempt to render the x87 `FCOMI`/`FSTSW`+`SAHF`-style compare-and-test-flags sequence as
bit-packed pseudocode. It is computing `constraint_distance_max * 0.9 <=> constraint_pos_offset`
and then `test ah, 0x5` checks the ZF/CF-equivalent bits packed into `ah` after `fnstsw ax`.
The semantic read: `p` is true when `(constraint_distance_max * 0.9) < constraint_pos_offset`
OR the compare was unordered (NaN) — i.e. `test ah,5` tests bits 0 (C0/"below") and 2
(C3/"equal") of the FPU status word as loaded into AH, the classic x87 `jbe`-equivalent
pattern. So: **`IsFullyConstrained` returns `false` (0) if `constraint_pos_offset >=
0.9 * constraint_distance_max` (or unordered), else returns `true` (1)**. In plain terms:
the object counts as "fully constrained" while it is still within 90% of the max leash
distance; once it has drifted past 90% of that distance it is no longer "fully" constrained
(this is the gate `CMotionInterp::jump_is_allowed` reads to block jump attempts while
straining at the very end of a constraint leash).
## ConstraintManager::~ConstraintManager — `0x005560f0`
```c
005560f0 void __fastcall ConstraintManager::~ConstraintManager(class ConstraintManager* this)
005560f0 {
005560f2 this->is_constrained = 0;
005560f5 this->constraint_pos_offset = 0f;
005560f8 this->physics_obj = 0;
005560fa this->constraint_pos.vtable = 0x79285c;
005560f0 }
```
NOTE: `this->constraint_pos.vtable = 0x79285c``constraint_pos` is a `Position` field
(struct member, not a pointer), so this is Binary Ninja's rendering of the embedded
`Frame`'s vtable-pointer slot being reset to its static vtable address as part of the
`Position`/`Frame` subobject's implicit destructor inlining. Not a real "vtable swap";
just the compiler zeroing/resetting the embedded Frame's identity field during teardown.
## ConstraintManager::Create — `0x00556110` (factory)
```c
00556110 class ConstraintManager* ConstraintManager::Create(class CPhysicsObj* arg1)
00556110 {
00556114 void* result = operator new(0x5c);
00556114
00556122 if (result == 0)
00556177 return 0;
00556177
00556124 *(uint32_t*)result = 0;
00556126 *(uint32_t*)((char*)result + 4) = 0;
00556129 *(uint32_t*)((char*)result + 8) = 0;
0055612f *(uint32_t*)((char*)result + 0xc) = 0x796910;
00556136 *(uint32_t*)((char*)result + 0x10) = 0;
00556139 *(uint32_t*)((char*)result + 0x14) = 0x3f800000;
0055613f *(uint32_t*)((char*)result + 0x18) = 0;
00556142 *(uint32_t*)((char*)result + 0x1c) = 0;
00556145 *(uint32_t*)((char*)result + 0x20) = 0;
00556148 *(uint32_t*)((char*)result + 0x48) = 0;
0055614b *(uint32_t*)((char*)result + 0x4c) = 0;
0055614e *(uint32_t*)((char*)result + 0x50) = 0;
00556151 Frame::cache(((char*)result + 0x14));
00556156 *(uint32_t*)((char*)result + 0x54) = 0;
00556159 *(uint32_t*)((char*)result + 0x58) = 0;
00556159
0055615e if (*(uint32_t*)result != 0)
0055615e {
00556160 *(uint32_t*)((char*)result + 4) = 0;
00556163 *(uint32_t*)((char*)result + 8) = 0;
00556166 *(uint32_t*)result = 0;
0055615e }
0055615e
0055616c *(uint32_t*)result = arg1;
00556172 return result;
00556110 }
```
NOTE: allocation is `0x5c` (92) bytes — `sizeof(ConstraintManager)`. Field-offset mapping
against the struct decl: `+0x0`=`physics_obj`, `+0x4`=`is_constrained`,
`+0x8`=`constraint_pos_offset`, `+0xc..0x50`=`constraint_pos` (embedded `Position`, whose
own `objcell_id`/`frame` subfields explain the `0x796910` vtable-like constant at `+0xc`
and the `Frame::cache` call seeding the rotation quaternion identity `w=1.0`
i.e. `0x3f800000` at `+0x14`), `+0x48`=`constraint_distance_start`,
`+0x4c`=`constraint_distance_max`. The trailing `+0x54`/`+0x58` zeroing is past the
declared struct fields in the header excerpt we have — likely padding/alignment or a
field the header comment block truncated; not load-bearing for the port (all consumed
fields — `physics_obj`, `is_constrained`, `constraint_pos_offset`, `constraint_pos`,
`constraint_distance_start`, `constraint_distance_max` — are accounted for).
The odd `if (*(uint32_t*)result != 0) { zero everything }` right after construction reads
as dead/defensive code from an inlined check that can't actually trigger here (the fields
were all just zeroed above) — flagging as NOTE, not altering.
## ConstraintManager::adjust_offset — `0x00556180`
```c
00556180 void __thiscall ConstraintManager::adjust_offset(class ConstraintManager* this, class Frame* arg2, double arg3)
00556180 {
00556186 class CPhysicsObj* physics_obj = this->physics_obj;
00556186
0055618a if (physics_obj != 0)
0055618a {
00556190 int32_t is_constrained = this->is_constrained;
00556190
00556195 if (is_constrained != 0)
00556195 {
005561a7 if ((physics_obj->transient_state & 1) != 0)
005561a7 {
005561a9 long double x87_r7_1 = ((long double)this->constraint_pos_offset);
005561ac long double temp1_1 = ((long double)this->constraint_distance_max);
005561ac (x87_r7_1 - temp1_1);
005561af physics_obj = ((((x87_r7_1 < temp1_1) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | (((((FCMP_UO(x87_r7_1, temp1_1))) ? 1 : 0) << 0xa) | ((((x87_r7_1 == temp1_1) ? 1 : 0) << 0xe) | 0))));
005561af
005561b4 if ((*(uint8_t*)((char*)physics_obj)[1] & 1) != 0)
005561b4 {
005561e7 long double x87_r7_2 = ((long double)this->constraint_pos_offset);
005561ea long double temp2_1 = ((long double)this->constraint_distance_start);
005561ea (x87_r7_2 - temp2_1);
005561ed physics_obj = ((((x87_r7_2 < temp2_1) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | (((((FCMP_UO(x87_r7_2, temp2_1))) ? 1 : 0) << 0xa) | ((((x87_r7_2 == temp2_1) ? 1 : 0) << 0xe) | 0))));
005561ef bool p_1 = /* bool p_1 = unimplemented {test ah, 0x41} */;
005561ef
005561f2 if (p_1)
005561f2 {
005561f7 int32_t is_constrained_1 = is_constrained;
00556209 Vector3::operator*=(&arg2->m_fOrigin, ((float)((((long double)this->constraint_distance_max) - ((long double)this->constraint_pos_offset)) / (((long double)this->constraint_distance_max) - ((long double)this->constraint_distance_start)))));
005561f2 }
005561b4 }
005561b4 else
005561b4 {
005561c2 arg2->m_fOrigin.x = 0;
005561c2 arg2->m_fOrigin.y = 0f;
005561c2 arg2->m_fOrigin.z = 0f;
005561b4 }
005561a7 }
005561a7
0055620e arg2->m_fOrigin;
00556211 arg2->m_fOrigin;
00556233 this->constraint_pos_offset = ((float)(((long double)arg2->m_fOrigin.x) + ((long double)this->constraint_pos_offset)));
00556195 }
0055618a }
00556180 }
```
NOTE (garbled bitfield mush + BN variable-reuse artifact): `physics_obj` gets *reused* as
a scratch int32 to hold the packed x87 comparison-flags value at `005561af` and again at
`005561ed` — this is Binary Ninja recycling the SSA slot, NOT a real reassignment of the
`CPhysicsObj*` pointer. The `this->physics_obj` local captured at `00556186` is what's
actually read at `005561a7` (`physics_obj->transient_state`) and `005561b4`
(`*(uint8_t*)((char*)physics_obj)[1] & 1` — this is checking a byte of `transient_state`
again, offset `+1`, i.e. a second flag byte inside the same bitfield/word). Do not port
the reused `physics_obj` int32 as if it becomes a different physics object; it's the same
pointer, just overwritten as dead-value scratch space by the decompiler's register
allocator view.
`bool p_1 = /* unimplemented {test ah, 0x41} */` is the same x87-flags-in-AH pattern as
`IsFullyConstrained` above, testing bits 0 and 6 this time (C0 + C6/C2 combo depending on
encoding) — the classic `jbe`-vs-`jae` variant. Given the surrounding compare
(`constraint_pos_offset < constraint_distance_start` or equal), and that the guarded body
computes a **lerp fraction** `(constraint_distance_max - constraint_pos_offset) /
(constraint_distance_max - constraint_distance_start)` applied to `arg2->m_fOrigin` via
`*=`, the semantic read is: **when the object has NOT yet reached (or has just reached)
`constraint_distance_start`, scale the frame's offset delta by how far through the
start→max leash band the object currently sits** (a ramp/taper multiplier — presumably
smoothly reducing how much of the requested frame delta gets applied as the leash
tightens). `test ah,0x41` semantically reads as "less-than-or-unordered-or-equal"
(`p_1` true when NOT clearly greater), so the ramp only applies while still inside the
band; once past `constraint_distance_start` typical port intent should skip the scale
(leave `arg2->m_fOrigin` alone) — consistent with the `else` branch two levels up which
zeroes `m_fOrigin` outright when `transient_state`'s second flag bit is clear.
Mechanically, regardless of the exact flag polarity (worth a live cdb single-step check
if the port's leash-taper feel diverges from retail), the function's shape is:
1. No-op if no `physics_obj` or not `is_constrained`.
2. If `transient_state & 1` (some "active"/"in contact" style flag):
- If a second transient-state flag byte's bit 0 is set: scale the incoming frame delta
`arg2->m_fOrigin` by a lerp fraction based on `(max - pos_offset) / (max - start)`,
gated by a comparison of `pos_offset` vs `constraint_distance_start`.
- Else: zero the incoming frame delta entirely (fully clamp movement).
3. Unconditionally (after the above), accumulate: `constraint_pos_offset +=
arg2->m_fOrigin.x` (note: only the `.x` component is added — `arg2->m_fOrigin` is read
twice at `0055620e`/`00556211` with no visible effect, likely a debug/no-op dead read
from the decompiler, or hints there's a per-component variant BN collapsed; only the
final `.x`-add survived as an observable store).
## ConstraintManager::ConstrainTo — `0x00556240`
```c
00556240 void __thiscall ConstraintManager::ConstrainTo(class ConstraintManager* this, class Position const* arg2, float arg3, float arg4)
00556240 {
00556248 this->is_constrained = 1;
00556259 this->constraint_pos.objcell_id = arg2->objcell_id;
0055625c Frame::operator=(&this->constraint_pos.frame, &arg2->frame);
00556271 this->constraint_distance_start = arg3;
00556274 this->constraint_distance_max = arg4;
0055627c this->constraint_pos_offset = ((float)Position::distance(arg2, &this->physics_obj->m_position));
00556240 }
```
Straightforward: pins the leash anchor (`constraint_pos` = copy of `arg2`'s cell+frame),
sets `is_constrained = true`, sets the start/max leash-band radii from `arg3`/`arg4`, and
initializes `constraint_pos_offset` to the CURRENT distance from the anchor to the
physics object's live position (`Position::distance(arg2, &physics_obj->m_position)`) —
i.e. the leash starts already "extended" to wherever the object presently is relative to
the constraint anchor, not to zero.
---
## PositionManager-level seams (the actual public API — ConstraintManager is
## lazily-created and private underneath)
`ConstraintManager` is never touched directly from outside `PositionManager`.
`PositionManager` lazily creates it on first `ConstrainTo` call and forwards through it.
```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 }
```
Chains ALL THREE sub-managers' `adjust_offset` in a fixed order:
`InterpolationManager` → `StickyManager` → `ConstraintManager`, each optional (only
called if that sub-manager exists). This is the per-frame(?) offset-adjustment
dispatcher `PositionManager` uses to let interpolation/sticky/constraint all have a say
in shaping a `Frame` delta before it's applied.
```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 }
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 }
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`) wires a freshly-allocated `PositionManager`'s
`physics_obj` back-pointer into any already-non-null sub-managers (only relevant for
copy/re-init paths since a fresh `PositionManager` starts with all-null sub-managers):
```c
005552d0 class PositionManager* PositionManager::Create(class CPhysicsObj* arg1)
005552d0 {
005552d3 void* result = operator new(0x10);
...
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 }
```
`PositionManager::Destroy` (`0x00555340`) tears down and `delete`s all three
sub-managers, `ConstraintManager` last:
```c
00555340 void __fastcall PositionManager::Destroy(class PositionManager* this)
00555340 {
...
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 }
```
---
## CPhysicsObj-level seams (public API callers actually use)
```c
0050ec10 void __fastcall CPhysicsObj::GetMaxConstraintDistance(class CPhysicsObj const* this)
0050ec10 {
0050ec16 if (this == CPhysicsObj::player_object)
0050ec16 {
0050ec18 this->m_position;
0050ec2d return;
0050ec16 }
0050ec16
0050ec35 this->m_position;
0050ec10 }
0050ebc0 void __fastcall CPhysicsObj::GetStartConstraintDistance(class CPhysicsObj const* this)
0050ebc0 {
0050ebc6 if (this == CPhysicsObj::player_object)
0050ebc6 {
0050ebc8 this->m_position;
0050ebdd return;
0050ebc6 }
0050ebc6
0050ebe5 this->m_position;
0050ebc0 }
```
NOTE (BN decompilation artifact / x87 return-value elision): both functions have `void`
signatures per BN's guessed prototype but are clearly meant to RETURN a float (they're
called as `ecx_26 = CPhysicsObj::GetMaxConstraintDistance(arg2)` immediately followed by
`(float)st0_6` casts at the call sites — an x87 FPU return value living in `st0` that
Binary Ninja failed to attach to the declared return type). Body-wise all we get is
`this->m_position;` as a bare expression on both the player-branch and fallthrough
paths — BN elided the actual field read/constant selection (this is likely
`this->m_position.something` or a per-type constant lookup that got collapsed to a
dead-looking statement). **This function needs a live cdb read of `st0` after the call,
or a Ghidra re-decompile with a corrected float-return signature, to recover the actual
values.** Given the call-site pattern (constraining the player and other movers to a
"home" position after teleport/movement-timeout in `SmartBox::HandleReceivedPosition`),
the two functions almost certainly return small fixed-radius constants (a "start easing"
radius and a "max leash" radius), likely DIFFERENT for the player vs. non-player case
(hence the `this == CPhysicsObj::player_object` branch in both). Do not guess the literal
values — flag as an open research item before porting numeric constants.
```c
00510520 void __thiscall CPhysicsObj::ConstrainTo(class CPhysicsObj* this, class Position const* arg2, float arg3, float arg4)
00510520 {
00510523 CPhysicsObj::MakePositionManager(this);
00510528 class PositionManager* position_manager = this->position_manager;
00510528
00510531 if (position_manager == 0)
00510538 return;
00510538
00510533 /* tailcall */
00510533 return PositionManager::ConstrainTo(position_manager, arg2, arg3, arg4);
00510520 }
```
`CPhysicsObj::ConstrainTo` lazily ensures a `PositionManager` exists
(`MakePositionManager`) then forwards. This is the entry point external code calls.
```c
0050ec60 int32_t __fastcall CPhysicsObj::IsFullyConstrained(class CPhysicsObj const* this)
0050ec60 {
0050ec60 class PositionManager* position_manager = this->position_manager;
0050ec60
0050ec68 if (position_manager == 0)
0050ec71 return 0;
0050ec71
0050ec6a /* tailcall */
0050ec6a return PositionManager::IsFullyConstrained(position_manager);
0050ec60 }
```
(Address correction noted at top of doc: this is `0x0050ec60`, not the task-supplied
`0x0050f730`.)
There is no separate `CPhysicsObj::UnConstrain` — callers go straight to
`PositionManager::UnConstrain(this->position_manager)` (see caller list below); only
`ConstrainTo` and `IsFullyConstrained` got a `CPhysicsObj`-level convenience wrapper.
---
## CALLERS — when does retail actually constrain an object?
### 1. `SmartBox::HandleReceivedPosition` (`0x00453fd0`) — THE constrain call site
All three live `CPhysicsObj::ConstrainTo` calls in the entire corpus are inside this one
function, at three different branches of its position-update-reconciliation logic:
**Branch A — non-player mover, after a successful move/teleport resolve (`0x00454254`):**
```c
0045414d if (arg2 != this->player)
0045414d {
00454254 if (CPhysicsObj::MoveOrTeleport(arg2, &var_48, arg8, arg5, arg6) != 0)
00454254 {
00454258 int32_t ecx_26;
00454258 ecx_26 = CPhysicsObj::GetMaxConstraintDistance(arg2);
0045425d int32_t var_68_14 = ecx_26;
00454263 ecx_28 = CPhysicsObj::GetStartConstraintDistance(arg2);
00454268 int32_t var_6c_9 = ecx_28;
00454272 CPhysicsObj::ConstrainTo(arg2, &arg2->m_position, ((float)st0_7), ((float)st0_6));
00454254 }
00454254
00454254 return;
0045414d }
```
For a non-player object (`arg2`), once `MoveOrTeleport` succeeds, it is constrained
**to its own current position** (`&arg2->m_position` as the anchor) with start/max radii
from `GetStartConstraintDistance`/`GetMaxConstraintDistance`.
**Branch B — player, on a fresh TELEPORT timestamp event (`0x0045415f`):**
```c
0045415f if (CPhysicsObj::newer_event(arg2, TELEPORT_TS, arg8) != 0)
0045415f {
00454168 SmartBox::TeleportPlayer(this, &var_48);
0045416f ecx_14 = CPhysicsObj::GetMaxConstraintDistance(arg2);
0045417a ecx_16 = CPhysicsObj::GetStartConstraintDistance(arg2);
0045418a CPhysicsObj::ConstrainTo(arg2, &var_48, ((float)st0_2), ((float)st0_1));
0045418f class CPhysicsObj* player_2 = this->player;
0045419c int32_t var_54 = 0; // zero vector
004541b4 CPhysicsObj::set_velocity(player_2, &var_54, 1);
004541c0 return;
0045415f }
```
On a server teleport of the local player, `SmartBox::TeleportPlayer` snaps position, then
the player is constrained **to the newly-received server position** (`&var_48`, the
decoded incoming `Position`), and velocity is zeroed.
**Branch C — player, fallthrough / non-teleport received-position path (`0x004541c9`):**
```c
004541c9 ecx_19 = CPhysicsObj::GetMaxConstraintDistance(this->player);
004541d8 ecx_21 = CPhysicsObj::GetStartConstraintDistance(this->player);
004541ec CPhysicsObj::ConstrainTo(this->player, &var_48, ((float)st0_5), ((float)st0_4));
004541f1 class CommandInterpreter* cmdinterp_1 = this->cmdinterp;
0045420a if ((cmdinterp_1->vtable->UsePositionFromServer(cmdinterp_1) != 0 && arg5 != 0))
0045420a {
... CPhysicsObj::InterpolateTo(arg2, &var_48, ...);
```
Every OTHER received server position update for the local player (not a teleport-flagged
event) ALSO constrains the player to the received position (`&var_48`), and then —
depending on `UsePositionFromServer`/autonomy settings — may additionally kick off
`InterpolateTo`. So the leash gets re-anchored on essentially every server position
correction, whether or not interpolation is used to visually smooth toward it.
**Summary for Branch A/B/C:** retail constrains an object to a `Position` (self or
server-received) with a start/max leash-band pair **every time `SmartBox` processes an
inbound position update for that object** — this is the "rubber-band" leash mechanism
that keeps the client's locally-simulated position from drifting too far from the
server-authoritative position between updates. It's re-armed (re-`ConstrainTo`'d) on
every inbound position packet, not set once.
### 2. `CPhysicsObj::teleport_hook` (`0x00514ed0`) — THE unconstrain call site
```c
00514ed0 void __fastcall CPhysicsObj::teleport_hook(class CPhysicsObj* this, int32_t arg2)
00514ed0 {
00514ed3 class MovementManager* movement_manager = this->movement_manager;
00514edb if (movement_manager != 0)
00514edb MovementManager::CancelMoveTo(movement_manager, edx);
00514ee4 class PositionManager* position_manager = this->position_manager;
00514eec if (position_manager != 0)
00514eee PositionManager::UnStick(position_manager);
00514ef3 class PositionManager* position_manager_1 = this->position_manager;
00514efb if (position_manager_1 != 0)
00514efd PositionManager::StopInterpolating(position_manager_1);
00514f02 class PositionManager* position_manager_2 = this->position_manager;
00514f0a if (position_manager_2 != 0)
00514f0c PositionManager::UnConstrain(position_manager_2);
00514f11 class TargetManager* target_manager = this->target_manager;
00514f19 if (target_manager != 0)
00514f19 {
00514f1b TargetManager::ClearTarget(target_manager);
00514f28 TargetManager::NotifyVoyeurOfEvent(this->target_manager, Teleported_TargetStatus);
00514f19 }
00514f31 CPhysicsObj::report_collision_end(this, 1);
00514ed0 }
```
The ONLY `UnConstrain` call in the corpus. `teleport_hook` is a general "this object just
got relocated in a way that invalidates all continuity state" cleanup: it cancels any
active `MoveTo`, un-sticks (StickyManager), stops interpolation, **un-constrains**, clears
target tracking, and ends collision reporting. So the leash is torn down whenever an
object teleports (any teleport, not just the player's) — makes sense, since a teleport by
definition means "the position just legitimately jumped," so the anti-drift leash from
the PREVIOUS anchor must be dropped rather than fight the teleport.
### 3. `CMotionInterp::jump_is_allowed` (`0x005282b0`) — THE read call site
```c
005282b0 uint32_t __thiscall CMotionInterp::jump_is_allowed(class CMotionInterp* this, float arg2, int32_t* arg3)
005282b0 {
005282b8 if (this->physics_obj != 0)
005282b8 {
...
005282fd if (CPhysicsObj::IsFullyConstrained(this->physics_obj) != 0)
00528305 return 0x47;
00528305
00528308 class LListData* head_ = this->pending_motions.head_;
...
```
`jump_is_allowed` reads `IsFullyConstrained` and, if true, immediately returns error code
`0x47` (rejecting the jump attempt) before even checking pending motions / jump-charge
state. This is the ONLY read-site of `IsFullyConstrained` in the corpus. Ties back to the
mechanical read of `ConstraintManager::IsFullyConstrained` above: while the object is
still within 90% of its max leash distance from the constraint anchor, it counts as
"fully constrained" and jumping is blocked outright — i.e. **you cannot jump while the
client's simulated position is being actively rubber-banded back toward a server-received
position inside the tight leash band.** Only once you've drifted past 90% of the leash
(or the leash has been dropped via `UnConstrain`/teleport) does the jump-blocking gate
open.
---
## CPhysicsObj-level "constrain" seam grep (exhaustive)
Full result of `grep "::ConstrainTo(\|::UnConstrain(\|::IsFullyConstrained("` across the
corpus — every call site, no filtering:
```
93007 CPhysicsObj::ConstrainTo(arg2, &arg2->m_position, ...) [SmartBox::HandleReceivedPosition, Branch A]
93024 CPhysicsObj::ConstrainTo(arg2, &var_48, ...) [SmartBox::HandleReceivedPosition, Branch B]
93041 CPhysicsObj::ConstrainTo(this->player, &var_48, ...) [SmartBox::HandleReceivedPosition, Branch C]
276520 CPhysicsObj::IsFullyConstrained (definition)
276529 -> PositionManager::IsFullyConstrained (tailcall)
278353 CPhysicsObj::ConstrainTo (definition)
278363 -> PositionManager::ConstrainTo (tailcall)
283140 PositionManager::UnConstrain(position_manager_2) [CPhysicsObj::teleport_hook]
305524 CPhysicsObj::IsFullyConstrained(this->physics_obj) != 0 [CMotionInterp::jump_is_allowed]
352186 PositionManager::ConstrainTo (definition)
352198 -> ConstraintManager::ConstrainTo (tailcall)
352203 PositionManager::UnConstrain (definition)
352212 -> ConstraintManager::UnConstrain (tailcall)
352217 PositionManager::IsFullyConstrained (definition)
352226 -> ConstraintManager::IsFullyConstrained (tailcall)
353405 ConstraintManager::UnConstrain (definition)
353413 ConstraintManager::IsFullyConstrained (definition)
353528 ConstraintManager::ConstrainTo (definition)
```
No other call sites exist anywhere in the 1.4M-line corpus. The entire constrain
mechanism is used EXCLUSIVELY by:
- `SmartBox::HandleReceivedPosition` to arm/re-arm the leash on inbound position updates
(3 branches: self-anchor for remote movers, server-anchor for player teleport, server-
anchor for player non-teleport updates), and
- `CPhysicsObj::teleport_hook` to disarm it on any teleport, and
- `CMotionInterp::jump_is_allowed` to read it as a jump-blocking gate.
This is a narrow, special-purpose "server position rubber-band leash," NOT a general
physics constraint/joint system.