Ship generic CultMesh playable world client

This commit is contained in:
Meta Vens 2026-07-10 14:13:53 +01:00
commit 7e65154d7f
9 changed files with 185 additions and 29 deletions

View file

@ -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

View file

@ -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<EveUnityCultMeshPlayableWorldProvider>();
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);

View file

@ -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.

View file

@ -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<EveUnitySceneProviderSurfaceDocument>? DocumentAvailable
{
add { Bridge.DocumentAvailable += value; }
remove { Bridge.DocumentAvailable -= value; }
}
public event Action<EveUnitySceneCommandReceipt>? 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);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 984ba05ee3424bb2a2eb147465a3f17d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -58,7 +58,7 @@ namespace GameCult.Eve.UnityScene
resolvedProvider);
var resolvedFallbackAssetProvider = ResolveOptionalBehaviour<IEveUnityGameObjectAssetProvider>(
fallbackAssetProvider,
null);
resolvedProvider);
sceneRoot = ResolveSceneRoot();
resolvedHost.ConnectOnEnable = false;

View file

@ -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",

View file

@ -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": {

View file

@ -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