using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using GameCult.Aetheria.State.Unity; using GameCult.Eve.Surface; using GameCult.Eve.UnityUIToolkit; using TMPro; using UniRx; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; using UnityEngine.UIElements; using static Unity.Mathematics.math; using float2 = Unity.Mathematics.float2; using Random = UnityEngine.Random; public class MainMenu : MonoBehaviour { private const string MenuSurfaceType = "surface-state"; private const string MenuSurfaceSchema = "gamecult.eve.surface.v1"; private const string MenuSurfaceProviderId = "aetheria"; private const string MenuSurfaceProviderKind = "game.menu"; private const string MainSurfaceId = "aetheria.main_menu.root"; private const string SettingsSurfaceId = "aetheria.main_menu.settings"; private const string InputSettingsSurfaceId = "aetheria.main_menu.input_settings"; private const string PlayerSettingsShellSurfaceId = "aetheria.main_menu.player_settings"; private const string VerseSettingsShellSurfaceId = "aetheria.main_menu.verse_settings"; private const string ContinueRunCommand = "aetheria.main_menu.root.continue"; private const string NewGameCommand = "aetheria.main_menu.root.new_game"; private const string ShowSettingsCommand = "aetheria.main_menu.root.show_settings"; private const string QuitCommand = "aetheria.main_menu.root.quit"; private const string OpenRuntimeInputScreenCommand = "aetheria.main_menu.input_settings.open_runtime_screen"; private const string ShowPlayerSettingsCommand = "aetheria.main_menu.settings.show_player_settings"; private const string ShowVerseSettingsCommand = "aetheria.main_menu.settings.show_verse_settings"; private const string ShowInputSettingsCommand = "aetheria.main_menu.settings.show_input_settings"; private const string BackToMainCommand = "aetheria.main_menu.settings.back_to_main"; private const string BackToSettingsCommand = "aetheria.main_menu.settings.back_to_settings"; public VolumeCloudRenderer CloudRenderer; public GameSettings Settings; public ConfirmationDialog Dialog; public bool InGame; private UIDocument _menuSurfaceDocument; void Start() { ShowMain(); } private void ShowMain() { var stateBoot = CurrentStateBoot(); RenderMenuSurface( BuildMainSurfaceDefinition( stateBoot, LatestContinueRun(stateBoot), LatestVerseHostSettings(stateBoot), InGame), HandleMainSurfaceCommand); } private void HandleMainSurfaceCommand(EveSurfaceCommandRequest request) { switch (request.Command) { case ContinueRunCommand: var continueRun = LatestContinueRun(CurrentStateBoot()); if (continueRun == null) { Debug.LogWarning("Main-menu Continue requested without a typed run state."); ShowMain(); return; } ContinueGame(continueRun); return; case NewGameCommand: StartNewGame(); return; case ShowSettingsCommand: ShowSettings(); return; case QuitCommand: Application.Quit(); return; default: Debug.LogWarning($"Unknown main menu command: {request.Command}"); return; } } private void StartNewGame() { var generatorState = "Loading typed catalog"; Action setState = s => generatorState = s; HideMenuSurface(); Dialog.Clear(); Dialog.Title.text = "Generating Galaxy"; Dialog.AddProperty(() => generatorState); Dialog.Show(); if (ActionGameManager.RuntimePlayerSettings.TutorialPassed) { Settings.SectorBackgroundSettings.NoisePosition = Random.value * 1000; ActionGameManager.IsTutorial = false; var generationSeed = NextGenerationSeed(); Task.Run(() => { var sector = new Galaxy( Settings.SectorGenerationSettings, Settings.SectorBackgroundSettings, Settings.NameGeneratorSettings, ActionGameManager.RuntimeCatalog, Debug.Log, setState, generationSeed); Observable.NextFrame().Subscribe(_ => { ActionGameManager.CurrentGalaxy = sector; SceneManager.LoadScene("ARPG"); }); }).WrapErrors(); } else { int iteration = 1; do { Settings.TutorialBackgroundSettings.NoisePosition = Random.value * 1000; setState($"Finding Galaxy Position: iteration {iteration++}"); } while (Settings.TutorialBackgroundSettings.CloudDensity(float2(0.5f)) < .5f); ActionGameManager.IsTutorial = true; var generationSeed = NextGenerationSeed(); Task.Run(() => { var sector = new Galaxy( Settings.TutorialGenerationSettings, Settings.TutorialBackgroundSettings, Settings.NameGeneratorSettings, ActionGameManager.RuntimeCatalog, ActionGameManager.RuntimePlayerSettings, ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"), Debug.Log, setState, generationSeed); Observable.NextFrame().Subscribe(_ => { ActionGameManager.CurrentGalaxy = sector; SceneManager.LoadScene("ARPG"); }); }).WrapErrors(); } } private static uint NextGenerationSeed() { var seed = (uint)Random.Range(1, int.MaxValue); return seed == 0 ? 1u : seed; } private static AetheriaRuntimeStateBootReport CurrentStateBoot() { return AetheriaRuntimeStateBoot.Inspect(ActionGameManager.GameDataDirectory); } private static AetheriaRuntimeRunStateSnapshot LatestContinueRun(AetheriaRuntimeStateBootReport stateBoot) { if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists) return null; try { return AetheriaRuntimeStateReader .ReadRunStates(stateBoot.StateFilePath) .Where(run => !string.IsNullOrWhiteSpace(run.RunId)) .OrderByDescending(run => run.UpdatedAtUtc, StringComparer.Ordinal) .ThenByDescending(run => run.RunId, StringComparer.Ordinal) .FirstOrDefault(); } catch (Exception ex) { Debug.LogError($"Failed to read typed Aetheria run state for Continue: {ex}"); return null; } } private static AetheriaRuntimeVerseHostSettingsSnapshot LatestVerseHostSettings(AetheriaRuntimeStateBootReport stateBoot) { if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists) return null; try { return AetheriaRuntimeStateReader.ReadVerseHostSettings(stateBoot.StateFilePath); } catch (Exception ex) { Debug.LogError($"Failed to read typed Aetheria Verse host settings for the main menu: {ex}"); return null; } } private void ContinueGame(AetheriaRuntimeRunStateSnapshot run) { if (run == null) return; var generatorState = "Loading typed run"; Action setState = s => generatorState = s; HideMenuSurface(); Dialog.Clear(); Dialog.Title.text = "Continuing Run"; Dialog.AddProperty(() => generatorState); Dialog.Show(); ActionGameManager.IsTutorial = run.IsTutorial; ActionGameManager.ContinueRunState = run; var generationSeed = run.GenerationSeed == 0 ? NextGenerationSeed() : run.GenerationSeed; var backgroundSettings = run.IsTutorial ? Settings.TutorialBackgroundSettings : Settings.SectorBackgroundSettings; backgroundSettings.NoisePosition = generationSeed; Task.Run(() => { var sector = run.IsTutorial ? new Galaxy( Settings.TutorialGenerationSettings, Settings.TutorialBackgroundSettings, Settings.NameGeneratorSettings, ActionGameManager.RuntimeCatalog, ActionGameManager.RuntimePlayerSettings, ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"), Debug.Log, setState, generationSeed) : new Galaxy( Settings.SectorGenerationSettings, backgroundSettings, Settings.NameGeneratorSettings, ActionGameManager.RuntimeCatalog, Debug.Log, setState, generationSeed); Observable.NextFrame().Subscribe(_ => { ActionGameManager.CurrentGalaxy = sector; SceneManager.LoadScene("ARPG"); }); }).WrapErrors(); } private void ShowSettings() { RenderMenuSurface(BuildSettingsSurfaceDefinition(), HandleSettingsSurfaceCommand); } private void ShowInputSettings() { RenderMenuSurface(BuildInputSettingsSurfaceDefinition(CanOpenRuntimeInputScreen(), InGame), HandleInputSettingsSurfaceCommand); } private void ShowPlayerSettingsSurface() { RenderMenuSurface( WithBackAction( ToEveSurfaceDocument(BuildPlayerSettingsSurfaceDefinition()), PlayerSettingsShellSurfaceId, BackToSettingsCommand, "Back"), HandlePlayerSettingsSurfaceCommand); } private void ShowVerseSettingsSurface() { RenderMenuSurface( WithBackAction( ToEveSurfaceDocument(BuildVerseSettingsSurfaceDefinition()), VerseSettingsShellSurfaceId, BackToSettingsCommand, "Back"), HandleVerseSettingsSurfaceCommand); } private void RenderMenuSurface( EveSurfaceDocument document, Action commandHandler) { var surfaceDocument = ResolveMenuSurfaceDocument(); surfaceDocument.gameObject.SetActive(true); var root = surfaceDocument.rootVisualElement; root.Clear(); root.style.flexGrow = 1; root.style.justifyContent = Justify.Center; root.style.alignItems = Align.Center; root.style.paddingLeft = 24; root.style.paddingRight = 24; root.style.paddingTop = 24; root.style.paddingBottom = 24; root.style.backgroundColor = new Color(0f, 0f, 0f, 0.6f); var shell = new VisualElement(); shell.style.flexDirection = FlexDirection.Column; shell.style.width = 560; shell.style.maxWidth = 560; shell.style.paddingLeft = 20; shell.style.paddingRight = 20; shell.style.paddingTop = 20; shell.style.paddingBottom = 20; shell.style.backgroundColor = new Color(0.08f, 0.1f, 0.14f, 0.96f); root.Add(shell); var lowerer = new EveUiToolkitSurfaceLowerer(); shell.Add(lowerer.Lower(document, commandHandler)); } private void HandleSettingsSurfaceCommand(EveSurfaceCommandRequest request) { switch (request.Command) { case ShowPlayerSettingsCommand: ShowPlayerSettingsSurface(); return; case ShowVerseSettingsCommand: ShowVerseSettingsSurface(); return; case ShowInputSettingsCommand: ShowInputSettings(); return; case BackToMainCommand: ShowMain(); return; default: Debug.LogWarning($"Unknown settings menu command: {request.Command}"); return; } } private void HandleInputSettingsSurfaceCommand(EveSurfaceCommandRequest request) { if (string.Equals(request.Command, BackToSettingsCommand, StringComparison.Ordinal)) { ShowSettings(); return; } if (string.Equals(request.Command, OpenRuntimeInputScreenCommand, StringComparison.Ordinal)) { if (!TryOpenRuntimeInputScreen()) { ShowInputSettings(); } return; } Debug.LogWarning($"Unknown input settings command: {request.Command}"); } private void HandlePlayerSettingsSurfaceCommand(EveSurfaceCommandRequest request) { if (string.Equals(request.Command, BackToSettingsCommand, StringComparison.Ordinal)) { ShowSettings(); return; } if (!ActionGameManager.CommitRuntimePlayerSettingsCommand(request.Command, request.Payload)) { Debug.LogWarning($"Unknown player-settings command: {request.Command}"); return; } CloudRenderer.quality = ActionGameManager.RuntimePlayerSettings.GraphicsSettings.NebulaQuality; ShowPlayerSettingsSurface(); } private void HandleVerseSettingsSurfaceCommand(EveSurfaceCommandRequest request) { if (string.Equals(request.Command, BackToSettingsCommand, StringComparison.Ordinal)) { ShowSettings(); return; } if (TryCommitClientTargetCommand(request.Command, request.Payload)) { ShowVerseSettingsSurface(); return; } if (TryQueueVerseHostCommand(request.Command, request.Payload)) { ShowVerseSettingsSurface(); return; } Debug.LogWarning($"Unknown verse-settings command: {request.Command}"); } private void HideMenuSurface() { if (_menuSurfaceDocument == null) return; _menuSurfaceDocument.rootVisualElement.Clear(); _menuSurfaceDocument.gameObject.SetActive(false); } private UIDocument ResolveMenuSurfaceDocument() { if (_menuSurfaceDocument != null) return _menuSurfaceDocument; var host = new GameObject("Aetheria Menu Surface"); host.transform.SetParent(transform, false); var document = host.AddComponent(); document.sortingOrder = 1000; host.SetActive(false); _menuSurfaceDocument = document; return document; } private static AetheriaRuntimeSurfaceDocument BuildPlayerSettingsSurfaceDefinition() { return AetheriaRuntimePlayerSettingsSurfaceBuilder.Build( new AetheriaRuntimePlayerSettingsSurfaceState( ActionGameManager.RuntimePlayerSettings.Name, ActionGameManager.RuntimePlayerSettings.TutorialPassed, ActionGameManager.ContinueRunState?.RunId ?? "", ActionGameManager.RuntimePlayerSettings.GameplaySettings.TemperatureUnit.ToString(), max(0, ActionGameManager.RuntimePlayerSettings.GameplaySettings.SignificantDigits), ActionGameManager.RuntimePlayerSettings.GraphicsSettings.NebulaQuality.ToString(), ActionGameManager.RuntimePlayerSettings.GraphicsSettings.ShowAsteroidsInMinimap, DateTime.UtcNow.ToString("O"))); } private static AetheriaRuntimeSurfaceDocument BuildVerseSettingsSurfaceDefinition() { var stateBoot = CurrentStateBoot(); var verseHost = LatestVerseHostSettings(stateBoot); return AetheriaRuntimeClientTargetSurfaceBuilder.Build( new AetheriaRuntimeClientTargetSurfaceState( stateBoot.TargetKind, stateBoot.Title, stateBoot.VerseId, stateBoot.CultMeshAddress, stateBoot.StateFilePath, stateBoot.TargetSource, stateBoot.SupportsLocalStateFileRead, stateBoot.FailureMessage, verseHost?.Title ?? stateBoot.Title, verseHost?.VerseId ?? stateBoot.VerseId, verseHost?.Visibility ?? "unknown", verseHost?.CultMeshAddress ?? stateBoot.CultMeshAddress, DateTime.UtcNow.ToString("O"))); } private static EveSurfaceDocument BuildMainSurfaceDefinition( AetheriaRuntimeStateBootReport stateBoot, AetheriaRuntimeRunStateSnapshot continueRun, AetheriaRuntimeVerseHostSettingsSnapshot verseHost, bool inGame) { var commands = new List(); var actionButtons = new List(); var cardChildren = new List(); var targetLabel = stateBoot.TargetLabel; var targetKind = string.IsNullOrWhiteSpace(stateBoot.TargetKind) ? "unknown" : stateBoot.TargetKind; var targetSource = string.IsNullOrWhiteSpace(stateBoot.TargetSource) ? "unknown" : stateBoot.TargetSource; var verseTitle = string.IsNullOrWhiteSpace(verseHost?.Title) ? "Unknown Verse" : verseHost.Title; var verseId = string.IsNullOrWhiteSpace(verseHost?.VerseId) ? "unknown" : verseHost.VerseId; var verseVisibility = string.IsNullOrWhiteSpace(verseHost?.Visibility) ? "unknown" : verseHost.Visibility; var verseMeshAddress = string.IsNullOrWhiteSpace(verseHost?.CultMeshAddress) ? "unpublished" : verseHost.CultMeshAddress; var verseLabel = string.Equals(verseTitle, verseId, StringComparison.Ordinal) ? verseTitle : $"{verseTitle} ({verseId})"; if (!inGame) { if (!stateBoot.SupportsLocalStateFileRead) { cardChildren.Add(Text( $"{MainSurfaceId}.continue.note", stateBoot.FailureMessage)); } else if (continueRun == null) { cardChildren.Add(Text( $"{MainSurfaceId}.continue.note", $"No typed run state is available in {verseLabel} yet. Start a new galaxy or connect to a Verse that already has one.")); } else { commands.Add(new EveCommandTemplate(ContinueRunCommand, "Continue", "unity-uitoolkit")); actionButtons.Add(Button($"{MainSurfaceId}.continue", "Continue", ContinueRunCommand)); cardChildren.Add(Metric( $"{MainSurfaceId}.continue.run", "Latest Run", continueRun.RunId)); } } cardChildren.Add(Metric( $"{MainSurfaceId}.target.title", "Client Target", targetLabel)); cardChildren.Add(Metric( $"{MainSurfaceId}.target.transport", "Transport", targetKind)); cardChildren.Add(Metric( $"{MainSurfaceId}.target.source", "Target Source", targetSource)); cardChildren.Add(Metric( $"{MainSurfaceId}.verse.title", "Verse", verseLabel)); cardChildren.Add(Metric( $"{MainSurfaceId}.verse.visibility", "Visibility", verseVisibility)); cardChildren.Add(Metric( $"{MainSurfaceId}.verse.mesh", "CultMesh", verseMeshAddress)); commands.Add(new EveCommandTemplate(NewGameCommand, "New Game", "unity-uitoolkit")); commands.Add(new EveCommandTemplate(ShowSettingsCommand, "Settings", "unity-uitoolkit")); commands.Add(new EveCommandTemplate(QuitCommand, "Quit", "unity-uitoolkit")); actionButtons.Add(Button($"{MainSurfaceId}.newGame", "New Game", NewGameCommand)); actionButtons.Add(Button($"{MainSurfaceId}.settings", "Settings", ShowSettingsCommand)); actionButtons.Add(Button($"{MainSurfaceId}.quit", "Quit", QuitCommand)); cardChildren.Add(Text( $"{MainSurfaceId}.note", $"The client lowers this shell through Eve. The client target chooses which Verse it follows; game truth belongs to the daemon serving {verseLabel}.")); cardChildren.Add(ButtonColumn($"{MainSurfaceId}.actions", actionButtons.ToArray())); return BuildMenuSurfaceDocument( MainSurfaceId, "Aetheria Terminus", commands, Card( $"{MainSurfaceId}.card", "Aetheria Terminus", cardChildren.ToArray())); } private static EveSurfaceDocument BuildSettingsSurfaceDefinition() { return BuildMenuSurfaceDocument( SettingsSurfaceId, "Aetheria Settings", new[] { new EveCommandTemplate(ShowPlayerSettingsCommand, "Player Settings", "unity-uitoolkit"), new EveCommandTemplate(ShowVerseSettingsCommand, "Verse", "unity-uitoolkit"), new EveCommandTemplate(ShowInputSettingsCommand, "Input", "unity-uitoolkit"), new EveCommandTemplate(BackToMainCommand, "Back", "unity-uitoolkit") }, Card( "aetheria.mainMenu.settings.card", "Settings", Text( "aetheria.mainMenu.settings.note", "Player settings, client target selection, and input rebinding all lower through typed Eve surfaces. Audio still has no typed owner."), ButtonRow( "aetheria.mainMenu.settings.actions", Button("aetheria.mainMenu.settings.playerSettings", "Player Settings", ShowPlayerSettingsCommand), Button("aetheria.mainMenu.settings.verse", "Verse", ShowVerseSettingsCommand), Button("aetheria.mainMenu.settings.input", "Input", ShowInputSettingsCommand), Button("aetheria.mainMenu.settings.back", "Back", BackToMainCommand)))); } private bool CanOpenRuntimeInputScreen() { return InGame && ActionGameManager.Instance != null && ActionGameManager.Instance.CanShowInputScreenFromMenu() && ActionGameManager.Instance.InputDisplayLayout != null; } private bool TryOpenRuntimeInputScreen() { if (!CanOpenRuntimeInputScreen()) return false; HideMenuSurface(); gameObject.SetActive(false); ActionGameManager.Instance.ShowInputScreenFromMenu(); return true; } private static bool TryCommitClientTargetCommand(string command, IReadOnlyDictionary payload) { if (!AetheriaRuntimeClientTargetCommands.IsKnown(command)) return false; if (string.Equals(command, AetheriaRuntimeClientTargetCommands.Refresh, StringComparison.Ordinal)) return true; try { var gameDataDirectory = ActionGameManager.GameDataDirectory; var clientTargetPath = AetheriaRuntimeStateBoundary.GetClientTargetPath(gameDataDirectory); var defaultStateFilePath = AetheriaRuntimeStateBoundary.GetStateFilePath(gameDataDirectory); AetheriaRuntimeClientTargetStore.Update( clientTargetPath, defaultStateFilePath, document => { switch (command) { case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.CycleTargetKind, StringComparison.Ordinal): document.TargetKind = string.Equals(document.TargetKind, AetheriaRuntimeClientTargetKinds.CultMeshVerse, StringComparison.Ordinal) ? AetheriaRuntimeClientTargetKinds.StateFile : AetheriaRuntimeClientTargetKinds.CultMeshVerse; break; case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetTitle, StringComparison.Ordinal): document.Title = ReadPayloadValue(payload, "value"); break; case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetVerseId, StringComparison.Ordinal): document.VerseId = ReadPayloadValue(payload, "value"); break; case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetCultMeshAddress, StringComparison.Ordinal): document.CultMeshAddress = ReadPayloadValue(payload, "value"); break; case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetStateFilePath, StringComparison.Ordinal): document.StateFilePath = ReadPayloadValue(payload, "value"); break; } if (string.IsNullOrWhiteSpace(document.TargetKind)) document.TargetKind = AetheriaRuntimeClientTargetKinds.StateFile; if (string.IsNullOrWhiteSpace(document.StateFilePath)) document.StateFilePath = defaultStateFilePath; document.UpdatedAtUtc = DateTime.UtcNow.ToString("O"); }); return true; } catch (Exception ex) { Debug.LogError($"Failed to update typed Aetheria client target: {ex}"); return true; } } private static bool TryQueueVerseHostCommand(string command, IReadOnlyDictionary payload) { if (!AetheriaRuntimeVerseHostCommands.IsKnown(command)) return false; var stateBoot = CurrentStateBoot(); if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists) { Debug.LogWarning( $"Cannot queue Verse-host command '{command}' because the active target is not a readable local Verse state file."); return true; } try { var request = new EveSurfaceCommandRequest( "aetheria", AetheriaRuntimeVerseHostCommands.SurfaceId, command, payload?.ToDictionary(entry => entry.Key, entry => entry.Value ?? "", StringComparer.Ordinal) ?? new Dictionary(StringComparer.Ordinal), DateTimeOffset.UtcNow, "unity-main-menu"); var envelope = AetheriaRuntimeEveCommandLog.QueueCommand(stateBoot.StateFilePath, request); Debug.Log($"Queued Aetheria Verse-host Eve command: {envelope.Path}"); return true; } catch (Exception ex) { Debug.LogError($"Failed to queue typed Aetheria Verse-host Eve command: {ex}"); return true; } } private static string ReadPayloadValue(IReadOnlyDictionary payload, string key) { return payload != null && payload.TryGetValue(key, out var value) ? value ?? "" : ""; } private static EveSurfaceDocument BuildInputSettingsSurfaceDefinition(bool canOpenRuntimeInputScreen, bool inGame) { var commands = new List { new EveCommandTemplate(BackToSettingsCommand, "Back", "unity-uitoolkit") }; var cardChildren = new List { Metric( "aetheria.mainMenu.input.bindingOverrides", "Binding Overrides", ActionGameManager.RuntimePlayerSettings.InputSettings.InputActionMap.Count.ToString()), Metric( "aetheria.mainMenu.input.actionBarInputs", "Action-Bar Inputs", ActionGameManager.RuntimePlayerSettings.InputSettings.ActionBarInputs.Count.ToString()) }; if (canOpenRuntimeInputScreen) { commands.Insert(0, new EveCommandTemplate(OpenRuntimeInputScreenCommand, "Open Remap Screen", "unity-uitoolkit")); cardChildren.Add(Text( "aetheria.mainMenu.input.note", "The runtime Eve input screen owns low-level InputSystem rebinding and action-bar input edits. This title shell reports typed player-settings state and hands off to that owner.")); } else if (inGame) { cardChildren.Add(Text( "aetheria.mainMenu.input.note", "The runtime Eve input screen should own rebinding here, but this scene has no active input surface to hand off to.")); } else { cardChildren.Add(Text( "aetheria.mainMenu.input.note", "This title shell reports the typed player-settings state. Launch a run to open the runtime Eve input screen that owns low-level InputSystem rebinding.")); } var buttons = new List(); if (canOpenRuntimeInputScreen) { buttons.Add(Button("aetheria.mainMenu.input.openRuntimeScreen", "Open Remap Screen", OpenRuntimeInputScreenCommand)); } buttons.Add(Button("aetheria.mainMenu.input.back", "Back", BackToSettingsCommand)); cardChildren.Add(ButtonRow("aetheria.mainMenu.input.actions", buttons.ToArray())); return BuildMenuSurfaceDocument( InputSettingsSurfaceId, "Aetheria Input Settings", commands, Card( "aetheria.mainMenu.input.card", "Input Settings", cardChildren.ToArray())); } private static EveSurfaceDocument WithBackAction( EveSurfaceDocument document, string surfaceId, string backCommand, string backLabel) { return new EveSurfaceDocument( document.Type, document.Schema, document.ProviderId, document.ProviderKind, document.Title, document.Version, document.UpdatedAtUtc, new EveSurfaceTree( surfaceId, Node( $"{surfaceId}.root", "surface", Array.Empty<(string Key, string Value)>(), document.Surface.Root, ButtonRow( $"{surfaceId}.actions", Button($"{surfaceId}.back", backLabel, backCommand))), document.Surface.Styles), document.Commands .Concat(new[] { new EveCommandTemplate(backCommand, backLabel, "unity-uitoolkit") }) .ToArray()); } private static EveSurfaceDocument BuildMenuSurfaceDocument( string surfaceId, string title, IReadOnlyList commands, params EveSurfaceComponent[] children) { return new EveSurfaceDocument( MenuSurfaceType, MenuSurfaceSchema, MenuSurfaceProviderId, MenuSurfaceProviderKind, title, version: 1, DateTime.UtcNow.ToString("O"), new EveSurfaceTree( surfaceId, Node($"{surfaceId}.root", "surface", Array.Empty<(string Key, string Value)>(), children), Array.Empty()), commands); } private static EveSurfaceComponent Card( string id, string title, params EveSurfaceComponent[] children) { return Node(id, "card", new[] { ("title", title) }, children); } private static EveSurfaceComponent Metric(string id, string label, string value) { return Node(id, "metric", new[] { ("label", label), ("value", value) }); } private static EveSurfaceComponent Text(string id, string value) { return Node(id, "text", new[] { ("value", value) }); } private static EveSurfaceComponent Button(string id, string label, string command) { return Node(id, "control.button", new[] { ("label", label), ("command", command) }); } private static EveSurfaceComponent ButtonRow( string id, params EveSurfaceComponent[] children) { return Node(id, "row", Array.Empty<(string Key, string Value)>(), children); } private static EveSurfaceComponent ButtonColumn( string id, params EveSurfaceComponent[] children) { return Node(id, "column", Array.Empty<(string Key, string Value)>(), children); } private static EveSurfaceComponent Node( string id, string kind, IEnumerable<(string Key, string Value)> props, params EveSurfaceComponent[] children) { return new EveSurfaceComponent( id, kind, props.ToDictionary(prop => prop.Key, prop => prop.Value, StringComparer.Ordinal), children ?? Array.Empty()); } private static EveSurfaceDocument ToEveSurfaceDocument(AetheriaRuntimeSurfaceDocument document) { return new EveSurfaceDocument( "surface-state", "gamecult.eve.surface.v1", document.ProviderId, document.ProviderKind, document.Title, document.Version, document.UpdatedAtUtc, new EveSurfaceTree( document.Surface.Id, ToEveSurfaceComponent(document.Surface.Root), document.Surface.Styles .Select(style => new EveStyleToken(style.Name, style.Value)) .ToArray()), document.Commands .Select(command => new EveCommandTemplate(command.Command, command.Label, command.Transport)) .ToArray()); } private static EveSurfaceComponent ToEveSurfaceComponent(AetheriaRuntimeSurfaceComponent component) { return new EveSurfaceComponent( component.Id, component.Kind, new Dictionary(component.Props, StringComparer.Ordinal), component.Children.Select(ToEveSurfaceComponent).ToArray()); } private void OnDestroy() { if (_menuSurfaceDocument != null) { Destroy(_menuSurfaceDocument.gameObject); _menuSurfaceDocument = null; } } }