feat(runtime): own initial create residence transaction

This commit is contained in:
Erik 2026-08-01 19:35:08 +02:00
parent 74103f75b5
commit 38fd4b8dc9
10 changed files with 3031 additions and 17 deletions

View file

@ -27,6 +27,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
int ContainerProjectionCount,
int EquipmentOwnerCount,
int PendingMoveCount,
int InitialCreateResidenceLeaseCount,
int StreamSubscriberCount,
int PlacementStreamSubscriberCount,
long StreamDispatchFailureCount,
@ -51,6 +52,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
&& ContainerProjectionCount == 0
&& EquipmentOwnerCount == 0
&& PendingMoveCount == 0
&& InitialCreateResidenceLeaseCount == 0
&& StreamSubscriberCount == 0
&& PlacementStreamSubscriberCount == 0
&& PendingDispatchCount == 0
@ -118,6 +120,9 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InventoryView = views.Inventory;
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
Physics.SetPosition.BindEventStream(Events);
InitialCreateResidences = new RuntimeInitialCreateResidenceState(
Entities,
Physics.SetPosition);
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
@ -143,6 +148,9 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InventoryView = views.Inventory;
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
Physics.SetPosition.BindEventStream(Events);
InitialCreateResidences = new RuntimeInitialCreateResidenceState(
Entities,
Physics.SetPosition);
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
@ -168,6 +176,9 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InventoryView = views.Inventory;
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
Physics.SetPosition.BindEventStream(Events);
InitialCreateResidences = new RuntimeInitialCreateResidenceState(
Entities,
Physics.SetPosition);
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
@ -180,10 +191,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
public IRuntimeInventoryView InventoryView { get; }
public RuntimeEntityObjectEventStream Events { get; }
public RuntimePlacementProjectionChannel Placements { get; }
internal RuntimeInitialCreateResidenceState InitialCreateResidences
{ get; }
public RuntimeEntityObjectOwnershipSnapshot CaptureOwnership()
{
ParentAttachmentState parents = Entities.ParentAttachments;
RuntimeInitialCreateResidenceOwnershipSnapshot initialResidence =
InitialCreateResidences.CaptureOwnership();
return new RuntimeEntityObjectOwnershipSnapshot(
Entities.Count,
Entities.PendingTeardownCount,
@ -198,6 +213,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Objects.ContainerProjectionCount,
Objects.EquipmentOwnerCount,
Objects.PendingMoveCount,
initialResidence.ActiveLeaseCount
+ initialResidence.PendingAdoptionCount,
Events.SubscriberCount,
Events.PlacementSubscriberCount,
Events.DispatchFailureCount,
@ -215,6 +232,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
EnsureNotDisposed();
Events.BindContext(generation, frameNumber);
Placements.BindGeneration(generation);
InitialCreateResidences.BindGeneration(generation);
}
/// <summary>
@ -225,7 +243,28 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
/// </summary>
public RuntimeEntityRegistrationResult RegisterEntity(
WorldSession.EntitySpawn incoming,
Func<RuntimeEntityRecord, Exception?>? retirePriorProjection = null)
Func<RuntimeEntityRecord, Exception?>? retirePriorProjection = null) =>
RegisterEntityCore(
incoming,
beginInitialResidence: false,
isLocalPlayer: false,
retirePriorProjection);
internal RuntimeEntityRegistrationResult RegisterEntityWithInitialResidence(
WorldSession.EntitySpawn incoming,
bool isLocalPlayer,
Func<RuntimeEntityRecord, Exception?>? retirePriorProjection = null) =>
RegisterEntityCore(
incoming,
beginInitialResidence: true,
isLocalPlayer,
retirePriorProjection);
private RuntimeEntityRegistrationResult RegisterEntityCore(
WorldSession.EntitySpawn incoming,
bool beginInitialResidence,
bool isLocalPlayer,
Func<RuntimeEntityRecord, Exception?>? retirePriorProjection)
{
EnsureNotDisposed();
if (_sessionClearInProgress)
@ -233,6 +272,18 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
throw new InvalidOperationException(
"A Runtime entity cannot register while its session lifetime is clearing.");
}
CreateObjectTimestampDisposition preview =
Entities.PreviewCreateDisposition(incoming);
bool requiresFreshResidenceAdmission = preview is
CreateObjectTimestampDisposition.InitialGeneration
or CreateObjectTimestampDisposition.NewGeneration;
if (beginInitialResidence
&& requiresFreshResidenceAdmission
&& !InitialCreateResidences.CanAcceptCreate(incoming))
{
throw new InvalidOperationException(
$"CreateObject 0x{incoming.Guid:X8} cannot acquire a structurally valid initial residence lease.");
}
InboundCreateResult result = Entities.AcceptCreate(incoming);
if (result.Disposition
@ -256,11 +307,16 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
incoming.Guid,
out RuntimeEntityRecord retained))
{
// Existing-generation CreateObject contributes untimestamped
// description fields here. Position/Parent/Pickup/State/etc.
// remain separate freshness-gated events and must not churn a
// pending initial placement or re-preclaim its wire cell.
Entities.RefreshSnapshot(
retained,
result.Snapshot,
refreshPosition: true);
Entities.AdvanceCreateAuthority(retained);
refreshPosition: !beginInitialResidence);
if (!beginInitialResidence)
Entities.AdvanceCreateAuthority(retained);
PublishEntity(RuntimeEntityChange.Updated, retained);
if (!IsCurrentOperation(
incoming.Guid,
@ -291,7 +347,24 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
ReplacedExistingGeneration: false);
}
if (beginInitialResidence
&& !InitialCreateResidences.CanAcceptCreate(result.Snapshot))
{
throw new InvalidOperationException(
$"Recovered CreateObject 0x{incoming.Guid:X8} cannot acquire a structurally valid initial residence lease.");
}
RuntimeEntityRecord recovered = Entities.AddActive(result.Snapshot);
if (!InitializeAcceptedCreateResidence(
recovered,
result,
beginInitialResidence,
isLocalPlayer))
{
throw FailInitialResidenceRegistration(
recovered,
publishDeleted: false);
}
PublishEntity(RuntimeEntityChange.Registered, recovered);
if (!IsCurrentOperation(
incoming.Guid,
@ -366,6 +439,16 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
}
RuntimeEntityRecord canonical = Entities.AddActive(result.Snapshot);
if (!InitializeAcceptedCreateResidence(
canonical,
result,
beginInitialResidence,
isLocalPlayer))
{
throw FailInitialResidenceRegistration(
canonical,
publishDeleted: false);
}
try
{
PublishEntity(RuntimeEntityChange.Registered, canonical);
@ -446,10 +529,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(canonical);
RuntimePlacementCancellationReceipt cancellation =
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(canonical);
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(
canonical,
releasePreparedMover: true);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(initialCancellation, ordinaryCancellation);
Physics.CollisionReports.Forget(canonical);
Physics.RemoveSpatialProjection(canonical);
Entities.SetRemoteMotion(canonical, null);
@ -540,10 +627,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
}
Entities.RefreshSnapshot(canonical, accepted);
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(canonical);
Entities.AdvancePositionAuthority(canonical);
Physics.CollisionReports.LeaveWorld(canonical);
RuntimePlacementCancellationReceipt cancellation =
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(canonical);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(initialCancellation, ordinaryCancellation);
Entities.SuspendObjectClock(canonical);
Entities.SetFullCell(canonical, 0u, 0u);
Entities.ParentAttachments.EndChildProjection(update.Guid);
@ -631,8 +722,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
return false;
}
RuntimePlacementCancellationReceipt cancellation =
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(canonical);
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(canonical);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(initialCancellation, ordinaryCancellation);
Physics.CollisionReports.LeaveWorld(canonical);
Entities.SuspendObjectClock(canonical);
Entities.SetFullCell(canonical, 0u, 0u);
@ -788,6 +883,25 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
out AcceptedPhysicsTimestamps timestamps)
{
EnsureNotDisposed();
RuntimeInitialCreateResidenceLease priorInitialResidence = default;
bool hadPendingInitialResidence =
Entities.TryGetActive(
update.Guid,
out RuntimeEntityRecord pendingCanonical)
&& InitialCreateResidences.TryGetTransaction(
pendingCanonical,
out priorInitialResidence);
if (hadPendingInitialResidence
&& !InitialCreateResidences.CanEnqueueAcceptedPosition(
pendingCanonical,
priorInitialResidence,
update))
{
disposition = PositionTimestampDisposition.Rejected;
accepted = default;
timestamps = default;
return false;
}
bool hadCanonical = Entities.TryGetActive(
update.Guid,
out RuntimeEntityRecord beforeCanonical);
@ -825,9 +939,47 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
};
}
RuntimePlacementCancellationReceipt cancellation = acceptedPosition
? Physics.SetPosition.Forget(canonical)
: default;
// Initial CreateObject residence is one immutable transaction. A
// Position packet accepted before its 0x11 placement completes is
// retained in the transaction's ordered continuation batch; it must
// not replace the initial mover operation, mutate the Runtime record
// used to prepare that operation, or escape through a host callback.
if (hadPendingInitialResidence)
{
if (!acceptedPosition)
return true;
if (!ReferenceEquals(canonical, pendingCanonical))
throw FailInitialResidenceRegistration(
canonical,
publishDeleted: true);
RuntimeInitialCreateResidenceLease retained =
InitialCreateResidences.EnqueueAcceptedPosition(
canonical,
priorInitialResidence,
update,
accepted,
disposition,
timestamps,
isLocalPlayer,
forcePositionRotation,
currentLocalVelocity,
projectionRequiresTeleportHook);
if (!retained.IsValid)
{
throw FailInitialResidenceRegistration(
canonical,
publishDeleted: true);
}
return true;
}
RuntimePlacementCancellationReceipt cancellation = default;
if (acceptedPosition)
{
cancellation = Physics.SetPosition.Forget(canonical);
}
Entities.RefreshSnapshot(
canonical,
snapshot,
@ -903,6 +1055,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
if (!Entities.IsCurrent(canonical))
return false;
RuntimePlacementCancellationReceipt cancellation =
ForgetInitialCreateResidence(canonical);
Physics.CollisionReports.LeaveWorld(canonical);
Entities.SuspendObjectClock(canonical);
Entities.SetFullCell(canonical, 0u, 0u);
@ -911,7 +1065,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Withdrawn,
() => canonical.SpatialAuthorityVersion == spatialVersion);
() => canonical.SpatialAuthorityVersion == spatialVersion,
cancellation);
}
public bool CommitChildNoDraw(
@ -981,11 +1136,17 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
&& active.Incarnation == delete.InstanceSequence
&& Entities.RemoveActive(active))
{
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(active);
Physics.CollisionReports.Forget(active);
RuntimePlacementCancellationReceipt cancellation =
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(
active,
releasePreparedMover: true);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(
initialCancellation,
ordinaryCancellation);
retiredCanonical = active;
Entities.RetainTeardown(active);
Physics.SetPosition.PublishCancellation(cancellation);
@ -1058,6 +1219,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
_sessionClearInProgress = true;
RuntimeEntityRecord[] active = Entities.ActiveRecords.ToArray();
InitialCreateResidences.Clear();
Physics.CollisionReports.LeaveWorldBatch(active);
Physics.ResetSessionPhysics();
Entities.BeginSessionClear();
@ -1193,10 +1355,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
}
Entities.RefreshSnapshot(canonical, accepted);
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(canonical);
Entities.AdvancePositionAuthority(canonical);
Physics.CollisionReports.LeaveWorld(canonical);
RuntimePlacementCancellationReceipt cancellation =
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(canonical);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(initialCancellation, ordinaryCancellation);
ulong positionVersion = canonical.PositionAuthorityVersion;
ulong spatialVersion = canonical.SpatialAuthorityVersion;
return AcknowledgeProjectionAndPublish(
@ -1251,6 +1417,108 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
private void EnsureNotDisposed() =>
ObjectDisposedException.ThrowIf(_disposed, this);
internal bool TryGetInitialCreateResidence(
RuntimeEntityRecord canonical,
out RuntimeInitialCreateResidenceLease lease)
{
EnsureNotDisposed();
return InitialCreateResidences.TryGetCurrent(canonical, out lease);
}
internal RuntimeInitialCreateResidenceCompletionStatus
CompleteInitialCreateResidence(
RuntimeEntityRecord canonical,
in RuntimeInitialCreateResidenceToken token,
out RuntimeInitialCreateResidenceReceipt receipt)
{
EnsureNotDisposed();
return InitialCreateResidences.Complete(
canonical,
token,
out receipt);
}
internal bool AcknowledgeInitialCreateResidenceAdoption(
RuntimeEntityRecord canonical,
in RuntimeInitialCreateResidenceAdoptionToken token)
{
EnsureNotDisposed();
return InitialCreateResidences.AcknowledgeAdoption(
canonical,
token);
}
private RuntimePlacementCancellationReceipt ForgetInitialCreateResidence(
RuntimeEntityRecord canonical)
{
return InitialCreateResidences.Forget(
canonical,
out _,
out RuntimePlacementCancellationReceipt cancellation)
? cancellation
: default;
}
private static RuntimePlacementCancellationReceipt PreferCancellation(
in RuntimePlacementCancellationReceipt initial,
in RuntimePlacementCancellationReceipt ordinary) =>
initial.IsValid ? initial : ordinary;
private bool InitializeAcceptedCreateResidence(
RuntimeEntityRecord canonical,
in InboundCreateResult accepted,
bool beginInitialResidence,
bool isLocalPlayer)
{
if (!beginInitialResidence)
return true;
// The explicit cutover path separates accepted wire authority from
// committed residence before any observer can hydrate it. Failure to
// own the exact route is fail-closed; the caller removes the canonical
// record before publishing Registered/Updated.
if (canonical.PositionAuthorityVersion == 0UL)
Entities.AdvancePositionAuthority(canonical);
if (canonical.FullCellId != 0u)
Entities.SetFullCell(canonical, 0u, 0u);
RuntimeInitialCreateResidenceLease lease =
InitialCreateResidences.Begin(
canonical,
accepted,
isLocalPlayer);
return lease.IsValid;
}
private Exception FailInitialResidenceRegistration(
RuntimeEntityRecord canonical,
bool publishDeleted)
{
if (!Entities.RemoveActive(canonical))
{
return new InvalidOperationException(
$"Initial residence for 0x{canonical.ServerGuid:X8} failed after its canonical incarnation was superseded.");
}
Exception? failure = null;
if (publishDeleted)
{
try
{
PublishEntity(RuntimeEntityChange.Deleted, canonical);
}
catch (Exception error)
{
failure = error;
}
}
failure = Combine(failure, RetireCanonicalOnly(canonical));
var cause = new InvalidOperationException(
$"Initial residence for 0x{canonical.ServerGuid:X8} could not acquire its exact Runtime placement lease.");
return failure is null
? cause
: new AggregateException(cause, failure);
}
private static Exception? Combine(
Exception? first,
Exception? second) =>