feat(runtime): C3c - production placement cutover: both hosts on the residence conductors (routes 1+8)
Campaign P remaining-physics-divergence, placement cutover slice C3c
(docs/plans/2026-08-02-placement-cutover.md). Both production hosts now
register every initial Create through the residence + continuation-
executor + first-entry-conductor machinery (C0-C3b):
- Graphical (route 1): RegisterEntityWithInitialResidence at Create; the
shared RuntimeFirstEntryDriveController pumps both conductors from the
placement-receipt flow; MaterializeProjection and RebucketLiveEntity
are presentation-only while a residence is ACTIVE (ExecutorCompleted is
the presentation-binding receipt); post-residence entities take the
full legacy path including the prepare_to_enter_world clock edges.
PlayerModeController attaches presentation to the Runtime-published
controller; its legacy resolve/step-heights/host-construction path is
deleted; presentation-only rollback (retail has no entry-flow rollback).
- Headless (route 8): OnSpawned registers with residence when a drive
exists; content-less sessions keep the pre-flip direct registration;
SynchronizeLocalPlayer/CreateController/ApplySetupStepHeights deleted;
prepared-collision read failure is a typed AwaitingCollisionSource
retry; far remotes outside the service window complete celless.
- RuntimeLocalPlayerMovementState.Controller setter sealed internal; all
controller mutation flows through the publication lifecycle.
Fix slices landed within this cutover, each dual-gated:
- F1: live movement-stat/server-physics application routed through the
Runtime ownership seam (post-logout ingest crash on the retired
controller eliminated; RuntimeMovementSkillProjection deleted).
- F2: login activation wedge - collision-admission prefix gate factored
out of the seal (reentrant-commit RejectedAuthority), rearm generation
identity corrected, PlayerModeAutoEntry requires the Runtime-published
controller (world reveal can no longer seal unmaterialized).
- F3: landblock-prefix 0-sentinel replaced by explicit absent-id guards;
map-corner landblocks (grid row/col 0) fully legal through admission,
park/rearm/retire, quiescence, and outdoor shadow seeds.
- F5: local-player first-entry ground contact seeded by the shared
SpawnPlacementSettler (moved App->Core) at FinalizeActivation - the
retail first-gravity-frame touch (enter_world 0x00516170 carries no
seed); the legacy unconditional force-seed is overwritten by a real
floor-found contact; airborne spawns stay airborne; outbound contact
bit verified end-to-end. Fixes the standing-cast 'You can't do that
while in the air!' rejections.
- R1 (dual-review round): login constraint leash armed at the committed
placement (HandleReceivedPosition 0x00453FD0 analog); register rows
AD-61 (settle-timing compression now covering the local player) and
AD-42 (repointed off the deleted resolve split) in this commit;
residence-conversion owner API; wire-landblock guards; drive-pending
ledger in IsConverged; route attach/detach latch; executor-drain drift
model documented + source-pinned.
Gates: Runtime 1,003, App 4,039/3 skips, Headless 79, complete solution
10,816/0 failed/4 skips (Release, -m:1); connected lifecycle/reconnect
gate PASS (logs/connected-world-gate-20260802-175401; graceful exits,
world-visible, zero airborne rejections). The nine-stop soak remains red
for the pre-existing 6b28ff99 whole-world collision-clone throughput
regression (attributed with evidence; scheduled as its own slice before
C5). Dual Opus reviews (retail-conformance + adversarial): delta PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
78f1eb1896
commit
529e0e9d88
68 changed files with 5977 additions and 831 deletions
354
src/AcDream.Runtime/Session/RuntimeFirstEntryDriveController.cs
Normal file
354
src/AcDream.Runtime/Session/RuntimeFirstEntryDriveController.cs
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
using AcDream.Content;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Physics;
|
||||
|
||||
namespace AcDream.Runtime.Session;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="DriveAll"/> 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
|
||||
/// <see cref="RuntimeEntityObjectLifetime.BindInitialResidenceBeginNotification"/>)
|
||||
/// 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
|
||||
/// <see cref="RuntimeSetPositionState.TryPrepareAndSubmitAuthoredPlacement"/>
|
||||
/// — 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.
|
||||
/// </summary>
|
||||
internal sealed class RuntimeFirstEntryDriveController
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<PlayerMovementConstructionOptions> _localOptions;
|
||||
private readonly Func<RuntimeEntityRecord,
|
||||
RuntimeLocalPlayerPhysicsActivationPreparation> _localActivation;
|
||||
private readonly Dictionary<RuntimeEntityKey, Pending> _pending = [];
|
||||
private readonly List<RuntimeEntityKey> _driveScratch = [];
|
||||
private bool _driving;
|
||||
/// <summary>C3c-R1 review F6: see <see cref="AttachRoute"/>.</summary>
|
||||
private object? _routeOwner;
|
||||
|
||||
internal RuntimeFirstEntryDriveController(
|
||||
RuntimeEntityObjectLifetime entityObjects,
|
||||
IGameRuntimeClock clock,
|
||||
IPreparedCollisionSource collisionSource,
|
||||
Func<PlayerMovementConstructionOptions> localOptions,
|
||||
Func<RuntimeEntityRecord,
|
||||
RuntimeLocalPlayerPhysicsActivationPreparation> 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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal void AttachRoute(object route)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Route-scoped teardown: clears every tracked entry, but ONLY when
|
||||
/// <paramref name="route"/> 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).
|
||||
/// </summary>
|
||||
internal void DetachRoute(object route)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(route);
|
||||
if (!ReferenceEquals(_routeOwner, route))
|
||||
return;
|
||||
_routeOwner = 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;
|
||||
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;
|
||||
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);
|
||||
return;
|
||||
}
|
||||
if (!awaitingContinuationPlacement)
|
||||
{
|
||||
// AwaitingCollisionSource / AwaitingActivation /
|
||||
// AwaitingPlacement / AwaitingReceiptAcknowledgement /
|
||||
// Contention — nothing more this pump can do synchronously.
|
||||
return;
|
||||
}
|
||||
if (!TryCompleteContinuationPlacement(key, pending.Record))
|
||||
return;
|
||||
// A continuation placement progressed — re-Advance so the
|
||||
// executor can consume the acknowledged completion and keep
|
||||
// draining.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes (or makes bounded progress on) the executor's pending
|
||||
/// continuation placement for <paramref name="key"/>. Returns true when
|
||||
/// enough progress happened that re-calling Advance can observe it.
|
||||
/// </summary>
|
||||
private bool TryCompleteContinuationPlacement(
|
||||
RuntimeEntityKey key,
|
||||
RuntimeEntityRecord record)
|
||||
{
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue