Carry CultMesh Verse and provider identity separately

This commit is contained in:
Meta Vens 2026-08-18 07:27:13 +02:00
commit f67632233a
7 changed files with 87 additions and 65 deletions

View file

@ -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<CultMeshRealtimeFrame> 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);
}

View file

@ -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<CultMeshRealtimeFrame> 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);
}

View file

@ -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<EveProviderAdvertisementDocument>(_endpointId)
.LeaseCollectionAsync<EveProviderAdvertisementDocument>(_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<EveSurfaceDocument>(_endpointId, _advertisedSurface!.RecordRef)
.ReadAsync<EveSurfaceDocument>(_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<TDocument>(_endpointId, recordKey)
var lease = _meshClient!.LeaseDocumentAsync<TDocument>(_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<TDocument>(_endpointId, includeInitialSnapshot)
var lease = _meshClient!.LeaseCollectionAsync<TDocument>(_target, includeInitialSnapshot)
.GetAwaiter().GetResult();
_documentLeases.Add(lease);
if (includeInitialSnapshot)
@ -1068,7 +1068,7 @@ namespace GameCult.Eve.UnityScene
return;
var catalog = _meshClient!
.ReadAsync<EveAssetCatalogDocument>(_endpointId, interaction.AssetManifestRecordRef)
.ReadAsync<EveAssetCatalogDocument>(_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<CultMeshCdnArtifactManifest>(_endpointId, uri)
var descriptor = _meshClient!.ReadAsync<CultMeshCdnArtifactManifest>(_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);

View file

@ -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);

View file

@ -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<string>())
.Where(id => !string.IsNullOrWhiteSpace(id))
.Distinct(StringComparer.Ordinal)
.OrderBy(id => id, StringComparer.Ordinal))
{
using var advertisementsLease = await mesh
.LeaseCollectionAsync<EveProviderAdvertisementDocument>(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<EveProviderAdvertisementDocument>(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}");
}
}
}

View file

@ -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",

View file

@ -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;