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