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 ObjectCount => _objects.Count;
|
||||||
public int ContainerCount => _containers.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<ClientObject> Objects => _objects.Values;
|
||||||
public IEnumerable<Container> Containers => _containers.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> _recoveryByChild = new();
|
||||||
private readonly Dictionary<uint, ParentAttachmentRelation> _lastAcceptedByChild = 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)
|
public void AcceptCreateObjectRelation(ParentAttachmentRelation relation)
|
||||||
{
|
{
|
||||||
_stagedByChild[relation.ChildGuid] = relation;
|
_stagedByChild[relation.ChildGuid] = relation;
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,18 @@ public sealed class RuntimeEntityDirectory
|
||||||
record.AdvanceMovementAuthority();
|
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)
|
public void AdvanceObjDescAuthority(RuntimeEntityRecord record)
|
||||||
{
|
{
|
||||||
EnsureKnown(record);
|
EnsureKnown(record);
|
||||||
|
|
@ -293,8 +305,7 @@ public sealed class RuntimeEntityDirectory
|
||||||
uint canonicalLandblockId)
|
uint canonicalLandblockId)
|
||||||
{
|
{
|
||||||
EnsureKnown(record);
|
EnsureKnown(record);
|
||||||
record.FullCellId = fullCellId;
|
record.SetFullCell(fullCellId, canonicalLandblockId);
|
||||||
record.CanonicalLandblockId = canonicalLandblockId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetFinalPhysicsState(
|
public void SetFinalPhysicsState(
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@ public interface IRuntimeEntityObjectEventSource
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// One synchronous, generation-stamped commit stream for the canonical entity
|
/// One synchronous, generation-stamped commit stream for the canonical entity
|
||||||
/// directory and retained object table. It owns no queue and dispatches on the
|
/// directory and retained object table. It owns no cross-frame queue and
|
||||||
/// caller's update/network-drain thread.
|
/// 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>
|
/// </summary>
|
||||||
public sealed class RuntimeEntityObjectEventStream
|
public sealed class RuntimeEntityObjectEventStream
|
||||||
: IRuntimeEntityObjectEventSource,
|
: IRuntimeEntityObjectEventSource,
|
||||||
|
|
@ -27,10 +29,12 @@ public sealed class RuntimeEntityObjectEventStream
|
||||||
private readonly ClientObjectTable _objects;
|
private readonly ClientObjectTable _objects;
|
||||||
private readonly RuntimeEventSequencer _sequencer = new();
|
private readonly RuntimeEventSequencer _sequencer = new();
|
||||||
private readonly object _observerGate = new();
|
private readonly object _observerGate = new();
|
||||||
|
private readonly List<PendingDispatch> _pendingDispatch = [];
|
||||||
private IRuntimeEntityObjectObserver[] _observers = [];
|
private IRuntimeEntityObjectObserver[] _observers = [];
|
||||||
private Func<RuntimeGenerationToken> _generation = static () => default;
|
private Func<RuntimeGenerationToken> _generation = static () => default;
|
||||||
private Func<ulong> _frameNumber = static () => 0UL;
|
private Func<ulong> _frameNumber = static () => 0UL;
|
||||||
private bool _contextBound;
|
private bool _contextBound;
|
||||||
|
private bool _dispatching;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
internal RuntimeEntityObjectEventStream(
|
internal RuntimeEntityObjectEventStream(
|
||||||
|
|
@ -49,6 +53,8 @@ public sealed class RuntimeEntityObjectEventStream
|
||||||
|
|
||||||
public ulong LastSequence => _sequencer.LastSequence;
|
public ulong LastSequence => _sequencer.LastSequence;
|
||||||
public int SubscriberCount => Volatile.Read(ref _observers).Length;
|
public int SubscriberCount => Volatile.Read(ref _observers).Length;
|
||||||
|
public int PendingDispatchCount => _pendingDispatch.Count;
|
||||||
|
public bool IsDispatching => _dispatching;
|
||||||
public long DispatchFailureCount { get; private set; }
|
public long DispatchFailureCount { get; private set; }
|
||||||
public Exception? LastDispatchFailure { get; private set; }
|
public Exception? LastDispatchFailure { get; private set; }
|
||||||
|
|
||||||
|
|
@ -123,19 +129,7 @@ public sealed class RuntimeEntityObjectEventStream
|
||||||
NextStamp(),
|
NextStamp(),
|
||||||
change,
|
change,
|
||||||
RuntimeEntityObjectViews.Snapshot(record));
|
RuntimeEntityObjectViews.Snapshot(record));
|
||||||
IRuntimeEntityObjectObserver[] observers =
|
EnqueueAndDrain(PendingDispatch.ForEntity(delta));
|
||||||
Volatile.Read(ref _observers);
|
|
||||||
foreach (IRuntimeEntityObjectObserver observer in observers)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
observer.OnEntity(in delta);
|
|
||||||
}
|
|
||||||
catch (Exception error)
|
|
||||||
{
|
|
||||||
RecordDispatchFailure(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
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) =>
|
private void OnObjectAdded(ClientObject item) =>
|
||||||
PublishInventory(RuntimeInventoryChange.Added, item);
|
PublishInventory(RuntimeInventoryChange.Added, item);
|
||||||
|
|
||||||
|
|
@ -206,13 +210,46 @@ public sealed class RuntimeEntityObjectEventStream
|
||||||
NextStamp(),
|
NextStamp(),
|
||||||
change,
|
change,
|
||||||
item);
|
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 =
|
IRuntimeEntityObjectObserver[] observers =
|
||||||
Volatile.Read(ref _observers);
|
Volatile.Read(ref _observers);
|
||||||
foreach (IRuntimeEntityObjectObserver observer in observers)
|
foreach (IRuntimeEntityObjectObserver observer in observers)
|
||||||
{
|
{
|
||||||
try
|
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)
|
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(
|
private sealed class ObserverSubscription(
|
||||||
RuntimeEntityObjectEventStream owner,
|
RuntimeEntityObjectEventStream owner,
|
||||||
IRuntimeEntityObjectObserver observer)
|
IRuntimeEntityObjectObserver observer)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,27 @@ public readonly record struct RuntimeEntityRegistrationResult(
|
||||||
bool ReplacedExistingGeneration,
|
bool ReplacedExistingGeneration,
|
||||||
Exception? PriorGenerationCleanupFailure = null);
|
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>
|
/// <summary>
|
||||||
/// One exact DeleteObject acceptance issued by a
|
/// One exact DeleteObject acceptance issued by a
|
||||||
/// <see cref="RuntimeEntityObjectLifetime"/>. The graphical host retires the
|
/// <see cref="RuntimeEntityObjectLifetime"/>. The graphical host retires the
|
||||||
|
|
@ -66,6 +87,31 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
public IRuntimeInventoryView InventoryView { get; }
|
public IRuntimeInventoryView InventoryView { get; }
|
||||||
public RuntimeEntityObjectEventStream Events { 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(
|
public void BindEventContext(
|
||||||
Func<RuntimeGenerationToken> generation,
|
Func<RuntimeGenerationToken> generation,
|
||||||
Func<ulong> frameNumber)
|
Func<ulong> frameNumber)
|
||||||
|
|
@ -181,6 +227,10 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
Exception? cleanupFailure = null;
|
Exception? cleanupFailure = null;
|
||||||
if (prior is not 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
|
try
|
||||||
{
|
{
|
||||||
PublishEntity(RuntimeEntityChange.Deleted, prior);
|
PublishEntity(RuntimeEntityChange.Deleted, prior);
|
||||||
|
|
@ -343,11 +393,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
|
|
||||||
Entities.RefreshSnapshot(canonical, accepted);
|
Entities.RefreshSnapshot(canonical, accepted);
|
||||||
Entities.AdvanceObjDescAuthority(canonical);
|
Entities.AdvanceObjDescAuthority(canonical);
|
||||||
acknowledgeProjection?.Invoke(canonical);
|
ulong authorityVersion = canonical.ObjDescAuthorityVersion;
|
||||||
if (!Entities.IsCurrent(canonical))
|
return AcknowledgeProjectionAndPublish(
|
||||||
return false;
|
canonical,
|
||||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
() => acknowledgeProjection?.Invoke(canonical),
|
||||||
return Entities.IsCurrent(canonical);
|
RuntimeEntityChange.Updated,
|
||||||
|
() => canonical.ObjDescAuthorityVersion == authorityVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryApplyPickup(
|
public bool TryApplyPickup(
|
||||||
|
|
@ -370,11 +421,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
Entities.SuspendObjectClock(canonical);
|
Entities.SuspendObjectClock(canonical);
|
||||||
Entities.SetFullCell(canonical, 0u, 0u);
|
Entities.SetFullCell(canonical, 0u, 0u);
|
||||||
Entities.ParentAttachments.EndChildProjection(update.Guid);
|
Entities.ParentAttachments.EndChildProjection(update.Guid);
|
||||||
acknowledgeProjection?.Invoke(canonical);
|
ulong positionVersion = canonical.PositionAuthorityVersion;
|
||||||
if (!Entities.IsCurrent(canonical))
|
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||||
return false;
|
return AcknowledgeProjectionAndPublish(
|
||||||
PublishEntity(RuntimeEntityChange.Withdrawn, canonical);
|
canonical,
|
||||||
return Entities.IsCurrent(canonical);
|
() => acknowledgeProjection?.Invoke(canonical),
|
||||||
|
RuntimeEntityChange.Withdrawn,
|
||||||
|
() => canonical.PositionAuthorityVersion == positionVersion
|
||||||
|
&& canonical.SpatialAuthorityVersion == spatialVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryApplyCreateParent(
|
public bool TryApplyCreateParent(
|
||||||
|
|
@ -427,11 +481,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
Entities.RefreshSnapshot(canonical, accepted);
|
Entities.RefreshSnapshot(canonical, accepted);
|
||||||
acknowledgeProjection?.Invoke(canonical);
|
Entities.AdvanceParentCommit(canonical);
|
||||||
if (!Entities.IsCurrent(canonical))
|
ulong parentCommitVersion = canonical.ParentCommitVersion;
|
||||||
return false;
|
return AcknowledgeProjectionAndPublish(
|
||||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
canonical,
|
||||||
return Entities.IsCurrent(canonical);
|
() => acknowledgeProjection?.Invoke(canonical),
|
||||||
|
RuntimeEntityChange.Updated,
|
||||||
|
() => canonical.ParentCommitVersion
|
||||||
|
== parentCommitVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CommitAcceptedParentCellless(
|
public bool CommitAcceptedParentCellless(
|
||||||
|
|
@ -449,15 +506,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
|
|
||||||
Entities.SuspendObjectClock(canonical);
|
Entities.SuspendObjectClock(canonical);
|
||||||
Entities.SetFullCell(canonical, 0u, 0u);
|
Entities.SetFullCell(canonical, 0u, 0u);
|
||||||
acknowledgeProjection?.Invoke(canonical);
|
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||||
if (!Entities.IsCurrent(canonical)
|
return AcknowledgeProjectionAndPublish(
|
||||||
|| canonical.PositionAuthorityVersion != positionAuthorityVersion)
|
canonical,
|
||||||
{
|
() => acknowledgeProjection?.Invoke(canonical),
|
||||||
return false;
|
RuntimeEntityChange.Withdrawn,
|
||||||
}
|
() => canonical.PositionAuthorityVersion
|
||||||
PublishEntity(RuntimeEntityChange.Withdrawn, canonical);
|
== positionAuthorityVersion
|
||||||
return Entities.IsCurrent(canonical)
|
&& canonical.SpatialAuthorityVersion == spatialVersion);
|
||||||
&& canonical.PositionAuthorityVersion == positionAuthorityVersion;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryApplyMotion(
|
public bool TryApplyMotion(
|
||||||
|
|
@ -483,11 +539,20 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
Entities.RefreshSnapshot(canonical, snapshot);
|
Entities.RefreshSnapshot(canonical, snapshot);
|
||||||
if (applied && retainPayload)
|
if (applied && retainPayload)
|
||||||
Entities.AdvanceMovementAuthority(canonical);
|
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);
|
acknowledgeProjection?.Invoke(canonical);
|
||||||
if (applied && Entities.IsCurrent(canonical))
|
|
||||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
|
||||||
if (applied && !Entities.IsCurrent(canonical))
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return applied;
|
return applied;
|
||||||
|
|
@ -510,11 +575,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
|
|
||||||
Entities.RefreshSnapshot(canonical, accepted);
|
Entities.RefreshSnapshot(canonical, accepted);
|
||||||
Entities.AdvanceVectorAuthority(canonical);
|
Entities.AdvanceVectorAuthority(canonical);
|
||||||
acknowledgeProjection?.Invoke(canonical);
|
ulong authorityVersion = canonical.VectorAuthorityVersion;
|
||||||
if (!Entities.IsCurrent(canonical))
|
return AcknowledgeProjectionAndPublish(
|
||||||
return false;
|
canonical,
|
||||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
() => acknowledgeProjection?.Invoke(canonical),
|
||||||
return Entities.IsCurrent(canonical);
|
RuntimeEntityChange.Updated,
|
||||||
|
() => canonical.VectorAuthorityVersion == authorityVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryApplyState(
|
public bool TryApplyState(
|
||||||
|
|
@ -539,16 +605,22 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
transition = Entities.ApplyRawPhysicsState(
|
transition = Entities.ApplyRawPhysicsState(
|
||||||
canonical,
|
canonical,
|
||||||
update.PhysicsState);
|
update.PhysicsState);
|
||||||
acknowledgeProjection?.Invoke(canonical, transition);
|
ulong stateVersion = canonical.StateAuthorityVersion;
|
||||||
if (!Entities.IsCurrent(canonical))
|
ulong physicsMutationVersion =
|
||||||
return false;
|
canonical.PhysicsStateMutationVersion;
|
||||||
PublishEntity(
|
RetailPhysicsStateTransition committedTransition = transition;
|
||||||
transition.HiddenTransition
|
return AcknowledgeProjectionAndPublish(
|
||||||
|
canonical,
|
||||||
|
() => acknowledgeProjection?.Invoke(
|
||||||
|
canonical,
|
||||||
|
committedTransition),
|
||||||
|
committedTransition.HiddenTransition
|
||||||
is RetailHiddenTransition.BecameHidden
|
is RetailHiddenTransition.BecameHidden
|
||||||
? RuntimeEntityChange.Hidden
|
? RuntimeEntityChange.Hidden
|
||||||
: RuntimeEntityChange.Updated,
|
: RuntimeEntityChange.Updated,
|
||||||
canonical);
|
() => canonical.StateAuthorityVersion == stateVersion
|
||||||
return Entities.IsCurrent(canonical);
|
&& canonical.PhysicsStateMutationVersion
|
||||||
|
== physicsMutationVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryApplyPosition(
|
public bool TryApplyPosition(
|
||||||
|
|
@ -611,18 +683,25 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
Entities.ParentAttachments.EndChildProjection(update.Guid);
|
Entities.ParentAttachments.EndChildProjection(update.Guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
acknowledgeProjection?.Invoke(canonical);
|
ulong positionVersion = canonical.PositionAuthorityVersion;
|
||||||
if (!Entities.IsCurrent(canonical))
|
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||||
return false;
|
if (!acceptedPosition)
|
||||||
if (acceptedPosition)
|
|
||||||
{
|
{
|
||||||
PublishEntity(
|
acknowledgeProjection?.Invoke(canonical);
|
||||||
beforeCell != canonical.FullCellId
|
return IsExpectedCanonical(
|
||||||
? RuntimeEntityChange.Rebucketed
|
canonical,
|
||||||
: RuntimeEntityChange.Updated,
|
() => canonical.PositionAuthorityVersion == positionVersion
|
||||||
canonical);
|
&& 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(
|
public bool CommitRebucket(
|
||||||
|
|
@ -641,12 +720,21 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
canonical,
|
canonical,
|
||||||
fullCellId,
|
fullCellId,
|
||||||
canonicalLandblockId);
|
canonicalLandblockId);
|
||||||
acknowledgeProjection?.Invoke(canonical);
|
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||||
if (!Entities.IsCurrent(canonical))
|
if (previous == fullCellId)
|
||||||
return false;
|
{
|
||||||
if (previous != fullCellId)
|
acknowledgeProjection?.Invoke(canonical);
|
||||||
PublishEntity(RuntimeEntityChange.Rebucketed, canonical);
|
return IsExpectedCanonical(
|
||||||
return Entities.IsCurrent(canonical);
|
canonical,
|
||||||
|
() => canonical.SpatialAuthorityVersion
|
||||||
|
== spatialVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AcknowledgeProjectionAndPublish(
|
||||||
|
canonical,
|
||||||
|
() => acknowledgeProjection?.Invoke(canonical),
|
||||||
|
RuntimeEntityChange.Rebucketed,
|
||||||
|
() => canonical.SpatialAuthorityVersion == spatialVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CommitWithdrawal(
|
public bool CommitWithdrawal(
|
||||||
|
|
@ -660,11 +748,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
|
|
||||||
Entities.SuspendObjectClock(canonical);
|
Entities.SuspendObjectClock(canonical);
|
||||||
Entities.SetFullCell(canonical, 0u, 0u);
|
Entities.SetFullCell(canonical, 0u, 0u);
|
||||||
acknowledgeProjection?.Invoke(canonical);
|
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||||
if (!Entities.IsCurrent(canonical))
|
return AcknowledgeProjectionAndPublish(
|
||||||
return false;
|
canonical,
|
||||||
PublishEntity(RuntimeEntityChange.Withdrawn, canonical);
|
() => acknowledgeProjection?.Invoke(canonical),
|
||||||
return Entities.IsCurrent(canonical);
|
RuntimeEntityChange.Withdrawn,
|
||||||
|
() => canonical.SpatialAuthorityVersion == spatialVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CommitChildNoDraw(
|
public bool CommitChildNoDraw(
|
||||||
|
|
@ -678,11 +767,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Entities.SetChildNoDraw(canonical, noDraw);
|
Entities.SetChildNoDraw(canonical, noDraw);
|
||||||
acknowledgeProjection?.Invoke(canonical);
|
ulong physicsMutationVersion =
|
||||||
if (!Entities.IsCurrent(canonical))
|
canonical.PhysicsStateMutationVersion;
|
||||||
return false;
|
return AcknowledgeProjectionAndPublish(
|
||||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
canonical,
|
||||||
return Entities.IsCurrent(canonical);
|
() => acknowledgeProjection?.Invoke(canonical),
|
||||||
|
RuntimeEntityChange.Updated,
|
||||||
|
() => canonical.PhysicsStateMutationVersion
|
||||||
|
== physicsMutationVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool RetireAfterProjectionAcquisitionFailure(
|
public bool RetireAfterProjectionAcquisitionFailure(
|
||||||
|
|
@ -694,6 +786,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Entities.AdvanceLifetimeMutation(canonical.ServerGuid);
|
Entities.AdvanceLifetimeMutation(canonical.ServerGuid);
|
||||||
|
Entities.RetainTeardown(canonical);
|
||||||
PublishEntity(RuntimeEntityChange.Deleted, canonical);
|
PublishEntity(RuntimeEntityChange.Deleted, canonical);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -731,6 +824,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
&& Entities.RemoveActive(active))
|
&& Entities.RemoveActive(active))
|
||||||
{
|
{
|
||||||
retiredCanonical = active;
|
retiredCanonical = active;
|
||||||
|
Entities.RetainTeardown(active);
|
||||||
PublishEntity(
|
PublishEntity(
|
||||||
removeRetainedObject
|
removeRetainedObject
|
||||||
? RuntimeEntityChange.Deleted
|
? RuntimeEntityChange.Deleted
|
||||||
|
|
@ -805,6 +899,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
{
|
{
|
||||||
if (!Entities.RemoveActive(canonical))
|
if (!Entities.RemoveActive(canonical))
|
||||||
continue;
|
continue;
|
||||||
|
Entities.RetainTeardown(canonical);
|
||||||
PublishEntity(RuntimeEntityChange.Deleted, canonical);
|
PublishEntity(RuntimeEntityChange.Deleted, canonical);
|
||||||
}
|
}
|
||||||
return retired;
|
return retired;
|
||||||
|
|
@ -827,8 +922,52 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
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(
|
private bool CommitPositionChannelUpdate(
|
||||||
|
|
@ -847,11 +986,14 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
|
|
||||||
Entities.RefreshSnapshot(canonical, accepted);
|
Entities.RefreshSnapshot(canonical, accepted);
|
||||||
Entities.AdvancePositionAuthority(canonical);
|
Entities.AdvancePositionAuthority(canonical);
|
||||||
acknowledgeProjection?.Invoke(canonical);
|
ulong positionVersion = canonical.PositionAuthorityVersion;
|
||||||
if (!Entities.IsCurrent(canonical))
|
ulong spatialVersion = canonical.SpatialAuthorityVersion;
|
||||||
return false;
|
return AcknowledgeProjectionAndPublish(
|
||||||
PublishEntity(RuntimeEntityChange.Updated, canonical);
|
canonical,
|
||||||
return Entities.IsCurrent(canonical);
|
() => acknowledgeProjection?.Invoke(canonical),
|
||||||
|
RuntimeEntityChange.Updated,
|
||||||
|
() => canonical.PositionAuthorityVersion == positionVersion
|
||||||
|
&& canonical.SpatialAuthorityVersion == spatialVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PublishEntity(
|
private void PublishEntity(
|
||||||
|
|
@ -859,6 +1001,37 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
RuntimeEntityRecord canonical) =>
|
RuntimeEntityRecord canonical) =>
|
||||||
Events.PublishEntity(change, 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() =>
|
private void EnsureNotDisposed() =>
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ public sealed class RuntimeEntityRecord
|
||||||
public uint CanonicalLandblockId { get; internal set; }
|
public uint CanonicalLandblockId { get; internal set; }
|
||||||
public uint RawPhysicsState { get; internal set; }
|
public uint RawPhysicsState { get; internal set; }
|
||||||
public PhysicsStateFlags FinalPhysicsState { get; internal set; }
|
public PhysicsStateFlags FinalPhysicsState { get; internal set; }
|
||||||
|
public ulong SpatialAuthorityVersion { get; private set; }
|
||||||
|
public ulong PhysicsStateMutationVersion { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Incarnation-stable retail <c>CPhysicsObj::update_time</c> owner.
|
/// 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 VectorAuthorityVersion { get; private set; }
|
||||||
public ulong VelocityAuthorityVersion { get; private set; }
|
public ulong VelocityAuthorityVersion { get; private set; }
|
||||||
public ulong MovementAuthorityVersion { 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 ObjDescAuthorityVersion { get; private set; }
|
||||||
public ulong CreateIntegrationVersion { get; private set; } = 1UL;
|
public ulong CreateIntegrationVersion { get; private set; } = 1UL;
|
||||||
public bool DeleteAcceptedForTeardown { get; internal set; }
|
public bool DeleteAcceptedForTeardown { get; internal set; }
|
||||||
|
|
@ -94,6 +98,7 @@ public sealed class RuntimeEntityRecord
|
||||||
internal RetailPhysicsStateTransition ApplyRawPhysicsState(uint rawState)
|
internal RetailPhysicsStateTransition ApplyRawPhysicsState(uint rawState)
|
||||||
{
|
{
|
||||||
StateAuthorityVersion++;
|
StateAuthorityVersion++;
|
||||||
|
PhysicsStateMutationVersion++;
|
||||||
RawPhysicsState = rawState;
|
RawPhysicsState = rawState;
|
||||||
RetailPhysicsStateTransition transition = RetailPhysicsStateTransitions.Apply(
|
RetailPhysicsStateTransition transition = RetailPhysicsStateTransitions.Apply(
|
||||||
FinalPhysicsState,
|
FinalPhysicsState,
|
||||||
|
|
@ -123,6 +128,10 @@ public sealed class RuntimeEntityRecord
|
||||||
VelocityAuthorityVersion++;
|
VelocityAuthorityVersion++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void AdvanceMovementCommit() => MovementCommitVersion++;
|
||||||
|
|
||||||
|
internal void AdvanceParentCommit() => ParentCommitVersion++;
|
||||||
|
|
||||||
internal void AdvanceObjDescAuthority() => ObjDescAuthorityVersion++;
|
internal void AdvanceObjDescAuthority() => ObjDescAuthorityVersion++;
|
||||||
|
|
||||||
internal void AdvanceCreateAuthority()
|
internal void AdvanceCreateAuthority()
|
||||||
|
|
@ -138,6 +147,7 @@ public sealed class RuntimeEntityRecord
|
||||||
|
|
||||||
internal void SetChildNoDraw(bool noDraw)
|
internal void SetChildNoDraw(bool noDraw)
|
||||||
{
|
{
|
||||||
|
PhysicsStateMutationVersion++;
|
||||||
FinalPhysicsState = noDraw
|
FinalPhysicsState = noDraw
|
||||||
? FinalPhysicsState | PhysicsStateFlags.NoDraw
|
? FinalPhysicsState | PhysicsStateFlags.NoDraw
|
||||||
: FinalPhysicsState & ~PhysicsStateFlags.NoDraw;
|
: FinalPhysicsState & ~PhysicsStateFlags.NoDraw;
|
||||||
|
|
@ -149,12 +159,22 @@ public sealed class RuntimeEntityRecord
|
||||||
{
|
{
|
||||||
if (refreshPosition && Snapshot.Position is { } position)
|
if (refreshPosition && Snapshot.Position is { } position)
|
||||||
{
|
{
|
||||||
FullCellId = position.LandblockId;
|
SetFullCell(
|
||||||
CanonicalLandblockId = (FullCellId & 0xFFFF0000u) | 0xFFFFu;
|
position.LandblockId,
|
||||||
|
(position.LandblockId & 0xFFFF0000u) | 0xFFFFu);
|
||||||
}
|
}
|
||||||
|
|
||||||
RawPhysicsState = Snapshot.Physics?.RawState
|
RawPhysicsState = Snapshot.Physics?.RawState
|
||||||
?? Snapshot.PhysicsState
|
?? Snapshot.PhysicsState
|
||||||
?? 0u;
|
?? 0u;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void SetFullCell(
|
||||||
|
uint fullCellId,
|
||||||
|
uint canonicalLandblockId)
|
||||||
|
{
|
||||||
|
SpatialAuthorityVersion++;
|
||||||
|
FullCellId = fullCellId;
|
||||||
|
CanonicalLandblockId = canonicalLandblockId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,32 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
spawn.Guid,
|
spawn.Guid,
|
||||||
Harness.PlayerGuid,
|
Harness.PlayerGuid,
|
||||||
newSlot: 3);
|
newSlot: 3);
|
||||||
|
var objDesc = new ObjDescEvent.Parsed(
|
||||||
|
spawn.Guid,
|
||||||
|
new CreateObject.ModelData(
|
||||||
|
0x04000001u,
|
||||||
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||||
|
Array.Empty<CreateObject.TextureChange>(),
|
||||||
|
Array.Empty<CreateObject.AnimPartChange>()),
|
||||||
|
spawn.InstanceSequence,
|
||||||
|
ObjDescSequence: 2);
|
||||||
|
Assert.True(direct.TryApplyObjDesc(
|
||||||
|
objDesc,
|
||||||
|
acknowledgeProjection: null,
|
||||||
|
out _));
|
||||||
|
var motion = new WorldSession.EntityMotionUpdate(
|
||||||
|
spawn.Guid,
|
||||||
|
new CreateObject.ServerMotionState(0x3D, 0x11),
|
||||||
|
spawn.InstanceSequence,
|
||||||
|
MovementSequence: 2,
|
||||||
|
ServerControlSequence: 2,
|
||||||
|
IsAutonomous: false);
|
||||||
|
Assert.True(direct.TryApplyMotion(
|
||||||
|
motion,
|
||||||
|
retainPayload: true,
|
||||||
|
acknowledgeProjection: null,
|
||||||
|
out _,
|
||||||
|
out _));
|
||||||
var vector = new VectorUpdate.Parsed(
|
var vector = new VectorUpdate.Parsed(
|
||||||
spawn.Guid,
|
spawn.Guid,
|
||||||
new Vector3(1f, 2f, 3f),
|
new Vector3(1f, 2f, 3f),
|
||||||
|
|
@ -216,6 +242,12 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
acknowledgeProjection: null,
|
acknowledgeProjection: null,
|
||||||
out _,
|
out _,
|
||||||
out _));
|
out _));
|
||||||
|
Assert.True(direct.CommitChildNoDraw(
|
||||||
|
directCanonical,
|
||||||
|
noDraw: true));
|
||||||
|
Assert.True(direct.CommitChildNoDraw(
|
||||||
|
directCanonical,
|
||||||
|
noDraw: false));
|
||||||
WorldSession.EntityPositionUpdate position =
|
WorldSession.EntityPositionUpdate position =
|
||||||
Position(spawn.Guid, spawn.InstanceSequence);
|
Position(spawn.Guid, spawn.InstanceSequence);
|
||||||
Assert.True(direct.TryApplyPosition(
|
Assert.True(direct.TryApplyPosition(
|
||||||
|
|
@ -228,6 +260,10 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
out _,
|
out _,
|
||||||
out _,
|
out _,
|
||||||
out _));
|
out _));
|
||||||
|
Assert.True(direct.CommitRebucket(
|
||||||
|
directCanonical,
|
||||||
|
0x12360001u,
|
||||||
|
0x1236FFFFu));
|
||||||
Assert.True(direct.TryApplyPickup(
|
Assert.True(direct.TryApplyPickup(
|
||||||
new PickupEvent.Parsed(
|
new PickupEvent.Parsed(
|
||||||
spawn.Guid,
|
spawn.Guid,
|
||||||
|
|
@ -253,8 +289,22 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
spawn.Guid,
|
spawn.Guid,
|
||||||
Harness.PlayerGuid,
|
Harness.PlayerGuid,
|
||||||
newSlot: 3);
|
newSlot: 3);
|
||||||
|
Assert.True(graphical.Entities.TryApplyObjDesc(
|
||||||
|
objDesc,
|
||||||
|
out _));
|
||||||
|
Assert.True(graphical.Entities.TryApplyMotion(
|
||||||
|
motion,
|
||||||
|
retainPayload: true,
|
||||||
|
out _,
|
||||||
|
out _));
|
||||||
Assert.True(graphical.Entities.TryApplyVector(vector, out _));
|
Assert.True(graphical.Entities.TryApplyVector(vector, out _));
|
||||||
Assert.True(graphical.Entities.TryApplyState(state, out _, out _));
|
Assert.True(graphical.Entities.TryApplyState(state, out _, out _));
|
||||||
|
Assert.True(graphical.Entities.SetAttachedChildNoDraw(
|
||||||
|
spawn.Guid,
|
||||||
|
noDraw: true));
|
||||||
|
Assert.True(graphical.Entities.SetAttachedChildNoDraw(
|
||||||
|
spawn.Guid,
|
||||||
|
noDraw: false));
|
||||||
Assert.True(graphical.Entities.TryApplyPosition(
|
Assert.True(graphical.Entities.TryApplyPosition(
|
||||||
position,
|
position,
|
||||||
isLocalPlayer: false,
|
isLocalPlayer: false,
|
||||||
|
|
@ -263,6 +313,9 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
out _,
|
out _,
|
||||||
out _,
|
out _,
|
||||||
out _));
|
out _));
|
||||||
|
Assert.True(graphical.Entities.RebucketLiveEntity(
|
||||||
|
spawn.Guid,
|
||||||
|
0x12360001u));
|
||||||
Assert.True(graphical.Entities.TryApplyPickup(
|
Assert.True(graphical.Entities.TryApplyPickup(
|
||||||
new PickupEvent.Parsed(
|
new PickupEvent.Parsed(
|
||||||
spawn.Guid,
|
spawn.Guid,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Vfx;
|
using AcDream.Core.Vfx;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using DatReaderWriter.Enums;
|
using DatReaderWriter.Enums;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
|
@ -244,6 +245,7 @@ public sealed class LiveEntityLifecycleStressTests
|
||||||
fixture.Effects.ClearNetworkState();
|
fixture.Effects.ClearNetworkState();
|
||||||
Assert.Equal(0, fixture.Runtime.Count);
|
Assert.Equal(0, fixture.Runtime.Count);
|
||||||
Assert.Equal(0, fixture.Effects.PendingPacketCount);
|
Assert.Equal(0, fixture.Effects.PendingPacketCount);
|
||||||
|
AssertOwnershipLedgerConverged(fixture);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -299,6 +301,98 @@ public sealed class LiveEntityLifecycleStressTests
|
||||||
Assert.Equal(0, fixture.Spatial.PendingBucketCount);
|
Assert.Equal(0, fixture.Spatial.PendingBucketCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AssertOwnershipLedgerConverged(Fixture fixture)
|
||||||
|
{
|
||||||
|
RuntimeEntityObjectOwnershipSnapshot canonical =
|
||||||
|
fixture.EntityObjects.CaptureOwnership();
|
||||||
|
var ledger = new J3OwnershipLedger(
|
||||||
|
canonical,
|
||||||
|
fixture.Runtime.MaterializedCount,
|
||||||
|
fixture.Runtime.VisibleRecords.Count,
|
||||||
|
fixture.Runtime.AnimationRuntimeCount,
|
||||||
|
fixture.Runtime.SpatialAnimationRuntimeCount,
|
||||||
|
fixture.Runtime.SpatialRemoteMotionRuntimeCount,
|
||||||
|
fixture.Runtime.SpatialProjectileRuntimeCount,
|
||||||
|
fixture.Runtime.SpatialRootObjectCount,
|
||||||
|
fixture.RenderOwners.Count,
|
||||||
|
fixture.RenderRegisterCount - fixture.RenderUnregisterCount,
|
||||||
|
fixture.Projectiles.Count,
|
||||||
|
fixture.Effects.ReadyOwnerCount,
|
||||||
|
fixture.Effects.PendingPacketCount,
|
||||||
|
fixture.Runner.ActiveOwnerCount,
|
||||||
|
fixture.Runner.ActiveScriptCount,
|
||||||
|
fixture.Runner.ScheduledCallPesCount,
|
||||||
|
fixture.Runner.OwnerAnchorCount,
|
||||||
|
fixture.Particles.ActiveEmitterCount,
|
||||||
|
fixture.Particles.ActiveParticleCount,
|
||||||
|
fixture.ParticleSink.ActiveBindingCount,
|
||||||
|
fixture.ParticleSink.LogicalEmitterCount,
|
||||||
|
fixture.ParticleSink.TrackedOwnerCount,
|
||||||
|
fixture.ParticleSink.RenderPassOwnerCount,
|
||||||
|
fixture.ParticleSink.HiddenPresentationOwnerCount,
|
||||||
|
fixture.Poses.Count,
|
||||||
|
fixture.Lights.RegisteredCount,
|
||||||
|
fixture.LiveLights.TrackedOwnerCount,
|
||||||
|
fixture.LiveLights.PresentedOwnerCount,
|
||||||
|
fixture.Lighting.OwnedLightOwnerCount,
|
||||||
|
fixture.Lighting.PoseTrackedOwnerCount,
|
||||||
|
fixture.Lighting.RetainedOwnerStateCount,
|
||||||
|
fixture.Engine.ShadowObjects.TotalRegistered,
|
||||||
|
fixture.Engine.ShadowObjects.RetainedRegistrationCount,
|
||||||
|
fixture.Engine.ShadowObjects.SuspendedRegistrationCount,
|
||||||
|
fixture.Spatial.PendingLiveEntityCount,
|
||||||
|
fixture.Spatial.PendingBucketCount,
|
||||||
|
fixture.Spatial.PendingRescueCount,
|
||||||
|
fixture.Spatial.PersistentGuidCount,
|
||||||
|
fixture.Spatial.PendingVisibilityTransitionCount,
|
||||||
|
fixture.Spatial.Entities.Count(entity =>
|
||||||
|
entity.ServerGuid != 0u));
|
||||||
|
|
||||||
|
Assert.Equal(default(J3OwnershipLedger), ledger);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct J3OwnershipLedger(
|
||||||
|
RuntimeEntityObjectOwnershipSnapshot Canonical,
|
||||||
|
int AppMaterializedCount,
|
||||||
|
int AppVisibleCount,
|
||||||
|
int AnimationRuntimeCount,
|
||||||
|
int SpatialAnimationRuntimeCount,
|
||||||
|
int SpatialRemoteMotionRuntimeCount,
|
||||||
|
int SpatialProjectileRuntimeCount,
|
||||||
|
int SpatialRootObjectCount,
|
||||||
|
int RenderOwnerCount,
|
||||||
|
int RenderRegistrationDelta,
|
||||||
|
int ProjectileCount,
|
||||||
|
int EffectReadyOwnerCount,
|
||||||
|
int EffectPendingPacketCount,
|
||||||
|
int ScriptOwnerCount,
|
||||||
|
int ActiveScriptCount,
|
||||||
|
int ScheduledCallPesCount,
|
||||||
|
int ScriptAnchorCount,
|
||||||
|
int EmitterCount,
|
||||||
|
int ParticleCount,
|
||||||
|
int ParticleBindingCount,
|
||||||
|
int LogicalEmitterCount,
|
||||||
|
int ParticleTrackedOwnerCount,
|
||||||
|
int ParticleRenderPassOwnerCount,
|
||||||
|
int HiddenParticlePresentationCount,
|
||||||
|
int PoseCount,
|
||||||
|
int RegisteredLightCount,
|
||||||
|
int TrackedLiveLightCount,
|
||||||
|
int PresentedLiveLightCount,
|
||||||
|
int OwnedLightCount,
|
||||||
|
int LightPoseCount,
|
||||||
|
int RetainedLightStateCount,
|
||||||
|
int ShadowCount,
|
||||||
|
int RetainedShadowCount,
|
||||||
|
int SuspendedShadowCount,
|
||||||
|
int PendingSpatialEntityCount,
|
||||||
|
int PendingBucketCount,
|
||||||
|
int PendingRescueCount,
|
||||||
|
int PersistentGuidCount,
|
||||||
|
int PendingVisibilityCount,
|
||||||
|
int MaterializedWorldEntityCount);
|
||||||
|
|
||||||
private sealed class Fixture : IDisposable
|
private sealed class Fixture : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Dictionary<uint, DatPhysicsScript> _scripts = new();
|
private readonly Dictionary<uint, DatPhysicsScript> _scripts = new();
|
||||||
|
|
@ -328,7 +422,8 @@ public sealed class LiveEntityLifecycleStressTests
|
||||||
randomUnit: () => 0.5,
|
randomUnit: () => 0.5,
|
||||||
canAdvanceOwner: ownerId => _effects?.CanAdvanceOwner(ownerId) ?? true);
|
canAdvanceOwner: ownerId => _effects?.CanAdvanceOwner(ownerId) ?? true);
|
||||||
|
|
||||||
Runtime = LiveEntityRuntimeFixture.Create(
|
EntityObjects = new RuntimeEntityObjectLifetime();
|
||||||
|
Runtime = new LiveEntityRuntime(
|
||||||
Spatial,
|
Spatial,
|
||||||
new DelegateLiveEntityResourceLifecycle(
|
new DelegateLiveEntityResourceLifecycle(
|
||||||
entity =>
|
entity =>
|
||||||
|
|
@ -358,7 +453,8 @@ public sealed class LiveEntityLifecycleStressTests
|
||||||
cleanups.Add(() => _liveLights?.Forget(record));
|
cleanups.Add(() => _liveLights?.Forget(record));
|
||||||
}
|
}
|
||||||
LiveEntityTeardown.Run(cleanups);
|
LiveEntityTeardown.Run(cleanups);
|
||||||
});
|
},
|
||||||
|
EntityObjects);
|
||||||
|
|
||||||
Effects = new EntityEffectController(
|
Effects = new EntityEffectController(
|
||||||
Runtime,
|
Runtime,
|
||||||
|
|
@ -386,6 +482,7 @@ public sealed class LiveEntityLifecycleStressTests
|
||||||
internal LightManager Lights { get; } = new();
|
internal LightManager Lights { get; } = new();
|
||||||
internal LightingHookSink Lighting { get; }
|
internal LightingHookSink Lighting { get; }
|
||||||
internal PhysicsScriptRunner Runner { get; }
|
internal PhysicsScriptRunner Runner { get; }
|
||||||
|
internal RuntimeEntityObjectLifetime EntityObjects { get; }
|
||||||
internal LiveEntityRuntime Runtime { get; }
|
internal LiveEntityRuntime Runtime { get; }
|
||||||
internal EntityEffectController Effects { get; }
|
internal EntityEffectController Effects { get; }
|
||||||
internal ProjectileController Projectiles { get; }
|
internal ProjectileController Projectiles { get; }
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,124 @@ public sealed class RuntimeEntityObjectLifetimeTests
|
||||||
Assert.Equal(1, lifetime.Events.SubscriberCount);
|
Assert.Equal(1, lifetime.Events.SubscriberCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReentrantCrossDomainCommit_PreservesSequenceForEveryObserver()
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
lifetime.BindEventContext(static () => new(6), static () => 12UL);
|
||||||
|
bool nested = false;
|
||||||
|
var mutating = new CallbackObserver(
|
||||||
|
onEntity: _ =>
|
||||||
|
{
|
||||||
|
if (nested)
|
||||||
|
return;
|
||||||
|
nested = true;
|
||||||
|
lifetime.Objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = 0x71000001u,
|
||||||
|
Name = "nested",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var recording = new RecordingObserver();
|
||||||
|
using IDisposable first = lifetime.Events.Subscribe(mutating);
|
||||||
|
using IDisposable second = lifetime.Events.Subscribe(recording);
|
||||||
|
|
||||||
|
_ = lifetime.RegisterEntity(
|
||||||
|
Spawn(0x7000001Au, 1, "outer"));
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
(RuntimeTraceKind.Entity, 1UL),
|
||||||
|
(RuntimeTraceKind.Inventory, 2UL),
|
||||||
|
],
|
||||||
|
recording.Order);
|
||||||
|
Assert.Equal(0, lifetime.Events.PendingDispatchCount);
|
||||||
|
Assert.False(lifetime.Events.IsDispatching);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ObjectTransactions_PublishCommittedBorrowedViewInExactOrder()
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
lifetime.BindEventContext(static () => new(7), static () => 13UL);
|
||||||
|
const uint itemId = 0x71000002u;
|
||||||
|
var observedViews =
|
||||||
|
new List<RuntimeInventoryItemSnapshot>();
|
||||||
|
var observer = new CallbackObserver(
|
||||||
|
onInventory: delta =>
|
||||||
|
{
|
||||||
|
if (delta.Change
|
||||||
|
is RuntimeInventoryChange.Removed
|
||||||
|
or RuntimeInventoryChange.Cleared)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(lifetime.InventoryView.TryGet(
|
||||||
|
delta.Item.ObjectId,
|
||||||
|
out RuntimeInventoryItemSnapshot current));
|
||||||
|
observedViews.Add(current);
|
||||||
|
});
|
||||||
|
var recording = new RecordingObserver();
|
||||||
|
using IDisposable first = lifetime.Events.Subscribe(observer);
|
||||||
|
using IDisposable second = lifetime.Events.Subscribe(recording);
|
||||||
|
|
||||||
|
lifetime.Objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = itemId,
|
||||||
|
Name = "transaction",
|
||||||
|
ContainerId = 0x50000001u,
|
||||||
|
ContainerSlot = 2,
|
||||||
|
StackSize = 3,
|
||||||
|
Value = 30,
|
||||||
|
});
|
||||||
|
Assert.True(lifetime.Objects.MoveItemOptimistic(
|
||||||
|
itemId,
|
||||||
|
0x50000002u,
|
||||||
|
newSlot: 0));
|
||||||
|
Assert.True(lifetime.Objects.RollbackMove(itemId));
|
||||||
|
Assert.True(lifetime.Objects.UpdateStackSize(
|
||||||
|
itemId,
|
||||||
|
stackSize: 2,
|
||||||
|
value: 20));
|
||||||
|
Assert.True(lifetime.Objects.RemoveLogicalGeneration(
|
||||||
|
itemId,
|
||||||
|
generation: 9));
|
||||||
|
lifetime.Objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = itemId,
|
||||||
|
Name = "replacement",
|
||||||
|
});
|
||||||
|
lifetime.ClearObjects();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
RuntimeInventoryChange.Added,
|
||||||
|
RuntimeInventoryChange.Moved,
|
||||||
|
RuntimeInventoryChange.Moved,
|
||||||
|
RuntimeInventoryChange.Updated,
|
||||||
|
RuntimeInventoryChange.Removed,
|
||||||
|
RuntimeInventoryChange.Added,
|
||||||
|
RuntimeInventoryChange.Cleared,
|
||||||
|
],
|
||||||
|
recording.Inventory
|
||||||
|
.Select(delta => delta.Change)
|
||||||
|
.ToArray());
|
||||||
|
Assert.Equal(
|
||||||
|
Enumerable.Range(1, 7).Select(value => (ulong)value),
|
||||||
|
recording.Inventory.Select(delta => delta.Stamp.Sequence));
|
||||||
|
Assert.Equal(
|
||||||
|
(ushort)9,
|
||||||
|
recording.Inventory[4].Item.Incarnation);
|
||||||
|
Assert.Equal(5, observedViews.Count);
|
||||||
|
Assert.Equal(0x50000002u, observedViews[1].ContainerId);
|
||||||
|
Assert.Equal(0x50000001u, observedViews[2].ContainerId);
|
||||||
|
Assert.Equal(2, observedViews[3].StackSize);
|
||||||
|
Assert.Equal("replacement", observedViews[4].Name);
|
||||||
|
Assert.Equal(0, lifetime.InventoryView.ObjectCount);
|
||||||
|
Assert.Equal(0, lifetime.Events.PendingDispatchCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DirectDelete_PublishesTerminalEntityBeforeInventoryRemoval()
|
public void DirectDelete_PublishesTerminalEntityBeforeInventoryRemoval()
|
||||||
{
|
{
|
||||||
|
|
@ -530,6 +648,97 @@ public sealed class RuntimeEntityObjectLifetimeTests
|
||||||
Assert.Equal(0, lifetime.Entities.PendingTeardownCount);
|
Assert.Equal(0, lifetime.Entities.PendingTeardownCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerationReplacement_RetainsExactReceiptBeforeTerminalCallback()
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
const uint guid = 0x70000030u;
|
||||||
|
RuntimeEntityRecord prior =
|
||||||
|
lifetime.RegisterEntity(Spawn(guid, 1, "prior")).Canonical!;
|
||||||
|
uint priorLocalId = prior.LocalEntityId!.Value;
|
||||||
|
bool observedRetainedReceipt = false;
|
||||||
|
var observer = new CallbackObserver(
|
||||||
|
onEntity: delta =>
|
||||||
|
{
|
||||||
|
if (delta.Change is not RuntimeEntityChange.Deleted)
|
||||||
|
return;
|
||||||
|
observedRetainedReceipt = lifetime.Entities.TryGetTeardown(
|
||||||
|
guid,
|
||||||
|
1,
|
||||||
|
out RuntimeEntityRecord retained)
|
||||||
|
&& ReferenceEquals(prior, retained)
|
||||||
|
&& lifetime.Entities.TryGetByLocalId(
|
||||||
|
priorLocalId,
|
||||||
|
out RuntimeEntityRecord identity)
|
||||||
|
&& ReferenceEquals(prior, identity);
|
||||||
|
});
|
||||||
|
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||||
|
|
||||||
|
RuntimeEntityRegistrationResult replacement =
|
||||||
|
lifetime.RegisterEntity(Spawn(guid, 2, "replacement"));
|
||||||
|
|
||||||
|
Assert.True(observedRetainedReceipt);
|
||||||
|
Assert.Equal((ushort)2, replacement.Canonical!.Incarnation);
|
||||||
|
Assert.Equal(0, lifetime.Entities.PendingTeardownCount);
|
||||||
|
Assert.Equal(1, lifetime.Entities.ClaimedLocalIdCount);
|
||||||
|
Assert.False(lifetime.Entities.TryGetByLocalId(
|
||||||
|
priorLocalId,
|
||||||
|
out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteAndSessionClear_ExposeExactReceiptsUntilAcknowledged()
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
const uint deletedGuid = 0x70000031u;
|
||||||
|
const uint clearedGuid = 0x70000032u;
|
||||||
|
RuntimeEntityRecord deleted = lifetime.RegisterEntity(
|
||||||
|
Spawn(deletedGuid, 1, "deleted")).Canonical!;
|
||||||
|
RuntimeEntityRecord cleared = lifetime.RegisterEntity(
|
||||||
|
Spawn(clearedGuid, 1, "cleared")).Canonical!;
|
||||||
|
var retainedDuringCallbacks = new HashSet<uint>();
|
||||||
|
var observer = new CallbackObserver(
|
||||||
|
onEntity: delta =>
|
||||||
|
{
|
||||||
|
if (delta.Change
|
||||||
|
is RuntimeEntityChange.Deleted
|
||||||
|
or RuntimeEntityChange.Withdrawn
|
||||||
|
&& lifetime.Entities.TryGetTeardown(
|
||||||
|
delta.Entity.Identity.ServerGuid,
|
||||||
|
delta.Entity.Identity.Incarnation,
|
||||||
|
out _))
|
||||||
|
{
|
||||||
|
retainedDuringCallbacks.Add(
|
||||||
|
delta.Entity.Identity.ServerGuid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||||
|
|
||||||
|
Assert.True(lifetime.TryAcceptDelete(
|
||||||
|
new DeleteObject.Parsed(deletedGuid, 1),
|
||||||
|
isLocalPlayer: false,
|
||||||
|
removeRetainedObject: false,
|
||||||
|
out RuntimeEntityDeleteAcceptance acceptance));
|
||||||
|
Assert.Contains(deletedGuid, retainedDuringCallbacks);
|
||||||
|
Assert.Equal(1, lifetime.Entities.PendingTeardownCount);
|
||||||
|
lifetime.CompleteAcceptedDelete(acceptance);
|
||||||
|
Assert.Null(lifetime.RetireCanonicalOnly(deleted));
|
||||||
|
|
||||||
|
IReadOnlyList<RuntimeEntityRecord> retired =
|
||||||
|
lifetime.BeginSessionClear();
|
||||||
|
Assert.Same(cleared, Assert.Single(retired));
|
||||||
|
Assert.Contains(clearedGuid, retainedDuringCallbacks);
|
||||||
|
Assert.Equal(1, lifetime.Entities.PendingTeardownCount);
|
||||||
|
Assert.Null(lifetime.RetireCanonicalOnly(cleared));
|
||||||
|
Assert.True(lifetime.CompleteSessionClearIfConverged());
|
||||||
|
Assert.Equal(
|
||||||
|
default(RuntimeEntityObjectOwnershipSnapshot) with
|
||||||
|
{
|
||||||
|
StreamSubscriberCount = 1,
|
||||||
|
},
|
||||||
|
lifetime.CaptureOwnership());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RegisterEntity_ReentrantNewerGenerationSupersedesOuterCreate()
|
public void RegisterEntity_ReentrantNewerGenerationSupersedesOuterCreate()
|
||||||
{
|
{
|
||||||
|
|
@ -685,6 +894,274 @@ public sealed class RuntimeEntityObjectLifetimeTests
|
||||||
Assert.False(lifetime.Entities.TryGetActive(guid, out _));
|
Assert.False(lifetime.Entities.TryGetActive(guid, out _));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AcceptedVector_ProjectionFailureStillPublishesCommittedFact()
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
lifetime.BindEventContext(static () => new(21), static () => 90UL);
|
||||||
|
var observer = new RecordingObserver();
|
||||||
|
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||||
|
const uint guid = 0x70000024u;
|
||||||
|
RuntimeEntityRecord canonical =
|
||||||
|
lifetime.RegisterEntity(Spawn(guid, 1, "vector")).Canonical!;
|
||||||
|
var velocity = new System.Numerics.Vector3(3f, 4f, 5f);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
lifetime.TryApplyVector(
|
||||||
|
new VectorUpdate.Parsed(
|
||||||
|
guid,
|
||||||
|
velocity,
|
||||||
|
System.Numerics.Vector3.Zero,
|
||||||
|
InstanceSequence: 1,
|
||||||
|
VectorSequence: 2),
|
||||||
|
_ => throw new InvalidOperationException(
|
||||||
|
"projection failed after canonical commit"),
|
||||||
|
out _));
|
||||||
|
|
||||||
|
Assert.True(lifetime.Entities.IsCurrent(canonical));
|
||||||
|
Assert.Equal(velocity, canonical.Snapshot.Physics!.Value.Velocity);
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
RuntimeEntityChange.Registered,
|
||||||
|
RuntimeEntityChange.Updated,
|
||||||
|
],
|
||||||
|
observer.Entities.Select(delta => delta.Change).ToArray());
|
||||||
|
Assert.Equal([1UL, 2UL], observer.Stamps
|
||||||
|
.Select(stamp => stamp.Sequence)
|
||||||
|
.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReentrantNewerVector_SupersedesOuterCommitPublication()
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
lifetime.BindEventContext(static () => new(22), static () => 91UL);
|
||||||
|
var observer = new RecordingObserver();
|
||||||
|
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||||
|
const uint guid = 0x70000025u;
|
||||||
|
_ = lifetime.RegisterEntity(Spawn(guid, 1, "vector"));
|
||||||
|
var newest = new System.Numerics.Vector3(9f, 8f, 7f);
|
||||||
|
|
||||||
|
bool outerRemainedCurrent = lifetime.TryApplyVector(
|
||||||
|
new VectorUpdate.Parsed(
|
||||||
|
guid,
|
||||||
|
new System.Numerics.Vector3(1f, 2f, 3f),
|
||||||
|
System.Numerics.Vector3.Zero,
|
||||||
|
InstanceSequence: 1,
|
||||||
|
VectorSequence: 2),
|
||||||
|
ignoredCanonical => Assert.True(lifetime.TryApplyVector(
|
||||||
|
new VectorUpdate.Parsed(
|
||||||
|
guid,
|
||||||
|
newest,
|
||||||
|
System.Numerics.Vector3.Zero,
|
||||||
|
InstanceSequence: 1,
|
||||||
|
VectorSequence: 3),
|
||||||
|
acknowledgeProjection: null,
|
||||||
|
out _)),
|
||||||
|
out _);
|
||||||
|
|
||||||
|
Assert.False(outerRemainedCurrent);
|
||||||
|
Assert.True(lifetime.Entities.TryGetActive(
|
||||||
|
guid,
|
||||||
|
out RuntimeEntityRecord current));
|
||||||
|
Assert.Equal(newest, current.Snapshot.Physics!.Value.Velocity);
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
RuntimeEntityChange.Registered,
|
||||||
|
RuntimeEntityChange.Updated,
|
||||||
|
],
|
||||||
|
observer.Entities.Select(delta => delta.Change).ToArray());
|
||||||
|
Assert.Equal(2UL, lifetime.Events.LastSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReentrantTimestampOnlyMotion_SupersedesOuterPublication()
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
lifetime.BindEventContext(static () => new(26), static () => 95UL);
|
||||||
|
var observer = new RecordingObserver();
|
||||||
|
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||||
|
const uint guid = 0x7000002Bu;
|
||||||
|
_ = lifetime.RegisterEntity(Spawn(guid, 1, "motion"));
|
||||||
|
var newestMotion =
|
||||||
|
new CreateObject.ServerMotionState(0x3D, 0x12);
|
||||||
|
|
||||||
|
bool outerRemainedCurrent = lifetime.TryApplyMotion(
|
||||||
|
new WorldSession.EntityMotionUpdate(
|
||||||
|
guid,
|
||||||
|
new CreateObject.ServerMotionState(0x3D, 0x11),
|
||||||
|
InstanceSequence: 1,
|
||||||
|
MovementSequence: 2,
|
||||||
|
ServerControlSequence: 1,
|
||||||
|
IsAutonomous: true),
|
||||||
|
retainPayload: false,
|
||||||
|
ignoredCanonical => Assert.True(lifetime.TryApplyMotion(
|
||||||
|
new WorldSession.EntityMotionUpdate(
|
||||||
|
guid,
|
||||||
|
newestMotion,
|
||||||
|
InstanceSequence: 1,
|
||||||
|
MovementSequence: 3,
|
||||||
|
ServerControlSequence: 1,
|
||||||
|
IsAutonomous: true),
|
||||||
|
retainPayload: false,
|
||||||
|
acknowledgeProjection: null,
|
||||||
|
out _,
|
||||||
|
out _)),
|
||||||
|
out _,
|
||||||
|
out _);
|
||||||
|
|
||||||
|
Assert.False(outerRemainedCurrent);
|
||||||
|
Assert.True(lifetime.Entities.TryGetActive(
|
||||||
|
guid,
|
||||||
|
out RuntimeEntityRecord current));
|
||||||
|
Assert.NotEqual(
|
||||||
|
newestMotion,
|
||||||
|
current.Snapshot.MotionState);
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
RuntimeEntityChange.Registered,
|
||||||
|
RuntimeEntityChange.Updated,
|
||||||
|
],
|
||||||
|
observer.Entities.Select(delta => delta.Change).ToArray());
|
||||||
|
Assert.Equal(2UL, lifetime.Events.LastSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Rebucket_ProjectionFailureStillPublishesCommittedCell()
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
lifetime.BindEventContext(static () => new(23), static () => 92UL);
|
||||||
|
var observer = new RecordingObserver();
|
||||||
|
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||||
|
RuntimeEntityRecord canonical = lifetime.RegisterEntity(
|
||||||
|
Spawn(0x70000026u, 1, "rebucket")).Canonical!;
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
lifetime.CommitRebucket(
|
||||||
|
canonical,
|
||||||
|
0x02020022u,
|
||||||
|
0x0202FFFFu,
|
||||||
|
_ => throw new InvalidOperationException(
|
||||||
|
"projection failed after spatial commit")));
|
||||||
|
|
||||||
|
Assert.Equal(0x02020022u, canonical.FullCellId);
|
||||||
|
RuntimeEntityDelta committed = observer.Entities[^1];
|
||||||
|
Assert.Equal(RuntimeEntityChange.Rebucketed, committed.Change);
|
||||||
|
Assert.Equal(0x02020022u, committed.Entity.CellId);
|
||||||
|
Assert.Equal(2UL, committed.Stamp.Sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReentrantChildStateMutation_SupersedesOuterPublication()
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
lifetime.BindEventContext(static () => new(24), static () => 93UL);
|
||||||
|
var observer = new RecordingObserver();
|
||||||
|
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||||
|
RuntimeEntityRecord canonical = lifetime.RegisterEntity(
|
||||||
|
Spawn(0x70000027u, 1, "child")).Canonical!;
|
||||||
|
|
||||||
|
bool outerRemainedCurrent = lifetime.CommitChildNoDraw(
|
||||||
|
canonical,
|
||||||
|
noDraw: true,
|
||||||
|
_ => Assert.True(lifetime.CommitChildNoDraw(
|
||||||
|
canonical,
|
||||||
|
noDraw: false)));
|
||||||
|
|
||||||
|
Assert.False(outerRemainedCurrent);
|
||||||
|
Assert.Equal(
|
||||||
|
PhysicsStateFlags.None,
|
||||||
|
canonical.FinalPhysicsState & PhysicsStateFlags.NoDraw);
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
RuntimeEntityChange.Registered,
|
||||||
|
RuntimeEntityChange.Updated,
|
||||||
|
],
|
||||||
|
observer.Entities.Select(delta => delta.Change).ToArray());
|
||||||
|
Assert.Equal(2UL, lifetime.Events.LastSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectedUpdate_ConsumesNoSharedSequence()
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
lifetime.BindEventContext(static () => new(25), static () => 94UL);
|
||||||
|
var observer = new RecordingObserver();
|
||||||
|
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||||
|
const uint guid = 0x70000028u;
|
||||||
|
_ = lifetime.RegisterEntity(Spawn(guid, 1, "stale"));
|
||||||
|
|
||||||
|
Assert.False(lifetime.TryApplyVector(
|
||||||
|
new VectorUpdate.Parsed(
|
||||||
|
guid,
|
||||||
|
new System.Numerics.Vector3(1f, 0f, 0f),
|
||||||
|
System.Numerics.Vector3.Zero,
|
||||||
|
InstanceSequence: 1,
|
||||||
|
VectorSequence: 1),
|
||||||
|
acknowledgeProjection: null,
|
||||||
|
out _));
|
||||||
|
|
||||||
|
Assert.Single(observer.Entities);
|
||||||
|
Assert.Equal(1UL, lifetime.Events.LastSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisposeWithLiveDirectState_ConvergesCompleteOwnershipLedger()
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
const uint guid = 0x70000029u;
|
||||||
|
RuntimeEntityRecord canonical =
|
||||||
|
lifetime.RegisterEntity(Spawn(guid, 1, "dispose")).Canonical!;
|
||||||
|
Assert.True(lifetime.ApplyAcceptedSpawn(
|
||||||
|
canonical,
|
||||||
|
canonical.CreateIntegrationVersion,
|
||||||
|
canonical.Snapshot,
|
||||||
|
replaceGeneration: false));
|
||||||
|
_ = lifetime.Events.Subscribe(new RecordingObserver());
|
||||||
|
|
||||||
|
lifetime.Dispose();
|
||||||
|
lifetime.Dispose();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
default(RuntimeEntityObjectOwnershipSnapshot),
|
||||||
|
lifetime.CaptureOwnership());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisposeAfterAcceptedDelete_ConvergesUnfinishedReceiptAndRelations()
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
const uint guid = 0x7000002Au;
|
||||||
|
RuntimeEntityRecord canonical =
|
||||||
|
lifetime.RegisterEntity(Spawn(guid, 1, "pending")).Canonical!;
|
||||||
|
Assert.True(lifetime.ApplyAcceptedSpawn(
|
||||||
|
canonical,
|
||||||
|
canonical.CreateIntegrationVersion,
|
||||||
|
canonical.Snapshot,
|
||||||
|
replaceGeneration: false));
|
||||||
|
lifetime.Entities.ParentAttachments.Enqueue(
|
||||||
|
new ParentEvent.Parsed(
|
||||||
|
ParentGuid: 0x7000F001u,
|
||||||
|
ChildGuid: guid,
|
||||||
|
ParentLocation: 1,
|
||||||
|
PlacementId: 2,
|
||||||
|
ParentInstanceSequence: 4,
|
||||||
|
ChildPositionSequence: 3));
|
||||||
|
Assert.True(lifetime.TryAcceptDelete(
|
||||||
|
new DeleteObject.Parsed(guid, 1),
|
||||||
|
isLocalPlayer: false,
|
||||||
|
removeRetainedObject: true,
|
||||||
|
out _));
|
||||||
|
Assert.Equal(1, lifetime.Entities.PendingTeardownCount);
|
||||||
|
Assert.Equal(1, lifetime.Objects.ObjectCount);
|
||||||
|
|
||||||
|
lifetime.Dispose();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
default(RuntimeEntityObjectOwnershipSnapshot),
|
||||||
|
lifetime.CaptureOwnership());
|
||||||
|
}
|
||||||
|
|
||||||
private static RuntimeEntityRecord Accept(
|
private static RuntimeEntityRecord Accept(
|
||||||
RuntimeEntityObjectLifetime lifetime,
|
RuntimeEntityObjectLifetime lifetime,
|
||||||
WorldSession.EntitySpawn spawn)
|
WorldSession.EntitySpawn spawn)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue