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,

View file

@ -92,6 +92,23 @@ public enum RuntimePortalKind
Portal,
}
/// <summary>
/// Immutable, presentation-independent destination from one canonical
/// accepted local-player Position packet. The frame remains cell-local;
/// graphical hosts may translate it into their current render origin, while
/// headless hosts consume the same full retail cell identity directly.
/// </summary>
public readonly record struct RuntimeTeleportDestination(
uint EntityGuid,
ushort InstanceSequence,
ushort PositionSequence,
ushort TeleportSequence,
ushort ForcePositionSequence,
Position Position)
{
public uint CellId => Position.ObjCellId;
}
public readonly record struct RuntimeDestinationReadiness(
long Generation,
uint DestinationCell,

View file

@ -3,13 +3,16 @@ using System.Globalization;
namespace AcDream.Runtime.World;
/// <summary>
/// Canonical presentation-independent owner of one login/portal reveal
/// generation. The graphical or headless host supplies typed destination
/// readiness acknowledgements; Runtime owns which generation and destination
/// those acknowledgements are allowed to advance.
/// Canonical presentation-independent owner of local F751/Position
/// correlation and one login/portal reveal generation. The graphical or
/// headless host supplies typed destination readiness acknowledgements;
/// Runtime owns which teleport sequence, generation, and destination those
/// acknowledgements are allowed to advance.
/// </summary>
/// <remarks>
/// Retail ordering follows <c>SmartBox::TeleportPlayer @ 0x00453910</c>,
/// Retail ordering follows
/// <c>SmartBox::HandlePlayerTeleport @ 0x00452150</c>,
/// <c>SmartBox::TeleportPlayer @ 0x00453910</c>,
/// <c>SmartBox::UseTime @ 0x00455410</c>, and
/// <c>gmSmartBoxUI::EndTeleportAnimation @ 0x004D65A0</c>. Destination
/// materialization resumes simulation before the later normal-world viewport
@ -22,8 +25,19 @@ public sealed class RuntimeWorldTransitState
TimeSpan.FromSeconds(5);
private readonly Action<string> _log;
private readonly Dictionary<ushort, RuntimeTeleportDestination>
_bufferedDestinations = [];
private RuntimePortalSnapshot _snapshot = RuntimePortalSnapshot.Idle;
private long _nextGeneration;
private bool _hasLastTeleportStart;
private ushort _lastTeleportStartSequence;
private bool _hasPendingTeleportStart;
private ushort _pendingTeleportStartSequence;
private bool _teleportActive;
private ushort _activeTeleportSequence;
private bool _destinationAccepted;
private bool _hasAcceptedDestination;
private RuntimeTeleportDestination _acceptedDestination;
public RuntimeWorldTransitState(Action<string>? log = null)
{
@ -33,10 +47,70 @@ public sealed class RuntimeWorldTransitState
public RuntimePortalSnapshot Snapshot => _snapshot;
public bool IsWorldSimulationAvailable =>
_snapshot.WorldSimulationAvailable;
public bool IsTeleportActive => _teleportActive;
public ushort ActiveTeleportSequence =>
_teleportActive ? _activeTeleportSequence : (ushort)0;
public bool HasPendingTeleportStart => _hasPendingTeleportStart;
public bool HasAcceptedTeleportDestination => _hasAcceptedDestination;
public int BufferedTeleportDestinationCount =>
_bufferedDestinations.Count;
public int DiagnosticFailureCount { get; private set; }
public Exception? LastDiagnosticFailure { get; private set; }
public long BeginReveal(
/// <summary>
/// Starts initial-login reveal. Portal reveals must instead claim the
/// exact F751-correlated destination through
/// <see cref="TryBeginPortalReveal"/>.
/// </summary>
public long BeginLoginReveal(uint destinationCell) =>
BeginRevealCore(RuntimePortalKind.Login, destinationCell);
/// <summary>
/// Atomically claims the one accepted Position paired with the active F751
/// sequence and starts its reveal generation.
/// </summary>
public bool TryBeginPortalReveal(
ushort teleportSequence,
uint destinationCell,
out long generation)
{
generation = 0;
if (!_teleportActive
|| teleportSequence != _activeTeleportSequence
|| !_hasAcceptedDestination)
{
LogRejected(
"portal-begin-without-destination",
$"active={_teleportActive} "
+ $"expectedSequence={_activeTeleportSequence} "
+ $"actualSequence={teleportSequence} "
+ $"accepted={_hasAcceptedDestination}");
return false;
}
RuntimeTeleportDestination destination = _acceptedDestination;
if (destination.TeleportSequence != teleportSequence
|| destination.CellId == 0u
|| destination.CellId != destinationCell)
{
FailInvariant(
"portal-begin-destination-mismatch",
$"sequence={teleportSequence} "
+ $"acceptedSequence={destination.TeleportSequence} "
+ $"expectedCell=0x{destination.CellId:X8} "
+ $"actualCell=0x{destinationCell:X8}");
return false;
}
generation = BeginRevealCore(
RuntimePortalKind.Portal,
destinationCell);
_hasAcceptedDestination = false;
_acceptedDestination = default;
return true;
}
private long BeginRevealCore(
RuntimePortalKind kind,
uint destinationCell)
{
@ -79,6 +153,151 @@ public sealed class RuntimeWorldTransitState
return generation;
}
/// <summary>
/// Returns whether an F751 sequence is newer than this transit owner's
/// already-observed notification history. Canonical player TELEPORT_TS is
/// checked independently by the Runtime entity/physics owner.
/// </summary>
public bool CanQueueTeleportStart(ushort sequence) =>
!_hasLastTeleportStart
|| IsNewer(_lastTeleportStartSequence, sequence);
/// <summary>
/// Records one fresh F751 notification without advancing canonical
/// TELEPORT_TS. Presentation activation is a separate host edge.
/// </summary>
public bool TryQueueTeleportStart(ushort sequence)
{
if (!CanQueueTeleportStart(sequence))
return false;
PruneDestinationsOlderThan(sequence);
_teleportActive = false;
_activeTeleportSequence = 0;
_hasPendingTeleportStart = true;
_pendingTeleportStartSequence = sequence;
_hasLastTeleportStart = true;
_lastTeleportStartSequence = sequence;
_destinationAccepted = false;
_hasAcceptedDestination = false;
_acceptedDestination = default;
return true;
}
/// <summary>
/// Promotes the queued F751 lifetime once its host can enter portal space.
/// A Position accepted before that host edge becomes the same active
/// destination; no position or physics state is applied here.
/// </summary>
public bool ActivateQueuedTeleport()
{
if (!_hasPendingTeleportStart)
return false;
_teleportActive = true;
_activeTeleportSequence = _pendingTeleportStartSequence;
_hasPendingTeleportStart = false;
_pendingTeleportStartSequence = 0;
_destinationAccepted = false;
_hasAcceptedDestination = false;
_acceptedDestination = default;
if (!_bufferedDestinations.Remove(
_activeTeleportSequence,
out RuntimeTeleportDestination buffered))
{
return true;
}
_destinationAccepted = true;
_hasAcceptedDestination = true;
_acceptedDestination = buffered;
return true;
}
/// <summary>
/// Offers a destination only after canonical Runtime physics has accepted
/// and applied its Position packet. Ordinary, non-advancing movement can
/// never become a future portal destination.
/// </summary>
public bool OfferTeleportDestination(
in RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced)
{
ushort sequence = destination.TeleportSequence;
if (destination.CellId == 0u)
return false;
if (teleportTimestampAdvanced)
PruneDestinationsOlderThan(sequence);
if (_teleportActive && sequence == _activeTeleportSequence)
{
if (_destinationAccepted)
return false;
_destinationAccepted = true;
_hasAcceptedDestination = true;
_acceptedDestination = destination;
return true;
}
if (_hasPendingTeleportStart
&& sequence == _pendingTeleportStartSequence)
{
_bufferedDestinations.TryAdd(sequence, destination);
return false;
}
ushort correlationSequence = _hasPendingTeleportStart
? _pendingTeleportStartSequence
: _activeTeleportSequence;
bool mayBelongToFutureStart =
(!_teleportActive && !_hasPendingTeleportStart)
|| IsNewer(correlationSequence, sequence);
if (!teleportTimestampAdvanced || !mayBelongToFutureStart)
return false;
_bufferedDestinations.TryAdd(sequence, destination);
return false;
}
public bool TryGetAcceptedTeleportDestination(
out RuntimeTeleportDestination destination)
{
destination = _acceptedDestination;
return _teleportActive && _hasAcceptedDestination;
}
/// <summary>
/// Read-only preflight used immediately before the host mutates its
/// presentation placement. The post-placement acknowledgement repeats the
/// same exact validation.
/// </summary>
public bool CanPlacePortalDestination(
long generation,
ushort teleportSequence,
uint destinationCell) =>
IsCurrentPortalDestination(
generation,
teleportSequence,
destinationCell);
/// <summary>
/// Ends one graphical/headless portal consumer while preserving
/// duplicate/reorder history for the current network session.
/// </summary>
public void EndTeleport()
{
_teleportActive = false;
_activeTeleportSequence = 0;
_hasPendingTeleportStart = false;
_pendingTeleportStartSequence = 0;
_destinationAccepted = false;
_hasAcceptedDestination = false;
_acceptedDestination = default;
}
public bool AcknowledgeDestinationReadiness(
in RuntimeDestinationReadiness acknowledgement)
{
@ -116,10 +335,24 @@ public sealed class RuntimeWorldTransitState
return true;
}
public bool AcknowledgeMaterialized(
public bool AcknowledgePortalMaterialized(
long generation,
ushort teleportSequence,
uint destinationCell)
{
if (!IsCurrentPortalDestination(
generation,
teleportSequence,
destinationCell))
{
LogRejected(
"materialized-portal-mismatch",
$"generation={generation} "
+ $"sequence={teleportSequence} "
+ $"cell=0x{destinationCell:X8}");
return false;
}
if (!ValidateActive(generation, destinationCell, "materialized"))
return false;
if (_snapshot.Materialized)
@ -247,8 +480,26 @@ public sealed class RuntimeWorldTransitState
public void ResetSession()
{
_snapshot = RuntimePortalSnapshot.Idle;
EndTeleport();
_hasLastTeleportStart = false;
_lastTeleportStartSequence = 0;
_bufferedDestinations.Clear();
}
private bool IsCurrentPortalDestination(
long generation,
ushort teleportSequence,
uint destinationCell) =>
_teleportActive
&& teleportSequence == _activeTeleportSequence
&& generation != 0
&& generation == _snapshot.Generation
&& _snapshot.Kind == RuntimePortalKind.Portal
&& !_snapshot.Cancelled
&& !_snapshot.Completed
&& destinationCell != 0u
&& destinationCell == _snapshot.DestinationCell;
private bool ValidateActive(
long generation,
uint destinationCell,
@ -360,6 +611,21 @@ public sealed class RuntimeWorldTransitState
+ $"failures={snapshot.InvariantFailureCount}");
}
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);
}
}
private static bool IsIndoor(uint cellId) =>
(cellId & 0xFFFFu) >= 0x0100u;
}