150 lines
5 KiB
C#
150 lines
5 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|