using AcDream.Content; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; using AcDream.Runtime.Physics; namespace AcDream.Runtime.Session; /// /// C3c: the host-driven pump that walks every initial-Create residence /// through its first-entry conductor. One instance per host session route; /// graphical and no-window hosts construct it with their own prepared /// collision source and local-player activation-preparation provider and /// call from their own cadence (post-Create /// hydration and the per-frame placement retry phase for the graphical /// host; spawn/position projection and the session tick for headless). /// /// The controller owns NO placement state — it records which entities hold /// a fresh residence lease (via /// ) /// and repeatedly calls the conductors, which re-validate all currency /// themselves. Terminal yields (Completed/RejectedToken/RejectedAuthority) /// drop the entry; every Awaiting*/Contention yield keeps it for the next /// pump. /// /// Continuation placements (the executor's AwaitingContinuationPlacement /// yield) are completed here through the C0 fused /// /// — legal for a continuation operation, which never has /// DormantLocalActivation set — followed by head acknowledgement. The /// production sink may consume the resulting Place first (the residence is /// already consumed by then, so the sink's residence gate does not fire); /// a failed acknowledgement after that is benign — the executor's /// ResumePendingPlacement keys off the retained acknowledged completion, /// not off who acknowledged. /// internal sealed class RuntimeFirstEntryDriveController { /// /// Bounded chase of synchronous progress inside one entity's drive — /// enough for mover-prep + placement + acknowledgement + a handful of /// continuation placements in a single pump without risking an unbounded /// loop against a livelocked yield. /// private const int MaxSynchronousStepsPerEntity = 16; private sealed class Pending { internal required RuntimeEntityRecord Record { get; init; } internal required RuntimeInitialCreateResidenceToken Token { get; init; } internal required bool IsLocalPlayer { get; init; } } private readonly RuntimeEntityObjectLifetime _entityObjects; private readonly IGameRuntimeClock _clock; private readonly IPreparedCollisionSource _collisionSource; private readonly Func _localOptions; private readonly Func _localActivation; private readonly Dictionary _pending = []; private readonly List _driveScratch = []; private bool _driving; /// C3c-R1 review F6: see . private object? _routeOwner; private Action? _localPlayerCompleted; internal RuntimeFirstEntryDriveController( RuntimeEntityObjectLifetime entityObjects, IGameRuntimeClock clock, IPreparedCollisionSource collisionSource, Func localOptions, Func localActivation) { _entityObjects = entityObjects ?? throw new ArgumentNullException(nameof(entityObjects)); _clock = clock ?? throw new ArgumentNullException(nameof(clock)); _collisionSource = collisionSource ?? throw new ArgumentNullException(nameof(collisionSource)); _localOptions = localOptions ?? throw new ArgumentNullException(nameof(localOptions)); _localActivation = localActivation ?? throw new ArgumentNullException(nameof(localActivation)); _entityObjects.BindInitialResidenceBeginNotification( NoteResidenceBegan); // C3c-R1 review F5: tracked-but-undriven entries fold into the // entity-object ownership snapshot instead of sitting outside every // ledger. _entityObjects.RegisterFirstEntryDriveOwnership(() => _pending.Count); } internal int PendingCount => _pending.Count; /// /// Records a fresh residence for a later pump. Runs synchronously inside /// the registration transaction (including the executor's deferred-child /// replays, which re-enter registration mid-Execute), so it must never /// call Advance here — only capture the exact key/token/dispatch facts. /// private void NoteResidenceBegan(RuntimeEntityRecord record) { if (record.Key is not { } key || !_entityObjects.TryGetInitialCreateResidence( record, out RuntimeInitialCreateResidenceLease lease)) { return; } _pending[key] = new Pending { Record = record, Token = lease.Token, // Dispatch is decided ONCE from the lease's classified route — // TryGetCurrent fails mid-drain (the residence moves to its // completed table at Complete), so the lease cannot be // re-fetched on a later pump. IsLocalPlayer = lease.Route.OperationKind is RuntimeSetPositionOperationKind.InitialLogin, }; } /// /// Drives every tracked first-entry sequence one bounded step. Safe to /// call from any host cadence point; re-entrant calls (a conductor's own /// synchronous callbacks reaching a host pump) fail closed into the next /// outer pump instead of interleaving. /// internal void DriveAll() { if (_driving || _pending.Count == 0) return; _driving = true; try { _driveScratch.Clear(); foreach (RuntimeEntityKey key in _pending.Keys) _driveScratch.Add(key); foreach (RuntimeEntityKey key in _driveScratch) { if (_pending.TryGetValue(key, out Pending? pending)) DriveOne(key, pending); } } finally { _driving = false; } } /// /// C3c-R1 review F6: the explicit one-route-at-a-time latch. A drive /// controller outlives its session routes (hosts reuse it across /// reconnects), and route teardown clears the tracked entries — so the /// "session reset precedes a new route" ordering the hosts rely on is /// asserted here instead of silently assumed: a second route attaching /// before the prior route detached would otherwise let the OLD route's /// dispose wipe the NEW route's tracked entries. /// internal void AttachRoute( object route, Action? localPlayerCompleted = null) { ArgumentNullException.ThrowIfNull(route); if (_routeOwner is not null && !ReferenceEquals(_routeOwner, route)) { throw new InvalidOperationException( "A first-entry 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; _localPlayerCompleted = localPlayerCompleted; } /// /// Route-scoped teardown: clears every tracked entry, but ONLY when /// is the attached owner — a route that never /// attached (construction rollback) or was displaced must not clear the /// live route's entries. The conductors and residence own their own /// convergence independently (retirement fan-out + session clear). /// internal void DetachRoute(object route) { ArgumentNullException.ThrowIfNull(route); if (!ReferenceEquals(_routeOwner, route)) return; _routeOwner = null; _localPlayerCompleted = null; _pending.Clear(); } private void DriveOne(RuntimeEntityKey key, Pending pending) { for (int step = 0; step < MaxSynchronousStepsPerEntity; step++) { if (pending.Record.Key != key) { // Post-teardown key release; the retirement fan-out already // reaped the conductors' own progress. _pending.Remove(key); return; } bool terminal; bool awaitingContinuationPlacement; bool localPlayerCompleted = false; if (pending.IsLocalPlayer) { RuntimeLocalPlayerFirstEntryStatus status = _entityObjects.LocalPlayerFirstEntry.Advance( pending.Record, pending.Token, _localOptions(), _localActivation(pending.Record), _collisionSource, _clock.SimulationTimeSeconds, inputs: default, out _); terminal = status is RuntimeLocalPlayerFirstEntryStatus.Completed or RuntimeLocalPlayerFirstEntryStatus.RejectedToken or RuntimeLocalPlayerFirstEntryStatus.RejectedAuthority; localPlayerCompleted = status is RuntimeLocalPlayerFirstEntryStatus.Completed; awaitingContinuationPlacement = status is RuntimeLocalPlayerFirstEntryStatus .AwaitingContinuationPlacement; } else { RuntimeRemoteFirstEntryStatus status = _entityObjects.RemoteFirstEntry.Advance( pending.Record, pending.Token, _collisionSource, _clock.SimulationTimeSeconds, inputs: default, out _, out _); terminal = status is RuntimeRemoteFirstEntryStatus.Completed or RuntimeRemoteFirstEntryStatus.RejectedToken or RuntimeRemoteFirstEntryStatus.RejectedAuthority; awaitingContinuationPlacement = status is RuntimeRemoteFirstEntryStatus .AwaitingContinuationPlacement; } if (terminal) { _pending.Remove(key); if (localPlayerCompleted) _localPlayerCompleted?.Invoke(pending.Record); return; } if (!awaitingContinuationPlacement) { // AwaitingCollisionSource / AwaitingActivation / // AwaitingPlacement / AwaitingReceiptAcknowledgement / // Contention — nothing more this pump can do synchronously. return; } if (!TryCompleteContinuationPlacement( key, pending.Record, resolveWorldOffsetFromRuntimeFrame: !pending.IsLocalPlayer)) return; // A continuation placement progressed — re-Advance so the // executor can consume the acknowledged completion and keep // draining. } } /// /// Completes (or makes bounded progress on) the executor's pending /// continuation placement for . Returns true when /// enough progress happened that re-calling Advance can observe it. /// private bool TryCompleteContinuationPlacement( RuntimeEntityKey key, RuntimeEntityRecord record, bool resolveWorldOffsetFromRuntimeFrame) { RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition; // A receipt of OURS already at the FIFO head (a Place from a prior // submit attempt, or the Withdraw of a deferred park) is consumed // first — acknowledgement is what re-arms a parked operation and what // ResumePendingPlacement's retained-completion check requires. bool acknowledgedSomething = false; while (setPosition.TryPeekProjection( out RuntimePlacementProjectionSnapshot head) && head.Token.Entity == key && head.Kind is RuntimePlacementProjectionKind.Place or RuntimePlacementProjectionKind.Withdraw) { if (!setPosition.AcknowledgeProjection(head.Token)) break; acknowledgedSomething = true; } if (!_entityObjects.InitialCreateExecution .TryGetPendingContinuationPlacement( key, out RuntimeEntityPlacementToken placement)) { // Flavor 2 (transient operation-slot contention): no token was // ever begun; the only correct action is a later Execute retry. return acknowledgedSomething; } if (!_entityObjects.InitialCreateExecution .TryGetPendingContinuationRoute( key, out RuntimeAuthoritativePositionRoute route)) { return acknowledgedSomething; } RuntimeSetPositionMoverPreparationStatus status = setPosition.TryPrepareAndSubmitAuthoredPlacement( record, placement, route.OperationKind, route.SetPositionFlags, _collisionSource, _clock.SimulationTimeSeconds, out RuntimeSetPositionOutcome outcome, resolveWorldOffsetFromRuntimeFrame: resolveWorldOffsetFromRuntimeFrame); if (status != RuntimeSetPositionMoverPreparationStatus.Prepared) { // RetrySetupUnavailable retries on a later pump; a rejected // preparation for an already-submitted-and-awaiting operation is // driven purely by the head acknowledgements above. return acknowledgedSomething; } switch (outcome.Status) { case RuntimeSetPositionStatus.CommittedHostAcknowledgementPending: // The synchronous publish may already have let the production // sink apply-and-acknowledge this exact receipt (the // residence is consumed by drain time, so the sink's // residence gate no longer declines it). A false return here // is therefore benign; the retained acknowledged completion // is what the executor consumes either way. _ = setPosition.AcknowledgeProjection(outcome.Projection); return true; case RuntimeSetPositionStatus.DeferredCell: // Parked with a published Withdraw; consume it if it is // already the head so the collision-generation wake can // resubmit. while (setPosition.TryPeekProjection( out RuntimePlacementProjectionSnapshot parked) && parked.Token.Entity == key && parked.Kind is RuntimePlacementProjectionKind.Withdraw) { if (!setPosition.AcknowledgeProjection(parked.Token)) break; acknowledgedSomething = true; } return acknowledgedSomething; default: // Rejected/Cancelled — authority moved; the next Advance // observes it and abandons through the conductor's own path. return true; } } }