feat(runtime): own initial create residence transaction
This commit is contained in:
parent
74103f75b5
commit
38fd4b8dc9
10 changed files with 3031 additions and 17 deletions
|
|
@ -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) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue