diff --git a/packages/org.gamecult.eve.surface/Runtime/EveCultMeshSurfaceSelection.cs b/packages/org.gamecult.eve.surface/Runtime/EveCultMeshSurfaceSelection.cs new file mode 100644 index 0000000..f4625d2 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveCultMeshSurfaceSelection.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GameCult.Mesh; + +#nullable enable + +namespace GameCult.Eve.Surface +{ + public sealed class EveSurfaceRequest + { + public string ProviderId { get; set; } = ""; + public string SurfaceId { get; set; } = ""; + public string SurfaceKind { get; set; } = ""; + public string LoweringTarget { get; set; } = ""; + public bool RequireActive { get; set; } = true; + } + + public sealed class EveSurfaceSelection + { + internal EveSurfaceSelection( + EveProviderAdvertisementDocument provider, + EveAdvertisedSurface advertisement, + CultMeshDocumentHandle surface) + { + Provider = provider; + Advertisement = advertisement; + Surface = surface; + } + + public EveProviderAdvertisementDocument Provider { get; } + public EveAdvertisedSurface Advertisement { get; } + public CultMeshDocumentHandle Surface { get; } + } + + public static class EveCultMeshSurfaceSelection + { + public static async Task SurfaceAsync( + this CultMeshClient mesh, + string endpointId, + EveSurfaceRequest? request = null, + CancellationToken cancellationToken = default) + { + if (mesh == null) throw new ArgumentNullException(nameof(mesh)); + var resolved = request ?? new EveSurfaceRequest(); + var providers = await mesh.CollectionAsync(endpointId, cancellationToken) + .ConfigureAwait(false); + var match = Select(await providers.LatestAsync().ConfigureAwait(false), resolved); + var surface = await mesh.DocumentAsync( + endpointId, + match.Surface.RecordRef, + cancellationToken) + .ConfigureAwait(false); + return new EveSurfaceSelection(match.Provider, match.Surface, surface); + } + + public static (EveProviderAdvertisementDocument Provider, EveAdvertisedSurface Surface) Select( + IEnumerable providers, + EveSurfaceRequest request) + { + if (providers == null) throw new ArgumentNullException(nameof(providers)); + if (request == null) throw new ArgumentNullException(nameof(request)); + var match = providers + .Where(provider => Matches(request.ProviderId, provider.ProviderId)) + .SelectMany(provider => provider.Surfaces.Select(surface => new { Provider = provider, Surface = surface })) + .Where(candidate => Matches(request.SurfaceId, candidate.Surface.SurfaceId)) + .Where(candidate => Matches(request.SurfaceKind, candidate.Surface.SurfaceKind)) + .Where(candidate => !request.RequireActive || string.Equals(candidate.Surface.Status, "active", StringComparison.OrdinalIgnoreCase)) + .Where(candidate => string.IsNullOrWhiteSpace(request.LoweringTarget) || + candidate.Surface.WorldInteraction?.LoweringTargets.Any(target => + string.Equals(target, request.LoweringTarget, StringComparison.Ordinal)) == true) + .OrderBy(candidate => candidate.Provider.ProviderId, StringComparer.Ordinal) + .ThenBy(candidate => candidate.Surface.SurfaceId, StringComparer.Ordinal) + .FirstOrDefault(); + if (match == null) + throw new InvalidOperationException( + $"No advertised Eve surface matched provider='{request.ProviderId}', surface='{request.SurfaceId}', " + + $"kind='{request.SurfaceKind}', loweringTarget='{request.LoweringTarget}'."); + if (!string.Equals(match.Surface.Transport, "cultmesh-record", StringComparison.Ordinal)) + throw new InvalidOperationException( + $"Eve surface '{match.Surface.SurfaceId}' uses unsupported transport '{match.Surface.Transport}'."); + if (string.IsNullOrWhiteSpace(match.Surface.RecordRef)) + throw new InvalidOperationException($"Eve surface '{match.Surface.SurfaceId}' does not advertise a record reference."); + return (match.Provider, match.Surface); + } + + private static bool Matches(string expected, string actual) => + string.IsNullOrWhiteSpace(expected) || string.Equals(expected, actual, StringComparison.Ordinal); + } +} diff --git a/packages/org.gamecult.eve.surface/Runtime/EveCultMeshSurfaceSelection.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveCultMeshSurfaceSelection.cs.meta new file mode 100644 index 0000000..5385db9 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveCultMeshSurfaceSelection.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b3c40df8a2a14d7db2f45f7018591fd3 diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveCultMeshSurfaceSelectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveCultMeshSurfaceSelectionTests.cs new file mode 100644 index 0000000..280dd24 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveCultMeshSurfaceSelectionTests.cs @@ -0,0 +1,79 @@ +using System; +using GameCult.Eve.Surface; +using NUnit.Framework; + +namespace GameCult.Eve.UnityScene.Tests +{ + public sealed class EveCultMeshSurfaceSelectionTests + { + [Test] + public void Select_MatchesDeveloperIntentWithoutPhysicalTopology() + { + var provider = Provider( + new EveAdvertisedSurface( + "aetheria.pilot", + EveSurfaceDocument.SchemaId, + "eve:surface:aetheria.pilot", + "cultmesh-record", + "active", + "interactive-world", + new EveWorldInteractionAdvertisement( + "world3d", + Array.Empty(), + "provider-command", + "eve:commands", + EveCommandReceiptDocument.SchemaId, + "eve:receipts", + "eve:assets", + new[] { "unity-scene", "web" }, + "provider"))); + + var selected = EveCultMeshSurfaceSelection.Select( + new[] { provider }, + new EveSurfaceRequest + { + ProviderId = "aetheria.daemon", + SurfaceKind = "interactive-world", + LoweringTarget = "unity-scene" + }); + + Assert.That(selected.Provider, Is.SameAs(provider)); + Assert.That(selected.Surface.SurfaceId, Is.EqualTo("aetheria.pilot")); + Assert.That(selected.Surface.RecordRef, Is.EqualTo("eve:surface:aetheria.pilot")); + } + + [Test] + public void Select_RejectsUnsupportedLoweringTarget() + { + var provider = Provider(new EveAdvertisedSurface( + "aetheria.pilot", + EveSurfaceDocument.SchemaId, + "eve:surface:aetheria.pilot", + "cultmesh-record", + "active", + "interactive-world", + new EveWorldInteractionAdvertisement( + "world3d", Array.Empty(), "", "", "", "", "", + new[] { "unity-scene" }, "provider"))); + + Assert.Throws(() => EveCultMeshSurfaceSelection.Select( + new[] { provider }, + new EveSurfaceRequest { LoweringTarget = "flutter" })); + } + + private static EveProviderAdvertisementDocument Provider(EveAdvertisedSurface surface) => + new EveProviderAdvertisementDocument( + "aetheria.daemon", + "aetheria", + "aetheria", + "Aetheria", + "game", + "aetheria.daemon", + "2026-07-12T00:00:00Z", + new EveProviderFreshness("fresh", "2026-07-12T00:00:00Z", 5000), + Array.Empty(), + Array.Empty(), + new[] { surface }, + Array.Empty()); + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveCultMeshSurfaceSelectionTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveCultMeshSurfaceSelectionTests.cs.meta new file mode 100644 index 0000000..c22c711 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveCultMeshSurfaceSelectionTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c578fb24db4b41fc88ea08bde4c42dd4