using System.Collections; using System.Numerics; using System.Reflection; using AcDream.App.Rendering; using AcDream.App.Rendering.Vfx; using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Content; using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.World; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; namespace AcDream.App.Tests.Rendering; public sealed class EquippedChildProjectionWithdrawalTests { [Fact] public void WithdrawalFailure_DoesNotPublishAttachedProjectionRemoval() { bool published = false; ExactProjectionWithdrawalOutcome outcome = EquippedChildRenderController.WithdrawAttachedProjection( ChildRecord(), positionAuthorityVersion: 1, projectionMutationVersion: 2, (_, _, _) => new ExactProjectionWithdrawalOutcome( ExactProjectionWithdrawalDisposition.Pending, new InvalidOperationException("injected component cleanup failure")), () => { published = true; return true; }); Assert.IsType(outcome.Failure); Assert.Equal(ExactProjectionWithdrawalDisposition.Pending, outcome.Disposition); Assert.False(published); } [Fact] public void ExactIncarnationRejection_DoesNotPublishAttachedProjectionRemoval() { bool published = false; ExactProjectionWithdrawalOutcome outcome = EquippedChildRenderController.WithdrawAttachedProjection( ChildRecord(), positionAuthorityVersion: 1, projectionMutationVersion: 2, (_, _, _) => new ExactProjectionWithdrawalOutcome( ExactProjectionWithdrawalDisposition.Superseded, Failure: null), () => { published = true; return true; }); Assert.True(published); Assert.Equal(ExactProjectionWithdrawalDisposition.Superseded, outcome.Disposition); } [Fact] public void SuccessfulWithdrawal_PublishesLocalProjectionRemovalOnce() { int publications = 0; ExactProjectionWithdrawalOutcome outcome = EquippedChildRenderController.WithdrawAttachedProjection( ChildRecord(), positionAuthorityVersion: 1, projectionMutationVersion: 2, (_, _, _) => new ExactProjectionWithdrawalOutcome( ExactProjectionWithdrawalDisposition.Completed, Failure: null), () => { publications++; return true; }); Assert.Equal(ExactProjectionWithdrawalDisposition.Completed, outcome.Disposition); Assert.Equal(1, publications); } [Fact] public void LogicalReplacement_RemovesExactAttachedMapWithoutWithdrawingReplacement() { int withdrawals = 0; using var fixture = new ControllerFixture( (_, _, _) => { withdrawals++; return new( ExactProjectionWithdrawalDisposition.Completed, Failure: null); }); LiveEntityRecord parent = fixture.Spawn(0x70000200u, generation: 1); LiveEntityRecord oldChild = fixture.Spawn( 0x70000201u, generation: 1, LiveEntityProjectionKind.Attached); fixture.InstallAttached(parent, oldChild); LiveEntityRecord replacement = fixture.Live.RegisterLiveEntity( ControllerFixture.SpawnData(0x70000201u, generation: 2)).Record!; Assert.Empty(fixture.Controller.AttachedEntityIds); Assert.True(fixture.Live.TryGetRecord(0x70000201u, out LiveEntityRecord current)); Assert.Same(replacement, current); Assert.Equal(0, withdrawals); } [Fact] public void LogicalDelete_NotificationFailureLeavesRetryableTombstoneButNoAttachedLeak() { using var fixture = new ControllerFixture((_, _, _) => new(ExactProjectionWithdrawalDisposition.Completed, Failure: null)); LiveEntityRecord parent = fixture.Spawn(0x70000210u, generation: 1); LiveEntityRecord child = fixture.Spawn( 0x70000211u, generation: 1, LiveEntityProjectionKind.Attached); fixture.InstallAttached(parent, child); fixture.Controller.ProjectionRemoved += Throw; Assert.Throws(() => fixture.Live.UnregisterLiveEntity( new DeleteObject.Parsed(0x70000211u, InstanceSequence: 1), isLocalPlayer: false)); Assert.Empty(fixture.Controller.AttachedEntityIds); fixture.Controller.ProjectionRemoved -= Throw; Assert.Equal(1, fixture.Live.RetryPendingTeardowns()); static void Throw(uint _) => throw new InvalidOperationException("injected projection observer failure"); } [Fact] public void PostCommitWithdrawalFailure_RetiresCapturedAttachedMap() { ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { bool committed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); Assert.True(committed); return new( ExactProjectionWithdrawalDisposition.Completed, new InvalidOperationException("observer failed after commit")); }); using (fixture) { LiveEntityRecord parent = fixture.Spawn(0x70000220u, generation: 1); LiveEntityRecord child = fixture.Spawn( 0x70000221u, generation: 1, LiveEntityProjectionKind.Attached); fixture.InstallAttached(parent, child); Assert.Throws(() => fixture.Controller.OnChildBecameUnparented(0x70000221u)); Assert.Empty(fixture.Controller.AttachedEntityIds); Assert.False(child.IsSpatiallyProjected); } } [Fact] public void PendingTopLevelWithdrawal_RetryRunsAcceptedContinuationOnce() { int attempts = 0; ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { attempts++; if (attempts == 1) { return new( ExactProjectionWithdrawalDisposition.Pending, new InvalidOperationException("component cleanup failed")); } bool committed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); return new( committed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); }); using (fixture) { LiveEntityRecord record = fixture.Spawn(0x70000230u, generation: 1); int continuations = 0; Assert.Throws(() => fixture.Controller.OnChildBecameUnparented( 0x70000230u, () => { continuations++; Assert.True(fixture.Live.RebucketLiveEntity( 0x70000230u, 0x01010001u)); })); fixture.Controller.Tick(); Assert.Equal(2, attempts); Assert.Equal(1, continuations); Assert.True(record.IsSpatiallyProjected); } } [Fact] public void PendingUnparent_NewerPositionSupersedesRecoveryContinuation() { using var fixture = new ControllerFixture((_, _, _) => new( ExactProjectionWithdrawalDisposition.Pending, new InvalidOperationException("component cleanup failed"))); LiveEntityRecord record = fixture.Spawn(0x70000240u, generation: 1); int continuations = 0; Assert.Throws(() => fixture.Controller.OnChildBecameUnparented( 0x70000240u, () => continuations++)); var newer = new WorldSession.EntityPositionUpdate( 0x70000240u, new CreateObject.ServerPosition( 0x01010001u, 4f, 5f, 6f, 1f, 0f, 0f, 0f), Velocity: null, PlacementId: null, IsGrounded: true, InstanceSequence: 1, PositionSequence: 2, TeleportSequence: 0, ForcePositionSequence: 0); Assert.True(fixture.Live.TryApplyPosition( newer, isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out _, out _, out _)); fixture.Controller.Tick(); Assert.Equal(0, continuations); Assert.True(record.IsSpatiallyProjected); } [Fact] public void AcceptedValidParent_WithdrawsWorldProjectionBeforePosePrerequisitesExist() { ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { bool completed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); return new( completed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); }); using (fixture) { fixture.Spawn(0x70000250u, generation: 1); LiveEntityRecord child = fixture.Spawn(0x70000251u, generation: 1); fixture.Controller.OnParentEvent(new ParentEvent.Parsed( ParentGuid: 0x70000250u, ChildGuid: 0x70000251u, ParentLocation: 0, PlacementId: 0, ParentInstanceSequence: 1, ChildPositionSequence: 2)); Assert.False(child.IsSpatiallyProjected); Assert.True(fixture.Live.ParentAttachments.TryGetProjection( 0x70000251u, out _)); } } [Fact] public void InvalidParent_ConsumesPositionTimestampButPreservesWorldProjection() { ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { bool completed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); return new( completed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); }); using (fixture) { fixture.Spawn(0x70000252u, generation: 1); LiveEntityRecord child = fixture.Spawn(0x70000253u, generation: 1); fixture.Controller.OnParentEvent(new ParentEvent.Parsed( ParentGuid: 0x70000252u, ChildGuid: 0x70000253u, ParentLocation: 1, PlacementId: 0, ParentInstanceSequence: 1, ChildPositionSequence: 2)); Assert.True(child.IsSpatiallyProjected); Assert.NotEqual(0u, child.FullCellId); Assert.True(fixture.Live.TryGetSnapshot( child.ServerGuid, out WorldSession.EntitySpawn snapshot)); Assert.NotNull(snapshot.Position); Assert.Null(snapshot.ParentGuid); Assert.Equal((ushort)2, snapshot.PositionSequence); Assert.False(fixture.Live.ParentAttachments.TryGetProjection( child.ServerGuid, out _)); } } [Fact] public void QueuedValidThenInvalidParent_LeavesValidParentCommitted() { ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { bool completed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); return new( completed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); }); using (fixture) { LiveEntityRecord validParent = fixture.Spawn(0x70000259u, generation: 1); LiveEntityRecord invalidParent = fixture.Spawn(0x7000025Au, generation: 1); const uint childGuid = 0x7000025Bu; fixture.Controller.OnParentEvent(new ParentEvent.Parsed( validParent.ServerGuid, childGuid, 0, 0, 1, 1)); fixture.Controller.OnParentEvent(new ParentEvent.Parsed( invalidParent.ServerGuid, childGuid, 1, 0, 1, 2)); LiveEntityRecord child = fixture.RegisterOnly( childGuid, generation: 1, hasPosition: true); fixture.Materialize(child); Assert.True(fixture.Live.TryGetSnapshot( childGuid, out WorldSession.EntitySpawn childSpawn)); fixture.Controller.OnSpawn(childSpawn); Assert.True(fixture.Live.TryGetSnapshot(childGuid, out childSpawn)); Assert.Equal(validParent.ServerGuid, childSpawn.ParentGuid); Assert.Null(childSpawn.Position); Assert.Equal((ushort)2, childSpawn.PositionSequence); var valid = new ParentAttachmentRelation( validParent.ServerGuid, childGuid, 0, 0, 1, 1); Assert.True(fixture.Live.ParentAttachments.IsCommitted(valid)); Assert.False(fixture.Live.ParentAttachments.TryGetStagedProjection( childGuid, out _)); } } [Fact] public void NoPositionCreateParent_CommitsAfterParentPartArrayValidation() { using var fixture = new ControllerFixture((_, _, _) => new ExactProjectionWithdrawalOutcome( ExactProjectionWithdrawalDisposition.Completed, Failure: null)); LiveEntityRecord parent = fixture.Spawn(0x70000254u, generation: 1); LiveEntityRecord child = fixture.RegisterOnly( 0x70000255u, generation: 1, hasPosition: false); var update = new CreateParentUpdate( child.ServerGuid, parent.ServerGuid, ParentLocation: 0, PlacementId: 0, ChildInstanceSequence: 1, ChildPositionSequence: 1); Assert.True(fixture.Live.TryApplyCreateParent(update, out _)); fixture.Controller.OnCreateParentAccepted(update); Assert.True(fixture.Live.TryGetSnapshot( child.ServerGuid, out WorldSession.EntitySpawn snapshot)); Assert.Equal(parent.ServerGuid, snapshot.ParentGuid); Assert.Null(snapshot.Position); Assert.Null(child.WorldEntity); var relation = new ParentAttachmentRelation( parent.ServerGuid, child.ServerGuid, 0, 0, 0, 1); Assert.True(fixture.Live.ParentAttachments.IsCommitted(relation)); fixture.Poses.Publish( parent.WorldEntity!, Array.Empty()); fixture.Controller.OnPosePublished(parent.ServerGuid); Assert.NotNull(child.WorldEntity); Assert.True(child.IsSpatiallyProjected); Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind); } [Fact] public void SpawnParentWithoutAnimationFrame_UsesRetailPlacementZero() { using var fixture = new ControllerFixture((_, _, _) => new ExactProjectionWithdrawalOutcome( ExactProjectionWithdrawalDisposition.Completed, Failure: null)); LiveEntityRecord parent = fixture.Spawn(0x70000270u, generation: 1); WorldSession.EntitySpawn childSpawn = ControllerFixture.SpawnData( 0x70000271u, generation: 1) with { Position = null, ParentGuid = parent.ServerGuid, ParentLocation = 0, PlacementId = null, PositionSequence = 0, }; LiveEntityRecord child = fixture.Live.RegisterLiveEntity(childSpawn).Record!; fixture.Controller.OnSpawn(childSpawn); var expected = new ParentAttachmentRelation( parent.ServerGuid, child.ServerGuid, ParentLocation: 0, PlacementId: 0, ParentInstanceSequence: 0, ChildPositionSequence: 0); Assert.True(fixture.Live.ParentAttachments.IsCommitted(expected)); fixture.Poses.Publish(parent.WorldEntity!, Array.Empty()); fixture.Controller.OnPosePublished(parent.ServerGuid); Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind); Assert.NotNull(child.WorldEntity); } [Fact] public void ObjDesc_ReprojectsAttachedChildWithoutWorldPositionOrIdentityChange() { ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { bool completed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); return new( completed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); }); using (fixture) { LiveEntityRecord parent = fixture.Spawn(0x70000272u, generation: 1); WorldSession.EntitySpawn childSpawn = ControllerFixture.SpawnData( 0x70000273u, generation: 1) with { Position = null, ParentGuid = parent.ServerGuid, ParentLocation = 0, PlacementId = 0, PositionSequence = 0, }; LiveEntityRecord child = fixture.Live.RegisterLiveEntity(childSpawn).Record!; fixture.Controller.OnSpawn(childSpawn); fixture.Poses.Publish(parent.WorldEntity!, Array.Empty()); fixture.Controller.OnPosePublished(parent.ServerGuid); WorldEntity entity = child.WorldEntity!; Assert.Null(entity.PaletteOverride); WorldSession.EntitySpawn grandchildSpawn = ControllerFixture.SpawnData( 0x70000274u, generation: 1) with { Position = null, ParentGuid = child.ServerGuid, ParentLocation = 0, PlacementId = 0, PositionSequence = 0, }; LiveEntityRecord grandchild = fixture.Live.RegisterLiveEntity( grandchildSpawn).Record!; fixture.Controller.OnSpawn(grandchildSpawn); WorldEntity grandchildEntity = grandchild.WorldEntity!; Assert.Equal( LiveEntityProjectionKind.Attached, grandchild.ProjectionKind); Assert.True(fixture.Live.TryApplyObjDesc( new ObjDescEvent.Parsed( child.ServerGuid, new CreateObject.ModelData( BasePaletteId: 0x04000022u, [new CreateObject.SubPaletteSwap( 0x0F000033u, Offset: 4, Length: 8)], Array.Empty(), Array.Empty()), InstanceSequence: 1, ObjDescSequence: 1), out _)); Assert.Null(child.Snapshot.Position); Assert.True(fixture.Controller.TryApplyAttachedAppearance( child, child.ObjDescAuthorityVersion)); Assert.Same(entity, child.WorldEntity); Assert.True(child.IsSpatiallyProjected); Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind); Assert.Equal((uint)0x04000022u, child.Snapshot.BasePaletteId); Assert.NotNull(entity.PaletteOverride); Assert.Equal(0x04000022u, entity.PaletteOverride!.BasePaletteId); PaletteOverride.SubPaletteRange range = Assert.Single(entity.PaletteOverride.SubPalettes); Assert.Equal(0x0F000033u, range.SubPaletteId); Assert.Equal((byte)4, range.Offset); Assert.Equal((byte)8, range.Length); Assert.Same(grandchildEntity, grandchild.WorldEntity); Assert.True(grandchild.IsSpatiallyProjected); Assert.Equal( LiveEntityProjectionKind.Attached, grandchild.ProjectionKind); } } [Fact] public void ChildWithoutSetup_StillCommitsLogicalParenting() { ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { bool completed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); return new( completed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); }); using (fixture) { LiveEntityRecord parent = fixture.Spawn(0x7000025Cu, generation: 1); LiveEntityRecord child = fixture.RegisterOnly( 0x7000025Du, generation: 1, hasPosition: true, hasSetup: false); fixture.Materialize(child); child.HasPartArray = false; var update = new ParentEvent.Parsed( parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1); fixture.Controller.OnParentEvent(update); Assert.True(fixture.Live.TryGetSnapshot( child.ServerGuid, out WorldSession.EntitySpawn snapshot)); Assert.Equal(parent.ServerGuid, snapshot.ParentGuid); Assert.Null(snapshot.Position); Assert.False(child.IsSpatiallyProjected); Assert.True(fixture.Live.ParentAttachments.IsCommitted(new( parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1))); } } [Fact] public void EmbeddedSameGenerationCreateParent_UsesStagedRoute() { ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { bool completed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); return new( completed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); }); using (fixture) { LiveEntityRecord parent = fixture.Spawn(0x7000025Eu, generation: 1); LiveEntityRecord child = fixture.Spawn(0x7000025Fu, generation: 1); PhysicsTimestamps timestamps = new( Position: 1, Movement: 0, State: 0, Vector: 0, Teleport: 0, ServerControlledMove: 0, ForcePosition: 0, ObjDesc: 0, Instance: 1); PhysicsSpawnData physics = new( RawState: 0x408u, Position: null, Movement: null, AnimationFrame: 0, SetupTableId: 0x02000001u, MotionTableId: null, SoundTableId: null, PhysicsScriptTableId: null, Parent: new PhysicsAttachment(parent.ServerGuid, 0), Children: null, Scale: null, Friction: null, Elasticity: null, Translucency: null, Velocity: null, Acceleration: null, AngularVelocity: null, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: timestamps); WorldSession.EntitySpawn incoming = ControllerFixture.SpawnData( child.ServerGuid, generation: 1) with { Position = null, PositionSequence = 1, PlacementId = 0, Physics = physics, }; LiveEntityRegistrationResult refresh = fixture.Live.RegisterLiveEntity(incoming); CreateParentUpdate update = Assert.IsType( refresh.Inbound.SameGenerationEvents!.Value.Parent); Assert.True(fixture.Live.TryApplyCreateParent(update, out _)); fixture.Controller.OnCreateParentAccepted(update); Assert.True(fixture.Live.TryGetSnapshot( child.ServerGuid, out WorldSession.EntitySpawn snapshot)); Assert.Equal(parent.ServerGuid, snapshot.ParentGuid); Assert.Null(snapshot.Position); } } [Fact] public void NewWaitingParent_DoesNotDisplaceCommittedRecovery() { ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { bool completed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); return new( completed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); }); using (fixture) { LiveEntityRecord oldParent = fixture.Spawn(0x70000256u, generation: 1); LiveEntityRecord child = fixture.Spawn( 0x70000257u, generation: 1, LiveEntityProjectionKind.Attached); LiveEntityRecord waitingParent = fixture.RegisterOnly( 0x70000258u, generation: 1, hasPosition: true); var oldRelation = new ParentAttachmentRelation( oldParent.ServerGuid, child.ServerGuid, 0, 0, 1, 1); fixture.Live.ParentAttachments.AcceptCreateObjectRelation(oldRelation); Assert.True(fixture.Live.TryApplyParent(new ParentEvent.Parsed( oldRelation.ParentGuid, oldRelation.ChildGuid, oldRelation.ParentLocation, oldRelation.PlacementId, oldRelation.ParentInstanceSequence, oldRelation.ChildPositionSequence), out _)); Assert.True(fixture.Live.CommitStagedParent(oldRelation, out _)); Assert.True(fixture.Live.ParentAttachments.CommitProjection(oldRelation)); fixture.Live.ParentAttachments.MarkProjected( oldRelation, ParentProjectionCandidateKind.Recovery); fixture.InstallAttached(oldParent, child); var newer = new ParentEvent.Parsed( waitingParent.ServerGuid, child.ServerGuid, ParentLocation: 0, PlacementId: 0, ParentInstanceSequence: 1, ChildPositionSequence: 2); fixture.Controller.OnParentEvent(newer); Assert.True(fixture.Live.ParentAttachments.TryGetStagedProjection( child.ServerGuid, out ParentAttachmentRelation staged)); Assert.Equal(waitingParent.ServerGuid, staged.ParentGuid); fixture.Controller.Tick(); Assert.Empty(fixture.Controller.AttachedEntityIds); Assert.True(fixture.Live.ParentAttachments.TryGetStagedProjection( child.ServerGuid, out staged)); Assert.Equal(waitingParent.ServerGuid, staged.ParentGuid); Assert.True(fixture.Live.ParentAttachments.TryGetRecoveryProjection( child.ServerGuid, out ParentAttachmentRelation recovery)); Assert.Equal(oldParent.ServerGuid, recovery.ParentGuid); fixture.Materialize(waitingParent); fixture.Controller.OnWorldEntityRegistered(waitingParent.ServerGuid); Assert.True(fixture.Live.ParentAttachments.IsCommitted(staged)); Assert.True(fixture.Live.ParentAttachments.TryGetRecoveryProjection( child.ServerGuid, out ParentAttachmentRelation committedRecovery)); Assert.Equal(staged, committedRecovery); Assert.True(fixture.Live.TryGetSnapshot( child.ServerGuid, out WorldSession.EntitySpawn snapshot)); Assert.Equal(waitingParent.ServerGuid, snapshot.ParentGuid); } } [Fact] public void OrdinaryRemoval_PendingFailureRetriesExactCaptureOnTick() { int attempts = 0; ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { attempts++; if (attempts == 1) { return new( ExactProjectionWithdrawalDisposition.Pending, new InvalidOperationException("ordinary cleanup failed")); } bool completed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); return new( completed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); }); using (fixture) { LiveEntityRecord parent = fixture.Spawn(0x70000260u, generation: 1); LiveEntityRecord child = fixture.Spawn( 0x70000261u, generation: 1, LiveEntityProjectionKind.Attached); fixture.InstallAttached(parent, child); fixture.Live.ParentAttachments.AcceptCreateObjectRelation(new( parent.ServerGuid, child.ServerGuid, ParentLocation: 0, PlacementId: 0, ParentInstanceSequence: 1, ChildPositionSequence: 1)); TargetInvocationException error = Assert.Throws(() => fixture.InvokeOrdinaryRemoval(0x70000261u)); Assert.IsType(error.InnerException); fixture.Controller.Tick(); Assert.Equal(2, attempts); Assert.Empty(fixture.Controller.AttachedEntityIds); Assert.False(child.IsSpatiallyProjected); Assert.False(fixture.Live.ParentAttachments.TryGetProjection( child.ServerGuid, out _)); } } [Fact] public void DetachedRemoval_PendingFailureRetriesAndPreservesRollbackHistory() { int attempts = 0; ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { attempts++; if (attempts == 1) { return new( ExactProjectionWithdrawalDisposition.Pending, new InvalidOperationException("detached cleanup failed")); } bool completed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); return new( completed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); }); using (fixture) { LiveEntityRecord parent = fixture.Spawn(0x70000262u, generation: 1); LiveEntityRecord child = fixture.Spawn( 0x70000263u, generation: 1, LiveEntityProjectionKind.Attached); fixture.InstallAttached(parent, child); var relation = new ParentAttachmentRelation( parent.ServerGuid, child.ServerGuid, ParentLocation: 0, PlacementId: 0, ParentInstanceSequence: 1, ChildPositionSequence: 1); fixture.Live.ParentAttachments.AcceptCreateObjectRelation(relation); Assert.True(fixture.Live.ParentAttachments.CommitProjection(relation)); fixture.Live.ParentAttachments.MarkProjected( relation, ParentProjectionCandidateKind.Recovery); TargetInvocationException error = Assert.Throws(() => fixture.InvokeDetachedRemoval(child.ServerGuid)); Assert.IsType(error.InnerException); fixture.Controller.Tick(); Assert.Equal(2, attempts); Assert.Empty(fixture.Controller.AttachedEntityIds); Assert.False(child.IsSpatiallyProjected); Assert.True(fixture.Live.ParentAttachments.RestoreLastAccepted( child.ServerGuid)); Assert.True(fixture.Live.ParentAttachments.TryGetRecoveryProjection( child.ServerGuid, out ParentAttachmentRelation restored)); Assert.Equal(relation, restored); } } [Fact] public void Unparent_WithdrawsCompleteAttachedSubtreeAndRecoversDescendants() { ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { bool completed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); return new( completed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); }); using (fixture) { LiveEntityRecord parent = fixture.Spawn(0x70000264u, generation: 1); LiveEntityRecord child = fixture.Spawn( 0x70000265u, generation: 1, LiveEntityProjectionKind.Attached); LiveEntityRecord grandchild = fixture.Spawn( 0x70000266u, generation: 1, LiveEntityProjectionKind.Attached); fixture.InstallAttached(parent, child); fixture.InstallAttached(child, grandchild); var childRelation = new ParentAttachmentRelation( parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1); var grandchildRelation = new ParentAttachmentRelation( child.ServerGuid, grandchild.ServerGuid, 0, 0, 1, 1); fixture.CommitRenderedRelation(childRelation); fixture.CommitRenderedRelation(grandchildRelation); ChildUnparentDisposition result = fixture.Controller.OnChildBecameUnparented(child.ServerGuid); Assert.Equal(ChildUnparentDisposition.Completed, result); Assert.Empty(fixture.Controller.AttachedEntityIds); Assert.False(child.IsSpatiallyProjected); Assert.False(grandchild.IsSpatiallyProjected); Assert.False(fixture.Live.ParentAttachments.RestoreLastAccepted( child.ServerGuid)); Assert.True(fixture.Live.ParentAttachments.TryGetRecoveryProjection( grandchild.ServerGuid, out ParentAttachmentRelation recovered)); Assert.Equal(grandchildRelation, recovered); } } [Fact] public void PendingDetachedRemoval_RollbackCancelsRetryAndKeepsProjection() { int attempts = 0; using var fixture = new ControllerFixture((_, _, _) => { attempts++; return new( ExactProjectionWithdrawalDisposition.Pending, new InvalidOperationException("component withdrawal failed")); }); LiveEntityRecord parent = fixture.Spawn(0x70000267u, generation: 1); LiveEntityRecord child = fixture.Spawn( 0x70000268u, generation: 1, LiveEntityProjectionKind.Attached); fixture.InstallAttached(parent, child); fixture.CommitRenderedRelation(new( parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1)); fixture.Poses.Publish(parent.WorldEntity!, Array.Empty()); fixture.Objects.AddOrUpdate(new ClientObject { ObjectId = child.ServerGuid, WielderId = parent.ServerGuid, CurrentlyEquippedLocation = EquipMask.MeleeWeapon, }); Assert.Throws(() => fixture.Objects.MoveItemOptimistic( child.ServerGuid, newContainerId: 0x50000001u, newSlot: 0)); Assert.True(fixture.Objects.RollbackMove(child.ServerGuid)); fixture.Controller.Tick(); Assert.Equal(1, attempts); Assert.True(child.IsSpatiallyProjected); Assert.Single(fixture.Controller.AttachedEntityIds); } [Fact] public void OrphanRollback_PendingFailureRetainsExactRetryOwner() { int attempts = 0; ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { attempts++; if (attempts == 1) { return new( ExactProjectionWithdrawalDisposition.Pending, new InvalidOperationException("orphan component cleanup failed")); } bool completed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); return new( completed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); }); using (fixture) { LiveEntityRecord orphan = fixture.Spawn(0x70000269u, generation: 1); TargetInvocationException error = Assert.Throws(() => fixture.InvokeOrphanRemoval(orphan)); Assert.IsType(error.InnerException); fixture.Controller.Tick(); Assert.Equal(2, attempts); Assert.False(orphan.IsSpatiallyProjected); } } [Fact] public void RecoveryContinuation_PostCommitFailureIsNotReplayed() { ControllerFixture? fixture = null; fixture = new ControllerFixture((record, positionVersion, projectionVersion) => { bool completed = fixture!.Live.WithdrawLiveEntityProjection( record, positionVersion, projectionVersion); return new( completed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); }); using (fixture) { LiveEntityRecord record = fixture.Spawn(0x70000270u, generation: 1); int continuations = 0; Assert.Throws(() => fixture.Controller.OnChildBecameUnparented( 0x70000270u, () => { continuations++; Assert.True(fixture.Live.RebucketLiveEntity( 0x70000270u, 0x01010001u)); throw new InvalidOperationException("observer failed after recovery"); })); fixture.Controller.Tick(); Assert.Equal(1, continuations); Assert.True(record.IsSpatiallyProjected); } } private static LiveEntityRecord ChildRecord() => new( new WorldSession.EntitySpawn( 0x70000100u, new CreateObject.ServerPosition( 0x01010001u, 0f, 0f, 0f, 1f, 0f, 0f, 0f), 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), BasePaletteId: null, ObjScale: null, Name: "attached fixture", ItemType: null, MotionState: null, MotionTableId: null, InstanceSequence: 1)); private sealed class ControllerFixture : IDisposable { private const uint Cell = 0x01010001u; private readonly DeferredLiveEntityRuntimeComponentLifecycle _lifecycle = new(); private readonly Setup _setup; internal ControllerFixture( Func withdraw) { Spatial.AddLandblock(new LoadedLandblock( (Cell & 0xFFFF0000u) | 0xFFFFu, new LandBlock(), Array.Empty())); Live = new LiveEntityRuntime( Spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }), _lifecycle); _setup = new Setup { HoldingLocations = { [(ParentLocation)0] = new LocationType { PartId = -1, Frame = new Frame { Orientation = Quaternion.Identity }, }, }, }; IDatReaderWriter dat = DispatchProxy.Create(); ((NullDatProxy)(object)dat).Setup = _setup; Controller = new EquippedChildRenderController( dat, new object(), Objects, Live, Poses, update => Live.TryApplyParent(update, out _), withdraw); _lifecycle.Bind(new DelegateLiveEntityRuntimeComponentLifecycle( Controller.OnLogicalTeardown)); } internal GpuWorldState Spatial { get; } = new(); internal ClientObjectTable Objects { get; } = new(); internal EntityEffectPoseRegistry Poses { get; } = new(); internal LiveEntityRuntime Live { get; } internal EquippedChildRenderController Controller { get; } internal LiveEntityRecord Spawn( uint guid, ushort generation, LiveEntityProjectionKind kind = LiveEntityProjectionKind.World) { LiveEntityRecord record = RegisterOnly(guid, generation, hasPosition: true); Materialize(record, kind); return record; } internal LiveEntityRecord RegisterOnly( uint guid, ushort generation, bool hasPosition, bool hasSetup = true) { WorldSession.EntitySpawn spawn = SpawnData(guid, generation); if (!hasPosition) spawn = spawn with { Position = null }; if (!hasSetup) spawn = spawn with { SetupTableId = null }; return Live.RegisterLiveEntity(spawn).Record!; } internal void Materialize( LiveEntityRecord record, LiveEntityProjectionKind kind = LiveEntityProjectionKind.World) { Live.MaterializeLiveEntity( record.ServerGuid, Cell, id => new WorldEntity { Id = id, ServerGuid = record.ServerGuid, SourceGfxObjOrSetupId = 0x02000001u, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), ParentCellId = Cell, }, kind); record.HasPartArray = true; } internal static WorldSession.EntitySpawn SpawnData(uint guid, ushort generation) => new( guid, new CreateObject.ServerPosition( Cell, 0f, 0f, 0f, 1f, 0f, 0f, 0f), 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), BasePaletteId: null, ObjScale: null, Name: "attached fixture", ItemType: null, MotionState: null, MotionTableId: null, InstanceSequence: generation); internal void InstallAttached( LiveEntityRecord parent, LiveEntityRecord child) { Type attachedType = typeof(EquippedChildRenderController).GetNestedType( "AttachedChild", BindingFlags.NonPublic)!; object attached = Activator.CreateInstance( attachedType, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, binder: null, args: [ parent, child, parent.ServerGuid, child.ServerGuid, (ParentLocation)0, (Placement)0, _setup, _setup, Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), 1f, child.WorldEntity!, ], culture: null)!; FieldInfo mapField = typeof(EquippedChildRenderController).GetField( "_attachedByChild", BindingFlags.Instance | BindingFlags.NonPublic)!; var map = (IDictionary)mapField.GetValue(Controller)!; map.Add(child.ServerGuid, attached); } internal void CommitRenderedRelation(ParentAttachmentRelation relation) { Live.ParentAttachments.AcceptCreateObjectRelation(relation); Assert.True(Live.ParentAttachments.CommitProjection(relation)); Live.ParentAttachments.MarkProjected( relation, ParentProjectionCandidateKind.Recovery); } internal void InvokeOrdinaryRemoval(uint guid) { MethodInfo method = typeof(EquippedChildRenderController).GetMethod( "TearDownCurrentObjectProjections", BindingFlags.Instance | BindingFlags.NonPublic)!; method.Invoke(Controller, [guid]); } internal void InvokeDetachedRemoval(uint guid) { MethodInfo method = typeof(EquippedChildRenderController).GetMethod( "BeginDetachedRemoval", BindingFlags.Instance | BindingFlags.NonPublic)!; method.Invoke(Controller, [guid]); } internal void InvokeOrphanRemoval(LiveEntityRecord record) { FieldInfo mapField = typeof(EquippedChildRenderController).GetField( "_pendingOrphanRemovalByChild", BindingFlags.Instance | BindingFlags.NonPublic)!; object map = mapField.GetValue(Controller)!; MethodInfo method = typeof(EquippedChildRenderController).GetMethod( "BeginProjectionSubtreeWithdrawal", BindingFlags.Instance | BindingFlags.NonPublic)!; method.Invoke(Controller, [map, record, false, false]); } public void Dispose() => Controller.Dispose(); } private class NullDatProxy : DispatchProxy { internal Setup? Setup { get; set; } protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) { if (targetMethod?.Name == "Get" && targetMethod.ReturnType == typeof(Setup)) { return Setup; } if (targetMethod?.ReturnType == typeof(void)) return null; if (targetMethod?.ReturnType.IsValueType == true) return Activator.CreateInstance(targetMethod.ReturnType); return null; } } }