feat(client): add rendezvous traversal coordinators (#12)
quality-gate / quality (push) Successful in 56s

This commit is contained in:
KyuubiYoru
2026-07-16 08:39:05 +02:00
parent 6d076c281a
commit b4b6072fe1
28 changed files with 2949 additions and 57 deletions
@@ -0,0 +1,124 @@
using System.Security.Cryptography;
using LiteNetLib;
namespace FinalFactory.Rendezvous.Client;
public enum RendezvousConnectionState
{
Punching = 1,
Connecting = 2,
Connected = 3,
Cancelled = 4,
TimedOut = 5,
Rejected = 6,
ManagerStopped = 7,
Disposed = 8,
}
public sealed class RendezvousConnectionCompletedEventArgs(
RendezvousConnectionState state,
NetPeer? peer = null) : EventArgs
{
public RendezvousConnectionState State { get; } = state;
public NetPeer? Peer { get; } = peer;
}
public sealed class RendezvousCoordinatorOptions
{
public int MaximumPunchRequests { get; set; } = 5;
public int MaximumAttemptChecksPerPoll { get; set; } = 128;
public TimeSpan InitialPunchRetryDelay { get; set; } = TimeSpan.FromMilliseconds(200);
public TimeSpan MaximumPunchRetryDelay { get; set; } = TimeSpan.FromSeconds(2);
public TimeSpan ConnectionTicketLifetime { get; set; } = TimeSpan.FromSeconds(20);
public double JitterRatio { get; set; } = 0.2;
internal RendezvousCoordinatorOptions CopyAndValidate()
{
if (MaximumPunchRequests is < 1 or > 20
|| MaximumAttemptChecksPerPoll is < 1 or > 1_024
|| InitialPunchRetryDelay < TimeSpan.FromMilliseconds(10)
|| MaximumPunchRetryDelay < InitialPunchRetryDelay
|| MaximumPunchRetryDelay > TimeSpan.FromSeconds(10)
|| ConnectionTicketLifetime <= TimeSpan.Zero
|| ConnectionTicketLifetime > TimeSpan.FromSeconds(20)
|| JitterRatio is < 0 or > 1)
{
throw new ArgumentOutOfRangeException(nameof(RendezvousCoordinatorOptions));
}
return new RendezvousCoordinatorOptions
{
MaximumPunchRequests = MaximumPunchRequests,
MaximumAttemptChecksPerPoll = MaximumAttemptChecksPerPoll,
InitialPunchRetryDelay = InitialPunchRetryDelay,
MaximumPunchRetryDelay = MaximumPunchRetryDelay,
ConnectionTicketLifetime = ConnectionTicketLifetime,
JitterRatio = JitterRatio,
};
}
}
internal interface IRendezvousCoordinatorClock
{
DateTimeOffset UtcNow { get; }
}
internal sealed class SystemRendezvousCoordinatorClock : IRendezvousCoordinatorClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
internal static class RendezvousManagerGuard
{
internal static void Validate(
NetManager manager,
RendezvousNetListener networkEvents)
{
networkEvents.ValidateManager(manager);
if (!manager.IsRunning)
{
throw new InvalidOperationException("The caller-owned LiteNetLib manager must be running.");
}
if (!manager.NatPunchEnabled
|| manager.UnsyncedEvents
|| manager.NatPunchModule.UnsyncedEvents)
{
throw new InvalidOperationException(
"The caller-owned manager must enable NAT punching and synchronized event dispatch.");
}
}
}
internal sealed class RendezvousPunchRetrySchedule(
RendezvousCoordinatorOptions options,
IRendezvousCoordinatorClock clock)
{
public int RequestsSent { get; private set; }
public DateTimeOffset NextRequestAt { get; private set; } = DateTimeOffset.MinValue;
public bool IsExhausted => RequestsSent >= options.MaximumPunchRequests;
public bool IsDue(DateTimeOffset now) => now >= NextRequestAt;
public void RecordRequest()
{
int exponent = Math.Min(RequestsSent, 30);
RequestsSent++;
double milliseconds = Math.Min(
options.InitialPunchRetryDelay.TotalMilliseconds * Math.Pow(2, exponent),
options.MaximumPunchRetryDelay.TotalMilliseconds);
if (options.JitterRatio > 0)
{
Span<byte> random = stackalloc byte[1];
RandomNumberGenerator.Fill(random);
double unit = random[0] / 255d;
double multiplier = 1 - options.JitterRatio + (2 * options.JitterRatio * unit);
milliseconds = Math.Min(
milliseconds * multiplier,
options.MaximumPunchRetryDelay.TotalMilliseconds);
}
NextRequestAt = clock.UtcNow + TimeSpan.FromMilliseconds(milliseconds);
}
}