using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Items; using AcDream.Runtime.Entities; namespace AcDream.Runtime.Tests.Entities; public sealed class RuntimeEntityObjectLifetimeTests { [Fact] public void Owner_ConstructsOneExactDirectoryAndObjectTablePerInstance() { var first = new RuntimeEntityObjectLifetime(); var second = new RuntimeEntityObjectLifetime(); Assert.NotSame(first.Entities, second.Entities); Assert.NotSame(first.Objects, second.Objects); Assert.Same(first.Entities, first.Entities); Assert.Same(first.Objects, first.Objects); } [Fact] public void Owner_ConstructionLoadsNoPresentationOrBackendAssembly() { _ = new RuntimeEntityObjectLifetime(); string[] loaded = AppDomain.CurrentDomain .GetAssemblies() .Select(assembly => assembly.GetName().Name ?? string.Empty) .ToArray(); Assert.DoesNotContain(loaded, name => name.StartsWith("AcDream.App", StringComparison.Ordinal) || name.StartsWith("AcDream.UI.", StringComparison.Ordinal) || name.StartsWith("Silk.NET", StringComparison.Ordinal) || name.StartsWith("OpenAL", StringComparison.Ordinal) || name.StartsWith("ImGui", StringComparison.Ordinal)); } [Fact] public void SeparateOwners_IsolateConflictingGuidsAndContents() { const uint guid = 0x70000001u; var first = new RuntimeEntityObjectLifetime(); var second = new RuntimeEntityObjectLifetime(); RuntimeEntityRecord firstRecord = Accept(first, Spawn(guid, 1, "first")); RuntimeEntityRecord secondRecord = Accept(second, Spawn(guid, 9, "second")); Assert.True(first.ApplyAcceptedSpawn( firstRecord, firstRecord.CreateIntegrationVersion, firstRecord.Snapshot, replaceGeneration: false)); Assert.True(second.ApplyAcceptedSpawn( secondRecord, secondRecord.CreateIntegrationVersion, secondRecord.Snapshot, replaceGeneration: false)); Assert.Equal("first", first.Objects.Get(guid)!.Name); Assert.Equal("second", second.Objects.Get(guid)!.Name); Assert.Equal((ushort)1, firstRecord.Incarnation); Assert.Equal((ushort)9, secondRecord.Incarnation); } [Fact] public void AcceptedSpawn_RejectsWrongOrSupersededCanonicalIncarnation() { var lifetime = new RuntimeEntityObjectLifetime(); WorldSession.EntitySpawn spawn = Spawn(0x70000002u, 3, "current"); RuntimeEntityRecord canonical = Accept(lifetime, spawn); var wrong = new RuntimeEntityObjectLifetime(); RuntimeEntityRecord wrongCanonical = Accept(wrong, spawn); Assert.False(lifetime.ApplyAcceptedSpawn( wrongCanonical, wrongCanonical.CreateIntegrationVersion, spawn, replaceGeneration: false)); ulong expectedVersion = canonical.CreateIntegrationVersion; lifetime.Entities.AdvanceCreateAuthority(canonical); Assert.False(lifetime.ApplyAcceptedSpawn( canonical, expectedVersion, spawn, replaceGeneration: false)); Assert.Null(lifetime.Objects.Get(spawn.Guid)); } [Fact] public void AcceptedSpawn_RevalidatesAfterSynchronousObjectCallbacks() { var lifetime = new RuntimeEntityObjectLifetime(); WorldSession.EntitySpawn spawn = Spawn(0x70000003u, 4, "reentrant"); RuntimeEntityRecord canonical = Accept(lifetime, spawn); ulong expectedVersion = canonical.CreateIntegrationVersion; lifetime.Objects.ObjectAdded += _ => lifetime.Entities.AdvanceCreateAuthority(canonical); Assert.False(lifetime.ApplyAcceptedSpawn( canonical, expectedVersion, spawn, replaceGeneration: false)); Assert.NotNull(lifetime.Objects.Get(spawn.Guid)); } [Fact] public void AcceptedDelete_RemovesOnlyMatchingLogicalGeneration() { var lifetime = new RuntimeEntityObjectLifetime(); WorldSession.EntitySpawn spawn = Spawn(0x70000004u, 6, "retained"); RuntimeEntityRecord canonical = Accept(lifetime, spawn); Assert.True(lifetime.ApplyAcceptedSpawn( canonical, canonical.CreateIntegrationVersion, spawn, replaceGeneration: true)); Assert.False(lifetime.TryAcceptDelete( new DeleteObject.Parsed(spawn.Guid, 7), isLocalPlayer: false, removeRetainedObject: true, out _)); Assert.NotNull(lifetime.Objects.Get(spawn.Guid)); Assert.True(lifetime.TryAcceptDelete( new DeleteObject.Parsed(spawn.Guid, 6), isLocalPlayer: false, removeRetainedObject: true, out RuntimeEntityDeleteAcceptance accepted)); Assert.Same(canonical, accepted.RetiredCanonical); Assert.False(lifetime.Entities.TryGetActive(spawn.Guid, out _)); lifetime.CompleteAcceptedDelete(accepted); Assert.Null(lifetime.RetireCanonicalOnly(canonical)); Assert.Null(lifetime.Objects.Get(spawn.Guid)); } [Fact] public void DeleteAcceptance_CannotCrossOwnersOrReplay() { var owner = new RuntimeEntityObjectLifetime(); var other = new RuntimeEntityObjectLifetime(); WorldSession.EntitySpawn spawn = Spawn(0x70000006u, 8, "token"); RuntimeEntityRecord canonical = Accept(owner, spawn); Assert.True(owner.ApplyAcceptedSpawn( canonical, canonical.CreateIntegrationVersion, spawn, replaceGeneration: true)); Assert.True(owner.TryAcceptDelete( new DeleteObject.Parsed(spawn.Guid, spawn.InstanceSequence), isLocalPlayer: false, removeRetainedObject: true, out RuntimeEntityDeleteAcceptance acceptance)); Assert.Throws( () => other.CompleteAcceptedDelete(acceptance)); owner.CompleteAcceptedDelete(acceptance); Assert.Throws( () => owner.CompleteAcceptedDelete(acceptance)); Assert.Null(owner.Objects.Get(spawn.Guid)); } [Fact] public void ClearObjects_DoesNotResetCanonicalEntityIdentity() { var lifetime = new RuntimeEntityObjectLifetime(); WorldSession.EntitySpawn spawn = Spawn(0x70000005u, 2, "clear"); RuntimeEntityRecord canonical = Accept(lifetime, spawn); Assert.True(lifetime.ApplyAcceptedSpawn( canonical, canonical.CreateIntegrationVersion, spawn, replaceGeneration: false)); lifetime.ClearObjects(); Assert.Null(lifetime.Objects.Get(spawn.Guid)); Assert.True(lifetime.Entities.TryGetActive(spawn.Guid, out var retained)); Assert.Same(canonical, retained); } [Fact] public void CanonicalRegistration_IssuesIdentityBeforeAnyProjection() { var lifetime = new RuntimeEntityObjectLifetime(); WorldSession.EntitySpawn spawn = Spawn( 0x70000010u, 4, "direct"); RuntimeEntityRecord canonical = Accept(lifetime, spawn); Assert.Equal( RuntimeEntityDirectory.FirstLocalEntityId, canonical.LocalEntityId); Assert.True(lifetime.EntityView.TryGet( spawn.Guid, out RuntimeEntitySnapshot snapshot)); Assert.Equal(canonical.LocalEntityId, snapshot.Identity.LocalEntityId); Assert.Equal(spawn.InstanceSequence, snapshot.Identity.Incarnation); Assert.Equal(spawn.Position!.Value.LandblockId, snapshot.CellId); } [Fact] public void EntityAndInventoryCommits_ShareOnePerGenerationSequence() { var lifetime = new RuntimeEntityObjectLifetime(); RuntimeGenerationToken generation = new(8); ulong frame = 20; lifetime.BindEventContext(() => generation, () => frame); var observer = new RecordingObserver(); using IDisposable subscription = lifetime.Events.Subscribe(observer); WorldSession.EntitySpawn spawn = Spawn( 0x70000011u, 2, "ordered"); RuntimeEntityRecord canonical = lifetime.RegisterEntity(spawn).Canonical!; Assert.True(lifetime.ApplyAcceptedSpawn( canonical, canonical.CreateIntegrationVersion, spawn, replaceGeneration: false)); frame++; lifetime.Objects.MoveItem( spawn.Guid, newContainerId: 0x50000001u, newSlot: 3); _ = lifetime.RegisterEntity(spawn); Assert.Equal( [ (RuntimeTraceKind.Entity, 1UL), (RuntimeTraceKind.Inventory, 2UL), (RuntimeTraceKind.Inventory, 3UL), (RuntimeTraceKind.Entity, 4UL), ], observer.Order); Assert.All( observer.Stamps, stamp => Assert.Equal(generation, stamp.Generation)); Assert.Equal(20UL, observer.Stamps[0].FrameNumber); Assert.Equal(21UL, observer.Stamps[^1].FrameNumber); generation = new RuntimeGenerationToken(9); lifetime.ClearObjects(); Assert.Equal(1UL, observer.Stamps[^1].Sequence); Assert.Equal(generation, observer.Stamps[^1].Generation); } [Fact] public void InventoryView_UsesCanonicalRuntimeIncarnationWithoutApp() { var lifetime = new RuntimeEntityObjectLifetime(); WorldSession.EntitySpawn spawn = Spawn( 0x70000012u, 12, "inventory"); RuntimeEntityRecord canonical = Accept(lifetime, spawn); Assert.True(lifetime.ApplyAcceptedSpawn( canonical, canonical.CreateIntegrationVersion, spawn, replaceGeneration: false)); Assert.True(lifetime.InventoryView.TryGet( spawn.Guid, out RuntimeInventoryItemSnapshot item)); Assert.Equal((ushort)12, item.Incarnation); Assert.Equal("inventory", item.Name); } [Fact] public void UnknownConfirmedMove_RetainsIdentityAndPlacementInStream() { var lifetime = new RuntimeEntityObjectLifetime(); var observer = new RecordingObserver(); using IDisposable subscription = lifetime.Events.Subscribe(observer); Assert.False(lifetime.Objects.ApplyConfirmedServerMove( 0x7000DEADu, newContainerId: 0x50000001u, newWielderId: 0u, newSlot: 7)); RuntimeInventoryDelta moved = Assert.Single(observer.Inventory); Assert.Equal(RuntimeInventoryChange.Moved, moved.Change); Assert.Equal(0x7000DEADu, moved.Item.ObjectId); Assert.Equal(0x50000001u, moved.Item.ContainerId); Assert.Equal(7, moved.Item.ContainerSlot); } [Fact] public void EventSubscriptions_AreExactAndDisposeWithoutRetention() { var lifetime = new RuntimeEntityObjectLifetime(); var observer = new RecordingObserver(); IDisposable subscription = lifetime.Events.Subscribe(observer); Assert.Equal(1, lifetime.Events.SubscriberCount); Assert.Throws( () => lifetime.Events.Subscribe(observer)); subscription.Dispose(); subscription.Dispose(); Assert.Equal(0, lifetime.Events.SubscriberCount); } [Fact] public void Dispose_DetachesEveryObserverAndRejectsLaterDispatch() { var lifetime = new RuntimeEntityObjectLifetime(); var observer = new RecordingObserver(); _ = lifetime.Events.Subscribe(observer); lifetime.Dispose(); lifetime.Dispose(); Assert.Equal(0, lifetime.Events.SubscriberCount); Assert.Throws( () => lifetime.RegisterEntity( Spawn(0x70000018u, 1, "disposed"))); lifetime.Objects.AddOrUpdate(new ClientObject { ObjectId = 0x70000019u, Name = "detached", }); Assert.Empty(observer.Order); } [Fact] public void ObserverFailure_DoesNotInterruptCommitOrLaterObservers() { var lifetime = new RuntimeEntityObjectLifetime(); var throwing = new CallbackObserver( onEntity: static _ => throw new InvalidOperationException("observer")); var recording = new RecordingObserver(); using IDisposable first = lifetime.Events.Subscribe(throwing); using IDisposable second = lifetime.Events.Subscribe(recording); RuntimeEntityRegistrationResult result = lifetime.RegisterEntity(Spawn(0x70000015u, 1, "committed")); Assert.True(result.LogicalRegistrationCreated); Assert.NotNull(result.Canonical); Assert.True(lifetime.Entities.IsCurrent(result.Canonical)); Assert.Single(recording.Entities); Assert.Equal(RuntimeEntityChange.Registered, recording.Entities[0].Change); Assert.Equal(1, lifetime.Events.DispatchFailureCount); Assert.IsType( lifetime.Events.LastDispatchFailure); } [Fact] public void ObserverMutationDuringDispatch_AffectsOnlyLaterCommits() { var lifetime = new RuntimeEntityObjectLifetime(); var removed = new RecordingObserver(); var added = new RecordingObserver(); IDisposable? removedSubscription = null; IDisposable? addedSubscription = null; bool mutated = false; var mutating = new CallbackObserver( onEntity: _ => { if (mutated) return; mutated = true; removedSubscription!.Dispose(); addedSubscription = lifetime.Events.Subscribe(added); }); using IDisposable first = lifetime.Events.Subscribe(mutating); removedSubscription = lifetime.Events.Subscribe(removed); RuntimeEntityRegistrationResult registration = lifetime.RegisterEntity(Spawn(0x70000016u, 1, "first")); _ = lifetime.RegisterEntity(registration.Canonical!.Snapshot); Assert.Single(removed.Entities); Assert.Single(added.Entities); Assert.Equal( RuntimeEntityChange.Registered, removed.Entities[0].Change); Assert.Equal( RuntimeEntityChange.Updated, added.Entities[0].Change); addedSubscription!.Dispose(); Assert.Equal(1, lifetime.Events.SubscriberCount); } [Fact] public void DirectDelete_PublishesTerminalEntityBeforeInventoryRemoval() { var lifetime = new RuntimeEntityObjectLifetime(); lifetime.BindEventContext(static () => new(4), static () => 11); var observer = new RecordingObserver(); using IDisposable subscription = lifetime.Events.Subscribe(observer); WorldSession.EntitySpawn spawn = Spawn(0x70000017u, 5, "delete"); RuntimeEntityRegistrationResult registration = lifetime.RegisterEntity(spawn); RuntimeEntityRecord canonical = registration.Canonical!; Assert.True(lifetime.ApplyAcceptedSpawn( canonical, canonical.CreateIntegrationVersion, spawn, replaceGeneration: true)); Assert.True(lifetime.TryAcceptDelete( new DeleteObject.Parsed(spawn.Guid, spawn.InstanceSequence), isLocalPlayer: false, removeRetainedObject: true, out RuntimeEntityDeleteAcceptance acceptance)); lifetime.CompleteAcceptedDelete(acceptance); Assert.Null(lifetime.RetireCanonicalOnly(canonical)); Assert.Equal( [ (RuntimeTraceKind.Entity, 1UL), (RuntimeTraceKind.Inventory, 2UL), (RuntimeTraceKind.Entity, 3UL), (RuntimeTraceKind.Inventory, 4UL), ], observer.Order); Assert.Equal( RuntimeEntityChange.Deleted, observer.Entities[^1].Change); Assert.False(lifetime.EntityView.TryGet(spawn.Guid, out _)); Assert.False(lifetime.InventoryView.TryGet(spawn.Guid, out _)); Assert.Equal(0, lifetime.Entities.ClaimedLocalIdCount); } [Fact] public void SessionClear_SealsOldGenerationAndNextStartsAtOne() { var lifetime = new RuntimeEntityObjectLifetime(); RuntimeGenerationToken generation = new(12); lifetime.BindEventContext(() => generation, static () => 30UL); var observer = new RecordingObserver(); bool rejectedReentrantCreate = false; var reentrant = new CallbackObserver( onEntity: delta => { if (delta.Change is not RuntimeEntityChange.Deleted) return; rejectedReentrantCreate = Assert.Throws( () => lifetime.RegisterEntity( Spawn(delta.Entity.Identity.ServerGuid, 2, "stale"))) is not null; }); using IDisposable first = lifetime.Events.Subscribe(reentrant); using IDisposable second = lifetime.Events.Subscribe(observer); WorldSession.EntitySpawn spawn = Spawn(0x70000020u, 1, "old"); RuntimeEntityRecord canonical = lifetime.RegisterEntity(spawn).Canonical!; Assert.True(lifetime.ApplyAcceptedSpawn( canonical, canonical.CreateIntegrationVersion, spawn, replaceGeneration: true)); IReadOnlyList retired = lifetime.BeginSessionClear(); lifetime.ClearObjects(); Assert.Null(lifetime.RetireCanonicalOnly(Assert.Single(retired))); Assert.True(lifetime.CompleteSessionClearIfConverged()); generation = new RuntimeGenerationToken(13); RuntimeEntityRecord replacement = lifetime.RegisterEntity(Spawn(spawn.Guid, 2, "new")).Canonical!; Assert.True(rejectedReentrantCreate); Assert.Equal((ushort)2, replacement.Incarnation); Assert.Equal( [ (new RuntimeGenerationToken(12), 1UL), (new RuntimeGenerationToken(12), 2UL), (new RuntimeGenerationToken(12), 3UL), (new RuntimeGenerationToken(12), 4UL), (new RuntimeGenerationToken(13), 1UL), ], observer.Stamps .Select(stamp => (stamp.Generation, stamp.Sequence)) .ToArray()); } [Fact] public void RegisterEntity_DirectHostPublishesCompleteGenerationOrder() { var lifetime = new RuntimeEntityObjectLifetime(); lifetime.BindEventContext(static () => new(3), static () => 9); var observer = new RecordingObserver(); using IDisposable subscription = lifetime.Events.Subscribe(observer); const uint guid = 0x70000013u; RuntimeEntityRegistrationResult first = lifetime.RegisterEntity(Spawn(guid, 1, "first")); RuntimeEntityRegistrationResult refresh = lifetime.RegisterEntity(Spawn(guid, 1, "refresh")); RuntimeEntityRegistrationResult replacement = lifetime.RegisterEntity(Spawn(guid, 2, "replacement")); Assert.True(first.LogicalRegistrationCreated); Assert.False(refresh.LogicalRegistrationCreated); Assert.True(replacement.ReplacedExistingGeneration); Assert.Equal( [ RuntimeEntityChange.Registered, RuntimeEntityChange.Updated, RuntimeEntityChange.Deleted, RuntimeEntityChange.Registered, ], observer.Entities.Select(delta => delta.Change).ToArray()); Assert.Equal( [1UL, 2UL, 3UL, 4UL], observer.Entities .Select(delta => delta.Stamp.Sequence) .ToArray()); Assert.Equal( (ushort)2, lifetime.EntityView.TryGet(guid, out RuntimeEntitySnapshot current) ? current.Identity.Incarnation : (ushort)0); Assert.Equal(0, lifetime.Entities.PendingTeardownCount); } [Fact] public void RegisterEntity_ReentrantNewerGenerationSupersedesOuterCreate() { var lifetime = new RuntimeEntityObjectLifetime(); const uint guid = 0x70000014u; _ = lifetime.RegisterEntity(Spawn(guid, 1, "first")); bool nested = false; var observer = new CallbackObserver( onEntity: delta => { if (nested || delta.Change is not RuntimeEntityChange.Deleted) { return; } nested = true; _ = lifetime.RegisterEntity(Spawn(guid, 3, "nested")); }); using IDisposable subscription = lifetime.Events.Subscribe(observer); RuntimeEntityRegistrationResult outer = lifetime.RegisterEntity(Spawn(guid, 2, "outer")); Assert.Equal( CreateObjectTimestampDisposition.StaleGeneration, outer.Inbound.Disposition); Assert.False(outer.LogicalRegistrationCreated); Assert.True(lifetime.Entities.TryGetActive( guid, out RuntimeEntityRecord current)); Assert.Equal((ushort)3, current.Incarnation); Assert.NotNull(current.LocalEntityId); } [Fact] public void RegisterEntity_ReentrantDeleteSupersedesSameGenerationRefresh() { var lifetime = new RuntimeEntityObjectLifetime(); const uint guid = 0x70000021u; RuntimeEntityRecord first = lifetime.RegisterEntity(Spawn(guid, 1, "first")).Canonical!; bool deleted = false; var observer = new CallbackObserver( onEntity: delta => { if (deleted || delta.Change is not RuntimeEntityChange.Updated) { return; } deleted = lifetime.TryAcceptDelete( new DeleteObject.Parsed(guid, 1), isLocalPlayer: false, removeRetainedObject: false, out RuntimeEntityDeleteAcceptance acceptance); if (deleted) { lifetime.CompleteAcceptedDelete(acceptance); Assert.Null(lifetime.RetireCanonicalOnly(first)); } }); using IDisposable subscription = lifetime.Events.Subscribe(observer); RuntimeEntityRegistrationResult refresh = lifetime.RegisterEntity(Spawn(guid, 1, "refresh")); Assert.True(deleted); Assert.Equal( CreateObjectTimestampDisposition.StaleGeneration, refresh.Inbound.Disposition); Assert.Null(refresh.Canonical); Assert.False(lifetime.Entities.TryGetActive(guid, out _)); } [Fact] public void RegisterEntity_ReentrantReplacementSupersedesInitialCommit() { var lifetime = new RuntimeEntityObjectLifetime(); const uint guid = 0x70000022u; bool replaced = false; var observer = new CallbackObserver( onEntity: delta => { if (replaced || delta.Change is not RuntimeEntityChange.Registered || delta.Entity.Identity.Incarnation != 1) { return; } replaced = true; _ = lifetime.RegisterEntity(Spawn(guid, 2, "replacement")); }); using IDisposable subscription = lifetime.Events.Subscribe(observer); RuntimeEntityRegistrationResult outer = lifetime.RegisterEntity(Spawn(guid, 1, "outer")); Assert.True(replaced); Assert.Equal( CreateObjectTimestampDisposition.StaleGeneration, outer.Inbound.Disposition); Assert.False(outer.LogicalRegistrationCreated); Assert.Equal( (ushort)2, Assert.IsType(outer.Canonical).Incarnation); } [Fact] public void AcceptedUpdate_RevalidatesAfterReentrantTerminalObserver() { var lifetime = new RuntimeEntityObjectLifetime(); const uint guid = 0x70000023u; RuntimeEntityRecord canonical = lifetime.RegisterEntity(Spawn(guid, 1, "vector")).Canonical!; bool deleted = false; var observer = new CallbackObserver( onEntity: delta => { if (deleted || delta.Change is not RuntimeEntityChange.Updated) { return; } deleted = lifetime.TryAcceptDelete( new DeleteObject.Parsed(guid, 1), isLocalPlayer: false, removeRetainedObject: false, out RuntimeEntityDeleteAcceptance acceptance); if (deleted) { lifetime.CompleteAcceptedDelete(acceptance); Assert.Null(lifetime.RetireCanonicalOnly(canonical)); } }); using IDisposable subscription = lifetime.Events.Subscribe(observer); bool remainedCurrent = lifetime.TryApplyVector( new VectorUpdate.Parsed( guid, new System.Numerics.Vector3(1f, 0f, 0f), System.Numerics.Vector3.Zero, InstanceSequence: 1, VectorSequence: 2), acknowledgeProjection: null, out _); Assert.True(deleted); Assert.False(remainedCurrent); Assert.False(lifetime.Entities.TryGetActive(guid, out _)); } private static RuntimeEntityRecord Accept( RuntimeEntityObjectLifetime lifetime, WorldSession.EntitySpawn spawn) { InboundCreateResult accepted = lifetime.Entities.AcceptCreate(spawn); return lifetime.Entities.AddActive(accepted.Snapshot); } private static WorldSession.EntitySpawn Spawn( uint guid, ushort instance, string name) { var position = new CreateObject.ServerPosition( 0x0101FFFFu, 10f, 20f, 5f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps( Position: 1, Movement: 1, State: 1, Vector: 1, Teleport: 0, ServerControlledMove: 1, ForcePosition: 0, ObjDesc: 1, Instance: instance); var physics = new PhysicsSpawnData( RawState: 0x408u, Position: position, Movement: null, AnimationFrame: null, SetupTableId: 0x02000001u, MotionTableId: 0x09000001u, SoundTableId: null, PhysicsScriptTableId: null, Parent: null, Children: null, Scale: null, Friction: null, Elasticity: null, Translucency: null, Velocity: null, Acceleration: null, AngularVelocity: null, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: timestamps); return new WorldSession.EntitySpawn( guid, position, 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), null, null, name, null, null, 0x09000001u, PhysicsState: 0x408u, InstanceSequence: instance, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } private sealed class RecordingObserver : IRuntimeEntityObjectObserver { public List<(RuntimeTraceKind Kind, ulong Sequence)> Order { get; } = []; public List Stamps { get; } = []; public List Entities { get; } = []; public List Inventory { get; } = []; public void OnEntity(in RuntimeEntityDelta delta) { Order.Add((RuntimeTraceKind.Entity, delta.Stamp.Sequence)); Stamps.Add(delta.Stamp); Entities.Add(delta); } public void OnInventory(in RuntimeInventoryDelta delta) { Order.Add((RuntimeTraceKind.Inventory, delta.Stamp.Sequence)); Stamps.Add(delta.Stamp); Inventory.Add(delta); } } private sealed class CallbackObserver( Action? onEntity = null, Action? onInventory = null) : IRuntimeEntityObjectObserver { public void OnEntity(in RuntimeEntityDelta delta) => onEntity?.Invoke(delta); public void OnInventory(in RuntimeInventoryDelta delta) => onInventory?.Invoke(delta); } }