using System.Numerics; using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.World; using DatReaderWriter.DBObjs; namespace AcDream.App.Tests.Streaming; public sealed class GpuWorldStateVisibilityTests { [Fact] public void PendingSpawnFlood_DoesNotPublishOrRebuildLoadedView() { const uint landblock = 0x0101FFFFu; const int count = 10_000; var state = new GpuWorldState(); int edges = 0; state.LiveProjectionVisibilityChanged += (_, _) => edges++; long commitsBefore = state.VisibilityCommitCount; using (state.BeginMutationBatch()) { for (int i = 0; i < count; i++) { state.PlaceLiveEntityProjection( landblock, Entity((uint)(i + 1), 0x70000000u + (uint)i)); } Assert.Empty(state.Entities); Assert.Equal(count, state.PendingLiveEntityCount); Assert.Equal(commitsBefore, state.VisibilityCommitCount); Assert.Equal(0, edges); } Assert.Empty(state.Entities); Assert.Equal(count, state.PendingLiveEntityCount); Assert.Equal(commitsBefore + 1, state.VisibilityCommitCount); Assert.Equal(0, edges); } [Fact] public void LoadedSpawnFlood_AppendsInPlaceAndPublishesOneBatch() { const uint landblock = 0x0101FFFFu; const int count = 10_000; var state = new GpuWorldState(); state.AddLandblock(new LoadedLandblock( landblock, new LandBlock(), Array.Empty())); Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? loaded)); IReadOnlyList residentBucket = loaded!.Entities; int edges = 0; state.LiveProjectionVisibilityChanged += (_, visible) => { Assert.True(visible); edges++; }; long commitsBefore = state.VisibilityCommitCount; using (state.BeginMutationBatch()) { for (int i = 0; i < count; i++) { state.PlaceLiveEntityProjection( landblock, Entity((uint)(i + 1), 0x71000000u + (uint)i)); } Assert.Same(residentBucket, loaded.Entities); Assert.Equal(count, residentBucket.Count); Assert.Equal(count, state.Entities.Count); Assert.Equal(0, edges); Assert.Equal(commitsBefore, state.VisibilityCommitCount); } Assert.Equal(count, edges); Assert.Equal(commitsBefore + 1, state.VisibilityCommitCount); Assert.Equal(count, state.Entities.Count); } [Fact] public void DenseSameBucketRebucketAndDelete_PreserveEveryProjectionIndex() { const uint sourceLandblock = 0x0101FFFFu; const uint targetLandblock = 0x0202FFFFu; const int count = 10_000; var state = new GpuWorldState(); state.AddLandblock(new LoadedLandblock( sourceLandblock, new LandBlock(), Array.Empty())); state.AddLandblock(new LoadedLandblock( targetLandblock, new LandBlock(), Array.Empty())); var entities = new WorldEntity[count]; using (state.BeginMutationBatch()) { for (int i = 0; i < count; i++) { entities[i] = Entity((uint)(i + 1), 0x72000000u + (uint)i); state.PlaceLiveEntityProjection(sourceLandblock, entities[i]); } } using (state.BeginMutationBatch()) { for (int i = 0; i < count; i++) state.RebucketLiveEntity(entities[i], targetLandblock); } Assert.True(state.TryGetLandblock(sourceLandblock, out LoadedLandblock? source)); Assert.True(state.TryGetLandblock(targetLandblock, out LoadedLandblock? target)); Assert.Empty(source!.Entities); Assert.Equal(count, target!.Entities.Count); Assert.Equal(count, state.Entities.Count); using (state.BeginMutationBatch()) { for (int i = 0; i < count; i++) state.RemoveLiveEntityProjection(entities[i]); } Assert.Empty(target.Entities); Assert.Empty(state.Entities); } [Fact] public void DenseDemotion_CompactsLiveEntriesAndKeepsTheirIndexesValid() { const uint sourceLandblock = 0x0101FFFFu; const uint targetLandblock = 0x0202FFFFu; const int liveCount = 2_000; var state = new GpuWorldState(); var mixed = new List(liveCount * 2); var live = new List(liveCount); for (int i = 0; i < liveCount; i++) { mixed.Add(Entity(0xC1000000u + (uint)i, 0u)); WorldEntity projection = Entity((uint)(i + 1), 0x73000000u + (uint)i); mixed.Add(projection); live.Add(projection); } state.AddLandblock(new LoadedLandblock( sourceLandblock, new LandBlock(), mixed)); state.AddLandblock(new LoadedLandblock( targetLandblock, new LandBlock(), Array.Empty())); state.RemoveEntitiesFromLandblock(sourceLandblock); Assert.True(state.TryGetLandblock(sourceLandblock, out LoadedLandblock? source)); Assert.Equal(liveCount, source!.Entities.Count); Assert.All(source.Entities, entity => Assert.NotEqual(0u, entity.ServerGuid)); using (state.BeginMutationBatch()) { foreach (WorldEntity entity in live) state.RebucketLiveEntity(entity, targetLandblock); } Assert.Empty(source.Entities); Assert.True(state.TryGetLandblock(targetLandblock, out LoadedLandblock? target)); Assert.Equal(liveCount, target!.Entities.Count); } [Fact] public void PendingDemotion_CompactsMixedStaticAndLiveEntriesAndKeepsLiveIndexesValid() { const uint pendingLandblock = 0x0101FFFFu; const uint targetLandblock = 0x0202FFFFu; var state = new GpuWorldState(); WorldEntity first = Entity(1u, 0x73500001u); WorldEntity second = Entity(2u, 0x73500002u); WorldEntity[] mixed = [ Entity(0xC1000001u, 0u), first, Entity(0xC1000002u, 0u), second, ]; Assert.False(state.AddEntitiesToExistingLandblock(pendingLandblock, mixed)); Assert.Equal(4, state.PendingLiveEntityCount); state.RemoveEntitiesFromLandblock(pendingLandblock); Assert.Equal(2, state.PendingLiveEntityCount); state.AddLandblock(new LoadedLandblock( pendingLandblock, new LandBlock(), Array.Empty())); state.AddLandblock(new LoadedLandblock( targetLandblock, new LandBlock(), Array.Empty())); Assert.True(state.TryGetLandblock(pendingLandblock, out LoadedLandblock? pending)); Assert.Equal([first, second], pending!.Entities); state.RebucketLiveEntity(first, targetLandblock); state.RebucketLiveEntity(second, targetLandblock); Assert.Empty(pending.Entities); Assert.True(state.TryGetLandblock(targetLandblock, out LoadedLandblock? target)); Assert.Equal(2, target!.Entities.Count); Assert.Contains(first, target.Entities); Assert.Contains(second, target.Entities); } [Fact] public void UnloadToPendingThenReload_PreservesProjectionIdentityAndVisibilityEdges() { const uint landblock = 0x0101FFFFu; var state = new GpuWorldState(); state.AddLandblock(new LoadedLandblock( landblock, new LandBlock(), Array.Empty())); WorldEntity entity = Entity(1u, 0x73600001u); state.PlaceLiveEntityProjection(landblock, entity); var edges = new List<(uint Guid, bool Visible)>(); state.LiveProjectionVisibilityChanged += (guid, visible) => edges.Add((guid, visible)); state.RemoveLandblock(landblock); Assert.Empty(state.Entities); Assert.Equal(1, state.PendingLiveEntityCount); Assert.False(state.IsLiveEntityVisible(entity.ServerGuid)); state.AddLandblock(new LoadedLandblock( landblock, new LandBlock(), Array.Empty())); Assert.Same(entity, Assert.Single(state.Entities)); Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? reloaded)); Assert.Same(entity, Assert.Single(reloaded!.Entities)); Assert.True(state.IsLiveEntityVisible(entity.ServerGuid)); Assert.Equal( [(entity.ServerGuid, false), (entity.ServerGuid, true)], edges); state.RemoveLiveEntityProjection(entity); Assert.Empty(state.Entities); Assert.Empty(reloaded.Entities); } [Fact] public void BatchedVisibilityObserver_SeesCommittedFlatAndResidentViews() { const uint landblock = 0x0101FFFFu; var state = new GpuWorldState(); state.AddLandblock(new LoadedLandblock( landblock, new LandBlock(), Array.Empty())); WorldEntity entity = Entity(1u, 0x73700001u); int callbacks = 0; state.LiveProjectionVisibilityChanged += (guid, visible) => { callbacks++; Assert.Equal(entity.ServerGuid, guid); Assert.True(visible); Assert.True(state.IsLiveEntityVisible(guid)); Assert.Same(entity, Assert.Single(state.Entities)); Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? loaded)); Assert.Same(entity, Assert.Single(loaded!.Entities)); }; using (state.BeginMutationBatch()) { state.PlaceLiveEntityProjection(landblock, entity); Assert.Equal(0, callbacks); } Assert.Equal(1, callbacks); } [Fact] public void LoadedToLoadedRebucket_EmitsNoVisibilityPulse() { const uint sourceLandblock = 0x0101FFFFu; const uint targetLandblock = 0x0202FFFFu; var state = new GpuWorldState(); state.AddLandblock(new LoadedLandblock( sourceLandblock, new LandBlock(), Array.Empty())); state.AddLandblock(new LoadedLandblock( targetLandblock, new LandBlock(), Array.Empty())); WorldEntity entity = Entity(1u, 0x73800001u); state.PlaceLiveEntityProjection(sourceLandblock, entity); var edges = new List<(uint Guid, bool Visible)>(); state.LiveProjectionVisibilityChanged += (guid, visible) => edges.Add((guid, visible)); state.RebucketLiveEntity(entity, targetLandblock); Assert.Empty(edges); Assert.True(state.IsLiveEntityVisible(entity.ServerGuid)); Assert.True(state.TryGetLandblock(sourceLandblock, out LoadedLandblock? source)); Assert.Empty(source!.Entities); Assert.True(state.TryGetLandblock(targetLandblock, out LoadedLandblock? target)); Assert.Same(entity, Assert.Single(target!.Entities)); Assert.Same(entity, Assert.Single(state.Entities)); } [Fact] public void SameGuidOverlap_ExactRemovalPromotesSecondaryWithoutVisibilityPulse() { const uint landblock = 0x0101FFFFu; const uint guid = 0x73900001u; var state = new GpuWorldState(); state.AddLandblock(new LoadedLandblock( landblock, new LandBlock(), Array.Empty())); WorldEntity first = Entity(1u, guid); WorldEntity second = Entity(2u, guid); state.PlaceLiveEntityProjection(landblock, first); state.PlaceLiveEntityProjection(landblock, second); var edges = new List<(uint Guid, bool Visible)>(); state.LiveProjectionVisibilityChanged += (edgeGuid, visible) => edges.Add((edgeGuid, visible)); state.RemoveLiveEntityProjection(first); Assert.Empty(edges); Assert.True(state.IsLiveEntityVisible(guid)); Assert.Same(second, Assert.Single(state.Entities)); state.RemoveLiveEntityProjection(guid); Assert.Equal([(guid, false)], edges); Assert.False(state.IsLiveEntityVisible(guid)); Assert.Empty(state.Entities); } [Fact] public void DuplicateDirectPlacement_IsRejectedBeforeAnyBucketMutation() { const uint landblock = 0x0101FFFFu; var state = new GpuWorldState(); state.AddLandblock(new LoadedLandblock( landblock, new LandBlock(), Array.Empty())); WorldEntity entity = Entity(1u, 0x74000001u); state.PlaceLiveEntityProjection(landblock, entity); Assert.Throws(() => state.PlaceLiveEntityProjection(landblock, entity)); Assert.Same(entity, Assert.Single(state.Entities)); Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? loaded)); Assert.Same(entity, Assert.Single(loaded!.Entities)); } [Fact] public void ThrowingObserver_DoesNotStrandLaterSubscribersOrQueuedOwnerEdges() { const uint landblock = 0x0101FFFFu; WorldEntity first = Entity(1u, 0x70000001u); WorldEntity second = Entity(2u, 0x70000002u); var state = new GpuWorldState(); state.PlaceLiveEntityProjection(landblock, first); state.PlaceLiveEntityProjection(landblock, second); var observed = new List<(uint Guid, bool Visible)>(); state.LiveProjectionVisibilityChanged += (_, _) => throw new InvalidOperationException("fixture observer failure"); state.LiveProjectionVisibilityChanged += (guid, visible) => observed.Add((guid, visible)); AggregateException error = Assert.Throws(() => state.AddLandblock(new LoadedLandblock( landblock, new LandBlock(), Array.Empty()))); Assert.Equal(2, error.InnerExceptions.Count); Assert.Equal( [(first.ServerGuid, true), (second.ServerGuid, true)], observed.OrderBy(edge => edge.Guid).ToArray()); Assert.True(state.IsLiveEntityVisible(first.ServerGuid)); Assert.True(state.IsLiveEntityVisible(second.ServerGuid)); Assert.Equal(0, state.PendingVisibilityTransitionCount); Assert.Equal(2, state.Entities.Count); } [Fact] public void ThrowingRuntimeObserver_DoesNotSkipOtherOwnersOrPresentationSubscribers() { const uint landblock = 0x0101FFFFu; const uint cell = 0x01010001u; var state = new GpuWorldState(); var runtime = new LiveEntityRuntime( state, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); WorldSession.EntitySpawn first = Spawn(0x70000011u, cell); WorldSession.EntitySpawn second = Spawn(0x70000012u, cell); runtime.RegisterLiveEntity(first); runtime.RegisterLiveEntity(second); runtime.MaterializeLiveEntity(first.Guid, cell, id => Entity(id, first.Guid)); runtime.MaterializeLiveEntity(second.Guid, cell, id => Entity(id, second.Guid)); var observed = new List<(uint Guid, bool Visible)>(); runtime.ProjectionVisibilityChanged += (_, _) => throw new InvalidOperationException("fixture presentation failure"); runtime.ProjectionVisibilityChanged += (record, visible) => observed.Add((record.ServerGuid, visible)); AggregateException error = Assert.Throws(() => state.AddLandblock(new LoadedLandblock( landblock, new LandBlock(), Array.Empty()))); Assert.Equal(2, error.Flatten().InnerExceptions.Count); Assert.Equal( [(first.Guid, true), (second.Guid, true)], observed.OrderBy(edge => edge.Guid).ToArray()); Assert.True(runtime.TryGetRecord(first.Guid, out LiveEntityRecord firstRecord)); Assert.True(runtime.TryGetRecord(second.Guid, out LiveEntityRecord secondRecord)); Assert.True(firstRecord.IsSpatiallyVisible); Assert.True(secondRecord.IsSpatiallyVisible); Assert.Equal(0, state.PendingVisibilityTransitionCount); } [Fact] public void CopyLiveEntitiesNearLandblock_UsesBoundedNeighborhoodAndSkipsStatics() { var centerLive = Entity(1, 0x70000001u); var neighborLive = Entity(2, 0x70000002u); var farLive = Entity(3, 0x70000003u); WorldEntity[] staticEntities = Enumerable.Range(0, 20_000) .Select(index => Entity((uint)(index + 10), 0u)) .ToArray(); var state = new GpuWorldState(); state.AddLandblock(new LoadedLandblock( 0x1010FFFFu, new LandBlock(), staticEntities.Append(centerLive).ToArray())); state.AddLandblock(new LoadedLandblock( 0x1110FFFFu, new LandBlock(), new[] { neighborLive })); state.AddLandblock(new LoadedLandblock( 0x1310FFFFu, new LandBlock(), new[] { farLive })); var destination = new List> { new(0xDEADBEEFu, farLive), }; state.CopyLiveEntitiesNearLandblock(0x10100001u, 1, destination); Assert.Equal(2, destination.Count); Assert.Contains(destination, pair => pair.Key == centerLive.ServerGuid); Assert.Contains(destination, pair => pair.Key == neighborLive.ServerGuid); Assert.DoesNotContain(destination, pair => pair.Key == farLive.ServerGuid); Assert.DoesNotContain(destination, pair => pair.Key == 0u); } private static WorldEntity Entity(uint id, uint guid) => new() { Id = id, ServerGuid = guid, SourceGfxObjOrSetupId = 0x02000001u, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), }; private static WorldSession.EntitySpawn Spawn(uint guid, uint cell) { var position = new CreateObject.ServerPosition( cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1); var physics = new PhysicsSpawnData( RawState: (uint)PhysicsStateFlags.ReportCollisions, Position: position, Movement: null, AnimationFrame: null, SetupTableId: 0x02000001u, MotionTableId: null, 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(), BasePaletteId: null, ObjScale: null, Name: "visibility fixture", ItemType: null, MotionState: null, MotionTableId: null, PhysicsState: (uint)PhysicsStateFlags.ReportCollisions, InstanceSequence: 1, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } }