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>
32 KiB
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 */)
/* 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
/* 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
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
005560c0 void __fastcall ConstraintManager::UnConstrain(class ConstraintManager* this)
005560c0 {
005560c0 this->is_constrained = 0;
005560c0 }
ConstraintManager::IsFullyConstrained — 0x005560d0
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
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)
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
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:
- No-op if no
physics_objor notis_constrained. - 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_fOriginby a lerp fraction based on(max - pos_offset) / (max - start), gated by a comparison ofpos_offsetvsconstraint_distance_start. - Else: zero the incoming frame delta entirely (fully clamp movement).
- If a second transient-state flag byte's bit 0 is set: scale the incoming frame delta
- Unconditionally (after the above), accumulate:
constraint_pos_offset += arg2->m_fOrigin.x(note: only the.xcomponent is added —arg2->m_fOriginis read twice at0055620e/00556211with 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
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.
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.
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):
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 deletes all three
sub-managers, ConstraintManager last:
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)
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.
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.
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):
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):
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):
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
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
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::HandleReceivedPositionto 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), andCPhysicsObj::teleport_hookto disarm it on any teleport, andCMotionInterp::jump_is_allowedto 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.