PhysicsBody.IsFullyConstrained now reflects real ConstraintManager state (pushed every tick by the same per-tick pumps commit 2 wired), so jump_is_allowed's already-ported gate (WeenieError 0x47) actually fires while an object is rubber-banding hard against a server position correction, closing the last piece of #167. Housekeeping: - Delete register row TS-35 (retired: the write side is no longer stubbed). - Rewrite the stale doc comments on PhysicsBody.IsFullyConstrained, ConstraintManager (class + IsFullyConstrained), PositionManager.ConstrainTo, EntityPhysicsHost.PositionManager, and PlayerMovementController.PositionManager that described the leash as permanently unarmed/stubbed. - Close #167 in ISSUES.md citing the research doc and commitse0629145/7719d25b. - Add an "as-ported" addendum to docs/research/2026-07-30-constraint-leash-constants.md naming the actual current seam owners (the doc's own open question flagged this as implementer-verify-required post-J-slices). - Update docs/plans/2026-07-29-physics-parity-campaign.md's P5 status and CLAUDE.md's Campaign P summary to reflect #167's closure (items #153/#72 remain open in P5). Verification: complete solution suite green - 9,978 tests, 5 skips, 0 failures across all 9 test projects (Core.Tests, Runtime.Tests, App.Tests, Headless.Tests, Core.Net.Tests, Content.Tests, UI.Abstractions.Tests, Bake.Tests, Cli.Tests).
145 lines
7.6 KiB
Markdown
145 lines
7.6 KiB
Markdown
# #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 <rdata> 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.
|