feat(physics): C4 route 2 — ForcePosition through the canonical placement

A local-player ForcePosition had TWO independent writers for one accepted
packet: LocalForcePositionTransaction snapped the physics body
(PlayerMovementController.BlipPosition, a raw SnapToCell with no collision
resolve), while LiveEntityNetworkUpdateController's generic tail separately
wrote position/cell/rotation to the render WorldEntity from the raw wire and
rebucketed it. Two stores, one packet — the divergence class 670f307c fixed on
the remote path. The outbound AutonomousPosition ack also fired BEFORE any
canonical commit existed: we told ACE "got it, I'm here" before deciding where
"here" was, and the trailing isCurrent() could only suppress the continuation,
never recall the packet.

RuntimeAcceptedPositionDriveController is now the one Runtime-owned seam. Both
hosts call the identical TryExecuteAcceptedLocalPosition; App and headless
project the committed result through the existing placement projection sink
(LiveEntityRuntime.TryApplyRuntimePlacementPlace already performed the same
four writes, from committed state rather than a wire guess).

Retail: SmartBox::HandleReceivedPosition @0x00453FD0's FORCE_POSITION branch is
get_heading -> Frame::set_heading -> SmartBox::BlipPlayer @0x00453940 -> stamp
POSITION_TS -> SendPositionEvent @0x00454091 -> return @0x0045409D. BlipPlayer
is CPhysicsObj::SetPositionSimple @0x005162B0 with flags 0x1012
(Teleport|Slide|SendPositionEvent) — a real collision-resolving SetPosition,
not a snap. The pinned classifier already encoded this exactly.

Named behaviour changes:

* The ack is now an OUTPUT of the committed route, fired strictly after the
  canonical commit and exactly once per accepted force packet.
* The ForcePosition route no longer re-arms the constraint leash. The force
  branch returns at 0x0045409D, ahead of all three ConstrainTo sites
  (0x00454272, 0x0045418A, 0x004541EC); the old re-arm cited retail's "Player,
  normal" branch, which BlipPlayer is not on. The teleport, CommitPreparedPosition
  and first-entry callers legitimately still constrain and are untouched.
* A force correction that terminates WITHOUT committing still sends its
  position event and is not retried — retail's BlipPlayer discards
  SetPositionSimple's SetPositionError return and acks unconditionally.

A single _pending funnel owns the in-flight placement, deciding on the token's
PositionAuthorityVersion against the record's: equal -> clear; advanced with the
newest accepted event still a force -> re-issue, re-classified; advanced to an
ordinary Apply -> clear, since newer server truth owns that pose. This closes a
double-apply/double-ack and a silently-dropped correction that two earlier
iterations of this slice each introduced.

AD-62 records the residual: a ForcePosition our async collision publication
cannot carry to a committed placement is not re-applied. Retail has no park —
its world is fully resident and its placement synchronous — so the state is
unreachable there. AP-131 is NOT retired; its legacy Position caller is route 4.

Deleted: LocalForcePositionTransaction, PlayerMovementController.BlipPosition,
HeadlessSessionWorldProjection.BlipLocalPlayer.

Gates: complete Release solution 10,858 passed / 4 skipped / 0 failed (baseline
10,844/4/0). Two independent Opus reviews (retail-conformance and
architecture/adversarial) PASS on the final diff after three FAIL rounds; every
intermediate state was fully green, so the suite caught none of the four real
defects. Connected acceptance is NOT run: nothing a user can do makes ACE emit
a ForcePosition without retail's @pklite, which acdream does not implement — see
docs/research/2026-08-03-c4-route-2-visual-gate.md.

Known gap, recorded not claimed: the plan's acceptance item 2 is unmet. The App
double-write check is a source pin, and "the committed projection moves the
render entity" is uncovered at any layer (#292). Filed alongside: #286-#291,
#293-#296.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-03 18:46:36 +02:00
parent 22a5c95400
commit 9966b53174
25 changed files with 4292 additions and 195 deletions

View file

@ -140,6 +140,302 @@ reconciling #281's 43 test failures.
fired — the log shows zero `world frame is unreachable` failures and zero
parked placements across 9 completed reveals.
## C4 route 2 — ForcePosition placement cutover — 2026-08-03
Plan: [`2026-08-03-c4-route-2-implementation-plan.md`](research/2026-08-03-c4-route-2-implementation-plan.md);
contract: [`2026-08-03-c4-route-2-contract.md`](research/2026-08-03-c4-route-2-contract.md).
- **#285 — DONE (2026-08-03) — a ForcePosition on the local player wrote two
independent stores from one packet, and the outbound ack left before any
canonical commit existed.** `LocalForcePositionTransaction.Apply`
(App)/`HeadlessSessionWorldProjection.BlipLocalPlayer` (headless) drove
`PlayerMovementController.BlipPosition` — a raw `PhysicsBody.SnapToCell`
with no transition, no collision, no contact-plane resolve, no
`FullCellId`/`PlacementCommitVersion` advance — while the generic tail
(`LiveEntityNetworkUpdateController.cs`) independently wrote the
render-facing `WorldEntity` from the same wire frame; the App/no-window ack
(`LocalPlayerOutboundController.SendImmediatePosition`) fired immediately
after the blip, before either write's result was known. Same divergence
class as the remote-placement bug `670f307c` fixed.
**Fix:** `RuntimeAcceptedPositionDriveController`
(`src/AcDream.Runtime/Session/RuntimeAcceptedPositionDriveController.cs`) is
the single Runtime-owned accepted-Position execution seam for a
ForcePosition on an already-live local player: it drives the SAME
`RuntimeSetPositionState.TryBeginExclusiveAuthoredPlacement` +
`TryPrepareAndSubmitAuthoredPlacement` transaction every other placement
uses (retail `CPhysicsObj::SetPositionSimple` @0x005162B0, flags `0x1012`,
called from `SmartBox::BlipPlayer` @0x00453940), reconciles the
controller's render-lerp/cell state
(`PlayerMovementController.CommitCanonicalForcePositionFrame`, replacing
the deleted `BlipPosition`), and fires the ack strictly AFTER that commit —
never before it. `LocalForcePositionTransaction.cs` and
`HeadlessSessionWorldProjection.BlipLocalPlayer` are deleted outright, not
adapted; the generic render-tail write is skipped for the local player's
ForcePosition (App now projects the committed result through the existing
`RuntimePlacementPresentationSink`, the same seam every other placement
already uses).
**Two named behaviour changes, both retail-exact per this session's
verification:** (1) the outbound `AutonomousPosition` ack is now an OUTPUT
of the committed route, not a step alongside it — retail's
`cmdinterp->SendPositionEvent()` @0x00454091 runs after
`SmartBox::BlipPlayer` @0x00454074 returns, and the deleted transaction's
trailing `isCurrent()` recheck (which could only suppress the
*continuation*, not an ack that had already left) is now structurally
impossible; (2) the constraint leash is NOT re-armed on this route — every
`CPhysicsObj::ConstrainTo` call in `HandleReceivedPosition`
(@0x00454272/0x0045418A/0x004541EC) is on a branch the FORCE_POSITION early
return (@0x0045409D) never reaches. `BlipPosition`'s leash re-arm (added at
#167 Slice P5, commit `7719d25b`) was an unbacked deviation for this exact
branch — it was correct for retail's "Player, normal" branch that Slice P5
was modeling in general, but `SmartBox::BlipPlayer` is not on that branch.
#167's own historical write-up (below) is superseded for its `BlipPosition`
half by this entry.
**New behaviour (retail fidelity gain, not a regression):** the ForcePosition
now runs retail's REAL `SetPosition` collision resolve — a placement sphere
or authored Setup, not a bare teleport-shaped snap — so a corrected Z can
differ from the wire's literal Z by the placement sphere's own settle.
**R7 review correction (2026-08-03):** the FIRST implementation pass wrote
this paragraph against a fixture bug, not retail behavior — its headless
test fixture's dummy Setup sphere had its centre AT the origin (offset ==
radius), which lifted a settled origin a FULL 0.48 m radius above the
floor and was asserted as if that were the retail-correct answer. Retail's
`BlipPlayer` has never lifted the origin by a sphere radius. The real dat
human Setup `0x02000001`'s foot sphere is `(0,0,0.475) r=.48`
(`Ts46SphereListConformanceTests.cs:35-39`), whose bottom sits at
`origin + 0.475 0.48 = origin 0.005` — so a settled origin lands
**within 5 mm** of the floor it rests on, not a sphere radius above it.
The headless fixture now uses the dat-exact sphere and asserts the
measured `Z = 50.005f` (`HeadlessSessionHostTests.cs`,
`WorldProjectionIgnoresNormalEchoButBlipsForcePosition`).
Runtime tests: `tests/AcDream.Runtime.Tests/Session/RuntimeAcceptedPositionDriveControllerTests.cs`
(classification/NotApplicable guards, ack-strictly-after-commit, ack
exactly once, the displaced-authority Contention case, the DeferredCell
park→wake→commit sequence, heading preservation, leash NOT re-armed, and —
added in the fix round — re-issue-from-canonical when a subsequent
accepted Position's merge-time `Forget` cancels a watched DeferredCell
park). **R8 review correction (2026-08-03):** the DeferredCell test's name
and assertions in the FIRST pass claimed a single-ack-after-wake sequence
the fixture does not exercise — this fixture's cross-landblock resolve
measures `InContact=false` (no subsequent physics tick sweeps the body
onto the terrain in this bare-Runtime harness), so no ack fires after the
wake at all today. The test is renamed
`DeferredCell_ParksThenCommitsAndNeverDoubleAcksAfterTheCollisionGenerationWakes`
and no longer pins the current zero-ack count with an assertion (a pinning
`Assert.Empty` would fail — and read as a regression — the day Contact
correctly starts flipping); it captures the ack count once and asserts
only that further `Advance()` pumps never change it. The
park→wake→commit→single-ack sequence remains unverified pending a harness
that drives a real physics tick to establish ground contact.
**Fix-round corrections (2026-08-03), both dual reviews having FAILed the
first pass — see
[`2026-08-03-c4-route-2-review-findings.md`](research/2026-08-03-c4-route-2-review-findings.md)
for the full R1-R9 list:** (R1, HIGH) the DeferredCell park could not
survive in production — `RuntimeEntityObjectLifetime.TryApplyPosition`
Forgets the entity's in-flight SetPosition on EVERY accepted Position (any
disposition), so a park outliving one ACE broadcast (~100-200 ms) was
cancelled before its collision generation could ever commit it, silently
losing the correction forever. `RuntimeAcceptedPositionDriveController.Advance`
now detects the dead watch (`IsPlacementCompletionTracked`) and re-issues
the SAME route from the entity's current canonical snapshot rather than
leaking `_pending`. (R2, HIGH) headless lost `BlipLocalPlayer`'s collision-
neighborhood re-centering; restored via the new
`IRuntimeDirectWorldProjection.CenterOnAcceptedForcePosition`. (R3, HIGH)
the headless login-window ForcePosition fallback was dropped; restored.
(R4, MEDIUM-HIGH) the force-ack was stealing a `Place` receipt the
presentation sink had legitimately declined for its own retry contract;
removed. (R5, MEDIUM-HIGH) corrected the `Rejected` status doc's false
"no SetPosition ran" claim. (R6, MEDIUM) `_pending` could leak forever on
a mid-session cancellation (now caught by the same R1 detection) and
`SubmitAndResolve` could silently overwrite a still-live pending (now
guarded, throws on the invariant violation). (R9, LOW hygiene) a stale
`BlipPosition` doc cref, `HeadlessSessionHost._currentSession` never
cleared on teardown, and the streaming observer/pose-dirty side effects
firing for a `Rejected`/`Contention` status the route explicitly declined
to place into. Complete Release solution after the fix round:
**10,853 passed / 4 skipped / 0 failed** (10,857 total) — App 4,058/3,
Bake 15/0, Cli 4/0, Content 124/0, Core.Net 762/0, Core 4,247/1, Headless
79/0, Runtime 1,021/0, UI.Abstractions 543/0.
**Round 2 fix (2026-08-03), both delta reviews having FAILed the fix round
— see the "ROUND 2" section of
[`2026-08-03-c4-route-2-review-findings.md`](research/2026-08-03-c4-route-2-review-findings.md):**
round 1 bolted the re-issue onto ad-hoc per-branch `_pending` bookkeeping,
which had no single owner and no single lifecycle rule; that is the shared
root cause of all three round-2 blockers. Replaced with ONE funnel:
`RuntimeAcceptedPositionDriveController.SettlePending` is the sole terminal
writer of `_pending`, `RetainPending` the sole outstanding writer, and
`AbandonPending` the sole teardown writer. Its ONE decision input is the
terminal operation token's `PositionAuthorityVersion`
(`RuntimeSetPositionState.cs:50`) versus the live record's current value —
equal ⇒ clear, no re-issue (fixes **N1**: one server correction now
produces exactly one canonical placement and exactly one outbound
`AutonomousPosition`, never two); advanced with the newest accepted event
still a ForcePosition ⇒ re-issue it, re-classified from the current record
via the newly recorded `_newestForce` observation (fixes **B1**: a
correction blocked by a woken park's retained completion is no longer lost);
advanced with the newest accepted event now an ordinary `Apply` ⇒ clear, no
re-issue (fixes **N2**: the round-1 shape reused the stale force route and
would have applied `Teleport|Slide` + an ack retail never sends to an
ordinary pose, skipping that branch's `ConstrainTo`). Complete Release
solution after round 2: **10,856 passed / 4 skipped / 0 failed** — App
4,058/3, Bake 15/0, Cli 4/0, Content 124/0, Core.Net 762/0, Core 4,247/1,
Headless 79/0, Runtime 1,024/0, UI.Abstractions 543/0.
Round 3 closed the conformance reviewer's one remaining blocker: a terminal
outcome that never committed now sends the packet's retail position event
carrying the body's unchanged pose, because `SmartBox::BlipPlayer`
@0x00453940 DISCARDS `CPhysicsObj::SetPositionSimple`'s
@0x005162B0 `enum SetPositionError` (other retail callers test it,
`== OK_SPE` @0x0055605D) and returns `void`, after which
`SmartBox::HandleReceivedPosition` @0x00453FD0 runs
`cmdinterp->SendPositionEvent()` @0x00454091 unconditionally and returns
@0x0045409D. Retail's rule is: attempt once, do not move on failure,
acknowledge regardless, never retry — so route 2 acks exactly once per begun
placement, from the commit path or from the settle, never both and never
zero.
Divergence register: **AD-62 filed** (round 2, rewritten round 3) — the
deferred-placement adaptation means a ForcePosition that cannot commit when
it arrives, and is then retired without commit, is not re-applied. As of
round 3 its position-event ack IS still sent whenever the placement was begun
AND that packet's descriptor reaches its own terminal settle. The ack is lost
in two narrower groups: where no placement was ever begun (an
externally-blocked `Contention`; a re-issue marker that never begins), and —
begun but displaced — where a newer force supersedes the packet before its
settle, since `SettlePending` nulls `_pending` without reading it (AD-62
shape (v)). Replaying that one would emit a stale-sequence report carrying
the newer packet's pose; the displacing packet always acks. Retail's
`SmartBox::BlipPlayer` is synchronous against a fully resident world and
reaches none of these states. The route-2 cutover ITSELF still adds no
deviation (it closes the
duplicate-authority + premature-ack bug); AP-131 is explicitly NOT retired
here (its named legacy `TryApplyPosition` caller is route 4's job, not
route 2's).
- **#286 — OPEN — headless never calls `RetryPending` on its placement
projection subscription.** `RuntimePlacementProjectionSubscription.RetryPending`
has exactly one caller in the tree, `GraphicalSessionEventRoute.cs:109,113`;
`HeadlessSessionEventRoute` constructs the subscription
(`HeadlessSessionEventRoute.cs:22`) but nothing pumps its retry. C4 route
2's R4 fix deliberately stopped the force-ack from consuming a `Place` the
sink declined, on the contract that the subscription's OWN retry re-offers
it — so on headless a declined `Place` would sit at the FIFO head and wedge
the ordered stream. Latent: not proven reachable today (the headless sink's
decline conditions may be unreachable given its bounded collision window).
Fix shape: give the headless host the same per-tick retry pump the
graphical route has, or prove the decline unreachable and record why.
Filed from the C4 route-2 round-2 review (N3).
- **#287 — OPEN — `RuntimeAcceptedPositionDriveController.Advance` has no
reentrancy latch.** Its template `RuntimeFirstEntryDriveController.DriveAll`
guards with `_driving` (`RuntimeFirstEntryDriveController.cs:61,130,132,146`);
the accepted-position drive does not, even though `Advance` can now re-enter
`SubmitAndResolve` through the `SettlePending` re-issue. No live re-entrant
path exists today (the funnel's recursion is bounded at one level and every
host pumps `Advance` from a single synchronous cadence point). Hygiene, not
a live defect. Filed from the C4 route-2 round-2 review (N4).
- **#288 — OPEN — two side effects were dropped when
`HeadlessSessionWorldProjection.BlipLocalPlayer` became
`CenterOnAcceptedForcePosition`.** (a) The deleted method also restored
`controller.LocalEntityId = record.LocalEntityId ?? 0u`; the replacement
(`HeadlessSessionWorldProjection.cs`, `CenterOnAcceptedForcePosition`) does
not. Inert today — nothing clears the id between publication and a
ForcePosition — but it is an unreplaced deletion, not a decision. (b)
`_movementTruthDiagnostics.OnServerEcho` no longer fires for a local
ForcePosition on the graphical host, because that route returns before the
generic tail (`LiveEntityNetworkUpdateController.cs`). Diagnostic-only.
Filed from the C4 route-2 round-2 review (N5).
- **#289 — OPEN — two doc comments still cite the deleted
`PlayerMovementController.BlipPosition`.** `src/AcDream.Core/Physics/Motion/ConstraintManager.cs:25`
and `src/AcDream.Core/Physics/PhysicsBody.cs:442` both name it inside `<c>`
tags, so they are build-safe (unlike a `<see cref="..."/>`, which R9 already
fixed) but false: C4 route 2 deleted the member. Left as-is they become the
citation a future session trusts. Filed from the C4 route-2 round-2 review
(R9 residue).
- **#290 — OPEN — route 1's classified `SendPositionImmediately` never fires
an ack.** The C4 plan required confirming whether the outbound position ack
fires while an initial-Create residence owns the record. It does not:
`RuntimeInitialCreateContinuationExecutor` consumes
`SendPositionImmediately` only as a trace fact (`:717`, `:2576`), never as
an outbound send. Not a regression (route 1 predates route 2 and behaves
exactly as before), but retail's FORCE_POSITION branch acks unconditionally
after `BlipPlayer`, so a ForcePosition admitted during the login residence
window is one retail ack we do not send. Decide at route 4/C5 whether the
residence tail should send it. Filed because the plan required filing it.
- **#291 — OPEN — the headless 3x3 collision window needs a divergence
register row.** C4 route 2's R2 fix promoted it to a NAMED member of the
Runtime-facing contract (`IRuntimeDirectWorldProjection.CenterOnAcceptedForcePosition`)
with an explicit ordering requirement — re-center BEFORE the placement
submits — that retail has no analogue for (retail has every landblock
resident). No existing row covers it: AD-6 is retired and AD-2 is the
graphical reveal barrier. Recommend one AD row naming the window, the
ordering requirement, and the symptom if it breaks (a `DeferredCell` park
the window can never publish). Filed from the C4 route-2 round-2 review.
- **#292 — OPEN — C4 route 2 acceptance item 2 is source-pinned, not
proven.** Recorded gap from round-2 finding B2 (the plan's own text is
corrected at
[`2026-08-02-placement-cutover.md`](plans/2026-08-02-placement-cutover.md)).
First half — "the generic tail no longer double-writes the local player" —
is pinned only by a source-text regex, which a differently spelled second
write would pass, and no test exercises the branch. Second half — "the
committed projection is what moves the render entity" — is uncovered at any
layer: no test drives a route-2 ForcePosition through
`RuntimePlacementPresentationSink` / `TryApplyRuntimePlacementPlace` and
asserts the `WorldEntity` moved. Given R4, that is exactly the seam whose
failure is silent (canonical body moves, render entity stays). Fix shape: an
App-layer end-to-end test asserting the render entity's position/cell came
from the committed placement receipt.
- **#293 — OPEN — the DeferredCell park still consumes `Withdraw` receipts the
sink may have declined.** `RuntimeAcceptedPositionDriveController.SubmitAndResolve`'s
`DeferredCell` branch drains the head of the placement FIFO while it is a
`Withdraw` for this entity and calls `AcknowledgeProjection` on it
(`RuntimeAcceptedPositionDriveController.cs:709-716`). That is the exact
receipt-stealing shape R4 removed for `Place`: the R4 fix's whole argument is
that `RuntimePlacementProjectionSubscription` deliberately leaves a receipt
the sink declined at the FIFO head for its own later retry, and that this
route has no follow-up binding to cover it. The `Withdraw` drain was left
unchanged in round 1 because it is copied verbatim from
`RuntimeFirstEntryDriveController.TryCompleteContinuationPlacement`, but the
same asymmetry applies — that controller's residence guarantees the decline,
this one's does not. Fix shape: decide whether a declined `Withdraw` is
reachable for this route and either drop the drain (letting the
subscription's retry own it, as `Place` now does) or record why the decline
cannot happen here. Filed from the C4 route-2 round-3 adversarial review.
- **#294 — OPEN — the deferred wake reconciles and acks BEFORE the funnel's
currency guard.** `RuntimeAcceptedPositionDriveController.Advance` consumes
the acknowledged placement and immediately calls `ReconcileAndAcknowledge`
(`:452`), and only then enters `SettlePending`, which is where the
entity-still-active / still-the-local-player / still-the-same-incarnation
checks live (`TryGetActive`, `ServerGuid`, `PhysicsBody`, `key !=
terminalToken.Entity`). So a wake that lands after the entity departed the
world, stopped being the local player, or released its incarnation still
runs the controller-local reconcile and can still send an outbound
`AutonomousPosition`. `ReconcileAndAcknowledge`'s own
`record.ServerGuid != _localPlayerServerGuid()` test uses the RETAINED
record, not a re-resolved active one, so it does not cover the departed
case. Ordering, not a workaround: the currency guard belongs before the
reconcile. Filed from the C4 route-2 round-3 adversarial review.
- **#295 — OPEN — the re-issue retry marker inflates
`AcceptedPositionDrivePendingCount`.** When `SettlePending`'s re-issue cannot
begin, it parks the terminal descriptor as a retry marker in `_pending`. That
marker is not an in-flight placement — its token is dead by construction —
but `PendingCount` (`:259`) and the ownership ledger registered at `:255-256`
both report it as one. Any convergence reader (`CaptureOwnership`,
`GameWindowLifetime.DisposeGameRuntime`'s non-convergence throw) therefore
sees a placement that does not exist, and a marker that outlives its
usefulness reads as a wedged operation rather than as "a re-issue is owed".
Fix shape: count in-flight placements and owed re-issues separately, or give
the marker its own field. Filed from the C4 route-2 round-3 adversarial
review.
- **#296 — OPEN — a retryable prepare is reported to hosts as `Contention`.**
`SubmitAndResolve` returns `RuntimeAcceptedPositionExecutionStatus.Contention`
for a retryable preparation status (`RetrySetupUnavailable` /
`RetryWorldFrameUnavailable`, `:661`), reusing the status whose documented
meaning is "begin failed; the entity already owns an operation". The two are
materially different: the retryable case DID begin, IS retained in `_pending`,
and WILL be re-driven by the next `Advance` pump, while true `Contention` may
have recorded nothing at all (register row AD-62 shape (iv)). Hosts cannot
distinguish them — `LiveEntityNetworkUpdateController` branches on the status
— and neither can a future reader of the enum doc. Fix shape: a distinct
status (or a documented union) so the retained-and-pumping case is not
conflated with the dropped case. Filed from the C4 route-2 round-3
adversarial review.
## C3c placement cutover — 2026-08-02
- **#276 — OPEN — SpawnPlacementSettler discards the settle's resolved
@ -4913,6 +5209,16 @@ gate is `PhysicsBody.IsFullyConstrained` (former TS-35) via
`jump_is_allowed`. Decomp: `docs/research/2026-07-03-r5-managers/`,
`docs/research/2026-07-30-constraint-leash-constants.md`.
**2026-08-03 correction (C4 route 2, #285):** the `BlipPosition` half of this
arming site was an unbacked deviation for the ForcePosition branch
specifically — `SmartBox::BlipPlayer` (0x00453940), the function
`HandleReceivedPosition`'s FORCE_POSITION branch calls, is not on the
"Player, normal" branch this slice modeled; retail's FORCE_POSITION early
return (0x0045409D) precedes every `ConstrainTo` call. `BlipPosition` is
deleted; the leash is no longer (re)armed on a ForcePosition. The arming
site for every OTHER inbound position (remotes, the local player's ordinary
teleport/`SetPosition`) is unaffected.
**Acceptance:** the two constants are recovered (byte-decoded from the
binary), acdream arms the leash on inbound server positions,
`IsFullyConstrained` fires while rubber-banding, and a jump attempt inside

View file

@ -62,7 +62,7 @@ accepted-divergence entries (#96, #49, #50).
---
## 2. Adaptation (AD) — 47 active rows (AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-42 refreshed same round — its cited App-side login resolve split was deleted by the C3c flip, the split survives only on the unflipped remote-teleport/headless portal-resync paths; AD-59/AD-60 filed 2026-08-02, continuation-executor slice)
## 2. Adaptation (AD) — 48 active rows (AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-42 refreshed same round — its cited App-side login resolve split was deleted by the C3c flip, the split survives only on the unflipped remote-teleport/headless portal-resync paths; AD-59/AD-60 filed 2026-08-02, continuation-executor slice)
Recent retirements: AD-3/AD-4 retired 2026-07-31 by exact active/per-candidate
visible-cell availability, full-catalog containment-root validation, and the
@ -155,6 +155,7 @@ readiness/requeue adaptation. See
| AD-59 | **Filed 2026-08-02 (physics campaign, continuation-executor slice).** The `SameIncarnationCreate` envelope buffers one publish per committed stage and flushes them ALL, in stage order, only after the LAST stage commits (constant-true per-field predicate, `IsCurrent`-checked at flush - the per-field closure variant was invalidated by WeenieDescription's six-field `AdvanceCreateAuthority`). A subscriber sees N back-to-back events with no interleaved observation point, each carrying the FINAL merged post-envelope record state, not per-stage state. | `src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs` (`ApplyEnvelope` buffered-publish tail; `Publish`/`PublishNow`) | Retail's own tail is one synchronous critical section, and retail emits ONE notice per Create (`ECM_Physics::SendNotice_CreateObject`, fired whenever a weenie exists, independent of the physics-registration outcome) - never N per-internal-step notices. The buffered flush is closer to retail's one-signal model than per-step publication would be, though not a literal 1:1 match. | A subscriber diffing consecutive `Updated` events from the SAME envelope to isolate one stage's delta gets every stage's cumulative state on each event - silently wrong incremental-diff logic, not a crash. | `SmartBox::HandleCreateObject` 0x00454C80 same-incarnation tail (one synchronous critical section); `ACCObjectMaint::CreateObject` 0x00558870 step 11 (`ECM_Physics::SendNotice_CreateObject`) |
| AD-60 | **Filed 2026-08-02 (physics campaign, continuation-executor slice).** Executor Position-continuation merges never directly commit residency: `ApplyPositionAction` refreshes `canonical.Snapshot.Position` with the retained wire pose but withholds the derived `FullCellId` (`RefreshSnapshot(..., refreshPosition: false)`); only a Runtime `SetPosition` commit (the continuation's own classified placement) or a later simulation full-cell commit may change residency. The LEGACY immediate-apply path's `RefreshSnapshot(canonical, snapshot, refreshPosition: acceptedPosition)` (`RuntimeEntityObjectLifetime.cs:1338`) still derives `FullCellId` from bare wire acceptance - that coarser rule is part of the AP-1 divergence this campaign is removing, not something this row blesses. | `src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs` (`ApplyPositionAction`, the CANONICAL CELL SEMANTICS comment) | Matches retail exactly: `HandleReceivedPosition` never writes a resident cell - `enter_world`/`MoveOrTeleport`'s placement commit and `SetPosition` do; also matches the classifier's documented cellless rule. | If a future change passes `refreshPosition: true` here, a wire Position would make a cellless canonical body resident without any placement/collision commit - the classic AP-1-shaped bug this campaign exists to close. | `SmartBox::HandleReceivedPosition` 0x00453FD0; `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition` comment |
| AD-61 | **Filed 2026-08-02 (C3c review round 1).** The #270 settle-timing compression now covers the LOCAL player: `RuntimeLocalPlayerPhysicsPublicationState.SettleFirstEntryGroundContact` runs the shared `SpawnPlacementSettler` exactly once after the dormant activation's final commit (suffix-current authority only), compressing retail's first post-`enter_world` gravity frame — which grants CONTACT/ON_WALKABLE from a real touch — into the placement transaction. The legacy App-era force-seed (`Contact\|OnWalkable\|Active` in `PlayerMovementController.SetPositionCore`) still RUNS during publication-candidate preparation and is then OVERWRITTEN by the faithful activation commit + settle (it was never deleted). Caveat (review minor M2): the settler commits `settle.Position` but discards `settle.CellId` — a settle whose few-cm sweep crosses a cell boundary keeps the placement cell until the next resolve corrects it (inherited #270 semantics; ISSUES entry filed) | `src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs` (`SettleFirstEntryGroundContact`); `src/AcDream.Core/Physics/SpawnPlacementSettler.cs` (`TrySettle`); overwritten seed `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs` (`SetPositionCore`) | Timing compression only: contact comes exclusively from the sweep's real touch (no caller-bool seeding, no forced transients), an airborne spawn stays genuinely airborne, and the overwritten force-seed leaves no observable residue past the activation commit — the committed state is exactly what retail's first gravity frame produces | A settle crossing a cell boundary reports the stale placement cell for the frames before the next resolve; a future reader trusting `SetPositionCore`'s "treat as grounded" seed comment could reintroduce the Contact-without-plane state the landing family calls unrepresentable | `CPhysicsObj::enter_world` 0x00516170; `SmartBox::HandleCreateObject` 0x00454C80 |
| AD-62 | **Filed 2026-08-03 (C4 route 2, round 2); rewritten round 3.** General rule: an accepted local-player ForcePosition that this route does not carry through to a committed canonical placement is never re-applied. That half matches retail — `SmartBox::BlipPlayer` attempts the placement exactly once and never retries. What diverges is that acdream has non-commit outcomes retail cannot reach at all, because retail's world is fully resident and its placement synchronous. Round 3 narrowed the loss to the re-apply alone wherever the packet's placement was actually BEGUN: the retail position event now fires at that packet's terminal outcome whether or not the placement committed (`SettlePending`'s `positionEventOwed` path), matching `BlipPlayer` discarding `SetPositionSimple`'s `enum SetPositionError` and `HandleReceivedPosition` acking unconditionally @0x00454091. Shapes losing ONLY the re-apply: (i) the destination landblock's collision generation is unpublished so the placement parks (`DeferredCell`) and is then retired by a non-position cause (collision-generation retirement, the lost-cell deadline, `ParkCollisionResidents`) with the accepted authority unmoved — the funnel's EQUAL branch; (ii) the same park superseded by a newer ordinary `Apply` Position which now owns the pose — the ADVANCED+ordinary branch; (iii) any OTHER `PositionAuthorityVersion` advance moving the record out from under the funnel's re-issue test — `TryApplyPickup` (`RuntimeEntityObjectLifetime.cs:1116`), `CommitPositionChannelUpdate` (`:2041`), `AdvanceCreateAuthority` (`:2466`) — effectively unreachable for a live local player, but they fail silently in the same direction and the funnel cannot tell them from (ii). Shapes still losing BOTH the re-apply and the ack because no placement was ever begun for that packet: (iv) a `Contention` whose blocking operation is EXTERNAL to this drive (a concurrent portal/teleport placement owns the entity) — nothing is recorded in `_pending`, so nothing pumps it and the packet is dropped outright; (vi) a re-issue retry marker whose re-issue never manages to begin before the funnel clears it. Losing BOTH for a DIFFERENT reason — the placement WAS begun, but the descriptor was displaced before reaching its own terminal settle: (v) a packet superseded by a newer force whose own placement begins cleanly — `SettlePending` opens by nulling `_pending` without reading it, so the older descriptor's owed ack is discarded. Replaying it would be worse than losing it (a stale-sequence report carrying the newer packet's committed pose), and the displacing packet always acks, so ACE always receives a report for the newest force. The `DeferredCell` park is NOT a precondition of this row: shapes (iv)-(vi) never park. In every shape the body stays where the last successful placement left it and the next accepted Position (ACE broadcasts at 5-10 Hz) carries the corrected pose forward. | `src/AcDream.Runtime/Session/RuntimeAcceptedPositionDriveController.cs` (`SettlePending` — the single terminal-outcome funnel: its `positionEventOwed` ack and its two non-reissuing branches; and `TryExecuteAcceptedLocalPosition`'s `Contention` return) | Retail has no park and no external placement authority: `SmartBox::BlipPlayer` runs synchronously against a fully resident world, so "arrived but not yet placeable" and "another placement owns this entity" are both unrepresentable there. Those are our async collision-publication and single-placement-authority adaptations. Re-issuing a retired force instead would be worse than not: shape (ii) would stamp the force route's `Teleport\|Slide` flags and an unconditional ack onto an ordinary echo's pose while skipping the `ConstrainTo` the ordinary branch runs (`RuntimeAuthoritativePositionRouteClassifier.cs:368-388`), and shape (i) can re-issue into the same persistent cancellation cause indefinitely. The drive still owns at most one in-flight placement and still re-issues whenever the newest accepted event IS a still-unserved ForcePosition. | A server correction whose destination collision is slow to publish, or which lands while another placement authority owns the entity, can be silently skipped: the player stays at the pre-correction pose for one broadcast interval (~100-200 ms). Sustained (a slow-publishing destination correcting repeatedly) this reads as rubber-banding that does not take. In shapes (iv)-(vi) ACE additionally receives one fewer `AutonomousPosition` than retail would have sent, so the server cannot tell its force was not applied. | `SmartBox::HandleReceivedPosition` @0x00453FD0 FORCE_POSITION branch (`SendPositionEvent` @0x00454091, early return @0x0045409D); `SmartBox::BlipPlayer` @0x00453940 (discards the error, returns void); `CPhysicsObj::SetPositionSimple` @0x005162B0 (returns `enum SetPositionError`; other callers test `== OK_SPE` @0x0055605D/@0x00556021); `CommandInterpreter::SendPositionEvent` @0x006B4770 |
---

View file

@ -252,9 +252,70 @@ same commit) → docs/handoff commit. No workarounds; no fused slices.
4 (remote Create/Position; delete `RemoteTeleportController`/`Placement`
and the inline MoveOrTeleport duplicate), 5 (projectile authoritative),
6 (drops + split-recovery marking), 7 (residual pickup/parent/delete
polish). — OPEN at the 2026-08-03 handoff.** May land as more than one
commit if a route proves large;
each sub-landing keeps the full review discipline.
polish). — route 2 implementation COMPLETE, pending review, at the
2026-08-03 handoff; routes 3/4/5/6/7 remain OPEN.**
**Route 2 (ForcePosition) — implemented 2026-08-03, contract:**
[`2026-08-03-c4-route-2-contract.md`](../research/2026-08-03-c4-route-2-contract.md),
**plan:** [`2026-08-03-c4-route-2-implementation-plan.md`](../research/2026-08-03-c4-route-2-implementation-plan.md).
`RuntimeAcceptedPositionDriveController`
(`src/AcDream.Runtime/Session/RuntimeAcceptedPositionDriveController.cs`)
is the single accepted-Position execution seam for a ForcePosition on the
already-live local player; `LocalForcePositionTransaction` and
`HeadlessSessionWorldProjection.BlipLocalPlayer` are deleted, and the
generic App render-tail is skipped for the local player's ForcePosition.
Named behaviour changes (both retail-exact, ISSUES #285): the outbound
ack now fires strictly after the canonical commit, and the constraint
leash is no longer re-armed on this route (retail's FORCE_POSITION branch
never reaches `ConstrainTo`).
**Fix round (2026-08-03):** both independent dual reviews (retail-
conformance + architecture/adversarial) FAILed the first pass — see
[`2026-08-03-c4-route-2-review-findings.md`](../research/2026-08-03-c4-route-2-review-findings.md)
for the full R1-R9 list. The critical finding (R1) was that the
DeferredCell park could not survive a single ACE broadcast interval in
production (`RuntimeEntityObjectLifetime.TryApplyPosition`'s unconditional
`Forget` on every accepted Position cancelled it before its collision
generation could commit), silently dropping the correction forever;
`RuntimeAcceptedPositionDriveController.Advance` now detects the dead
watch and re-issues from the entity's current canonical snapshot. R2/R3
restored headless's collision re-centering and login-window fallback; R4
stopped the force-ack from stealing a receipt the presentation sink had
legitimately declined; R5/R6/R9 corrected false doc claims, closed a
`_pending`-leak/overwrite gap, and fixed streaming-observer/pose-dirty
side effects firing on a declined placement. R7 corrected a fixture bug
(a dummy Setup sphere with its centre at the origin) that had been
written up as a retail fidelity gain; R8 added App-layer double-write
source pins and corrected an overclaimed single-ack test. Full detail:
[`2026-08-03-c4-route-2-review-findings.md`](../research/2026-08-03-c4-route-2-review-findings.md).
Complete Release solution after the fix round: **10,853 passed / 4
skipped / 0 failed** (baseline 10,844/4/0; first pass 10,848/4/0).
**Acceptance item 2 is NOT met — recorded gap, B2 (2026-08-03 round 2).**
An earlier revision of this paragraph claimed R8 "added the App-layer
double-write source pins the plan's own acceptance item required". That was
a claim of coverage this changeset does not have, and it is corrected here
rather than left as the citation a future session trusts (same rule that
produced R7). The truth, per the adversarial review:
- *First half — "the generic tail no longer double-writes the local
player":* **source-pinned, not proven.** The pin is a regex/`Assert.Single`
over `LiveEntityNetworkUpdateController`'s source text, so it would still
pass if a second write were spelled differently, and **no test exercises
the branch** at runtime.
- *Second half — "the committed projection is what moves the render
entity":* **uncovered at any layer.** No test drives a route-2
ForcePosition through `RuntimePlacementPresentationSink` /
`TryApplyRuntimePlacementPlace` and asserts the `WorldEntity` actually
moved. Given R4 (the force-ack no longer consumes a declined `Place`),
this is precisely the seam whose failure mode is silent: the canonical
body moves and the render entity stays put.
Closing this gap needs an App-layer test that runs the accepted
ForcePosition end to end and asserts the render entity's position/cell came
from the committed placement receipt — carry it into C5's parity tests or
file it before this sub-landing closes.
**Not yet done:** both reviews must be RE-RUN on this fixed diff, and the
connected (user-gated) acceptance gate this campaign's standing
discipline requires, before this sub-landing is considered closed — those,
and the commit itself, are next. May land as more than one commit if a
route proves large; each sub-landing keeps the full review discipline.
- **C5 — legacy deletion + closeout gates — OPEN.** Delete every superseded legacy
path; parity tests; exact lifecycle/reconnect + canonical nine-stop
connected routes; two-client observation; **user visual matrix** (the

View file

@ -0,0 +1,291 @@
# 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 (<force ts is not older>)
{
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 -- <path>`.
- 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.

View file

@ -0,0 +1,479 @@
# C4 route 2 — dual review findings and required fixes (2026-08-03)
Both mandated reviews returned **FAIL** on the first implementation pass.
Nothing is committed. This is the consolidated fix list; it supersedes the
implementer's own closing report where they disagree.
Reviews: retail-conformance (Opus) and architecture/adversarial (Opus), run
independently against the same uncommitted diff.
## Verified correct — do not churn these
Both reviews independently confirmed, with addresses:
- `AuthoritativeTeleportFlags` = `Teleport|Slide|SendPositionEvent` = `0x1012`,
byte-exact against `CPhysicsObj::SetPositionSimple` @0x005162B0.
- Leash re-arm removal is retail-correct: the FORCE_POSITION branch returns at
0x0045409D, strictly before all three `ConstrainTo` sites (0x00454272,
0x0045418A, 0x004541EC). The teleport / `CommitPreparedPosition` /
`ArmConstraintLeashAtCommittedPlacement` callers correctly still constrain.
- Heading preservation happens exactly once, upstream in
`InboundPhysicsStateController.ApplyAcceptedPosition:788-798`. The seam does
not re-apply or drop it.
- Ack ordering is correct and fires exactly once on both the synchronous and
the deferred path; the `CanSendPositionEvent` gate matches retail's
`CommandInterpreter::SendPositionEvent` @0x006B4770, and correctly does NOT
apply `ShouldSendPositionEvent`'s rate limit (retail's force branch calls
SendPositionEvent directly).
- Route-request field derivation matches the continuation executor field for
field.
- Contract items 2, 4 (the Runtime command itself), 6, and 7 pass. AP-131
correctly not retired.
- The re-modelled `PlayerMovementControllerTests` are relocations, not
weakenings.
## Required fixes, in priority order
### R1 — HIGH — the DeferredCell park cannot survive in production
`RuntimeEntityObjectLifetime.cs:1636` calls `Physics.SetPosition.Forget(canonical)`
on EVERY accepted Position, which unconditionally cancels the entity's in-flight
operation. ACE broadcasts at 5-10 Hz, so any park lasting longer than ~100-200 ms
is guaranteed to be cancelled before its collision generation commits — exactly
the far-destination case the deferred path exists to serve.
Chain: park -> cancelled -> `RetryDeferred` never runs -> no `Place` receipt ->
`ReconcileAndAcknowledge` never runs -> **the body is never moved and no ack is
ever sent.** The old `LocalForcePositionTransaction` / `BlipLocalPlayer` pair
applied the correction synchronously and unconditionally. Retail's `BlipPlayer`
@0x00453940 has no "give up quietly" state at all.
**Park-and-hope is not a valid mechanism here. Required direction:**
1. Do not open a park that can never wake. Before submitting, establish that the
destination's collision is publishable (R2), and mirror the existing
C3c-R1-F7 guard shape (`IHeadlessCollisionNeighborhood.IsWithinServiceWindow`,
`HeadlessSessionWorldProjection.cs:20-27`) rather than inventing a new one.
2. Where a park still legitimately occurs, the seam must detect that its
operation was cancelled — `RuntimeSetPositionState.IsPlacementCompletionTracked`
(`:1245`) is the existing read-only query — and **re-issue the placement from
the current canonical snapshot** on the next accepted Position or `Advance()`.
The snapshot already carries the latest accepted pose, so re-issuing is
correct, not a replay of stale state.
3. A force correction must never be silently dropped. That is the retail
invariant this route exists to preserve.
Do NOT resolve this with a timeout, a settle window, a retry counter, or by
exempting ForcePosition from `Forget`. If you conclude the correct answer is a
deliberate, cited divergence, STOP and report rather than shipping one.
### R2 — HIGH — headless lost its destination-collision publication
`HeadlessSessionWorldProjection.BlipLocalPlayer` (deleted) called
`_collision.CenterOn(position.LandblockId)`. The headless collision neighborhood
is a hard 3x3 window (`BuildPublicationPlan`, `:462-488`) moved ONLY by
`CenterOn`. The surviving callers are spawn (`:562`), the controller-null login
branch (`:603`), teleport prep (`:640`) and portal arrival (`:683`) — a
ForcePosition on a live local player now reaches none of them. `PumpFirstEntry`
(`:624`) polls the stale `_requestedLocalPlayerCell`, which nothing updates on
this path either.
Restore a real mechanism (re-center plus the `_requestedLocalPlayerCell`
update), or gate on `IsWithinServiceWindow` and handle out-of-window explicitly.
The deleted `CenterCount` assertion in `HeadlessSessionHostTests.cs:430` is the
invariant; restore it rather than the changed number.
The in-test justification ("retail's BlipPlayer has no streaming-window
concept") is true of retail and irrelevant: the window is OUR adaptation, and
retail has no equivalent because retail has every landblock resident.
### R3 — HIGH — login-window ForcePosition now does nothing at all
`RuntimeLiveEntitySessionController.cs:229-254`. Previously every accepted local
Position ran `_worldProjection.ProjectPosition`, whose controller-null branch
(`HeadlessSessionWorldProjection.cs:593-607`) set `_requestedLocalPlayerCell`,
called `CenterOn`, and pumped `_firstEntry.DriveAll()`. Now a ForcePosition
takes the new branch, the drive returns `NotApplicable` (residence active), and
nothing happens.
Restore the pump for the controller-absent case, and delete the comment at
`:239-240` claiming "there is no legacy fallback to run instead" — there was
one; it is the `else` branch this change routed around.
### R4 — MEDIUM-HIGH — the force-ack steals a receipt the sink declined
`RuntimeAcceptedPositionDriveController.cs:366-375` unconditionally calls
`AcknowledgeProjection(outcome.Projection)`. `RuntimePlacementProjectionSubscription`
deliberately leaves a declined `Place` at the FIFO head for a later retry
(`:118-121`); this consumes and destroys it.
The sink declines for real production reasons — `!_spatial.IsLoaded(landblock)`
(`LiveEntityRuntime.cs:1210-1219`) and stale transit authority
(`RuntimePlacementPresentationSink.cs:90-96`). In those cases `entity.SetPosition`
/ `Rotation` / `ParentCellId` / `RebucketLiveEntity` / `IsSpatiallyProjected`
are never written, and because the generic tail is now skipped there is no
second writer to cover it — the render entity silently stays put while the
canonical body moved.
The `RuntimeFirstEntryDriveController` mirror is safe ONLY because a residence
makes the sink decline by design and a follow-up `ExecutorCompleted` receipt
re-binds presentation. Route 2 has no such follow-up. Drop the force-ack and let
the subscription's retry contract stand, or provide a real follow-up binding.
### R5 — MEDIUM-HIGH — `Contention` and `Rejected` silently drop the correction
`RuntimeAcceptedPositionDriveController.cs:265-271` returns `Contention` when
`TryBeginExclusiveAuthoredPlacement` fails; neither status creates a pending
entry, and both hosts then return without blipping or acking
(`LiveEntityNetworkUpdateController.cs:1140`). The `Contention` doc comment
promises "a later accepted Position, or this controller's own Advance pump,
retries" — the pump provably cannot retry something never recorded, and a later
Position carries a different pose. Retail always applies.
The most likely trigger is R1's parked operation, which makes every subsequent
ForcePosition `Contention`. Fixing R1 largely fixes this; the status handling
must still not silently drop.
Also fix the `Rejected` enum doc: it claims "No SetPosition ran; no ack was
sent", which is false at the two `SubmitAndResolve` sites (`:361`, `:415`) where
a SetPosition ran and was cancelled.
### R6 — MEDIUM — `_pending` leaks and can be silently overwritten
`RuntimeAcceptedPositionDriveController.cs:289-324`. `Advance()` exits only on
record-key release or acknowledged completion. A mid-session cancellation
(supersession, lost-cell deadline, `ParkCollisionResidents`, generation cancel —
all route through `ForgetPlacementCompletionCore`) leaves `_pending` set
forever, so `AcceptedPositionDrivePendingCount` keeps `IsConverged` false for
the rest of the session. `GameWindowLifetime.DisposeGameRuntime:490-498` throws
on non-convergence.
Plan §4c required dropping the pending ack on cancellation, supersession,
teardown, generation change and reset. Only teardown and reset shipped. Use
`IsPlacementCompletionTracked`. Also: the `_pending = null` cleanups are all
guarded by `if (!firstAttempt)`, and `SubmitAndResolve(firstAttempt: true)`
assigns `_pending` without inspecting an existing one — make it refuse to
overwrite a live pending.
### R7 — MEDIUM — the 0.48 fixture, the changed assertion, and the false doc
**This is a test-fixture artifact, NOT a production regression.** The headless
fixture's `LoadedSetupCollisionSource` returns one sphere
`(Vector3.Zero, 0.48f)` — centre AT the origin, bottom 0.48 m below the feet.
The real human Setup `0x02000001` is `(0,0,0.475) r=0.48` plus
`(0,0,1.350) r=0.48` (`Ts46SphereListConformanceTests.cs:35-39`), so the foot
sphere's bottom is origin - 0.005 and a settled origin lands on the floor within
5 mm. The control is in this same changeset: the new Runtime fixture uses the
dummy sphere (offset == radius) and asserts the origin lands exactly on the
floor (`RuntimeAcceptedPositionDriveControllerTests.cs:178`).
Required:
1. Give the headless fixture the retail offset `(0f, 0f, 0.475f) r=0.48` and
**restore the `Z == 50f` assertion**. Changing an assertion to match new
output is the plan's own forbidden move.
2. Delete the false comment at `HeadlessSessionHostTests.cs:419-427` — it
describes the sphere CENTRE and then asserts it about `controller.Position`,
which is the ORIGIN (`PlayerMovementController.cs:304` -> `PhysicsBody.cs:153`,
retail `CPhysicsObj::m_position.frame.origin`).
3. Correct the `docs/ISSUES.md` #285 "retail fidelity gain" paragraph. Retail's
`BlipPlayer` has never lifted the origin by a sphere radius. Left as-is this
becomes the citation a future session trusts.
### R8 — MEDIUM — acceptance gaps
- No App-layer test exists proving the generic tail no longer double-writes the
local player and that the committed projection is what moves the render
entity. The plan's acceptance item 2 is unmet; three App tests were deleted
and replaced with a comment. Given R4, this is precisely the seam that is
broken.
- `RuntimeAcceptedPositionDriveControllerTests.cs:310-311` asserts
`acksAfterFirstResolve <= 1`, so the test **passes with zero acks** — the
DeferredCell park -> wake -> commit -> single-ack sequence is unverified,
while `docs/ISSUES.md` claims it is covered. Fix the fixture so the contact
gate is satisfied and assert exactly one, or state plainly that it is
unverified. Do not leave the overclaim in the record.
### R9 — LOW — hygiene
- `RuntimeAcceptedPositionDriveController` is `public sealed` with an internal
ctor and all-internal members; its template `RuntimeFirstEntryDriveController`
is `internal sealed`. Make it internal unless the public surface is genuinely
required (if it is, say why).
- `PlayerMovementController.cs:632` has a stale `<see cref="BlipPosition"/>` to
a deleted member. Harmless only while `GenerateDocumentationFile` is off;
`TreatWarningsAsErrors` is on, so it breaks the build the day docs are enabled.
- `HeadlessSessionHost._currentSession` is never cleared on teardown.
- Headless without a content lease leaves the drive controller null, so the
ForcePosition and its ack are dropped entirely
(`HeadlessSessionHost.cs:568-581`); previously the ack fired unconditionally.
- `LiveEntityNetworkUpdateController.cs:1140-1155` fires
`MarkLiveOwnerPoseDirty` and `ObserveAcceptedLocalPosition` for ANY non-
`NotApplicable` status including `Rejected` and `Contention` — moving the
streaming observer to a landblock we explicitly refused to place into.
## Gate
Unchanged: complete Release solution suite, not a focused subset. Pre-change
baseline is 10,844 / 4 skipped / 0 failed; the first pass reached 10,848 with
the defects above, so a green suite is necessary and demonstrably not
sufficient. Both reviews must be re-run on the fixed diff before commit.
---
# ROUND 2 — residuals after the R1-R9 fix round (2026-08-03)
Both delta reviews returned FAIL again. Suite is green at 10,853 / 4 / 0, which
again proves nothing. R2, R3, R4, R5, R6, R7 and R9 are confirmed genuinely
fixed and must not be churned. Three blocking residuals remain, and **two of
them are defects in the R1 reissue mechanism itself.**
**Root of the problem: `_pending` has no single owner and no single lifecycle
rule.** Round 1 bolted reissue onto ad-hoc per-branch bookkeeping. B1 wants MORE
reissuing, N1 wants LESS, and N2 wants reissue to be a DIFFERENT route — they
look contradictory only because there is no unifying rule. There is one.
## The unified mechanism (implement exactly this — it replaces the ad-hoc rules)
`RuntimeEntityPlacementToken` already carries `PositionAuthorityVersion`
(`RuntimeSetPositionState.cs:50`). Make that the single decision input.
**One rule:** the drive owns at most one in-flight placement for the local
player. After any terminal outcome, and on every `Advance()`, compare the
committed/parked token's `PositionAuthorityVersion` against the live record's
current `PositionAuthorityVersion`:
- **Equal** — the canonical accepted authority has not moved since this
operation began. Nothing is outstanding. Clear `_pending`. Do not reissue.
- **Advanced** — a newer accepted Position arrived while we were in flight, and
it may have been the thing that killed our operation. Consult the newest
accepted event's disposition (do NOT reuse `stale.Route`):
- still **ForcePosition** — reissue, re-classifying from the current record.
- now an ordinary **Apply** — clear `_pending` and do NOT reissue. The
correction was superseded by newer server truth; the ordinary route owns
that pose. This is not a silent drop: retail applies each event as it
arrives, and a force correction overtaken by a newer position is moot.
Route every terminal branch through one `_pending` funnel. No branch may assign
or clear it directly.
### B1 — BLOCKING — a force correction is still silently dropped (conformance)
When a parked operation wakes and its `Place` is ACCEPTED by the sink, the
operation leaves `_operations` but the completion is retained. `CancelCoreDeferred`
then returns early at `RuntimeSetPositionState.cs:5141` without reaching
`ForgetPlacementCompletionCore`, so the retained completion survives. The next
ForcePosition hits `HasRetainedCompletion` (`:1319`) -> invalid token ->
`Contention` (`RuntimeAcceptedPositionDriveController.cs:289-295`); both hosts
return without placing or acking, and the next `Advance()` consumes the OLD
completion and acks the OLD pose. That packet's correction is lost.
One-frame window on the graphical host only (`_session.Tick()` inbound dispatch
precedes `RetryPending()` in `RetailLiveFrameCoordinator`); headless is immune
because its pump is adjacent to the readiness check. The DECLINED-`Place`
variant is unaffected and needs no change.
The unified rule fixes this: the new packet advanced `PositionAuthorityVersion`
past the committed token, and the newest event is a ForcePosition, so it
reissues.
### N1 — BLOCKING — stale `_pending` causes a second placement AND a second ack
`SubmitAndResolve(firstAttempt: true)` (`:297`) never inspects `_pending`, and
only the `!firstAttempt` branches clear it (`:508-511`, `:482-484`, `:549-551`).
So: park -> `Forget` wipes the watch -> the same packet's ForcePosition Begins
cleanly and Commits -> ack fires -> `_pending` still holds the dead P1 -> next
`Advance()` finds the watch dead -> `ReissueFromCanonical` -> **a second
canonical placement and a second outbound `AutonomousPosition` for one server
correction.**
That is the double-apply/double-ack class this entire slice exists to delete
(`670f307c`), reintroduced. `AssignPending` does not catch it because it is only
reached on the DeferredCell/retryable branches. The unified funnel fixes it:
equal versions -> clear, no reissue.
### N2 — BLOCKING — reissue applies force semantics to an ordinary pose
`ReissueFromCanonical` (`:418-447`) reuses `stale.Route` verbatim —
`SetPositionSimple` + `Teleport|Slide|SendPositionEvent` +
`SendPositionImmediately: true`. But the commonest way a park dies is an
ordinary `Apply` Position, whose retail route
(`RuntimeAuthoritativePositionRouteClassifier.cs:368-388`) is
`Interpolate`/`NoPositionOperation`, `PhysicsSetPositionFlags.None`,
`ConstrainPhase.BeforePositionOperation`, `SendPositionImmediately: false`.
So the reissue converts an ordinary server echo into a hard `Teleport|Slide`
canonical placement, sends an ack retail would never send on that branch, and
skips the `ConstrainTo` the ordinary branch runs. My round-1 direction sanctioned
re-issuing THE CORRECTION; it did not sanction re-classifying a different
disposition's pose as a force. The unified rule fixes this by consulting the
newest accepted disposition.
If any residual divergence remains after this, it needs a
`docs/architecture/retail-divergence-register.md` row in the same commit.
### B2 — BLOCKING (record accuracy) — the plan claims coverage it does not have
`docs/plans/2026-08-02-placement-cutover.md:287` reads "R8 added the App-layer
double-write source pins the plan's own acceptance item required." That is a
claim of coverage. The truth, per the adversarial review:
- acceptance item 2's first half is **source-pinned, not proven** — the
`Assert.Single` regex would still pass if a second write were spelled
differently, and no test exercises the branch;
- acceptance item 2's second half — **"the committed projection is what moves
the render entity"** — is **uncovered at any layer**. No test drives a route-2
ForcePosition through `RuntimePlacementPresentationSink` /
`TryApplyRuntimePlacementPlace` and asserts the `WorldEntity` moved.
Correct the text to record the gap explicitly. A documented gap is acceptable;
a false claim of coverage is not. Same rule that produced R7.
## Non-blocking — record, do not fix in this round
- **N3** — headless never calls `RetryPending` after construction (grep finds no
caller outside `src/AcDream.App/`). R4's fix depends on the subscription's
retry, so a declined headless `Place` would wedge the ordered stream. Latent,
not proven reachable. File it.
- **N4**`Advance()` lacks the `_driving` reentrancy latch its template
`RuntimeFirstEntryDriveController.DriveAll:128-148` has. No live re-entrant
path today. Hygiene.
- **N5**`CenterOnAcceptedForcePosition` does not restore
`controller.LocalEntityId = record.LocalEntityId ?? 0u` (inert today, but an
unreplaced deletion); `_movementTruthDiagnostics.OnServerEcho` no longer fires
for a local ForcePosition (diagnostic only).
- **R9 residue**`ConstraintManager.cs:25` and `PhysicsBody.cs:442` still cite
the deleted `BlipPosition` in `<c>` tags (build-safe, but false docs).
- **Route-1 ack** — the plan required confirming whether the ack fires while an
initial-Create residence owns the record. It does not; `SendPositionImmediately`
is consumed only as a trace fact in the continuation executor (`:717`, `:2576`).
Not a regression, but the plan said file it. File it.
- **AD register row** — the headless 3x3 collision window is now a named member
of the Runtime-facing `IRuntimeDirectWorldProjection` contract with an ordering
requirement retail has no analogue for, and no existing row covers it (AD-6 is
retired; AD-2 is the graphical reveal barrier). Recommend a row.
- **Stale comment** — the `HeadlessSessionHostTests` comment references "the
previous 50.48f assertion", which does not exist at HEAD.
---
# ROUND 3 — final round (2026-08-03)
The round-3 **adversarial** review returned **PASS**. The round-3
**conformance** review returned **FAIL on one item**. This round closes that
item and files the adversarial review's non-blocking findings. The round-2
unified `_pending` funnel was confirmed sound by both reviewers and was NOT
restructured.
## The blocker — a terminal-without-commit sent no ack (CLOSED)
`SettlePending`'s Equal branch was reached both by a successful commit and by a
terminal outcome that never committed (non-retryable prepare failure, the
`default:` Rejected/Cancelled branch, and both `Advance` death branches). In the
non-commit case the body never moved AND no outbound `AutonomousPosition` left.
The conformance reviewer proposed a `committed` flag plus a
one-re-issue-per-`Advance` guard. That is more than retail requires, and the
retail evidence was re-verified from
`docs/research/named-retail/acclient_2013_pseudo_c.txt` before coding:
- `SmartBox::BlipPlayer` @0x00453940 (line 92528) calls
`CPhysicsObj::SetPositionSimple(this->player, edi_1, 1)` @0x00453968 and
**discards its return value**. `BlipPlayer` itself returns `void`.
- `CPhysicsObj::SetPositionSimple` @0x005162B0 (line 284276) is declared
`enum SetPositionError __thiscall`. Other retail call sites DO test it —
`if (CPhysicsObj::SetPositionSimple(...) == OK_SPE)` at @0x0055605D and
@0x00556021 — which proves the discard in `BlipPlayer` is deliberate, not a
decompiler artifact.
- `SmartBox::HandleReceivedPosition` @0x00453FD0's FORCE_POSITION branch calls
`SmartBox::BlipPlayer` @0x00454074, stamps `update_times[0]` @0x00454079, then
runs `cmdinterp->SendPositionEvent()` @0x00454091 **unconditionally** and
returns @0x0045409D.
Retail's semantics are therefore: **attempt the placement once; if it fails the
body simply does not move; acknowledge regardless; never retry.** No commit flag
and no recursion guard are needed to express that.
**Implemented.** The ack is now sent exactly once per BEGUN placement, at its
terminal outcome — from `ReconcileAndAcknowledge` on the commit path, or from
`SettlePending` on a non-commit terminal. `SettlePending` gained a
`positionEventOwed` parameter; `Pending` gained a `PositionEventOwed` field so
the re-issue retry marker (which stands for a packet whose placement was never
begun) cannot double-ack. The commit paths pass `false` because their ack has
already left. The non-commit ack runs no reconciliation — the body did not move,
so there is no committed frame to reconcile — and therefore carries the body's
unchanged pose, which is exactly what retail's ack carries after a failed
`SetPositionSimple` and is informative to the server: its force did not take.
The `CanSendPositionEvent` gate was left untouched; it is retail's own
(`CommandInterpreter::SendPositionEvent` @0x006B4770 tests the transient-state
contact bits), so a legitimately airborne body still suppresses the send on both
paths.
`ReconcileAndAcknowledge` was split so both paths share one outbound site,
`SendPositionEvent`.
## Other items closed this round
- **AD-62 rewritten.** The `DeferredCell`-park precondition is dropped — it was
never required, and the never-parked failure paths reach the same outcome. The
row now leads with the general rule and keeps the named shapes as examples,
adds the externally-blocked `Contention` shape (nothing recorded, nothing
pumps it) and the other `PositionAuthorityVersion` advances (`TryApplyPickup`
`RuntimeEntityObjectLifetime.cs:1116`, `CommitPositionChannelUpdate` `:2041`,
`AdvanceCreateAuthority` `:2466`), and separates the shapes that now lose only
the re-apply from the narrower shapes that still lose the ack too.
- **Overstated doc corrected.** `AcceptedForceObservation`'s comment claimed the
record's current version equals the recorded force's version *if and only if*
the newest accepted event was that force. False — `AdvancePositionAuthority`
has four call sites. It is now stated as the one-way test it actually is.
- **Vacuous assertion deleted.** The `gameActions.Count <= 2` assertion in
`Advanced_ReissuesWhenTheNewestAcceptedEventIsStillAForcePosition` could not
fail: that fixture's `CommitLandblockCollision` adds both landblocks at
`worldOffsetX/Y: 0f` while the world frame places the deferred landblock at
+192/+192, so the body lands over no terrain, `InContact` is false, and every
ack is suppressed — the count is 0. It was also too loose to encode "at most
one per packet". Deleted rather than shipped; the test's real discriminators
(body position, `PendingCount`) stay.
## Tests
Two added, both with a verified discrimination check (the implementation was
temporarily broken in each direction and the intended test observed to fail,
then reverted and re-verified):
- `TerminalWithoutCommit_SendsExactlyOnePositionEventAndLeavesTheBodyUnmoved`
a park retired without committing sends exactly one `AutonomousPosition`,
performs no placement of its own, and never repeats on further pumps.
- `Committed_SendsExactlyOnePositionEventAcrossTheCommitAndTheSettle` — the new
settle-side ack does not become a second ack on the committed path.
Two existing tests changed their ack expectation from `Assert.Empty` to
`Assert.Single`, because they exercise terminal-without-commit paths whose
`Empty` encoded the defect this round removes:
`Equal_ClearsPendingWithoutReissuingWhenNoNewerAcceptedAuthorityArrived` and
`Advanced_DoesNotReissueWhenTheNewestAcceptedEventIsAnOrdinaryApply`. In the
latter the single ack belongs to the FORCE packet, not to the ordinary echo —
retail's ordinary branch has no unconditional `SendPositionEvent`.
Measurement note, recorded because it corrects an assumption in this file's
round-2 text: the `DeferredCell` park these fixtures use is the POST-engine
quiescence park, so `_physics.Engine.SetPosition` has already moved the
canonical body to the destination while the placement itself is withdrawn and
parked. The new test therefore captures its unmoved/no-further-placement
baselines at the park, and asserts them across the terminal settle, which is the
thing under test.
## Filed, not fixed
`docs/ISSUES.md` #293 (the `DeferredCell` branch still consumes `Withdraw`
receipts the sink may have declined — the same shape R4 removed for `Place`),
#294 (`ReconcileAndAcknowledge` runs before the funnel's currency guard on the
deferred wake), #295 (the retry marker inflates
`AcceptedPositionDrivePendingCount`, which is an in-flight-placement counter),
#296 (a retryable prepare is reported to hosts as `Contention`, conflating a
retained-and-pumping case with a dropped one).
## Gate
Complete Release solution suite, unchanged discipline: green is necessary and
demonstrably not sufficient — all four prior states were green and three were
defective.

View file

@ -0,0 +1,135 @@
# C4 route 2 — ForcePosition: connected visual gate (2026-08-03)
The user-facing acceptance test for route 2. Route 2 is code-complete and
suite-green before this runs; this document is the hand-off.
## Why the obvious recipe does NOT work
The route-2 contract's acceptance line says *"ACE `@teleport`-style
displacement"*. That is wrong about which route it exercises, and following it
would have produced a false pass.
Verified in ACE this session: `PositionPack`'s constructor
(`references/ACE/Source/ACE.Server/Network/Structure/PositionPack.cs:36-57`)
advances `ObjectTeleport` when `adminMove == true` (lines 49-52) and only ever
*reads* `ObjectForcePosition` (line 54). Every admin move command —
`@teleto`, `@teletome`, `@teleloc`, `@movetome` — routes through
`Player_Location.cs:654 Teleport()` / `SendUpdatePosition(true)` and therefore
advances **TELEPORT_TS, not FORCE_POSITION_TS**. Those commands exercise
**route 3**, not route 2.
`SequenceType.ObjectForcePosition` is advanced at exactly **two** places in the
whole ACE tree:
1. `references/ACE/Source/ACE.Server/WorldObjects/Player.cs:1148` — the PK Lite
entry collision bump.
2. `references/ACE/Source/ACE.Server/WorldObjects/Player_Tick.cs:488` — the
anti-cheat z-position rubber-band.
(2) requires the server to believe you are fly-hacking — same landblock, a
claimed Z more than 10 units above your last ground contact, more than a second
after your last jump, Jump skill under 1000, and still flagged airborne. It is
not reachable by legitimate play and there is no command path into it. Do not
build the gate on it.
So (1) is the gate.
## The only reliable lever is deferred — what that means
The reachable trigger is the PK Lite entry-collision bump, which requires
retail's `@pklite`. That is a **client** command, not a server one — ACE has no
`pklite` text-command handler, so typing `@pklite` into chat today forwards as
inert text. Retail's client turns it into a game action instead:
- `ClientCommunicationSystem::DoPKLite` @`0x0057A490`
(`acclient_2013_pseudo_c.txt:390106`) — rejects with error `0x507` when
`ACCWeenieObject::IsPlayerKiller` @`0x0058C910` is true (PK bit `0x20` OR
PKLite bit `0x2000000`), otherwise calls
- `CM_Character::Event_EnterPKLite` @`0x006A13F0` (line 680071) — a 12-byte
parameterless game action, opcode `0x28F`, no payload.
acdream does not implement it, and **the user deferred implementing it
(2026-08-03).** Scoping is preserved in this session's research so it can be
picked up cheaply: ~40 lines of production code across `ClientCommandId`,
`RetailClientCommandCatalog`, `ClientCommandRequests.BuildParameterless`,
`WorldSession`, `ClientCommandController`, `LiveSessionCommandRouter`, and
`LiveSessionRuntimeFactory` — every pattern already exists, including
`WeenieError 0x0507`.
**Consequence, stated plainly: route 2's own behaviour is NOT visually
verifiable in this campaign.** Nothing a user can do makes ACE emit a
ForcePosition. Route 2's acceptance therefore rests on its automated Runtime /
App / Headless tests and the complete Release suite. The connected pass below
is a **regression check on the blast radius**, not acceptance of the new route.
Do not record it as the latter.
## What the connected pass actually covers
Launch acdream in **Release** with `ACDREAM_RETAIL_UI=1` against the local ACE
at `127.0.0.1:9000`.
Route 2 deletes `PlayerMovementController.BlipPosition` and
`HeadlessSessionWorldProjection.BlipLocalPlayer`, removes App's generic-tail
double-write for the local player, and re-routes the local player's accepted
placement through the canonical Runtime SetPosition transaction. So the things
to confirm are that ordinary play is untouched:
- Ordinary running, turning, walk/run toggle. No tethering, no rubber-band, no
drift against the server.
- A jump and a landing.
- Walking through a doorway into an interior and back out.
- A portal recall, and a portal into a dungeon.
- Two-client observation: `+Acdream` seen from the retail client moves smoothly
and lands where acdream shows it.
Anything wrong there is route 2's fault even though route 2 did not intend to
touch it.
## Optional cheap shot (may not fire)
The second ACE call site is the anti-cheat z-position rubber-band
(`Player_Tick.cs:459-490`). It needs: same landblock as your last ground
contact, a claimed Z more than 10 units above it, more than a second since your
last jump, Jump skill under 1000, and the server still flagging you airborne.
There is no command path into it and normal play cannot satisfy it (falling
makes the Z delta negative, and walking up terrain keeps refreshing the ground
position), but a `@teleloc` straight up ~15 units within the same landblock is
a two-minute experiment with a definitive tell: ACE's console logs
`z-pos hacking detected for +Acdream` at `Player_Tick.cs:486` immediately
before it force-bumps you.
If that line appears, you have a genuine ForcePosition and the checks below
apply. If it does not, the route is untested by observation — which is the
expected outcome.
**If a ForcePosition does occur, must be true:**
- You end up at the corrected position and stay there. No visible double-apply,
no snap-then-yank-back over one or two frames.
- **Your facing does not change.** Retail's force branch replaces the
destination heading with your current heading before placing
(`HandleReceivedPosition` @`0x00453FD0`, `Frame::set_heading` at
`0x00454068`).
- Exactly **one** outbound `AutonomousPosition` for the correction.
**The two named behaviour changes:**
1. **The ack now fires after the canonical commit, not before it.** Previously
the client told ACE "got it, I'm here" before deciding where "here" was.
Symptom of a regression: ACE re-sending corrections, or a visible fight
between client and server position after the bump.
2. **The ForcePosition route no longer re-arms the constraint leash.** Retail's
force branch returns at `0x0045409D`, ahead of all three `ConstrainTo` call
sites (`0x00454272`, `0x0045418A`, `0x004541EC`); our old `BlipPosition`
re-armed the leash citing a branch it was not on. #167 was a leash bug, so
this is the change most worth your eyes if you get a bump. Symptom of a
problem: after the bump, movement feels tethered, rubber-bands back toward
the pre-bump spot, or the leash trips on ordinary running shortly after.
## Known, deliberately unchanged
Retail's `SmartBox::PlayerPositionUpdated` (@`0x00453870`) gates its
`set_viewer` / `LScape::update_viewpoint` re-seat on
`distance >= GetAutonomyBlipDistance` (`0x004538C0-0x004538E2`). We publish the
render root unconditionally. Not changed in this slice; recorded here so the
next reader does not rediscover it as a bug.

View file

@ -573,6 +573,21 @@ internal sealed class SessionPlayerCompositionPhase
.RegisteredAuthoredPayload
: RuntimeLocalPlayerShadowDisposition.ProvenShapeless);
});
// C4 route 2 (2026-08-03): the graphical accepted-Position drive
// controller — the Runtime-owned execution seam for a ForcePosition
// on the already-live local player. Constructed once per session
// route alongside firstEntryDrive; both share the SAME entity
// lifetime, collision source, and clock.
var acceptedPositionDrive = new RuntimeAcceptedPositionDriveController(
d.EntityObjects,
d.Runtime.Clock,
firstEntryCollision,
d.PlayerOutbound,
() => d.Runtime.Generation,
() => d.PlayerIdentity.ServerGuid,
() => d.PlayerController.Controller,
() => d.Character.UsePositionFromServer,
() => liveSessionSource.CurrentSession);
var hydration = new LiveEntityHydrationController(
live.LiveEntities,
d.EntityObjects,
@ -591,7 +606,8 @@ internal sealed class SessionPlayerCompositionPhase
deletion,
dormantLiveEntities,
d.Options.DumpLiveSpawns ? d.Log : null,
firstEntryDrive);
firstEntryDrive,
acceptedPositionDrive);
bindings.Adopt(
"landblock-loaded hydration",
live.LandblockLoaded.Bind(hydration));
@ -637,6 +653,7 @@ internal sealed class SessionPlayerCompositionPhase
liveSessionSource,
localPhysicsTimestamps.Publish,
d.MovementDiagnostics,
acceptedPositionDrive,
worldDropProjection);
var liveness = new LiveEntityLivenessController(
live.LiveEntities,
@ -959,7 +976,8 @@ internal sealed class SessionPlayerCompositionPhase
live.RenderSceneShadow,
live.PlacementProjection,
placementProjectionRetry,
firstEntryDrive),
firstEntryDrive,
acceptedPositionDrive),
liveSessionCommands,
d.Log);
LiveSessionHost sessionHost = sessionRuntimeFactory.Create(

View file

@ -17,6 +17,7 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
private readonly Func<RuntimeGenerationToken> _generation;
private readonly RuntimePlacementProjectionRetrySlot _retries;
private readonly RuntimeFirstEntryDriveController? _firstEntry;
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
private readonly Action<RuntimeEntityRecord>? _localPlayerCompleted;
private RuntimePlacementProjectionSubscription? _subscription;
private IDisposable? _retryLease;
@ -30,7 +31,8 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
IRuntimePlacementProjectionSink placements,
RuntimePlacementProjectionRetrySlot retries,
RuntimeFirstEntryDriveController? firstEntry = null,
Action<RuntimeEntityRecord>? localPlayerCompleted = null)
Action<RuntimeEntityRecord>? localPlayerCompleted = null,
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
: this(
events,
() => new RuntimePlacementProjectionSubscription(
@ -40,7 +42,8 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
() => runtime.Generation,
retries,
firstEntry,
localPlayerCompleted)
localPlayerCompleted,
acceptedPositionDrive)
{
ArgumentNullException.ThrowIfNull(runtime);
ArgumentNullException.ThrowIfNull(placements);
@ -52,7 +55,8 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
Func<RuntimeGenerationToken> generation,
RuntimePlacementProjectionRetrySlot retries,
RuntimeFirstEntryDriveController? firstEntry = null,
Action<RuntimeEntityRecord>? localPlayerCompleted = null)
Action<RuntimeEntityRecord>? localPlayerCompleted = null,
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
{
_events = events ?? throw new ArgumentNullException(nameof(events));
_createSubscription = createSubscription
@ -62,6 +66,7 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
_retries = retries ?? throw new ArgumentNullException(nameof(retries));
_firstEntry = firstEntry;
_localPlayerCompleted = localPlayerCompleted;
_acceptedPositionDrive = acceptedPositionDrive;
}
public void Attach()
@ -75,6 +80,10 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
// detached — session reset precedes a new route — before this route
// takes ownership of the shared drive controller's tracked entries.
_firstEntry?.AttachRoute(this, _localPlayerCompleted);
// C4 route 2: same one-route-at-a-time latch for the accepted-
// Position drive controller (RuntimeAcceptedPositionDriveController
// .AttachRoute's doc comment).
_acceptedPositionDrive?.AttachRoute(this);
_events.Attach();
RuntimePlacementProjectionSubscription? subscription = null;
@ -90,10 +99,13 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
// republishing the pending FIFO head — a conductor's own
// Advance is what consumes conductor-owned receipts, and the
// subsequent RetryPending lets the presentation sink apply
// whatever new head the drive surfaced.
// whatever new head the drive surfaced. C4 route 2: the
// accepted-Position drive's own Advance resolves a parked
// DeferredCell ForcePosition the same way.
() =>
{
_firstEntry?.DriveAll();
_acceptedPositionDrive?.Advance();
return boundSubscription.RetryPending();
});
_subscription = subscription;
@ -123,6 +135,7 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
// route-scoped — a route that never attached cannot clear a live
// route's entries.
_firstEntry?.DetachRoute(this);
_acceptedPositionDrive?.DetachRoute(this);
if (!_eventsDisposed)
{
_events.Dispose();

View file

@ -87,7 +87,8 @@ internal sealed record LiveSessionWorldRuntime(
RenderSceneShadowRuntime? RenderSceneShadow,
RuntimePlacementPresentationSink PlacementProjection,
RuntimePlacementProjectionRetrySlot PlacementRetries,
RuntimeFirstEntryDriveController FirstEntryDrive);
RuntimeFirstEntryDriveController FirstEntryDrive,
RuntimeAcceptedPositionDriveController AcceptedPositionDrive);
/// <summary>
/// Builds the exact per-generation route/reset graph for the canonical live
@ -264,7 +265,8 @@ internal sealed class LiveSessionRuntimeFactory
_world.PlacementProjection,
_world.PlacementRetries,
_world.FirstEntryDrive,
_ => session.SendGameAction(GameActionLoginComplete.Build()));
_ => session.SendGameAction(GameActionLoginComplete.Build()),
_world.AcceptedPositionDrive);
}
private LiveInventorySessionBindings CreateInventoryBindings() => new(

View file

@ -15,6 +15,7 @@ using AcDream.Core.Items;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session;
using AcDream.Core.Selection;
using AcDream.Core.World;
using DatReaderWriter;
@ -63,6 +64,7 @@ internal sealed class LiveEntityNetworkUpdateController
private readonly IMovementTruthDiagnosticSink _movementTruthDiagnostics;
private readonly InventoryWorldDropProjectionController?
_worldDropProjection;
private readonly RuntimeAcceptedPositionDriveController _acceptedPositionDrive;
private PlayerMovementController? _playerController => _playerControllerSource.Controller;
private EntityPhysicsHost? _playerHost => _playerHostSource.Host;
@ -103,6 +105,7 @@ internal sealed class LiveEntityNetworkUpdateController
ILiveWorldSessionSource session,
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
IMovementTruthDiagnosticSink movementTruthDiagnostics,
RuntimeAcceptedPositionDriveController acceptedPositionDrive,
InventoryWorldDropProjectionController? worldDropProjection = null)
{
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
@ -138,6 +141,8 @@ internal sealed class LiveEntityNetworkUpdateController
publishTimestamps);
_movementTruthDiagnostics = movementTruthDiagnostics
?? throw new ArgumentNullException(nameof(movementTruthDiagnostics));
_acceptedPositionDrive = acceptedPositionDrive
?? throw new ArgumentNullException(nameof(acceptedPositionDrive));
_worldDropProjection = worldDropProjection;
}
@ -1113,20 +1118,61 @@ internal sealed class LiveEntityNetworkUpdateController
bool forceLocal = timestampDisposition is AcDream.Core.Physics.PositionTimestampDisposition.ForcePosition
&& update.Guid == _playerServerGuid
&& _playerController is not null;
if (!LocalForcePositionTransaction.Apply(
forceLocal,
() => IsCurrentPositionOwner(),
() => _playerController!.BlipPosition(
worldPos,
p.LandblockId,
new System.Numerics.Vector3(
p.PositionX,
p.PositionY,
p.PositionZ)),
() => _localPlayerOutbound.SendImmediatePosition(
_session.CurrentSession,
_playerController)))
return;
if (forceLocal)
{
if (!IsCurrentPositionOwner())
return;
// C4 route 2 (2026-08-03): the Runtime-owned accepted-Position
// execution seam replaces the deleted LocalForcePositionTransaction.
// Ownership validation is the operation's own currency check,
// the body commit is Runtime's canonical SetPosition transaction
// (retail CPhysicsObj::SetPositionSimple @0x005162B0, called from
// SmartBox::BlipPlayer @0x00453940), and the outbound ack is an
// OUTPUT of that committed route, fired strictly after it.
RuntimeAcceptedPositionExecutionStatus forceStatus =
_acceptedPositionDrive.TryExecuteAcceptedLocalPosition(
acceptedPositionCanonical,
update,
timestampDisposition,
timestamps,
timestamps.PreviousTeleport);
if (forceStatus is RuntimeAcceptedPositionExecutionStatus.Committed
or RuntimeAcceptedPositionExecutionStatus.DeferredCell)
{
// App projects the committed (or parked-toward) result
// through the existing Runtime placement sink
// (RuntimePlacementPresentationSink), which observes the
// SAME Runtime SetPosition FIFO every other placement uses —
// it must not ALSO independently mutate the render-facing
// WorldEntity here (the retired duplicate-write authority:
// the generic tail below). The two local-player side effects
// this neighbourhood still owns independently of WorldEntity
// position (owned-VFX pose-dirty tracking and the
// pre-player-mode streaming landblock tracker) are
// preserved — DeferredCell included, since following the
// destination is what lets its streaming/collision window
// eventually publish the generation the park is waiting on.
_entityEffects?.MarkLiveOwnerPoseDirty(update.Guid);
_authorityGate.ObserveAcceptedLocalPosition(
update.Position.LandblockId);
return;
}
if (forceStatus is not RuntimeAcceptedPositionExecutionStatus.NotApplicable)
{
// R9 review fix (2026-08-03): Rejected/Contention — no
// correction was applied or parked for this exact packet.
// Do NOT move the streaming observer or mark the render pose
// dirty for a landblock this route explicitly declined to
// place into, and do not fall through to the generic tail
// below either (that would resurrect the retired duplicate-
// write authority this whole route exists to remove).
return;
}
// NotApplicable — e.g. an initial-Create residence still owns
// this record (route 1's job). Fall through to the pre-existing
// path unchanged, exactly as every other disposition does.
}
// A leave-world transition deliberately retains WorldEntity as the
// logical/render-resource owner while IsSpatiallyProjected is false.

View file

@ -1,29 +0,0 @@
namespace AcDream.App.Physics;
/// <summary>
/// Preserves retail ForcePosition's atomic local order: validate the accepted
/// Position authority, blip once, acknowledge once, then stop if the
/// acknowledgement synchronously displaced that authority.
/// </summary>
internal static class LocalForcePositionTransaction
{
internal static bool Apply(
bool isForcePosition,
Func<bool> isCurrent,
Action blip,
Action acknowledge)
{
ArgumentNullException.ThrowIfNull(isCurrent);
ArgumentNullException.ThrowIfNull(blip);
ArgumentNullException.ThrowIfNull(acknowledge);
if (!isForcePosition)
return true;
if (!isCurrent())
return false;
blip();
acknowledge();
return isCurrent();
}
}

View file

@ -189,6 +189,13 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
/// controller without one.
/// </summary>
private readonly RuntimeFirstEntryDriveController? _firstEntry;
/// <summary>
/// C4 route 2: the graphical accepted-Position drive controller — pumped
/// alongside <see cref="_firstEntry"/> so a ForcePosition parked awaiting
/// its destination collision generation resolves promptly. Optional for
/// the same reason <see cref="_firstEntry"/> is.
/// </summary>
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
private readonly Dictionary<RuntimeEntityRecord, CanonicalProjectionOperation>
_projectionOperations =
new(ReferenceEqualityComparer.Instance);
@ -213,7 +220,8 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
LiveEntityDeletionController deletion,
DormantLiveEntityStore? dormant = null,
Action<string>? diagnostic = null,
RuntimeFirstEntryDriveController? firstEntry = null)
RuntimeFirstEntryDriveController? firstEntry = null,
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_entityObjects = entityObjects
@ -230,6 +238,7 @@ internal sealed class LiveEntityHydrationController : ILiveEntityLandblockLoaded
_dormant = dormant ?? new DormantLiveEntityStore();
_diagnostic = diagnostic;
_firstEntry = firstEntry;
_acceptedPositionDrive = acceptedPositionDrive;
}
internal event Action<uint>? AppearanceApplied;
@ -403,6 +412,8 @@ AppearanceSynchronization:
// destination cell, FIFO ahead of us) is retried by the
// per-frame placement retry phase.
_firstEntry?.DriveAll();
// C4 route 2: same pump for a parked ForcePosition.
_acceptedPositionDrive?.Advance();
}
}

View file

@ -17,6 +17,7 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
private readonly GameRuntime _runtime;
private readonly IRuntimePlacementProjectionSink _placements;
private readonly RuntimeFirstEntryDriveController? _firstEntry;
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
private readonly Action<RuntimeEntityRecord>? _localPlayerCompleted;
private RuntimePlacementProjectionSubscription? _subscription;
private bool _attachStarted;
@ -28,7 +29,8 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
GameRuntime runtime,
IRuntimePlacementProjectionSink placements,
RuntimeFirstEntryDriveController? firstEntry = null,
Action<RuntimeEntityRecord>? localPlayerCompleted = null)
Action<RuntimeEntityRecord>? localPlayerCompleted = null,
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
{
_events = events ?? throw new ArgumentNullException(nameof(events));
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
@ -36,6 +38,7 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
?? throw new ArgumentNullException(nameof(placements));
_firstEntry = firstEntry;
_localPlayerCompleted = localPlayerCompleted;
_acceptedPositionDrive = acceptedPositionDrive;
}
public void Attach()
@ -52,6 +55,9 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
// detached — session reset precedes a new route — before this route
// takes ownership of the shared drive controller's tracked entries.
_firstEntry?.AttachRoute(this, _localPlayerCompleted);
// C4 route 2: same one-route-at-a-time latch for the accepted-
// Position drive controller.
_acceptedPositionDrive?.AttachRoute(this);
_events.Attach();
_subscription = new RuntimePlacementProjectionSubscription(
_runtime,
@ -73,6 +79,7 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
// route-scoped — a route that never attached cannot clear a live
// route's entries.
_firstEntry?.DetachRoute(this);
_acceptedPositionDrive?.DetachRoute(this);
if (!_eventsDisposed)
{
_events.Dispose();

View file

@ -126,6 +126,11 @@ internal sealed class HeadlessSessionHost : IDisposable
/// Runtime lifetime) plus the active world projection it pumps
/// through.</summary>
private RuntimeFirstEntryDriveController? _firstEntryDrive;
/// <summary>C4 route 2 (2026-08-03): see the ctor comment in
/// <see cref="CreateEventRoute"/> — cached across reconnects exactly
/// like <see cref="_firstEntryDrive"/>.</summary>
private RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
private AcDream.Core.Net.WorldSession? _currentSession;
private HeadlessSessionWorldProjection? _worldProjection;
private int _disposeStage;
private long _reconnectDeadline;
@ -311,8 +316,20 @@ internal sealed class HeadlessSessionHost : IDisposable
_policy.Tick(Runtime, Commands);
}
internal RuntimeTeardownAcknowledgement Stop() =>
Commands.Session.Stop(Runtime.Generation);
internal RuntimeTeardownAcknowledgement Stop()
{
RuntimeTeardownAcknowledgement result =
Commands.Session.Stop(Runtime.Generation);
// R9 review fix (2026-08-03): _currentSession is cached across
// reconnects (see CreateEventRoute's comment) so the accepted-
// position drive controller's outbound-ack accessor always reads the
// CURRENT session, never one captured at first construction. Without
// clearing it here, that accessor would keep returning a stopped
// (possibly disposed) WorldSession in the window between this Stop
// and the next CreateEventRoute call.
_currentSession = null;
return result;
}
internal void Quarantine(Exception error)
{
@ -536,6 +553,25 @@ internal sealed class HeadlessSessionHost : IDisposable
private ILiveSessionEventRouting CreateEventRoute(
AcDream.Core.Net.WorldSession session)
{
// C4 route 2 (2026-08-03): reconnects construct a FRESH WorldSession
// each call, but the accepted-Position drive controller below is
// cached across reconnects (`??=`) exactly like _firstEntryDrive —
// its ack-firing accessor must therefore read the CURRENT session
// through this field, never one captured at first construction.
_currentSession = session;
// R9 review note (2026-08-03): a content-less host (_contentLease is
// null — a validated-legal headless configuration, see
// RuntimeLiveEntitySessionController.OnSpawned's own R3 comment)
// never constructs _acceptedPositionDrive OR worldProjection below,
// so a ForcePosition on such a host resolves NotApplicable with no
// ProjectPosition fallback either. This is NOT a behaviour change:
// a content-less host never registers a first-entry residence, so
// Runtime.MovementOwner.Controller is always null there too, and
// LocalPlayerOutboundController.SendImmediatePosition's own
// controller-null guard already made the PRE-route-2 unconditional
// ack call a no-op in this exact configuration. There is no live
// controller for either the old or the new path to place or
// acknowledge.
IRuntimeDirectWorldProjection? worldProjection = null;
if (_contentLease is { } content)
{
@ -557,10 +593,24 @@ internal sealed class HeadlessSessionHost : IDisposable
Radius: 0.48f,
Height: 1.835f,
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
// C4 route 2: one drive controller per host, mirroring
// _firstEntryDrive exactly — same persistent Runtime lifetime,
// collision source, and clock.
_acceptedPositionDrive ??= new RuntimeAcceptedPositionDriveController(
Runtime.EntityObjects,
Runtime.Clock,
content.PreparedCollision,
new LocalPlayerOutboundController((_, _, _, _, _, _) => { }),
() => Runtime.Generation,
() => Runtime.PlayerIdentity.ServerGuid,
() => Runtime.MovementOwner.Controller,
() => Runtime.CharacterOwner.UsePositionFromServer,
() => _currentSession);
var projection = new HeadlessSessionWorldProjection(
Runtime,
content,
_firstEntryDrive);
_firstEntryDrive,
_acceptedPositionDrive);
_worldProjection = projection;
worldProjection = projection;
}
@ -571,7 +621,8 @@ internal sealed class HeadlessSessionHost : IDisposable
_descriptor.Id,
message,
Runtime.Generation.Value),
worldProjection);
worldProjection,
_acceptedPositionDrive);
var route = new LiveSessionEventRouter(
session,
entities.CreateSink(),
@ -620,7 +671,8 @@ internal sealed class HeadlessSessionHost : IDisposable
Runtime,
new HeadlessRuntimePlacementProjectionSink(Runtime),
_firstEntryDrive,
_ => session.SendGameAction(GameActionLoginComplete.Build()));
_ => session.SendGameAction(GameActionLoginComplete.Build()),
_acceptedPositionDrive);
}
private static LiveSessionCharacterSelector MapCharacterSelector(

View file

@ -504,29 +504,39 @@ internal sealed class HeadlessSessionWorldProjection
private readonly GameRuntime _runtime;
private readonly IHeadlessCollisionNeighborhood _collision;
private readonly RuntimeFirstEntryDriveController? _firstEntry;
/// <summary>
/// C4 route 2 (2026-08-03): pumped alongside <see cref="_firstEntry"/> so
/// a ForcePosition parked awaiting its destination collision generation
/// resolves promptly.
/// </summary>
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
private uint _requestedLocalPlayerCell;
internal HeadlessSessionWorldProjection(
GameRuntime runtime,
HeadlessProcessContentOwner.HeadlessProcessContentLease content,
RuntimeFirstEntryDriveController? firstEntry = null)
RuntimeFirstEntryDriveController? firstEntry = null,
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
: this(
runtime,
new HeadlessCollisionNeighborhood(runtime, content),
firstEntry)
firstEntry,
acceptedPositionDrive)
{
}
internal HeadlessSessionWorldProjection(
GameRuntime runtime,
IHeadlessCollisionNeighborhood collision,
RuntimeFirstEntryDriveController? firstEntry = null)
RuntimeFirstEntryDriveController? firstEntry = null,
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
{
_runtime = runtime
?? throw new ArgumentNullException(nameof(runtime));
_collision = collision
?? throw new ArgumentNullException(nameof(collision));
_firstEntry = firstEntry;
_acceptedPositionDrive = acceptedPositionDrive;
}
public void ProjectSpawn(
@ -569,6 +579,7 @@ internal sealed class HeadlessSessionWorldProjection
.TryConvertInitialResidenceToCellessRoute(record);
}
_firstEntry?.DriveAll();
_acceptedPositionDrive?.Advance();
}
public void ProjectPosition(
@ -592,11 +603,43 @@ internal sealed class HeadlessSessionWorldProjection
_collision.CenterOn(position.LandblockId);
}
_firstEntry?.DriveAll();
_acceptedPositionDrive?.Advance();
}
// C4 route 2 (2026-08-03): a ForcePosition on the local player is
// dispatched directly to RuntimeAcceptedPositionDriveController
// instead of running the branch below this comment — the deleted
// BlipLocalPlayer's body-commit/controller-reconciliation/ack job is
// that controller's now (CenterOnAcceptedForcePosition covers the
// re-centering half). R3 review fix (2026-08-03): when that call
// returns NotApplicable — most commonly THIS controller-null branch,
// reached while route 1 hasn't published a controller yet —
// RuntimeLiveEntitySessionController.OnPositionUpdated still falls
// back to calling this method, exactly like every other disposition.
}
/// <summary>
/// R2 review fix (2026-08-03): the centering half of the deleted
/// <c>BlipLocalPlayer</c> — <c>_collision.CenterOn(position.LandblockId)</c>
/// plus the <see cref="_requestedLocalPlayerCell"/> update
/// <see cref="PumpFirstEntry"/> polls. Without this, a ForcePosition to a
/// destination outside the neighborhood's current 3x3 window
/// (<c>HeadlessCollisionNeighborhood.BuildPublicationPlan</c>) would open
/// a <c>DeferredCell</c> park this host's window can never publish —
/// see <see cref="AcDream.Runtime.Session.RuntimeAcceptedPositionDriveController.Advance"/>'s
/// R1 doc comment. Called BEFORE the drive controller submits so the
/// destination is already inside the window by the time it decides
/// whether to park.
/// </summary>
public void CenterOnAcceptedForcePosition(RuntimeEntityRecord record)
{
if (record.ServerGuid != _runtime.PlayerIdentity.ServerGuid
|| record.Snapshot.Position is not { LandblockId: not 0u } position)
{
return;
}
if (disposition is PositionTimestampDisposition.ForcePosition)
BlipLocalPlayer(record);
_requestedLocalPlayerCell = position.LandblockId;
_collision.CenterOn(position.LandblockId);
}
/// <summary>
@ -610,6 +653,7 @@ internal sealed class HeadlessSessionWorldProjection
if (_requestedLocalPlayerCell != 0u)
_ = _collision.IsReady(_requestedLocalPlayerCell);
_firstEntry?.DriveAll();
_acceptedPositionDrive?.Advance();
}
public void BeginTeleport()
@ -704,26 +748,4 @@ internal sealed class HeadlessSessionWorldProjection
controller.SetBodyOrientation(orientation);
}
private void BlipLocalPlayer(RuntimeEntityRecord record)
{
if (record.ServerGuid
!= _runtime.PlayerIdentity.ServerGuid
|| record.Snapshot.Position is not { } position
|| _runtime.MovementOwner.Controller is not { } controller)
{
return;
}
_collision.CenterOn(position.LandblockId);
Vector3 wirePosition = new(
position.PositionX,
position.PositionY,
position.PositionZ);
controller.LocalEntityId = record.LocalEntityId ?? 0u;
controller.BlipPosition(
wirePosition,
position.LandblockId,
wirePosition);
}
}

View file

@ -79,7 +79,16 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
/// Previously outside every ledger; gated by <see cref="IsConverged"/>
/// like the conductor counts it pumps.
/// </summary>
int FirstEntryDrivePendingCount = 0)
int FirstEntryDrivePendingCount = 0,
/// <summary>
/// C4 route 2: outstanding host <c>RuntimeAcceptedPositionDriveController</c>
/// pending operations (a not-yet-committed or not-yet-acknowledged
/// ForcePosition on the local player), summed over every drive
/// registered against this lifetime via
/// <see cref="RuntimeEntityObjectLifetime.RegisterAcceptedPositionDriveOwnership"/>.
/// Gated by <see cref="IsConverged"/> — a leaked pending ack cannot hide.
/// </summary>
int AcceptedPositionDrivePendingCount = 0)
{
public bool IsConverged =>
IsDisposed
@ -104,6 +113,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
&& LocalPlayerFirstEntryActiveCount == 0
&& RemoteFirstEntryActiveCount == 0
&& FirstEntryDrivePendingCount == 0
&& AcceptedPositionDrivePendingCount == 0
&& StreamSubscriberCount == 0
&& PlacementStreamSubscriberCount == 0
&& PendingDispatchCount == 0
@ -151,6 +161,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
private Action<RuntimeEntityRecord>? _initialResidenceBegan;
/// <summary>C3c-R1 review F5: see <see cref="RegisterFirstEntryDriveOwnership"/>.</summary>
private readonly List<Func<int>> _firstEntryDriveOwnership = [];
/// <summary>C4 route 2: see <see cref="RegisterAcceptedPositionDriveOwnership"/>.</summary>
private readonly List<Func<int>> _acceptedPositionDriveOwnership = [];
public RuntimeEntityObjectLifetime(
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
@ -452,7 +464,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InitialCreateExecution.PendingCompletionReceiptCount,
LocalPlayerFirstEntry.CaptureOwnership().ActiveCount,
RemoteFirstEntry.CaptureOwnership().ActiveCount,
CaptureFirstEntryDrivePendingCount());
CaptureFirstEntryDrivePendingCount(),
CaptureAcceptedPositionDrivePendingCount());
}
private int CaptureFirstEntryDrivePendingCount()
@ -463,6 +476,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
return total;
}
private int CaptureAcceptedPositionDrivePendingCount()
{
int total = 0;
for (int i = 0; i < _acceptedPositionDriveOwnership.Count; i++)
total = checked(total + _acceptedPositionDriveOwnership[i]());
return total;
}
/// <summary>
/// C3c-R1 review F5: registers one host first-entry drive controller's
/// pending-count provider into this lifetime's ownership snapshot, so
@ -478,6 +499,21 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
_firstEntryDriveOwnership.Add(pendingCount);
}
/// <summary>
/// C4 route 2: registers one host <c>RuntimeAcceptedPositionDriveController</c>'s
/// pending-count provider into this lifetime's ownership snapshot,
/// mirroring <see cref="RegisterFirstEntryDriveOwnership"/> — a leaked
/// pending ForcePosition ack must not sit outside every ledger. The
/// drive controller registers itself at construction; multiple
/// registrations sum (one per host route sharing this lifetime).
/// </summary>
public void RegisterAcceptedPositionDriveOwnership(Func<int> pendingCount)
{
ArgumentNullException.ThrowIfNull(pendingCount);
EnsureNotDisposed();
_acceptedPositionDriveOwnership.Add(pendingCount);
}
public void BindEventContext(
Func<RuntimeGenerationToken> generation,
Func<ulong> frameNumber)

View file

@ -629,8 +629,13 @@ public sealed class PlayerMovementController
/// stick down (retail <c>teleport_hook</c> @0x00514eee) and, as of
/// Campaign P P5 (#167), also tears down and immediately re-arms the
/// constraint leash (<c>UnConstrain</c> then <c>ConstrainTo</c>);
/// <see cref="BlipPosition"/> arms the leash without tearing it down first
/// (retail <c>SmartBox::BlipPlayer</c> survives motion/velocity/stick).
/// <see cref="CommitCanonicalForcePositionFrame"/>'s ForcePosition route
/// does NOT touch the leash at all — retail's FORCE_POSITION branch of
/// <c>SmartBox::HandleReceivedPosition</c> (@0x00453FD0) returns at
/// 0x0045409D, before every <c>ConstrainTo</c> call (C4 route 2 review
/// fix, 2026-08-03 — the deleted <c>BlipPosition</c>'s unconditional
/// leash re-arm here was an unbacked deviation for that exact branch;
/// see docs/research/2026-08-03-c4-route-2-implementation-plan.md §1b).
/// </summary>
public AcDream.Core.Physics.Motion.PositionManager? PositionManager
{
@ -1915,25 +1920,33 @@ public sealed class PlayerMovementController
}
/// <summary>
/// Retail <c>SmartBox::BlipPlayer</c> (0x00453940): apply a server
/// FORCE_POSITION correction through <c>CPhysicsObj::SetPositionSimple</c>
/// without the teleport hook. Active motion, velocity, contact state, and
/// PositionManager stick relationships deliberately survive the blip.
/// C4 route 2: the controller-local half of a ForcePosition commit whose
/// body write already happened inside Runtime's canonical
/// <c>RuntimeSetPositionState.CommitCanonical</c> (retail
/// <c>CPhysicsObj::SetPositionSimple</c> @0x005162B0 with flags
/// <c>0x1012</c>, called from <c>SmartBox::BlipPlayer</c> @0x00453940,
/// acclient_2013_pseudo_c.txt:284276/92528). Resets the render-lerp
/// anchors and republishes the render-root cell — the same two
/// controller-local jobs the deleted <c>BlipPosition</c> performed after
/// its own (now-Runtime-owned) body snap.
///
/// Deliberately does NOT call <see cref="RearmConstraintLeashAtCurrentPosition"/>:
/// retail's FORCE_POSITION branch of <c>SmartBox::HandleReceivedPosition</c>
/// (@0x00453FD0) returns at 0x0045409D, before every
/// <c>CPhysicsObj::ConstrainTo</c> call (0x00454272/0x0045418A/
/// 0x004541EC) — the deleted <c>BlipPosition</c>'s re-arm here was an
/// unbacked deviation (docs/research/2026-08-03-c4-route-2-implementation-plan.md
/// §1b); this route retires it. Active motion, velocity, contact state,
/// and PositionManager stick relationships are untouched, matching
/// retail's BlipPlayer path exactly (Runtime's canonical commit — not
/// this method — is what already wrote the body).
/// </summary>
public void BlipPosition(Vector3 pos, uint cellId, Vector3 cellLocal)
internal void CommitCanonicalForcePositionFrame()
{
EnsurePublishedForRuntimeOperation();
_body.SnapToCell(cellId, pos, cellLocal);
_prevPhysicsPos = pos;
_currPhysicsPos = pos;
_prevPhysicsPos = _body.Position;
_currPhysicsPos = _body.Position;
UpdateCellId(_body.CellPosition.ObjCellId, "force-position");
// #167 (Campaign P P5): retail "Player, normal" branch of
// SmartBox::HandleReceivedPosition (0x00453fd0) — ConstrainTo anchored
// to the received position, with NO teardown call (this is the
// BlipPlayer path: motion, velocity, and PositionManager stick
// relationships all deliberately survive the blip per the class
// comment above, and the leash is no different).
RearmConstraintLeashAtCurrentPosition();
}
private Vector3 ComputeRenderPosition()

View file

@ -0,0 +1,906 @@
using System.Numerics;
using AcDream.Content;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Session;
/// <summary>
/// Typed yields for
/// <see cref="RuntimeAcceptedPositionDriveController.TryExecuteAcceptedLocalPosition"/>.
/// </summary>
internal enum RuntimeAcceptedPositionExecutionStatus : byte
{
/// <summary>
/// Out of this route's scope (not a ForcePosition, not the local player,
/// no canonical body, or an initial-Create residence still owns the
/// record). The caller's own pre-flip path runs unchanged.
/// </summary>
NotApplicable,
/// <summary>
/// Either (a) the classifier itself rejected this exact frame (non-finite
/// data, an invalid entity kind) before any SetPosition ran and no ack was
/// sent, or (b) a SetPosition WAS begun and submitted but was then
/// rejected/cancelled by <see cref="RuntimeSetPositionState.TryPrepareAndSubmitAuthoredPlacement"/>
/// (invalid prepared data, authority displaced mid-submit) — R5 review
/// fix (2026-08-03): the two call sites inside <c>SubmitAndResolve</c>
/// both reach this status AFTER a SetPosition ran, so "no SetPosition
/// ran" is only true for case (a).
///
/// Round 3 (2026-08-03): the two statuses differ in acknowledgement. Case
/// (a) sends nothing — a classifier-rejected frame is not an accepted
/// authority. Case (b) IS an accepted force packet whose placement was
/// begun and failed, so <c>SettlePending</c> sends its retail position
/// event carrying the body's unchanged pose (<c>SmartBox::BlipPlayer</c>
/// @0x00453940 discards <c>SetPositionSimple</c>'s error and
/// <c>SmartBox::HandleReceivedPosition</c> @0x00453FD0 acks
/// unconditionally @0x00454091).
/// </summary>
Rejected,
/// <summary>
/// The entity already holds an active SetPosition operation (or an
/// unacknowledged completion); begin failed on transient contention, not
/// staleness. This packet's classified force route was still recorded as
/// the drive's newest accepted force observation BEFORE the failed begin
/// (see <c>_newestForce</c>), so when the contending operation is this
/// controller's OWN tracked <c>_pending</c> — the B1 case: a park that
/// woke and had its <c>Place</c> ACCEPTED, whose retained completion
/// survives <c>RuntimeSetPositionState.Forget</c>'s early return and
/// blocks the next begin — the single <c>_pending</c> funnel
/// (<c>SettlePending</c>) re-issues THIS packet's correction the moment
/// that older completion is consumed. When it is a genuinely EXTERNAL
/// operation (a concurrent portal/teleport placement on the same entity)
/// and nothing of this route's own is pending, this route correctly
/// yields — a later accepted Position re-attempts once that other
/// placement completes, exactly like every other route yields to a
/// concurrent placement authority.
/// </summary>
Contention,
/// <summary>
/// The destination landblock's collision generation was not ready; the
/// operation parked. The ack fires later, once
/// <see cref="RuntimeAcceptedPositionDriveController.Advance"/> observes
/// the deferred commit.
/// </summary>
DeferredCell,
/// <summary>
/// The canonical SetPosition committed synchronously (retail
/// <c>CPhysicsObj::SetPositionSimple</c> @0x005162B0, called from
/// <c>SmartBox::BlipPlayer</c> @0x00453940). The controller-local
/// reconciliation ran and the outbound ack (if any) already went out.
/// </summary>
Committed,
}
/// <summary>
/// C4 route 2: the Runtime-owned accepted-Position execution seam for a
/// ForcePosition on an already-live local player. Retail
/// <c>SmartBox::HandleReceivedPosition</c> (@0x00453FD0,
/// acclient_2013_pseudo_c.txt:92896) FORCE_POSITION branch:
///
/// <code>
/// get_heading(player);
/// Frame::set_heading(&amp;dest, heading); // 00454068 preserve OUR heading
/// SmartBox::BlipPlayer(this, &amp;dest); // 00454074
/// player-&gt;update_times[0] = arg7; // 00454079 stamp POSITION_TS
/// cmdinterp-&gt;SendPositionEvent(); // 00454091 ack AFTER the commit
/// return; // 0045409d
/// </code>
///
/// <c>SmartBox::BlipPlayer</c> (@0x00453940, line 92528) calls
/// <c>CPhysicsObj::SetPositionSimple</c> (@0x005162B0, line 284276), which
/// with a non-null destination frame builds a <c>SetPositionStruct</c> with
/// flags <c>0x1012</c> (<c>Teleport|Slide|SendPositionEvent</c> —
/// <see cref="RuntimeAuthoritativePositionRouteClassifier"/>'s
/// <c>AuthoritativeTeleportFlags</c>) and calls
/// <c>CPhysicsObj::SetPosition</c>. This class is the Runtime consumer the
/// classifier's ForcePosition branch never had: it drives
/// <see cref="RuntimeSetPositionState.TryBeginExclusiveAuthoredPlacement"/> +
/// <see cref="RuntimeSetPositionState.TryPrepareAndSubmitAuthoredPlacement"/>
/// exactly like <see cref="RuntimeFirstEntryDriveController"/>'s continuation
/// completion, then fires the ack strictly AFTER the canonical commit.
///
/// Two named behaviour changes versus the deleted App/no-window authorities
/// this replaces:
/// (1) the outbound <c>AutonomousPosition</c> ack now fires only after the
/// canonical commit (previously it left before any Runtime commit existed —
/// see the deleted <c>LocalForcePositionTransaction</c>/
/// <c>HeadlessSessionWorldProjection.BlipLocalPlayer</c> pair);
/// (2) the constraint leash is NOT re-armed here — every
/// <c>CPhysicsObj::ConstrainTo</c> call in <c>HandleReceivedPosition</c>
/// (@0x00454272/0x0045418A/0x004541EC) is on a branch the FORCE_POSITION
/// early return (@0x0045409D) never reaches; the deleted
/// <c>PlayerMovementController.BlipPosition</c>'s re-arm was an unbacked
/// deviation this route retires (docs/research/2026-08-03-c4-route-2-implementation-plan.md §1b).
///
/// One instance per host session route (graphical/headless), constructed
/// once per host process and reused across reconnects exactly like
/// <see cref="RuntimeFirstEntryDriveController"/> — <see cref="AttachRoute"/>/
/// <see cref="DetachRoute"/> assert the same "session reset precedes a new
/// route" ordering and clear any pending operation left by a torn-down
/// session. The controller tracks at most one pending operation (the local
/// player is the only entity this route ever touches).
///
/// R9 review note (2026-08-03): kept <c>public</c> — unlike its template
/// <see cref="RuntimeFirstEntryDriveController"/> (<c>internal sealed</c>),
/// this class is a required parameter type on
/// <see cref="RuntimeLiveEntitySessionController"/>'s own <c>public</c>
/// constructor (<c>src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs</c>),
/// so C# accessibility rules (CS0051) require it. Every constructor
/// parameter and member below is <c>internal</c>; only the type name itself
/// is public, and only because the class it is threaded through already is.
/// Narrowing <see cref="RuntimeLiveEntitySessionController"/> itself is a
/// separate, broader change outside this fix's scope.
/// </summary>
public sealed class RuntimeAcceptedPositionDriveController
{
private sealed class Pending
{
internal required RuntimeEntityRecord Record { get; init; }
internal required RuntimeEntityPlacementToken Token { get; init; }
internal required RuntimeAuthoritativePositionRoute Route { get; init; }
internal required bool AwaitingCommitWake { get; init; }
/// <summary>
/// Round 3 (2026-08-03): true while this descriptor stands for an
/// accepted force packet whose placement WAS begun and whose retail
/// position event (<c>CommandInterpreter::SendPositionEvent</c>
/// @0x006B4770, called unconditionally at
/// <c>SmartBox::HandleReceivedPosition</c> @0x00454091) has not gone
/// out yet. It is <c>false</c> only for the re-issue retry marker
/// <see cref="SettlePending"/> parks when a re-issue cannot even
/// begin: that marker stands for a packet whose placement was never
/// begun, so its ack is owed by the eventual re-issue's own terminal
/// outcome, not by the marker.
/// </summary>
internal required bool PositionEventOwed { get; init; }
}
/// <summary>
/// Round 2 unified mechanism (2026-08-03): the newest accepted local-player
/// ForcePosition this drive has been handed, stamped with the canonical
/// record's <c>PositionAuthorityVersion</c> as observed at that packet's
/// own merge. It is the drive's ONLY knowledge of "what disposition was
/// the newest accepted position event", because
/// <see cref="TryExecuteAcceptedLocalPosition"/> is dispatched by both
/// hosts for ForcePosition and only for ForcePosition — an ordinary
/// <c>Apply</c> merges (advancing <c>PositionAuthorityVersion</c>) without
/// ever reaching this class.
///
/// Round 3 correction (2026-08-03): this is a ONE-WAY test, not a
/// biconditional. If the live record's current
/// <c>PositionAuthorityVersion</c> differs from
/// <see cref="PositionAuthorityVersion"/>, then some other authority
/// advance has happened since this force was recorded and the funnel must
/// not re-issue it. The converse does NOT hold: equality does not prove
/// the newest accepted event was this ForcePosition, because
/// <c>RuntimeEntityRecordTable.AdvancePositionAuthority</c> has four call
/// sites, not one — the ordinary accepted-Position merge
/// (<c>RuntimeEntityObjectLifetime.cs:1647</c>) plus
/// <c>TryApplyPickup</c> (<c>:1116</c>),
/// <c>CommitPositionChannelUpdate</c> (<c>:2041</c>) and
/// <c>AdvanceCreateAuthority</c> (<c>:2466</c>). The latter three are
/// effectively unreachable for a live local player, but the funnel's
/// safety does not depend on that: an unnoticed advance can only make the
/// funnel decline a re-issue it might have made (register row
/// <b>AD-62</b>), never make it re-issue a stale pose.
/// </summary>
private readonly record struct AcceptedForceObservation(
RuntimeEntityKey Entity,
ulong PositionAuthorityVersion,
RuntimeAuthoritativePositionRoute Route);
private readonly RuntimeEntityObjectLifetime _entityObjects;
private readonly IGameRuntimeClock _clock;
private readonly IPreparedCollisionSource _collisionSource;
private readonly LocalPlayerOutboundController _localPlayerOutbound;
private readonly Func<RuntimeGenerationToken> _generation;
private readonly Func<uint> _localPlayerServerGuid;
private readonly Func<PlayerMovementController?> _localController;
private readonly Func<bool> _usePositionFromServer;
private readonly Func<WorldSession?> _session;
/// <summary>
/// The drive's at-most-one in-flight placement for the local player.
/// Round 2 unified mechanism (2026-08-03): exactly THREE members write
/// this field — <see cref="RetainPending"/> (the operation is still
/// outstanding), <see cref="SettlePending"/> (the single terminal-outcome
/// funnel), and <see cref="AbandonPending"/> (route teardown, which is
/// outside the operation lifecycle entirely). No branch of
/// <see cref="TryExecuteAcceptedLocalPosition"/>,
/// <see cref="SubmitAndResolve"/> or <see cref="Advance"/> assigns or
/// clears it directly. Round 1 spread that ownership across the
/// individual branches, which is the shared root cause of B1, N1 and N2
/// in docs/research/2026-08-03-c4-route-2-review-findings.md.
/// </summary>
private Pending? _pending;
private AcceptedForceObservation? _newestForce;
private object? _routeOwner;
internal RuntimeAcceptedPositionDriveController(
RuntimeEntityObjectLifetime entityObjects,
IGameRuntimeClock clock,
IPreparedCollisionSource collisionSource,
LocalPlayerOutboundController localPlayerOutbound,
Func<RuntimeGenerationToken> generation,
Func<uint> localPlayerServerGuid,
Func<PlayerMovementController?> localController,
Func<bool> usePositionFromServer,
Func<WorldSession?> session)
{
_entityObjects = entityObjects
?? throw new ArgumentNullException(nameof(entityObjects));
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
_collisionSource = collisionSource
?? throw new ArgumentNullException(nameof(collisionSource));
_localPlayerOutbound = localPlayerOutbound
?? throw new ArgumentNullException(nameof(localPlayerOutbound));
_generation = generation
?? throw new ArgumentNullException(nameof(generation));
_localPlayerServerGuid = localPlayerServerGuid
?? throw new ArgumentNullException(nameof(localPlayerServerGuid));
_localController = localController
?? throw new ArgumentNullException(nameof(localController));
_usePositionFromServer = usePositionFromServer
?? throw new ArgumentNullException(nameof(usePositionFromServer));
_session = session ?? throw new ArgumentNullException(nameof(session));
_entityObjects.RegisterAcceptedPositionDriveOwnership(
() => _pending is null ? 0 : 1);
}
internal int PendingCount => _pending is null ? 0 : 1;
/// <summary>
/// C3c-R1-style one-route-at-a-time latch (mirrors
/// <see cref="RuntimeFirstEntryDriveController.AttachRoute"/>): this
/// controller outlives its session routes (hosts reuse it across
/// reconnects), so the "session reset precedes a new route" ordering is
/// asserted, not assumed.
/// </summary>
internal void AttachRoute(object route)
{
ArgumentNullException.ThrowIfNull(route);
if (_routeOwner is not null && !ReferenceEquals(_routeOwner, route))
{
throw new InvalidOperationException(
"An accepted-position drive controller serves one session "
+ "route at a time; the prior route must be disposed "
+ "(session reset precedes a new route) before a "
+ "replacement attaches.");
}
_routeOwner = route;
}
/// <summary>
/// Route-scoped teardown: abandons any pending operation, but ONLY when
/// <paramref name="route"/> is the attached owner.
/// </summary>
internal void DetachRoute(object route)
{
ArgumentNullException.ThrowIfNull(route);
if (!ReferenceEquals(_routeOwner, route))
return;
_routeOwner = null;
AbandonPending();
}
/// <summary>
/// Route teardown — the one <see cref="_pending"/> write that is NOT a
/// terminal operation outcome. The torn-down route's newest accepted
/// force observation dies with it: a reconnect re-merges its own
/// positions, and a stale observation must never survive to authorize a
/// re-issue against a later session's record.
/// </summary>
private void AbandonPending()
{
_newestForce = null;
if (_pending is not { } pending)
return;
_pending = null;
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
setPosition.ForgetPlacementCompletion(pending.Token);
RuntimePlacementCancellationReceipt cancellation =
setPosition.ForgetExactPlacement(pending.Token);
if (cancellation.IsValid)
setPosition.PublishCancellation(cancellation);
}
/// <summary>
/// Executes a ForcePosition Position update against the canonical
/// Runtime SetPosition owner. <paramref name="record"/>'s Snapshot,
/// timestamps, and PositionAuthorityVersion must already reflect the
/// merge <see cref="RuntimeEntityObjectLifetime.TryApplyPosition"/>
/// performed for this exact <paramref name="update"/> — this method
/// never re-merges the wire frame; it only routes the already-accepted
/// pose through the canonical placement transaction.
/// </summary>
internal RuntimeAcceptedPositionExecutionStatus TryExecuteAcceptedLocalPosition(
RuntimeEntityRecord record,
in WorldSession.EntityPositionUpdate update,
PositionTimestampDisposition disposition,
in AcceptedPhysicsTimestamps timestamps,
ushort previousTeleportSequence)
{
ArgumentNullException.ThrowIfNull(record);
if (disposition is not PositionTimestampDisposition.ForcePosition
|| record.ServerGuid != _localPlayerServerGuid()
|| record.PhysicsBody is null
|| record.Key is not { } key
// Route 1 owns an active initial-Create residence: the
// executor already retains the Position as its own tail
// action (RuntimeInitialCreateContinuationExecutor
// .ApplyPositionAction) and already carries
// SendPositionImmediately. Route 2 must not double-drive it.
|| _entityObjects.TryGetInitialCreateResidence(record, out _))
{
return RuntimeAcceptedPositionExecutionStatus.NotApplicable;
}
RuntimeAuthoritativePositionRoute route = ClassifyForcePosition(
record, key, update, disposition, timestamps, previousTeleportSequence);
if (!route.Accepted)
{
// The wire's accepted timestamp/position channels were already
// merged into record.Snapshot upstream
// (RuntimeEntityObjectLifetime.TryApplyPosition) unconditionally
// on the disposition being non-Rejected — unlike the initial-
// Create continuation flow (whose merge happens INSIDE the
// route.Accepted branch), there is no separate "stamp-only"
// write left to perform here. A classifier rejection at this
// point is a data-validity failure (non-finite frame), not a
// staleness one; the caller's own currency re-check already
// covers staleness upstream.
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
// Round 2 unified mechanism: record THIS packet as the newest accepted
// force BEFORE attempting to begin, so a failed begin (Contention)
// still leaves the funnel able to re-issue this exact correction. A
// classifier-rejected frame deliberately never gets here: it is not an
// accepted authority and must never authorize a re-issue.
ulong acceptedVersion = record.PositionAuthorityVersion;
_newestForce = new AcceptedForceObservation(key, acceptedVersion, route);
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
RuntimeEntityPlacementToken token =
setPosition.TryBeginExclusiveAuthoredPlacement(
record,
acceptedVersion,
route.OperationKind);
if (!token.IsValid)
{
// The entity already owns an active operation (or an
// unacknowledged completion). Not staleness — see the Contention
// status doc.
return RuntimeAcceptedPositionExecutionStatus.Contention;
}
return SubmitAndResolve(record, token, route);
}
/// <summary>
/// Host cadence pump: resolves a parked DeferredCell operation once its
/// destination landblock's collision generation eventually commits it
/// (<c>RuntimeSetPositionState.CommitCollisionGeneration</c> →
/// <c>RetryDeferred</c> → <c>CommitCanonical</c>, driven entirely by
/// unrelated collision/streaming machinery — this pump never re-submits
/// the operation itself). Retries a preparation-only retry status
/// (<c>RetrySetupUnavailable</c>/<c>RetryWorldFrameUnavailable</c>) by
/// re-calling the SAME prepare+submit pair, exactly like
/// <see cref="RuntimeFirstEntryDriveController"/>'s own continuation
/// completion. Safe to call from any host cadence point; a no-op when
/// nothing is pending.
///
/// R1 review fix (2026-08-03): <c>RuntimeEntityObjectLifetime.TryApplyPosition</c>
/// calls <c>RuntimeSetPositionState.Forget</c> on EVERY accepted Position
/// for this entity — any disposition, not only ForcePosition — which
/// unconditionally cancels whatever operation this controller has
/// in-flight (<c>Forget</c> → <c>CancelCoreDeferred</c> →
/// <c>ForgetPlacementCompletionCore</c>, which drops the token from both
/// <c>_placementCompletionWatches</c> and
/// <c>_acknowledgedPlacementCompletions</c> with no trace). ACE broadcasts
/// at 5-10 Hz, so a <see cref="RuntimeAcceptedPositionExecutionStatus.DeferredCell"/>
/// park surviving past one broadcast interval is cancelled before its
/// collision generation can ever commit it — the exact far-destination
/// case the park exists to serve. The SAME cancellation path is also how
/// supersession, the lost-cell deadline, <c>ParkCollisionResidents</c>,
/// and a generation change retire an operation
/// (<c>ForgetPlacementCompletionCore</c> is their common funnel too).
///
/// Retail <c>SmartBox::BlipPlayer</c> (@0x00453940) has no "give up
/// quietly" state — every accepted Position it sees gets applied. So
/// rather than silently leaking <see cref="_pending"/> forever (which
/// would also pin <c>AcceptedPositionDrivePendingCount</c> non-zero for
/// the rest of the session — <c>GameWindowLifetime.DisposeGameRuntime</c>
/// throws on non-convergence), this pump detects the cancellation via the
/// existing read-only <c>RuntimeSetPositionState.IsPlacementCompletionTracked</c>
/// query (a watched-and-not-yet-cancelled DeferredCell park) or
/// <c>IsPlacementCurrent</c> (a still-in-flight prepare retry) and, when
/// neither holds, hands the dead operation to the single
/// <see cref="SettlePending"/> funnel, which decides on ONE input whether
/// a re-issue is owed.
/// </summary>
internal void Advance()
{
if (_pending is not { } pending)
return;
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
if (pending.AwaitingCommitWake)
{
if (setPosition.TryPeekAcknowledgedPlacement(
pending.Token,
out RuntimePlacementProjectionToken projection))
{
if (!setPosition.ConsumeAcknowledgedPlacement(
pending.Token, projection))
{
// Raced against a concurrent consumer; retry next pump.
return;
}
// Retail order: the deferred commit's own reconciliation and
// ack come first, THEN the funnel decides whether a newer
// accepted force is still owed a placement (B1).
ReconcileAndAcknowledge(pending.Record, pending.Route);
SettlePending(
pending.Record,
pending.Token,
pending.Route,
positionEventOwed: false);
return;
}
if (setPosition.IsPlacementCompletionTracked(pending.Token))
{
// Still parked, watch alive. Nothing more this pump can do.
return;
}
// The watch died — most likely a subsequent accepted Position's
// merge-time Forget. The funnel owns what happens next, including
// this packet's still-unsent position event (retail acks whether
// or not the placement took — see SettlePending).
SettlePending(
pending.Record,
pending.Token,
pending.Route,
pending.PositionEventOwed);
return;
}
if (setPosition.IsPlacementCurrent(pending.Token))
{
_ = SubmitAndResolve(pending.Record, pending.Token, pending.Route);
return;
}
// The prepare-retry operation died the same way. (A re-issue retry
// marker also lands here, carrying PositionEventOwed: false — its
// packet's placement was never begun, so its ack belongs to the
// eventual re-issue's terminal outcome.)
SettlePending(
pending.Record,
pending.Token,
pending.Route,
pending.PositionEventOwed);
}
/// <summary>
/// Round 2 unified mechanism (2026-08-03) — the SINGLE terminal-outcome
/// funnel for <see cref="_pending"/>. Every branch that ends an operation
/// (committed, rejected, cancelled, watch died, key released) calls this
/// and nothing else touches the field. One decision input: the terminal
/// operation's own <c>PositionAuthorityVersion</c> (carried on its
/// <see cref="RuntimeEntityPlacementToken"/>,
/// <c>RuntimeSetPositionState.cs:50</c>) compared against the live
/// canonical record's CURRENT <c>PositionAuthorityVersion</c>:
///
/// <list type="bullet">
/// <item><description><b>Equal</b> — the canonical accepted authority has
/// not moved since this operation began, so nothing is outstanding. Clear
/// and do not re-issue. This is what makes one server correction produce
/// exactly ONE canonical placement and ONE outbound
/// <c>AutonomousPosition</c> even when a stale dead entry was still parked
/// in <see cref="_pending"/> when the fresh packet committed (N1 — the
/// double-apply/double-ack class <c>670f307c</c> deleted).</description></item>
/// <item><description><b>Advanced, newest accepted event still a
/// ForcePosition</b> — a newer force arrived while we were in flight and
/// could not begin (see the Contention status doc). Re-issue it,
/// re-classified from the CURRENT record via <see cref="_newestForce"/> —
/// never the terminal operation's own route (B1).</description></item>
/// <item><description><b>Advanced, newest accepted event is an ordinary
/// Apply</b> — clear and do not re-issue. The correction was superseded by
/// newer server truth and the ordinary route owns that pose. Re-issuing
/// here would apply the force route's <c>Teleport|Slide</c> flags to an
/// ordinary pose, send an ack retail never sends on that branch, and skip
/// the <c>ConstrainTo</c> the ordinary branch runs
/// (<c>RuntimeAuthoritativePositionRouteClassifier.cs:368-388</c>) — N2.
/// This is not a silent drop: retail applies each event as it arrives, and
/// a force overtaken by a newer position is moot.</description></item>
/// </list>
///
/// Recursion is bounded at one level: a re-issue always begins at the
/// record's CURRENT version, so its own terminal settle necessarily takes
/// the Equal branch (nothing can advance the authority between a begin and
/// its synchronous submit — only an inbound merge does, and inbound
/// dispatch is what called us).
///
/// <para><b>Round 3 (2026-08-03) — the terminal-without-commit ack.</b>
/// Retail acknowledges an accepted force packet whether or not its
/// placement took. <c>SmartBox::BlipPlayer</c> @0x00453940 calls
/// <c>CPhysicsObj::SetPositionSimple</c> @0x005162B0 — which returns an
/// <c>enum SetPositionError</c> that other retail call sites DO test
/// (<c>== OK_SPE</c> @0x0055605D, @0x00556021) — and DISCARDS it;
/// <c>BlipPlayer</c> itself returns <c>void</c>. Its caller
/// <c>SmartBox::HandleReceivedPosition</c> @0x00453FD0 then runs
/// <c>cmdinterp-&gt;SendPositionEvent()</c> @0x00454091 unconditionally and
/// returns @0x0045409D. So retail's semantics are: attempt the placement;
/// if it fails the body simply does not move; acknowledge regardless;
/// never retry. This funnel therefore sends the position event for any
/// terminal outcome whose placement was begun and did NOT commit
/// (<paramref name="positionEventOwed"/>), carrying the body's UNCHANGED
/// pose — which is exactly what retail's ack carries after a failed
/// <c>SetPositionSimple</c>, and is informative to the server: its force
/// did not take. It does NOT re-issue that correction, because retail
/// never retries. The commit paths pass <c>false</c>: their ack already
/// left through <see cref="ReconcileAndAcknowledge"/>, so exactly one
/// position event goes out per begun placement THAT REACHES ITS OWN
/// TERMINAL SETTLE — never two.</para>
///
/// <para>It is deliberately NOT "never zero per begun placement". This
/// method opens by nulling <c>_pending</c> without reading it, so a
/// descriptor still carrying <c>PositionEventOwed</c> is discarded when a
/// NEWER ForcePosition displaces it (the merge-time
/// <c>Forget</c> clears the block, the newer packet begins cleanly, and
/// its terminal settle nulls the field). That older packet's owed ack is
/// lost — AD-62 shape (v), pinned by
/// <c>OneServerCorrectionProducesExactlyOnePlacementAndOneAck</c>, which
/// feeds two force packets and asserts a single ack. Replaying it would be
/// worse: the message would carry a stale sequence against the newer
/// packet's committed pose. The displacing packet always acks, so ACE
/// always receives a report for the NEWEST force.</para>
///
/// <para>Divergence: the two non-reissuing branches mean a ForcePosition
/// retired without committing is never re-applied —
/// <c>docs/architecture/retail-divergence-register.md</c> row <b>AD-62</b>
/// (the park itself is our async collision-publication adaptation; retail
/// <c>SmartBox::BlipPlayer</c> @0x00453940 is synchronous against a fully
/// resident world and cannot reach this state). The ack is no longer part
/// of that loss for a begun placement; AD-62 names the narrower shapes
/// where the packet's placement was never begun at all and the ack is
/// still lost.</para>
/// </summary>
private void SettlePending(
RuntimeEntityRecord terminalRecord,
in RuntimeEntityPlacementToken terminalToken,
in RuntimeAuthoritativePositionRoute terminalRoute,
bool positionEventOwed)
{
_pending = null;
if (!_entityObjects.Entities.TryGetActive(
terminalRecord.ServerGuid, out RuntimeEntityRecord record)
|| record.ServerGuid != _localPlayerServerGuid()
|| record.PhysicsBody is null
|| record.Key is not { } key
|| key != terminalToken.Entity)
{
// The entity departed the world, is no longer the local player,
// has no canonical body, or released/replaced the incarnation this
// operation belonged to (the post-teardown key release). Nothing
// left in the world for this operation to place — and nothing left
// to acknowledge a position for either.
return;
}
// Retail order: the packet's own position event first (@0x00454091),
// THEN whatever the next accepted force is owed. No reconciliation
// runs here — the body did not move, so there is no committed frame to
// reconcile; only the ack is owed.
if (positionEventOwed && _localController() is { } terminalController)
SendPositionEvent(terminalController, terminalRoute);
ulong current = record.PositionAuthorityVersion;
if (terminalToken.PositionAuthorityVersion == current)
return;
if (_newestForce is not { } newest
|| newest.Entity != key
|| newest.PositionAuthorityVersion != current)
{
return;
}
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
RuntimeEntityPlacementToken token =
setPosition.TryBeginExclusiveAuthoredPlacement(
record,
current,
newest.Route.OperationKind);
if (!token.IsValid)
{
// Could not even begin the re-issue (a concurrent placement
// authority still owns the entity). The correction is still owed,
// so keep the terminal descriptor as the retry marker: the next
// Advance() pump re-enters this funnel and re-evaluates the SAME
// decision against the record as it then stands.
//
// PositionEventOwed: false — the marker stands for the NEWEST
// accepted force, whose placement was never begun. Its ack belongs
// to that re-issue's own terminal outcome, so re-entering this
// funnel through the marker must never fire a second position
// event for a packet the re-issue will ack itself.
_pending = new Pending
{
Record = terminalRecord,
Token = terminalToken,
Route = terminalRoute,
AwaitingCommitWake = false,
PositionEventOwed = false,
};
return;
}
_ = SubmitAndResolve(record, token, newest.Route);
}
private RuntimeAcceptedPositionExecutionStatus SubmitAndResolve(
RuntimeEntityRecord record,
in RuntimeEntityPlacementToken token,
in RuntimeAuthoritativePositionRoute route)
{
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
RuntimeSetPositionMoverPreparationStatus status =
setPosition.TryPrepareAndSubmitAuthoredPlacement(
record,
token,
route.OperationKind,
route.SetPositionFlags,
_collisionSource,
_clock.SimulationTimeSeconds,
out RuntimeSetPositionOutcome outcome,
resolveWorldOffsetFromRuntimeFrame: true);
if (status != RuntimeSetPositionMoverPreparationStatus.Prepared)
{
if (status.IsRetryable())
{
RetainPending(setPosition, new Pending
{
Record = record,
Token = token,
Route = route,
AwaitingCommitWake = false,
PositionEventOwed = true,
});
return RuntimeAcceptedPositionExecutionStatus.Contention;
}
CancelToken(setPosition, token);
SettlePending(record, token, route, positionEventOwed: true);
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
switch (outcome.Status)
{
case RuntimeSetPositionStatus.CommittedHostAcknowledgementPending:
// R4 review fix (2026-08-03): unlike
// RuntimeFirstEntryDriveController.TryCompleteContinuationPlacement
// (whose ack here is benign because ITS residence is what
// just drained, so the sink's residence gate can no longer
// decline), this route's sink can legitimately decline this
// exact receipt for real production reasons unrelated to
// anything this route does (!IsLoaded(landblock), stale
// transit authority — RuntimePlacementProjectionSubscription
// deliberately leaves a declined Place at the FIFO head for
// its OWN later retry). Acknowledging it here would consume
// and destroy that retry with no second writer to cover the
// render-facing projection (the generic App tail is skipped
// for this route). So this route does NOT acknowledge the
// projection itself — the production subscription already
// did, synchronously, inside the SetPosition call above, if
// it was going to; if it declined, its own retry contract
// owns the receipt from here, exactly like every other
// placement kind.
ReconcileAndAcknowledge(record, route);
SettlePending(record, token, route, positionEventOwed: false);
return RuntimeAcceptedPositionExecutionStatus.Committed;
case RuntimeSetPositionStatus.DeferredCell:
// Parked with a published Withdraw; consume it if it is
// already the head so the collision-generation wake can
// resubmit (RuntimeFirstEntryDriveController
// .TryCompleteContinuationPlacement:351-365's exact pattern).
while (setPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot parked)
&& parked.Token.Entity == token.Entity
&& parked.Kind is RuntimePlacementProjectionKind.Withdraw)
{
if (!setPosition.AcknowledgeProjection(parked.Token))
break;
}
if (!setPosition.WatchPlacementCompletion(token))
{
// Something displaced the operation between Submit and
// here — no cross-pump tracking is possible; cancel
// rather than leak a watch we can never resolve.
CancelToken(setPosition, token);
SettlePending(record, token, route, positionEventOwed: true);
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
RetainPending(setPosition, new Pending
{
Record = record,
Token = token,
Route = route,
AwaitingCommitWake = true,
PositionEventOwed = true,
});
return RuntimeAcceptedPositionExecutionStatus.DeferredCell;
default:
// Rejected/Cancelled — authority moved out from under this
// operation, so the body never moved. Retail still
// acknowledges the packet (SettlePending's positionEventOwed
// path); it just does not re-apply it.
CancelToken(setPosition, token);
SettlePending(record, token, route, positionEventOwed: true);
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
}
/// <summary>
/// R6 review fix (2026-08-03): the previous shape assigned
/// <see cref="_pending"/> unconditionally, which could silently
/// overwrite a still-tracked live pending (losing the only reference
/// able to later consume its eventual acknowledged completion — a
/// permanent <c>HasRetainedCompletion</c> orphan that would block every
/// future Begin for this entity with Contention forever). Given
/// <see cref="RuntimeSetPositionState.TryBeginExclusiveAuthoredPlacement"/>
/// already refuses to Begin while the entity holds an active operation
/// or an unconsumed acknowledged completion, <paramref name="next"/>'s
/// token could only have successfully begun if any DIFFERENT existing
/// <see cref="_pending"/> is already dead — so this is a defensive
/// invariant check, not a routine code path.
///
/// Round 2 (2026-08-03): this is the ONLY "the operation is still
/// outstanding" writer of <see cref="_pending"/>; every terminal outcome
/// goes through <see cref="SettlePending"/> instead.
/// </summary>
private void RetainPending(
RuntimeSetPositionState setPosition,
Pending next)
{
if (_pending is { } existing
&& existing.Token != next.Token
&& setPosition.IsPlacementCurrent(existing.Token))
{
throw new InvalidOperationException(
"RuntimeAcceptedPositionDriveController tracks at most one "
+ "pending accepted-position operation (the local player); "
+ "a still-live pending operation must never be silently "
+ "overwritten by a new one.");
}
_pending = next;
}
private static void CancelToken(
RuntimeSetPositionState setPosition,
in RuntimeEntityPlacementToken token)
{
RuntimePlacementCancellationReceipt cancellation =
setPosition.ForgetExactPlacement(token);
if (cancellation.IsValid)
setPosition.PublishCancellation(cancellation);
}
/// <summary>
/// §4a/§4b: the controller-local reconciliation
/// <c>CommitCanonical</c> does not perform, followed by the outbound ack
/// — an OUTPUT of the committed route, never a step performed alongside
/// it (retail <c>cmdinterp-&gt;SendPositionEvent()</c> @0x00454091 runs
/// after <c>SmartBox::BlipPlayer</c> @0x00454074 returns).
/// </summary>
private void ReconcileAndAcknowledge(
RuntimeEntityRecord record,
in RuntimeAuthoritativePositionRoute route)
{
if (record.ServerGuid != _localPlayerServerGuid())
return;
if (_localController() is not { } controller)
return;
controller.CommitCanonicalForcePositionFrame();
SendPositionEvent(controller, route);
}
/// <summary>
/// Retail <c>cmdinterp-&gt;SendPositionEvent()</c> @0x00454091 — the sole
/// outbound-ack site for this route, shared by the committed path
/// (<see cref="ReconcileAndAcknowledge"/>, which reconciles the moved
/// frame first) and the terminal-without-commit path
/// (<see cref="SettlePending"/>, which has no moved frame to reconcile and
/// so sends the body's unchanged pose).
///
/// The <c>CanSendPositionEvent</c> admission inside
/// <see cref="LocalPlayerOutboundController.SendImmediatePosition"/> is
/// retail's own (<c>CommandInterpreter::SendPositionEvent</c> @0x006B4770
/// tests the transient-state contact bits), so a legitimately airborne
/// body still suppresses the send on BOTH paths — that suppression is
/// retail behaviour, not a divergence.
/// </summary>
private void SendPositionEvent(
PlayerMovementController controller,
in RuntimeAuthoritativePositionRoute route)
{
if (!route.SendPositionImmediately)
return;
_localPlayerOutbound.SendImmediatePosition(_session(), controller);
}
private RuntimeAuthoritativePositionRoute ClassifyForcePosition(
RuntimeEntityRecord record,
RuntimeEntityKey key,
in WorldSession.EntityPositionUpdate update,
PositionTimestampDisposition disposition,
in AcceptedPhysicsTimestamps timestamps,
ushort previousTeleportSequence)
{
var authority = new RuntimeAuthoritativePositionAuthority(
_generation(),
key,
record.PositionAuthorityVersion,
update.PositionSequence,
previousTeleportSequence,
timestamps.Teleport,
disposition);
// Round 3 A3 (mirrored from RuntimeInitialCreateContinuationExecutor
// .ApplyPositionAction): contact is SOLELY the retained wire
// packet's own IsGrounded bit — never a live body query.
bool hasContact = update.IsGrounded;
bool hasAnimations = (record.Snapshot.MotionTableId
?? record.Snapshot.Physics?.MotionTableId) is { } motionTableId
&& motionTableId != 0u;
bool usePositionFromServer = _usePositionFromServer();
float playerDistance = 0f;
if (_localController() is { } controllerForDistance
&& (record.Snapshot.Physics?.Position
?? record.Snapshot.Position) is { } acceptedForDistance)
{
var target = new Vector3(
acceptedForDistance.PositionX,
acceptedForDistance.PositionY,
acceptedForDistance.PositionZ);
playerDistance = Vector3.Distance(
target, controllerForDistance.Position);
}
var request = new RuntimeAcceptedPositionRouteRequest(
authority,
RuntimePositionEntityKind.LocalPlayer,
RuntimeAcceptedPositionSource.PositionEvent,
update.Position,
update.PlacementId,
update.Velocity,
record.FullCellId,
hasContact,
playerDistance,
usePositionFromServer,
hasAnimations,
new RuntimePositionPlacementFacts(
record.FinalPhysicsState,
record.Snapshot.SetupTableId is not null));
return RuntimeAuthoritativePositionRouteClassifier
.ClassifyAcceptedPosition(request);
}
}

View file

@ -19,6 +19,24 @@ public interface IRuntimeDirectWorldProjection
bool isLocalPlayer,
PositionTimestampDisposition disposition);
/// <summary>
/// R2 review fix (2026-08-03): a ForcePosition on the local player is
/// dispatched directly to <see cref="RuntimeAcceptedPositionDriveController"/>
/// and never reaches <see cref="ProjectPosition"/> at all, so THIS is
/// where a host that keeps a narrow collision/streaming window (the
/// deleted <c>HeadlessSessionWorldProjection.BlipLocalPlayer</c>'s own
/// <c>_collision.CenterOn</c> call) re-centers on the destination BEFORE
/// the drive controller submits — establishing that the destination's
/// collision generation is one this host's window can ever publish is a
/// precondition for a <c>DeferredCell</c> park to be a real park rather
/// than a dead end (see <c>RuntimeAcceptedPositionDriveController.Advance</c>'s
/// R1 doc comment). A host with no narrow window (the graphical host,
/// whose landblock streaming already follows the accepted position via
/// <c>LiveEntityInboundAuthorityGate.ObserveAcceptedLocalPosition</c>) is
/// a no-op here.
/// </summary>
void CenterOnAcceptedForcePosition(RuntimeEntityRecord record);
void BeginTeleport();
RuntimeDestinationReadiness PrepareDestination(
@ -38,20 +56,28 @@ public sealed class RuntimeLiveEntitySessionController
private readonly WorldSession _session;
private readonly Action<string> _log;
private readonly IRuntimeDirectWorldProjection? _worldProjection;
private readonly LocalPlayerOutboundController _localPlayerOutbound =
new((_, _, _, _, _, _) => { });
/// <summary>
/// C4 route 2 (2026-08-03): the headless accepted-Position drive
/// controller. Owns its own outbound-ack collaborator internally; the
/// ForcePosition + manual <c>LocalPlayerOutboundController.SendImmediatePosition</c>
/// pair this class used to drive directly is retired (the deleted
/// <c>HeadlessSessionWorldProjection.BlipLocalPlayer</c>).
/// </summary>
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
private bool _initialLoginCompleteSent;
public RuntimeLiveEntitySessionController(
GameRuntime runtime,
WorldSession session,
Action<string>? log = null,
IRuntimeDirectWorldProjection? worldProjection = null)
IRuntimeDirectWorldProjection? worldProjection = null,
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_session = session ?? throw new ArgumentNullException(nameof(session));
_log = log ?? (_ => { });
_worldProjection = worldProjection;
_acceptedPositionDrive = acceptedPositionDrive;
}
public LiveEntitySessionSink CreateSink() => new(
@ -218,16 +244,55 @@ public sealed class RuntimeLiveEntitySessionController
update.Guid,
out RuntimeEntityRecord record))
{
_worldProjection?.ProjectPosition(
record,
isLocalPlayer: true,
disposition);
}
if (disposition is PositionTimestampDisposition.ForcePosition)
{
_localPlayerOutbound.SendImmediatePosition(
_session,
_runtime.MovementOwner.Controller);
if (disposition is PositionTimestampDisposition.ForcePosition)
{
// R2 review fix (2026-08-03): re-center BEFORE submitting —
// see IRuntimeDirectWorldProjection.CenterOnAcceptedForcePosition's
// doc comment. This is what the deleted BlipLocalPlayer's own
// _collision.CenterOn call used to guarantee.
_worldProjection?.CenterOnAcceptedForcePosition(record);
// C4 route 2 (2026-08-03): the Runtime-owned accepted-
// Position execution seam replaces the deleted
// HeadlessSessionWorldProjection.BlipLocalPlayer + manual
// SendImmediatePosition pair. Canonical commit, controller
// reconciliation, and the outbound ack (an OUTPUT of the
// committed route, not a step alongside it) all run inside
// the call below.
RuntimeAcceptedPositionExecutionStatus forceStatus =
_acceptedPositionDrive?.TryExecuteAcceptedLocalPosition(
record,
update,
disposition,
timestamps,
timestamps.PreviousTeleport)
?? RuntimeAcceptedPositionExecutionStatus.NotApplicable;
if (forceStatus is RuntimeAcceptedPositionExecutionStatus
.NotApplicable)
{
// R3 review fix (2026-08-03): NotApplicable (e.g. an
// initial-Create residence still owns this record —
// route 1's job, or the login-window controller-null
// branch before route 1 has even published a
// controller) is NOT the terminal case the previous
// comment here claimed. ProjectPosition's own
// controller-null branch is the pre-existing legacy
// fallback this disposition always had — it must still
// run, exactly as every other disposition's fallback
// does below.
_worldProjection?.ProjectPosition(
record,
isLocalPlayer: true,
disposition);
}
}
else
{
_worldProjection?.ProjectPosition(
record,
isLocalPlayer: true,
disposition);
}
}
TryCompletePortal();
}

View file

@ -1,3 +1,4 @@
using System.Text.RegularExpressions;
using AcDream.App.Physics;
namespace AcDream.App.Tests.Physics;
@ -46,51 +47,97 @@ public sealed class LiveEntityNetworkBranchRoutingTests
Assert.Equal(["projectile", "canonical", "ordinary"], calls);
}
[Fact]
public void ForcePosition_BlipsAndAcknowledgesExactlyOnce()
// C4 route 2 (2026-08-03): LocalForcePositionTransaction and its
// ForcePosition_* coverage here are RETIRED, not adapted — the class is
// deleted outright (docs/research/2026-08-03-c4-route-2-contract.md
// §"The contract" item 2). Its three jobs (ownership validation, the
// blip/commit, and the exactly-once ack including the displaced-
// authority case its trailing isCurrent() covered) are now properties of
// the Runtime-owned RuntimeAcceptedPositionDriveController and are tested
// there: tests/AcDream.Runtime.Tests/Session/RuntimeAcceptedPositionDriveControllerTests.cs.
/// <summary>
/// R8 review fix (2026-08-03): source pins for the generic-tail
/// double-write guard in <c>LiveEntityNetworkUpdateController.OnPosition</c>.
/// A full behavioral fixture is impractical here for the SAME reason
/// <c>C3cF1ProductionWiringTests</c> gives — the controller's dependency
/// set is composition-only (67+ collaborators wired only by
/// <c>SessionPlayerComposition</c>) — so this follows that file's exact
/// established pattern: assert the STRUCTURE of the production source
/// rather than construct the class. The Runtime-level behavioral
/// coverage for the seam itself lives in
/// RuntimeAcceptedPositionDriveControllerTests; these pins are what stop
/// this App-layer call site from silently reintroducing the retired
/// duplicate-write authority (the deleted LocalForcePositionTransaction
/// pair + the generic render-tail writing the SAME accepted Position a
/// second time — 670f307c's divergence class).
/// </summary>
public sealed class LiveEntityNetworkUpdateControllerForcePositionWiringTests
{
int currentChecks = 0;
int blips = 0;
int acknowledgements = 0;
[Fact]
public void LocalForcePositionTransactionIsNeverCalledFromThisFile()
{
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
bool completed = LocalForcePositionTransaction.Apply(
isForcePosition: true,
() => { currentChecks++; return true; },
() => blips++,
() => acknowledgements++);
// The name may still appear in a comment explaining what
// replaced it (contract §"the deleted LocalForcePositionTransaction");
// what must be gone is any actual call into it.
Assert.DoesNotContain(
"LocalForcePositionTransaction.Apply(",
source,
StringComparison.Ordinal);
}
Assert.True(completed);
Assert.Equal(2, currentChecks);
Assert.Equal(1, blips);
Assert.Equal(1, acknowledgements);
}
[Fact]
public void GenericTailWriteIsNeverDuplicatedForTheLocalForcePositionPath()
{
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
[Fact]
public void ForcePosition_AcknowledgementInvalidationStopsTheTail()
{
bool current = true;
int acknowledgements = 0;
// The generic render-tail's WorldEntity write is the ONE
// remaining writer of an accepted Position — it must serve
// remotes only, never a second local-player write alongside the
// Runtime-committed one.
Assert.Single(
Regex.Matches(source, @"entity\.SetPosition\(worldPos\);")
.Cast<Match>());
}
bool completed = LocalForcePositionTransaction.Apply(
isForcePosition: true,
() => current,
() => { },
() => { acknowledgements++; current = false; });
[Fact]
public void CommittedOrDeferredCellReturnsBeforeReachingTheGenericTail()
{
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
Assert.False(completed);
Assert.Equal(1, acknowledgements);
}
// The Committed/DeferredCell branch must still return
// immediately after its two preserved side effects — a missing
// `return` here would fall through into the generic tail below
// and resurrect the double-write.
Assert.Matches(
new Regex(
@"ObserveAcceptedLocalPosition\(\s*"
+ @"update\.Position\.LandblockId\);\s*return;",
RegexOptions.Singleline),
source);
}
[Fact]
public void OrdinaryPositionDoesNotBlipOrAcknowledge()
{
int calls = 0;
private static string ReadSource(string fileName)
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
{
return File.ReadAllText(Path.Combine(
directory.FullName,
"src",
"AcDream.App",
"Physics",
fileName));
}
Assert.True(LocalForcePositionTransaction.Apply(
isForcePosition: false,
() => { calls++; return false; },
() => calls++,
() => calls++));
Assert.Equal(0, calls);
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
}
}

View file

@ -393,16 +393,56 @@ public sealed class HeadlessSessionHostTests
acknowledgeProjection: null,
out PositionTimestampDisposition disposition,
out _,
out _));
out AcceptedPhysicsTimestamps timestamps));
Assert.Equal(
PositionTimestampDisposition.ForcePosition,
disposition);
projection.ProjectPosition(
record,
isLocalPlayer: true,
disposition);
Assert.Equal(new Vector3(72f, 73f, 50f), controller.Position);
// C4 route 2 (2026-08-03): a ForcePosition on the local player no
// longer routes through HeadlessSessionWorldProjection.ProjectPosition
// at all — RuntimeLiveEntitySessionController.OnPositionUpdated
// dispatches it directly to RuntimeAcceptedPositionDriveController
// instead (the deleted BlipLocalPlayer's replacement), but R2 review
// fix (2026-08-03): it re-centers the collision neighborhood on the
// destination FIRST — the deleted BlipLocalPlayer's own CenterOn
// side effect, restored via CenterOnAcceptedForcePosition, because a
// DeferredCell park this neighborhood's window can never publish is
// a dead end, not a real park
// (RuntimeAcceptedPositionDriveController.Advance's R1 doc comment).
projection.CenterOnAcceptedForcePosition(record);
RuntimeAcceptedPositionDriveController acceptedPositionDrive =
CreateAcceptedPositionDrive(runtime);
RuntimeAcceptedPositionExecutionStatus forceStatus =
acceptedPositionDrive.TryExecuteAcceptedLocalPosition(
record,
force,
disposition,
timestamps,
timestamps.PreviousTeleport);
Assert.Equal(
RuntimeAcceptedPositionExecutionStatus.Committed,
forceStatus);
// R7 review fix (2026-08-03): Z is 50.005f — within 5 mm of the
// wire's bare 50f — NOT 50.48f. The dat-exact human Setup's foot
// sphere is (0,0,0.475) r=.48 (LoadedSetupCollisionSource above);
// its bottom sits at origin + 0.475 0.48 = origin 0.005, so a
// settled origin lands 0.005 m ABOVE the floor it rests on (measured
// empirically against this exact fixture), not a full sphere RADIUS
// above it. Retail's BlipPlayer (CPhysicsObj::SetPositionSimple
// @0x005162B0, called from SmartBox::BlipPlayer @0x00453940) has
// never lifted the origin by a sphere radius — the C4-route-2 FIRST
// implementation pass (uncommitted; this file asserts a bare 50f at
// HEAD, never 50.48f) had fitted a 50.48f assertion to a dummy
// fixture sphere whose offset happened to equal its own radius, not
// to retail behavior. The comment it carried described the sphere's
// CENTRE, then wrongly asserted that description about
// controller.Position, which is the body's ORIGIN
// (PlayerMovementController.cs -> PhysicsBody.cs Position), not the
// sphere centre.
Assert.Equal(new Vector3(72f, 73f, 50.005f), controller.Position);
// CenterCount is 2: ProjectSpawn's initial centering plus the
// ForcePosition's own re-centering above (R2's restored mechanism).
Assert.Equal(2, collision.CenterCount);
}
@ -1296,6 +1336,25 @@ public sealed class HeadlessSessionHostTests
Height: 1.835f,
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
/// <summary>
/// C4 route 2 (2026-08-03): mirrors <see cref="CreateFirstEntryDrive"/>'s
/// construction pattern for the accepted-Position drive controller. No
/// real WorldSession is needed for these fixture tests —
/// LocalPlayerOutboundController.SendImmediatePosition no-ops on a null
/// session.
/// </summary>
private static RuntimeAcceptedPositionDriveController
CreateAcceptedPositionDrive(GameRuntime runtime) => new(
runtime.EntityObjects,
runtime.Clock,
new LoadedSetupCollisionSource(),
new LocalPlayerOutboundController((_, _, _, _, _, _) => { }),
() => runtime.Generation,
() => runtime.PlayerIdentity.ServerGuid,
() => runtime.MovementOwner.Controller,
() => runtime.CharacterOwner.UsePositionFromServer,
() => null);
private sealed class LoadedSetupCollisionSource
: AcDream.Content.IPreparedCollisionSource
{
@ -1312,7 +1371,23 @@ public sealed class HeadlessSessionHostTests
.Loaded(new FlatSetupCollision(
System.Collections.Immutable.ImmutableArray<
FlatCollisionCylinder>.Empty,
[new FlatCollisionSphere(Vector3.Zero, 0.48f)],
// R7 review fix (2026-08-03): the dat-exact human Setup
// 0x02000001 spheres (Ts46SphereListConformanceTests.cs
// :35-39) — foot (0,0,0.475) r=.48, head/torso
// (0,0,1.350) r=.48. The PREVIOUS single dummy sphere at
// (0,0,0) r=.48 (offset == radius) made a settled origin
// rest a FULL radius above the floor; the real foot
// sphere's bottom is origin + 0.475 0.48 = origin
// 0.005, so a settled origin lands ON the floor within
// 5 mm. Retail's BlipPlayer has never lifted the origin
// by a sphere radius — that was a fixture artifact, not
// a retail-fidelity gain (docs/ISSUES.md #285 correction).
[
new FlatCollisionSphere(
new Vector3(0f, 0f, 0.475f), 0.48f),
new FlatCollisionSphere(
new Vector3(0f, 0f, 1.350f), 0.48f),
],
height: 0f,
radius: 0f,
stepUpHeight: 0.4f,

View file

@ -569,7 +569,7 @@ public class PlayerMovementControllerTests
}
[Fact]
public void BlipPosition_ResnapsPoseWithoutStoppingActiveMotion()
public void CommitCanonicalForcePositionFrame_ReconcilesPoseWithoutStoppingActiveMotion()
{
var controller = new PlayerMovementController(MakeFlatEngine());
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
@ -579,7 +579,14 @@ public class PlayerMovementControllerTests
Assert.True(velocity.LengthSquared() > 0f);
var corrected = new Vector3(100f, 98f, 50f);
controller.BlipPosition(corrected, 0x0001, corrected);
// C4 route 2: simulates Runtime's canonical SetPosition commit
// (RuntimeSetPositionState.CommitCanonical:4459-4462), which snaps
// the SAME PhysicsBody directly BEFORE the controller reconciles
// its render-lerp/cell state. The deleted BlipPosition used to do
// both steps itself; CommitCanonicalForcePositionFrame only does the
// second.
controller.PhysicsBody.SnapToCell(0x0001, corrected, corrected);
controller.CommitCanonicalForcePositionFrame();
Assert.Equal(corrected, controller.Position);
Assert.Equal(corrected, controller.RenderPosition);
@ -587,13 +594,14 @@ public class PlayerMovementControllerTests
}
[Fact]
public void BlipPosition_PublishesCanonicalOutdoorCellAndLocalFrame()
public void CommitCanonicalForcePositionFrame_PublishesCanonicalOutdoorCellAndLocalFrame()
{
var controller = new PlayerMovementController(MakeFlatEngine());
var world = new Vector3(150f, 193f, 50f);
var wireLocal = new Vector3(150f, 193f, 50f);
controller.BlipPosition(world, 0xA9B30038u, wireLocal);
controller.PhysicsBody.SnapToCell(0xA9B30038u, world, wireLocal);
controller.CommitCanonicalForcePositionFrame();
Assert.Equal(0xA9B40031u, controller.CellId);
Assert.Equal(controller.CellId, controller.CellPosition.ObjCellId);
@ -959,27 +967,35 @@ public class PlayerMovementControllerTests
}
[Fact]
public void BlipPosition_ArmsConstraintButDoesNotTearDownOrZeroVelocity()
public void CommitCanonicalForcePositionFrame_DoesNotRearmConstraintLeashOrTouchVelocity()
{
var (controller, _) = MakeControllerWithHost();
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
var initial = new Vector3(96f, 96f, 50f);
controller.SetPosition(initial, 0x0001);
controller.Update(ObjectTick, new MovementInput(Forward: true));
Vector3 velocityBeforeBlip = controller.BodyVelocity;
Assert.NotEqual(Vector3.Zero, velocityBeforeBlip); // sanity: actually moving
controller.BlipPosition(
new Vector3(150f, 150f, 50f),
0x0001,
new Vector3(150f, 150f, 50f));
// BlipPlayer (retail 0x00453940) survives motion/velocity/stick — the
// leash is no different: ConstrainTo runs with NO preceding UnConstrain
// and no StopCompletely.
Assert.Equal(velocityBeforeBlip, controller.BodyVelocity);
Vector3 velocityBeforeCommit = controller.BodyVelocity;
Assert.NotEqual(Vector3.Zero, velocityBeforeCommit); // sanity: actually moving
ConstraintManager cm = controller.PositionManager!.Constraint!;
Assert.True(cm.IsConstrained);
Assert.Equal(controller.Position, cm.ConstraintPos.Frame.Origin);
Assert.Equal(0f, cm.ConstraintPosOffset, 3);
Vector3 leashAnchorBeforeCommit = cm.ConstraintPos.Frame.Origin;
var corrected = new Vector3(150f, 150f, 50f);
// Simulates Runtime's canonical SetPosition commit writing the SAME
// PhysicsBody directly, exactly as CommitCanonicalForcePositionFrame
// expects to find it.
controller.PhysicsBody.SnapToCell(0x0001, corrected, corrected);
controller.CommitCanonicalForcePositionFrame();
// C4 route 2 (2026-08-03): retail's FORCE_POSITION branch of
// SmartBox::HandleReceivedPosition (0x00453FD0) returns at
// 0x0045409D, before every CPhysicsObj::ConstrainTo call
// (0x00454272/0x0045418A/0x004541EC) — the branch BlipPlayer runs
// on is not one of them. Unlike the deleted BlipPosition (which
// re-armed the leash to the corrected position — an unbacked
// deviation), this method must leave the leash anchored exactly
// where it already was. Motion/velocity are untouched either way.
Assert.Equal(velocityBeforeCommit, controller.BodyVelocity);
Assert.Equal(leashAnchorBeforeCommit, cm.ConstraintPos.Frame.Origin);
Assert.NotEqual(corrected, cm.ConstraintPos.Frame.Origin);
}
[Fact]
@ -1133,10 +1149,8 @@ public class PlayerMovementControllerTests
Vector3.One,
0xA9B40021u,
Vector3.One));
Assert.Throws<InvalidOperationException>(() => candidate.BlipPosition(
Vector3.One,
0xA9B40021u,
Vector3.One));
Assert.Throws<InvalidOperationException>(() =>
candidate.CommitCanonicalForcePositionFrame());
Assert.Throws<InvalidOperationException>(() =>
candidate.CaptureMovementResult(mouseLookEvent: false));
Assert.Throws<InvalidOperationException>(() =>

View file

@ -2975,10 +2975,8 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
Vector3.One,
Cell,
Vector3.One));
Assert.Throws<InvalidOperationException>(() => controller.BlipPosition(
Vector3.One,
Cell,
Vector3.One));
Assert.Throws<InvalidOperationException>(() =>
controller.CommitCanonicalForcePositionFrame());
Assert.Throws<InvalidOperationException>(() =>
controller.ApplyPhysicsState(PhysicsStateFlags.Frozen));
Assert.Throws<InvalidOperationException>(() => controller.Yaw = 1f);

View file

@ -246,6 +246,73 @@ public sealed class RuntimeLiveEntitySessionControllerTests
Assert.True(runtime.Portal.Snapshot.Completed);
}
/// <summary>
/// R2/R3 review fix (2026-08-03). No accepted-position drive is supplied
/// (mirrors <see cref="ContentLessDirectSink_KeepsPreFlipLegacyRegistration"/>'s
/// "content-less" shape, applied to route 2 specifically), so
/// <c>TryExecuteAcceptedLocalPosition</c> can never run and every
/// ForcePosition resolves <c>NotApplicable</c> at the call site. Proves
/// two things the first implementation pass got wrong: (R2)
/// <see cref="IRuntimeDirectWorldProjection.CenterOnAcceptedForcePosition"/>
/// still fires — a host is not left holding a stale collision/streaming
/// window just because the drive is momentarily absent or NotApplicable
/// — and (R3) the pre-existing <c>ProjectPosition</c> fallback still runs
/// for a NotApplicable result, exactly like it did before route 2 existed
/// (the "no legacy fallback to run instead" comment the review found at
/// this exact call site was false).
/// </summary>
[Fact]
public void ForcePositionWithoutAnAcceptedPositionDrive_StillCentersAndFallsBackToProjectPosition()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
const uint playerGuid = 0x50000005u;
runtime.PlayerIdentity.ServerGuid = playerGuid;
using var session = new WorldSession(
new IPEndPoint(IPAddress.Loopback, 9000),
new FixtureTransport());
session.GameActionCapture = _ => { };
var projection = new FixtureWorldProjection();
// Deliberately no acceptedPositionDrive argument — mirrors a
// content-less headless host, where the accepted-position drive is
// null and route 2 can never apply.
var controller = new RuntimeLiveEntitySessionController(
runtime,
session,
worldProjection: projection);
LiveEntitySessionSink sink = controller.CreateSink();
WorldSession.EntitySpawn spawn =
Spawn(playerGuid, incarnation: 1);
sink.Spawned(spawn);
Assert.Equal(1, projection.SpawnCount);
Assert.Equal(0, projection.CenterOnForceCount);
sink.PositionUpdated(new WorldSession.EntityPositionUpdate(
playerGuid,
spawn.Position!.Value with
{
PositionX = 30f,
},
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 0,
ForcePositionSequence: 1));
Assert.Equal(1, projection.CenterOnForceCount);
Assert.Equal(playerGuid, projection.LastCenteredRecord?.ServerGuid);
// The fallback ran: ProjectPosition observed this exact
// ForcePosition disposition, not just the earlier Apply-shaped spawn
// follow-up.
Assert.Equal(1, projection.PositionCount);
Assert.Equal(
PositionTimestampDisposition.ForcePosition,
projection.LastPositionDisposition);
}
/// <summary>
/// C3c-R1 review F6: the drive controller outlives its session routes,
/// so "session reset precedes a new route" is an asserted latch, not a
@ -674,6 +741,8 @@ public sealed class RuntimeLiveEntitySessionControllerTests
public int PositionCount { get; private set; }
public int TeleportStartCount { get; private set; }
public int PrepareCount { get; private set; }
public int CenterOnForceCount { get; private set; }
public RuntimeEntityRecord? LastCenteredRecord { get; private set; }
public bool LastSpawnWasLocal { get; private set; }
public bool LastPositionWasLocal { get; private set; }
public PositionTimestampDisposition LastPositionDisposition
@ -704,6 +773,12 @@ public sealed class RuntimeLiveEntitySessionControllerTests
LastPositionDisposition = disposition;
}
public void CenterOnAcceptedForcePosition(RuntimeEntityRecord record)
{
CenterOnForceCount++;
LastCenteredRecord = record;
}
public void BeginTeleport() => TeleportStartCount++;
public RuntimeDestinationReadiness PrepareDestination(