refactor(runtime): own teleport destination correlation

This commit is contained in:
Erik 2026-07-26 17:52:34 +02:00
parent 38b3773cb9
commit 6a063a27d4
23 changed files with 1209 additions and 487 deletions

View file

@ -622,7 +622,7 @@ public sealed class GpuWorldStateVisibilityTests
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
long generation =
transit.BeginReveal(RuntimePortalKind.Portal, cell);
RuntimeWorldTransitTestDriver.BeginPortal(transit, cell);
var state = new GpuWorldState(availability: availability);
state.AddLandblock(new LoadedLandblock(
landblock,
@ -658,7 +658,10 @@ public sealed class GpuWorldStateVisibilityTests
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true)));
Assert.True(transit.AcknowledgeMaterialized(generation, cell));
Assert.True(RuntimeWorldTransitTestDriver.MaterializePortal(
transit,
generation,
cell));
Assert.True(transit.Complete(generation));
Assert.True(state.IsLiveEntityVisible(projectionKey));
Assert.True(record.IsSpatiallyVisible);
@ -705,7 +708,9 @@ public sealed class GpuWorldStateVisibilityTests
{
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
transit.BeginReveal(RuntimePortalKind.Portal, 0x10100001u);
RuntimeWorldTransitTestDriver.BeginPortal(
transit,
0x10100001u);
var centerStatic = Entity(1, 0u);
var centerLive = Entity(2, 0x70000001u);
var neighborStatic = Entity(3, 0u);

View file

@ -38,7 +38,7 @@ public sealed class LocalPlayerTeleportControllerTests
public void DestinationBeforeStart_IsReplayedOnceAfterPresentationActivates()
{
var harness = new Harness();
WorldSession.EntityPositionUpdate destination = Position(
RuntimeTeleportDestination destination = Position(
cellId: 0x20210001u,
teleportSequence: 7,
x: 12f,
@ -231,6 +231,24 @@ public sealed class LocalPlayerTeleportControllerTests
Assert.Equal(1, harness.Reveal.PortalMaterializationCount);
}
[Fact]
public void Place_RechecksExactRuntimeLifetimeBeforeMutatingPresentation()
{
var harness = new Harness();
harness.Controller.OnTeleportStarted(81);
harness.Controller.OfferDestination(
Position(0x20210001u, 81, 4f, 5f, 6f),
teleportTimestampAdvanced: true);
long generation = harness.Reveal.Snapshot.Generation;
Assert.True(harness.Transit.Cancel(generation));
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
harness.Controller.Tick(0.016f);
Assert.Equal(default, harness.Placement.Position);
Assert.Equal(0, harness.Reveal.PortalMaterializationCount);
}
[Fact]
public void LoginComplete_EntersWorldThenSendsThenCompletesAndResets()
{
@ -565,29 +583,21 @@ public sealed class LocalPlayerTeleportControllerTests
return int.MaxValue;
}
private static WorldSession.EntityPositionUpdate Position(
private static RuntimeTeleportDestination Position(
uint cellId,
ushort teleportSequence,
float x,
float y,
float z) => new(
0x50000001u,
new CreateObject.ServerPosition(
EntityGuid: 0x50000001u,
InstanceSequence: 1,
PositionSequence: 1,
TeleportSequence: teleportSequence,
ForcePositionSequence: 1,
Position: new Position(
cellId,
x,
y,
z,
1f,
0f,
0f,
0f),
null,
null,
true,
1,
1,
teleportSequence,
1);
new Vector3(x, y, z),
Quaternion.Identity));
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell) => new(
Guid: guid,
@ -651,6 +661,7 @@ public sealed class LocalPlayerTeleportControllerTests
public readonly FakePlacement Placement;
public readonly FakeSession Session;
public readonly FakePresentation Presentation;
public readonly RuntimeWorldTransitState Transit;
public readonly WorldRevealCoordinator Reveal;
public readonly LocalPlayerTeleportController Controller;
@ -666,8 +677,9 @@ public sealed class LocalPlayerTeleportControllerTests
Placement = new FakePlacement(order);
Session = new FakeSession(order);
Presentation = new FakePresentation(order);
Transit = new RuntimeWorldTransitState(order.Add);
Reveal = new WorldRevealCoordinator(
new RuntimeWorldTransitState(order.Add),
Transit,
isRenderNeighborhoodReady: (_, _) => worldReady,
isSpawnCellReady: _ => worldReady,
isTerrainNeighborhoodReady: (_, _) => worldReady,
@ -681,6 +693,7 @@ public sealed class LocalPlayerTeleportControllerTests
Input,
Mode,
Streaming,
Transit,
Reveal,
Placement,
Session,
@ -830,7 +843,7 @@ public sealed class LocalPlayerTeleportControllerTests
public List<uint> Starts { get; } = [];
public void OnTeleportStarted(uint sequence) => Starts.Add(sequence);
public void OfferDestination(
WorldSession.EntityPositionUpdate update,
RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced)
{
}

View file

@ -0,0 +1,50 @@
using AcDream.App.Streaming;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Runtime;
namespace AcDream.App.Tests.Streaming;
public sealed class RuntimeTeleportDestinationAdapterTests
{
[Fact]
public void AcceptedPosition_PreservesExactIdentityCellFrameAndSequences()
{
var update = new WorldSession.EntityPositionUpdate(
Guid: 0x50000001u,
Position: new CreateObject.ServerPosition(
LandblockId: 0x30310123u,
PositionX: -12.5f,
PositionY: 8.25f,
PositionZ: 91.75f,
RotationW: 0.5f,
RotationX: 0.25f,
RotationY: -0.125f,
RotationZ: 0.75f),
Velocity: new System.Numerics.Vector3(1f, 2f, 3f),
PlacementId: 0x65u,
IsGrounded: false,
InstanceSequence: 0x1234,
PositionSequence: 0x2345,
TeleportSequence: 0x3456,
ForcePositionSequence: 0x4567);
RuntimeTeleportDestination destination =
RuntimeTeleportDestinationAdapter.FromAcceptedPosition(update);
Assert.Equal(update.Guid, destination.EntityGuid);
Assert.Equal(update.InstanceSequence, destination.InstanceSequence);
Assert.Equal(update.PositionSequence, destination.PositionSequence);
Assert.Equal(update.TeleportSequence, destination.TeleportSequence);
Assert.Equal(
update.ForcePositionSequence,
destination.ForcePositionSequence);
Assert.Equal(update.Position.LandblockId, destination.CellId);
Assert.Equal(
new System.Numerics.Vector3(-12.5f, 8.25f, 91.75f),
destination.Position.Frame.Origin);
Assert.Equal(
new System.Numerics.Quaternion(0.25f, -0.125f, 0.75f, 0.5f),
destination.Position.Frame.Orientation);
}
}

View file

@ -1,133 +0,0 @@
using AcDream.App.Streaming;
namespace AcDream.App.Tests.Streaming;
public sealed class TeleportTransitCoordinatorTests
{
[Fact]
public void ReplacementRejectsDelayedPriorDestinationAndAcceptsCurrentAdvance()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.True(transit.QueueStart(10));
Assert.False(transit.Activate(out _));
transit.EndActive();
Assert.True(transit.QueueStart(11));
Assert.False(transit.Activate(out _));
Assert.False(transit.OfferDestination(10, true, 100, out _));
Assert.True(transit.OfferDestination(11, true, 110, out int accepted));
Assert.Equal(110, accepted);
}
[Fact]
public void PositionBeforeF751IsBufferedAndEqualFollowupCannotReplaceIt()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.False(transit.OfferDestination(11, true, 110, out _));
Assert.True(transit.QueueStart(11));
Assert.True(transit.Activate(out int buffered));
Assert.Equal(110, buffered);
Assert.False(transit.OfferDestination(11, false, 111, out _));
}
[Fact]
public void InactiveOrdinaryPositionIsNeverBuffered()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.False(transit.OfferDestination(11, false, 110, out _));
Assert.True(transit.QueueStart(11));
Assert.False(transit.Activate(out _));
}
[Fact]
public void DuplicateStartIsRejectedButSequenceWrapIsFresh()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.True(transit.QueueStart(ushort.MaxValue));
Assert.False(transit.Activate(out _));
transit.EndActive();
Assert.False(transit.CanBegin(ushort.MaxValue));
Assert.False(transit.QueueStart(ushort.MaxValue));
Assert.True(transit.CanBegin(0));
Assert.True(transit.QueueStart(0));
Assert.True(transit.HasPendingStart);
}
[Fact]
public void ExactHalfRangeUsesRetailLowerStampTieBreak()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.True(transit.QueueStart(0x8000));
Assert.False(transit.Activate(out _));
transit.EndActive();
Assert.True(transit.CanBegin(0x0000));
Assert.True(transit.QueueStart(0x0000));
}
[Fact]
public void ClearSessionAllowsSameSequenceInNewSession()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.True(transit.QueueStart(7));
Assert.False(transit.Activate(out _));
transit.ClearSession();
Assert.False(transit.IsActive);
Assert.True(transit.CanBegin(7));
}
[Fact]
public void PendingStartAcceptsEqualPositionUntilPresentationCanActivate()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.True(transit.QueueStart(11));
Assert.False(transit.OfferDestination(11, false, 110, out _));
Assert.True(transit.Activate(out int buffered));
Assert.Equal(110, buffered);
Assert.True(transit.IsActive);
}
[Fact]
public void AdvancingPositionPrunesSkippedDestination()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.False(transit.OfferDestination(11, true, 110, out _));
Assert.False(transit.OfferDestination(12, true, 120, out _));
Assert.True(transit.QueueStart(12));
Assert.True(transit.Activate(out int buffered));
Assert.Equal(120, buffered);
}
[Fact]
public void ObsoleteDestinationCannotSurviveSequenceWrap()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.False(transit.OfferDestination(ushort.MaxValue, true, 999, out _));
Assert.False(transit.OfferDestination(0, true, 0, out _));
Assert.True(transit.QueueStart(0));
Assert.True(transit.Activate(out int wrapped));
Assert.Equal(0, wrapped);
transit.EndActive();
// Advance in legal retail-newer hops until 0xFFFF is current again.
Assert.True(transit.QueueStart(0x7FFF));
Assert.False(transit.Activate(out _));
transit.EndActive();
Assert.True(transit.QueueStart(0xFFFE));
Assert.False(transit.Activate(out _));
transit.EndActive();
Assert.True(transit.QueueStart(ushort.MaxValue));
Assert.False(transit.Activate(out _));
}
}

View file

@ -37,7 +37,9 @@ public sealed class WorldGenerationQuiescenceTests
Assert.Single(world.LandblockBounds);
long generation =
transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell);
RuntimeWorldTransitTestDriver.BeginPortal(
transit,
DestinationCell);
world.CopyLiveEntitiesNearLandblock(LandblockId, 0, nearby);
Assert.False(availability.IsWorldAvailable);
@ -53,7 +55,8 @@ public sealed class WorldGenerationQuiescenceTests
Assert.False(availability.IsWorldAvailable);
Assert.True(transit.AcknowledgeDestinationReadiness(
Ready(generation, DestinationCell)));
Assert.True(transit.AcknowledgeMaterialized(
Assert.True(RuntimeWorldTransitTestDriver.MaterializePortal(
transit,
generation,
DestinationCell));
Assert.True(transit.Complete(generation));
@ -87,12 +90,14 @@ public sealed class WorldGenerationQuiescenceTests
WorldGenerationQuiescenceEdge firstEdge =
quiescence.CaptureBegin();
long first =
transit.BeginReveal(RuntimePortalKind.Login, DestinationCell);
transit.BeginLoginReveal(DestinationCell);
quiescence.CommitBegin(firstEdge);
WorldGenerationQuiescenceEdge secondEdge =
quiescence.CaptureBegin();
long second =
transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell);
RuntimeWorldTransitTestDriver.BeginPortal(
transit,
DestinationCell);
quiescence.CommitBegin(secondEdge);
Assert.False(transit.Complete(first));
@ -104,7 +109,8 @@ public sealed class WorldGenerationQuiescenceTests
Assert.True(transit.AcknowledgeDestinationReadiness(
Ready(second, DestinationCell)));
Assert.True(transit.AcknowledgeMaterialized(
Assert.True(RuntimeWorldTransitTestDriver.MaterializePortal(
transit,
second,
DestinationCell));
Assert.True(transit.Complete(second));
@ -130,7 +136,9 @@ public sealed class WorldGenerationQuiescenceTests
audio: null);
WorldGenerationQuiescenceEdge edge = quiescence.CaptureBegin();
transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell);
RuntimeWorldTransitTestDriver.BeginPortal(
transit,
DestinationCell);
quiescence.CommitBegin(edge);
Assert.Equal(inventoryGuid, selection.SelectedObjectId);

View file

@ -1,5 +1,7 @@
using System.Numerics;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Core.Selection;
using AcDream.Runtime;
using AcDream.Runtime.World;
@ -10,6 +12,7 @@ public sealed class WorldRevealCoordinatorTests
{
private sealed class State
{
public RuntimeWorldTransitState Transit { get; private set; } = null!;
public bool RenderReady { get; set; }
public bool CollisionReady { get; set; }
public bool CompositesReady { get; set; }
@ -24,6 +27,7 @@ public sealed class WorldRevealCoordinatorTests
{
transit ??= new RuntimeWorldTransitState(
logs is null ? null : new Action<string>(logs.Add));
Transit = transit;
return new WorldRevealCoordinator(
transit,
isRenderNeighborhoodReady: (_, _) => RenderReady,
@ -48,7 +52,7 @@ public sealed class WorldRevealCoordinatorTests
var state = new State { CompositesReady = true };
WorldRevealCoordinator coordinator = state.Build();
long generation = coordinator.Begin(RuntimePortalKind.Login, 0x11340021u);
long generation = coordinator.BeginLogin(0x11340021u);
Assert.Equal(1, generation);
Assert.Equal(1, state.InvalidateCount);
@ -66,7 +70,7 @@ public sealed class WorldRevealCoordinatorTests
CollisionReady = true,
};
WorldRevealCoordinator coordinator = state.Build();
coordinator.Begin(RuntimePortalKind.Login, outdoorCell);
coordinator.BeginLogin(outdoorCell);
WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell);
state.CompositesReady = true;
@ -91,12 +95,16 @@ public sealed class WorldRevealCoordinatorTests
CompositesReady = true,
};
WorldRevealCoordinator coordinator = state.Build(logs);
coordinator.Begin(RuntimePortalKind.Portal, outdoorCell);
long generation = BeginPortal(
coordinator,
state.Transit,
outdoorCell,
sequence: 1);
state.CompositesReady = true;
Assert.True(coordinator.Evaluate(outdoorCell).IsReady);
coordinator.ObserveMaterialized();
coordinator.ObserveMaterialized();
coordinator.ObserveMaterialized(generation, 1, outdoorCell);
coordinator.ObserveMaterialized(generation, 1, outdoorCell);
coordinator.ObserveWorldViewportVisible();
coordinator.Complete();
@ -108,6 +116,40 @@ public sealed class WorldRevealCoordinatorTests
Assert.Single(logs, line => line.Contains("event=materialized", StringComparison.Ordinal));
}
[Fact]
public void PortalBegin_WrongSequenceOrCellCannotAcquireHostResources()
{
const uint destinationCell = 0x3032001Cu;
var state = new State { CompositesReady = true };
var scheduler = new RecordingDestinationScheduler();
WorldRevealCoordinator coordinator = state.Build(streaming: scheduler);
RuntimeWorldTransitTestDriver.AcceptPortalDestination(
state.Transit,
destinationCell,
sequence: 7);
Assert.False(coordinator.TryBeginPortal(
8,
destinationCell,
out _));
Assert.False(coordinator.TryBeginPortal(
7,
0x11340021u,
out _));
Assert.Empty(scheduler.Begins);
Assert.Equal(0, state.InvalidateCount);
Assert.Equal(0, coordinator.Snapshot.Generation);
Assert.True(coordinator.TryBeginPortal(
7,
destinationCell,
out long generation));
Assert.Equal(
(generation, destinationCell, 1),
Assert.Single(scheduler.Begins));
Assert.Equal(1, state.InvalidateCount);
}
[Fact]
public void Cancel_PreventsLateDestinationWorkFromMutatingLifecycle()
{
@ -118,11 +160,18 @@ public sealed class WorldRevealCoordinatorTests
CompositesReady = true,
};
WorldRevealCoordinator coordinator = state.Build();
coordinator.Begin(RuntimePortalKind.Portal, 0x11340021u);
long generation = BeginPortal(
coordinator,
state.Transit,
0x11340021u,
sequence: 1);
coordinator.Cancel();
coordinator.PrepareAndEvaluate(0x11340021u);
coordinator.ObserveMaterialized();
coordinator.ObserveMaterialized(
generation,
1,
0x11340021u);
coordinator.ObserveWorldViewportVisible();
coordinator.Complete();
@ -155,14 +204,22 @@ public sealed class WorldRevealCoordinatorTests
isSpawnClaimUnhydratable: _ => false,
quiescence: quiescence);
Assert.Equal(1, coordinator.Begin(RuntimePortalKind.Login, 0x11340021u));
Assert.Equal(2, coordinator.Begin(RuntimePortalKind.Portal, 0x11340021u));
Assert.Equal(1, coordinator.BeginLogin(0x11340021u));
long generation = BeginPortal(
coordinator,
transit,
0x11340021u,
sequence: 1);
Assert.Equal(2, generation);
Assert.False(availability.IsWorldAvailable);
Assert.Equal(2, availability.QuiescedGeneration);
Assert.Equal(1, audio.SuspendCalls);
Assert.True(coordinator.Evaluate(0x11340021u).IsReady);
coordinator.ObserveMaterialized();
coordinator.ObserveMaterialized(
generation,
1,
0x11340021u);
coordinator.Complete();
Assert.True(availability.IsWorldAvailable);
@ -193,14 +250,21 @@ public sealed class WorldRevealCoordinatorTests
isSpawnClaimUnhydratable: _ => false,
quiescence: quiescence,
streaming: scheduler);
long generation = coordinator.Begin(
RuntimePortalKind.Portal,
0x3032001Cu);
const uint destinationCell = 0x3032001Cu;
const ushort teleportSequence = 1;
long generation = BeginPortal(
coordinator,
transit,
destinationCell,
teleportSequence);
Assert.False(availability.IsWorldAvailable);
Assert.True(coordinator.Evaluate(0x3032001Cu).IsReady);
Assert.True(coordinator.Evaluate(destinationCell).IsReady);
coordinator.ObserveMaterialized();
coordinator.ObserveMaterialized(
generation,
teleportSequence,
destinationCell);
Assert.True(availability.IsWorldAvailable);
Assert.False(coordinator.Snapshot.WorldViewportObserved);
@ -259,13 +323,21 @@ public sealed class WorldRevealCoordinatorTests
() => calls.Add("spatial")),
availability);
coordinator.Begin(RuntimePortalKind.Portal, 0x3032001Cu);
const uint destinationCell = 0x3032001Cu;
long generation = BeginPortal(
coordinator,
transit,
destinationCell,
sequence: 1);
frame.Tick(1f / 60f);
Assert.Equal(["network", "commands"], calls);
calls.Clear();
Assert.True(coordinator.Evaluate(0x3032001Cu).IsReady);
coordinator.ObserveMaterialized();
Assert.True(coordinator.Evaluate(destinationCell).IsReady);
coordinator.ObserveMaterialized(
generation,
1,
destinationCell);
frame.Tick(1f / 60f);
Assert.Equal(["objects", "network", "commands", "spatial"], calls);
@ -297,9 +369,11 @@ public sealed class WorldRevealCoordinatorTests
isSpawnClaimUnhydratable: _ => false,
quiescence: quiescence,
streaming: scheduler);
long generation = coordinator.Begin(
RuntimePortalKind.Portal,
0x3032001Cu);
long generation = BeginPortal(
coordinator,
transit,
0x3032001Cu,
sequence: 1);
coordinator.ResetSession();
@ -317,12 +391,17 @@ public sealed class WorldRevealCoordinatorTests
var scheduler = new RecordingDestinationScheduler();
WorldRevealCoordinator coordinator = state.Build(streaming: scheduler);
long first = coordinator.Begin(
RuntimePortalKind.Portal,
0x11340021u);
long second = coordinator.Begin(
RuntimePortalKind.Portal,
0x20210123u);
long first = BeginPortal(
coordinator,
state.Transit,
0x11340021u,
sequence: 1);
state.Transit.EndTeleport();
long second = BeginPortal(
coordinator,
state.Transit,
0x20210123u,
sequence: 2);
Assert.Equal(
[
@ -342,12 +421,17 @@ public sealed class WorldRevealCoordinatorTests
WorldRevealCoordinator coordinator =
state.Build(renderResources: resources);
long first = coordinator.Begin(
RuntimePortalKind.Portal,
0x11340021u);
long second = coordinator.Begin(
RuntimePortalKind.Portal,
0x20210123u);
long first = BeginPortal(
coordinator,
state.Transit,
0x11340021u,
sequence: 1);
state.Transit.EndTeleport();
long second = BeginPortal(
coordinator,
state.Transit,
0x20210123u,
sequence: 2);
Assert.Equal([first, second], resources.Begins);
coordinator.RevealWorldViewport();
@ -356,6 +440,33 @@ public sealed class WorldRevealCoordinatorTests
Assert.Equal([second], resources.Ends);
}
private static long BeginPortal(
WorldRevealCoordinator coordinator,
RuntimeWorldTransitState transit,
uint destinationCell,
ushort sequence)
{
Assert.True(transit.TryQueueTeleportStart(sequence));
Assert.True(transit.ActivateQueuedTeleport());
Assert.True(transit.OfferTeleportDestination(
new RuntimeTeleportDestination(
EntityGuid: 0x50000001u,
InstanceSequence: 1,
PositionSequence: 1,
TeleportSequence: sequence,
ForcePositionSequence: 1,
Position: new Position(
destinationCell,
Vector3.Zero,
Quaternion.Identity)),
teleportTimestampAdvanced: true));
Assert.True(coordinator.TryBeginPortal(
sequence,
destinationCell,
out long generation));
return generation;
}
private sealed class RecordingAudioQuiescence : AcDream.App.Audio.IWorldAudioQuiescence
{
public int SuspendCalls { get; private set; }