fix(streaming): preserve portal destination ownership

Detach old-world spatial ownership atomically, prioritize destination retirement dependencies, and reveal the viewport at the retail transition edge. Give private paperdoll views independent mesh ownership and retain dormant ACE entities so portal revisits preserve server objects without extending active GPU lifetimes.
This commit is contained in:
Erik 2026-07-25 08:35:12 +02:00
parent 2c848d4167
commit 823936ec31
57 changed files with 2551 additions and 324 deletions

View file

@ -636,19 +636,140 @@ public sealed class LiveEntityHydrationControllerTests
}
[Fact]
public void Prune_UsesTheSameExactDeleteTransaction()
public void Prune_ReleasesActiveOwnersButRetainsColdAuthoritativeSnapshot()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
ClientObject retainedObject = fixture.Objects.Get(Guid)!;
Assert.True(fixture.Controller.OnPrune(
new LiveEntityPruneCandidate(Guid, Generation: 1)));
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
Assert.Null(fixture.Objects.Get(Guid));
Assert.Same(retainedObject, fixture.Objects.Get(Guid));
Assert.True(fixture.Dormant.Contains(Guid, generation: 1));
Assert.Equal(1, fixture.Teardown.TearDownCount);
}
[Fact]
public void LoadedLandblock_RestoresDormantSnapshotThroughCanonicalHydration()
{
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)));
fixture.Controller.OnLandblockLoaded(Cell);
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord restored));
Assert.Equal((ushort)1, restored.Generation);
Assert.NotNull(restored.WorldEntity);
Assert.False(fixture.Dormant.Contains(Guid, generation: 1));
Assert.Equal(2, fixture.Resources.RegisterCount);
Assert.Equal(1, fixture.Resources.UnregisterCount);
Assert.Equal(2, fixture.Ready.PublishCount);
}
[Fact]
public void ExactDelete_RemovesDormantSnapshotAndRetainedObject()
{
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.True(fixture.Controller.OnDelete(
new DeleteObject.Parsed(Guid, InstanceSequence: 1)));
Assert.False(fixture.Dormant.Contains(Guid, generation: 1));
Assert.Null(fixture.Objects.Get(Guid));
fixture.Controller.OnLandblockLoaded(Cell);
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
}
[Fact]
public void DormantNewerGeneration_RejectsStaleCreateAndAcceptsReplacement()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 2, PositionSequence: 1));
Assert.True(fixture.Controller.OnPrune(
new LiveEntityPruneCandidate(Guid, Generation: 2)));
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 2,
Name: "stale"));
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
Assert.True(fixture.Dormant.Contains(Guid, generation: 2));
fixture.Controller.OnCreate(Spawn(
Generation: 3,
PositionSequence: 1,
Name: "replacement"));
Assert.Equal((ushort)3, fixture.Record.Generation);
Assert.Equal("replacement", fixture.Objects.Get(Guid)!.Name);
Assert.False(fixture.Dormant.Contains(Guid, generation: 2));
}
[Fact]
public void DormantSameGenerationCreate_UsesRetainedTimestampGate()
{
using var fixture = new Fixture(originKnown: true);
WorldSession.EntitySpawn accepted = Spawn(
Generation: 1,
PositionSequence: 5);
fixture.Controller.OnCreate(accepted);
Assert.True(fixture.Controller.OnPrune(
new LiveEntityPruneCandidate(Guid, Generation: 1)));
CreateObject.ServerPosition stalePosition =
accepted.Position!.Value with
{
LandblockId = 0x02020001u,
PositionX = 99f,
};
PhysicsSpawnData stalePhysics = accepted.Physics!.Value with
{
Position = stalePosition,
Timestamps = accepted.Physics.Value.Timestamps with
{
Position = 4,
},
};
fixture.Controller.OnCreate(accepted with
{
Position = stalePosition,
PositionSequence = 4,
Physics = stalePhysics,
});
Assert.Equal(accepted.Position, fixture.Record.Snapshot.Position);
Assert.Equal((ushort)5, fixture.Record.Snapshot.PositionSequence);
}
[Fact]
public void DormantReplacementCreate_ReplacesAbsentOldQualities()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(
Spawn(Generation: 1, PositionSequence: 1) with
{
Value = 123,
});
Assert.True(fixture.Controller.OnPrune(
new LiveEntityPruneCandidate(Guid, Generation: 1)));
fixture.Controller.OnCreate(
Spawn(Generation: 2, PositionSequence: 1) with
{
Value = null,
});
Assert.Equal(0, fixture.Objects.Get(Guid)!.Value);
}
[Fact]
public void Delete_DrainsIndependentFailuresAndRetryDoesNotReplayObjectRemoval()
{
@ -1690,6 +1811,7 @@ public sealed class LiveEntityHydrationControllerTests
public readonly RecordingOrigin Origin;
public readonly RecordingNetworkSink Network = new();
public readonly RecordingTeardownCoordinator Teardown = new();
public readonly DormantLiveEntityStore Dormant = new();
public readonly RecordingTimestampPublisher Timestamps;
public readonly LiveEntityHydrationController Controller;
@ -1741,7 +1863,8 @@ public sealed class LiveEntityHydrationControllerTests
Runtime,
Objects,
Teardown,
identity);
identity,
Dormant);
Controller = new LiveEntityHydrationController(
Runtime,
Objects,
@ -1753,7 +1876,8 @@ public sealed class LiveEntityHydrationControllerTests
Network,
Timestamps,
identity,
deletion);
deletion,
Dormant);
}
public LiveEntityRecord Record