acdream/src/AcDream.Runtime/World/RuntimeWorldTransitState.cs

631 lines
20 KiB
C#

using System.Globalization;
namespace AcDream.Runtime.World;
/// <summary>
/// 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::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
/// edge.
/// </remarks>
public sealed class RuntimeWorldTransitState
: IRuntimePortalView
{
public static readonly TimeSpan RetailWaitCueDelay =
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)
{
_log = log ?? (_ => { });
}
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; }
/// <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)
{
if (kind is RuntimePortalKind.None)
throw new ArgumentOutOfRangeException(nameof(kind));
if (destinationCell == 0u)
throw new ArgumentOutOfRangeException(nameof(destinationCell));
if (_snapshot.Generation != 0
&& !_snapshot.Cancelled
&& !_snapshot.WorldViewportObserved)
{
Log("superseded", _snapshot);
}
long generation = checked(++_nextGeneration);
int failures = _snapshot.InvariantFailureCount;
int materializations = _snapshot.PortalMaterializationCount;
_snapshot = new RuntimePortalSnapshot(
generation,
kind,
new RuntimeDestinationReadiness(
generation,
destinationCell,
IsIndoor: IsIndoor(destinationCell),
IsUnhydratable: false,
RequiredRenderRadius: 0,
IsRenderNeighborhoodReady: false,
AreCompositeTexturesReady: false,
IsCollisionReady: false),
Materialized: false,
Completed: false,
Cancelled: false,
WorldViewportObserved: false,
WorldSimulationAvailable: false,
InvariantFailureCount: failures,
WaitCueShown: false,
PortalMaterializationCount: materializations);
Log("begin", _snapshot);
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)
{
if (!ValidateActive(
acknowledgement.Generation,
acknowledgement.DestinationCell,
"readiness"))
{
return false;
}
if (_snapshot.Readiness.IsReady)
return false;
bool isIndoor = IsIndoor(acknowledgement.DestinationCell);
int requiredRenderRadius = isIndoor ? 0 : 1;
if (acknowledgement.IsIndoor != isIndoor
|| acknowledgement.RequiredRenderRadius
!= requiredRenderRadius)
{
FailInvariant(
"invalid-readiness-shape",
$"indoor={acknowledgement.IsIndoor} "
+ $"expectedIndoor={isIndoor} "
+ $"radius={acknowledgement.RequiredRenderRadius} "
+ $"expectedRadius={requiredRenderRadius}");
return false;
}
if (_snapshot.Readiness == acknowledgement)
return false;
_snapshot = _snapshot with { Readiness = acknowledgement };
Log("readiness", _snapshot);
return true;
}
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)
return false;
if (!_snapshot.Readiness.IsReady)
{
FailInvariant("materialized-before-ready", null);
return false;
}
int count = _snapshot.PortalMaterializationCount;
if (_snapshot.Kind == RuntimePortalKind.Portal)
count = checked(count + 1);
_snapshot = _snapshot with
{
Materialized = true,
WorldSimulationAvailable = true,
PortalMaterializationCount = count,
};
Log("materialized", _snapshot);
return true;
}
public bool AcknowledgeWorldViewportVisible(long generation)
{
if (!ValidateGeneration(
generation,
"world-visible",
allowCompleted: true))
{
return false;
}
if (_snapshot.WorldViewportObserved)
return false;
if (!_snapshot.Readiness.IsReady)
{
FailInvariant("viewport-before-ready", null);
return false;
}
_snapshot = _snapshot with { WorldViewportObserved = true };
Log("world-visible", _snapshot);
return true;
}
public bool ObserveWait(long generation, TimeSpan elapsed)
{
if (generation == 0
|| generation != _snapshot.Generation
|| _snapshot.Cancelled
|| _snapshot.Completed
|| elapsed < RetailWaitCueDelay)
{
return false;
}
if (!_snapshot.WaitCueShown)
{
_snapshot = _snapshot with { WaitCueShown = true };
SafeLog(
$"[world-reveal] event=wait-cue "
+ $"elapsedMs={elapsed.TotalMilliseconds:F0} "
+ Describe(_snapshot));
}
return true;
}
public bool Complete(long generation)
{
if (!ValidateGeneration(
generation,
"complete",
allowCompleted: true))
{
return false;
}
if (_snapshot.Completed)
return false;
if (!_snapshot.Readiness.IsReady)
{
FailInvariant("complete-before-ready", null);
return false;
}
if (_snapshot.Kind == RuntimePortalKind.Portal
&& !_snapshot.Materialized)
{
FailInvariant("portal-complete-before-materialized", null);
return false;
}
_snapshot = _snapshot with
{
Completed = true,
WorldSimulationAvailable = true,
};
Log("complete", _snapshot);
return true;
}
public bool Cancel(long generation)
{
if (generation == 0
|| generation != _snapshot.Generation
|| _snapshot.Cancelled
|| _snapshot.Completed)
{
return false;
}
_snapshot = _snapshot with
{
Cancelled = true,
WorldSimulationAvailable = true,
};
Log("cancel", _snapshot);
return true;
}
/// <summary>
/// Clears session-visible state while retaining the monotonically
/// increasing generation counter so a delayed prior-session host
/// acknowledgement cannot match a future generation.
/// </summary>
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,
string eventName)
{
if (!ValidateGeneration(
generation,
eventName,
allowCompleted: false))
{
return false;
}
if (destinationCell == 0u
|| destinationCell != _snapshot.DestinationCell)
{
FailInvariant(
$"{eventName}-destination-mismatch",
$"expected=0x{_snapshot.DestinationCell:X8} "
+ $"actual=0x{destinationCell:X8}");
return false;
}
return true;
}
private bool ValidateGeneration(
long generation,
string eventName,
bool allowCompleted)
{
if (generation == 0 || generation != _snapshot.Generation)
{
LogRejected(
$"{eventName}-generation-mismatch",
$"expected={_snapshot.Generation} actual={generation}");
return false;
}
if (_snapshot.Cancelled
|| (!allowCompleted && _snapshot.Completed))
{
LogRejected(
$"{eventName}-after-terminal",
$"completed={_snapshot.Completed} "
+ $"cancelled={_snapshot.Cancelled}");
return false;
}
return true;
}
private void FailInvariant(string reason, string? detail)
{
_snapshot = _snapshot with
{
InvariantFailureCount =
checked(_snapshot.InvariantFailureCount + 1),
};
string suffix =
string.IsNullOrWhiteSpace(detail) ? string.Empty : $" {detail}";
SafeLog(
$"[world-reveal] event=invariant-failure "
+ $"reason={reason}{suffix} {Describe(_snapshot)}");
}
private void LogRejected(string reason, string? detail)
{
string suffix =
string.IsNullOrWhiteSpace(detail) ? string.Empty : $" {detail}";
SafeLog(
$"[world-reveal] event=rejected "
+ $"reason={reason}{suffix} {Describe(_snapshot)}");
}
private void Log(string eventName, RuntimePortalSnapshot snapshot) =>
SafeLog($"[world-reveal] event={eventName} {Describe(snapshot)}");
private void SafeLog(string message)
{
try
{
_log(message);
}
catch (Exception error)
{
DiagnosticFailureCount =
checked(DiagnosticFailureCount + 1);
LastDiagnosticFailure = error;
}
}
private static string Describe(RuntimePortalSnapshot snapshot)
{
RuntimeDestinationReadiness ready = snapshot.Readiness;
return string.Create(
CultureInfo.InvariantCulture,
$"generation={snapshot.Generation} kind={snapshot.Kind} "
+ $"cell=0x{ready.DestinationCell:X8} "
+ $"indoor={ready.IsIndoor} "
+ $"radius={ready.RequiredRenderRadius} "
+ $"unhydratable={ready.IsUnhydratable} "
+ $"render={ready.IsRenderNeighborhoodReady} "
+ $"composites={ready.AreCompositeTexturesReady} "
+ $"collision={ready.IsCollisionReady} ready={ready.IsReady} "
+ $"materialized={snapshot.Materialized} "
+ $"completed={snapshot.Completed} "
+ $"cancelled={snapshot.Cancelled} "
+ $"visible={snapshot.WorldViewportObserved} "
+ $"simulation={snapshot.WorldSimulationAvailable} "
+ $"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;
}