diff --git a/docs/research/2026-08-04-c4-route-4b-1-review-findings.md b/docs/research/2026-08-04-c4-route-4b-1-review-findings.md
new file mode 100644
index 00000000..aec7c1c3
--- /dev/null
+++ b/docs/research/2026-08-04-c4-route-4b-1-review-findings.md
@@ -0,0 +1,175 @@
+# C4 route 4b-1 — dual review FAIL, and a design correction (2026-08-04)
+
+Both mandated reviews returned **FAIL**. Nothing is committed. This supersedes
+the 4b-1 contract, **whose central premise was factually wrong.**
+
+## The premise error — mine
+
+The contract justified refuse-rather-than-park with: *"retail's world is fully
+resident, so 'arrived but not placeable' is unrepresentable there."*
+
+**False.** Retail represents it explicitly, with a working park:
+
+- `CPhysicsObj::SetPositionInternal` @0x00515BD0 — when `AdjustPosition` yields
+ no cell (@0x00515C1D): `prepare_to_leave_visibility` @0x00515CDA,
+ `store_position` @0x00515CE2 (**the destination pose IS committed**),
+ `CObjectMaint::GotoLostCell` @0x00515CF2, clear transient 0x80 @0x00515CF7,
+ return `OK_SPE` @0x00515D07.
+- `CObjectMaint::GotoLostCell` @0x00508210 appends the object to that cell's
+ lost list.
+- `CObjectMaint::InitObjCell` @0x00508260 drains the list on cell load and calls
+ `reenter_visibility` per object @0x00508296.
+- `MoveOrTeleport` discards the `SetPositionError` from both
+ `SetPosition` @0x00516420 and `SetPositionSimple` @0x005163D9 and returns 1
+ regardless, so `ConstrainTo` @0x00454272 is armed **even when the placement
+ failed**.
+
+Retail's reaction to an unplaceable remote: advance the pose, hide the object,
+register it lost, wake it on cell arrival, arm the leash anyway. Our refusal
+leaves the remote **visible at a stale pose**. For a remote that teleports into
+a non-resident landblock and then stops moving — ACE stops broadcasting for a
+stationary entity — "the next packet is the retry" never arrives and the
+divergence is permanent.
+
+## Refuse cannot be made complete — the second reason to abandon it
+
+The guard checks ONE landblock (`accepted.LandblockId`). Core defers on at least
+four independent conditions:
+
+1. **`PlacementTouchesPrefix` matches the entity's CURRENT cell**, not only the
+ destination (`RuntimeSetPositionState.cs:3668-3673`, consumed `:2916`). A
+ remote standing in a quiescing landblock, moving to a perfectly published
+ destination, parks. That is the contract's own producing sequence with
+ ordinary streaming churn behind it.
+2. **`ResultTouchesPrefix` matches every cell in `QueriedCellIds`**
+ (`:3675-3690`, consumed `:2981`) — the sweep footprint spans neighbouring
+ landblocks near a boundary, so a quiescing NEIGHBOUR parks a fine placement.
+3. **Engine-level non-residency after `AdjustToOutside`**
+ (`PhysicsEngine.cs:1309-1318`, `:1464-1475`) — the adjusted cell can land in
+ an adjacent landblock, evaluated against THAT landblock. Not visible to any
+ pre-flight caller.
+4. Headless, where the predicate is weaker still (see below).
+
+**A pre-flight guard cannot close conditions that only Core can see.** Refuse is
+structurally incapable of being complete.
+
+## The actual root cause — and it is shipped, not new
+
+`ParkDeferred` (`RuntimeSetPositionState.cs:4088-4120`) withdraws the entity:
+`body.InWorld = false`, `Active` cleared, `WithdrawCanonical` (→
+`RemoveSpatialProjection` + `SetFullCell(record, 0u, 0u)`), `SuspendObjectClock`.
+
+`CancelCoreDeferred` (`:5131-5198`) removes the operation, rewrites
+`Withdraw`→`Discard`, and **restores none of it**. The only `InWorld = true` in
+the file is the local-player dormant-activation commit (`:2591`).
+
+So cancelling a wakeable park is strictly worse than retaining one: the park is
+at least wakeable; the cancel destroys the only object that could wake it.
+
+**This affects route 2's DeferredCell path too.** Route 2 compensates with its
+re-issue funnel — which is correct for a one-shot ForcePosition and wrong for a
+5-10 Hz remote stream. So the underlying defect has been masked, not fixed.
+
+## Corrected direction for the next round
+
+**Make the park work, at the source, modelled on retail's lost-cell.** A
+cancellation of a wakeable park must restore what `ParkDeferred` withdrew —
+`InWorld`, the object clock, and canonical residency — or the park must survive
+the merge-time `Forget` so its collision-generation wake can still fire.
+
+The 4b-1 contract said a withdrawal-restore inside `RuntimeSetPositionState`
+required STOP-and-report. **That stop has now happened and this is the answer**:
+refuse is structurally incomplete, retail has a working park, and the restore
+fixes route 2's latent path as well. Proceed with it deliberately.
+
+The service-window guard still has value as an OPTIMISATION — avoiding parks we
+can cheaply predict — but it is no longer the correctness mechanism and must not
+be presented as one.
+
+Whatever residual divergence remains after this needs a
+`docs/architecture/retail-divergence-register.md` row measured against retail's
+`GotoLostCell`/`reenter_visibility` behaviour, not against a "retail-shaped"
+label.
+
+## The other blocking findings
+
+**B1 — the headless predicate is not a service window.**
+`HeadlessSessionWorldProjection.cs:254-266` is a pure 3x3 Chebyshev GEOMETRY
+test against `_requestedCenterLandblock`, and returns `true` outright when no
+centre has been requested. Its own pre-existing doc says "can EVER
+collision-publish". The new interface promises "currently published". Signature
+match, predicate mismatch — the same over-permissiveness the graphical adapter
+explicitly rejected `IsNearTierOrPending` for. `IsReady` (`:268-285`) is the
+correct shape and sits fourteen lines below. Remove the "same question" claim.
+
+**B2 — the `ParkCollisionResidents` evidence is void, and the real hazard is a
+different one.** The delivered test demonstrates the two states that were
+already safe and calls `ParkCollisionResidents` DIRECTLY, bypassing
+`TryAcquireCollisionPrefixMutationPermission`'s `HasOldPrefixPlacementDebt`
+check (`:3641-3666`, consumed `:887`) — which is the thing that actually makes
+the throw unreachable. The genuine hazard is not a throw but an **indefinite
+streaming stall**: that predicate refuses permission on every poll while a
+retained retry is held, so the landblock never retires. `RetrySetupUnavailable`
+on an asset that never loads makes it permanent, and `DetachRoute` clears
+`_pending` WITHOUT cancelling the operation, orphaning it until session reset
+while it continues to pin the prefix.
+
+**B3 — `Advance()` re-submits with no service-window re-check** (`:298-332`,
+`SubmitAndResolve` `:334-394`). A retained entry can sit across many frames
+while its destination retires.
+
+**B4 — `Committed` leaves an untracked live operation.** It returns and retains
+nothing while the operation sits at `AwaitingCommitAcknowledgement`, retired
+only by `AcknowledgeProjection`. `RemotePlacementDrivePendingCount` cannot see
+it, so the ledger is blind to exactly the class that produces B2's stall.
+
+**B5 — N3's actual fix is untested.** `HeadlessSessionHost.cs:328` has zero
+coverage; the new test hand-builds the route and never touches `Tick`. Also:
+per-tick `RetryPendingProjections()` does `_pendingProjection.Values.ToArray()`,
+a new per-tick allocation K4's 30-session envelope was measured without; and
+headless dereferences the route directly with no generation latch, where
+graphical goes through `RuntimePlacementProjectionRetrySlot` which refuses a
+stale-generation callback.
+
+**B6 — `OwnsPlacement` keys on Disposition alone** (`:189-191`). The classifier
+also emits `SetPositionSimple` for the local player's FORCE_POSITION and
+teleport branches, and `SetPosition` for every initial Create. `record` and
+`route` are separate parameters, so a mismatched pair is expressible. One
+`route.OperationKind is RemoteAuthoritative` guard makes ownership exact.
+
+**B7 — three comments cite a "route 4b-1 report" that does not exist**
+(`GraphicalRemotePlacementServiceWindow.cs:57`,
+`RuntimeRemotePlacementDriveController.cs:122`, and the test file `:28`). Two of
+them point at precisely the evidence the contract demanded.
+
+**B8 — advisory for 4b-2/4b-3**: retail arms `ConstrainTo` even when the
+placement failed, so the successors must arm on refusal/rejection too, not only
+on commit. "Arm on Committed" is the natural misreading and is the same shape as
+the already-recorded unarmed-leash bug.
+
+## Verified correct — do not churn
+
+- Both omissions are right: `SendPositionEvent` is local-player-FORCE-only
+ (@0x00454091 inside the @0x0045400C gate); the remote arm @0x0045414D has no
+ equivalent, and retail never re-attempts — stale timestamps just bump
+ `error_count` @0x004542AC.
+- The disposition mapping is exact for Remote-kind routes: `SetPosition` ≡
+ teleport-or-cell-less (@0x00516386, flags 0x1012), `SetPositionSimple` ≡ far
+ snap (@0x005163C1-E8).
+- The graphical co-extensivity argument holds in both directions, independently
+ verified by both reviewers. `IsNearTier` over `IsNearTierOrPending` is right.
+- Contract item 2 holds: no production caller, no behaviour change, ledger
+ member always 0 in production.
+- The "Do NOT touch" list was respected — AP-135's writes, the teleport classes,
+ the legacy far halves, the single `ConstrainTo` site, route 1's executor.
+- Per-entity mechanics are otherwise sound: incarnation-keyed identity,
+ self-heal, `_driveScratch` snapshotting, `_driving` re-entry guard.
+- N3's ordering matches the graphical route.
+
+## Gate
+
+Complete Release suite, not a subset. Baseline **10,938 / 4 / 0**; the 4b-1
+state measured 10,955 / 4 / 0 while being defective. Two known flakes, do not
+chase and do not conflate: **#302** (`PortalProjectionTests…`, GC-allocation
+assertion, App.Tests) and **#308** (`NakEmissionTests.LossSoak_…`, wall-clock
+deadline, Core.Net.Tests, full-suite load only).
diff --git a/src/AcDream.App/Streaming/GraphicalRemotePlacementServiceWindow.cs b/src/AcDream.App/Streaming/GraphicalRemotePlacementServiceWindow.cs
new file mode 100644
index 00000000..49a5f17f
--- /dev/null
+++ b/src/AcDream.App/Streaming/GraphicalRemotePlacementServiceWindow.cs
@@ -0,0 +1,91 @@
+using AcDream.Runtime.Session;
+
+namespace AcDream.App.Streaming;
+
+///
+/// C4 route 4b-1: the graphical host's
+/// — headless already had
+/// one (HeadlessCollisionNeighborhood.IsWithinServiceWindow,
+/// HeadlessSessionWorldProjection.cs:27/:245-257); the
+/// graphical host had none.
+///
+///
+///
+/// Predicate choice — , not
+/// . The contract asks
+/// whether near-tier residency is exactly co-extensive with collision
+/// publication and, if not, to propose the correct predicate rather than
+/// ship the assumption. It is not — for the "pending" half specifically:
+/// IsNearTierOrPending is true for a landblock that has only been
+/// PARKED as a pending near-tier entity bucket
+/// (GpuWorldState.CommitEntitiesToExistingLandblockSpatialCore's
+/// parkIfMissing branch, :1919-1952) — i.e. queued, not yet
+/// collision-published. Using it here would let this guard say "go" for a
+/// destination whose collision generation has not committed, reopening
+/// exactly the DeferredCell park this route exists to prevent.
+/// alone is the correct predicate:
+///
+///
+/// Promotion direction (verified by reading, not assumed): the only
+/// two writers of _tierByLandblock[...] = LandblockStreamTier.Near are
+/// GpuWorldState.CommitLandblockSpatialCore (:969) and
+/// CommitEntitiesToExistingLandblockSpatialCore (:1972), both called
+/// from LandblockPresentationPipeline.Advance's
+/// SpatialPresentationCommitted stage — which runs strictly AFTER the
+/// preceding PresentationCommitted stage, the one that drives
+/// LandblockPhysicsPublisher's staged collision/EnvCell advance to
+/// completion (LandblockPresentationPipeline.cs ~826-935). A
+/// landblock's tier cannot read Near before its collision has committed.
+///
+///
+/// Retirement direction (verified by reading, not assumed):
+/// GpuWorldState.DetachNearLayer (:1755-1834) flips the tier to Far as
+/// the FIRST, synchronous step of a "detach-first" landblock retirement —
+/// LandblockRetirementCoordinator.AdoptDetachedFull's own doc comment
+/// states spatial detachment has already committed by the time its retirement
+/// ticket is constructed, and that ticket's LandblockRetirementStage.Physics
+/// step (_physics.AdvanceRemoval → RuntimeSetPositionState
+/// .WithdrawCollision → the collision-side retirement
+/// ParkCollisionResidents can reach) runs strictly AFTER. A
+/// landblock's tier cannot still read Near once its collision has begun
+/// retiring.
+///
+///
+/// Residual, not closed: a landblock-prefix collision MUTATION that is
+/// not a full retirement (a live in-place quiescence/refresh while the tier
+/// stays Near) is not ruled out by this reading and is not exercised by any
+/// gate this route adds. RuntimeSetPositionState's private
+/// TryGetBlockingQuiescence (:3820-3863) is Core's own check for
+/// exactly this case — an active CollisionPrefixQuiescence entry for
+/// the destination's landblock prefix, independent of tier/residency — and
+/// it is what a placement can still hit even after this guard passes,
+/// producing the narrow DeferredCell residual
+/// 's
+/// class doc describes.
+///
+///
+internal sealed class GraphicalRemotePlacementServiceWindow
+ : IRuntimeRemotePlacementServiceWindow
+{
+ private readonly GpuWorldState _state;
+
+ internal GraphicalRemotePlacementServiceWindow(GpuWorldState state)
+ {
+ _state = state ?? throw new ArgumentNullException(nameof(state));
+ }
+
+ ///
+ /// is a full ACE cell id (landblock high
+ /// word + cell low word — CreateObject.ServerPosition.LandblockId
+ /// carries the full id despite its name; see
+ /// RuntimeAcceptedPositionDriveControllerTests's own
+ /// SourceCell = SourceLandblock | 0x0001u fixture shape).
+ /// Canonicalized to 0xAAAAFFFF the same way every
+ /// tier writer/reader does
+ /// (DetachNearLayer, CommitLandblockSpatialCore) before the
+ /// dictionary lookup — itself does
+ /// not canonicalize its argument.
+ ///
+ public bool IsWithinServiceWindow(uint landblockId) =>
+ _state.IsNearTier((landblockId & 0xFFFF0000u) | 0xFFFFu);
+}
diff --git a/src/AcDream.Headless/Hosting/HeadlessSessionEventRoute.cs b/src/AcDream.Headless/Hosting/HeadlessSessionEventRoute.cs
index cfb87ef7..24a5d1fa 100644
--- a/src/AcDream.Headless/Hosting/HeadlessSessionEventRoute.cs
+++ b/src/AcDream.Headless/Hosting/HeadlessSessionEventRoute.cs
@@ -20,6 +20,17 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
private readonly Action? _localPlayerCompleted;
private RuntimePlacementProjectionSubscription? _subscription;
+ ///
+ /// B5(c) review fix: the graphical host's per-frame retry callback goes
+ /// through RuntimePlacementProjectionRetrySlot, which refuses a
+ /// callback whose bound generation is no longer current
+ /// (RetryPending's own guard). Headless calls this route's
+ /// directly with no equivalent latch — this
+ /// field plus the check inside restore that
+ /// same "the generation this route attached under must still be the
+ /// live one" guard.
+ ///
+ private RuntimeGenerationToken _attachedGeneration;
private bool _attachStarted;
private bool _eventsDisposed;
private bool _disposed;
@@ -51,6 +62,12 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
// succeeds and throws, LiveSessionHost's retryable rollback still
// invokes Dispose on the underlying route.
_attachStarted = true;
+ // B5(c) review fix: capture the generation this route is attaching
+ // under — RetryPending's own check below refuses a call reached
+ // after Runtime has since moved to a newer generation, mirroring
+ // RuntimePlacementProjectionRetrySlot.BindOwned/RetryPending's
+ // guard on the graphical side.
+ _attachedGeneration = _runtime.Generation;
// C3c-R1 review F6: assert (not assume) that the prior route
// detached — session reset precedes a new route — before this route
// takes ownership of the shared drive controller's tracked entries.
@@ -64,6 +81,59 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
_placements);
}
+ ///
+ /// C4 route 4b-1 (N3): republishes the canonical placement FIFO's
+ /// current head. only retries once, at subscribe
+ /// time (retryPendingOnSubscribe: true) — the ONLY
+ /// RetryPending call headless made before this fix. The graphical
+ /// host's per-frame retry lease also drives pending first-entry/accepted-
+ /// position sequences first and republishes the FIFO last
+ /// (GraphicalSessionEventRoute.Attach's retry-lease callback);
+ /// calls this in the same order
+ /// immediately after HeadlessSessionWorldProjection.PumpFirstEntry
+ /// so a declined head left behind by
+ ///
+ /// (a delta that is not yet the FIFO head is never revisited on its own)
+ /// gets a retry every headless tick, not only once per session.
+ ///
+ ///
+ /// B5(c) review fix: refuses when 's generation has
+ /// moved past the one this route attached under — the graphical host's
+ /// RuntimePlacementProjectionRetrySlot already refuses a
+ /// stale-generation callback the same way; headless dereferenced
+ /// directly with no equivalent guard before
+ /// this fix.
+ ///
+ ///
+ /// C2-2 review fix (delta round): the earlier version of this comment
+ /// claimed RuntimeSetPositionState.cs was "not permitted to
+ /// touch" as a blanket premise — false; that file simply was NOT this
+ /// session's file to edit (a concurrent, separately-owned change was
+ /// landing in it). The early-out below is now real: it checks
+ ///
+ /// (added for this fix) rather than the Runtime placement channel
+ /// directly, so it never trips
+ /// RuntimePhysicsOwnershipTests.ProductionHostsUseSharedPlacementSubscriptionWithoutDirectChannel
+ /// — hosts still consume placement state ONLY through the subscription.
+ /// This closes the EMPTY-FIFO case (the overwhelming common case in
+ /// steady state). The non-empty case still allocates inside
+ /// RetryPendingProjections's own
+ /// _pendingProjection.Values.ToArray() snapshot, which needs a
+ /// change to RuntimeSetPositionState.cs this session did not make
+ /// — filed as docs/ISSUES.md #311 rather than worked around.
+ ///
+ ///
+ internal bool RetryPending()
+ {
+ if (_subscription is null
+ || _attachedGeneration != _runtime.Generation
+ || !_subscription.HasPendingReceipts)
+ {
+ return false;
+ }
+ return _subscription.RetryPending();
+ }
+
public void Dispose()
{
if (_disposed)
diff --git a/src/AcDream.Headless/Hosting/HeadlessSessionHost.cs b/src/AcDream.Headless/Hosting/HeadlessSessionHost.cs
index 2fe59664..19aded57 100644
--- a/src/AcDream.Headless/Hosting/HeadlessSessionHost.cs
+++ b/src/AcDream.Headless/Hosting/HeadlessSessionHost.cs
@@ -121,6 +121,17 @@ internal sealed class HeadlessSessionHost : IDisposable
private readonly RuntimeLocalPlayerFrameController _localPlayerFrame;
private readonly HeadlessProcessContentOwner.HeadlessProcessContentLease?
_contentLease;
+ ///
+ /// B5(a) review fix: test-only seam (mirrors policyOverride's own
+ /// pattern) letting a focused test substitute a deterministic fake
+ /// placement sink for the production ,
+ /// so a test can drive itself — the real
+ /// _eventRoute?.RetryPending() call this fix covers — instead of
+ /// hand-constructing a outside
+ /// this host. null (every production caller) keeps today's exact
+ /// behavior.
+ ///
+ private readonly IRuntimePlacementProjectionSink? _placementSinkOverride;
/// C3c: one per-host first-entry drive controller (lazy — its
/// residence-begin subscription binds once against the persistent
/// Runtime lifetime) plus the active world projection it pumps
@@ -132,6 +143,14 @@ internal sealed class HeadlessSessionHost : IDisposable
private RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
private AcDream.Core.Net.WorldSession? _currentSession;
private HeadlessSessionWorldProjection? _worldProjection;
+ /// C4 route 4b-1 (N3): the exact route
+ /// last constructed, so can republish the canonical
+ /// placement FIFO every tick — mirrors the graphical host's per-frame
+ /// retry lease (GraphicalSessionEventRoute.Attach). Reassigned on
+ /// every reconnect exactly like ; the prior
+ /// route's own disposal (via LiveSessionHost's route replacement)
+ /// is independent of this field.
+ private HeadlessSessionEventRoute? _eventRoute;
private int _disposeStage;
private long _reconnectDeadline;
private bool _reconnectPending;
@@ -150,7 +169,8 @@ internal sealed class HeadlessSessionHost : IDisposable
TimeSpan? reconnectQuiescence = null,
HeadlessProcessContentOwner.HeadlessProcessContentLease?
contentLease = null,
- IHeadlessBotPolicy? policyOverride = null)
+ IHeadlessBotPolicy? policyOverride = null,
+ IRuntimePlacementProjectionSink? placementSinkOverride = null)
{
_descriptor = descriptor
?? throw new ArgumentNullException(nameof(descriptor));
@@ -158,6 +178,7 @@ internal sealed class HeadlessSessionHost : IDisposable
?? throw new ArgumentNullException(nameof(credential));
_diagnostics = diagnostics
?? throw new ArgumentNullException(nameof(diagnostics));
+ _placementSinkOverride = placementSinkOverride;
_timeProvider = timeProvider ?? TimeProvider.System;
_reconnectQuiescence = reconnectQuiescence
?? (sessionOperations is null
@@ -311,6 +332,13 @@ internal sealed class HeadlessSessionHost : IDisposable
// collision-generation progress and freshly accepted Creates both
// surface here, mirroring the graphical per-frame retry phase.
_worldProjection?.PumpFirstEntry();
+ // C4 route 4b-1 (N3): republish the canonical placement FIFO LAST,
+ // same order as the graphical host's retry-lease callback (drives
+ // first, retry last) — a declined Place left at the FIFO head by
+ // RuntimePlacementProjectionSubscription is otherwise never
+ // revisited, because Attach's retryPendingOnSubscribe only fires
+ // once, at subscribe time.
+ _eventRoute?.RetryPending();
_localPlayerFrame.RunPostNetworkCommandPhase();
Runtime.ActionOwner.CombatAttack.Tick();
_policy.Tick(Runtime, Commands);
@@ -666,13 +694,16 @@ internal sealed class HeadlessSessionHost : IDisposable
Runtime.CommunicationOwner.TurbineChat,
Runtime.CommunicationOwner.Friends,
Runtime.CommunicationOwner.Squelch));
- return new HeadlessSessionEventRoute(
+ var eventRoute = new HeadlessSessionEventRoute(
route,
Runtime,
- new HeadlessRuntimePlacementProjectionSink(Runtime),
+ _placementSinkOverride
+ ?? new HeadlessRuntimePlacementProjectionSink(Runtime),
_firstEntryDrive,
_ => session.SendGameAction(GameActionLoginComplete.Build()),
_acceptedPositionDrive);
+ _eventRoute = eventRoute;
+ return eventRoute;
}
private static LiveSessionCharacterSelector MapCharacterSelector(
diff --git a/src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs b/src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs
index d7cea790..e2991694 100644
--- a/src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs
+++ b/src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs
@@ -174,8 +174,52 @@ internal sealed class HeadlessCollisionGenerationTransaction
/// DAT and pak inputs. Every session retains its own engine, data cache,
/// cell graph, shadow registry, and publication ledger.
///
+///
+/// C4 route 4b-1: also implements
+/// .
+/// B1 review fix: despite the identical bool IsWithinServiceWindow(uint)
+/// signature, the two interfaces ask DIFFERENT questions —
+/// is a
+/// pure geometry test ("can this landblock EVER collision-publish inside the
+/// requested 3x3 window" — true outright when no center has been
+/// requested yet), while
+///
+/// promises "is it collision-published RIGHT NOW". Aliasing the new
+/// interface straight to the geometry test would readmit exactly the
+/// over-permissiveness the graphical adapter's own doc explicitly rejected
+/// IsNearTierOrPending for. asks the right
+/// question (it consults actual collision-publication residency), but it
+/// MUTATES — it calls to progress publication work
+/// as a side effect, which a per-packet service-window pre-flight check must
+/// never do. is
+/// 's residency shape with BOTH of its mutations
+/// removed — the call AND the
+/// _requestedFullCell field write (an earlier version of this comment
+/// said "the ONE mutating call"; that undercounted — a field write is a
+/// mutation too, just a cheaper one than pumping publication work). C2-3
+/// review fix (delta round): it is ALSO widened from 's
+/// own _centerLandblock != center restriction (correct for
+/// 's own use — "is the ONE landblock I most recently
+/// centered on ready" — but wrong here) to membership in
+/// , the full set
+/// actually publishes (the requested center plus its 3x3 neighbors). Without
+/// the widening this predicate answered true for at most ONE of the nine
+/// landblocks this host has actually published, refusing a remote one
+/// landblock off-center — exactly the boundary population route 4b-1 exists
+/// to serve — and Advance would then drop its retry once the service
+/// window (this predicate) forbade a destination that was genuinely
+/// servable. is already scoped to the CURRENT
+/// window by construction: 's re-center path fully
+/// retires the old center's entries (via
+/// 's reset-then-retire sequence) before the new
+/// center's landblocks are ever added, so a stale neighbor from a PRIOR
+/// center cannot linger in and be misread as
+/// currently published. The explicit interface implementation below routes
+/// to it instead of the geometry test.
+///
internal sealed class HeadlessCollisionNeighborhood
- : IHeadlessCollisionNeighborhood
+ : IHeadlessCollisionNeighborhood,
+ AcDream.Runtime.Session.IRuntimeRemotePlacementServiceWindow
{
private readonly record struct PublicationSpec(
uint LandblockId,
@@ -276,6 +320,49 @@ internal sealed class HeadlessCollisionNeighborhood
.GetCellStruct(fullCellId) is not null;
}
+ ///
+ /// B1 review fix: a residency check — resident, terrain-published, and
+ /// (for indoor cells) the CellStruct itself resolved — derived
+ /// from with both of its mutations removed (the
+ /// call and the _requestedFullCell field
+ /// write). This is what
+ ///
+ /// needs: "is collision published for this landblock RIGHT NOW", read
+ /// only, safe to call from a per-packet pre-flight check without racing
+ /// or pumping this neighborhood's own cadence-driven publication work.
+ /// C2-3 review fix (delta round): unlike , this
+ /// checks membership in the FULL set (the
+ /// requested center plus its published 3x3 neighbors) rather than
+ /// requiring an exact match against — see
+ /// the class remarks for why the narrower check was wrong for this
+ /// interface's question.
+ ///
+ private bool IsCollisionCurrentlyPublished(uint fullCellId)
+ {
+ uint landblock = CanonicalLandblock(fullCellId);
+ if (!_resident.Contains(landblock)
+ || !_runtime.EntityObjects.Physics.Engine
+ .IsLandblockTerrainResident(landblock))
+ {
+ return false;
+ }
+ return (fullCellId & 0xFFFFu) < 0x0100u
+ || _runtime.EntityObjects.Physics.DataCache
+ .GetCellStruct(fullCellId) is not null;
+ }
+
+ ///
+ /// B1 review fix: explicit implementation so this class can answer the
+ /// two IsWithinServiceWindow questions differently despite the
+ /// identical method signature — see the class remarks. Routes to
+ /// , never to the geometry
+ /// test implicitly implements for
+ /// .
+ ///
+ bool AcDream.Runtime.Session.IRuntimeRemotePlacementServiceWindow
+ .IsWithinServiceWindow(uint fullCellId) =>
+ IsCollisionCurrentlyPublished(fullCellId);
+
private HeadlessCollisionGenerationTransaction? CreatePublication(
uint landblockId,
Vector3 origin,
diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
index cc9d53cc..453d32e9 100644
--- a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
+++ b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
@@ -88,7 +88,20 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
/// .
/// Gated by — a leaked pending ack cannot hide.
///
- int AcceptedPositionDrivePendingCount = 0)
+ int AcceptedPositionDrivePendingCount = 0,
+ ///
+ /// C4 route 4b-1: outstanding
+ /// AcDream.Runtime.Session.RuntimeRemotePlacementDriveController
+ /// preparation-retry entries (a not-yet-resolved
+ /// RetrySetupUnavailable/RetryWorldFrameUnavailable for a
+ /// remote), summed over every drive registered against this lifetime via
+ /// .
+ /// Gated by , mirroring
+ /// — steady-state
+ /// remotes hold no operations, and this count proves it at every
+ /// convergence checkpoint the same way.
+ ///
+ int RemotePlacementDrivePendingCount = 0)
{
public bool IsConverged =>
IsDisposed
@@ -114,6 +127,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
&& RemoteFirstEntryActiveCount == 0
&& FirstEntryDrivePendingCount == 0
&& AcceptedPositionDrivePendingCount == 0
+ && RemotePlacementDrivePendingCount == 0
&& StreamSubscriberCount == 0
&& PlacementStreamSubscriberCount == 0
&& PendingDispatchCount == 0
@@ -163,6 +177,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
private readonly List> _firstEntryDriveOwnership = [];
/// C4 route 2: see .
private readonly List> _acceptedPositionDriveOwnership = [];
+ /// C4 route 4b-1: see .
+ private readonly List> _remotePlacementDriveOwnership = [];
///
/// C4 route 4a: captured by alongside the
/// other generation-consuming children so
@@ -486,7 +502,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
LocalPlayerFirstEntry.CaptureOwnership().ActiveCount,
RemoteFirstEntry.CaptureOwnership().ActiveCount,
CaptureFirstEntryDrivePendingCount(),
- CaptureAcceptedPositionDrivePendingCount());
+ CaptureAcceptedPositionDrivePendingCount(),
+ CaptureRemotePlacementDrivePendingCount());
}
private int CaptureFirstEntryDrivePendingCount()
@@ -505,6 +522,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
return total;
}
+ private int CaptureRemotePlacementDrivePendingCount()
+ {
+ int total = 0;
+ for (int i = 0; i < _remotePlacementDriveOwnership.Count; i++)
+ total = checked(total + _remotePlacementDriveOwnership[i]());
+ return total;
+ }
+
///
/// C3c-R1 review F5: registers one host first-entry drive controller's
/// pending-count provider into this lifetime's ownership snapshot, so
@@ -535,6 +560,22 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
_acceptedPositionDriveOwnership.Add(pendingCount);
}
+ ///
+ /// C4 route 4b-1: registers one host
+ /// RuntimeRemotePlacementDriveController's pending-count provider
+ /// into this lifetime's ownership snapshot, mirroring
+ /// — a leaked
+ /// remote preparation retry must not sit outside every ledger. The drive
+ /// controller registers itself at construction; multiple registrations
+ /// sum (one per host route sharing this lifetime).
+ ///
+ public void RegisterRemotePlacementDriveOwnership(Func pendingCount)
+ {
+ ArgumentNullException.ThrowIfNull(pendingCount);
+ EnsureNotDisposed();
+ _remotePlacementDriveOwnership.Add(pendingCount);
+ }
+
public void BindEventContext(
Func generation,
Func frameNumber)
diff --git a/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs b/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs
index c6d95943..877674c5 100644
--- a/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs
+++ b/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs
@@ -454,6 +454,21 @@ public sealed class RuntimePhysicsState : IDisposable
_collisionGenerationCommittedObservers = new();
private bool _disposed;
+ ///
+ /// C2-1 review fix (delta round): lets a ledger-provider callback
+ /// registered against (e.g.
+ /// RuntimeRemotePlacementDriveController.CountLiveAwaitingAcknowledgement)
+ /// check disposal state BEFORE calling into ,
+ /// whose own IsPlacementCurrent throws ObjectDisposedException
+ /// once disposed. Dispose() below disposes
+ /// strictly before setting this flag, so IsDisposed == true here
+ /// guarantees is already disposed too — a
+ /// post-Dispose() CaptureOwnership() read is the designed
+ /// contract (GameWindowLifetime.DisposeGameRuntime), so every
+ /// ledger provider must survive it without throwing.
+ ///
+ internal bool IsDisposed => _disposed;
+
public event Action? CellCommitted;
public event Action?
CollisionGenerationCommitted
diff --git a/src/AcDream.Runtime/Physics/RuntimePlacementProjectionSubscription.cs b/src/AcDream.Runtime/Physics/RuntimePlacementProjectionSubscription.cs
index 622cea4e..0347f12d 100644
--- a/src/AcDream.Runtime/Physics/RuntimePlacementProjectionSubscription.cs
+++ b/src/AcDream.Runtime/Physics/RuntimePlacementProjectionSubscription.cs
@@ -86,6 +86,20 @@ public sealed class RuntimePlacementProjectionSubscription
public bool HasAppliedReceiptAwaitingAcknowledgement =>
_appliedAwaitingAcknowledgement.IsValid;
+ ///
+ /// C2-2 review fix (delta round, B5(b)): true when Runtime's placement
+ /// FIFO has at least one outstanding receipt, so a host can early-out
+ /// before calling without reaching the
+ /// Runtime placement channel directly — the architectural boundary
+ /// RuntimePhysicsOwnershipTests.ProductionHostsUseSharedPlacementSubscriptionWithoutDirectChannel
+ /// enforces (hosts consume placement state ONLY through this
+ /// subscription). Only closes the EMPTY-FIFO case: when the count is
+ /// nonzero, RetryPending still reaches
+ /// RuntimeSetPositionState.RetryPendingProjections's per-call
+ /// array snapshot — see docs/ISSUES.md for that residual.
+ ///
+ public bool HasPendingReceipts => _channel.PendingCount != 0;
+
///
/// Republishes Runtime's complete still-pending FIFO. Later receipts are
/// ignored until the exact oldest receipt projects and acknowledges.
diff --git a/src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs b/src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs
new file mode 100644
index 00000000..6c16f5f5
--- /dev/null
+++ b/src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs
@@ -0,0 +1,634 @@
+using AcDream.Content;
+using AcDream.Core.Net.Messages;
+using AcDream.Core.Physics;
+using AcDream.Runtime.Entities;
+using AcDream.Runtime.Physics;
+
+namespace AcDream.Runtime.Session;
+
+///
+/// C4 route 4b-1: per-entity remote-placement service-window predicate. A
+/// host implements this over whatever collision-publication residency it
+/// actually tracks — GpuWorldState.IsNearTier for the graphical host,
+/// HeadlessCollisionNeighborhood's explicit implementation (backed by
+/// its private IsCollisionCurrentlyPublished — NOT
+/// ,
+/// which is a pure geometry test over the requested 3x3 window and answers a
+/// different question: "can this landblock EVER collision-publish", not "is
+/// it collision-published right now" — B1 review fix) for headless — so
+/// can
+/// ask, BEFORE attempting any canonical SetPosition, whether the accepted
+/// destination is one this host can actually place a remote into right now.
+///
+public interface IRuntimeRemotePlacementServiceWindow
+{
+ ///
+ /// True when 's collision is currently
+ /// published by this host, so a canonical SetPosition into it can be
+ /// attempted without risking an un-wakeable DeferredCell park (see
+ /// docs/research/2026-08-04-c4-route-4b-1-contract.md's "central
+ /// decision"). may be a full cell id — the
+ /// implementation canonicalizes to the containing landblock.
+ ///
+ bool IsWithinServiceWindow(uint landblockId);
+}
+
+///
+/// Typed yields for
+/// .
+///
+internal enum RuntimeRemotePlacementExecutionStatus : byte
+{
+ /// Out of this route's scope: not a disposition this
+ /// controller owns (),
+ /// no canonical body, or no incarnation key.
+ NotApplicable,
+
+ ///
+ /// The central decision (see the class doc on
+ /// ): the accepted
+ /// destination is not one this host can currently collision-publish, or
+ /// the canonical SetPosition attempted anyway and Core still deferred it.
+ /// No operation is retained either way — the entity keeps its last
+ /// committed pose and waits for the next accepted Position, which for a
+ /// remote is a 5-10 Hz stream away.
+ ///
+ Refused,
+
+ /// The entity already holds an active operation — a concurrent
+ /// placement authority (portal/teleport/another host route) or this
+ /// controller's own still-outstanding preparation retry.
+ Contention,
+
+ /// The canonical SetPosition committed synchronously.
+ Committed,
+
+ /// Rejected/cancelled by Core (invalid prepared data, authority
+ /// displaced mid-submit).
+ Rejected,
+}
+
+///
+/// C4 route 4b-1: the Runtime-owned, per-entity accepted-Position execution
+/// seam for a remote whose classification is SetPosition (teleport /
+/// cell-less — 4b-3's eventual disposition) or SetPositionSimple (far
+/// snap, >=96 m — 4b-2's). Route 4a's
+/// already owns the two
+/// dispositions that perform no SetPosition at all
+/// (NoPositionOperation/Interpolate); this controller is the
+/// architectural sibling for the remaining two, built from route 2's
+/// controller shape with two deliberate omissions and one deliberate
+/// generalization:
+///
+///
+/// - No ack. Retail's remote arm has no
+/// SendPositionEvent — HandleReceivedPosition @0x00453FD0 calls
+/// it only on the local-player FORCE_POSITION branch. There is nothing here
+/// resembling route 2's PositionEventOwed/SendPositionEvent
+/// pair.
+/// - No re-issue funnel. Route 2 re-issues a dead
+/// operation because a ForcePosition is a one-shot correction ACE never
+/// repeats. A remote Position is a REPEATED stream — re-issuing packet N
+/// after N+1 has already merged would apply a pose the newer packet already
+/// superseded. When this controller's own tracked operation dies for any
+/// reason (superseded, torn down, forgotten by the entity's next accepted
+/// Position), it is simply dropped: the next packet supplies the current
+/// truth on its own.
+/// - Per-entity, not per-session. Route 2's
+/// _pending is a single slot because the local player is the only
+/// entity that route ever touches. Remotes are N entities, so
+/// is a per-key map and every entry is independent —
+/// route 2's single-owner invariant (Begin refuses a second live entry for
+/// the SAME key) is unchanged, just re-derived per entity instead of
+/// globally.
+///
+///
+///
+/// The central decision — refuse, do not park.
+/// 's DeferredCell park
+/// withdraws the entity from the world (ParkDeferred sets
+/// body.InWorld = false, suspends the object clock, and publishes a
+/// Withdraw) — and RuntimeEntityObjectLifetime.TryApplyPosition
+/// calls RuntimeSetPositionState.Forget on EVERY subsequent accepted
+/// Position for that same entity, regardless of disposition. Forget's
+/// CancelCoreDeferred removes the operation and rewrites the
+/// Withdraw into a Discard WITHOUT restoring InWorld,
+/// resuming the clock, or re-entering residency. Because ACE broadcasts a
+/// remote's Position every 100-150 ms — almost always faster than the
+/// collision-generation wake this park would need to resolve on its own —
+/// any DeferredCell park opened here would be cancelled by the entity's own
+/// next packet long before it could wake, leaving the entity invisible AND
+/// intangible for the rest of the session.
+/// exists to prevent this controller from ever attempting a SetPosition
+/// whose destination cannot be placed right now:
+/// checks it BEFORE calling
+/// ,
+/// and if Core still returns DeferredCell despite the guard passing,
+/// the operation is cancelled immediately rather than retained, so the
+/// ledger still converges even though the guard's invariant did not hold for
+/// that one packet. Why the guard can still miss (the co-extensivity
+/// finding and its residual): a tier/residency-backed service window
+/// (the graphical host's GpuWorldState.IsNearTier, headless's
+/// collision-published check) is co-extensive with collision PUBLICATION in
+/// both directions — verified by reading both tier-writer call sites
+/// (promotion cannot read Near before collision commits) and the retirement
+/// call site (the tier flips to Far as the FIRST, synchronous step of
+/// retirement, strictly before collision-side withdrawal). It is NOT
+/// co-extensive with a live in-place collision-prefix MUTATION that leaves
+/// the tier/residency reading unchanged while the prefix quiesces —
+/// 's private TryGetBlockingQuiescence
+/// is Core's own check for exactly that case, and it is what a placement can
+/// still hit even after this guard passes. That one narrow window is the
+/// residual this guard cannot close from outside Core.
+///
+///
+///
+/// One instance per host session route, constructed once per host process
+/// and reused across reconnects exactly like
+/// and
+/// —
+/// / assert the same
+/// "session reset precedes a new route" ordering and clear any tracked
+/// entries left by a torn-down session.
+///
+///
+internal sealed class RuntimeRemotePlacementDriveController
+{
+ private sealed class Pending
+ {
+ internal required RuntimeEntityRecord Record { get; init; }
+ internal required RuntimeEntityPlacementToken Token { get; init; }
+ internal required RuntimeAuthoritativePositionRoute Route { get; init; }
+ }
+
+ private readonly RuntimeEntityObjectLifetime _entityObjects;
+ private readonly IGameRuntimeClock _clock;
+ private readonly IPreparedCollisionSource _collisionSource;
+ private readonly IRuntimeRemotePlacementServiceWindow _serviceWindow;
+
+ ///
+ /// Per-entity preparation-retry map (bounded to the two retryable
+ /// reasons — Setup
+ /// or world-frame data not resolved yet, NEITHER of which withdraws the
+ /// entity). A DeferredCell outcome never enters this map — see the
+ /// class doc's central decision.
+ ///
+ private readonly Dictionary _pending = [];
+ ///
+ /// B4 review fix: per-entity tokens whose
+ /// outcome was CommittedHostAcknowledgementPending and were STILL
+ /// live (not synchronously consumed-and-acknowledged by the production
+ /// placement-projection subscription inside that same call) the instant
+ /// returned Committed. Without this,
+ /// the ledger went blind the moment Committed was returned, hiding
+ /// exactly the declined-sink class the FIFO retry mechanism exists for.
+ /// Pruned lazily (self-healing) on every read — see
+ /// — never gates any
+ /// placement decision itself; Core's own _operations map remains
+ /// the sole authority TryBeginExclusiveAuthoredPlacement consults.
+ ///
+ private readonly Dictionary
+ _awaitingAcknowledgement = [];
+ private readonly List _driveScratch = [];
+ ///
+ /// C2-1 review fix (delta round): dedicated reusable scratch list for
+ /// 's self-heal removal
+ /// pass — kept separate from (owned by
+ /// ) so a ledger read reached while Advance
+ /// is mid-iteration can never corrupt its scratch buffer.
+ ///
+ private readonly List _awaitingAcknowledgementScratch = [];
+ private bool _driving;
+ private object? _routeOwner;
+
+ internal RuntimeRemotePlacementDriveController(
+ RuntimeEntityObjectLifetime entityObjects,
+ IGameRuntimeClock clock,
+ IPreparedCollisionSource collisionSource,
+ IRuntimeRemotePlacementServiceWindow serviceWindow)
+ {
+ _entityObjects = entityObjects
+ ?? throw new ArgumentNullException(nameof(entityObjects));
+ _clock = clock ?? throw new ArgumentNullException(nameof(clock));
+ _collisionSource = collisionSource
+ ?? throw new ArgumentNullException(nameof(collisionSource));
+ _serviceWindow = serviceWindow
+ ?? throw new ArgumentNullException(nameof(serviceWindow));
+ _entityObjects.RegisterRemotePlacementDriveOwnership(
+ () => _pending.Count);
+ // B4 review fix: a second, independent registration — multiple
+ // registrations sum (RegisterRemotePlacementDriveOwnership's own doc
+ // comment) — so the awaiting-acknowledgement dimension is visible in
+ // the SAME ledger without changing what _pending itself reports.
+ _entityObjects.RegisterRemotePlacementDriveOwnership(
+ CountLiveAwaitingAcknowledgement);
+ }
+
+ ///
+ /// Preparation-stage retries only (see 's own doc).
+ /// Deliberately does NOT include —
+ /// that dimension is reported to the lifetime's ownership ledger via the
+ /// constructor's second RegisterRemotePlacementDriveOwnership call
+ /// (B4 review fix) and has no separate test-visible counter of its own.
+ ///
+ internal int PendingCount => _pending.Count;
+
+ ///
+ /// True when route 4b owns this classification for a remote — the two
+ /// dispositions route 4a's
+ /// does NOT already handle. Everything else (Interpolate,
+ /// NoPositionOperation, RejectedAuthority,
+ /// RejectedData, AwaitFreshPosition) is out of scope here.
+ /// B6 review fix: Disposition alone is not exact — the classifier
+ /// (RuntimeAuthoritativePositionRouteClassifier.cs) assigns
+ /// SetPosition/SetPositionSimple at several independent
+ /// call sites (:256, :332, :355, :403, :459) and derives
+ /// OperationKind separately (:560-575) — the SAME disposition
+ /// covers the LOCAL PLAYER's FORCE_POSITION/teleport branches
+ /// (RuntimeSetPositionOperationKind.LocalAuthoritative).
+ /// is a parameter separate from the entity
+ /// record at every call site, so a mismatched pair is expressible;
+ /// gating on OperationKind narrows to remotes AND the local
+ /// player's own initial Create — OperationKind's own switch
+ /// (:559-570) maps InitialLogin to the LOCAL PLAYER's Create
+ /// ONLY; a REMOTE top-level Create maps to RemoteAuthoritative
+ /// exactly like a remote accepted Position does (correcting this
+ /// comment's earlier, wrong claim that all initial Creates map to
+ /// InitialLogin).
+ ///
+ /// C2-4 review fix (delta round): so OperationKind alone still
+ /// does not exclude a remote top-level Create — ClassifyCreate
+ /// (:254-273) emits SetPosition + RemoteAuthoritative +
+ /// InitialCreateFlags (Placement|Slide) for one, while
+ /// ClassifyAcceptedPosition's remote branches (:401-416,
+ /// :454-472) always carry AuthoritativeTeleportFlags
+ /// (Teleport|Slide|SendPositionEvent) for the SAME disposition/
+ /// OperationKind pair. The Teleport bit is the exact
+ /// discriminator retail's own flag choice provides — Create asks for
+ /// placement collision, an accepted Position asks for a teleport
+ /// resolve — so requiring it here excludes Creates (route 4b-1 is a
+ /// POSITION-only route; the first-entry conductor owns every Create)
+ /// without excluding either remote Position shape.
+ ///
+ ///
+ internal static bool OwnsPlacement(RuntimeAuthoritativePositionRoute route) =>
+ route.OperationKind is RuntimeSetPositionOperationKind.RemoteAuthoritative
+ && route.Disposition is RuntimeAuthoritativePositionDisposition.SetPosition
+ or RuntimeAuthoritativePositionDisposition.SetPositionSimple
+ && (route.SetPositionFlags & PhysicsSetPositionFlags.Teleport) != 0;
+
+ ///
+ /// Mirrors and
+ /// : this
+ /// controller outlives its session routes, so the "session reset
+ /// precedes a new route" ordering is asserted, not assumed.
+ ///
+ internal void AttachRoute(object route)
+ {
+ ArgumentNullException.ThrowIfNull(route);
+ if (_routeOwner is not null && !ReferenceEquals(_routeOwner, route))
+ {
+ throw new InvalidOperationException(
+ "A remote placement 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;
+ }
+
+ ///
+ /// Route-scoped teardown: abandons every tracked entry, but ONLY when
+ /// is the attached owner.
+ ///
+ ///
+ /// C2-1 review fix (delta round) — the prior version of this method (and
+ /// this comment) was wrong: it cleared and
+ /// as if they were pure
+ /// bookkeeping, citing
+ /// as the mirror. That is the WRONG sibling — first-entry's tracked
+ /// entries have an INDEPENDENT owner (the residence lease) that survives
+ /// route teardown on its own, so clearing first-entry's local map merely
+ /// stops WATCHING an operation something else still owns. These two maps
+ /// have no such owner: EVERY entry holds a Core operation THIS
+ /// controller alone began — at
+ /// AwaitingPreparation (already begun via
+ /// TryBeginExclusiveAuthoredPlacement),
+ /// at AwaitingCommitAcknowledgement with a published Place.
+ /// Clearing the local dictionary without cancelling the Core operation
+ /// left it live forever, pinning its landblock prefix
+ /// (HasOldPrefixPlacementDebt) — docs/ISSUES.md #310's unbounded
+ /// streaming-stall hazard, now reachable from an ordinary reconnect/
+ /// session-reset instead of only a stuck asset retry. The correct mirror
+ /// is route 2's RuntimeAcceptedPositionDriveController.DetachRoute
+ /// → AbandonPending, which this now matches: cancel every live
+ /// operation (, restoreCancelledPark: true
+ /// — these are cancellations of an abandoned placement INTENT, not
+ /// withdrawals, so any park rolls back rather than stranding the body)
+ /// before clearing the local maps.
+ ///
+ ///
+ internal void DetachRoute(object route)
+ {
+ ArgumentNullException.ThrowIfNull(route);
+ if (!ReferenceEquals(_routeOwner, route))
+ return;
+ _routeOwner = null;
+
+ RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
+ if (_pending.Count != 0)
+ {
+ Pending[] abandoned = [.. _pending.Values];
+ _pending.Clear();
+ foreach (Pending entry in abandoned)
+ {
+ setPosition.ForgetPlacementCompletion(entry.Token);
+ CancelToken(setPosition, entry.Token);
+ }
+ }
+ if (_awaitingAcknowledgement.Count != 0)
+ {
+ RuntimeEntityPlacementToken[] abandoned =
+ [.. _awaitingAcknowledgement.Values];
+ _awaitingAcknowledgement.Clear();
+ foreach (RuntimeEntityPlacementToken token in abandoned)
+ {
+ setPosition.ForgetPlacementCompletion(token);
+ CancelToken(setPosition, token);
+ }
+ }
+ }
+
+ ///
+ /// Executes an already-classified remote accepted Position against the
+ /// canonical Runtime SetPosition owner. 's
+ /// Snapshot and PositionAuthorityVersion must already reflect the merge
+ /// performed —
+ /// this method never re-merges the wire frame, and
+ /// must already be the result of
+ ///
+ /// for the SAME packet (the shared classification builder every remote
+ /// caller uses — never re-derived here).
+ ///
+ internal RuntimeRemotePlacementExecutionStatus TryExecuteAcceptedRemotePosition(
+ RuntimeEntityRecord record,
+ in RuntimeAuthoritativePositionRoute route)
+ {
+ ArgumentNullException.ThrowIfNull(record);
+ if (!OwnsPlacement(route)
+ || record.PhysicsBody is null
+ || record.Key is not { } key)
+ {
+ return RuntimeRemotePlacementExecutionStatus.NotApplicable;
+ }
+
+ RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
+
+ // A retained preparation retry whose operation died some other way
+ // (superseded, torn down, generation change, or simply forgotten by
+ // an unrelated accepted Position for this same entity) must not
+ // block a fresh Begin for THIS packet — self-heal rather than report
+ // a Contention nothing is actually contending.
+ if (_pending.TryGetValue(key, out Pending? stale)
+ && !setPosition.IsPlacementCurrent(stale.Token))
+ {
+ _pending.Remove(key);
+ }
+
+ CreateObject.ServerPosition? destination =
+ record.Snapshot.Physics?.Position ?? record.Snapshot.Position;
+ if (destination is not { } accepted
+ || !_serviceWindow.IsWithinServiceWindow(accepted.LandblockId))
+ {
+ // The central decision: refuse rather than open a park this
+ // host's own service window could never wake.
+ return RuntimeRemotePlacementExecutionStatus.Refused;
+ }
+
+ RuntimeEntityPlacementToken token =
+ setPosition.TryBeginExclusiveAuthoredPlacement(
+ record,
+ record.PositionAuthorityVersion,
+ route.OperationKind);
+ if (!token.IsValid)
+ return RuntimeRemotePlacementExecutionStatus.Contention;
+
+ return SubmitAndResolve(record, token, route);
+ }
+
+ ///
+ /// Host cadence pump: retries a preparation-only retry status
+ /// (RetrySetupUnavailable/RetryWorldFrameUnavailable) by
+ /// re-calling the SAME prepare+submit pair, exactly like
+ /// 's own continuation
+ /// completion. Bounded, non-allocating iteration mirrors
+ /// 's _driveScratch
+ /// template. Safe to call from any host cadence point; a no-op when
+ /// nothing is pending.
+ ///
+ internal void Advance()
+ {
+ if (_driving || _pending.Count == 0)
+ return;
+ _driving = true;
+ try
+ {
+ _driveScratch.Clear();
+ foreach (RuntimeEntityKey key in _pending.Keys)
+ _driveScratch.Add(key);
+
+ RuntimeSetPositionState setPosition =
+ _entityObjects.Physics.SetPosition;
+ foreach (RuntimeEntityKey key in _driveScratch)
+ {
+ if (!_pending.TryGetValue(key, out Pending? pending))
+ continue;
+ if (!setPosition.IsPlacementCurrent(pending.Token))
+ {
+ // Forgotten by some other accepted Position for this
+ // same entity before this retry resolved. No re-issue
+ // funnel for remotes: the next packet supplies current
+ // truth on its own.
+ _pending.Remove(key);
+ continue;
+ }
+ _pending.Remove(key);
+
+ // B3 review fix: a retry can sit retained across many host
+ // cadence pumps (bounded only by how long the asset stayed
+ // unavailable) while its destination's collision publication
+ // retires out from under it. Re-check the SAME service-window
+ // guard the entry point uses BEFORE resubmitting — dropping
+ // (and cancelling the already-begun token) rather than
+ // resubmitting matches the entry point's own Refused
+ // semantics: no operation survives, the entity keeps its last
+ // committed pose, and the next packet supplies current truth
+ // on its own. Without this re-check a destination that fell
+ // out of the window would just keep coming back Contention
+ // forever (the asset source has not changed), never
+ // converging even though the window already knows better.
+ CreateObject.ServerPosition? destination =
+ pending.Record.Snapshot.Physics?.Position
+ ?? pending.Record.Snapshot.Position;
+ if (destination is not { } accepted
+ || !_serviceWindow.IsWithinServiceWindow(
+ accepted.LandblockId))
+ {
+ CancelToken(setPosition, pending.Token);
+ continue;
+ }
+
+ _ = SubmitAndResolve(pending.Record, pending.Token, pending.Route);
+ }
+ }
+ finally
+ {
+ _driving = false;
+ }
+ }
+
+ private RuntimeRemotePlacementExecutionStatus 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())
+ {
+ _pending[token.Entity] = new Pending
+ {
+ Record = record,
+ Token = token,
+ Route = route,
+ };
+ return RuntimeRemotePlacementExecutionStatus.Contention;
+ }
+
+ CancelToken(setPosition, token);
+ return RuntimeRemotePlacementExecutionStatus.Rejected;
+ }
+
+ switch (outcome.Status)
+ {
+ case RuntimeSetPositionStatus.CommittedHostAcknowledgementPending:
+ // No ack for remotes. The production placement-projection
+ // subscription (shared infrastructure, not owned here) has
+ // already applied-and-acknowledged the Place receipt
+ // synchronously inside the SetPosition call above, if it was
+ // going to — exactly like route 2's own commit branch, minus
+ // the ack call route 2 makes for the local player. B4 review
+ // fix: when the sink declined instead (host not ready — the
+ // exact scenario the FIFO retry mechanism exists for), the
+ // operation is STILL LIVE in Core's _operations map until
+ // some later AcknowledgeProjection retires it. Track it so
+ // the ledger can see that class of outstanding operation
+ // instead of going blind the instant this method returns.
+ if (setPosition.IsPlacementCurrent(token))
+ _awaitingAcknowledgement[token.Entity] = token;
+ return RuntimeRemotePlacementExecutionStatus.Committed;
+
+ case RuntimeSetPositionStatus.DeferredCell:
+ // Central decision: this branch means the service-window
+ // guard passed but Core still deferred the destination — the
+ // narrow residual the class doc's central-decision paragraph
+ // explains (a live in-place collision-prefix quiescence the
+ // tier/residency guard cannot see from outside Core, per
+ // RuntimeSetPositionState's private TryGetBlockingQuiescence).
+ // Cancel immediately rather than retain a watch: no
+ // re-issue, no park survives this controller.
+ CancelToken(setPosition, token);
+ return RuntimeRemotePlacementExecutionStatus.Refused;
+
+ default:
+ // Rejected/Cancelled — authority moved out from under this
+ // operation, so the body never moved.
+ CancelToken(setPosition, token);
+ return RuntimeRemotePlacementExecutionStatus.Rejected;
+ }
+ }
+
+ ///
+ /// Self-healing read: prunes every
+ /// entry whose token Core no longer considers current (the ack already
+ /// landed through whatever path — the synchronous in-call apply, a later
+ /// FIFO retry, or Runtime's own session-reset/generation-change teardown
+ /// clearing the operation outright) before returning the live count.
+ /// This is what lets the constructor's second
+ /// RegisterRemotePlacementDriveOwnership registration converge to
+ /// zero (B4 review fix) without this controller needing a separate
+ /// periodic pump for this one dictionary — every ownership-snapshot read
+ /// (including the exact convergence checks teardown/reset/generation
+ /// change assert) sees the truth as of that read.
+ ///
+ ///
+ /// C2-1 review fix (delta round), disposal safety: IsPlacementCurrent's
+ /// first statement is EnsureNotDisposed, which THROWS once
+ /// is disposed. A post-Dispose()
+ /// CaptureOwnership() read is the designed contract
+ /// (GameWindowLifetime.DisposeGameRuntime: runtime.Dispose();
+ /// runtime.CaptureOwnership();), so this — the first ledger provider
+ /// to reach into another disposable subsystem — must survive it. When
+ /// is already true, Core
+ /// itself is gone; there is nothing left to ask, so this returns
+ /// whatever count is STILL in the local map rather than calling into the
+ /// disposed state. A healthy teardown already cancelled and cleared
+ /// every entry via before disposal, so this
+ /// branch reports 0 in the healthy path and a genuine nonzero leak
+ /// otherwise — never an exception either way.
+ ///
+ ///
+ /// C2-1 review fix (delta round), allocation: reuses
+ /// instead of a per-call
+ /// List<RuntimeEntityKey>, mirroring 's
+ /// own template.
+ ///
+ ///
+ private int CountLiveAwaitingAcknowledgement()
+ {
+ if (_awaitingAcknowledgement.Count == 0)
+ return 0;
+ if (_entityObjects.Physics.IsDisposed)
+ return _awaitingAcknowledgement.Count;
+
+ RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
+ _awaitingAcknowledgementScratch.Clear();
+ foreach ((RuntimeEntityKey key, RuntimeEntityPlacementToken token)
+ in _awaitingAcknowledgement)
+ {
+ if (!setPosition.IsPlacementCurrent(token))
+ _awaitingAcknowledgementScratch.Add(key);
+ }
+ foreach (RuntimeEntityKey key in _awaitingAcknowledgementScratch)
+ _awaitingAcknowledgement.Remove(key);
+ return _awaitingAcknowledgement.Count;
+ }
+
+ private static void CancelToken(
+ RuntimeSetPositionState setPosition,
+ in RuntimeEntityPlacementToken token)
+ {
+ // Cancellation, not withdrawal: this controller abandons a placement
+ // intent while the remote stays in the world, so a DeferredCell park
+ // must roll back rather than strand the entity invisible and
+ // intangible (RuntimeSetPositionState.Forget).
+ RuntimePlacementCancellationReceipt cancellation =
+ setPosition.ForgetExactPlacement(
+ token,
+ restoreCancelledPark: true);
+ if (cancellation.IsValid)
+ setPosition.PublishCancellation(cancellation);
+ }
+}
diff --git a/tests/AcDream.App.Tests/Streaming/GraphicalRemotePlacementServiceWindowTests.cs b/tests/AcDream.App.Tests/Streaming/GraphicalRemotePlacementServiceWindowTests.cs
new file mode 100644
index 00000000..82c742c2
--- /dev/null
+++ b/tests/AcDream.App.Tests/Streaming/GraphicalRemotePlacementServiceWindowTests.cs
@@ -0,0 +1,84 @@
+using AcDream.App.Streaming;
+using AcDream.Core.World;
+using DatReaderWriter.DBObjs;
+
+namespace AcDream.App.Tests.Streaming;
+
+///
+/// C4 route 4b-1: focused tests for the graphical host's
+/// — the App-side
+/// implementation of IRuntimeRemotePlacementServiceWindow that did not
+/// exist before this route. Verifies the predicate matches
+/// exactly (not
+/// , which is also true for a
+/// merely-queued landblock — see the class's own doc comment) and
+/// canonicalizes a full ACE cell id to the landblock the tier map keys on.
+///
+public sealed class GraphicalRemotePlacementServiceWindowTests
+{
+ // GpuWorldState's tier map is keyed by the CANONICAL 0xFFFF-ending form
+ // (LoadedLandblock.LandblockId/AddLandblock require callers to already
+ // pass it that way — see StreamingControllerReadinessTests' own
+ // 0x1236FFFFu convention). The window itself is exercised with a full
+ // ACE outdoor-cell id (landblock high word + a real cell low word),
+ // matching CreateObject.ServerPosition.LandblockId's actual shape, to
+ // prove IsWithinServiceWindow canonicalizes it the same way
+ // GpuWorldState's own writers do.
+ private const uint LandblockHighWord = 0x0A0B0000u;
+ private const uint CanonicalLandblock = LandblockHighWord | 0xFFFFu;
+ private const uint OutdoorCell = LandblockHighWord | 0x0001u;
+
+ [Fact]
+ public void True_WhenTheLandblockIsNearTier()
+ {
+ var state = new GpuWorldState();
+ state.AddLandblock(new LoadedLandblock(
+ CanonicalLandblock,
+ new LandBlock(),
+ Array.Empty()));
+ var window = new GraphicalRemotePlacementServiceWindow(state);
+
+ Assert.True(window.IsWithinServiceWindow(OutdoorCell));
+ }
+
+ [Fact]
+ public void False_WhenTheLandblockWasNeverPublished()
+ {
+ var state = new GpuWorldState();
+ var window = new GraphicalRemotePlacementServiceWindow(state);
+
+ Assert.False(window.IsWithinServiceWindow(OutdoorCell));
+ }
+
+ [Fact]
+ public void False_WhenTheLandblockIsOnlyPendingNotYetCollisionPublished()
+ {
+ var state = new GpuWorldState();
+ // A live projection arriving before its landblock loads parks as
+ // "pending near tier" — GpuWorldState.IsNearTierOrPending reads true
+ // for this, but collision has NOT been published yet.
+ Assert.False(
+ state.AddEntitiesToExistingLandblock(
+ LandblockHighWord, Array.Empty()));
+ Assert.True(state.IsNearTierOrPending(CanonicalLandblock));
+ var window = new GraphicalRemotePlacementServiceWindow(state);
+
+ Assert.False(window.IsWithinServiceWindow(OutdoorCell));
+ }
+
+ [Fact]
+ public void False_AfterTheLandblockRetiresViaDetachNearLayer()
+ {
+ var state = new GpuWorldState();
+ state.AddLandblock(new LoadedLandblock(
+ CanonicalLandblock,
+ new LandBlock(),
+ Array.Empty()));
+ var window = new GraphicalRemotePlacementServiceWindow(state);
+ Assert.True(window.IsWithinServiceWindow(OutdoorCell));
+
+ _ = state.DetachNearLayer(LandblockHighWord);
+
+ Assert.False(window.IsWithinServiceWindow(OutdoorCell));
+ }
+}
diff --git a/tests/AcDream.Headless.Tests/HeadlessCollisionNeighborhoodServiceWindowTests.cs b/tests/AcDream.Headless.Tests/HeadlessCollisionNeighborhoodServiceWindowTests.cs
new file mode 100644
index 00000000..bcc110d7
--- /dev/null
+++ b/tests/AcDream.Headless.Tests/HeadlessCollisionNeighborhoodServiceWindowTests.cs
@@ -0,0 +1,215 @@
+using System.Collections.Immutable;
+using System.Reflection;
+using AcDream.Content;
+using AcDream.Core.Combat;
+using AcDream.Core.Items;
+using AcDream.Core.Physics;
+using AcDream.Core.Spells;
+using AcDream.Headless.Configuration;
+using AcDream.Headless.Hosting;
+using AcDream.Runtime;
+using AcDream.Runtime.Gameplay;
+using AcDream.Runtime.Session;
+
+namespace AcDream.Headless.Tests;
+
+///
+/// B1 review fix: implements TWO
+/// interfaces that share the identical bool IsWithinServiceWindow(uint)
+/// signature but ask different questions —
+/// is a
+/// pure geometry test ("can this landblock EVER collision-publish", true
+/// outright with no center requested), while
+///
+/// must answer "is it collision-published RIGHT NOW". This is the focused
+/// proof that the two answers genuinely diverge — before this fix a single
+/// method satisfied both interfaces, so both answers were identical (and
+/// wrong for the new interface's contract).
+///
+public sealed class HeadlessCollisionNeighborhoodServiceWindowTests
+{
+ [Fact]
+ public void ServiceWindowIsResidencyNotGeometry_UnpublishedLandblockIsRefusedDespiteGeometricMembership()
+ {
+ var factory = new FixtureContentFactory();
+ using var owner = new HeadlessProcessContentOwner(
+ ContentDescriptor(),
+ _ => { },
+ factory);
+ using HeadlessProcessContentOwner.HeadlessProcessContentLease lease =
+ owner.AcquireLease("fixture");
+ var operations = new FixtureGameplayOperations();
+ using var runtime = new GameRuntime(new GameRuntimeDependencies(
+ operations, operations, operations, operations));
+ var neighborhood = new HeadlessCollisionNeighborhood(runtime, lease);
+
+ // CenterOn was never called, so the geometry interface's own
+ // documented contract applies: "no center requested yet" => true
+ // (this landblock could theoretically EVER be served). Nothing has
+ // published ANY collision for it, though — the residency-based
+ // interface must say false.
+ const uint cell = 0xA9B40001u;
+ Assert.True(
+ ((IHeadlessCollisionNeighborhood)neighborhood)
+ .IsWithinServiceWindow(cell));
+ Assert.False(
+ ((IRuntimeRemotePlacementServiceWindow)neighborhood)
+ .IsWithinServiceWindow(cell));
+ }
+
+ ///
+ /// C2-3 review fix (delta round): BuildPublicationPlan publishes
+ /// the requested center's FULL 3x3 window, not just the exact center —
+ /// so a landblock this host HAS published but which is not the exact
+ /// _centerLandblock (a remote sitting one landblock off-center,
+ /// exactly the boundary population this route exists to serve) must
+ /// still read as currently published. Before this fix the predicate
+ /// inherited 's own
+ /// _centerLandblock != center restriction — correct for
+ /// IsReady's narrower question, wrong here — so only ONE of the
+ /// nine published landblocks would ever read true.
+ ///
+ /// Seeds _resident directly via reflection (no lightweight DAT
+ /// fixture in this test project can drive real 3x3 publication through
+ /// CenterOn — its dummy proxy makes
+ /// LandblockLoader.Load fail for every landblock, including a
+ /// REQUIRED center) — mirrors the existing reflection precedent
+ /// HeadlessSessionHostTests.SeedRuntimePlacement already uses for
+ /// otherwise-unreachable internal state. _centerLandblock is
+ /// deliberately left at its default (never set) — the whole point is
+ /// that this predicate no longer depends on it.
+ ///
+ ///
+ [Fact]
+ public void ServiceWindowCoversAPublishedNeighborLandblockNotOnlyTheExactCenter()
+ {
+ var factory = new FixtureContentFactory();
+ using var owner = new HeadlessProcessContentOwner(
+ ContentDescriptor(),
+ _ => { },
+ factory);
+ using HeadlessProcessContentOwner.HeadlessProcessContentLease lease =
+ owner.AcquireLease("fixture");
+ var operations = new FixtureGameplayOperations();
+ using var runtime = new GameRuntime(new GameRuntimeDependencies(
+ operations, operations, operations, operations));
+ var neighborhood = new HeadlessCollisionNeighborhood(runtime, lease);
+
+ const uint neighborLandblock = 0xA9B5FFFFu;
+ const uint neighborCell = 0xA9B50001u;
+ runtime.EntityObjects.Physics.Engine.AddLandblock(
+ neighborLandblock,
+ new TerrainSurface(new byte[81], new float[256]),
+ Array.Empty(),
+ Array.Empty(),
+ worldOffsetX: 0f,
+ worldOffsetY: 0f);
+ SeedResident(neighborhood, neighborLandblock);
+
+ Assert.True(
+ ((IRuntimeRemotePlacementServiceWindow)neighborhood)
+ .IsWithinServiceWindow(neighborCell));
+ }
+
+ private static void SeedResident(
+ HeadlessCollisionNeighborhood neighborhood,
+ uint landblockId)
+ {
+ FieldInfo field = typeof(HeadlessCollisionNeighborhood).GetField(
+ "_resident",
+ BindingFlags.NonPublic | BindingFlags.Instance)
+ ?? throw new MissingFieldException(
+ nameof(HeadlessCollisionNeighborhood), "_resident");
+ var resident = (HashSet)field.GetValue(neighborhood)!;
+ resident.Add(landblockId);
+ }
+
+ private static HeadlessContentDescriptor ContentDescriptor() => new()
+ {
+ DatDirectory = "fixture-dats",
+ PreparedAssetPath = "fixture.pak",
+ };
+
+ private sealed class FixtureContentFactory
+ : IHeadlessProcessContentFactory
+ {
+ internal FixtureContentFactory()
+ {
+ DatsResource =
+ DispatchProxy.Create();
+ PreparedResource =
+ DispatchProxy.Create();
+ }
+
+ internal IDatReaderWriter DatsResource { get; }
+ internal ITestPreparedSource PreparedResource { get; }
+
+ public HeadlessOpenedProcessContent Open(
+ HeadlessContentDescriptor descriptor,
+ Action diagnostic) =>
+ new(
+ DatsResource,
+ PreparedResource,
+ MagicCatalog.Empty,
+ ImmutableArray.CreateRange(new float[256]));
+ }
+
+ private sealed class FixtureGameplayOperations
+ : IRuntimeCombatAttackOperations,
+ IRuntimeCombatTargetOperations,
+ IRuntimeCombatModeOperations,
+ IRuntimeSpellCastOperations
+ {
+ public bool CanStartAttack() => false;
+ public void PrepareAttackRequest()
+ {
+ }
+
+ public bool SendAttack(AttackHeight height, float power) => false;
+ public void SendCancelAttack()
+ {
+ }
+
+ public bool IsDualWield => false;
+ public bool PlayerReadyForAttack => false;
+ public bool AutoRepeatAttack => false;
+ public bool AutoTarget => false;
+ public uint? SelectClosestTarget() => null;
+ public bool IsInWorld => false;
+ public IReadOnlyList GetOrderedEquipment() => [];
+ public void NotifyExplicitCombatModeRequest()
+ {
+ }
+
+ public void SendChangeCombatMode(CombatMode mode)
+ {
+ }
+
+ public uint LocalPlayerId => 0u;
+ public bool CanSend => false;
+ public bool HasRequiredComponents(uint spellId) => false;
+
+ public bool IsTargetCompatible(
+ uint targetId, SpellMetadata spell, bool showMessage) => false;
+
+ public void StopCompletely()
+ {
+ }
+
+ public void SendUntargeted(uint spellId)
+ {
+ }
+
+ public void SendTargeted(uint targetId, uint spellId)
+ {
+ }
+
+ public void DisplayMessage(string message)
+ {
+ }
+
+ public void IncrementBusy()
+ {
+ }
+ }
+}
diff --git a/tests/AcDream.Headless.Tests/HeadlessSessionEventRouteRetryPendingTests.cs b/tests/AcDream.Headless.Tests/HeadlessSessionEventRouteRetryPendingTests.cs
new file mode 100644
index 00000000..cd9d3f2c
--- /dev/null
+++ b/tests/AcDream.Headless.Tests/HeadlessSessionEventRouteRetryPendingTests.cs
@@ -0,0 +1,497 @@
+using System.Net;
+using System.Numerics;
+using AcDream.Content;
+using AcDream.Content.Pak;
+using AcDream.Core.Combat;
+using AcDream.Core.Items;
+using AcDream.Core.Net;
+using AcDream.Core.Net.Messages;
+using AcDream.Core.Physics;
+using AcDream.Core.Spells;
+using AcDream.Headless.Hosting;
+using AcDream.Runtime;
+using AcDream.Runtime.Entities;
+using AcDream.Runtime.Gameplay;
+using AcDream.Runtime.Physics;
+using AcDream.Runtime.Session;
+
+namespace AcDream.Headless.Tests;
+
+///
+/// C4 route 4b-1 (N3): HeadlessSessionEventRoute.Attach constructs its
+/// RuntimePlacementProjectionSubscription with
+/// retryPendingOnSubscribe: true, and that was the ONLY
+/// RetryPending call headless ever made — a Place a host sink declines
+/// (landblock not loaded, stale transit authority) is left at the FIFO head
+/// for its own later retry
+/// (RuntimePlacementProjectionSubscription.OnPlacement's doc comment),
+/// but nothing headless did ever asked again. This is the focused proof that
+/// — the method
+/// HeadlessSessionHost.Tick now calls every tick, immediately after
+/// HeadlessSessionWorldProjection.PumpFirstEntry — actually re-offers a
+/// declined head. Without a SECOND call, the declined receipt sits forever.
+///
+///
+/// Uses the SAME lightweight LiveSessionHost + no-op event/command
+/// route fixture as
+/// RuntimeAcceptedPositionDriveControllerTests.StartRuntime — it
+/// produces a genuine nonzero GameRuntime.Generation (required:
+/// RuntimePlacementProjectionChannel.IsCurrent rejects generation 0
+/// outright) WITHOUT wiring any real placement-projection subscription, so
+/// this test's own injected fake sink is the ONLY observer of the FIFO.
+/// HeadlessSessionHost.Start would also work generation-wise, but its
+/// own internal route always uses the real
+/// HeadlessRuntimePlacementProjectionSink, which would consume-and-
+/// acknowledge this test's synthetic Place before this test's own route ever
+/// subscribed.
+///
+///
+public sealed class HeadlessSessionEventRouteRetryPendingTests
+{
+ private const uint PlayerGuid = 0x50000001u;
+ private const uint Landblock = 0xC1000000u;
+ private const uint Cell = Landblock | 0x0001u;
+ private const float Height = 6f;
+
+ [Fact]
+ public void RetryPending_ReoffersAPreviouslyDeclinedHeadUntilTheSinkAccepts()
+ {
+ using StartedRuntime started = StartRuntime();
+ GameRuntime runtime = started.Runtime;
+ Assert.NotEqual(0UL, runtime.Generation.Value);
+
+ CommitLandblockCollision(runtime, Landblock);
+ RuntimeEntityRecord record = CreateRemoteRecord(runtime, 0x70004001u);
+ AttachBody(runtime, record, Cell);
+
+ RuntimeEntityPlacementToken token = runtime.EntityObjects.Physics
+ .SetPosition.TryBeginExclusiveAuthoredPlacement(
+ record,
+ record.PositionAuthorityVersion,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative);
+ Assert.True(token.IsValid);
+ RuntimeSetPositionMoverPreparationStatus status = runtime.EntityObjects
+ .Physics.SetPosition.TryPrepareAndSubmitAuthoredPlacement(
+ record,
+ token,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative,
+ PhysicsSetPositionFlags.Teleport | PhysicsSetPositionFlags.Slide,
+ new UnusedCollisionSource(),
+ gameTime: 10d,
+ out RuntimeSetPositionOutcome outcome,
+ resolveWorldOffsetFromRuntimeFrame: true);
+ Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared, status);
+ Assert.Equal(
+ RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
+ outcome.Status);
+
+ var sink = new DecliningThenAcceptingSink();
+ var events = new NoOpEventRoute();
+ var route = new HeadlessSessionEventRoute(events, runtime, sink);
+
+ // Attach's own subscribe-time retry (retryPendingOnSubscribe: true)
+ // is the ONLY chance the receipt gets today — the sink is still
+ // declining, so it must remain unacknowledged.
+ route.Attach();
+ Assert.Equal(1, sink.CallCount);
+ Assert.True(
+ runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
+ out _));
+
+ // The sink starts accepting (mirrors a landblock finishing streaming
+ // in) — but without a SECOND RetryPending call nothing re-offers the
+ // head. This is the exact gap N3 closes.
+ sink.Accept = true;
+ bool retried = route.RetryPending();
+
+ Assert.True(retried);
+ Assert.Equal(2, sink.CallCount);
+ Assert.False(
+ runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
+ out _));
+
+ route.Dispose();
+ }
+
+ ///
+ /// B5(c) review fix:
+ /// must refuse once Runtime's generation has moved past the one this
+ /// route attached under — mirroring the graphical host's
+ /// RuntimePlacementProjectionRetrySlot, which already refuses a
+ /// stale-generation callback the same way (BindOwned/
+ /// RetryPending's own guard). Before this fix headless
+ /// dereferenced its subscription directly with no equivalent latch, so a
+ /// route left live across a generation change (a reconnect race window)
+ /// could still fire a callback against a retired generation.
+ ///
+ [Fact]
+ public void RetryPending_RefusesOnceRuntimeGenerationHasMovedPastAttach()
+ {
+ using StartedRuntime started = StartRuntime();
+ GameRuntime runtime = started.Runtime;
+
+ CommitLandblockCollision(runtime, Landblock);
+ RuntimeEntityRecord record = CreateRemoteRecord(runtime, 0x70004002u);
+ AttachBody(runtime, record, Cell);
+
+ RuntimeEntityPlacementToken token = runtime.EntityObjects.Physics
+ .SetPosition.TryBeginExclusiveAuthoredPlacement(
+ record,
+ record.PositionAuthorityVersion,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative);
+ Assert.True(token.IsValid);
+ RuntimeSetPositionMoverPreparationStatus status = runtime.EntityObjects
+ .Physics.SetPosition.TryPrepareAndSubmitAuthoredPlacement(
+ record,
+ token,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative,
+ PhysicsSetPositionFlags.Teleport | PhysicsSetPositionFlags.Slide,
+ new UnusedCollisionSource(),
+ gameTime: 10d,
+ out RuntimeSetPositionOutcome outcome,
+ resolveWorldOffsetFromRuntimeFrame: true);
+ Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared, status);
+ Assert.Equal(
+ RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
+ outcome.Status);
+
+ var sink = new DecliningThenAcceptingSink();
+ var events = new NoOpEventRoute();
+ var route = new HeadlessSessionEventRoute(events, runtime, sink);
+ route.Attach();
+ Assert.Equal(1, sink.CallCount);
+
+ sink.Accept = true;
+ RuntimeGenerationToken attachedGeneration = runtime.Generation;
+ RuntimeTeardownAcknowledgement stopped =
+ started.Live.Stop(attachedGeneration);
+ Assert.True(stopped.IsComplete);
+ Assert.NotEqual(attachedGeneration, runtime.Generation);
+
+ // The route is STILL live here (never Disposed) — exactly the shape
+ // a reconnect race could leave it in for one host-tick window before
+ // the owner swaps in the replacement route.
+ bool retried = route.RetryPending();
+
+ Assert.False(retried);
+ // The stale-generation refusal must short-circuit BEFORE ever
+ // touching the subscription — the sink's call count must not move.
+ Assert.Equal(1, sink.CallCount);
+
+ route.Dispose();
+ }
+
+ // ── Fixture (mirrors RuntimeAcceptedPositionDriveControllerTests) ──────
+
+ private sealed class StartedRuntime : IDisposable
+ {
+ internal required GameRuntime Runtime { get; init; }
+ internal required LiveSessionHost Live { get; init; }
+
+ public void Dispose()
+ {
+ _ = Live.Stop(Runtime.Generation);
+ Runtime.Dispose();
+ }
+ }
+
+ private static StartedRuntime StartRuntime()
+ {
+ var operations = new FixtureGameplayOperations();
+ var sessionOperations = new FixtureSessionOperations();
+ var runtime = new GameRuntime(new GameRuntimeDependencies(
+ operations, operations, operations, operations,
+ SessionOperations: sessionOperations));
+ var resetHost = new FixtureResetHost();
+ var options = new LiveSessionConnectOptions(
+ true, "127.0.0.1", 9000, "account", "password");
+ var live = new LiveSessionHost(
+ runtime.Session,
+ new LiveSessionHostBindings(
+ new LiveSessionRoutingFactories(
+ _ => new NoOpEventRoute(),
+ _ => new NoOpCommandRoute()),
+ generation => runtime.ResetGeneration(generation, resetHost),
+ new LiveSessionSelectionBindings(
+ id => runtime.PlayerIdentity.ServerGuid = id,
+ _ => { },
+ runtime.CommunicationOwner.Chat.SetLocalPlayerGuid,
+ _ => { },
+ _ => { },
+ runtime.ActionOwner.Combat.Clear),
+ new LiveSessionEnteredWorldBindings(
+ _ => { }, () => { }, () => { }, _ => { }, () => { }),
+ (_, _, _) => { },
+ () => { }),
+ options);
+ LiveSessionStartResult startResult = live.Start(options);
+ Assert.Equal(LiveSessionStartStatus.Connected, startResult.Status);
+ Assert.NotEqual(0UL, runtime.Generation.Value);
+ return new StartedRuntime { Runtime = runtime, Live = live };
+ }
+
+ private static void CommitLandblockCollision(
+ GameRuntime runtime, uint landblockId)
+ {
+ var heights = new byte[81];
+ Array.Fill(heights, (byte)Height);
+ var heightTable = new float[256];
+ for (int index = 0; index < heightTable.Length; index++)
+ heightTable[index] = index;
+ runtime.EntityObjects.Physics.ObserveLocalWorldFrame(
+ landblockId | 0x0001u, teleportAdvanced: false);
+ runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
+ landblockId, 1UL);
+ runtime.EntityObjects.Physics.Engine.AddLandblock(
+ landblockId,
+ new TerrainSurface(heights, heightTable),
+ Array.Empty(),
+ Array.Empty(),
+ worldOffsetX: 0f,
+ worldOffsetY: 0f);
+ runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
+ landblockId, 1UL, ready: true);
+ }
+
+ private static RuntimeEntityRecord CreateRemoteRecord(
+ GameRuntime runtime, uint guid)
+ {
+ RuntimeEntityRecord record = runtime.EntityObjects.RegisterEntity(
+ new WorldSession.EntitySpawn(
+ Guid: guid,
+ Position: new CreateObject.ServerPosition(
+ Cell, 10f, 10f, Height, 1f, 0f, 0f, 0f),
+ SetupTableId: null,
+ AnimPartChanges: Array.Empty(),
+ TextureChanges: Array.Empty(),
+ SubPalettes: Array.Empty(),
+ BasePaletteId: null,
+ ObjScale: null,
+ Name: "remote",
+ ItemType: null,
+ MotionState: null,
+ MotionTableId: 0x09000001u))
+ .Canonical!;
+ runtime.EntityObjects.Entities.SetFinalPhysicsState(
+ record, PhysicsStateFlags.Gravity);
+ return record;
+ }
+
+ private static void AttachBody(
+ GameRuntime runtime, RuntimeEntityRecord record, uint cellId)
+ {
+ runtime.EntityObjects.Entities.SetFullCell(
+ record, cellId, (cellId & 0xFFFF0000u) | 0xFFFFu);
+ var body = new PhysicsBody
+ {
+ Position = new Vector3(10f, 10f, Height),
+ Orientation = Quaternion.Identity,
+ LastUpdateTime = 1d,
+ State = PhysicsStateFlags.Gravity,
+ TransientState = TransientStateFlags.Active,
+ };
+ body.SnapToCell(cellId, body.Position, body.Position);
+ runtime.EntityObjects.Entities.SetPhysicsBody(record, body);
+ record.ObjectClock.Activate();
+ runtime.EntityObjects.Physics.AcknowledgeSpatialProjection(
+ record, spatial: true);
+ }
+
+ private sealed class DecliningThenAcceptingSink
+ : IRuntimePlacementProjectionSink
+ {
+ internal int CallCount { get; private set; }
+ internal bool Accept { get; set; }
+
+ public bool TryApply(in RuntimePlacementProjectionSnapshot projection)
+ {
+ CallCount++;
+ return Accept;
+ }
+ }
+
+ private sealed class NoOpEventRoute : ILiveSessionEventRouting
+ {
+ public void Attach()
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+
+ private sealed class NoOpCommandRoute : ILiveSessionCommandRouting
+ {
+ public void Activate()
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+
+ private sealed class FixtureSessionOperations : ILiveSessionOperations
+ {
+ public IPEndPoint ResolveEndpoint(string host, int port) =>
+ new(IPAddress.Loopback, port);
+
+ public WorldSession CreateSession(IPEndPoint endpoint) =>
+ new(endpoint, new FixtureTransport());
+
+ public void Connect(WorldSession session, string user, string password)
+ {
+ }
+
+ public CharacterList.Parsed GetCharacters(WorldSession session) =>
+ new(
+ 0u,
+ [new CharacterList.Character(PlayerGuid, "Direct", 0u)],
+ [],
+ 11,
+ "account",
+ true,
+ true);
+
+ public void EnterWorld(WorldSession session, int activeCharacterIndex)
+ {
+ }
+
+ public void Tick(WorldSession session)
+ {
+ }
+
+ public void DisposeSession(WorldSession session) => session.Dispose();
+ }
+
+ private sealed class FixtureTransport : IWorldSessionTransport
+ {
+ public void Send(ReadOnlySpan datagram)
+ {
+ }
+
+ public void Send(IPEndPoint remote, ReadOnlySpan datagram)
+ {
+ }
+
+ public int Receive(
+ Span destination, TimeSpan timeout, out IPEndPoint? from)
+ {
+ from = null;
+ return -1;
+ }
+
+ public ValueTask ReceiveAsync(
+ Memory destination, CancellationToken cancellationToken) =>
+ ValueTask.FromException(
+ new OperationCanceledException(cancellationToken));
+
+ public void Dispose()
+ {
+ }
+ }
+
+ private sealed class FixtureResetHost : IRuntimeGenerationResetHost
+ {
+ public void RetireEntityProjection(RuntimeEntityRecord entity)
+ {
+ }
+
+ public void DrainEntityProjectionBoundary()
+ {
+ }
+
+ public void CompleteEntityProjectionRetirement()
+ {
+ }
+ }
+
+ private sealed class FixtureGameplayOperations
+ : IRuntimeCombatAttackOperations,
+ IRuntimeCombatTargetOperations,
+ IRuntimeCombatModeOperations,
+ IRuntimeSpellCastOperations
+ {
+ public bool CanStartAttack() => false;
+ public void PrepareAttackRequest()
+ {
+ }
+
+ public bool SendAttack(AttackHeight height, float power) => false;
+ public void SendCancelAttack()
+ {
+ }
+
+ public bool IsDualWield => false;
+ public bool PlayerReadyForAttack => false;
+ public bool AutoRepeatAttack => false;
+ public bool AutoTarget => false;
+ public uint? SelectClosestTarget() => null;
+ public bool IsInWorld => false;
+ public IReadOnlyList GetOrderedEquipment() => [];
+ public void NotifyExplicitCombatModeRequest()
+ {
+ }
+
+ public void SendChangeCombatMode(CombatMode mode)
+ {
+ }
+
+ public uint LocalPlayerId => 0u;
+ public bool CanSend => false;
+ public bool HasRequiredComponents(uint spellId) => false;
+
+ public bool IsTargetCompatible(
+ uint targetId, SpellMetadata spell, bool showMessage) => false;
+
+ public void StopCompletely()
+ {
+ }
+
+ public void SendUntargeted(uint spellId)
+ {
+ }
+
+ public void SendTargeted(uint targetId, uint spellId)
+ {
+ }
+
+ public void DisplayMessage(string message)
+ {
+ }
+
+ public void IncrementBusy()
+ {
+ }
+ }
+
+ private sealed class UnusedCollisionSource : IPreparedCollisionSource
+ {
+ public PreparedAssetPresence ProbeCollision(
+ PakAssetType type, uint sourceFileId) =>
+ PreparedAssetPresence.Available;
+
+ public PreparedCollisionReadResult ReadSetupCollision(
+ uint sourceFileId, CancellationToken cancellationToken = default) =>
+ PreparedCollisionReadResult.Missing;
+
+ public PreparedCollisionReadResult ReadGfxObjCollision(
+ uint sourceFileId, CancellationToken cancellationToken = default) =>
+ throw new NotSupportedException();
+
+ public PreparedCollisionReadResult ReadCellStructureCollision(
+ uint sourceFileId, CancellationToken cancellationToken = default) =>
+ throw new NotSupportedException();
+
+ public PreparedCollisionReadResult ReadEnvCellTopology(
+ uint sourceFileId, CancellationToken cancellationToken = default) =>
+ throw new NotSupportedException();
+
+ public PreparedCollisionSourceStats CollisionStats => default;
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs b/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs
index 08a02703..2dca2dce 100644
--- a/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs
+++ b/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs
@@ -225,6 +225,125 @@ public sealed class HeadlessSessionHostTests
Assert.True(host.Runtime.CaptureOwnership().IsConverged);
}
+ ///
+ /// B5(a) review fix:
+ /// proved the underlying re-offer MECHANISM works, but hand-constructed
+ /// directly and called
+ /// route.RetryPending() itself — it never touches
+ /// 's own
+ /// _eventRoute?.RetryPending() call. This test drives Tick
+ /// itself (via the placementSinkOverride test seam added for this
+ /// fix, mirroring the existing policyOverride parameter) so a
+ /// regression that deletes or reorders that exact line would fail HERE,
+ /// not just in the lower-level subscription test.
+ ///
+ [Fact]
+ public void TickRetriesAPreviouslyDeclinedPlacementThroughTheRealEventRoute()
+ {
+ const uint remote = 0x70004301u;
+ const uint landblock = 0xA9B40000u;
+ const uint cell = landblock | 0x0001u;
+ const float height = 6f;
+
+ var operations = new FixtureSessionOperations();
+ using var credential = new HeadlessCredentialSecret(
+ "fixture",
+ "password");
+ var sink = new DecliningThenAcceptingPlacementSink();
+ using var host = new HeadlessSessionHost(
+ Descriptor(),
+ credential,
+ new HeadlessDiagnosticWriter(TextWriter.Null),
+ operations,
+ placementSinkOverride: sink);
+ GameRuntime runtime = host.Runtime;
+ Assert.Equal(
+ RuntimeSessionStartStatus.Connected,
+ host.Start().Status);
+
+ runtime.EntityObjects.Physics.ObserveLocalWorldFrame(
+ cell, teleportAdvanced: false);
+ runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
+ landblock, 1UL);
+ AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
+ runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
+ landblock, 1UL, ready: true);
+
+ RuntimeEntityRecord record = runtime.EntityObjects
+ .RegisterEntity(Spawn(remote, cell))
+ .Canonical!;
+ runtime.EntityObjects.Entities.SetFinalPhysicsState(
+ record, PhysicsStateFlags.Gravity);
+ runtime.EntityObjects.Entities.SetFullCell(
+ record, cell, landblock);
+ var body = new PhysicsBody
+ {
+ Position = new Vector3(10f, 10f, height),
+ Orientation = Quaternion.Identity,
+ LastUpdateTime = 1d,
+ State = PhysicsStateFlags.Gravity,
+ TransientState = TransientStateFlags.Active,
+ };
+ body.SnapToCell(cell, body.Position, body.Position);
+ runtime.EntityObjects.Entities.SetPhysicsBody(record, body);
+ record.ObjectClock.Activate();
+ runtime.EntityObjects.Physics.AcknowledgeSpatialProjection(
+ record, spatial: true);
+
+ RuntimeEntityPlacementToken token = runtime.EntityObjects.Physics
+ .SetPosition.TryBeginExclusiveAuthoredPlacement(
+ record,
+ record.PositionAuthorityVersion,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative);
+ Assert.True(token.IsValid);
+ RuntimeSetPositionMoverPreparationStatus status = runtime.EntityObjects
+ .Physics.SetPosition.TryPrepareAndSubmitAuthoredPlacement(
+ record,
+ token,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative,
+ PhysicsSetPositionFlags.Teleport | PhysicsSetPositionFlags.Slide,
+ new LoadedSetupCollisionSource(),
+ gameTime: runtime.Clock.SimulationTimeSeconds,
+ out RuntimeSetPositionOutcome outcome,
+ resolveWorldOffsetFromRuntimeFrame: true);
+ Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared, status);
+ Assert.Equal(
+ RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
+ outcome.Status);
+
+ // The production HeadlessSessionEventRoute's subscription attached
+ // during host.Start() already observed this Place synchronously —
+ // the fake sink is still declining, so it must remain unacknowledged.
+ Assert.Equal(1, sink.CallCount);
+ Assert.True(
+ runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
+ out _));
+
+ // The sink starts accepting (mirrors a landblock finishing streaming
+ // in) — driving ONE real host tick is what must re-offer the head,
+ // through Tick's own wiring, not a hand-built route.
+ sink.Accept = true;
+ host.Tick(0.015d);
+
+ Assert.Equal(2, sink.CallCount);
+ Assert.False(
+ runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
+ out _));
+ }
+
+ private sealed class DecliningThenAcceptingPlacementSink
+ : IRuntimePlacementProjectionSink
+ {
+ internal int CallCount { get; private set; }
+ internal bool Accept { get; set; }
+
+ public bool TryApply(in RuntimePlacementProjectionSnapshot projection)
+ {
+ CallCount++;
+ return Accept;
+ }
+ }
+
[Fact]
public void WorldProjectionHydratesCanonicalMovementAndTeleportState()
{
diff --git a/tests/AcDream.Runtime.Tests/Session/RuntimeRemotePlacementDriveControllerTests.cs b/tests/AcDream.Runtime.Tests/Session/RuntimeRemotePlacementDriveControllerTests.cs
new file mode 100644
index 00000000..3d44298a
--- /dev/null
+++ b/tests/AcDream.Runtime.Tests/Session/RuntimeRemotePlacementDriveControllerTests.cs
@@ -0,0 +1,1072 @@
+using System.Numerics;
+using AcDream.Content;
+using AcDream.Content.Pak;
+using AcDream.Core.Net;
+using AcDream.Core.Net.Messages;
+using AcDream.Core.Physics;
+using AcDream.Runtime.Entities;
+using AcDream.Runtime.Physics;
+using AcDream.Runtime.Session;
+
+namespace AcDream.Runtime.Tests.Session;
+
+///
+/// C4 route 4b-1: focused tests for the Runtime-owned, per-entity remote
+/// placement infrastructure. This route wires NO production caller — every
+/// test below constructs an already-classified
+/// directly (the same shape
+///
+/// would hand a real caller) so each scenario is exercised precisely, mirroring
+/// RuntimeSetPositionStateTests' bare-
+/// fixture rather than the full GameRuntime/LiveSessionHost
+/// harness route 2's tests use — this controller has no local-player
+/// controller, outbound session, or generation dependency to bootstrap.
+///
+/// Each test was verified to actually discriminate its own fix by temporarily
+/// reverting the corresponding guard/omission in
+/// and confirming the
+/// matching test failed, then restoring it.
+///
+public sealed class RuntimeRemotePlacementDriveControllerTests
+{
+ private const uint SourceLandblock = 0xB1000000u;
+ private const uint SourceCell = SourceLandblock | 0x0001u;
+ private const uint DestinationLandblock = 0xB2000000u;
+ private const uint DestinationCell = DestinationLandblock | 0x0001u;
+ private const float SpawnHeight = 7f;
+
+ [Fact]
+ public void NotApplicable_WhenDispositionIsNotOwnedByThisRoute()
+ {
+ foreach (RuntimeAuthoritativePositionDisposition disposition in
+ new[]
+ {
+ RuntimeAuthoritativePositionDisposition.Interpolate,
+ RuntimeAuthoritativePositionDisposition.NoPositionOperation,
+ RuntimeAuthoritativePositionDisposition.RejectedAuthority,
+ RuntimeAuthoritativePositionDisposition.RejectedData,
+ RuntimeAuthoritativePositionDisposition.AwaitFreshPosition,
+ })
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003001u);
+ AttachBody(lifetime, record, SourceCell);
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+
+ RuntimeAuthoritativePositionRoute route = MakeRoute(
+ record, disposition, DestinationCell);
+
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.NotApplicable,
+ drive.TryExecuteAcceptedRemotePosition(record, route));
+ Assert.Equal(0, drive.PendingCount);
+ AssertConverged(lifetime);
+ }
+ }
+
+ [Fact]
+ public void NotApplicable_WhenTheEntityHasNoCanonicalBody()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003002u);
+ // Deliberately never attach a body.
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+
+ RuntimeAuthoritativePositionRoute route = MakeRoute(
+ record,
+ RuntimeAuthoritativePositionDisposition.SetPosition,
+ DestinationCell);
+
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.NotApplicable,
+ drive.TryExecuteAcceptedRemotePosition(record, route));
+ }
+
+ ///
+ /// B6 review fix: OwnsPlacement keyed on Disposition alone
+ /// would claim this route too — the classifier emits
+ /// SetPositionSimple for the LOCAL PLAYER's FORCE_POSITION/
+ /// teleport branches (RuntimeSetPositionOperationKind.LocalAuthoritative),
+ /// not only for remotes. The static predicate itself is exercised
+ /// directly (no entity/body needed) since it takes only the route.
+ ///
+ [Fact]
+ public void OwnsPlacement_FalseWhenOperationKindIsNotRemoteAuthoritative()
+ {
+ // RuntimeSetPositionOperationKind is internal, so a public [Theory]
+ // cannot take it as a parameter (CS0051) — iterate directly instead,
+ // mirroring NotApplicable_WhenDispositionIsNotOwnedByThisRoute's own
+ // foreach-over-internal-enum shape.
+ foreach (RuntimeSetPositionOperationKind operationKind in
+ new[]
+ {
+ RuntimeSetPositionOperationKind.InitialLogin,
+ RuntimeSetPositionOperationKind.LocalAuthoritative,
+ RuntimeSetPositionOperationKind.ProjectileAuthoritative,
+ })
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x7000300Fu);
+
+ RuntimeAuthoritativePositionRoute route = MakeRoute(
+ record,
+ RuntimeAuthoritativePositionDisposition.SetPositionSimple,
+ DestinationCell,
+ operationKind: operationKind);
+
+ Assert.False(
+ RuntimeRemotePlacementDriveController.OwnsPlacement(route));
+
+ // End to end: this controller must decline the SAME route as
+ // NotApplicable, not merely the static predicate in isolation.
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive =
+ CreateDrive(lifetime, window);
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.NotApplicable,
+ drive.TryExecuteAcceptedRemotePosition(record, route));
+ Assert.Equal(0, drive.PendingCount);
+ }
+ }
+
+ ///
+ /// C2-4 review fix (delta round): the OperationKind is RemoteAuthoritative
+ /// guard alone is STILL not exact — RuntimeAuthoritativePositionRouteClassifier
+ /// .ClassifyCreate emits Disposition = SetPosition AND
+ /// OperationKind = RemoteAuthoritative for a REMOTE top-level
+ /// initial Create too (only the LOCAL PLAYER's Create maps to
+ /// InitialLogin), so both the disposition and operation-kind
+ /// guards pass for a Create-shaped route. Retail's own flag choice is
+ /// what actually distinguishes them: a Create carries
+ /// InitialCreateFlags (Placement|Slide, no
+ /// Teleport); every remote accepted-Position SetPosition/
+ /// SetPositionSimple route carries AuthoritativeTeleportFlags
+ /// (Teleport|Slide|SendPositionEvent). Route 4b-1 is a
+ /// POSITION-only route — the first-entry conductor owns every Create —
+ /// so a Create-shaped route reaching this controller must be declined.
+ ///
+ [Fact]
+ public void OwnsPlacement_FalseForARemoteTopLevelCreateShapedRoute()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003014u);
+
+ RuntimeAuthoritativePositionRoute createRoute = MakeRoute(
+ record,
+ RuntimeAuthoritativePositionDisposition.SetPosition,
+ DestinationCell,
+ operationKind: RuntimeSetPositionOperationKind.RemoteAuthoritative,
+ setPositionFlags: PhysicsSetPositionFlags.Placement
+ | PhysicsSetPositionFlags.Slide);
+
+ Assert.False(
+ RuntimeRemotePlacementDriveController.OwnsPlacement(createRoute));
+
+ // End to end: this controller must decline the SAME route as
+ // NotApplicable, not merely the static predicate in isolation.
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive =
+ CreateDrive(lifetime, window);
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.NotApplicable,
+ drive.TryExecuteAcceptedRemotePosition(record, createRoute));
+ Assert.Equal(0, drive.PendingCount);
+
+ // The genuine remote accepted-Position shape (SAME disposition, SAME
+ // operation kind, Teleport-flagged) must still be owned — the fix
+ // must not have over-corrected into refusing everything.
+ RuntimeAuthoritativePositionRoute positionRoute = MakeRoute(
+ record,
+ RuntimeAuthoritativePositionDisposition.SetPosition,
+ DestinationCell);
+ Assert.True(
+ RuntimeRemotePlacementDriveController.OwnsPlacement(positionRoute));
+ }
+
+ ///
+ /// The central decision's core pin: a destination this host's service
+ /// window does not currently cover must be refused, not parked — no
+ /// operation may open at all, so the body's own InWorld/object
+ /// clock are never touched.
+ ///
+ [Fact]
+ public void Refused_WhenDestinationIsNotWithinServiceWindow_NoOperationOpensAndBodyStaysInWorld()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003003u);
+ PhysicsBody body = AttachBody(lifetime, record, SourceCell);
+ // The service window allows nothing — mirrors a destination
+ // landblock this host has never collision-published.
+ var window = new FakeServiceWindow();
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+
+ RuntimeAuthoritativePositionRoute route = MakeRoute(
+ record,
+ RuntimeAuthoritativePositionDisposition.SetPosition,
+ DestinationCell);
+
+ RuntimeRemotePlacementExecutionStatus status =
+ drive.TryExecuteAcceptedRemotePosition(record, route);
+
+ Assert.Equal(RuntimeRemotePlacementExecutionStatus.Refused, status);
+ Assert.True(body.InWorld);
+ Assert.True(record.ObjectClock.IsActive);
+ Assert.Equal(
+ 0,
+ lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
+ AssertConverged(lifetime);
+ }
+
+ ///
+ /// The exact §"central decision" sequence the contract's acceptance
+ /// section pins: attempt a placement whose destination is refused, then
+ /// deliver the entity's NEXT accepted Position — which, whatever it
+ /// classifies to, ALWAYS runs RuntimeSetPositionState.Forget for
+ /// this entity first
+ /// (, called
+ /// unconditionally regardless of disposition). Because nothing was ever
+ /// parked in the first step, that unconditional Forget has nothing to
+ /// discard, and the entity is STILL in the world afterward — the
+ /// invisible-and-intangible failure this route exists to prevent.
+ ///
+ [Fact]
+ public void CentralDecision_RefusedPlacementThenNextAcceptedPositionForgetsNothing_EntityStaysInWorld()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003004u);
+ PhysicsBody body = AttachBody(lifetime, record, SourceCell);
+ var window = new FakeServiceWindow();
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+
+ RuntimeAuthoritativePositionRoute route = MakeRoute(
+ record,
+ RuntimeAuthoritativePositionDisposition.SetPosition,
+ DestinationCell);
+
+ // Packet N: destination not placeable now.
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.Refused,
+ drive.TryExecuteAcceptedRemotePosition(record, route));
+ Assert.True(body.InWorld);
+ Assert.True(record.ObjectClock.IsActive);
+
+ // Packet N+1 ~150 ms later: whatever it classifies to (here,
+ // Interpolate — the caller never reaches this controller at all for
+ // that disposition), production's merge unconditionally Forgets any
+ // in-flight operation for this entity FIRST. Simulated directly
+ // (mirrors RuntimeAcceptedPositionDriveControllerTests'
+ // Equal_ClearsPendingWithoutReissuing... fixture, which drives the
+ // same production Forget call without a full second network
+ // round-trip).
+ RuntimePlacementCancellationReceipt cancellation =
+ lifetime.Physics.SetPosition.Forget(record);
+ if (cancellation.IsValid)
+ lifetime.Physics.SetPosition.PublishCancellation(cancellation);
+
+ Assert.True(body.InWorld);
+ Assert.True(record.ObjectClock.IsActive);
+ Assert.Equal(SourceCell, record.FullCellId);
+ AssertConverged(lifetime);
+ }
+
+ [Fact]
+ public void Committed_WhenDestinationIsWithinServiceWindowAndCollisionGenerationCommitted()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ CommitLandblockCollision(lifetime, DestinationLandblock);
+ RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003005u);
+ PhysicsBody body = AttachBody(lifetime, record, SourceCell);
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+
+ var destination = new Vector3(12f, 14f, SpawnHeight);
+ RuntimeAuthoritativePositionRoute route = MakeRoute(
+ record,
+ RuntimeAuthoritativePositionDisposition.SetPosition,
+ DestinationCell,
+ destination);
+
+ RuntimeRemotePlacementExecutionStatus status =
+ drive.TryExecuteAcceptedRemotePosition(record, route);
+
+ Assert.Equal(RuntimeRemotePlacementExecutionStatus.Committed, status);
+ // Runtime's world-frame resolution shifts an authored landblock-local
+ // position into Runtime's continuous world frame
+ // (resolveWorldOffsetFromRuntimeFrame: true) — DestinationLandblock
+ // sits +192m on X from SourceLandblock per CommitLandblockCollision.
+ Assert.Equal(destination + new Vector3(192f, 0f, 0f), body.Position);
+ Assert.Equal(0, drive.PendingCount);
+ // No host subscription is wired in this bare fixture — drain the
+ // Place receipt exactly like production's placement-projection
+ // subscription would, so the operation itself (not this drive's own
+ // ledger) also converges to zero.
+ DrainPlacementFifo(lifetime);
+ Assert.Equal(
+ 0, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
+ AssertConverged(lifetime);
+ }
+
+ ///
+ /// B4 review fix: SubmitAndResolve's Committed branch returns and
+ /// retains nothing in _pending, but when nothing synchronously
+ /// consumes-and-acknowledges the Place receipt (this bare fixture has no
+ /// host subscription wired — production's declined-sink retry scenario
+ /// is the identical shape), the operation stays live in Core's
+ /// _operations map until something later calls
+ /// AcknowledgeProjection. Before this fix
+ /// RemotePlacementDrivePendingCount was blind to that live
+ /// operation the instant Committed was returned. This proves it is
+ /// now visible, and that it still converges to zero once the receipt is
+ /// actually acknowledged.
+ ///
+ [Fact]
+ public void Committed_UnacknowledgedOperationStaysVisibleInTheLedgerUntilAcknowledged()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ CommitLandblockCollision(lifetime, DestinationLandblock);
+ RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003010u);
+ AttachBody(lifetime, record, SourceCell);
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+
+ var destination = new Vector3(12f, 14f, SpawnHeight);
+ RuntimeAuthoritativePositionRoute route = MakeRoute(
+ record,
+ RuntimeAuthoritativePositionDisposition.SetPosition,
+ DestinationCell,
+ destination);
+
+ RuntimeRemotePlacementExecutionStatus status =
+ drive.TryExecuteAcceptedRemotePosition(record, route);
+
+ Assert.Equal(RuntimeRemotePlacementExecutionStatus.Committed, status);
+ // No host subscription is wired in this bare fixture, so the Place
+ // receipt is still genuinely unacknowledged — the ledger must SEE it.
+ Assert.Equal(
+ 1, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
+ Assert.Equal(
+ 1,
+ lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
+
+ DrainPlacementFifo(lifetime);
+
+ Assert.Equal(
+ 0, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
+ AssertConverged(lifetime);
+ }
+
+ [Fact]
+ public void Contention_WhenTheEntityAlreadyOwnsAnActiveOperation()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ CommitLandblockCollision(lifetime, DestinationLandblock);
+ RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003006u);
+ PhysicsBody body = AttachBody(lifetime, record, SourceCell);
+ Vector3 positionBefore = body.Position;
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+
+ // An external placement authority (portal/teleport/another route)
+ // already owns this entity's SetPosition operation.
+ RuntimeEntityPlacementToken displaced = lifetime.Physics.SetPosition
+ .TryBeginExclusiveAuthoredPlacement(
+ record,
+ record.PositionAuthorityVersion,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative);
+ Assert.True(displaced.IsValid);
+
+ RuntimeAuthoritativePositionRoute route = MakeRoute(
+ record,
+ RuntimeAuthoritativePositionDisposition.SetPosition,
+ DestinationCell);
+
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.Contention,
+ drive.TryExecuteAcceptedRemotePosition(record, route));
+ Assert.Equal(positionBefore, body.Position);
+ Assert.Equal(0, drive.PendingCount);
+
+ RuntimePlacementCancellationReceipt cancellation = lifetime.Physics
+ .SetPosition.ForgetExactPlacement(displaced);
+ if (cancellation.IsValid)
+ lifetime.Physics.SetPosition.PublishCancellation(cancellation);
+ }
+
+ ///
+ /// Per-entity independence: two remotes interleaved must not share any
+ /// state — one entity's Contention must never block the other's Begin,
+ /// and each converges to zero independently.
+ ///
+ [Fact]
+ public void PerEntityIndependence_TwoRemotesInterleavedDoNotBlockEachOther()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ CommitLandblockCollision(lifetime, DestinationLandblock);
+ RuntimeEntityRecord first = CreateRemoteRecord(lifetime, 0x70003007u);
+ RuntimeEntityRecord second = CreateRemoteRecord(lifetime, 0x70003008u);
+ PhysicsBody firstBody = AttachBody(lifetime, first, SourceCell);
+ PhysicsBody secondBody = AttachBody(lifetime, second, SourceCell);
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+
+ // First entity's own operation is still outstanding (an external
+ // authority holds it) when the second entity's Position arrives.
+ RuntimeEntityPlacementToken firstDisplaced = lifetime.Physics.SetPosition
+ .TryBeginExclusiveAuthoredPlacement(
+ first,
+ first.PositionAuthorityVersion,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative);
+ Assert.True(firstDisplaced.IsValid);
+
+ RuntimeAuthoritativePositionRoute firstRoute = MakeRoute(
+ first, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
+ RuntimeAuthoritativePositionRoute secondRoute = MakeRoute(
+ second, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell,
+ new Vector3(20f, 22f, SpawnHeight));
+
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.Contention,
+ drive.TryExecuteAcceptedRemotePosition(first, firstRoute));
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.Committed,
+ drive.TryExecuteAcceptedRemotePosition(second, secondRoute));
+
+ Assert.Equal(
+ new Vector3(20f, 22f, SpawnHeight) + new Vector3(192f, 0f, 0f),
+ secondBody.Position);
+ Assert.Equal(0, drive.PendingCount);
+
+ RuntimePlacementCancellationReceipt cancellation = lifetime.Physics
+ .SetPosition.ForgetExactPlacement(firstDisplaced);
+ if (cancellation.IsValid)
+ lifetime.Physics.SetPosition.PublishCancellation(cancellation);
+ DrainPlacementFifo(lifetime);
+ _ = firstBody;
+ }
+
+ ///
+ /// Per-entity independence, the shape route 2's single _pending
+ /// slot cannot express: TWO entities each hold their OWN outstanding
+ /// preparation retry at the same time. Route 2's
+ /// RetainPending throws if a second live entry would displace the
+ /// first; this per-key map must track both simultaneously and let each
+ /// resolve (or die) independently.
+ ///
+ [Fact]
+ public void PerEntityIndependence_TwoConcurrentPreparationRetriesDoNotCollide()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ CommitLandblockCollision(lifetime, DestinationLandblock);
+ RuntimeEntityRecord first = CreateRemoteRecord(
+ lifetime, 0x7000300Du, setupTableId: 0x02000001u);
+ RuntimeEntityRecord second = CreateRemoteRecord(
+ lifetime, 0x7000300Eu, setupTableId: 0x02000001u);
+ AttachBody(lifetime, first, SourceCell);
+ AttachBody(lifetime, second, SourceCell);
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+
+ RuntimeAuthoritativePositionRoute firstRoute = MakeRoute(
+ first, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
+ RuntimeAuthoritativePositionRoute secondRoute = MakeRoute(
+ second, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
+
+ // Both entities' Setup assets are unresolved (Missing) — both retry.
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.Contention,
+ drive.TryExecuteAcceptedRemotePosition(first, firstRoute));
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.Contention,
+ drive.TryExecuteAcceptedRemotePosition(second, secondRoute));
+ Assert.Equal(2, drive.PendingCount);
+
+ // The FIRST entity's retry dies (an unrelated accepted Position for
+ // IT ALONE forgets it); the second must remain completely
+ // unaffected — a shared/overwriting single slot could not represent
+ // this.
+ RuntimePlacementCancellationReceipt cancellation =
+ lifetime.Physics.SetPosition.Forget(first);
+ if (cancellation.IsValid)
+ lifetime.Physics.SetPosition.PublishCancellation(cancellation);
+
+ drive.Advance();
+
+ Assert.Equal(1, drive.PendingCount);
+ // The second entity's own operation is still tracked — the first's
+ // cancellation removed exactly one operation, not both.
+ Assert.Equal(
+ 1, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
+
+ // Cleanup: retire the second entity's still-live retry too.
+ RuntimePlacementCancellationReceipt secondCancellation =
+ lifetime.Physics.SetPosition.Forget(second);
+ if (secondCancellation.IsValid)
+ {
+ lifetime.Physics.SetPosition
+ .PublishCancellation(secondCancellation);
+ }
+ drive.Advance();
+ Assert.Equal(0, drive.PendingCount);
+ }
+
+ ///
+ /// B3 review fix: a retained preparation retry must re-check the SAME
+ /// service-window guard the entry point uses before Advance()
+ /// resubmits it — a destination that retired its collision publication in
+ /// the frames since the retry was retained must be dropped, not
+ /// resubmitted. Discriminates from the OLD unconditional resubmit:
+ /// resubmitting against the SAME always-Missing collision source
+ /// returns the SAME retryable status, so without the re-check the entry
+ /// would simply be re-retained (PendingCount stays 1) even though
+ /// the window already forbids the destination — never converging.
+ ///
+ [Fact]
+ public void Advance_DropsRetainedRetryWhenTheServiceWindowNoLongerCoversItsDestination()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ RuntimeEntityRecord record = CreateRemoteRecord(
+ lifetime, 0x70003011u, setupTableId: 0x02000001u);
+ AttachBody(lifetime, record, SourceCell);
+ CommitLandblockCollision(lifetime, DestinationLandblock);
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+
+ RuntimeAuthoritativePositionRoute route = MakeRoute(
+ record, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
+
+ // The Setup asset is unresolved (Missing) — the first attempt
+ // retries rather than commits, retaining the entry.
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.Contention,
+ drive.TryExecuteAcceptedRemotePosition(record, route));
+ Assert.Equal(1, drive.PendingCount);
+ Assert.Equal(
+ 1, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
+
+ // The remote wandered back out of range between this retry being
+ // retained and the next host cadence pump.
+ window.Forbid(DestinationLandblock);
+
+ drive.Advance();
+
+ Assert.Equal(0, drive.PendingCount);
+ Assert.Equal(
+ 0, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
+ AssertConverged(lifetime);
+ }
+
+ ///
+ /// Currency: a preparation-retry entry left tracking a token some OTHER
+ /// caller already Forgot must not leak once this entity's NEXT packet
+ /// takes a path that itself never touches _pending (here, a
+ /// service-window refusal) — proving the self-heal is doing real work,
+ /// not merely being masked by the retry branch's own dictionary
+ /// overwrite (which would hide the leak if this test only ever re-hit
+ /// the SAME retryable status).
+ ///
+ [Fact]
+ public void StalePreparationRetry_SelfHealsRatherThanLeakingWhenTheNextPacketNeverTouchesPending()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ RuntimeEntityRecord record = CreateRemoteRecord(
+ lifetime, 0x70003009u, setupTableId: 0x02000001u);
+ AttachBody(lifetime, record, SourceCell);
+ CommitLandblockCollision(lifetime, DestinationLandblock);
+ // A Setup asset that never resolves (Missing) — the first attempt
+ // through this collision source retries rather than commits.
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+
+ RuntimeAuthoritativePositionRoute route = MakeRoute(
+ record, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
+
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.Contention,
+ drive.TryExecuteAcceptedRemotePosition(record, route));
+ Assert.Equal(1, drive.PendingCount);
+
+ // An unrelated accepted Position for this SAME entity Forgets the
+ // retained retry before it ever resolves — the mundane 5-10 Hz case.
+ RuntimePlacementCancellationReceipt cancellation =
+ lifetime.Physics.SetPosition.Forget(record);
+ if (cancellation.IsValid)
+ lifetime.Physics.SetPosition.PublishCancellation(cancellation);
+
+ // The destination is no longer serviceable by the NEXT packet (the
+ // remote wandered back out of range) — the Refused branch never
+ // assigns or clears _pending itself, so only the entry-point
+ // self-heal can retire the dead entry left behind above.
+ window.Forbid(DestinationLandblock);
+ RuntimeAuthoritativePositionRoute refusedRoute = MakeRoute(
+ record, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
+
+ RuntimeRemotePlacementExecutionStatus status =
+ drive.TryExecuteAcceptedRemotePosition(record, refusedRoute);
+ Assert.Equal(RuntimeRemotePlacementExecutionStatus.Refused, status);
+ Assert.Equal(0, drive.PendingCount);
+ AssertConverged(lifetime);
+ }
+
+ ///
+ /// C2-1 review fix (delta round): the retained _pending entry
+ /// holds a LIVE Core operation (already begun via
+ /// TryBeginExclusiveAuthoredPlacement, sitting at
+ /// AwaitingPreparation) — clearing this controller's own map on
+ /// detach is not enough; the operation itself must be cancelled or it
+ /// pins its landblock prefix forever (docs/ISSUES.md #310). Asserting
+ /// SetPositionOperationCount == 0 in addition to the ledger going
+ /// to zero is what actually proves the Core operation died, not just
+ /// that this controller stopped watching it — sibling tests in this file
+ /// (Committed_..., ParkCollisionResidents_...) already
+ /// check this dimension; this one previously did not, and would have
+ /// passed even with the old clear-only DetachRoute.
+ ///
+ [Fact]
+ public void LedgerConverges_AfterDetachRouteClearsTrackedEntries()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ RuntimeEntityRecord record = CreateRemoteRecord(
+ lifetime, 0x7000300Au, setupTableId: 0x02000001u);
+ AttachBody(lifetime, record, SourceCell);
+ CommitLandblockCollision(lifetime, DestinationLandblock);
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+ var route = new object();
+ drive.AttachRoute(route);
+
+ RuntimeAuthoritativePositionRoute positionRoute = MakeRoute(
+ record, RuntimeAuthoritativePositionDisposition.SetPosition, DestinationCell);
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.Contention,
+ drive.TryExecuteAcceptedRemotePosition(record, positionRoute));
+ Assert.Equal(1, drive.PendingCount);
+ Assert.Equal(
+ 1, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
+
+ drive.DetachRoute(route);
+
+ Assert.Equal(0, drive.PendingCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
+ Assert.Equal(
+ 0, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
+ }
+
+ ///
+ /// C2-1 review fix (delta round): the OTHER map — _awaitingAcknowledgement
+ /// — holds an equally live Core operation (a published, unacknowledged
+ /// Place sitting at AwaitingCommitAcknowledgement), and the
+ /// prior DetachRoute left it live the same way. Mirrors the
+ /// _pending case above for the SECOND map DetachRoute must cancel.
+ ///
+ [Fact]
+ public void LedgerConverges_AfterDetachRouteCancelsAnUnacknowledgedCommit()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ CommitLandblockCollision(lifetime, DestinationLandblock);
+ RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003012u);
+ AttachBody(lifetime, record, SourceCell);
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+ var route = new object();
+ drive.AttachRoute(route);
+
+ RuntimeAuthoritativePositionRoute positionRoute = MakeRoute(
+ record,
+ RuntimeAuthoritativePositionDisposition.SetPosition,
+ DestinationCell,
+ new Vector3(12f, 14f, SpawnHeight));
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.Committed,
+ drive.TryExecuteAcceptedRemotePosition(record, positionRoute));
+ // No host subscription is wired in this bare fixture, so the Place
+ // receipt is still genuinely unacknowledged — the SAME shape the
+ // production declined-sink FIFO retry class leaves behind.
+ Assert.Equal(
+ 1, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
+ Assert.Equal(
+ 1, lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
+
+ drive.DetachRoute(route);
+
+ Assert.Equal(
+ 0, lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
+ Assert.Equal(
+ 0, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
+ }
+
+ ///
+ /// C2-1 review fix (delta round), disposal safety:
+ /// CountLiveAwaitingAcknowledgement calls
+ /// RuntimeSetPositionState.IsPlacementCurrent, whose first
+ /// statement is EnsureNotDisposed — it THROWS once disposed. A
+ /// post-Dispose() CaptureOwnership() read is the designed
+ /// contract (GameWindowLifetime.DisposeGameRuntime:
+ /// runtime.Dispose(); runtime.CaptureOwnership();), so this
+ /// dimension must survive it too. Deliberately leaves the entry
+ /// UNDRAINED at dispose time — a genuine leak, exactly what this ledger
+ /// exists to report — to prove the disposed branch reports it rather
+ /// than hiding it behind a thrown exception.
+ ///
+ [Fact]
+ public void CountLiveAwaitingAcknowledgement_SurvivesReadAfterDisposeWithoutThrowing()
+ {
+ var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ CommitLandblockCollision(lifetime, DestinationLandblock);
+ RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70003013u);
+ AttachBody(lifetime, record, SourceCell);
+ var window = new FakeServiceWindow();
+ window.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+
+ RuntimeAuthoritativePositionRoute positionRoute = MakeRoute(
+ record,
+ RuntimeAuthoritativePositionDisposition.SetPosition,
+ DestinationCell,
+ new Vector3(12f, 14f, SpawnHeight));
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.Committed,
+ drive.TryExecuteAcceptedRemotePosition(record, positionRoute));
+
+ lifetime.Dispose();
+ int pendingAfterDispose = -1;
+ Exception? thrown = Record.Exception(() =>
+ pendingAfterDispose =
+ lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
+
+ Assert.Null(thrown);
+ Assert.Equal(1, pendingAfterDispose);
+ }
+
+ [Fact]
+ public void AttachRoute_ThrowsForADifferentRouteWhileTheFirstIsStillLive()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ var window = new FakeServiceWindow();
+ RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
+ var firstRoute = new object();
+ var secondRoute = new object();
+ drive.AttachRoute(firstRoute);
+
+ Assert.Throws(
+ () => drive.AttachRoute(secondRoute));
+
+ drive.DetachRoute(firstRoute);
+ drive.AttachRoute(secondRoute);
+ }
+
+ ///
+ /// Gate item: ParkCollisionResidents throws on overlap for every
+ /// spatial root in a retiring prefix that holds an active operation
+ /// (RuntimeSetPositionState.cs:3387-3395). This is the concrete
+ /// proof it stays unreachable under this route's design — both after a
+ /// refusal (no operation ever opened) and after a normal commit
+ /// (operation retired to zero), the SAME prefix can be "retired" without
+ /// throwing.
+ ///
+ [Fact]
+ public void ParkCollisionResidents_StaysUnreachable_AfterRefusalAndAfterCommit()
+ {
+ using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
+ CommitLandblockCollision(lifetime, DestinationLandblock);
+ RuntimeEntityRecord refusedEntity = CreateRemoteRecord(lifetime, 0x7000300Bu);
+ AttachBody(lifetime, refusedEntity, DestinationCell);
+ RuntimeEntityRecord committedEntity = CreateRemoteRecord(lifetime, 0x7000300Cu);
+ AttachBody(lifetime, committedEntity, SourceCell);
+
+ var refusingWindow = new FakeServiceWindow();
+ RuntimeRemotePlacementDriveController refusingDrive =
+ CreateDrive(lifetime, refusingWindow);
+ RuntimeAuthoritativePositionRoute refusedRoute = MakeRoute(
+ refusedEntity,
+ RuntimeAuthoritativePositionDisposition.SetPosition,
+ DestinationCell);
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.Refused,
+ refusingDrive.TryExecuteAcceptedRemotePosition(refusedEntity, refusedRoute));
+
+ var allowingWindow = new FakeServiceWindow();
+ allowingWindow.Allow(DestinationLandblock);
+ RuntimeRemotePlacementDriveController committingDrive =
+ CreateDrive(lifetime, allowingWindow);
+ RuntimeAuthoritativePositionRoute committedRoute = MakeRoute(
+ committedEntity,
+ RuntimeAuthoritativePositionDisposition.SetPosition,
+ DestinationCell,
+ new Vector3(30f, 30f, SpawnHeight));
+ Assert.Equal(
+ RuntimeRemotePlacementExecutionStatus.Committed,
+ committingDrive.TryExecuteAcceptedRemotePosition(
+ committedEntity, committedRoute));
+ // No host subscription is wired in this bare fixture — drain the
+ // Place receipt exactly like production's placement-projection
+ // subscription would, converging the operation to zero the same way
+ // a live host's synchronous consumption does.
+ DrainPlacementFifo(lifetime);
+
+ // Both entities now sit resident in the destination prefix with NO
+ // active operation. Retiring that prefix must not throw.
+ Exception? thrown = Record.Exception(() =>
+ lifetime.Physics.SetPosition.ParkCollisionResidents(
+ DestinationLandblock, includeOutdoorCells: true));
+ Assert.Null(thrown);
+ }
+
+ // ── Fixture ──────────────────────────────────────────────────────────
+
+ ///
+ /// Stands in for the production placement-projection subscription this
+ /// bare fixture never wires — a committed placement's Place
+ /// receipt would otherwise sit unacknowledged forever, exactly like
+ /// RuntimeAcceptedPositionDriveControllerTests.DrainPlacementFifo.
+ ///
+ private static void DrainPlacementFifo(RuntimeEntityObjectLifetime lifetime)
+ {
+ while (lifetime.Physics.SetPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot head))
+ {
+ if (!lifetime.Physics.SetPosition.AcknowledgeProjection(head.Token))
+ break;
+ }
+ }
+
+ private static void AssertConverged(RuntimeEntityObjectLifetime lifetime)
+ {
+ Assert.Equal(
+ 0,
+ lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
+ }
+
+ private static RuntimeRemotePlacementDriveController CreateDrive(
+ RuntimeEntityObjectLifetime lifetime,
+ IRuntimeRemotePlacementServiceWindow window) =>
+ new(
+ lifetime,
+ new GameRuntimeClock(),
+ new UnusedCollisionSource(),
+ window);
+
+ private static RuntimeAuthoritativePositionRoute MakeRoute(
+ RuntimeEntityRecord record,
+ RuntimeAuthoritativePositionDisposition disposition,
+ uint destinationCellId,
+ Vector3? destinationPosition = null,
+ RuntimeSetPositionOperationKind operationKind =
+ RuntimeSetPositionOperationKind.RemoteAuthoritative,
+ PhysicsSetPositionFlags? setPositionFlags = null)
+ {
+ // Publishes the destination into the record's accepted Snapshot —
+ // exactly what the upstream merge
+ // (RuntimeEntityObjectLifetime.TryApplyPosition) already did before a
+ // real caller would ever reach this controller — so the controller's
+ // own service-window read (record.Snapshot.Position.LandblockId)
+ // observes the SAME destination the route was classified against.
+ Vector3 position = destinationPosition ?? new Vector3(10f, 10f, SpawnHeight);
+ record.Snapshot = record.Snapshot with
+ {
+ Position = new CreateObject.ServerPosition(
+ destinationCellId,
+ position.X,
+ position.Y,
+ position.Z,
+ 1f,
+ 0f,
+ 0f,
+ 0f),
+ };
+
+ var authority = new RuntimeAuthoritativePositionAuthority(
+ new RuntimeGenerationToken(1),
+ record.Key!.Value,
+ record.PositionAuthorityVersion,
+ AcceptedPositionSequence: 2,
+ PreviousTeleportSequence: 0,
+ AcceptedTeleportSequence: 0,
+ PositionTimestampDisposition.Apply);
+
+ bool performsSetPosition = disposition is
+ RuntimeAuthoritativePositionDisposition.SetPosition
+ or RuntimeAuthoritativePositionDisposition.SetPositionSimple;
+
+ return new RuntimeAuthoritativePositionRoute(
+ authority,
+ disposition,
+ operationKind,
+ setPositionFlags ?? (performsSetPosition
+ ? PhysicsSetPositionFlags.Teleport
+ | PhysicsSetPositionFlags.Slide
+ : PhysicsSetPositionFlags.None),
+ PlacementFrame: 0u,
+ UnparentBeforeRouting: true,
+ ApplyPlacementFrameBeforeRouting: false,
+ LeaveWorld: false,
+ TeleportHookPhase: RuntimeTeleportHookPhase.None,
+ StopInterpolating: false,
+ ConstrainPhase: RuntimePositionConstrainPhase.AfterPositionOperation,
+ PreserveHeading: false,
+ ZeroVelocity: false,
+ SendPositionImmediately: false,
+ CollisionBatchEligible: true);
+ }
+
+ private static RuntimeEntityRecord CreateRemoteRecord(
+ RuntimeEntityObjectLifetime lifetime,
+ uint guid,
+ uint? setupTableId = null)
+ {
+ RuntimeEntityRecord record = lifetime.RegisterEntity(
+ Spawn(guid, setupTableId)).Canonical!;
+ lifetime.Entities.SetFinalPhysicsState(record, PhysicsStateFlags.Gravity);
+ return record;
+ }
+
+ private static PhysicsBody AttachBody(
+ RuntimeEntityObjectLifetime lifetime,
+ RuntimeEntityRecord record,
+ uint cellId)
+ {
+ lifetime.Entities.SetFullCell(
+ record, cellId, (cellId & 0xFFFF0000u) | 0xFFFFu);
+ var body = new PhysicsBody
+ {
+ Position = new Vector3(10f, 10f, SpawnHeight),
+ Orientation = Quaternion.Identity,
+ LastUpdateTime = 1d,
+ State = PhysicsStateFlags.Gravity,
+ TransientState = TransientStateFlags.Active,
+ };
+ body.SnapToCell(cellId, body.Position, body.Position);
+ lifetime.Entities.SetPhysicsBody(record, body);
+ record.ObjectClock.Activate();
+ lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
+ return body;
+ }
+
+ private static WorldSession.EntitySpawn Spawn(uint guid, uint? setupTableId) =>
+ new(
+ guid,
+ new CreateObject.ServerPosition(
+ SourceCell, 10f, 10f, SpawnHeight, 1f, 0f, 0f, 0f),
+ setupTableId,
+ AnimPartChanges: Array.Empty(),
+ TextureChanges: Array.Empty(),
+ SubPalettes: Array.Empty(),
+ BasePaletteId: null,
+ ObjScale: null,
+ Name: "remote",
+ ItemType: null,
+ MotionState: null,
+ MotionTableId: 0x09000001u);
+
+ private static PhysicsEngine FlatEngine()
+ {
+ var engine = new PhysicsEngine
+ {
+ DataCache = new PhysicsDataCache(),
+ };
+ engine.AddLandblock(
+ SourceLandblock,
+ new TerrainSurface(new byte[81], new float[256]),
+ Array.Empty(),
+ Array.Empty(),
+ worldOffsetX: 0f,
+ worldOffsetY: 0f);
+ return engine;
+ }
+
+ ///
+ /// Mirrors RuntimeAcceptedPositionDriveControllerTests.CommitLandblockCollision
+ /// exactly, against a bare
+ /// instead of a full GameRuntime. Also observes the SOURCE
+ /// landblock's world frame once — Runtime's world-frame resolution is
+ /// arithmetic thereafter, so a destination landblock never needs its own
+ /// call (route 2's own fixture comment).
+ ///
+ private static void CommitLandblockCollision(
+ RuntimeEntityObjectLifetime lifetime,
+ uint landblockId)
+ {
+ var heights = new byte[81];
+ Array.Fill(heights, (byte)SpawnHeight);
+ var heightTable = new float[256];
+ for (int index = 0; index < heightTable.Length; index++)
+ heightTable[index] = index;
+ lifetime.Physics.ObserveLocalWorldFrame(
+ SourceCell, teleportAdvanced: false);
+ lifetime.Physics.SetPosition.BeginCollisionGeneration(landblockId, 1UL);
+ lifetime.Physics.Engine.AddLandblock(
+ landblockId,
+ new TerrainSurface(heights, heightTable),
+ Array.Empty(),
+ Array.Empty(),
+ worldOffsetX: 192f,
+ worldOffsetY: 0f);
+ lifetime.Physics.SetPosition.CommitCollisionGeneration(
+ landblockId, 1UL, ready: true);
+ }
+
+ private sealed class FakeServiceWindow : IRuntimeRemotePlacementServiceWindow
+ {
+ private readonly HashSet _within = [];
+
+ internal void Allow(uint landblockId) =>
+ _within.Add(Canonical(landblockId));
+
+ internal void Forbid(uint landblockId) =>
+ _within.Remove(Canonical(landblockId));
+
+ public bool IsWithinServiceWindow(uint landblockId) =>
+ _within.Contains(Canonical(landblockId));
+
+ private static uint Canonical(uint landblockId) =>
+ (landblockId & 0xFFFF0000u) | 0xFFFFu;
+ }
+
+ private sealed class UnusedCollisionSource : IPreparedCollisionSource
+ {
+ public PreparedAssetPresence ProbeCollision(
+ PakAssetType type,
+ uint sourceFileId) =>
+ PreparedAssetPresence.Available;
+
+ public PreparedCollisionReadResult ReadSetupCollision(
+ uint sourceFileId,
+ CancellationToken cancellationToken = default) =>
+ PreparedCollisionReadResult.Missing;
+
+ public PreparedCollisionReadResult ReadGfxObjCollision(
+ uint sourceFileId,
+ CancellationToken cancellationToken = default) =>
+ throw new NotSupportedException();
+
+ public PreparedCollisionReadResult ReadCellStructureCollision(
+ uint sourceFileId,
+ CancellationToken cancellationToken = default) =>
+ throw new NotSupportedException();
+
+ public PreparedCollisionReadResult ReadEnvCellTopology(
+ uint sourceFileId,
+ CancellationToken cancellationToken = default) =>
+ throw new NotSupportedException();
+
+ public PreparedCollisionSourceStats CollisionStats => default;
+
+ public void Dispose()
+ {
+ }
+ }
+}