diff --git a/docs/research/2026-08-03-c4-route-4-scoping.md b/docs/research/2026-08-03-c4-route-4-scoping.md new file mode 100644 index 00000000..ad733510 --- /dev/null +++ b/docs/research/2026-08-03-c4-route-4-scoping.md @@ -0,0 +1,175 @@ +# C4 route 4 — scoping, and a stop-and-re-plan (2026-08-03) + +Scoped at HEAD `e3b766d9`, after route 2 shipped. **This is not a contract.** The +scoping says route 4 is materially larger than the budget it was given, so the +correct next action is a user decision, not an implementation pass. + +## The budget, and how it failed + +After route 2 I pinned a falsifiable bet: *routes 4-7 reuse the seam route 2 +built, so their marginal cost should be far lower — well under 400 production +lines. If route 4 also costs ~900, the bet is dead and that is the signal to +stop and simplify.* + +**Estimate: 1,500-2,500 lines of new/changed production code**, plus ~1,700 +lines of test re-modelling. The bet failed before implementation started. + +But it failed for a reason worth stating precisely, because it changes what to +do about it: **the seam generalises fine. Route 2 was just not a representative +unit.** + +| | route 2 | route 4 | +|---|---|---| +| entities | 1 (local player) | N (every visible remote) | +| dispositions | 1 (ForcePosition) | 4 (SetPosition / SetPositionSimple / Interpolate / NoPositionOperation) | +| execution paths | 1 | 2 (canonical SetPosition **and** the interpolation queue) | +| teleport hook | never | `BeforePositionOperation` | +| constrain phase | never | `AfterPositionOperation` | +| duplicate authorities to delete | 2 | **6** | +| outbound ack | owns it | none | +| frequency | ~never | 5-10 Hz x N | + +I chose route 2 first because it was simplest to reason about, then used it to +calibrate everything after it. That was the error: a one-entity, one-disposition, +no-interpolation route is the floor, not the median. + +## What is genuinely reusable (the good news) + +- The begin/prepare/submit/commit chain is **entity-kind agnostic** — no + local-player precondition anywhere in `TryBeginExclusiveAuthoredPlacement` → + `TryPrepareAndSubmitAuthoredPlacement` → `SubmitPreparedPlacementCore`. +- The classifier's remote branches are **retail-exact already** (verified against + `CPhysicsObj::MoveOrTeleport` @0x00516330 on all five behaviours). +- The placement receipt → presentation path is already entity-agnostic; C3c + drives remote Creates through it today. +- **All remote physics state already lives in Runtime** (J5.5) — `RemoteMotion`, + `EntityPhysicsHost`, the interpolation queue. No assembly boundary to cross. +- **`RuntimeAcceptedPositionDriveController` itself is ~15-20% liftable** — the + architecture, not the code. It is single-entity and ack-owning by construction + (`_pending` is one slot and `RetainPending` throws on a second). + +## Four findings that change the campaign plan, not just route 4 + +**1. Route 4's Create half is already done.** C3b/C3c landed +`RuntimeRemoteFirstEntryState` + `RuntimeRemoteBodyDescription` and flipped both +hosts' Creates onto the residence conductor. Route 4's remaining work is +**steady-state remote Position plus the deletions**. The route title is +misleading and will send an implementer hunting for a flip that already happened. + +**2. AP-131 cannot be retired by route 4.** The row covers two halves: the +`!HasAnims` gate on `SetPlacementFrame`, and the FORCE_POSITION branch skipping +`unset_parent`. Route 2 did **not** fix the second — it consumes an +already-merged snapshot, so a ForcePosition still passes +`installPlacementFrame: true, clearParent: true`. Retiring AP-131 needs the +unconditional merge gone for *every* Position: remote (route 4), local +ForcePosition (a route-2 residual), **and local ordinary-Apply — which no route +in the eight-route inventory owns.** Either widen route 4 or move AP-131 to C5 +with those two as prerequisites. Pinning "route 4 retires AP-131" pins something +route 4 alone cannot deliver. + +**3. #277's bound breaks.** Its safety argument is that the collision window is +strictly larger than ACE's Create-broadcast group, so the far set is empty. That +is a statement about *Creates*. A steady-state Position can carry a remote out of +the window with no Create at all — a creature wandering off, or the >=96 m +far-snap branch by definition targeting something distant. Route 4 introduces a +second producer of far parks the radius argument never covered. Needs a +Position-time service-window guard on **both** hosts; headless has the predicate, +the graphical host has none and one must be built. + +**4. N3 stops being latent.** The route-2 round-2 finding — headless never calls +`RetryPending` after construction, so a declined `Place` would wedge the ordered +stream — was filed as latent because headless remotes produced no placement +receipts. Route 4 makes them produce receipts for the first time. **Fix N3 in +route 4 or the headless stream wedges.** + +## Three unfiled divergences found during scoping + +Our production remote path diverges from the retail classifier on five +behaviours; three have no register row: + +- **NPC airborne hard-snap** (`LiveEntityNetworkUpdateController.cs:1819-1823`) + writes `Body.Position`/`Orientation` and never consults `update.IsGrounded` at + all — it branches on the client-tracked `rmState.Airborne`. Retail returns 0 + and writes nothing (@0x0051636D). +- **`ConstrainTo` armed before the operation, unconditionally** (`:1522-1529`), + so it arms on the airborne no-op retail skips and anchors to the *pre*-move + position on the far branch. Retail arms it *after* a nonzero `MoveOrTeleport`, + anchored to the post-move `m_position` (@0x00454254-@0x00454272). +- **`ConstrainTo` never armed on the remote teleport branch** (the path returns + at `:1504`, before `:1522`), where retail *does* re-arm after `teleport_hook`'s + `UnConstrain`. + +Route 4 fixes all three by construction — which means **route 4 is a behaviour +change to every visible creature in the game, not a refactor.** + +## Allocation: not the blocker anyone feared + +The inventory's "frame-frequency routes are blocked on placement allocation" +warning was written pre-C2 and before anyone checked which branch remotes take. +The steady state — nearby, grounded, non-teleporting — classifies to +**`Interpolate`, which runs no SetPosition at all**. The 944 B/op cost applies +only to teleports, cellless first placements, and >=96 m remotes. + +Arithmetic (assumptions, not a capture): 40 remotes x 7 Hz with ~10% on a +SetPosition branch ~= 26 KB/s. Pathological dense-town worst case ~= 264 KB/s, +about 2.4% of the Slice-G5 profile. **Not a blocker — provided the near branch +stays out of `SetPosition`.** The contract must forbid routing every remote +Position through it "for uniformity". + +Unmeasured and worth measuring before pinning a budget: the legacy path's actual +cost (no gate covers `PhysicsEngine.SetPosition`), and the real near/far split in +live play. + +## Route 4's one big advantage: it is trivially observable + +Route 2 cost five review rounds largely because nothing a user could do would +trigger it. Route 4 is the opposite — every remote entity exercises it +continuously. Six checks, all within two minutes of ordinary play with a second +character: + +1. **Near interpolate** — second character walks/runs in a circle 5-15 m away. + Smooth motion, no per-packet stepping. +2. **Airborne no-op** — they jump, and jump off a ledge. Clean parabola, clean + landing. Watch for invisible-but-solid, the #184 signature. +3. **Far-snap** — they run >96 m and back. No freeze at the boundary, no wrong Z. +4. **Remote teleport** — portal/recall out and back, plus an admin `@teleto`. + Arrive grounded, animating, immediately targetable. +5. **The leash** — route 4 moves `ConstrainTo` from before to after, stops arming + it on the airborne no-op, starts arming it on teleport. #167 was a leash bug. +6. **Creature blast radius** — pull a drudge, let it chase, melee it, let it die. + +Weight this gate heavily. All four route-2 states were green and three were +defective; route 4 finally has a real oracle. + +## The decision + +Route 4 is worth doing — it fixes three unfiled divergences affecting every +creature, retires the largest duplicate-authority cluster in the codebase, and +builds the seam route 5 needs (the classifier gives Projectile *identical* +branches to Remote). But it is a 1,500-2,500 line behaviour change, not a wiring +job, and it should not start on a budget everyone knows is wrong. + +Options, in the order I would rank them: + +**A. Split route 4 into 4a and 4b.** 4a = the interpolation/near path plus the +airborne no-op (the steady state, highest visible value, no park hazard, no +service-window work). 4b = teleport/far/cellless, which is where the parks, the +service-window guard, N3 and #277 all live. Each gets its own contract, gate and +review. Roughly halves the blast radius per commit and puts the observable win +first. + +**B. Do route 4 whole against a corrected budget** (~2,000 lines, 2-3 +implementation passes, one adversarial review per pass). Honest, but it is a +large single commit touching every visible creature. + +**C. Defer route 4; do routes 5-7 first.** Rejected — route 5 needs the same +seam, so this just moves the cost. + +**D. Stop the campaign here and simplify instead.** The complexity concern that +prompted the budget is real, and `RuntimeSetPositionState` at 5,652 lines is the +obvious target. But route 4 is where three live divergences get fixed, so +stopping leaves known-wrong behaviour in the client. + +**Recommendation: A.** It respects the budget's intent — keep each landing small +enough to review — without abandoning work that fixes real bugs. It also front- +loads the part with a clean live gate and defers the part with the known traps.