Detach old-world spatial ownership atomically, prioritize destination retirement dependencies, and reveal the viewport at the retail transition edge. Give private paperdoll views independent mesh ownership and retain dormant ACE entities so portal revisits preserve server objects without extending active GPU lifetimes.
716 lines
27 KiB
C#
716 lines
27 KiB
C#
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<WorldEntity>()));
|
|
Assert.True(state.TryGetLandblock(landblock, out LoadedLandblock? loaded));
|
|
IReadOnlyList<WorldEntity> 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<WorldEntity>()));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
targetLandblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
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<WorldEntity>(liveCount * 2);
|
|
var live = new List<WorldEntity>(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<WorldEntity>()));
|
|
|
|
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<WorldEntity>()));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
targetLandblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
|
|
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>()));
|
|
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<WorldEntity>()));
|
|
|
|
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 OriginRecenterSpatialSwap_RetainsLiveIdentityAndReturnsExactReceipts()
|
|
{
|
|
const uint firstLandblock = 0x1010FFFFu;
|
|
const uint secondLandblock = 0x1011FFFFu;
|
|
const uint pendingLandblock = 0x2020FFFFu;
|
|
const uint playerGuid = 0x50000001u;
|
|
var firstStatic = Entity(100u, 0u);
|
|
var secondStatic = Entity(101u, 0u);
|
|
var remote = Entity(1u, 0x70000001u);
|
|
var pending = Entity(2u, 0x70000002u);
|
|
var player = Entity(3u, playerGuid);
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
firstLandblock,
|
|
new LandBlock(),
|
|
[firstStatic]));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
secondLandblock,
|
|
new LandBlock(),
|
|
[secondStatic]));
|
|
state.PlaceLiveEntityProjection(firstLandblock, remote);
|
|
state.MarkPersistent(playerGuid);
|
|
state.PlaceLiveEntityProjection(secondLandblock, player);
|
|
state.PlaceLiveEntityProjection(pendingLandblock, pending);
|
|
var edges = new List<(uint Guid, bool Visible)>();
|
|
state.LiveProjectionVisibilityChanged += (guid, visible) =>
|
|
edges.Add((guid, visible));
|
|
|
|
GpuWorldRecenterRetirement result =
|
|
state.DetachAllForOriginRecenter();
|
|
|
|
Assert.Null(result.ObserverFailure);
|
|
Assert.Equal(
|
|
[firstLandblock, secondLandblock, pendingLandblock],
|
|
result.Landblocks.Select(retirement => retirement.LandblockId));
|
|
Assert.Contains(
|
|
result.Landblocks.Single(
|
|
retirement => retirement.LandblockId == firstLandblock).Entities,
|
|
entity => ReferenceEquals(entity, firstStatic));
|
|
Assert.Contains(
|
|
result.Landblocks.Single(
|
|
retirement => retirement.LandblockId == firstLandblock).Entities,
|
|
entity => ReferenceEquals(entity, remote));
|
|
Assert.Contains(
|
|
result.Landblocks.Single(
|
|
retirement => retirement.LandblockId == secondLandblock).Entities,
|
|
entity => ReferenceEquals(entity, secondStatic));
|
|
Assert.Contains(
|
|
result.Landblocks.Single(
|
|
retirement => retirement.LandblockId == secondLandblock).Entities,
|
|
entity => ReferenceEquals(entity, player));
|
|
Assert.Same(
|
|
pending,
|
|
Assert.Single(result.Landblocks.Single(
|
|
retirement => retirement.LandblockId == pendingLandblock).Entities));
|
|
|
|
Assert.Empty(state.LoadedLandblockIds);
|
|
Assert.Empty(state.Entities);
|
|
Assert.Equal(2, state.PendingLiveEntityCount);
|
|
Assert.Same(player, Assert.Single(state.DrainRescued()));
|
|
Assert.Equal(
|
|
[(remote.ServerGuid, false), (player.ServerGuid, false)],
|
|
edges);
|
|
|
|
state.AddLandblock(new LoadedLandblock(
|
|
firstLandblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
Assert.Same(remote, Assert.Single(state.Entities));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
pendingLandblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
Assert.Contains(state.Entities, entity => ReferenceEquals(entity, pending));
|
|
}
|
|
|
|
[Fact]
|
|
public void OriginRecenterSpatialSwap_PreservesCurrentLiveBucketOrder()
|
|
{
|
|
const uint landblock = 0x3030FFFFu;
|
|
var staticEntity = Entity(100u, 0u);
|
|
var removed = Entity(1u, 0x70000001u);
|
|
var middle = Entity(2u, 0x70000002u);
|
|
var movedTail = Entity(3u, 0x70000003u);
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
[staticEntity]));
|
|
state.PlaceLiveEntityProjection(landblock, removed);
|
|
state.PlaceLiveEntityProjection(landblock, middle);
|
|
state.PlaceLiveEntityProjection(landblock, movedTail);
|
|
|
|
// Removing the first live projection swap-moves the tail into its
|
|
// bucket slot. Dictionary insertion order is now intentionally
|
|
// different from the canonical bucket order.
|
|
state.RemoveLiveEntityProjection(removed);
|
|
Assert.Equal(
|
|
[staticEntity, movedTail, middle],
|
|
state.Entities);
|
|
|
|
state.DetachAllForOriginRecenter();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
[staticEntity]));
|
|
|
|
Assert.Equal(
|
|
[staticEntity, movedTail, middle],
|
|
state.Entities);
|
|
}
|
|
|
|
[Fact]
|
|
public void BatchedVisibilityObserver_SeesCommittedFlatAndResidentViews()
|
|
{
|
|
const uint landblock = 0x0101FFFFu;
|
|
var state = new GpuWorldState();
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
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<WorldEntity>()));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
targetLandblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
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>()));
|
|
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>()));
|
|
WorldEntity entity = Entity(1u, 0x74000001u);
|
|
state.PlaceLiveEntityProjection(landblock, entity);
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
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<AggregateException>(() =>
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>())));
|
|
|
|
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<AggregateException>(() =>
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>())));
|
|
|
|
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 QuiescedDestinationProjection_AcquiresSpatialPresentationBeforeWorldReveal()
|
|
{
|
|
const uint landblock = 0x0101FFFFu;
|
|
const uint cell = 0x01010001u;
|
|
const uint guid = 0x70000021u;
|
|
var availability = new WorldGenerationAvailabilityState();
|
|
availability.Begin(7);
|
|
var state = new GpuWorldState(availability: availability);
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblock,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var runtime = new LiveEntityRuntime(
|
|
state,
|
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
|
|
WorldSession.EntitySpawn spawn = Spawn(guid, cell);
|
|
runtime.RegisterLiveEntity(spawn);
|
|
var edges = new List<bool>();
|
|
runtime.ProjectionVisibilityChanged += (_, visible) => edges.Add(visible);
|
|
|
|
runtime.MaterializeLiveEntity(
|
|
guid,
|
|
cell,
|
|
id => Entity(id, guid));
|
|
|
|
Assert.False(state.IsLiveEntityVisible(guid));
|
|
Assert.True(state.IsLiveEntityProjectionResident(guid));
|
|
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
|
Assert.True(record.IsSpatiallyVisible);
|
|
Assert.Equal([true], edges);
|
|
|
|
Assert.True(availability.End(7));
|
|
Assert.True(state.IsLiveEntityVisible(guid));
|
|
Assert.True(record.IsSpatiallyVisible);
|
|
}
|
|
|
|
[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<KeyValuePair<uint, WorldEntity>>
|
|
{
|
|
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);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReadinessNeighborhood_CopiesStaticAndLiveWhileWorldIsQuiesced()
|
|
{
|
|
var availability = new WorldGenerationAvailabilityState();
|
|
availability.Begin(7);
|
|
var centerStatic = Entity(1, 0u);
|
|
var centerLive = Entity(2, 0x70000001u);
|
|
var neighborStatic = Entity(3, 0u);
|
|
var farStatic = Entity(4, 0u);
|
|
var state = new GpuWorldState(availability: availability);
|
|
state.AddLandblock(new LoadedLandblock(
|
|
0x1010FFFFu,
|
|
new LandBlock(),
|
|
new[] { centerStatic, centerLive }));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
0x1110FFFFu,
|
|
new LandBlock(),
|
|
new[] { neighborStatic }));
|
|
state.AddLandblock(new LoadedLandblock(
|
|
0x1310FFFFu,
|
|
new LandBlock(),
|
|
new[] { farStatic }));
|
|
var destination = new List<WorldEntity> { farStatic };
|
|
|
|
state.CopyPublishedEntitiesNearLandblockForReadiness(
|
|
0x10100001u,
|
|
1,
|
|
destination);
|
|
|
|
Assert.Equal(3, destination.Count);
|
|
Assert.Contains(centerStatic, destination);
|
|
Assert.Contains(centerLive, destination);
|
|
Assert.Contains(neighborStatic, destination);
|
|
Assert.DoesNotContain(farStatic, destination);
|
|
}
|
|
|
|
private static WorldEntity Entity(uint id, uint guid) => new()
|
|
{
|
|
Id = id,
|
|
ServerGuid = guid,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
};
|
|
|
|
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<CreateObject.AnimPartChange>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
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);
|
|
}
|
|
}
|