From ebb5eb617c0bbb170418afab396b68584b7f992e Mon Sep 17 00:00:00 2001 From: KyuubiYoru Date: Thu, 16 Jul 2026 19:18:03 +0200 Subject: [PATCH] docs(integration): record SpaceGame pilot checkpoint (#21) --- README.md | 4 + deploy/compose/appsettings.Production.json | 8 +- docs/evidence/consumers/spacegame.json | 62 ++++++++++ docs/integration/sdk-seams.md | 5 + docs/integration/spacegame-pilot.md | 107 ++++++++++++++++++ scripts/mint-local-publisher-credential.sh | 4 +- src/FinalFactory.Rendezvous.Client/README.md | 5 + .../DocumentationContractTests.cs | 28 ++++- 8 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 docs/evidence/consumers/spacegame.json create mode 100644 docs/integration/spacegame-pilot.md diff --git a/README.md b/README.md index fd6b838..ea9e435 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,10 @@ versioning, and secure rollout seams are in the The always-on three-party scenarios, optional Linux namespace topology, and simulation limits are documented in the [deterministic topology harness](docs/integration/topology-harness.md). +The current consumer evidence and the still-open public-package, Godot-process, +fallback, reconnect, Linux, and external-NAT gates are tracked in the +[SpaceGame consumer pilot](docs/integration/spacegame-pilot.md). + ## Development diff --git a/deploy/compose/appsettings.Production.json b/deploy/compose/appsettings.Production.json index 6eb504c..ae8e1f4 100644 --- a/deploy/compose/appsettings.Production.json +++ b/deploy/compose/appsettings.Production.json @@ -39,14 +39,16 @@ "GameId": "space-game", "EnvironmentId": "smoke", "Enabled": true, - "ProtocolVersions": [1], + "ProtocolVersions": [1, 2], "Regions": ["local"], "VisibilityModes": ["Public"], "PublisherTrustModes": ["ManagedDedicated"], - "MetadataValueMaxBytes": {}, + "MetadataValueMaxBytes": { + "mode": 32 + }, "RequiredMetadataKeys": [], "MetadataMaxBytes": 512, - "MetadataMaxKeys": 0, + "MetadataMaxKeys": 1, "MaxListingsPerPrincipal": 10, "MaxAnonymousListingsPerAddress": 0, "MaxActiveJoinAttempts": 100, diff --git a/docs/evidence/consumers/spacegame.json b/docs/evidence/consumers/spacegame.json new file mode 100644 index 0000000..4d36d24 --- /dev/null +++ b/docs/evidence/consumers/spacegame.json @@ -0,0 +1,62 @@ +{ + "schemaVersion": "1.0", + "recordedAt": "2026-07-16", + "issue": 21, + "consumerIssue": "Kyuubi/SpaceGame#3", + "result": "checkpoint-pass-with-open-gates", + "rendezvousCommit": "7fb85059fb7003ca0984ebd0cc9541218d33ae93", + "consumerCommit": "86424ebde69a7eeae110106ab5f84c68f04af89b", + "packages": { + "FinalFactory.Rendezvous.Client": { + "version": "1.0.0", + "source": "local-candidate", + "sha256": "606711e4961867ca8fe95fde973aa85f2b6e7483cafe4867ca8daa918aa78b6b" + }, + "FinalFactory.Rendezvous.Contracts": { + "version": "1.0.0", + "source": "local-candidate", + "sha256": "394acf0a1419690829ff38eb214b1833cf64edbc85bbb7182345bfe77b8ec1f0" + }, + "LiteNetLib": { + "version": "2.1.4" + } + }, + "localRun": { + "processes": ["Rendezvous", "SpaceGame host pilot", "SpaceGame client pilot"], + "typedOutcome": "Connected", + "gameAdmission": "Accepted", + "directGameplay": true, + "directInputs": 1, + "directSnapshots": 1, + "rendezvousGameplayPayloads": 0, + "deregistered": true + }, + "negativePaths": { + "incompatibleProtocol": "proven", + "staleHostPresence": "proven", + "punchTimeout": "proven", + "invalidAdmission": "unit-and-protocol-boundary-proven", + "capacity": "regression-tested", + "fallbackConnection": "open", + "reconnect": "open" + }, + "verification": { + "debugBuild": "passed", + "releaseBuild": "passed", + "debugTests": { "passed": 29, "failed": 0 }, + "releaseTests": { "passed": 29, "failed": 0 }, + "format": "passed", + "shellcheck": "passed", + "twoProcessHarness": "passed", + "adversarialReview": "passed-after-fixes" + }, + "openGates": [ + "public-package-restore", + "actual-godot-process-integration", + "dedicated-fallback-connection", + "disconnect-reconnect", + "remote-linux-dedicated", + "representative-external-nat", + "spacegame-imgui-native-runtime-dependency" + ] +} diff --git a/docs/integration/sdk-seams.md b/docs/integration/sdk-seams.md index 0973552..16bbb76 100644 --- a/docs/integration/sdk-seams.md +++ b/docs/integration/sdk-seams.md @@ -70,6 +70,7 @@ thread and do not also call `NetManager.PollEvents()` during that period. ```csharp 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."); @@ -85,6 +86,10 @@ using RendezvousHostCoordinator host = new( 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 diff --git a/docs/integration/spacegame-pilot.md b/docs/integration/spacegame-pilot.md new file mode 100644 index 0000000..0db6015 --- /dev/null +++ b/docs/integration/spacegame-pilot.md @@ -0,0 +1,107 @@ +# SpaceGame consumer pilot + +Tracking: Rendezvous #21 and SpaceGame #3. + +The first SpaceGame checkpoint proves that the v1 client boundary can establish +authenticated direct LiteNetLib traffic without taking ownership of the game's +protocol, admission, player identity, entity identity, capacity, or gameplay +payloads. It does not yet pass the complete consumer-pilot gate. The public +package restore, actual Godot-process integration, connected fallback transport, +remote Linux dedicated run, representative external NAT run, and reconnect path +remain required before #21 can close. + +## Pinned checkpoint + +| Input | Value | +| --- | --- | +| Rendezvous source | `7fb85059fb7003ca0984ebd0cc9541218d33ae93` plus the #21 compatibility changes | +| SpaceGame source | `86424ebde69a7eeae110106ab5f84c68f04af89b` | +| Client package | `FinalFactory.Rendezvous.Client` `1.0.0` | +| Contracts package | `FinalFactory.Rendezvous.Contracts` `1.0.0` | +| LiteNetLib | `2.1.4` | +| HTTP, UDP, ticket contracts | `1` | +| SpaceGame gameplay protocol | `2` | + +At the checkpoint date, the Final Factory Gitea NuGet service was reachable but +both `FinalFactory.Rendezvous.*` `1.0.0` registrations returned HTTP 404. The run +therefore restored locally built candidate packages with the hashes recorded in +[`spacegame.json`](../evidence/consumers/spacegame.json). This proves candidate +compatibility, not immutable registry publication. The release package restore +must be repeated from the public feed. + +## Proven local path + +The SpaceGame host and client each create one caller-owned `NetManager`, set its +three gameplay QoS channels before `Start`, and give the same manager and +`RendezvousNetListener` to the coordinator. Rendezvous authenticates discovery, +join authorization, host presence, mediation, and connection outcome reporting. +After traversal, SpaceGame performs a separate audience-bound admission exchange +on its own reliable command channel. A trusted game-auth boundary mints the +opaque assertion; the player process never receives the signing key. + +The authoritative host rejects expired, replayed, incorrectly signed, +wrong-listing, duplicate-player, over-capacity, identity-mismatched, +out-of-sequence, and over-rate traffic. It assigns a canonical game entity ID +only after admission. The player ID, entity ID, listing ID, join-attempt ID, and +LiteNetLib peer ID remain distinct values. + +The bounded two-process harness observed: + +- host publication and lease maintenance; +- browser compatibility filtering and join authorization; +- typed traversal outcome `Connected`; +- successful audience-bound game admission; +- one reliable ordered input and one sequenced state snapshot on the direct + gameplay socket; +- zero SpaceGame gameplay payloads handled by Rendezvous; and +- successful host deregistration. + +Both Debug and Release builds passed. Both Debug and Release test runs passed 29 +tests with zero failures. The focused formatter, shell checker, clean +candidate-package restore, and adversarial branch review also passed. + +## Failure evidence + +| Path | Evidence | Status | +| --- | --- | --- | +| Incompatible protocol | protocol `999` returns no compatible listing and starts no traversal | Proven | +| Stale/no host presence | typed `NoHostPresence/RendezvousService/HostPresence/Mediation` | Proven | +| Traversal timeout | non-listening mediator produces typed `PunchTimedOut/LocalTraversal/NatTraversal/NatTraversal` | Proven | +| Rejected game admission | invalid signature and wrong audience deny gameplay; expiry and replay are regression-tested | Proven at protocol/unit boundary | +| Capacity and duplicate player | game-owned roster rejects both and publishes current capacity | Regression-tested | +| Configured fallback | policy excludes authorization, compatibility, and capacity failures | Policy only; transport path open | +| Disconnect | direct session ends and host deregisters | Proven for checkpoint shutdown | +| Reconnect | a second authenticated session after disconnect | Open | + +## Rendezvous-side compatibility fixes + +The pilot found generic integration gaps and keeps their fixes in this +repository: + +- the local production-shaped smoke tenant accepts SpaceGame gameplay protocol + `2` and the bounded `mode` metadata key; +- SDK guidance requires games using multiple LiteNetLib QoS channels to set + `ChannelsCount` before `Start` and states that Rendezvous reserves no gameplay + channel; and +- the local credential helper rejects any signing-key file with group or other + permissions, in addition to its ownership, symlink, and hard-link checks. + +Documentation contract tests cover these generic requirements. + +## Remaining acceptance gates + +Do not mark #21 passed until all of these have direct evidence: + +1. restore the exact immutable `1.0.0` packages from the public Gitea feed; +2. drive the host and client through actual Godot game processes, not only the + console pilot; +3. implement and exercise the configured dedicated fallback connection with the + same game admission and identity rules; +4. prove disconnect followed by a fresh authenticated reconnect; +5. run a remote client against a Linux dedicated host and capture normal + termination/drain behavior; +6. run representative external NAT canaries and record the network topology and + typed outcome; and +7. clear the current SpaceGame Linux runtime blocker: its pre-existing ImGui + GDExtension native library is absent. A headless process exit code of zero + while that load error is present is not runtime proof. diff --git a/scripts/mint-local-publisher-credential.sh b/scripts/mint-local-publisher-credential.sh index 330d385..b6151ac 100755 --- a/scripts/mint-local-publisher-credential.sh +++ b/scripts/mint-local-publisher-credential.sh @@ -42,8 +42,8 @@ if stat.S_ISLNK(parent.st_mode) or not stat.S_ISDIR(parent.st_mode): raise SystemExit(f"Local Compose secret directory must be a non-symlink directory: {parent_path}") if parent.st_uid != os.geteuid() or parent.st_mode & 0o077: raise SystemExit(f"Local Compose secret directory must be owned by this user with mode 0700: {parent_path}") -if metadata.st_uid != os.geteuid() or metadata.st_mode & 0o022 or metadata.st_nlink != 1: - raise SystemExit(f"Local Compose smoke key must be owned by this user, single-linked, and not group/world writable: {key_path}") +if metadata.st_uid != os.geteuid() or metadata.st_mode & 0o077 or metadata.st_nlink != 1: + raise SystemExit(f"Local Compose smoke key must be owned by this user, single-linked, and private to its owner: {key_path}") with open(key_path, "rb") as key_file: key = key_file.read(33) diff --git a/src/FinalFactory.Rendezvous.Client/README.md b/src/FinalFactory.Rendezvous.Client/README.md index 0910767..9ed7f9c 100644 --- a/src/FinalFactory.Rendezvous.Client/README.md +++ b/src/FinalFactory.Rendezvous.Client/README.md @@ -67,12 +67,17 @@ 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 diff --git a/tests/FinalFactory.Rendezvous.Tests/Documentation/DocumentationContractTests.cs b/tests/FinalFactory.Rendezvous.Tests/Documentation/DocumentationContractTests.cs index 5cafdb9..99800fd 100644 --- a/tests/FinalFactory.Rendezvous.Tests/Documentation/DocumentationContractTests.cs +++ b/tests/FinalFactory.Rendezvous.Tests/Documentation/DocumentationContractTests.cs @@ -53,7 +53,7 @@ public sealed partial class DocumentationContractTests Assert.Contains("now + 600", helper, StringComparison.Ordinal); Assert.Contains("stat.S_ISLNK", helper, StringComparison.Ordinal); Assert.Contains("parent.st_mode & 0o077", helper, StringComparison.Ordinal); - Assert.Contains("metadata.st_mode & 0o022", helper, StringComparison.Ordinal); + Assert.Contains("metadata.st_mode & 0o077", helper, StringComparison.Ordinal); Assert.Contains("metadata.st_nlink != 1", helper, StringComparison.Ordinal); Assert.Contains("mint-local-publisher-credential.sh", smoke, StringComparison.Ordinal); Assert.DoesNotContain("hexkey:", smoke, StringComparison.Ordinal); @@ -184,6 +184,32 @@ public sealed partial class DocumentationContractTests Assert.Equal(httpVersion, udpVersion); Assert.Equal(httpVersion, ticketVersion); Assert.Contains($"contract version `{httpVersion}`", guide, StringComparison.Ordinal); + Assert.Contains("gameplayNetwork.ChannelsCount = 3", guide, StringComparison.Ordinal); + Assert.Contains("defaults to one QoS channel", guide, StringComparison.Ordinal); + Assert.Contains("Rendezvous does not choose, remap, or reserve", guide, StringComparison.Ordinal); + } + + [Fact] + public void SpaceGamePilotEvidenceSeparatesProvenBehaviorFromOpenGates() + { + string root = FindRepositoryRoot(); + string guide = File.ReadAllText(Path.Combine(root, "docs", "integration", "spacegame-pilot.md")); + using JsonDocument evidence = JsonDocument.Parse(File.ReadAllText( + Path.Combine(root, "docs", "evidence", "consumers", "spacegame.json"))); + JsonElement record = evidence.RootElement; + + Assert.Equal("checkpoint-pass-with-open-gates", record.GetProperty("result").GetString()); + Assert.Equal(0, record.GetProperty("localRun").GetProperty("rendezvousGameplayPayloads").GetInt32()); + Assert.True(record.GetProperty("localRun").GetProperty("directGameplay").GetBoolean()); + Assert.Equal(29, record.GetProperty("verification").GetProperty("debugTests").GetProperty("passed").GetInt32()); + Assert.Equal(29, record.GetProperty("verification").GetProperty("releaseTests").GetProperty("passed").GetInt32()); + Assert.Contains("public-package-restore", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString())); + Assert.Contains("actual-godot-process-integration", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString())); + Assert.Contains("dedicated-fallback-connection", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString())); + Assert.Contains("Do not mark #21 passed", guide, StringComparison.Ordinal); + Assert.Contains("Rendezvous reserves no gameplay", guide, StringComparison.Ordinal); + Assert.Contains("not runtime proof", guide, StringComparison.Ordinal); + Assert.DoesNotMatch(ReusableCredential(), guide); } [GeneratedRegex(@"rv1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+", RegexOptions.CultureInvariant)]