# FinalFactory.Rendezvous.Client Godot-independent .NET publisher, browser, join, and LiteNetLib traversal SDK for Rendezvous v1. The package targets `netstandard2.1` and uses a caller-owned `HttpClient`. ```csharp using FinalFactory.Rendezvous.Client; using FinalFactory.Rendezvous.Contracts; using HttpClient http = new() { BaseAddress = new Uri("https://rendezvous.example/"), }; string publisherCredential = Environment.GetEnvironmentVariable( "RENDEZVOUS_PUBLISHER_CREDENTIAL") ?? throw new InvalidOperationException("Publisher credential is not configured."); CancellationToken cancellationToken = default; RendezvousPublisherClient publisher = new(http); RendezvousClientResult registered = await publisher.RegisterAsync( new RegisterSessionRequest { IdempotencyKey = Guid.NewGuid().ToString("N"), GameId = new("space-game"), EnvironmentId = new("production"), RegionId = new("eu-central"), ProtocolVersion = 7, BuildVersion = "1.0.0", DisplayName = "My server", Visibility = ListingVisibility.Public, Capacity = new() { CurrentPlayers = 1, MaximumPlayers = 8 }, DedicatedFallback = new() { AddressFamily = AddressFamilyKind.Ipv4, Address = "203.0.113.40", Port = 7777, }, }, publisherCredential, cancellationToken); if (!registered.IsSuccess || registered.Value is null) { throw new InvalidOperationException( $"Registration failed: {registered.Error} ({registered.Message})"); } ``` Load `publisherCredential` from the game's deployment secret boundary; never embed it in a client build or source control. A successful registration returns a `PublishedSession` containing the lease and host-presence capabilities. Send a periodic presence request from the host's gameplay `NetManager` using the server-controlled refresh interval and the fixed-size native token: ```csharp string presenceToken = NatPunchRequestTokenCodec.Encode( NatPunchPeerRole.HostPresence, session.HostPresenceHandle, session.HostPresenceCapability); gameplayNetManager.NatPunchModule.SendNatIntroduceRequest(mediator, presenceToken); ``` For direct connections, let the SDK drive those tokens from the same caller-owned LiteNetLib socket that carries gameplay. Ask the routing listener to create the bound manager, then configure and start that caller-owned manager yourself. The factory does not open a socket, and synchronized events must remain enabled: ```csharp RendezvousNetListener networkEvents = new(); NetManager gameplayNetManager = networkEvents.CreateManager(); gameplayNetManager.ChannelsCount = 3; // example: configure the game protocol first if (!gameplayNetManager.Start(0)) { throw new InvalidOperationException("The gameplay UDP socket could not start."); } ``` LiteNetLib defaults to one QoS channel. Set `ChannelsCount` before `Start` when the game protocol uses more than one; both game processes must agree. Rendezvous does not reserve or reinterpret any gameplay channel. The host polls join invitations asynchronously; that method only queues a snapshot and never calls the manager. `Poll()` is the sole SDK path that invokes LiteNetLib and dispatches its synchronized callbacks. Call it once per game frame on the thread that owns the manager: ```csharp RendezvousJoinClient joins = new(http); using RendezvousHostCoordinator host = new( gameplayNetManager, networkEvents, mediatorEndPoint, session, joins); // Run periodically from the game's normal async scheduling path. await host.RefreshJoinAttemptsAsync(cancellationToken); // Godot _Process, Update, or the equivalent main-thread frame callback. host.Poll(); ``` Do not also call `gameplayNetManager.PollEvents()` or `gameplayNetManager.NatPunchModule.PollEvents()` when a coordinator owns polling. The host coordinator refreshes host presence, punches for queued invitations, validates the introduction ticket, and accepts the direct request. Subscribe to `AttemptCompleted`; a `Connected` result is raised only after LiteNetLib reports the accepted peer as connected. Register ordinary gameplay callbacks on `networkEvents.GameplayEvents`; the routing listener reserves Rendezvous direct requests for ticket validation and forwards every other callback normally. The joining game first requests an attempt through the typed start API. It returns exactly one issued attempt or one terminal service outcome, so service authority is not confused with a later locally observed traversal failure: ```csharp RendezvousConnectionStartResult start = await joins.CreateConnectionAttemptAsync( createJoinRequest, cancellationToken: cancellationToken); if (start.Outcome is { } serviceOutcome) { ShowConnectionFailure(serviceOutcome.Kind, serviceOutcome.Category); return; } CreateJoinAttemptResponse attempt = start.Attempt ?? throw new InvalidOperationException("The typed start result was invalid."); using RendezvousClientCoordinator client = new( gameplayNetManager, networkEvents, mediatorEndPoint, attempt); // Godot _Process, Update, or the equivalent main-thread frame callback. client.Poll(); ``` NAT introduction changes the client state to `Connecting`; it is not success. Only a `Connected` outcome supplies `Peer`. Completion exposes a stable kind, source, category, phase, and elapsed duration. The default HTTP silence, punch, and direct-connect budgets are five, ten, and five seconds respectively; configure them through `RendezvousClientOptions` and `RendezvousCoordinatorOptions` when a game has measured reasons to do so. The signed attempt expiry is always the absolute upper bound. Call `Cancel()` and then `Poll()` for local cancellation, or `CancelAsync(joins, cancellationToken)` to also revoke the service attempt. Terminal client paths complete exactly once and release all event subscriptions, so late packets and callbacks are inert. Disposing a coordinator never stops or disposes the caller-owned manager and does not touch an in-flight peer; call `Cancel()` followed by `Poll()` first when that peer must also be disconnected. After terminal completion, reporting is explicit and safe to retry. It sends only the authenticated outcome enum and a coarse elapsed bucket—never the endpoint, exact duration, diagnostic text, metadata, or player identity: ```csharp RendezvousClientResult report = await client.ReportOutcomeAsync(joins, cancellationToken); ``` An optional `DedicatedFallback` is copied from the authoritative listing into the issued attempt and terminal outcome. A local deployment may replace it with `RendezvousCoordinatorOptions.DedicatedFallbackOverride`. The SDK only returns the endpoint; it never connects automatically. The game must explicitly decide whether to use it and then connect and authenticate through its own gameplay transport. If the outcome has no fallback, v1 offers no relay. Lease renewal is explicit and caller-controlled: ```csharp PublishedSession session = registered.Value; await using SessionLeaseMaintainer maintainer = publisher.CreateLeaseMaintainer( session, publisherCredential); LeaseMaintenanceResult stopped = await maintainer.RunAsync(cancellationToken); ``` Creating the maintainer does not start background work. Await its run and dispose it when hosting stops. Use `IRendezvousPublisherClient` and `IRendezvousSessionBrowserClient` as injection seams in game tests. The SDK disposes the requests and responses it creates but never disposes the supplied `HttpClient`. The host-side `ConnectionTicketValidator` is a bounded, thread-safe one-time gate. Authorize only tickets delivered by the authenticated Rendezvous introduction, then consume the exact ticket presented by the direct LiteNetLib connection: ```csharp using ConnectionTicketValidator tickets = new(); tickets.TryAuthorize(attemptId, expectedTicket, expiresAt); ConnectionTicketConsumptionResult admission = tickets.Consume( attemptId, presentedTicket); ``` An `Accepted` ticket authorizes only this connection attempt. The game must still apply its own player identity, capacity, ban, and gameplay admission rules. Revoke the attempt on cancellation and dispose the validator during host shutdown so its keyed ticket digests are zeroed. See the repository's ADR 0007 for HTTP ownership/retry semantics, ADR 0008 for join-capability and connection-ticket security semantics, and ADR 0010 for typed outcomes, deadlines, reporting, and caller-owned fallback.