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}.");
}
}
}