using FinalFactory.Rendezvous.Contracts; using LiteNetLib; namespace FinalFactory.Rendezvous.Client; public enum RendezvousConnectionOutcomeSource { RendezvousService = 1, LocalTraversal = 2, RemoteHost = 3, Caller = 4, Lifecycle = 5, } public enum RendezvousConnectionFailureCategory { None = 0, Directory = 1, Compatibility = 2, Authorization = 3, Capacity = 4, HostPresence = 5, Service = 6, Mediation = 7, NatTraversal = 8, DirectConnection = 9, Lifecycle = 10, } public enum RendezvousConnectionPhase { Directory = 1, Authorization = 2, Mediation = 3, NatTraversal = 4, DirectConnection = 5, Complete = 6, } public sealed class RendezvousConnectionOutcome { private readonly NetworkEndpoint? _dedicatedFallback; private RendezvousConnectionOutcome( ConnectionOutcomeKind kind, RendezvousConnectionOutcomeSource source, RendezvousConnectionFailureCategory category, RendezvousConnectionPhase phase, TimeSpan elapsed, RendezvousErrorCode? serviceError, NetworkEndpoint? dedicatedFallback, NetPeer? peer) { if (elapsed < TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(elapsed)); } if (dedicatedFallback is not null && !ContractValidation.IsNetworkEndpointValid(dedicatedFallback)) { throw new ArgumentException("The dedicated fallback endpoint is invalid.", nameof(dedicatedFallback)); } Kind = kind; Source = source; Category = category; Phase = phase; Elapsed = elapsed; ServiceError = serviceError; _dedicatedFallback = RendezvousEndpoint.Copy(dedicatedFallback); Peer = peer; } public ConnectionOutcomeKind Kind { get; } public RendezvousConnectionOutcomeSource Source { get; } public RendezvousConnectionFailureCategory Category { get; } public RendezvousConnectionPhase Phase { get; } public TimeSpan Elapsed { get; } public RendezvousErrorCode? ServiceError { get; } public NetworkEndpoint? DedicatedFallback => RendezvousEndpoint.Copy(_dedicatedFallback); public NetPeer? Peer { get; } public bool IsSuccess => Kind == ConnectionOutcomeKind.Connected; public bool HasDedicatedFallback => _dedicatedFallback is not null; public static RendezvousConnectionOutcome FromServiceError( RendezvousErrorCode error, TimeSpan elapsed, NetworkEndpoint? dedicatedFallback = null) { if (error == RendezvousErrorCode.None) { throw new ArgumentException("A service failure outcome requires an error.", nameof(error)); } (ConnectionOutcomeKind kind, RendezvousConnectionFailureCategory category, RendezvousConnectionPhase phase) = error switch { RendezvousErrorCode.NotFound => ( ConnectionOutcomeKind.DirectoryNotFound, RendezvousConnectionFailureCategory.Directory, RendezvousConnectionPhase.Directory), RendezvousErrorCode.Expired => ( ConnectionOutcomeKind.AttemptExpired, RendezvousConnectionFailureCategory.Authorization, RendezvousConnectionPhase.Authorization), RendezvousErrorCode.IncompatibleProtocol => ( ConnectionOutcomeKind.IncompatibleProtocol, RendezvousConnectionFailureCategory.Compatibility, RendezvousConnectionPhase.Directory), RendezvousErrorCode.AuthenticationRequired or RendezvousErrorCode.Forbidden or RendezvousErrorCode.ReplayRejected => ( ConnectionOutcomeKind.Unauthorized, RendezvousConnectionFailureCategory.Authorization, RendezvousConnectionPhase.Authorization), RendezvousErrorCode.RateLimited or RendezvousErrorCode.CapacityExceeded => ( ConnectionOutcomeKind.RateLimited, RendezvousConnectionFailureCategory.Capacity, RendezvousConnectionPhase.Authorization), RendezvousErrorCode.StaleHost => ( ConnectionOutcomeKind.NoHostPresence, RendezvousConnectionFailureCategory.HostPresence, RendezvousConnectionPhase.Mediation), RendezvousErrorCode.ServiceUnavailable => ( ConnectionOutcomeKind.ServiceUnavailable, RendezvousConnectionFailureCategory.Service, RendezvousConnectionPhase.Authorization), _ => ( ConnectionOutcomeKind.ServiceRejected, RendezvousConnectionFailureCategory.Service, RendezvousConnectionPhase.Authorization), }; return new( kind, RendezvousConnectionOutcomeSource.RendezvousService, category, phase, elapsed, error, dedicatedFallback, null); } public static ConnectionElapsedBucket BucketElapsed(TimeSpan elapsed) { if (elapsed < TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(elapsed)); } return elapsed.TotalSeconds switch { < 1 => ConnectionElapsedBucket.UnderOneSecond, < 5 => ConnectionElapsedBucket.OneToFiveSeconds, < 15 => ConnectionElapsedBucket.FiveToFifteenSeconds, < 30 => ConnectionElapsedBucket.FifteenToThirtySeconds, _ => ConnectionElapsedBucket.ThirtySecondsOrMore, }; } public override string ToString() => $"[RendezvousConnectionOutcome {Kind}; {Source}; credentials redacted]"; internal static RendezvousConnectionOutcome Create( ConnectionOutcomeKind kind, RendezvousConnectionOutcomeSource source, RendezvousConnectionFailureCategory category, RendezvousConnectionPhase phase, TimeSpan elapsed, NetworkEndpoint? dedicatedFallback = null, NetPeer? peer = null) => new( kind, source, category, phase, elapsed, null, dedicatedFallback, peer); }