acdream/docs/research/2026-08-03-c4-route-4a-contract.md
Erik 2f0c26c8a4 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>
2026-08-03 22:17:22 +02:00

7.7 KiB

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.

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-1871the 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.