diff --git a/samples/eve-browser-network/EveBrowserNetworkSample.csproj b/samples/eve-browser-network/EveBrowserNetworkSample.csproj index 7de0bdbd..c83a42df 100644 --- a/samples/eve-browser-network/EveBrowserNetworkSample.csproj +++ b/samples/eve-browser-network/EveBrowserNetworkSample.csproj @@ -4,11 +4,20 @@ enable enable $(MSBuildThisFileDirectory)..\..\..\Eve + false + 1.0.33 + 0.3.2 + $(PackageFeed) - + + + + + + diff --git a/scripts/build-unity-package.ps1 b/scripts/build-unity-package.ps1 index bb12e0c6..08366cc5 100644 --- a/scripts/build-unity-package.ps1 +++ b/scripts/build-unity-package.ps1 @@ -1,6 +1,7 @@ param( [string] $Configuration = "Release", [string] $OutputDirectory = "artifacts\unity\org.gamecult.cultlib", + [string] $NuGetConfig = "", [switch] $UpdateTemplate ) @@ -30,11 +31,19 @@ if (Test-Path -LiteralPath $outputRoot) { Remove-Item -LiteralPath $outputRoot -Recurse -Force } -dotnet publish $projectPath -c $Configuration -o $publishRoot +$publishArguments = @("publish", $projectPath, "-c", $Configuration, "-o", $publishRoot) +if (-not [string]::IsNullOrWhiteSpace($NuGetConfig)) { + $publishArguments += "-p:RestoreConfigFile=$([IO.Path]::GetFullPath($NuGetConfig))" +} +& dotnet @publishArguments if ($LASTEXITCODE -ne 0) { throw "CultLib publish failed with exit code $LASTEXITCODE" } -dotnet publish $quicProjectPath -c $Configuration -o $quicPublishRoot +$quicPublishArguments = @("publish", $quicProjectPath, "-c", $Configuration, "-o", $quicPublishRoot) +if (-not [string]::IsNullOrWhiteSpace($NuGetConfig)) { + $quicPublishArguments += "-p:RestoreConfigFile=$([IO.Path]::GetFullPath($NuGetConfig))" +} +& dotnet @quicPublishArguments if ($LASTEXITCODE -ne 0) { throw "CultLib native QUIC managed publish failed with exit code $LASTEXITCODE" } diff --git a/scripts/pack-nuget.ps1 b/scripts/pack-nuget.ps1 index 14fa7340..d6f4a224 100644 --- a/scripts/pack-nuget.ps1 +++ b/scripts/pack-nuget.ps1 @@ -1,6 +1,7 @@ param( [string] $Configuration = "Release", [string] $OutputDirectory = "artifacts\nuget", + [string] $PackageVersion = "", [switch] $SkipConsumerSmoke ) @@ -18,6 +19,7 @@ $projects = @( "src\GameCult.Caching.MessagePack.Analyzers\GameCult.Caching.MessagePack.Analyzers.csproj", "src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj", "src\GameCult.Networking\GameCult.Networking.csproj", + "src\GameCult.Networking.WebSockets\GameCult.Networking.WebSockets.csproj", "src\GameCult.Mesh\GameCult.Mesh.csproj" ) @@ -27,8 +29,14 @@ if (Test-Path -LiteralPath $outputRoot) { New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null foreach ($project in $projects) { - dotnet pack (Join-Path $repoRoot $project) -c $Configuration -o $outputRoot ` - --nologo --verbosity quiet -p:NoWarn=1591%3BCS8632 + $packArguments = @( + "pack", (Join-Path $repoRoot $project), "-c", $Configuration, "-o", $outputRoot, + "--nologo", "--verbosity", "quiet", "-p:NoWarn=1591%3BCS8632" + ) + if (-not [string]::IsNullOrWhiteSpace($PackageVersion)) { + $packArguments += "-p:Version=$PackageVersion" + } + & dotnet @packArguments if ($LASTEXITCODE -ne 0) { throw "NuGet pack failed for $project with exit code $LASTEXITCODE" } @@ -48,6 +56,7 @@ $expectedPackages = @( "GameCult.Caching.MessagePack.Analyzers.$version.nupkg", "GameCult.Caching.MessagePack.$version.nupkg", "GameCult.Networking.$version.nupkg", + "GameCult.Networking.WebSockets.$version.nupkg", "GameCult.Mesh.$version.nupkg" ) foreach ($package in $expectedPackages) { diff --git a/scripts/verify-eve-browser-network.mjs b/scripts/verify-eve-browser-network.mjs index 20d819a0..8c865b1d 100644 --- a/scripts/verify-eve-browser-network.mjs +++ b/scripts/verify-eve-browser-network.mjs @@ -14,6 +14,14 @@ import { chromium } from "playwright-core"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const eveFlag = process.argv.indexOf("--eve-root"); const eveRoot = resolve(eveFlag >= 0 ? process.argv[eveFlag + 1] : join(repoRoot, "..", "Eve")); +const packageFeedFlag = process.argv.indexOf("--package-feed"); +const packageFeed = packageFeedFlag >= 0 ? resolve(process.argv[packageFeedFlag + 1]) : undefined; +const cultLibVersionFlag = process.argv.indexOf("--cultlib-package-version"); +const cultLibPackageVersion = cultLibVersionFlag >= 0 ? process.argv[cultLibVersionFlag + 1] : undefined; +const eveSurfaceVersionFlag = process.argv.indexOf("--eve-surface-package-version"); +const eveSurfacePackageVersion = eveSurfaceVersionFlag >= 0 ? process.argv[eveSurfaceVersionFlag + 1] : undefined; +const nugetConfigFlag = process.argv.indexOf("--nuget-config"); +const nugetConfig = nugetConfigFlag >= 0 ? resolve(process.argv[nugetConfigFlag + 1]) : undefined; const sampleRoot = join(repoRoot, "samples", "eve-browser-network"); const workRoot = await mkdtemp(join(tmpdir(), "cultmesh-browser-network-")); const statePath = join(workRoot, "counter.cc"); @@ -51,15 +59,30 @@ try { const bundle = await readFile(bundlePath, "utf8"); assert.doesNotMatch(bundle, /(?:from\s*["']node:|require\(["']node:)/u, "browser bundle contains a Node builtin import"); - await run("dotnet", [ + const dotnetBuildArguments = [ "build", join(sampleRoot, "EveBrowserNetworkSample.csproj"), "-m:1", "--verbosity", "quiet", "-p:NoWarn=1591%3BCS8632", - `-p:EveRoot=${eveRoot}`, - `-p:CultLibRoot=${repoRoot}`, - ]); + "-p:NuGetAudit=false", + "-p:RestoreIgnoreFailedSources=true", + ]; + if (nugetConfig) dotnetBuildArguments.push(`-p:RestoreConfigFile=${nugetConfig}`); + if (packageFeed) { + if (!cultLibPackageVersion || !eveSurfacePackageVersion) { + throw new Error("Package artifact builds require both CultLib and Eve surface package versions."); + } + dotnetBuildArguments.push( + "-p:UsePackageArtifacts=true", + `-p:PackageFeed=${packageFeed}`, + `-p:CultLibPackageVersion=${cultLibPackageVersion}`, + `-p:EveSurfacePackageVersion=${eveSurfacePackageVersion}`, + ); + } else { + dotnetBuildArguments.push(`-p:EveRoot=${eveRoot}`, `-p:CultLibRoot=${repoRoot}`); + } + await run("dotnet", dotnetBuildArguments); const providerPort = await freePort(); const decoyProviderPort = await freePort(); const replacementProviderPort = await freePort(); diff --git a/scripts/verify-eve-getting-started.ps1 b/scripts/verify-eve-getting-started.ps1 index eb653aa6..e4143645 100644 --- a/scripts/verify-eve-getting-started.ps1 +++ b/scripts/verify-eve-getting-started.ps1 @@ -25,43 +25,62 @@ if ($cultMeshClientSource -match 'ConnectAsync\s*\(\s*string\s+(endpointId|verse $cultMeshClientSource -match 'Lease(Document|Collection)Async[^\(]*\(\s*string\s+(endpointId|verseId)') { throw "CultMeshClient must not expose an ambiguous one-string session identity." } -$discoverySource = Get-Content -LiteralPath (Join-Path $cultLibRoot "src\GameCult.Mesh\CultMeshDiscoveryService.cs") -Raw -if ($discoverySource -notmatch 'AuthorityRuntimeIds\.Contains\(query\.AuthorityRuntimeId') { - throw "CultMesh discovery must prove that a selected Verse route advertises the requested authority runtime." -} - $dotnetPackageRoot = Join-Path ([IO.Path]::GetTempPath()) ` ("eve-getting-started-nuget-" + [guid]::NewGuid().ToString("N")) +$sourceRevision = (& git -C $cultLibRoot rev-parse --short=12 HEAD).Trim() +if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($sourceRevision)) { + throw "Could not determine the CultLib source revision for local package identity." +} +$verificationNonce = [guid]::NewGuid().ToString("N").Substring(0, 8) +$cultLibLocalVersion = "1.0.33-local.$sourceRevision.$verificationNonce" +$surfaceLocalVersion = "0.3.2-local.$sourceRevision.$verificationNonce" try { & (Join-Path $EveRoot "scripts\pack-dotnet-surface.ps1") ` -CultLibRoot $cultLibRoot ` - -OutputDirectory $dotnetPackageRoot + -OutputDirectory $dotnetPackageRoot ` + -CultLibPackageVersion $cultLibLocalVersion ` + -SurfacePackageVersion $surfaceLocalVersion if ($LASTEXITCODE -ne 0) { throw "The clean .NET package-consumer checkpoint failed." } + + $cultLibPackage = Get-ChildItem -LiteralPath $dotnetPackageRoot -Filter "GameCult.Mesh.*.nupkg" | + Where-Object { $_.Name -notlike "*.snupkg" } | + Select-Object -First 1 + $surfacePackage = Get-ChildItem -LiteralPath $dotnetPackageRoot -Filter "GameCult.Eve.Surface.*.nupkg" | + Where-Object { $_.Name -notlike "*.snupkg" } | + Select-Object -First 1 + if (-not $cultLibPackage -or -not $surfacePackage) { + throw "The local package feed is missing CultMesh or Eve surface artifacts." + } + $cultLibVersion = [regex]::Match($cultLibPackage.Name, '^GameCult\.Mesh\.(.+)\.nupkg$').Groups[1].Value + $surfaceVersion = [regex]::Match($surfacePackage.Name, '^GameCult\.Eve\.Surface\.(.+)\.nupkg$').Groups[1].Value + + & (Join-Path $PSScriptRoot "verify-eve-two-runtime-sample.ps1") ` + -EveRoot $EveRoot ` + -SkipDependencyInstall:$SkipDependencyInstall + if ($LASTEXITCODE -ne 0) { + throw "The clean package-consumer checkpoint failed." + } + + $nodeCommand = Get-Command node -ErrorAction Stop + $playwrightCli = Join-Path $cultLibRoot "node_modules\playwright-core\cli.js" + if (-not (Test-Path -LiteralPath $playwrightCli -PathType Leaf)) { + throw "playwright-core is missing after dependency setup: $playwrightCli" + } + + & $nodeCommand.Source (Join-Path $PSScriptRoot "verify-eve-browser-network.mjs") ` + --eve-root $EveRoot ` + --package-feed $dotnetPackageRoot ` + --cultlib-package-version $cultLibVersion ` + --eve-surface-package-version $surfaceVersion + if ($LASTEXITCODE -ne 0) { + throw "The real browser/C# network checkpoint failed." + } + + Write-Host "Eve getting-started verification passed: clean .NET and TypeScript artifacts, DOM and headless convergence, real Chromium and C# clients, Odin discovery, authority-bound routes, receipts, persistence, and route replacement." } finally { if (Test-Path -LiteralPath $dotnetPackageRoot) { Remove-Item -LiteralPath $dotnetPackageRoot -Recurse -Force } } - -& (Join-Path $PSScriptRoot "verify-eve-two-runtime-sample.ps1") ` - -EveRoot $EveRoot ` - -SkipDependencyInstall:$SkipDependencyInstall -if ($LASTEXITCODE -ne 0) { - throw "The clean package-consumer checkpoint failed." -} - -$nodeCommand = Get-Command node -ErrorAction Stop -$playwrightCli = Join-Path $cultLibRoot "node_modules\playwright-core\cli.js" -if (-not (Test-Path -LiteralPath $playwrightCli -PathType Leaf)) { - throw "playwright-core is missing after dependency setup: $playwrightCli" -} - -& $nodeCommand.Source (Join-Path $PSScriptRoot "verify-eve-browser-network.mjs") ` - --eve-root $EveRoot -if ($LASTEXITCODE -ne 0) { - throw "The real browser/C# network checkpoint failed." -} - -Write-Host "Eve getting-started verification passed: clean .NET and TypeScript artifacts, DOM and headless convergence, real Chromium and C# clients, Odin discovery, receipts, persistence, and route replacement." diff --git a/src/GameCult.Mesh/docs/getting-started/01-persist-typed-state.md b/src/GameCult.Mesh/docs/getting-started/01-persist-typed-state.md index c2667e9b..7520d67f 100644 --- a/src/GameCult.Mesh/docs/getting-started/01-persist-typed-state.md +++ b/src/GameCult.Mesh/docs/getting-started/01-persist-typed-state.md @@ -34,7 +34,7 @@ using var node = await CultMesh.CreateNodeAsync("counter.cc", new CultMeshNodeOp CacheOptions = new CultCacheOpenOptions { Registry = registry }, DatabaseOptions = new CultNetDatabaseOptions { - RuntimeId = "sample.counter-provider", + RuntimeId = "sample.counter-daemon", DocumentRegistry = documents } }); diff --git a/src/GameCult.Mesh/docs/getting-started/02-connect-by-identity.md b/src/GameCult.Mesh/docs/getting-started/02-connect-by-identity.md index 5cf90a0e..ee505acb 100644 --- a/src/GameCult.Mesh/docs/getting-started/02-connect-by-identity.md +++ b/src/GameCult.Mesh/docs/getting-started/02-connect-by-identity.md @@ -1,13 +1,17 @@ # 2. Connect A Client By Stable Identity Clients address a live session by stable Verse/provider identity. In this tutorial, -`sample.counter` is the Verse id and `sample.counter-provider` is the authority +`sample.counter` is the Verse id and `sample.counter-daemon` is the authority runtime Odin currently advertises for it. Application clients pass that typed pair, never a socket address and never one ambiguous string that might mean either identity. A product may expose only a Verse choice to its user; its selection owner then resolves the provider runtime from Odin's descriptor before opening the session. Discovery owns the current physical routes, and the session manager owns connection reuse, path rotation, and transport failure state. +Each Odin route binds that authority runtime to one endpoint, protocol set, and +generation. The client verifies the same tuple with the connected peer before +the session becomes online; runtime membership and endpoint reachability are +not independent routing evidence. ```csharp using GameCult.Mesh; @@ -15,7 +19,9 @@ using GameCult.Mesh; var odinEndpoint = Environment.GetEnvironmentVariable("ODIN_ENDPOINT") ?? throw new InvalidOperationException("Set ODIN_ENDPOINT to the configured rendezvous route."); const string verseId = "sample.counter"; -var target = new CultMeshSessionTarget(verseId, "sample.counter-provider"); +var target = new CultMeshSessionTarget(verseId, "sample.counter-daemon"); +using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30)); +var cancellationToken = timeout.Token; using var mesh = new CultMeshClient(new CultMeshClientOptions { RendezvousEndpoints = new[] { odinEndpoint } @@ -65,6 +71,11 @@ network proof): dotnet test tests/GameCult.Mesh.Tests/GameCult.Mesh.Tests.csproj --filter FullyQualifiedName~CultMeshSessionManagerTests ``` +The real checkpoint in chapter 4 additionally advertises a better-priority +endpoint for the wrong authority runtime. Both the C# and browser clients must +ignore it, verify the intended peer, survive route replacement, and reject +wrong-source operation responses. + Advanced hosts may inject lookup sources, connectors, clocks, persistence, and diagnostics through the lower-level discovery and session APIs. Application features should not need them. diff --git a/src/GameCult.Mesh/docs/getting-started/03-publish-an-eve-surface.md b/src/GameCult.Mesh/docs/getting-started/03-publish-an-eve-surface.md index bbeff475..5cb5b742 100644 --- a/src/GameCult.Mesh/docs/getting-started/03-publish-an-eve-surface.md +++ b/src/GameCult.Mesh/docs/getting-started/03-publish-an-eve-surface.md @@ -97,6 +97,10 @@ GameCult dependency is `PackageReference Include="GameCult.Eve.Surface"`. This proves the renderer-neutral provider contract does not require a sibling Eve or CultLib source tree after packaging. +The real-process C# sample is then rebuilt from that same temporary NuGet feed. +The source project references in its `.csproj` are contributor convenience only +and are disabled by the checkpoint; they are not the consumer contract. + The first layer builds and packs `cultcache-ts`, `cultnet-ts`, `cultmesh-ts`, `@gamecult/eve-contracts`, and `@gamecult/eve-browser-lowering`, installs the tarballs into an empty temporary consumer, and runs @@ -108,11 +112,11 @@ both consumers converge on the same state and canonical receipt identity, a duplicate idempotency key is not applied twice, and the state plus receipt survive reopening the `.cc` store. -The second layer runs a durable C# provider, a separate local Odin fixture, a -real Chromium Eve lowerer, and an independent C# headless observer over binary +The second layer runs a durable C# provider, a wrong-authority decoy, a separate +local Odin fixture, a real Chromium Eve lowerer, and an independent C# headless observer over binary CultNet WebSocket lanes. The open browser resolves the `sample.counter` Verse -through the canonical catalog, survives its provider restarting on a different route, -and resubscribes. The retained C# client then invokes a second typed operation +through the canonical catalog, rejects the decoy route, survives its provider +restarting on a different route, and resubscribes. The retained C# client then invokes a second typed operation through `CultMeshClient.InvokeAsync`; Chromium and C# observe the same provider-authored state and receipt chronology. The same retained C# session then completes 10,000 typed no-op operations under explicit p99 and post-GC diff --git a/src/GameCult.Mesh/docs/getting-started/04-run-the-multiruntime-game.md b/src/GameCult.Mesh/docs/getting-started/04-run-the-multiruntime-game.md index ba9888c3..ce18fdb3 100644 --- a/src/GameCult.Mesh/docs/getting-started/04-run-the-multiruntime-game.md +++ b/src/GameCult.Mesh/docs/getting-started/04-run-the-multiruntime-game.md @@ -10,6 +10,8 @@ The counter is now one small multiplayer game: typed operation boundary; - both clients remain attached when Odin moves the Verse to a replacement physical provider route. +- a better-priority route bound to another runtime remains unreachable to both + clients throughout the chronology. Run the whole chronology from the CultLib checkout: @@ -34,8 +36,10 @@ packages, installs those artifacts into an empty temporary consumer, and proves DOM/headless convergence plus `.cc` reopen. This rejects a sample that only works through repository-relative source imports. -The third checkpoint boots separate Odin, provider, Chromium, and C# headless -processes. Chromium performs the first increment. The provider then restarts on +The third checkpoint boots separate Odin, intended provider, wrong-authority +decoy provider, Chromium, and C# headless processes. Its C# application is built +from the temporary NuGet feed produced by the first checkpoint, not sibling +project references. Chromium performs the first increment. The provider then restarts on a different route, Odin advertises the replacement, and the retained clients resubscribe without application reconnect code. C# performs the second increment. Both clients must observe count 2 and both canonical receipt ids. diff --git a/src/GameCult.Mesh/docs/getting-started/README.md b/src/GameCult.Mesh/docs/getting-started/README.md index b32052a7..66ded7a4 100644 --- a/src/GameCult.Mesh/docs/getting-started/README.md +++ b/src/GameCult.Mesh/docs/getting-started/README.md @@ -4,14 +4,22 @@ This series builds one typed application state and presents it through multiple Eve runtimes. It is maintained as an API acceptance test: examples use public APIs, name the owner of each decision, and include a verification checkpoint. -Run the complete getting-started checkpoint: +Application consumers install released packages and do not need the source +repositories. This checkout also contains a maintainer checkpoint that creates +the same clean consumers from local package artifacts: ```powershell pwsh -File ./scripts/verify-eve-getting-started.ps1 ``` -With clean sibling CultLib and Eve checkouts, the command installs missing build -dependencies and runs three explicit evidence layers. First it packs the +The maintainer command takes an explicit `-EveRoot` when Eve is not a sibling: + +```powershell +pwsh -File ./scripts/verify-eve-getting-started.ps1 -EveRoot D:\src\Eve +``` + +It installs missing build dependencies and runs three explicit evidence layers. +First it packs the CultLib managed dependency closure plus `GameCult.Eve.Surface`, then restores and runs an empty .NET consumer using only `PackageReference`. Next it packs the five TypeScript packages into another empty temporary consumer, validates the Eve surface @@ -20,7 +28,8 @@ headless observer converge, and reopens the durable `.cc` store. Then it boots a durable C# provider, local Odin fixture, real Chromium lowerer, and independent C# client. Chromium invokes before provider replacement; C# invokes afterward. Both retained clients prove discovery by identity, canonical receipts, provider -restart on another route, resubscription, and durable convergence. The retained +restart on another route, rejection of a better-priority wrong-authority route, +resubscription, and durable convergence. The retained C# session also completes 10,000 non-persistent typed operations under p99 and post-GC managed-memory gates. Set `CHROME_PATH` only when no installed Chrome, Chromium, Edge, or Playwright @@ -58,3 +67,6 @@ native projection. CultMesh owns discovery and connection continuity. pull request. - A setup step that depends on repository internals is an API defect, not acceptable tutorial folklore. +- The maintainer command may read owner checkouts to create packages. Every + sample process it launches must consume those package artifacts rather than + compiling through sibling project references. diff --git a/src/GameCult.Networking.WebSockets/GameCult.Networking.WebSockets.csproj b/src/GameCult.Networking.WebSockets/GameCult.Networking.WebSockets.csproj index 40214247..6ced9b42 100644 --- a/src/GameCult.Networking.WebSockets/GameCult.Networking.WebSockets.csproj +++ b/src/GameCult.Networking.WebSockets/GameCult.Networking.WebSockets.csproj @@ -4,6 +4,8 @@ enable enable CultNet schema-v0 WebSocket host adapter for browser and web runtimes + GameCult.Networking.WebSockets + game;networking;websocket;cultnet;cultmesh