# C4 route 2 — ForcePosition: implementation plan (2026-08-03) Executes `docs/research/2026-08-03-c4-route-2-contract.md`. The contract is the WHAT; this is the verified HOW. Every claim below was checked against source or the named retail decomp in this session — do not re-derive them, and do not contradict them without new evidence. ## 1. Retail truth (verified this session, not inherited) `SmartBox::HandleReceivedPosition` @0x00453FD0 (`docs/research/named-retail/acclient_2013_pseudo_c.txt:92896`). The FORCE_POSITION branch is the whole route: ``` if (arg2 == player && newer_event(player, FORCE_POSITION_TS, arg9)) { if () { get_heading(player); Frame::set_heading(&dest, heading); // 00454068 preserve OUR heading SmartBox::BlipPlayer(this, &dest); // 00454074 player->update_times[0] = arg7; // 00454079 stamp POSITION_TS cmdinterp->SendPositionEvent(); // 00454091 ack return; // 0045409d } } ``` `SmartBox::BlipPlayer` @0x00453940 (line 92528) is: ``` distance = Position::distance(&player->m_position, dest); CPhysicsObj::SetPositionSimple(player, dest, 1); // 00453968 SmartBox::PlayerPositionUpdated(this, 0, distance); // 00453976 ``` `CPhysicsObj::SetPositionSimple` @0x005162B0 (line 284276) with `arg3 != 0` builds `SetPositionStruct` with flags **`0x1012`** and calls `CPhysicsObj::SetPosition`. `0x1012` decodes against `src/AcDream.Core/Physics/PhysicsSetPosition.cs:63-76` as `Teleport(0x002) | Slide(0x010) | SendPositionEvent(0x1000)` — byte-for-byte `RuntimeAuthoritativePositionRouteClassifier.AuthoritativeTeleportFlags` (`RuntimeAuthoritativePositionRouteClassifier.cs:198-200`). **The pinned classifier route is confirmed correct; do not touch it.** Three consequences that decide this slice: ### 1a. ForcePosition is a real SetPosition, not a snap Retail runs the full transition with Teleport|Slide. Today's `PlayerMovementController.BlipPosition` (`src/AcDream.Runtime/Gameplay/PlayerMovementController.cs:1923-1937`) is `_body.SnapToCell(...)` — no transition, no collision, no contact plane, no shadow commit, no `FullCellId`/`PlacementCommitVersion` advance. Closing that gap is the point of route 2. ### 1b. The force branch runs NO ConstrainTo Every `CPhysicsObj::ConstrainTo` call in `HandleReceivedPosition` is at 0x00454272 (remote MoveOrTeleport tail), 0x0045418A (player teleport-newer branch), and 0x004541EC (player ordinary branch). The force branch returns at 0x0045409D, **before all three**. The classifier already encodes this as `ConstrainPhase: None`. `BlipPosition` calls `RearmConstraintLeashAtCurrentPosition()` and its comment cites *"retail 'Player, normal' branch"* — a branch `BlipPlayer` is not on. That leash re-arm is an unbacked deviation on this route. **Required:** the new route honours `ConstrainPhase: None` — no leash re-arm on ForcePosition. Call it out explicitly in the commit message as a named behaviour change with these addresses, and add it to the connected gate's watch list (#167 was a leash bug; the user's live observation governs). Do not touch `ArmConstraintLeashAtCommittedPlacement` (C3c/AD-42 first-entry) or the teleport/`CommitPreparedPosition` callers — they are on branches that DO constrain. ### 1c. Heading preservation already happens upstream — do not re-derive it Retail replaces the destination heading with the player's current heading BEFORE the SetPosition. Our accepted-position merge already does this via the `forcePositionRotation` argument (`RuntimeLiveEntitySessionController.cs:179`, App's `_authorityGate.TryAcceptPosition(..., _playerController.BodyOrientation, ...)` at `LiveEntityNetworkUpdateController.cs:1050-1052`). Verify it lands in `record.Snapshot` before you build the route request; assert it in a test. The seam must NOT apply a second heading substitution. Also noted, NOT in scope: retail's `PlayerPositionUpdated(this, 0, distance)` gates `set_viewer`/`LScape::update_viewpoint` on `distance >= GetAutonomyBlipDistance` (0x004538C0-0x004538E2). We publish the render root unconditionally. File it as a follow-up observation in the closeout note; do not change it here. ## 2. Verified mechanism map | Thing | Location | Note | |---|---|---| | Route classifier (pinned, correct) | `RuntimeAuthoritativePositionRouteClassifier.cs:308` | one production consumer today: `RuntimeInitialCreateContinuationExecutor.cs:1948` | | Begin | `RuntimeSetPositionState.TryBeginExclusiveAuthoredPlacement:1309` | no first-entry-only precondition | | Prepare+submit+commit | `RuntimeSetPositionState.TryPrepareAndSubmitAuthoredPlacement:1658` → `PrepareMover:1522` → `SubmitPreparedPlacementCore:2777` → `Engine.SetPosition:2962` → `CommitCanonical:4411` | | | Deferred wake | `CommitCollisionGeneration:3973` → `RetryDeferred:4192` → `CommitCanonical:4374` | drives parked operations without our help | | **The template to mirror** | `RuntimeFirstEntryDriveController.TryCompleteContinuationPlacement:280-380` | begin/prepare/submit + outcome switch + projection acknowledgement | | Runtime world frame | `RuntimePhysicsState.ObserveLocalWorldFrame:535`; `resolveWorldOffsetFromRuntimeFrame` param | **use it** — satisfies contract §7, one conversion site | | App projector (already exists) | `LiveEntityRuntime.TryApplyRuntimePlacementPlace` (`LiveEntityRuntime.cs:1231+`) | writes `entity.SetPosition(projection.WorldPosition)`, `entity.Rotation`, `entity.ParentCellId = token.ExactCellId`, `RebucketLiveEntity` — from the COMMITTED result | | App sink | `RuntimePlacementPresentationSink.TryApply:67` / `TryPublishPlace:162` | | | Headless sink | `HeadlessRuntimePlacementProjectionSink` | | **This is the crux:** `TryApplyRuntimePlacementPlace` already performs, from canonical committed state, exactly the four writes the generic tail performs from raw wire (`LiveEntityNetworkUpdateController.cs:1264-1281`). Deleting the generic tail for the local player is a straight substitution of the committed result for the wire guess — not a loss of function. ## 3. The two duplicate authorities to delete **Graphical** — `src/AcDream.App/Physics/LocalForcePositionTransaction.cs` (whole file) and its single call site `LiveEntityNetworkUpdateController.cs:1113-1129`; plus the generic tail `:1264-1282` **for the local player only** (remotes still need it — that is route 4). **Headless** — `RuntimeLiveEntitySessionController.OnPositionUpdated:217-231`'s `ProjectPosition(..., isLocalPlayer: true, ForcePosition)` + `SendImmediatePosition` pair, and `HeadlessSessionWorldProjection.BlipLocalPlayer:707-727`. Headless has no `WorldEntity` in this path, so it has only the first duplicate — but it is a duplicate all the same, and contract §4 requires both hosts on the identical Runtime path. ## 4. Design New Runtime type, modelled directly on `RuntimeFirstEntryDriveController`: **`src/AcDream.Runtime/Session/RuntimeAcceptedPositionDriveController.cs`** Constructor takes the same collaborators that controller takes (`_entityObjects`, `IPreparedCollisionSource`, the simulation clock, `LocalPlayerOutboundController`, the session accessor, the local-player identity). Follow that file's ctor and null-validation style exactly. ### Entry point ```csharp internal RuntimeAcceptedPositionExecutionStatus TryExecuteAcceptedLocalPosition( RuntimeEntityRecord record, in WorldSession.EntityPositionUpdate update, PositionTimestampDisposition disposition, in AcceptedPhysicsTimestamps timestamps, ushort previousTeleportSequence); ``` **Scope this slice to ForcePosition on the live local player.** Any other disposition, any other entity kind, and the not-applicable cases below return `NotApplicable`, and the caller then does exactly what it does today. Route 2 must not perturb routes 1/3/4. Return `NotApplicable` when: - `disposition is not PositionTimestampDisposition.ForcePosition`; - the record is not the local player; - `record.PhysicsBody is null` (no canonical body → nothing to place); - **an initial-Create residence is still active for the record.** Route 1 owns it: the executor already retains the Position as a tail action (`RuntimeInitialCreateContinuationExecutor.ApplyPositionAction`) and already carries `SendPositionImmediately` (`:717`, `:2576`). Confirm that ack actually fires on that path and say so in the closeout; if it does not, that is a route-1 defect — file it, do not paper over it here. ### Body 1. Build `RuntimeAcceptedPositionRouteRequest` deriving every field the way `RuntimeInitialCreateContinuationExecutor.ApplyPositionAction:1907-1946` derives it. Specifically: `HasContact = update.IsGrounded` (the wire bit, never a live body query); `HasAnimations` from `Snapshot.MotionTableId ?? Snapshot.Physics?.MotionTableId` non-zero; `CommittedCellId = record.FullCellId`; `PlacementFacts` from `record.FinalPhysicsState` and `Snapshot.SetupTableId is not null`; `Source = PositionEvent`; `UsePositionFromServer` and `PlayerDistance` from the same Runtime owners C0 established (`RuntimeCharacterState.AutonomyLevel != 2`; the live movement controller). Read C0's notes in `docs/plans/2026-08-02-placement-cutover.md` first. 2. `ClassifyAcceptedPosition`. Not accepted → stamp-only, mirroring `:1950-1984`; return without ack. 3. `TryBeginExclusiveAuthoredPlacement(record, record.PositionAuthorityVersion, route.OperationKind)`. Invalid token → `Contention`; the caller retries on a later packet. Do NOT invent a retry loop. 4. `TryPrepareAndSubmitAuthoredPlacement(record, token, route.OperationKind, route.SetPositionFlags, _collisionSource, _clock.SimulationTimeSeconds, out outcome, resolveWorldOffsetFromRuntimeFrame: true)`. 5. Outcome switch, mirroring `TryCompleteContinuationPlacement:340-380`: - `CommittedHostAcknowledgementPending` → §4a reconcile, then §4b ack. - `DeferredCell` → §4c. - anything else → forget + `PublishCancellation`; **no ack**. ### 4a. Post-commit local reconciliation `CommitCanonical` writes the body, contact plane, `FullCellId`, `PlacementCommitVersion`, shadow membership and the spatial acknowledgement. It does NOT do the three controller-local things `BlipPosition` also did. Add ONE new method to `PlayerMovementController` next to `BlipPosition`, e.g. `CommitCanonicalForcePositionFrame()`, that: - resets `_prevPhysicsPos`/`_currPhysicsPos` to the committed body position (kills the render-lerp residual); - calls `UpdateCellId(_body.CellPosition.ObjCellId, "force-position")` so the render root chokepoint `PhysicsEngine.UpdatePlayerCurrCell` still runs; - does **not** re-arm the constraint leash (§1b); - does **not** write the body — the canonical commit already did. Then `BlipPosition` has no remaining caller: **delete it** along with `HeadlessSessionWorldProjection.BlipLocalPlayer`. If a test is its only other caller, the test moves to the new path — do not keep the method alive for tests. ### 4b. The ack `SendPositionImmediately` is an OUTPUT of the committed route. Fire `LocalPlayerOutboundController.SendImmediatePosition(session, controller)` only after §4a, only when `route.SendPositionImmediately`, and only once. This is the named behaviour change the contract calls out: today the packet leaves before any commit and the trailing `isCurrent()` cannot recall it. ### 4c. Deferred cell `DeferredCell` means the destination landblock's collision generation is not ready; the operation parks and the existing `CommitCollisionGeneration` wake resubmits and commits it. The ack must still fire exactly once, after that commit. Retain the pending ack keyed by the placement token, and resolve it on a pump `Advance()` called from the SAME two host sites that already call `RuntimeFirstEntryDriveController.DriveAll()` — `LiveEntityHydrationController.cs:405` (graphical) and `HeadlessSessionWorldProjection.cs:571,594,612` (headless). Find the existing read-only way to ask "did this token's operation commit / is it gone" before adding anything; only add a minimal internal query to `RuntimeSetPositionState` if none exists. Fire once on the commit transition; drop the pending ack on cancellation, supersession, entity teardown, generation change and reset, and fold its count into the ownership ledger / `IsConverged` so a leaked pending ack cannot hide. Also consume the parked `Withdraw` at the FIFO head exactly the way `TryCompleteContinuationPlacement:351-365` does, if and only if it is ours. ### 4d. Host cutover - **App** `LiveEntityNetworkUpdateController.OnPosition`: replace the `LocalForcePositionTransaction.Apply` block with the Runtime call. When the status is anything other than `NotApplicable`, return before the generic tail — App projects the committed result through the existing placement sink. Keep every currency re-check that is still meaningful. Inject the Runtime seam the way the class already borrows Runtime owners (`_localPlayerOutbound` is the precedent); do not add a service locator or a window back-reference. - **Headless** `RuntimeLiveEntitySessionController.OnPositionUpdated`: replace the `ProjectPosition(isLocalPlayer: true, ForcePosition)` + `SendImmediatePosition` pair with the same call. Leave the `Apply`- disposition `OfferTeleportDestination` and `TryCompletePortal` alone — route 3. - Delete `LocalForcePositionTransaction.cs`. ## 5. Non-negotiables - Root causes only. No timeout, grace period, suppression flag, catch-and-swallow, duplicated placement writer, or test-only bypass. - Never `git add -A` / `git add .` / `git reset --hard` / `git checkout -- `. - Do not weaken an existing assertion to make a test pass. If a fixture models the old duplicate-write behaviour, re-model it on the committed projection. - Cite retail as `named symbol @address` (+ the pseudo-C line) in every comment on ported behaviour. - Update `docs/ISSUES.md` and `docs/architecture/retail-divergence-register.md` in the SAME commit as the behaviour change. AP-131 is **not** retired here — its named legacy Position caller is route 4. ## 6. Acceptance 1. Focused Runtime tests for the seam: force route classification; ack strictly after commit; ack exactly once; no ack on a rejected/cancelled operation; the displaced-authority case that `LocalForcePositionTransaction`'s trailing `isCurrent()` covered today; the `DeferredCell` → wake → commit → single ack sequence; heading preserved; leash NOT re-armed; `NotApplicable` while a first-entry residence is active. 2. App tests proving the generic tail no longer double-writes the local player, and that the committed projection is what moves the render entity. 3. Headless tests proving the identical Runtime path. 4. `dotnet build -c Release` clean. 5. **Complete Release solution suite green — not a focused subset.** Baseline **10,844 passed / 4 skipped / 0 failed**. Any deviation is a regression introduced by this work. 6. Connected (user-gated): a server-forced correction leaves the player at the corrected position, heading preserved, exactly one outbound `AutonomousPosition`, no double-apply, and no leash misbehaviour after the correction.