fix(portal): synchronize destination presentation state

This commit is contained in:
Erik 2026-07-16 21:17:13 +02:00
parent 4b1bceefbb
commit e95f55f25b
42 changed files with 2815 additions and 288 deletions

View file

@ -1,3 +1,4 @@
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.World;
using AcDream.Content.Vfx;
@ -57,7 +58,7 @@ public sealed class RecallTeleportAnimationTests
}
[Fact]
public void InstalledRecall_RetiresBeforeTeleportHiddenStateIsDispatched()
public void InstalledRecall_HiddenEnterWorldPublishesFinishedCyclicPoseWithoutAdvancingTime()
{
string datDir = @"C:\Turbine\Asheron's Call";
if (!File.Exists(Path.Combine(datDir, "client_portal.dat")))
@ -81,38 +82,29 @@ public sealed class RecallTeleportAnimationTests
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(
// 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,
"Recall must reach the Ready cyclic tail before Hidden freezes PartArray playback.");
"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);
}
}