docs: pin the C4 route 4b-2 contract (remote far-snap)
4b-2 flips one classifier branch on: SetPositionSimple for remotes, contact, PlayerDistance >= 96 m, routed through 4b-1's dormant owner. Teleport and cell-less stay legacy for 4b-3. Pins the two retail facts that decide the slice: StopInterpolating runs BEFORE SetPositionSimple (@0x005163CB before @0x005163D9), and the branch returns 1 so HandleReceivedPosition arms ConstrainTo @0x00454272 anchored post-move. Since MoveOrTeleport discards SetPositionSimple's error return and returns 1 regardless, the leash must be armed on refusal and rejection too — "arm on Committed" is the natural misreading and is the same shape as the already-filed unarmed-leash bug. Names the trap up front: deleting the legacy far block removes the only handler for null and Rejected* classifications, and during the login window null is every remote packet, so remotes would not move at all until the local controller exists. Requires a stated policy rather than a silent drop — the same shape as route 4a's "'not Interpolate' is not 'far'" finding. Records that AP-87's 4 m / !willBeDrTicked guards are near-branch only and are subsumed by the far branch's unconditional snap, so they must not be carried forward — while the near-branch copies stay, since those are 4a's and still load-bearing. Requires behavioural App tests explicitly: route 2 settled for a source pin (#292) and route 4a's first attempt shipped tautologies that passed with production reverted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2e8e09acd0
commit
d5bdc35598
1 changed files with 119 additions and 0 deletions
119
docs/research/2026-08-04-c4-route-4b-2-contract.md
Normal file
119
docs/research/2026-08-04-c4-route-4b-2-contract.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# C4 route 4b-2 — remote far-snap: pinned contract (2026-08-04)
|
||||
|
||||
Split rationale:
|
||||
[`2026-08-04-c4-route-4b-scoping-and-split.md`](2026-08-04-c4-route-4b-scoping-and-split.md).
|
||||
4b-1 (the infrastructure) landed dormant at `2e8e09ac`; the park restore it
|
||||
depends on landed at `634bc551`.
|
||||
|
||||
**4b-2 flips ONE classifier branch on: `SetPositionSimple` — contact,
|
||||
`PlayerDistance >= 96 m`.** Teleport and cell-less stay legacy; 4b-3 owns them.
|
||||
|
||||
## Retail truth — verify each yourself
|
||||
|
||||
`CPhysicsObj::MoveOrTeleport` @0x00516330, far branch @0x005163C1-@0x005163E8:
|
||||
|
||||
```
|
||||
if (player_distance >= 96f) {
|
||||
if (position_manager != 0) PositionManager::StopInterpolating(position_manager);
|
||||
CPhysicsObj::SetPositionSimple(this_1, arg2, 1);
|
||||
return 1; // @0x005163E8
|
||||
}
|
||||
```
|
||||
|
||||
Two things that decide this slice:
|
||||
|
||||
1. **`StopInterpolating` runs BEFORE `SetPositionSimple`** (@0x005163CB before
|
||||
@0x005163D9). The classifier already carries `StopInterpolating: !nearby`.
|
||||
Preserve the order.
|
||||
2. **The branch returns 1**, so `SmartBox::HandleReceivedPosition`
|
||||
@0x00454254 takes the nonzero path and arms `ConstrainTo` @0x00454272,
|
||||
anchored to `&arg2->m_position` read live — i.e. POST-move.
|
||||
|
||||
`SetPositionSimple` @0x005162B0 with `arg3 != 0` builds flags `0x1012`
|
||||
(`Teleport|Slide|SendPositionEvent`) — the classifier's
|
||||
`AuthoritativeTeleportFlags`.
|
||||
|
||||
**`MoveOrTeleport` discards `SetPositionSimple`'s `SetPositionError` return and
|
||||
returns 1 regardless.** So retail arms the leash even when the placement failed.
|
||||
4b-2 must arm on refusal and rejection too, not only on commit. "Arm on
|
||||
Committed" is the natural misreading and is the same shape as the already-filed
|
||||
unarmed-leash bug.
|
||||
|
||||
## Scope
|
||||
|
||||
**Flip on:** `SetPositionSimple` for `RuntimePositionEntityKind.Remote`, routed
|
||||
through 4b-1's `RuntimeRemotePlacementDriveController`.
|
||||
|
||||
**Delete** (far half only, both arms):
|
||||
- The player-remote far branch and its `MaxPhysicsDistance = 96f` /
|
||||
`BodySnapThreshold = 4f` constants.
|
||||
- The NPC-remote copy and its `MaxPhysicsDistanceNpc` / `BodySnapThresholdNpc`.
|
||||
- Both `_playerController?.Position ?? Vector3.Zero` fabrications on those
|
||||
paths. `GameRuntime` states the rule for this field: a null controller "must
|
||||
yield null, never a fabricated Vector3.Zero that would misclassify every
|
||||
remote entity as implausibly far."
|
||||
|
||||
**Do NOT touch:** `RemoteTeleportController`, `RemoteTeleportPlacement`,
|
||||
`remotePlacementRequired`, the teleport/cell-less branches, AP-135's two
|
||||
airborne writes, the near/interpolate path 4a owns, the airborne no-op, or
|
||||
route 1's executor.
|
||||
|
||||
## The trap that will bite you
|
||||
|
||||
**Deleting the legacy far block removes the only handler for `null` and
|
||||
`Rejected*` classifications.** `ClassifyRemoteAcceptedPosition` returns null
|
||||
whenever `_playerController` is null — **during the login window that is every
|
||||
remote packet.** Today those fall into the legacy block and hard-snap. Delete it
|
||||
without an explicit replacement and remotes will not move at all until the local
|
||||
controller exists.
|
||||
|
||||
Retail has no analogue because retail always has a player. This needs a stated
|
||||
acdream policy, decided deliberately and recorded — it is the same shape as
|
||||
route 4a's "'not Interpolate' is not 'far'" finding, one level up.
|
||||
|
||||
## AP-87 does not apply here
|
||||
|
||||
AP-87's `bodyToTarget > 4 m` and `!willBeDrTicked` conditions are **near-branch**
|
||||
guards: they decide snap-vs-enqueue within `Interpolate`. The far branch snaps
|
||||
unconditionally, so they are subsumed. **Do not carry them forward into a branch
|
||||
that does not need them** — but do not delete the near-branch copies either;
|
||||
those are 4a's and still load-bearing.
|
||||
|
||||
## Contract
|
||||
|
||||
1. `SetPositionSimple` for remotes executes through 4b-1's owner; nothing else
|
||||
changes classification.
|
||||
2. `StopInterpolating` precedes the placement, matching @0x005163CB.
|
||||
3. `ConstrainTo` is armed once, after the operation, anchored post-move, on
|
||||
commit **and** on refusal/rejection — retail arms on any nonzero return.
|
||||
Do not add a second arming site; retail has exactly one (@0x00454272).
|
||||
4. `null` and `Rejected*` have an explicit, stated handler. No silent drop.
|
||||
5. No behaviour change to teleport, cell-less, near/interpolate, or airborne.
|
||||
6. Both hosts drive the identical Runtime entry point, or the divergence is
|
||||
stated plainly rather than satisfied vacuously.
|
||||
7. Per-entity currency across GUID reuse, incarnation, generation, teardown.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Focused Runtime tests for the far branch, the `StopInterpolating` ordering,
|
||||
the arm-on-failure case, and the null/`Rejected*` policy.
|
||||
- **Behavioural** App tests — not source-text pins. Route 2 settled for a pin
|
||||
and that gap is #292; route 4a's first attempt shipped tautologies that
|
||||
passed with production reverted. A test that cannot fail against a broken
|
||||
implementation is worse than no test.
|
||||
- Complete Release suite green. Baseline **10,973 / 4 / 0**. Two known flakes,
|
||||
do not chase and do NOT conflate: **#302** (`PortalProjectionTests…`,
|
||||
GC-allocation assertion, App.Tests) and **#308** (`NakEmissionTests.LossSoak_…`,
|
||||
wall-clock deadline, Core.Net.Tests, full-suite load only).
|
||||
- **Connected gate (user-gated), and it is easy**: stand still in open ground;
|
||||
second character runs past ~100 m, stops, turns, runs back, three or four
|
||||
times. Correct: they stay visible and correctly positioned at range, and
|
||||
resume smooth interpolation on the way back in with no jolt at the crossover.
|
||||
Regressions: a freeze or stutter at the boundary, a Z pop, a vanish, or —
|
||||
the loud one — invisible-but-solid on the way back in.
|
||||
|
||||
## Budget
|
||||
|
||||
**~350-500 non-comment production lines.** For calibration: route 4a was 364
|
||||
(91% of its ~400), and 4b-1 was 230 plus a 57-line park fix. Exceed 500 and stop
|
||||
and report rather than pushing through.
|
||||
Loading…
Add table
Add a link
Reference in a new issue