Add deterministic 96-owner lifecycle and renderer-resource stress gates plus an exact twelve-cycle recall/portal ownership gate. Prove zero retained records, projectiles, spatial buckets, script queues, particle/light owners, shadows, pending effects, and mesh references after churn, GUID reuse, deletion, and session reset. Make logical teardown incarnation-specific and reentrancy-safe with lifetime epochs, atomic resource registration, generation-aware effect/teleport cleanup, projection mutation tokens, and failure-isolated visibility fan-out. Finish canonical spatial transactions before reporting observer failures and never discard superseded cleanup failures. Synchronize architecture, roadmap, milestones, retail research, divergence bookkeeping, and durable memory. All three independent review tracks are clean; Release build and the full 5,454-pass/5-skip suite are green. Co-Authored-By: Codex <noreply@openai.com>
1757 lines
72 KiB
C#
1757 lines
72 KiB
C#
using AcDream.App.Streaming;
|
|
using AcDream.App.World;
|
|
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 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 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 CallbackResources : ILiveEntityResourceLifecycle
|
|
{
|
|
public Action<WorldEntity>? OnRegister { get; set; }
|
|
public Action<WorldEntity>? OnUnregister { get; set; }
|
|
public int RegisterCount { get; private set; }
|
|
public int UnregisterCount { get; private set; }
|
|
public Dictionary<uint, int> 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<bool>();
|
|
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 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<CreateObject.SubPaletteSwap>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.AnimPartChange>()),
|
|
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<InvalidOperationException>(() => 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 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<AggregateException>(() => runtime.Clear());
|
|
|
|
Assert.Equal(2, resources.UnregisterCount);
|
|
Assert.Equal(0, runtime.Count);
|
|
Assert.Equal(0, runtime.MaterializedCount);
|
|
Assert.Empty(runtime.WorldEntities);
|
|
Assert.Empty(spatial.Entities);
|
|
}
|
|
|
|
[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<ArgumentOutOfRangeException>(() => 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<AggregateException>(() =>
|
|
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<AggregateException>(() =>
|
|
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<AggregateException>(() =>
|
|
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<AggregateException>(() =>
|
|
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<InvalidOperationException>(() =>
|
|
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<InvalidOperationException>(() =>
|
|
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<AggregateException>(() => 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(0, runtime.MaterializedCount);
|
|
Assert.Empty(runtime.WorldEntities);
|
|
Assert.Empty(runtime.MaterializedWorldEntities);
|
|
Assert.Empty(spatial.Entities);
|
|
Assert.Equal(0, spatial.PendingLiveEntityCount);
|
|
Assert.Equal(1, resources.RegisterCount);
|
|
Assert.Equal(1, 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<AggregateException>(() => 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);
|
|
}
|
|
|
|
private static LoadedLandblock EmptyLandblock(uint canonicalId) =>
|
|
new(canonicalId, new LandBlock(), Array.Empty<WorldEntity>());
|
|
|
|
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<MeshRef>(),
|
|
};
|
|
|
|
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<CreateObject.AnimPartChange>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
null,
|
|
null,
|
|
"fixture",
|
|
null,
|
|
null,
|
|
0x09000001u,
|
|
PhysicsState: (uint)state,
|
|
InstanceSequence: instance,
|
|
MovementSequence: 1,
|
|
ServerControlSequence: 1,
|
|
PositionSequence: positionSequence,
|
|
Physics: physics);
|
|
}
|
|
}
|