refactor(world): extract live entity teardown
This commit is contained in:
parent
4427cfb2c7
commit
f38822c490
8 changed files with 565 additions and 159 deletions
|
|
@ -26,6 +26,10 @@ public sealed class GameWindowLiveEntityCompositionTests
|
|||
[InlineData("OnLiveEntityPickedUp")]
|
||||
[InlineData("OnLiveParentUpdated")]
|
||||
[InlineData("TryAcceptParentForRender")]
|
||||
[InlineData("OnLiveEntityDeleted")]
|
||||
[InlineData("OnLiveEntityPruned")]
|
||||
[InlineData("TearDownLiveEntityRuntimeComponents")]
|
||||
[InlineData("CreateLiveEntityRuntimeTeardownPlan")]
|
||||
public void GameWindow_DoesNotReacquireExtractedLiveEntityBodies(string methodName)
|
||||
{
|
||||
Assert.Null(typeof(GameWindow).GetMethod(methodName, PrivateImplementation));
|
||||
|
|
@ -47,6 +51,7 @@ public sealed class GameWindowLiveEntityCompositionTests
|
|||
[InlineData(typeof(LiveEntityAppearanceBinding))]
|
||||
[InlineData(typeof(LiveEntityHydrationController))]
|
||||
[InlineData(typeof(DatLiveEntityProjectionMaterializer))]
|
||||
[InlineData(typeof(LiveEntityRuntimeTeardownController))]
|
||||
public void ExtractedHelpers_DoNotOwnGuidIndexesOrBackendState(Type helperType)
|
||||
{
|
||||
foreach (FieldInfo field in helperType.GetFields(
|
||||
|
|
|
|||
|
|
@ -550,6 +550,152 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Assert.Equal(0, fixture.Runtime.PendingTeardownCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownDelete_ForgetsOnlyPendingOwnerState()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
|
||||
Assert.False(fixture.Controller.OnDelete(
|
||||
new DeleteObject.Parsed(Guid, InstanceSequence: 1)));
|
||||
|
||||
Assert.Equal([Guid], fixture.Teardown.ForgottenUnknownOwners);
|
||||
Assert.Equal(0, fixture.Teardown.TearDownCount);
|
||||
Assert.Equal(0, fixture.Runtime.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaleDelete_DoesNotEndRemoteLiveIncarnation()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 2, PositionSequence: 1));
|
||||
LiveEntityRecord current = fixture.Record;
|
||||
|
||||
Assert.False(fixture.Controller.OnDelete(
|
||||
new DeleteObject.Parsed(Guid, InstanceSequence: 1)));
|
||||
|
||||
Assert.Same(current, fixture.Record);
|
||||
Assert.NotNull(fixture.Objects.Get(Guid));
|
||||
Assert.Equal(0, fixture.Teardown.TearDownCount);
|
||||
Assert.Empty(fixture.Teardown.ForgottenUnknownOwners);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LocalPlayerDelete_IsSideEffectFreeBeforeAndAfterCreate()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true, playerGuid: Guid);
|
||||
|
||||
Assert.False(fixture.Controller.OnDelete(
|
||||
new DeleteObject.Parsed(Guid, InstanceSequence: 1)));
|
||||
Assert.Empty(fixture.Teardown.ForgottenUnknownOwners);
|
||||
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 2, PositionSequence: 1));
|
||||
LiveEntityRecord current = fixture.Record;
|
||||
|
||||
Assert.False(fixture.Controller.OnDelete(
|
||||
new DeleteObject.Parsed(Guid, InstanceSequence: 2)));
|
||||
|
||||
Assert.Same(current, fixture.Record);
|
||||
Assert.NotNull(fixture.Objects.Get(Guid));
|
||||
Assert.Equal(0, fixture.Teardown.TearDownCount);
|
||||
Assert.Empty(fixture.Teardown.ForgottenUnknownOwners);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExactDelete_RemovesObjectAndTearsDownOnce()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
|
||||
Assert.True(fixture.Controller.OnDelete(
|
||||
new DeleteObject.Parsed(Guid, InstanceSequence: 1)));
|
||||
|
||||
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
|
||||
Assert.Null(fixture.Objects.Get(Guid));
|
||||
Assert.Equal(1, fixture.Teardown.TearDownCount);
|
||||
Assert.Equal(0, fixture.Runtime.PendingTeardownCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prune_UsesTheSameExactDeleteTransaction()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
|
||||
Assert.True(fixture.Controller.OnPrune(
|
||||
new LiveEntityPruneCandidate(Guid, Generation: 1)));
|
||||
|
||||
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
|
||||
Assert.Null(fixture.Objects.Get(Guid));
|
||||
Assert.Equal(1, fixture.Teardown.TearDownCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delete_DrainsIndependentFailuresAndRetryDoesNotReplayObjectRemoval()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
int objectRemovedCalls = 0;
|
||||
fixture.Objects.ObjectRemoved += _ =>
|
||||
{
|
||||
objectRemovedCalls++;
|
||||
throw new InvalidOperationException("fixture object-table observer failure");
|
||||
};
|
||||
bool failTeardown = true;
|
||||
fixture.Teardown.TearDownAction = _ =>
|
||||
{
|
||||
if (!failTeardown)
|
||||
return;
|
||||
failTeardown = false;
|
||||
throw new InvalidOperationException("fixture component teardown failure");
|
||||
};
|
||||
|
||||
AggregateException error = Assert.Throws<AggregateException>(() =>
|
||||
fixture.Controller.OnDelete(
|
||||
new DeleteObject.Parsed(Guid, InstanceSequence: 1)));
|
||||
|
||||
Assert.Contains(error.Flatten().InnerExceptions,
|
||||
exception => exception.Message.Contains("object-table observer", StringComparison.Ordinal));
|
||||
Assert.Contains(error.Flatten().InnerExceptions,
|
||||
exception => exception.Message.Contains("component teardown", StringComparison.Ordinal));
|
||||
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
|
||||
Assert.Null(fixture.Objects.Get(Guid));
|
||||
Assert.Equal(1, objectRemovedCalls);
|
||||
Assert.Equal(1, fixture.Runtime.PendingTeardownCount);
|
||||
|
||||
Assert.Equal(1, fixture.Controller.RetryPendingTeardowns());
|
||||
Assert.Equal(0, fixture.Runtime.PendingTeardownCount);
|
||||
Assert.Equal(1, objectRemovedCalls);
|
||||
Assert.Equal(2, fixture.Teardown.TearDownCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteCallback_CanInstallNewerGenerationWithoutOldCleanupRemovingIt()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
LiveEntityRecord old = fixture.Record;
|
||||
bool replaced = false;
|
||||
fixture.Teardown.TearDownAction = record =>
|
||||
{
|
||||
Assert.Same(old, record);
|
||||
if (replaced)
|
||||
return;
|
||||
replaced = true;
|
||||
LiveEntityRegistrationResult result = fixture.Runtime.RegisterLiveEntity(
|
||||
Spawn(Generation: 2, PositionSequence: 2, Name: "replacement"));
|
||||
Assert.True(result.LogicalRegistrationCreated);
|
||||
};
|
||||
|
||||
Assert.True(fixture.Controller.OnDelete(
|
||||
new DeleteObject.Parsed(Guid, InstanceSequence: 1)));
|
||||
|
||||
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord replacement));
|
||||
Assert.NotSame(old, replacement);
|
||||
Assert.Equal((ushort)2, replacement.Generation);
|
||||
Assert.Equal("replacement", replacement.Snapshot.Name);
|
||||
Assert.Equal(0, fixture.Runtime.PendingTeardownCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PartialProjection_IsRetriedInsteadOfMistakenForCompletedHydration()
|
||||
{
|
||||
|
|
@ -1447,6 +1593,7 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
public readonly RecordingReadyPublisher Ready;
|
||||
public readonly RecordingOrigin Origin;
|
||||
public readonly RecordingNetworkSink Network = new();
|
||||
public readonly RecordingTeardownCoordinator Teardown = new();
|
||||
public readonly LiveEntityHydrationController Controller;
|
||||
|
||||
public Action<uint, AcceptedPhysicsTimestamps>? PublishTimestampsAction { get; set; }
|
||||
|
|
@ -1465,7 +1612,7 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Runtime = new LiveEntityRuntime(
|
||||
spatial,
|
||||
Resources,
|
||||
new DelegateLiveEntityRuntimeComponentLifecycle(_ => { }));
|
||||
Teardown);
|
||||
Materializer = new RecordingMaterializer(Runtime, Operations);
|
||||
Relationships = new RecordingRelationships(Operations);
|
||||
Ready = new RecordingReadyPublisher(Operations);
|
||||
|
|
@ -1493,6 +1640,7 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Ready,
|
||||
Origin,
|
||||
Network,
|
||||
Teardown,
|
||||
(guid, timestamps) =>
|
||||
{
|
||||
Operations.Add($"timestamps:{timestamps.Instance}");
|
||||
|
|
@ -1743,6 +1891,23 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingTeardownCoordinator
|
||||
: ILiveEntityTeardownCoordinator
|
||||
{
|
||||
public int TearDownCount { get; private set; }
|
||||
public List<uint> ForgottenUnknownOwners { get; } = [];
|
||||
public Action<LiveEntityRecord>? TearDownAction { get; set; }
|
||||
|
||||
public void TearDown(LiveEntityRecord record)
|
||||
{
|
||||
TearDownCount++;
|
||||
TearDownAction?.Invoke(record);
|
||||
}
|
||||
|
||||
public void ForgetUnknownOwner(uint serverGuid) =>
|
||||
ForgottenUnknownOwners.Add(serverGuid);
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn(
|
||||
ushort Generation,
|
||||
ushort PositionSequence,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
using AcDream.App.World;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
||||
public sealed class LiveEntityRuntimeTeardownControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Retry_ReusesPlanAndDoesNotReplayCompletedOwners()
|
||||
{
|
||||
LiveEntityRecord record = new(Spawn());
|
||||
int factoryCalls = 0;
|
||||
int completedOwnerCalls = 0;
|
||||
int retryingOwnerCalls = 0;
|
||||
bool failOnce = true;
|
||||
var controller = new LiveEntityRuntimeTeardownController(
|
||||
_ =>
|
||||
{
|
||||
factoryCalls++;
|
||||
return new LiveEntityTeardownPlan([
|
||||
() => completedOwnerCalls++,
|
||||
() =>
|
||||
{
|
||||
retryingOwnerCalls++;
|
||||
if (failOnce)
|
||||
{
|
||||
failOnce = false;
|
||||
throw new InvalidOperationException("fixture failure");
|
||||
}
|
||||
},
|
||||
]);
|
||||
},
|
||||
_ => { });
|
||||
|
||||
Assert.Throws<AggregateException>(() => controller.TearDown(record));
|
||||
controller.TearDown(record);
|
||||
|
||||
Assert.Equal(1, factoryCalls);
|
||||
Assert.Equal(1, completedOwnerCalls);
|
||||
Assert.Equal(2, retryingOwnerCalls);
|
||||
Assert.True(record.RuntimeComponentTeardownPlan!.IsComplete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownOwner_IsForwardedWithoutCreatingATeardownPlan()
|
||||
{
|
||||
uint forgotten = 0u;
|
||||
int factoryCalls = 0;
|
||||
var controller = new LiveEntityRuntimeTeardownController(
|
||||
_ =>
|
||||
{
|
||||
factoryCalls++;
|
||||
return new LiveEntityTeardownPlan([]);
|
||||
},
|
||||
guid => forgotten = guid);
|
||||
|
||||
controller.ForgetUnknownOwner(Guid);
|
||||
|
||||
Assert.Equal(Guid, forgotten);
|
||||
Assert.Equal(0, factoryCalls);
|
||||
}
|
||||
|
||||
private const uint Guid = 0x70000001u;
|
||||
private const uint Cell = 0x01010001u;
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn()
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
Cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
|
||||
var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
Position: position,
|
||||
Movement: null,
|
||||
AnimationFrame: null,
|
||||
SetupTableId: 0x02000001u,
|
||||
MotionTableId: 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,
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
"fixture",
|
||||
(uint)ItemType.Creature,
|
||||
null,
|
||||
0x09000001u,
|
||||
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
InstanceSequence: 1,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
Physics: physics);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue