acdream/docs/research/2026-08-03-c4-route-4-scoping.md
Erik 40f5721354 docs: C4 route 4 scoping — the stated budget failed, stop and re-plan
After route 2 I pinned a falsifiable bet: routes 4-7 reuse the seam route 2
built, so their marginal cost should be well under 400 production lines, and if
route 4 also cost ~900 the bet was dead. Scoping estimates 1,500-2,500 lines
plus ~1,700 lines of test re-modelling. Honouring the bet: no implementation
pass until the scope is re-planned.

The bet failed for an instructive reason. The seam generalises fine — the
begin/prepare/submit chain has no local-player precondition, the classifier's
remote branches are already retail-exact, and all remote physics state is
already in Runtime. Route 2 was simply not a representative unit: one entity vs
N, one disposition vs four, one execution path vs two (canonical SetPosition
AND the interpolation queue), no teleport hook, no constrain phase, two
duplicate authorities vs six. Picking the simplest route first and then
calibrating everything against it was the error.

Four findings that change the campaign plan, not just route 4:

- Route 4's Create half is already done (C3b/C3c). The remaining work is
  steady-state remote Position plus deletions; the route title misleads.
- AP-131 cannot be retired by route 4. Route 2 did not fix its FORCE_POSITION
  half, and its local ordinary-Apply half is owned by no route in the inventory.
- #277's safety bound breaks: it argues about Creates, while a steady-state
  Position can carry a remote out of the collision window with no Create at all.
  Needs a Position-time service-window guard on both hosts; the graphical host
  has no such predicate today.
- N3 (headless never calls RetryPending) stops being latent the moment route 4
  makes headless remotes produce placement receipts.

Also records three previously unfiled divergences found while scoping: the NPC
airborne hard-snap that ignores the wire IsGrounded bit, ConstrainTo armed
before the operation instead of after, and ConstrainTo never armed on the remote
teleport branch. Route 4 fixes all three by construction, which makes it a
behaviour change to every visible creature rather than a refactor.

Allocation is NOT the blocker the inventory feared: the steady state classifies
to Interpolate, which runs no SetPosition at all.

Recommends splitting route 4 into 4a (near/interpolate + airborne no-op — the
observable win, no park hazard) and 4b (teleport/far/cellless — where the parks,
the service-window guard, N3 and #277 live).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 19:46:01 +02:00

175 lines
9.3 KiB
Markdown

# 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.