feat(physics): C4 route 4b-1 — remote placement infrastructure (dormant)

Builds the machinery route 4b-2 and 4b-3 will flip on, and changes no remote
behaviour: it has no production caller, so RemotePlacementDrivePendingCount is
provably 0 and IsConverged is unchanged.

Five pieces: a per-entity remote placement owner (RuntimeRemotePlacementDriveController),
a Position-time service-window guard with a Runtime interface plus BOTH host
implementations, N3's headless RetryPending pump, parked-count observability in
the ownership ledger, and the service-window optimisation that avoids parks we
can cheaply predict.

Landed alone because it is where the park-withdraws-the-entity failure was
decided; that decision is fixed at the source in the preceding commit and must
not share a review signal with a behaviour flip.

Two parts of route 2's controller are deliberately NOT ported, both verified
against retail rather than assumed. There is no ack: SendPositionEvent is called
only inside HandleReceivedPosition's local-player FORCE_POSITION gate
@0x0045400C-@0x00454091, and the remote arm @0x0045414D has no equivalent. There
is no re-issue funnel: retail never re-attempts a position it could not apply —
stale timestamps merely bump error_count @0x004542AC — and re-issuing packet N
after N+1 has merged would apply a pose the newer packet already superseded,
which is correct for a one-shot ForcePosition and wrong for a 5-10 Hz stream.

The service-window guard is an OPTIMISATION, not the correctness mechanism. The
original contract had it the other way round, justified by a claim that retail
cannot represent "arrived but not placeable" — false, and corrected in the
review findings: retail's GotoLostCell/reenter_visibility path represents it
exactly. A pre-flight guard also cannot be complete, because Core defers on the
entity's CURRENT cell, on the swept QueriedCellIds footprint spanning
neighbouring landblocks, and on residency evaluated after AdjustToOutside —
conditions only Core can see.

Review found and this commit fixes: DetachRoute cleared two maps of LIVE Core
operations without cancelling them (route 2's AbandonPending is the correct
mirror, not the first-entry controller) and its test asserted that blindness as
convergence; the headless predicate answered "can ever publish" rather than "is
published", and after the first fix still matched only 1 of the 9 landblocks
this host publishes; OwnsPlacement admitted remote top-level Creates until
gated on the Teleport flag as well as the disposition; Advance re-submitted
without re-checking the window; and four comments cited a report that did not
exist.

Contract item 6 is met by the structural proof, not the earlier test:
HasOldPrefixPlacementDebt refuses collision-prefix mutation permission before
ParkCollisionResidents is ever entered, so its overlap throw is unreachable.
That same mechanism is the unbounded stall filed as #310, which 4b-1 does not
bound — it only avoids widening it.

#311 files the remaining per-tick allocation in RetryPendingProjections; the
early-out for the empty-FIFO case landed via a new HasPendingReceipts accessor
so hosts still never touch .Placements. directly.

Gates: complete Release solution 10,973 passed / 4 skipped / 0 failed (baseline
10,938). Four review rounds; every fix discrimination-verified by revert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-04 04:08:19 +02:00
parent 634bc5513a
commit 2e8e09acd0
14 changed files with 3151 additions and 6 deletions

View file

@ -0,0 +1,91 @@
using AcDream.Runtime.Session;
namespace AcDream.App.Streaming;
/// <summary>
/// C4 route 4b-1: the graphical host's
/// <see cref="IRuntimeRemotePlacementServiceWindow"/> — headless already had
/// one (<c>HeadlessCollisionNeighborhood.IsWithinServiceWindow</c>,
/// <c>HeadlessSessionWorldProjection.cs:27</c>/<c>:245-257</c>); the
/// graphical host had none.
/// </summary>
/// <remarks>
/// <para>
/// <b>Predicate choice — <see cref="GpuWorldState.IsNearTier"/>, not
/// <see cref="GpuWorldState.IsNearTierOrPending"/>.</b> 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:
/// <c>IsNearTierOrPending</c> is true for a landblock that has only been
/// PARKED as a pending near-tier entity bucket
/// (<c>GpuWorldState.CommitEntitiesToExistingLandblockSpatialCore</c>'s
/// <c>parkIfMissing</c> branch, <c>:1919-1952</c>) — 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.
/// <see cref="GpuWorldState.IsNearTier"/> alone is the correct predicate:
/// </para>
/// <para>
/// <b>Promotion direction (verified by reading, not assumed):</b> the only
/// two writers of <c>_tierByLandblock[...] = LandblockStreamTier.Near</c> are
/// <c>GpuWorldState.CommitLandblockSpatialCore</c> (:969) and
/// <c>CommitEntitiesToExistingLandblockSpatialCore</c> (:1972), both called
/// from <c>LandblockPresentationPipeline.Advance</c>'s
/// <c>SpatialPresentationCommitted</c> stage — which runs strictly AFTER the
/// preceding <c>PresentationCommitted</c> stage, the one that drives
/// <c>LandblockPhysicsPublisher</c>'s staged collision/EnvCell advance to
/// completion (<c>LandblockPresentationPipeline.cs</c> ~826-935). A
/// landblock's tier cannot read Near before its collision has committed.
/// </para>
/// <para>
/// <b>Retirement direction (verified by reading, not assumed):</b>
/// <c>GpuWorldState.DetachNearLayer</c> (:1755-1834) flips the tier to Far as
/// the FIRST, synchronous step of a "detach-first" landblock retirement —
/// <c>LandblockRetirementCoordinator.AdoptDetachedFull</c>'s own doc comment
/// states spatial detachment has already committed by the time its retirement
/// ticket is constructed, and that ticket's <c>LandblockRetirementStage.Physics</c>
/// step (<c>_physics.AdvanceRemoval</c> → <c>RuntimeSetPositionState
/// .WithdrawCollision</c> → the collision-side retirement
/// <c>ParkCollisionResidents</c> can reach) runs strictly AFTER. A
/// landblock's tier cannot still read Near once its collision has begun
/// retiring.
/// </para>
/// <para>
/// <b>Residual, not closed:</b> 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. <c>RuntimeSetPositionState</c>'s private
/// <c>TryGetBlockingQuiescence</c> (:3820-3863) is Core's own check for
/// exactly this case — an active <c>CollisionPrefixQuiescence</c> 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 <c>DeferredCell</c> residual
/// <see cref="AcDream.Runtime.Session.RuntimeRemotePlacementDriveController"/>'s
/// class doc describes.
/// </para>
/// </remarks>
internal sealed class GraphicalRemotePlacementServiceWindow
: IRuntimeRemotePlacementServiceWindow
{
private readonly GpuWorldState _state;
internal GraphicalRemotePlacementServiceWindow(GpuWorldState state)
{
_state = state ?? throw new ArgumentNullException(nameof(state));
}
/// <summary>
/// <paramref name="landblockId"/> is a full ACE cell id (landblock high
/// word + cell low word — <c>CreateObject.ServerPosition.LandblockId</c>
/// carries the full id despite its name; see
/// <c>RuntimeAcceptedPositionDriveControllerTests</c>'s own
/// <c>SourceCell = SourceLandblock | 0x0001u</c> fixture shape).
/// Canonicalized to <c>0xAAAAFFFF</c> the same way every
/// <see cref="GpuWorldState"/> tier writer/reader does
/// (<c>DetachNearLayer</c>, <c>CommitLandblockSpatialCore</c>) before the
/// dictionary lookup — <see cref="GpuWorldState.IsNearTier"/> itself does
/// not canonicalize its argument.
/// </summary>
public bool IsWithinServiceWindow(uint landblockId) =>
_state.IsNearTier((landblockId & 0xFFFF0000u) | 0xFFFFu);
}

View file

@ -20,6 +20,17 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
private readonly Action<RuntimeEntityRecord>? _localPlayerCompleted;
private RuntimePlacementProjectionSubscription? _subscription;
/// <summary>
/// B5(c) review fix: the graphical host's per-frame retry callback goes
/// through <c>RuntimePlacementProjectionRetrySlot</c>, which refuses a
/// callback whose bound generation is no longer current
/// (<c>RetryPending</c>'s own guard). Headless calls this route's
/// <see cref="RetryPending"/> directly with no equivalent latch — this
/// field plus the check inside <see cref="RetryPending"/> restore that
/// same "the generation this route attached under must still be the
/// live one" guard.
/// </summary>
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);
}
/// <summary>
/// C4 route 4b-1 (N3): republishes the canonical placement FIFO's
/// current head. <see cref="Attach"/> only retries once, at subscribe
/// time (<c>retryPendingOnSubscribe: true</c>) — the ONLY
/// <c>RetryPending</c> 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
/// (<c>GraphicalSessionEventRoute.Attach</c>'s retry-lease callback);
/// <see cref="HeadlessSessionHost.Tick"/> calls this in the same order
/// immediately after <c>HeadlessSessionWorldProjection.PumpFirstEntry</c>
/// so a declined head left behind by
/// <see cref="AcDream.Runtime.Physics.RuntimePlacementProjectionSubscription.OnPlacement"/>
/// (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.
///
/// <para>
/// B5(c) review fix: refuses when <see cref="_runtime"/>'s generation has
/// moved past the one this route attached under — the graphical host's
/// <c>RuntimePlacementProjectionRetrySlot</c> already refuses a
/// stale-generation callback the same way; headless dereferenced
/// <see cref="_subscription"/> directly with no equivalent guard before
/// this fix.
/// </para>
/// <para>
/// C2-2 review fix (delta round): the earlier version of this comment
/// claimed <c>RuntimeSetPositionState.cs</c> 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
/// <see cref="RuntimePlacementProjectionSubscription.HasPendingReceipts"/>
/// (added for this fix) rather than the Runtime placement channel
/// directly, so it never trips
/// <c>RuntimePhysicsOwnershipTests.ProductionHostsUseSharedPlacementSubscriptionWithoutDirectChannel</c>
/// — 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
/// <c>RetryPendingProjections</c>'s own
/// <c>_pendingProjection.Values.ToArray()</c> snapshot, which needs a
/// change to <c>RuntimeSetPositionState.cs</c> this session did not make
/// — filed as docs/ISSUES.md #311 rather than worked around.
/// </para>
/// </summary>
internal bool RetryPending()
{
if (_subscription is null
|| _attachedGeneration != _runtime.Generation
|| !_subscription.HasPendingReceipts)
{
return false;
}
return _subscription.RetryPending();
}
public void Dispose()
{
if (_disposed)

View file

@ -121,6 +121,17 @@ internal sealed class HeadlessSessionHost : IDisposable
private readonly RuntimeLocalPlayerFrameController _localPlayerFrame;
private readonly HeadlessProcessContentOwner.HeadlessProcessContentLease?
_contentLease;
/// <summary>
/// B5(a) review fix: test-only seam (mirrors <c>policyOverride</c>'s own
/// pattern) letting a focused test substitute a deterministic fake
/// placement sink for the production <see cref="HeadlessRuntimePlacementProjectionSink"/>,
/// so a test can drive <see cref="Tick"/> itself — the real
/// <c>_eventRoute?.RetryPending()</c> call this fix covers — instead of
/// hand-constructing a <see cref="HeadlessSessionEventRoute"/> outside
/// this host. <c>null</c> (every production caller) keeps today's exact
/// behavior.
/// </summary>
private readonly IRuntimePlacementProjectionSink? _placementSinkOverride;
/// <summary>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;
/// <summary>C4 route 4b-1 (N3): the exact route <see cref="CreateEventRoute"/>
/// last constructed, so <see cref="Tick"/> can republish the canonical
/// placement FIFO every tick — mirrors the graphical host's per-frame
/// retry lease (<c>GraphicalSessionEventRoute.Attach</c>). Reassigned on
/// every reconnect exactly like <see cref="_worldProjection"/>; the prior
/// route's own disposal (via <c>LiveSessionHost</c>'s route replacement)
/// is independent of this field.</summary>
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(

View file

@ -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.
/// </summary>
/// <remarks>
/// C4 route 4b-1: also implements
/// <see cref="AcDream.Runtime.Session.IRuntimeRemotePlacementServiceWindow"/>.
/// B1 review fix: despite the identical <c>bool IsWithinServiceWindow(uint)</c>
/// signature, the two interfaces ask DIFFERENT questions —
/// <see cref="IHeadlessCollisionNeighborhood.IsWithinServiceWindow"/> is a
/// pure geometry test ("can this landblock EVER collision-publish inside the
/// requested 3x3 window" — <c>true</c> outright when no center has been
/// requested yet), while
/// <see cref="AcDream.Runtime.Session.IRuntimeRemotePlacementServiceWindow.IsWithinServiceWindow"/>
/// 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
/// <c>IsNearTierOrPending</c> for. <see cref="IsReady"/> asks the right
/// question (it consults actual collision-publication residency), but it
/// MUTATES — it calls <see cref="AdvanceWork"/> to progress publication work
/// as a side effect, which a per-packet service-window pre-flight check must
/// never do. <see cref="IsCollisionCurrentlyPublished"/> is
/// <see cref="IsReady"/>'s residency shape with BOTH of its mutations
/// removed — the <see cref="AdvanceWork"/> call AND the
/// <c>_requestedFullCell</c> 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 <see cref="IsReady"/>'s
/// own <c>_centerLandblock != center</c> restriction (correct for
/// <see cref="IsReady"/>'s own use — "is the ONE landblock I most recently
/// centered on ready" — but wrong here) to membership in
/// <see cref="_resident"/>, the full set <see cref="BuildPublicationPlan"/>
/// 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 <c>Advance</c> would then drop its retry once the service
/// window (this predicate) forbade a destination that was genuinely
/// servable. <see cref="_resident"/> is already scoped to the CURRENT
/// window by construction: <see cref="CenterOn"/>'s re-center path fully
/// retires the old center's <see cref="_resident"/> entries (via
/// <see cref="AdvanceWork"/>'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 <see cref="_resident"/> and be misread as
/// currently published. The explicit interface implementation below routes
/// to it instead of the geometry test.
/// </remarks>
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;
}
/// <summary>
/// B1 review fix: a residency check — resident, terrain-published, and
/// (for indoor cells) the <c>CellStruct</c> itself resolved — derived
/// from <see cref="IsReady"/> with both of its mutations removed (the
/// <see cref="AdvanceWork"/> call and the <c>_requestedFullCell</c> field
/// write). This is what
/// <see cref="AcDream.Runtime.Session.IRuntimeRemotePlacementServiceWindow.IsWithinServiceWindow"/>
/// 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 <see cref="IsReady"/>, this
/// checks membership in the FULL <see cref="_resident"/> set (the
/// requested center plus its published 3x3 neighbors) rather than
/// requiring an exact match against <see cref="_centerLandblock"/> — see
/// the class remarks for why the narrower check was wrong for this
/// interface's question.
/// </summary>
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;
}
/// <summary>
/// B1 review fix: explicit implementation so this class can answer the
/// two <c>IsWithinServiceWindow</c> questions differently despite the
/// identical method signature — see the class remarks. Routes to
/// <see cref="IsCollisionCurrentlyPublished"/>, never to the geometry
/// test <see cref="IsWithinServiceWindow"/> implicitly implements for
/// <see cref="IHeadlessCollisionNeighborhood"/>.
/// </summary>
bool AcDream.Runtime.Session.IRuntimeRemotePlacementServiceWindow
.IsWithinServiceWindow(uint fullCellId) =>
IsCollisionCurrentlyPublished(fullCellId);
private HeadlessCollisionGenerationTransaction? CreatePublication(
uint landblockId,
Vector3 origin,

View file

@ -88,7 +88,20 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
/// <see cref="RuntimeEntityObjectLifetime.RegisterAcceptedPositionDriveOwnership"/>.
/// Gated by <see cref="IsConverged"/> — a leaked pending ack cannot hide.
/// </summary>
int AcceptedPositionDrivePendingCount = 0)
int AcceptedPositionDrivePendingCount = 0,
/// <summary>
/// C4 route 4b-1: outstanding
/// <c>AcDream.Runtime.Session.RuntimeRemotePlacementDriveController</c>
/// preparation-retry entries (a not-yet-resolved
/// <c>RetrySetupUnavailable</c>/<c>RetryWorldFrameUnavailable</c> for a
/// remote), summed over every drive registered against this lifetime via
/// <see cref="RuntimeEntityObjectLifetime.RegisterRemotePlacementDriveOwnership"/>.
/// Gated by <see cref="IsConverged"/>, mirroring
/// <see cref="AcceptedPositionDrivePendingCount"/> — steady-state
/// remotes hold no operations, and this count proves it at every
/// convergence checkpoint the same way.
/// </summary>
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<Func<int>> _firstEntryDriveOwnership = [];
/// <summary>C4 route 2: see <see cref="RegisterAcceptedPositionDriveOwnership"/>.</summary>
private readonly List<Func<int>> _acceptedPositionDriveOwnership = [];
/// <summary>C4 route 4b-1: see <see cref="RegisterRemotePlacementDriveOwnership"/>.</summary>
private readonly List<Func<int>> _remotePlacementDriveOwnership = [];
/// <summary>
/// C4 route 4a: captured by <see cref="BindEventContext"/> 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;
}
/// <summary>
/// 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);
}
/// <summary>
/// C4 route 4b-1: registers one host
/// <c>RuntimeRemotePlacementDriveController</c>'s pending-count provider
/// into this lifetime's ownership snapshot, mirroring
/// <see cref="RegisterAcceptedPositionDriveOwnership"/> — 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).
/// </summary>
public void RegisterRemotePlacementDriveOwnership(Func<int> pendingCount)
{
ArgumentNullException.ThrowIfNull(pendingCount);
EnsureNotDisposed();
_remotePlacementDriveOwnership.Add(pendingCount);
}
public void BindEventContext(
Func<RuntimeGenerationToken> generation,
Func<ulong> frameNumber)

View file

@ -454,6 +454,21 @@ public sealed class RuntimePhysicsState : IDisposable
_collisionGenerationCommittedObservers = new();
private bool _disposed;
/// <summary>
/// C2-1 review fix (delta round): lets a ledger-provider callback
/// registered against <see cref="RuntimeEntityObjectLifetime"/> (e.g.
/// <c>RuntimeRemotePlacementDriveController.CountLiveAwaitingAcknowledgement</c>)
/// check disposal state BEFORE calling into <see cref="SetPosition"/>,
/// whose own <c>IsPlacementCurrent</c> throws <c>ObjectDisposedException</c>
/// once disposed. <c>Dispose()</c> below disposes <see cref="SetPosition"/>
/// strictly before setting this flag, so <c>IsDisposed == true</c> here
/// guarantees <see cref="SetPosition"/> is already disposed too — a
/// post-<c>Dispose()</c> <c>CaptureOwnership()</c> read is the designed
/// contract (<c>GameWindowLifetime.DisposeGameRuntime</c>), so every
/// ledger provider must survive it without throwing.
/// </summary>
internal bool IsDisposed => _disposed;
public event Action<RuntimePhysicsCellCommit>? CellCommitted;
public event Action<RuntimeCollisionGenerationCommitted>?
CollisionGenerationCommitted

View file

@ -86,6 +86,20 @@ public sealed class RuntimePlacementProjectionSubscription
public bool HasAppliedReceiptAwaitingAcknowledgement =>
_appliedAwaitingAcknowledgement.IsValid;
/// <summary>
/// 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 <see cref="RetryPending"/> without reaching the
/// Runtime placement channel directly — the architectural boundary
/// <c>RuntimePhysicsOwnershipTests.ProductionHostsUseSharedPlacementSubscriptionWithoutDirectChannel</c>
/// enforces (hosts consume placement state ONLY through this
/// subscription). Only closes the EMPTY-FIFO case: when the count is
/// nonzero, <c>RetryPending</c> still reaches
/// <c>RuntimeSetPositionState.RetryPendingProjections</c>'s per-call
/// array snapshot — see docs/ISSUES.md for that residual.
/// </summary>
public bool HasPendingReceipts => _channel.PendingCount != 0;
/// <summary>
/// Republishes Runtime's complete still-pending FIFO. Later receipts are
/// ignored until the exact oldest receipt projects and acknowledges.

View file

@ -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;
/// <summary>
/// C4 route 4b-1: per-entity remote-placement service-window predicate. A
/// host implements this over whatever collision-publication residency it
/// actually tracks — <c>GpuWorldState.IsNearTier</c> for the graphical host,
/// <c>HeadlessCollisionNeighborhood</c>'s explicit implementation (backed by
/// its private <c>IsCollisionCurrentlyPublished</c> — NOT
/// <see cref="AcDream.Headless.Hosting.IHeadlessCollisionNeighborhood.IsWithinServiceWindow"/>,
/// 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
/// <see cref="RuntimeRemotePlacementDriveController"/> can
/// ask, BEFORE attempting any canonical SetPosition, whether the accepted
/// destination is one this host can actually place a remote into right now.
/// </summary>
public interface IRuntimeRemotePlacementServiceWindow
{
/// <summary>
/// True when <paramref name="landblockId"/>'s collision is currently
/// published by this host, so a canonical SetPosition into it can be
/// attempted without risking an un-wakeable <c>DeferredCell</c> park (see
/// docs/research/2026-08-04-c4-route-4b-1-contract.md's "central
/// decision"). <paramref name="landblockId"/> may be a full cell id — the
/// implementation canonicalizes to the containing landblock.
/// </summary>
bool IsWithinServiceWindow(uint landblockId);
}
/// <summary>
/// Typed yields for
/// <see cref="RuntimeRemotePlacementDriveController.TryExecuteAcceptedRemotePosition"/>.
/// </summary>
internal enum RuntimeRemotePlacementExecutionStatus : byte
{
/// <summary>Out of this route's scope: not a disposition this
/// controller owns (<see cref="RuntimeRemotePlacementDriveController.OwnsPlacement"/>),
/// no canonical body, or no incarnation key.</summary>
NotApplicable,
/// <summary>
/// The central decision (see the class doc on
/// <see cref="RuntimeRemotePlacementDriveController"/>): 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.
/// </summary>
Refused,
/// <summary>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.</summary>
Contention,
/// <summary>The canonical SetPosition committed synchronously.</summary>
Committed,
/// <summary>Rejected/cancelled by Core (invalid prepared data, authority
/// displaced mid-submit).</summary>
Rejected,
}
/// <summary>
/// C4 route 4b-1: the Runtime-owned, per-entity accepted-Position execution
/// seam for a remote whose classification is <c>SetPosition</c> (teleport /
/// cell-less — 4b-3's eventual disposition) or <c>SetPositionSimple</c> (far
/// snap, &gt;=96 m — 4b-2's). Route 4a's
/// <see cref="RuntimeRemoteSteadyStatePosition"/> already owns the two
/// dispositions that perform no SetPosition at all
/// (<c>NoPositionOperation</c>/<c>Interpolate</c>); 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:
///
/// <list type="bullet">
/// <item><description><b>No ack.</b> Retail's remote arm has no
/// <c>SendPositionEvent</c> — <c>HandleReceivedPosition</c> @0x00453FD0 calls
/// it only on the local-player FORCE_POSITION branch. There is nothing here
/// resembling route 2's <c>PositionEventOwed</c>/<c>SendPositionEvent</c>
/// pair.</description></item>
/// <item><description><b>No re-issue funnel.</b> 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.</description></item>
/// <item><description><b>Per-entity, not per-session.</b> Route 2's
/// <c>_pending</c> is a single slot because the local player is the only
/// entity that route ever touches. Remotes are N entities, so
/// <see cref="_pending"/> 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.</description></item>
/// </list>
///
/// <para>
/// <b>The central decision — refuse, do not park.</b>
/// <see cref="RuntimeSetPositionState"/>'s <c>DeferredCell</c> park
/// withdraws the entity from the world (<c>ParkDeferred</c> sets
/// <c>body.InWorld = false</c>, suspends the object clock, and publishes a
/// <c>Withdraw</c>) — and <c>RuntimeEntityObjectLifetime.TryApplyPosition</c>
/// calls <c>RuntimeSetPositionState.Forget</c> on EVERY subsequent accepted
/// Position for that same entity, regardless of disposition. <c>Forget</c>'s
/// <c>CancelCoreDeferred</c> removes the operation and rewrites the
/// <c>Withdraw</c> into a <c>Discard</c> WITHOUT restoring <c>InWorld</c>,
/// 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. <see cref="_serviceWindow"/>
/// exists to prevent this controller from ever attempting a SetPosition
/// whose destination cannot be placed right now: <see cref="TryExecuteAcceptedRemotePosition"/>
/// checks it BEFORE calling
/// <see cref="RuntimeSetPositionState.TryBeginExclusiveAuthoredPlacement"/>,
/// and if Core still returns <c>DeferredCell</c> 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. <b>Why the guard can still miss (the co-extensivity
/// finding and its residual):</b> a tier/residency-backed service window
/// (the graphical host's <c>GpuWorldState.IsNearTier</c>, 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 —
/// <see cref="RuntimeSetPositionState"/>'s private <c>TryGetBlockingQuiescence</c>
/// 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.
/// </para>
///
/// <para>
/// One instance per host session route, constructed once per host process
/// and reused across reconnects exactly like
/// <see cref="RuntimeFirstEntryDriveController"/> and
/// <see cref="RuntimeAcceptedPositionDriveController"/> —
/// <see cref="AttachRoute"/>/<see cref="DetachRoute"/> assert the same
/// "session reset precedes a new route" ordering and clear any tracked
/// entries left by a torn-down session.
/// </para>
/// </summary>
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;
/// <summary>
/// Per-entity preparation-retry map (bounded to the two retryable
/// <see cref="RuntimeSetPositionMoverPreparationStatus"/> reasons — Setup
/// or world-frame data not resolved yet, NEITHER of which withdraws the
/// entity). A <c>DeferredCell</c> outcome never enters this map — see the
/// class doc's central decision.
/// </summary>
private readonly Dictionary<RuntimeEntityKey, Pending> _pending = [];
/// <summary>
/// B4 review fix: per-entity tokens whose <see cref="SubmitAndResolve"/>
/// outcome was <c>CommittedHostAcknowledgementPending</c> and were STILL
/// live (not synchronously consumed-and-acknowledged by the production
/// placement-projection subscription inside that same call) the instant
/// <see cref="SubmitAndResolve"/> returned <c>Committed</c>. Without this,
/// the ledger went blind the moment <c>Committed</c> was returned, hiding
/// exactly the declined-sink class the FIFO retry mechanism exists for.
/// Pruned lazily (self-healing) on every read — see
/// <see cref="CountLiveAwaitingAcknowledgement"/> — never gates any
/// placement decision itself; Core's own <c>_operations</c> map remains
/// the sole authority <c>TryBeginExclusiveAuthoredPlacement</c> consults.
/// </summary>
private readonly Dictionary<RuntimeEntityKey, RuntimeEntityPlacementToken>
_awaitingAcknowledgement = [];
private readonly List<RuntimeEntityKey> _driveScratch = [];
/// <summary>
/// C2-1 review fix (delta round): dedicated reusable scratch list for
/// <see cref="CountLiveAwaitingAcknowledgement"/>'s self-heal removal
/// pass — kept separate from <see cref="_driveScratch"/> (owned by
/// <see cref="Advance"/>) so a ledger read reached while <c>Advance</c>
/// is mid-iteration can never corrupt its scratch buffer.
/// </summary>
private readonly List<RuntimeEntityKey> _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);
}
/// <summary>
/// Preparation-stage retries only (see <see cref="_pending"/>'s own doc).
/// Deliberately does NOT include <see cref="_awaitingAcknowledgement"/> —
/// that dimension is reported to the lifetime's ownership ledger via the
/// constructor's second <c>RegisterRemotePlacementDriveOwnership</c> call
/// (B4 review fix) and has no separate test-visible counter of its own.
/// </summary>
internal int PendingCount => _pending.Count;
/// <summary>
/// True when route 4b owns this classification for a remote — the two
/// dispositions route 4a's <see cref="RuntimeRemoteSteadyStatePosition"/>
/// does NOT already handle. Everything else (<c>Interpolate</c>,
/// <c>NoPositionOperation</c>, <c>RejectedAuthority</c>,
/// <c>RejectedData</c>, <c>AwaitFreshPosition</c>) is out of scope here.
/// B6 review fix: <c>Disposition</c> alone is not exact — the classifier
/// (<c>RuntimeAuthoritativePositionRouteClassifier.cs</c>) assigns
/// <c>SetPosition</c>/<c>SetPositionSimple</c> at several independent
/// call sites (:256, :332, :355, :403, :459) and derives
/// <c>OperationKind</c> separately (:560-575) — the SAME disposition
/// covers the LOCAL PLAYER's FORCE_POSITION/teleport branches
/// (<c>RuntimeSetPositionOperationKind.LocalAuthoritative</c>).
/// <paramref name="route"/> is a parameter separate from the entity
/// record at every call site, so a mismatched pair is expressible;
/// gating on <c>OperationKind</c> narrows to remotes AND the local
/// player's own initial Create — <c>OperationKind</c>'s own switch
/// (:559-570) maps <c>InitialLogin</c> to the LOCAL PLAYER's Create
/// ONLY; a REMOTE top-level Create maps to <c>RemoteAuthoritative</c>
/// exactly like a remote accepted Position does (correcting this
/// comment's earlier, wrong claim that all initial Creates map to
/// <c>InitialLogin</c>).
/// <para>
/// C2-4 review fix (delta round): so <c>OperationKind</c> alone still
/// does not exclude a remote top-level Create — <c>ClassifyCreate</c>
/// (:254-273) emits <c>SetPosition</c> + <c>RemoteAuthoritative</c> +
/// <c>InitialCreateFlags</c> (<c>Placement|Slide</c>) for one, while
/// <c>ClassifyAcceptedPosition</c>'s remote branches (:401-416,
/// :454-472) always carry <c>AuthoritativeTeleportFlags</c>
/// (<c>Teleport|Slide|SendPositionEvent</c>) for the SAME disposition/
/// OperationKind pair. The <c>Teleport</c> 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.
/// </para>
/// </summary>
internal static bool OwnsPlacement(RuntimeAuthoritativePositionRoute route) =>
route.OperationKind is RuntimeSetPositionOperationKind.RemoteAuthoritative
&& route.Disposition is RuntimeAuthoritativePositionDisposition.SetPosition
or RuntimeAuthoritativePositionDisposition.SetPositionSimple
&& (route.SetPositionFlags & PhysicsSetPositionFlags.Teleport) != 0;
/// <summary>
/// Mirrors <see cref="RuntimeFirstEntryDriveController.AttachRoute"/> and
/// <see cref="RuntimeAcceptedPositionDriveController.AttachRoute"/>: this
/// controller outlives its session routes, so the "session reset
/// precedes a new route" ordering is asserted, not assumed.
/// </summary>
internal void AttachRoute(object route)
{
ArgumentNullException.ThrowIfNull(route);
if (_routeOwner is not null && !ReferenceEquals(_routeOwner, route))
{
throw new InvalidOperationException(
"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;
}
/// <summary>
/// Route-scoped teardown: abandons every tracked entry, but ONLY when
/// <paramref name="route"/> is the attached owner.
///
/// <para>
/// C2-1 review fix (delta round) — the prior version of this method (and
/// this comment) was wrong: it cleared <see cref="_pending"/> and
/// <see cref="_awaitingAcknowledgement"/> as if they were pure
/// bookkeeping, citing <see cref="RuntimeFirstEntryDriveController.DetachRoute"/>
/// 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 — <see cref="_pending"/> at
/// <c>AwaitingPreparation</c> (already begun via
/// <c>TryBeginExclusiveAuthoredPlacement</c>), <see cref="_awaitingAcknowledgement"/>
/// at <c>AwaitingCommitAcknowledgement</c> with a published <c>Place</c>.
/// Clearing the local dictionary without cancelling the Core operation
/// left it live forever, pinning its landblock prefix
/// (<c>HasOldPrefixPlacementDebt</c>) — 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 <c>RuntimeAcceptedPositionDriveController.DetachRoute</c>
/// → <c>AbandonPending</c>, which this now matches: cancel every live
/// operation (<see cref="CancelToken"/>, <c>restoreCancelledPark: true</c>
/// — 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.
/// </para>
/// </summary>
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);
}
}
}
/// <summary>
/// Executes an already-classified remote accepted Position against the
/// canonical Runtime SetPosition owner. <paramref name="record"/>'s
/// Snapshot and PositionAuthorityVersion must already reflect the merge
/// <see cref="RuntimeEntityObjectLifetime.TryApplyPosition"/> performed —
/// this method never re-merges the wire frame, and <paramref name="route"/>
/// must already be the result of
/// <see cref="RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition"/>
/// for the SAME packet (the shared classification builder every remote
/// caller uses — never re-derived here).
/// </summary>
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);
}
/// <summary>
/// Host cadence pump: retries a preparation-only retry status
/// (<c>RetrySetupUnavailable</c>/<c>RetryWorldFrameUnavailable</c>) by
/// re-calling the SAME prepare+submit pair, exactly like
/// <see cref="RuntimeFirstEntryDriveController"/>'s own continuation
/// completion. Bounded, non-allocating iteration mirrors
/// <see cref="RuntimeFirstEntryDriveController"/>'s <c>_driveScratch</c>
/// template. Safe to call from any host cadence point; a no-op when
/// nothing is pending.
/// </summary>
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;
}
}
/// <summary>
/// Self-healing read: prunes every <see cref="_awaitingAcknowledgement"/>
/// 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
/// <c>RegisterRemotePlacementDriveOwnership</c> 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.
///
/// <para>
/// C2-1 review fix (delta round), disposal safety: <c>IsPlacementCurrent</c>'s
/// first statement is <c>EnsureNotDisposed</c>, which THROWS once
/// <see cref="RuntimeSetPositionState"/> is disposed. A post-<c>Dispose()</c>
/// <c>CaptureOwnership()</c> read is the designed contract
/// (<c>GameWindowLifetime.DisposeGameRuntime</c>: <c>runtime.Dispose();
/// runtime.CaptureOwnership();</c>), so this — the first ledger provider
/// to reach into another disposable subsystem — must survive it. When
/// <see cref="RuntimePhysicsState.IsDisposed"/> 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 <see cref="DetachRoute"/> before disposal, so this
/// branch reports 0 in the healthy path and a genuine nonzero leak
/// otherwise — never an exception either way.
/// </para>
/// <para>
/// C2-1 review fix (delta round), allocation: reuses
/// <see cref="_awaitingAcknowledgementScratch"/> instead of a per-call
/// <c>List&lt;RuntimeEntityKey&gt;</c>, mirroring <see cref="_driveScratch"/>'s
/// own template.
/// </para>
/// </summary>
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);
}
}