Preserve navigation authority through Unity discovery

This commit is contained in:
Meta Vens 2026-08-19 03:31:16 +02:00
commit 781e612c23
6 changed files with 47 additions and 10 deletions

View file

@ -1601,7 +1601,8 @@ namespace GameCult.Eve.UnityScene
receipt.Navigation.ProviderId,
receipt.Navigation.SurfaceId,
receipt.Navigation.SurfaceKind,
receipt.Navigation.RendezvousEndpoints)));
receipt.Navigation.RendezvousEndpoints,
receipt.Navigation.AuthorityRuntimeId)));
}
private sealed class ReceiptSubscription : IDisposable

View file

@ -170,6 +170,7 @@ namespace GameCult.Eve.UnityScene
target.SurfaceId,
requiredSurfaceKind,
target.VerseId,
target.AuthorityRuntimeId,
_navigationAuthorityTrust,
CancellationToken.None).ConfigureAwait(false);
@ -352,6 +353,7 @@ namespace GameCult.Eve.UnityScene
surfaceFilter,
surfaceKind,
verseFilter,
"",
_authorityTrust,
cancellationToken).ConfigureAwait(false);
TraceStartup($"discovery and transport preparation {elapsed.Elapsed.TotalMilliseconds:0.###}ms");
@ -364,6 +366,7 @@ namespace GameCult.Eve.UnityScene
string requiredSurfaceId,
string requiredSurfaceKind,
string requiredVerseId,
string requiredAuthorityRuntimeId,
CultMeshAuthorityTrustPolicy trust,
CancellationToken cancellationToken)
{
@ -379,7 +382,8 @@ namespace GameCult.Eve.UnityScene
requiredSurfaceId,
requiredSurfaceKind,
requiredVerseId,
cancellationToken).ConfigureAwait(false);
cancellationToken,
requiredAuthorityRuntimeId).ConfigureAwait(false);
var resolvedCachePath = string.IsNullOrWhiteSpace(cacheDirectory)
? Path.Combine(Application.temporaryCachePath, $"eve-unity-{GetInstanceID()}")
: cacheDirectory;

View file

@ -53,7 +53,8 @@ namespace GameCult.Eve.UnityScene
string surfaceId = "",
string surfaceKind = "interactive-world",
string verseId = "",
CancellationToken cancellationToken = default)
CancellationToken cancellationToken = default,
string requiredAuthorityRuntimeId = "")
{
if (string.IsNullOrWhiteSpace(rendezvousEndpoint))
throw new ArgumentException("Rendezvous endpoint must be non-empty.", nameof(rendezvousEndpoint));
@ -101,10 +102,9 @@ namespace GameCult.Eve.UnityScene
});
foreach (var candidate in candidates)
{
foreach (var authorityRuntimeId in (candidate.AuthorityRuntimeIds ?? Array.Empty<string>())
.Where(id => !string.IsNullOrWhiteSpace(id))
.Distinct(StringComparer.Ordinal)
.OrderBy(id => id, StringComparer.Ordinal))
foreach (var authorityRuntimeId in EligibleAuthorityRuntimeIds(
candidate.AuthorityRuntimeIds,
requiredAuthorityRuntimeId))
{
try
{
@ -146,11 +146,22 @@ namespace GameCult.Eve.UnityScene
}
}
var filter = $"provider='{providerId}', surface='{surfaceId}', kind='{surfaceKind}'";
var filter = $"provider='{providerId}', surface='{surfaceId}', kind='{surfaceKind}', authority='{requiredAuthorityRuntimeId}'";
var detail = failures.Count == 0 ? "" : $" Endpoint failures: {string.Join(" | ", failures)}";
var observedDetail = observed.Count == 0 ? "" : $" Observed: {string.Join(" | ", observed)}";
throw new InvalidOperationException($"No advertised Eve surface matched {filter}.{detail}{observedDetail}");
}
internal static IReadOnlyList<string> EligibleAuthorityRuntimeIds(
IEnumerable<string>? authorityRuntimeIds,
string requiredAuthorityRuntimeId) =>
(authorityRuntimeIds ?? Array.Empty<string>())
.Where(id => !string.IsNullOrWhiteSpace(id))
.Where(id => string.IsNullOrWhiteSpace(requiredAuthorityRuntimeId) ||
string.Equals(id, requiredAuthorityRuntimeId, StringComparison.Ordinal))
.Distinct(StringComparer.Ordinal)
.OrderBy(id => id, StringComparer.Ordinal)
.ToArray();
}
}

View file

@ -253,9 +253,11 @@ namespace GameCult.Eve.UnityScene
string providerId,
string surfaceId,
string surfaceKind,
IReadOnlyList<string>? rendezvousEndpoints = null)
IReadOnlyList<string>? rendezvousEndpoints = null,
string authorityRuntimeId = "")
{
VerseId = verseId ?? "";
AuthorityRuntimeId = authorityRuntimeId ?? "";
ProviderId = providerId ?? "";
SurfaceId = surfaceId ?? "";
SurfaceKind = surfaceKind ?? "";
@ -263,6 +265,7 @@ namespace GameCult.Eve.UnityScene
}
public string VerseId { get; }
public string AuthorityRuntimeId { get; }
public string ProviderId { get; }
public string SurfaceId { get; }
public string SurfaceKind { get; }

View file

@ -168,6 +168,24 @@ namespace GameCult.Eve.UnityScene.Tests
Assert.That(typeof(EveUnityCultMeshProviderSelection).GetProperty("Endpoint"), Is.Null);
}
[Test]
public void NavigationAuthorityCannotBeReplacedByAlphabeticallyEarlierPeer()
{
var target = new EveUnitySceneNavigationTarget(
"gamecult.aetheria",
"aetheria.daemon",
"aetheria.pilot",
"interactive-world",
new[] { "cultnet+tcp://odin.example:3075" },
"z-owner");
var eligible = EveUnityCultMeshProviderDiscovery.EligibleAuthorityRuntimeIds(
new[] { "a-decoy", "z-owner" },
target.AuthorityRuntimeId);
Assert.That(target.AuthorityRuntimeId, Is.EqualTo("z-owner"));
CollectionAssert.AreEqual(new[] { "z-owner" }, eligible);
}
private static EveSurfaceCommandRequest Request(string id, string command) => new EveSurfaceCommandRequest(
"test.provider",
"test.surface",

View file

@ -1,7 +1,7 @@
{
"name": "org.gamecult.eve.unity-scene",
"displayName": "Eve Unity Scene Lowering",
"version": "0.3.119",
"version": "0.3.120",
"unity": "6000.0",
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
"dependencies": {