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.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; 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) { _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); _session = session ?? throw new ArgumentNullException(nameof(session)); _log = log ?? (_ => { }); _worldProjection = worldProjection; _acceptedPositionDrive = acceptedPositionDrive; _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, _ => { }, _ => { }); 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()); } } } 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) { 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 || !isLocal || disposition is PositionTimestampDisposition.Rejected) { 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. _worldProjection?.ProjectPosition( record, isLocalPlayer: true, disposition); } } else { _worldProjection?.ProjectPosition( record, isLocalPlayer: true, disposition); } } TryCompletePortal(); } 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; } ulong positionAuthorityVersion = canonical.PositionAuthorityVersion; if (!Entities.TryCommitParent(staged, acknowledgeProjection: null, out _) || !relations.CommitProjection(staged)) { 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, 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}"); } /// /// 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}."); } } }