From 7e65154d7f42cf1c4ac55871c35676e7ccd2e5b2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 10 Jul 2026 14:13:53 +0100 Subject: [PATCH] Ship generic CultMesh playable world client --- README.md | 4 + .../PlayMode/GenericWorldCaptureTests.cs | 39 ++--- .../org.gamecult.eve.unity-scene/README.md | 7 +- .../EveUnityCultMeshPlayableWorldProvider.cs | 139 ++++++++++++++++++ ...UnityCultMeshPlayableWorldProvider.cs.meta | 11 ++ .../EveUnityPlayableWorldClientBootstrap.cs | 2 +- .../eve-runtime-capability.json | 2 + .../org.gamecult.eve.unity-scene/package.json | 2 +- scripts/run-aetheria-daemon-world-witness.ps1 | 8 +- 9 files changed, 185 insertions(+), 29 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs.meta diff --git a/README.md b/README.md index 6289eac..3e36f56 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ discover a Verse from one CultMesh rendezvous endpoint, select an advertised Aetheria's daemon-published 3D ARPG surface is the product witness; no Aetheria assembly is part of the client. +`EveUnityCultMeshPlayableWorldProvider` is the shipped composition root. Its +provider, surface, and Verse filters default to empty, so a client can select by +the generic `interactive-world` contract alone. + ```powershell powershell -ExecutionPolicy Bypass -File .\scripts\run-package-tests.ps1 powershell -ExecutionPolicy Bypass -File .\scripts\pack.ps1 diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index e87d133..d5dc99b 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -180,8 +180,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var lightObject = new GameObject("Generic Eve World Light"); RenderTexture target = null; Texture2D pixels = null; - EveUnityCultMeshLiveProviderTransport transport = null; - EveUnitySceneLiveProviderBridge bridge = null; + EveUnityCultMeshPlayableWorldProvider provider = null; EveUnityPlayableWorldRuntime runtime = null; WitnessReceipt movementReceipt = null; WitnessReceipt focusReceipt = null; @@ -191,27 +190,21 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests try { - var selection = new EveUnityCultMeshProviderDiscovery().Discover( + provider = root.AddComponent(); + provider.Configure( rendezvousEndpoint, + replicaPath, providerId, surfaceId, - "interactive-world"); - Assert.That(selection.VerseId, Is.Not.Empty); - Assert.That(selection.ProviderId, Is.Not.Empty); - Assert.That(selection.SurfaceId, Is.Not.Empty); - transport = new EveUnityCultMeshLiveProviderTransport( - replicaPath, - selection.Endpoint, - selection.ProviderId, - selection.SurfaceId, - $"eve-unity-test-{Guid.NewGuid():N}"); + requiredSurfaceKind: "interactive-world", + clientRuntimeId: $"eve-unity-test-{Guid.NewGuid():N}"); var publicationDeadline = Time.realtimeSinceStartup + 10f; while (true) { var published = false; try { - transport.Refresh(); + provider.Refresh(); published = true; } catch (InvalidOperationException) when (Time.realtimeSinceStartup < publicationDeadline) @@ -220,15 +213,17 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (published) break; yield return new WaitForSecondsRealtime(0.1f); } - bridge = new EveUnitySceneLiveProviderBridge(transport); - bridge.Connect(); + Assert.That(provider.Selection, Is.Not.Null); + Assert.That(provider.Selection.VerseId, Is.Not.Empty); + Assert.That(provider.Selection.ProviderId, Is.Not.Empty); + Assert.That(provider.Selection.SurfaceId, Is.Not.Empty); runtime = EveUnityPlayableWorldRuntime.CreateForGameObjectScene( root.transform, - bridge, - bridge, - assetManifestDocuments: null, - receiptSource: bridge, - fallbackAssetProvider: transport); + provider, + provider, + null, + provider, + provider); var initial = runtime.Connect(); Assert.That(initial.ActiveEntities, Is.GreaterThan(0)); Assert.That(runtime.ActiveWorld.PlayerEntityId, Is.Not.Empty); @@ -357,8 +352,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (target != null) UnityEngine.Object.DestroyImmediate(target); if (pixels != null) UnityEngine.Object.DestroyImmediate(pixels); runtime?.Dispose(); - bridge?.Dispose(); - transport?.Dispose(); UnityEngine.Object.DestroyImmediate(lightObject); UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(root); diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index ba81d98..f8e0ddc 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -7,5 +7,10 @@ Providers implement the surface-document, asset-manifest, command-sink, and receipt-source ports. `EveUnityPlayableWorldClientBootstrap` wires those ports to the generic scene host, input driver, camera rig, and `GameObject` sink. -The package never imports provider product assemblies. +`EveUnityCultMeshPlayableWorldProvider` is the runtime composition root for a +networked client. Give it one Verse rendezvous endpoint and it discovers an +advertised `interactive-world`, constructs the generic CultMesh transport, and +provides every bootstrap port. Provider, surface, and Verse IDs are optional +selection filters; they are not product knowledge required by the client. +The package never imports provider product assemblies. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs new file mode 100644 index 0000000..612bb5e --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -0,0 +1,139 @@ +using System; +using System.IO; +using GameCult.Eve.Surface; +using UnityEngine; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityCultMeshPlayableWorldProvider : + MonoBehaviour, + IEveUnitySceneProviderSurfaceDocumentSource, + IEveUnitySceneProviderSurfaceDocumentConnection, + IEveUnitySceneCommandSink, + IEveUnitySceneCommandReceiptSource, + IEveUnityProviderRefreshSource, + IEveUnityGameObjectAssetProvider + { + [SerializeField] private string rendezvousEndpoint = ""; + [SerializeField] private string providerFilter = ""; + [SerializeField] private string surfaceFilter = ""; + [SerializeField] private string verseFilter = ""; + [SerializeField] private string surfaceKind = "interactive-world"; + [SerializeField] private string replicaPath = ""; + [SerializeField] private string runtimeId = "eve-unity"; + + private EveUnityCultMeshLiveProviderTransport? _transport; + private EveUnitySceneLiveProviderBridge? _bridge; + + public EveUnityCultMeshProviderSelection? Selection { get; private set; } + + public string SinkKind => Bridge.SinkKind; + + public EveUnitySceneProviderSurfaceDocument CurrentDocument => Bridge.CurrentSurfaceDocument; + + public event Action? DocumentAvailable + { + add { Bridge.DocumentAvailable += value; } + remove { Bridge.DocumentAvailable -= value; } + } + + public event Action? ReceiptAvailable + { + add { Bridge.ReceiptAvailable += value; } + remove { Bridge.ReceiptAvailable -= value; } + } + + public void Configure( + string endpoint, + string replicaStorePath = "", + string providerId = "", + string surfaceId = "", + string verseId = "", + string requiredSurfaceKind = "interactive-world", + string clientRuntimeId = "eve-unity") + { + if (_bridge != null) + throw new InvalidOperationException("Disconnect the active provider before changing discovery configuration."); + + rendezvousEndpoint = endpoint ?? ""; + replicaPath = replicaStorePath ?? ""; + providerFilter = providerId ?? ""; + surfaceFilter = surfaceId ?? ""; + verseFilter = verseId ?? ""; + surfaceKind = string.IsNullOrWhiteSpace(requiredSurfaceKind) ? "interactive-world" : requiredSurfaceKind; + runtimeId = string.IsNullOrWhiteSpace(clientRuntimeId) ? "eve-unity" : clientRuntimeId; + } + + public void Connect() + { + Bridge.Connect(); + } + + public void Disconnect() + { + _bridge?.Disconnect(); + } + + public void Refresh() + { + Bridge.Connect(); + Bridge.Refresh(); + } + + public void Submit(EveSurfaceCommandRequest request) + { + Bridge.Submit(request); + } + + public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) + { + EnsureTransport(); + return _transport!.ResolvePrefab(asset); + } + + private void OnDestroy() + { + _bridge?.Dispose(); + _transport?.Dispose(); + _bridge = null; + _transport = null; + Selection = null; + } + + private EveUnitySceneLiveProviderBridge Bridge + { + get + { + EnsureTransport(); + return _bridge!; + } + } + + private void EnsureTransport() + { + if (_transport != null) + return; + if (string.IsNullOrWhiteSpace(rendezvousEndpoint)) + throw new InvalidOperationException("EveUnity requires a CultMesh rendezvous endpoint."); + + Selection = new EveUnityCultMeshProviderDiscovery().Discover( + rendezvousEndpoint, + providerFilter, + surfaceFilter, + surfaceKind, + verseFilter); + var resolvedReplicaPath = string.IsNullOrWhiteSpace(replicaPath) + ? Path.Combine(Application.temporaryCachePath, $"eve-unity-{GetInstanceID()}.cc") + : replicaPath; + _transport = new EveUnityCultMeshLiveProviderTransport( + resolvedReplicaPath, + Selection.Endpoint, + Selection.ProviderId, + Selection.SurfaceId, + runtimeId); + _bridge = new EveUnitySceneLiveProviderBridge(_transport); + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs.meta new file mode 100644 index 0000000..a3c87c9 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 984ba05ee3424bb2a2eb147465a3f17d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientBootstrap.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientBootstrap.cs index a78caf9..1883517 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientBootstrap.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientBootstrap.cs @@ -58,7 +58,7 @@ namespace GameCult.Eve.UnityScene resolvedProvider); var resolvedFallbackAssetProvider = ResolveOptionalBehaviour( fallbackAssetProvider, - null); + resolvedProvider); sceneRoot = ResolveSceneRoot(); resolvedHost.ConnectOnEnable = false; diff --git a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json index f82d5ff..3f2f5a8 100644 --- a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json +++ b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json @@ -16,6 +16,7 @@ "supportedFeatures": [ "providerAdvertisements", "verseDiscovery", + "genericPlayableWorldDiscovery", "commandTransport", "providerSurfaceSession", "providerSurfaceSource", @@ -86,6 +87,7 @@ "packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneLiveProviderBridge.cs", "packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs", + "packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 0e18f0e..72d85a1 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.1.0", + "version": "0.1.1", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 306c224..c53caa8 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -1,6 +1,7 @@ param( [string] $UnityExe = "C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Unity.exe", [string] $AetheriaRoot = "E:\Projects\Aetheria", + [string] $ClientProject = "ReleaseConsumerProject", [int] $Port = 3076, [string] $OutputDirectory = "artifacts\aetheria-daemon", [ValidateSet("auto", "cold", "warm")] @@ -10,7 +11,7 @@ param( $ErrorActionPreference = "Stop" $repoRoot = Split-Path -Parent $PSScriptRoot -$projectRoot = "ReleaseConsumerProject" +$projectRoot = $ClientProject $outputRoot = if ([IO.Path]::IsPathRooted($OutputDirectory)) { $OutputDirectory } else { Join-Path $repoRoot $OutputDirectory } $resultsPath = Join-Path $outputRoot "results.xml" $unityLogPath = Join-Path $outputRoot "unity.log" @@ -126,8 +127,9 @@ try { if (-not (Test-Path $factsPath)) { throw "Live world witness facts are missing: $factsPath" } $facts = Get-Content -LiteralPath $factsPath -Raw | ConvertFrom-Json $releaseLock = Get-Content -LiteralPath (Join-Path $repoRoot "ReleaseConsumerProject\Packages\packages-lock.json") -Raw | ConvertFrom-Json - $facts | Add-Member -NotePropertyName releasedPackageClient -NotePropertyValue $true - $facts | Add-Member -NotePropertyName clientProject -NotePropertyValue "ReleaseConsumerProject" + $releasedPackageClient = $projectRoot -eq "ReleaseConsumerProject" + $facts | Add-Member -NotePropertyName releasedPackageClient -NotePropertyValue $releasedPackageClient + $facts | Add-Member -NotePropertyName clientProject -NotePropertyValue $projectRoot $facts | Add-Member -NotePropertyName eveUnityPackageCommit -NotePropertyValue $releaseLock.dependencies.'org.gamecult.eve.unity-scene'.hash $facts | Add-Member -NotePropertyName cultLibPackageCommit -NotePropertyValue $releaseLock.dependencies.'org.gamecult.cultlib'.hash $capture = Get-Item -LiteralPath $capturePath