using AcDream.App.Streaming; using AcDream.App.World; using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Vfx; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Vfx; using AcDream.Core.World; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; using DatPhysicsScript = DatReaderWriter.DBObjs.PhysicsScript; namespace AcDream.App.Tests.World; public sealed class LiveEntityRuntimeTests { private sealed class RecordingResources : ILiveEntityResourceLifecycle { public int RegisterCount { get; private set; } public int UnregisterCount { get; private set; } public void Register(WorldEntity entity) => RegisterCount++; public void Unregister(WorldEntity entity) => UnregisterCount++; } private sealed class AnimationRuntime(WorldEntity entity) : ILiveEntityAnimationRuntime { public WorldEntity Entity { get; } = entity; } private sealed class EffectProfile : ILiveEntityEffectProfile { } private sealed class RemoteMotionRuntime : ILiveEntityRemoteMotionRuntime { public PhysicsBody Body { get; } = new(); } private sealed class ProjectileRuntime(PhysicsBody body) : ILiveEntityProjectileRuntime { public PhysicsBody Body { get; } = body; } private sealed class FailingRegisterResources : ILiveEntityResourceLifecycle { public int RegisterCount { get; private set; } public int UnregisterCount { get; private set; } public void Register(WorldEntity entity) { RegisterCount++; throw new InvalidOperationException("fixture registration failure"); } public void Unregister(WorldEntity entity) => UnregisterCount++; } private sealed class FailingRegisterAndRollbackOnceResources : ILiveEntityResourceLifecycle { private bool _registerFailurePending = true; private bool _unregisterFailurePending = true; public int RegisterCount { get; private set; } public int UnregisterCount { get; private set; } public void Register(WorldEntity entity) { RegisterCount++; if (_registerFailurePending) { _registerFailurePending = false; throw new InvalidOperationException("partial registration failure"); } } public void Unregister(WorldEntity entity) { UnregisterCount++; if (_unregisterFailurePending) { _unregisterFailurePending = false; throw new InvalidOperationException("rollback failure"); } } } private sealed class FailingUnregisterResources(uint failingGuid) : ILiveEntityResourceLifecycle { public int UnregisterCount { get; private set; } public void Register(WorldEntity entity) { } public void Unregister(WorldEntity entity) { UnregisterCount++; if (entity.ServerGuid == failingGuid) throw new InvalidOperationException("fixture unregister failure"); } } private sealed class FailingOnceUnregisterResources(uint failingGuid) : ILiveEntityResourceLifecycle { private bool _failurePending = true; public int UnregisterCount { get; private set; } public void Register(WorldEntity entity) { } public void Unregister(WorldEntity entity) { UnregisterCount++; if (entity.ServerGuid == failingGuid && _failurePending) { _failurePending = false; throw new InvalidOperationException("fixture one-shot unregister failure"); } } } private sealed class CallbackTextureLifetime : IEntityTextureLifetime { public Action? OnRelease { get; set; } public int ReleaseCount { get; private set; } public void ReleaseOwner(uint localEntityId) { ReleaseCount++; OnRelease?.Invoke(); } } private sealed class NullAnimationLoader : IAnimationLoader { public Animation? LoadAnimation(uint id) => null; } private sealed class CallbackResources : ILiveEntityResourceLifecycle { public Action? OnRegister { get; set; } public Action? OnUnregister { get; set; } public int RegisterCount { get; private set; } public int UnregisterCount { get; private set; } public Dictionary UnregisterCountsByGuid { get; } = new(); public void Register(WorldEntity entity) { RegisterCount++; OnRegister?.Invoke(entity); } public void Unregister(WorldEntity entity) { UnregisterCount++; UnregisterCountsByGuid[entity.ServerGuid] = UnregisterCountsByGuid.GetValueOrDefault(entity.ServerGuid) + 1; OnUnregister?.Invoke(entity); } } [Fact] public void RegisterRebucketWithdrawAndRestore_UsesOneLogicalCreate() { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); spatial.AddLandblock(EmptyLandblock(0x0102FFFFu)); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x70000001u, 1, 1, 0x01010001u); LiveEntityRegistrationResult registered = runtime.RegisterLiveEntity(spawn); WorldEntity? entity = runtime.MaterializeLiveEntity( spawn.Guid, spawn.Position!.Value.LandblockId, id => Entity(id, spawn.Guid)); runtime.SetAnimationRuntime(spawn.Guid, new AnimationRuntime(entity!)); var visibilityEdges = new List(); runtime.ProjectionVisibilityChanged += (_, visible) => visibilityEdges.Add(visible); Assert.True(registered.LogicalRegistrationCreated); Assert.Equal(1, resources.RegisterCount); Assert.Single(spatial.Entities); Assert.True(runtime.RebucketLiveEntity(spawn.Guid, 0x01020001u)); Assert.Equal(1, resources.RegisterCount); Assert.Single(spatial.Entities); Assert.True(runtime.WithdrawLiveEntityProjection(spawn.Guid)); Assert.Empty(spatial.Entities); Assert.Empty(runtime.WorldEntities); Assert.Equal(1, resources.RegisterCount); Assert.Equal(0, resources.UnregisterCount); Assert.True(runtime.TryGetAnimationRuntime(entity!.Id, out _)); Assert.True(runtime.RebucketLiveEntity(spawn.Guid, 0x01010001u)); Assert.Single(spatial.Entities); Assert.Equal(1, resources.RegisterCount); Assert.True(runtime.TryGetAnimationRuntime(entity!.Id, out _)); Assert.Equal(new[] { false, true }, visibilityEdges); } [Fact] public void SpatialComponentWorksets_FollowLoadedProjectionWhileLogicalOwnersSurvive() { const uint guid = 0x70000070u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; var animation = new AnimationRuntime(entity); var remote = new RemoteMotionRuntime(); var projectile = new ProjectileRuntime(remote.Body); runtime.SetAnimationRuntime(guid, animation); runtime.SetRemoteMotionRuntime(guid, remote); runtime.SetProjectileRuntime(guid, projectile); Assert.Same(animation, Assert.Single(runtime.SpatialAnimationRuntimes).Value); Assert.Same(remote, Assert.Single(runtime.SpatialRemoteMotionRuntimes).Value); Assert.Same(projectile, Assert.Single(runtime.SpatialProjectileRuntimes).Value); // Hidden suppresses presentation, not the live CPhysicsObj workset. Assert.True(runtime.TryApplyState(new SetState.Parsed( guid, (uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.IgnoreCollisions), 1, 2), out _)); Assert.Single(runtime.SpatialAnimationRuntimes); Assert.Single(runtime.SpatialRemoteMotionRuntimes); Assert.Single(runtime.SpatialProjectileRuntimes); // A pending projection retains every logical component but disappears // from all per-frame worksets. Assert.True(runtime.RebucketLiveEntity(guid, 0x02020001u)); Assert.Empty(runtime.SpatialAnimationRuntimes); Assert.Empty(runtime.SpatialRemoteMotionRuntimes); Assert.Empty(runtime.SpatialProjectileRuntimes); Assert.True(runtime.TryGetAnimationRuntime(entity.Id, out var retainedAnimation)); Assert.Same(animation, retainedAnimation); Assert.True(runtime.TryGetRemoteMotionRuntime(guid, out var retainedRemote)); Assert.Same(remote, retainedRemote); Assert.True(runtime.TryGetProjectileRuntime(guid, out var retainedProjectile)); Assert.Same(projectile, retainedProjectile); spatial.AddLandblock(EmptyLandblock(0x0202FFFFu)); Assert.Single(runtime.SpatialAnimationRuntimes); Assert.Single(runtime.SpatialRemoteMotionRuntimes); Assert.Single(runtime.SpatialProjectileRuntimes); Assert.True(runtime.WithdrawLiveEntityProjection(guid)); Assert.Empty(runtime.SpatialAnimationRuntimes); Assert.Empty(runtime.SpatialRemoteMotionRuntimes); Assert.Empty(runtime.SpatialProjectileRuntimes); } [Fact] public void AnimationView_EnumeratesSpatialOwnersButRetainsLogicalLookupAndRemoval() { const uint guid = 0x70000071u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity entity = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; var animation = new AnimationRuntime(entity); runtime.SetAnimationRuntime(guid, animation); var view = new LiveEntityAnimationRuntimeView(() => runtime); Assert.Equal(1, view.Count); Assert.Equal(entity.Id, Assert.Single(view.Keys)); Assert.Same(animation, Assert.Single(view).Value); Assert.True(runtime.RebucketLiveEntity(guid, 0x02020001u)); Assert.Equal(0, view.Count); Assert.Empty(view.Keys); Assert.Empty(view); Assert.True(view.TryGetValue(entity.Id, out AnimationRuntime retained)); Assert.Same(animation, retained); Assert.True(view.Remove(entity.Id)); Assert.False(view.TryGetValue(entity.Id, out _)); Assert.Empty(runtime.AnimationRuntimes); Assert.Empty(runtime.SpatialAnimationRuntimes); } [Fact] public void AnimationView_RebucketCallbackSkipsDisplacedSnapshotOwner() { const uint firstGuid = 0x70000073u; const uint secondGuid = 0x70000074u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); foreach (uint guid in new[] { firstGuid, secondGuid }) { runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity entity = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; runtime.SetAnimationRuntime(guid, new AnimationRuntime(entity)); } var view = new LiveEntityAnimationRuntimeView(() => runtime); var visited = new List(); foreach (KeyValuePair pair in view) { uint guid = pair.Value.Entity.ServerGuid; visited.Add(guid); uint other = guid == firstGuid ? secondGuid : firstGuid; Assert.True(runtime.RebucketLiveEntity(other, 0x02020001u)); } Assert.Single(visited); Assert.Single(runtime.SpatialAnimationRuntimes); Assert.Equal(2, runtime.AnimationRuntimes.Count); } [Fact] public void SpatialComponentWorksets_IsolateGuidReuseAcrossVisibilityCallbacks() { const uint guid = 0x70000072u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity oldEntity = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; var oldAnimation = new AnimationRuntime(oldEntity); runtime.SetAnimationRuntime(guid, oldAnimation); runtime.ProjectionVisibilityChanged += (record, visible) => { if (visible || record.Generation != 1) return; runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010002u)); WorldEntity replacement = runtime.MaterializeLiveEntity( guid, 0x01010002u, id => Entity(id, guid))!; runtime.SetAnimationRuntime(guid, new AnimationRuntime(replacement)); }; Assert.False(runtime.WithdrawLiveEntityProjection(guid)); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current)); Assert.Equal((ushort)2, current.Generation); KeyValuePair indexed = Assert.Single(runtime.SpatialAnimationRuntimes); Assert.Same(current.AnimationRuntime, indexed.Value); Assert.NotSame(oldAnimation, indexed.Value); } [Fact] public void EffectProfile_SurvivesRebucketAndClearsWithLogicalTeardown() { const uint guid = 0x70000030u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); spatial.AddLandblock(EmptyLandblock(0x0102FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); WorldSession.EntitySpawn spawn = Spawn(guid, 6, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); var profile = new EffectProfile(); runtime.SetEffectProfile(guid, profile); runtime.MaterializeLiveEntity( guid, spawn.Position!.Value.LandblockId, id => Entity(id, guid)); Assert.True(runtime.RebucketLiveEntity(guid, 0x01020001u)); Assert.True(runtime.TryGetEffectProfile(guid, out var retained)); Assert.Same(profile, retained); Assert.True(runtime.UnregisterLiveEntity( new DeleteObject.Parsed(guid, spawn.InstanceSequence), isLocalPlayer: false)); Assert.False(runtime.TryGetEffectProfile(guid, out _)); } [Fact] public void InitialChildCreate_PreservesParentEventQueuedForMissingChild() { const uint parentGuid = 0x70000020u; const uint childGuid = 0x70000021u; var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources()); runtime.RegisterLiveEntity(Spawn(parentGuid, 9, 1, 0x01010001u)); runtime.ParentAttachments.Enqueue(new ParentEvent.Parsed( parentGuid, childGuid, 1, 2, 9, 5)); ResolveParent(runtime, childGuid); runtime.RegisterLiveEntity(Spawn(childGuid, 3, 4, 0x01010001u)); ResolveParent(runtime, childGuid); Assert.True(runtime.ParentAttachments.TryGetProjection( childGuid, out ParentAttachmentRelation relation)); Assert.Equal(parentGuid, relation.ParentGuid); Assert.True(runtime.TryGetSnapshot(childGuid, out WorldSession.EntitySpawn child)); Assert.Null(child.Position); } [Fact] public void InitialParentCreate_PreservesCreateObjectRelationWaitingForParent() { const uint parentGuid = 0x70000022u; const uint childGuid = 0x70000023u; var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources()); runtime.RegisterLiveEntity(Spawn(childGuid, 3, 4, 0x01010001u)); runtime.ParentAttachments.AcceptCreateObjectRelation(new ParentAttachmentRelation( parentGuid, childGuid, 1, 2, 9, 4)); runtime.RegisterLiveEntity(Spawn(parentGuid, 9, 1, 0x01010001u)); Assert.True(runtime.ParentAttachments.TryGetProjection( childGuid, out ParentAttachmentRelation relation)); Assert.Equal(parentGuid, relation.ParentGuid); } [Fact] public void SameGenerationCreate_DoesNotReconstructProjection() { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn first = Spawn(0x70000002u, 4, 10, 0x01010001u); runtime.RegisterLiveEntity(first); WorldEntity original = runtime.MaterializeLiveEntity( first.Guid, first.Position!.Value.LandblockId, id => Entity(id, first.Guid))!; LiveEntityRegistrationResult refresh = runtime.RegisterLiveEntity( Spawn(first.Guid, 4, 11, 0x01010001u)); WorldEntity retained = runtime.MaterializeLiveEntity( first.Guid, first.Position.Value.LandblockId, _ => throw new InvalidOperationException("same generation reconstructed"))!; Assert.False(refresh.LogicalRegistrationCreated); Assert.Same(original, retained); Assert.Equal(1, resources.RegisterCount); Assert.Equal(0, resources.UnregisterCount); } [Fact] public void NewGeneration_TearsDownExactlyOnceAndReusesNoLocalIdentity() { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); int runtimeTearDowns = 0; var runtime = new LiveEntityRuntime( spatial, resources, _ => runtimeTearDowns++); const uint guid = 0x70000003u; runtime.RegisterLiveEntity(Spawn(guid, 9, 1, 0x01010001u)); WorldEntity old = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; LiveEntityRegistrationResult replacement = runtime.RegisterLiveEntity( Spawn(guid, 10, 1, 0x01010001u)); WorldEntity current = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; Assert.True(replacement.ReplacedExistingGeneration); Assert.NotEqual(old.Id, current.Id); Assert.Equal(2, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); Assert.Equal(1, runtimeTearDowns); Assert.Single(spatial.Entities); Assert.Same(current, spatial.Entities[0]); Assert.False(runtime.TryGetServerGuid(old.Id, out _)); Assert.True(runtime.TryGetServerGuid(current.Id, out uint mapped)); Assert.Equal(guid, mapped); } [Fact] public void AppearanceUpdate_MutatesSnapshotWithoutChangingIdentity() { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x70000004u, 2, 3, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( spawn.Guid, 0x01010001u, id => Entity(id, spawn.Guid))!; var update = new ObjDescEvent.Parsed( spawn.Guid, new CreateObject.ModelData( 0x04000001u, Array.Empty(), Array.Empty(), Array.Empty()), InstanceSequence: 2, ObjDescSequence: 2); Assert.True(runtime.TryApplyObjDesc(update, out _)); Assert.True(runtime.TryGetRecord(spawn.Guid, out LiveEntityRecord record)); Assert.Same(entity, record.WorldEntity); Assert.Equal((uint)0x04000001u, record.Snapshot.BasePaletteId); Assert.Equal(1, resources.RegisterCount); Assert.Equal(0, resources.UnregisterCount); } [Fact] public void DeleteAndGuidReuse_HaveSeparateLogicalLifetimes() { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); const uint guid = 0x70000005u; runtime.RegisterLiveEntity(Spawn(guid, 3, 1, 0x01010001u)); WorldEntity first = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; Assert.True(runtime.UnregisterLiveEntity( new DeleteObject.Parsed(guid, 3), isLocalPlayer: false)); Assert.Empty(spatial.Entities); Assert.False(runtime.TryGetRecord(guid, out _)); runtime.RegisterLiveEntity(Spawn(guid, 3, 1, 0x01010001u)); WorldEntity second = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; Assert.NotEqual(first.Id, second.Id); Assert.Equal(2, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); } [Fact] public void AttachedProjection_RendersWithoutEnteringTopLevelWorldIndex() { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x70000006u, 3, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity attached = runtime.MaterializeLiveEntity( spawn.Guid, spawn.Position!.Value.LandblockId, id => Entity(id, spawn.Guid), LiveEntityProjectionKind.Attached)!; Assert.Single(spatial.Entities); Assert.Empty(runtime.WorldEntities); Assert.False(runtime.TryMarkWorldSpawnPublished(spawn.Guid)); Assert.True(runtime.TryGetWorldEntity(spawn.Guid, out WorldEntity resolved)); Assert.Same(attached, resolved); Assert.Equal(1, resources.RegisterCount); attached.IsAncestorDrawVisible = false; WorldEntity same = runtime.MaterializeLiveEntity( spawn.Guid, spawn.Position.Value.LandblockId, _ => throw new InvalidOperationException("attachment reconstructed"), LiveEntityProjectionKind.World)!; Assert.Same(attached, same); Assert.True(same.IsAncestorDrawVisible); Assert.Same(attached, Assert.Single(runtime.WorldEntities).Value); Assert.True(runtime.TryMarkWorldSpawnPublished(spawn.Guid)); Assert.False(runtime.TryMarkWorldSpawnPublished(spawn.Guid)); Assert.Equal(1, resources.RegisterCount); } [Fact] public void PendingToLoadedTransition_DoesNotReplayLogicalRegistration() { var spatial = new GpuWorldState(); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x70000007u, 1, 1, 0x01020001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( spawn.Guid, spawn.Position!.Value.LandblockId, id => Entity(id, spawn.Guid))!; Assert.Empty(spatial.Entities); Assert.Equal(1, spatial.PendingLiveEntityCount); Assert.Equal(1, resources.RegisterCount); Assert.Empty(runtime.WorldEntities); Assert.Same(entity, Assert.Single(runtime.MaterializedWorldEntities).Value); spatial.AddLandblock(EmptyLandblock(0x0102FFFFu)); Assert.Same(entity, Assert.Single(spatial.Entities)); Assert.Same(entity, Assert.Single(runtime.WorldEntities).Value); Assert.True(runtime.RebucketLiveEntity(spawn.Guid, 0x01020001u)); Assert.Same(entity, Assert.Single(spatial.Entities)); Assert.Equal(1, resources.RegisterCount); Assert.Equal(0, resources.UnregisterCount); } [Fact] public void PendingProjection_RemainsCanonicalAcrossAuthoritativeUpdatesWithoutReregister() { const uint guid = 0x70000025u; var spatial = new GpuWorldState(); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(guid, 2, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( guid, spawn.Position!.Value.LandblockId, id => Entity(id, guid))!; Assert.Empty(runtime.WorldEntities); Assert.Same(entity, Assert.Single(runtime.MaterializedWorldEntities).Value); Assert.True(runtime.TryApplyVector( new VectorUpdate.Parsed( guid, new System.Numerics.Vector3(2f, 3f, 4f), System.Numerics.Vector3.Zero, spawn.InstanceSequence, 2), out _)); var positionUpdate = new WorldSession.EntityPositionUpdate( guid, new CreateObject.ServerPosition( 0x01020001u, 20f, 30f, 6f, 1f, 0f, 0f, 0f), null, null, true, spawn.InstanceSequence, 2, 0, 0); Assert.True(runtime.TryApplyPosition( positionUpdate, isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out PositionTimestampDisposition disposition, out WorldSession.EntitySpawn accepted, out _)); Assert.Equal(PositionTimestampDisposition.Apply, disposition); Assert.True(runtime.RebucketLiveEntity(guid, accepted.Position!.Value.LandblockId)); Assert.Empty(runtime.WorldEntities); Assert.Same(entity, Assert.Single(runtime.MaterializedWorldEntities).Value); Assert.Equal(1, resources.RegisterCount); Assert.Equal(0, resources.UnregisterCount); Assert.Equal(1, spatial.PendingLiveEntityCount); } [Fact] public void PickupLeaveWorld_PreservesOwnersButPausesRootUntilPositionReentry() { const uint guid = 0x70000026u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(guid, 4, 10, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( guid, spawn.Position!.Value.LandblockId, id => Entity(id, guid))!; var animation = new AnimationRuntime(entity); runtime.SetAnimationRuntime(guid, animation); uint localId = entity.Id; Assert.True(runtime.TryMarkWorldSpawnPublished(guid)); Assert.True(runtime.TryApplyPickup( new PickupEvent.Parsed(guid, spawn.InstanceSequence, 11), out _)); Assert.True(runtime.WithdrawLiveEntityProjection(guid)); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record)); Assert.Equal(0u, record.FullCellId); Assert.Equal(0u, record.CanonicalLandblockId); Assert.False(runtime.ShouldAdvanceRootRuntime(guid)); Assert.True(runtime.TryGetAnimationRuntime(localId, out var retainedAnimation)); Assert.Same(animation, retainedAnimation); Assert.Equal(1, resources.RegisterCount); Assert.Equal(0, resources.UnregisterCount); var positionUpdate = new WorldSession.EntityPositionUpdate( guid, new CreateObject.ServerPosition( 0x01010002u, 30f, 40f, 7f, 1f, 0f, 0f, 0f), null, null, true, spawn.InstanceSequence, 12, 0, 0); Assert.True(runtime.TryApplyPosition( positionUpdate, isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out PositionTimestampDisposition disposition, out WorldSession.EntitySpawn accepted, out _)); Assert.Equal(PositionTimestampDisposition.Apply, disposition); WorldEntity same = runtime.MaterializeLiveEntity( guid, accepted.Position!.Value.LandblockId, _ => throw new InvalidOperationException("re-entry reconstructed the projection"))!; Assert.Same(entity, same); Assert.Equal(localId, same.Id); Assert.False(runtime.TryMarkWorldSpawnPublished(guid)); Assert.True(runtime.ShouldAdvanceRootRuntime(guid)); Assert.Equal(0x01010002u, record.FullCellId); Assert.True(runtime.TryGetAnimationRuntime(localId, out retainedAnimation)); Assert.Same(animation, retainedAnimation); Assert.Equal(1, resources.RegisterCount); Assert.Equal(0, resources.UnregisterCount); } [Fact] public void RootRuntimePausesWhilePendingOrFrozenAndResumesInVisibleCell() { const uint guid = 0x70000027u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); runtime.MaterializeLiveEntity( guid, spawn.Position!.Value.LandblockId, id => Entity(id, guid)); Assert.True(runtime.ShouldAdvanceRootRuntime(guid)); Assert.True(runtime.RebucketLiveEntity(guid, 0x02020001u)); Assert.False(runtime.ShouldAdvanceRootRuntime(guid)); spatial.AddLandblock(EmptyLandblock(0x0202FFFFu)); Assert.True(runtime.ShouldAdvanceRootRuntime(guid)); Assert.True(runtime.TryApplyState( new SetState.Parsed( guid, (uint)(PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Frozen), 1, 2), out _)); Assert.False(runtime.ShouldAdvanceRootRuntime(guid)); Assert.True(runtime.TryApplyState( new SetState.Parsed( guid, (uint)PhysicsStateFlags.ReportCollisions, 1, 3), out _)); Assert.True(runtime.ShouldAdvanceRootRuntime(guid)); } [Fact] public void PhysicsStateAndRemoteBodyStaySynchronizedAcrossEitherArrivalOrder() { const uint stateBeforeBindGuid = 0x70000037u; const uint bindBeforeStateGuid = 0x70000038u; var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources()); runtime.RegisterLiveEntity(Spawn(stateBeforeBindGuid, 1, 1, 0x01010001u)); runtime.RegisterLiveEntity(Spawn(bindBeforeStateGuid, 1, 1, 0x01010001u)); PhysicsStateFlags firstState = PhysicsStateFlags.Hidden | PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions; Assert.True(runtime.TryApplyState( new SetState.Parsed(stateBeforeBindGuid, (uint)firstState, 1, 2), out _)); var lateBody = new RemoteMotionRuntime(); runtime.SetRemoteMotionRuntime(stateBeforeBindGuid, lateBody); var earlyBody = new RemoteMotionRuntime(); runtime.SetRemoteMotionRuntime(bindBeforeStateGuid, earlyBody); PhysicsStateFlags secondState = PhysicsStateFlags.Static | PhysicsStateFlags.Ethereal | PhysicsStateFlags.NoDraw; Assert.True(runtime.TryApplyState( new SetState.Parsed(bindBeforeStateGuid, (uint)secondState, 1, 2), out _)); Assert.Equal((firstState & ~PhysicsStateFlags.ReportCollisions) | PhysicsStateFlags.IgnoreCollisions, lateBody.Body.State); Assert.Equal(secondState, earlyBody.Body.State); } [Fact] public void ParentDrivenChildNoDrawKeepsBoundBodyInCanonicalState() { const uint guid = 0x70000039u; var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); var remote = new RemoteMotionRuntime(); runtime.SetRemoteMotionRuntime(guid, remote); Assert.True(runtime.SetAttachedChildNoDraw(guid, true)); Assert.True(remote.Body.State.HasFlag(PhysicsStateFlags.NoDraw)); Assert.True(runtime.SetAttachedChildNoDraw(guid, false)); Assert.False(remote.Body.State.HasFlag(PhysicsStateFlags.NoDraw)); } [Fact] public void InitiallyVisibleCreate_DoesNotManufactureUnHideTransition() { const uint guid = 0x70000040u; var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record)); Assert.True(record.TryDequeueStateTransition(out RetailPhysicsStateTransition transition)); Assert.Equal(RetailHiddenTransition.None, transition.HiddenTransition); Assert.Equal(PhysicsStateFlags.ReportCollisions, record.FinalPhysicsState); } [Fact] public void HiddenCreate_SuppressesMeshInteractionButRetainsNarrowObjectTick() { const uint guid = 0x70000041u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn( guid, 1, 1, 0x01010001u, PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions)); WorldEntity entity = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; Assert.False(entity.IsDrawVisible); Assert.Empty(runtime.WorldEntities); Assert.Same(entity, Assert.Single(runtime.MaterializedWorldEntities).Value); Assert.Single(spatial.Entities); Assert.True(runtime.ShouldAdvanceRootRuntime(guid)); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record)); Assert.Equal(PhysicsStateFlags.None, record.FinalPhysicsState & PhysicsStateFlags.ReportCollisions); Assert.NotEqual(PhysicsStateFlags.None, record.FinalPhysicsState & PhysicsStateFlags.IgnoreCollisions); } [Fact] public void HiddenThenVisibleState_RestoresSameMeshAndInteractionIdentity() { const uint guid = 0x70000042u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity entity = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; Assert.True(runtime.TryApplyState(new SetState.Parsed( guid, (uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions), 1, 2), out _, out RetailPhysicsStateTransition hidden)); Assert.Equal(RetailHiddenTransition.BecameHidden, hidden.HiddenTransition); Assert.False(entity.IsDrawVisible); Assert.Empty(runtime.WorldEntities); Assert.False(runtime.TryGetInteractionEligibleEntity(guid, out _)); Assert.True(runtime.TryApplyState(new SetState.Parsed( guid, 0u, 1, 3), out _, out RetailPhysicsStateTransition visible)); Assert.Equal(RetailHiddenTransition.BecameVisible, visible.HiddenTransition); Assert.True(entity.IsDrawVisible); Assert.True(runtime.TryGetInteractionEligibleEntity(guid, out var eligible)); Assert.Same(entity, eligible); Assert.Same(entity, Assert.Single(runtime.WorldEntities).Value); Assert.Same(entity, Assert.Single(spatial.Entities)); } [Fact] public void PositionAfterPickup_RequiresTeleportHookEvenWithEqualTeleportStamp() { const uint guid = 0x70000043u; var runtime = new LiveEntityRuntime(new GpuWorldState(), new RecordingResources()); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); Assert.True(runtime.TryApplyPickup( new PickupEvent.Parsed(guid, 1, 2), out _)); var update = new WorldSession.EntityPositionUpdate( guid, new CreateObject.ServerPosition( 0x01010002u, 12f, 13f, 5f, 1f, 0f, 0f, 0f), null, null, true, 1, 3, 0, 0); Assert.True(runtime.TryApplyPosition( update, isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out PositionTimestampDisposition disposition, out _, out AcceptedPhysicsTimestamps timestamps)); Assert.Equal(PositionTimestampDisposition.Apply, disposition); Assert.False(timestamps.TeleportAdvanced); Assert.True(timestamps.TeleportHookRequired); } [Fact] public void PositionFromPendingProjection_RequiresTeleportHookWithEqualTeleportStamp() { const uint guid = 0x70000044u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid)); spatial.RemoveLandblock(0x0101FFFFu); Assert.True(runtime.TryApplyPosition( new WorldSession.EntityPositionUpdate( guid, new CreateObject.ServerPosition( 0x01010002u, 12f, 13f, 5f, 1f, 0f, 0f, 0f), null, null, true, 1, 2, 0, 0), isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out PositionTimestampDisposition disposition, out _, out AcceptedPhysicsTimestamps timestamps)); Assert.Equal(PositionTimestampDisposition.Apply, disposition); Assert.False(timestamps.TeleportAdvanced); Assert.True(timestamps.TeleportHookRequired); } [Fact] public void LandblockUnload_HidesTopLevelViewAndReloadRestoresSameIdentity() { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x70000024u, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( spawn.Guid, 0x01010001u, id => Entity(id, spawn.Guid))!; spatial.RemoveLandblock(0x0101FFFFu); Assert.Empty(runtime.WorldEntities); Assert.True(runtime.TryGetRecord(spawn.Guid, out LiveEntityRecord record)); Assert.True(record.IsSpatiallyProjected); Assert.False(record.IsSpatiallyVisible); Assert.Equal(1, spatial.PendingLiveEntityCount); Assert.Equal(1, resources.RegisterCount); Assert.Equal(0, resources.UnregisterCount); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); Assert.Same(entity, Assert.Single(runtime.WorldEntities).Value); Assert.True(record.IsSpatiallyVisible); Assert.Equal(1, resources.RegisterCount); } [Fact] public void LoadedToPendingToLoaded_RemovesOldDrawSlotWithoutLogicalRecreate() { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x70000009u, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( spawn.Guid, spawn.Position!.Value.LandblockId, id => Entity(id, spawn.Guid))!; Assert.Same(entity, Assert.Single(spatial.Entities)); Assert.True(runtime.RebucketLiveEntity(spawn.Guid, 0x01020001u)); Assert.Empty(spatial.Entities); Assert.Equal(1, spatial.PendingLiveEntityCount); Assert.Equal(1, resources.RegisterCount); spatial.AddLandblock(EmptyLandblock(0x0102FFFFu)); Assert.Same(entity, Assert.Single(spatial.Entities)); Assert.Equal(1, resources.RegisterCount); Assert.Equal(0, resources.UnregisterCount); } [Fact] public void SessionClear_IsSymmetricAndIdempotent() { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); int runtimeTearDowns = 0; var runtime = new LiveEntityRuntime(spatial, resources, _ => runtimeTearDowns++); WorldSession.EntitySpawn spawn = Spawn(0x70000008u, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); runtime.MaterializeLiveEntity( spawn.Guid, spawn.Position!.Value.LandblockId, id => Entity(id, spawn.Guid)); runtime.Clear(); runtime.Clear(); Assert.Equal(0, runtime.Count); Assert.Empty(runtime.WorldEntities); Assert.Empty(spatial.Entities); Assert.Equal(1, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); Assert.Equal(1, runtimeTearDowns); } [Fact] public void ResourceRegistrationFailure_RollsBackMaterializedIdentityAndSpatialState() { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingRegisterResources(); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x7000000Au, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); Assert.Throws(() => runtime.MaterializeLiveEntity( spawn.Guid, spawn.Position!.Value.LandblockId, id => Entity(id, spawn.Guid))); Assert.True(runtime.TryGetRecord(spawn.Guid, out LiveEntityRecord record)); Assert.Null(record.WorldEntity); Assert.Equal(0, runtime.MaterializedCount); Assert.Empty(runtime.WorldEntities); Assert.Empty(spatial.Entities); Assert.Equal(1, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); } [Fact] public void RegistrationRollbackFailureRetainsTombstoneUntilRetryConverges() { const uint guid = 0x7000005Au; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingRegisterAndRollbackOnceResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); Assert.Throws(() => runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))); Assert.Equal(0, runtime.Count); Assert.Equal(1, runtime.PendingTeardownCount); Assert.Equal(1, runtime.MaterializedCount); Assert.False(runtime.TryGetRecord(guid, out _)); spatial.MarkPersistent(guid); Assert.Equal(1, runtime.RetryPendingTeardowns()); Assert.Equal(0, runtime.PendingTeardownCount); Assert.Equal(0, runtime.MaterializedCount); Assert.Equal(2, resources.UnregisterCount); Assert.Equal(1, spatial.PersistentGuidCount); LiveEntityRegistrationResult recovered = runtime.RegisterLiveEntity( Spawn(guid, 1, 1, 0x01010001u)); Assert.True(recovered.LogicalRegistrationCreated); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); spatial.RemoveLandblock(0x0101FFFFu); Assert.Equal(1, spatial.PendingRescueCount); Assert.Equal(1, spatial.PersistentGuidCount); } [Fact] public void RegistrationRollbackTombstoneIsIsolatedFromSameGuidReplacement() { const uint guid = 0x7000005Bu; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingRegisterAndRollbackOnceResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); Assert.Throws(() => runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))); runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010001u)); WorldEntity replacement = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; Assert.Equal(1, runtime.RetryPendingTeardowns()); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current)); Assert.Same(replacement, current.WorldEntity); Assert.Same(replacement, Assert.Single(runtime.MaterializedWorldEntities).Value); Assert.Equal(0, runtime.PendingTeardownCount); } [Fact] public void SameGenerationCreateCannotRecoverUntilRegistrationTombstoneConverges() { const uint guid = 0x7000005Cu; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingRegisterAndRollbackOnceResources(); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); Assert.Throws(() => runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))); LiveEntityRegistrationResult retransmit = runtime.RegisterLiveEntity(spawn); Assert.False(retransmit.LogicalRegistrationCreated); Assert.Null(retransmit.Record); Assert.Equal(0, runtime.Count); Assert.Equal(1, runtime.PendingTeardownCount); Assert.True(runtime.UnregisterLiveEntity( new DeleteObject.Parsed(guid, InstanceSequence: 1), isLocalPlayer: false)); Assert.Equal(0, runtime.PendingTeardownCount); Assert.False(runtime.TryGetSnapshot(guid, out _)); } [Fact] public void SessionClear_AttemptsEveryTeardownAndClearsIdentityWhenOneResourceFails() { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingUnregisterResources(0x7000000Bu); var runtime = new LiveEntityRuntime(spatial, resources); foreach (uint guid in new[] { 0x7000000Bu, 0x7000000Cu }) { WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); runtime.MaterializeLiveEntity( guid, spawn.Position!.Value.LandblockId, id => Entity(id, guid)); } Assert.Throws(() => runtime.Clear()); Assert.Equal(2, resources.UnregisterCount); Assert.Equal(0, runtime.Count); Assert.Equal(1, runtime.PendingTeardownCount); Assert.Equal(1, runtime.MaterializedCount); Assert.Empty(runtime.WorldEntities); Assert.Empty(spatial.Entities); } [Fact] public void Delete_UnregisterFailureRetainsTombstoneAndSameDeleteRetriesCleanup() { const uint guid = 0x7000004Au; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingOnceUnregisterResources(guid); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); var delete = new DeleteObject.Parsed(guid, InstanceSequence: 1); Assert.Throws( () => runtime.UnregisterLiveEntity(delete, isLocalPlayer: false)); Assert.Equal(0, runtime.Count); Assert.Equal(1, runtime.PendingTeardownCount); Assert.Equal(1, runtime.MaterializedCount); Assert.Equal(1, resources.UnregisterCount); Assert.True(runtime.UnregisterLiveEntity(delete, isLocalPlayer: false)); Assert.Equal(0, runtime.PendingTeardownCount); Assert.Equal(0, runtime.MaterializedCount); Assert.Equal(2, resources.UnregisterCount); } [Fact] public void SessionClear_OneShotUnregisterFailure_SecondClearConverges() { const uint guid = 0x7000004Bu; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingOnceUnregisterResources(guid); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); Assert.Throws(() => runtime.Clear()); Assert.Equal(1, runtime.PendingTeardownCount); Assert.Equal(1, runtime.MaterializedCount); runtime.Clear(); Assert.Equal(0, runtime.PendingTeardownCount); Assert.Equal(0, runtime.MaterializedCount); Assert.Empty(spatial.Entities); Assert.Equal(2, resources.UnregisterCount); } [Fact] public void ReentrantDeleteDuringPresentationSuspend_IsQueuedAndNextUpdateCompletesIt() { const uint guid = 0x7000004Cu; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var textures = new CallbackTextureLifetime(); var adapter = new EntitySpawnAdapter( textures, _ => new AnimationSequencer(new Setup(), new MotionTable(), new NullAnimationLoader())); var resources = new DelegateLiveEntityResourceLifecycle( entity => adapter.OnCreate(entity), entity => { if (!adapter.OnRemove(entity)) throw new InvalidOperationException("presentation owner was not registered"); }); var runtime = new LiveEntityRuntime(spatial, resources); runtime.ProjectionVisibilityChanged += (record, visible) => { if (record.WorldEntity is { } entity) adapter.SetPresentationResident(entity, visible); }; WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); WorldEntity entity = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; var delete = new DeleteObject.Parsed(guid, InstanceSequence: 1); textures.OnRelease = () => { textures.OnRelease = null; runtime.UnregisterLiveEntity(delete, isLocalPlayer: false); }; Assert.Throws( () => adapter.SetPresentationResident(entity, resident: false)); Assert.Equal(1, runtime.PendingTeardownCount); Assert.NotNull(adapter.GetState(guid)); Assert.Equal(1, runtime.RetryPendingTeardowns()); Assert.Equal(0, runtime.PendingTeardownCount); Assert.Null(adapter.GetState(guid)); Assert.Equal(0, runtime.MaterializedCount); } [Fact] public void GenerationReplacement_InstallsNewRecordBeforeReportingOldCleanupFailure() { const uint guid = 0x7000000Eu; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new FailingUnregisterResources(guid); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn first = Spawn(guid, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(first); runtime.MaterializeLiveEntity( guid, first.Position!.Value.LandblockId, id => Entity(id, guid)); LiveEntityRegistrationResult replacement = runtime.RegisterLiveEntity( Spawn(guid, 2, 1, 0x01010001u)); Assert.NotNull(replacement.PriorGenerationCleanupFailure); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record)); Assert.Equal((ushort)2, record.Generation); Assert.Null(record.WorldEntity); WorldEntity installed = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; Assert.Equal(guid, installed.ServerGuid); Assert.Single(spatial.Entities); } [Fact] public void CanonicalOnlyRebucket_DoesNotOverwriteAuthoritativeFullCell() { const uint guid = 0x7000000Fu; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); spatial.AddLandblock(EmptyLandblock(0x0102FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010022u)); runtime.MaterializeLiveEntity(guid, 0x01010022u, id => Entity(id, guid)); runtime.RebucketLiveEntity(guid, 0x0102FFFFu); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record)); Assert.Equal(0x01010022u, record.FullCellId); Assert.Equal(0x0102FFFFu, record.CanonicalLandblockId); runtime.RebucketLiveEntity(guid, 0x01020033u); Assert.Equal(0x01020033u, record.FullCellId); } [Fact] public void AttachedProjection_CrossesLandblocksWithoutChangingIdentityOrResources() { const uint guid = 0x70000010u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); spatial.AddLandblock(EmptyLandblock(0x0102FFFFu)); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity attached = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid), LiveEntityProjectionKind.Attached)!; runtime.RebucketLiveEntity(guid, 0x01020001u); Assert.Same(attached, Assert.Single(spatial.Entities)); Assert.Empty(runtime.WorldEntities); Assert.Equal(1, resources.RegisterCount); Assert.Equal(0, resources.UnregisterCount); var destination = Assert.Single( spatial.LandblockEntries, entry => entry.LandblockId == 0x0102FFFFu); Assert.Same(attached, Assert.Single(destination.Entities)); } [Fact] public void LocalIdAllocator_WrapsInsideLiveNamespaceWithoutReturningZero() { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime( spatial, new RecordingResources(), firstLocalEntityId: LiveEntityRuntime.LastLiveEntityId); WorldSession.EntitySpawn first = Spawn(0x70000011u, 1, 1, 0x01010001u); WorldSession.EntitySpawn second = Spawn(0x70000012u, 1, 1, 0x01010001u); runtime.RegisterLiveEntity(first); runtime.RegisterLiveEntity(second); WorldEntity firstEntity = runtime.MaterializeLiveEntity( first.Guid, 0x01010001u, id => Entity(id, first.Guid))!; WorldEntity secondEntity = runtime.MaterializeLiveEntity( second.Guid, 0x01010001u, id => Entity(id, second.Guid))!; Assert.Equal(LiveEntityRuntime.LastLiveEntityId, firstEntity.Id); Assert.Equal(LiveEntityRuntime.FirstLiveEntityId, secondEntity.Id); Assert.NotEqual(0u, secondEntity.Id); Assert.True(runtime.TryGetLocalEntityId(second.Guid, out uint resolved)); Assert.Equal(secondEntity.Id, resolved); Assert.Throws(() => new LiveEntityRuntime( new GpuWorldState(), new RecordingResources(), firstLocalEntityId: uint.MaxValue)); } [Fact] public void PersistentRescue_DeleteBeforeDrain_ForgetsReferenceAndPersistenceClass() { const uint guid = 0x7000002Au; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid)); spatial.MarkPersistent(guid); spatial.RemoveLandblock(0x0101FFFFu); Assert.Equal(1, spatial.PendingRescueCount); Assert.Equal(1, spatial.PersistentGuidCount); Assert.True(runtime.UnregisterLiveEntity( new DeleteObject.Parsed(guid, InstanceSequence: 1), isLocalPlayer: false)); Assert.Equal(0, spatial.PendingRescueCount); Assert.Equal(0, spatial.PersistentGuidCount); Assert.Empty(spatial.DrainRescued()); Assert.Equal(0, runtime.Count); } [Fact] public void SessionClear_ForgetsPersistentClassificationWithoutMaterializedRecord() { var spatial = new GpuWorldState(); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); spatial.MarkPersistent(0x7000002Bu); runtime.Clear(); Assert.Equal(0, spatial.PersistentGuidCount); Assert.Equal(0, spatial.PendingRescueCount); } [Fact] public void PersistentClassification_SurvivesSameGuidGenerationReplacementUntilSessionReset() { const uint guid = 0x7000002Du; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid)); spatial.MarkPersistent(guid); runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid)); Assert.Equal(1, spatial.PersistentGuidCount); spatial.RemoveLandblock(0x0101FFFFu); Assert.Equal(1, spatial.PendingRescueCount); runtime.Clear(); Assert.Equal(0, spatial.PersistentGuidCount); Assert.Equal(0, spatial.PendingRescueCount); } [Fact] public void RebucketObserverFailure_CommitsCanonicalCellAndProjectionBeforeRethrow() { const uint guid = 0x70000040u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity entity = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; spatial.LiveProjectionVisibilityChanged += (_, _) => throw new InvalidOperationException("observer failed after spatial commit"); Assert.Throws(() => runtime.RebucketLiveEntity(guid, 0x02020022u)); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record)); Assert.Equal(0x02020022u, record.FullCellId); Assert.Equal(0x0202FFFFu, record.CanonicalLandblockId); Assert.True(record.IsSpatiallyProjected); Assert.False(record.IsSpatiallyVisible); Assert.Same(entity, Assert.Single(runtime.MaterializedWorldEntities).Value); Assert.Empty(runtime.WorldEntities); Assert.Equal(1, spatial.PendingLiveEntityCount); Assert.Equal(0, spatial.PendingVisibilityTransitionCount); } [Fact] public void WithdrawObserverFailure_CommitsCanonicalWithdrawalBeforeRethrow() { const uint guid = 0x70000041u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid)); runtime.ProjectionVisibilityChanged += (_, visible) => { if (!visible) throw new InvalidOperationException("observer failed after withdrawal"); }; Assert.Throws(() => runtime.WithdrawLiveEntityProjection(guid)); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record)); Assert.False(record.IsSpatiallyProjected); Assert.False(record.IsSpatiallyVisible); Assert.Empty(runtime.MaterializedWorldEntities); Assert.Empty(runtime.WorldEntities); Assert.Empty(spatial.Entities); Assert.Equal(0, spatial.PendingLiveEntityCount); Assert.Equal(0, spatial.PendingVisibilityTransitionCount); } [Fact] public void WithdrawVisibilityCallback_GuidReplacementPreservesNewIncarnationProjection() { const uint guid = 0x70000044u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); bool replaced = false; runtime.ProjectionVisibilityChanged += (edgeRecord, visible) => { if (replaced || visible || edgeRecord.Generation != 1) return; replaced = true; Assert.True(runtime.UnregisterLiveEntity( new DeleteObject.Parsed(guid, InstanceSequence: 1), isLocalPlayer: false)); runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010011u)); runtime.MaterializeLiveEntity(guid, 0x01010011u, id => Entity(id, guid)); }; Assert.False(runtime.WithdrawLiveEntityProjection(guid)); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord replacement)); Assert.Equal((ushort)2, replacement.Generation); Assert.Equal(0x01010011u, replacement.FullCellId); Assert.True(replacement.IsSpatiallyProjected); Assert.True(replacement.IsSpatiallyVisible); Assert.Same(replacement.WorldEntity, Assert.Single(runtime.MaterializedWorldEntities).Value); Assert.Same(replacement.WorldEntity, Assert.Single(runtime.WorldEntities).Value); Assert.Same(replacement.WorldEntity, Assert.Single(spatial.Entities)); } [Fact] public void RebucketSpatialCallback_GuidReplacementPreservesNewIncarnationProjection() { const uint guid = 0x70000045u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); bool replaced = false; spatial.LiveProjectionVisibilityChanged += (edgeGuid, visible) => { if (replaced || visible || edgeGuid != guid) return; replaced = true; Assert.True(runtime.UnregisterLiveEntity( new DeleteObject.Parsed(guid, InstanceSequence: 1), isLocalPlayer: false)); runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010012u)); runtime.MaterializeLiveEntity(guid, 0x01010012u, id => Entity(id, guid)); }; Assert.False(runtime.RebucketLiveEntity(guid, 0x02020022u)); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord replacement)); Assert.Equal((ushort)2, replacement.Generation); Assert.Equal(0x01010012u, replacement.FullCellId); Assert.Equal(0x0101FFFFu, replacement.CanonicalLandblockId); Assert.True(replacement.IsSpatiallyProjected); Assert.True(replacement.IsSpatiallyVisible); Assert.Same(replacement.WorldEntity, Assert.Single(runtime.MaterializedWorldEntities).Value); Assert.Same(replacement.WorldEntity, Assert.Single(runtime.WorldEntities).Value); Assert.Same(replacement.WorldEntity, Assert.Single(spatial.Entities)); Assert.Equal(0, spatial.PendingLiveEntityCount); } [Fact] public void WithdrawVisibilityCallback_RebucketSameRecordSupersedesOuterWithdrawal() { const uint guid = 0x70000046u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); bool reprojected = false; runtime.ProjectionVisibilityChanged += (_, visible) => { if (reprojected || visible) return; reprojected = true; Assert.True(runtime.RebucketLiveEntity(guid, 0x01010022u)); }; Assert.False(runtime.WithdrawLiveEntityProjection(guid)); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record)); Assert.Equal(0x01010022u, record.FullCellId); Assert.True(record.IsSpatiallyProjected); Assert.True(record.IsSpatiallyVisible); Assert.Same(record.WorldEntity, Assert.Single(runtime.MaterializedWorldEntities).Value); Assert.Same(record.WorldEntity, Assert.Single(runtime.WorldEntities).Value); Assert.Same(record.WorldEntity, Assert.Single(spatial.Entities)); } [Fact] public void RebucketSpatialCallback_NewerSameRecordRebucketSupersedesOuterMove() { const uint guid = 0x70000047u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); spatial.AddLandblock(EmptyLandblock(0x0303FFFFu)); var runtime = new LiveEntityRuntime(spatial, new RecordingResources()); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); bool rebucketed = false; spatial.LiveProjectionVisibilityChanged += (edgeGuid, visible) => { if (rebucketed || visible || edgeGuid != guid) return; rebucketed = true; Assert.True(runtime.RebucketLiveEntity(guid, 0x03030033u)); }; Assert.False(runtime.RebucketLiveEntity(guid, 0x02020022u)); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record)); Assert.Equal(0x03030033u, record.FullCellId); Assert.Equal(0x0303FFFFu, record.CanonicalLandblockId); Assert.True(record.IsSpatiallyProjected); Assert.True(record.IsSpatiallyVisible); Assert.Same(record.WorldEntity, Assert.Single(runtime.MaterializedWorldEntities).Value); Assert.Same(record.WorldEntity, Assert.Single(runtime.WorldEntities).Value); Assert.Same(record.WorldEntity, Assert.Single(spatial.Entities)); Assert.Equal(0, spatial.PendingLiveEntityCount); } [Fact] public void ReentrantVisibilityGuidReuse_DoesNotApplyQueuedOldEdgeToReplacement() { const uint guid = 0x7000002Cu; var spatial = new GpuWorldState(); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid)); var edges = new List<(ushort Generation, bool Visible)>(); bool replaced = false; runtime.ProjectionVisibilityChanged += (record, visible) => { edges.Add((record.Generation, visible)); if (replaced || record.Generation != 1 || !visible) return; replaced = true; Assert.True(runtime.UnregisterLiveEntity( new DeleteObject.Parsed(guid, InstanceSequence: 1), isLocalPlayer: false)); runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid)); }; spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); Assert.Equal([(1, true), (2, true)], edges); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord replacement)); Assert.Equal((ushort)2, replacement.Generation); Assert.True(replacement.IsSpatiallyVisible); Assert.True(spatial.IsLiveEntityVisible(guid)); Assert.Equal(2, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); } [Fact] public void SessionClearInsideVisibilityCallback_DropsOldQueuedEdgesBeforeReuse() { const uint guid = 0x7000002Eu; var spatial = new GpuWorldState(); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid)); var edges = new List<(ushort Generation, bool Visible)>(); bool reset = false; runtime.ProjectionVisibilityChanged += (record, visible) => { edges.Add((record.Generation, visible)); if (reset || record.Generation != 1 || !visible) return; reset = true; runtime.Clear(); runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid)); }; spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); Assert.Equal([(1, true), (2, true)], edges); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord replacement)); Assert.Equal((ushort)2, replacement.Generation); Assert.True(replacement.IsSpatiallyVisible); Assert.Equal(0, spatial.PendingVisibilityTransitionCount); Assert.Equal(2, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); } [Fact] public void DeleteCallback_CanRegisterNextGenerationButCannotMaterializeBeforeOldTeardown() { const uint guid = 0x70000031u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); WorldEntity old = runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid))!; AggregateException error = Assert.Throws(() => runtime.UnregisterLiveEntity( new DeleteObject.Parsed(guid, InstanceSequence: 1), isLocalPlayer: false, beforeTeardown: () => { runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010001u)); runtime.MaterializeLiveEntity( guid, 0x01010001u, id => Entity(id, guid)); })); Assert.Contains( error.Flatten().InnerExceptions, exception => exception is InvalidOperationException && exception.Message.Contains("active logical-lifetime transition", StringComparison.Ordinal)); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current)); Assert.Equal((ushort)2, current.Generation); Assert.Null(current.WorldEntity); Assert.Empty(spatial.Entities); Assert.DoesNotContain(guid, runtime.WorldEntities.Keys); Assert.Equal(1, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); Assert.False(runtime.TryGetServerGuid(old.Id, out _)); } [Fact] public void GenerationTeardownCallback_NewerReentrantGenerationSupersedesOuterCreate() { const uint guid = 0x70000032u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); resources.OnUnregister = _ => { resources.OnUnregister = null; runtime.RegisterLiveEntity(Spawn(guid, 3, 1, 0x01010001u)); }; LiveEntityRegistrationResult outer = runtime.RegisterLiveEntity( Spawn(guid, 2, 1, 0x01010001u)); Assert.Equal(CreateObjectTimestampDisposition.StaleGeneration, outer.Inbound.Disposition); Assert.False(outer.LogicalRegistrationCreated); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current)); Assert.Equal((ushort)3, current.Generation); Assert.Null(current.WorldEntity); Assert.Empty(spatial.Entities); Assert.Equal(1, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); } [Fact] public void GenerationTeardownCallback_UnrelatedRegistrationDoesNotSupersedeOuterCreate() { const uint guid = 0x7000003Bu; const uint unrelatedGuid = 0x7000003Cu; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); resources.OnUnregister = _ => { resources.OnUnregister = null; runtime.RegisterLiveEntity(Spawn(unrelatedGuid, 1, 1, 0x01010001u)); }; LiveEntityRegistrationResult outer = runtime.RegisterLiveEntity( Spawn(guid, 2, 1, 0x01010001u)); Assert.Equal(CreateObjectTimestampDisposition.NewGeneration, outer.Inbound.Disposition); Assert.True(outer.LogicalRegistrationCreated); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord replacement)); Assert.Equal((ushort)2, replacement.Generation); Assert.True(runtime.TryGetRecord(unrelatedGuid, out _)); Assert.Equal(2, runtime.Count); } [Fact] public void GenerationTeardownCallback_AcceptedDeletePreventsOuterGenerationResurrection() { const uint guid = 0x70000035u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); resources.OnUnregister = _ => { resources.OnUnregister = null; Assert.True(runtime.UnregisterLiveEntity( new DeleteObject.Parsed(guid, InstanceSequence: 2), isLocalPlayer: false)); }; LiveEntityRegistrationResult outer = runtime.RegisterLiveEntity( Spawn(guid, 2, 1, 0x01010001u)); Assert.Equal(CreateObjectTimestampDisposition.StaleGeneration, outer.Inbound.Disposition); Assert.False(outer.LogicalRegistrationCreated); Assert.False(runtime.TryGetRecord(guid, out _)); Assert.False(runtime.TryGetSnapshot(guid, out _)); Assert.Empty(spatial.Entities); Assert.Equal(1, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); } [Fact] public void GenerationTeardownCallback_DeleteThenRegisterCannotAbaOuterEpoch() { const uint guid = 0x70000042u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); resources.OnUnregister = _ => { resources.OnUnregister = null; Assert.True(runtime.UnregisterLiveEntity( new DeleteObject.Parsed(guid, InstanceSequence: 2), isLocalPlayer: false)); runtime.RegisterLiveEntity(Spawn(guid, 3, 1, 0x01010001u)); }; LiveEntityRegistrationResult outer = runtime.RegisterLiveEntity( Spawn(guid, 2, 1, 0x01010001u)); Assert.Equal(CreateObjectTimestampDisposition.StaleGeneration, outer.Inbound.Disposition); Assert.False(outer.LogicalRegistrationCreated); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current)); Assert.Equal((ushort)3, current.Generation); Assert.Null(current.WorldEntity); Assert.Equal(1, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); } [Fact] public void GenerationTeardownCallback_NewerGenerationAndFailureIsNeverSilentlyDiscarded() { const uint guid = 0x70000043u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); resources.OnUnregister = _ => { resources.OnUnregister = null; runtime.RegisterLiveEntity(Spawn(guid, 3, 1, 0x01010001u)); throw new InvalidOperationException("old incarnation cleanup failed"); }; AggregateException error = Assert.Throws(() => runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010001u))); Assert.Contains( error.Flatten().InnerExceptions, exception => exception is InvalidOperationException && exception.Message == "old incarnation cleanup failed"); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current)); Assert.Equal((ushort)3, current.Generation); Assert.Null(current.WorldEntity); Assert.Equal(1, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); } [Fact] public void GenerationTeardownCallback_SessionClearPreventsOuterGenerationResurrection() { const uint guid = 0x70000036u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid)); resources.OnUnregister = _ => { resources.OnUnregister = null; runtime.Clear(); }; LiveEntityRegistrationResult outer = runtime.RegisterLiveEntity( Spawn(guid, 2, 1, 0x01010001u)); Assert.Equal(CreateObjectTimestampDisposition.StaleGeneration, outer.Inbound.Disposition); Assert.False(outer.LogicalRegistrationCreated); Assert.False(runtime.TryGetRecord(guid, out _)); Assert.False(runtime.TryGetSnapshot(guid, out _)); Assert.Empty(spatial.Entities); Assert.Equal(1, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); } [Fact] public void ResourceRegistrationCallback_CannotReplaceIncarnationAndRollsBackOwner() { const uint guid = 0x70000037u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); resources.OnRegister = _ => runtime.RegisterLiveEntity(Spawn(guid, 2, 1, 0x01010001u)); InvalidOperationException error = Assert.Throws(() => runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid))); Assert.Contains("atomic resource registration", error.Message, StringComparison.Ordinal); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current)); Assert.Equal((ushort)1, current.Generation); Assert.Null(current.WorldEntity); Assert.Equal(1, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); Assert.Equal(0, runtime.MaterializedCount); Assert.Empty(spatial.Entities); } [Fact] public void ResourceRegistrationCallback_CannotClearSessionAndRollsBackOwner() { const uint guid = 0x70000038u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(guid, 1, 1, 0x01010001u)); resources.OnRegister = _ => runtime.Clear(); InvalidOperationException error = Assert.Throws(() => runtime.MaterializeLiveEntity(guid, 0x01010001u, id => Entity(id, guid))); Assert.Contains("atomic resource registration", error.Message, StringComparison.Ordinal); Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current)); Assert.Null(current.WorldEntity); Assert.Equal(1, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); Assert.Equal(0, runtime.MaterializedCount); Assert.Empty(spatial.Entities); } [Fact] public void SessionClear_ReentrantDeleteOfLaterSnapshot_TearsEachRecordDownOnce() { const uint firstGuid = 0x70000039u; const uint secondGuid = 0x7000003Au; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(firstGuid, 1, 1, 0x01010001u)); runtime.RegisterLiveEntity(Spawn(secondGuid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(firstGuid, 0x01010001u, id => Entity(id, firstGuid)); runtime.MaterializeLiveEntity(secondGuid, 0x01010001u, id => Entity(id, secondGuid)); resources.OnUnregister = entity => { if (entity.ServerGuid != firstGuid) return; resources.OnUnregister = null; Assert.True(runtime.UnregisterLiveEntity( new DeleteObject.Parsed(secondGuid, InstanceSequence: 1), isLocalPlayer: false)); }; runtime.Clear(); Assert.Equal(1, resources.UnregisterCountsByGuid[firstGuid]); Assert.Equal(1, resources.UnregisterCountsByGuid[secondGuid]); Assert.Equal(2, resources.UnregisterCount); Assert.Equal(0, runtime.Count); Assert.Equal(0, runtime.MaterializedCount); Assert.Empty(spatial.Entities); } [Fact] public void SessionClear_RejectsRegistrationFromTeardownCallbackWithoutLeakingOwner() { const uint oldGuid = 0x70000033u; const uint callbackGuid = 0x70000034u; var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new CallbackResources(); var runtime = new LiveEntityRuntime(spatial, resources); runtime.RegisterLiveEntity(Spawn(oldGuid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity(oldGuid, 0x01010001u, id => Entity(id, oldGuid)); resources.OnUnregister = _ => { resources.OnUnregister = null; runtime.RegisterLiveEntity(Spawn(callbackGuid, 1, 1, 0x01010001u)); runtime.MaterializeLiveEntity( callbackGuid, 0x01010001u, id => Entity(id, callbackGuid)); }; AggregateException error = Assert.Throws(() => runtime.Clear()); Assert.Contains( error.Flatten().InnerExceptions, exception => exception is InvalidOperationException && exception.Message.Contains("while the session lifetime is clearing", StringComparison.Ordinal)); Assert.Equal(0, runtime.Count); Assert.Equal(1, runtime.PendingTeardownCount); Assert.Equal(1, runtime.MaterializedCount); Assert.Empty(runtime.WorldEntities); Assert.Empty(spatial.Entities); Assert.Equal(0, spatial.PendingLiveEntityCount); Assert.Equal(1, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); runtime.Clear(); Assert.Equal(0, runtime.PendingTeardownCount); Assert.Equal(0, runtime.MaterializedCount); Assert.Empty(runtime.MaterializedWorldEntities); Assert.Equal(2, resources.UnregisterCount); } [Fact] public void Delete_CleansLogicalRecordEvenWhenPreTeardownCallbackFails() { var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(0x0101FFFFu)); var resources = new RecordingResources(); var runtime = new LiveEntityRuntime(spatial, resources); WorldSession.EntitySpawn spawn = Spawn(0x7000000Du, 4, 1, 0x01010001u); runtime.RegisterLiveEntity(spawn); runtime.MaterializeLiveEntity( spawn.Guid, spawn.Position!.Value.LandblockId, id => Entity(id, spawn.Guid)); Assert.Throws(() => runtime.UnregisterLiveEntity( new DeleteObject.Parsed(spawn.Guid, spawn.InstanceSequence), isLocalPlayer: false, beforeTeardown: () => throw new InvalidOperationException("fixture callback failure"))); Assert.Equal(0, runtime.Count); Assert.Empty(runtime.WorldEntities); Assert.Empty(spatial.Entities); Assert.Equal(1, resources.UnregisterCount); } [Fact] public void IncarnationCleanup_OldTombstoneCannotMutateReplacementGuidState() { const uint guid = 0x70000035u; var oldRecord = new LiveEntityRecord(Spawn(guid, 1, 1, 0x01010001u)); var replacement = new LiveEntityRecord(Spawn(guid, 2, 1, 0x01010001u)); LiveEntityRecord? current = replacement; var cleanup = new LiveEntityIncarnationCleanup(oldRecord, _ => current); object oldHost = new(); object newHost = new(); var hosts = new Dictionary { [guid] = newHost }; bool guidStateCleared = false; var plan = new LiveEntityTeardownPlan( [ () => cleanup.RunIfNoReplacement(() => guidStateCleared = true), () => cleanup.RemoveCaptured(hosts, oldHost), ]); plan.Advance(); Assert.False(guidStateCleared); Assert.Same(newHost, hosts[guid]); Assert.True(plan.IsComplete); } [Fact] public void IncarnationCleanup_RemovesOnlyCapturedOwnerAndResumesAfterReplacementEnds() { const uint guid = 0x70000036u; var oldRecord = new LiveEntityRecord(Spawn(guid, 1, 1, 0x01010001u)); var replacement = new LiveEntityRecord(Spawn(guid, 2, 1, 0x01010001u)); LiveEntityRecord? current = replacement; var cleanup = new LiveEntityIncarnationCleanup(oldRecord, _ => current); object oldHost = new(); var hosts = new Dictionary { [guid] = oldHost }; int guidCleanupCount = 0; cleanup.RemoveCaptured(hosts, oldHost); cleanup.RunIfNoReplacement(() => guidCleanupCount++); current = null; cleanup.RunIfNoReplacement(() => guidCleanupCount++); Assert.Empty(hosts); Assert.Equal(1, guidCleanupCount); } private static LoadedLandblock EmptyLandblock(uint canonicalId) => new(canonicalId, new LandBlock(), Array.Empty()); private static void ResolveParent(LiveEntityRuntime runtime, uint childGuid) => runtime.ParentAttachments.Resolve( childGuid, guid => runtime.TryGetSnapshot(guid, out _), guid => runtime.TryGetSnapshot(guid, out WorldSession.EntitySpawn spawn) ? spawn.InstanceSequence : null, update => runtime.TryApplyParent(update, out _)); private static WorldEntity Entity(uint id, uint guid) => new() { Id = id, ServerGuid = guid, SourceGfxObjOrSetupId = 0x02000001u, Position = System.Numerics.Vector3.Zero, Rotation = System.Numerics.Quaternion.Identity, MeshRefs = Array.Empty(), }; private static WorldSession.EntitySpawn Spawn( uint guid, ushort instance, ushort positionSequence, uint cell, PhysicsStateFlags state = PhysicsStateFlags.ReportCollisions) { var position = new CreateObject.ServerPosition( cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps( positionSequence, 1, 1, 1, 0, 1, 0, 1, instance); var physics = new PhysicsSpawnData( RawState: (uint)state, 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, "fixture", null, null, 0x09000001u, PhysicsState: (uint)state, InstanceSequence: instance, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: positionSequence, Physics: physics); } }