using AcDream.App.Rendering; using AcDream.App.World; using AcDream.Content.Vfx; using AcDream.Core.Physics; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Options; using DatReaderWriter.Types; using Xunit.Sdk; namespace AcDream.App.Tests.World; public sealed class RecallTeleportAnimationTests { private const uint HumanSetup = 0x02000001u; private const uint HumanMotionTable = 0x09000001u; private const uint NonCombat = 0x8000003Du; private const uint Ready = 0x41000003u; private const uint LifestoneRecall = 0x10000153u; [Fact] public void LiveFrame_AdvancesObjectRuntimeBeforeInboundNetwork() { var calls = new List(); var frame = new RetailLiveFrameCoordinator( _ => calls.Add("objects"), () => calls.Add("network"), () => calls.Add("commands"), () => calls.Add("spatial")); frame.Tick(1f / 60f); Assert.Equal(["objects", "network", "commands", "spatial"], calls); } [Fact] public void LiveFrame_InvalidHostDeltaCannotBlockNetworkDispatch() { var deltas = new List(); int networkDispatches = 0; var frame = new RetailLiveFrameCoordinator( deltas.Add, () => networkDispatches++, () => { }, () => { }); frame.Tick(float.NaN); frame.Tick(float.PositiveInfinity); frame.Tick(-1f); Assert.Equal([0f, 0f, 0f], deltas); Assert.Equal(3, networkDispatches); Assert.Equal(0.0, RetailLiveFrameCoordinator.NormalizeDeltaSeconds(double.NaN)); Assert.Equal(0.0, RetailLiveFrameCoordinator.NormalizeDeltaSeconds(double.NegativeInfinity)); Assert.Equal(0.0, RetailLiveFrameCoordinator.NormalizeDeltaSeconds(double.PositiveInfinity)); Assert.Equal(0.0, RetailLiveFrameCoordinator.NormalizeDeltaSeconds(double.MaxValue)); } [Fact] public void InstalledRecall_RetiresBeforeTeleportHiddenStateIsDispatched() { 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 loader = new RetailAnimationLoader(dats); var sequencer = new AnimationSequencer(setup, table, loader); sequencer.SetCycle(NonCombat, Ready); sequencer.PlayAction(LifestoneRecall); int readyKey = unchecked((int)((NonCombat << 16) | (Ready & 0xFFFFFFu))); MotionData action = table.Links[readyKey].MotionData[unchecked((int)LifestoneRecall)]; double aceDuration = 0.0; foreach (var anim in action.Anims) { Animation loaded = Assert.IsType(loader.LoadAnimation((uint)anim.AnimId)); int high = anim.HighFrame == -1 ? loaded.PartFrames.Count : anim.HighFrame; aceDuration += (high - anim.LowFrame) / Math.Abs(anim.Framerate); } const float updateStep = 1f / 60f; double elapsed = 0.0; bool hidden = false; bool hiddenPacketReady = false; var frame = new RetailLiveFrameCoordinator( dt => { if (!hidden) sequencer.Advance(dt); }, () => { if (hiddenPacketReady) hidden = true; }, () => { }, () => { }); // The recall motion is dispatched after an object's UseTime phase. // On the first later frame whose timestamp reaches ACE's scheduled // teleport boundary, retail advances the PartArray and only then // consumes the Hidden SetState from the inbound queue. while (elapsed < aceDuration) { hiddenPacketReady = elapsed + updateStep >= aceDuration; frame.Tick(updateStep); elapsed += updateStep; } Assert.True(hidden); Assert.True( sequencer.CurrentNodeDiag.IsLooping, "Recall must reach the Ready cyclic tail before Hidden freezes PartArray playback."); } }