diff --git a/README.md b/README.md index ea9e435..cd4e7f5 100644 --- a/README.md +++ b/README.md @@ -119,9 +119,9 @@ 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). +The current consumer evidence and remaining external gates are tracked in the +[SpaceGame consumer pilot](docs/integration/spacegame-pilot.md) and independent +[Unscouted consumer pilot](docs/integration/unscouted-pilot.md). ## Development diff --git a/docs/evidence/consumers/unscouted.json b/docs/evidence/consumers/unscouted.json new file mode 100644 index 0000000..945d238 --- /dev/null +++ b/docs/evidence/consumers/unscouted.json @@ -0,0 +1,82 @@ +{ + "schemaVersion": "1.0", + "recordedAt": "2026-07-16", + "issue": 22, + "consumerIssue": "HeiKyu/Unscouted#459", + "result": "checkpoint-pass-with-external-gates", + "rendezvousConfigurationCommit": "f368fec6eb4344a6042974f58f888cf0f1ac8e8e", + "consumerImplementationCommit": "1e5886aa7f1e44689b4c75e32693eb7b19fd72d7", + "consumerEvidenceCommit": "f0574a7de82aadff6495ca5657dfc19cf7c2f67c", + "consumerIssueComment": 11499, + "packages": { + "FinalFactory.Rendezvous.Client": { + "version": "1.0.0", + "source": "local-candidate", + "sourceCommit": "07004cd75fe172aa5dfdb3edda22fc280a4c4477", + "sha256": "fb156cf48b49f75c244dd25ea7cc4aa9fc6fab0a878393bb7efd5d9b131d0395" + }, + "FinalFactory.Rendezvous.Contracts": { + "version": "1.0.0", + "source": "local-candidate", + "sourceCommit": "07004cd75fe172aa5dfdb3edda22fc280a4c4477", + "sha256": "a82ba986d3905d599096d1d8ce8f32cd4feb104abfca37b0f65e0d2ef3df9a6f" + }, + "LiteNetLib": { + "version": "2.1.4" + } + }, + "configuration": { + "gameId": "unscouted", + "environmentId": "smoke", + "regionId": "local", + "protocolVersion": 1, + "publisherTrust": "ManagedDedicated", + "fallbackPolicy": "DedicatedEndpointAllowed", + "metadataKeys": ["mode", "world", "mods"], + "metadataMaxKeys": 3, + "metadataMaxBytes": 512 + }, + "godotRun": { + "runtime": "Godot 4.7 .NET Linux x86_64", + "processes": [ + "Rendezvous hardened Compose service", + "Godot Unscouted host", + "Godot incompatible-protocol client", + "Godot direct client", + "Godot fallback client" + ], + "gameplayTransport": "unscouted-litenetlib", + "rendezvousGameplayPayloadPath": "none", + "directGameplay": true, + "fallbackGameplay": true, + "authenticatedSessions": 2, + "gameplayExchanges": 2, + "hostLeaseRenewed": true, + "deregistered": true, + "playerIdentityOwner": "unscouted", + "canonicalGameStateOwner": "unscouted" + }, + "negativePaths": { + "incompatibleProtocol": "proven-no-compatible-listing", + "wrongGame": "proven-exact-NotFound", + "wrongEnvironment": "proven-exact-NotFound", + "punchTimeout": "proven-typed-failure-then-game-owned-fallback", + "unexpectedMetadata": "consumer-regression-tested" + }, + "verification": { + "rendezvousDebugTests": { "passed": 299, "failed": 0 }, + "rendezvousReleaseTests": { "passed": 299, "failed": 0 }, + "consumerDebugTests": { "passed": 3310, "skipped": 15, "failed": 0 }, + "consumerReleaseTests": { "passed": 3310, "skipped": 15, "failed": 0 }, + "consumerGdUnitTests": { "passed": 360, "skipped": 0, "failed": 0 }, + "consumerExport": "not-applicable-no-export-presets", + "format": "passed", + "shellcheck": "passed", + "godotPilot": "passed", + "adversarialReview": "passed-after-fixes" + }, + "openGates": [ + "public-package-restore", + "representative-external-nat" + ] +} diff --git a/docs/integration/unscouted-pilot.md b/docs/integration/unscouted-pilot.md new file mode 100644 index 0000000..48120d6 --- /dev/null +++ b/docs/integration/unscouted-pilot.md @@ -0,0 +1,99 @@ +# Unscouted consumer pilot + +Tracking: Rendezvous #22 and Unscouted #459. + +The current checkpoint independently proves that the v1 contracts are not +shaped only around SpaceGame. A real Godot Unscouted host and clients consume +the same Client and Contracts package surface, use one caller-owned LiteNetLib +socket for NAT callbacks and gameplay, perform Unscouted's own keypair +authentication and host admission, exchange gameplay, and exercise a +game-owned fallback. The public package restore and representative external +NAT/CGNAT canary remain required before #22 can close. + +## Pinned checkpoint + +| Input | Value | +| --- | --- | +| Rendezvous configuration source | `f368fec6eb4344a6042974f58f888cf0f1ac8e8e` | +| Rendezvous package source | `07004cd75fe172aa5dfdb3edda22fc280a4c4477` | +| Unscouted implementation | `1e5886aa7f1e44689b4c75e32693eb7b19fd72d7` | +| Unscouted evidence | `f0574a7de82aadff6495ca5657dfc19cf7c2f67c` | +| Client package | `FinalFactory.Rendezvous.Client` `1.0.0` | +| Contracts package | `FinalFactory.Rendezvous.Contracts` `1.0.0` | +| LiteNetLib | `2.1.4` | +| Godot | `4.7.stable.mono.arch_linux.5b4e0cb0f` | +| Game / environment / region | `unscouted` / `smoke` / `local` | +| Rendezvous and gameplay protocol | `1` | + +The exact package hashes are recorded in +[`unscouted.json`](../evidence/consumers/unscouted.json). A clean restore into an +empty package directory using only the consumer's checked-in `NuGet.config` +returns `NU1101` for both packages. The verified local run used those exact +candidate package files from the existing cache. This proves compatibility, +not immutable registry publication. + +## Game-neutral service boundary + +Rendezvous #22 adds provisioning data, not an Unscouted branch in the server or +SDK. The local production-shaped tenant permits protocol `1`, region `local`, +public managed-dedicated listings, and the three bounded presentation keys +`mode`, `world`, and `mods`. The short-lived credential helper accepts only the +explicitly provisioned `space-game` and `unscouted` scopes and selects a +distinct game-scoped signing-key ID and subject. + +The consumer rejects any metadata key outside its three-key presentation +schema and neutralizes control/BBCode characters before display. Rendezvous +never receives Unscouted player keys or resolved identities, colony authority, +simulation or persistence state, fog/interest state, or gameplay packets. + +## Proven real Godot path + +The normal `NetLaunch` argument path recognizes `--rendezvous-pilot` and opens a +dedicated scene. That scene uses Unscouted's real `LiteNetLibTransport`, +`GameServer`, `GameClient`, `ServerAuthenticator`, and `ClientAuthenticator`. +It is not a copied SDK adapter. + +One bounded run against the hardened Compose service started a host plus: + +- a protocol-`999` client that found no compatible listing; +- a direct client that received an authorized introduction, completed + same-socket traversal, passed Unscouted keypair admission, and exchanged an + Unscouted gameplay ping/pong; and +- a client pointed at a non-listening mediator that received a typed traversal + failure, applied the fallback decision in Unscouted code, repeated admission, + and exchanged the same gameplay ping/pong through the ordinary game + transport. + +The direct client also proved that both a `space-game` join request and a +`production` environment join request return exact `NotFound` results for the +Unscouted listing. The host renewed its lease, admitted two independently +authenticated sessions, completed two gameplay exchanges, and deregistered the +listing on shutdown. + +## Verification + +- Rendezvous Debug and Release: 299 tests passed in each configuration, zero + failures. +- Unscouted Debug and Release: non-incremental builds passed; 3,310 tests passed + with 15 intentional skips in each configuration. +- Unscouted gdUnit/Godot: 360 tests passed, zero skipped or failed. The harness + fix in Unscouted #461 keeps compilation headless and leaves the open editor's + build tree unchanged. +- The final Godot pilot, ShellCheck, JSON/whitespace checks, formatting gate, + and adversarial branch review passed. +- Export is not applicable because the Unscouted checkout has no + `export_presets.cfg`; both C# configurations and the actual Godot entry point + were exercised. + +## Remaining acceptance gates + +Do not mark #22 passed until both external gates have direct evidence: + +1. publish or expose the exact immutable `1.0.0` packages on the configured + Gitea feed and repeat the empty-cache consumer restore; and +2. run the same Godot host/client path across representative residential, + CGNAT, and IPv6/multi-host networks, recording the topology and typed + direct/fallback outcome. + +The loopback run proves the real process, socket, authentication, and gameplay +shape. It does not claim production Internet traversal coverage. diff --git a/tests/FinalFactory.Rendezvous.Tests/Documentation/DocumentationContractTests.cs b/tests/FinalFactory.Rendezvous.Tests/Documentation/DocumentationContractTests.cs index e5cf04c..835d449 100644 --- a/tests/FinalFactory.Rendezvous.Tests/Documentation/DocumentationContractTests.cs +++ b/tests/FinalFactory.Rendezvous.Tests/Documentation/DocumentationContractTests.cs @@ -259,6 +259,37 @@ public sealed partial class DocumentationContractTests Assert.DoesNotMatch(ReusableCredential(), guide); } + [Fact] + public void UnscoutedPilotEvidenceProvesAnIndependentGameBoundaryAndKeepsExternalGatesOpen() + { + string root = FindRepositoryRoot(); + string guide = File.ReadAllText(Path.Combine(root, "docs", "integration", "unscouted-pilot.md")); + using JsonDocument evidence = JsonDocument.Parse(File.ReadAllText( + Path.Combine(root, "docs", "evidence", "consumers", "unscouted.json"))); + JsonElement record = evidence.RootElement; + JsonElement run = record.GetProperty("godotRun"); + JsonElement negative = record.GetProperty("negativePaths"); + + Assert.Equal("checkpoint-pass-with-external-gates", record.GetProperty("result").GetString()); + Assert.Equal("unscouted", record.GetProperty("configuration").GetProperty("gameId").GetString()); + Assert.Equal(["mode", "world", "mods"], record.GetProperty("configuration").GetProperty("metadataKeys") + .EnumerateArray().Select(static value => value.GetString())); + Assert.Equal("none", run.GetProperty("rendezvousGameplayPayloadPath").GetString()); + Assert.Equal("unscouted-litenetlib", run.GetProperty("gameplayTransport").GetString()); + Assert.True(run.GetProperty("directGameplay").GetBoolean()); + Assert.True(run.GetProperty("fallbackGameplay").GetBoolean()); + Assert.Equal(2, run.GetProperty("authenticatedSessions").GetInt32()); + Assert.Equal("proven-exact-NotFound", negative.GetProperty("wrongGame").GetString()); + Assert.Equal("proven-exact-NotFound", negative.GetProperty("wrongEnvironment").GetString()); + Assert.Equal(3310, record.GetProperty("verification").GetProperty("consumerDebugTests").GetProperty("passed").GetInt32()); + Assert.Equal(360, record.GetProperty("verification").GetProperty("consumerGdUnitTests").GetProperty("passed").GetInt32()); + Assert.Contains("public-package-restore", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString())); + Assert.Contains("representative-external-nat", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString())); + Assert.Contains("Do not mark #22 passed", guide, StringComparison.Ordinal); + Assert.Contains("not an Unscouted branch", guide, StringComparison.Ordinal); + Assert.DoesNotMatch(ReusableCredential(), guide); + } + [GeneratedRegex(@"rv1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+", RegexOptions.CultureInvariant)] private static partial Regex ReusableCredential();