Routes the classifier's two NO-PLACEMENT remote branches — Interpolate (contact, PlayerDistance < 96 m) and NoPositionOperation (no contact) — through a Runtime-owned seam, and fixes the two divergences they carried. Teleport, far-snap and cell-less stay on the legacy App path; 4b owns them. Route 4 was split into 4a/4b after scoping put the whole route at 1,500-2,500 lines against a ~400 budget. 4a's branches perform no SetPosition, so this slice carries no deferred-cell park, no service-window guard and no allocation exposure — which is what made the split worth doing. Divergences fixed, both previously unfiled: * D1 — the NPC airborne branch hard-snapped Body.Position/Orientation and branched on the client-tracked rmState.Airborne, never consulting the wire IsGrounded bit. Retail's MoveOrTeleport @0x00516330 returns 0 at 0x0051636D and writes nothing. Player remotes were already correct; NPCs were not. * D2 — ConstrainTo was armed before the operation, unconditionally, so it fired on the airborne no-op retail skips and anchored to the PRE-move position. Retail arms it at 0x00454272, only when MoveOrTeleport returns nonzero, anchored to &arg2->m_position read live, i.e. post-move. AP-87 and TS-44 were carried deliberately, not delegated away. AP-87's three conditions — including firstUp, which one round silently dropped — are preserved as an explicit acdream policy layer applied AFTER the classifier commits to Interpolate; the two previously separate player/NPC copies are now one. TS-44 stays an NPC-only caller gate; extending sticky suppression to player remotes has no retail basis and no live evidence, so it was declined rather than absorbed. Landing is explicitly carved out of 4a's ownership on both arms. A landing packet classifies Interpolate, so an ordering slip would ENQUEUE a body that must PLANT and a creature knocked off a ledge would glide down over a packet interval. The carve-out is a named entry point returning AirborneSnap/SteadyStateInterpolate/ Legacy precisely so the PRECEDENCE is observable and testable rather than implied by statement order — that is how the slip happened once and was caught. The player/NPC asymmetry on landing is real and NOT resolved here: retail draws no such distinction, but converging them is a behaviour decision needing its own evidence. Filed into the 4b plan. Register: AP-135 filed for the two bookkeeping writes the airborne branch deliberately retains (rmState.CellId, LastServerPos/Time) — not retail's model, but load-bearing for our catch-up sweep and staleness timer, and verified not to be a canonical cell commit for ordinary remotes. AP-87 and TS-44 rewritten to describe the code. Honest remainder: App still owns branch selection, the airborne return, the cell write, the entity write and the shadow publish, and headless satisfies "both hosts drive the identical entry point" only vacuously since it returns early for remotes. That is written into the 4b bullet rather than left implicit. Cost: 364 non-comment production lines, 91% of the ~400 budget — the split did isolate the cheap half, but not by much. Do not carry "well under" into 4b's scoping. Gates: complete Release solution 10,938 passed / 4 skipped / 0 failed (pre-4a baseline 10,909). Four review rounds; the first three each introduced a new behavioural defect while fixing another, and each left a comment asserting behaviour that no longer matched — the final round's precedence matrix was traced cell-by-cell against HEAD with only the D1-intended difference. App tests call production entry points against a real WorldEntity and real classifier output, closing route 2's #292 gap rather than repeating it. Connected acceptance NOT run — needs a live second character. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
191 lines
10 KiB
Markdown
191 lines
10 KiB
Markdown
# 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 RENDER-POSE writes (`entity.SetPosition` /
|
|
`ParentCellId` / `Rotation`) for a remote whose classification is
|
|
`Interpolate` or `NoPositionOperation`.
|
|
|
|
> **CORRECTED 2026-08-03 (review finding R1,
|
|
> [`2026-08-03-c4-route-4a-review-findings.md`](2026-08-03-c4-route-4a-review-findings.md)).**
|
|
> This bullet originally also listed `RebucketLiveEntity`, and said the
|
|
> result "must reach the render entity through the existing
|
|
> placement-projection sink instead — the same substitution route 2 made."
|
|
> That instruction was copied from route 2 and does not transfer: route 2
|
|
> performs a placement and therefore has a committed receipt to project,
|
|
> while NEITHER 4a branch performs a placement, so nothing substitutes for
|
|
> the bucket transaction. **The rebucket keeps running for both 4a
|
|
> classifications.** It is the only site that moves an ordinary moving
|
|
> remote's draw bucket, commits its canonical `FullCellId`, and recovers a
|
|
> pending bucket promotion (`GpuWorldState`, the 2026-07-03 invisible-player
|
|
> fix); deleting it produced the #184-class invisible-but-solid creature
|
|
> through a different door.
|
|
- 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.
|
|
|
|
> **AMENDED 2026-08-03/04 (review finding R4).** As shipped, the branch
|
|
> retains exactly two acdream bookkeeping writes — `rmState.CellId` and
|
|
> `LastServerPos`/`LastServerPosTime` — on both arms. These are not part of
|
|
> retail's model (retail's `MoveOrTeleport` has no catch-up sweep and no
|
|
> staleness timer to keep alive), and dropping them would break the
|
|
> per-tick free-fall sweep and the staleness timer respectively. Verified
|
|
> NOT a canonical cell commit for ordinary remotes: `RemoteMotion.CellId`
|
|
> only delegates to `RuntimePhysicsState` when `_canonicalCellWriter` is
|
|
> bound, which is projectiles-only, and projectiles return earlier.
|
|
> Recorded as register row **AP-135**. Everything else the branch used to
|
|
> do — body pose, queue, leash, render entity, shadow publish, and the
|
|
> velocity-derived animation cycle — is genuinely skipped.
|
|
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.
|
|
- **Added 2026-08-03 (review finding R15).** D1 makes ACE's wire `IsGrounded`
|
|
newly load-bearing for NPC remotes: ACE emits it from
|
|
`TransientState & OnWalkable` (`PositionPack.cs:73`), while acdream's NPC
|
|
free-fall was gated on the client-tracked `rmState.Airborne` (set only by
|
|
`0xF74E` VectorUpdate or `!Body.OnWalkable`, never from the wire bit). A
|
|
creature ACE reports as not-in-contact while the client believes it grounded
|
|
now receives NO correction where the legacy routing pulled it. **Push a
|
|
monster off a ledge / pull one down a cliff**, and confirm it falls and
|
|
lands without hovering, without a mid-air correction, and without an
|
|
invisible-but-solid body.
|
|
|
|
## 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.
|