228 lines
10 KiB
Markdown
228 lines
10 KiB
Markdown
# Game integration seams
|
|
|
|
Tracking: #20
|
|
|
|
Use the [TestClient start-to-finish guide](test-client.md) 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`](../releases/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:
|
|
|
|
```xml
|
|
<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.
|
|
|
|
```xml
|
|
<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](../releases/README.md)
|
|
when changing any dimension, and validate the generated
|
|
[OpenAPI v1 document](../api/rendezvous-v1.json) 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.
|
|
|
|
```csharp
|
|
RendezvousNetListener networkEvents = new();
|
|
NetManager gameplayNetwork = networkEvents.CreateManager();
|
|
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
|
|
```
|
|
|
|
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:
|
|
|
|
```csharp
|
|
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](../../src/FinalFactory.Rendezvous.Client/README.md).
|
|
|
|
## 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:
|
|
|
|
```csharp
|
|
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](../security/provisioning.md) 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:
|
|
|
|
1. install a new authorized key inside its `NotBefore`/`SignUntil` window;
|
|
2. begin issuing with it while the old key remains verify-only;
|
|
3. wait at least the maximum credential lifetime plus allowed clock skew;
|
|
4. retire the old verifier after `VerifyUntil` and 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](../operations/incident-runbooks.md#signing-key-or-issuer-compromise).
|
|
|
|
## 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:
|
|
|
|
1. provision the game/environment policy and externally held signing keys;
|
|
2. deploy one active service behind the source-preserving HTTPS/UDP topology in
|
|
[secure single-active Linux deployment](../deployment/linux.md);
|
|
3. install matching exact package versions in the game and set its service and
|
|
mediator endpoints through environment-specific configuration;
|
|
4. pass the TestClient health/publish/browse/punch/direct-traffic smoke using a
|
|
short-lived diagnostic credential;
|
|
5. run the topology harness and representative consumer-network trials; and
|
|
6. 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.
|