refactor(runtime): own teleport destination correlation
This commit is contained in:
parent
38b3773cb9
commit
6a063a27d4
23 changed files with 1209 additions and 487 deletions
|
|
@ -741,6 +741,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
streamingOriginRecenter,
|
||||
streaming,
|
||||
sealedDungeonCells),
|
||||
live.WorldTransit,
|
||||
worldReveal,
|
||||
new LocalPlayerTeleportPlacement(
|
||||
d.PhysicsEngine,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 -> 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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue