Lower EveUnity through the CultMesh client
This commit is contained in:
parent
5118387a44
commit
e836884f16
5 changed files with 85 additions and 403 deletions
|
|
@ -1,84 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e2ece888321b9854fb4424a65448f703
|
||||
|
|
@ -34,12 +34,12 @@ namespace GameCult.Eve.UnityScene
|
|||
typeof(CultMeshCdnArtifactChunk)
|
||||
};
|
||||
|
||||
private readonly string _replicaPath;
|
||||
private readonly string _endpoint;
|
||||
private readonly EveUnityCultMeshProviderSelection _selection;
|
||||
private readonly CultMeshClient _mesh;
|
||||
private readonly string _endpointId;
|
||||
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);
|
||||
|
|
@ -48,36 +48,24 @@ namespace GameCult.Eve.UnityScene
|
|||
private readonly List<AssetBundle> _assetBundles = new List<AssetBundle>();
|
||||
private readonly Dictionary<string, int> _cameraCullingMasks = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
private readonly ConcurrentQueue<object> _liveDocuments = new ConcurrentQueue<object>();
|
||||
private CultMeshNode? _node;
|
||||
private CultMeshSnapshotSession? _snapshot;
|
||||
private CultNetDocumentRegistry? _networkRegistry;
|
||||
private CultNetShardDescriptor? _replicaShard;
|
||||
private CultNetDatabaseSubscriptionClient? _subscriptions;
|
||||
private readonly List<IDisposable> _liveWatches = new List<IDisposable>();
|
||||
private bool _liveSubscriptionsOpen;
|
||||
private EveProviderAdvertisementDocument? _advertisement;
|
||||
private EveAdvertisedSurface? _advertisedSurface;
|
||||
|
||||
public EveUnityCultMeshLiveProviderTransport(
|
||||
string replicaPath,
|
||||
string endpoint,
|
||||
string providerId,
|
||||
string surfaceId,
|
||||
string runtimeId = "eve-unity",
|
||||
EveUnityCultMeshConnectionContext? connection = null)
|
||||
EveUnityCultMeshProviderSelection selection,
|
||||
string runtimeId = "eve-unity")
|
||||
{
|
||||
_replicaPath = string.IsNullOrWhiteSpace(replicaPath)
|
||||
? throw new ArgumentException("Replica path must be non-empty.", nameof(replicaPath))
|
||||
: Path.GetFullPath(replicaPath);
|
||||
_endpoint = string.IsNullOrWhiteSpace(endpoint)
|
||||
? throw new ArgumentException("CultMesh endpoint must be non-empty.", nameof(endpoint))
|
||||
: endpoint.Trim();
|
||||
_providerId = string.IsNullOrWhiteSpace(providerId)
|
||||
? throw new ArgumentException("Provider id must be non-empty.", nameof(providerId))
|
||||
: providerId.Trim();
|
||||
_surfaceId = string.IsNullOrWhiteSpace(surfaceId)
|
||||
? throw new ArgumentException("Surface id must be non-empty.", nameof(surfaceId))
|
||||
: surfaceId.Trim();
|
||||
_ = replicaPath;
|
||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
_mesh = selection.Mesh;
|
||||
_endpointId = selection.EndpointId;
|
||||
_providerId = selection.ProviderId;
|
||||
_surfaceId = selection.SurfaceId;
|
||||
_runtimeId = string.IsNullOrWhiteSpace(runtimeId) ? "eve-unity" : runtimeId.Trim();
|
||||
_connection = connection;
|
||||
CurrentSurfaceDocument = EmptySurfaceDocument();
|
||||
CurrentAssetManifestDocument = EmptyAssetManifest();
|
||||
}
|
||||
|
|
@ -159,10 +147,9 @@ namespace GameCult.Eve.UnityScene
|
|||
SourceRuntimeId = _runtimeId,
|
||||
SourceRole = "eve-unity"
|
||||
});
|
||||
new CultNetSchemaWriteForwarder(new CultNetSchemaWriteForwarderOptions())
|
||||
.ForwardPutAsync(_replicaShard!, message)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
message.ShardId = RemoteShardId;
|
||||
message.ShardEpoch = 1;
|
||||
_documentSession!.SendCultNet(message);
|
||||
_pendingCommandIds.Add(commandId);
|
||||
}
|
||||
|
||||
|
|
@ -172,15 +159,11 @@ namespace GameCult.Eve.UnityScene
|
|||
_assetBundles.Clear();
|
||||
_prefabs.Clear();
|
||||
_nativeAssets.Clear();
|
||||
_node?.Dispose();
|
||||
_snapshot?.Dispose();
|
||||
_subscriptions?.Dispose();
|
||||
_connection?.Dispose();
|
||||
_node = null;
|
||||
_snapshot = null;
|
||||
_subscriptions = null;
|
||||
foreach (var watch in _liveWatches) watch.Dispose();
|
||||
_liveWatches.Clear();
|
||||
_mesh.Dispose();
|
||||
_documentSession = null;
|
||||
_networkRegistry = null;
|
||||
_replicaShard = null;
|
||||
_pendingCommandIds.Clear();
|
||||
_publishedReceiptIds.Clear();
|
||||
}
|
||||
|
|
@ -206,103 +189,24 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
private void EnsureOpen()
|
||||
{
|
||||
if (_node != null)
|
||||
if (_documentSession != null)
|
||||
return;
|
||||
|
||||
var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(WireDocumentTypes);
|
||||
_networkRegistry = CultMesh.CreateCultNetDocumentRegistry(WireDocumentTypes, cacheRegistry);
|
||||
_replicaShard = new CultNetShardDescriptor(
|
||||
RemoteShardId,
|
||||
_providerId,
|
||||
epoch: 1,
|
||||
isPrimary: false,
|
||||
schemaIds: WireDocumentTypes.Select(type => cacheRegistry.GetRequired(type).SchemaId),
|
||||
primaryEndpoints: new[] { _endpoint });
|
||||
_node = CultMesh.CreateNodeAsync(
|
||||
_replicaPath,
|
||||
new CultMeshNodeOptions
|
||||
{
|
||||
StartServer = false,
|
||||
EnableDurableShardLogs = true,
|
||||
CacheOptions = new CultCacheOpenOptions
|
||||
{
|
||||
Registry = cacheRegistry,
|
||||
PullOnOpen = false,
|
||||
StoreFlushOnDispose = true,
|
||||
UseDirectoryStore = true
|
||||
},
|
||||
DatabaseOptions = new CultNetDatabaseOptions
|
||||
{
|
||||
RuntimeId = _runtimeId,
|
||||
Shards = new[] { _replicaShard },
|
||||
DocumentRegistry = _networkRegistry
|
||||
}
|
||||
})
|
||||
.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
|
||||
{
|
||||
ShardId = RemoteShardId,
|
||||
ShardEpoch = 1,
|
||||
ConnectTimeout = TimeSpan.FromSeconds(5),
|
||||
ResponseTimeout = TimeSpan.FromSeconds(10),
|
||||
MessageIdPrefix = "eve-unity",
|
||||
RudpRuntimeId = _runtimeId,
|
||||
RudpMaxFragmentBytes = 1024
|
||||
},
|
||||
_networkRegistry);
|
||||
_documentSession = _mesh.ConnectAsync(_endpointId, CultMeshProtocols.Documents)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private void ResolveAdvertisement()
|
||||
{
|
||||
var documents = _snapshot!
|
||||
.FetchDocumentsAsync<EveProviderAdvertisementDocument>()
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
_advertisement = documents.FirstOrDefault(document =>
|
||||
string.Equals(document.ProviderId, _providerId, StringComparison.Ordinal))
|
||||
?? throw new InvalidOperationException($"Provider '{_providerId}' did not publish an Eve advertisement.");
|
||||
_advertisedSurface = _advertisement.Surfaces.FirstOrDefault(surface =>
|
||||
string.Equals(surface.SurfaceId, _surfaceId, StringComparison.Ordinal))
|
||||
?? throw new InvalidOperationException($"Provider '{_providerId}' did not advertise surface '{_surfaceId}'.");
|
||||
if (!string.Equals(_advertisedSurface.Transport, "cultmesh-record", StringComparison.Ordinal))
|
||||
throw new InvalidOperationException("EveUnity CultMesh transport requires a cultmesh-record surface advertisement.");
|
||||
if (string.IsNullOrWhiteSpace(_advertisedSurface.RecordRef))
|
||||
throw new InvalidOperationException("The provider surface advertisement does not publish a record reference.");
|
||||
_advertisement = _selection.Surface.Provider;
|
||||
_advertisedSurface = _selection.Surface.Advertisement;
|
||||
}
|
||||
|
||||
private void RefreshSurface()
|
||||
{
|
||||
var surface = _snapshot!
|
||||
.FetchDocumentsAsync<EveSurfaceDocument>(
|
||||
recordKeys: new[] { _advertisedSurface!.RecordRef },
|
||||
schemaIds: new[] { EveSurfaceDocument.SchemaId })
|
||||
.GetAwaiter()
|
||||
.GetResult()
|
||||
.FirstOrDefault()
|
||||
?? throw new InvalidOperationException("The advertised Eve surface record was not available.");
|
||||
var surface = _selection.Surface.Surface.LatestAsync().GetAwaiter().GetResult();
|
||||
PublishSurface(surface);
|
||||
}
|
||||
|
||||
|
|
@ -327,25 +231,9 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
private void EnsureLiveSubscriptions()
|
||||
{
|
||||
if (_subscriptions != null) return;
|
||||
var client = _documentSession?.OpenSchemaClient()
|
||||
?? CultNetSchemaClients.CreateForEndpoint(_endpoint);
|
||||
if (_documentSession == null)
|
||||
{
|
||||
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!);
|
||||
_subscriptions.Changed += OnReplicatedDocumentChanged;
|
||||
if (_liveSubscriptionsOpen) return;
|
||||
_liveSubscriptionsOpen = true;
|
||||
_liveWatches.Add(_selection.Surface.Surface.Watch(surface => _liveDocuments.Enqueue(surface)));
|
||||
var lowered = new EveUnitySceneSurfaceLowerer()
|
||||
.Lower(CurrentSurfaceDocument.SurfaceDocument, CurrentSurfaceDocument.AdvertisedSurface);
|
||||
string entityViewPointer = lowered.PlayableWorld == null
|
||||
|
|
@ -353,31 +241,16 @@ namespace GameCult.Eve.UnityScene
|
|||
: lowered.PlayableWorld.EntityViewPointerId;
|
||||
if (!string.IsNullOrWhiteSpace(entityViewPointer))
|
||||
{
|
||||
_subscriptions.SubscribeAsync(
|
||||
"eve-unity-entity-view",
|
||||
recordKeys: new[] { entityViewPointer },
|
||||
schemaIds: new[] { EveEntitySoaViewDocument.SchemaId })
|
||||
var entities = _mesh.DocumentAsync<EveEntitySoaViewDocument>(_endpointId, entityViewPointer)
|
||||
.GetAwaiter().GetResult();
|
||||
if (_node.Database.Cache.Get(new CultRecordKey(entityViewPointer)) is EveEntitySoaViewDocument current)
|
||||
_liveDocuments.Enqueue(current);
|
||||
_liveDocuments.Enqueue(entities.LatestAsync().GetAwaiter().GetResult());
|
||||
_liveWatches.Add(entities.Watch(view => _liveDocuments.Enqueue(view)));
|
||||
}
|
||||
_subscriptions.SubscribeAsync(
|
||||
"eve-unity-surface",
|
||||
recordKeys: new[] { _advertisedSurface!.RecordRef },
|
||||
schemaIds: new[] { EveSurfaceDocument.SchemaId },
|
||||
includeSnapshot: false)
|
||||
.GetAwaiter().GetResult();
|
||||
_subscriptions.SubscribeAsync(
|
||||
"eve-unity-receipts",
|
||||
schemaIds: new[] { EveCommandReceiptDocument.SchemaId },
|
||||
includeSnapshot: false)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private void OnReplicatedDocumentChanged(CultNetReplicatedDocumentChange change)
|
||||
{
|
||||
if (change.Document is EveEntitySoaViewDocument or EveSurfaceDocument or EveCommandReceiptDocument)
|
||||
_liveDocuments.Enqueue(change.Document);
|
||||
var receipts = _mesh.CollectionAsync<EveCommandReceiptDocument>(_endpointId).GetAwaiter().GetResult();
|
||||
_liveWatches.Add(receipts.WatchChanges(change =>
|
||||
{
|
||||
if (change.Document != null) _liveDocuments.Enqueue(change.Document);
|
||||
}));
|
||||
}
|
||||
|
||||
private void RefreshAssetCatalog()
|
||||
|
|
@ -386,13 +259,8 @@ namespace GameCult.Eve.UnityScene
|
|||
if (string.IsNullOrWhiteSpace(interaction.AssetManifestRecordRef))
|
||||
return;
|
||||
|
||||
var catalog = _snapshot!
|
||||
.FetchDocumentsAsync<EveAssetCatalogDocument>(
|
||||
recordKeys: new[] { interaction.AssetManifestRecordRef },
|
||||
schemaIds: new[] { EveAssetCatalogDocument.SchemaId })
|
||||
.GetAwaiter()
|
||||
.GetResult()
|
||||
.FirstOrDefault();
|
||||
var catalog = _mesh.ReadAsync<EveAssetCatalogDocument>(_endpointId, interaction.AssetManifestRecordRef)
|
||||
.GetAwaiter().GetResult();
|
||||
if (catalog == null || catalog.Version == CurrentAssetCatalogVersion)
|
||||
return;
|
||||
|
||||
|
|
@ -415,14 +283,8 @@ namespace GameCult.Eve.UnityScene
|
|||
foreach (var group in selected.GroupBy(selection => selection.Variant!.Uri, StringComparer.Ordinal))
|
||||
{
|
||||
var variant = group.First().Variant!;
|
||||
var manifest = _snapshot!.FetchDocumentsAsync<CultMeshCdnArtifactManifest>(
|
||||
recordKeys: new[] { variant.Uri },
|
||||
schemaIds: new[] { CultMeshCdnSchemaVersions.ArtifactManifest })
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
var descriptor = manifest.FirstOrDefault();
|
||||
if (descriptor == null)
|
||||
throw new InvalidOperationException($"Provider asset bundle manifest '{variant.Uri}' was not available.");
|
||||
var descriptor = _mesh.ReadAsync<CultMeshCdnArtifactManifest>(_endpointId, variant.Uri)
|
||||
.GetAwaiter().GetResult();
|
||||
var bytes = ReadCachedBundle(descriptor, variant);
|
||||
VerifyBundle(bytes, variant);
|
||||
var bundle = AssetBundle.LoadFromMemory(bytes);
|
||||
|
|
@ -489,12 +351,10 @@ namespace GameCult.Eve.UnityScene
|
|||
for (var index = 0; index < references.Length; index += 2)
|
||||
{
|
||||
var batch = references.Skip(index).Take(2).ToArray();
|
||||
var chunks = _snapshot!
|
||||
.FetchDocumentsAsync<CultMeshCdnArtifactChunk>(
|
||||
recordKeys: batch.Select(reference => reference.RecordKey).ToArray(),
|
||||
schemaIds: new[] { CultMeshCdnSchemaVersions.ArtifactChunk })
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
var chunks = batch.Select(reference => _mesh
|
||||
.ReadAsync<CultMeshCdnArtifactChunk>(_endpointId, reference.RecordKey)
|
||||
.GetAwaiter().GetResult())
|
||||
.ToArray();
|
||||
foreach (var reference in batch)
|
||||
{
|
||||
var chunk = chunks.FirstOrDefault(candidate =>
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ namespace GameCult.Eve.UnityScene
|
|||
_transport = null;
|
||||
Selection = null;
|
||||
if (!transportOwnedConnection && _discovery?.Status == TaskStatus.RanToCompletion)
|
||||
_discovery.Result.Connection.Dispose();
|
||||
_discovery.Result.Mesh.Dispose();
|
||||
_discovery = null;
|
||||
}
|
||||
|
||||
|
|
@ -185,11 +185,8 @@ namespace GameCult.Eve.UnityScene
|
|||
: replicaPath;
|
||||
_transport = new EveUnityCultMeshLiveProviderTransport(
|
||||
resolvedReplicaPath,
|
||||
Selection.Endpoint,
|
||||
Selection.ProviderId,
|
||||
Selection.SurfaceId,
|
||||
runtimeId,
|
||||
Selection.Connection);
|
||||
Selection,
|
||||
runtimeId);
|
||||
_transport.EntityViewAvailable += view => _pendingEntityViews.Enqueue(view);
|
||||
_bridge = new EveUnitySceneLiveProviderBridge(_transport);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Mesh;
|
||||
using GameCult.Networking;
|
||||
|
||||
#nullable enable
|
||||
|
||||
|
|
@ -13,46 +10,27 @@ namespace GameCult.Eve.UnityScene
|
|||
{
|
||||
public sealed class EveUnityCultMeshProviderSelection
|
||||
{
|
||||
public EveUnityCultMeshProviderSelection(
|
||||
string endpoint,
|
||||
string verseId,
|
||||
string providerId,
|
||||
string surfaceId,
|
||||
string surfaceKind,
|
||||
EveUnityCultMeshConnectionContext connection)
|
||||
internal EveUnityCultMeshProviderSelection(
|
||||
string endpointId,
|
||||
CultMeshClient mesh,
|
||||
EveSurfaceSelection surface)
|
||||
{
|
||||
Endpoint = endpoint ?? "";
|
||||
VerseId = verseId ?? "";
|
||||
ProviderId = providerId ?? "";
|
||||
SurfaceId = surfaceId ?? "";
|
||||
SurfaceKind = surfaceKind ?? "";
|
||||
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
EndpointId = endpointId;
|
||||
Mesh = mesh;
|
||||
Surface = surface;
|
||||
}
|
||||
|
||||
public string Endpoint { get; }
|
||||
public string VerseId { get; }
|
||||
public string ProviderId { get; }
|
||||
public string SurfaceId { get; }
|
||||
public string SurfaceKind { get; }
|
||||
public EveUnityCultMeshConnectionContext Connection { get; }
|
||||
public string EndpointId { get; }
|
||||
public string VerseId => Surface.Provider.VerseId;
|
||||
public string ProviderId => Surface.Provider.ProviderId;
|
||||
public string SurfaceId => Surface.Advertisement.SurfaceId;
|
||||
public string SurfaceKind => Surface.Advertisement.SurfaceKind;
|
||||
public CultMeshClient Mesh { get; }
|
||||
public EveSurfaceSelection Surface { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnityCultMeshProviderDiscovery
|
||||
{
|
||||
private static readonly Type[] AdvertisementDocumentTypes =
|
||||
{
|
||||
typeof(EveProviderAdvertisementDocument)
|
||||
};
|
||||
|
||||
public EveUnityCultMeshProviderSelection Discover(
|
||||
string rendezvousEndpoint,
|
||||
string providerId = "",
|
||||
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 = "",
|
||||
|
|
@ -63,100 +41,33 @@ namespace GameCult.Eve.UnityScene
|
|||
{
|
||||
if (string.IsNullOrWhiteSpace(rendezvousEndpoint))
|
||||
throw new ArgumentException("Rendezvous endpoint must be non-empty.", nameof(rendezvousEndpoint));
|
||||
var endpointId = !string.IsNullOrWhiteSpace(verseId) ? verseId : providerId;
|
||||
if (string.IsNullOrWhiteSpace(endpointId))
|
||||
throw new ArgumentException("Provider or Verse identity must be supplied for CultMesh discovery.", nameof(providerId));
|
||||
|
||||
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) ||
|
||||
string.Equals(verse.VerseId, verseId, StringComparison.Ordinal))
|
||||
.SelectMany(verse => (verse.DiscoveryEndpoints ?? Array.Empty<string>())
|
||||
.Select(endpoint => new { Verse = verse, Endpoint = endpoint }))
|
||||
.Where(candidate => !string.IsNullOrWhiteSpace(candidate.Endpoint))
|
||||
.ToArray();
|
||||
if (candidates.Length == 0)
|
||||
throw new InvalidOperationException("The rendezvous endpoint advertised no compatible Verse endpoints.");
|
||||
|
||||
var failures = new List<string>();
|
||||
foreach (var candidate in candidates)
|
||||
var mesh = new CultMeshClient(new CultMeshClientOptions
|
||||
{
|
||||
EveUnityCultMeshConnectionContext? connection = null;
|
||||
try
|
||||
{
|
||||
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
|
||||
{
|
||||
Document = document,
|
||||
Surface = document.Surfaces.FirstOrDefault(surface =>
|
||||
(string.IsNullOrWhiteSpace(surfaceId) ||
|
||||
string.Equals(surface.SurfaceId, surfaceId, StringComparison.Ordinal)) &&
|
||||
(string.IsNullOrWhiteSpace(surfaceKind) ||
|
||||
string.Equals(surface.SurfaceKind, surfaceKind, StringComparison.Ordinal)))
|
||||
})
|
||||
.FirstOrDefault(match => match.Surface != null);
|
||||
if (advertisement?.Surface == null)
|
||||
continue;
|
||||
|
||||
return new EveUnityCultMeshProviderSelection(
|
||||
candidate.Endpoint,
|
||||
candidate.Verse.VerseId,
|
||||
advertisement.Document.ProviderId,
|
||||
advertisement.Surface.SurfaceId,
|
||||
advertisement.Surface.SurfaceKind,
|
||||
connection);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
connection?.Dispose();
|
||||
failures.Add($"{candidate.Endpoint}: {error.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var filter = $"provider='{providerId}', surface='{surfaceId}', kind='{surfaceKind}'";
|
||||
var detail = failures.Count == 0 ? "" : $" Endpoint failures: {string.Join(" | ", failures)}";
|
||||
throw new InvalidOperationException($"No advertised Eve surface matched {filter}.{detail}");
|
||||
}
|
||||
|
||||
private static async Task<EveProviderAdvertisementDocument[]> FetchAdvertisementsAsync(
|
||||
EveUnityCultMeshConnectionContext connection,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(AdvertisementDocumentTypes);
|
||||
var networkRegistry = CultMesh.CreateCultNetDocumentRegistry(AdvertisementDocumentTypes, cacheRegistry);
|
||||
using var snapshot = await CultMeshSnapshotSession.ConnectAsync(
|
||||
connection.Sessions,
|
||||
connection.EndpointId,
|
||||
new CultMeshSnapshotEndpointOptions
|
||||
{
|
||||
Context = CultMesh.Verse("eve.discovery", "eve-unity").Context,
|
||||
DocumentRegistry = networkRegistry,
|
||||
Request = new CultMeshSnapshotRequestOptions
|
||||
RendezvousEndpoints = new[] { rendezvousEndpoint }
|
||||
});
|
||||
try
|
||||
{
|
||||
var surface = await mesh.SurfaceAsync(
|
||||
endpointId,
|
||||
new EveSurfaceRequest
|
||||
{
|
||||
ShardId = "provider",
|
||||
ShardEpoch = 1,
|
||||
ConnectTimeout = TimeSpan.FromSeconds(5),
|
||||
ResponseTimeout = TimeSpan.FromSeconds(10),
|
||||
MessageIdPrefix = "eve-unity-discovery",
|
||||
RudpRuntimeId = "eve-unity.discovery",
|
||||
RudpMaxFragmentBytes = 1024
|
||||
}
|
||||
}.Request,
|
||||
networkRegistry,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return (await snapshot.FetchDocumentsAsync<EveProviderAdvertisementDocument>()
|
||||
.ConfigureAwait(false)).ToArray();
|
||||
ProviderId = providerId,
|
||||
SurfaceId = surfaceId,
|
||||
SurfaceKind = surfaceKind,
|
||||
LoweringTarget = "unity-scene"
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return new EveUnityCultMeshProviderSelection(endpointId, mesh, surface);
|
||||
}
|
||||
catch
|
||||
{
|
||||
mesh.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue