From e0acc8a1b5b8a84523cdafec52664edaa73c3b94 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 28 Jun 2026 01:37:43 +0100 Subject: [PATCH] Route viewport projections through managed documents --- Aetheria.State.Verify/Program.cs | 16 ++- .../AetheriaUnityRenderSplatViewportSource.cs | 5 +- .../Tests/DaemonRuntimeDocumentTests.cs | 31 ++++++ Assets/Scripts/UI/Menu/InventoryMenu.cs | 5 +- Assets/Scripts/UI/Menu/InventoryPanel.cs | 5 +- Assets/Scripts/UI/Menu/MapRenderer.cs | 10 +- Assets/Scripts/UI/Menu/SectorRenderer.cs | 5 +- Assets/Scripts/Zone Display/ZoneRenderer.cs | 5 +- .../Runtime/AetheriaClient.cs | 24 ++--- .../Runtime/AetheriaClientState.cs | 98 +++++++++++++++++++ .../Runtime/AetheriaRuntimeVerseClient.cs | 73 ++++++++++++++ 11 files changed, 251 insertions(+), 26 deletions(-) diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 7af722c3f..2e32decb2 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -2395,7 +2395,8 @@ static void RequireDaemonRenderQueryAuthority(string root) "LoadAsteroidBelt(beltPose)", "void LoadAsteroidBelt(AetheriaRuntimeZoneRenderAsteroidBeltPose beltPose)", "private void SyncDaemonEntityInstances()", - "ObjectsViewportAsync(ToViewportBounds(viewport))", + ".Viewports", + ".Objects(ToViewportBounds(viewport))", "foreach (var entity in objects?.Objects ?? Array.Empty())", "_observedEntityFacadesByDaemonIndex.TryGetValue(entityIndex, out var entity)", "Loading entity {entity.Name} from daemon presentation query", @@ -4482,7 +4483,8 @@ static void RequireSectorMapZoneDetailsUseEveSurface(string root) ".Aetheria()", ".SectorMap", ".LatestAsync()", - "ZoneDetailsAsync(zoneIndex)", + ".Details", + ".Zone(zoneIndex)", "OpenRuntimeCatalog()", "PlayerSettingsAsync()", "AetheriaClient", @@ -11406,13 +11408,16 @@ static void RequireUnityObserverDoesNotTickLocalSimulation(string root) } if (!mapRenderer.Contains("private AetheriaClient _client", StringComparison.Ordinal) || - !mapRenderer.Contains("ObjectsViewportAsync", StringComparison.Ordinal) || + !mapRenderer.Contains(".Viewports", StringComparison.Ordinal) || + !mapRenderer.Contains(".Objects(viewport)", StringComparison.Ordinal) || + !mapRenderer.Contains(".RenderSplats(viewport)", StringComparison.Ordinal) || !mapRenderer.Contains("PlayerSettingsAsync()", StringComparison.Ordinal) || !sectorRenderer.Contains("private AetheriaClient _client", StringComparison.Ordinal) || !sectorRenderer.Contains(".Aetheria()", StringComparison.Ordinal) || !sectorRenderer.Contains(".SectorMap", StringComparison.Ordinal) || !sectorRenderer.Contains(".LatestAsync()", StringComparison.Ordinal) || - !sectorRenderer.Contains("ZoneDetailsAsync(zoneIndex)", StringComparison.Ordinal) || + !sectorRenderer.Contains(".Details", StringComparison.Ordinal) || + !sectorRenderer.Contains(".Zone(zoneIndex)", StringComparison.Ordinal) || !sectorRenderer.Contains("OpenRuntimeCatalog()", StringComparison.Ordinal) || !sectorRenderer.Contains("PlayerSettingsAsync()", StringComparison.Ordinal) || !sectorMap.Contains("private AetheriaClient _client", StringComparison.Ordinal) || @@ -13741,7 +13746,8 @@ static void RequireInventoryDoubleClickTransferRequestAuthority(string root) "TryResolveTypedInventoryRows(", "TryValidateTypedCargoSlot(", "TryValidateTypedEquipmentSlot(", - "InventoryAsync(entityIndex)", + ".Details", + ".Inventory(entityIndex)", "origin.Cargo.TryGetValue(item, out var originPosition)", "SourceIndex == cargoIndex", "SourceIndex == equipmentIndex", diff --git a/Assets/Scripts/Gameplay/AetheriaUnityRenderSplatViewportSource.cs b/Assets/Scripts/Gameplay/AetheriaUnityRenderSplatViewportSource.cs index 90892d56b..4d5c0cc77 100644 --- a/Assets/Scripts/Gameplay/AetheriaUnityRenderSplatViewportSource.cs +++ b/Assets/Scripts/Gameplay/AetheriaUnityRenderSplatViewportSource.cs @@ -108,7 +108,10 @@ public sealed class AetheriaUnityRenderSplatViewportSource : MonoBehaviour try { _document = ResolveClient() - .RenderSplatsViewportAsync(ResolveViewportBounds()) + .Aetheria() + .Viewports + .RenderSplats(ResolveViewportBounds()) + .LatestAsync() .GetAwaiter() .GetResult(); } diff --git a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs index 34c43d977..20387463d 100644 --- a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs +++ b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs @@ -176,6 +176,31 @@ public class DaemonRuntimeDocumentTests .LatestAsync() .GetAwaiter() .GetResult(); + var viewport = new AetheriaRuntimeRtsViewportBounds + { + MinX = -100, + MinY = -100, + MaxX = 100, + MaxY = 100 + }; + var objectsViewport = client.Aetheria() + .Viewports + .Objects(viewport) + .LatestAsync() + .GetAwaiter() + .GetResult(); + var zoneDetails = client.Aetheria() + .Details + .Zone(0) + .LatestAsync() + .GetAwaiter() + .GetResult(); + var inventory = client.Aetheria() + .Details + .Inventory(0) + .LatestAsync() + .GetAwaiter() + .GetResult(); var currentEntityFromClientType = client .LatestAsync() .GetAwaiter() @@ -211,6 +236,12 @@ public class DaemonRuntimeDocumentTests Assert.AreEqual(AetheriaRuntimeDaemonSchemas.Frame, latestFrame.Schema); Assert.AreEqual(frame.FrameId, latestFrame.FrameId); Assert.AreEqual(frame.FrameId, latestFrameByType.FrameId); + Assert.AreEqual(AetheriaRuntimeDaemonSchemas.ObjectsViewport, objectsViewport.Schema); + Assert.AreEqual("Player", objectsViewport.Objects.FirstOrDefault()?.DisplayName); + Assert.AreEqual(AetheriaRuntimeDaemonSchemas.ZoneDetails, zoneDetails.Schema); + Assert.AreEqual(0, zoneDetails.ZoneIndex); + Assert.AreEqual(AetheriaRuntimeDaemonSchemas.Inventory, inventory.Schema); + Assert.AreEqual("zone.0.entity.0", inventory.EntityKey); Assert.AreEqual("zone.0.entity.0", currentEntity.EntityKey); Assert.AreEqual(0, currentEntity.EntityIndex); Assert.AreEqual("Player", currentEntity.Entity?.DisplayName); diff --git a/Assets/Scripts/UI/Menu/InventoryMenu.cs b/Assets/Scripts/UI/Menu/InventoryMenu.cs index aaef18792..d36bfdec5 100644 --- a/Assets/Scripts/UI/Menu/InventoryMenu.cs +++ b/Assets/Scripts/UI/Menu/InventoryMenu.cs @@ -870,7 +870,10 @@ public class InventoryMenu : MonoBehaviour return false; var inventory = ResolveClient() - .InventoryAsync(entityIndex) + .Aetheria() + .Details + .Inventory(entityIndex) + .LatestAsync() .GetAwaiter() .GetResult(); if (inventory == null || !string.Equals(inventory.EntityKey, entityKey, StringComparison.Ordinal)) diff --git a/Assets/Scripts/UI/Menu/InventoryPanel.cs b/Assets/Scripts/UI/Menu/InventoryPanel.cs index 4250d317c..51ffba1f3 100644 --- a/Assets/Scripts/UI/Menu/InventoryPanel.cs +++ b/Assets/Scripts/UI/Menu/InventoryPanel.cs @@ -1289,7 +1289,10 @@ private void Update() return false; var inventory = ResolveClient() - .InventoryAsync(entityIndex) + .Aetheria() + .Details + .Inventory(entityIndex) + .LatestAsync() .GetAwaiter() .GetResult(); if (inventory == null || !string.Equals(inventory.EntityKey, entityKey, StringComparison.Ordinal)) diff --git a/Assets/Scripts/UI/Menu/MapRenderer.cs b/Assets/Scripts/UI/Menu/MapRenderer.cs index a841ca0dd..69044399f 100644 --- a/Assets/Scripts/UI/Menu/MapRenderer.cs +++ b/Assets/Scripts/UI/Menu/MapRenderer.cs @@ -133,11 +133,17 @@ public class MapRenderer : MonoBehaviour var client = ResolveClient(); var viewport = ResolveViewportBounds(); _objectsViewport = client - .ObjectsViewportAsync(viewport) + .Aetheria() + .Viewports + .Objects(viewport) + .LatestAsync() .GetAwaiter() .GetResult(); _renderSplatsViewport = client - .RenderSplatsViewportAsync(viewport) + .Aetheria() + .Viewports + .RenderSplats(viewport) + .LatestAsync() .GetAwaiter() .GetResult(); diff --git a/Assets/Scripts/UI/Menu/SectorRenderer.cs b/Assets/Scripts/UI/Menu/SectorRenderer.cs index 0b72bc4cb..d9e5aaa1b 100644 --- a/Assets/Scripts/UI/Menu/SectorRenderer.cs +++ b/Assets/Scripts/UI/Menu/SectorRenderer.cs @@ -200,7 +200,10 @@ public class SectorRenderer : MonoBehaviour, IBeginDragHandler, IDragHandler, IS try { return ResolveClient() - .ZoneDetailsAsync(zoneIndex) + .Aetheria() + .Details + .Zone(zoneIndex) + .LatestAsync() .GetAwaiter() .GetResult(); } diff --git a/Assets/Scripts/Zone Display/ZoneRenderer.cs b/Assets/Scripts/Zone Display/ZoneRenderer.cs index c9d3e0362..896f82693 100644 --- a/Assets/Scripts/Zone Display/ZoneRenderer.cs +++ b/Assets/Scripts/Zone Display/ZoneRenderer.cs @@ -896,7 +896,10 @@ public class ZoneRenderer : MonoBehaviour try { var objects = ResolveClient() - .ObjectsViewportAsync(ToViewportBounds(viewport)) + .Aetheria() + .Viewports + .Objects(ToViewportBounds(viewport)) + .LatestAsync() .GetAwaiter() .GetResult(); foreach (var entity in objects?.Objects ?? Array.Empty()) diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClient.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClient.cs index 58c9264c7..fa26e4526 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClient.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClient.cs @@ -177,32 +177,28 @@ namespace GameCult.Aetheria.State.Verse AetheriaRuntimeRtsViewportBounds viewport) { ThrowIfDisposed(); - var frame = await RequireFrameAsync().ConfigureAwait(false); - return AetheriaRuntimeRtsProjection.ProjectViewport(frame, viewport); + return await State.Viewports.Map(viewport).LatestAsync().ConfigureAwait(false); } public async Task ObjectsViewportAsync( AetheriaRuntimeRtsViewportBounds viewport) { ThrowIfDisposed(); - var frame = await RequireFrameAsync().ConfigureAwait(false); - return AetheriaRuntimeRtsProjection.ProjectObjectsViewport(frame, viewport); + return await State.Viewports.Objects(viewport).LatestAsync().ConfigureAwait(false); } public async Task GravityViewportAsync( AetheriaRuntimeRtsViewportBounds viewport) { ThrowIfDisposed(); - var frame = await RequireFrameAsync().ConfigureAwait(false); - return AetheriaRuntimeRtsProjection.ProjectGravityViewport(frame, viewport); + return await State.Viewports.Gravity(viewport).LatestAsync().ConfigureAwait(false); } public async Task RenderSplatsViewportAsync( AetheriaRuntimeRtsViewportBounds viewport) { ThrowIfDisposed(); - var frame = await RequireFrameAsync().ConfigureAwait(false); - return AetheriaRuntimeRtsProjection.ProjectRenderSplatsViewport(frame, viewport); + return await State.Viewports.RenderSplats(viewport).LatestAsync().ConfigureAwait(false); } public async Task AssetManifestAsync() @@ -253,8 +249,7 @@ namespace GameCult.Aetheria.State.Verse public async Task ZoneDetailsAsync(int zoneIndex) { ThrowIfDisposed(); - var frame = await RequireFrameAsync().ConfigureAwait(false); - return AetheriaRuntimeRtsProjection.ProjectZoneDetails(frame, zoneIndex); + return await State.Details.Zone(zoneIndex).LatestAsync().ConfigureAwait(false); } public async Task ZoneRenderAsync() @@ -268,6 +263,9 @@ namespace GameCult.Aetheria.State.Verse AetheriaRuntimeStarbridgeSessionDocument? session = null) { ThrowIfDisposed(); + if (scenario == null && session == null) + return await State.Starbridge.Summary.LatestAsync().ConfigureAwait(false); + var frame = await RequireFrameAsync().ConfigureAwait(false); scenario ??= await _verse.GetStarbridgeScenarioAsync().ConfigureAwait(false); session ??= await _verse.GetStarbridgeSessionAsync().ConfigureAwait(false); @@ -281,15 +279,13 @@ namespace GameCult.Aetheria.State.Verse public async Task SelectedObjectAsync(int entityIndex) { ThrowIfDisposed(); - var frame = await RequireFrameAsync().ConfigureAwait(false); - return AetheriaRuntimeRtsProjection.ProjectSelectedObject(frame, entityIndex); + return await State.Details.SelectedObject(entityIndex).LatestAsync().ConfigureAwait(false); } public async Task InventoryAsync(int entityIndex) { ThrowIfDisposed(); - var frame = await RequireFrameAsync().ConfigureAwait(false); - return AetheriaRuntimeRtsProjection.ProjectInventory(frame, entityIndex); + return await State.Details.Inventory(entityIndex).LatestAsync().ConfigureAwait(false); } public Task DaemonHealthAsync() diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs index 6181790d4..7adc5ec80 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs @@ -22,8 +22,16 @@ namespace GameCult.Aetheria.State.Verse CultMeshDocumentHandle stationRefit, CultMeshDocumentHandle sectorMap, CultMeshDocumentHandle zoneRender, + Func> mapViewport, + Func> objectsViewport, + Func> gravityViewport, + Func> renderSplatsViewport, + Func> zoneDetails, + Func> selectedObject, + Func> inventory, CultMeshDocumentHandle starbridgeScenario, CultMeshDocumentHandle starbridgeSession, + CultMeshDocumentHandle starbridgeSummary, Func> starbridgePlayerSeat) { LatestFrame = latestFrame ?? throw new ArgumentNullException(nameof(latestFrame)); @@ -33,10 +41,20 @@ namespace GameCult.Aetheria.State.Verse StationRefit = stationRefit ?? throw new ArgumentNullException(nameof(stationRefit)); SectorMap = sectorMap ?? throw new ArgumentNullException(nameof(sectorMap)); ZoneRender = zoneRender ?? throw new ArgumentNullException(nameof(zoneRender)); + Viewports = new AetheriaClientViewportState( + mapViewport, + objectsViewport, + gravityViewport, + renderSplatsViewport); + Details = new AetheriaClientDetailState( + zoneDetails, + selectedObject, + inventory); DockingState = new AetheriaClientDockingState(Current.Entity, Current.Docking, StationRefit); Starbridge = new AetheriaClientStarbridgeState( starbridgeScenario, starbridgeSession, + starbridgeSummary, starbridgePlayerSeat); _documents = CultMesh.Documents( LatestFrame, @@ -60,6 +78,10 @@ namespace GameCult.Aetheria.State.Verse public AetheriaClientDockingState DockingState { get; } + public AetheriaClientViewportState Viewports { get; } + + public AetheriaClientDetailState Details { get; } + public AetheriaClientStarbridgeState Starbridge { get; } public CultMeshDocumentHandle ZoneContacts { get; } @@ -232,6 +254,78 @@ namespace GameCult.Aetheria.State.Verse public bool IsDocked => StationRefit?.IsDocked == true && CurrentDockingBay != null; } + public sealed class AetheriaClientViewportState + { + private readonly Func> _map; + private readonly Func> _objects; + private readonly Func> _gravity; + private readonly Func> _renderSplats; + + internal AetheriaClientViewportState( + Func> map, + Func> objects, + Func> gravity, + Func> renderSplats) + { + _map = map ?? throw new ArgumentNullException(nameof(map)); + _objects = objects ?? throw new ArgumentNullException(nameof(objects)); + _gravity = gravity ?? throw new ArgumentNullException(nameof(gravity)); + _renderSplats = renderSplats ?? throw new ArgumentNullException(nameof(renderSplats)); + } + + public CultMeshDocumentHandle Map(AetheriaRuntimeRtsViewportBounds viewport) + { + return _map(viewport ?? new AetheriaRuntimeRtsViewportBounds()); + } + + public CultMeshDocumentHandle Objects(AetheriaRuntimeRtsViewportBounds viewport) + { + return _objects(viewport ?? new AetheriaRuntimeRtsViewportBounds()); + } + + public CultMeshDocumentHandle Gravity(AetheriaRuntimeRtsViewportBounds viewport) + { + return _gravity(viewport ?? new AetheriaRuntimeRtsViewportBounds()); + } + + public CultMeshDocumentHandle RenderSplats(AetheriaRuntimeRtsViewportBounds viewport) + { + return _renderSplats(viewport ?? new AetheriaRuntimeRtsViewportBounds()); + } + } + + public sealed class AetheriaClientDetailState + { + private readonly Func> _zoneDetails; + private readonly Func> _selectedObject; + private readonly Func> _inventory; + + internal AetheriaClientDetailState( + Func> zoneDetails, + Func> selectedObject, + Func> inventory) + { + _zoneDetails = zoneDetails ?? throw new ArgumentNullException(nameof(zoneDetails)); + _selectedObject = selectedObject ?? throw new ArgumentNullException(nameof(selectedObject)); + _inventory = inventory ?? throw new ArgumentNullException(nameof(inventory)); + } + + public CultMeshDocumentHandle Zone(int zoneIndex) + { + return _zoneDetails(zoneIndex); + } + + public CultMeshDocumentHandle SelectedObject(int entityIndex) + { + return _selectedObject(entityIndex); + } + + public CultMeshDocumentHandle Inventory(int entityIndex) + { + return _inventory(entityIndex); + } + } + public sealed class AetheriaClientStarbridgeState { private readonly Func> _playerSeat; @@ -239,10 +333,12 @@ namespace GameCult.Aetheria.State.Verse internal AetheriaClientStarbridgeState( CultMeshDocumentHandle scenario, CultMeshDocumentHandle session, + CultMeshDocumentHandle summary, Func> playerSeat) { Scenario = scenario ?? throw new ArgumentNullException(nameof(scenario)); Session = session ?? throw new ArgumentNullException(nameof(session)); + Summary = summary ?? throw new ArgumentNullException(nameof(summary)); _playerSeat = playerSeat ?? throw new ArgumentNullException(nameof(playerSeat)); } @@ -250,6 +346,8 @@ namespace GameCult.Aetheria.State.Verse public CultMeshDocumentHandle Session { get; } + public CultMeshDocumentHandle Summary { get; } + public CultMeshDocumentHandle PlayerSeat(string seatId) { if (string.IsNullOrWhiteSpace(seatId)) diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs index 0e2de0358..61f1778e2 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -761,10 +762,45 @@ namespace GameCult.Aetheria.State.Verse "aetheria.zone.render", frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectZoneRender(frame)), AetheriaRuntimeDaemonSchemas.ZoneRender), + viewport => ProjectedDocument( + ViewportDocumentId("aetheria.viewport.map", viewport), + frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectViewport(frame, viewport)), + AetheriaRuntimeDaemonSchemas.RtsViewport), + viewport => ProjectedDocument( + ViewportDocumentId("aetheria.viewport.objects", viewport), + frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectObjectsViewport(frame, viewport)), + AetheriaRuntimeDaemonSchemas.ObjectsViewport), + viewport => ProjectedDocument( + ViewportDocumentId("aetheria.viewport.gravity", viewport), + frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectGravityViewport(frame, viewport)), + AetheriaRuntimeDaemonSchemas.GravityViewport), + viewport => ProjectedDocument( + ViewportDocumentId("aetheria.viewport.render_splats", viewport), + frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectRenderSplatsViewport(frame, viewport)), + AetheriaRuntimeDaemonSchemas.RenderSplatsViewport), + zoneIndex => ProjectedDocument( + IndexedDocumentId("aetheria.zone.details", zoneIndex), + frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectZoneDetails(frame, zoneIndex)), + AetheriaRuntimeDaemonSchemas.ZoneDetails), + entityIndex => ProjectedDocument( + IndexedDocumentId("aetheria.object.selected", entityIndex), + frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectSelectedObject(frame, entityIndex)), + AetheriaRuntimeDaemonSchemas.SelectedObject), + entityIndex => ProjectedDocument( + IndexedDocumentId("aetheria.inventory", entityIndex), + frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectInventory(frame, entityIndex)), + AetheriaRuntimeDaemonSchemas.Inventory), Document( AetheriaRuntimeVerseRecordKeys.StarbridgeScenarioLatest), Document( AetheriaRuntimeVerseRecordKeys.StarbridgeSessionLatest), + ProjectedDocument( + "aetheria.starbridge.summary", + ProjectStarbridgeSummaryAsync, + AetheriaRuntimeDaemonSchemas.StarbridgeSessionSummary, + CultMesh.ProjectionSource(AetheriaRuntimeVerseRecordKeys.StarbridgeScenarioLatest.ToString()), + CultMesh.ProjectionSource(AetheriaRuntimeVerseRecordKeys.StarbridgeSessionLatest.ToString()), + CultMesh.ProjectionSource("catalog:aetheria.runtime")), seatId => Document( AetheriaRuntimeVerseRecordKeys.StarbridgePlayerSeat(seatId))); @@ -805,6 +841,43 @@ namespace GameCult.Aetheria.State.Verse var catalog = OpenRuntimeCatalog(); return AetheriaRuntimeRtsProjection.ProjectStationRefit(frame, loadoutTemplates, catalog); } + + async Task ProjectStarbridgeSummaryAsync( + AetheriaRuntimeDaemonFrameDocument frame) + { + var scenario = await GetStarbridgeScenarioAsync().ConfigureAwait(false); + var session = await GetStarbridgeSessionAsync().ConfigureAwait(false); + return AetheriaRuntimeStarbridgeProjection.ProjectSessionSummary( + frame, + scenario, + session, + OpenRuntimeCatalog()); + } + + static string IndexedDocumentId(string prefix, int index) + { + return $"{prefix}.{index.ToString(CultureInfo.InvariantCulture)}"; + } + + static string ViewportDocumentId(string prefix, AetheriaRuntimeRtsViewportBounds viewport) + { + var normalized = AetheriaRuntimeRtsProjection.Normalize(viewport); + return string.Join( + ".", + prefix, + ViewportToken(normalized.MinX), + ViewportToken(normalized.MinY), + ViewportToken(normalized.MaxX), + ViewportToken(normalized.MaxY)); + } + + static string ViewportToken(double value) + { + return value + .ToString("0.###", CultureInfo.InvariantCulture) + .Replace('-', 'n') + .Replace('.', 'p'); + } } private async Task RequireFrameAsync()