diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 0bfa008..8d8cef4 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -310,7 +310,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(provider.Selection.SurfaceId, Is.Not.Empty); var quicProof = ReceiveAdvertisedRealtimeFrameAsync( rendezvousEndpoint, - provider.Selection.VerseId); + provider.Selection.VerseId, + provider.Selection.AuthorityRuntimeId); var quicDeadline = Time.realtimeSinceStartup + 15f; while (!quicProof.IsCompleted && Time.realtimeSinceStartup < quicDeadline) yield return new WaitForSecondsRealtime(0.05f); @@ -1200,7 +1201,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private static async Task ReceiveAdvertisedRealtimeFrameAsync( string rendezvousEndpoint, - string verseId) + string verseId, + string authorityRuntimeId) { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15)); using var client = new CultMeshClient(new CultMeshClientOptions @@ -1211,7 +1213,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests new CultMeshNativeQuicRealtimeTransportConnector() } }); - using var session = await client.ConnectRealtimeAsync(verseId, timeout.Token); + using var session = await client.ConnectRealtimeAsync( + new CultMeshSessionTarget(verseId, authorityRuntimeId), + timeout.Token); return await session.ReceiveAsync(timeout.Token); } diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 0bfa008..8d8cef4 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -310,7 +310,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(provider.Selection.SurfaceId, Is.Not.Empty); var quicProof = ReceiveAdvertisedRealtimeFrameAsync( rendezvousEndpoint, - provider.Selection.VerseId); + provider.Selection.VerseId, + provider.Selection.AuthorityRuntimeId); var quicDeadline = Time.realtimeSinceStartup + 15f; while (!quicProof.IsCompleted && Time.realtimeSinceStartup < quicDeadline) yield return new WaitForSecondsRealtime(0.05f); @@ -1200,7 +1201,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private static async Task ReceiveAdvertisedRealtimeFrameAsync( string rendezvousEndpoint, - string verseId) + string verseId, + string authorityRuntimeId) { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15)); using var client = new CultMeshClient(new CultMeshClientOptions @@ -1211,7 +1213,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests new CultMeshNativeQuicRealtimeTransportConnector() } }); - using var session = await client.ConnectRealtimeAsync(verseId, timeout.Token); + using var session = await client.ConnectRealtimeAsync( + new CultMeshSessionTarget(verseId, authorityRuntimeId), + timeout.Token); return await session.ReceiveAsync(timeout.Token); } 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 06a3175..26c22db 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -26,7 +26,7 @@ namespace GameCult.Eve.UnityScene { private readonly string _cachePath; private readonly string _rendezvousEndpoint; - private readonly string _endpointId; + private readonly CultMeshSessionTarget _target; private readonly string _providerId; private readonly string _surfaceId; private readonly string _runtimeId; @@ -83,7 +83,8 @@ namespace GameCult.Eve.UnityScene public EveUnityCultMeshLiveProviderTransport( string cachePath, string rendezvousEndpoint, - string endpointId, + string verseId, + string authorityRuntimeId, string providerId, string surfaceId, string runtimeId = "eve-unity", @@ -95,9 +96,7 @@ namespace GameCult.Eve.UnityScene _rendezvousEndpoint = string.IsNullOrWhiteSpace(rendezvousEndpoint) ? throw new ArgumentException("CultMesh rendezvous endpoint must be non-empty.", nameof(rendezvousEndpoint)) : rendezvousEndpoint.Trim(); - _endpointId = string.IsNullOrWhiteSpace(endpointId) - ? throw new ArgumentException("CultMesh stable endpoint identity must be non-empty.", nameof(endpointId)) - : endpointId.Trim(); + _target = new CultMeshSessionTarget(verseId, authorityRuntimeId); _providerId = string.IsNullOrWhiteSpace(providerId) ? throw new ArgumentException("Provider id must be non-empty.", nameof(providerId)) : providerId.Trim(); @@ -189,7 +188,7 @@ namespace GameCult.Eve.UnityScene catch (Exception error) when (error is IOException || error is SocketException || error is TimeoutException) { throw new InvalidOperationException( - $"CultMesh provider '{_providerId}' bootstrap through stable endpoint '{_endpointId}' failed.", + $"CultMesh provider '{_providerId}' bootstrap through target '{_target}' failed.", error); } } @@ -361,7 +360,7 @@ namespace GameCult.Eve.UnityScene { var recordKey = ChildRecordKey(interaction.CommandRecordRef, commandId); _meshClient!.SubmitDocumentAsync( - _endpointId, + _target, recordKey, request, _runtimeId, @@ -466,7 +465,7 @@ namespace GameCult.Eve.UnityScene if (!forceRefresh && _advertisement != null && _advertisedSurface != null) return; using var advertisements = _meshClient! - .LeaseCollectionAsync(_endpointId) + .LeaseCollectionAsync(_target) .GetAwaiter() .GetResult(); _advertisement = advertisements.Handle.LatestAsync().GetAwaiter().GetResult().FirstOrDefault(document => @@ -490,7 +489,7 @@ namespace GameCult.Eve.UnityScene var mappedRoot = _cachePath; var networkBodies = _meshClient!.BodyProvider( _advertisement.ProviderId, - _advertisement.ServiceId, + _target, new CultMeshSessionBodyProviderOptions { ResponseTimeout = TimeSpan.FromSeconds(2) }); return new CultMeshBodyPublicationResolver(new CultMeshBodyTransportService( new ICultMeshBodyTransportAdapter[] @@ -604,19 +603,20 @@ namespace GameCult.Eve.UnityScene throw new InvalidOperationException( $"Eve provider '{_providerId}' does not advertise the Verse identity required for realtime state."); _realtimeLifetime = new CancellationTokenSource(); - _realtimePump = Task.Run(() => PumpRealtimeEntityStateAsync( - _advertisement.VerseId, - _realtimeLifetime.Token)); + if (!string.Equals(_advertisement.VerseId, _target.VerseId, StringComparison.Ordinal)) + throw new InvalidOperationException( + $"Eve provider '{_providerId}' advertisement Verse '{_advertisement.VerseId}' does not match selected Verse '{_target.VerseId}'."); + _realtimePump = Task.Run(() => PumpRealtimeEntityStateAsync(_realtimeLifetime.Token)); } - private async Task PumpRealtimeEntityStateAsync(string verseId, CancellationToken cancellationToken) + private async Task PumpRealtimeEntityStateAsync(CancellationToken cancellationToken) { try { _realtimeSession = await _meshClient! - .ConnectRealtimeAsync(verseId, cancellationToken) + .ConnectRealtimeAsync(_target, cancellationToken) .ConfigureAwait(false); - TraceHotState($"realtime state connected transport={_realtimeSession.TransportId} verse={verseId}"); + TraceHotState($"realtime state connected transport={_realtimeSession.TransportId} target={_target}"); while (!cancellationToken.IsCancellationRequested) { var frame = await _realtimeSession.ReceiveAsync(cancellationToken).ConfigureAwait(false); @@ -823,7 +823,7 @@ namespace GameCult.Eve.UnityScene private void RefreshSurface() { var surface = _meshClient! - .ReadAsync(_endpointId, _advertisedSurface!.RecordRef) + .ReadAsync(_target, _advertisedSurface!.RecordRef) .GetAwaiter() .GetResult(); PublishBaseSurface(surface); @@ -1010,7 +1010,7 @@ namespace GameCult.Eve.UnityScene bool publishInitial = true) where TDocument : class { - var lease = _meshClient!.LeaseDocumentAsync(_endpointId, recordKey) + var lease = _meshClient!.LeaseDocumentAsync(_target, recordKey) .GetAwaiter().GetResult(); _documentLeases.Add(lease); if (publishInitial) publish(lease.Handle.Latest()); @@ -1022,7 +1022,7 @@ namespace GameCult.Eve.UnityScene bool includeInitialSnapshot = true) where TDocument : class { - var lease = _meshClient!.LeaseCollectionAsync(_endpointId, includeInitialSnapshot) + var lease = _meshClient!.LeaseCollectionAsync(_target, includeInitialSnapshot) .GetAwaiter().GetResult(); _documentLeases.Add(lease); if (includeInitialSnapshot) @@ -1068,7 +1068,7 @@ namespace GameCult.Eve.UnityScene return; var catalog = _meshClient! - .ReadAsync(_endpointId, interaction.AssetManifestRecordRef) + .ReadAsync(_target, interaction.AssetManifestRecordRef) .GetAwaiter() .GetResult(); PublishAssetCatalog(catalog); @@ -1157,7 +1157,7 @@ namespace GameCult.Eve.UnityScene EnsureBundleLoaded(dependency); var groupElapsed = Stopwatch.StartNew(); - var descriptor = _meshClient!.ReadAsync(_endpointId, uri) + var descriptor = _meshClient!.ReadAsync(_target, uri) .GetAwaiter() .GetResult(); TraceStartup("asset-manifest", groupElapsed); @@ -1280,7 +1280,7 @@ namespace GameCult.Eve.UnityScene { _meshClient!.ContentProvider( _advertisement.ProviderId, - _advertisement.ServiceId) + _target) }, new CultMeshContentTransferOptions(cacheRoot), _assetBodyMappings); 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 3064459..1e71df7 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -250,7 +250,8 @@ namespace GameCult.Eve.UnityScene _transport = new EveUnityCultMeshLiveProviderTransport( resolvedCachePath, Selection.RendezvousEndpoint, - Selection.EndpointId, + Selection.VerseId, + Selection.AuthorityRuntimeId, Selection.ProviderId, Selection.SurfaceId, runtimeId); 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 62e6837..74d3040 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs @@ -14,23 +14,23 @@ namespace GameCult.Eve.UnityScene { public EveUnityCultMeshProviderSelection( string rendezvousEndpoint, - string endpointId, string verseId, + string authorityRuntimeId, string providerId, string surfaceId, string surfaceKind) { RendezvousEndpoint = rendezvousEndpoint ?? ""; - EndpointId = endpointId ?? ""; VerseId = verseId ?? ""; + AuthorityRuntimeId = authorityRuntimeId ?? ""; ProviderId = providerId ?? ""; SurfaceId = surfaceId ?? ""; SurfaceKind = surfaceKind ?? ""; } public string RendezvousEndpoint { get; } - public string EndpointId { get; } public string VerseId { get; } + public string AuthorityRuntimeId { get; } public string ProviderId { get; } public string SurfaceId { get; } public string SurfaceKind { get; } @@ -95,41 +95,48 @@ namespace GameCult.Eve.UnityScene }); foreach (var candidate in candidates) { - try + foreach (var authorityRuntimeId in (candidate.AuthorityRuntimeIds ?? Array.Empty()) + .Where(id => !string.IsNullOrWhiteSpace(id)) + .Distinct(StringComparer.Ordinal) + .OrderBy(id => id, StringComparer.Ordinal)) { - using var advertisementsLease = await mesh - .LeaseCollectionAsync(candidate.VerseId) - .ConfigureAwait(false); - var advertisements = await advertisementsLease.Handle.LatestAsync().ConfigureAwait(false); - observed.AddRange(advertisements.Select(document => - $"{candidate.VerseId}: {document.ProviderId}[{string.Join(",", document.Surfaces.Select(surface => $"{surface.SurfaceId}:{surface.SurfaceKind}"))}]")); - var advertisement = advertisements - .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; + try + { + var target = new CultMeshSessionTarget(candidate.VerseId, authorityRuntimeId); + using var advertisementsLease = await mesh + .LeaseCollectionAsync(target) + .ConfigureAwait(false); + var advertisements = await advertisementsLease.Handle.LatestAsync().ConfigureAwait(false); + observed.AddRange(advertisements.Select(document => + $"{target}: {document.ProviderId}[{string.Join(",", document.Surfaces.Select(surface => $"{surface.SurfaceId}:{surface.SurfaceKind}"))}]")); + var advertisement = advertisements + .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( - rendezvousEndpoint, - candidate.VerseId, - candidate.VerseId, - advertisement.Document.ProviderId, - advertisement.Surface.SurfaceId, - advertisement.Surface.SurfaceKind); - } - catch (Exception error) - { - failures.Add($"{candidate.VerseId}: {error.Message}"); + return new EveUnityCultMeshProviderSelection( + rendezvousEndpoint, + candidate.VerseId, + authorityRuntimeId, + advertisement.Document.ProviderId, + advertisement.Surface.SurfaceId, + advertisement.Surface.SurfaceKind); + } + catch (Exception error) + { + failures.Add($"{candidate.VerseId}/{authorityRuntimeId}: {error.Message}"); + } } } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs index f45bc97..5f1d489 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs @@ -26,8 +26,8 @@ namespace GameCult.Eve.UnityScene.Tests "interactive-world"); Assert.That(selection.RendezvousEndpoint, Is.EqualTo("cultnet+tcp://odin.example:3075")); - Assert.That(selection.EndpointId, Is.EqualTo("aetheria.public")); Assert.That(selection.VerseId, Is.EqualTo("aetheria.public")); + Assert.That(selection.AuthorityRuntimeId, Is.EqualTo("aetheria.public")); Assert.That(typeof(EveUnityCultMeshProviderSelection).GetProperty("Endpoint"), Is.Null); } @@ -180,6 +180,7 @@ namespace GameCult.Eve.UnityScene.Tests "cultnet+tcp://127.0.0.1:1", "provider", "provider", + "provider", "surface"); var variant = new EveAssetVariant( "unity-scene", diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index ffbda32..f6312b8 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -32,6 +32,7 @@ namespace GameCult.Eve.UnityScene.Tests "cultnet://127.0.0.1:3075", "aetheria", "aetheria", + "aetheria", "aetheria.pilot"); }); } @@ -67,6 +68,7 @@ namespace GameCult.Eve.UnityScene.Tests "cultnet://127.0.0.1:3075", "aetheria", "aetheria", + "aetheria", "aetheria.pilot"); var view = EntityLeaseDocument(); var publication = BodyPublication(view, view.Sequence + 1); @@ -102,6 +104,7 @@ namespace GameCult.Eve.UnityScene.Tests "cultnet://127.0.0.1:3075", "aetheria", "aetheria", + "aetheria", "aetheria.pilot"); var view = EntityLeaseDocument(); var publication = BodyPublication(view, view.Sequence); @@ -131,6 +134,7 @@ namespace GameCult.Eve.UnityScene.Tests "cultnet://127.0.0.1:3075", "provider", "provider", + "provider", "provider.pilot"); var enqueue = typeof(EveUnityCultMeshLiveProviderTransport) .GetMethod("EnqueueRealtimeEntityFrame", BindingFlags.NonPublic | BindingFlags.Instance)!; @@ -161,6 +165,7 @@ namespace GameCult.Eve.UnityScene.Tests "cultnet://127.0.0.1:3075", "provider", "provider", + "provider", "provider.pilot"); var view = EntityLeaseDocument(); view.Sequence = 0;