docs: split C4 route 4 into 4a/4b and pin the 4a contract
User-directed after scoping put whole-route 4 at 1,500-2,500 production lines against a stated ~400 budget. 4a is the steady state: the classifier's Interpolate (contact, < 96 m) and NoPositionOperation (no contact) branches. Neither performs a SetPosition, so 4a carries no deferred-cell park, no service-window guard, no allocation exposure, and no interaction with the Forget-on-every-accepted-Position behaviour that dominated route 2's review rounds. It also fixes two of the three unfiled divergences: the NPC airborne hard-snap that ignores the wire IsGrounded bit (retail returns 0 and writes nothing, MoveOrTeleport @0x0051636D), and ConstrainTo armed before the operation instead of after (retail arms it post-move only on a nonzero return, @0x00454272). 4b takes the edges — teleport, far-snap, cell-less — where the parks, the Position-time service-window guard, #277's broken bound, N3, and the third divergence live. The contract sanctions exactly one dual path: 4a routes its two classifications through the new seam and leaves the other two on the legacy path until 4b. That is a staged cutover rather than a duplicate authority ONLY because the discriminator is the classifier itself and the classifications are mutually exclusive; the contract says so explicitly and requires the fallback deleted in 4b. Two carried acdream additions are called out as load-bearing rather than left to be discovered: AP-87's 4 m / !willBeDrTicked snap conditions (which prevent the #184 invisible-but-solid monster and are NOT in the classifier) and TS-44's sticky suppression. Silently dropping them by delegating to the classifier is named as the failure mode. Acceptance requires a BEHAVIOURAL App test, not the source-text pin route 2 settled for (#292). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
62e906b136
commit
2f0c26c8a4
2 changed files with 175 additions and 0 deletions
|
|
@ -254,6 +254,26 @@ same commit) → docs/handoff commit. No workarounds; no fused slices.
|
||||||
6 (drops + split-recovery marking), 7 (residual pickup/parent/delete
|
6 (drops + split-recovery marking), 7 (residual pickup/parent/delete
|
||||||
polish). — route 2 COMPLETE AND USER-ACCEPTED 2026-08-03 (`9966b531`);
|
polish). — route 2 COMPLETE AND USER-ACCEPTED 2026-08-03 (`9966b531`);
|
||||||
routes 3/4/5/6/7 remain OPEN.**
|
routes 3/4/5/6/7 remain OPEN.**
|
||||||
|
**Route 4 SPLIT into 4a and 4b (user-directed 2026-08-03).** Scoping
|
||||||
|
([`2026-08-03-c4-route-4-scoping.md`](../research/2026-08-03-c4-route-4-scoping.md))
|
||||||
|
put whole-route 4 at 1,500-2,500 production lines against a stated ~400
|
||||||
|
budget, so it is split to keep each landing reviewable:
|
||||||
|
- **4a — the steady state.** The classifier's `Interpolate` (contact,
|
||||||
|
`PlayerDistance < 96 m`) and `NoPositionOperation` (no contact) branches.
|
||||||
|
NEITHER runs a `SetPosition`, so 4a has no deferred-cell park, no
|
||||||
|
service-window work, and no placement-allocation exposure. Fixes two of the
|
||||||
|
three unfiled divergences (the NPC airborne hard-snap that ignores the wire
|
||||||
|
`IsGrounded` bit; `ConstrainTo` armed before the operation instead of
|
||||||
|
after). Highest visible value — this is what makes creatures move smoothly.
|
||||||
|
- **4b — the edges.** `SetPosition` / `SetPositionSimple`: teleport, far-snap
|
||||||
|
(>= 96 m), and cell-less first placement. This is where the parks, the
|
||||||
|
Position-time service-window guard, #277's broken bound, N3 (headless never
|
||||||
|
calls `RetryPending`), and the third divergence (`ConstrainTo` never armed
|
||||||
|
on the remote teleport branch) all live.
|
||||||
|
Note the route-4 Create half is ALREADY DONE (C3b/C3c); the remaining work is
|
||||||
|
steady-state remote Position plus the deletions. AP-131 is NOT retired by
|
||||||
|
either sub-slice — see the scoping doc for why route 4 alone cannot.
|
||||||
|
4a contract: [`2026-08-03-c4-route-4a-contract.md`](../research/2026-08-03-c4-route-4a-contract.md).
|
||||||
**Route 2 connected gate PASSED (user, 2026-08-03).** Provoked with the
|
**Route 2 connected gate PASSED (user, 2026-08-03).** Provoked with the
|
||||||
retail `@pklite` entry-collision bump (`69ba9486` — the only reachable ACE
|
retail `@pklite` entry-collision bump (`69ba9486` — the only reachable ACE
|
||||||
trigger for `ObjectForcePosition`; admin teleports advance `ObjectTeleport`
|
trigger for `ObjectForcePosition`; admin teleports advance `ObjectTeleport`
|
||||||
|
|
|
||||||
155
docs/research/2026-08-03-c4-route-4a-contract.md
Normal file
155
docs/research/2026-08-03-c4-route-4a-contract.md
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
# C4 route 4a — remote steady-state Position: pinned contract (2026-08-03)
|
||||||
|
|
||||||
|
Route 4 split into 4a/4b by user direction after scoping put the whole route at
|
||||||
|
1,500-2,500 lines against a ~400 budget. Scoping:
|
||||||
|
[`2026-08-03-c4-route-4-scoping.md`](2026-08-03-c4-route-4-scoping.md).
|
||||||
|
|
||||||
|
**4a is the steady state: the two classifier branches that perform NO
|
||||||
|
`SetPosition`.** Everything that parks, teleports, or snaps is 4b.
|
||||||
|
|
||||||
|
## Scope — exactly two classifier branches
|
||||||
|
|
||||||
|
`RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition`
|
||||||
|
(`:393-472`), remote/projectile arms:
|
||||||
|
|
||||||
|
| Branch | Condition | Runs SetPosition? | In 4a? |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `NoPositionOperation` | `!effectiveContact` (wire `IsGrounded == false`) | no | **YES** |
|
||||||
|
| `Interpolate` | contact, `PlayerDistance < 96 m` | no | **YES** |
|
||||||
|
| `SetPositionSimple` | contact, `PlayerDistance >= 96 m` | yes | no — 4b |
|
||||||
|
| `SetPosition` | `TeleportAdvanced` or `CommittedCellId == 0` | yes | no — 4b |
|
||||||
|
|
||||||
|
Because neither 4a branch performs a placement, 4a has **no deferred-cell park,
|
||||||
|
no service-window guard, no allocation exposure, and no interaction with
|
||||||
|
`Forget`-on-every-accepted-Position.** That is the entire reason for the split;
|
||||||
|
do not drag any of it in.
|
||||||
|
|
||||||
|
## Staged cutover — the one sanctioned dual path
|
||||||
|
|
||||||
|
4a routes the two no-placement branches through the new Runtime seam and leaves
|
||||||
|
the other two on the legacy App path until 4b. **This is a staged cutover, not a
|
||||||
|
duplicate authority**, and it is only sanctioned under these conditions:
|
||||||
|
|
||||||
|
1. The discriminator is the CLASSIFIER ITSELF, not a heuristic, a flag, or a
|
||||||
|
guess. One classification, one owner, mutually exclusive by construction.
|
||||||
|
2. For a classification 4a owns, the legacy path must not run **at all** — not
|
||||||
|
partially, not "just the render write". Route 2's original defect was exactly
|
||||||
|
a second writer running after the canonical one.
|
||||||
|
3. The fallback is temporary and 4b deletes it. Record that in the code comment
|
||||||
|
at the branch point, with a pointer to this contract.
|
||||||
|
|
||||||
|
If you find yourself needing a condition beyond "what did the classifier say",
|
||||||
|
STOP and report — that means the seam is wrong.
|
||||||
|
|
||||||
|
## Retail truth (verify each yourself; do not trust this table)
|
||||||
|
|
||||||
|
`CPhysicsObj::MoveOrTeleport` @0x00516330 (pseudo-C 284304):
|
||||||
|
|
||||||
|
- **Airborne no-op** @0x0051638E / @0x0051636D: `arg4 == 0` (the wire
|
||||||
|
`has_contact` bit) -> **return 0. Nothing is written at all.**
|
||||||
|
- **Near interpolate** @0x005163AF: `player_distance < 96f` ->
|
||||||
|
`InterpolateTo(arg2, IsMovingTo())`. No body write.
|
||||||
|
- `player_distance` is retail's own field — distance to the local player.
|
||||||
|
|
||||||
|
`SmartBox::HandleReceivedPosition` @0x00453FD0, remote branch:
|
||||||
|
|
||||||
|
- `unset_parent` @0x00454129 unconditional; `SetPlacementFrame` @0x00454142
|
||||||
|
gated on `!HasAnims`.
|
||||||
|
- **`ConstrainTo` @0x00454272 runs AFTER `MoveOrTeleport` returns nonzero**,
|
||||||
|
anchored to `&arg2->m_position` — the object's own position read live, i.e.
|
||||||
|
POST-move. It does not run when `MoveOrTeleport` returned 0 (the airborne
|
||||||
|
no-op).
|
||||||
|
|
||||||
|
## The two divergences 4a must fix
|
||||||
|
|
||||||
|
**D1 — the NPC airborne branch ignores the wire bit.**
|
||||||
|
`LiveEntityNetworkUpdateController.cs:1819-1823` hard-snaps
|
||||||
|
`Body.Position`/`Orientation` for NPCs and branches on the client-tracked
|
||||||
|
`rmState.Airborne` flag, never consulting `update.IsGrounded`. Retail writes
|
||||||
|
nothing. Player remotes already behave correctly (`:1590-1599`); NPCs do not.
|
||||||
|
No register row exists.
|
||||||
|
|
||||||
|
**D2 — `ConstrainTo` is armed before the operation, unconditionally.**
|
||||||
|
`:1522-1529` arms it ahead of the branch, so it fires on the airborne no-op
|
||||||
|
retail skips and anchors to the PRE-move position. Retail arms it after, only on
|
||||||
|
a nonzero return, anchored post-move. No register row exists.
|
||||||
|
|
||||||
|
(The third divergence — `ConstrainTo` never armed on the remote teleport branch
|
||||||
|
— is on a 4b path. Leave it; 4b owns it.)
|
||||||
|
|
||||||
|
## Duplicate authorities 4a deletes
|
||||||
|
|
||||||
|
Only the parts reachable from the two 4a branches:
|
||||||
|
|
||||||
|
- The generic tail's remote writes (`entity.SetPosition` / `ParentCellId` /
|
||||||
|
`Rotation` / `RebucketLiveEntity`) for a remote whose classification is
|
||||||
|
`Interpolate` or `NoPositionOperation`. The committed result must reach the
|
||||||
|
render entity through the existing placement-projection sink instead — the
|
||||||
|
same substitution route 2 made.
|
||||||
|
- The player-remote near/far routing at `:1653-1702` and the NPC copy at
|
||||||
|
`:1826-1871` — **the near half only.** Each has its own duplicated copies of
|
||||||
|
`MaxPhysicsDistance = 96f` and `BodySnapThreshold = 4f`; the far half stays
|
||||||
|
until 4b.
|
||||||
|
- The airborne no-op blocks at `:1590-1599` (player) and `:1819-1823` (NPC).
|
||||||
|
- The unconditional `ConstrainTo` at `:1522-1529`.
|
||||||
|
|
||||||
|
Do NOT touch `RemoteTeleportController`, `RemoteTeleportPlacement`, or the
|
||||||
|
`remotePlacementRequired` path — all 4b.
|
||||||
|
|
||||||
|
## Load-bearing acdream additions that must survive
|
||||||
|
|
||||||
|
**AP-87** (`retail-divergence-register.md:242`) — the `bodyToTarget > 4 m` and
|
||||||
|
`!willBeDrTicked` snap conditions on the near branch are NOT in retail and NOT
|
||||||
|
in the classifier. They are load-bearing: they prevent the #184
|
||||||
|
invisible-but-solid monster. Either carry them explicitly as an acdream policy
|
||||||
|
layer over the retail classification, or retire them with live evidence and the
|
||||||
|
register row deleted in the same commit. **Silently dropping them by delegating
|
||||||
|
to the classifier is the failure mode.** Say which you chose.
|
||||||
|
|
||||||
|
**TS-44** (`:1792-1801`) — sticky-melee suppression of the NPC snap. Same rule.
|
||||||
|
|
||||||
|
## Contract
|
||||||
|
|
||||||
|
1. One Runtime owner executes the accepted remote Position for the two 4a
|
||||||
|
classifications; App projects the result.
|
||||||
|
2. Both hosts drive the identical Runtime entry point.
|
||||||
|
3. The interpolation queue (`RemoteMotion.Interp`) stays the owner of near
|
||||||
|
motion — 4a routes to it, it does not replace it. All of it is already in
|
||||||
|
Runtime (J5.5); no assembly boundary is crossed.
|
||||||
|
4. Stale sequences, GUID reuse, incarnation change, and generation change cannot
|
||||||
|
commit old state. N entities, so per-entity currency — route 2's `_pending`
|
||||||
|
was one slot and `RetainPending` threw on a second; that shape does not
|
||||||
|
transfer.
|
||||||
|
5. The airborne branch writes NOTHING (retail returns 0). Not the body, not the
|
||||||
|
render entity, not the cell.
|
||||||
|
6. `ConstrainTo` moves to after the operation, anchored post-move, and does not
|
||||||
|
run on the airborne branch.
|
||||||
|
7. No behaviour change to the far, teleport, or cell-less branches.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
- Focused Runtime tests for both branches, per-entity currency, and the AP-87 /
|
||||||
|
TS-44 decision.
|
||||||
|
- App tests proving the generic tail no longer double-writes a remote on a 4a
|
||||||
|
classification — **behavioural, not a source-text pin.** Route 2's equivalent
|
||||||
|
was source-pinned and that gap is filed as #292; do not repeat it.
|
||||||
|
- Complete Release suite green. Baseline at the time of writing:
|
||||||
|
**10,904 passed / 4 skipped / 0 failed**. Known flake #302
|
||||||
|
(`PortalProjectionTests.ClipToRegion_FrameOwnedStore_ReusesExactResultArray`,
|
||||||
|
~1 in 6) — re-run, do not chase.
|
||||||
|
- **Connected (user-gated), and unlike route 2 this is trivial:** stand still,
|
||||||
|
have a second character walk and run in a circle 5-15 m away, turning in
|
||||||
|
place. Motion must be continuous and smooth with no per-packet stepping. Then
|
||||||
|
have them jump and jump off a ledge — a clean parabola, clean landing, no
|
||||||
|
mid-air correction, and critically no invisible-but-solid body (the #184 /
|
||||||
|
AP-87 signature). Then pull a drudge, let it chase, melee it, let it die.
|
||||||
|
- Divergence ledger: D1 and D2 retired by fix in the same commit. AP-131 stays.
|
||||||
|
AP-87 / TS-44 either stay with justification or are retired with evidence.
|
||||||
|
|
||||||
|
## Budget
|
||||||
|
|
||||||
|
Stated up front so it can fail: **4a should be well under 400 production
|
||||||
|
lines**, because it adds no placement machinery — it routes two no-op
|
||||||
|
classifications and deletes their duplicates. If it exceeds that, stop and
|
||||||
|
report before continuing; that would mean the split did not actually isolate the
|
||||||
|
cheap half and 4b needs re-planning too.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue