Open Unity state through remote CultMesh replicas
This commit is contained in:
parent
e3e37d082a
commit
847af3772f
6 changed files with 128 additions and 2 deletions
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
Loading…
Reference in a new issue