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

@ -741,6 +741,7 @@ internal sealed class SessionPlayerCompositionPhase
streamingOriginRecenter,
streaming,
sealedDungeonCells),
live.WorldTransit,
worldReveal,
new LocalPlayerTeleportPlacement(
d.PhysicsEngine,

View file

@ -5,6 +5,7 @@ using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Content;
@ -1764,7 +1765,8 @@ internal sealed class LiveEntityNetworkUpdateController
&& update.Guid == _playerServerGuid)
{
_localPlayerTeleport.OfferDestination(
update,
RuntimeTeleportDestinationAdapter.FromAcceptedPosition(
update),
timestamps.TeleportAdvanced);
}
}

View file

@ -5,12 +5,12 @@ using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Rendering;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Streaming;
@ -19,7 +19,7 @@ internal interface ILocalPlayerTeleportNetworkSink
void OnTeleportStarted(uint sequence);
void OfferDestination(
WorldSession.EntityPositionUpdate update,
RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced);
void ResetSession();
@ -56,9 +56,9 @@ internal sealed class DeferredLocalPlayerTeleportNetworkSink
public void OnTeleportStarted(uint sequence) => Required().OnTeleportStarted(sequence);
public void OfferDestination(
WorldSession.EntityPositionUpdate update,
RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced) =>
Required().OfferDestination(update, teleportTimestampAdvanced);
Required().OfferDestination(destination, teleportTimestampAdvanced);
public void ResetSession() => Required().ResetSession();
@ -372,9 +372,10 @@ internal sealed class LocalPlayerTeleportPresentation
}
/// <summary>
/// Single update-thread owner of the local F751/Position-correlated portal
/// lifetime: activation, destination aim, readiness hold, Place, viewport
/// transitions, LoginComplete, and session/reset symmetry.
/// Graphical-host orchestrator for Runtime's local F751/Position-correlated
/// portal lifetime. Runtime owns sequence/destination/reveal authority; this
/// update-thread adapter owns portal viewport activation, render-space aim,
/// readiness preparation, placement callbacks, and LoginComplete presentation.
/// </summary>
internal sealed class LocalPlayerTeleportController
: ILocalPlayerTeleportFramePhase,
@ -382,7 +383,7 @@ internal sealed class LocalPlayerTeleportController
AcDream.App.Interaction.ISelectionViewPlaneSource,
IDisposable
{
private readonly TeleportTransitCoordinator<WorldSession.EntityPositionUpdate> _transit = new();
private readonly RuntimeWorldTransitState _transit;
private readonly ILocalPlayerTeleportAuthority _authority;
private readonly ILocalPlayerTeleportInputLifetime _input;
private readonly ILocalPlayerTeleportModeOperations _mode;
@ -395,8 +396,7 @@ internal sealed class LocalPlayerTeleportController
private Vector3 _pendingPosition;
private uint _pendingCell;
private Quaternion _pendingRotation = Quaternion.Identity;
private bool _hasAcceptedDestination;
private WorldSession.EntityPositionUpdate _acceptedDestination;
private long _pendingRevealGeneration;
private float _holdSeconds;
private long _lifetimeGeneration;
private bool _disposed;
@ -406,6 +406,7 @@ internal sealed class LocalPlayerTeleportController
ILocalPlayerTeleportInputLifetime input,
ILocalPlayerTeleportModeOperations mode,
ILocalPlayerTeleportStreamingOperations streaming,
RuntimeWorldTransitState transit,
WorldRevealCoordinator worldReveal,
ILocalPlayerTeleportPlacement placement,
ILocalPlayerTeleportSession session,
@ -415,22 +416,24 @@ internal sealed class LocalPlayerTeleportController
_input = input ?? throw new ArgumentNullException(nameof(input));
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
_placement = placement ?? throw new ArgumentNullException(nameof(placement));
_session = session ?? throw new ArgumentNullException(nameof(session));
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
}
public bool IsActive => _transit.IsActive;
public bool IsActive => _transit.IsTeleportActive;
public bool IsPortalViewportVisible => _presentation.IsPortalViewportVisible;
public uint ActiveDestinationCell => _transit.IsActive ? _pendingCell : 0u;
public uint ActiveDestinationCell =>
_transit.IsTeleportActive ? _pendingCell : 0u;
public void OnTeleportStarted(uint sequence)
{
ThrowIfDisposed();
ushort teleportSequence = (ushort)sequence;
if (!_authority.IsFreshStart(teleportSequence)
|| !_transit.CanBegin(teleportSequence))
|| !_transit.CanQueueTeleportStart(teleportSequence))
{
return;
}
@ -444,24 +447,21 @@ internal sealed class LocalPlayerTeleportController
if (_lifetimeGeneration != resetGeneration)
return;
_transit.QueueStart(teleportSequence);
if (!_transit.TryQueueTeleportStart(teleportSequence))
return;
TryActivatePendingPresentation();
Console.WriteLine($"live: teleport queued (seq={sequence})");
}
public void OfferDestination(
WorldSession.EntityPositionUpdate update,
RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced)
{
ThrowIfDisposed();
if (_transit.OfferDestination(
update.TeleportSequence,
teleportTimestampAdvanced,
update,
out WorldSession.EntityPositionUpdate destination))
{
AcceptDestination(destination);
}
_transit.OfferTeleportDestination(
destination,
teleportTimestampAdvanced);
TryAimAcceptedDestination();
}
public void Tick(float deltaSeconds)
@ -469,11 +469,11 @@ internal sealed class LocalPlayerTeleportController
ThrowIfDisposed();
TryActivatePendingPresentation();
TryAimAcceptedDestination();
if (!_transit.IsActive)
if (!_transit.IsTeleportActive)
return;
long generation = _lifetimeGeneration;
ushort sequence = _transit.Sequence;
ushort sequence = _transit.ActiveTeleportSequence;
if (!_mode.TryEnterPortalSpace()
|| !IsCurrentLifetime(generation, sequence)
|| _mode.Controller is null)
@ -506,13 +506,23 @@ internal sealed class LocalPlayerTeleportController
switch (teleportEvent)
{
case TeleportAnimEvent.Place:
if (!_worldReveal.CanPlacePortalDestination(
_pendingRevealGeneration,
sequence,
_pendingCell))
{
return;
}
_placement.Place(
_pendingPosition,
_pendingCell,
_pendingRotation);
if (!IsCurrentLifetime(generation, sequence))
return;
_worldReveal.ObserveMaterialized();
_worldReveal.ObserveMaterialized(
_pendingRevealGeneration,
sequence,
_pendingCell);
if (!IsCurrentLifetime(generation, sequence))
return;
break;
@ -571,43 +581,43 @@ internal sealed class LocalPlayerTeleportController
private void TryActivatePendingPresentation()
{
if (!_transit.HasPendingStart)
if (!_transit.HasPendingTeleportStart)
return;
long generation = _lifetimeGeneration;
if (!_mode.TryEnterPortalSpace()
|| _lifetimeGeneration != generation
|| !_transit.HasPendingStart)
|| !_transit.HasPendingTeleportStart)
{
return;
}
_holdSeconds = 0f;
_presentation.Begin(_mode.Projection);
if (_lifetimeGeneration != generation || !_transit.HasPendingStart)
if (_lifetimeGeneration != generation
|| !_transit.HasPendingTeleportStart)
{
return;
}
bool hasBufferedDestination = _transit.Activate(out var destination);
if (hasBufferedDestination)
AcceptDestination(destination);
Console.WriteLine(
$"live: teleport presentation started (seq={_transit.Sequence})");
}
private void AcceptDestination(WorldSession.EntityPositionUpdate destination)
{
_acceptedDestination = destination;
_hasAcceptedDestination = true;
if (!_transit.ActivateQueuedTeleport())
return;
TryAimAcceptedDestination();
Console.WriteLine(
$"live: teleport presentation started "
+ $"(seq={_transit.ActiveTeleportSequence})");
}
private void TryAimAcceptedDestination()
{
if (!_hasAcceptedDestination || !_transit.IsActive)
if (!_transit.TryGetAcceptedTeleportDestination(
out RuntimeTeleportDestination destination))
{
return;
}
long generation = _lifetimeGeneration;
ushort sequence = _transit.Sequence;
ushort sequence = _transit.ActiveTeleportSequence;
PlayerMovementController? controller = _mode.Controller;
if (controller is null)
{
@ -622,26 +632,19 @@ internal sealed class LocalPlayerTeleportController
return;
}
WorldSession.EntityPositionUpdate destination = _acceptedDestination;
if (!AimDestination(destination, controller, generation, sequence))
return;
if (IsCurrentLifetime(generation, sequence))
{
_hasAcceptedDestination = false;
_acceptedDestination = default;
}
}
private bool AimDestination(
WorldSession.EntityPositionUpdate update,
RuntimeTeleportDestination destination,
PlayerMovementController controller,
long generation,
ushort sequence)
{
var position = update.Position;
int landblockX = (int)((position.LandblockId >> 24) & 0xFFu);
int landblockY = (int)((position.LandblockId >> 16) & 0xFFu);
Position position = destination.Position;
int landblockX = (int)((position.ObjCellId >> 24) & 0xFFu);
int landblockY = (int)((position.ObjCellId >> 16) & 0xFFu);
uint streamingOriginLandblockId = StreamingRegion.EncodeLandblockId(
_streaming.CenterX,
_streaming.CenterY);
@ -649,14 +652,11 @@ internal sealed class LocalPlayerTeleportController
(landblockX - _streaming.CenterX) * 192f,
(landblockY - _streaming.CenterY) * 192f,
0f);
var translated = new Vector3(
position.PositionX,
position.PositionY,
position.PositionZ) + origin;
Vector3 translated = position.Frame.Origin + origin;
TeleportLandblockTransition transition = TeleportLandblockTransition.Classify(
controller.CellId,
position.LandblockId,
position.ObjCellId,
streamingOriginLandblockId);
int oldX = (int)((transition.SourceLandblockId >> 24) & 0xFFu);
int oldY = (int)((transition.SourceLandblockId >> 16) & 0xFFu);
@ -669,7 +669,14 @@ internal sealed class LocalPlayerTeleportController
// Retail SmartBox enters blocking_for_cells before old-world object,
// physics, landscape, and ambient owners may advance again. Begin the
// reveal generation before a recenter can start detaching that world.
_worldReveal.Begin(RuntimePortalKind.Portal, position.LandblockId);
if (!_worldReveal.TryBeginPortal(
sequence,
position.ObjCellId,
out long revealGeneration))
{
return false;
}
_pendingRevealGeneration = revealGeneration;
if (!IsCurrentLifetime(generation, sequence))
return false;
@ -677,7 +684,7 @@ internal sealed class LocalPlayerTeleportController
if (transition.ChangesStreamingCenter)
{
bool isSealedDungeon = _streaming.IsSealedDungeon(
position.LandblockId);
position.ObjCellId);
if (!IsCurrentLifetime(generation, sequence))
return false;
@ -688,28 +695,24 @@ internal sealed class LocalPlayerTeleportController
if (!IsCurrentLifetime(generation, sequence))
return false;
worldPosition = new Vector3(
position.PositionX,
position.PositionY,
position.PositionZ);
position.Frame.Origin.X,
position.Frame.Origin.Y,
position.Frame.Origin.Z);
}
else
{
worldPosition = translated;
}
_pendingRotation = new Quaternion(
position.RotationX,
position.RotationY,
position.RotationZ,
position.RotationW);
_pendingRotation = position.Frame.Orientation;
_pendingPosition = worldPosition;
_pendingCell = position.LandblockId;
_pendingCell = position.ObjCellId;
_holdSeconds = 0f;
PhysicsDiagnostics.LogTeleport(
"AIM",
position.LandblockId,
$"seq={update.TeleportSequence} lb={landblockX},{landblockY} "
+ $"indoor={((position.LandblockId & 0xFFFFu) >= 0x0100u)} "
position.ObjCellId,
$"seq={destination.TeleportSequence} lb={landblockX},{landblockY} "
+ $"indoor={((position.ObjCellId & 0xFFFFu) >= 0x0100u)} "
+ $"playerCross={transition.CrossesLandblock} "
+ $"centerChange={transition.ChangesStreamingCenter}");
return true;
@ -718,20 +721,11 @@ internal sealed class LocalPlayerTeleportController
private long ResetTransit(bool clearSession)
{
long generation = checked(++_lifetimeGeneration);
if (clearSession)
{
_transit.ClearSession();
}
else
{
_transit.EndActive();
}
_pendingPosition = default;
_pendingCell = 0u;
_pendingRotation = Quaternion.Identity;
_hasAcceptedDestination = false;
_acceptedDestination = default;
_pendingRevealGeneration = 0;
_holdSeconds = 0f;
_streaming.ResetRecenter(clearSession);
@ -741,7 +735,10 @@ internal sealed class LocalPlayerTeleportController
if (clearSession)
_worldReveal.ResetSession();
else
{
_transit.EndTeleport();
_worldReveal.Cancel();
}
if (_lifetimeGeneration != generation)
return generation;
@ -754,8 +751,8 @@ internal sealed class LocalPlayerTeleportController
private bool IsCurrentLifetime(long generation, ushort sequence) =>
_lifetimeGeneration == generation
&& _transit.IsActive
&& _transit.Sequence == sequence;
&& _transit.IsTeleportActive
&& _transit.ActiveTeleportSequence == sequence;
private void ThrowIfDisposed() =>
ObjectDisposedException.ThrowIf(_disposed, this);

View file

@ -0,0 +1,37 @@
using System.Numerics;
using AcDream.Core.Net;
using AcDream.Core.Physics;
using AcDream.Runtime;
namespace AcDream.App.Streaming;
/// <summary>
/// One-way boundary from an already accepted wire Position into Runtime's
/// presentation-independent teleport destination. Canonical physics consumes
/// the packet first; this adapter neither gates nor applies it.
/// </summary>
internal static class RuntimeTeleportDestinationAdapter
{
public static RuntimeTeleportDestination FromAcceptedPosition(
in WorldSession.EntityPositionUpdate update)
{
var position = update.Position;
return new RuntimeTeleportDestination(
update.Guid,
update.InstanceSequence,
update.PositionSequence,
update.TeleportSequence,
update.ForcePositionSequence,
new Position(
position.LandblockId,
new Vector3(
position.PositionX,
position.PositionY,
position.PositionZ),
new Quaternion(
position.RotationX,
position.RotationY,
position.RotationZ,
position.RotationW)));
}
}

View file

@ -1,150 +0,0 @@
namespace AcDream.App.Streaming;
/// <summary>
/// Correlates F751 notifications with accepted Position packets without
/// imposing presentation order on canonical physics. F751 deliberately does
/// not consume retail's TELEPORT_TS, and the matching Position may be observed
/// on either side of the notification.
/// </summary>
internal sealed class TeleportTransitCoordinator<TDestination>
where TDestination : struct
{
private bool _hasLastStart;
private ushort _lastStartSequence;
private readonly Dictionary<ushort, TDestination> _bufferedDestinations = new();
private bool _hasPendingStart;
private ushort _pendingStartSequence;
private bool _destinationAccepted;
public bool IsActive { get; private set; }
public ushort Sequence { get; private set; }
public bool HasPendingStart => _hasPendingStart;
/// <summary>Reject retransmitted or older F751 notifications.</summary>
public bool CanBegin(ushort sequence) =>
!_hasLastStart || IsNewer(_lastStartSequence, sequence);
/// <summary>
/// Record a fresh notification lifetime. Presentation activation is a
/// separate edge because acdream's optional developer camera can
/// temporarily withdraw the normal player projection.
/// </summary>
public bool QueueStart(ushort sequence)
{
if (!CanBegin(sequence))
return false;
PruneDestinationsOlderThan(sequence);
IsActive = false;
Sequence = 0;
_hasPendingStart = true;
_pendingStartSequence = sequence;
_hasLastStart = true;
_lastStartSequence = sequence;
_destinationAccepted = false;
return true;
}
/// <summary>
/// Activate the queued presentation and consume a destination that was
/// accepted before the player projection became available.
/// </summary>
public bool Activate(out TDestination bufferedDestination)
{
bufferedDestination = default;
if (!_hasPendingStart)
return false;
IsActive = true;
Sequence = _pendingStartSequence;
_hasPendingStart = false;
_pendingStartSequence = 0;
_destinationAccepted = false;
if (!_bufferedDestinations.Remove(Sequence, out bufferedDestination))
return false;
_destinationAccepted = true;
return true;
}
/// <summary>
/// Offer an accepted Position packet. A matching active transit consumes
/// its first packet even when TELEPORT_TS was already advanced. Without a
/// matching notification, only a timestamp-advancing packet is buffered;
/// ordinary movement can never become a future portal destination.
/// </summary>
public bool OfferDestination(
ushort sequence,
bool teleportTimestampAdvanced,
TDestination destination,
out TDestination acceptedDestination)
{
acceptedDestination = default;
// An advancing TELEPORT_TS makes every older destination impossible
// to match a future F751. Retire those generations now so a skipped
// notification cannot retain a stale destination through u16 wrap.
if (teleportTimestampAdvanced)
PruneDestinationsOlderThan(sequence);
if (IsActive && sequence == Sequence)
{
if (_destinationAccepted)
return false;
_destinationAccepted = true;
acceptedDestination = destination;
return true;
}
if (_hasPendingStart && sequence == _pendingStartSequence)
{
_bufferedDestinations.TryAdd(sequence, destination);
return false;
}
ushort correlationSequence = _hasPendingStart
? _pendingStartSequence
: Sequence;
bool mayBelongToFutureStart = (!IsActive && !_hasPendingStart)
|| IsNewer(correlationSequence, sequence);
if (!teleportTimestampAdvanced || !mayBelongToFutureStart)
return false;
_bufferedDestinations.TryAdd(sequence, destination);
return false;
}
/// <summary>End presentation while retaining duplicate and reorder history.</summary>
public void EndActive()
{
IsActive = false;
Sequence = 0;
_hasPendingStart = false;
_pendingStartSequence = 0;
_destinationAccepted = false;
}
/// <summary>Clear every correlation value at a network-session boundary.</summary>
public void ClearSession()
{
EndActive();
_hasLastStart = false;
_lastStartSequence = 0;
_bufferedDestinations.Clear();
}
private static bool IsNewer(ushort current, ushort incoming)
=> AcDream.Core.Physics.PhysicsTimestampGate.IsNewer(current, incoming);
private void PruneDestinationsOlderThan(ushort sequence)
{
foreach (ushort bufferedSequence in _bufferedDestinations.Keys.ToArray())
{
if (IsNewer(bufferedSequence, sequence))
_bufferedDestinations.Remove(bufferedSequence);
}
}
}

View file

@ -77,7 +77,7 @@ internal sealed class WorldRevealCoordinator
_transit.Snapshot.PortalMaterializationCount;
public bool WaitCueShown => _transit.Snapshot.WaitCueShown;
public long Begin(RuntimePortalKind kind, uint destinationCell)
public long BeginLogin(uint destinationCell)
{
if (destinationCell == 0u)
throw new ArgumentOutOfRangeException(nameof(destinationCell));
@ -85,7 +85,45 @@ internal sealed class WorldRevealCoordinator
WorldGenerationQuiescenceEdge quiescenceEdge =
_quiescence?.CaptureBegin() ?? default;
_readiness.Begin();
long generation = _transit.BeginReveal(kind, destinationCell);
long generation = _transit.BeginLoginReveal(destinationCell);
BeginHostLifetime(
generation,
destinationCell,
quiescenceEdge);
return generation;
}
public bool TryBeginPortal(
ushort teleportSequence,
uint destinationCell,
out long generation)
{
if (destinationCell == 0u)
throw new ArgumentOutOfRangeException(nameof(destinationCell));
WorldGenerationQuiescenceEdge quiescenceEdge =
_quiescence?.CaptureBegin() ?? default;
if (!_transit.TryBeginPortalReveal(
teleportSequence,
destinationCell,
out generation))
{
return false;
}
_readiness.Begin();
BeginHostLifetime(
generation,
destinationCell,
quiescenceEdge);
return true;
}
private void BeginHostLifetime(
long generation,
uint destinationCell,
in WorldGenerationQuiescenceEdge quiescenceEdge)
{
_quiescence?.CommitBegin(quiescenceEdge);
_reservationGeneration = generation;
_worldViewportReleased = false;
@ -94,7 +132,6 @@ internal sealed class WorldRevealCoordinator
destinationCell,
WorldRevealReadinessBarrier.RequiredRenderRadius(destinationCell));
_renderResources?.BeginDestinationReveal(generation);
return generation;
}
/// <summary>
@ -138,17 +175,27 @@ internal sealed class WorldRevealCoordinator
/// <c>TAS_TUNNEL_CONTINUE</c>. The normal world viewport is not restored
/// until the later <c>TUNNEL_FADE_OUT -&gt; WORLD_FADE_IN</c> edge.
/// </remarks>
public void ObserveMaterialized()
{
RuntimePortalSnapshot snapshot = _transit.Snapshot;
if (snapshot.Generation == 0)
return;
public bool CanPlacePortalDestination(
long generation,
ushort teleportSequence,
uint destinationCell) =>
_transit.CanPlacePortalDestination(
generation,
teleportSequence,
destinationCell);
public bool ObserveMaterialized(
long generation,
ushort teleportSequence,
uint destinationCell)
{
bool wasAvailable = _transit.IsWorldSimulationAvailable;
_transit.AcknowledgeMaterialized(
snapshot.Generation,
snapshot.DestinationCell);
bool acknowledged = _transit.AcknowledgePortalMaterialized(
generation,
teleportSequence,
destinationCell);
ObserveSimulationRelease(wasAvailable);
return acknowledged;
}
public void ObserveWorldViewportVisible()

View file

@ -238,7 +238,7 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
lbY,
isSealedDungeon: _sealedDungeonCells.IsSealedDungeon(
position.LandblockId));
_worldReveal.Begin(RuntimePortalKind.Login, position.LandblockId);
_worldReveal.BeginLogin(position.LandblockId);
return new(
true,