refactor(world): extract live entity teardown

This commit is contained in:
Erik 2026-07-21 18:25:59 +02:00
parent 4427cfb2c7
commit f38822c490
8 changed files with 565 additions and 159 deletions

View file

@ -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,