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
+70 -5
View File
@@ -1,6 +1,6 @@
# FinalFactory.Rendezvous.Client
Godot-independent .NET publisher and session-browser SDK for Rendezvous v1.
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
@@ -53,10 +53,75 @@ string presenceToken = NatPunchRequestTokenCodec.Encode(
gameplayNetManager.NatPunchModule.SendNatIntroduceRequest(mediator, presenceToken);
```
The same codec creates `Host` tokens for host-polled invitations and `Client`
tokens for a created join attempt. Always send them from the same LiteNetLib
socket that will carry the direct game connection; the mediator ignores any
caller-supplied public endpoint.
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();
if (!gameplayNetManager.Start(0))
{
throw new InvalidOperationException("The gameplay UDP socket could not start.");
}
```
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 creates the HTTP attempt, then uses its own already-started
gameplay manager in the same frame loop:
```csharp
CreateJoinAttemptResponse attempt = (await joins.CreateAsync(
createJoinRequest,
cancellationToken)).Value
?? throw new InvalidOperationException("Join issuance failed.");
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 `Connected` supplies `ConnectedPeer`. Call `Cancel()` and then `Poll()` for
local cancellation, or `CancelAsync(joins, cancellationToken)` to also revoke the
service attempt. Terminal client paths release all event subscriptions. 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.
Lease renewal is explicit and caller-controlled: