using System.Numerics; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; using AcDream.Runtime.Physics; using AcDream.Runtime.World; namespace AcDream.Runtime.Session; public interface IRuntimeDirectWorldProjection { void ProjectSpawn( RuntimeEntityRecord record, bool isLocalPlayer); void ProjectPosition( RuntimeEntityRecord record, bool isLocalPlayer, PositionTimestampDisposition disposition); /// /// R2 review fix (2026-08-03): a ForcePosition on the local player is /// dispatched directly to /// and never reaches at all, so THIS is /// where a host that keeps a narrow collision/streaming window (the /// deleted HeadlessSessionWorldProjection.BlipLocalPlayer's own /// _collision.CenterOn call) re-centers on the destination BEFORE /// the drive controller submits — establishing that the destination's /// collision generation is one this host's window can ever publish is a /// precondition for a DeferredCell park to be a real park rather /// than a dead end (see RuntimeAcceptedPositionDriveController.Advance's /// R1 doc comment). A host with no narrow window (the graphical host, /// whose landblock streaming already follows the accepted position via /// LiveEntityInboundAuthorityGate.ObserveAcceptedLocalPosition) is /// a no-op here. /// void CenterOnAcceptedForcePosition(RuntimeEntityRecord record); void BeginTeleport(); /// /// C4 route 3 (D-T6): is the SAME host token /// /// just registered via TryRegisterHostProjection — the producer's /// generation/sequence/projection are all already in scope here, so no /// new WorldRevealCoordinator-style exposure is needed on this /// side either. /// RuntimeDestinationReadiness PrepareDestination( long revealGeneration, RuntimeTeleportDestination destination, RuntimeWorldHostProjectionToken portal); } /// /// Presentation-free inbound entity route for a direct Runtime host. It /// applies the same canonical identity, timestamp, object-table, and transit /// owners used by the graphical route without constructing App hydration, /// rendering, animation, or effect projections. /// public sealed class RuntimeLiveEntitySessionController { private readonly GameRuntime _runtime; private readonly WorldSession _session; private readonly Action _log; private readonly IRuntimeDirectWorldProjection? _worldProjection; /// /// C4 route 2 (2026-08-03): the headless accepted-Position drive /// controller. Owns its own outbound-ack collaborator internally; the /// ForcePosition + manual LocalPlayerOutboundController.SendImmediatePosition /// pair this class used to drive directly is retired (the deleted /// HeadlessSessionWorldProjection.BlipLocalPlayer). /// private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive; /// /// Campaign OP slice OP7 (2026-08-11): passive observation hook — fires /// AFTER either of this controller's own two internal /// GameActionLoginComplete send sites ('s /// content-less immediate-admission path; 's /// portal-space materialization completion). Never changes when or /// whether LoginComplete is sent — purely additive, so a headless host /// can learn "ACE's FirstEnterWorldDone gate is now open" (set- /// character-options-wire.md §5.2) without duplicating this controller's /// own dual-path completion logic. The THIRD production send site — direct /// (non-portal) first-entry completion via /// RuntimeFirstEntryDriveController's localPlayerCompleted /// callback — lives one level up in HeadlessSessionHost, which /// wires the same observer there directly. /// private readonly Action? _onLoginCompleteSent; private bool _initialLoginCompleteSent; // A6 (architecture review): D5's ResolveAndCommitChildAttachment ran on // every accepted spawn and every ParentEvent, allocating three // this-capturing closures per call. These capture nothing per-call // (only `this`), so cache them once instead of per invocation. private readonly Func _isChildGuidKnown; private readonly Func _resolveParentInstance; private readonly Func _acceptParentEvent; public RuntimeLiveEntitySessionController( GameRuntime runtime, WorldSession session, Action? log = null, IRuntimeDirectWorldProjection? worldProjection = null, RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null, Action? onLoginCompleteSent = null) { _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); _session = session ?? throw new ArgumentNullException(nameof(session)); _log = log ?? (_ => { }); _worldProjection = worldProjection; _acceptedPositionDrive = acceptedPositionDrive; _onLoginCompleteSent = onLoginCompleteSent; _isChildGuidKnown = guid => Entities.Entities.TryGetSnapshot(guid, out _); _resolveParentInstance = guid => Entities.Entities.TryGetSnapshot(guid, out WorldSession.EntitySpawn spawn) ? spawn.InstanceSequence : null; _acceptParentEvent = candidate => Entities.TryApplyParent( candidate, acknowledgeProjection: null, out _); } public LiveEntitySessionSink CreateSink() => new( OnSpawned, OnDeleted, OnPickedUp, OnMotionUpdated, OnPositionUpdated, OnVectorUpdated, OnStateUpdated, OnParentUpdated, OnTeleportStarted, OnAppearanceUpdated, // Effect and sound playback are presentation: the no-window host parses // these packets and discards them, exactly as it does F754/F755. _ => { }, _ => { }, _ => { }); private RuntimeEntityObjectLifetime Entities => _runtime.EntityObjects; private void OnSpawned(WorldSession.EntitySpawn spawn) { // C3c route-8 flip: every direct-host Create enters the SAME initial // residence lease graphical route 1 uses; the conductor drive (via // IRuntimeDirectWorldProjection.ProjectSpawn and the host's pump) // owns mover preparation, body/controller construction, placement, // and the FIFO drain from here. // // C3c-R1 review R3: a CONTENT-LESS host (a validated-legal headless // configuration — HeadlessConfigurationLoader.ValidateContent // accepts a null process.content) constructs no world projection // and therefore no first-entry drive; opening a residence with no // drive to pump it would park every Create (and every position/ // state packet queued behind its pending residence) forever. That // configuration keeps the exact pre-flip legacy registration: // presentation-free RegisterEntity plus the direct accepted-frame // commit below. C4/C5 revisit: unify once the direct-host conductor // drive no longer requires prepared content. RuntimeEntityRegistrationResult registration = _worldProjection is null ? Entities.RegisterEntity(spawn) : Entities.RegisterEntityWithInitialResidence( spawn, isLocalPlayer: spawn.Guid == _runtime.PlayerIdentity.ServerGuid); if (registration.Canonical is not { } canonical) return; ulong integrationVersion = canonical.CreateIntegrationVersion; bool applied = Entities.ApplyAcceptedSpawn( canonical, integrationVersion, canonical.Snapshot, replaceGeneration: registration.Inbound.Disposition is CreateObjectTimestampDisposition.NewGeneration); if (applied) { _worldProjection?.ProjectSpawn( canonical, canonical.ServerGuid == _runtime.PlayerIdentity.ServerGuid); // D5: this spawn may be the parent a standalone ParentEvent // already named before its own CreateObject arrived. RetryChildrenWaitingForParent(canonical.ServerGuid); if (_worldProjection is null && canonical.ServerGuid == _runtime.PlayerIdentity.ServerGuid && !_initialLoginCompleteSent) { // A content-less direct host has no first-entry placement // conductor. Its accepted local Create is therefore its // truthful terminal admission edge. _initialLoginCompleteSent = true; _session.SendGameAction(GameActionLoginComplete.Build()); _onLoginCompleteSent?.Invoke(); } } } private void OnDeleted(DeleteObject.Parsed delete) { if (delete.Guid == _runtime.PlayerIdentity.ServerGuid || !Entities.TryAcceptDelete( delete, isLocalPlayer: false, removeRetainedObject: true, out RuntimeEntityDeleteAcceptance acceptance)) { return; } Entities.CompleteAcceptedDelete(acceptance); if (acceptance.RetiredCanonical is { } retired) { Exception? failure = Entities.RetireCanonicalOnly(retired); if (failure is not null) throw failure; } } private void OnPickedUp(PickupEvent.Parsed pickup) => _ = Entities.TryApplyPickup( pickup, acknowledgeProjection: null, out _); private void OnMotionUpdated( WorldSession.EntityMotionUpdate update) { bool isLocal = update.Guid == _runtime.PlayerIdentity.ServerGuid; _ = Entities.TryApplyMotion( update, retainPayload: !isLocal || !update.IsAutonomous, acknowledgeProjection: null, out _, out _); } private void OnPositionUpdated( WorldSession.EntityPositionUpdate update) { // C5b follow-up (2026-08-05), retail finding F2 / architecture // finding L-A, found independently by both re-reviewers. The // graphical route validates the wire payload BEFORE the merge — // LiveEntityNetworkUpdateController.OnPosition computes // `payloadIsValid` from ProjectileController.CanAcceptPositionPayload // (retail Position::IsValid @0x005A9480 composed with Frame::IsValid // @0x00534ED0, plus finite origin/velocity) and // LiveEntityInboundAuthorityGate.TryAcceptPosition returns false on // it, ahead of the timestamp gate and every wire-cell writer. Despite // its name that check is not projectile-scoped; it runs for every // guid. This route had no equivalent, so an invalid payload merged // here and then — since D1 — fed its unvalidated LandblockId into // CommitWireCellRebucket, whose own doc calls a 0 landblock "the // withdrawal shape": cell 0 + landblock 0, silently de-residencing // the entity in the exact field every bot reads as // RuntimeEntitySnapshot.CellId. // // The predicate is not re-derived here. It is // RuntimeAuthoritativePositionRouteClassifier.IsValidCreateWirePosition // plus the finite-velocity term — literally the pair // RuntimeEntityObjectLifetime.TryApplyPosition already applies on its // initial-residence branch, and the same composition the graphical // gate applies. Rejecting BEFORE the merge (rather than before the // cell commit alone) is what makes the two hosts genuinely // symmetric: neither one lets an invalid payload advance the // timestamp gate. if (!RuntimeAuthoritativePositionRouteClassifier .IsValidCreateWirePosition(update.Position) || update.Velocity is { } wireVelocity && !(float.IsFinite(wireVelocity.X) && float.IsFinite(wireVelocity.Y) && float.IsFinite(wireVelocity.Z))) { return; } bool isLocal = update.Guid == _runtime.PlayerIdentity.ServerGuid; PlayerMovementController? localController = isLocal ? _runtime.MovementOwner.Controller : null; bool known = Entities.TryApplyPosition( update, isLocal, forcePositionRotation: localController?.BodyOrientation, currentLocalVelocity: localController?.BodyVelocity, acknowledgeProjection: null, out PositionTimestampDisposition disposition, out _, out AcceptedPhysicsTimestamps timestamps); if (!known || disposition is PositionTimestampDisposition.Rejected) { // Rejected writes nothing anywhere — the same shape the // graphical authority gate produces by returning false from // LiveEntityInboundAuthorityGate.TryAcceptPosition, which is // ahead of every wire-cell writer. return; } if (!isLocal) { // D1 (C5b architecture review): the no-window host's half of // AD-60's W2. The graphical route commits the accepted wire // cell for EVERY classification that reaches its generic tail, // remotes included; this route used to return here, so a // headless remote's FullCellId was written once at // create/placement and then frozen for the whole session — // and RuntimeEntityObjectViews.Snapshot feeds exactly that // field to every bot's RuntimeEntitySnapshot.CellId. TryCommitAcceptedWireCell(update); return; } if (disposition is PositionTimestampDisposition.Apply) { var position = update.Position; var destination = new RuntimeTeleportDestination( update.Guid, update.InstanceSequence, update.PositionSequence, update.TeleportSequence, update.ForcePositionSequence, new Position( position.LandblockId, new Vector3( position.PositionX, position.PositionY, position.PositionZ), new Quaternion( position.RotationX, position.RotationY, position.RotationZ, position.RotationW))); _runtime.TransitOwner.OfferTeleportDestination( destination, timestamps.TeleportAdvanced); } if (Entities.Entities.TryGetActive( update.Guid, out RuntimeEntityRecord record)) { if (disposition is PositionTimestampDisposition.ForcePosition) { // R2 review fix (2026-08-03): re-center BEFORE submitting — // see IRuntimeDirectWorldProjection.CenterOnAcceptedForcePosition's // doc comment. This is what the deleted BlipLocalPlayer's own // _collision.CenterOn call used to guarantee. _worldProjection?.CenterOnAcceptedForcePosition(record); // C4 route 2 (2026-08-03): the Runtime-owned accepted- // Position execution seam replaces the deleted // HeadlessSessionWorldProjection.BlipLocalPlayer + manual // SendImmediatePosition pair. Canonical commit, controller // reconciliation, and the outbound ack (an OUTPUT of the // committed route, not a step alongside it) all run inside // the call below. RuntimeAcceptedPositionExecutionStatus forceStatus = _acceptedPositionDrive?.TryExecuteAcceptedLocalPosition( record, update, disposition, timestamps, timestamps.PreviousTeleport) ?? RuntimeAcceptedPositionExecutionStatus.NotApplicable; if (forceStatus is RuntimeAcceptedPositionExecutionStatus .NotApplicable) { // R3 review fix (2026-08-03): NotApplicable (e.g. an // initial-Create residence still owns this record — // route 1's job, or the login-window controller-null // branch before route 1 has even published a // controller) is NOT the terminal case the previous // comment here claimed. ProjectPosition's own // controller-null branch is the pre-existing legacy // fallback this disposition always had — it must still // run, exactly as every other disposition's fallback // does below. // // D1: ... and so does the wire-cell commit. A force the // drive HANDLED (Committed/DeferredCell) is // placement-receipt-authoritative for residency, and a // Rejected/Contention force leaves the last committed // cell alone (AD-62's shapes) — both are exactly why // the graphical route returns ahead of W2 on every // status except NotApplicable. Ordering matters as well // as reachability: the drive submits its placement // BEFORE this point, so its first submit reads the // pre-commit FullCellId, which is the source landblock // — AP-138's amended route-2 measurement, matched here // rather than accidentally improved on. TryCommitAcceptedWireCell(update); _worldProjection?.ProjectPosition( record, isLocalPlayer: true, disposition); } } else { TryCommitAcceptedWireCell(update); _worldProjection?.ProjectPosition( record, isLocalPlayer: true, disposition); } } TryCompletePortal(); } /// /// D1 (C5b architecture review): commits the accepted wire cell to /// canonical residency for a no-window host, under reachability rules /// derived one by one from the graphical OnPosition route's own /// early returns for AD-60's W2. They are NOT identical, and AD-64 /// enumerates every place they differ — the two absent gates, the /// residence gate's weaker predicate, and the missile gate's structural /// drift risk. The committed VALUE is /// 's — /// one rule, shared by both hosts, including its landblock-vs-cell /// branch. /// /// /// Two gates mirror callers the graphical route has and this one does /// not. The initial-create residence is /// LiveEntityRuntime.RebucketLiveEntity's own early return /// (MaterializationResidence is AwaitRuntimePlacement && /// HasActiveInitialCreateResidence): while the lease is live, /// Runtime's SetPosition conductor is the sole cell authority. /// Only the residence half is tested here, because it IS the whole /// test on this side — the App enum's AwaitRuntimePlacement value /// exists to mark records that took the residence route, which is every /// record a projection-backed direct host registers, and a content-less /// direct host opens no lease at all /// (). A missile packet is routed by the /// graphical host through the canonical projectile placement owner and /// returns before W2; the predicate below is the exact conjunction that /// route's own null-classification arm uses /// (LiveEntityNetworkUpdateController.OnPosition, the /// isMissilePacket ternary), which its D-P1 comment records as /// equivalent to the classifier's ProjectileAuthoritative /// operation kind. Committing a wire cell for a projectile here would /// invent residency a placement route owns. /// /// /// /// Two known imprecisions, both host-symmetric and both pre-existing; /// filed at AD-64 rather than papered over here. (1) The residence /// gate uses TryGetInitialCreateResidence (TryGetCurrent), /// while RuntimeEntityObjectLifetime.TryApplyPosition's own FIFO /// branch uses the strictly WEAKER TryGetPendingInitialResidence /// (TryGetTransaction = current OR a completed-but-unretired /// lease). In that window the merge enqueues the packet as a /// continuation while this gate reads "no residence" and commits the /// wire cell ahead of the continuation that will replay it. The /// graphical route's RebucketLiveEntity reads the same weaker /// predicate, so both hosts have it identically. (2) The missile /// predicate below is the graphical route's FALLBACK conjunction; that /// route PREFERS earlyRemoteRoute.OperationKind is /// ProjectileAuthoritative and drops to the conjunction only when /// the classification is null. The two agree today — the conjunction is /// what the classifier's own projectile test is built from — but they /// are separate expressions and only one of them is reachable here, /// because this route classifies nothing for a remote. /// /// private void TryCommitAcceptedWireCell( WorldSession.EntityPositionUpdate update) { if (!Entities.Entities.TryGetActive( update.Guid, out RuntimeEntityRecord canonical) || Entities.TryGetInitialCreateResidence(canonical, out _) || IsMissilePacket(canonical, update.Guid)) { return; } // The bool is discarded, where the graphical caller // (LiveEntityRuntime.RebucketLiveEntity) treats false as // ThrowAfterCommittedProjectionChange. That is not a suppressed // failure: false means `Entities.IsCurrent(canonical)` went stale, and // TryGetActive above returned the CURRENT record synchronously three // statements earlier on the same thread, with nothing in between that // can retire it. The graphical caller needs the test because it has // already published spatial/presentation changes by that point and a // stale canonical would leave them orphaned; this route publishes // nothing ahead of the commit, so there is no half-applied state to // detect. Asserting on it would be asserting on an unreachable value. _ = Entities.CommitWireCellRebucket( canonical, update.Position.LandblockId); } private bool IsMissilePacket( RuntimeEntityRecord canonical, uint guid) => guid != _runtime.PlayerIdentity.ServerGuid && (canonical.FinalPhysicsState & PhysicsStateFlags.Missile) != 0 && canonical.Projectile is { } projectile && ReferenceEquals(canonical.PhysicsBody, projectile.Body); private void OnVectorUpdated(VectorUpdate.Parsed update) => _ = Entities.TryApplyVector( update, acknowledgeProjection: null, out _); private void OnStateUpdated(SetState.Parsed update) => _ = Entities.TryApplyState( update, acknowledgeProjection: null, out _, out _); private void OnParentUpdated(ParentEvent.Parsed update) { Entities.Entities.ParentAttachments.Enqueue(update); ResolveAndCommitChildAttachment(update.ChildGuid); } /// /// C4 route 7 D5: the headless parent-realize drive. Resolves a queued /// standalone through the SAME staged /// -> committed protocol the graphical /// EquippedChildRenderController.ResolveAndTryRealize / /// PrepareAndTryRealize pair runs — /// , then /// -> /// -> /// /// (which carries D1's attach re-cell) — so a direct/no-window host /// gets the same canonical child-cell commit the graphical host has /// always had. Deliberately does NOT drive pose composition, the /// render bucket, or ValidateParentProjection's self-parenting /// / part-array / Setup.HoldingLocations checks — see AP-143. /// /// /// KNOWN GAP, stated rather than silently left implicit (retail- /// conformance review R6): if has a /// PENDING initial-create residence when the relation resolves to /// staged, 's /// gate (InboundPhysicsStateController.TryCommitParent's /// gate.PositionTimestamp == positionSequence check) is not yet /// satisfied and this method returns . Nothing /// re-drives it: the residence executor's own parent-attach tail /// (RuntimeInitialCreateContinuationExecutor.CommitParentAttachment) /// deliberately does not commit the relation either — that has always /// been the graphical host's job — and headless has no /// EquippedChildRenderController-equivalent post-drain retry. /// The relation stays staged and the child stays cell-less until SOME /// other event re-invokes /// for the same child (a later ParentEvent, or a spawn naming the same /// parent guid via — which /// does not cover this case either, since the relation is already /// staged, not unresolved). Not a regression (headless committed /// nothing on this path before D5 existed), and the invariant-9 /// dormant-residence deferrals themselves are untouched — but a /// headless ParentEvent arriving during a child's own pending initial /// residence is NOT closed by this slice. /// /// private bool ResolveAndCommitChildAttachment(uint childGuid) { ParentAttachmentState relations = Entities.Entities.ParentAttachments; relations.Resolve( childGuid, _isChildGuidKnown, _resolveParentInstance, _acceptParentEvent); if (!relations.TryGetStagedProjection( childGuid, out ParentAttachmentRelation staged)) { return false; } if (!Entities.Entities.TryGetActive( childGuid, out RuntimeEntityRecord canonical)) { return false; } // #319 A1 (architecture review, 2026-08-05): same ordering fix as // the graphical host's PrepareAndTryRealize - the incarnation // tripwire must run before TryCommitParent's canonical mutation, // never after, so a mismatch refuses cleanly instead of tearing the // transaction. if (!relations.CanCommitIncarnation(staged, _resolveParentInstance)) { relations.RejectProjection(staged); return false; } ulong positionAuthorityVersion = canonical.PositionAuthorityVersion; if (!Entities.TryCommitParent(staged, acknowledgeProjection: null, out _) || !relations.CommitProjection(staged, _resolveParentInstance)) { return false; } bool committed = Entities.CommitAcceptedParentCellless( canonical, positionAuthorityVersion, acknowledgeProjection: null); if (committed && PhysicsDiagnostics.ProbeChildCellEnabled) { Console.WriteLine(FormattableString.Invariant( $"[child-cell] parent=0x{staged.ParentGuid:X8} child=0x{canonical.ServerGuid:X8} new=0x{canonical.FullCellId:X8} cause=headless-attach")); } return committed; } /// /// D5 companion: a ParentEvent can precede the parent's own CreateObject /// (retail: the standalone parent handler queues by parent guid). Retry /// every child waiting on the guid that just became addressable. /// /// /// A6 (architecture review): unlike the graphical /// EquippedChildRenderController.RetryWaitingDescendants → /// ParentAttachmentState.ChildrenWaitingForParent, this drive's /// OWN Resolve/TryGetStagedProjection/CommitProjection /// sequence in consumes a /// relation out of _stagedByChild within the SAME synchronous /// call it was staged in, for the ordinary case. **Correction (B1/B2, /// round-3 review): this is NOT an absolute "never populates /// _stagedByChild" claim** — the R6 gap documented on /// is exactly the /// counter-example: a relation CAN be left sitting in /// _stagedByChild across calls when the child has a pending /// initial-create residence, because TryCommitParent's gate /// isn't satisfied yet. What is true is narrower: THIS retry method /// never needs ChildrenWaitingForParent's STAGED/RECOVERY sweeps /// to find that dangling relation, because it re-resolves through /// childGuid directly via ResolveAndCommitChildAttachment /// on every retry rather than needing a separate discovery query for /// already-staged children — only the UNRESOLVED sweep matters for /// discovering a NEW parent guid becoming addressable. Scanning the /// shared (heavier) ChildrenWaitingForParent on every accepted /// headless spawn would pay for two sweeps and a HashSet /// allocation this discovery step never needs; /// ChildrenUnresolvedForParent scans only /// _unresolvedByChild. **Correction (B4, round-3 review): the /// first round shared a reused scratch buffer across calls for this /// query, which broke reentrancy safety a fresh-array return had — /// a reentrant call into this method (or into /// 's loop below) could /// clear/refill the SAME shared list the outer call was still /// iterating. Reverted to a fresh return per call, matching /// ChildrenWaitingForParent's own allocation shape**, since this /// query already only allocates when it has something to return (most /// parent guids have no unresolved children waiting on them). This is /// a narrower claim than "0 B" either way — the per-child /// queue.Any(lambda) predicate check still allocates a closure /// per call, same as the pre-existing graphical sweep; a full /// incremental parent-guid->children index would remove both /// allocations and is not done in this slice. /// /// private void RetryChildrenWaitingForParent(uint parentGuid) { IReadOnlyList waiting = Entities.Entities.ParentAttachments .ChildrenUnresolvedForParent(parentGuid); for (int i = 0; i < waiting.Count; i++) ResolveAndCommitChildAttachment(waiting[i]); } private void OnTeleportStarted(uint rawSequence) { ushort sequence = unchecked((ushort)rawSequence); RuntimeWorldTransitState transit = _runtime.TransitOwner; if (!transit.TryQueueTeleportStart(sequence)) return; _worldProjection?.BeginTeleport(); if (!transit.ActivateQueuedTeleport()) { throw new InvalidOperationException( "Runtime rejected its queued headless teleport activation."); } TryCompletePortal(); } private void OnAppearanceUpdated(ObjDescEvent.Parsed update) => _ = Entities.TryApplyObjDesc( update, acknowledgeProjection: null, out _); /// /// A1/A3 review fix (2026-08-05): the generation/destination/projection /// of an accepted portal reveal that registered its host projection but /// has not yet actually placed the local player. Headless is /// message-driven, not per-frame — used /// to run the ENTIRE completion sequence (readiness ack, materialized /// ack, complete, LoginComplete, EndTeleport) unconditionally in one /// synchronous call, discarding the canonical portal arm's own status /// (architecture review A3). A DeferredCell park is a NORMAL /// headless outcome — 's /// doc explains why the narrow collision window makes a park real /// rather than a dead end — so this field lets /// retry on the host's own per-tick /// cadence (HeadlessSessionHost.Tick) instead of either /// completing a materialization that never happened or throwing on /// every ordinary "destination not resident yet" park. /// private (long Generation, RuntimeTeleportDestination Destination, RuntimeWorldHostProjectionToken Projection)? _pendingPortalCompletion; /// /// B4 review fix (2026-08-05): the retry count for the CURRENT /// , reset whenever a NEW portal /// begins. Graphical's equivalent wait has a user-visible cue (AD-2's /// centered wait state) when a park runs long; headless had neither a /// cue, a bound, nor a log — an indefinitely stuck park (a destination /// landblock whose collision generation never publishes) was silent and /// undiagnosable. This does not make the retry fatal — K4's 30-session /// endurance profile must survive a legitimately slow-publishing /// landblock — it only makes a stuck park OBSERVABLE via periodic log /// lines instead of running forever in silence. /// private int _pendingPortalCompletionRetryCount; private const int PendingPortalCompletionLogInterval = 100; private void TryCompletePortal() { RuntimeWorldTransitState transit = _runtime.TransitOwner; if (!transit.TryGetAcceptedTeleportDestination( out RuntimeTeleportDestination destination) || !transit.TryBeginPortalReveal( destination.TeleportSequence, destination.CellId, out long generation)) { return; } if (!transit.TryRegisterHostProjection( generation, destination.CellId, out RuntimeWorldHostProjectionToken projection)) { throw new InvalidOperationException( "Runtime rejected the headless portal projection."); } Acknowledge( transit, projection, RuntimeWorldHostAcknowledgementStage.ProjectionRegistered); _pendingPortalCompletion = (generation, destination, projection); _pendingPortalCompletionRetryCount = 0; TryAdvancePortalCompletion(); } /// /// A1/A3 review fix: the retryable second half of /// . Attempts the canonical placement /// (via , which owns the drive controller) /// exactly once per call; if it has not committed yet, this returns /// having mutated nothing beyond what the attempt itself did (a /// DeferredCell park, safely retryable by construction — see /// 's own /// doc), and calls this again on the /// next host tick. Once IsCollisionReady comes back true — which /// only happens after a genuine Committed status — the full /// readiness/materialized/complete/LoginComplete/EndTeleport sequence /// runs exactly as before this fix, unconditionally, in one call. /// private void TryAdvancePortalCompletion() { if (_pendingPortalCompletion is not { } pending) return; (long generation, RuntimeTeleportDestination destination, RuntimeWorldHostProjectionToken projection) = pending; RuntimeWorldTransitState transit = _runtime.TransitOwner; bool indoor = (destination.CellId & 0xFFFFu) >= 0x0100u; RuntimeDestinationReadiness readiness = _worldProjection?.PrepareDestination( generation, destination, projection) ?? new RuntimeDestinationReadiness( generation, destination.CellId, indoor, IsUnhydratable: false, // #280: this "centre ring" token is NOT the graphical host's // derived reveal radius and must not be made to track it. This // host has no streaming window, no render publication and no // composites, so there is nothing wider for a radius to mean // here. Runtime validates only the SHAPE (indoor => 0, // outdoor => >= 1), which this satisfies by construction. RequiredRenderRadius: indoor ? 0 : 1, IsRenderNeighborhoodReady: true, AreCompositeTexturesReady: true, IsCollisionReady: true); if (!readiness.IsCollisionReady) { // Still parked - PrepareDestination attempted (or is waiting on // an outstanding DeferredCell wake) and has not committed yet. // Nothing acknowledged, nothing completed; PumpPortalCompletion // retries next tick. // // B4 review fix: periodic diagnostic so an indefinitely-stuck // park is observable instead of silent. Not bounded to a throw - // a slow-publishing landblock is a legitimate transient this // host must ride out (N3's lesson: don't make a transient // fatal). _pendingPortalCompletionRetryCount++; if (_pendingPortalCompletionRetryCount % PendingPortalCompletionLogInterval == 0) { _log( $"headless: portal completion still parked after " + $"{_pendingPortalCompletionRetryCount} retries " + $"generation={generation} cell=0x{destination.CellId:X8}"); } return; } _pendingPortalCompletion = null; _pendingPortalCompletionRetryCount = 0; if (!transit.AcknowledgeDestinationReadiness( readiness)) { throw new InvalidOperationException( "Runtime rejected headless destination readiness."); } if (!transit.AcknowledgePortalMaterialized( generation, destination.TeleportSequence, destination.CellId)) { throw new InvalidOperationException( "Runtime rejected headless portal materialization."); } Acknowledge( transit, projection, RuntimeWorldHostAcknowledgementStage .SimulationReleaseProjected); if (!transit.RequireDestinationReservationRelease(projection)) { throw new InvalidOperationException( "Runtime rejected headless destination release."); } Acknowledge( transit, projection, RuntimeWorldHostAcknowledgementStage .DestinationReservationReleased); if (!transit.AcknowledgeWorldViewportVisible(generation) || !transit.Complete(generation)) { throw new InvalidOperationException( "Runtime rejected headless portal completion."); } Acknowledge( transit, projection, RuntimeWorldHostAcknowledgementStage.TerminalProjected); _session.SendGameAction(GameActionLoginComplete.Build()); transit.EndTeleport(); _log( $"headless: portal complete generation={generation} " + $"cell=0x{destination.CellId:X8}"); // SF-3 (Campaign OP OP7 review fix, 2026-08-11): invoke AFTER the // teleport-completion tail, matching the other production call // site's shape (OnSpawned, above). The observer body is not // trivial — it runs the full diff, real SendGameActions, and // event-hub publication to bot policies — so a throw from it must // not abort transit.EndTeleport() with the retry token already // discarded. _onLoginCompleteSent?.Invoke(); } /// /// A1/A3 review fix: called from HeadlessSessionHost.Tick /// alongside HeadlessSessionWorldProjection.PumpFirstEntry — /// retries a parked portal completion on the host's own per-tick /// cadence. A no-op whenever nothing is pending. /// public void PumpPortalCompletion() => TryAdvancePortalCompletion(); private static void Acknowledge( RuntimeWorldTransitState transit, RuntimeWorldHostProjectionToken projection, RuntimeWorldHostAcknowledgementStage stage) { if (!transit.AcknowledgeHostProjection( new RuntimeWorldHostAcknowledgement( projection, stage))) { throw new InvalidOperationException( $"Runtime rejected headless host acknowledgement {stage}."); } } }