# #167 ConstraintManager leash — constants recovered + arming flow (Campaign P P5) **2026-07-30.** Both #167 blockers are now research-solved; only the port remains. No cdb session was needed: the two "unknown x87 constants" were recovered by decoding the raw machine code of the matching binary (`C:\Users\erikn\Downloads\acclient.exe`, v11.4186, PDB-paired — verified GUID match per the retail debugger toolchain doc). ## 1. The getters, byte-decoded (FACT) `CPhysicsObj::GetStartConstraintDistance @ 0x0050ebc0` and `GetMaxConstraintDistance @ 0x0050ec10` are FPU-return getters whose `fld` operands Binary Ninja elided (the pseudo-C shows a bare `this->m_position;`). Raw bytes (file offset 0x10ebc0/0x10ec10): ``` 3b 0d 58 3d 84 00 cmp ecx, [0x00843d58] ; this == player_object? 75 1d jnz non_player 8b 41 4c mov eax, [ecx+0x4c] ; m_position.objcell_id 25 ff ff 00 00 and eax, 0xFFFF 3d 00 01 00 00 cmp eax, 0x100 73 07 jae indoor ; low16 >= 0x100 = EnvCell d9 05 fld dword [outdoor_const] c3 ret indoor: d9 05 <..> fld dword [indoor_const] c3 ret non_player: ; identical cell test, second constant pair ``` Constant values read from `.rdata`: | | player outdoor | player indoor | remote outdoor | remote indoor | |---|---|---|---|---| | Start (0x007c6abc..c8) | **10.0** | **5.0** | 10.0 | 5.0 | | Max (0x007c6acc..d8) | **50.0** | **20.0** | 50.0 | 20.0 | Two consequences (FACT): 1. **The player-vs-remote branch is vestigial** — both sides load identical values. Effective semantics: `start = outdoor 10 m / indoor 5 m`, `max = outdoor 50 m / indoor 20 m` (indoor = cell low16 ≥ 0x100). 2. **ACE's `GetStartConstraintDistance` is INVERTED** (`ACE PhysicsObj.cs:620`: outdoor 5 / indoor 10). ACE's max mapping (outdoor 50 / indoor 20) matches the binary. Do NOT copy ACE's start mapping. (feedback_acme_oracle / binary-wins rule.) ## 2. The arming flow — `SmartBox::HandleReceivedPosition @ 0x00453fd0` (FACT) Pseudo-C lines ~92940-93060. After the update-time staleness gates and `unset_parent`/`SetPlacementFrame` handling: - **Remote object** (`arg2 != this->player`): call `MoveOrTeleport(obj, &recvPos, ts, arg5, arg6)`; **only if it returns nonzero** (the position was NOT hard-teleport-applied), arm the leash **anchored to the object's own current position**: `ConstrainTo(obj, &obj->m_position, start, max)` (0x00454254-72). - **Player, teleport-newer** (`newer_event(TELEPORT_TS, ts)`): `SmartBox::TeleportPlayer(&recvPos)`, then `ConstrainTo(player, &recvPos, start, max)` — anchored to the **received** position — then `set_velocity(player, {0,0,0}, 1)` (0x0045415f-c0). - **Player, normal**: `ConstrainTo(player, &recvPos, start, max)` anchored to the received position; then, if `cmdinterp->UsePositionFromServer() && arg5`, `InterpolateTo(&recvPos, -GetAutonomyLevel())` (0x004541c9-422c). The taper/enforcement side (`ConstraintManager::UseTime` feeding `adjust_offset`, `IsFullyConstrained = ConstraintDistanceMax * 0.9 < offset`) is already ported in `src/AcDream.Core/Physics/Motion/ConstraintManager.cs` (R5-V1, `docs/research/2026-07-03-r5-managers/`); it has simply never been armed. ## 3. Port shape for P5 (INFERENCE — implementation guidance) 1. Add the four-constant getters (outdoor/indoor by full cell id low16) at the body/host layer; keep the vestigial player/remote split OUT (note it in a code comment with this doc as the cite). 2. Arm at acdream's inbound-position equivalents of the three branches: the remote UpdatePosition acceptance tail (post-`MoveOrTeleport` routing in the live-entity network update path) and the local player's accepted-Position path (normal + teleport). Anchor per §2. 3. `PhysicsBody.IsFullyConstrained` (register TS-35 stub) becomes a read through `PositionManager`/`ConstraintManager`, so `jump_is_allowed`'s ported gate fires (WeenieError 0x47) while rubber-banding. TS-35 and #167 retire together, same commit. 4. Conformance tests: constant table incl. the ACE-inversion pin (outdoor start MUST be 10, not 5); leash-armed jump refusal; remote-vs-player anchor difference; teleport-branch velocity zero. ## Open questions None for the constants/flow. Remaining implementation risk is only where acdream's position-acceptance seams sit today (J6.3 moved teleport correlation into Runtime — the implementer must find the current owner rather than trusting older file cites). ## As-ported (Campaign P Slice P5, 2026-07-30) The implementation risk flagged above resolved to these CURRENT seam owners (post-J-slices) — recorded here so the next reader doesn't have to re-derive them: - **Constants** — `src/AcDream.Core/Physics/Motion/ConstraintDistance.cs`. Keyed purely on the object's own full cell id's low 16 bits (`>= 0x0100` = indoor); the vestigial player/remote branch from §1 is deliberately not represented as an API parameter. - **Remote arm** — `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs`, the inbound `UpdatePosition` handler's remote branch (`update.Guid != _playerServerGuid`), immediately after the `remotePlacementRequired` hard-teleport block returns (that block already covers retail's `MoveOrTeleport` Branch A / hard-place case; everything reached past it is "did not hard-place"). One call site covers BOTH player-remote and NPC remotes — retail's `SmartBox::HandleReceivedPosition` doesn't distinguish them either, only `GetStart/MaxConstraintDistance`'s now-omitted vestigial branch did. Anchored to the live `IPhysicsObjHost.Position` (which reads `RemoteMotion.Body.Position` + the tracked cell id), matching retail's "anchored to the object's own current position" — since the anchor and `_host.Position` read are the same value at call time, `ConstraintManager.ConstrainTo`'s initial offset is always 0 regardless of whether the routing above just far-snapped or left a near-correction queued. - **Remote per-tick taper + `IsFullyConstrained` push** — already wired pre-P5 for the taper (`RuntimeRemotePhysicsUpdater.Tick`/`TickHidden` call `PositionManager.AdjustOffset` every tick via the pre-existing R5-V3 sticky/constraint chain); P5 added the `PhysicsBody.IsFullyConstrained = host.PositionManager.IsFullyConstrained()` push at the same two call sites, since `MotionInterpreter` only holds a `PhysicsBody` (no host reference) and needs a live value to read. - **Local player arm** — `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs`: `SetPositionCore` (teleport: `UnConstrain` then re-`ConstrainTo` after the existing `StopCompletelyAtPhysicsObjectBoundary` velocity zero — composed, not duplicated) and `CommitPreparedPosition` (mirrors the same pair for the deferred player-mode-entry commit path); `BlipPosition` (ForcePosition: `ConstrainTo` only, no teardown — matches `SmartBox::BlipPlayer` surviving motion/velocity/stick). Anchored to `_body.CellPosition` (the just-applied received position). - **Local player per-tick taper + push** — `PlayerMovementController.Update` already called `PositionManager.AdjustOffset` every physics tick pre-P5; P5 added the `_body.IsFullyConstrained = PositionManager?.IsFullyConstrained() ?? false` push immediately after, at the same chokepoint. - **TS-35 retirement** — `PhysicsBody.IsFullyConstrained` stayed a plain settable bool (not a computed property) so the ~40 pre-existing direct-set unit tests keep working; the per-tick pumps above are its single writers now, matching the project's per-entity single-owner-write pattern.