From 0dee5246628268a2eb476a38d0dd7c72e824792d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 12 Jul 2026 20:45:25 +0100 Subject: [PATCH] Route EveUnity provider state through CultMesh sessions --- .../EveUnityCultMeshConnectionContext.cs | 84 +++++++++++++++++++ .../EveUnityCultMeshConnectionContext.cs.meta | 2 + .../EveUnityCultMeshLiveProviderTransport.cs | 49 ++++++++--- .../EveUnityCultMeshPlayableWorldProvider.cs | 39 +++++++-- .../EveUnityCultMeshProviderDiscovery.cs | 52 ++++++++---- 5 files changed, 195 insertions(+), 31 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectionContext.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectionContext.cs.meta diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectionContext.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectionContext.cs new file mode 100644 index 0000000..d6f7abf --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectionContext.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GameCult.Mesh; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityCultMeshConnectionContext : IDisposable + { + private readonly CultMeshDiscoveryService _discovery; + + internal EveUnityCultMeshConnectionContext( + CultMeshEndpointId endpointId, + CultMeshDiscoveryService discovery, + CultMeshSessionManager sessions) + { + EndpointId = endpointId ?? throw new ArgumentNullException(nameof(endpointId)); + _discovery = discovery ?? throw new ArgumentNullException(nameof(discovery)); + Sessions = sessions ?? throw new ArgumentNullException(nameof(sessions)); + } + + public CultMeshEndpointId EndpointId { get; } + public CultMeshSessionManager Sessions { get; } + + public Task OpenDocumentsAsync(CancellationToken cancellationToken = default) => + Sessions.ConnectAsync(EndpointId, CultMeshProtocols.Documents, cancellationToken); + + public void Dispose() + { + Sessions.Dispose(); + _discovery.Dispose(); + } + + internal static EveUnityCultMeshConnectionContext Create( + string endpointId, + IEnumerable routes) + { + var identity = CultMeshEndpointId.Parse(endpointId); + var discovery = new CultMeshDiscoveryService(new[] { new FixedRouteSource(routes) }); + return new EveUnityCultMeshConnectionContext( + identity, + discovery, + new CultMeshSessionManager(discovery, new ICultMeshTransportConnector[] + { + new CultMeshSchemaTransportConnector() + })); + } + + private sealed class FixedRouteSource : ICultMeshLookupSource + { + private readonly CultMeshVerseDescriptor[] _routes; + + public FixedRouteSource(IEnumerable routes) + { + _routes = routes?.ToArray() ?? throw new ArgumentNullException(nameof(routes)); + } + + public string SourceId => "eve-unity-rendezvous"; + + public Task> LookupAsync( + CultMeshDiscoveryQuery query, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + var now = DateTimeOffset.UtcNow; + IReadOnlyList observations = _routes + .Where(route => string.Equals(route.VerseId, query.EndpointId, StringComparison.Ordinal)) + .Select(route => new CultMeshDiscoveryObservation( + route, + SourceId, + now, + now.AddMinutes(5), + CultMeshDiscoveryTrust.Unsigned, + "CultMesh rendezvous catalog")) + .ToArray(); + return Task.FromResult(observations); + } + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectionContext.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectionContext.cs.meta new file mode 100644 index 0000000..7d74b1b --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectionContext.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e2ece888321b9854fb4424a65448f703 \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 566b19e..0cae7e9 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -39,6 +39,8 @@ namespace GameCult.Eve.UnityScene private readonly string _providerId; private readonly string _surfaceId; private readonly string _runtimeId; + private readonly EveUnityCultMeshConnectionContext? _connection; + private CultMeshSession? _documentSession; private readonly HashSet _pendingCommandIds = new HashSet(StringComparer.Ordinal); private readonly HashSet _publishedReceiptIds = new HashSet(StringComparer.Ordinal); private readonly Dictionary _prefabs = new Dictionary(StringComparer.Ordinal); @@ -59,7 +61,8 @@ namespace GameCult.Eve.UnityScene string endpoint, string providerId, string surfaceId, - string runtimeId = "eve-unity") + string runtimeId = "eve-unity", + EveUnityCultMeshConnectionContext? connection = null) { _replicaPath = string.IsNullOrWhiteSpace(replicaPath) ? throw new ArgumentException("Replica path must be non-empty.", nameof(replicaPath)) @@ -74,6 +77,7 @@ namespace GameCult.Eve.UnityScene ? throw new ArgumentException("Surface id must be non-empty.", nameof(surfaceId)) : surfaceId.Trim(); _runtimeId = string.IsNullOrWhiteSpace(runtimeId) ? "eve-unity" : runtimeId.Trim(); + _connection = connection; CurrentSurfaceDocument = EmptySurfaceDocument(); CurrentAssetManifestDocument = EmptyAssetManifest(); } @@ -171,6 +175,7 @@ namespace GameCult.Eve.UnityScene _node?.Dispose(); _snapshot?.Dispose(); _subscriptions?.Dispose(); + _connection?.Dispose(); _node = null; _snapshot = null; _subscriptions = null; @@ -235,6 +240,26 @@ namespace GameCult.Eve.UnityScene }) .GetAwaiter() .GetResult(); + if (_connection != null) + { + _documentSession = _connection.OpenDocumentsAsync().GetAwaiter().GetResult(); + _snapshot = CultMeshSnapshotSession.ConnectAsync( + _connection.Sessions, + _connection.EndpointId, + new CultMeshSnapshotRequestOptions + { + ShardId = RemoteShardId, + ShardEpoch = 1, + ConnectTimeout = TimeSpan.FromSeconds(5), + ResponseTimeout = TimeSpan.FromSeconds(10), + MessageIdPrefix = "eve-unity", + RudpRuntimeId = _runtimeId, + RudpMaxFragmentBytes = 1024 + }, + _networkRegistry) + .GetAwaiter().GetResult(); + return; + } _snapshot = CultMesh.SnapshotSession( _endpoint, new CultMeshSnapshotRequestOptions @@ -303,16 +328,20 @@ namespace GameCult.Eve.UnityScene private void EnsureLiveSubscriptions() { if (_subscriptions != null) return; - var endpoint = new Uri(_endpoint); - var client = CultNetSchemaClients.CreateForEndpoint(_endpoint); - client.Connect(endpoint.Host, endpoint.Port); - var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5); - while (!client.Connected && DateTime.UtcNow < deadline) - System.Threading.Thread.Sleep(1); - if (!client.Connected) + var client = _documentSession?.OpenSchemaClient() + ?? CultNetSchemaClients.CreateForEndpoint(_endpoint); + if (_documentSession == null) { - client.Dispose(); - throw new TimeoutException($"Timed out connecting live Eve state subscription to '{_endpoint}'."); + var endpoint = new Uri(_endpoint); + client.Connect(endpoint.Host, endpoint.Port); + var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5); + while (!client.Connected && DateTime.UtcNow < deadline) + System.Threading.Thread.Sleep(1); + if (!client.Connected) + { + client.Dispose(); + throw new TimeoutException($"Timed out connecting live Eve state subscription to '{_endpoint}'."); + } } _subscriptions = new CultNetDatabaseSubscriptionClient(client, _node!.Database.Cache, _networkRegistry!); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs index ac39266..884c818 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Collections.Concurrent; +using System.Threading.Tasks; using GameCult.Eve.Surface; using UnityEngine; @@ -29,6 +30,7 @@ namespace GameCult.Eve.UnityScene private EveUnityCultMeshLiveProviderTransport? _transport; private EveUnitySceneLiveProviderBridge? _bridge; + private Task? _discovery; private readonly ConcurrentQueue _pendingEntityViews = new ConcurrentQueue(); public EveEntitySoaViewDocument? CurrentEntityView { get; private set; } @@ -117,15 +119,20 @@ namespace GameCult.Eve.UnityScene private void OnDestroy() { + var transportOwnedConnection = _transport != null; _bridge?.Dispose(); _transport?.Dispose(); _bridge = null; _transport = null; Selection = null; + if (!transportOwnedConnection && _discovery?.Status == TaskStatus.RanToCompletion) + _discovery.Result.Connection.Dispose(); + _discovery = null; } private void Update() { + CompleteDiscoveryOnMainThread(); _transport?.PumpLiveEvents(); EveEntitySoaViewDocument? latest = null; while (_pendingEntityViews.TryDequeue(out var next)) latest = next; @@ -150,12 +157,29 @@ namespace GameCult.Eve.UnityScene if (string.IsNullOrWhiteSpace(rendezvousEndpoint)) throw new InvalidOperationException("EveUnity requires a CultMesh rendezvous endpoint."); - Selection = new EveUnityCultMeshProviderDiscovery().Discover( - rendezvousEndpoint, - providerFilter, - surfaceFilter, - surfaceKind, - verseFilter); + if (_discovery == null) + _discovery = new EveUnityCultMeshProviderDiscovery().DiscoverAsync( + rendezvousEndpoint, + providerFilter, + surfaceFilter, + surfaceKind, + verseFilter); + CompleteDiscoveryOnMainThread(); + if (_transport == null) + throw new InvalidOperationException("EveUnity CultMesh provider discovery is still in progress."); + } + + private void CompleteDiscoveryOnMainThread() + { + if (_transport != null || _discovery == null || !_discovery.IsCompleted) + return; + if (_discovery.IsFaulted) + throw _discovery.Exception?.GetBaseException() + ?? new InvalidOperationException("EveUnity CultMesh provider discovery failed."); + if (_discovery.IsCanceled) + throw new InvalidOperationException("EveUnity CultMesh provider discovery was cancelled."); + + Selection = _discovery.Result; var resolvedReplicaPath = string.IsNullOrWhiteSpace(replicaPath) ? Path.Combine(Application.temporaryCachePath, $"eve-unity-{GetInstanceID()}.cc") : replicaPath; @@ -164,7 +188,8 @@ namespace GameCult.Eve.UnityScene Selection.Endpoint, Selection.ProviderId, Selection.SurfaceId, - runtimeId); + runtimeId, + Selection.Connection); _transport.EntityViewAvailable += view => _pendingEntityViews.Enqueue(view); _bridge = new EveUnitySceneLiveProviderBridge(_transport); } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs index b2941d7..cb6a641 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using GameCult.Eve.Surface; using GameCult.Mesh; @@ -17,13 +18,15 @@ namespace GameCult.Eve.UnityScene string verseId, string providerId, string surfaceId, - string surfaceKind) + string surfaceKind, + EveUnityCultMeshConnectionContext connection) { Endpoint = endpoint ?? ""; VerseId = verseId ?? ""; ProviderId = providerId ?? ""; SurfaceId = surfaceId ?? ""; SurfaceKind = surfaceKind ?? ""; + Connection = connection ?? throw new ArgumentNullException(nameof(connection)); } public string Endpoint { get; } @@ -31,6 +34,7 @@ namespace GameCult.Eve.UnityScene public string ProviderId { get; } public string SurfaceId { get; } public string SurfaceKind { get; } + public EveUnityCultMeshConnectionContext Connection { get; } } public sealed class EveUnityCultMeshProviderDiscovery @@ -46,17 +50,28 @@ namespace GameCult.Eve.UnityScene string surfaceId = "", string surfaceKind = "interactive-world", string verseId = "") + => DiscoverAsync(rendezvousEndpoint, providerId, surfaceId, surfaceKind, verseId) + .GetAwaiter().GetResult(); + + public async Task DiscoverAsync( + string rendezvousEndpoint, + string providerId = "", + string surfaceId = "", + string surfaceKind = "interactive-world", + string verseId = "", + CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(rendezvousEndpoint)) throw new ArgumentException("Rendezvous endpoint must be non-empty.", nameof(rendezvousEndpoint)); - var response = RunNetwork(() => CultMesh.CreateVerseDiscoveryClient().FetchAsync( + var response = await CultMesh.CreateVerseDiscoveryClient().FetchAsync( rendezvousEndpoint, new CultMeshVerseCatalogRequestMessage { VerseIds = string.IsNullOrWhiteSpace(verseId) ? null : new[] { verseId }, TransportVersion = "cultmesh.v0" - })); + }).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); var candidates = response.Verses .Where(verse => string.IsNullOrWhiteSpace(verseId) || @@ -71,9 +86,13 @@ namespace GameCult.Eve.UnityScene var failures = new List(); foreach (var candidate in candidates) { + EveUnityCultMeshConnectionContext? connection = null; try { - var advertisement = FetchAdvertisements(candidate.Endpoint) + connection = EveUnityCultMeshConnectionContext.Create( + candidate.Verse.VerseId, + new[] { candidate.Verse.ToVerseDescriptor() }); + var advertisement = (await FetchAdvertisementsAsync(connection, cancellationToken).ConfigureAwait(false)) .Where(document => string.IsNullOrWhiteSpace(providerId) || string.Equals(document.ProviderId, providerId, StringComparison.Ordinal)) .Select(document => new @@ -94,10 +113,12 @@ namespace GameCult.Eve.UnityScene candidate.Verse.VerseId, advertisement.Document.ProviderId, advertisement.Surface.SurfaceId, - advertisement.Surface.SurfaceKind); + advertisement.Surface.SurfaceKind, + connection); } catch (Exception error) { + connection?.Dispose(); failures.Add($"{candidate.Endpoint}: {error.Message}"); } } @@ -107,12 +128,15 @@ namespace GameCult.Eve.UnityScene throw new InvalidOperationException($"No advertised Eve surface matched {filter}.{detail}"); } - private static EveProviderAdvertisementDocument[] FetchAdvertisements(string endpoint) + private static async Task FetchAdvertisementsAsync( + EveUnityCultMeshConnectionContext connection, + CancellationToken cancellationToken) { var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(AdvertisementDocumentTypes); var networkRegistry = CultMesh.CreateCultNetDocumentRegistry(AdvertisementDocumentTypes, cacheRegistry); - var snapshot = CultMesh.SnapshotEndpoint( - endpoint, + using var snapshot = await CultMeshSnapshotSession.ConnectAsync( + connection.Sessions, + connection.EndpointId, new CultMeshSnapshotEndpointOptions { Context = CultMesh.Verse("eve.discovery", "eve-unity").Context, @@ -127,12 +151,12 @@ namespace GameCult.Eve.UnityScene RudpRuntimeId = "eve-unity.discovery", RudpMaxFragmentBytes = 1024 } - }); - return RunNetwork(() => snapshot.FetchDocumentsAsync()) - .ToArray(); + }.Request, + networkRegistry, + cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + return (await snapshot.FetchDocumentsAsync() + .ConfigureAwait(false)).ToArray(); } - - private static T RunNetwork(Func> operation) => - Task.Run(operation).GetAwaiter().GetResult(); } }