110 lines
4.6 KiB
C#
110 lines
4.6 KiB
C#
using System.Numerics;
|
|
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<string>();
|
|
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<float>();
|
|
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_HiddenEnterWorldPublishesFinishedCyclicPoseWithoutAdvancingTime()
|
|
{
|
|
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<Setup>(dats.Get<Setup>(HumanSetup));
|
|
MotionTable table = Assert.IsType<MotionTable>(dats.Get<MotionTable>(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<Animation>(loader.LoadAnimation((uint)anim.AnimId));
|
|
int high = anim.HighFrame == -1 ? loaded.PartFrames.Count : anim.HighFrame;
|
|
aceDuration += (high - anim.LowFrame) / Math.Abs(anim.Framerate);
|
|
}
|
|
|
|
// Advance returns a reusable scratch buffer; snapshot the authored tail
|
|
// before HandleEnterWorld/SampleCurrentPose overwrites that buffer.
|
|
PartTransform[] recallTail = sequencer.Advance((float)aceDuration).ToArray();
|
|
Assert.False(
|
|
sequencer.CurrentNodeDiag.IsLooping,
|
|
"At ACE's exact teleport boundary the authored recall link is still the current node.");
|
|
|
|
// set_hidden -> CPartArray::HandleEnterWorld ->
|
|
// MotionTableManager::HandleEnterWorld removes link animations and
|
|
// selects the first cyclic (Ready) node. Hidden then suppresses time
|
|
// advance, but set_frame/UpdateParts still samples this new pose.
|
|
sequencer.Manager.HandleEnterWorld();
|
|
IReadOnlyList<PartTransform> finishedPose = sequencer.SampleCurrentPose();
|
|
|
|
Assert.True(sequencer.CurrentNodeDiag.IsLooping);
|
|
Assert.Equal(recallTail.Length, finishedPose.Count);
|
|
Assert.Contains(
|
|
Enumerable.Range(0, finishedPose.Count),
|
|
index => Vector3.DistanceSquared(
|
|
recallTail[index].Origin,
|
|
finishedPose[index].Origin) > 1e-6f
|
|
|| MathF.Abs(Quaternion.Dot(
|
|
Quaternion.Normalize(recallTail[index].Orientation),
|
|
Quaternion.Normalize(finishedPose[index].Orientation))) < 0.999999f);
|
|
}
|
|
}
|