harden(runtime): close canonical entity object lifetime
Retain exact teardown receipts before terminal callbacks, preserve ordered re-entrant entity/object publication, publish committed facts across projection failures, and make direct disposal converge every canonical owner. Add a complete ownership ledger plus adversarial direct/graphical parity, callback, GUID reuse, reset, and resource-churn gates. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
84954c8b77
commit
119b7c1151
9 changed files with 992 additions and 95 deletions
|
|
@ -195,6 +195,32 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
spawn.Guid,
|
||||
Harness.PlayerGuid,
|
||||
newSlot: 3);
|
||||
var objDesc = new ObjDescEvent.Parsed(
|
||||
spawn.Guid,
|
||||
new CreateObject.ModelData(
|
||||
0x04000001u,
|
||||
Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||
Array.Empty<CreateObject.TextureChange>(),
|
||||
Array.Empty<CreateObject.AnimPartChange>()),
|
||||
spawn.InstanceSequence,
|
||||
ObjDescSequence: 2);
|
||||
Assert.True(direct.TryApplyObjDesc(
|
||||
objDesc,
|
||||
acknowledgeProjection: null,
|
||||
out _));
|
||||
var motion = new WorldSession.EntityMotionUpdate(
|
||||
spawn.Guid,
|
||||
new CreateObject.ServerMotionState(0x3D, 0x11),
|
||||
spawn.InstanceSequence,
|
||||
MovementSequence: 2,
|
||||
ServerControlSequence: 2,
|
||||
IsAutonomous: false);
|
||||
Assert.True(direct.TryApplyMotion(
|
||||
motion,
|
||||
retainPayload: true,
|
||||
acknowledgeProjection: null,
|
||||
out _,
|
||||
out _));
|
||||
var vector = new VectorUpdate.Parsed(
|
||||
spawn.Guid,
|
||||
new Vector3(1f, 2f, 3f),
|
||||
|
|
@ -216,6 +242,12 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
acknowledgeProjection: null,
|
||||
out _,
|
||||
out _));
|
||||
Assert.True(direct.CommitChildNoDraw(
|
||||
directCanonical,
|
||||
noDraw: true));
|
||||
Assert.True(direct.CommitChildNoDraw(
|
||||
directCanonical,
|
||||
noDraw: false));
|
||||
WorldSession.EntityPositionUpdate position =
|
||||
Position(spawn.Guid, spawn.InstanceSequence);
|
||||
Assert.True(direct.TryApplyPosition(
|
||||
|
|
@ -228,6 +260,10 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
out _,
|
||||
out _,
|
||||
out _));
|
||||
Assert.True(direct.CommitRebucket(
|
||||
directCanonical,
|
||||
0x12360001u,
|
||||
0x1236FFFFu));
|
||||
Assert.True(direct.TryApplyPickup(
|
||||
new PickupEvent.Parsed(
|
||||
spawn.Guid,
|
||||
|
|
@ -253,8 +289,22 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
spawn.Guid,
|
||||
Harness.PlayerGuid,
|
||||
newSlot: 3);
|
||||
Assert.True(graphical.Entities.TryApplyObjDesc(
|
||||
objDesc,
|
||||
out _));
|
||||
Assert.True(graphical.Entities.TryApplyMotion(
|
||||
motion,
|
||||
retainPayload: true,
|
||||
out _,
|
||||
out _));
|
||||
Assert.True(graphical.Entities.TryApplyVector(vector, out _));
|
||||
Assert.True(graphical.Entities.TryApplyState(state, out _, out _));
|
||||
Assert.True(graphical.Entities.SetAttachedChildNoDraw(
|
||||
spawn.Guid,
|
||||
noDraw: true));
|
||||
Assert.True(graphical.Entities.SetAttachedChildNoDraw(
|
||||
spawn.Guid,
|
||||
noDraw: false));
|
||||
Assert.True(graphical.Entities.TryApplyPosition(
|
||||
position,
|
||||
isLocalPlayer: false,
|
||||
|
|
@ -263,6 +313,9 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
out _,
|
||||
out _,
|
||||
out _));
|
||||
Assert.True(graphical.Entities.RebucketLiveEntity(
|
||||
spawn.Guid,
|
||||
0x12360001u));
|
||||
Assert.True(graphical.Entities.TryApplyPickup(
|
||||
new PickupEvent.Parsed(
|
||||
spawn.Guid,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using AcDream.Core.Net.Messages;
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Vfx;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
|
|
@ -244,6 +245,7 @@ public sealed class LiveEntityLifecycleStressTests
|
|||
fixture.Effects.ClearNetworkState();
|
||||
Assert.Equal(0, fixture.Runtime.Count);
|
||||
Assert.Equal(0, fixture.Effects.PendingPacketCount);
|
||||
AssertOwnershipLedgerConverged(fixture);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -299,6 +301,98 @@ public sealed class LiveEntityLifecycleStressTests
|
|||
Assert.Equal(0, fixture.Spatial.PendingBucketCount);
|
||||
}
|
||||
|
||||
private static void AssertOwnershipLedgerConverged(Fixture fixture)
|
||||
{
|
||||
RuntimeEntityObjectOwnershipSnapshot canonical =
|
||||
fixture.EntityObjects.CaptureOwnership();
|
||||
var ledger = new J3OwnershipLedger(
|
||||
canonical,
|
||||
fixture.Runtime.MaterializedCount,
|
||||
fixture.Runtime.VisibleRecords.Count,
|
||||
fixture.Runtime.AnimationRuntimeCount,
|
||||
fixture.Runtime.SpatialAnimationRuntimeCount,
|
||||
fixture.Runtime.SpatialRemoteMotionRuntimeCount,
|
||||
fixture.Runtime.SpatialProjectileRuntimeCount,
|
||||
fixture.Runtime.SpatialRootObjectCount,
|
||||
fixture.RenderOwners.Count,
|
||||
fixture.RenderRegisterCount - fixture.RenderUnregisterCount,
|
||||
fixture.Projectiles.Count,
|
||||
fixture.Effects.ReadyOwnerCount,
|
||||
fixture.Effects.PendingPacketCount,
|
||||
fixture.Runner.ActiveOwnerCount,
|
||||
fixture.Runner.ActiveScriptCount,
|
||||
fixture.Runner.ScheduledCallPesCount,
|
||||
fixture.Runner.OwnerAnchorCount,
|
||||
fixture.Particles.ActiveEmitterCount,
|
||||
fixture.Particles.ActiveParticleCount,
|
||||
fixture.ParticleSink.ActiveBindingCount,
|
||||
fixture.ParticleSink.LogicalEmitterCount,
|
||||
fixture.ParticleSink.TrackedOwnerCount,
|
||||
fixture.ParticleSink.RenderPassOwnerCount,
|
||||
fixture.ParticleSink.HiddenPresentationOwnerCount,
|
||||
fixture.Poses.Count,
|
||||
fixture.Lights.RegisteredCount,
|
||||
fixture.LiveLights.TrackedOwnerCount,
|
||||
fixture.LiveLights.PresentedOwnerCount,
|
||||
fixture.Lighting.OwnedLightOwnerCount,
|
||||
fixture.Lighting.PoseTrackedOwnerCount,
|
||||
fixture.Lighting.RetainedOwnerStateCount,
|
||||
fixture.Engine.ShadowObjects.TotalRegistered,
|
||||
fixture.Engine.ShadowObjects.RetainedRegistrationCount,
|
||||
fixture.Engine.ShadowObjects.SuspendedRegistrationCount,
|
||||
fixture.Spatial.PendingLiveEntityCount,
|
||||
fixture.Spatial.PendingBucketCount,
|
||||
fixture.Spatial.PendingRescueCount,
|
||||
fixture.Spatial.PersistentGuidCount,
|
||||
fixture.Spatial.PendingVisibilityTransitionCount,
|
||||
fixture.Spatial.Entities.Count(entity =>
|
||||
entity.ServerGuid != 0u));
|
||||
|
||||
Assert.Equal(default(J3OwnershipLedger), ledger);
|
||||
}
|
||||
|
||||
private readonly record struct J3OwnershipLedger(
|
||||
RuntimeEntityObjectOwnershipSnapshot Canonical,
|
||||
int AppMaterializedCount,
|
||||
int AppVisibleCount,
|
||||
int AnimationRuntimeCount,
|
||||
int SpatialAnimationRuntimeCount,
|
||||
int SpatialRemoteMotionRuntimeCount,
|
||||
int SpatialProjectileRuntimeCount,
|
||||
int SpatialRootObjectCount,
|
||||
int RenderOwnerCount,
|
||||
int RenderRegistrationDelta,
|
||||
int ProjectileCount,
|
||||
int EffectReadyOwnerCount,
|
||||
int EffectPendingPacketCount,
|
||||
int ScriptOwnerCount,
|
||||
int ActiveScriptCount,
|
||||
int ScheduledCallPesCount,
|
||||
int ScriptAnchorCount,
|
||||
int EmitterCount,
|
||||
int ParticleCount,
|
||||
int ParticleBindingCount,
|
||||
int LogicalEmitterCount,
|
||||
int ParticleTrackedOwnerCount,
|
||||
int ParticleRenderPassOwnerCount,
|
||||
int HiddenParticlePresentationCount,
|
||||
int PoseCount,
|
||||
int RegisteredLightCount,
|
||||
int TrackedLiveLightCount,
|
||||
int PresentedLiveLightCount,
|
||||
int OwnedLightCount,
|
||||
int LightPoseCount,
|
||||
int RetainedLightStateCount,
|
||||
int ShadowCount,
|
||||
int RetainedShadowCount,
|
||||
int SuspendedShadowCount,
|
||||
int PendingSpatialEntityCount,
|
||||
int PendingBucketCount,
|
||||
int PendingRescueCount,
|
||||
int PersistentGuidCount,
|
||||
int PendingVisibilityCount,
|
||||
int MaterializedWorldEntityCount);
|
||||
|
||||
private sealed class Fixture : IDisposable
|
||||
{
|
||||
private readonly Dictionary<uint, DatPhysicsScript> _scripts = new();
|
||||
|
|
@ -328,7 +422,8 @@ public sealed class LiveEntityLifecycleStressTests
|
|||
randomUnit: () => 0.5,
|
||||
canAdvanceOwner: ownerId => _effects?.CanAdvanceOwner(ownerId) ?? true);
|
||||
|
||||
Runtime = LiveEntityRuntimeFixture.Create(
|
||||
EntityObjects = new RuntimeEntityObjectLifetime();
|
||||
Runtime = new LiveEntityRuntime(
|
||||
Spatial,
|
||||
new DelegateLiveEntityResourceLifecycle(
|
||||
entity =>
|
||||
|
|
@ -358,7 +453,8 @@ public sealed class LiveEntityLifecycleStressTests
|
|||
cleanups.Add(() => _liveLights?.Forget(record));
|
||||
}
|
||||
LiveEntityTeardown.Run(cleanups);
|
||||
});
|
||||
},
|
||||
EntityObjects);
|
||||
|
||||
Effects = new EntityEffectController(
|
||||
Runtime,
|
||||
|
|
@ -386,6 +482,7 @@ public sealed class LiveEntityLifecycleStressTests
|
|||
internal LightManager Lights { get; } = new();
|
||||
internal LightingHookSink Lighting { get; }
|
||||
internal PhysicsScriptRunner Runner { get; }
|
||||
internal RuntimeEntityObjectLifetime EntityObjects { get; }
|
||||
internal LiveEntityRuntime Runtime { get; }
|
||||
internal EntityEffectController Effects { get; }
|
||||
internal ProjectileController Projectiles { get; }
|
||||
|
|
|
|||
|
|
@ -393,6 +393,124 @@ public sealed class RuntimeEntityObjectLifetimeTests
|
|||
Assert.Equal(1, lifetime.Events.SubscriberCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReentrantCrossDomainCommit_PreservesSequenceForEveryObserver()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
lifetime.BindEventContext(static () => new(6), static () => 12UL);
|
||||
bool nested = false;
|
||||
var mutating = new CallbackObserver(
|
||||
onEntity: _ =>
|
||||
{
|
||||
if (nested)
|
||||
return;
|
||||
nested = true;
|
||||
lifetime.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = 0x71000001u,
|
||||
Name = "nested",
|
||||
});
|
||||
});
|
||||
var recording = new RecordingObserver();
|
||||
using IDisposable first = lifetime.Events.Subscribe(mutating);
|
||||
using IDisposable second = lifetime.Events.Subscribe(recording);
|
||||
|
||||
_ = lifetime.RegisterEntity(
|
||||
Spawn(0x7000001Au, 1, "outer"));
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
(RuntimeTraceKind.Entity, 1UL),
|
||||
(RuntimeTraceKind.Inventory, 2UL),
|
||||
],
|
||||
recording.Order);
|
||||
Assert.Equal(0, lifetime.Events.PendingDispatchCount);
|
||||
Assert.False(lifetime.Events.IsDispatching);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectTransactions_PublishCommittedBorrowedViewInExactOrder()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
lifetime.BindEventContext(static () => new(7), static () => 13UL);
|
||||
const uint itemId = 0x71000002u;
|
||||
var observedViews =
|
||||
new List<RuntimeInventoryItemSnapshot>();
|
||||
var observer = new CallbackObserver(
|
||||
onInventory: delta =>
|
||||
{
|
||||
if (delta.Change
|
||||
is RuntimeInventoryChange.Removed
|
||||
or RuntimeInventoryChange.Cleared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.True(lifetime.InventoryView.TryGet(
|
||||
delta.Item.ObjectId,
|
||||
out RuntimeInventoryItemSnapshot current));
|
||||
observedViews.Add(current);
|
||||
});
|
||||
var recording = new RecordingObserver();
|
||||
using IDisposable first = lifetime.Events.Subscribe(observer);
|
||||
using IDisposable second = lifetime.Events.Subscribe(recording);
|
||||
|
||||
lifetime.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = itemId,
|
||||
Name = "transaction",
|
||||
ContainerId = 0x50000001u,
|
||||
ContainerSlot = 2,
|
||||
StackSize = 3,
|
||||
Value = 30,
|
||||
});
|
||||
Assert.True(lifetime.Objects.MoveItemOptimistic(
|
||||
itemId,
|
||||
0x50000002u,
|
||||
newSlot: 0));
|
||||
Assert.True(lifetime.Objects.RollbackMove(itemId));
|
||||
Assert.True(lifetime.Objects.UpdateStackSize(
|
||||
itemId,
|
||||
stackSize: 2,
|
||||
value: 20));
|
||||
Assert.True(lifetime.Objects.RemoveLogicalGeneration(
|
||||
itemId,
|
||||
generation: 9));
|
||||
lifetime.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = itemId,
|
||||
Name = "replacement",
|
||||
});
|
||||
lifetime.ClearObjects();
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
RuntimeInventoryChange.Added,
|
||||
RuntimeInventoryChange.Moved,
|
||||
RuntimeInventoryChange.Moved,
|
||||
RuntimeInventoryChange.Updated,
|
||||
RuntimeInventoryChange.Removed,
|
||||
RuntimeInventoryChange.Added,
|
||||
RuntimeInventoryChange.Cleared,
|
||||
],
|
||||
recording.Inventory
|
||||
.Select(delta => delta.Change)
|
||||
.ToArray());
|
||||
Assert.Equal(
|
||||
Enumerable.Range(1, 7).Select(value => (ulong)value),
|
||||
recording.Inventory.Select(delta => delta.Stamp.Sequence));
|
||||
Assert.Equal(
|
||||
(ushort)9,
|
||||
recording.Inventory[4].Item.Incarnation);
|
||||
Assert.Equal(5, observedViews.Count);
|
||||
Assert.Equal(0x50000002u, observedViews[1].ContainerId);
|
||||
Assert.Equal(0x50000001u, observedViews[2].ContainerId);
|
||||
Assert.Equal(2, observedViews[3].StackSize);
|
||||
Assert.Equal("replacement", observedViews[4].Name);
|
||||
Assert.Equal(0, lifetime.InventoryView.ObjectCount);
|
||||
Assert.Equal(0, lifetime.Events.PendingDispatchCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DirectDelete_PublishesTerminalEntityBeforeInventoryRemoval()
|
||||
{
|
||||
|
|
@ -530,6 +648,97 @@ public sealed class RuntimeEntityObjectLifetimeTests
|
|||
Assert.Equal(0, lifetime.Entities.PendingTeardownCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerationReplacement_RetainsExactReceiptBeforeTerminalCallback()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
const uint guid = 0x70000030u;
|
||||
RuntimeEntityRecord prior =
|
||||
lifetime.RegisterEntity(Spawn(guid, 1, "prior")).Canonical!;
|
||||
uint priorLocalId = prior.LocalEntityId!.Value;
|
||||
bool observedRetainedReceipt = false;
|
||||
var observer = new CallbackObserver(
|
||||
onEntity: delta =>
|
||||
{
|
||||
if (delta.Change is not RuntimeEntityChange.Deleted)
|
||||
return;
|
||||
observedRetainedReceipt = lifetime.Entities.TryGetTeardown(
|
||||
guid,
|
||||
1,
|
||||
out RuntimeEntityRecord retained)
|
||||
&& ReferenceEquals(prior, retained)
|
||||
&& lifetime.Entities.TryGetByLocalId(
|
||||
priorLocalId,
|
||||
out RuntimeEntityRecord identity)
|
||||
&& ReferenceEquals(prior, identity);
|
||||
});
|
||||
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||
|
||||
RuntimeEntityRegistrationResult replacement =
|
||||
lifetime.RegisterEntity(Spawn(guid, 2, "replacement"));
|
||||
|
||||
Assert.True(observedRetainedReceipt);
|
||||
Assert.Equal((ushort)2, replacement.Canonical!.Incarnation);
|
||||
Assert.Equal(0, lifetime.Entities.PendingTeardownCount);
|
||||
Assert.Equal(1, lifetime.Entities.ClaimedLocalIdCount);
|
||||
Assert.False(lifetime.Entities.TryGetByLocalId(
|
||||
priorLocalId,
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteAndSessionClear_ExposeExactReceiptsUntilAcknowledged()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
const uint deletedGuid = 0x70000031u;
|
||||
const uint clearedGuid = 0x70000032u;
|
||||
RuntimeEntityRecord deleted = lifetime.RegisterEntity(
|
||||
Spawn(deletedGuid, 1, "deleted")).Canonical!;
|
||||
RuntimeEntityRecord cleared = lifetime.RegisterEntity(
|
||||
Spawn(clearedGuid, 1, "cleared")).Canonical!;
|
||||
var retainedDuringCallbacks = new HashSet<uint>();
|
||||
var observer = new CallbackObserver(
|
||||
onEntity: delta =>
|
||||
{
|
||||
if (delta.Change
|
||||
is RuntimeEntityChange.Deleted
|
||||
or RuntimeEntityChange.Withdrawn
|
||||
&& lifetime.Entities.TryGetTeardown(
|
||||
delta.Entity.Identity.ServerGuid,
|
||||
delta.Entity.Identity.Incarnation,
|
||||
out _))
|
||||
{
|
||||
retainedDuringCallbacks.Add(
|
||||
delta.Entity.Identity.ServerGuid);
|
||||
}
|
||||
});
|
||||
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||
|
||||
Assert.True(lifetime.TryAcceptDelete(
|
||||
new DeleteObject.Parsed(deletedGuid, 1),
|
||||
isLocalPlayer: false,
|
||||
removeRetainedObject: false,
|
||||
out RuntimeEntityDeleteAcceptance acceptance));
|
||||
Assert.Contains(deletedGuid, retainedDuringCallbacks);
|
||||
Assert.Equal(1, lifetime.Entities.PendingTeardownCount);
|
||||
lifetime.CompleteAcceptedDelete(acceptance);
|
||||
Assert.Null(lifetime.RetireCanonicalOnly(deleted));
|
||||
|
||||
IReadOnlyList<RuntimeEntityRecord> retired =
|
||||
lifetime.BeginSessionClear();
|
||||
Assert.Same(cleared, Assert.Single(retired));
|
||||
Assert.Contains(clearedGuid, retainedDuringCallbacks);
|
||||
Assert.Equal(1, lifetime.Entities.PendingTeardownCount);
|
||||
Assert.Null(lifetime.RetireCanonicalOnly(cleared));
|
||||
Assert.True(lifetime.CompleteSessionClearIfConverged());
|
||||
Assert.Equal(
|
||||
default(RuntimeEntityObjectOwnershipSnapshot) with
|
||||
{
|
||||
StreamSubscriberCount = 1,
|
||||
},
|
||||
lifetime.CaptureOwnership());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterEntity_ReentrantNewerGenerationSupersedesOuterCreate()
|
||||
{
|
||||
|
|
@ -685,6 +894,274 @@ public sealed class RuntimeEntityObjectLifetimeTests
|
|||
Assert.False(lifetime.Entities.TryGetActive(guid, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcceptedVector_ProjectionFailureStillPublishesCommittedFact()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
lifetime.BindEventContext(static () => new(21), static () => 90UL);
|
||||
var observer = new RecordingObserver();
|
||||
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||
const uint guid = 0x70000024u;
|
||||
RuntimeEntityRecord canonical =
|
||||
lifetime.RegisterEntity(Spawn(guid, 1, "vector")).Canonical!;
|
||||
var velocity = new System.Numerics.Vector3(3f, 4f, 5f);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
lifetime.TryApplyVector(
|
||||
new VectorUpdate.Parsed(
|
||||
guid,
|
||||
velocity,
|
||||
System.Numerics.Vector3.Zero,
|
||||
InstanceSequence: 1,
|
||||
VectorSequence: 2),
|
||||
_ => throw new InvalidOperationException(
|
||||
"projection failed after canonical commit"),
|
||||
out _));
|
||||
|
||||
Assert.True(lifetime.Entities.IsCurrent(canonical));
|
||||
Assert.Equal(velocity, canonical.Snapshot.Physics!.Value.Velocity);
|
||||
Assert.Equal(
|
||||
[
|
||||
RuntimeEntityChange.Registered,
|
||||
RuntimeEntityChange.Updated,
|
||||
],
|
||||
observer.Entities.Select(delta => delta.Change).ToArray());
|
||||
Assert.Equal([1UL, 2UL], observer.Stamps
|
||||
.Select(stamp => stamp.Sequence)
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReentrantNewerVector_SupersedesOuterCommitPublication()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
lifetime.BindEventContext(static () => new(22), static () => 91UL);
|
||||
var observer = new RecordingObserver();
|
||||
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||
const uint guid = 0x70000025u;
|
||||
_ = lifetime.RegisterEntity(Spawn(guid, 1, "vector"));
|
||||
var newest = new System.Numerics.Vector3(9f, 8f, 7f);
|
||||
|
||||
bool outerRemainedCurrent = lifetime.TryApplyVector(
|
||||
new VectorUpdate.Parsed(
|
||||
guid,
|
||||
new System.Numerics.Vector3(1f, 2f, 3f),
|
||||
System.Numerics.Vector3.Zero,
|
||||
InstanceSequence: 1,
|
||||
VectorSequence: 2),
|
||||
ignoredCanonical => Assert.True(lifetime.TryApplyVector(
|
||||
new VectorUpdate.Parsed(
|
||||
guid,
|
||||
newest,
|
||||
System.Numerics.Vector3.Zero,
|
||||
InstanceSequence: 1,
|
||||
VectorSequence: 3),
|
||||
acknowledgeProjection: null,
|
||||
out _)),
|
||||
out _);
|
||||
|
||||
Assert.False(outerRemainedCurrent);
|
||||
Assert.True(lifetime.Entities.TryGetActive(
|
||||
guid,
|
||||
out RuntimeEntityRecord current));
|
||||
Assert.Equal(newest, current.Snapshot.Physics!.Value.Velocity);
|
||||
Assert.Equal(
|
||||
[
|
||||
RuntimeEntityChange.Registered,
|
||||
RuntimeEntityChange.Updated,
|
||||
],
|
||||
observer.Entities.Select(delta => delta.Change).ToArray());
|
||||
Assert.Equal(2UL, lifetime.Events.LastSequence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReentrantTimestampOnlyMotion_SupersedesOuterPublication()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
lifetime.BindEventContext(static () => new(26), static () => 95UL);
|
||||
var observer = new RecordingObserver();
|
||||
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||
const uint guid = 0x7000002Bu;
|
||||
_ = lifetime.RegisterEntity(Spawn(guid, 1, "motion"));
|
||||
var newestMotion =
|
||||
new CreateObject.ServerMotionState(0x3D, 0x12);
|
||||
|
||||
bool outerRemainedCurrent = lifetime.TryApplyMotion(
|
||||
new WorldSession.EntityMotionUpdate(
|
||||
guid,
|
||||
new CreateObject.ServerMotionState(0x3D, 0x11),
|
||||
InstanceSequence: 1,
|
||||
MovementSequence: 2,
|
||||
ServerControlSequence: 1,
|
||||
IsAutonomous: true),
|
||||
retainPayload: false,
|
||||
ignoredCanonical => Assert.True(lifetime.TryApplyMotion(
|
||||
new WorldSession.EntityMotionUpdate(
|
||||
guid,
|
||||
newestMotion,
|
||||
InstanceSequence: 1,
|
||||
MovementSequence: 3,
|
||||
ServerControlSequence: 1,
|
||||
IsAutonomous: true),
|
||||
retainPayload: false,
|
||||
acknowledgeProjection: null,
|
||||
out _,
|
||||
out _)),
|
||||
out _,
|
||||
out _);
|
||||
|
||||
Assert.False(outerRemainedCurrent);
|
||||
Assert.True(lifetime.Entities.TryGetActive(
|
||||
guid,
|
||||
out RuntimeEntityRecord current));
|
||||
Assert.NotEqual(
|
||||
newestMotion,
|
||||
current.Snapshot.MotionState);
|
||||
Assert.Equal(
|
||||
[
|
||||
RuntimeEntityChange.Registered,
|
||||
RuntimeEntityChange.Updated,
|
||||
],
|
||||
observer.Entities.Select(delta => delta.Change).ToArray());
|
||||
Assert.Equal(2UL, lifetime.Events.LastSequence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rebucket_ProjectionFailureStillPublishesCommittedCell()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
lifetime.BindEventContext(static () => new(23), static () => 92UL);
|
||||
var observer = new RecordingObserver();
|
||||
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||
RuntimeEntityRecord canonical = lifetime.RegisterEntity(
|
||||
Spawn(0x70000026u, 1, "rebucket")).Canonical!;
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
lifetime.CommitRebucket(
|
||||
canonical,
|
||||
0x02020022u,
|
||||
0x0202FFFFu,
|
||||
_ => throw new InvalidOperationException(
|
||||
"projection failed after spatial commit")));
|
||||
|
||||
Assert.Equal(0x02020022u, canonical.FullCellId);
|
||||
RuntimeEntityDelta committed = observer.Entities[^1];
|
||||
Assert.Equal(RuntimeEntityChange.Rebucketed, committed.Change);
|
||||
Assert.Equal(0x02020022u, committed.Entity.CellId);
|
||||
Assert.Equal(2UL, committed.Stamp.Sequence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReentrantChildStateMutation_SupersedesOuterPublication()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
lifetime.BindEventContext(static () => new(24), static () => 93UL);
|
||||
var observer = new RecordingObserver();
|
||||
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||
RuntimeEntityRecord canonical = lifetime.RegisterEntity(
|
||||
Spawn(0x70000027u, 1, "child")).Canonical!;
|
||||
|
||||
bool outerRemainedCurrent = lifetime.CommitChildNoDraw(
|
||||
canonical,
|
||||
noDraw: true,
|
||||
_ => Assert.True(lifetime.CommitChildNoDraw(
|
||||
canonical,
|
||||
noDraw: false)));
|
||||
|
||||
Assert.False(outerRemainedCurrent);
|
||||
Assert.Equal(
|
||||
PhysicsStateFlags.None,
|
||||
canonical.FinalPhysicsState & PhysicsStateFlags.NoDraw);
|
||||
Assert.Equal(
|
||||
[
|
||||
RuntimeEntityChange.Registered,
|
||||
RuntimeEntityChange.Updated,
|
||||
],
|
||||
observer.Entities.Select(delta => delta.Change).ToArray());
|
||||
Assert.Equal(2UL, lifetime.Events.LastSequence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectedUpdate_ConsumesNoSharedSequence()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
lifetime.BindEventContext(static () => new(25), static () => 94UL);
|
||||
var observer = new RecordingObserver();
|
||||
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
||||
const uint guid = 0x70000028u;
|
||||
_ = lifetime.RegisterEntity(Spawn(guid, 1, "stale"));
|
||||
|
||||
Assert.False(lifetime.TryApplyVector(
|
||||
new VectorUpdate.Parsed(
|
||||
guid,
|
||||
new System.Numerics.Vector3(1f, 0f, 0f),
|
||||
System.Numerics.Vector3.Zero,
|
||||
InstanceSequence: 1,
|
||||
VectorSequence: 1),
|
||||
acknowledgeProjection: null,
|
||||
out _));
|
||||
|
||||
Assert.Single(observer.Entities);
|
||||
Assert.Equal(1UL, lifetime.Events.LastSequence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisposeWithLiveDirectState_ConvergesCompleteOwnershipLedger()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
const uint guid = 0x70000029u;
|
||||
RuntimeEntityRecord canonical =
|
||||
lifetime.RegisterEntity(Spawn(guid, 1, "dispose")).Canonical!;
|
||||
Assert.True(lifetime.ApplyAcceptedSpawn(
|
||||
canonical,
|
||||
canonical.CreateIntegrationVersion,
|
||||
canonical.Snapshot,
|
||||
replaceGeneration: false));
|
||||
_ = lifetime.Events.Subscribe(new RecordingObserver());
|
||||
|
||||
lifetime.Dispose();
|
||||
lifetime.Dispose();
|
||||
|
||||
Assert.Equal(
|
||||
default(RuntimeEntityObjectOwnershipSnapshot),
|
||||
lifetime.CaptureOwnership());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisposeAfterAcceptedDelete_ConvergesUnfinishedReceiptAndRelations()
|
||||
{
|
||||
var lifetime = new RuntimeEntityObjectLifetime();
|
||||
const uint guid = 0x7000002Au;
|
||||
RuntimeEntityRecord canonical =
|
||||
lifetime.RegisterEntity(Spawn(guid, 1, "pending")).Canonical!;
|
||||
Assert.True(lifetime.ApplyAcceptedSpawn(
|
||||
canonical,
|
||||
canonical.CreateIntegrationVersion,
|
||||
canonical.Snapshot,
|
||||
replaceGeneration: false));
|
||||
lifetime.Entities.ParentAttachments.Enqueue(
|
||||
new ParentEvent.Parsed(
|
||||
ParentGuid: 0x7000F001u,
|
||||
ChildGuid: guid,
|
||||
ParentLocation: 1,
|
||||
PlacementId: 2,
|
||||
ParentInstanceSequence: 4,
|
||||
ChildPositionSequence: 3));
|
||||
Assert.True(lifetime.TryAcceptDelete(
|
||||
new DeleteObject.Parsed(guid, 1),
|
||||
isLocalPlayer: false,
|
||||
removeRetainedObject: true,
|
||||
out _));
|
||||
Assert.Equal(1, lifetime.Entities.PendingTeardownCount);
|
||||
Assert.Equal(1, lifetime.Objects.ObjectCount);
|
||||
|
||||
lifetime.Dispose();
|
||||
|
||||
Assert.Equal(
|
||||
default(RuntimeEntityObjectOwnershipSnapshot),
|
||||
lifetime.CaptureOwnership());
|
||||
}
|
||||
|
||||
private static RuntimeEntityRecord Accept(
|
||||
RuntimeEntityObjectLifetime lifetime,
|
||||
WorldSession.EntitySpawn spawn)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue