From 847af3772fbb71f0965f381a909d3fc0df8f64c2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 10 Jul 2026 03:03:13 +0100 Subject: [PATCH] Open Unity state through remote CultMesh replicas --- .../Tests/DaemonRuntimeDocumentTests.cs | 25 ++++++ Packages/manifest.json | 2 +- .../README.md | 8 ++ .../AetheriaEveUnitySceneProviderBridge.cs | 2 +- .../Runtime/AetheriaRuntimeVerseClient.cs | 87 +++++++++++++++++++ docs/aetheria-perfect-machine-map.md | 6 ++ 6 files changed, 128 insertions(+), 2 deletions(-) diff --git a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs index 86de24365..46133cc19 100644 --- a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs +++ b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs @@ -414,6 +414,31 @@ public class DaemonRuntimeDocumentTests source.SourceId == AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest.ToString())); } + [Test] + public void AetheriaRuntimeVerseClientRemoteModeCreatesNonAuthoritativeReplicaShard() + { + var statePath = Path.Combine( + Path.GetTempPath(), + "aetheria-remote-verse-client-tests", + Path.GetRandomFileName(), + "replica.cc"); + + using var verse = AetheriaRuntimeVerseClient + .OpenRemoteAsync( + statePath, + "cultnet://127.0.0.1:39751", + "unity-remote-test", + synchronizeOnOpen: false) + .GetAwaiter() + .GetResult(); + + var shard = verse.Database.Shards.Single(); + Assert.IsTrue(verse.IsRemoteReplica); + Assert.AreEqual("cultnet://127.0.0.1:39751", verse.RemoteEndpoint); + Assert.IsFalse(shard.IsPrimary); + CollectionAssert.Contains(shard.PrimaryEndpoints, verse.RemoteEndpoint); + } + [Test] public void TickRunnerAppliesObservedCommandsAndPublishesFrame() { diff --git a/Packages/manifest.json b/Packages/manifest.json index 32767d45d..3142bc818 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -22,7 +22,7 @@ "org.gamecult.aetheria.state": "file:org.gamecult.aetheria.state", "org.gamecult.cultmath": "file:../../CultMath", "org.gamecult.eve.surface": "file:../../Eve/packages/org.gamecult.eve.surface", - "org.gamecult.eve.unity-scene": "file:../EveUnity/packages/org.gamecult.eve.unity-scene", + "org.gamecult.eve.unity-scene": "file:../../EveUnity/packages/org.gamecult.eve.unity-scene", "org.gamecult.eve.unity-uitoolkit": "file:../../Eve/packages/org.gamecult.eve.unity-uitoolkit", "com.unity.modules.accessibility": "1.0.0", "com.unity.modules.adaptiveperformance": "1.0.0", diff --git a/Packages/org.gamecult.aetheria.eve-runtime/README.md b/Packages/org.gamecult.aetheria.eve-runtime/README.md index 2fd8ac07c..8ec0e59aa 100644 --- a/Packages/org.gamecult.aetheria.eve-runtime/README.md +++ b/Packages/org.gamecult.aetheria.eve-runtime/README.md @@ -3,6 +3,14 @@ This package mounts provider-owned Eve surfaces from Aetheria's typed CultCache state into Unity UI Toolkit. +`AetheriaEveUnitySceneProviderBridge` is the local-replica adapter. It reads a +provider-owned `.cc` replica in the Unity process; it is not evidence of a +network connection to the daemon. Remote clients open +`AetheriaRuntimeVerseClient.OpenRemoteAsync`, synchronize published documents +through CultMesh, and forward writes to the daemon's primary shard. The generic +EveUnity PlayMode client must compose that remote path before this package can +claim live daemon playability. + `AetheriaEveSurfacePresenter` is a typed surface document lowerer: - it resolves the active client target from `GameData/aetheria-client.cc`; diff --git a/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveUnitySceneProviderBridge.cs b/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveUnitySceneProviderBridge.cs index b099e7f27..16fd787a6 100644 --- a/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveUnitySceneProviderBridge.cs +++ b/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveUnitySceneProviderBridge.cs @@ -45,7 +45,7 @@ namespace GameCult.Aetheria.EveRuntime "aetheria.daemon"); } - public string TransportKind => "aetheria-cultmesh-cultcache-transport"; + public string TransportKind => "aetheria-local-cultmesh-replica"; public string SurfacePointer => CurrentDocument.SourcePointer; diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs index 1f957dbd4..a700f57db 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs @@ -169,6 +169,7 @@ namespace GameCult.Aetheria.State.Verse public sealed class AetheriaRuntimeVerseClient : IDisposable { public const string DefaultRuntimeId = "aetheria-verse-client"; + private const string RemoteShardId = "primary"; private readonly CultMeshNode _node; private AetheriaClientState? _aetheriaState; @@ -197,6 +198,10 @@ namespace GameCult.Aetheria.State.Verse public CultNetDatabase Database => _node.Database; + public string RemoteEndpoint { get; private set; } = ""; + + public bool IsRemoteReplica => !string.IsNullOrWhiteSpace(RemoteEndpoint); + public static async Task OpenAsync( string statePath, string runtimeId = DefaultRuntimeId, @@ -233,6 +238,88 @@ namespace GameCult.Aetheria.State.Verse return new AetheriaRuntimeVerseClient(fullPath, effectiveRuntimeId, node); } + public static async Task OpenRemoteAsync( + string replicaStatePath, + string endpoint, + string runtimeId = DefaultRuntimeId, + bool pullOnOpen = false, + bool synchronizeOnOpen = true) + { + if (string.IsNullOrWhiteSpace(endpoint)) + throw new ArgumentException("CultMesh endpoint must be non-empty.", nameof(endpoint)); + + var fullPath = Path.GetFullPath(replicaStatePath ?? throw new ArgumentNullException(nameof(replicaStatePath))); + var effectiveRuntimeId = string.IsNullOrWhiteSpace(runtimeId) ? DefaultRuntimeId : runtimeId; + var registry = AetheriaRuntimeVerseContractRegistry.CreateCultCacheRegistry(); + var shard = new CultNetShardDescriptor( + RemoteShardId, + "aetheria-daemon", + epoch: 1, + isPrimary: false, + primaryEndpoints: new[] { endpoint.Trim() }); + var node = await CultMesh.CreateNodeAsync( + fullPath, + new CultMeshNodeOptions + { + StartServer = false, + EnableDurableShardLogs = true, + CacheOptions = new CultCacheOpenOptions + { + Registry = registry, + PullOnOpen = pullOnOpen, + StoreFlushOnDispose = true, + UseDirectoryStore = true + }, + DatabaseOptions = new CultNetDatabaseOptions + { + RuntimeId = effectiveRuntimeId, + Shards = new[] { shard }, + DocumentRegistry = AetheriaRuntimeVerseContractRegistry.CreateCultNetRegistry(registry) + } + }) + .ConfigureAwait(false); + + var client = new AetheriaRuntimeVerseClient(fullPath, effectiveRuntimeId, node) + { + RemoteEndpoint = endpoint.Trim() + }; + if (synchronizeOnOpen) + await client.RefreshRemoteAsync().ConfigureAwait(false); + return client; + } + + public async Task RefreshRemoteAsync( + IReadOnlyList? recordKeys = null, + TimeSpan? connectTimeout = null, + TimeSpan? responseTimeout = null) + { + ThrowIfDisposed(); + if (!IsRemoteReplica) + throw new InvalidOperationException("Remote refresh requires a client opened with OpenRemoteAsync."); + + var snapshot = CultMesh.SnapshotEndpoint( + RemoteEndpoint, + new CultMeshSnapshotEndpointOptions + { + Context = CultMesh.Verse("aetheria.remote", RuntimeId).Context, + DocumentRegistry = AetheriaRuntimeVerseContractRegistry.CreateCultNetRegistry(), + Request = new CultMeshSnapshotRequestOptions + { + RecordKeys = recordKeys, + ShardId = RemoteShardId, + ShardEpoch = 1, + ConnectTimeout = connectTimeout ?? TimeSpan.FromSeconds(5), + ResponseTimeout = responseTimeout ?? TimeSpan.FromSeconds(10), + MessageIdPrefix = "aetheria-unity", + RudpRuntimeId = RuntimeId, + RudpMaxFragmentBytes = 1200 + } + }); + var result = await snapshot.SyncSnapshotAsync(_node).ConfigureAwait(false); + await _node.FlushAsync().ConfigureAwait(false); + return result.AppliedCount; + } + public AetheriaClientState Aetheria() { ThrowIfDisposed(); diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 5850e52da..3129173a5 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -1485,6 +1485,12 @@ First Aetheria surfaces to publish: - Keep any transport bridge only if it delegates to CultMesh and protects a named external compatibility contract. - Publish Verse descriptors and state subscriptions through CultMesh. + - In progress: the Unity state client can now open an explicit remote + replica with `AetheriaRuntimeVerseClient.OpenRemoteAsync`. Snapshot reads + arrive through CultMesh and non-primary writes route to the daemon shard; + the local `.cc` scene adapter is no longer described as a live daemon + transport. The remaining proof is a separately running daemon, observed + receipts, and PlayMode reconciliation in an EveUnity-owned client. 6. Eve UI - Done: publish the typed catalog operator surface from `Aetheria.State`.