using System.Text.Json; using FinalFactory.Rendezvous.Contracts; using FinalFactory.Rendezvous.Server.State; namespace FinalFactory.Rendezvous.Server.Browser; internal sealed record BrowserServiceResult(RendezvousErrorCode Error, T? Value = default) { public bool Succeeded => Error == RendezvousErrorCode.None; } internal sealed class SessionBrowserService( IEphemeralRendezvousStore store, SessionBrowserCursorCodec cursors, IWallClock clock) { public BrowserServiceResult Browse( BrowseSessionsRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); RendezvousErrorCode validation = Validate(request); if (validation != RendezvousErrorCode.None) { return new(validation); } TenantScope scope = new(request.GameId, request.EnvironmentId); if (!cursors.TryDecode( request.Cursor, scope, request.ProtocolVersion, request.RegionId, request.ExcludeFull, clock.UtcNow, out SessionListingId? after)) { return new(RendezvousErrorCode.InvalidRequest); } VisibleListingQuery query = new( scope, request.ProtocolVersion, request.RegionId, request.PageSize + 1, after, request.ExcludeFull); StoreResult> found = store.BrowseVisibleListings( query, cancellationToken); if (!found.Succeeded || found.Value is null) { return new(found.Code == StoreResultCode.ServiceUnavailable ? RendezvousErrorCode.ServiceUnavailable : RendezvousErrorCode.InternalError); } List items = found.Value .Take(request.PageSize) .Select(ToContract) .ToList(); bool hasMore = found.Value.Count > request.PageSize; while (items.Count > 0) { string? nextCursor = hasMore ? cursors.Encode(query, items[^1].ListingId, clock.UtcNow) : null; BrowseSessionsResponse response = new() { Items = items, NextCursor = nextCursor }; if (JsonSerializer.SerializeToUtf8Bytes(response, ContractJson.Options).Length <= ContractLimits.BrowserResponseMaxBytes) { return new(RendezvousErrorCode.None, response); } items.RemoveAt(items.Count - 1); hasMore = true; } return new(RendezvousErrorCode.None, new BrowseSessionsResponse()); } public BrowserServiceResult Get( SessionListingId listingId, GameId gameId, EnvironmentId environmentId, uint protocolVersion, CancellationToken cancellationToken = default) { if (listingId.Value == Guid.Empty || string.IsNullOrEmpty(gameId.Value) || string.IsNullOrEmpty(environmentId.Value) || protocolVersion == 0) { return new(RendezvousErrorCode.InvalidRequest); } StoreResult found = store.GetListing(listingId, true, cancellationToken); if (!found.Succeeded || found.Value is null) { return new(found.Code == StoreResultCode.ServiceUnavailable ? RendezvousErrorCode.ServiceUnavailable : RendezvousErrorCode.NotFound); } StoredListing listing = found.Value; if (listing.Definition.Scope != new TenantScope(gameId, environmentId) || listing.Definition.ProtocolVersion != protocolVersion) { return new(RendezvousErrorCode.NotFound); } return new(RendezvousErrorCode.None, new GetSessionResponse { Session = ToContract(listing), }); } private static RendezvousErrorCode Validate(BrowseSessionsRequest request) { RendezvousErrorCode version = ContractValidation.ValidateContractVersion(request.ContractVersion); if (version != RendezvousErrorCode.None) { return version; } return string.IsNullOrEmpty(request.GameId.Value) || string.IsNullOrEmpty(request.EnvironmentId.Value) || request.ProtocolVersion == 0 || (request.RegionId.HasValue && string.IsNullOrEmpty(request.RegionId.Value.Value)) || !ContractValidation.IsPageSizeValid(request.PageSize) || !ContractValidation.IsCursorValid(request.Cursor) ? RendezvousErrorCode.InvalidRequest : RendezvousErrorCode.None; } private static SessionListing ToContract(StoredListing stored) => new() { ListingId = stored.Definition.ListingId, GameId = stored.Definition.Scope.GameId, EnvironmentId = stored.Definition.Scope.EnvironmentId, RegionId = stored.Definition.RegionId, ProtocolVersion = stored.Definition.ProtocolVersion, BuildVersion = stored.Definition.BuildVersion, DisplayName = stored.Definition.DisplayName, Visibility = stored.Definition.Visibility, PublisherTrustMode = stored.Definition.TrustMode, Capacity = new() { CurrentPlayers = stored.Definition.CurrentPlayers, MaximumPlayers = stored.Definition.MaximumPlayers, }, Metadata = stored.Definition.Metadata.ToDictionary( static item => item.Key, static item => item.Value, StringComparer.Ordinal), DedicatedFallback = StoredListing.CopyEndpoint(stored.Definition.DedicatedFallback), }; }