using System.Numerics; using AcDream.App.Input; using AcDream.App.Rendering; using AcDream.App.Rendering.Scene; using AcDream.App.Streaming; using AcDream.App.UI; 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; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; 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 ParentAcceptanceOwnedBindingReleasesExactlyAndAllowsRebind() { var bridge = new DeferredLiveEntityParentAcceptance(); Func first = _ => true; Func second = _ => false; IDisposable firstBinding = bridge.BindOwned(first); Assert.True(bridge.TryAccept(default)); Assert.Throws(() => bridge.BindOwned(second)); firstBinding.Dispose(); using IDisposable secondBinding = bridge.BindOwned(second); firstBinding.Dispose(); Assert.False(bridge.TryAccept(default)); } [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)); RuntimeEntityRecord canonical = fixture.Canonical; Assert.False(fixture.Runtime.TryGetRecord(Guid, out _)); Assert.NotNull(canonical.LocalEntityId); Assert.Equal(0, fixture.Resources.RegisterCount); Assert.Empty(fixture.Materializer.Calls); fixture.Origin.IsKnownValue = true; fixture.Controller.OnLandblockLoaded(Cell); LiveEntityRecord record = fixture.Record; 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); } /// /// #319 F2 (contract §3.1). Dual-parent-class matrix: a player-range /// parent with a nonzero incarnation and a creature/static-range parent /// at sequence 0. #319 existed precisely because every pre-existing /// test in this file used only sequence-0 (non-player) parents - a /// matrix in which the two classes could diverge silently is the /// defect's habitat. /// [Theory] [InlineData(0x50000123u, (ushort)7)] [InlineData(0x70000099u, (ushort)0)] public void CommittedChild_IsExcludedFromLandblockLoadCandidates( uint parentGuid, ushort parentInstanceSequence) { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); LiveEntityRecord record = fixture.Record; WorldEntity entity = record.WorldEntity!; // Simulate the post-#319-fix state: a committed parent relation // exists for this child (D1/D2, exercised elsewhere, would give it // a nonzero canonical cell equal to the parent's). Pre-fix the // relation DID commit for a player-class parent too - HasCommittedParent // is child-keyed and CommitProjection succeeded under (playerGuid, 0) // (retail review R3). // // The two InlineData rows differ in what was reachable PRE-fix // (retail review round 2, D2 - qualified per parent class): // - PLAYER row (0x50000123u): the relation committed under the // WRONG key (playerGuid, 0), so D1's `parent.Incarnation == // parentInstanceSequence` gate never matched and the child's // FullCellId stayed 0 forever pre-fix - this candidate gate // this test exercises never saw a live population for this row // before the fix. // - CREATURE row (0x70000099u): the hardcoded 0 genuinely MATCHES // a creature-class parent's real incarnation, so D1 already // passed pre-fix and this child ALREADY carried a nonzero // FullCellId at HEAD - this row's scenario was already LIVE // pre-fix, and the child was already a hydration candidate // taking the legacy RebucketLiveEntity path this gate now // excludes (contract §3.1's correction). Both rows exercise the // SAME gate; only their pre-fix reachability differs. var relation = new ParentAttachmentRelation( parentGuid, Guid, ParentLocation: 0, PlacementId: 0, parentInstanceSequence, ChildPositionSequence: 1); fixture.Runtime.ParentAttachments.AcceptCreateObjectRelation(relation); Assert.True(fixture.Runtime.ParentAttachments.CommitProjection(relation)); // Decoy: the legacy RebucketLiveEntity branch below would overwrite // this back to the canonical cell (0x01010001u, matching `Cell`) if // this candidate were not excluded - see // LiveEntityRuntime.RebucketLiveEntity's unconditional // `entity.ParentCellId = spatialCellOrLandblockId` write. entity.ParentCellId = 0x02020002u; fixture.Controller.OnLandblockLoaded(Cell); // #319 F2: a committed child is never independently re-placed by // landblock load (retail's update_object `parent != 0` early-out, // @0x00515D40) - the decoy must survive untouched. Sabotage: // deleting the `HasCommittedParent` gate in // LiveEntityHydrationController.OnLandblockLoaded makes this fail // for BOTH rows (the decoy gets corrected back to 0x01010001u). Assert.Equal(0x02020002u, entity.ParentCellId); } [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); RuntimeEntityRecord canonical = fixture.Canonical; Assert.False(fixture.Runtime.TryGetRecord(Guid, out _)); Assert.NotNull(canonical.LocalEntityId); Assert.True(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u))); LiveEntityRecord record = fixture.Record; 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!; // C3c/C3b: the first-entry conductor constructs the canonical // physics body at Create (retail ACCObjectMaint::CreateObject / // set_description). Capture that existing body — the identity the // pickup/re-enter cycle must preserve — instead of seeding one. PhysicsBody body = fixture.Runtime.GetOrCreatePhysicsBody( record.ServerGuid, static _ => throw new InvalidOperationException( "The conductor-built canonical body should already exist.")); 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)); // C5b (#275): the accepted merge no longer stamps the wire cell onto // the canonical record, so the recovery above rebuilds the logical / // render-resource owner but installs no spatial bucket — the // residence-managed materializer's self-projection branch // (DatLiveEntityProjectionMaterializer, `FullCellId != 0`) correctly // declines to project from an UNPLACED wire claim. Production // installs the bucket a few statements later in the SAME // OnPosition call, at the prologue rebucket // (LiveEntityNetworkUpdateController -> RebucketLiveEntity, W2); // nothing between the recovery call and that rebucket returns early // on an unbucketed record. Drive that exact production step here // rather than asserting a projection at a boundary that no longer // owns it. Assert.True(fixture.Runtime.RebucketLiveEntity(Guid, Cell)); Assert.Same(entity, record.WorldEntity); Assert.Same(body, record.PhysicsBody); Assert.True(record.IsSpatiallyProjected); Assert.Equal(Cell, record.Canonical.FullCellId); 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 PositionAfterInventoryOnlyCreate_ConstructsFirstSpatialProjection() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(CelllessSpawn( PositionSequence: 1, parentGuid: null)); RuntimeEntityRecord canonical = fixture.Canonical; Assert.False(fixture.Runtime.TryGetRecord(Guid, out _)); Assert.Equal(0, fixture.Resources.RegisterCount); Assert.True(fixture.Runtime.TryApplyPosition( PositionUpdate(sequence: 2, x: 14f), isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out _, out _, out _)); ulong authorityVersion = canonical.PositionAuthorityVersion; Assert.True(fixture.Controller.RecoverCanonicalProjection( canonical, authorityVersion, out LiveEntityRecord record)); // C5b (#275): as above — the wire cell is no longer merged onto the // canonical record, so the first spatial projection's BUCKET arrives // at the OnPosition prologue rebucket (W2), not at recovery. The // logical construction, resource registration and ready publication // this test is about all still happen at recovery. Assert.NotNull(record.WorldEntity); Assert.True(fixture.Runtime.RebucketLiveEntity(Guid, Cell)); Assert.True(record.IsSpatiallyProjected); Assert.Equal(Cell, canonical.FullCellId); Assert.True(record.InitialHydrationCompleted); Assert.Equal(1, fixture.Resources.RegisterCount); Assert.Equal(1, fixture.Ready.PublishCount); Assert.Contains( fixture.Materializer.Calls, call => call.Purpose is LiveProjectionPurpose.SpatialRecovery); Assert.True(fixture.Controller.RecoverCanonicalProjection( canonical, authorityVersion, out LiveEntityRecord same)); Assert.Same(record, same); Assert.Equal(1, fixture.Resources.RegisterCount); Assert.Equal(1, fixture.Ready.PublishCount); } [Fact] public void CanonicalProjectionRecovery_RejectsSupersededPositionAuthority() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(CelllessSpawn( PositionSequence: 1, parentGuid: null)); RuntimeEntityRecord canonical = fixture.Canonical; Assert.True(fixture.Runtime.TryApplyPosition( PositionUpdate(sequence: 2, x: 14f), isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out _, out _, out _)); ulong staleAuthorityVersion = canonical.PositionAuthorityVersion; Assert.True(fixture.Runtime.TryApplyPosition( PositionUpdate(sequence: 3, x: 18f), isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out _, out _, out _)); Assert.False(fixture.Controller.RecoverCanonicalProjection( canonical, staleAuthorityVersion, out _)); Assert.False(fixture.Runtime.TryGetRecord(Guid, out _)); Assert.Equal(0, fixture.Resources.RegisterCount); Assert.Equal(0, fixture.Ready.PublishCount); } [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))); RuntimeEntityRecord canonical = fixture.Canonical; Assert.False(fixture.Runtime.TryGetRecord(Guid, out _)); Assert.NotNull(canonical.LocalEntityId); Assert.Equal(1, resources.RegisterCount); Assert.Equal(1, resources.UnregisterCount); fixture.Controller.OnLandblockLoaded(Cell); LiveEntityRecord record = fixture.Record; 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 UnknownDelete_ForgetsOnlyPendingOwnerState() { using var fixture = new Fixture(originKnown: true); Assert.False(fixture.Controller.OnDelete( new DeleteObject.Parsed(Guid, InstanceSequence: 1))); Assert.Equal([Guid], fixture.Teardown.ForgottenUnknownOwners); Assert.Equal(0, fixture.Teardown.TearDownCount); Assert.Equal(0, fixture.Runtime.Count); } [Fact] public void StaleDelete_DoesNotEndRemoteLiveIncarnation() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 2, PositionSequence: 1)); LiveEntityRecord current = fixture.Record; Assert.False(fixture.Controller.OnDelete( new DeleteObject.Parsed(Guid, InstanceSequence: 1))); Assert.Same(current, fixture.Record); Assert.NotNull(fixture.Objects.Get(Guid)); Assert.Equal(0, fixture.Teardown.TearDownCount); Assert.Empty(fixture.Teardown.ForgottenUnknownOwners); } [Fact] public void LocalPlayerDelete_IsSideEffectFreeBeforeAndAfterCreate() { using var fixture = new Fixture(originKnown: true, playerGuid: Guid); Assert.False(fixture.Controller.OnDelete( new DeleteObject.Parsed(Guid, InstanceSequence: 1))); Assert.Empty(fixture.Teardown.ForgottenUnknownOwners); fixture.Controller.OnCreate(Spawn(Generation: 2, PositionSequence: 1)); LiveEntityRecord current = fixture.Record; Assert.False(fixture.Controller.OnDelete( new DeleteObject.Parsed(Guid, InstanceSequence: 2))); Assert.Same(current, fixture.Record); Assert.NotNull(fixture.Objects.Get(Guid)); Assert.Equal(0, fixture.Teardown.TearDownCount); Assert.Empty(fixture.Teardown.ForgottenUnknownOwners); } [Fact] public void ExactDelete_RemovesObjectAndTearsDownOnce() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); Assert.True(fixture.Controller.OnDelete( new DeleteObject.Parsed(Guid, InstanceSequence: 1))); Assert.False(fixture.Runtime.TryGetRecord(Guid, out _)); Assert.Null(fixture.Objects.Get(Guid)); Assert.Equal(1, fixture.Teardown.TearDownCount); Assert.Equal(0, fixture.Runtime.PendingTeardownCount); } [Fact] public void Prune_ReleasesActiveOwnersButRetainsColdAuthoritativeSnapshot() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); ClientObject retainedObject = fixture.Objects.Get(Guid)!; Assert.True(fixture.Controller.OnPrune( new LiveEntityPruneCandidate( fixture.Record.ProjectionKey!.Value, Guid))); Assert.False(fixture.Runtime.TryGetRecord(Guid, out _)); Assert.Same(retainedObject, fixture.Objects.Get(Guid)); Assert.True(fixture.Dormant.Contains(Guid, generation: 1)); Assert.Equal(1, fixture.Teardown.TearDownCount); } [Fact] public void LoadedLandblock_RestoresDormantSnapshotThroughCanonicalHydration() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); Assert.True(fixture.Controller.OnPrune( new LiveEntityPruneCandidate( fixture.Record.ProjectionKey!.Value, Guid))); fixture.Controller.OnLandblockLoaded(Cell); Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord restored)); Assert.Equal((ushort)1, restored.Generation); Assert.NotNull(restored.WorldEntity); Assert.False(fixture.Dormant.Contains(Guid, generation: 1)); Assert.Equal(2, fixture.Resources.RegisterCount); Assert.Equal(1, fixture.Resources.UnregisterCount); Assert.Equal(2, fixture.Ready.PublishCount); } [Fact] public void ExactDelete_RemovesDormantSnapshotAndRetainedObject() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); Assert.True(fixture.Controller.OnPrune( new LiveEntityPruneCandidate( fixture.Record.ProjectionKey!.Value, Guid))); Assert.True(fixture.Controller.OnDelete( new DeleteObject.Parsed(Guid, InstanceSequence: 1))); Assert.False(fixture.Dormant.Contains(Guid, generation: 1)); Assert.Null(fixture.Objects.Get(Guid)); fixture.Controller.OnLandblockLoaded(Cell); Assert.False(fixture.Runtime.TryGetRecord(Guid, out _)); } [Fact] public void DormantNewerGeneration_RejectsStaleCreateAndAcceptsReplacement() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 2, PositionSequence: 1)); Assert.True(fixture.Controller.OnPrune( new LiveEntityPruneCandidate( fixture.Record.ProjectionKey!.Value, Guid))); fixture.Controller.OnCreate(Spawn( Generation: 1, PositionSequence: 2, Name: "stale")); Assert.False(fixture.Runtime.TryGetRecord(Guid, out _)); Assert.True(fixture.Dormant.Contains(Guid, generation: 2)); fixture.Controller.OnCreate(Spawn( Generation: 3, PositionSequence: 1, Name: "replacement")); Assert.Equal((ushort)3, fixture.Record.Generation); Assert.Equal("replacement", fixture.Objects.Get(Guid)!.Name); Assert.False(fixture.Dormant.Contains(Guid, generation: 2)); } [Fact] public void DormantSameGenerationCreate_UsesRetainedTimestampGate() { using var fixture = new Fixture(originKnown: true); WorldSession.EntitySpawn accepted = Spawn( Generation: 1, PositionSequence: 5); fixture.Controller.OnCreate(accepted); Assert.True(fixture.Controller.OnPrune( new LiveEntityPruneCandidate( fixture.Record.ProjectionKey!.Value, Guid))); CreateObject.ServerPosition stalePosition = accepted.Position!.Value with { LandblockId = 0x02020001u, PositionX = 99f, }; PhysicsSpawnData stalePhysics = accepted.Physics!.Value with { Position = stalePosition, Timestamps = accepted.Physics.Value.Timestamps with { Position = 4, }, }; fixture.Controller.OnCreate(accepted with { Position = stalePosition, PositionSequence = 4, Physics = stalePhysics, }); Assert.Equal(accepted.Position, fixture.Record.Snapshot.Position); Assert.Equal((ushort)5, fixture.Record.Snapshot.PositionSequence); } [Fact] public void DormantReplacementCreate_ReplacesAbsentOldQualities() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate( Spawn(Generation: 1, PositionSequence: 1) with { Value = 123, }); Assert.True(fixture.Controller.OnPrune( new LiveEntityPruneCandidate( fixture.Record.ProjectionKey!.Value, Guid))); fixture.Controller.OnCreate( Spawn(Generation: 2, PositionSequence: 1) with { Value = null, }); Assert.Equal(0, fixture.Objects.Get(Guid)!.Value); } [Fact] public void Delete_DrainsIndependentFailuresAndRetryDoesNotReplayObjectRemoval() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); int objectRemovedCalls = 0; fixture.Objects.ObjectRemoved += _ => { objectRemovedCalls++; throw new InvalidOperationException("fixture object-table observer failure"); }; bool failTeardown = true; fixture.Teardown.TearDownAction = _ => { if (!failTeardown) return; failTeardown = false; throw new InvalidOperationException("fixture component teardown failure"); }; AggregateException error = Assert.Throws(() => fixture.Controller.OnDelete( new DeleteObject.Parsed(Guid, InstanceSequence: 1))); Assert.Contains(error.Flatten().InnerExceptions, exception => exception.Message.Contains("object-table observer", StringComparison.Ordinal)); Assert.Contains(error.Flatten().InnerExceptions, exception => exception.Message.Contains("component teardown", StringComparison.Ordinal)); Assert.False(fixture.Runtime.TryGetRecord(Guid, out _)); Assert.Null(fixture.Objects.Get(Guid)); Assert.Equal(1, objectRemovedCalls); Assert.Equal(1, fixture.Runtime.PendingTeardownCount); Assert.Equal(1, fixture.Controller.RetryPendingTeardowns()); Assert.Equal(0, fixture.Runtime.PendingTeardownCount); Assert.Equal(1, objectRemovedCalls); Assert.Equal(2, fixture.Teardown.TearDownCount); } [Fact] public void DeleteCallback_CanInstallNewerGenerationWithoutOldCleanupRemovingIt() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); LiveEntityRecord old = fixture.Record; bool replaced = false; fixture.Teardown.TearDownAction = record => { Assert.Same(old, record); if (replaced) return; replaced = true; LiveEntityRegistrationResult result = fixture.Runtime.RegisterLiveEntity( Spawn(Generation: 2, PositionSequence: 2, Name: "replacement")); Assert.True(result.LogicalRegistrationCreated); }; Assert.True(fixture.Controller.OnDelete( new DeleteObject.Parsed(Guid, InstanceSequence: 1))); RuntimeEntityRecord replacement = fixture.Canonical; Assert.Equal((ushort)2, replacement.Generation); Assert.Equal("replacement", replacement.Snapshot.Name); Assert.NotNull(replacement.LocalEntityId); Assert.False(fixture.Runtime.TryGetRecord(Guid, out _)); 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. // C3c: the failed Create transaction unwound before OnCreateCore's // own drive pump ran, leaving the residence pending with FullCellId // 0 (streaming callbacks key candidates off the committed cell). In // production the per-frame first-entry pump completes the conductor // independently of the failed hydration transaction; model that // pump here, then let the streaming callback recover the partial // projection exactly as before. fixture.FirstEntry.DriveAll(); 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); // C3c: the nested fresher same-generation Create is admitted into // the outer create's ACTIVE residence FIFO (AD-59) and its facts // commit at the executor drain, in array order. The single // materialization therefore runs from the admission-frozen seq-1 // create; the seq-2 facts (including the name asserted above) land // through the drain and bind via the completion receipt. Assert.Equal((ushort)1, 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); // C3c: the nested fresher same-generation Create is admitted into // the replacement generation's ACTIVE residence FIFO (AD-59) and its // facts commit at the executor drain, in array order. The // materialization therefore runs from the admission-frozen seq-2 // replacement create; the seq-3 facts (including the name asserted // above) land through the drain and never re-materialize. Assert.Equal((ushort)2, fixture.Materializer.PositionSequences[^1]); Assert.DoesNotContain( (ushort)3, fixture.Materializer.PositionSequences); } [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); // C3c: a post-residence same-generation Create is description-only // at registration — its position churn flows through the // freshness-gated events tail (which is what makes RecoverProjection // return false above), and create authority advances only inside the // residence transaction. No drift means no nested // CreateSupersessionRecovery re-materialization: the recovery's own // SpatialRecovery attempt stays the last call at the original // installed version. Assert.Equal([1, 1], fixture.Materializer.PositionSequences); Assert.Equal( LiveProjectionPurpose.SpatialRecovery, fixture.Materializer.Calls[^1].Purpose); Assert.Equal(1UL, 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; // C3c: a fresher same-generation CreateObject no longer advances // create authority at registration (its advance lands at the // residence drain's WeenieDescription stage). Model that exact // advance directly so the drift-retry machinery under test still // fires. // C3c-R1 F3 (coordinator resolution): this hand-call is an // honest MODEL of the executor drain's advance // (RuntimeInitialCreateContinuationExecutor // .ApplyWeenieDescriptionAction — the sole production site, // source-pinned by C3cR1F3DriftModelSourcePinTests). A nested // production OnCreate can no longer reach it here: // post-residence ExistingGeneration registration is // description-only (RuntimeEntityObjectLifetime gates the // advance on !beginInitialResidence, :660-665) and // ConsumeExecuted already removed the completed residence // entry, closing the FIFO-adoption path. record.Canonical.AdvanceCreateAuthority(); }; 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); // C3c: the retry retransmit (a post-residence same-generation // Create) no longer advances create authority itself, so both retry // paths install the drift probe's version (2), not a // retransmit-advanced 3. Assert.Equal( 2UL, 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; // C3c: a fresher same-generation CreateObject no longer advances // create authority at registration (its advance lands at the // residence drain's WeenieDescription stage). Model that exact // advance directly so the drift-retry machinery under test still // fires. // C3c-R1 F3 (coordinator resolution): this hand-call is an // honest MODEL of the executor drain's advance // (RuntimeInitialCreateContinuationExecutor // .ApplyWeenieDescriptionAction — the sole production site, // source-pinned by C3cR1F3DriftModelSourcePinTests). A nested // production OnCreate can no longer reach it here: // post-residence ExistingGeneration registration is // description-only (RuntimeEntityObjectLifetime gates the // advance on !beginInitialResidence, :660-665) and // ConsumeExecuted already removed the completed residence // entry, closing the FIFO-adoption path. record.Canonical.AdvanceCreateAuthority(); }; 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)); RuntimeEntityRecord canonical = fixture.Canonical; Assert.NotNull(canonical.LocalEntityId); Assert.False(fixture.Runtime.TryGetRecord(Guid, out _)); 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)); LiveEntityRecord record = fixture.Record; 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); // C3c: a Create whose parent is not addressable is now queued under // the parent's GUID (retail QueueBlobForObject) instead of applying // immediately. Register the parent so the nested parented Create // routes exactly as before. fixture.Runtime.RegisterLiveEntity( Spawn(Generation: 1, PositionSequence: 1) with { Guid = parentGuid }); 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)); // C3c: mirror the production relationship owner // (EquippedChildRenderController.TryAttach), which converts a // residence-managed child's sticky residence to LegacyImmediate // at the world -> attached kind transition. if (record.MaterializationResidence is AcDream.App.World.LiveEntityMaterializationResidence .AwaitRuntimePlacement) { record.MaterializationResidence = AcDream.App.World .LiveEntityMaterializationResidence.LegacyImmediate; } 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)); // C3c: a post-residence same-generation Create no longer // advances create authority at registration (the advance lands // at a residence drain's WeenieDescription stage). Model that // advance directly so the supersession-recovery machinery under // test still fires and completes at the attached-ready boundary. record.Canonical.AdvanceCreateAuthority(); }; 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); // C3c: a Create whose parent is not addressable is now queued under // the parent's GUID (retail QueueBlobForObject) instead of applying // immediately. Register the parent so the initial parented Create // routes exactly as before — placed in a DIFFERENT landblock so this // scaffolding identity is not itself a candidate for the recovered // landblock's projection sweep (the child's RegisterCount assertions // count only the child's resources). WorldSession.EntitySpawn parentSpawn = Spawn(Generation: 1, PositionSequence: 1) with { Guid = parentGuid }; var parentPosition = new CreateObject.ServerPosition( 0x01020001u, 10f, 10f, 5f, 1f, 0f, 0f, 0f); fixture.Runtime.RegisterLiveEntity(parentSpawn with { Position = parentPosition, Physics = parentSpawn.Physics!.Value with { Position = parentPosition, }, }); fixture.Ready.FailPublishCount = 2; fixture.Network.ApplyAction = events => { if (events.Parent is { } parent) fixture.Runtime.TryApplyCreateParent(parent, out _); }; fixture.Relationships.OnSpawnAction = _ => { RuntimeEntityRecord canonical = fixture.Canonical; if (fixture.Runtime.TryGetRecord( Guid, out LiveEntityRecord _)) return; WorldEntity? attached = fixture.Runtime.MaterializeLiveEntity( canonical, Cell, id => new WorldEntity { Id = id, ServerGuid = Guid, SourceGfxObjOrSetupId = 0x02000001u, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = [], ParentCellId = Cell, }, LiveEntityProjectionKind.Attached, initializeProjection: null, out LiveEntityRecord? record); Assert.NotNull(attached); Assert.NotNull(record); 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; // C3c: a fresher same-generation CreateObject no longer // advances create authority at registration — its advance // lands at the residence drain's WeenieDescription stage. // Model that exact advance directly so the between-stage // revalidation guard stays covered. // C3c-R1 F3: honest MODEL of the executor drain's advance // (ApplyWeenieDescriptionAction — the sole production site, // source-pinned by C3cR1F3DriftModelSourcePinTests); a // nested production OnCreate can no longer reach it — // post-residence registration is description-only // (RuntimeEntityObjectLifetime :660-665, // !beginInitialResidence gate) and ConsumeExecuted already // removed the completed residence entry. expected.Canonical.AdvanceCreateAuthority(); } 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); } [Theory] [InlineData(true)] [InlineData(false)] public void ReadyPublisher_PublishesRenderProjectionAfterExistingReadyStages( bool projectionAccepted) { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); LiveEntityRecord record = fixture.Record; var operations = new List(); var projection = new RecordingLiveProjectionSink( _ => { operations.Add("projection"); return projectionAccepted; }); var publisher = new LiveEntityReadyPublisher( fixture.Runtime, _ => { operations.Add("effects-owner"); return true; }, _ => { operations.Add("state"); return true; }, _ => { operations.Add("effects-replay"); return true; }, projection); bool result = publisher.Publish( LiveEntityReadyCandidate.Capture(record)); Assert.Equal(projectionAccepted, result); Assert.Equal( ["effects-owner", "state", "effects-replay", "projection"], operations); } [Fact] public void ReadyPublisher_RevalidatesAfterReentrantRenderProjectionCallback() { using var fixture = new Fixture(originKnown: true); fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1)); LiveEntityReadyCandidate candidate = LiveEntityReadyCandidate.Capture(fixture.Record); var projection = new RecordingLiveProjectionSink( _ => { // C3c: a fresher same-generation CreateObject's authority // advance now lands at the residence drain's // WeenieDescription stage; model that advance directly. // C3c-R1 F3: honest MODEL of the executor drain's advance // (ApplyWeenieDescriptionAction — the sole production site, // source-pinned by C3cR1F3DriftModelSourcePinTests); a // nested production OnCreate can no longer reach it — // post-residence registration is description-only // (RuntimeEntityObjectLifetime :660-665, // !beginInitialResidence gate) and ConsumeExecuted already // removed the completed residence entry. fixture.Record.Canonical.AdvanceCreateAuthority(); return true; }); var publisher = new LiveEntityReadyPublisher( fixture.Runtime, static _ => true, static _ => true, static _ => true, projection); Assert.False(publisher.Publish(candidate)); 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 observing a fresher // same-generation CreateObject's authority advance before // EntityReady is emitted. C3c: that advance now lands at the // residence drain's WeenieDescription stage // (AdvanceCreateAuthority), not at registration; model it directly. // C3c-R1 F3: honest MODEL of the executor drain's advance // (ApplyWeenieDescriptionAction — the sole production site, // source-pinned by C3cR1F3DriftModelSourcePinTests); a nested // production OnCreate can no longer reach it — post-residence // registration is description-only (RuntimeEntityObjectLifetime // :660-665, !beginInitialResidence gate) and ConsumeExecuted // already removed the completed residence entry. record.Canonical.AdvanceCreateAuthority(); 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.Single(fixture.Runtime.MaterializedRecords); Assert.Empty(fixture.Runtime.VisibleRecords); } [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())); } [Fact] public void DeferredNetworkOwnedBindingReleasesExactlyAndAllowsRebind() { var deferred = new DeferredLiveEntityNetworkUpdateSink(); var first = new RecordingNetworkSink(); var second = new RecordingNetworkSink(); IDisposable firstBinding = deferred.BindOwned(first); Assert.Throws(() => deferred.BindOwned(second)); firstBinding.Dispose(); using IDisposable secondBinding = deferred.BindOwned(second); firstBinding.Dispose(); deferred.ApplySameGeneration(default); Assert.Equal(0, first.ApplyCount); Assert.Equal(1, second.ApplyCount); } // C4 route 6 (docs/research/2026-08-04-c4-route-6-contract.md): drops and // split-recovery require zero production lines -- C3c already flipped // both hosts' Create paths onto the canonical residence lease, and a // dropped item is byte-for-byte route 1's create classification. These // tests measure that convergence end-to-end through the same Fixture the // rest of this file uses, plus a small real ItemInteractionController // (DropHarness) to drive the actual dispatch surface a player action // uses. [Fact] public void WholeItemDrop_DispatchesNoSplitMarkerAndEntersCanonicalCreatePlacementTransaction() { using var fixture = new Fixture(originKnown: true); var drop = new DropHarness(fixture); const uint itemGuid = 0x50000B01u; fixture.Controller.OnCreate(ItemSpawn( itemGuid, position: null, containerId: DropHarness.PlayerGuid, stackSize: null)); bool dispatched = false; drop.Interaction.WorldDropDispatched += _ => dispatched = true; var payload = new ItemDragPayload( itemGuid, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot()); Assert.True(drop.Interaction.DropToWorld(payload)); // Retail's UIAttemptPutIn3D @0x0058D700 records no split marker for a // whole-item drop (only UIAttemptSplitTo3D / UIAttemptSplitToContainer // do); the split-recovery machinery must stay dormant. Assert.False(dispatched); // The server's CreateObject for the dropped item returns through the // ordinary wire pump -- model that directly, bypassing the (dormant) // split projection entirely, and confirm it lands in the same // canonical create-placement transaction as any other TopLevel // create (RuntimeInitialCreateResidenceState.cs:600-606). A fresh // world-visible incarnation carries a new InstanceSequence (retail's // instance_ts): the item's contained CreateObject and its // now-dropped CreateObject are not the same generation. fixture.Controller.OnCreate(ItemSpawn( itemGuid, position: new CreateObject.ServerPosition(Cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f), containerId: 0u, stackSize: null, instanceSequence: 2)); Assert.True(fixture.Runtime.TryGetRecord(itemGuid, out LiveEntityRecord record)); Assert.NotNull(record.WorldEntity); Assert.True(record.InitialHydrationCompleted); Assert.Equal(1, fixture.Resources.RegisterCount); } [Fact] public void SplitSourceWithRetainedMovementTimestamps_StillRecovers() { // #314 REGRESSION TEST (defect found 2026-08-04 during C4 route 6's // evidence-gathering, fixed in the commit immediately after route 6's // zero-production closure). This test was originally written to // DOCUMENT the throw; it now pins the fix. // // BuildSpawn resets the top-level MovementSequence / // ServerControlSequence to 0 (InventoryWorldDropProjectionController // .cs:200-201) but its Timestamps `with` override list only touches // Position / Teleport / ForcePosition / Instance // (InventoryWorldDropProjectionController.cs:182-188) -- it does NOT // correspondingly reset Physics.Timestamps.Movement / // .ServerControlledMove. Retail's per-object update_times channels // are monotonic counters that do not reset when an item re-enters a // container, so a split source that EVER received a Movement or // ServerControlledMove wire update during an earlier stint with // world presence (e.g. dropped once before, picked back up, split // again) carries nonzero values in exactly those two Timestamps // fields. The resulting spawn then failed // RuntimeEntityObjectLifetime.HasConsistentCreateIdentityAndParent, // and split recovery THREW instead of completing the canonical // create-placement transaction -- which is exactly the property // route 6's "enters the same canonical transaction" claim depends on // holding unconditionally. // // The fix resets Movement/ServerControlledMove inside BuildSpawn's // Timestamps `with` block, alongside the top-level // MovementSequence/ServerControlSequence zeroing that was already // there. Zero is the honest value for a fresh split GUID, which has // no movement history by construction -- not a placation of the // predicate. using var fixture = new Fixture(originKnown: true); var drop = new DropHarness(fixture); const uint sourceGuid = 0x50000C40u; WorldSession.EntitySpawn baseSource = ItemSpawn( sourceGuid, position: null, containerId: DropHarness.PlayerGuid, stackSize: 6); WorldSession.EntitySpawn source = baseSource with { MovementSequence = 2, ServerControlSequence = 6, Physics = baseSource.Physics!.Value with { Timestamps = baseSource.Physics.Value.Timestamps with { Movement = 2, ServerControlledMove = 6, }, }, }; fixture.Controller.OnCreate(source); Assert.True(drop.DispatchSplit(sourceGuid, stackSize: 6, splitAmount: 1)); const uint splitResultGuid = 0x80000D01u; bool recovered = drop.Projection.TryRecoverUnknownPosition( DropPositionUpdate(splitResultGuid, x: 30f)); // Recovers rather than throwing, and reaches the same canonical // create-placement transaction the zero-timestamp sibling // (SplitToWorld_NewGuidPositionRecovery_EntersSameCanonicalTransaction // AsDrop) asserts — the source's retained movement history must not // change the outcome at all. Assert.True(recovered); Assert.True(fixture.Runtime.TryGetSnapshot( splitResultGuid, out WorldSession.EntitySpawn spawn)); Assert.Equal(1, spawn.StackSize); Assert.Equal(0u, spawn.ContainerId); Assert.True(fixture.Runtime.TryGetRecord( splitResultGuid, out LiveEntityRecord record)); Assert.NotNull(record.WorldEntity); // The positive half (#314's actual fix): the split result's movement // channels are zeroed in BOTH projections, not merely consistent with // each other. Asserting only "it didn't throw" would pass against a // fix that made HasConsistentCreateIdentityAndParent lenient instead. Assert.Equal(0, spawn.MovementSequence); Assert.Equal(0, spawn.ServerControlSequence); Assert.Equal(0u, spawn.Physics!.Value.Timestamps.Movement); Assert.Equal(0u, spawn.Physics.Value.Timestamps.ServerControlledMove); } [Fact] public void SplitToWorld_NewGuidPositionRecovery_EntersSameCanonicalTransactionAsDrop() { using var fixture = new Fixture(originKnown: true); var drop = new DropHarness(fixture); const uint sourceGuid = 0x50000C01u; fixture.Controller.OnCreate(ItemSpawn( sourceGuid, position: null, containerId: DropHarness.PlayerGuid, stackSize: 10)); Assert.True(drop.DispatchSplit(sourceGuid, stackSize: 10, splitAmount: 3)); const uint splitResultGuid = 0x80000901u; Assert.False(fixture.Runtime.TryGetSnapshot(splitResultGuid, out _)); bool recovered = drop.Projection.TryRecoverUnknownPosition( DropPositionUpdate(splitResultGuid, x: 15f)); Assert.True(recovered); Assert.True(fixture.Runtime.TryGetSnapshot( splitResultGuid, out WorldSession.EntitySpawn spawn)); Assert.Equal(3, spawn.StackSize); Assert.Equal(0u, spawn.ContainerId); Assert.True(fixture.Runtime.TryGetRecord(splitResultGuid, out LiveEntityRecord record)); Assert.NotNull(record.WorldEntity); Assert.Equal(1, fixture.Resources.RegisterCount); } [Fact] public void SecondSplitDrop_ResolvesIndependentlyWithoutClobberingFirst() { using var fixture = new Fixture(originKnown: true); var drop = new DropHarness(fixture); const uint firstSource = 0x50000C10u; const uint secondSource = 0x50000C20u; fixture.Controller.OnCreate(ItemSpawn( firstSource, position: null, containerId: DropHarness.PlayerGuid, stackSize: 5)); fixture.Controller.OnCreate(ItemSpawn( secondSource, position: null, containerId: DropHarness.PlayerGuid, stackSize: 8)); Assert.True(drop.DispatchSplit(firstSource, stackSize: 5, splitAmount: 2)); const uint firstResult = 0x80000A01u; Assert.True(drop.Projection.TryRecoverUnknownPosition( DropPositionUpdate(firstResult, x: 11f))); // ACE confirms the first split by updating the remaining stack; that // is also what clears ItemInteractionController's one-at-a-time // pending-request gate so a second request can dispatch, exactly as // retail's IsPlayerReadyToMakeInventoryRequest requires. fixture.Objects.UpdateStackSize(firstSource, 3, 0); Assert.True(drop.DispatchSplit(secondSource, stackSize: 8, splitAmount: 4)); const uint secondResult = 0x80000A02u; Assert.True(drop.Projection.TryRecoverUnknownPosition( DropPositionUpdate(secondResult, x: 12f, positionSequence: 6))); Assert.True(fixture.Runtime.TryGetSnapshot( firstResult, out WorldSession.EntitySpawn firstSpawn)); Assert.True(fixture.Runtime.TryGetSnapshot( secondResult, out WorldSession.EntitySpawn secondSpawn)); Assert.Equal(2, firstSpawn.StackSize); Assert.Equal(4, secondSpawn.StackSize); Assert.Equal(11f, firstSpawn.Position!.Value.PositionX); Assert.Equal(12f, secondSpawn.Position!.Value.PositionX); Assert.Equal(2, fixture.Resources.RegisterCount); } [Fact] public void UnknownPositionWithNoPendingSplit_IsRejectedWithoutCreatingAnything() { using var fixture = new Fixture(originKnown: true); var drop = new DropHarness(fixture); const uint unrelatedGuid = 0x80000B01u; bool recovered = drop.Projection.TryRecoverUnknownPosition( DropPositionUpdate(unrelatedGuid, x: 20f)); Assert.False(recovered); Assert.False(fixture.Runtime.TryGetSnapshot(unrelatedGuid, out _)); Assert.Equal(0, fixture.Resources.RegisterCount); } [Fact] public void NewerPositionAfterPendingConsumed_DoesNotReopenOrDoubleRegister() { using var fixture = new Fixture(originKnown: true); var drop = new DropHarness(fixture); const uint sourceGuid = 0x50000C30u; fixture.Controller.OnCreate(ItemSpawn( sourceGuid, position: null, containerId: DropHarness.PlayerGuid, stackSize: 6)); Assert.True(drop.DispatchSplit(sourceGuid, stackSize: 6, splitAmount: 1)); const uint firstUnknown = 0x80000C01u; const uint secondUnknown = 0x80000C02u; Assert.True(drop.Projection.TryRecoverUnknownPosition( DropPositionUpdate(firstUnknown, x: 21f))); // A second, newer Position for a DIFFERENT unknown guid arrives after // the one pending split identity is already consumed. Retail's // static split identity (UIAttemptSplitTo3D @0x0058D850) is // single-slot; there is nothing left to recognize this packet // against, so it must fall through untouched rather than being // mistaken for a second recovery. bool secondRecovered = drop.Projection.TryRecoverUnknownPosition( DropPositionUpdate(secondUnknown, x: 22f, positionSequence: 6)); Assert.False(secondRecovered); Assert.False(fixture.Runtime.TryGetSnapshot(secondUnknown, out _)); Assert.True(fixture.Runtime.TryGetSnapshot(firstUnknown, out _)); Assert.Equal(1, fixture.Resources.RegisterCount); } private static WorldSession.EntitySpawn ItemSpawn( uint guid, CreateObject.ServerPosition? position, uint? containerId, int? stackSize, string name = "test item", ushort instanceSequence = 1) { // Movement / ServerControlledMove timestamps are held at 0 (both here // and at the top-level EntitySpawn projection below) to model the // common baseline of an item that has never received a Movement or // ServerControlledMove wire update while contained -- see // SplitSourceWithRetainedMovementTimestamps_ThrowsInsteadOfRecovering // below for what happens (a discovered defect, not exercised by this // helper) when a split source's retained timestamps are nonzero // instead. var timestamps = new PhysicsTimestamps( 1, 0, 1, 1, 0, 0, 0, 1, instanceSequence); var physics = new PhysicsSpawnData( RawState: (uint)PhysicsStateFlags.ReportCollisions, Position: position, Movement: null, AnimationFrame: null, SetupTableId: 0x02000050u, MotionTableId: 0x09000050u, 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: guid, Position: position, SetupTableId: 0x02000050u, AnimPartChanges: [], TextureChanges: [], SubPalettes: [], BasePaletteId: null, ObjScale: null, Name: name, ItemType: (uint)ItemType.Misc, MotionState: null, MotionTableId: 0x09000050u, PhysicsState: (uint)PhysicsStateFlags.ReportCollisions, WeenieClassId: 273u, StackSize: stackSize, StackSizeMax: 1000, ContainerId: containerId, InstanceSequence: instanceSequence, MovementSequence: 0, ServerControlSequence: 0, PositionSequence: 1, Physics: physics); } private static WorldSession.EntityPositionUpdate DropPositionUpdate( uint guid, float x, ushort positionSequence = 5) => new( guid, new CreateObject.ServerPosition(Cell, x, 12f, 6f, 1f, 0f, 0f, 0f), Velocity: Vector3.Zero, // Open-ground placement, not resting against another object's // contact frame -- matches ItemSpawn's source AnimationFrame: // null. BuildSpawn forwards this straight to the top-level // PlacementId without touching Physics.AnimationFrame // (InventoryWorldDropProjectionController.cs:206), so the two // must already agree in the source for the recovered spawn to // pass HasConsistentCreateIdentityAndParent's // PlacementId == Physics.AnimationFrame check. PlacementId: null, IsGrounded: true, InstanceSequence: 1, PositionSequence: positionSequence, TeleportSequence: 0, ForcePositionSequence: 0); /// /// A minimal, real + /// pair wired to a /// shared , for exercising the actual drop/split /// dispatch surface a player action drives (rather than reaching into /// PendingSplitToWorldProjection directly, which the sibling /// PendingSplitToWorldProjectionTests already covers at the unit level). /// private sealed class DropHarness { public const uint PlayerGuid = 0x50000001u; public readonly ItemInteractionController Interaction; public readonly InventoryWorldDropProjectionController Projection; public readonly StackSplitQuantityState SplitQuantity = new(); public uint SelectedObject; public DropHarness(Fixture fixture) { fixture.Objects.AddOrUpdate(new ClientObject { ObjectId = PlayerGuid, Name = "Player", Type = ItemType.Creature, }); var shared = new InventoryTransactionState(fixture.Objects); var runtimeTransactions = new RuntimeInteractionTransactionState(shared); Interaction = new ItemInteractionController( fixture.Objects, runtimeTransactions, new InteractionState(), playerGuid: () => PlayerGuid, sendUse: null, sendUseWithTarget: null, sendWield: null, sendDrop: _ => { }, sendSplitToWorld: (_, _) => { }, selectedObjectId: () => SelectedObject, stackSplitQuantity: SplitQuantity, playerOnGround: () => true); Projection = new InventoryWorldDropProjectionController( Interaction, fixture.Objects, fixture.Runtime, fixture.Controller, () => 100.0); } public bool DispatchSplit(uint itemGuid, uint stackSize, uint splitAmount) { SelectedObject = itemGuid; SplitQuantity.Reset(stackSize); SplitQuantity.SetValue(splitAmount); var payload = new ItemDragPayload( itemGuid, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot()); return Interaction.DropToWorld(payload); } } private const uint Guid = 0x70000001u; private const uint Cell = 0x01010001u; private sealed class Fixture : IDisposable { public readonly List Operations = []; public readonly RuntimeEntityObjectLifetime EntityObjects = new(); public ClientObjectTable Objects => EntityObjects.Objects; 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 RecordingTeardownCoordinator Teardown = new(); public readonly DormantLiveEntityStore Dormant = new(); public readonly RecordingTimestampPublisher Timestamps; public readonly LiveEntityHydrationController Controller; public Action? PublishTimestampsAction { get => Timestamps.PublishAction; set => Timestamps.PublishAction = value; } public Fixture( bool originKnown, RecordingResources? resources = null, uint playerGuid = 0u) { Resources = resources ?? new RecordingResources(); // C3c: initial-Create registration begins the canonical // residence, whose admission requires a live generation; the // fixture also commits the wire landblock's collision generation // and wires the production first-entry drive pump so each Create // transaction completes its conductor synchronously, exactly // like the composed graphical host. EntityObjects.BindEventContext( static () => new AcDream.Runtime.RuntimeGenerationToken(1UL), static () => 1UL); EntityObjects.Physics.SetPosition.BeginCollisionGeneration( Cell & 0xFFFF0000u, 1UL); EntityObjects.Physics.Engine.AddLandblock( Cell & 0xFFFF0000u, new AcDream.Core.Physics.TerrainSurface( new byte[81], new float[256]), Array.Empty(), Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); EntityObjects.Physics.SetPosition.CommitCollisionGeneration( Cell & 0xFFFF0000u, 1UL, ready: true); // 670f307c: Create frames are landblock-local, so a first-entry // conductor resolves them through Runtime's world frame // (RuntimeSetPositionState.PrepareMover:1526-1544) and parks on // RetrySetupUnavailable until it exists. Only the accepted // local-player Create publishes it // (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570), and // production always registers the player first. Model that // ambient world fact beside the resident landblock above so // these hydration tests keep their exact single-entity ledgers. EntityObjects.Physics.ObserveLocalWorldFrame( Cell, teleportAdvanced: false); Movement = new AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState(); IdentityState = new AcDream.Runtime.Gameplay.RuntimeLocalPlayerIdentityState(); var publication = new AcDream.Runtime.Gameplay .RuntimeLocalPlayerPhysicsPublicationState( EntityObjects.Entities, EntityObjects.Physics, Movement, IdentityState); Movement.AttachPhysicsPublication(publication); EntityObjects.LocalPlayerFirstEntry.BindPublication(publication); IdentityState.ServerGuid = playerGuid; FirstEntry = new AcDream.Runtime.Session.RuntimeFirstEntryDriveController( EntityObjects, new AcDream.Runtime.GameRuntimeClock(), new HydrationNullCollisionSource(), () => AcDream.Runtime.Gameplay.PlayerMovementConstructionOptions.Fallback, static _ => new AcDream.Runtime.Gameplay .RuntimeLocalPlayerPhysicsActivationPreparation( 0.48f, 1.835f, AcDream.Runtime.Gameplay .RuntimeLocalPlayerShadowDisposition.ProvenShapeless)); // C3c: the real per-session placement subscription — without it // the first entity's unacknowledged ExecutorCompleted receipt // wedges the one ordered FIFO and every later Create's conductor // yields AwaitingReceiptAcknowledgement forever. Ack rules mirror // production: Discard/ExecutorCompleted acknowledge-only; // Place/Withdraw stay at the head for the conductor machinery. var spatial = new GpuWorldState(); spatial.AddLandblock(new LoadedLandblock( 0x0101FFFFu, new DatReaderWriter.DBObjs.LandBlock(), Array.Empty())); Runtime = new LiveEntityRuntime( spatial, Resources, Teardown, EntityObjects); _placements = new AcDream.Runtime.Physics .RuntimePlacementProjectionSubscription( EntityObjects.Placements, static () => new AcDream.Runtime.RuntimeGenerationToken(1UL), new FixturePlacementSink(Runtime)); Materializer = new RecordingMaterializer(Runtime, Operations); Relationships = new RecordingRelationships(Operations); Ready = new RecordingReadyPublisher(Operations); Origin = new RecordingOrigin(originKnown); Timestamps = new RecordingTimestampPublisher(Operations); Network.ApplyAction = events => { if (events.Position is not { } position) return; Runtime.TryApplyPosition( position, isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out _, out _, out _); }; var identity = new LocalPlayerIdentityState { ServerGuid = playerGuid, }; var deletion = new LiveEntityDeletionController( Runtime, EntityObjects, Teardown, identity, Dormant); Controller = new LiveEntityHydrationController( Runtime, EntityObjects, new object(), Materializer, Relationships, Ready, Origin, Network, Timestamps, identity, deletion, Dormant, firstEntry: FirstEntry); } public AcDream.Runtime.Session.RuntimeFirstEntryDriveController FirstEntry { get; } private readonly AcDream.Runtime.Physics .RuntimePlacementProjectionSubscription _placements; private sealed class FixturePlacementSink(LiveEntityRuntime runtime) : AcDream.Runtime.Physics.IRuntimePlacementProjectionSink { public bool TryApply( in AcDream.Runtime.Physics.RuntimePlacementProjectionSnapshot projection) { if (projection.Kind is AcDream.Runtime.Physics .RuntimePlacementProjectionKind.Discard) { return true; } if (projection.Kind is AcDream.Runtime.Physics .RuntimePlacementProjectionKind.ExecutorCompleted) { return projection.Token.ExactCellId == 0u || runtime.TryApplyInitialCreateCompletionPresentation( in projection); } return !runtime.HasActiveInitialCreateResidence( projection.Token.Entity) && runtime.TryApplyRuntimePlacementProjection(in projection); } } public AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState Movement { get; } public AcDream.Runtime.Gameplay.RuntimeLocalPlayerIdentityState IdentityState { get; } private sealed class HydrationNullCollisionSource : AcDream.Content.IPreparedCollisionSource { public AcDream.Content.PreparedAssetPresence ProbeCollision( AcDream.Content.Pak.PakAssetType type, uint sourceFileId) => AcDream.Content.PreparedAssetPresence.Available; public AcDream.Content.PreparedCollisionReadResult< AcDream.Core.Physics.FlatSetupCollision> ReadSetupCollision( uint sourceFileId, CancellationToken cancellationToken = default) => AcDream.Content.PreparedCollisionReadResult< AcDream.Core.Physics.FlatSetupCollision>.Loaded( new AcDream.Core.Physics.FlatSetupCollision( System.Collections.Immutable.ImmutableArray< AcDream.Core.Physics.FlatCollisionCylinder>.Empty, [new AcDream.Core.Physics.FlatCollisionSphere( System.Numerics.Vector3.Zero, 0.48f)], height: 0f, radius: 0f, stepUpHeight: 0.4f, stepDownHeight: 0.4f)); public AcDream.Content.PreparedCollisionReadResult< AcDream.Core.Physics.FlatGfxObjCollisionAsset> ReadGfxObjCollision( uint sourceFileId, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public AcDream.Content.PreparedCollisionReadResult< AcDream.Core.Physics.FlatCellStructureCollisionAsset> ReadCellStructureCollision( uint sourceFileId, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public AcDream.Content.PreparedCollisionReadResult< AcDream.Core.Physics.FlatEnvCellTopology> ReadEnvCellTopology( uint sourceFileId, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public AcDream.Content.PreparedCollisionSourceStats CollisionStats => default; public void Dispose() { } } public LiveEntityRecord Record { get { Assert.True(Runtime.TryGetRecord(Guid, out LiveEntityRecord record)); return record; } } public RuntimeEntityRecord Canonical { get { Assert.True(Runtime.TryGetCanonical( Guid, out RuntimeEntityRecord canonical)); return canonical; } } public void Dispose() { try { Runtime.Clear(); } catch { // Individual failure-path tests assert the relevant exception. } } } private sealed class RecordingTimestampPublisher : IAcceptedLocalPhysicsTimestampPublisher { private readonly List _operations; public RecordingTimestampPublisher(List operations) => _operations = operations; public Action? PublishAction { get; set; } public void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps) { _operations.Add($"timestamps:{timestamps.Instance}"); PublishAction?.Invoke(serverGuid, timestamps); } } 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( RuntimeEntityRecord expectedCanonical, WorldSession.EntitySpawn canonicalSpawn, LiveProjectionPurpose purpose, ulong expectedCreateIntegrationVersion, AcDream.App.Rendering.LiveEntityAppearanceUpdateState? appearanceUpdate = null) { Calls.Add((purpose, expectedCanonical.Generation)); PositionSequences.Add(canonicalSpawn.PositionSequence); operations.Add($"materialize:{purpose}:{expectedCanonical.Generation}"); BeforeMaterialize?.Invoke(expectedCanonical, canonicalSpawn); if (!runtime.IsCurrentCreateIntegration( expectedCanonical, 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( expectedCanonical, 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, }, LiveEntityProjectionKind.World, initializeProjection: null, out LiveEntityRecord? expectedRecord, // C3c: mirror the production materializer // (DatLiveEntityProjectionMaterializer.MaterializeProjection), // which materializes route-1 world creates residence-managed. // The legacy-immediate default would commit the wire cell // out-of-band and retire the fresh residence lease before the // fixture's drive pump ever ran (diagnosed RejectedToken). AcDream.App.World.LiveEntityMaterializationResidence .AwaitRuntimePlacement); // C3c: mirror the production materializer's self-projection // branch — when the residence-driven placement already committed // (or a legacy post-residence path committed the cell) before // this sidecar could exist, its completion receipt is gone, so // presentation self-projects from the committed canonical state // through the presentation-only bucket path. if (entity is not null && expectedRecord is not null && runtime.IsCurrentCreateIntegration( expectedCanonical, expectedCreateIntegrationVersion) && expectedCanonical.FullCellId != 0u && !runtime.HasActiveInitialCreateResidence(expectedCanonical) && !runtime.RebucketLiveEntity( canonicalSpawn.Guid, expectedCanonical.FullCellId)) { return false; } if (ThrowAfterMaterializePurposeOnce == purpose) { ThrowAfterMaterializePurposeOnce = null; throw new InvalidOperationException( "fixture supersession projection failure"); } AfterMaterialize?.Invoke(expectedCanonical, canonicalSpawn); return entity is not null && expectedRecord 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 RecordingLiveProjectionSink( Func ready) : ILiveRenderProjectionSink { public bool OnEntityReady(LiveEntityReadyCandidate candidate) => ready(candidate); public void OnProjectionVisibilityChanged( LiveEntityRecord record, bool visible) { } public void OnProjectionPoseReady(uint serverGuid) { } public void OnProjectionRemoved(LiveEntityRecord record) { } public void OnResourceUnregister(WorldEntity entity) { } public void SynchronizeActiveSources() { } } 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 sealed class RecordingTeardownCoordinator : ILiveEntityTeardownCoordinator { public int TearDownCount { get; private set; } public List ForgottenUnknownOwners { get; } = []; public Action? TearDownAction { get; set; } public void TearDown(LiveEntityRecord record) { TearDownCount++; TearDownAction?.Invoke(record); } public void ForgetUnknownOwner(uint serverGuid) => ForgottenUnknownOwners.Add(serverGuid); } private static WorldSession.EntitySpawn Spawn( ushort Generation, ushort PositionSequence, 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); } private static WorldSession.EntityPositionUpdate PositionUpdate( ushort sequence, float x) => new( Guid, new CreateObject.ServerPosition( Cell, x, 12f, 6f, 1f, 0f, 0f, 0f), Velocity: null, PlacementId: 0, IsGrounded: true, InstanceSequence: 1, PositionSequence: sequence, TeleportSequence: 0, ForcePositionSequence: 0); }