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 ParentAcceptanceBridge_IsFailFastAndSingleAssignment() { var bridge = new DeferredLiveEntityParentAcceptance(); var update = new ParentEvent.Parsed(1, 2, 3, 4, 5, 6); Assert.Throws(() => bridge.TryAccept(update)); ParentEvent.Parsed accepted = default; bridge.Bind(value => { accepted = value; return true; }); Assert.True(bridge.TryAccept(update)); Assert.Equal(update, accepted); Assert.Throws(() => bridge.Bind(_ => false)); } [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.OnAppearance(ObjDesc(2, 0x04000022u))); 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 AppearanceUpdate_ReplacesOnlyProjectionAndPreservesExactOwners() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); LiveEntityRecord record = fixture.Record; WorldEntity entity = record.WorldEntity!; int applied = 0; fixture.Controller.AppearanceApplied += _ => applied++; Assert.True(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u))); Assert.Same(record, fixture.Record); Assert.Same(entity, record.WorldEntity); Assert.Equal((uint)0x04000022u, record.Snapshot.BasePaletteId); Assert.False(record.AppearanceProjectionSynchronizationPending); Assert.Equal(LiveProjectionPurpose.AppearanceMutation, fixture.Materializer.Calls[^1].Purpose); Assert.Equal(1, fixture.Resources.RegisterCount); Assert.Equal(0, fixture.Resources.UnregisterCount); Assert.Equal(1, applied); } [Fact] public void NestedAppearanceUpdate_ReplaysNewestCanonicalSnapshotAfterOuterPublication() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); int applied = 0; fixture.Controller.AppearanceApplied += _ => applied++; bool nested = false; bool nestedResult = true; fixture.Materializer.BeforeMaterialize = (_, _) => { if (fixture.Materializer.Calls[^1].Purpose is not LiveProjectionPurpose.AppearanceMutation || nested) { return; } nested = true; nestedResult = fixture.Controller.OnAppearance(ObjDesc(3, 0x04000033u)); }; Assert.True(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u))); Assert.False(nestedResult); Assert.Equal((uint)0x04000033u, fixture.Record.Snapshot.BasePaletteId); Assert.False(fixture.Record.AppearanceProjectionSynchronizationPending); Assert.Equal( 2, fixture.Materializer.Calls.Count(call => call.Purpose is LiveProjectionPurpose.AppearanceMutation)); Assert.Equal(1, applied); } [Fact] public void AppearancePublicationFailure_RetainsObligationForLandblockRetry() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); int applied = 0; fixture.Controller.AppearanceApplied += _ => applied++; fixture.Materializer.ThrowAfterMaterializePurposeOnce = LiveProjectionPurpose.AppearanceMutation; Assert.Throws(() => fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u))); Assert.True(fixture.Record.AppearanceProjectionSynchronizationPending); Assert.Equal(0, applied); fixture.Controller.OnLandblockLoaded(Cell); Assert.False(fixture.Record.AppearanceProjectionSynchronizationPending); Assert.Equal((uint)0x04000022u, fixture.Record.Snapshot.BasePaletteId); Assert.Equal(1, fixture.Resources.RegisterCount); Assert.Equal(1, applied); } [Fact] public void AttachedAppearance_UsesRelationshipProjectionWithoutWorldPosition() { const uint parentGuid = 0x70000002u; using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); LiveEntityRecord record = fixture.Record; WorldEntity entity = record.WorldEntity!; Assert.True(fixture.Runtime.TryApplyCreateParent( new CreateParentUpdate(Guid, parentGuid, 1, 0, 1, 2), out _)); Assert.True(fixture.Runtime.CommitStagedParent( new ParentAttachmentRelation( parentGuid, Guid, ParentLocation: 1, PlacementId: 0, ParentInstanceSequence: 0, ChildPositionSequence: 2), out _)); record.ProjectionKind = LiveEntityProjectionKind.Attached; record.FullCellId = 0u; int materializerCalls = fixture.Materializer.Calls.Count; int relationshipCalls = 0; int applied = 0; fixture.Relationships.ApplyAttachedAppearance = (expected, version) => { relationshipCalls++; Assert.Same(record, expected); Assert.True(fixture.Runtime.IsCurrentObjDescAuthority(expected, version)); Assert.Null(expected.Snapshot.Position); Assert.Equal((uint)0x04000022u, expected.Snapshot.BasePaletteId); return true; }; fixture.Controller.AppearanceApplied += _ => applied++; Assert.True(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u))); Assert.Same(entity, record.WorldEntity); Assert.Equal(LiveEntityProjectionKind.Attached, record.ProjectionKind); Assert.False(record.AppearanceProjectionSynchronizationPending); Assert.Equal(materializerCalls, fixture.Materializer.Calls.Count); Assert.Equal(1, relationshipCalls); Assert.Equal(1, applied); } [Theory] [InlineData(1)] [InlineData(0)] public void StaleOrEqualAppearance_HasNoProjectionOrNotificationSideEffects( ushort sequence) { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); int calls = fixture.Materializer.Calls.Count; int applied = 0; fixture.Controller.AppearanceApplied += _ => applied++; Assert.False(fixture.Controller.OnAppearance( ObjDesc(sequence, 0x04000022u))); Assert.Equal(calls, fixture.Materializer.Calls.Count); Assert.Equal(0, applied); Assert.False(fixture.Record.AppearanceProjectionSynchronizationPending); Assert.Null(fixture.Record.Snapshot.BasePaletteId); } [Fact] public void AppearanceSequenceWrap_PublishesExactlyOnce() { using var fixture = new Fixture(originKnown: true); WorldSession.EntitySpawn spawn = Spawn(Generation: 1, PositionSequence: 1); spawn = spawn with { Physics = spawn.Physics!.Value with { Timestamps = spawn.Physics.Value.Timestamps with { ObjDesc = 0xFFFF, }, }, }; fixture.Controller.OnCreate(spawn); int applied = 0; fixture.Controller.AppearanceApplied += _ => applied++; Assert.True(fixture.Controller.OnAppearance(ObjDesc(0, 0x04000022u))); Assert.False(fixture.Record.AppearanceProjectionSynchronizationPending); Assert.Equal((uint)0x04000022u, fixture.Record.Snapshot.BasePaletteId); Assert.Equal(1, applied); } [Fact] public void FailedAppearance_IsClearedAtGenerationReplacementAndCannotLeak() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); LiveEntityRecord retired = fixture.Record; fixture.Materializer.SkipMaterializationCount = 1; Assert.False(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u))); Assert.True(retired.AppearanceProjectionSynchronizationPending); fixture.Controller.OnCreate(Spawn(Generation: 2, PositionSequence: 1)); LiveEntityRecord replacement = fixture.Record; fixture.Controller.OnLandblockLoaded(Cell); Assert.NotSame(retired, replacement); Assert.False(retired.AppearanceProjectionSynchronizationPending); Assert.False(retired.AppearanceHydrationInProgress); Assert.False(replacement.AppearanceProjectionSynchronizationPending); Assert.Null(replacement.Snapshot.BasePaletteId); } [Fact] public void Pickup_LeavesWorldWithoutEndingLogicalOrResourceIdentity() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); LiveEntityRecord record = fixture.Record; WorldEntity entity = record.WorldEntity!; fixture.Relationships.OnUnparentAction = _ => fixture.Runtime.WithdrawLiveEntityProjection(record) ? ChildUnparentDisposition.Completed : ChildUnparentDisposition.Superseded; Assert.True(fixture.Controller.OnPickup( new PickupEvent.Parsed(Guid, 1, 2))); Assert.Same(record, fixture.Record); Assert.Same(entity, record.WorldEntity); Assert.Equal(0u, record.FullCellId); Assert.False(record.IsSpatiallyProjected); Assert.True(record.ResourcesRegistered); Assert.Equal(1, fixture.Resources.RegisterCount); Assert.Equal(0, fixture.Resources.UnregisterCount); Assert.Contains($"unparent:{Guid:X8}", fixture.Operations); } [Fact] public void PositionAfterPickup_ReentersWithSameEntityBodyAndResources() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); LiveEntityRecord record = fixture.Record; WorldEntity entity = record.WorldEntity!; var body = new PhysicsBody(); record.PhysicsBody = body; fixture.Relationships.OnUnparentAction = _ => fixture.Runtime.WithdrawLiveEntityProjection(record) ? ChildUnparentDisposition.Completed : ChildUnparentDisposition.Superseded; Assert.True(fixture.Controller.OnPickup( new PickupEvent.Parsed(Guid, 1, 2))); int applied = 0; fixture.Controller.AppearanceApplied += _ => applied++; Assert.False(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u))); Assert.True(record.AppearanceProjectionSynchronizationPending); Assert.Equal(0, applied); Assert.True(fixture.Runtime.TryApplyPosition( new WorldSession.EntityPositionUpdate( Guid, new CreateObject.ServerPosition( Cell, 14f, 12f, 6f, 1f, 0f, 0f, 0f), Velocity: null, PlacementId: 0, IsGrounded: true, InstanceSequence: 1, PositionSequence: 3, TeleportSequence: 0, ForcePositionSequence: 0), isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out _, out WorldSession.EntitySpawn accepted, out _)); ulong positionAuthority = record.PositionAuthorityVersion; Assert.True(fixture.Controller.RecoverProjection( record, positionAuthority, accepted)); Assert.Same(entity, record.WorldEntity); Assert.Same(body, record.PhysicsBody); Assert.True(record.IsSpatiallyProjected); Assert.False(record.AppearanceProjectionSynchronizationPending); Assert.Equal((uint)0x04000022u, record.Snapshot.BasePaletteId); Assert.Equal(1, applied); Assert.Equal(1, fixture.Resources.RegisterCount); Assert.Equal(0, fixture.Resources.UnregisterCount); } [Fact] public void ParentEvent_IsRetainedByRelationshipOwnerAndAcceptedThroughHydrationGate() { using var fixture = new Fixture(originKnown: true); const uint parentGuid = 0x70000002u; fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); fixture.Runtime.RegisterLiveEntity( Spawn(Generation: 4, PositionSequence: 1) with { Guid = parentGuid }); bool accepted = false; fixture.Relationships.OnParentAction = update => accepted = fixture.Controller.TryAcceptParentForProjection(update); var update = new ParentEvent.Parsed( parentGuid, Guid, ParentLocation: 1, PlacementId: 0, ParentInstanceSequence: 4, ChildPositionSequence: 2); fixture.Controller.OnParent(update); Assert.True(accepted); Assert.Equal((ushort)2, fixture.Record.Snapshot.PositionSequence); Assert.Contains($"parent:{Guid:X8}", fixture.Operations); } [Fact] public void SameGenerationCreateParent_AdvancesSharedPositionAndPublishesRelation() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); var update = new CreateParentUpdate( Guid, ParentGuid: 0x70000002u, ParentLocation: 3, PlacementId: 0, ChildInstanceSequence: 1, ChildPositionSequence: 2); Assert.True(fixture.Controller.OnCreateParentAccepted(update)); Assert.Equal((ushort)2, fixture.Record.Snapshot.PositionSequence); Assert.Contains($"create-parent:{Guid:X8}", fixture.Operations); } [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(() => 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(() => 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(() => 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(() => fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1))); LiveEntityRecord record = fixture.Record; WorldEntity partial = Assert.IsType(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(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(() => 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(() => 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( LiveEntityReadyCandidate.Capture(record)); }; 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( LiveEntityReadyCandidate.Capture(record)); }; 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( LiveEntityReadyCandidate.Capture(expected))); Assert.Equal(expectedPrepare, prepare); Assert.Equal(expectedPresent, present); Assert.Equal(expectedReplay, replay); Assert.Same(expected, fixture.Record); Assert.Equal(2UL, fixture.Record.CreateIntegrationVersion); } [Theory] [InlineData(0, 1, 0, 0)] [InlineData(1, 1, 1, 0)] [InlineData(2, 1, 1, 1)] public void ReadyPublisher_RejectsSameIdentityProjectionChangeBetweenStages( int withdrawalStage, 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; LiveEntityReadyCandidate candidate = LiveEntityReadyCandidate.Capture(expected); int prepare = 0; int present = 0; int replay = 0; bool withdrawn = false; bool Stage(int stage) { if (!withdrawn && withdrawalStage == stage) { withdrawn = true; Assert.True(fixture.Runtime.TryApplyPickup( new PickupEvent.Parsed(Guid, 1, 2), out _)); Assert.True(fixture.Runtime.WithdrawLiveEntityProjection(expected)); } return true; } var publisher = new LiveEntityReadyPublisher( fixture.Runtime, _ => { prepare++; return Stage(0); }, _ => { present++; return Stage(1); }, _ => { replay++; return Stage(2); }); Assert.False(publisher.Publish(candidate)); Assert.Equal(expectedPrepare, prepare); Assert.Equal(expectedPresent, present); Assert.Equal(expectedReplay, replay); Assert.Same(expected, fixture.Record); Assert.False(expected.IsSpatiallyProjected); } [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, LiveEntityReadyCandidate.Capture(record) with { CreateIntegrationVersion = capturedCreateIntegrationVersion, WorldEntity = entity, }, _ => published = true)); Assert.False(published); } [Fact] public void EquippedChildReadyCandidate_RejectsPickupWithdrawalByPoseCallback() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); LiveEntityRecord record = fixture.Record; LiveEntityReadyCandidate candidate = LiveEntityReadyCandidate.Capture(record); // Models ProjectionPoseReady accepting Pickup and withdrawing the // retained projection before EntityReady is emitted. Assert.True(fixture.Runtime.TryApplyPickup( new PickupEvent.Parsed(Guid, 1, 2), out _)); Assert.True(fixture.Runtime.WithdrawLiveEntityProjection(record)); bool published = false; Assert.False(EquippedChildRenderController.PublishEntityReadyExact( fixture.Runtime, candidate, _ => 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(() => deferred.ApplySameGeneration(events)); var first = new RecordingNetworkSink(); deferred.Bind(first); deferred.ApplySameGeneration(events); Assert.Equal(1, first.ApplyCount); Assert.Throws(() => deferred.Bind(new RecordingNetworkSink())); } private const uint Guid = 0x70000001u; private const uint Cell = 0x01010001u; private sealed class Fixture : IDisposable { public readonly List 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? 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())); 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 operations) : ILiveEntityProjectionMaterializer { public readonly List<(LiveProjectionPurpose Purpose, ushort Generation)> Calls = []; public readonly List PositionSequences = []; public ulong? InstalledCreateIntegrationVersion { get; private set; } public Action? BeforeMaterialize { get; set; } public Action? 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 operations) : ILiveEntityRelationshipProjection { public Action? OnSpawnAction { get; set; } public Action? OnParentAction { get; set; } public Action? OnCreateParentAction { get; set; } public Func? OnUnparentAction { get; set; } public Func? ApplyAttachedAppearance { get; set; } public void OnSpawn(WorldSession.EntitySpawn spawn) { operations.Add($"relationship:{spawn.InstanceSequence}"); OnSpawnAction?.Invoke(spawn); } public void OnParent(ParentEvent.Parsed update) { operations.Add($"parent:{update.ChildGuid:X8}"); OnParentAction?.Invoke(update); } public void OnCreateParentAccepted(CreateParentUpdate update) { operations.Add($"create-parent:{update.ChildGuid:X8}"); OnCreateParentAction?.Invoke(update); } public ChildUnparentDisposition OnChildBecameUnparented(uint childGuid) { operations.Add($"unparent:{childGuid:X8}"); return OnUnparentAction?.Invoke(childGuid) ?? ChildUnparentDisposition.Completed; } public bool TryApplyAttachedAppearance( LiveEntityRecord record, ulong objDescAuthorityVersion) { operations.Add($"attached-appearance:{record.ServerGuid:X8}"); return ApplyAttachedAppearance?.Invoke(record, objDescAuthorityVersion) ?? false; } } private sealed class RecordingReadyPublisher(List operations) : ILiveEntityReadyPublisher { public int PublishCount { get; private set; } public int FailPublishCount { get; set; } public Action? BeforePublish { get; set; } public bool Publish(LiveEntityReadyCandidate candidate) { LiveEntityRecord expectedRecord = candidate.Record; 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 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? 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 ObjDescEvent.Parsed ObjDesc( ushort sequence, uint basePaletteId) => new( Guid, new CreateObject.ModelData( basePaletteId, Array.Empty(), Array.Empty(), Array.Empty()), InstanceSequence: 1, ObjDescSequence: sequence); 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); } }