using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; 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, out _)); Assert.NotNull(lifetime.Objects.Get(spawn.Guid)); Assert.True(lifetime.TryAcceptDelete( new DeleteObject.Parsed(spawn.Guid, 6), isLocalPlayer: false, out RuntimeEntityDeleteAcceptance accepted)); lifetime.CompleteAcceptedDelete(accepted); 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, 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); } 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); } }