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

@ -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,