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

@ -523,7 +523,9 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
/// </summary>
public event Action<LiveEntityRecord, bool>? ProjectionVisibilityChanged;
public LiveEntityRegistrationResult RegisterLiveEntity(WorldSession.EntitySpawn incoming)
public LiveEntityRegistrationResult RegisterLiveEntity(
WorldSession.EntitySpawn incoming,
bool isLocalPlayer = false)
{
if (_isClearing || _sessionClearPendingFinalization || _isRegisteringResources)
{
@ -533,9 +535,16 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
: "A live entity cannot register from inside atomic resource registration.");
}
// C3c route-1 flip: every graphical initial Create enters the
// canonical initial-residence lease. The accepted wire frame stays on
// the canonical record with FullCell 0 until the authored Runtime
// SetPosition operation commits; the host's first-entry drive
// controller walks the conductors from the residence-begin
// notification.
RuntimeEntityRegistrationResult registration =
_entityObjects.RegisterEntity(
_entityObjects.RegisterEntityWithInitialResidence(
incoming,
isLocalPlayer,
RetirePriorProjection);
RuntimeEntityRecord? canonical = registration.Canonical;
LiveEntityRecord? projection = canonical is null
@ -795,11 +804,32 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|| record.WorldEntity is not { } entity)
return false;
if (record.MaterializationResidence is
LiveEntityMaterializationResidence.AwaitRuntimePlacement)
LiveEntityMaterializationResidence.AwaitRuntimePlacement
&& HasActiveInitialCreateResidence(record.Canonical))
{
// The private Runtime Place path below performs a presentation-
// only bucket update. This legacy API also commits canonical
// Runtime residence and cannot touch a cut-over incarnation.
// C3c: while the initial-create residence is ACTIVE, Runtime's
// SetPosition owner is the sole canonical position/cell/
// object-clock authority and even the graphical bucket stays
// suppressed: the conductor's completion receipt (which reaches
// presentation through
// TryApplyInitialCreateCompletionPresentation, not this API) is
// the entity's first world-visible moment. Without this gate a
// re-entrant caller (e.g. a resource-registration observer)
// could install a bucket for a suppressed record before its
// placement ever committed. A STALE residence is lazily retired
// by this same query, after which legacy moves flow.
//
// C3c-R1 review R2: the gate is the EXACT-token residence
// activity view, never the sticky MaterializationResidence enum
// alone. Post-residence (the lease completed and was consumed)
// this method falls through to the FULL legacy branch below:
// the unflipped update routes (network position/state, remote
// and local teleports, streaming reprojection, hydration
// recovery) are the position authority again, and retail's
// prepare_to_enter_world (0x00511FA0) clock rebase must run on
// every root-workset membership edge — the earlier
// presentation-only shortcut skipped CommitRebucket and that
// clock edge for the entity's whole post-residence lifetime.
return false;
}
@ -930,6 +960,197 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
return true;
}
/// <summary>
/// C3c: the graphical-bucket-only projection of a conductor-owned
/// initial placement — called ONLY from
/// <see cref="TryApplyInitialCreateCompletionPresentation"/> (the
/// completion receipt at the initial-create residence boundary), never
/// from the public <see cref="RebucketLiveEntity"/> (C3c-R1 review R2:
/// post-residence moves take the full legacy branch there).
/// Deliberately never calls <c>CommitRebucket</c>,
/// <c>SuspendObjectClock</c>, or <c>ResetObjectClockForEnterWorld</c> —
/// Runtime's SetPosition commit already owns all of those for the
/// residence-driven placement this receipt projects. May place into a
/// pending (not-yet-loaded) bucket exactly like the legacy Create path
/// did; the pending drain publishes visibility when the landblock loads.
/// </summary>
private bool RebucketLiveEntityPresentationOnly(
uint serverGuid,
LiveEntityRecord record,
WorldEntity entity,
uint spatialCellOrLandblockId)
{
RuntimeEntityKey key = RequireProjectionKey(record);
bool wasProjected = record.IsSpatiallyProjected;
bool wasVisible = record.IsSpatiallyVisible;
ulong projectionOperation = ++record.ProjectionMutationVersion;
record.IsSpatiallyProjected = true;
Exception? spatialNotificationFailure = null;
uint priorRebucketingGuid = _rebucketingGuid;
_rebucketingGuid = serverGuid;
BeginPresentationOnlySpatialMutation(key);
try
{
try
{
_spatial.RebucketLiveEntity(
key,
entity,
spatialCellOrLandblockId);
}
catch (AggregateException error)
{
spatialNotificationFailure = error;
}
}
finally
{
EndPresentationOnlySpatialMutation(key);
_rebucketingGuid = priorRebucketingGuid;
}
if (!IsCurrentProjectionOperation(serverGuid, record, projectionOperation))
{
ThrowAfterCommittedProjectionChange(
serverGuid,
spatialNotificationFailure,
runtimeNotificationFailure: null);
return false;
}
bool visible = _spatial.IsLiveEntityProjectionResident(key);
record.IsSpatiallyVisible = visible;
RefreshSpatialPresentationIndexes(record);
RefreshPresentation(record);
RefreshSpatialRuntimeIndexes(record);
Exception? runtimeNotificationFailure = null;
if (!wasProjected || wasVisible != visible)
{
try
{
PublishProjectionVisibilityChanged(record, visible);
}
catch (Exception error)
{
runtimeNotificationFailure = error;
}
}
if (!IsCurrentProjectionOperation(serverGuid, record, projectionOperation))
{
ThrowAfterCommittedProjectionChange(
serverGuid,
spatialNotificationFailure,
runtimeNotificationFailure);
return false;
}
ThrowAfterCommittedProjectionChange(
serverGuid,
spatialNotificationFailure,
runtimeNotificationFailure);
return true;
}
/// <summary>
/// C3c: applies one initial-Create ExecutorCompleted receipt's
/// presentation — the graphical binding point for a residence-driven
/// initial placement. Runtime committed position, cell, body, clocks,
/// and worksets during the conductor's drain; this installs the
/// committed frame on the sidecar and moves its graphical bucket
/// (pending buckets allowed — the legacy Create path's own semantics).
/// Superseded facts (a later legacy-path move already advanced the
/// record past the receipt) are treated as already-projected: the
/// receipt is stale for presentation and must not snap the entity back.
/// </summary>
internal bool TryApplyInitialCreateCompletionPresentation(
in RuntimePlacementProjectionSnapshot projection)
{
RuntimePlacementProjectionToken token = projection.Token;
if (!token.IsValid
|| token.SessionLifetimeVersion != _directory.SessionLifetimeVersion
|| !_projections.TryGet(token.Entity, out LiveEntityRecord? record)
|| !_directory.IsCurrent(record.Canonical)
|| record.Canonical.Key != token.Entity
|| record.WorldEntity is not { } entity)
{
// No sidecar (a deferred-child replay materializes later and
// self-projects from canonical state) or a displaced identity —
// acknowledge-only.
return true;
}
if (record.FullCellId != token.ExactCellId
|| record.Canonical.PlacementCommitVersion
!= token.PlacementCommitVersion)
{
// A newer move superseded this receipt's facts after the drain.
return true;
}
entity.SetPosition(projection.WorldPosition);
entity.Rotation = projection.Orientation;
entity.ParentCellId = token.ExactCellId;
entity.EffectCellId = token.ExactCellId;
return RebucketLiveEntityPresentationOnly(
record.ServerGuid,
record,
entity,
token.ExactCellId);
}
/// <summary>
/// C3c: true while the exact incarnation behind <paramref name="key"/>
/// holds an initial-create residence lease — the discriminator the
/// placement sink uses to leave conductor-owned Place/Withdraw receipts
/// at the FIFO head for the drive controller to consume.
/// </summary>
internal bool HasActiveInitialCreateResidence(RuntimeEntityKey key) =>
_directory.TryGetByLocalId(
key.LocalEntityId,
out RuntimeEntityRecord canonical)
&& _directory.IsCurrent(canonical)
&& canonical.Key == key
&& _entityObjects.TryGetInitialCreateResidence(canonical, out _);
/// <summary>
/// C3c: true when the exact incarnation behind <paramref name="canonical"/>
/// holds an initial-create residence lease. Used by materialization to
/// decide whether presentation must await the conductor's completion
/// receipt or may self-project from already-committed canonical state.
/// </summary>
internal bool HasActiveInitialCreateResidence(
RuntimeEntityRecord canonical) =>
_entityObjects.TryGetInitialCreateResidence(canonical, out _);
/// <summary>
/// C3c-R1 review F1: the ONLY sanctioned mutation of the otherwise
/// sticky <see cref="LiveEntityRecord.MaterializationResidence"/> — a
/// world-created (residence-managed) entity converting to an attached
/// projection at a same-incarnation kind transition (the equipped-child
/// world→attached path). Attached children have no Runtime placement,
/// so the sticky-residence rule expects them to carry
/// <see cref="LiveEntityMaterializationResidence.LegacyImmediate"/>.
/// Owned here so the invariant is asserted at the owner: converting
/// while the initial-create residence is still ACTIVE would let an
/// attached materialization race the conductor's pending placement.
/// </summary>
internal void ConvertMaterializationResidenceToLegacyImmediate(
LiveEntityRecord record)
{
ArgumentNullException.ThrowIfNull(record);
if (record.MaterializationResidence is not
LiveEntityMaterializationResidence.AwaitRuntimePlacement)
{
return;
}
if (HasActiveInitialCreateResidence(record.Canonical))
{
throw new InvalidOperationException(
$"Live entity 0x{record.ServerGuid:X8}/"
+ $"{record.Canonical.Incarnation} cannot convert to "
+ "LegacyImmediate residence while its initial-create "
+ "residence lease is still active.");
}
record.MaterializationResidence =
LiveEntityMaterializationResidence.LegacyImmediate;
}
/// <summary>
/// Applies one canonical Runtime placement receipt to the graphical
/// sidecar only. Runtime has already committed identity, position,