Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bad659c12 | |||
| f368fec6eb | |||
| 9e863ebf64 | |||
| ebb5eb617c |
@@ -119,6 +119,10 @@ versioning, and secure rollout seams are in the
|
|||||||
The always-on three-party scenarios, optional Linux namespace topology, and
|
The always-on three-party scenarios, optional Linux namespace topology, and
|
||||||
simulation limits are documented in the
|
simulation limits are documented in the
|
||||||
[deterministic topology harness](docs/integration/topology-harness.md).
|
[deterministic topology harness](docs/integration/topology-harness.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
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"AllowedHosts": "localhost;127.0.0.1",
|
"AllowedHosts": "localhost;127.0.0.1;rendezvous",
|
||||||
"Rendezvous": {
|
"Rendezvous": {
|
||||||
"Deployment": {
|
"Deployment": {
|
||||||
"PublicHttpBaseUrl": "https://localhost/",
|
"PublicHttpBaseUrl": "https://localhost/",
|
||||||
@@ -32,6 +32,16 @@
|
|||||||
"NotBefore": "2026-01-01T00:00:00Z",
|
"NotBefore": "2026-01-01T00:00:00Z",
|
||||||
"SignUntil": "2100-01-01T00:00:00Z",
|
"SignUntil": "2100-01-01T00:00:00Z",
|
||||||
"VerifyUntil": "2100-01-02T00:00:00Z"
|
"VerifyUntil": "2100-01-02T00:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"KeyId": "local-smoke-unscouted-1",
|
||||||
|
"SecretReference": "file:/run/secrets/rendezvous-signing-key",
|
||||||
|
"CredentialKinds": ["DedicatedPublisher"],
|
||||||
|
"GameId": "unscouted",
|
||||||
|
"EnvironmentId": "smoke",
|
||||||
|
"NotBefore": "2026-01-01T00:00:00Z",
|
||||||
|
"SignUntil": "2100-01-01T00:00:00Z",
|
||||||
|
"VerifyUntil": "2100-01-02T00:00:00Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Games": [
|
"Games": [
|
||||||
@@ -39,18 +49,41 @@
|
|||||||
"GameId": "space-game",
|
"GameId": "space-game",
|
||||||
"EnvironmentId": "smoke",
|
"EnvironmentId": "smoke",
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
|
"ProtocolVersions": [1, 2],
|
||||||
|
"Regions": ["local"],
|
||||||
|
"VisibilityModes": ["Public"],
|
||||||
|
"PublisherTrustModes": ["ManagedDedicated"],
|
||||||
|
"MetadataValueMaxBytes": {
|
||||||
|
"mode": 32
|
||||||
|
},
|
||||||
|
"RequiredMetadataKeys": [],
|
||||||
|
"MetadataMaxBytes": 512,
|
||||||
|
"MetadataMaxKeys": 1,
|
||||||
|
"MaxListingsPerPrincipal": 10,
|
||||||
|
"MaxAnonymousListingsPerAddress": 0,
|
||||||
|
"MaxActiveJoinAttempts": 100,
|
||||||
|
"FallbackPolicy": "DedicatedEndpointAllowed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GameId": "unscouted",
|
||||||
|
"EnvironmentId": "smoke",
|
||||||
|
"Enabled": true,
|
||||||
"ProtocolVersions": [1],
|
"ProtocolVersions": [1],
|
||||||
"Regions": ["local"],
|
"Regions": ["local"],
|
||||||
"VisibilityModes": ["Public"],
|
"VisibilityModes": ["Public"],
|
||||||
"PublisherTrustModes": ["ManagedDedicated"],
|
"PublisherTrustModes": ["ManagedDedicated"],
|
||||||
"MetadataValueMaxBytes": {},
|
"MetadataValueMaxBytes": {
|
||||||
"RequiredMetadataKeys": [],
|
"mode": 32,
|
||||||
|
"world": 64,
|
||||||
|
"mods": 64
|
||||||
|
},
|
||||||
|
"RequiredMetadataKeys": ["mode", "world", "mods"],
|
||||||
"MetadataMaxBytes": 512,
|
"MetadataMaxBytes": 512,
|
||||||
"MetadataMaxKeys": 0,
|
"MetadataMaxKeys": 3,
|
||||||
"MaxListingsPerPrincipal": 10,
|
"MaxListingsPerPrincipal": 10,
|
||||||
"MaxAnonymousListingsPerAddress": 0,
|
"MaxAnonymousListingsPerAddress": 0,
|
||||||
"MaxActiveJoinAttempts": 100,
|
"MaxActiveJoinAttempts": 100,
|
||||||
"FallbackPolicy": "Disabled"
|
"FallbackPolicy": "DedicatedEndpointAllowed"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,9 +42,11 @@ test "$RENDEZVOUS_UID" -ne 0
|
|||||||
docker compose -f deploy/compose/compose.yaml up --build --detach
|
docker compose -f deploy/compose/compose.yaml up --build --detach
|
||||||
```
|
```
|
||||||
|
|
||||||
`deploy/compose/appsettings.Production.json` is an isolated loopback smoke
|
`deploy/compose/appsettings.Production.json` is a local/private-bridge smoke
|
||||||
profile, not an Internet template: it deliberately opts into private advertised
|
profile, not an Internet template: TCP is published only on host loopback, the
|
||||||
endpoints and has no TLS proxy. Its random key is ignored by Git and must be
|
explicit `rendezvous` host name serves isolated clients on the Compose network,
|
||||||
|
and the profile deliberately opts into private advertised endpoints without a
|
||||||
|
TLS proxy. Its random key is ignored by Git and must be
|
||||||
deleted after use. Its deliberately long key window only keeps this disposable
|
deleted after use. Its deliberately long key window only keeps this disposable
|
||||||
local fixture usable; production keys require short, reviewed rotation windows.
|
local fixture usable; production keys require short, reviewed rotation windows.
|
||||||
Production configuration must use its real public names and must leave
|
Production configuration must use its real public names and must leave
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"recordedAt": "2026-07-16",
|
||||||
|
"issue": 21,
|
||||||
|
"consumerIssue": "Kyuubi/SpaceGame#3",
|
||||||
|
"result": "checkpoint-pass-with-external-gates",
|
||||||
|
"rendezvousBaseCommit": "ebb5eb617c0bbb170418afab396b68584b7f992e",
|
||||||
|
"consumerCommit": "f3f5bc29810c362656cd7143bec1ddc2cfaf9f22",
|
||||||
|
"consumerIssueComment": 11469,
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"localRun": {
|
||||||
|
"processes": ["Rendezvous", "Godot SpaceGame host", "two sequential Godot SpaceGame clients"],
|
||||||
|
"typedOutcome": "Connected",
|
||||||
|
"gameAdmission": "Accepted",
|
||||||
|
"directGameplay": true,
|
||||||
|
"authenticatedSessions": 2,
|
||||||
|
"directInputs": 2,
|
||||||
|
"directSnapshots": 2,
|
||||||
|
"lifecyclePackets": 4,
|
||||||
|
"gameplayTransport": "caller-owned-litenetlib",
|
||||||
|
"rendezvousGameplayPayloadPath": "none",
|
||||||
|
"hostLeaseRenewed": true,
|
||||||
|
"reconnected": true,
|
||||||
|
"deregistered": true
|
||||||
|
},
|
||||||
|
"linuxRun": {
|
||||||
|
"runtime": "Godot 4.7 .NET Linux x86_64",
|
||||||
|
"freshExport": true,
|
||||||
|
"sourceDirty": false,
|
||||||
|
"optimized": true,
|
||||||
|
"dedicatedHostNamespace": "docker",
|
||||||
|
"remoteClientNamespace": "docker",
|
||||||
|
"topology": "private-bridge",
|
||||||
|
"directGameplay": true,
|
||||||
|
"fallback": "PunchTimedOut to explicit Docker-gateway endpoint, then Accepted game admission and direct gameplay",
|
||||||
|
"artifactHashes": "SpaceGame issue #3 comment 11469"
|
||||||
|
},
|
||||||
|
"negativePaths": {
|
||||||
|
"incompatibleProtocol": "proven",
|
||||||
|
"staleHostPresence": "proven",
|
||||||
|
"punchTimeout": "proven",
|
||||||
|
"invalidAdmission": "integration-proven",
|
||||||
|
"capacity": "regression-tested",
|
||||||
|
"fallbackConnection": "godot-and-isolated-linux-proven",
|
||||||
|
"reconnect": "proven"
|
||||||
|
},
|
||||||
|
"verification": {
|
||||||
|
"debugBuild": "passed",
|
||||||
|
"releaseBuild": "passed",
|
||||||
|
"debugTests": { "passed": 31, "failed": 0 },
|
||||||
|
"releaseTests": { "passed": 31, "failed": 0 },
|
||||||
|
"exportRelease": "optimized-without-debug-symbols",
|
||||||
|
"format": "passed",
|
||||||
|
"shellcheck": "passed",
|
||||||
|
"godotReconnectHarness": "passed",
|
||||||
|
"godotFallbackHarness": "passed",
|
||||||
|
"linuxContainerHarness": "passed-clean-source",
|
||||||
|
"failureMatrix": "passed",
|
||||||
|
"adversarialReview": "passed-after-fixes"
|
||||||
|
},
|
||||||
|
"openGates": [
|
||||||
|
"public-package-restore",
|
||||||
|
"representative-external-nat"
|
||||||
|
],
|
||||||
|
"relatedSpaceGameGates": [
|
||||||
|
"production-enet-replacement",
|
||||||
|
"capacity-profiles-64-and-128",
|
||||||
|
"sigterm-drain-save"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -70,6 +70,7 @@ thread and do not also call `NetManager.PollEvents()` during that period.
|
|||||||
```csharp
|
```csharp
|
||||||
RendezvousNetListener networkEvents = new();
|
RendezvousNetListener networkEvents = new();
|
||||||
NetManager gameplayNetwork = networkEvents.CreateManager();
|
NetManager gameplayNetwork = networkEvents.CreateManager();
|
||||||
|
gameplayNetwork.ChannelsCount = 3; // set the game's required count before Start
|
||||||
if (!gameplayNetwork.Start(gameplayPort))
|
if (!gameplayNetwork.Start(gameplayPort))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Gameplay UDP socket could not start.");
|
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
|
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
|
Register normal game callbacks on `networkEvents.GameplayEvents`. Rendezvous
|
||||||
reserves only its authenticated direct requests and forwards other callbacks.
|
reserves only its authenticated direct requests and forwards other callbacks.
|
||||||
The same socket sends host presence, punches through the mediator, establishes
|
The same socket sends host presence, punches through the mediator, establishes
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# SpaceGame consumer pilot
|
||||||
|
|
||||||
|
Tracking: Rendezvous #21 and SpaceGame #3.
|
||||||
|
|
||||||
|
The current SpaceGame checkpoint proves that the v1 client boundary establishes
|
||||||
|
authenticated direct LiteNetLib traffic without taking ownership of the game's
|
||||||
|
protocol, admission, player identity, entity identity, capacity, lifecycle, or
|
||||||
|
gameplay payloads. Real Godot processes, reconnect, an explicit dedicated
|
||||||
|
fallback, and a fresh Linux export now pass. The public package restore and a
|
||||||
|
representative external NAT/CGNAT canary remain required before #21 can close.
|
||||||
|
|
||||||
|
## Pinned checkpoint
|
||||||
|
|
||||||
|
| Input | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| Rendezvous compatibility source | `ebb5eb617c0bbb170418afab396b68584b7f992e` plus the current #21 configuration/evidence changes |
|
||||||
|
| Rendezvous package source | `07004cd75fe172aa5dfdb3edda22fc280a4c4477` |
|
||||||
|
| SpaceGame source | `f3f5bc29810c362656cd7143bec1ddc2cfaf9f22` |
|
||||||
|
| 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 real-process harnesses observed:
|
||||||
|
|
||||||
|
- host publication and lease maintenance;
|
||||||
|
- browser compatibility filtering and join authorization;
|
||||||
|
- typed traversal outcome `Connected`;
|
||||||
|
- successful audience-bound game admission;
|
||||||
|
- reliable ordered frame-definition and spawn lifecycle records, reliable
|
||||||
|
ordered input, and sequenced state snapshots on the caller-owned gameplay
|
||||||
|
socket;
|
||||||
|
- disconnect and a new authenticated session for the same durable player while
|
||||||
|
LiteNetLib peers and canonical entity IDs change;
|
||||||
|
- immediate host lease renewal and successful host deregistration;
|
||||||
|
- a fresh optimized Linux export running the host and client in distinct
|
||||||
|
hardened container namespaces; and
|
||||||
|
- a forced punch timeout that connects the isolated client to an explicitly
|
||||||
|
advertised, non-loopback Docker-gateway fallback and repeats game admission.
|
||||||
|
|
||||||
|
Rendezvous exposes no gameplay relay API; all lifecycle, command, and snapshot
|
||||||
|
bytes are sent by SpaceGame through its caller-owned `NetManager`. Both Debug
|
||||||
|
and Release builds passed. Both Debug and Release test runs passed 31 tests with
|
||||||
|
zero failures. ExportRelease is optimized with debug symbols removed. The
|
||||||
|
focused formatter, shell checker, fresh-export provenance gate, 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 denies gameplay in the process matrix; wrong audience, expiry, and replay are regression-tested | Proven |
|
||||||
|
| Capacity and duplicate player | game-owned roster rejects both and publishes current capacity | Regression-tested |
|
||||||
|
| Configured fallback | typed `PunchTimedOut`, explicit non-loopback endpoint, same game admission, direct gameplay | Proven locally and across Linux namespaces |
|
||||||
|
| Disconnect | host observes zero active players and final admitted count zero | Proven |
|
||||||
|
| Reconnect | same durable player enters a second authenticated session with new peer/entity IDs | Proven |
|
||||||
|
|
||||||
|
## 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;
|
||||||
|
- the Compose smoke tenant explicitly allows its private-network service name
|
||||||
|
and enables only the dedicated-endpoint fallback policy;
|
||||||
|
- 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 both remaining external gates have direct evidence:
|
||||||
|
|
||||||
|
1. restore the exact immutable `1.0.0` packages from the public Gitea feed; and
|
||||||
|
2. run representative external NAT/CGNAT canaries and record the network
|
||||||
|
topology and typed outcome.
|
||||||
|
|
||||||
|
SpaceGame #3 remains open independently for the production ENet replacement,
|
||||||
|
64/128-player profiles, and SIGTERM/drain/save evidence. The consumer pilot
|
||||||
|
does not claim those broader game-migration gates.
|
||||||
@@ -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.
|
||||||
@@ -3,9 +3,25 @@ set -euo pipefail
|
|||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
LOCAL_KEY="${RENDEZVOUS_SMOKE_LOCAL_KEY:-$ROOT/deploy/compose/secrets/signing-key}"
|
LOCAL_KEY="${RENDEZVOUS_SMOKE_LOCAL_KEY:-$ROOT/deploy/compose/secrets/signing-key}"
|
||||||
|
GAME_ID="${RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID:-space-game}"
|
||||||
|
|
||||||
|
case "$GAME_ID" in
|
||||||
|
space-game)
|
||||||
|
KEY_ID="local-smoke-1"
|
||||||
|
SUBJECT="local-smoke-host"
|
||||||
|
;;
|
||||||
|
unscouted)
|
||||||
|
KEY_ID="local-smoke-unscouted-1"
|
||||||
|
SUBJECT="local-smoke-unscouted-host"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID must be space-game or unscouted.\n' >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
if (( $# != 0 )); then
|
if (( $# != 0 )); then
|
||||||
printf 'This helper accepts no arguments and mints only the fixed local Compose smoke scope.\n' >&2
|
printf 'This helper accepts no arguments; select only a provisioned local game through RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID.\n' >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -17,7 +33,7 @@ command -v python3 >/dev/null || {
|
|||||||
# This is deliberately a local-fixture tool, not a general credential issuer.
|
# This is deliberately a local-fixture tool, not a general credential issuer.
|
||||||
# Python reads the raw key from the protected file; key material never appears in
|
# Python reads the raw key from the protected file; key material never appears in
|
||||||
# a child process argument, environment value, temporary file, or command output.
|
# a child process argument, environment value, temporary file, or command output.
|
||||||
python3 - "$LOCAL_KEY" <<'PY'
|
python3 - "$LOCAL_KEY" "$GAME_ID" "$KEY_ID" "$SUBJECT" <<'PY'
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
@@ -29,6 +45,9 @@ import sys
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
key_path = sys.argv[1]
|
key_path = sys.argv[1]
|
||||||
|
game_id = sys.argv[2]
|
||||||
|
key_id = sys.argv[3]
|
||||||
|
subject = sys.argv[4]
|
||||||
try:
|
try:
|
||||||
metadata = os.lstat(key_path)
|
metadata = os.lstat(key_path)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -42,8 +61,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}")
|
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:
|
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}")
|
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:
|
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 not group/world writable: {key_path}")
|
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:
|
with open(key_path, "rb") as key_file:
|
||||||
key = key_file.read(33)
|
key = key_file.read(33)
|
||||||
@@ -55,9 +74,9 @@ payload = {
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"issuer": "final-factory-rendezvous-smoke",
|
"issuer": "final-factory-rendezvous-smoke",
|
||||||
"audience": "rendezvous-service",
|
"audience": "rendezvous-service",
|
||||||
"subject": "local-smoke-host",
|
"subject": subject,
|
||||||
"kind": "dedicatedPublisher",
|
"kind": "dedicatedPublisher",
|
||||||
"gameId": "space-game",
|
"gameId": game_id,
|
||||||
"environmentId": "smoke",
|
"environmentId": "smoke",
|
||||||
"regions": ["local"],
|
"regions": ["local"],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
@@ -71,7 +90,7 @@ def base64url(value: bytes) -> str:
|
|||||||
return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
|
return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
|
||||||
|
|
||||||
encoded = base64url(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
|
encoded = base64url(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
|
||||||
signed = f"rv1.local-smoke-1.{encoded}"
|
signed = f"rv1.{key_id}.{encoded}"
|
||||||
signature = base64url(hmac.new(key, signed.encode("ascii"), hashlib.sha256).digest())
|
signature = base64url(hmac.new(key, signed.encode("ascii"), hashlib.sha256).digest())
|
||||||
print(f"{signed}.{signature}")
|
print(f"{signed}.{signature}")
|
||||||
PY
|
PY
|
||||||
|
|||||||
@@ -67,12 +67,17 @@ factory does not open a socket, and synchronized events must remain enabled:
|
|||||||
```csharp
|
```csharp
|
||||||
RendezvousNetListener networkEvents = new();
|
RendezvousNetListener networkEvents = new();
|
||||||
NetManager gameplayNetManager = networkEvents.CreateManager();
|
NetManager gameplayNetManager = networkEvents.CreateManager();
|
||||||
|
gameplayNetManager.ChannelsCount = 3; // example: configure the game protocol first
|
||||||
if (!gameplayNetManager.Start(0))
|
if (!gameplayNetManager.Start(0))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("The gameplay UDP socket could not start.");
|
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
|
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
|
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
|
LiteNetLib and dispatches its synchronized callbacks. Call it once per game
|
||||||
|
|||||||
@@ -238,6 +238,10 @@ public sealed class ProductionProcessTests
|
|||||||
"--Rendezvous:Provisioning:SigningKeys:0:NotBefore", now.AddHours(-1).ToString("O"),
|
"--Rendezvous:Provisioning:SigningKeys:0:NotBefore", now.AddHours(-1).ToString("O"),
|
||||||
"--Rendezvous:Provisioning:SigningKeys:0:SignUntil", now.AddHours(1).ToString("O"),
|
"--Rendezvous:Provisioning:SigningKeys:0:SignUntil", now.AddHours(1).ToString("O"),
|
||||||
"--Rendezvous:Provisioning:SigningKeys:0:VerifyUntil", now.AddHours(2).ToString("O"),
|
"--Rendezvous:Provisioning:SigningKeys:0:VerifyUntil", now.AddHours(2).ToString("O"),
|
||||||
|
"--Rendezvous:Provisioning:SigningKeys:1:SecretReference", $"file:{secretPath}",
|
||||||
|
"--Rendezvous:Provisioning:SigningKeys:1:NotBefore", now.AddHours(-1).ToString("O"),
|
||||||
|
"--Rendezvous:Provisioning:SigningKeys:1:SignUntil", now.AddHours(1).ToString("O"),
|
||||||
|
"--Rendezvous:Provisioning:SigningKeys:1:VerifyUntil", now.AddHours(2).ToString("O"),
|
||||||
"--Rendezvous:Udp:Port", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
"--Rendezvous:Udp:Port", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||||
"--Rendezvous:Deployment:PublicUdpPort", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
"--Rendezvous:Deployment:PublicUdpPort", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,26 +40,57 @@ public sealed partial class DocumentationContractTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LocalCredentialHelperIsFixedScopeAndSmokeDelegatesToIt()
|
public void LocalCredentialHelperWhitelistsProvisionedGameScopesAndSmokeDelegatesToIt()
|
||||||
{
|
{
|
||||||
string root = FindRepositoryRoot();
|
string root = FindRepositoryRoot();
|
||||||
string helper = File.ReadAllText(Path.Combine(root, "scripts", "mint-local-publisher-credential.sh"));
|
string helper = File.ReadAllText(Path.Combine(root, "scripts", "mint-local-publisher-credential.sh"));
|
||||||
string smoke = File.ReadAllText(Path.Combine(root, "scripts", "smoke-deployment.sh"));
|
string smoke = File.ReadAllText(Path.Combine(root, "scripts", "smoke-deployment.sh"));
|
||||||
|
|
||||||
Assert.Contains("if (( $# != 0 ));", helper, StringComparison.Ordinal);
|
Assert.Contains("if (( $# != 0 ));", helper, StringComparison.Ordinal);
|
||||||
Assert.Contains("\"gameId\": \"space-game\"", helper, StringComparison.Ordinal);
|
Assert.Contains("space-game)", helper, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("unscouted)", helper, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("KEY_ID=\"local-smoke-1\"", helper, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("KEY_ID=\"local-smoke-unscouted-1\"", helper, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID must be space-game or unscouted", helper, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("\"gameId\": game_id", helper, StringComparison.Ordinal);
|
||||||
Assert.Contains("\"environmentId\": \"smoke\"", helper, StringComparison.Ordinal);
|
Assert.Contains("\"environmentId\": \"smoke\"", helper, StringComparison.Ordinal);
|
||||||
Assert.Contains("\"regions\": [\"local\"]", helper, StringComparison.Ordinal);
|
Assert.Contains("\"regions\": [\"local\"]", helper, StringComparison.Ordinal);
|
||||||
Assert.Contains("now + 600", helper, StringComparison.Ordinal);
|
Assert.Contains("now + 600", helper, StringComparison.Ordinal);
|
||||||
Assert.Contains("stat.S_ISLNK", helper, StringComparison.Ordinal);
|
Assert.Contains("stat.S_ISLNK", helper, StringComparison.Ordinal);
|
||||||
Assert.Contains("parent.st_mode & 0o077", 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("metadata.st_nlink != 1", helper, StringComparison.Ordinal);
|
||||||
Assert.Contains("mint-local-publisher-credential.sh", smoke, StringComparison.Ordinal);
|
Assert.Contains("mint-local-publisher-credential.sh", smoke, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("hexkey:", smoke, StringComparison.Ordinal);
|
Assert.DoesNotContain("hexkey:", smoke, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("openssl dgst", smoke, StringComparison.Ordinal);
|
Assert.DoesNotContain("openssl dgst", smoke, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UnscoutedComposeTenantIsGameScopedAndMetadataBounded()
|
||||||
|
{
|
||||||
|
string root = FindRepositoryRoot();
|
||||||
|
using JsonDocument settings = JsonDocument.Parse(File.ReadAllText(
|
||||||
|
Path.Combine(root, "deploy", "compose", "appsettings.Production.json")));
|
||||||
|
JsonElement provisioning = settings.RootElement.GetProperty("Rendezvous").GetProperty("Provisioning");
|
||||||
|
JsonElement game = provisioning.GetProperty("Games").EnumerateArray().Single(
|
||||||
|
static item => item.GetProperty("GameId").GetString() == "unscouted");
|
||||||
|
JsonElement key = provisioning.GetProperty("SigningKeys").EnumerateArray().Single(
|
||||||
|
static item => item.GetProperty("KeyId").GetString() == "local-smoke-unscouted-1");
|
||||||
|
|
||||||
|
Assert.Equal("smoke", game.GetProperty("EnvironmentId").GetString());
|
||||||
|
Assert.Equal([1], game.GetProperty("ProtocolVersions").EnumerateArray().Select(static value => value.GetInt32()));
|
||||||
|
Assert.Equal(["mode", "mods", "world"], game.GetProperty("MetadataValueMaxBytes")
|
||||||
|
.EnumerateObject().Select(static property => property.Name).Order(StringComparer.Ordinal));
|
||||||
|
Assert.Equal(["mode", "mods", "world"], game.GetProperty("RequiredMetadataKeys")
|
||||||
|
.EnumerateArray().Select(static value => value.GetString()).Order(StringComparer.Ordinal));
|
||||||
|
Assert.Equal(3, game.GetProperty("MetadataMaxKeys").GetInt32());
|
||||||
|
Assert.Equal("DedicatedEndpointAllowed", game.GetProperty("FallbackPolicy").GetString());
|
||||||
|
Assert.Equal("unscouted", key.GetProperty("GameId").GetString());
|
||||||
|
Assert.Equal("smoke", key.GetProperty("EnvironmentId").GetString());
|
||||||
|
Assert.Equal(["DedicatedPublisher"], key.GetProperty("CredentialKinds")
|
||||||
|
.EnumerateArray().Select(static value => value.GetString()));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EveryIncidentRunbookHasDetectContainRecoverAndVerifyGates()
|
public void EveryIncidentRunbookHasDetectContainRecoverAndVerifyGates()
|
||||||
{
|
{
|
||||||
@@ -184,6 +215,79 @@ public sealed partial class DocumentationContractTests
|
|||||||
Assert.Equal(httpVersion, udpVersion);
|
Assert.Equal(httpVersion, udpVersion);
|
||||||
Assert.Equal(httpVersion, ticketVersion);
|
Assert.Equal(httpVersion, ticketVersion);
|
||||||
Assert.Contains($"contract version `{httpVersion}`", guide, StringComparison.Ordinal);
|
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"));
|
||||||
|
string deploymentGuide = File.ReadAllText(Path.Combine(root, "docs", "deployment", "linux.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-external-gates", record.GetProperty("result").GetString());
|
||||||
|
Assert.Equal("none", record.GetProperty("localRun").GetProperty("rendezvousGameplayPayloadPath").GetString());
|
||||||
|
Assert.Equal("caller-owned-litenetlib", record.GetProperty("localRun").GetProperty("gameplayTransport").GetString());
|
||||||
|
Assert.True(record.GetProperty("localRun").GetProperty("directGameplay").GetBoolean());
|
||||||
|
Assert.True(record.GetProperty("localRun").GetProperty("reconnected").GetBoolean());
|
||||||
|
Assert.True(record.GetProperty("linuxRun").GetProperty("freshExport").GetBoolean());
|
||||||
|
Assert.False(record.GetProperty("linuxRun").GetProperty("sourceDirty").GetBoolean());
|
||||||
|
Assert.Equal(31, record.GetProperty("verification").GetProperty("debugTests").GetProperty("passed").GetInt32());
|
||||||
|
Assert.Equal(31, record.GetProperty("verification").GetProperty("releaseTests").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.DoesNotContain("actual-godot-process-integration", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString()));
|
||||||
|
Assert.DoesNotContain("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("private-network service name", guide, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("local/private-bridge smoke", deploymentGuide, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("explicit `rendezvous` host name", deploymentGuide, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
using JsonDocument composeSettings = JsonDocument.Parse(File.ReadAllText(
|
||||||
|
Path.Combine(root, "deploy", "compose", "appsettings.Production.json")));
|
||||||
|
JsonElement compose = composeSettings.RootElement;
|
||||||
|
Assert.Contains("rendezvous", compose.GetProperty("AllowedHosts").GetString()!.Split(';'));
|
||||||
|
JsonElement game = compose.GetProperty("Rendezvous").GetProperty("Provisioning").GetProperty("Games")[0];
|
||||||
|
Assert.Contains(2, game.GetProperty("ProtocolVersions").EnumerateArray().Select(static version => version.GetInt32()));
|
||||||
|
Assert.Equal("DedicatedEndpointAllowed", game.GetProperty("FallbackPolicy").GetString());
|
||||||
|
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)]
|
[GeneratedRegex(@"rv1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+", RegexOptions.CultureInvariant)]
|
||||||
|
|||||||
Reference in New Issue
Block a user