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:
parent
634bc5513a
commit
2e8e09acd0
14 changed files with 3151 additions and 6 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue