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:
Erik 2026-07-26 07:13:42 +02:00
parent 84954c8b77
commit 119b7c1151
9 changed files with 992 additions and 95 deletions

View file

@ -195,6 +195,32 @@ public sealed class CurrentGameRuntimeAdapterTests
spawn.Guid,
Harness.PlayerGuid,
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(
spawn.Guid,
new Vector3(1f, 2f, 3f),
@ -216,6 +242,12 @@ public sealed class CurrentGameRuntimeAdapterTests
acknowledgeProjection: null,
out _,
out _));
Assert.True(direct.CommitChildNoDraw(
directCanonical,
noDraw: true));
Assert.True(direct.CommitChildNoDraw(
directCanonical,
noDraw: false));
WorldSession.EntityPositionUpdate position =
Position(spawn.Guid, spawn.InstanceSequence);
Assert.True(direct.TryApplyPosition(
@ -228,6 +260,10 @@ public sealed class CurrentGameRuntimeAdapterTests
out _,
out _,
out _));
Assert.True(direct.CommitRebucket(
directCanonical,
0x12360001u,
0x1236FFFFu));
Assert.True(direct.TryApplyPickup(
new PickupEvent.Parsed(
spawn.Guid,
@ -253,8 +289,22 @@ public sealed class CurrentGameRuntimeAdapterTests
spawn.Guid,
Harness.PlayerGuid,
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.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(
position,
isLocalPlayer: false,
@ -263,6 +313,9 @@ public sealed class CurrentGameRuntimeAdapterTests
out _,
out _,
out _));
Assert.True(graphical.Entities.RebucketLiveEntity(
spawn.Guid,
0x12360001u));
Assert.True(graphical.Entities.TryApplyPickup(
new PickupEvent.Parsed(
spawn.Guid,

View file

@ -9,6 +9,7 @@ using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Vfx;
using AcDream.Core.World;
using AcDream.Runtime.Entities;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
@ -244,6 +245,7 @@ public sealed class LiveEntityLifecycleStressTests
fixture.Effects.ClearNetworkState();
Assert.Equal(0, fixture.Runtime.Count);
Assert.Equal(0, fixture.Effects.PendingPacketCount);
AssertOwnershipLedgerConverged(fixture);
}
[Fact]
@ -299,6 +301,98 @@ public sealed class LiveEntityLifecycleStressTests
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 readonly Dictionary<uint, DatPhysicsScript> _scripts = new();
@ -328,7 +422,8 @@ public sealed class LiveEntityLifecycleStressTests
randomUnit: () => 0.5,
canAdvanceOwner: ownerId => _effects?.CanAdvanceOwner(ownerId) ?? true);
Runtime = LiveEntityRuntimeFixture.Create(
EntityObjects = new RuntimeEntityObjectLifetime();
Runtime = new LiveEntityRuntime(
Spatial,
new DelegateLiveEntityResourceLifecycle(
entity =>
@ -358,7 +453,8 @@ public sealed class LiveEntityLifecycleStressTests
cleanups.Add(() => _liveLights?.Forget(record));
}
LiveEntityTeardown.Run(cleanups);
});
},
EntityObjects);
Effects = new EntityEffectController(
Runtime,
@ -386,6 +482,7 @@ public sealed class LiveEntityLifecycleStressTests
internal LightManager Lights { get; } = new();
internal LightingHookSink Lighting { get; }
internal PhysicsScriptRunner Runner { get; }
internal RuntimeEntityObjectLifetime EntityObjects { get; }
internal LiveEntityRuntime Runtime { get; }
internal EntityEffectController Effects { get; }
internal ProjectileController Projectiles { get; }

View file

@ -393,6 +393,124 @@ public sealed class RuntimeEntityObjectLifetimeTests
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]
public void DirectDelete_PublishesTerminalEntityBeforeInventoryRemoval()
{
@ -530,6 +648,97 @@ public sealed class RuntimeEntityObjectLifetimeTests
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]
public void RegisterEntity_ReentrantNewerGenerationSupersedesOuterCreate()
{
@ -685,6 +894,274 @@ public sealed class RuntimeEntityObjectLifetimeTests
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(
RuntimeEntityObjectLifetime lifetime,
WorldSession.EntitySpawn spawn)