acdream/docs/research/2026-08-04-c4-route-4b-scoping-and-split.md
Erik dda1e2a03a docs: C4 route 4b scoping — split three ways, and two corrections
Scoping at 44830a0e puts 4b at 1,300-2,200 production lines (centred ~1,700)
plus ~2,500-3,500 lines of test work — 4-6x route 4a and ~2x route 2, the two
largest landings in this campaign, which took 4 and 5 review rounds. Split into
4b-1 (infrastructure, no behaviour change), 4b-2 (far branch), 4b-3 (teleport
and cell-less, with the ~739-line class deletions). 4b-1 stays separate
regardless of appetite for landings.

Corrects two errors in documents from yesterday:

AP-135 does NOT retire with 4b. Its own condition is retirement with the
free-fall sweep gate, which 4b does not touch, and its sites are the airborne
no-op branches — 4a-owned dispositions. The trap is that its two writes sit
inside OnPosition, which 4b rewrites heavily.

Retail has exactly ONE ConstrainTo on the remote arm (@0x00454272); all three
nonzero-returning MoveOrTeleport branches funnel through it. My route-4 scoping
implied a distinct remote-teleport arming site. There is none, so 4b must not
add a second one — the post-operation arm 4a introduced becomes the only arm.

Records a new failure mode 4b must not create: a DeferredCell park WITHDRAWS the
entity (InWorld false, Active cleared, clock suspended, residency dropped), and
Forget-on-every-accepted-Position kills the park without restoring any of it. If
the next packet classifies Interpolate, no placement runs and the remote stays
withdrawn indefinitely — invisible AND intangible, the #184 class through a
third door. Direction: refuse rather than park; the next packet is the retry,
because remote Positions are a 5-10 Hz stream.

Two transfer errors named explicitly so they are not repeated: do not port route
2's re-issue funnel (re-issuing a superseded pose is wrong for a repeated
stream), and do not port its ack machinery (retail's remote arm has no
SendPositionEvent).

Also records that remotePlacementRequired fires for every non-visible remote on
the graphical host — a routine hot path, not a teleport rarity — and that
deleting the legacy blocks removes the only handler for null/Rejected*, which
during the login window is every remote packet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 00:51:32 +02:00

8.5 KiB

C4 route 4b — scoping, a three-way split, and two doc corrections (2026-08-04)

Scoped at HEAD 44830a0e (route 4a landed). 4b is 4-6x route 4a and must be split again. This also corrects two errors in documents I wrote yesterday.

Correction 1 — the plan is wrong about AP-135

docs/plans/2026-08-02-placement-cutover.md says AP-135 "retires with 4b's transition machinery, not before." That is wrong.

AP-135's own retirement condition is "retire together with the free-fall sweep gate, when the remote arc is resolved by the same transition machinery the local player uses" — that gate is RuntimeRemotePhysicsUpdater.cs:342, which 4b does not touch. Worse, AP-135's sites are the airborne no-op branches, which are 4a-owned dispositions, not 4b's. 4b owns far-snap, teleport and cell-less.

AP-135 stays open after 4b. The trap is real: its two writes sit physically inside OnPosition, which 4b rewrites heavily, so an implementer will assume they go. The contract must say they do not.

Correction 2 — retail has ONE ConstrainTo on the remote arm, not two

My route-4 scoping described the third divergence as retail "re-arming after teleport_hook's UnConstrain", implying a distinct remote-teleport site. There is none. Retail has exactly one: ConstrainTo @0x00454272, and all three nonzero-returning MoveOrTeleport branches — teleport (returns 1 @0x00516438), near-interpolate (@0x005163BE), far-snap (@0x005163E8) — funnel through it.

Consequence for the contract: 4b must not add a second arming site. The single post-operation arm 4a introduced becomes the ONLY arm, with remotePlacementRequired folded into it rather than returning ahead of it.

The behaviour claim still stands: today a remote hard-teleport arms the leash nowhere, because the remotePlacementRequired block returns before every arming site while RemoteTeleportHook has already called UnConstrain. Severity is narrow — the next Position re-arms ~100-200 ms later — except for a remote that teleports and then stands still, since ACE stops broadcasting for a stationary entity.

The new failure mode 4b must not create

A Runtime DeferredCell park WITHDRAWS the entity from the world. ParkDeferred sets body.InWorld = false, clears Active, suspends the object clock, calls WithdrawCanonical, and publishes a Withdraw.

Forget-on-every-accepted-Position then kills the park without restoring any of it. CancelCoreDeferred removes the operation and rewrites the Withdraw into a Discard; it does not set InWorld back, resume the clock, or re-enter residency.

So: packet N parks E (now invisible AND intangible). Packet N+1 ~150 ms later Forgets the park. If N+1 classifies Interpolate — near, in contact, committed cell — no placement runs and E stays withdrawn indefinitely. The sequence "remote appears beyond 96 m, walks toward you, crosses inside 96 m" produces it. That is the #184 class through a third door, and it is worse than #184 because the entity is intangible too.

Required direction: refuse rather than park. A remote whose destination is not placeable keeps its last committed pose and waits for the next packet — the next packet IS the retry, because remote Positions are a 5-10 Hz stream. That is retail-shaped (retail's world is fully resident; "arrived but not placeable" is unrepresentable) and avoids withdrawal-restore surgery inside a 5,652-line class.

Do NOT port route 2's re-issue funnel. It exists because a ForcePosition is a one-shot correction ACE never repeats. Re-issuing remote packet N after N+1 has merged would apply a pose the newer packet already superseded — the same class of route-2-to-4a transfer error that cost a review round, in reverse.

Do NOT port the ack machinery. Retail's remote arm has no SendPositionEvent; the whole PositionEventOwed apparatus has no analogue.

Two traps that will bite an implementer

T1 — remotePlacementRequired and the classifier's cellless are different predicates. remotePlacementRequired derives from wasCellless measured BEFORE the merge, plus projectionRequiresTeleportHook — which on the graphical host includes !IsSpatiallyVisible, so it fires for every remote that is not currently visible, a routine hot path rather than a teleport rarity. The classifier's cellless reads FullCellId AFTER the merge. They disagree in both directions. Reconciling them is a design decision, not a rename.

T2 — deleting the legacy near/far blocks removes the only handler for null and Rejected*. ClassifyRemoteAcceptedPosition returns null whenever _playerController is null — which during the login window is every remote packet. Today those fall into the legacy blocks and hard-snap. Delete the blocks without a replacement and remotes will not move at all until the local controller exists. Retail has no analogue; this needs a stated acdream policy.

The split

Estimate: 1,300-2,200 non-comment production lines, centred ~1,700, plus ~2,500-3,500 lines of test work. That is 4-6x route 4a and ~2x route 2 — the two largest landings in this campaign, which took 4 and 5 review rounds.

  • 4b-1 — infrastructure, no remote behaviour change (~700-1,000 lines). The per-entity remote placement owner (route 2's controller minus the ack and re-issue funnel, plus an N-way pending map and ledger); the service-window guard (Runtime interface + a graphical implementation over GpuWorldState/StreamingController, which does not exist today); the refuse-rather-than-park policy; N3's headless RetryPending pump; and the report.json parked-count wiring #277 asks for. No production caller, or called for zero classifications. Gate: focused Runtime tests, Release suite, existing connected routes unchanged, and a proof that ParkCollisionResidents's overlap throw is unreachable. This is where the park semantics get decided and reviewed on their own, without a behaviour change confusing the signal.
  • 4b-2 — the far branch only (~350-500 lines). SetPositionSimple, PlayerDistance >= 96 m. Deletes both duplicated 96f/4f constant pairs and both fabricated Vector3.Zero reads. Trivially observable.
  • 4b-3 — teleport and cell-less (~400-700 lines). Deletes RemoteTeleportController (605), RemoteTeleportPlacement (85), RemoteShadowPlacementSynchronizer (49) and 7 wiring sites, plus 1,709 lines of their tests. Largest blast radius; needs the two-client teleport gate.

If the campaign will not tolerate three landings, merge 4b-2 into 4b-3 — but keep 4b-1 separate. It is where the invisible-remote failure decides, and it must not be reviewed at the same time as a 700-line deletion.

What 4b does NOT do

  • AP-131 is not retired. Its site is one shared merge call serving every entity kind; threading classified flags there necessarily changes the local player's ordinary Apply, which no route owns. Stays for C5.
  • AP-135 is not retired. Correction 1.
  • #276 is not closed. 4b retires its remote-placement half by construction (the canonical transaction owns cell and contact together), but SeedRemoteSpawnPlacement is not classification-gated and still fires under Interpolate, and the AD-61 local-player settle is untouched. Narrow its scope; do not pin it closed.

Two collateral hazards

  • ParkCollisionResidents throws on overlap — for every spatial root in a retiring landblock prefix holding an active operation. With N remotes holding operations, an ordinary streaming retirement becomes session-fatal. Unreachable today only because steady-state remotes hold no operations. Any 4b design must prove it stays unreachable.
  • Lost-cell deadlines leak their family. ArmLostFamilyDeadlines arms root + equipped children; the cancel path clears the root only, and no caller passes cancelLostFamily: true. Inert today because the reaper has no production caller — do not let 4b be the commit that makes it live.

Undetermined — flagged, not guessed

  1. Whether GpuWorldState.IsNearTier residency is exactly co-extensive with collision publication (gating sites are consistent; the retirement side is unverified).
  2. Whether EntityPhysicsHost.NotifyTeleported() covers retail's TargetManager::ClearTarget @0x00514F1B.
  3. Whether the merge can zero a previously-nonzero FullCellId, which decides whether classifier-cellless is a strict subset of remotePlacementRequired.
  4. Allocation cost of the current legacy far/teleport path — no gate covers PhysicsEngine.SetPosition.