using AcDream.Content; using AcDream.Runtime.Entities; using AcDream.Runtime.Physics; namespace AcDream.Runtime.Gameplay; /// /// Typed yields for . /// Mirrors the executor's own RuntimeInitialCreateExecutionStatus /// shape (terminal Completed/RejectedToken/RejectedAuthority plus named /// retryable yields) rather than inventing a parallel vocabulary. /// internal enum RuntimeLocalPlayerFirstEntryStatus : byte { /// /// The underlying /// call reported Completed: residence consumed, initial tail and /// FIFO drained, ExecutorCompleted receipt dispatched on the placement /// stream. Terminal; the conductor's own progress entry is removed. /// Completed, /// /// The authored-mover Setup read /// () is not /// yet available (RetrySetupUnavailable) — the prepared-asset /// package has not landed. Retry /// with the same arguments once it has; no Runtime state changed. /// AwaitingCollisionSource, /// /// /// or /// yielded DeferredCell or a retryable RejectedPlacement — /// the destination cell's collision generation is not ready, or the /// placement needs re-evaluation after some other change. Retry later /// (e.g. on a collision-generation wake); the dormant activation lease /// itself remains intact and is re-driven from the same stage. /// AwaitingActivation, /// /// The activation committed but its Place projection has not been /// acknowledged yet — either /// was not (yet) the exact FIFO head, or the subsequent /// call /// still observed PendingPlacement from /// . Retry the /// same stage. /// AwaitingReceiptAcknowledgement, /// /// Passthrough of the executor's own AwaitingContinuationPlacement /// — a later FIFO continuation (a Position update accepted while this /// entity's initial placement was in flight) needs its own authored /// placement prepared/submitted/acknowledged before the drain can /// finish. Entirely the executor's own concern from this point forward; /// the conductor's job (residence -> publication -> Execute) is done as /// soon as it reaches this yield. /// AwaitingContinuationPlacement, /// /// A reentrant /// call for the SAME entity arrived while an outer call for it was still /// on the stack (mirrors the executor's own _executing fail-closed /// guard). Not a Runtime-state rejection — retry once the outer call has /// returned. /// Contention, /// The residence/placement token no longer matches anything tracked. RejectedToken, /// /// An authority-shaped failure (stale epoch/session/identity, deleted or /// replaced record, GUID reuse, disposed identity, or an inner /// currency check failing during a reentrant callback). Abandoned: the /// conductor's own in-flight publication candidate/activation (if any) /// is discarded through the shared choke points and its progress entry /// is removed. The caller must begin a fresh first-entry sequence /// (a new residence lease) rather than retry this exact call. /// RejectedAuthority, } internal readonly record struct RuntimeLocalPlayerFirstEntryOwnershipSnapshot( int ActiveCount) { internal bool IsConverged => ActiveCount == 0; } /// /// The dormant, resumable Runtime transaction that dissolves C3's Finding B: /// the local player's initial residence lease opens its SetPosition operation /// at Create time, but nothing wires the mover-preparation -> /// body/controller /// attach -> placement acknowledgement -> FIFO drain sequence together into /// one driveable state machine. This class ORCHESTRATES the existing, /// already-tested residence /// (), publication /// (), mover /// (), and /// executor () /// machinery — it reimplements none of their validation and bypasses none of /// their staged semantics. /// /// Required order (campaign handoff route-1, /// docs/research/2026-07-31-remaining-physics-campaign-handoff.md:280-292): /// residence Begin (already done at registration, before this class is ever /// invoked) -> authored-mover preparation (Setup read + PrepareMover, which /// MUST precede publication Prepare — /// 's own /// CanPrepare gate requires /// to /// already be true) -> publication Prepare -> publication Commit (the /// /// seam attaches the body) -> activation Evaluate/Commit/Finalize -> /// Place-receipt acknowledgement -> /// (FIFO drain) -> ExecutorCompleted receipt. /// /// Once /// sets an operation's DormantLocalActivation flag, /// (and the /// fused ) /// must never be called against it again — see RetryDeferred's "must /// never bypass that path through the ordinary remote CommitCanonical tail" /// comment — so this class calls the mover-only /// half instead /// and never the fused method. /// /// PRODUCTION-DRIVEN since the C3c flip: /// fully constructs and wires this class (construction, publication binding, /// retirement fan-out, bulk session-clear cleanup, ownership fold) exactly /// like every other owner it builds, and the host first-entry drive /// (RuntimeFirstEntryDriveController, pumped by the graphical /// hydration/frame-retry cadence and the headless spawn/position/tick /// cadence) calls for every local-player /// initial-create residence. /// internal sealed class RuntimeLocalPlayerFirstEntryState { private enum Stage : byte { /// No progress yet, or the mover has not been prepared. AwaitingMoverPreparation, /// Mover command in hand; publication Prepare+Commit not run yet. MoverPrepared, /// /// Publication Prepare+Commit succeeded (the body/controller are /// attached to the canonical record); Evaluate+CommitActivation not /// yet reached Committed. Also the retry point for /// DeferredCell/RejectedPlacement. /// PublicationCommitted, /// /// CommitActivation reached Committed; the Place projection is /// known but not yet acknowledged. /// ActivationCommitted, /// /// The Place projection has been acknowledged. Only /// /// remains; re-acknowledging the same (already-consumed) token would /// fail, so this stage is never re-entered by the acknowledgement /// step. /// Acknowledged, } private sealed class Progress { internal required ulong LeaseId { get; init; } internal Stage Stage { get; set; } = Stage.AwaitingMoverPreparation; internal RuntimeSetPositionCommand PreparedCommand { get; set; } internal RuntimeLocalPlayerPhysicsPublicationToken PublicationToken { get; set; } internal RuntimeLocalPlayerPhysicsActivationToken ActivationToken { get; set; } internal RuntimePlacementProjectionToken Projection { get; set; } } private readonly RuntimeInitialCreateResidenceState _residences; private readonly RuntimeInitialCreateContinuationExecutor _executor; private readonly RuntimePhysicsState _physics; private RuntimeLocalPlayerPhysicsPublicationState? _publication; private readonly Dictionary _progress = []; private readonly HashSet _executing = []; /// /// F2: constructs this class /// (alongside the residence and executor it also owns) BEFORE /// exists — /// GameRuntime creates RuntimeLocalPlayerMovementState and /// attaches its physics publication only after the entity-object lifetime /// is already built. This mirrors the SAME late-bind pattern already /// used throughout this class family (BindGeneration, /// BindRetirementNotification, BindLiveInputs, /// RuntimeLocalPlayerMovementState.PhysicsPublication's own /// throws-if-unbound accessor) rather than requiring the caller to /// construct things out of their natural order. /// internal RuntimeLocalPlayerFirstEntryState( RuntimeInitialCreateResidenceState residences, RuntimeInitialCreateContinuationExecutor executor, RuntimePhysicsState physics) { _residences = residences ?? throw new ArgumentNullException(nameof(residences)); _executor = executor ?? throw new ArgumentNullException(nameof(executor)); _physics = physics ?? throw new ArgumentNullException(nameof(physics)); } internal void BindPublication( RuntimeLocalPlayerPhysicsPublicationState publication) { ArgumentNullException.ThrowIfNull(publication); if (_publication is not null) { throw new InvalidOperationException( "The local-player first-entry conductor's publication owner is already bound."); } _publication = publication; } private RuntimeLocalPlayerPhysicsPublicationState Publication => _publication ?? throw new InvalidOperationException( "The local-player first-entry conductor's publication owner is not yet bound."); /// /// One resumable step. Callers pass the SAME arguments on every retry; /// this method re-reads currency from the owning states on every entry /// rather than trusting anything cached beyond its own stage cursor and /// the exact token/receipt/projection structs each owning method itself /// requires as arguments — there is no other source for those; they are /// the "exact keys" this class carries, not a second copy of any owning /// state's internal record. /// internal RuntimeLocalPlayerFirstEntryStatus Advance( RuntimeEntityRecord record, in RuntimeInitialCreateResidenceToken residenceToken, PlayerMovementConstructionOptions options, in RuntimeLocalPlayerPhysicsActivationPreparation activationPreparation, IPreparedCollisionSource collisionSource, double gameTime, in RuntimeInitialCreateExecutionInputs inputs, out RuntimeInitialCreateExecutionReceipt receipt) { ArgumentNullException.ThrowIfNull(record); ArgumentNullException.ThrowIfNull(collisionSource); receipt = default; if (!residenceToken.IsValid || record.Key is not { } key) return RuntimeLocalPlayerFirstEntryStatus.RejectedToken; // Mirrors RuntimeInitialCreateContinuationExecutor.Execute's own // _executing.Add(key) guard: a synchronous reentrant call for the // SAME entity (e.g. from a collision-report/placement observer // invoked mid-Advance) fails closed rather than interleaving two // drains of the same stage machine. if (!_executing.Add(key)) return RuntimeLocalPlayerFirstEntryStatus.Contention; try { return AdvanceCore( record, residenceToken, options, activationPreparation, collisionSource, gameTime, inputs, key, out receipt); } finally { _executing.Remove(key); } } private RuntimeLocalPlayerFirstEntryStatus AdvanceCore( RuntimeEntityRecord record, in RuntimeInitialCreateResidenceToken residenceToken, PlayerMovementConstructionOptions options, in RuntimeLocalPlayerPhysicsActivationPreparation activationPreparation, IPreparedCollisionSource collisionSource, double gameTime, in RuntimeInitialCreateExecutionInputs inputs, RuntimeEntityKey key, out RuntimeInitialCreateExecutionReceipt receipt) { receipt = default; // H2: fail transactionally, before any state mutation, if // Publication has not been bound yet. Without this upfront check, // an unbound call could still get as far as authored-mover // preparation (which mutates RuntimeSetPositionState's own // _preparedMovers) and creating THIS class's own Progress entry // (stored into _progress) before the first Publication dereference // (inside the MoverPrepared stage below) throws — leaving a // poisoned Progress entry that a later, unrelated Discard/DiscardAll // call (from a retirement notification or session-clear fan-out) // would ALSO throw on. Referencing the accessor here throws // immediately with nothing yet mutated. _ = Publication; _progress.TryGetValue(key, out Progress? progress); // ABA/GUID-reuse guard, exactly like the executor's own Progress // reconciliation: an existing entry for a DIFFERENT (older or // reused) lease id can never be resumed by this call. if (progress is not null && progress.LeaseId != residenceToken.LeaseId) { Discard(key); progress = null; } if (progress is null || progress.Stage is Stage.AwaitingMoverPreparation) { if (!_residences.TryGetCurrent( record, out RuntimeInitialCreateResidenceLease lease) || lease.Token != residenceToken) { // No progress was ever tracked for this key under this exact // lease — mirrors RuntimeInitialCreateResidenceState.Complete's // own convention (a token matching neither its active nor its // completed table is RejectedToken, not RejectedAuthority). // Only abandon (RejectedAuthority) when THIS class was // actually tracking in-flight publication/activation state // that must now be discarded. if (progress is null) return RuntimeLocalPlayerFirstEntryStatus.RejectedToken; Discard(key); return RuntimeLocalPlayerFirstEntryStatus.RejectedAuthority; } if (!lease.Route.PerformsSetPosition) { // Parented/PickedUp residence — never true for a real // login, but kept for structural completeness: no // SetPosition operation exists at all, so there is nothing // for the publication chain to attach a body to. Skip // straight to Execute (idempotent/retryable on its own). progress ??= new Progress { LeaseId = residenceToken.LeaseId }; progress.Stage = Stage.Acknowledged; _progress[key] = progress; return RunExecute(record, residenceToken, inputs, key, out receipt); } RuntimeSetPositionMoverPreparationStatus moverStatus = _physics .SetPosition.TryPrepareAuthoredMover( record, lease.Placement, lease.Route.OperationKind, lease.Route.SetPositionFlags, collisionSource, gameTime, out RuntimeSetPositionCommand command); // #284: every retryable reason resumes on a later pump. Comparing // against one reason would silently reclassify a new one as a // hard rejection. if (moverStatus.IsRetryable()) { return RuntimeLocalPlayerFirstEntryStatus .AwaitingCollisionSource; } if (moverStatus != RuntimeSetPositionMoverPreparationStatus.Prepared) { if (progress is not null) Discard(key); return RuntimeLocalPlayerFirstEntryStatus.RejectedAuthority; } progress ??= new Progress { LeaseId = residenceToken.LeaseId }; progress.PreparedCommand = command; progress.Stage = Stage.MoverPrepared; _progress[key] = progress; } if (progress.Stage is Stage.MoverPrepared) { if (!_residences.TryGetCurrent( record, out RuntimeInitialCreateResidenceLease lease) || lease.Token != residenceToken) { Discard(key); return RuntimeLocalPlayerFirstEntryStatus.RejectedAuthority; } RuntimeLocalPlayerPhysicsPublicationStatus prepareStatus = Publication.Prepare( record, lease.Placement, progress.PreparedCommand, options, activationPreparation, out RuntimeLocalPlayerPhysicsPublicationToken pubToken); if (prepareStatus != RuntimeLocalPlayerPhysicsPublicationStatus.Prepared) { // Prepare mutates nothing canonical on rejection (its own // second CanPrepare recheck discards any just-built // candidate itself); there is nothing further for this // class to undo beyond dropping its own progress entry. Discard(key); return RuntimeLocalPlayerFirstEntryStatus.RejectedAuthority; } progress.PublicationToken = pubToken; RuntimeLocalPlayerPhysicsPublicationStatus commitStatus = Publication.Commit( pubToken, out RuntimeLocalPlayerPhysicsActivationToken activationToken); if (commitStatus != RuntimeLocalPlayerPhysicsPublicationStatus.Committed) { Discard(key); return commitStatus is RuntimeLocalPlayerPhysicsPublicationStatus.RejectedToken ? RuntimeLocalPlayerFirstEntryStatus.RejectedToken : RuntimeLocalPlayerFirstEntryStatus.RejectedAuthority; } progress.ActivationToken = activationToken; progress.Stage = Stage.PublicationCommitted; } if (progress.Stage is Stage.PublicationCommitted) { // A single combined retry point for Evaluate+CommitActivation. // Every publication test that hits DeferredCell/RejectedPlacement // WITHOUT an intervening CommitActivation call chains both calls // together and retries both together; running EvaluateActivation // again before every CommitActivation retry is safe even for // CommitActivation's own internal AwaitingFinalShadowPreparation // resumption (its top-of-method check re-validates // activation.Receipt == receipt, which a fresh Evaluate call // satisfies, before consulting the untouched stored // PendingFinalCommit). // // EvaluateActivation's DeferredCell status is overloaded: once a // PRIOR CommitActivation call has already registered this lease // as awaiting a specific cell/collision generation // (IsDormantLocalActivationAwaitingCell), a REPEATED // EvaluateActivation call that is still not ready returns // DeferredCell WITHOUT ever populating receipt (it stays // default/invalid) — see // DeferredCommitWaitsThenRearmsSameLeaseAfterExactGenerationWake // in the publication test suite, which asserts exactly // `waiting.IsValid == false` on that repeat call and never feeds // it to CommitActivation. Calling CommitActivation with that // invalid receipt would hit its own `!receipt.IsValid` guard and // incorrectly report RejectedAuthority instead of "still // waiting" — so this class must check validity first and simply // yield AwaitingActivation again without calling CommitActivation // at all in that case. RuntimeLocalPlayerPhysicsActivationStatus evalStatus = Publication.EvaluateActivation( progress.ActivationToken, out RuntimeLocalPlayerPhysicsActivationReceipt evalReceipt); if (evalStatus is RuntimeLocalPlayerPhysicsActivationStatus.RejectedToken or RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority) { Discard(key); return evalStatus is RuntimeLocalPlayerPhysicsActivationStatus.RejectedToken ? RuntimeLocalPlayerFirstEntryStatus.RejectedToken : RuntimeLocalPlayerFirstEntryStatus.RejectedAuthority; } if (!evalReceipt.IsValid) { // DeferredCell with nothing to commit — the destination // cell/collision generation genuinely is not resolvable yet. return RuntimeLocalPlayerFirstEntryStatus.AwaitingActivation; } RuntimeDormantSetPositionCommitStatus commitActivationStatus = Publication.CommitActivation( evalReceipt, out RuntimePlacementProjectionToken projection); switch (commitActivationStatus) { case RuntimeDormantSetPositionCommitStatus.Committed: progress.Projection = projection; progress.Stage = Stage.ActivationCommitted; break; case RuntimeDormantSetPositionCommitStatus.DeferredCell: case RuntimeDormantSetPositionCommitStatus.RejectedPlacement: // Stage stays PublicationCommitted — retry re-runs both // EvaluateActivation and CommitActivation next Advance. return RuntimeLocalPlayerFirstEntryStatus.AwaitingActivation; default: Discard(key); return RuntimeLocalPlayerFirstEntryStatus.RejectedAuthority; } } if (progress.Stage is Stage.ActivationCommitted) { if (!_physics.SetPosition.AcknowledgeProjection( progress.Projection)) { // A failed acknowledge is retryable ONLY while nothing has // moved authority out from under this exact projection — a // genuinely later entity simply sitting ahead of ours in the // FIFO. It is NOT automatically retryable: a mid-flight // delete (TryAcceptDelete -> CompleteProjectionRetirement -> // Physics.SetPosition.Forget -> CancelCoreDeferred) rewrites // the SAME pending slot from Place to Discard with a bumped // Revision, so the exact struct this class cached in // progress.Projection can never match the FIFO head again — // without this check, AcknowledgeProjection would fail // forever and this progress entry would never converge. if (!IsAcknowledgementStillPending( record, residenceToken, progress.Projection)) { Discard(key); return RuntimeLocalPlayerFirstEntryStatus.RejectedAuthority; } return RuntimeLocalPlayerFirstEntryStatus .AwaitingReceiptAcknowledgement; } progress.Stage = Stage.Acknowledged; } return RunExecute(record, residenceToken, inputs, key, out receipt); } /// /// Re-validates authority after a failed acknowledge. C3b review M2: /// the mechanism (residence-lease currency + exact-head-token match; a /// DIFFERENT entity's head stays retryable) is shared verbatim with the /// remote conductor via /// — one /// body, so the abandonment fix that stops a delete-rewritten Discard /// head from producing an infinite AwaitingReceiptAcknowledgement retry /// cannot regress independently in either conductor. Full rationale on /// the shared helper. /// private bool IsAcknowledgementStillPending( RuntimeEntityRecord record, in RuntimeInitialCreateResidenceToken residenceToken, in RuntimePlacementProjectionToken expected) => RuntimeFirstEntryAcknowledgement.IsStillPending( _residences, _physics.SetPosition, record, residenceToken, expected); private RuntimeLocalPlayerFirstEntryStatus RunExecute( RuntimeEntityRecord record, in RuntimeInitialCreateResidenceToken residenceToken, in RuntimeInitialCreateExecutionInputs inputs, RuntimeEntityKey key, out RuntimeInitialCreateExecutionReceipt receipt) { RuntimeInitialCreateExecutionStatus executeStatus = _executor.Execute( record, residenceToken, inputs, out receipt); switch (executeStatus) { case RuntimeInitialCreateExecutionStatus.Completed: _progress.Remove(key); return RuntimeLocalPlayerFirstEntryStatus.Completed; case RuntimeInitialCreateExecutionStatus.PendingPlacement: return RuntimeLocalPlayerFirstEntryStatus .AwaitingReceiptAcknowledgement; case RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement: return RuntimeLocalPlayerFirstEntryStatus .AwaitingContinuationPlacement; case RuntimeInitialCreateExecutionStatus.RejectedToken: _progress.Remove(key); return RuntimeLocalPlayerFirstEntryStatus.RejectedToken; default: _progress.Remove(key); return RuntimeLocalPlayerFirstEntryStatus.RejectedAuthority; } } /// /// Discards any in-flight publication candidate/activation this class /// owns for and drops its own progress entry. /// Safe to call at every stage: /// and /// are both no-ops against a default/invalid token (an unreached stage's /// token field holds exactly that) AND against a token whose activation /// has already reached FinalizeActivation's terminal success path /// — FinalizeActivation itself nulls the publication's own /// tracked activation the instant the commit succeeds, because the /// controller is now genuinely live/published, not a discardable /// in-progress candidate. Calling this once /// (or later) has been reached is therefore correctly a no-op on the /// controller/body — an abandoned Place acknowledgement never /// retroactively un-publishes an already-live entity; that is ordinary /// entity teardown's job (the SAME path any other live entity's delete /// already runs), not this class's. The residence and executor own /// their own convergence independently (their existing retirement/reset /// paths are untouched by this class). /// private void Discard(RuntimeEntityKey key) { if (!_progress.TryGetValue(key, out Progress? progress)) return; _progress.Remove(key); // H2: post-H2, a Progress entry can only exist at all if Advance's // own upfront check already found Publication bound — this should // therefore be structurally unreachable. Guarded anyway // (belt-and-suspenders) so a future caller shape can never turn an // already-surfaced Advance failure into a SECOND throw from inside // an unrelated retirement/session-clear teardown fan-out. if (_publication is null) return; Publication.Discard(progress.PublicationToken); Publication.DiscardActivation(progress.ActivationToken); } /// /// Cleanup for one key this class is tracking. /// binds this into 's /// multicast retirement notification (alongside the executor's own /// DiscardProgress), so any residence retirement path — delete, /// reset, generation replacement, a host discovering staleness — reaps /// this class's progress automatically. The notification always carries /// the exact the residence itself tracked /// internally, so it converges correctly even after /// has gone null (e.g. post-delete /// teardown released the local id) — unlike re-deriving a key from the /// record, which cannot do once that happens. /// Still exposed directly for a caller that captured a key before a /// teardown this class was not notified about (e.g. constructed /// standalone in a test without the lifetime's fan-out). /// internal void Forget(RuntimeEntityKey key) => Discard(key); /// /// Bulk cleanup mirroring /// — wired into the same session-clear sequence /// (). Discards /// every tracked key's in-flight publication candidate/activation before /// dropping the whole progress table, exactly like a per-key /// for each entry. /// internal void DiscardAll() { // H2: same belt-and-suspenders tolerance as Discard above — a // structurally unreachable case post-H2, guarded so bulk session // clear can never throw from an unbound Publication either. if (_publication is not null) { foreach (Progress progress in _progress.Values) { Publication.Discard(progress.PublicationToken); Publication.DiscardActivation(progress.ActivationToken); } } _progress.Clear(); } internal RuntimeLocalPlayerFirstEntryOwnershipSnapshot CaptureOwnership() => new(_progress.Count); }