Flips the SetPositionSimple classification (contact, PlayerDistance >= 96 m) for remotes onto 4b-1's drive controller and deletes both legacy far blocks, both duplicated 96f/4f constant pairs, and both `?? Vector3.Zero` fabrications. The 4 m constant now exists exactly once. Teleport and cell-less stay legacy for 4b-3. Retail: MoveOrTeleport @0x00516330's far branch runs StopInterpolating @0x005163CB before SetPositionSimple @0x005163D9 and returns 1 @0x005163E8 regardless — the SetPositionError is discarded — so HandleReceivedPosition arms ConstrainTo @0x00454272 post-move on commit AND on failure. The x87 parity decode at @0x00516393-@0x0051639E puts exactly 96.0 on the far branch. SetPositionSimple @0x005162B0 builds flags 0x1012 at @0x005162C4. Non-commit outcomes still advance the body, because retail's SetPositionInternal @0x00515BD0 commits the destination via store_position @0x00515CE2 when no cell resolves. The partition is by STAGE, not heuristic, enforced by an exhaustive switch: Refused/Contention/NotApplicable/RejectedPreparation store (the placement never executed); Committed/Deferred/RejectedByPlacement do not (the engine ran and refused, matching retail's non-storing returns @0x00515CB2 and @0x00515CD5). Without this a refused far snap froze the remote with an emptied queue. Also fixes a shipped defect this route made live: ParkDeferred's quiescence parks withdrew the entity (InWorld=false, clock suspended, residency removed) and were never restorable, while Forget(restoreCancelledPark: true) runs for every accepted Position on every entity. The restorable decision now lives inside ParkDeferred AFTER SnapToCell, reading body.CellPosition.ObjCellId — the value RestoreParkWithdrawal actually restores at — against every live quiescence rather than one minimum-OperationId token. The three pre-snap fields are hoisted into locals because SnapToCell ends with InWorld = true. ParkCollisionResidents passes restorableOnCancel: false explicitly; the plain unplaceable park is provably unchanged. RestoreParkWithdrawal re-tests the prefix at restore time so a retained route-2 park cannot re-admit into a prefix that began quiescing during the park. CanAttemptDestination is retained as an OPTIMISATION only, with the two Core predicates it cannot reproduce written down at the pre-flight, plus the two properties that depend on it staying there. Four fix rounds and eight Opus reviews. The slice was fully green at 10,990, 10,997 and 11,004 while containing real defects — a frozen remote pinned as correct by its own test, a fallback that over-wrote on the exact retail paths that decline to store, and a park guard incomplete on two independent axes. Register: AP-137 (leftover classifications take AP-87's catch-up; states the cell-less enqueue-vs-place delta deferred to 4b-3, that RejectedData is applied anyway, and the headless divergence), AP-138 (the refusable far placement), AP-136 narrowed to match the relocation. #309's acceptance steps rewritten — step 5 previously asserted a recovery the code does not perform — and gated on a new ACDREAM_PROBE_PARK=1 signal so the check cannot pass while broken. Suite 11,009 passed / 4 skipped / 0 failed against a measured 10,968 baseline. The 10,973 figure recorded earlier was wrong and is corrected here. Connected gate outstanding: the two-client far-snap walk and #309. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
161 lines
6.8 KiB
C#
161 lines
6.8 KiB
C#
using AcDream.Runtime;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Physics;
|
|
using AcDream.Runtime.Session;
|
|
|
|
namespace AcDream.App.Net;
|
|
|
|
/// <summary>
|
|
/// Owns the inbound route, canonical placement observer, and update-thread
|
|
/// retry lease for one exact graphical session generation.
|
|
/// </summary>
|
|
internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
|
|
{
|
|
private readonly ILiveSessionEventRouting _events;
|
|
private readonly Func<RuntimePlacementProjectionSubscription>
|
|
_createSubscription;
|
|
private readonly Func<RuntimeGenerationToken> _generation;
|
|
private readonly RuntimePlacementProjectionRetrySlot _retries;
|
|
private readonly RuntimeFirstEntryDriveController? _firstEntry;
|
|
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
|
|
private readonly RuntimeRemotePlacementDriveController? _remotePlacementDrive;
|
|
private readonly Action<RuntimeEntityRecord>? _localPlayerCompleted;
|
|
private RuntimePlacementProjectionSubscription? _subscription;
|
|
private IDisposable? _retryLease;
|
|
private bool _attachStarted;
|
|
private bool _eventsDisposed;
|
|
private bool _disposed;
|
|
|
|
internal GraphicalSessionEventRoute(
|
|
ILiveSessionEventRouting events,
|
|
GameRuntime runtime,
|
|
IRuntimePlacementProjectionSink placements,
|
|
RuntimePlacementProjectionRetrySlot retries,
|
|
RuntimeFirstEntryDriveController? firstEntry = null,
|
|
Action<RuntimeEntityRecord>? localPlayerCompleted = null,
|
|
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null,
|
|
RuntimeRemotePlacementDriveController? remotePlacementDrive = null)
|
|
: this(
|
|
events,
|
|
() => new RuntimePlacementProjectionSubscription(
|
|
runtime,
|
|
placements,
|
|
retryPendingOnSubscribe: false),
|
|
() => runtime.Generation,
|
|
retries,
|
|
firstEntry,
|
|
localPlayerCompleted,
|
|
acceptedPositionDrive,
|
|
remotePlacementDrive)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(runtime);
|
|
ArgumentNullException.ThrowIfNull(placements);
|
|
}
|
|
|
|
internal GraphicalSessionEventRoute(
|
|
ILiveSessionEventRouting events,
|
|
Func<RuntimePlacementProjectionSubscription> createSubscription,
|
|
Func<RuntimeGenerationToken> generation,
|
|
RuntimePlacementProjectionRetrySlot retries,
|
|
RuntimeFirstEntryDriveController? firstEntry = null,
|
|
Action<RuntimeEntityRecord>? localPlayerCompleted = null,
|
|
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null,
|
|
RuntimeRemotePlacementDriveController? remotePlacementDrive = null)
|
|
{
|
|
_events = events ?? throw new ArgumentNullException(nameof(events));
|
|
_createSubscription = createSubscription
|
|
?? throw new ArgumentNullException(nameof(createSubscription));
|
|
_generation = generation
|
|
?? throw new ArgumentNullException(nameof(generation));
|
|
_retries = retries ?? throw new ArgumentNullException(nameof(retries));
|
|
_firstEntry = firstEntry;
|
|
_localPlayerCompleted = localPlayerCompleted;
|
|
_acceptedPositionDrive = acceptedPositionDrive;
|
|
_remotePlacementDrive = remotePlacementDrive;
|
|
}
|
|
|
|
public void Attach()
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
if (_attachStarted)
|
|
return;
|
|
|
|
_attachStarted = true;
|
|
// 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.
|
|
_firstEntry?.AttachRoute(this, _localPlayerCompleted);
|
|
// C4 route 2: same one-route-at-a-time latch for the accepted-
|
|
// Position drive controller (RuntimeAcceptedPositionDriveController
|
|
// .AttachRoute's doc comment).
|
|
_acceptedPositionDrive?.AttachRoute(this);
|
|
// C4 route 4b-2: route 4b-1's controller gained its first production
|
|
// caller, so its one-route-at-a-time latch and its route-scoped
|
|
// cancellation on detach are load-bearing rather than dormant.
|
|
_remotePlacementDrive?.AttachRoute(this);
|
|
_events.Attach();
|
|
|
|
RuntimePlacementProjectionSubscription? subscription = null;
|
|
IDisposable? retryLease = null;
|
|
try
|
|
{
|
|
subscription = _createSubscription();
|
|
RuntimePlacementProjectionSubscription boundSubscription =
|
|
subscription;
|
|
retryLease = _retries.BindOwned(
|
|
_generation(),
|
|
// C3c: drive pending first-entry sequences before
|
|
// republishing the pending FIFO head — a conductor's own
|
|
// Advance is what consumes conductor-owned receipts, and the
|
|
// subsequent RetryPending lets the presentation sink apply
|
|
// whatever new head the drive surfaced. C4 route 2: the
|
|
// accepted-Position drive's own Advance resolves a parked
|
|
// DeferredCell ForcePosition the same way.
|
|
() =>
|
|
{
|
|
_firstEntry?.DriveAll();
|
|
_acceptedPositionDrive?.Advance();
|
|
// C4 route 4b-2: resolves a retained remote preparation
|
|
// retry (Setup or world frame not resolved yet) on the
|
|
// same cadence, before the FIFO head is republished.
|
|
_remotePlacementDrive?.Advance();
|
|
return boundSubscription.RetryPending();
|
|
});
|
|
_subscription = subscription;
|
|
_retryLease = retryLease;
|
|
_ = subscription.RetryPending();
|
|
}
|
|
catch
|
|
{
|
|
retryLease?.Dispose();
|
|
subscription?.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
// Unpublish the frame callback before detaching the observer. A frame
|
|
// can therefore never retry a retired generation or disposed route.
|
|
Interlocked.Exchange(ref _retryLease, null)?.Dispose();
|
|
Interlocked.Exchange(ref _subscription, null)?.Dispose();
|
|
// C3c: the drive controller's tracked entries die with this exact
|
|
// session route; Runtime's own retirement/session-clear fan-out owns
|
|
// conductor/residence convergence independently. C3c-R1 review F6:
|
|
// route-scoped — a route that never attached cannot clear a live
|
|
// route's entries.
|
|
_firstEntry?.DetachRoute(this);
|
|
_acceptedPositionDrive?.DetachRoute(this);
|
|
_remotePlacementDrive?.DetachRoute(this);
|
|
if (!_eventsDisposed)
|
|
{
|
|
_events.Dispose();
|
|
_eventsDisposed = true;
|
|
}
|
|
|
|
_disposed = true;
|
|
}
|
|
}
|