using System.Numerics; using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Content.Vfx; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.World; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Options; using Xunit.Sdk; namespace AcDream.App.Tests.World; public sealed class LiveEntityPresentationControllerTests { [Fact] public void InitialVisibleObject_DoesNotPlayUnHide() { Fixture fixture = new(PhysicsStateFlags.ReportCollisions); Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid)); Assert.Empty(fixture.TypedPlays); Assert.True(fixture.Entity.IsDrawVisible); Assert.Equal(1, fixture.Shadows.TotalRegistered); } [Fact] public void HiddenThenVisible_PlaysTypedEffectsMutatesChildrenAndRestoresShadow() { Fixture fixture = new( PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions); Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid)); Assert.Equal( [(fixture.Entity.Id, LiveEntityPresentationController.HiddenScriptType, 1f)], fixture.TypedPlays); Assert.Equal([(Fixture.Guid, true)], fixture.ChildNoDraw); Assert.Equal([Fixture.Guid], fixture.InvalidTargets); Assert.Equal([fixture.Entity.Id], fixture.PartArrayEnterWorld); Assert.Equal([0], fixture.PartArrayShadowCounts); Assert.Equal( ["effect:76", "children:True", "part-array", "target"], fixture.PresentationOrder); Assert.Equal(0, fixture.Shadows.TotalRegistered); Assert.False(fixture.Entity.IsDrawVisible); Assert.True(fixture.Runtime.TryApplyState( new SetState.Parsed(Fixture.Guid, 0u, 1, 2), out _, out _)); Assert.True(fixture.Controller.OnStateAccepted(Fixture.Guid)); Assert.Equal( [ (fixture.Entity.Id, LiveEntityPresentationController.HiddenScriptType, 1f), (fixture.Entity.Id, LiveEntityPresentationController.UnHideScriptType, 1f), ], fixture.TypedPlays); Assert.Equal([(Fixture.Guid, true), (Fixture.Guid, false)], fixture.ChildNoDraw); Assert.Equal( [fixture.Entity.Id, fixture.Entity.Id], fixture.PartArrayEnterWorld); Assert.Equal([0, 0], fixture.PartArrayShadowCounts); Assert.Equal( [ "effect:76", "children:True", "part-array", "target", "effect:75", "children:False", "part-array", ], fixture.PresentationOrder); Assert.Equal(1, fixture.Shadows.TotalRegistered); Assert.True(fixture.Entity.IsDrawVisible); Assert.Same(fixture.Entity, Assert.Single(fixture.Runtime.WorldEntities).Value); } [Fact] public void SpellRecall_HiddenAndUnHide_RetireMagicTimelineThroughController() { const uint humanSetup = 0x02000001u; const uint humanMotionTable = 0x09000001u; const uint magic = 0x80000049u; const uint ready = 0x41000003u; const uint magicPortal = 0x40000038u; string datDir = @"C:\Turbine\Asheron's Call"; if (!File.Exists(Path.Combine(datDir, "client_portal.dat"))) throw SkipException.ForSkip("Installed retail DATs are required."); using var dats = new DatCollection(datDir, DatAccessType.Read); Setup setup = Assert.IsType(dats.Get(humanSetup)); MotionTable table = Assert.IsType(dats.Get(humanMotionTable)); var sequencer = new AnimationSequencer( setup, table, new RetailAnimationLoader(dats)); var fixture = new Fixture( PhysicsStateFlags.ReportCollisions, _ => sequencer.Manager.HandleEnterWorld()); Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid)); sequencer.SetCycle(magic, ready); sequencer.PlayAction(magicPortal); Assert.False(sequencer.CurrentNodeDiag.IsLooping); Assert.True(fixture.Runtime.TryApplyState( new SetState.Parsed( Fixture.Guid, (uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions), 1, 2), out _, out _)); Assert.True(fixture.Controller.OnStateAccepted(Fixture.Guid)); Assert.True(sequencer.CurrentNodeDiag.IsLooping); Assert.Empty(sequencer.Manager.PendingAnimations); // A motion accepted while the mesh is hidden is retired again at // retail's matching UnHide PartArray boundary before cell visibility // returns. var unhideSequencer = new AnimationSequencer( setup, table, new RetailAnimationLoader(dats)); var unhideFixture = new Fixture( PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions, _ => unhideSequencer.Manager.HandleEnterWorld()); Assert.True(unhideFixture.Controller.OnLiveEntityReady(Fixture.Guid)); unhideSequencer.SetCycle(magic, ready); unhideSequencer.PlayAction(magicPortal); Assert.False(unhideSequencer.CurrentNodeDiag.IsLooping); Assert.True(unhideFixture.Runtime.TryApplyState( new SetState.Parsed(Fixture.Guid, 0u, 1, 3), out _, out _)); Assert.True(unhideFixture.Controller.OnStateAccepted(Fixture.Guid)); Assert.True(unhideSequencer.CurrentNodeDiag.IsLooping); Assert.Empty(unhideSequencer.Manager.PendingAnimations); Assert.Equal([0], fixture.PartArrayShadowCounts); Assert.Equal([0, 0], unhideFixture.PartArrayShadowCounts); Assert.Equal(1, unhideFixture.Shadows.TotalRegistered); } [Fact] public void IdenticalOrStaleState_DoesNotReplayHiddenEffect() { Fixture fixture = new(PhysicsStateFlags.ReportCollisions); fixture.Controller.OnLiveEntityReady(Fixture.Guid); var hidden = new SetState.Parsed( Fixture.Guid, (uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions), 1, 2); Assert.True(fixture.Runtime.TryApplyState(hidden, out _, out _)); fixture.Controller.OnStateAccepted(Fixture.Guid); Assert.False(fixture.Runtime.TryApplyState(hidden, out _, out _)); fixture.Controller.OnStateAccepted(Fixture.Guid); Assert.Single(fixture.TypedPlays); Assert.Equal(LiveEntityPresentationController.HiddenScriptType, fixture.TypedPlays[0].Type); Assert.Equal([fixture.Entity.Id], fixture.PartArrayEnterWorld); } [Fact] public void UnHideWhileLandblockPending_RestoresShadowWhenProjectionReturns() { Fixture fixture = new(PhysicsStateFlags.ReportCollisions); fixture.Controller.OnLiveEntityReady(Fixture.Guid); Assert.True(fixture.Runtime.TryApplyState( new SetState.Parsed( Fixture.Guid, (uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions), 1, 2), out _, out _)); fixture.Controller.OnStateAccepted(Fixture.Guid); Assert.Equal(0, fixture.Shadows.TotalRegistered); Assert.True(fixture.Runtime.RebucketLiveEntity(Fixture.Guid, 0x02020001u)); Assert.False(fixture.Runtime.WorldEntities.ContainsKey(Fixture.Guid)); Assert.True(fixture.Runtime.TryApplyState( new SetState.Parsed(Fixture.Guid, 0u, 1, 3), out _, out _)); fixture.Controller.OnStateAccepted(Fixture.Guid); Assert.Equal(0, fixture.Shadows.TotalRegistered); fixture.Spatial.AddLandblock(new LoadedLandblock( 0x0202FFFFu, new LandBlock(), Array.Empty())); Assert.True(fixture.Runtime.WorldEntities.ContainsKey(Fixture.Guid)); Assert.Equal(1, fixture.Shadows.TotalRegistered); } [Fact] public void ActivePlacement_IsGenerationScopedAndClearsOnTeardownAndReset() { Fixture fixture = new(PhysicsStateFlags.ReportCollisions); Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid)); Assert.True(fixture.Controller.BeginAuthoritativePlacement(Fixture.Guid, 1)); Assert.True(fixture.Controller.HasActivePlacement(Fixture.Guid)); Assert.True(fixture.Runtime.TryGetRecord(Fixture.Guid, out LiveEntityRecord oldRecord)); fixture.Controller.Forget(oldRecord); Assert.True(fixture.Runtime.UnregisterLiveEntity( new DeleteObject.Parsed(Fixture.Guid, 1), isLocalPlayer: false)); Assert.False(fixture.Controller.HasActivePlacement(Fixture.Guid)); fixture.Runtime.RegisterLiveEntity(Fixture.Spawn( PhysicsStateFlags.ReportCollisions, instanceSequence: 2)); fixture.Runtime.MaterializeLiveEntity( Fixture.Guid, 0x01010001u, id => new WorldEntity { Id = id, ServerGuid = Fixture.Guid, SourceGfxObjOrSetupId = 0x02000001u, Position = new Vector3(10f, 10f, 5f), Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), }); Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid)); Assert.False(fixture.Controller.CompleteAuthoritativePlacement( Fixture.Guid, generation: 1, deferShadowRestore: true)); Assert.False(fixture.Controller.HasDeferredShadowRestore(Fixture.Guid)); Assert.True(fixture.Controller.BeginAuthoritativePlacement(Fixture.Guid, 2)); Assert.True(fixture.Controller.HasActivePlacement(Fixture.Guid)); fixture.Controller.Clear(); Assert.False(fixture.Controller.HasActivePlacement(Fixture.Guid)); Assert.False(fixture.Controller.HasDeferredShadowRestore(Fixture.Guid)); } private sealed class Fixture { public const uint Guid = 0x70000051u; public List<(uint Owner, uint Type, float Intensity)> TypedPlays { get; } = []; public List<(uint Parent, bool NoDraw)> ChildNoDraw { get; } = []; public List InvalidTargets { get; } = []; public List PartArrayEnterWorld { get; } = []; public List PartArrayShadowCounts { get; } = []; public List PresentationOrder { get; } = []; public GpuWorldState Spatial { get; } public LiveEntityRuntime Runtime { get; } public ShadowObjectRegistry Shadows { get; } = new(); public WorldEntity Entity { get; } public LiveEntityPresentationController Controller { get; } public Fixture( PhysicsStateFlags initialState, Action? onPartArrayEnterWorld = null) { Spatial = new GpuWorldState(); Spatial.AddLandblock(new LoadedLandblock( 0x0101FFFFu, new LandBlock(), Array.Empty())); Runtime = new LiveEntityRuntime( Spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); WorldSession.EntitySpawn spawn = Spawn(initialState); Runtime.RegisterLiveEntity(spawn); Entity = Runtime.MaterializeLiveEntity( Guid, 0x01010001u, id => new WorldEntity { Id = id, ServerGuid = Guid, SourceGfxObjOrSetupId = 0x02000001u, Position = new Vector3(10f, 10f, 5f), Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; Shadows.Register( Entity.Id, 0x01000001u, Entity.Position, Entity.Rotation, 1f, 0f, 0f, 0x01010000u, state: (uint)initialState, seedCellId: 0x01010001u); Controller = new LiveEntityPresentationController( Runtime, Shadows, (owner, type, intensity) => { TypedPlays.Add((owner, type, intensity)); PresentationOrder.Add($"effect:{type:X2}"); return true; }, (parent, noDraw) => { ChildNoDraw.Add((parent, noDraw)); PresentationOrder.Add($"children:{noDraw}"); }, guid => { InvalidTargets.Add(guid); PresentationOrder.Add("target"); }, () => (1, 1), handlePartArrayEnterWorld: localEntityId => { PartArrayEnterWorld.Add(localEntityId); PartArrayShadowCounts.Add(Shadows.TotalRegistered); PresentationOrder.Add("part-array"); onPartArrayEnterWorld?.Invoke(localEntityId); }); } internal static WorldSession.EntitySpawn Spawn( PhysicsStateFlags state, ushort instanceSequence = 1) { var position = new CreateObject.ServerPosition( 0x01010001u, 10f, 10f, 5f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1); var physics = new PhysicsSpawnData( RawState: (uint)state, 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, MotionTableId: 0x09000001u, PhysicsState: (uint)state, InstanceSequence: instanceSequence, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } } }