using AcDream.App.Rendering; using AcDream.App.World; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Items; namespace AcDream.App.Tests.World; public sealed class ParentAttachmentStateTests { [Fact] public void ParentEventBeforeChildCreateResolvesToCanonicalAttachment() { const uint parentGuid = 0x70000100u; const uint childGuid = 0x70000101u; var inbound = new InboundPhysicsStateController(); var relations = new ParentAttachmentState(); inbound.AcceptCreate(Spawn(parentGuid, instance: 9, positionSequence: 1)); relations.Enqueue(new ParentEvent.Parsed(parentGuid, childGuid, 1, 2, 9, 5)); Resolve(relations, inbound, childGuid); Assert.False(relations.TryGetProjection(childGuid, out _)); inbound.AcceptCreate(Spawn(childGuid, instance: 3, positionSequence: 4)); Resolve(relations, inbound, childGuid); Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection)); Assert.Equal(parentGuid, projection.ParentGuid); Assert.True(inbound.TryGetSnapshot(childGuid, out WorldSession.EntitySpawn child)); Assert.Null(child.Position); Assert.Equal(parentGuid, child.ParentGuid); Assert.Equal((ushort)5, child.PositionSequence); } [Fact] public void MultipleQueuedRelationsRemainOrderedAndNewestAcceptedWins() { const uint parentGuid = 0x70000110u; const uint childGuid = 0x70000111u; var gate = new PhysicsTimestampGate(); gate.SeedForCreateObject(4, 0, 0, 0, 0, 0, 0, 0, 3); var relations = new ParentAttachmentState(); relations.Enqueue(new ParentEvent.Parsed(parentGuid, childGuid, 1, 2, 9, 5)); relations.Enqueue(new ParentEvent.Parsed(parentGuid, childGuid, 1, 3, 9, 6)); relations.Resolve( childGuid, _ => true, guid => guid == parentGuid ? (ushort)9 : null, update => gate.TryAcceptPositionChannelEvent(3, update.ChildPositionSequence)); Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection)); Assert.Equal((ushort)6, projection.ChildPositionSequence); Assert.Equal((uint)3, projection.PlacementId); relations.MarkProjected(childGuid); Assert.True(relations.RestoreLastAccepted(childGuid)); Assert.True(relations.TryGetProjection(childGuid, out projection)); Assert.Equal((ushort)6, projection.ChildPositionSequence); } [Fact] public void RejectedStaleRelationCannotReplaceAcceptedCreateRelation() { const uint childGuid = 0x70000121u; var gate = new PhysicsTimestampGate(); gate.SeedForCreateObject(10, 0, 0, 0, 0, 0, 0, 0, 3); var relations = new ParentAttachmentState(); relations.AcceptCreateObjectRelation(new ParentAttachmentRelation( 0x70000120u, childGuid, 1, 2, 0, 10)); relations.Enqueue(new ParentEvent.Parsed( 0x70000122u, childGuid, 1, 4, 8, 9)); relations.Resolve( childGuid, _ => true, guid => guid == 0x70000122u ? (ushort)8 : null, update => gate.TryAcceptPositionChannelEvent(3, update.ChildPositionSequence)); Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection)); Assert.Equal(0x70000120u, projection.ParentGuid); Assert.Equal((ushort)10, projection.ChildPositionSequence); } [Fact] public void FutureParentGenerationDoesNotBlockCurrentRelationBehindIt() { const uint childGuid = 0x70000141u; const uint parentGuid = 0x70000140u; var gate = new PhysicsTimestampGate(); gate.SeedForCreateObject(4, 0, 0, 0, 0, 0, 0, 0, 3); var relations = new ParentAttachmentState(); relations.Enqueue(new ParentEvent.Parsed( parentGuid, childGuid, 1, 2, 10, 6)); relations.Enqueue(new ParentEvent.Parsed( parentGuid, childGuid, 1, 3, 9, 5)); relations.Resolve( childGuid, _ => true, guid => guid == parentGuid ? (ushort)9 : null, update => gate.TryAcceptPositionChannelEvent(3, update.ChildPositionSequence)); Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection)); Assert.Equal((ushort)5, projection.ChildPositionSequence); Assert.Equal((uint)3, projection.PlacementId); Assert.Contains(childGuid, relations.ChildrenWaitingForParent(parentGuid)); } [Fact] public void FutureRelationSurvivesAtomicParentGenerationReplacement() { const uint parentGuid = 0x70000150u; const uint childGuid = 0x70000151u; var inbound = new InboundPhysicsStateController(); var relations = new ParentAttachmentState(); var objects = new ClientObjectTable(); inbound.AcceptCreate(Spawn(parentGuid, instance: 9, positionSequence: 1)); inbound.AcceptCreate(Spawn(childGuid, instance: 3, positionSequence: 4)); relations.AcceptCreateObjectRelation(new ParentAttachmentRelation( parentGuid, childGuid, 1, 2, 0, 4)); relations.Enqueue(new ParentEvent.Parsed( parentGuid, childGuid, 1, 3, 10, 5)); Resolve(relations, inbound, childGuid); objects.Ingest(MinimalWeenie(parentGuid, "generation 9")); objects.ObjectRemovalClassified += removal => { if (removal.Reason == ClientObjectRemovalReason.GenerationReplacement) relations.EndGeneration(removal.Object.ObjectId, removal.Generation); }; inbound.AcceptCreate(Spawn(parentGuid, instance: 10, positionSequence: 1)); objects.ReplaceGeneration(MinimalWeenie(parentGuid, "generation 10"), 10); Resolve(relations, inbound, childGuid); Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection)); Assert.Equal(parentGuid, projection.ParentGuid); Assert.Equal((ushort)10, projection.ParentInstanceSequence); Assert.Equal((ushort)5, projection.ChildPositionSequence); Assert.True(inbound.TryGetSnapshot(childGuid, out WorldSession.EntitySpawn child)); Assert.Equal(parentGuid, child.ParentGuid); } [Fact] public void FutureParentRelationSurvivesChildGenerationReplacement() { const uint parentGuid = 0x70000180u; const uint childGuid = 0x70000181u; var relations = new ParentAttachmentState(); relations.Enqueue(new ParentEvent.Parsed( parentGuid, childGuid, 1, 3, 10, 5)); // Parent generation 9 exists, so retail queues this blob against the // future parent rather than against the already-live child. relations.Resolve( childGuid, _ => true, guid => guid == parentGuid ? (ushort)9 : null, _ => throw new InvalidOperationException("future parent must not apply")); relations.EndGeneration(childGuid, replacementGeneration: 4); var replacementGate = new PhysicsTimestampGate(); replacementGate.SeedForCreateObject(4, 0, 0, 0, 0, 0, 0, 0, 4); relations.Resolve( childGuid, _ => true, guid => guid == parentGuid ? (ushort)10 : null, update => replacementGate.TryAcceptPositionChannelEvent( 4, update.ChildPositionSequence)); Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection)); Assert.Equal((ushort)10, projection.ParentInstanceSequence); Assert.Equal((ushort)5, projection.ChildPositionSequence); } [Fact] public void RemoveAndClearDiscardPendingAndRollbackState() { const uint parentGuid = 0x70000130u; const uint childGuid = 0x70000131u; var relations = new ParentAttachmentState(); relations.AcceptCreateObjectRelation(new ParentAttachmentRelation( parentGuid, childGuid, 1, 2, 0, 4)); relations.Enqueue(new ParentEvent.Parsed(parentGuid, childGuid, 1, 3, 9, 5)); relations.RemoveObject(parentGuid); Assert.False(relations.TryGetProjection(childGuid, out _)); Assert.False(relations.RestoreLastAccepted(childGuid)); Assert.Empty(relations.ChildrenWaitingForParent(parentGuid)); relations.AcceptCreateObjectRelation(new ParentAttachmentRelation( parentGuid, childGuid, 1, 2, 0, 4)); relations.Clear(); Assert.False(relations.TryGetProjection(childGuid, out _)); Assert.False(relations.RestoreLastAccepted(childGuid)); } [Fact] public void UnparentProjectionRetainsFresherPendingParentEvent() { const uint parentGuid = 0x70000160u; const uint childGuid = 0x70000161u; var gate = new PhysicsTimestampGate(); gate.SeedForCreateObject(4, 0, 0, 0, 0, 0, 0, 0, 3); var relations = new ParentAttachmentState(); relations.AcceptCreateObjectRelation(new ParentAttachmentRelation( 0x70000162u, childGuid, 1, 2, 0, 4)); relations.Enqueue(new ParentEvent.Parsed( parentGuid, childGuid, 1, 3, 9, 5)); relations.EndChildProjection(childGuid); Assert.False(relations.TryGetProjection(childGuid, out _)); relations.Resolve( childGuid, _ => true, guid => guid == parentGuid ? (ushort)9 : null, update => gate.TryAcceptPositionChannelEvent(3, update.ChildPositionSequence)); Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection)); Assert.Equal(parentGuid, projection.ParentGuid); Assert.Equal((ushort)5, projection.ChildPositionSequence); } [Fact] public void ExactParentDeleteRetainsOnlyStrictlyFutureGenerationRelation() { const uint parentGuid = 0x70000170u; const uint childGuid = 0x70000171u; var gate = new PhysicsTimestampGate(); gate.SeedForCreateObject(4, 0, 0, 0, 0, 0, 0, 0, 3); var relations = new ParentAttachmentState(); relations.Enqueue(new ParentEvent.Parsed( parentGuid, childGuid, 1, 2, 9, 5)); relations.Enqueue(new ParentEvent.Parsed( parentGuid, childGuid, 1, 3, 10, 6)); relations.DeleteGeneration(parentGuid, deletedGeneration: 9); relations.Resolve( childGuid, _ => true, guid => guid == parentGuid ? (ushort)10 : null, update => gate.TryAcceptPositionChannelEvent(3, update.ChildPositionSequence)); Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection)); Assert.Equal((ushort)10, projection.ParentInstanceSequence); Assert.Equal((ushort)6, projection.ChildPositionSequence); } private static void Resolve( ParentAttachmentState relations, InboundPhysicsStateController inbound, uint childGuid) => relations.Resolve( childGuid, guid => inbound.TryGetSnapshot(guid, out _), guid => inbound.TryGetSnapshot(guid, out WorldSession.EntitySpawn spawn) ? spawn.InstanceSequence : null, update => inbound.TryApplyParent(update, out _)); private static WorldSession.EntitySpawn Spawn( uint guid, ushort instance, ushort positionSequence) { var position = new CreateObject.ServerPosition( 0x0101FFFFu, 10f, 10f, 5f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps( positionSequence, 1, 1, 1, 0, 1, 0, 1, instance); var physics = new PhysicsSpawnData( RawState: 0x408u, 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, Array.Empty(), Array.Empty(), Array.Empty(), null, null, "fixture", null, null, 0x09000001u, PhysicsState: 0x408u, InstanceSequence: instance, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: positionSequence, Physics: physics); } private static WeenieData MinimalWeenie(uint guid, string name) => new( Guid: guid, Name: name, Type: null, WeenieClassId: 0, IconId: 0, IconOverlayId: 0, IconUnderlayId: 0, Effects: 0, Value: null, StackSize: null, StackSizeMax: null, Burden: null, ContainerId: null, WielderId: null, ValidLocations: null, CurrentWieldedLocation: null, Priority: null, ItemsCapacity: null, ContainersCapacity: null, Structure: null, MaxStructure: null, Workmanship: null); }