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>
679 lines
32 KiB
Markdown
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.
|