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:
Erik 2026-08-02 18:10:33 +02:00
parent 78f1eb1896
commit 529e0e9d88
68 changed files with 5977 additions and 831 deletions

View 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;
}
}
}