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
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue