refactor(runtime): own teleport destination correlation
This commit is contained in:
parent
38b3773cb9
commit
6a063a27d4
23 changed files with 1209 additions and 487 deletions
|
|
@ -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