diff --git a/Assets/Scripts/UI/MainMenu.cs b/Assets/Scripts/UI/MainMenu.cs index 5a54e5d3f..7bf687329 100644 --- a/Assets/Scripts/UI/MainMenu.cs +++ b/Assets/Scripts/UI/MainMenu.cs @@ -3,16 +3,12 @@ using System.Collections.Generic; using GameCult.Aetheria.EveRuntime; using GameCult.Aetheria.State.Verse; using GameCult.Eve.Surface; -using GameCult.Mesh; -using TMPro; using UniRx; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; using UnityEngine.UIElements; using UiButton = UnityEngine.UIElements.Button; -using TextCoreFontAsset = UnityEngine.TextCore.Text.FontAsset; -using GlyphRenderMode = UnityEngine.TextCore.LowLevel.GlyphRenderMode; public class MainMenu : MonoBehaviour { @@ -35,8 +31,6 @@ public class MainMenu : MonoBehaviour private bool _fading; private float _fadeLerp; private readonly List _hoverButtons = new List(); - private static TextCoreFontAsset _titleFont; - private static TextCoreFontAsset _labelFont; private readonly AetheriaEveUnitySurfaceChrome _menuSurfaceChrome = new AetheriaEveUnitySurfaceChrome { UseShell = false, @@ -63,19 +57,8 @@ public class MainMenu : MonoBehaviour private void ShowMain(bool animateFromRight = true) { - var stateBoot = CurrentStateBoot(); - var sectorMap = ResolveSectorMap(stateBoot); - var verseHost = ResolveVerseHostSettings(stateBoot); - var playerSettings = ResolvePlayerSettings(stateBoot); RenderMenuSurface( - AetheriaRuntimeMainMenuSurfaceBuilder.BuildRoot( - stateBoot, - sectorMap, - verseHost, - playerSettings, - CanOpenRuntimeInputScreen(), - InGame, - DateTime.UtcNow.ToString("O")), + ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.RootSurfaceId), HandleMainSurfaceCommand, animateFromRight); } @@ -177,44 +160,6 @@ public class MainMenu : MonoBehaviour } } - private AetheriaRuntimePlayerSettingsDocument ResolvePlayerSettings(AetheriaRuntimeStateBootReport stateBoot) - { - if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists) - return null; - - try - { - return AetheriaUnityRuntimeClientProvider - .RuntimeState(stateBoot, "unity-main-menu") - .PlayerSettings - .Latest(); - } - catch (Exception ex) - { - Debug.LogError($"Failed to bind typed Aetheria player settings for the main menu: {ex}"); - return null; - } - } - - private AetheriaRuntimeVerseHostSettingsDocument ResolveVerseHostSettings(AetheriaRuntimeStateBootReport stateBoot) - { - if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists) - return null; - - try - { - return AetheriaUnityRuntimeClientProvider - .RuntimeState(stateBoot, "unity-main-menu") - .VerseHostSettings - .Latest(); - } - catch (Exception ex) - { - Debug.LogError($"Failed to bind typed Aetheria Verse host settings for the main menu: {ex}"); - return null; - } - } - private static bool IsMissingDaemonFrame(KeyNotFoundException ex) { return ex?.Message?.Contains("daemon:aetheria.frame.latest.v1", StringComparison.Ordinal) == true; @@ -231,21 +176,15 @@ public class MainMenu : MonoBehaviour private void ShowSettings(bool animateFromRight = true) { RenderMenuSurface( - AetheriaRuntimeMainMenuSurfaceBuilder.BuildSettings(DateTime.UtcNow.ToString("O")), + ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.SettingsSurfaceId), HandleSettingsSurfaceCommand, animateFromRight); } private void ShowInputSettings(bool animateFromRight = true) { - var stateBoot = CurrentStateBoot(); RenderMenuSurface( - AetheriaRuntimeMainMenuSurfaceBuilder.BuildInputSettings( - stateBoot, - ResolvePlayerSettings(stateBoot), - CanOpenRuntimeInputScreen(), - InGame, - DateTime.UtcNow.ToString("O")), + ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.InputSettingsSurfaceId), HandleInputSettingsSurfaceCommand, animateFromRight); } @@ -253,26 +192,28 @@ public class MainMenu : MonoBehaviour private void ShowPlayerSettingsSurface(bool animateFromRight = true) { RenderMenuSurface( - AetheriaRuntimeMainMenuSurfaceBuilder.BuildPlayerSettingsShell( - ResolvePlayerSettings(CurrentStateBoot()), - DateTime.UtcNow.ToString("O")), + ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.PlayerSettingsShellSurfaceId), HandlePlayerSettingsSurfaceCommand, animateFromRight); } private void ShowVerseSettingsSurface(bool animateFromRight = true) { - var stateBoot = CurrentStateBoot(); RenderMenuSurface( - AetheriaRuntimeMainMenuSurfaceBuilder.BuildVerseSettingsShell( - AetheriaRuntimeClientTargetSurfaceBuilder.Build( - stateBoot, - ResolveVerseHostSettings(stateBoot), - DateTime.UtcNow.ToString("O"))), + ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.VerseSettingsShellSurfaceId), HandleVerseSettingsSurfaceCommand, animateFromRight); } + private AetheriaRuntimeSurfaceDocument ResolveMainMenuSurface(string surfaceId) + { + var stateBoot = CurrentStateBoot(); + return AetheriaUnityRuntimeClientProvider + .RuntimeState(stateBoot, "unity-main-menu") + .MainMenuSurface(surfaceId, CanOpenRuntimeInputScreen(), InGame) + .Latest(); + } + private void RenderMenuSurface( AetheriaRuntimeSurfaceDocument document, Action commandHandler, @@ -375,7 +316,6 @@ public class MainMenu : MonoBehaviour return; StyleMenuTree(root, rootMainMenu); - ApplyDefaultTextFont(root); var surface = rootMainMenu ? FindElement(root, $"{AetheriaRuntimeMainMenuCommands.RootSurfaceId}.root") @@ -391,7 +331,6 @@ public class MainMenu : MonoBehaviour var title = FindElement(root, $"{AetheriaRuntimeMainMenuCommands.RootSurfaceId}.title") as Label; if (title != null) { - title.style.unityFontDefinition = new StyleFontDefinition(TitleFont); title.style.fontSize = 86f; title.style.color = new Color(0.82f, 0.95f, 1f, 0.96f); title.style.unityFontStyleAndWeight = FontStyle.Normal; @@ -404,7 +343,6 @@ public class MainMenu : MonoBehaviour var subtitle = FindElement(root, $"{AetheriaRuntimeMainMenuCommands.RootSurfaceId}.subtitle") as Label; if (subtitle != null) { - subtitle.style.unityFontDefinition = new StyleFontDefinition(TitleFont); subtitle.style.fontSize = 39f; subtitle.style.color = new Color(0.82f, 0.95f, 1f, 0.92f); subtitle.style.unityFontStyleAndWeight = FontStyle.Normal; @@ -417,12 +355,6 @@ public class MainMenu : MonoBehaviour RegisterHoverButtons(root); } - private static void ApplyDefaultTextFont(VisualElement root) - { - root.Query().ForEach(element => - element.style.unityFontDefinition = new StyleFontDefinition(LabelFont)); - } - private void StyleMenuTree(VisualElement element, bool rootMainMenu) { if (element == null) @@ -508,7 +440,6 @@ public class MainMenu : MonoBehaviour private static void StyleMenuLabel(Label label, bool rootMainMenu) { label.style.color = new Color(0.86f, 0.98f, 1f, 0.94f); - label.style.unityFontDefinition = new StyleFontDefinition(LabelFont); label.style.unityFontStyleAndWeight = FontStyle.Normal; label.style.unityTextAlign = TextAnchor.MiddleLeft; label.style.whiteSpace = WhiteSpace.Normal; @@ -556,14 +487,12 @@ public class MainMenu : MonoBehaviour field.style.marginTop = 0f; field.style.marginBottom = rootMainMenu ? 4f : 8f; field.style.color = new Color(0.86f, 0.98f, 1f, 0.96f); - field.style.unityFontDefinition = new StyleFontDefinition(LabelFont); field.style.fontSize = rootMainMenu ? 18f : 15f; field.pickingMode = PickingMode.Position; field.labelElement.style.minWidth = rootMainMenu ? 100f : 190f; field.labelElement.style.width = rootMainMenu ? 100f : 190f; field.labelElement.style.color = new Color(0.78f, 0.92f, 0.98f, 0.84f); - field.labelElement.style.unityFontDefinition = new StyleFontDefinition(LabelFont); } private static void StyleMenuButton(UiButton button, bool rootMainMenu) @@ -574,7 +503,6 @@ public class MainMenu : MonoBehaviour button.style.borderTopWidth = 0f; button.style.borderBottomWidth = 0f; button.style.color = new Color(0.86f, 0.98f, 1f, 0.96f); - button.style.unityFontDefinition = new StyleFontDefinition(LabelFont); button.style.fontSize = rootMainMenu ? 24f : 16f; button.style.unityFontStyleAndWeight = FontStyle.Normal; button.style.unityTextAlign = TextAnchor.MiddleLeft; @@ -862,60 +790,6 @@ public class MainMenu : MonoBehaviour } } - private static TextCoreFontAsset TitleFont => - _titleFont ??= ResolveFontAsset( - "Assets/Fonts/Ubuntu/Montserrat-Thin SDF.asset", - "Montserrat Thin", - "Montserrat"); - - private static TextCoreFontAsset LabelFont => - _labelFont ??= ResolveFontAsset( - "Assets/Fonts/Ubuntu/Ubuntu-R SDF.asset", - "Ubuntu", - "Ubuntu Regular"); - - private static TextCoreFontAsset ResolveFontAsset(string assetPath, params string[] osFontNames) - { -#if UNITY_EDITOR - foreach (var asset in UnityEditor.AssetDatabase.LoadAllAssetsAtPath(assetPath)) - { - if (asset is TextCoreFontAsset editorFontAsset) - return editorFontAsset; - } -#endif - - foreach (var osFontName in osFontNames) - { - try - { - var fontAsset = TextCoreFontAsset.CreateFontAsset( - osFontName, - "Regular", - 90, - 9, - GlyphRenderMode.SDFAA); - if (fontAsset != null) - return fontAsset; - } - catch - { - // Try the next configured face name before falling back. - } - } - - Debug.LogWarning($"Failed to resolve UI font asset '{assetPath}'. Falling back to Arial."); - var fallback = TextCoreFontAsset.CreateFontAsset( - "Arial", - "Regular", - 90, - 9, - GlyphRenderMode.SDFAA); - if (fallback != null) - return fallback; - - throw new InvalidOperationException($"Failed to create fallback UI font asset for '{assetPath}'."); - } - private sealed class MenuHoverButton { public MenuHoverButton(UiButton button) diff --git a/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveUnitySurfaceHost.cs b/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveUnitySurfaceHost.cs index 1fbd449b3..bd69c28af 100644 --- a/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveUnitySurfaceHost.cs +++ b/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveUnitySurfaceHost.cs @@ -6,7 +6,9 @@ using GameCult.Eve.Surface; using GameCult.Eve.UnityUIToolkit; using GameCult.Mesh; using UnityEngine; +using UnityEngine.TextCore.LowLevel; using UnityEngine.UIElements; +using TextCoreFontAsset = UnityEngine.TextCore.Text.FontAsset; namespace GameCult.Aetheria.EveRuntime { @@ -42,6 +44,7 @@ namespace GameCult.Aetheria.EveRuntime public static class AetheriaEveUnitySurfaceHost { private static PanelSettings _runtimePanelSettings; + private static readonly Dictionary FontCache = new Dictionary(StringComparer.Ordinal); public static UIDocument Render( Transform owner, @@ -73,15 +76,20 @@ namespace GameCult.Aetheria.EveRuntime : null); surface = AetheriaRuntimeSurfaceDocuments.ResolveStateRefs(surface, effectiveStateRefResolver); var lowerer = new EveUiToolkitSurfaceLowerer(new EveUiToolkitSurfaceOptions(embeddedDocumentResolver)); + VisualElement lowered; if (chrome.UseShell) { var shell = CreateShell(chrome); root.Add(shell); - shell.Add(lowerer.Lower(surface, commandHandler)); + lowered = lowerer.Lower(surface, commandHandler); + ApplyTheme(lowered, surface); + shell.Add(lowered); } else { - root.Add(lowerer.Lower(surface, commandHandler)); + lowered = lowerer.Lower(surface, commandHandler); + ApplyTheme(lowered, surface); + root.Add(lowered); } Debug.Log( @@ -230,6 +238,75 @@ namespace GameCult.Aetheria.EveRuntime return shell; } + private static void ApplyTheme(VisualElement root, EveSurfaceDocument surface) + { + if (root == null || surface?.Surface?.Styles == null) + return; + + var tokens = surface.Surface.Styles + .Where(token => !string.IsNullOrWhiteSpace(token?.Name)) + .GroupBy(token => token.Name, StringComparer.Ordinal) + .ToDictionary(group => group.Key, group => group.Last().Value ?? "", StringComparer.Ordinal); + var bodyFont = ResolveFont(tokens, "body"); + var titleFont = ResolveFont(tokens, "title") ?? bodyFont; + + if (bodyFont != null) + { + root.Query().ForEach(element => + element.style.unityFontDefinition = new StyleFontDefinition(bodyFont)); + } + + if (titleFont != null) + { + root.Query(className: "eve-kind-text-title").ForEach(element => + element.style.unityFontDefinition = new StyleFontDefinition(titleFont)); + } + } + + private static TextCoreFontAsset ResolveFont(IReadOnlyDictionary tokens, string role) + { + if (tokens == null || string.IsNullOrWhiteSpace(role)) + return null; + + if (!tokens.TryGetValue($"font.{role}.family", out var family) || string.IsNullOrWhiteSpace(family)) + return null; + + tokens.TryGetValue($"font.{role}.style", out var style); + style = string.IsNullOrWhiteSpace(style) ? "Regular" : style; + var key = $"{family}\n{style}"; + if (FontCache.TryGetValue(key, out var cached)) + return cached; + + var font = TryCreateFontAsset(family, style) + ?? TryCreateFontAsset(family, "Regular") + ?? TryCreateFontAsset($"{family} {style}", "Regular"); + if (font == null) + { + Debug.LogWarning($"Aetheria Eve UI could not resolve runtime font '{family}' style '{style}'."); + return null; + } + + FontCache[key] = font; + return font; + } + + private static TextCoreFontAsset TryCreateFontAsset(string family, string style) + { + try + { + return TextCoreFontAsset.CreateFontAsset( + family, + string.IsNullOrWhiteSpace(style) ? "Regular" : style, + 90, + 9, + GlyphRenderMode.SDFAA); + } + catch + { + return null; + } + } + private static bool ContainsStateRefs(EveSurfaceDocument surface) { return surface?.Surface?.Root != null && ContainsStateRefs(surface.Surface.Root); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs index cf919ac06..b387bed6a 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs @@ -25,6 +25,7 @@ namespace GameCult.Aetheria.State.Verse private readonly Func> _zoneDetailsSurface; private readonly Func> _inventoryPanelSurface; private readonly Func> _inventoryDropdownSurface; + private readonly Func> _mainMenuSurface; private readonly Func> _selectedObject; private readonly Func> _inventory; private readonly Func> _starbridgePlayerSeat; @@ -63,6 +64,7 @@ namespace GameCult.Aetheria.State.Verse Func> zoneDetailsSurface, Func> inventoryPanelSurface, Func> inventoryDropdownSurface, + Func> mainMenuSurface, Func> selectedObject, Func> inventory, CultMeshDocumentHandle starbridgeScenario, @@ -99,6 +101,7 @@ namespace GameCult.Aetheria.State.Verse _zoneDetailsSurface = zoneDetailsSurface ?? throw new ArgumentNullException(nameof(zoneDetailsSurface)); _inventoryPanelSurface = inventoryPanelSurface ?? throw new ArgumentNullException(nameof(inventoryPanelSurface)); _inventoryDropdownSurface = inventoryDropdownSurface ?? throw new ArgumentNullException(nameof(inventoryDropdownSurface)); + _mainMenuSurface = mainMenuSurface ?? throw new ArgumentNullException(nameof(mainMenuSurface)); _selectedObject = selectedObject ?? throw new ArgumentNullException(nameof(selectedObject)); _inventory = inventory ?? throw new ArgumentNullException(nameof(inventory)); StarbridgeScenario = starbridgeScenario ?? throw new ArgumentNullException(nameof(starbridgeScenario)); @@ -255,6 +258,17 @@ namespace GameCult.Aetheria.State.Verse return _inventoryDropdownSurface(request ?? new AetheriaRuntimeInventoryDropdownSurfaceRequest()); } + public CultMeshDocumentHandle MainMenuSurface( + string surfaceId, + bool canOpenRuntimeInputScreen, + bool inGame) + { + if (string.IsNullOrWhiteSpace(surfaceId)) + throw new ArgumentException("Main menu surface id must be non-empty.", nameof(surfaceId)); + + return _mainMenuSurface(surfaceId, canOpenRuntimeInputScreen, inGame); + } + public CultMeshDocumentHandle SelectedObject(int zoneIndex) { if (zoneIndex < 0) diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMainMenuSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMainMenuSurfaceBuilder.cs index 03c234786..48f8dcf9d 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMainMenuSurfaceBuilder.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMainMenuSurfaceBuilder.cs @@ -27,6 +27,9 @@ namespace GameCult.Aetheria.State.Verse public static class AetheriaRuntimeMainMenuSurfaceBuilder { + public const string ProviderId = "aetheria"; + public const string ProviderKind = "game.menu"; + public static AetheriaRuntimeSurfaceDocument BuildRoot( AetheriaRuntimeStateBootReport stateBoot, AetheriaRuntimePlayerSettingsDocument playerSettings, @@ -327,18 +330,34 @@ namespace GameCult.Aetheria.State.Verse params AetheriaRuntimeSurfaceComponent[] children) { return new AetheriaRuntimeSurfaceDocument( - providerId: "aetheria", - providerKind: "game.menu", + providerId: ProviderId, + providerKind: ProviderKind, title: title, version: version, updatedAtUtc: updatedAtUtc, surface: new AetheriaRuntimeSurfaceTree( surfaceId, Node($"{surfaceId}.root", "surface", Array.Empty<(string Key, string Value)>(), children), - Array.Empty()), + MainMenuStyleTokens()), commands: commands); } + private static IReadOnlyList MainMenuStyleTokens() + { + return new[] + { + new AetheriaRuntimeSurfaceStyleToken("font.title.family", "Montserrat"), + new AetheriaRuntimeSurfaceStyleToken("font.title.style", "Thin"), + new AetheriaRuntimeSurfaceStyleToken("font.title.weight", "100"), + new AetheriaRuntimeSurfaceStyleToken("font.body.family", "Ubuntu"), + new AetheriaRuntimeSurfaceStyleToken("font.body.style", "Regular"), + new AetheriaRuntimeSurfaceStyleToken("font.body.weight", "400"), + new AetheriaRuntimeSurfaceStyleToken( + "font.web.google", + "https://fonts.googleapis.com/css2?family=Montserrat:wght@100&family=Ubuntu:wght@400&display=swap") + }; + } + private static AetheriaRuntimeSurfaceCommandTemplate Command(string command, string label) { return new AetheriaRuntimeSurfaceCommandTemplate(command, label, AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs index f650d8833..412b4c9a1 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs @@ -391,6 +391,11 @@ namespace GameCult.Aetheria.State.Verse () => 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( @@ -427,11 +432,7 @@ namespace GameCult.Aetheria.State.Verse catalogDocument, loadoutTemplatesDocument, playerSettingsDocument, - BootstrapCatalogDocument( - "aetheria.settings.verse_host", - () => Task.FromResult(BootstrapVerseHostSettingsDocument()), - AetheriaRuntimeVerseHostSettingsDocument.SchemaId, - CatalogBootstrapSource("catalog:aetheria.verse_host_settings")), + verseHostSettingsDocument, ManagedFrameDocument( "aetheria.current.zone", frame => Task.FromResult(AetheriaRuntimeRtsDocuments.CurrentZone(frame)), @@ -501,6 +502,10 @@ namespace GameCult.Aetheria.State.Verse 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(AetheriaRuntimeRtsDocuments.SelectedObject(frame, entityIndex)), @@ -608,6 +613,39 @@ namespace GameCult.Aetheria.State.Verse 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) { @@ -714,6 +752,99 @@ namespace GameCult.Aetheria.State.Verse 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, + TryCurrentSectorMap(), + 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.PlayerSettingsShellSurfaceId, StringComparison.Ordinal)) + { + return AetheriaRuntimeMainMenuSurfaceBuilder.BuildPlayerSettingsShell( + TryCurrentPlayerSettings(), + updatedAtUtc); + } + + if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.VerseSettingsShellSurfaceId, StringComparison.Ordinal)) + { + return AetheriaRuntimeMainMenuSurfaceBuilder.BuildVerseSettingsShell( + 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; + } + } + + AetheriaRuntimeSectorMapDocument? TryCurrentSectorMap() + { + try + { + var frame = latestFrameDocument.Latest(); + return frame == null ? null : AetheriaRuntimeRtsDocuments.SectorMap(frame); + } + catch (Exception) + { + return null; + } + } + static string IndexedDocumentId(string prefix, int index) { return $"{prefix}.{index.ToString(CultureInfo.InvariantCulture)}";