diff --git a/CLAUDE.md b/CLAUDE.md index a030a823..eeed4445 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,8 +131,10 @@ was user-accepted 2026-07-29. **Campaign P — physics retail-feel parity (`docs/plans/2026-07-29-physics-parity-campaign.md`) is the ACTIVE user-directed detour** closing every physics-scope gap from the 2026-07-29 audit (stat-coupled movement, response-layer edge family, remote residuals, -entry restrictions/water, #262/#153/#167, ledger). Vendor browsing (Slice 5) -and authoritative vendor transactions resume after Campaign P closes. +entry restrictions/water, #262/#153/#167, ledger). P5 item 1 (#167, +ConstraintManager leash constants + arming) is CLOSED 2026-07-30; #153 and +#72 remain open in P5. Vendor browsing (Slice 5) and authoritative vendor +transactions resume after Campaign P closes. **Modern Runtime/performance status:** Slices A–K of `docs/plans/2026-07-24-modern-runtime-architecture.md` are complete. Slice L is diff --git a/docs/ISSUES.md b/docs/ISSUES.md index b01f33cb..12ecd79f 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -4217,7 +4217,20 @@ Test: `GpuWorldStateTests.RelocateEntity_StrandedInPending_MovesToLoadedTarget` ## #167 — ConstraintManager leash unported (arming + two unknown x87 constants) -**Status:** OPEN (deferred, filed 2026-07-03 during R5-V1) +**Status:** DONE — 2026-07-30 (Campaign P Slice P5). Both blockers were +research-solved without a cdb session: the two x87-elided constants were +recovered by disassembling the matching retail binary's raw machine code +(`docs/research/2026-07-30-constraint-leash-constants.md`), and the arming +site is now every current acdream inbound-position acceptance seam. Commit +`e0629145` (constants + `ConstraintDistance`), commit `7719d25b` (arming at +`LiveEntityNetworkUpdateController` for remotes and +`PlayerMovementController.SetPosition`/`BlipPosition` for the local player, +plus the per-tick `PhysicsBody.IsFullyConstrained` push), and this commit +(TS-35 retirement + stale-comment cleanup). Register row **TS-35** is +deleted in the same session. Full Core/Runtime/App suites pass with no +regressions; new conformance tests cover leash-armed jump refusal, +teleport-vs-blip anchor/teardown behavior, taper reduction over ticks, and +the remote-tick `IsFullyConstrained` push. **Severity:** LOW (server-position rubber-band + jump-during-rubber-band gate) **Component:** physics, constraint @@ -4233,25 +4246,31 @@ not `SmartBox`, so nothing calls `PositionManager.ConstrainTo`, and `IsFullyConstrained` stays false (= register **TS-35**'s current stub behavior — jump never blocked by the leash). -**Blockers:** (1) the two distance constants are **x87 float returns BN -elided** — `GetStart/MaxConstraintDistance` decompile to a bare +**Blockers (RESOLVED):** (1) the two distance constants were **x87 float +returns BN elided** — `GetStart/MaxConstraintDistance` decompile to a bare `this->m_position;` expression with the actual returned value lost to the -FPU-return-elision artifact. Recovering them needs a live cdb read of `st0` -after the call (retail debugger toolchain) or a Ghidra re-decompile with a -corrected float-return signature. (2) The arming site (`SmartBox`'s inbound -position-reconciliation branches A/B/C) has no acdream equivalent yet — wiring -it means teaching acdream's position path to re-anchor the leash on every -server position update, then feeding the `adjust_offset` taper into the body -integration (same chokepoint as the sticky wiring, R5-V3). +FPU-return-elision artifact. Recovered by disassembling the matching binary's +raw machine code directly (no cdb needed): outdoor start 10 / indoor 5, +outdoor max 50 / indoor 20 — ACE's start mapping is INVERTED (outdoor 5 / +indoor 10); the binary wins. (2) The arming site (`SmartBox`'s inbound +position-reconciliation branches A/B/C) had no acdream equivalent — wired at +`LiveEntityNetworkUpdateController` (remotes, anchored to the object's own +position) and `PlayerMovementController.SetPosition`/`BlipPosition` (local +player, anchored to the received position), feeding the `AdjustOffset` taper +into the body integration at the same per-tick chokepoint as the sticky +wiring (R5-V3). -**Where:** `src/AcDream.Core/Physics/Motion/ConstraintManager.cs` (class, -unarmed); the read gate is `PhysicsBody.IsFullyConstrained` (TS-35) via -`jump_is_allowed`. Decomp: `docs/research/2026-07-03-r5-managers/`. +**Where:** `src/AcDream.Core/Physics/Motion/ConstraintManager.cs` (armed), +`src/AcDream.Core/Physics/Motion/ConstraintDistance.cs` (constants); the read +gate is `PhysicsBody.IsFullyConstrained` (former TS-35) via +`jump_is_allowed`. Decomp: `docs/research/2026-07-03-r5-managers/`, +`docs/research/2026-07-30-constraint-leash-constants.md`. -**Acceptance:** the two constants are recovered (cdb/Ghidra), acdream arms the -leash on inbound server positions, `IsFullyConstrained` fires while -rubber-banding, and a jump attempt inside the tight leash is blocked -(0x47) matching retail; TS-35 + this issue retire together. +**Acceptance:** the two constants are recovered (byte-decoded from the +binary), acdream arms the leash on inbound server positions, +`IsFullyConstrained` fires while rubber-banding, and a jump attempt inside +the tight leash is blocked (0x47) matching retail; TS-35 + this issue retired +together. ## #160 — Remote moveto: run animation pace vs actual movement speed mismatch diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 1175d64f..013c0ebf 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -262,7 +262,6 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | TS-32 | `ClientObjectTable` has no pre-queue for a child `CreateObject` that arrives before its parent (out-of-order PARENTED create); such objects are ingested as root objects and their `ContainerId` links a not-yet-known container. Retail's `null_object_table` + `null_weenie_object_table` hold unresolvable objects until the parent arrives | `src/AcDream.Core/Items/ClientObjectTable.cs` (`Ingest`) | PD↔`CreateObject` ordering is handled (upsert semantics); out-of-order PARENTED creates are observed only at high packet loss or in vendor/corpse multi-object bursts on non-loopback links; deferred to D.5.5+ | A container's child object arriving before the container is ingested as a root item — it won't appear in `GetContents` until the next `RecordMembership` or a move event corrects the parent link | `CObjectMaint::null_object_table` / `null_weenie_object_table` (acclient.h / named-retail pc) | | TS-33 | **NARROWED 2026-07-15** — full AP tracker semantics are ported: MTS stamps time only; AP stamps complete cell-local Position + contact plane + time; `ShouldSendPositionEvent` compares cell/contact inside the interval and the complete Frame including orientation afterward. Residual: acdream's single update path snapshots the AP predicate, emits a same-update MTS first when input changed, then AP. Retail proves `UseTime` performs Should→AP, but MTS originates in separate input callbacks; their relative same-tick callback/wire order is not yet traced | `src/AcDream.Runtime/Gameplay/LocalPlayerOutboundController.cs` (pre/post network slots); `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs` (ported tracker) | Preserve the pre-existing acdream wire order until a focused retail packet/breakpoint trace establishes input callback versus `UseTime`; do not infer it from `UseTime` alone | In the rare update where both packets are due, ACE may observe their position timestamps/action sequences in the opposite order from retail, shifting only that correction tick; stationary target-facing is live-gated because full-frame orientation now publishes | `CommandInterpreter::UseTime` 0x006B3BF0; `SendMovementEvent` 0x006B4680; `SendPositionEvent` 0x006B4770; `ShouldSendPositionEvent` 0x006B45E0; `Frame::is_equal` 0x00424C30 | | TS-40 | Retail's `physics_obj->cell` ("placed in the world") is proxied by the explicit `PhysicsBody.InWorld` flag — set by `SnapToCell` (local player placement) and `RemoteMotion` construction (remotes exist only for world entities); consumed by `CMotionInterp`'s detached-object link-strip guards (`if (cell == 0) RemoveLinkAnimations`, raw @305627). Replaces the UNREGISTERED `CellPosition.ObjCellId == 0` proxy, which only the local player ever seeded (#145 `SnapToCell`), so every REMOTE body read "detached" and every dispatched transition link (door swings, remote walk↔run links) was stripped the same tick it was appended — the 2026-07-03 door-snap bug | `src/AcDream.Core/Physics/PhysicsBody.cs` (`InWorld`); `src/AcDream.Core/Physics/MotionInterpreter.cs` (3 guard sites) | acdream has no per-body CObjCell pointer; a boolean placement flag carries exactly the guard's retail meaning until cell-pointer plumbing exists | A body used without either placement path (a future entity class constructing bodies directly) reads detached and loses transition links until its creation site sets the flag | `CMotionInterp::DoInterpretedMotion` 0x00528360 tail @305627; `CPhysicsObj::RemoveLinkAnimations` | -| TS-35 | `PhysicsBody.IsFullyConstrained` is a stub property (default `false`, never set by any physics code), read by `jump_is_allowed`'s verbatim `IsFullyConstrained` gate (raw 305524-305525) | `src/AcDream.Core/Physics/PhysicsBody.cs` (`IsFullyConstrained`) | R3-W3 needed the read site to port `jump_is_allowed`'s full chain. **R5-V1 CORRECTED the mechanism** (the earlier "per-cell contact-plane / doorway-jamming" guess was WRONG): the write side is the **ConstraintManager server-position rubber-band leash** — armed by `SmartBox::HandleReceivedPosition` on every inbound server position, `IsFullyConstrained` = `max*0.9 < offset`. R5-V1 ported `ConstraintManager` (`src/AcDream.Core/Physics/Motion/ConstraintManager.cs`) but does NOT arm it (no acdream `SmartBox` + two x87 distance constants BN elided) — so this read stays false. Arming = issue #167 | A body retail would consider fully constrained (still rubber-banding toward a server position inside the tight leash) never refuses the jump (0x47) — a jump succeeds mid-rubber-band where retail blocks it. Low practical risk (the leash band is tight + short-lived) | `CPhysicsObj::IsFullyConstrained` 0x0050ec60 → `ConstraintManager::IsFullyConstrained` 0x005560d0; `jump_is_allowed` 0x005282b0; arming `SmartBox::HandleReceivedPosition` 0x00453fd0 (issue #167) | | TS-37 | RETIRED misattribution note (not a live divergence — kept here as the historical record R3-W3 closes): the S2a port had `contact_allows_move` (0x00528240) arm `StandingLongJump` as a side effect, explicitly flagged "PRE-EXISTING acdream side effect (not part of 0x00528240)". R3-W3 deletes that side effect; `ChargeJump` (0x005281c0) is now the ONLY arming site, matching retail exactly. No further action — recorded per the register's retire-in-same-commit rule | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`contact_allows_move`, `ChargeJump`) | N/A — retired | N/A — retired | `CMotionInterp::charge_jump` 0x005281c0 @305448 | | TS-38 | `MotionInterpreter.Initted` defaults to `true` in both constructors, not retail's `false` — retail's `CMotionInterp` is never observed pre-`enter_default_state` (every real construction path calls it before exposing the interpreter); acdream's constructors are used directly by ~40 pre-existing tests and both App call sites as complete, immediately-usable objects with no separate "enter default state" step | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`Initted` property + both constructors) | Defaulting `true` is the C# equivalent of "the constructor already did what `enter_default_state` would have done to this flag" — `EnterDefaultState()` remains available, verbatim, for the REST of retail's reset semantics (state defaults, sentinel enqueue, `LeaveGround` tail) when a caller wants them | None observed: no code path needs `apply_current_movement`/`ReportExhaustion` to no-op before an explicit `EnterDefaultState()` call, since nothing constructs a `MotionInterpreter` and defers initialization today. If a future caller DOES need staged construction (build now, `EnterDefaultState()` later), it must explicitly set `Initted = false` first | `CMotionInterp::enter_default_state` 0x00528c80 @306124 sets `initted = 1`; retire if/when construction is staged through `EnterDefaultState()` uniformly | | ~~TS-41~~ | **RETIRED 2026-07-07 (remote-creature de-overlap #184)** — the SERVERVEL synth-velocity body-drive (`Body.Velocity = ServerVelocity` / `get_state_velocity()` leg) is DELETED. Grounded NPC remotes now translate by the retail interp CATCH-UP (`RemoteMotionCombiner.ComputeOffset` → `InterpolationManager::adjust_offset` toward the MoveOrTeleport-queued server waypoint) and `MovementManager::UseTime` (`TickRemoteMoveTo`) runs UNCONDITIONALLY per tick — the retail `UpdateObjectInternal` shape (no wire-velocity leg-driver). The de-overlap sweep resolves the catch-up movement; the resolved position is written back into the SHADOW (AP-86) so it persists. Residual: the non-retail anim-cycle stale-stop heuristic (`ApplyServerControlledVelocityCycle(Zero)` on a >0.6 s velocity-staleness timer) is kept as ANIM-only and stays covered by **AP-80**; it no longer drives the body. | `src/AcDream.App/Physics/RemotePhysicsUpdater.cs` (grounded NPC branch) | — | — | `CPhysicsObj::UpdateObjectInternal` 0x005156b0 (`MovementManager::UseTime` @0x00515998, unconditional); `MoveOrTeleport` 0x00516330; `InterpolationManager::adjust_offset` 0x00555d30 | diff --git a/docs/plans/2026-07-29-physics-parity-campaign.md b/docs/plans/2026-07-29-physics-parity-campaign.md index 3cb2534c..21ac740e 100644 --- a/docs/plans/2026-07-29-physics-parity-campaign.md +++ b/docs/plans/2026-07-29-physics-parity-campaign.md @@ -220,6 +220,19 @@ RestrictionTables residuals. Suite 9,956/0/5. ### P5 — Deferred fidelity — closes #167, #153, #72 +**Status (2026-07-30): item 1 (#167) COMPLETE.** Both blockers resolved +without Ghidra/cdb — the two x87-elided constants were byte-decoded +straight from the matching binary's raw machine code +(`docs/research/2026-07-30-constraint-leash-constants.md`). The leash is +now armed at every current acdream inbound-position acceptance seam +(`ConstraintDistance`, `LiveEntityNetworkUpdateController`, +`PlayerMovementController.SetPosition`/`BlipPosition`), the per-tick +`PhysicsBody.IsFullyConstrained` push replaces the always-false stub, and +register row TS-35 is deleted. Full Core/Runtime/App suites pass with new +conformance tests (leash-armed jump refusal, teleport-vs-blip +anchor/teardown, taper reduction, remote-tick push). Items 2 (#153) and 3 +(#72) remain open. + 1. **#167:** decode the two unknown x87 ConstraintManager constants (Ghidra) and port leash arming. 2. **#153:** the far-teleport arrival onto an unstreamed landblock near diff --git a/docs/research/2026-07-30-constraint-leash-constants.md b/docs/research/2026-07-30-constraint-leash-constants.md index 20015f6b..4a7a2cfb 100644 --- a/docs/research/2026-07-30-constraint-leash-constants.md +++ b/docs/research/2026-07-30-constraint-leash-constants.md @@ -94,3 +94,52 @@ 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. diff --git a/src/AcDream.Core/Physics/Motion/ConstraintManager.cs b/src/AcDream.Core/Physics/Motion/ConstraintManager.cs index 72e8ecbf..9d2c85d8 100644 --- a/src/AcDream.Core/Physics/Motion/ConstraintManager.cs +++ b/src/AcDream.Core/Physics/Motion/ConstraintManager.cs @@ -13,16 +13,18 @@ namespace AcDream.Core.Physics.Motion; /// CMotionInterp::jump_is_allowed (block jump while /// ). /// -/// Arming is UNPORTED in acdream (R5). Retail arms the leash ONLY -/// from SmartBox::HandleReceivedPosition (on every inbound server -/// position packet) with two constants from -/// CPhysicsObj::GetStart/MaxConstraintDistance whose values BN elided -/// (x87 returns — unknown, need a cdb read). acdream's position reconciliation -/// is not SmartBox, so nothing calls — the leash -/// stays disarmed and stays false, matching -/// register TS-35's current stub behavior. The class is ported for structural -/// completeness of ; the leash-arming port + the -/// two unknown constants are a deferred issue (port-plan §Constraint scope). +/// Armed as of Campaign P P5 (2026-07-30, #167, closed +/// register row TS-35). Retail arms the leash ONLY from +/// SmartBox::HandleReceivedPosition (0x00453fd0, on every inbound +/// server position packet) with the start/max band from +/// CPhysicsObj::GetStart/MaxConstraintDistance — byte-decoded (the x87 +/// returns BN elided) in . acdream's +/// equivalents call at every accepted-position +/// seam: LiveEntityNetworkUpdateController for remotes (anchored to +/// the object's own position, right after the hard-teleport branch returns) +/// and PlayerMovementController.SetPosition/BlipPosition for +/// the local player (anchored to the received position). See +/// docs/research/2026-07-30-constraint-leash-constants.md. /// public sealed class ConstraintManager { @@ -77,8 +79,11 @@ public sealed class ConstraintManager /// Retail ConstraintManager::IsFullyConstrained (0x005560d0): /// constraint_distance_max * 0.9 < constraint_pos_offset — the /// object counts as fully constrained once it has strained past 90 % of the - /// max leash. Read by jump_is_allowed to block jumps. Always false - /// while the leash is disarmed (acdream never arms it — see class note). + /// max leash. Read by jump_is_allowed to block jumps. False while + /// unconstrained (no prior or after + /// ) since and + /// both default to zero (0*0.9 < 0 + /// is false). /// public bool IsFullyConstrained() => ConstraintDistanceMax * 0.9f < ConstraintPosOffset; diff --git a/src/AcDream.Core/Physics/Motion/PositionManager.cs b/src/AcDream.Core/Physics/Motion/PositionManager.cs index 0ed02f04..67fd334b 100644 --- a/src/AcDream.Core/Physics/Motion/PositionManager.cs +++ b/src/AcDream.Core/Physics/Motion/PositionManager.cs @@ -54,8 +54,8 @@ public sealed class PositionManager /// Retail PositionManager::ConstrainTo (0x00555280) — /// lazily create the and arm the leash. - /// (Unused in acdream — no arming call site; see - /// .) + /// Armed as of Campaign P P5 (#167) at every accepted-position seam — see + /// 's class doc for the call sites. public void ConstrainTo(Position anchor, float startDistance, float maxDistance) { _constraint ??= new ConstraintManager(_host); diff --git a/src/AcDream.Core/Physics/PhysicsBody.cs b/src/AcDream.Core/Physics/PhysicsBody.cs index d83cc5ff..5eaa3300 100644 --- a/src/AcDream.Core/Physics/PhysicsBody.cs +++ b/src/AcDream.Core/Physics/PhysicsBody.cs @@ -408,16 +408,33 @@ public sealed class PhysicsBody public double LastUpdateTime { get; set; } /// - /// R3-W3 stub for retail CPhysicsObj::IsFullyConstrained - /// (0x0050f730), read by CMotionInterp::jump_is_allowed (raw - /// 305524-305525: if (IsFullyConstrained(physics_obj) != 0) return - /// 0x47;). Retail's body walks per-cell contact-plane constraints - /// (a mover pinned between opposing walkable surfaces / doorway - /// jamming); acdream has no equivalent constraint-tracking yet. - /// Register row: stubbed false (never fires) — a real port needs the - /// per-cell shadow-list contact accounting the physics digest tracks. - /// See docs/architecture/retail-divergence-register.md (added same - /// commit as this field). + /// Retail CPhysicsObj::IsFullyConstrained (0x0050f730), read by + /// CMotionInterp::jump_is_allowed (raw 305524-305525: + /// if (IsFullyConstrained(physics_obj) != 0) return 0x47;) to block + /// a jump while the object is rubber-banding hard against a server + /// position correction. + /// + /// R3-W3 originally stubbed this as an always-false property under + /// a WRONG mechanism guess (per-cell contact-plane / doorway-jamming). + /// R5-V1 corrected the mechanism: the real retail source is + /// ConstraintManager::IsFullyConstrained (0x005560d0, + /// constraint_distance_max * 0.9 < constraint_pos_offset) — + /// 's constraint sub-manager. R5-V1 + /// ported but did not arm it (#167, + /// former register row TS-35). + /// + /// Campaign P P5 (2026-07-30) armed the leash at every current + /// inbound-position acceptance seam + /// (LiveEntityNetworkUpdateController for remotes, + /// PlayerMovementController.SetPosition/BlipPosition for the + /// local player — see docs/research/2026-07-30-constraint-leash-constants.md + /// §2/§3). only holds a + /// reference (no host), so this property stays a + /// plain settable bool; the per-tick pump that already runs + /// PositionManager.AdjustOffset (PlayerMovementController.Update, + /// RuntimeRemotePhysicsUpdater.Tick/TickHidden) is the single + /// owner that pushes PositionManager.IsFullyConstrained() here every + /// tick, so this read is now live, not stubbed. /// public bool IsFullyConstrained { get; set; } diff --git a/src/AcDream.Runtime/Gameplay/PlayerMovementController.cs b/src/AcDream.Runtime/Gameplay/PlayerMovementController.cs index 688da3bf..d10d788d 100644 --- a/src/AcDream.Runtime/Gameplay/PlayerMovementController.cs +++ b/src/AcDream.Runtime/Gameplay/PlayerMovementController.cs @@ -503,7 +503,11 @@ public sealed class PlayerMovementController /// motion) and UseTime after the completed-motions sweep (retail /// UpdateObjectInternal tail @0x005159b3 — the sticky 1 s lease /// watchdog). tears any - /// stick down (retail teleport_hook @0x00514eee). + /// stick down (retail teleport_hook @0x00514eee) and, as of + /// Campaign P P5 (#167), also tears down and immediately re-arms the + /// constraint leash (UnConstrain then ConstrainTo); + /// arms the leash without tearing it down first + /// (retail SmartBox::BlipPlayer survives motion/velocity/stick). /// public AcDream.Core.Physics.Motion.PositionManager? PositionManager { get; set; } diff --git a/src/AcDream.Runtime/Physics/EntityPhysicsHost.cs b/src/AcDream.Runtime/Physics/EntityPhysicsHost.cs index 275451e5..b3e50ab8 100644 --- a/src/AcDream.Runtime/Physics/EntityPhysicsHost.cs +++ b/src/AcDream.Runtime/Physics/EntityPhysicsHost.cs @@ -170,10 +170,13 @@ public sealed class EntityPhysicsHost : IPhysicsObjHost public TargetManager TargetManager => _targetManager; /// R5-V3 — the owned facade (retail - /// CPhysicsObj::position_manager): sticky follow + (unarmed) - /// constraint leash. Seam targets: MoveToManager.StickTo/Unstick, + /// CPhysicsObj::position_manager): sticky follow + the + /// server-position constraint leash (armed as of Campaign P P5, #167). + /// Seam targets: MoveToManager.StickTo/Unstick, /// MotionInterpreter.UnstickFromObject, the per-tick - /// AdjustOffset/UseTime drivers. + /// AdjustOffset/UseTime drivers, and + /// LiveEntityNetworkUpdateController's remote-position + /// ConstrainTo arm. public PositionManager PositionManager { get; } // ── IPhysicsObjHost fan-out / target-tracking seams ────────────────────