harden(runtime): close canonical entity object lifetime
Retain exact teardown receipts before terminal callbacks, preserve ordered re-entrant entity/object publication, publish committed facts across projection failures, and make direct disposal converge every canonical owner. Add a complete ownership ledger plus adversarial direct/graphical parity, callback, GUID reuse, reset, and resource-churn gates. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
84954c8b77
commit
119b7c1151
9 changed files with 992 additions and 95 deletions
|
|
@ -201,6 +201,9 @@ public sealed class ClientObjectTable
|
|||
|
||||
public int ObjectCount => _objects.Count;
|
||||
public int ContainerCount => _containers.Count;
|
||||
public int ContainerProjectionCount => _containerIndex.Count;
|
||||
public int EquipmentOwnerCount => _equipmentIndex.Count;
|
||||
public int PendingMoveCount => _pendingMoves.Count;
|
||||
|
||||
public IEnumerable<ClientObject> Objects => _objects.Values;
|
||||
public IEnumerable<Container> Containers => _containers.Values;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ public sealed class ParentAttachmentState
|
|||
private readonly Dictionary<uint, ParentAttachmentRelation> _recoveryByChild = new();
|
||||
private readonly Dictionary<uint, ParentAttachmentRelation> _lastAcceptedByChild = new();
|
||||
|
||||
public int UnresolvedRelationCount =>
|
||||
_unresolvedByChild.Values.Sum(queue => queue.Count);
|
||||
public int StagedRelationCount => _stagedByChild.Count;
|
||||
public int RecoveryRelationCount => _recoveryByChild.Count;
|
||||
public int CommittedRelationCount => _lastAcceptedByChild.Count;
|
||||
|
||||
public void AcceptCreateObjectRelation(ParentAttachmentRelation relation)
|
||||
{
|
||||
_stagedByChild[relation.ChildGuid] = relation;
|
||||
|
|
|
|||
|
|
@ -247,6 +247,18 @@ public sealed class RuntimeEntityDirectory
|
|||
record.AdvanceMovementAuthority();
|
||||
}
|
||||
|
||||
public void AdvanceMovementCommit(RuntimeEntityRecord record)
|
||||
{
|
||||
EnsureKnown(record);
|
||||
record.AdvanceMovementCommit();
|
||||
}
|
||||
|
||||
public void AdvanceParentCommit(RuntimeEntityRecord record)
|
||||
{
|
||||
EnsureKnown(record);
|
||||
record.AdvanceParentCommit();
|
||||
}
|
||||
|
||||
public void AdvanceObjDescAuthority(RuntimeEntityRecord record)
|
||||
{
|
||||
EnsureKnown(record);
|
||||
|
|
@ -293,8 +305,7 @@ public sealed class RuntimeEntityDirectory
|
|||
uint canonicalLandblockId)
|
||||
{
|
||||
EnsureKnown(record);
|
||||
record.FullCellId = fullCellId;
|
||||
record.CanonicalLandblockId = canonicalLandblockId;
|
||||
record.SetFullCell(fullCellId, canonicalLandblockId);
|
||||
}
|
||||
|
||||
public void SetFinalPhysicsState(
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@ public interface IRuntimeEntityObjectEventSource
|
|||
|
||||
/// <summary>
|
||||
/// One synchronous, generation-stamped commit stream for the canonical entity
|
||||
/// directory and retained object table. It owns no queue and dispatches on the
|
||||
/// caller's update/network-drain thread.
|
||||
/// directory and retained object table. It owns no cross-frame queue and
|
||||
/// dispatches on the caller's update/network-drain thread. A retained
|
||||
/// synchronous drain list preserves sequence order when an observer commits
|
||||
/// another mutation re-entrantly.
|
||||
/// </summary>
|
||||
public sealed class RuntimeEntityObjectEventStream
|
||||
: IRuntimeEntityObjectEventSource,
|
||||
|
|
@ -27,10 +29,12 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
private readonly ClientObjectTable _objects;
|
||||
private readonly RuntimeEventSequencer _sequencer = new();
|
||||
private readonly object _observerGate = new();
|
||||
private readonly List<PendingDispatch> _pendingDispatch = [];
|
||||
private IRuntimeEntityObjectObserver[] _observers = [];
|
||||
private Func<RuntimeGenerationToken> _generation = static () => default;
|
||||
private Func<ulong> _frameNumber = static () => 0UL;
|
||||
private bool _contextBound;
|
||||
private bool _dispatching;
|
||||
private bool _disposed;
|
||||
|
||||
internal RuntimeEntityObjectEventStream(
|
||||
|
|
@ -49,6 +53,8 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
|
||||
public ulong LastSequence => _sequencer.LastSequence;
|
||||
public int SubscriberCount => Volatile.Read(ref _observers).Length;
|
||||
public int PendingDispatchCount => _pendingDispatch.Count;
|
||||
public bool IsDispatching => _dispatching;
|
||||
public long DispatchFailureCount { get; private set; }
|
||||
public Exception? LastDispatchFailure { get; private set; }
|
||||
|
||||
|
|
@ -123,19 +129,7 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
NextStamp(),
|
||||
change,
|
||||
RuntimeEntityObjectViews.Snapshot(record));
|
||||
IRuntimeEntityObjectObserver[] observers =
|
||||
Volatile.Read(ref _observers);
|
||||
foreach (IRuntimeEntityObjectObserver observer in observers)
|
||||
{
|
||||
try
|
||||
{
|
||||
observer.OnEntity(in delta);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
RecordDispatchFailure(error);
|
||||
}
|
||||
}
|
||||
EnqueueAndDrain(PendingDispatch.ForEntity(delta));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -154,6 +148,16 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
}
|
||||
}
|
||||
|
||||
internal void DetachObservers()
|
||||
{
|
||||
lock (_observerGate)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
Volatile.Write(ref _observers, []);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnObjectAdded(ClientObject item) =>
|
||||
PublishInventory(RuntimeInventoryChange.Added, item);
|
||||
|
||||
|
|
@ -206,13 +210,46 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
NextStamp(),
|
||||
change,
|
||||
item);
|
||||
EnqueueAndDrain(PendingDispatch.ForInventory(delta));
|
||||
}
|
||||
|
||||
private void EnqueueAndDrain(PendingDispatch pending)
|
||||
{
|
||||
_pendingDispatch.Add(pending);
|
||||
if (_dispatching)
|
||||
return;
|
||||
|
||||
_dispatching = true;
|
||||
try
|
||||
{
|
||||
for (int index = 0; index < _pendingDispatch.Count; index++)
|
||||
Dispatch(_pendingDispatch[index]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pendingDispatch.Clear();
|
||||
_dispatching = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Dispatch(PendingDispatch pending)
|
||||
{
|
||||
IRuntimeEntityObjectObserver[] observers =
|
||||
Volatile.Read(ref _observers);
|
||||
foreach (IRuntimeEntityObjectObserver observer in observers)
|
||||
{
|
||||
try
|
||||
{
|
||||
observer.OnInventory(in delta);
|
||||
if (pending.Kind is PendingDispatchKind.Entity)
|
||||
{
|
||||
RuntimeEntityDelta delta = pending.Entity;
|
||||
observer.OnEntity(in delta);
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInventoryDelta delta = pending.Inventory;
|
||||
observer.OnInventory(in delta);
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
|
|
@ -258,6 +295,26 @@ public sealed class RuntimeEntityObjectEventStream
|
|||
}
|
||||
}
|
||||
|
||||
private enum PendingDispatchKind : byte
|
||||
{
|
||||
Entity,
|
||||
Inventory,
|
||||
}
|
||||
|
||||
private readonly record struct PendingDispatch(
|
||||
PendingDispatchKind Kind,
|
||||
RuntimeEntityDelta Entity,
|
||||
RuntimeInventoryDelta Inventory)
|
||||
{
|
||||
public static PendingDispatch ForEntity(
|
||||
RuntimeEntityDelta entity) =>
|
||||
new(PendingDispatchKind.Entity, entity, default);
|
||||
|
||||
public static PendingDispatch ForInventory(
|
||||
RuntimeInventoryDelta inventory) =>
|
||||
new(PendingDispatchKind.Inventory, default, inventory);
|
||||
}
|
||||
|
||||
private sealed class ObserverSubscription(
|
||||
RuntimeEntityObjectEventStream owner,
|
||||
IRuntimeEntityObjectObserver observer)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,27 @@ public readonly record struct RuntimeEntityRegistrationResult(
|
|||
bool ReplacedExistingGeneration,
|
||||
Exception? PriorGenerationCleanupFailure = null);
|
||||
|
||||
public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
||||
int ActiveEntityCount,
|
||||
int TeardownEntityCount,
|
||||
int ClaimedLocalIdCount,
|
||||
int AcceptedSnapshotCount,
|
||||
int UnresolvedParentRelationCount,
|
||||
int StagedParentRelationCount,
|
||||
int RecoveryParentRelationCount,
|
||||
int CommittedParentRelationCount,
|
||||
int ObjectCount,
|
||||
int ContainerCount,
|
||||
int ContainerProjectionCount,
|
||||
int EquipmentOwnerCount,
|
||||
int PendingMoveCount,
|
||||
int StreamSubscriberCount,
|
||||
long StreamDispatchFailureCount,
|
||||
bool HasLastStreamDispatchFailure,
|
||||
int PendingDispatchCount,
|
||||
bool IsDispatching,
|
||||
bool IsSessionClearInProgress);
|
||||
|
||||
/// <summary>
|
||||
/// One exact DeleteObject acceptance issued by a
|
||||
/// <see cref="RuntimeEntityObjectLifetime"/>. The graphical host retires the
|
||||
|
|
@ -66,6 +87,31 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
public IRuntimeInventoryView InventoryView { get; }
|
||||
public RuntimeEntityObjectEventStream Events { get; }
|
||||
|
||||
public RuntimeEntityObjectOwnershipSnapshot CaptureOwnership()
|
||||
{
|
||||
ParentAttachmentState parents = Entities.ParentAttachments;
|
||||
return new RuntimeEntityObjectOwnershipSnapshot(
|
||||
Entities.Count,
|
||||
Entities.PendingTeardownCount,
|
||||
Entities.ClaimedLocalIdCount,
|
||||
Entities.Snapshots.Count,
|
||||
parents.UnresolvedRelationCount,
|
||||
parents.StagedRelationCount,
|
||||
parents.RecoveryRelationCount,
|
||||
parents.CommittedRelationCount,
|
||||
Objects.ObjectCount,
|
||||
Objects.ContainerCount,
|
||||
Objects.ContainerProjectionCount,
|
||||
Objects.EquipmentOwnerCount,
|
||||
Objects.PendingMoveCount,
|
||||
Events.SubscriberCount,
|
||||
Events.DispatchFailureCount,
|
||||
Events.LastDispatchFailure is not null,
|
||||
Events.PendingDispatchCount,
|
||||
Events.IsDispatching,
|
||||
_sessionClearInProgress);
|
||||
}
|
||||
|
||||
public void BindEventContext(
|
||||
Func<RuntimeGenerationToken> generation,
|
||||
Func<ulong> frameNumber)
|
||||
|
|
@ -181,6 +227,10 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
Exception? cleanupFailure = null;
|
||||
if (prior is not null)
|
||||
{
|
||||
// Removing the active GUID is an ownership transfer, not a gap.
|
||||
// Retain the exact incarnation before arbitrary synchronous
|
||||
// observers can re-enter registration, reset, or disposal.
|
||||
Entities.RetainTeardown(prior);
|
||||
try
|
||||
{
|
||||
PublishEntity(RuntimeEntityChange.Deleted, prior);
|
||||
|
|
@ -343,11 +393,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
|
||||
Entities.RefreshSnapshot(canonical, accepted);
|
||||
Entities.AdvanceObjDescAuthority(canonical);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
ulong authorityVersion = canonical.ObjDescAuthorityVersion;
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
canonical,
|
||||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
RuntimeEntityChange.Updated,
|
||||
() => canonical.ObjDescAuthorityVersion == authorityVersion);
|
||||
}
|
||||
|
||||
public bool TryApplyPickup(
|
||||
|
|
@ -370,11 +421,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
Entities.SuspendObjectClock(canonical);
|
||||
Entities.SetFullCell(canonical, 0u, 0u);
|
||||
Entities.ParentAttachments.EndChildProjection(update.Guid);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Withdrawn, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
ulong positionVersion = canonical.PositionAuthorityVersion;
|
||||
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
canonical,
|
||||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
RuntimeEntityChange.Withdrawn,
|
||||
() => canonical.PositionAuthorityVersion == positionVersion
|
||||
&& canonical.SpatialAuthorityVersion == spatialVersion);
|
||||
}
|
||||
|
||||
public bool TryApplyCreateParent(
|
||||
|
|
@ -427,11 +481,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
}
|
||||
|
||||
Entities.RefreshSnapshot(canonical, accepted);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
Entities.AdvanceParentCommit(canonical);
|
||||
ulong parentCommitVersion = canonical.ParentCommitVersion;
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
canonical,
|
||||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
RuntimeEntityChange.Updated,
|
||||
() => canonical.ParentCommitVersion
|
||||
== parentCommitVersion);
|
||||
}
|
||||
|
||||
public bool CommitAcceptedParentCellless(
|
||||
|
|
@ -449,15 +506,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
|
||||
Entities.SuspendObjectClock(canonical);
|
||||
Entities.SetFullCell(canonical, 0u, 0u);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical)
|
||||
|| canonical.PositionAuthorityVersion != positionAuthorityVersion)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
PublishEntity(RuntimeEntityChange.Withdrawn, canonical);
|
||||
return Entities.IsCurrent(canonical)
|
||||
&& canonical.PositionAuthorityVersion == positionAuthorityVersion;
|
||||
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
canonical,
|
||||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
RuntimeEntityChange.Withdrawn,
|
||||
() => canonical.PositionAuthorityVersion
|
||||
== positionAuthorityVersion
|
||||
&& canonical.SpatialAuthorityVersion == spatialVersion);
|
||||
}
|
||||
|
||||
public bool TryApplyMotion(
|
||||
|
|
@ -483,11 +539,20 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
Entities.RefreshSnapshot(canonical, snapshot);
|
||||
if (applied && retainPayload)
|
||||
Entities.AdvanceMovementAuthority(canonical);
|
||||
if (applied)
|
||||
{
|
||||
Entities.AdvanceMovementCommit(canonical);
|
||||
ulong movementCommitVersion =
|
||||
canonical.MovementCommitVersion;
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
canonical,
|
||||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
RuntimeEntityChange.Updated,
|
||||
() => canonical.MovementCommitVersion
|
||||
== movementCommitVersion);
|
||||
}
|
||||
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (applied && Entities.IsCurrent(canonical))
|
||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
||||
if (applied && !Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
}
|
||||
|
||||
return applied;
|
||||
|
|
@ -510,11 +575,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
|
||||
Entities.RefreshSnapshot(canonical, accepted);
|
||||
Entities.AdvanceVectorAuthority(canonical);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
ulong authorityVersion = canonical.VectorAuthorityVersion;
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
canonical,
|
||||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
RuntimeEntityChange.Updated,
|
||||
() => canonical.VectorAuthorityVersion == authorityVersion);
|
||||
}
|
||||
|
||||
public bool TryApplyState(
|
||||
|
|
@ -539,16 +605,22 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
transition = Entities.ApplyRawPhysicsState(
|
||||
canonical,
|
||||
update.PhysicsState);
|
||||
acknowledgeProjection?.Invoke(canonical, transition);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(
|
||||
transition.HiddenTransition
|
||||
ulong stateVersion = canonical.StateAuthorityVersion;
|
||||
ulong physicsMutationVersion =
|
||||
canonical.PhysicsStateMutationVersion;
|
||||
RetailPhysicsStateTransition committedTransition = transition;
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
canonical,
|
||||
() => acknowledgeProjection?.Invoke(
|
||||
canonical,
|
||||
committedTransition),
|
||||
committedTransition.HiddenTransition
|
||||
is RetailHiddenTransition.BecameHidden
|
||||
? RuntimeEntityChange.Hidden
|
||||
: RuntimeEntityChange.Updated,
|
||||
canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
() => canonical.StateAuthorityVersion == stateVersion
|
||||
&& canonical.PhysicsStateMutationVersion
|
||||
== physicsMutationVersion);
|
||||
}
|
||||
|
||||
public bool TryApplyPosition(
|
||||
|
|
@ -611,18 +683,25 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
Entities.ParentAttachments.EndChildProjection(update.Guid);
|
||||
}
|
||||
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
if (acceptedPosition)
|
||||
ulong positionVersion = canonical.PositionAuthorityVersion;
|
||||
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||
if (!acceptedPosition)
|
||||
{
|
||||
PublishEntity(
|
||||
beforeCell != canonical.FullCellId
|
||||
? RuntimeEntityChange.Rebucketed
|
||||
: RuntimeEntityChange.Updated,
|
||||
canonical);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
return IsExpectedCanonical(
|
||||
canonical,
|
||||
() => canonical.PositionAuthorityVersion == positionVersion
|
||||
&& canonical.SpatialAuthorityVersion == spatialVersion);
|
||||
}
|
||||
return Entities.IsCurrent(canonical);
|
||||
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
canonical,
|
||||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
beforeCell != canonical.FullCellId
|
||||
? RuntimeEntityChange.Rebucketed
|
||||
: RuntimeEntityChange.Updated,
|
||||
() => canonical.PositionAuthorityVersion == positionVersion
|
||||
&& canonical.SpatialAuthorityVersion == spatialVersion);
|
||||
}
|
||||
|
||||
public bool CommitRebucket(
|
||||
|
|
@ -641,12 +720,21 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
canonical,
|
||||
fullCellId,
|
||||
canonicalLandblockId);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
if (previous != fullCellId)
|
||||
PublishEntity(RuntimeEntityChange.Rebucketed, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||
if (previous == fullCellId)
|
||||
{
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
return IsExpectedCanonical(
|
||||
canonical,
|
||||
() => canonical.SpatialAuthorityVersion
|
||||
== spatialVersion);
|
||||
}
|
||||
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
canonical,
|
||||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
RuntimeEntityChange.Rebucketed,
|
||||
() => canonical.SpatialAuthorityVersion == spatialVersion);
|
||||
}
|
||||
|
||||
public bool CommitWithdrawal(
|
||||
|
|
@ -660,11 +748,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
|
||||
Entities.SuspendObjectClock(canonical);
|
||||
Entities.SetFullCell(canonical, 0u, 0u);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Withdrawn, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
canonical,
|
||||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
RuntimeEntityChange.Withdrawn,
|
||||
() => canonical.SpatialAuthorityVersion == spatialVersion);
|
||||
}
|
||||
|
||||
public bool CommitChildNoDraw(
|
||||
|
|
@ -678,11 +767,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
return false;
|
||||
|
||||
Entities.SetChildNoDraw(canonical, noDraw);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
ulong physicsMutationVersion =
|
||||
canonical.PhysicsStateMutationVersion;
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
canonical,
|
||||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
RuntimeEntityChange.Updated,
|
||||
() => canonical.PhysicsStateMutationVersion
|
||||
== physicsMutationVersion);
|
||||
}
|
||||
|
||||
public bool RetireAfterProjectionAcquisitionFailure(
|
||||
|
|
@ -694,6 +786,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
return false;
|
||||
|
||||
Entities.AdvanceLifetimeMutation(canonical.ServerGuid);
|
||||
Entities.RetainTeardown(canonical);
|
||||
PublishEntity(RuntimeEntityChange.Deleted, canonical);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -731,6 +824,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
&& Entities.RemoveActive(active))
|
||||
{
|
||||
retiredCanonical = active;
|
||||
Entities.RetainTeardown(active);
|
||||
PublishEntity(
|
||||
removeRetainedObject
|
||||
? RuntimeEntityChange.Deleted
|
||||
|
|
@ -805,6 +899,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
{
|
||||
if (!Entities.RemoveActive(canonical))
|
||||
continue;
|
||||
Entities.RetainTeardown(canonical);
|
||||
PublishEntity(RuntimeEntityChange.Deleted, canonical);
|
||||
}
|
||||
return retired;
|
||||
|
|
@ -827,8 +922,52 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
Events.Dispose();
|
||||
|
||||
// Disposal is terminal, unlike a session reset. Quiesce borrowers
|
||||
// before committing internal cleanup so no observer can re-enter a
|
||||
// lifetime whose owner is being destroyed.
|
||||
Events.DetachObservers();
|
||||
List<Exception>? failures = null;
|
||||
try
|
||||
{
|
||||
if (!_sessionClearInProgress)
|
||||
_ = BeginSessionClear();
|
||||
|
||||
try
|
||||
{
|
||||
ClearObjects();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= []).Add(error);
|
||||
}
|
||||
|
||||
foreach (RuntimeEntityRecord canonical
|
||||
in Entities.TeardownRecords.ToArray())
|
||||
{
|
||||
Exception? failure = RetireCanonicalOnly(canonical);
|
||||
if (failure is not null)
|
||||
(failures ??= []).Add(failure);
|
||||
}
|
||||
|
||||
if (!CompleteSessionClearIfConverged())
|
||||
{
|
||||
(failures ??= []).Add(new InvalidOperationException(
|
||||
"Runtime entity/object disposal did not converge every canonical owner."));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_disposed = true;
|
||||
Events.Dispose();
|
||||
}
|
||||
|
||||
if (failures is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Runtime entity/object disposal failed to converge.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CommitPositionChannelUpdate(
|
||||
|
|
@ -847,11 +986,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
|
||||
Entities.RefreshSnapshot(canonical, accepted);
|
||||
Entities.AdvancePositionAuthority(canonical);
|
||||
acknowledgeProjection?.Invoke(canonical);
|
||||
if (!Entities.IsCurrent(canonical))
|
||||
return false;
|
||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
||||
return Entities.IsCurrent(canonical);
|
||||
ulong positionVersion = canonical.PositionAuthorityVersion;
|
||||
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||
return AcknowledgeProjectionAndPublish(
|
||||
canonical,
|
||||
() => acknowledgeProjection?.Invoke(canonical),
|
||||
RuntimeEntityChange.Updated,
|
||||
() => canonical.PositionAuthorityVersion == positionVersion
|
||||
&& canonical.SpatialAuthorityVersion == spatialVersion);
|
||||
}
|
||||
|
||||
private void PublishEntity(
|
||||
|
|
@ -859,6 +1001,37 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
RuntimeEntityRecord canonical) =>
|
||||
Events.PublishEntity(change, canonical);
|
||||
|
||||
private bool AcknowledgeProjectionAndPublish(
|
||||
RuntimeEntityRecord canonical,
|
||||
Action acknowledgeProjection,
|
||||
RuntimeEntityChange change,
|
||||
Func<bool> matchesCommittedMutation)
|
||||
{
|
||||
try
|
||||
{
|
||||
acknowledgeProjection();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (IsExpectedCanonical(
|
||||
canonical,
|
||||
matchesCommittedMutation))
|
||||
{
|
||||
PublishEntity(change, canonical);
|
||||
}
|
||||
}
|
||||
|
||||
return IsExpectedCanonical(
|
||||
canonical,
|
||||
matchesCommittedMutation);
|
||||
}
|
||||
|
||||
private bool IsExpectedCanonical(
|
||||
RuntimeEntityRecord canonical,
|
||||
Func<bool> matchesCommittedMutation) =>
|
||||
Entities.IsCurrent(canonical)
|
||||
&& matchesCommittedMutation();
|
||||
|
||||
private void EnsureNotDisposed() =>
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ public sealed class RuntimeEntityRecord
|
|||
public uint CanonicalLandblockId { get; internal set; }
|
||||
public uint RawPhysicsState { get; internal set; }
|
||||
public PhysicsStateFlags FinalPhysicsState { get; internal set; }
|
||||
public ulong SpatialAuthorityVersion { get; private set; }
|
||||
public ulong PhysicsStateMutationVersion { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Incarnation-stable retail <c>CPhysicsObj::update_time</c> owner.
|
||||
|
|
@ -72,6 +74,8 @@ public sealed class RuntimeEntityRecord
|
|||
public ulong VectorAuthorityVersion { get; private set; }
|
||||
public ulong VelocityAuthorityVersion { get; private set; }
|
||||
public ulong MovementAuthorityVersion { get; private set; }
|
||||
public ulong MovementCommitVersion { get; private set; } = 1UL;
|
||||
public ulong ParentCommitVersion { get; private set; }
|
||||
public ulong ObjDescAuthorityVersion { get; private set; }
|
||||
public ulong CreateIntegrationVersion { get; private set; } = 1UL;
|
||||
public bool DeleteAcceptedForTeardown { get; internal set; }
|
||||
|
|
@ -94,6 +98,7 @@ public sealed class RuntimeEntityRecord
|
|||
internal RetailPhysicsStateTransition ApplyRawPhysicsState(uint rawState)
|
||||
{
|
||||
StateAuthorityVersion++;
|
||||
PhysicsStateMutationVersion++;
|
||||
RawPhysicsState = rawState;
|
||||
RetailPhysicsStateTransition transition = RetailPhysicsStateTransitions.Apply(
|
||||
FinalPhysicsState,
|
||||
|
|
@ -123,6 +128,10 @@ public sealed class RuntimeEntityRecord
|
|||
VelocityAuthorityVersion++;
|
||||
}
|
||||
|
||||
internal void AdvanceMovementCommit() => MovementCommitVersion++;
|
||||
|
||||
internal void AdvanceParentCommit() => ParentCommitVersion++;
|
||||
|
||||
internal void AdvanceObjDescAuthority() => ObjDescAuthorityVersion++;
|
||||
|
||||
internal void AdvanceCreateAuthority()
|
||||
|
|
@ -138,6 +147,7 @@ public sealed class RuntimeEntityRecord
|
|||
|
||||
internal void SetChildNoDraw(bool noDraw)
|
||||
{
|
||||
PhysicsStateMutationVersion++;
|
||||
FinalPhysicsState = noDraw
|
||||
? FinalPhysicsState | PhysicsStateFlags.NoDraw
|
||||
: FinalPhysicsState & ~PhysicsStateFlags.NoDraw;
|
||||
|
|
@ -149,12 +159,22 @@ public sealed class RuntimeEntityRecord
|
|||
{
|
||||
if (refreshPosition && Snapshot.Position is { } position)
|
||||
{
|
||||
FullCellId = position.LandblockId;
|
||||
CanonicalLandblockId = (FullCellId & 0xFFFF0000u) | 0xFFFFu;
|
||||
SetFullCell(
|
||||
position.LandblockId,
|
||||
(position.LandblockId & 0xFFFF0000u) | 0xFFFFu);
|
||||
}
|
||||
|
||||
RawPhysicsState = Snapshot.Physics?.RawState
|
||||
?? Snapshot.PhysicsState
|
||||
?? 0u;
|
||||
}
|
||||
|
||||
internal void SetFullCell(
|
||||
uint fullCellId,
|
||||
uint canonicalLandblockId)
|
||||
{
|
||||
SpatialAuthorityVersion++;
|
||||
FullCellId = fullCellId;
|
||||
CanonicalLandblockId = canonicalLandblockId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue