acdream/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs

1413 lines
52 KiB
C#

using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
namespace AcDream.App.Tests.World;
public sealed class LiveEntityHydrationControllerTests
{
[Fact]
public void FreshCreate_PublishesCanonicalOrderAndReadyAfterMaterialization()
{
using var fixture = new Fixture(originKnown: true);
WorldSession.EntitySpawn spawn = Spawn(Generation: 1, PositionSequence: 1);
fixture.Materializer.BeforeMaterialize = (_, _) =>
Assert.NotNull(fixture.Objects.Get(Guid));
fixture.Controller.OnCreate(spawn);
Assert.Equal(
["timestamps:1", "relationship:1", "materialize:LogicalRegistration:1", "ready"],
fixture.Operations);
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord record));
Assert.NotNull(record.WorldEntity);
Assert.True(record.InitialHydrationCompleted);
Assert.Equal(1, fixture.Resources.RegisterCount);
}
[Fact]
public void UnknownOrigin_DefersFirstMaterializationUntilLandblockRecovery()
{
using var fixture = new Fixture(originKnown: false);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord record));
Assert.Null(record.WorldEntity);
Assert.Equal(0, fixture.Resources.RegisterCount);
Assert.Empty(fixture.Materializer.Calls);
fixture.Origin.IsKnownValue = true;
fixture.Controller.OnLandblockLoaded(Cell);
Assert.NotNull(record.WorldEntity);
Assert.Single(fixture.Materializer.Calls);
Assert.Equal(LiveProjectionPurpose.SpatialRecovery, fixture.Materializer.Calls[0].Purpose);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(1, fixture.Ready.PublishCount);
}
[Fact]
public void InitialPlayerOriginRecovery_DoesNotReenterSameHydrationTransaction()
{
using var fixture = new Fixture(originKnown: true, playerGuid: Guid);
fixture.Origin.AlreadyLoadedLandblocks = [Cell];
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
Assert.True(fixture.Record.InitialHydrationCompleted);
Assert.Single(fixture.Materializer.Calls);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(1, fixture.Ready.PublishCount);
}
[Fact]
public void LoadedCallback_RebucketsMaterializedIdentityWithoutReactivation()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
WorldEntity entity = record.WorldEntity!;
fixture.Controller.OnLandblockLoaded(Cell);
Assert.Same(entity, record.WorldEntity);
Assert.Single(fixture.Materializer.Calls);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(1, fixture.Ready.PublishCount);
}
[Fact]
public void AppearanceAfterDeferredProjection_FirstMaterializesAndPublishesReady()
{
using var fixture = new Fixture(originKnown: true);
fixture.Materializer.SkipMaterializationCount = 1;
WorldSession.EntitySpawn spawn = Spawn(Generation: 1, PositionSequence: 1);
fixture.Controller.OnCreate(spawn);
LiveEntityRecord record = fixture.Record;
Assert.Null(record.WorldEntity);
Assert.True(fixture.Controller.ApplyAppearance(
record,
record.Snapshot,
appearanceUpdate: null));
Assert.NotNull(record.WorldEntity);
Assert.Equal(2, fixture.Materializer.Calls.Count);
Assert.Equal(LiveProjectionPurpose.SpatialRecovery, fixture.Materializer.Calls[1].Purpose);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(1, fixture.Ready.PublishCount);
}
[Fact]
public void SameOlderNewerAndWrappedGenerations_RouteOrReplaceExactlyOnce()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 0xFFFF, PositionSequence: 1));
LiveEntityRecord first = fixture.Record;
fixture.Controller.OnCreate(Spawn(Generation: 0xFFFF, PositionSequence: 2));
Assert.Equal(1, fixture.Network.ApplyCount);
Assert.Same(first, fixture.Record);
fixture.Controller.OnCreate(Spawn(Generation: 0xFFFE, PositionSequence: 3));
Assert.Equal(1, fixture.Network.ApplyCount);
Assert.Same(first, fixture.Record);
fixture.Controller.OnCreate(Spawn(Generation: 0, PositionSequence: 4));
Assert.NotSame(first, fixture.Record);
Assert.Equal((ushort)0, fixture.Record.Generation);
Assert.Equal(2, fixture.Resources.RegisterCount);
Assert.Equal(1, fixture.Resources.UnregisterCount);
Assert.Equal(2, fixture.Ready.PublishCount);
}
[Fact]
public void RelationshipCallback_ReplacesGuidAndSupersedesOuterProjection()
{
using var fixture = new Fixture(originKnown: true);
bool replaced = false;
fixture.Relationships.OnSpawnAction = spawn =>
{
if (!replaced && spawn.InstanceSequence == 1)
{
replaced = true;
fixture.Controller.OnCreate(Spawn(Generation: 2, PositionSequence: 2));
}
};
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
Assert.Equal((ushort)2, fixture.Record.Generation);
Assert.Single(fixture.Materializer.Calls);
Assert.Equal((ushort)2, fixture.Materializer.Calls[0].Generation);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(1, fixture.Ready.PublishCount);
}
[Fact]
public void RegistrationFailure_RollsBackAndLandblockRecoveryRetriesOnce()
{
var resources = new FailingOnceResources();
using var fixture = new Fixture(originKnown: true, resources);
Assert.Throws<InvalidOperationException>(() =>
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)));
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord record));
Assert.Null(record.WorldEntity);
Assert.Equal(1, resources.RegisterCount);
Assert.Equal(1, resources.UnregisterCount);
fixture.Controller.OnLandblockLoaded(Cell);
Assert.NotNull(record.WorldEntity);
Assert.Equal(2, resources.RegisterCount);
Assert.Equal(1, resources.UnregisterCount);
Assert.Equal(1, fixture.Ready.PublishCount);
}
[Fact]
public void SameGenerationRetransmit_RecoversAfterRegistrationTombstoneConverges()
{
var resources = new FailingRegisterAndRollbackOnceResources();
using var fixture = new Fixture(originKnown: true, resources);
WorldSession.EntitySpawn spawn = Spawn(Generation: 1, PositionSequence: 1);
Assert.Throws<AggregateException>(() => fixture.Controller.OnCreate(spawn));
Assert.Equal(0, fixture.Runtime.Count);
Assert.Equal(1, fixture.Runtime.PendingTeardownCount);
Assert.Equal(1, fixture.Runtime.RetryPendingTeardowns());
fixture.Controller.OnCreate(spawn);
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord recovered));
Assert.NotNull(recovered.WorldEntity);
Assert.True(recovered.InitialHydrationCompleted);
Assert.Equal(2, resources.RegisterCount);
Assert.Equal(2, resources.UnregisterCount);
Assert.Equal(1, fixture.Ready.PublishCount);
}
[Fact]
public void PriorGenerationCleanupFailure_ReportsAfterReplacementIsFullyInstalled()
{
var resources = new FailingUnregisterOnceResources();
using var fixture = new Fixture(originKnown: true, resources);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
Assert.Throws<AggregateException>(() => fixture.Controller.OnCreate(Spawn(
Generation: 2,
PositionSequence: 2,
Name: "replacement")));
Assert.Equal((ushort)2, fixture.Record.Generation);
Assert.True(fixture.Record.InitialHydrationCompleted);
Assert.Equal("replacement", fixture.Objects.Get(Guid)!.Name);
Assert.Equal(1, fixture.Runtime.PendingTeardownCount);
Assert.Equal(1, fixture.Runtime.RetryPendingTeardowns());
Assert.Equal(0, fixture.Runtime.PendingTeardownCount);
}
[Fact]
public void PartialProjection_IsRetriedInsteadOfMistakenForCompletedHydration()
{
using var fixture = new Fixture(originKnown: true, playerGuid: Guid);
bool failOnce = true;
fixture.Materializer.AfterMaterialize = (_, _) =>
{
if (!failOnce)
return;
failOnce = false;
throw new InvalidOperationException("fixture post-registration failure");
};
Assert.Throws<InvalidOperationException>(() =>
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)));
LiveEntityRecord record = fixture.Record;
WorldEntity partial = Assert.IsType<WorldEntity>(record.WorldEntity);
Assert.False(record.InitialHydrationCompleted);
// Local-player records ordinarily do not rebucket from streaming
// callbacks; an incomplete initial transaction must still recover.
fixture.Controller.OnLandblockLoaded(Cell);
Assert.Same(partial, record.WorldEntity);
Assert.True(record.InitialHydrationCompleted);
Assert.Equal(2, fixture.Materializer.Calls.Count);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(1, fixture.Ready.PublishCount);
}
[Fact]
public void FailedReadyBarrier_RetriesSameProjectionOnRetransmit()
{
using var fixture = new Fixture(originKnown: true);
fixture.Ready.FailPublishCount = 1;
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
WorldEntity partial = Assert.IsType<WorldEntity>(record.WorldEntity);
Assert.False(record.InitialHydrationCompleted);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 2));
Assert.Same(partial, record.WorldEntity);
Assert.True(record.InitialHydrationCompleted);
Assert.Equal(2, fixture.Materializer.Calls.Count);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(2, fixture.Ready.PublishCount);
}
[Fact]
public void TimestampCallbackReplacement_StopsOuterObjectAndProjectionTail()
{
using var fixture = new Fixture(originKnown: true);
bool replaced = false;
fixture.PublishTimestampsAction = (_, timestamps) =>
{
if (replaced || timestamps.Instance != 1)
return;
replaced = true;
fixture.Controller.OnCreate(Spawn(
Generation: 2,
PositionSequence: 2,
Name: "generation 2"));
};
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 1,
Name: "generation 1"));
Assert.Equal((ushort)2, fixture.Record.Generation);
Assert.Equal("generation 2", fixture.Objects.Get(Guid)!.Name);
Assert.Single(fixture.Materializer.Calls);
Assert.Equal((ushort)2, fixture.Materializer.Calls[0].Generation);
}
[Fact]
public void TimestampCallbackFresherSameGeneration_StopsOuterCreateVersion()
{
using var fixture = new Fixture(originKnown: true);
bool refreshed = false;
fixture.PublishTimestampsAction = (_, timestamps) =>
{
if (refreshed)
return;
refreshed = true;
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 2,
Name: "same generation newer"));
};
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 1,
Name: "same generation older"));
Assert.Equal("same generation newer", fixture.Objects.Get(Guid)!.Name);
Assert.True(fixture.Record.InitialHydrationCompleted);
Assert.Single(fixture.Materializer.Calls);
Assert.Equal((ushort)2, fixture.Materializer.PositionSequences[0]);
}
[Fact]
public void ObjectRemovalCallbackNewerGeneration_CannotBeOverwrittenByOuterCreate()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 1,
Name: "generation 1"));
bool replaced = false;
fixture.Objects.ObjectRemovalClassified += removal =>
{
if (replaced
|| removal.Reason is not ClientObjectRemovalReason.GenerationReplacement)
{
return;
}
replaced = true;
fixture.Controller.OnCreate(Spawn(
Generation: 3,
PositionSequence: 3,
Name: "generation 3"));
};
fixture.Controller.OnCreate(Spawn(
Generation: 2,
PositionSequence: 2,
Name: "generation 2"));
Assert.Equal((ushort)3, fixture.Record.Generation);
Assert.Equal("generation 3", fixture.Objects.Get(Guid)!.Name);
Assert.Equal((ushort)3, fixture.Materializer.Calls[^1].Generation);
Assert.DoesNotContain(
fixture.Materializer.Calls,
call => call.Generation == 2);
}
[Fact]
public void ObjectRemovalCallbackFresherSameGeneration_WinsOuterReplacement()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(
Generation: 0,
PositionSequence: 1,
Name: "generation zero"));
bool refreshed = false;
fixture.Objects.ObjectRemovalClassified += removal =>
{
if (refreshed
|| removal.Reason is not ClientObjectRemovalReason.GenerationReplacement)
{
return;
}
refreshed = true;
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 3,
Name: "same generation newer"));
};
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 2,
Name: "same generation older"));
Assert.Equal((ushort)1, fixture.Record.Generation);
Assert.Equal("same generation newer", fixture.Objects.Get(Guid)!.Name);
Assert.True(fixture.Record.InitialHydrationCompleted);
Assert.Equal((ushort)3, fixture.Materializer.PositionSequences[^1]);
Assert.DoesNotContain(
(ushort)2,
fixture.Materializer.PositionSequences.Skip(1));
}
[Fact]
public void MaterializerCallbackFresherSameGeneration_ReplaysCanonicalHydrationOnce()
{
using var fixture = new Fixture(originKnown: true);
bool refreshed = false;
fixture.Materializer.BeforeMaterialize = (_, spawn) =>
{
if (refreshed || spawn.PositionSequence != 1)
return;
refreshed = true;
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 2,
Name: "same generation newer"));
};
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 1,
Name: "same generation older"));
Assert.Equal("same generation newer", fixture.Objects.Get(Guid)!.Name);
Assert.Equal([1, 2], fixture.Materializer.PositionSequences);
Assert.Equal(
LiveProjectionPurpose.CreateSupersessionRecovery,
fixture.Materializer.Calls[^1].Purpose);
Assert.Equal(2UL, fixture.Materializer.InstalledCreateIntegrationVersion);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(1, fixture.Ready.PublishCount);
Assert.True(fixture.Record.InitialHydrationCompleted);
}
[Fact]
public void ReadyCallbackFresherSameGeneration_ReplaysCanonicalHydrationBeforeCompletion()
{
using var fixture = new Fixture(originKnown: true);
bool refreshed = false;
fixture.Ready.BeforePublish = _ =>
{
if (refreshed)
return;
refreshed = true;
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 2,
Name: "same generation newer"));
};
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 1,
Name: "same generation older"));
Assert.Equal("same generation newer", fixture.Objects.Get(Guid)!.Name);
Assert.Equal([1, 2], fixture.Materializer.PositionSequences);
Assert.Equal(
LiveProjectionPurpose.CreateSupersessionRecovery,
fixture.Materializer.Calls[^1].Purpose);
Assert.Equal(2UL, fixture.Materializer.InstalledCreateIntegrationVersion);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(2, fixture.Ready.PublishCount);
Assert.True(fixture.Record.InitialHydrationCompleted);
}
[Fact]
public void CompletedSpatialRecovery_DetectsCreateVersionDriftWithoutNestedHydrationRequest()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
ulong stalePositionAuthority = record.PositionAuthorityVersion;
bool refreshed = false;
fixture.Materializer.BeforeMaterialize = (_, spawn) =>
{
if (refreshed || spawn.PositionSequence != 1)
return;
refreshed = true;
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 2,
Name: "same generation newer"));
};
Assert.False(fixture.Controller.RecoverProjection(
record,
stalePositionAuthority,
record.Snapshot));
Assert.True(record.InitialHydrationCompleted);
Assert.Equal("same generation newer", fixture.Objects.Get(Guid)!.Name);
Assert.Equal([1, 1, 2], fixture.Materializer.PositionSequences);
Assert.Equal(
LiveProjectionPurpose.CreateSupersessionRecovery,
fixture.Materializer.Calls[^1].Purpose);
Assert.Equal(2UL, fixture.Materializer.InstalledCreateIntegrationVersion);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void FailedCompletedSupersession_RemainsPendingUntilExactRetry(
bool retryFromLandblock)
{
using var fixture = new Fixture(
originKnown: true,
playerGuid: retryFromLandblock ? Guid : 0u);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
WorldEntity retained = record.WorldEntity!;
ulong positionAuthority = record.PositionAuthorityVersion;
bool refreshed = false;
fixture.Materializer.BeforeMaterialize = (_, spawn) =>
{
if (refreshed || spawn.PositionSequence != 1)
return;
refreshed = true;
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 2,
Name: "recovery v2"));
};
fixture.Materializer.ThrowAfterMaterializePurposeOnce =
LiveProjectionPurpose.CreateSupersessionRecovery;
Assert.Throws<InvalidOperationException>(() =>
fixture.Controller.RecoverProjection(
record,
positionAuthority,
record.Snapshot));
Assert.True(record.InitialHydrationCompleted);
Assert.True(record.CreateProjectionSynchronizationPending);
Assert.Same(retained, record.WorldEntity);
Assert.Equal(1, fixture.Resources.RegisterCount);
fixture.Materializer.BeforeMaterialize = null;
if (retryFromLandblock)
{
fixture.Controller.OnLandblockLoaded(Cell);
}
else
{
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 3,
Name: "recovery v3"));
}
Assert.False(record.CreateProjectionSynchronizationPending);
Assert.Same(retained, record.WorldEntity);
Assert.Equal(
retryFromLandblock ? 2UL : 3UL,
fixture.Materializer.InstalledCreateIntegrationVersion);
Assert.Equal(
LiveProjectionPurpose.CreateSupersessionRecovery,
fixture.Materializer.Calls[^1].Purpose);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(0, fixture.Resources.UnregisterCount);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void StaleProjectionExceptionAfterCreateDrift_PersistsExactRetryObligation(
bool retryFromLandblock)
{
using var fixture = new Fixture(
originKnown: true,
playerGuid: retryFromLandblock ? Guid : 0u);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
WorldEntity retained = record.WorldEntity!;
ulong positionAuthority = record.PositionAuthorityVersion;
bool refreshed = false;
fixture.Materializer.BeforeMaterialize = (_, spawn) =>
{
if (refreshed || spawn.PositionSequence != 1)
return;
refreshed = true;
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 2,
Name: "drift v2"));
throw new InvalidOperationException("fixture stale projection failure");
};
Assert.Throws<InvalidOperationException>(() =>
fixture.Controller.RecoverProjection(
record,
positionAuthority,
record.Snapshot));
Assert.True(record.InitialHydrationCompleted);
Assert.True(record.CreateProjectionSynchronizationPending);
Assert.Same(retained, record.WorldEntity);
fixture.Materializer.BeforeMaterialize = null;
if (retryFromLandblock)
{
fixture.Controller.OnLandblockLoaded(Cell);
}
else
{
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 3,
Name: "drift v3"));
}
Assert.False(record.CreateProjectionSynchronizationPending);
Assert.Same(retained, record.WorldEntity);
Assert.Equal(
LiveProjectionPurpose.CreateSupersessionRecovery,
fixture.Materializer.Calls[^1].Purpose);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(0, fixture.Resources.UnregisterCount);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void CompletedSupersessionReadyFailure_RetriesWholeCommitBoundary(
bool retryFromLandblock)
{
using var fixture = new Fixture(
originKnown: true,
playerGuid: retryFromLandblock ? Guid : 0u);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
WorldEntity retained = record.WorldEntity!;
ulong positionAuthority = record.PositionAuthorityVersion;
bool refreshed = false;
fixture.Materializer.BeforeMaterialize = (_, spawn) =>
{
if (refreshed || spawn.PositionSequence != 1)
return;
refreshed = true;
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 2,
Name: "ready v2"));
};
fixture.Ready.FailPublishCount = 1;
Assert.False(fixture.Controller.RecoverProjection(
record,
positionAuthority,
record.Snapshot));
Assert.True(record.CreateProjectionSynchronizationPending);
fixture.Materializer.BeforeMaterialize = null;
if (retryFromLandblock)
{
fixture.Controller.OnLandblockLoaded(Cell);
}
else
{
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 3,
Name: "ready v3"));
}
Assert.False(record.CreateProjectionSynchronizationPending);
Assert.Same(retained, record.WorldEntity);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(0, fixture.Resources.UnregisterCount);
Assert.True(fixture.Ready.PublishCount >= 3);
}
[Fact]
public void PickupSupersession_CompletesCelllessWithoutDatMaterialization()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
ulong positionAuthority = record.PositionAuthorityVersion;
fixture.Network.ApplyAction = events =>
{
Assert.NotNull(events.Pickup);
Assert.True(fixture.Runtime.TryApplyPickup(events.Pickup!.Value, out _));
Assert.True(fixture.Runtime.WithdrawLiveEntityProjectionToCellless(Guid));
};
bool refreshed = false;
fixture.Materializer.BeforeMaterialize = (_, spawn) =>
{
if (refreshed || spawn.PositionSequence != 1)
return;
refreshed = true;
fixture.Controller.OnCreate(CelllessSpawn(
PositionSequence: 2,
parentGuid: null));
};
Assert.False(fixture.Controller.RecoverProjection(
record,
positionAuthority,
record.Snapshot));
Assert.False(record.CreateProjectionSynchronizationPending);
Assert.Equal(0u, record.FullCellId);
Assert.False(record.IsSpatiallyProjected);
Assert.DoesNotContain(
fixture.Materializer.Calls,
call => call.Purpose is LiveProjectionPurpose.CreateSupersessionRecovery);
Assert.Equal(1, fixture.Resources.RegisterCount);
}
[Fact]
public void PickupSupersedesInitialHydration_WithoutInventingRenderOwner()
{
using var fixture = new Fixture(originKnown: true);
fixture.Network.ApplyAction = events =>
{
Assert.NotNull(events.Pickup);
Assert.True(fixture.Runtime.TryApplyPickup(events.Pickup!.Value, out _));
};
bool refreshed = false;
fixture.Materializer.BeforeMaterialize = (_, spawn) =>
{
if (refreshed || spawn.PositionSequence != 1)
return;
refreshed = true;
fixture.Controller.OnCreate(CelllessSpawn(
PositionSequence: 2,
parentGuid: null));
};
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
Assert.False(record.CreateProjectionSynchronizationPending);
Assert.False(record.InitialHydrationCompleted);
Assert.Null(record.WorldEntity);
Assert.Equal(0, fixture.Resources.RegisterCount);
fixture.Materializer.BeforeMaterialize = null;
fixture.Network.ApplyAction = events =>
{
if (events.Position is { } position)
{
fixture.Runtime.TryApplyPosition(
position,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _);
}
};
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 3));
Assert.True(record.InitialHydrationCompleted);
Assert.NotNull(record.WorldEntity);
Assert.Equal(1, fixture.Resources.RegisterCount);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ParentSupersession_CompletesAtExactAttachedReadyBoundary(
bool failFirstReadyAttempt)
{
const uint parentGuid = 0x70000002u;
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
WorldEntity retained = record.WorldEntity!;
ulong positionAuthority = record.PositionAuthorityVersion;
fixture.Ready.FailPublishCount = failFirstReadyAttempt ? 2 : 0;
CreateParentUpdate? pendingParent = null;
fixture.Network.ApplyAction = events =>
{
Assert.NotNull(events.Parent);
Assert.True(fixture.Runtime.TryApplyCreateParent(
events.Parent!.Value,
out _));
pendingParent = events.Parent.Value;
};
fixture.Relationships.OnSpawnAction = _ =>
{
if (pendingParent is not { } parent
|| !record.CreateProjectionSynchronizationPending)
{
return;
}
Assert.True(fixture.Runtime.CommitStagedParent(
new ParentAttachmentRelation(
parent.ParentGuid,
parent.ChildGuid,
parent.ParentLocation,
parent.PlacementId,
ParentInstanceSequence: 0,
parent.ChildPositionSequence),
out _));
ulong positionVersion = record.PositionAuthorityVersion;
Assert.True(fixture.Runtime.CommitAcceptedParentCellless(
record,
positionVersion));
Assert.True(fixture.Runtime.WithdrawLiveEntityProjection(record));
WorldEntity? attached = fixture.Runtime.MaterializeLiveEntity(
Guid,
Cell,
_ => throw new InvalidOperationException("retained identity expected"),
LiveEntityProjectionKind.Attached);
Assert.Same(retained, attached);
fixture.Controller.OnEntityReady(
new LiveEntityReadyCandidate(
record,
record.CreateIntegrationVersion));
};
bool refreshed = false;
fixture.Materializer.BeforeMaterialize = (_, spawn) =>
{
if (refreshed || spawn.PositionSequence != 1)
return;
refreshed = true;
fixture.Controller.OnCreate(CelllessSpawn(
PositionSequence: 2,
parentGuid));
};
Assert.False(fixture.Controller.RecoverProjection(
record,
positionAuthority,
record.Snapshot));
if (failFirstReadyAttempt)
{
Assert.True(record.CreateProjectionSynchronizationPending);
fixture.Materializer.BeforeMaterialize = null;
fixture.Controller.OnCreate(CelllessSpawn(
PositionSequence: 3,
parentGuid));
}
Assert.False(record.CreateProjectionSynchronizationPending);
Assert.Equal(LiveEntityProjectionKind.Attached, record.ProjectionKind);
Assert.Same(retained, record.WorldEntity);
Assert.DoesNotContain(
fixture.Materializer.Calls,
call => call.Purpose is LiveProjectionPurpose.CreateSupersessionRecovery);
Assert.Equal(1, fixture.Resources.RegisterCount);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void InitialParentReadyFailure_RetriesCelllessReadyBoundary(
bool retryFromLandblock)
{
const uint parentGuid = 0x70000002u;
using var fixture = new Fixture(
originKnown: true,
playerGuid: retryFromLandblock ? Guid : 0u);
fixture.Ready.FailPublishCount = 2;
fixture.Network.ApplyAction = events =>
{
if (events.Parent is { } parent)
fixture.Runtime.TryApplyCreateParent(parent, out _);
};
fixture.Relationships.OnSpawnAction = _ =>
{
LiveEntityRecord record = fixture.Record;
if (record.WorldEntity is not null)
return;
WorldEntity? attached = fixture.Runtime.MaterializeLiveEntity(
Guid,
Cell,
id => new WorldEntity
{
Id = id,
ServerGuid = Guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = [],
ParentCellId = Cell,
},
LiveEntityProjectionKind.Attached);
Assert.NotNull(attached);
fixture.Controller.OnEntityReady(new LiveEntityReadyCandidate(
record,
record.CreateIntegrationVersion));
};
fixture.Controller.OnCreate(CelllessSpawn(
PositionSequence: 1,
parentGuid));
LiveEntityRecord initial = fixture.Record;
Assert.False(initial.InitialHydrationCompleted);
Assert.Equal(LiveEntityProjectionKind.Attached, initial.ProjectionKind);
Assert.Equal(1, fixture.Resources.RegisterCount);
if (retryFromLandblock)
{
fixture.Controller.OnLandblockLoaded(Cell);
}
else
{
fixture.Controller.OnCreate(CelllessSpawn(
PositionSequence: 2,
parentGuid));
}
Assert.True(initial.InitialHydrationCompleted);
Assert.False(initial.CreateProjectionSynchronizationPending);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(0, fixture.Resources.UnregisterCount);
}
[Theory]
[InlineData(0, 1, 0, 0)]
[InlineData(1, 1, 1, 0)]
[InlineData(2, 1, 1, 1)]
public void ReadyPublisher_RevalidatesExactRecordBetweenEveryStage(
int replacementStage,
int expectedPrepare,
int expectedPresent,
int expectedReplay)
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord expected = fixture.Record;
int prepare = 0;
int present = 0;
int replay = 0;
bool replaced = false;
bool Stage(int stage)
{
if (!replaced && replacementStage == stage)
{
replaced = true;
fixture.Runtime.RegisterLiveEntity(
Spawn(Generation: 1, PositionSequence: 2));
}
return true;
}
var publisher = new LiveEntityReadyPublisher(
fixture.Runtime,
_ => { prepare++; return Stage(0); },
_ => { present++; return Stage(1); },
_ => { replay++; return Stage(2); });
Assert.False(publisher.Publish(expected));
Assert.Equal(expectedPrepare, prepare);
Assert.Equal(expectedPresent, present);
Assert.Equal(expectedReplay, replay);
Assert.Same(expected, fixture.Record);
Assert.Equal(2UL, fixture.Record.CreateIntegrationVersion);
}
[Fact]
public void EquippedChildReadyCandidate_RejectsVersionAdvancedByPoseCallback()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
WorldEntity entity = record.WorldEntity!;
ulong capturedCreateIntegrationVersion = record.CreateIntegrationVersion;
// Models ProjectionPoseReady synchronously accepting a fresher
// same-generation CreateObject before EntityReady is emitted.
fixture.Runtime.RegisterLiveEntity(
Spawn(Generation: 1, PositionSequence: 2));
bool published = false;
Assert.False(EquippedChildRenderController.PublishEntityReadyExact(
fixture.Runtime,
record,
capturedCreateIntegrationVersion,
entity,
_ => published = true));
Assert.False(published);
}
[Fact]
public void PositionAuthorityGuard_PreventsStaleRecoveryAfterNestedUpdate()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
fixture.Runtime.WithdrawLiveEntityProjection(Guid);
ulong staleVersion = record.PositionAuthorityVersion;
Assert.True(fixture.Runtime.TryApplyPosition(
new WorldSession.EntityPositionUpdate(
Guid,
new CreateObject.ServerPosition(Cell, 12f, 10f, 5f, 1f, 0f, 0f, 0f),
Velocity: null,
PlacementId: 0,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 0,
ForcePositionSequence: 0),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out WorldSession.EntitySpawn accepted,
out _));
Assert.False(fixture.Controller.RecoverProjection(
record,
staleVersion,
accepted));
Assert.Empty(fixture.Runtime.MaterializedWorldEntities);
}
[Fact]
public void DeferredNetworkSink_IsSingleAssignmentAndFailsBeforeBind()
{
var deferred = new DeferredLiveEntityNetworkUpdateSink();
SameGenerationCreateObjectEvents events = default;
Assert.Throws<InvalidOperationException>(() =>
deferred.ApplySameGeneration(events));
var first = new RecordingNetworkSink();
deferred.Bind(first);
deferred.ApplySameGeneration(events);
Assert.Equal(1, first.ApplyCount);
Assert.Throws<InvalidOperationException>(() =>
deferred.Bind(new RecordingNetworkSink()));
}
private const uint Guid = 0x70000001u;
private const uint Cell = 0x01010001u;
private sealed class Fixture : IDisposable
{
public readonly List<string> Operations = [];
public readonly ClientObjectTable Objects = new();
public readonly RecordingResources Resources;
public readonly LiveEntityRuntime Runtime;
public readonly RecordingMaterializer Materializer;
public readonly RecordingRelationships Relationships;
public readonly RecordingReadyPublisher Ready;
public readonly RecordingOrigin Origin;
public readonly RecordingNetworkSink Network = new();
public readonly LiveEntityHydrationController Controller;
public Action<uint, AcceptedPhysicsTimestamps>? PublishTimestampsAction { get; set; }
public Fixture(
bool originKnown,
RecordingResources? resources = null,
uint playerGuid = 0u)
{
Resources = resources ?? new RecordingResources();
var spatial = new GpuWorldState();
spatial.AddLandblock(new LoadedLandblock(
0x0101FFFFu,
new DatReaderWriter.DBObjs.LandBlock(),
Array.Empty<WorldEntity>()));
Runtime = new LiveEntityRuntime(
spatial,
Resources,
new DelegateLiveEntityRuntimeComponentLifecycle(_ => { }));
Materializer = new RecordingMaterializer(Runtime, Operations);
Relationships = new RecordingRelationships(Operations);
Ready = new RecordingReadyPublisher(Operations);
Origin = new RecordingOrigin(originKnown);
Network.ApplyAction = events =>
{
if (events.Position is not { } position)
return;
Runtime.TryApplyPosition(
position,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _);
};
Controller = new LiveEntityHydrationController(
Runtime,
Objects,
new object(),
Materializer,
Relationships,
Ready,
Origin,
Network,
(guid, timestamps) =>
{
Operations.Add($"timestamps:{timestamps.Instance}");
PublishTimestampsAction?.Invoke(guid, timestamps);
},
() => playerGuid);
}
public LiveEntityRecord Record
{
get
{
Assert.True(Runtime.TryGetRecord(Guid, out LiveEntityRecord record));
return record;
}
}
public void Dispose()
{
try
{
Runtime.Clear();
}
catch
{
// Individual failure-path tests assert the relevant exception.
}
}
}
private class RecordingResources : ILiveEntityResourceLifecycle
{
public int RegisterCount { get; protected set; }
public int UnregisterCount { get; protected set; }
public virtual void Register(WorldEntity entity) => RegisterCount++;
public virtual void Unregister(WorldEntity entity) => UnregisterCount++;
}
private sealed class FailingOnceResources : RecordingResources
{
private bool _fail = true;
public override void Register(WorldEntity entity)
{
base.Register(entity);
if (_fail)
{
_fail = false;
throw new InvalidOperationException("fixture registration failure");
}
}
}
private sealed class FailingRegisterAndRollbackOnceResources : RecordingResources
{
private bool _failRegister = true;
private bool _failRollback = true;
public override void Register(WorldEntity entity)
{
base.Register(entity);
if (_failRegister)
{
_failRegister = false;
throw new InvalidOperationException("fixture registration failure");
}
}
public override void Unregister(WorldEntity entity)
{
base.Unregister(entity);
if (_failRollback)
{
_failRollback = false;
throw new InvalidOperationException("fixture rollback failure");
}
}
}
private sealed class FailingUnregisterOnceResources : RecordingResources
{
private bool _fail = true;
public override void Unregister(WorldEntity entity)
{
base.Unregister(entity);
if (_fail)
{
_fail = false;
throw new InvalidOperationException("fixture generation cleanup failure");
}
}
}
private sealed class RecordingMaterializer(
LiveEntityRuntime runtime,
List<string> operations) : ILiveEntityProjectionMaterializer
{
public readonly List<(LiveProjectionPurpose Purpose, ushort Generation)> Calls = [];
public readonly List<ushort> PositionSequences = [];
public ulong? InstalledCreateIntegrationVersion { get; private set; }
public Action<LiveEntityRecord, WorldSession.EntitySpawn>? BeforeMaterialize { get; set; }
public Action<LiveEntityRecord, WorldSession.EntitySpawn>? AfterMaterialize { get; set; }
public LiveProjectionPurpose? ThrowAfterMaterializePurposeOnce { get; set; }
public int SkipMaterializationCount { get; set; }
public bool TryMaterialize(
LiveEntityRecord expectedRecord,
WorldSession.EntitySpawn canonicalSpawn,
LiveProjectionPurpose purpose,
ulong expectedCreateIntegrationVersion,
AcDream.App.Rendering.LiveEntityAppearanceUpdateState? appearanceUpdate = null)
{
Calls.Add((purpose, expectedRecord.Generation));
PositionSequences.Add(canonicalSpawn.PositionSequence);
operations.Add($"materialize:{purpose}:{expectedRecord.Generation}");
BeforeMaterialize?.Invoke(expectedRecord, canonicalSpawn);
if (!runtime.IsCurrentCreateIntegration(
expectedRecord,
expectedCreateIntegrationVersion))
{
return false;
}
if (InstalledCreateIntegrationVersion is null
|| purpose is LiveProjectionPurpose.CreateSupersessionRecovery)
{
InstalledCreateIntegrationVersion = expectedCreateIntegrationVersion;
}
if (SkipMaterializationCount > 0)
{
SkipMaterializationCount--;
return false;
}
if (canonicalSpawn.Position is not { } position
|| canonicalSpawn.SetupTableId is null)
{
return false;
}
WorldEntity? entity = runtime.MaterializeLiveEntity(
canonicalSpawn.Guid,
position.LandblockId,
id => new WorldEntity
{
Id = id,
ServerGuid = canonicalSpawn.Guid,
SourceGfxObjOrSetupId = canonicalSpawn.SetupTableId.Value,
Position = new Vector3(position.PositionX, position.PositionY, position.PositionZ),
Rotation = Quaternion.Identity,
MeshRefs = [],
ParentCellId = position.LandblockId,
});
if (ThrowAfterMaterializePurposeOnce == purpose)
{
ThrowAfterMaterializePurposeOnce = null;
throw new InvalidOperationException(
"fixture supersession projection failure");
}
AfterMaterialize?.Invoke(expectedRecord, canonicalSpawn);
return entity is not null
&& runtime.IsCurrentRecord(expectedRecord);
}
public void ResetSessionState() { }
}
private sealed class RecordingRelationships(List<string> operations)
: ILiveEntitySpawnRelationshipSink
{
public Action<WorldSession.EntitySpawn>? OnSpawnAction { get; set; }
public void OnSpawn(WorldSession.EntitySpawn spawn)
{
operations.Add($"relationship:{spawn.InstanceSequence}");
OnSpawnAction?.Invoke(spawn);
}
}
private sealed class RecordingReadyPublisher(List<string> operations)
: ILiveEntityReadyPublisher
{
public int PublishCount { get; private set; }
public int FailPublishCount { get; set; }
public Action<LiveEntityRecord>? BeforePublish { get; set; }
public bool Publish(LiveEntityRecord expectedRecord)
{
PublishCount++;
operations.Add("ready");
BeforePublish?.Invoke(expectedRecord);
if (FailPublishCount > 0)
{
FailPublishCount--;
return false;
}
return true;
}
}
private sealed class RecordingOrigin(bool isKnown) : ILiveEntityWorldOriginCoordinator
{
public bool IsKnownValue { get; set; } = isKnown;
public IReadOnlyList<uint> AlreadyLoadedLandblocks { get; set; } = [];
public bool IsKnown => IsKnownValue;
public LiveEntityOriginInitialization TryInitialize(WorldSession.EntitySpawn spawn) =>
new(IsKnownValue, AlreadyLoadedLandblocks);
}
private sealed class RecordingNetworkSink : ILiveEntityNetworkUpdateSink
{
public int ApplyCount { get; private set; }
public Action<SameGenerationCreateObjectEvents>? ApplyAction { get; set; }
public void ApplySameGeneration(SameGenerationCreateObjectEvents events)
{
ApplyCount++;
ApplyAction?.Invoke(events);
}
}
private static WorldSession.EntitySpawn Spawn(
ushort Generation,
ushort PositionSequence,
string Name = "fixture")
{
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, Generation);
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,
Name,
(uint)ItemType.Creature,
null,
0x09000001u,
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
InstanceSequence: Generation,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: PositionSequence,
Physics: physics);
}
private static WorldSession.EntitySpawn CelllessSpawn(
ushort PositionSequence,
uint? parentGuid)
{
var timestamps = new PhysicsTimestamps(
PositionSequence, 1, 1, 1, 0, 1, 0, 1, 1);
PhysicsAttachment? parent = parentGuid is { } guid
? new PhysicsAttachment(guid, LocationId: 1u)
: null;
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.ReportCollisions,
Position: null,
Movement: null,
AnimationFrame: parentGuid is null ? null : 1u,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: parent,
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: null,
SetupTableId: 0x02000001u,
AnimPartChanges: [],
TextureChanges: [],
SubPalettes: [],
BasePaletteId: null,
ObjScale: null,
Name: parentGuid is null ? "pickup" : "parented",
ItemType: (uint)ItemType.Creature,
MotionState: null,
MotionTableId: 0x09000001u,
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: PositionSequence,
ParentGuid: parentGuid,
ParentLocation: parentGuid is null ? null : 1u,
PlacementId: parentGuid is null ? null : 1u,
Physics: physics);
}
}