namespace AcDream.App.Streaming; /// /// 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. /// internal sealed class TeleportTransitCoordinator where TDestination : struct { private bool _hasLastStart; private ushort _lastStartSequence; private readonly Dictionary _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; /// Reject retransmitted or older F751 notifications. public bool CanBegin(ushort sequence) => !_hasLastStart || IsNewer(_lastStartSequence, sequence); /// /// Record a fresh notification lifetime. Presentation activation is a /// separate edge because acdream's optional developer camera can /// temporarily withdraw the normal player projection. /// 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; } /// /// Activate the queued presentation and consume a destination that was /// accepted before the player projection became available. /// 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; } /// /// 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. /// 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; } /// End presentation while retaining duplicate and reorder history. public void EndActive() { IsActive = false; Sequence = 0; _hasPendingStart = false; _pendingStartSequence = 0; _destinationAccepted = false; } /// Clear every correlation value at a network-session boundary. 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); } } }