acdream/tests/AcDream.App.Tests/World/LiveEntityRuntimeTests.cs
Erik 1e98d81448 feat(vfx): port retail hidden and teleport presentation
Preserve canonical live-object ownership across Hidden transitions and remote teleport placement so effects, collision, streaming, and targeting remain synchronized.
2026-07-14 14:59:48 +02:00

1057 lines
42 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");
}
}
[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 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);
}
}