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.ReplicaStateFilePath, string.Join(", ", stateBoot.DiscoveryEndpoints ?? Array.Empty()), stateBoot.DiscoveredVerses ?? Array.Empty(), stateBoot.LastDiscoveryAtUtc, stateBoot.LastDiscoveryError, stateBoot.LastReplicaSyncAtUtc, stateBoot.LastReplicaSyncError, 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); if (string.Equals(command, AetheriaRuntimeClientTargetCommands.DiscoverVerses, StringComparison.Ordinal)) { AetheriaRuntimeVerseDiscovery.RefreshClientTarget(clientTargetPath, defaultStateFilePath); return true; } if (string.Equals(command, AetheriaRuntimeClientTargetCommands.SyncReplica, StringComparison.Ordinal)) { var target = AetheriaRuntimeClientTargetStore.ReadOrInitialize(clientTargetPath, defaultStateFilePath); var syncedAtUtc = DateTime.UtcNow.ToString("O"); try { var result = AetheriaRuntimeVerseReplicaBridge.Sync(gameDataDirectory, target); AetheriaRuntimeClientTargetStore.Update( clientTargetPath, defaultStateFilePath, document => { document.ReplicaStateFilePath = result.ReplicaStateFilePath; document.LastReplicaSyncAtUtc = syncedAtUtc; document.LastReplicaSyncError = ""; document.UpdatedAtUtc = syncedAtUtc; }); } catch (Exception ex) { AetheriaRuntimeClientTargetStore.Update( clientTargetPath, defaultStateFilePath, document => { document.LastReplicaSyncAtUtc = syncedAtUtc; document.LastReplicaSyncError = ex.Message ?? ex.GetType().Name; document.UpdatedAtUtc = syncedAtUtc; }); throw; } return true; } 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; document.LastReplicaSyncError = ""; 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"); document.ReplicaStateFilePath = AetheriaRuntimeStateBoundary.GetReplicaStateFilePath(gameDataDirectory, document.VerseId); document.LastReplicaSyncError = ""; break; case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetCultMeshAddress, StringComparison.Ordinal): document.CultMeshAddress = ReadPayloadValue(payload, "value"); document.LastReplicaSyncError = ""; break; case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetStateFilePath, StringComparison.Ordinal): document.StateFilePath = ReadPayloadValue(payload, "value"); break; case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetDiscoveryEndpoints, StringComparison.Ordinal): document.DiscoveryEndpoints = ParseDiscoveryEndpoints(ReadPayloadValue(payload, "value")); document.LastDiscoveryError = ""; break; case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SelectDiscoveredVerse, StringComparison.Ordinal): document.TargetKind = AetheriaRuntimeClientTargetKinds.CultMeshVerse; document.Title = ReadPayloadValue(payload, "title"); document.VerseId = ReadPayloadValue(payload, "verseId"); document.CultMeshAddress = ReadPayloadValue(payload, "cultMeshAddress"); document.ReplicaStateFilePath = AetheriaRuntimeStateBoundary.GetReplicaStateFilePath(gameDataDirectory, document.VerseId); document.DiscoveryEndpoints = ParseDiscoveryEndpoints(ReadPayloadValue(payload, "discoveryEndpoints")); document.LastDiscoveryError = ""; document.LastReplicaSyncError = ""; 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 string[] ParseDiscoveryEndpoints(string value) { return (value ?? "") .Split(new[] { ',', ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) .Select(endpoint => endpoint.Trim()) .Where(endpoint => !string.IsNullOrWhiteSpace(endpoint)) .Distinct(StringComparer.Ordinal) .ToArray(); } 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; } } }