10 KiB
Game integration seams
Tracking: #20
Use the TestClient start-to-finish guide before integrating a game. It proves the service and network path without engine or game code. This page documents only the seams that the diagnostic cannot choose for a game: package/version policy, ownership of the gameplay socket, host admission, metadata, credential custody, and deployment compatibility.
Packages and compatibility
Consume FinalFactory.Rendezvous.Client and
FinalFactory.Rendezvous.Contracts from the approved Gitea NuGet source and pin
both to the same exact released version. Do not use a floating version range.
The current release matrix is machine-readable in
compatibility.json; the same window is
available from authenticated GET /v1/operator/status.
The authoritative package feed is
https://git.finalfactory.de/api/packages/HeiKyu/nuget/index.json. Add it to the
consumer's NuGet.config and map only Rendezvous packages to it; retain the
consumer's existing NuGet.org mapping for other dependencies:
<packageSources>
<add key="FinalFactory" value="https://git.finalfactory.de/api/packages/HeiKyu/nuget/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="FinalFactory">
<package pattern="FinalFactory.Rendezvous.*" />
</packageSource>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
When the feed is anonymously readable, no reader credential is needed. If
registry policy requires authentication, use the platform's NuGet credential
provider or a protected per-user/CI NuGet configuration populated by the secret
manager. Never put a registry token in the project file, repository,
package-source URL, or dotnet command argument.
<ItemGroup>
<PackageReference Include="FinalFactory.Rendezvous.Client" Version="1.0.0" />
<PackageReference Include="FinalFactory.Rendezvous.Contracts" Version="1.0.0" />
</ItemGroup>
Run dotnet restore, then dotnet list package --include-transitive and verify
that Client and Contracts resolve to the same exact version and LiteNetLib to the
release matrix version before compiling the game.
Release 1.0.0 targets netstandard2.1, requires LiteNetLib 2.1.4, speaks HTTP,
UDP, and connection-ticket contract version 1, and requires an exact
tenant-configured gameplay protocol match. A package patch does not silently
change a wire version. Follow the release and migration policy
when changing any dimension, and validate the generated
OpenAPI v1 document rather than hand-building HTTP.
One caller-owned gameplay socket
Create the game's LiteNetLib manager through RendezvousNetListener; do not open
a separate NAT socket. The game owns start, stop, and disposal. A coordinator
owns polling while it is active, so call its Poll() once from the game/network
thread and do not also call NetManager.PollEvents() during that period.
RendezvousNetListener networkEvents = new();
NetManager gameplayNetwork = networkEvents.CreateManager();
gameplayNetwork.ChannelsCount = 3; // set the game's required count before Start
if (!gameplayNetwork.Start(gameplayPort))
{
throw new InvalidOperationException("Gameplay UDP socket could not start.");
}
using RendezvousHostCoordinator host = new(
gameplayNetwork,
networkEvents,
mediatorEndPoint,
publishedSession,
joinClient);
host.Poll(); // call each game frame while this coordinator owns polling
LiteNetLib defaults to one QoS channel. Set ChannelsCount before Start when
the game protocol uses additional channels; both peers must configure the same
count. Rendezvous does not choose, remap, or reserve a gameplay channel.
Register normal game callbacks on networkEvents.GameplayEvents. Rendezvous
reserves only its authenticated direct requests and forwards other callbacks.
The same socket sends host presence, punches through the mediator, establishes
the peer, and then carries gameplay. A NAT introduction is not success; accept a
peer only after the coordinator reports the typed Connected outcome.
Poll() does not fetch new invitations. Schedule
RefreshJoinAttemptsAsync repeatedly for the entire hosting lifetime using a
bounded caller-owned timer (the diagnostic uses 250 ms), never allow two refreshes
to overlap, and inspect each typed result. The refresh performs HTTP work and
queues a snapshot; it does not call the LiteNetLib manager. Continue calling
Poll() on the manager's owning thread so the queued snapshot, presence traffic,
and callbacks are processed. Run the lease maintainer concurrently and cancel
both loops before disposing the coordinator.
For example, start one sequential refresh loop when hosting begins and await it during shutdown:
static async Task RefreshInvitationsAsync(
RendezvousHostCoordinator host,
CancellationToken cancellationToken)
{
using PeriodicTimer timer = new(TimeSpan.FromMilliseconds(250));
do
{
RendezvousClientResult<int> result =
await host.RefreshJoinAttemptsAsync(cancellationToken);
if (!result.IsSuccess)
{
ObserveBoundedHostRefreshFailure(result.Error);
}
}
while (await timer.WaitForNextTickAsync(cancellationToken));
}
On the joining side, create an attempt through RendezvousJoinClient, then give
the issued attempt to RendezvousClientCoordinator using the same manager and
listener. Cancellation, outcome reporting, bounded deadlines, fallback, and
lease-maintainer examples are in the packaged
FinalFactory.Rendezvous.Client README.
Host admission remains game-owned
The coordinator privately validates and consumes the signed one-time connection
ticket before accepting the LiteNetLib transport request. Do not create a second
ConnectionTicketValidator beside it: the coordinator deliberately does not
expose the expected or presented ticket. A connected transport proves only that
Rendezvous authorized one attempt; it does not prove player identity,
entitlement, capacity, ban status, or gameplay compatibility.
Treat AttemptCompleted with a successful outcome and non-null Peer as the
start of game-owned admission. Keep that peer outside authoritative gameplay
until the game's normal authentication and admission exchange succeeds; disconnect
it on rejection or timeout:
host.AttemptCompleted += (_, completed) =>
{
if (!completed.Outcome.IsSuccess || completed.Peer is null)
{
return;
}
BeginBoundedGameAuthentication(
completed.Peer,
onAccepted: AdmitToAuthoritativeGameplay,
onRejected: peer => peer.Disconnect());
};
Revoke an attempt when the game cancels it. Never log a ticket or capability. A
successful Rendezvous check must not bypass the game's authentication or
authoritative server rules. ConnectionTicketValidator is a lower-level
primitive for a custom transport integration that owns the complete request
acceptance path; it is not an extra gate for RendezvousHostCoordinator.
Provision each game and environment
Provision game/environment scope before issuing credentials. The policy fixes enabled regions, exact gameplay protocols, visibility and publisher trust modes, metadata schema and byte budgets, quotas, and whether a dedicated fallback may be published. Unknown or disabled scope fails closed. Follow game provisioning and signing-key lifecycle for the complete schema, principal kinds, secret providers, overlap, and revocation.
Dedicated publisher credentials belong only on trusted hosting infrastructure. Never ship one in a player build, repository, image layer, appsettings file, URL, argument, log, crash report, or analytics event. Issue a short-lived credential scoped to one game/environment and its allowed regions from the trusted deployment boundary. Player-host grants are issued to an authenticated player session at runtime and are never embedded in the build. Player-host grants, dedicated publishers, anonymous unlisted hosts, and operators are separate principal kinds; do not interchange them.
Rotate signing keys with an overlap:
- install a new authorized key inside its
NotBefore/SignUntilwindow; - begin issuing with it while the old key remains verify-only;
- wait at least the maximum credential lifetime plus allowed clock skew;
- retire the old verifier after
VerifyUntiland preserve custody records.
A suspected compromise is not routine rotation: stop issuance, revoke the exact key through the protected operator route, remove or replace it in provisioning, invalidate affected credentials, and follow the key-compromise runbook.
Metadata is public and policy-owned
Treat listing metadata as untrusted public input. Define a small allowlist in
each provisioned game's MetadataValueMaxBytes, set RequiredMetadataKeys, and
keep MetadataMaxKeys and MetadataMaxBytes to the smallest useful values. Values
must be display data only—for example a bounded map or ruleset identifier. Never
publish player identity, free-form chat, secrets, access tokens, internal
addresses, world state, or data needed for authoritative gameplay.
The platform contract caps metadata at 32 keys, 256 UTF-8 bytes per value, and 4096 encoded bytes total; tenant policy can and should be smaller. Build version and display name are separately bounded public fields. Games must escape metadata for their UI and must not infer trust from a listing being present.
Local, staging, and production path
Use the checked-in Compose profile only for the local TestClient guide. For a real environment:
- provision the game/environment policy and externally held signing keys;
- deploy one active service behind the source-preserving HTTPS/UDP topology in secure single-active Linux deployment;
- install matching exact package versions in the game and set its service and mediator endpoints through environment-specific configuration;
- pass the TestClient health/publish/browse/punch/direct-traffic smoke using a short-lived diagnostic credential;
- run the topology harness and representative consumer-network trials; and
- monitor typed outcomes and bounded metrics before broad rollout.
Rendezvous v1 has no relay, account system, matchmaking engine, server-browser UI, gameplay authority, or durable session database. A game owns player-facing recovery and an explicit fallback. Do not describe direct traversal as guaranteed.