Route EveUnity provider state through CultMesh sessions
This commit is contained in:
parent
6ddc038e09
commit
0dee524662
5 changed files with 195 additions and 31 deletions
|
|
@ -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<CultMeshSession> OpenDocumentsAsync(CancellationToken cancellationToken = default) =>
|
||||
Sessions.ConnectAsync(EndpointId, CultMeshProtocols.Documents, cancellationToken);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Sessions.Dispose();
|
||||
_discovery.Dispose();
|
||||
}
|
||||
|
||||
internal static EveUnityCultMeshConnectionContext Create(
|
||||
string endpointId,
|
||||
IEnumerable<CultMeshVerseDescriptor> 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<CultMeshVerseDescriptor> routes)
|
||||
{
|
||||
_routes = routes?.ToArray() ?? throw new ArgumentNullException(nameof(routes));
|
||||
}
|
||||
|
||||
public string SourceId => "eve-unity-rendezvous";
|
||||
|
||||
public Task<IReadOnlyList<CultMeshDiscoveryObservation>> LookupAsync(
|
||||
CultMeshDiscoveryQuery query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
IReadOnlyList<CultMeshDiscoveryObservation> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e2ece888321b9854fb4424a65448f703
|
||||
|
|
@ -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<string> _pendingCommandIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
private readonly HashSet<string> _publishedReceiptIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, GameObject> _prefabs = new Dictionary<string, GameObject>(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!);
|
||||
|
|
|
|||
|
|
@ -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<EveUnityCultMeshProviderSelection>? _discovery;
|
||||
private readonly ConcurrentQueue<EveEntitySoaViewDocument> _pendingEntityViews = new ConcurrentQueue<EveEntitySoaViewDocument>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<EveUnityCultMeshProviderSelection> 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<string>();
|
||||
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<EveProviderAdvertisementDocument[]> 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<EveProviderAdvertisementDocument>())
|
||||
.ToArray();
|
||||
}.Request,
|
||||
networkRegistry,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return (await snapshot.FetchDocumentsAsync<EveProviderAdvertisementDocument>()
|
||||
.ConfigureAwait(false)).ToArray();
|
||||
}
|
||||
|
||||
private static T RunNetwork<T>(Func<Task<T>> operation) =>
|
||||
Task.Run(operation).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue