Open Unity state through remote CultMesh replicas

This commit is contained in:
Meta Vens 2026-07-10 03:03:13 +01:00
commit 847af3772f
6 changed files with 128 additions and 2 deletions

View file

@ -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()
{

View file

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

View file

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

View file

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

View file

@ -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<AetheriaRuntimeVerseClient> 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<AetheriaRuntimeVerseClient> 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<int> RefreshRemoteAsync(
IReadOnlyList<string>? 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();

View file

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