# C4 route 2 — ForcePosition: pinned contract (2026-08-03) Scoping is complete; implementation has not started. This is the pinned contract the plan's standing discipline requires before code (`docs/plans/2026-08-02-placement-cutover.md`, "Standing discipline per slice"). ## Scoping finding — this is not a wiring job `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition` already produces the retail-exact ForcePosition route, but grep shows exactly ONE production consumer: `RuntimeInitialCreateContinuationExecutor.cs:1948`, the route-1 Create continuation. For an already-live local player receiving a Position there is **no Runtime consumer at all** — `LiveEntityNetworkUpdateController.OnPosition` does the work itself in App. So route 2 must build the accepted-Position execution seam and then cut App over. The Create executor is the model to follow, not a component to reuse unchanged. ## Today's behaviour (the two duplicate authorities) Wire `0xF748` → `WorldSession.cs:1767` → `LiveEntitySessionController.cs:67` → `LiveEntityNetworkUpdateController.OnPosition:1024` → `_authorityGate.TryAcceptPosition` → `PhysicsTimestampGate.TryAcceptPositionEvent` (retail `SmartBox::HandleReceivedPosition` @0x00453FD0, FORCE_POSITION branch) → `forceLocal` computed at `:1112`. **Authority 1 — `LocalForcePositionTransaction.Apply` (`src/AcDream.App/Physics/LocalForcePositionTransaction.cs`):** `if (!isCurrent()) return false; blip(); acknowledge(); return isCurrent();` where - `blip` = `PlayerMovementController.BlipPosition` (`_body.SnapToCell`, then `UpdateCellId`, which also publishes the render root via `_physics.UpdatePlayerCurrCell`); - `acknowledge` = `LocalPlayerOutboundController.SendImmediatePosition` — **the outbound `AutonomousPosition` ack is sent to ACE BEFORE any canonical Runtime commit exists.** We tell the server "got it, I'm here" before deciding where "here" is. **Authority 2 — the generic tail (`LiveEntityNetworkUpdateController.cs:1264-1281`):** execution falls through unconditionally into `entity.SetPosition(worldPos)`, `entity.ParentCellId = p.LandblockId`, `entity.Rotation = rot`, then `RebucketLiveEntity`. That is a SECOND independent mutation of the SAME accepted Position, against the render-facing `WorldEntity`, parallel to the `PlayerMovementController` body mutated by authority 1. Two stores, one packet — the same divergence class as the remote-placement bug `670f307c` fixed. ## Retail truth `SmartBox::HandleReceivedPosition` @0x00453FD0. The FORCE_POSITION branch is an early return (~92932) that precedes `unset_parent` (~92990) and the `!HasAnims`-gated `SetPlacementFrame` (~92992). The classifier already encodes this exactly: | Field | Value | Meaning | |---|---|---| | Disposition | `SetPositionSimple` | one plain SetPosition | | Flags | `AuthoritativeTeleportFlags` | server-authoritative placement | | `UnparentBeforeRouting` | false | branch precedes `unset_parent` | | `ApplyPlacementFrameBeforeRouting` | false | branch precedes `SetPlacementFrame` | | `TeleportHookPhase` | `None` | not a teleport | | `PreserveHeading` | **true** | force keeps the player's facing | | `ZeroVelocity` | false | velocity untouched | | `SendPositionImmediately` | **true** | ack AFTER the position operation | `SendPositionImmediately` being a property of the executed route is the key point: the acknowledgement is an *output of* the committed operation, never a step performed alongside it. ## The contract 1. One Runtime `SetPosition` transaction owns the accepted frame, exact cell, collision result, shadow/workset membership, and deferred-cell lifetime for a ForcePosition on a live local player. 2. `LocalForcePositionTransaction` is **deleted**, not adapted. Its three jobs become properties of the executed route: ownership validation is the operation's own currency check, `blip` is the SetPosition commit, and `acknowledge` is `SendPositionImmediately` fired after commit. 3. The generic tail must not independently mutate the same accepted Position for the local player. App projects the committed result. 4. Graphical and headless drive the identical Runtime command and state path. 5. Stale sequences, GUID reuse, missing cells, portal generations, and replaced collision generations cannot commit old state. 6. No route reconstructs from a stale spawn; no legacy outdoor demotion or terrain-Z lift. 7. The world-frame invariant from #283 (`LiveWorldOriginState.EnsureAgreesWithRuntimeFrame`) holds across the new path — route 2 converts landblock-local wire origins and must not introduce a second conversion site. ## Acceptance - Focused Runtime tests for the accepted-Position execution seam, including the ack-after-commit ordering and the displaced-authority case that `LocalForcePositionTransaction`'s trailing `isCurrent()` currently covers. - App tests proving the generic tail no longer double-writes the local player. - **Complete Release solution suite green before the commit** — not a focused subset. This gate is what the #281–#284 regressions bypassed. - Connected: a server-forced correction (ACE `@teleport`-style displacement or a rubber-band) leaves the player at the corrected position with heading preserved, one outbound ack, and no double-apply. - Divergence ledger: AP-131 is retired only when the legacy Position caller it names is actually gone, which is route 4, not route 2. ## Risk notes for the implementer - This is the hottest inbound path in the client; every local-player Position crosses it. Prefer extending the existing accepted-Position classifier consumption over inventing a parallel executor. - `PhysicsTimestampGate.TryAcceptPositionEvent` already returns `ForcePosition` only when newer AND `teleport == current teleport stamp`. Do not re-derive that condition at the new seam. - The ack currently fires even when the commit is subsequently displaced; the trailing `isCurrent()` only suppresses the CONTINUATION, not the ack that already went out. Ack-after-commit fixes this by construction — call it out in the commit message as a behaviour change, because it is one.