using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; using GameCult.Caching; using GameCult.Caching.MessagePack; using GameCult.Eve.Surface; using GameCult.Mesh; using GameCult.Networking; using R3; using EveUiCommandRequest = GameCult.Eve.Surface.EveSurfaceCommandRequest; using EveSurfaceDocument = GameCult.Eve.Surface.EveSurfaceDocument; #nullable enable namespace GameCult.Aetheria.State.Verse { public static class AetheriaRuntimeVerseRecordKeys { public static CultRecordKey DaemonProviderAdvertisement { get; } = new CultRecordKey("daemon:aetheria.provider_advertisement.v1"); public static CultRecordKey EveProviderAdvertisement { get; } = new CultRecordKey("eve:provider:aetheria.daemon"); public const string EveCommandRecordPrefix = "eve:commands:aetheria.daemon"; public const string EveReceiptRecordPrefix = "eve:receipts:aetheria.daemon"; public static CultRecordKey DaemonHealth { get; } = new CultRecordKey("daemon:aetheria.health.v1"); public static CultRecordKey DaemonCommandBoundary { get; } = new CultRecordKey("daemon:aetheria.command_boundary.v1"); public static CultRecordKey DaemonAssetManifest { get; } = new CultRecordKey("daemon:aetheria.asset_manifest.latest.v1"); public static CultRecordKey EveAssetCatalog { get; } = new CultRecordKey("eve:assets:aetheria.daemon"); public static CultRecordKey VerseAuthorityPolicy { get; } = new CultRecordKey(AetheriaRuntimeVerseAuthorityPolicyDocument.DocumentKey); public static CultRecordKey DaemonFrameLatest { get; } = new CultRecordKey("daemon:aetheria.frame.latest.v1"); public static CultRecordKey DaemonSoaViewLatest { get; } = new CultRecordKey("daemon:aetheria.soa_view.latest.v1"); public static CultRecordKey StarbridgeScenarioLatest { get; } = new CultRecordKey("starbridge:aetheria.scenario.latest.v1"); public static CultRecordKey StarbridgeSessionLatest { get; } = new CultRecordKey("starbridge:aetheria.session.latest.v1"); public static CultRecordKey StarbridgeSessionSummary { get; } = new CultRecordKey("daemon:aetheria.starbridge.session.latest.v1"); public static CultRecordKey StarbridgePlayerSeat(string seatId) { return new CultRecordKey(AetheriaRuntimeStarbridgePlayerSeatDocument.RecordKey(seatId)); } public static CultRecordKey DaemonGameSurface { get; } = new CultRecordKey("eve:surface:aetheria.daemon.game"); public static CultRecordKey DaemonGameTuiSurface { get; } = new CultRecordKey("eve:surface:aetheria.daemon.game.tui"); public static CultRecordKey DaemonEditorSurface { get; } = new CultRecordKey("eve:surface:aetheria.daemon.editor"); public static CultRecordKey DaemonEditorTuiSurface { get; } = new CultRecordKey("eve:surface:aetheria.daemon.editor.tui"); public static CultRecordKey MainMenuSurface { get; } = new CultRecordKey("eve:surface:aetheria.main_menu.root"); public static CultRecordKey MainMenuSettingsSurface { get; } = new CultRecordKey("eve:surface:aetheria.main_menu.settings"); public static CultRecordKey MainMenuInputSettingsSurface { get; } = new CultRecordKey("eve:surface:aetheria.main_menu.input_settings"); public static CultRecordKey MainMenuPlayerSettingsSurface { get; } = new CultRecordKey("eve:surface:aetheria.main_menu.player_settings"); public static CultRecordKey MainMenuVerseSettingsSurface { get; } = new CultRecordKey("eve:surface:aetheria.main_menu.verse_settings"); public static CultRecordKey InventoryPanelSurface { get; } = new CultRecordKey("eve:surface:aetheria.inventory.panel"); public static CultRecordKey InventoryDropdownSurface { get; } = new CultRecordKey("eve:surface:aetheria.inventory.panel.dropdown"); public static CultRecordKey MapMenuSurface { get; } = new CultRecordKey("eve:surface:aetheria.map.zone_details"); public static CultRecordKey TradeMenuSurface { get; } = new CultRecordKey("eve:surface:aetheria.trade.menu"); public static CultRecordKey DaemonCommand(string commandId) { return new CultRecordKey($"daemon:commands:{StableToken(commandId)}:gamecult.aetheria.daemon_command.v1"); } public static CultRecordKey EveCommand(string commandId) { return new CultRecordKey($"eve:commands:{StableToken(commandId)}:gamecult.eve.command.v1"); } public static CultRecordKey EveReceiptForCommand(string commandId) { if (string.IsNullOrWhiteSpace(commandId)) throw new ArgumentException("Eve receipt command id must be non-empty.", nameof(commandId)); return new CultRecordKey($"{EveReceiptRecordPrefix}:{commandId}"); } public static string StableToken(string value) { if (string.IsNullOrWhiteSpace(value)) return "empty"; var chars = value .Select(character => char.IsLetterOrDigit(character) ? character : '-') .ToArray(); var token = new string(chars).Trim('-').ToLowerInvariant(); while (token.Contains("--", StringComparison.Ordinal)) token = token.Replace("--", "-", StringComparison.Ordinal); return string.IsNullOrWhiteSpace(token) ? "empty" : token; } } public static class AetheriaRuntimeVerseContractRegistry { private static readonly Type[] RuntimeDocumentTypes = { typeof(AetheriaRuntimeDaemonProviderAdvertisementDocument), typeof(AetheriaRuntimeDaemonHealthDocument), typeof(AetheriaRuntimeDaemonCommandBoundaryDocument), typeof(AetheriaRuntimeVerseAuthorityPolicyDocument), typeof(AetheriaRuntimeDaemonFrameDocument), typeof(AetheriaRuntimeDaemonSoaViewDocument), typeof(AetheriaRuntimeCatalogSnapshot), typeof(AetheriaRuntimeLoadoutTemplatesDocument), typeof(AetheriaRuntimeAssetManifestDocument), typeof(AetheriaRuntimeObjectsViewportDocument), typeof(AetheriaRuntimeGravityViewportDocument), typeof(AetheriaRuntimeCurrentZoneDocument), typeof(AetheriaRuntimeCurrentEntityDocument), typeof(AetheriaRuntimeCurrentDockingDocument), typeof(AetheriaRuntimeZoneContactsDocument), typeof(AetheriaRuntimeStationRefitDocument), typeof(AetheriaRuntimeSectorMapDocument), typeof(AetheriaRuntimeZoneDetailsDocument), typeof(AetheriaRuntimeZoneRenderDocument), typeof(AetheriaRuntimeSelectedObjectDocument), typeof(AetheriaRuntimeInventoryDocument), typeof(AetheriaRuntimeStarbridgeScenarioDocument), typeof(AetheriaRuntimeStarbridgeSessionDocument), typeof(AetheriaRuntimeStarbridgeSessionSummaryDocument), typeof(AetheriaRuntimeStarbridgePlayerSeatDocument), typeof(AetheriaRuntimePlayerSettingsDocument), typeof(AetheriaRuntimeVerseHostSettingsDocument), typeof(AetheriaRuntimeSurfaceDocument), typeof(EveProviderAdvertisementDocument), typeof(EveSurfaceDocument), typeof(EveSurfaceCommandRequest), typeof(EveCommandReceiptDocument), typeof(EveAssetCatalogDocument), typeof(AetheriaRuntimeDaemonCommandDocument), typeof(AetheriaRuntimeEveCommandDocument), typeof(AetheriaRuntimeCommittedCommandFactDocument) }; public static CultDocumentRegistry CreateCultCacheRegistry() { return CultMesh.CreateCultCacheDocumentRegistry(RuntimeDocumentTypes); } public static CultNetDocumentRegistry CreateCultNetRegistry(CultDocumentRegistry? cacheRegistry = null) { var registry = cacheRegistry ?? CreateCultCacheRegistry(); return CultMesh.CreateCultNetDocumentRegistry(RuntimeDocumentTypes, registry); } } public sealed class AetheriaRuntimeVerseClient : IDisposable { public const string DefaultRuntimeId = "aetheria-verse-client"; private const string RemoteShardId = "primary"; private readonly CultMeshNode _node; private AetheriaClientState? _aetheriaState; private CultMeshReactiveDocument? _managedDaemonFrame; private CultMeshReactiveDocument? _managedCatalog; private CultMeshReactiveDocument? _managedLoadoutTemplates; private CultMeshReactiveDocument? _managedPlayerSettings; private CultMeshReactiveDocument? _managedStarbridgeScenario; private CultMeshReactiveDocument? _managedStarbridgeSession; private bool _disposed; private AetheriaRuntimeVerseClient(string statePath, string runtimeId, CultMeshNode node) { StatePath = statePath; RuntimeId = runtimeId; _node = node ?? throw new ArgumentNullException(nameof(node)); } public string StatePath { get; } public string RuntimeId { get; } public CultMeshNode Node => _node; public CultCache Cache => _node.Cache; public CultNetDatabase Database => _node.Database; public string RemoteEndpoint { get; private set; } = ""; public bool IsRemoteReplica => !string.IsNullOrWhiteSpace(RemoteEndpoint); public static async Task OpenAsync( string statePath, string runtimeId = DefaultRuntimeId, bool startServer = false, bool pullOnOpen = true) { if (string.IsNullOrWhiteSpace(statePath)) throw new ArgumentException("State path must be non-empty.", nameof(statePath)); var fullPath = Path.GetFullPath(statePath); var effectiveRuntimeId = string.IsNullOrWhiteSpace(runtimeId) ? DefaultRuntimeId : runtimeId; var registry = AetheriaRuntimeVerseContractRegistry.CreateCultCacheRegistry(); var node = await CultMesh.CreateNodeAsync( fullPath, new CultMeshNodeOptions { StartServer = startServer, EnableDurableShardLogs = true, CacheOptions = new CultCacheOpenOptions { Registry = registry, PullOnOpen = pullOnOpen, StoreFlushOnDispose = true, UseDirectoryStore = true }, DatabaseOptions = new CultNetDatabaseOptions { RuntimeId = effectiveRuntimeId, DocumentRegistry = AetheriaRuntimeVerseContractRegistry.CreateCultNetRegistry(registry) } }) .ConfigureAwait(false); return new AetheriaRuntimeVerseClient(fullPath, effectiveRuntimeId, node); } public static async Task OpenRemoteAsync( string replicaStatePath, string endpoint, string runtimeId = DefaultRuntimeId, bool pullOnOpen = false, bool synchronizeOnOpen = true) { if (string.IsNullOrWhiteSpace(endpoint)) throw new ArgumentException("CultMesh endpoint must be non-empty.", nameof(endpoint)); var fullPath = Path.GetFullPath(replicaStatePath ?? throw new ArgumentNullException(nameof(replicaStatePath))); var effectiveRuntimeId = string.IsNullOrWhiteSpace(runtimeId) ? DefaultRuntimeId : runtimeId; var registry = AetheriaRuntimeVerseContractRegistry.CreateCultCacheRegistry(); var shard = new CultNetShardDescriptor( RemoteShardId, "aetheria-daemon", epoch: 1, isPrimary: false, primaryEndpoints: new[] { endpoint.Trim() }); var node = await CultMesh.CreateNodeAsync( fullPath, new CultMeshNodeOptions { StartServer = false, EnableDurableShardLogs = true, CacheOptions = new CultCacheOpenOptions { Registry = registry, PullOnOpen = pullOnOpen, StoreFlushOnDispose = true, UseDirectoryStore = true }, DatabaseOptions = new CultNetDatabaseOptions { RuntimeId = effectiveRuntimeId, Shards = new[] { shard }, DocumentRegistry = AetheriaRuntimeVerseContractRegistry.CreateCultNetRegistry(registry) } }) .ConfigureAwait(false); var client = new AetheriaRuntimeVerseClient(fullPath, effectiveRuntimeId, node) { RemoteEndpoint = endpoint.Trim() }; if (synchronizeOnOpen) await client.RefreshRemoteAsync().ConfigureAwait(false); return client; } public async Task RefreshRemoteAsync( IReadOnlyList? recordKeys = null, TimeSpan? connectTimeout = null, TimeSpan? responseTimeout = null) { ThrowIfDisposed(); if (!IsRemoteReplica) throw new InvalidOperationException("Remote refresh requires a client opened with OpenRemoteAsync."); var snapshot = CultMesh.SnapshotEndpoint( RemoteEndpoint, new CultMeshSnapshotEndpointOptions { Context = CultMesh.Verse("aetheria.remote", RuntimeId).Context, DocumentRegistry = AetheriaRuntimeVerseContractRegistry.CreateCultNetRegistry(), Request = new CultMeshSnapshotRequestOptions { RecordKeys = recordKeys, ShardId = RemoteShardId, ShardEpoch = 1, ConnectTimeout = connectTimeout ?? TimeSpan.FromSeconds(5), ResponseTimeout = responseTimeout ?? TimeSpan.FromSeconds(10), MessageIdPrefix = "aetheria-unity", RudpRuntimeId = RuntimeId, RudpMaxFragmentBytes = 1200 } }); var result = await snapshot.SyncSnapshotAsync(_node).ConfigureAwait(false); await _node.FlushAsync().ConfigureAwait(false); return result.AppliedCount; } public AetheriaClientState Aetheria() { ThrowIfDisposed(); return _aetheriaState ??= CreateAetheriaState(); } public CultMeshMutableStatePointer MutableDocument( CultRecordKey key) where TDocument : class { ThrowIfDisposed(); if (string.IsNullOrWhiteSpace(key.Value)) throw new ArgumentException("Value must be non-empty.", nameof(key)); return MutableDocumentPointer(key); } public CultMeshDocumentHandle Document( CultRecordKey key, string? documentId = null) where TDocument : class { ThrowIfDisposed(); if (string.IsNullOrWhiteSpace(key.Value)) throw new ArgumentException("Value must be non-empty.", nameof(key)); var descriptor = CultDocumentRegistry.Shared.GetRequired(); return CultMesh.Document( Database, key, CultMesh.Verse("aetheria.local", RuntimeId), documentId, new[] { CultMesh.ProjectionSource(key.ToString(), descriptor.SchemaId, "Aetheria Verse database document") }, new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "Aetheria Verse database")); } public Observable> WatchRecord( CultRecordKey key) where TDocument : class { ThrowIfDisposed(); if (string.IsNullOrWhiteSpace(key.Value)) throw new ArgumentException("Value must be non-empty.", nameof(key)); return Database.WatchRecord(key); } internal async Task SubmitDaemonCommandAsync( AetheriaRuntimeDaemonCommandDocument command, bool flush = true) { ThrowIfDisposed(); if (command == null) throw new ArgumentNullException(nameof(command)); command.Schema = AetheriaRuntimeDaemonSchemas.Command; if (string.IsNullOrWhiteSpace(command.CommandId)) command.CommandId = Guid.NewGuid().ToString("N"); if (string.IsNullOrWhiteSpace(command.ClientId)) command.ClientId = RuntimeId; if (string.IsNullOrWhiteSpace(command.IssuedAtUtc)) command.IssuedAtUtc = DateTime.UtcNow.ToString("O"); await Database.PutAsync(AetheriaRuntimeVerseRecordKeys.DaemonCommand(command.CommandId), command) .ConfigureAwait(false); if (flush) await FlushAsync().ConfigureAwait(false); return AetheriaRuntimeDaemonOperationClient.ToEnvelope(command); } internal async Task SubmitEveCommandAsync( AetheriaRuntimeEveCommandDocument command, bool flush = true) { ThrowIfDisposed(); if (command == null) throw new ArgumentNullException(nameof(command)); AetheriaRuntimeEveCommandClient.NormalizeDocument(command); command.Schema = AetheriaRuntimeEveCommandDocument.SchemaId; if (string.IsNullOrWhiteSpace(command.CommandId)) command.CommandId = Guid.NewGuid().ToString("N"); if (string.IsNullOrWhiteSpace(command.IssuedAtUtc)) command.IssuedAtUtc = DateTime.UtcNow.ToString("O"); await Database.PutAsync(AetheriaRuntimeVerseRecordKeys.EveCommand(command.CommandId), command) .ConfigureAwait(false); if (flush) await FlushAsync().ConfigureAwait(false); return AetheriaRuntimeEveCommandClient.ToEnvelope(command); } internal Task SubmitInputSettingsCommandAsync( AetheriaRuntimeEveCommandKind command, AetheriaRuntimeInputSettingsCommandBody body, string clientId, bool flush = true) { ThrowIfDisposed(); return SubmitEveCommandAsync( AetheriaRuntimeEveCommandClient.ToDocument( AetheriaRuntimeEveCommandClient.CreateInputSettingsCommand(command, body, clientId)), flush); } internal Task SubmitLoadoutTemplateCommandAsync( AetheriaRuntimeLoadoutTemplateCommit loadoutTemplate, string clientId, bool flush = true) { ThrowIfDisposed(); return SubmitEveCommandAsync( AetheriaRuntimeEveCommandClient.ToDocument( AetheriaRuntimeEveCommandClient.CreateLoadoutTemplateCommand(loadoutTemplate, clientId)), flush); } internal Task SubmitKnownSurfaceCommandAsync( EveUiCommandRequest request, string clientId, bool flush = true) { ThrowIfDisposed(); if (!AetheriaRuntimeEveCommandClient.TryCreateKnownSurfaceCommand(request, out var envelope)) { throw new InvalidOperationException( $"Unknown Aetheria Eve surface command: {request?.ProviderId}/{request?.SurfaceId}/{request?.Command}"); } var document = AetheriaRuntimeEveCommandClient.ToDocument(envelope!); if (!string.IsNullOrWhiteSpace(clientId)) document.ClientId = clientId; return SubmitEveCommandAsync(document, flush); } public Task FlushAsync(bool soft = false) { ThrowIfDisposed(); return _node.FlushAsync(soft); } public void Dispose() { if (_disposed) return; _disposed = true; _aetheriaState?.Dispose(); _aetheriaState = null; _managedDaemonFrame?.Dispose(); _managedCatalog?.Dispose(); _managedLoadoutTemplates?.Dispose(); _managedPlayerSettings?.Dispose(); _managedStarbridgeScenario?.Dispose(); _managedStarbridgeSession?.Dispose(); _node.Dispose(); } private void ThrowIfDisposed() { if (_disposed) throw new ObjectDisposedException(nameof(AetheriaRuntimeVerseClient)); } private AetheriaClientState CreateAetheriaState() { var frameChanges = WatchRecord( AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest) .Where(change => change.Document != null) .Select(change => change.Document!); var catalogDocument = BootstrapCatalogDocument( "aetheria.catalog.runtime", () => Task.FromResult(BootstrapRuntimeCatalogSnapshot()), AetheriaRuntimeCatalogSnapshot.SchemaId, CatalogBootstrapSource("catalog:aetheria.runtime")); var loadoutTemplatesDocument = BootstrapCatalogDocument( "aetheria.catalog.loadout_templates", () => Task.FromResult(BootstrapLoadoutTemplatesDocument()), AetheriaRuntimeLoadoutTemplatesDocument.SchemaId, CatalogBootstrapSource("catalog:aetheria.loadout_templates")); var playerSettingsDocument = BootstrapCatalogDocument( "aetheria.settings.player", () => Task.FromResult(BootstrapPlayerSettingsDocument()), AetheriaRuntimePlayerSettingsDocument.SchemaId, CatalogBootstrapSource("catalog:aetheria.player_settings")); var verseHostSettingsDocument = BootstrapCatalogDocument( "aetheria.settings.verse_host", () => Task.FromResult(BootstrapVerseHostSettingsDocument()), AetheriaRuntimeVerseHostSettingsDocument.SchemaId, CatalogBootstrapSource("catalog:aetheria.verse_host_settings")); var starbridgeScenarioDocument = Document( AetheriaRuntimeVerseRecordKeys.StarbridgeScenarioLatest); var starbridgeSessionDocument = Document( AetheriaRuntimeVerseRecordKeys.StarbridgeSessionLatest); var latestFrameDocument = Document( AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest); CultMeshReactiveDocument? managedDaemonFrame = null; CultMeshReactiveDocument? managedCatalog = null; CultMeshReactiveDocument? managedLoadoutTemplates = null; CultMeshReactiveDocument? managedPlayerSettings = null; CultMeshReactiveDocument? managedStarbridgeScenario = null; CultMeshReactiveDocument? managedStarbridgeSession = null; var state = new AetheriaClientState( Document( AetheriaRuntimeVerseRecordKeys.DaemonProviderAdvertisement), Document( AetheriaRuntimeVerseRecordKeys.DaemonHealth), Document( AetheriaRuntimeVerseRecordKeys.DaemonCommandBoundary), Document( AetheriaRuntimeVerseRecordKeys.DaemonAssetManifest), Document( AetheriaRuntimeVerseRecordKeys.VerseAuthorityPolicy), latestFrameDocument, Document( AetheriaRuntimeVerseRecordKeys.DaemonSoaViewLatest), Document( AetheriaRuntimeVerseRecordKeys.DaemonGameSurface), Document( AetheriaRuntimeVerseRecordKeys.DaemonGameTuiSurface), Document( AetheriaRuntimeVerseRecordKeys.DaemonEditorSurface), Document( AetheriaRuntimeVerseRecordKeys.DaemonEditorTuiSurface), catalogDocument, loadoutTemplatesDocument, playerSettingsDocument, verseHostSettingsDocument, ManagedFrameDocument( "aetheria.current.zone", frame => Task.FromResult(AetheriaRuntimeGameDocuments.CurrentZone(frame)), AetheriaRuntimeDaemonSchemas.CurrentZone), ManagedFrameDocument( "aetheria.current.entity", frame => Task.FromResult(AetheriaRuntimeGameDocuments.CurrentEntity(frame)), AetheriaRuntimeDaemonSchemas.CurrentEntity), ManagedFrameDocument( "aetheria.current.docking", frame => Task.FromResult(AetheriaRuntimeGameDocuments.CurrentDocking(frame)), AetheriaRuntimeDaemonSchemas.CurrentDocking), ManagedFrameDocument( "aetheria.zone.contacts", frame => Task.FromResult(AetheriaRuntimeGameDocuments.ZoneContacts(frame)), AetheriaRuntimeDaemonSchemas.ZoneContacts), ManagedFrameDocument( "aetheria.station.refit", StationRefitAsync, AetheriaRuntimeDaemonSchemas.StationRefit, CultMesh.ProjectionSource("catalog:aetheria.runtime"), CultMesh.ProjectionSource("loadout-templates:aetheria.runtime")), ManagedFrameDocument( "aetheria.sector.map", frame => Task.FromResult(AetheriaRuntimeGameDocuments.SectorMap(frame)), AetheriaRuntimeDaemonSchemas.SectorMap), ManagedFrameDocument( "aetheria.zone.render", frame => Task.FromResult(AetheriaRuntimeGameDocuments.ZoneRender(frame)), AetheriaRuntimeDaemonSchemas.ZoneRender), viewport => ManagedFrameDocument( ViewportDocumentId("aetheria.viewport.map", viewport), frame => Task.FromResult(AetheriaRuntimeGameDocuments.Viewport(frame, viewport)), AetheriaRuntimeDaemonSchemas.GameViewport), viewport => ManagedFrameDocument( ViewportDocumentId("aetheria.viewport.objects", viewport), frame => Task.FromResult(AetheriaRuntimeGameDocuments.ObjectsViewport(frame, viewport)), AetheriaRuntimeDaemonSchemas.ObjectsViewport), viewport => ManagedFrameDocument( ViewportDocumentId("aetheria.viewport.gravity", viewport), frame => Task.FromResult(AetheriaRuntimeGameDocuments.GravityViewport(frame, viewport)), AetheriaRuntimeDaemonSchemas.GravityViewport), viewport => ManagedFrameDocument( ViewportDocumentId("aetheria.viewport.render_splats", viewport), frame => Task.FromResult(AetheriaRuntimeGameDocuments.RenderSplatsViewport(frame, viewport)), AetheriaRuntimeDaemonSchemas.RenderSplatsViewport), zoneIndex => ManagedFrameDocument( IndexedDocumentId("aetheria.zone.details", zoneIndex), frame => Task.FromResult(AetheriaRuntimeGameDocuments.ZoneDetails(frame, zoneIndex)), AetheriaRuntimeDaemonSchemas.ZoneDetails), zoneIndex => ManagedFrameDocument( IndexedDocumentId("aetheria.zone.details.surface", zoneIndex), frame => ZoneDetailsSurfaceAsync(frame, zoneIndex), AetheriaRuntimeZoneDetailsSurfaceBuilder.SurfaceId, CultMesh.ProjectionSource("catalog:aetheria.runtime"), CultMesh.ProjectionSource("catalog:aetheria.player_settings")), request => ManagedFrameDocument( InventoryPanelSurfaceDocumentId(request), frame => InventoryPanelSurfaceAsync(frame, request), AetheriaRuntimeInventoryPanelSurfaceBuilder.SurfaceId, CultMesh.ProjectionSource("catalog:aetheria.runtime"), CultMesh.ProjectionSource("catalog:aetheria.player_settings"), CultMesh.ProjectionSource("loadout-templates:aetheria.runtime")), request => ManagedFrameDocument( InventoryDropdownSurfaceDocumentId(request), frame => InventoryDropdownSurfaceAsync(frame, request), AetheriaRuntimeInventoryDropdownSurfaceBuilder.SurfaceId, CultMesh.ProjectionSource("catalog:aetheria.runtime"), CultMesh.ProjectionSource("loadout-templates:aetheria.runtime")), (surfaceId, canOpenRuntimeInputScreen, inGame) => ManagedMainMenuSurfaceDocument( surfaceId, canOpenRuntimeInputScreen, inGame), entityIndex => ManagedFrameDocument( IndexedDocumentId("aetheria.object.selected", entityIndex), frame => Task.FromResult(AetheriaRuntimeGameDocuments.SelectedObject(frame, entityIndex)), AetheriaRuntimeDaemonSchemas.SelectedObject), entityIndex => ManagedFrameDocument( IndexedDocumentId("aetheria.inventory", entityIndex), frame => Task.FromResult(AetheriaRuntimeGameDocuments.Inventory(frame, entityIndex)), AetheriaRuntimeDaemonSchemas.Inventory), starbridgeScenarioDocument, starbridgeSessionDocument, ManagedFrameDocument( "aetheria.starbridge.summary", StarbridgeSummaryAsync, AetheriaRuntimeDaemonSchemas.StarbridgeSessionSummary, CultMesh.ProjectionSource(AetheriaRuntimeVerseRecordKeys.StarbridgeScenarioLatest.ToString()), CultMesh.ProjectionSource(AetheriaRuntimeVerseRecordKeys.StarbridgeSessionLatest.ToString()), CultMesh.ProjectionSource("catalog:aetheria.runtime")), seatId => Document( AetheriaRuntimeVerseRecordKeys.StarbridgePlayerSeat(seatId))); managedPlayerSettings = state.PlayerSettings.Reactive(); _managedPlayerSettings = managedPlayerSettings; return state; CultMeshDocumentHandle ManagedFrameDocument( string documentId, Func> derive, string schemaId, params CultMeshProjectionSource[] additionalSources) where TDocument : class { var sources = new[] { CultMesh.ProjectionSource( AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest.ToString(), AetheriaRuntimeDaemonSchemas.Frame, "latest authoritative daemon frame") } .Concat(additionalSources ?? Array.Empty()) .Append(CultMesh.ProjectionSource(documentId, schemaId, "managed Aetheria client document")) .ToArray(); var verse = CultMesh.Verse("aetheria.local", RuntimeId); return CultMesh.Document( documentId, verse, async _ => await derive(RequireManagedFrame()).ConfigureAwait(false), _ => frameChanges .SelectAwait(async (frame, cancellationToken) => await derive(frame).ConfigureAwait(false)), sources: sources, routeHint: new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "Aetheria managed typed state")); } AetheriaRuntimeDaemonFrameDocument RequireManagedFrame() { managedDaemonFrame ??= latestFrameDocument.Reactive(); _managedDaemonFrame = managedDaemonFrame; return managedDaemonFrame?.Current ?? throw new InvalidOperationException("Aetheria Verse client has no daemon frame yet."); } AetheriaRuntimeCatalogSnapshot RequireManagedCatalog() { managedCatalog ??= catalogDocument.Reactive(); _managedCatalog = managedCatalog; return managedCatalog?.Current ?? throw new InvalidOperationException("Aetheria Verse client has no runtime catalog document yet."); } AetheriaRuntimeLoadoutTemplatesDocument RequireManagedLoadoutTemplates() { managedLoadoutTemplates ??= loadoutTemplatesDocument.Reactive(); _managedLoadoutTemplates = managedLoadoutTemplates; return managedLoadoutTemplates?.Current ?? throw new InvalidOperationException("Aetheria Verse client has no loadout templates document yet."); } AetheriaRuntimePlayerSettingsDocument RequireManagedPlayerSettings() { return managedPlayerSettings?.Current ?? throw new InvalidOperationException("Aetheria Verse client has no player settings document yet."); } CultMeshDocumentHandle BootstrapCatalogDocument( string documentId, Func> bootstrap, string schemaId, params CultMeshProjectionSource[] additionalSources) where TDocument : class { var sources = (additionalSources ?? Array.Empty()) .Append(CultMesh.ProjectionSource(documentId, schemaId, "managed Aetheria catalog document")) .ToArray(); var verse = CultMesh.Verse("aetheria.local", RuntimeId); return CultMesh.Document( documentId, verse, async _ => await bootstrap().ConfigureAwait(false), _ => frameChanges .SelectAwait(async (_, cancellationToken) => await bootstrap().ConfigureAwait(false)), sources: sources, routeHint: new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "Aetheria managed catalog state")); } CultMeshDocumentHandle ManagedMainMenuSurfaceDocument( string surfaceId, bool canOpenRuntimeInputScreen, bool inGame) { surfaceId = string.IsNullOrWhiteSpace(surfaceId) ? AetheriaRuntimeMainMenuCommands.RootSurfaceId : surfaceId; var documentId = string.Join( ".", "aetheria.main_menu.surface", AetheriaRuntimeVerseRecordKeys.StableToken(surfaceId), canOpenRuntimeInputScreen ? "input-open" : "input-closed", inGame ? "in-game" : "title"); var descriptor = CultDocumentRegistry.Shared.GetRequired(); var sources = new[] { CultMesh.ProjectionSource(documentId, descriptor.SchemaId, "managed Aetheria main menu surface"), CultMesh.ProjectionSource("catalog:aetheria.player_settings"), CultMesh.ProjectionSource("catalog:aetheria.verse_host_settings"), CultMesh.ProjectionSource(AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest.ToString()) }; var verse = CultMesh.Verse("aetheria.local", RuntimeId); return CultMesh.Document( documentId, verse, _ => Task.FromResult(BuildMainMenuSurface(surfaceId, canOpenRuntimeInputScreen, inGame)), _ => frameChanges .Select(_ => BuildMainMenuSurface(surfaceId, canOpenRuntimeInputScreen, inGame)), sources: sources, routeHint: new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "Aetheria managed main menu surface")); } Task StationRefitAsync( AetheriaRuntimeDaemonFrameDocument frame) { return Task.FromResult(AetheriaRuntimeGameDocuments.StationRefit( frame, RequireManagedLoadoutTemplates().Templates, RequireManagedCatalog())); } Task StarbridgeSummaryAsync( AetheriaRuntimeDaemonFrameDocument frame) { return Task.FromResult(AetheriaRuntimeStarbridgeDocuments.SessionSummary( frame, CurrentStarbridgeScenario(), CurrentStarbridgeSession(), RequireManagedCatalog())); } AetheriaRuntimeStarbridgeScenarioDocument? CurrentStarbridgeScenario() { try { managedStarbridgeScenario ??= starbridgeScenarioDocument.Reactive(); _managedStarbridgeScenario = managedStarbridgeScenario; return managedStarbridgeScenario.Current; } catch (KeyNotFoundException) { return null; } catch (MessagePack.FormatterNotRegisteredException) { return null; } } AetheriaRuntimeStarbridgeSessionDocument? CurrentStarbridgeSession() { try { managedStarbridgeSession ??= starbridgeSessionDocument.Reactive(); _managedStarbridgeSession = managedStarbridgeSession; return managedStarbridgeSession.Current; } catch (KeyNotFoundException) { return null; } catch (MessagePack.FormatterNotRegisteredException) { return null; } } Task ZoneDetailsSurfaceAsync( AetheriaRuntimeDaemonFrameDocument frame, int zoneIndex) { return Task.FromResult(AetheriaRuntimeZoneDetailsSurfaceBuilder.BuildFromDocuments( AetheriaRuntimeGameDocuments.ZoneDetails(frame, zoneIndex), AetheriaRuntimeGameDocuments.SectorMap(frame), RequireManagedCatalog(), RequireManagedPlayerSettings(), DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture))); } Task InventoryPanelSurfaceAsync( AetheriaRuntimeDaemonFrameDocument frame, AetheriaRuntimeInventoryPanelSurfaceRequest request) { request ??= new AetheriaRuntimeInventoryPanelSurfaceRequest(); var entityIndex = request.DisplayedEntityIndex >= 0 ? request.DisplayedEntityIndex : request.DisplayedCargoEntityIndex; var inventory = entityIndex < 0 ? new AetheriaRuntimeInventoryDocument() : AetheriaRuntimeGameDocuments.Inventory(frame, entityIndex); return Task.FromResult(AetheriaRuntimeInventoryPanelSurfaceBuilder.BuildFromDocuments( AetheriaRuntimeGameDocuments.CurrentEntity(frame), AetheriaRuntimeGameDocuments.StationRefit( frame, RequireManagedLoadoutTemplates().Templates, RequireManagedCatalog()), inventory, RequireManagedCatalog(), RequireManagedPlayerSettings(), request, InventoryDropdownSurfaceDocumentId(ToDropdownRequest(request)), DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture))); } Task InventoryDropdownSurfaceAsync( AetheriaRuntimeDaemonFrameDocument frame, AetheriaRuntimeInventoryDropdownSurfaceRequest request) { return Task.FromResult(AetheriaRuntimeInventoryDropdownSurfaceBuilder.BuildFromDocuments( AetheriaRuntimeGameDocuments.StationRefit( frame, RequireManagedLoadoutTemplates().Templates, RequireManagedCatalog()), request, DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture))); } AetheriaRuntimeSurfaceDocument BuildMainMenuSurface( string surfaceId, bool canOpenRuntimeInputScreen, bool inGame) { var updatedAtUtc = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture); var stateBoot = AetheriaRuntimeStateBoot.Inspect( new DirectoryInfo(Path.GetDirectoryName(StatePath) ?? "."), StatePath); if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.RootSurfaceId, StringComparison.Ordinal)) { return AetheriaRuntimeMainMenuSurfaceBuilder.BuildRoot( stateBoot, TryCurrentDaemonFrame(), TryCurrentVerseHostSettings(), TryCurrentPlayerSettings(), canOpenRuntimeInputScreen, inGame, updatedAtUtc); } if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.SettingsSurfaceId, StringComparison.Ordinal)) return AetheriaRuntimeMainMenuSurfaceBuilder.BuildSettings(updatedAtUtc); if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.InputSettingsSurfaceId, StringComparison.Ordinal)) { return AetheriaRuntimeMainMenuSurfaceBuilder.BuildInputSettings( stateBoot, TryCurrentPlayerSettings(), canOpenRuntimeInputScreen, inGame, updatedAtUtc); } if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId, StringComparison.Ordinal)) { return AetheriaRuntimeMainMenuSurfaceBuilder.BuildPlayerSettings( TryCurrentPlayerSettings(), updatedAtUtc); } if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId, StringComparison.Ordinal)) { return AetheriaRuntimeMainMenuSurfaceBuilder.BuildVerseSettings( AetheriaRuntimeClientTargetSurfaceBuilder.Build( stateBoot, TryCurrentVerseHostSettings(), updatedAtUtc)); } throw new ArgumentException($"Unknown Aetheria main menu surface id '{surfaceId}'.", nameof(surfaceId)); } AetheriaRuntimePlayerSettingsDocument? TryCurrentPlayerSettings() { try { managedPlayerSettings ??= playerSettingsDocument.Reactive(); _managedPlayerSettings = managedPlayerSettings; return managedPlayerSettings.Current; } catch (Exception) { return null; } } AetheriaRuntimeVerseHostSettingsDocument? TryCurrentVerseHostSettings() { try { return verseHostSettingsDocument.Latest(); } catch (Exception) { return null; } } AetheriaRuntimeDaemonFrameDocument? TryCurrentDaemonFrame() { try { return latestFrameDocument.Latest(); } catch (Exception) { return null; } } static string IndexedDocumentId(string prefix, int index) { return $"{prefix}.{index.ToString(CultureInfo.InvariantCulture)}"; } static string ViewportDocumentId(string prefix, AetheriaRuntimeViewportBounds viewport) { var normalized = AetheriaRuntimeGameDocuments.Normalize(viewport); return string.Join( ".", prefix, ViewportToken(normalized.MinX), ViewportToken(normalized.MinY), ViewportToken(normalized.MaxX), ViewportToken(normalized.MaxY)); } static string InventoryPanelSurfaceDocumentId(AetheriaRuntimeInventoryPanelSurfaceRequest? request) { request ??= new AetheriaRuntimeInventoryPanelSurfaceRequest(); return string.Join( ".", "aetheria.inventory.panel.surface", AetheriaRuntimeVerseRecordKeys.StableToken(request.DisplayedEntityKey), request.DisplayedEntityIndex.ToString(CultureInfo.InvariantCulture), AetheriaRuntimeVerseRecordKeys.StableToken(request.DisplayedCargoEntityKey), request.DisplayedCargoEntityIndex.ToString(CultureInfo.InvariantCulture), request.DisplayedCargoIndex.ToString(CultureInfo.InvariantCulture), request.ThermalView ? "thermal" : "inventory"); } static string InventoryDropdownSurfaceDocumentId(AetheriaRuntimeInventoryDropdownSurfaceRequest? request) { request ??= new AetheriaRuntimeInventoryDropdownSurfaceRequest(); return string.Join( ".", "aetheria.inventory.dropdown.surface", AetheriaRuntimeVerseRecordKeys.StableToken(request.DisplayedEntityKey), AetheriaRuntimeVerseRecordKeys.StableToken(request.DisplayedCargoEntityKey), request.DisplayedCargoIndex.ToString(CultureInfo.InvariantCulture), request.CanSaveLoadout ? "save" : "readonly"); } static AetheriaRuntimeInventoryDropdownSurfaceRequest ToDropdownRequest( AetheriaRuntimeInventoryPanelSurfaceRequest? request) { request ??= new AetheriaRuntimeInventoryPanelSurfaceRequest(); return new AetheriaRuntimeInventoryDropdownSurfaceRequest { CurrentView = request.ViewTitle ?? "", DisplayedEntityKey = request.DisplayedEntityKey ?? "", DisplayedCargoEntityKey = request.DisplayedCargoEntityKey ?? "", DisplayedCargoIndex = request.DisplayedCargoIndex, CanSaveLoadout = !string.IsNullOrWhiteSpace(request.DisplayedEntityKey) }; } static string ViewportToken(double value) { return value .ToString("0.###", CultureInfo.InvariantCulture) .Replace('-', 'n') .Replace('.', 'p'); } } private CultMeshMutableStatePointer MutableDocumentPointer(CultRecordKey key) where T : class { return CultMesh.MutableStatePointer( key.ToString(), _ => Database.GetAsync(key), _ => Database.WatchRecord(key) .Where(change => change.Document != null) .Select(change => change.Document!), async (_, value) => { await Database.PutAsync(key, value).ConfigureAwait(false); }, sources: new[] { CultMesh.ProjectionSource(key.ToString()) }); } private static CultMeshProjectionSource CatalogBootstrapSource(string sourceId) { return CultMesh.ProjectionSource( sourceId, description: "managed Aetheria document bootstrap seed"); } private AetheriaRuntimeCatalogSnapshot BootstrapRuntimeCatalogSnapshot() { return AetheriaRuntimeCatalogStore.OpenReadOnly(StatePath); } public IReadOnlyList CommittedCommandFacts() { ThrowIfDisposed(); return Cache.GetAll().ToArray(); } private AetheriaRuntimeLoadoutTemplatesDocument BootstrapLoadoutTemplatesDocument() { return new AetheriaRuntimeLoadoutTemplatesDocument( AetheriaRuntimeCatalogStore.ReadLoadoutTemplates(StatePath)); } private AetheriaRuntimePlayerSettingsDocument BootstrapPlayerSettingsDocument() { return AetheriaRuntimePlayerSettingsDocument.FromSnapshot( AetheriaRuntimeCatalogStore.ReadPlayerSettings(StatePath)); } private AetheriaRuntimeVerseHostSettingsDocument BootstrapVerseHostSettingsDocument() { return AetheriaRuntimeVerseHostSettingsDocument.FromSnapshot( AetheriaRuntimeCatalogStore.ReadVerseHostSettings(StatePath)); } } }