203 lines
8.4 KiB
Markdown
203 lines
8.4 KiB
Markdown
# 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<PublishedSession> 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<ReportConnectionOutcomeResponse> 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.
|