From b86635fda55179bb2948ed4ce7eecbc37175a8bd Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 5 Jul 2026 18:07:21 +0100 Subject: [PATCH] Move RTS menu panel authority to daemon --- Aetheria.Rts.Web/Client/app.ts | 108 ++--------------- .../aetheria-rts-generated-bindings.ts | 10 ++ Aetheria.State.Daemon/Program.cs | 17 ++- Aetheria.State.Verify/Program.cs | 110 ++++++++++++++---- Aetheria.State/AetheriaDocumentRegistry.cs | 1 + Aetheria.State/AetheriaEveCommandBridge.cs | 72 ++++++++++++ Aetheria.State/AetheriaStateNode.cs | 3 + .../AetheriaEveCommandAcceptanceStatus.cs | 3 + .../Documents/AetheriaMainMenuState.cs | 26 +++++ .../AetheriaCultUiCompositionSurfaceWindow.cs | 4 +- Assets/Scripts/UI/MainMenu.cs | 4 +- ...AetheriaRuntimeDaemonGameSurfaceBuilder.cs | 29 ++++- .../AetheriaRuntimeEveCommandClient.cs | 92 +++++++++++++++ .../AetheriaRuntimeEveCommandDocument.cs | 12 +- .../AetheriaRuntimeMainMenuSurfaceBuilder.cs | 65 +---------- 15 files changed, 361 insertions(+), 195 deletions(-) create mode 100644 Aetheria.State/Documents/AetheriaMainMenuState.cs diff --git a/Aetheria.Rts.Web/Client/app.ts b/Aetheria.Rts.Web/Client/app.ts index cf0c44b26..844c7fe68 100644 --- a/Aetheria.Rts.Web/Client/app.ts +++ b/Aetheria.Rts.Web/Client/app.ts @@ -5,7 +5,6 @@ import { type EveSurfaceComponent, } from "../node_modules/@gamecult/eve-browser-lowering/dist/index.js"; import type { - AetheriaMenuSurfaceComponent, AetheriaMenuSurfaceDocument, AetheriaRtsApi, Viewport, @@ -22,7 +21,6 @@ declare global { const host = requiredElement("#eve-surface-host"); const statusEl = requiredElement("#status"); let latestEveSurfaceKey = ""; -let activeMainMenuSurfaceId = "aetheria.main_menu.root"; let renderDaemonEveSurfaceNow: (() => Promise) | null = null; let eveSurfacePoll: number | null = null; @@ -48,14 +46,13 @@ async function showDaemonEveSurface(): Promise { const surface = await window.aetheriaRts.eveSurface({ recordKey: "eve:surface:aetheria.daemon.game", }); - const loweredSurface = withActiveMainMenuPanel(surface, activeMainMenuSurfaceId); - const surfaceKey = `${surface.providerId}:${surface.surface.id}:${surface.version}:${surface.updatedAtUtc}:${activeMainMenuSurfaceId}`; + const surfaceKey = `${surface.providerId}:${surface.surface.id}:${surface.version}:${surface.updatedAtUtc}`; if (surfaceKey === latestEveSurfaceKey) { return; } latestEveSurfaceKey = surfaceKey; - renderEveSurface(loweredSurface, host, { + renderEveSurface(surface, host, { body: document.body, assetUrlResolver: resolveAetheriaAssetUrl, clientId: "aetheria.rts.electron", @@ -79,70 +76,6 @@ async function showDaemonEveSurface(): Promise { } } -async function showMainMenuPanel(surfaceId: string): Promise { - activeMainMenuSurfaceId = normalizeMainMenuSurfaceId(surfaceId); - latestEveSurfaceKey = ""; - if (renderDaemonEveSurfaceNow) { - await renderDaemonEveSurfaceNow(); - return; - } - - await showDaemonEveSurface(); -} - -function normalizeMainMenuSurfaceId(surfaceId: string): string { - switch (surfaceId) { - case "aetheria.main_menu.settings": - case "aetheria.main_menu.player_settings": - case "aetheria.main_menu.verse_settings": - case "aetheria.main_menu.input_settings": - return surfaceId; - default: - return "aetheria.main_menu.root"; - } -} - -function withActiveMainMenuPanel( - document: AetheriaMenuSurfaceDocument, - panelSurfaceId: string, -): AetheriaMenuSurfaceDocument { - return { - ...document, - surface: { - ...document.surface, - root: rewriteMainMenuPanelSlot(document.surface.root, panelSurfaceId), - }, - }; -} - -type MutableEveComponent = AetheriaMenuSurfaceComponent & { - layout?: Record; - style?: Record; -}; - -function rewriteMainMenuPanelSlot( - component: AetheriaMenuSurfaceComponent, - panelSurfaceId: string, -): AetheriaMenuSurfaceComponent { - const source = component as MutableEveComponent; - const props = source.props ?? {}; - const embeddedDocuments = source.embeddedDocuments ?? []; - const isMainMenuSlot = props.slotId === "mainMenuPanel" || - embeddedDocuments.some(slot => slot.slotId === "mainMenuPanel"); - return { - ...source, - props: isMainMenuSlot - ? { ...props, documentId: panelSurfaceId } - : props, - embeddedDocuments: isMainMenuSlot - ? embeddedDocuments.map(slot => slot.slotId === "mainMenuPanel" - ? { ...slot, documentId: panelSurfaceId } - : slot) - : source.embeddedDocuments, - children: source.children.map(child => rewriteMainMenuPanelSlot(child, panelSurfaceId)), - }; -} - async function resolveEveDocument( request: EveEmbeddedDocumentRequest, component: EveSurfaceComponent, @@ -219,11 +152,6 @@ function resolveAetheriaAssetUrl(uri: string): string { } async function submitEveCommand(intent: EveCommandIntent): Promise { - if (intent.command.startsWith("aetheria.main_menu.")) { - handleMenuCommand(intent.command); - return; - } - try { const receipt = await window.aetheriaRts.submitEveCommand({ providerId: intent.providerId, @@ -234,37 +162,17 @@ async function submitEveCommand(intent: EveCommandIntent): Promise { payload: intent.payload, }); setStatus(`${intent.command}: ${receipt.accepted ? "submitted" : "rejected"}`); + latestEveSurfaceKey = ""; + if (renderDaemonEveSurfaceNow) { + window.setTimeout(() => { + void renderDaemonEveSurfaceNow?.(); + }, 120); + } } catch (error) { setStatus(error instanceof Error ? error.message : `Failed to submit ${intent.command}.`); } } -function handleMenuCommand(command: string): void { - switch (command) { - case "aetheria.main_menu.root.show_settings": - void showMainMenuPanel("aetheria.main_menu.settings"); - return; - case "aetheria.main_menu.settings.show_player_settings": - void showMainMenuPanel("aetheria.main_menu.player_settings"); - return; - case "aetheria.main_menu.settings.show_verse_settings": - void showMainMenuPanel("aetheria.main_menu.verse_settings"); - return; - case "aetheria.main_menu.settings.show_input_settings": - void showMainMenuPanel("aetheria.main_menu.input_settings"); - return; - case "aetheria.main_menu.settings.back_to_settings": - void showMainMenuPanel("aetheria.main_menu.settings"); - return; - case "aetheria.main_menu.settings.back_to_main": - void showMainMenuPanel("aetheria.main_menu.root"); - return; - default: - setStatus(`Aetheria Starbridge: ${command || "no command"}`); - return; - } -} - function wireWindowControls(): void { document.querySelectorAll("[data-window-control]").forEach(button => { if (button.dataset.windowControlWired === "true") { diff --git a/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts b/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts index 1dd11d66b..507b75ac8 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts @@ -98,6 +98,16 @@ export const AetheriaRuntimeEveCommandKinds = { setTradeValueQualityMaximum: 21, setTradeValueQualityExponent: 22, setTradeValueTierQuality: 23, + mainMenuContinueRun: 24, + mainMenuNewGame: 25, + mainMenuShowSettings: 26, + mainMenuQuit: 27, + mainMenuShowPlayerSettings: 28, + mainMenuShowVerseSettings: 29, + mainMenuShowInputSettings: 30, + mainMenuBackToMain: 31, + mainMenuBackToSettings: 32, + mainMenuOpenRuntimeInputScreen: 33, } as const; export const AetheriaRtsIpcChannels = { diff --git a/Aetheria.State.Daemon/Program.cs b/Aetheria.State.Daemon/Program.cs index a50d244b6..9ebe5e55f 100644 --- a/Aetheria.State.Daemon/Program.cs +++ b/Aetheria.State.Daemon/Program.cs @@ -1089,10 +1089,24 @@ static async Task PublishDaemonApiDocumentsAsync( await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.StarbridgeSessionSummary) .ReplaceAsync(result.StarbridgeSessionSummary) .ConfigureAwait(false); + var mainMenuState = await node.MutableDocument(AetheriaStateNode.MainMenuStateKey) + .ReadAsync() + .ConfigureAwait(false); + var activeMainMenuSurfaceId = string.IsNullOrWhiteSpace(mainMenuState?.ActiveSurfaceId) + ? AetheriaRuntimeMainMenuCommands.RootSurfaceId + : mainMenuState.ActiveSurfaceId; if (result.GameSurface != null) + { + var gameSurface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build( + result.Frame, + result.Health ?? new AetheriaRuntimeDaemonHealthDocument(), + result.CommandBoundary ?? AetheriaRuntimeDaemonCommandBoundaryDocument.Create(options.DaemonId), + result.StarbridgeSessionSummary, + activeMainMenuSurfaceId); await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.DaemonGameSurface) - .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(result.GameSurface)) + .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(gameSurface)) .ConfigureAwait(false); + } if (result.GameTuiSurface != null) await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.DaemonGameTuiSurface) .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(result.GameTuiSurface)) @@ -1375,6 +1389,7 @@ static async Task AcceptEveCommandsAsync(AetheriaStateNode node, AetheriaDaemonH AppliedInputSettingsCommands = report.AcceptedInputSettingsCommands, AppliedLoadoutTemplateCommands = report.AcceptedLoadoutTemplateCommands, AppliedVerseHostCommands = report.AcceptedVerseHostCommands, + AppliedMainMenuCommands = report.AcceptedMainMenuCommands, AccountedCommandIds = report.AccountedCommandIds, LastRejectedCommand = report.LastRejectedCommand, LastRejectedReason = report.LastRejectedReason, diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 2d562a469..53a4fafcc 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -1,6 +1,8 @@ using Aetheria.State; using Aetheria.State.Documents; using GameCult.Aetheria.State.Verse; +using GameCult.Eve.Surface; +using GameCult.Mesh; var root = args.Length > 0 ? Path.GetFullPath(args[0]) : Directory.GetCurrentDirectory(); var statePath = args.Length > 1 @@ -124,6 +126,7 @@ var surface = AetheriaEveSurfaceDocuments.BuildCatalogSurface(catalog, DateTimeO var tradeValuePolicy = await node.MutableDocument(AetheriaStateNode.TradeValuePolicyKey).ReadAsync() ?? throw new InvalidOperationException("Missing authored trade value policy document."); await RequireTradeValuePolicyEveCommandPersistsAsync(); +await RequireMainMenuEveCommandPersistsAsync(); var runtimeCatalog = catalog; RequireCount(ledger, "aetheria.item_definition.v1", items.Length); @@ -3935,7 +3938,7 @@ static void RequireMainMenuSettingsShellUsesEveSurface(string root) "BuildVerseSettings(", "private static AetheriaRuntimeSurfaceDocument BuildRoot(", "private static AetheriaRuntimeSurfaceDocument BuildInputSettings(", - "var verseLabel = VerseLabel(verseTitle, verseId)", + "\"STARBRIDGE\"", "AetheriaRuntimePlayerSettingsSurfaceBuilder.Build(playerSettings, updatedAtUtc, version)", "AetheriaRuntimeSurfaceDocument document", "WithBackAction(", @@ -4096,7 +4099,9 @@ static void RequireMainMenuRootUsesEveSurface(string root) "AetheriaRuntimeMainMenuCommands.ShowSettings", "AetheriaRuntimeMainMenuCommands.Quit", "private static AetheriaRuntimeSurfaceDocument BuildRoot(", - "var verseLabel = VerseLabel(verseTitle, verseId)", + "\"STARBRIDGE\"", + "\"Aetheria Starbridge\"", + "GravitySurface($", "public enum AetheriaRuntimeMainMenuCommandKind", "public readonly struct AetheriaRuntimeMainMenuCommand", "public static class AetheriaRuntimeMainMenuSurfaceCommands", @@ -9870,11 +9875,9 @@ static void RequireClientTargetBootAuthority(string root) var requiredMainMenuBuilderSymbols = new[] { "private static AetheriaRuntimeSurfaceDocument BuildRoot(", - "var verseLabel = VerseLabel(verseTitle, verseId)", - "\"Verse\"", - "\"Target\"", - "\"Daemon\"", - "\"Transport\"" + "\"STARBRIDGE\"", + "AetheriaMainMenuGravityField.FromDaemonFrame(daemonFrame)", + "GravitySurface($" }; var missingMainMenuSymbols = requiredMainMenuSymbols .Where(symbol => !mainMenu.Contains(symbol, StringComparison.Ordinal)) @@ -11614,7 +11617,10 @@ static void RequireDaemonVersePublication(string root) "public const string DaemonGameSurfaceKey = \"eve:surface:aetheria.daemon.game\"", "public const string DaemonGameTuiSurfaceKey = \"eve:surface:aetheria.daemon.game.tui\"", "public const string DaemonEditorSurfaceKey = \"eve:surface:aetheria.daemon.editor\"", - "public const string DaemonEditorTuiSurfaceKey = \"eve:surface:aetheria.daemon.editor.tui\"", + "public const string DaemonEditorTuiSurfaceKey = \"eve:surface:aetheria.daemon.editor.tui\"" + }; + var requiredDaemonProviderDocumentSymbols = new[] + { "AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId", "AetheriaRuntimeDaemonGameSurfaceBuilder.TuiSurfaceId", "AetheriaRuntimeDaemonEditorSurfaceBuilder.SurfaceId", @@ -11629,6 +11635,15 @@ static void RequireDaemonVersePublication(string root) "Odin-visible provider advertisement no longer points at managed daemon CultMesh records: " + string.Join(", ", missingProviderAdvertisementSymbols)); } + var missingDaemonProviderDocumentSymbols = requiredDaemonProviderDocumentSymbols + .Where(symbol => !daemonDocuments.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + if (missingDaemonProviderDocumentSymbols.Length > 0) + { + throw new InvalidOperationException( + "Daemon provider advertisement document no longer owns daemon surface ids: " + + string.Join(", ", missingDaemonProviderDocumentSymbols)); + } var forbiddenProviderAdvertisementSymbols = new[] { @@ -13542,19 +13557,11 @@ static void RequireMainMenuVerseHostDocumentAccess(string root) "public static AetheriaRuntimeSurfaceDocument BuildRoot(", "AetheriaRuntimeStateBootReport stateBoot", "AetheriaRuntimeVerseHostSettingsDocument verseHost", - "verseHost?.Title ?? stateBoot.Title", - "verseHost?.VerseId ?? stateBoot.VerseId", - "verseHost?.Visibility ?? \"unknown\"", - "verseHost?.CultMeshAddress ?? stateBoot.CultMeshAddress", - ".TitleSubtitle(\"AETHERIA\", \"TERMINUS\")", - ".Metric(\"Verse\", verseLabel)", - ".Metric(\"Target\", targetLine)", - ".Metric(\"Daemon\", daemonLine)", - ".Metric(\"Transport\", transportLine)", - "private static string VerseLabel(", - "private static string TargetLine(", - "\"No daemon frame\"", - "\"Run {daemonRunId} / frame {daemonFrameId}\"" + "AetheriaMainMenuGravityField.FromDaemonFrame(daemonFrame)", + "GravitySurface($", + "\"AETHERIA\"", + "\"STARBRIDGE\"", + "\"Aetheria Starbridge\"" }; var requiredClientTargetBuilderSymbols = new[] { @@ -16349,6 +16356,67 @@ static async Task RequireTradeValuePolicyEveCommandPersistsAsync() } } +static async Task RequireMainMenuEveCommandPersistsAsync() +{ + var tempStatePath = Path.Combine( + Path.GetTempPath(), + $"aetheria-main-menu-command-{Guid.NewGuid():N}.cc"); + + try + { + await using var commandNode = await AetheriaStateNode.OpenAsync( + tempStatePath, + "aetheria-state-verify-main-menu", + enableDurableShardLogs: false); + + if (!AetheriaRuntimeEveCommandClient.TryCreateKnownSurfaceCommand( + new EveSurfaceCommandRequest( + AetheriaRuntimeMainMenuSurfaceBuilder.ProviderId, + AetheriaRuntimeMainMenuCommands.RootSurfaceId, + CultMesh.OperationInvocation( + AetheriaRuntimeMainMenuCommands.ShowSettings, + AetheriaRuntimeEveCommandDocument.SchemaId, + new CultMeshRouteHint(CultMeshLocalityKind.Network, "aetheria-eve-command")), + CultMesh.OperationPayload(), + DateTimeOffset.UtcNow, + "aetheria-state-verify"), + out var envelope) || + envelope == null) + { + throw new InvalidOperationException("Main menu Eve surface command did not create a typed command envelope."); + } + + var command = AetheriaRuntimeEveCommandClient.ToDocument(envelope); + await commandNode.SubmitEveCommandAsync(command).ConfigureAwait(false); + + var report = await AetheriaEveCommandBridge.AcceptObservedAsync(commandNode).ConfigureAwait(false); + if (report.AcceptedMainMenuCommands != 1) + { + throw new InvalidOperationException( + "Main menu Eve command was not accepted by the typed command bridge."); + } + + var menuState = await commandNode.MutableDocument(AetheriaStateNode.MainMenuStateKey).ReadAsync().ConfigureAwait(false) + ?? throw new InvalidOperationException("Main menu state was not created by the typed Eve command."); + if (!string.Equals(menuState.ActiveSurfaceId, AetheriaRuntimeMainMenuCommands.SettingsSurfaceId, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + $"Main menu Eve command did not persist daemon-owned panel state. Expected {AetheriaRuntimeMainMenuCommands.SettingsSurfaceId}, got {menuState.ActiveSurfaceId}."); + } + } + finally + { + try + { + File.Delete(tempStatePath); + } + catch + { + // Best-effort cleanup for verifier scratch state. + } + } +} + static void RequireDeadPropertiesPanelShellDeleted(string root) { var deletedShellPaths = new[] diff --git a/Aetheria.State/AetheriaDocumentRegistry.cs b/Aetheria.State/AetheriaDocumentRegistry.cs index bf15454bf..e3c4ac7c6 100644 --- a/Aetheria.State/AetheriaDocumentRegistry.cs +++ b/Aetheria.State/AetheriaDocumentRegistry.cs @@ -37,6 +37,7 @@ public static class AetheriaDocumentRegistry typeof(AetheriaZoneState), typeof(AetheriaEntitySnapshot), typeof(AetheriaVerseHostSettings), + typeof(AetheriaMainMenuState), typeof(AetheriaEveCommandAcceptanceStatus), typeof(AetheriaRuntimeDaemonProviderAdvertisementDocument), typeof(AetheriaRuntimeDaemonHealthDocument), diff --git a/Aetheria.State/AetheriaEveCommandBridge.cs b/Aetheria.State/AetheriaEveCommandBridge.cs index 8d7659616..79d5df404 100644 --- a/Aetheria.State/AetheriaEveCommandBridge.cs +++ b/Aetheria.State/AetheriaEveCommandBridge.cs @@ -14,6 +14,7 @@ public sealed class AetheriaEveCommandAcceptanceReport public int AcceptedLoadoutTemplateCommands { get; set; } public int AcceptedVerseHostCommands { get; set; } public int AcceptedTradeValuePolicyCommands { get; set; } + public int AcceptedMainMenuCommands { get; set; } public int RejectedCommands { get; set; } public string[] AcceptedCommandIds { get; set; } = []; public string[] RejectedCommandIds { get; set; } = []; @@ -104,6 +105,19 @@ public static class AetheriaEveCommandBridge await ExecuteTradeValuePolicyCommandAsync(node, command).ConfigureAwait(false); report.AcceptedTradeValuePolicyCommands++; break; + case AetheriaRuntimeEveCommandKind.MainMenuContinueRun: + case AetheriaRuntimeEveCommandKind.MainMenuNewGame: + case AetheriaRuntimeEveCommandKind.MainMenuShowSettings: + case AetheriaRuntimeEveCommandKind.MainMenuQuit: + case AetheriaRuntimeEveCommandKind.MainMenuShowPlayerSettings: + case AetheriaRuntimeEveCommandKind.MainMenuShowVerseSettings: + case AetheriaRuntimeEveCommandKind.MainMenuShowInputSettings: + case AetheriaRuntimeEveCommandKind.MainMenuBackToMain: + case AetheriaRuntimeEveCommandKind.MainMenuBackToSettings: + case AetheriaRuntimeEveCommandKind.MainMenuOpenRuntimeInputScreen: + await ExecuteMainMenuCommandAsync(node, command).ConfigureAwait(false); + report.AcceptedMainMenuCommands++; + break; } accepted.Add(command.CommandId ?? ""); @@ -352,6 +366,64 @@ public static class AetheriaEveCommandBridge } } + private static async Task ExecuteMainMenuCommandAsync( + AetheriaStateNode node, + AetheriaRuntimeEveCommandDocument command) + { + var state = await node.MutableDocument(AetheriaStateNode.MainMenuStateKey).ReadAsync().ConfigureAwait(false) ?? + new AetheriaMainMenuState(); + var activeSurfaceId = ActiveMainMenuSurfaceFor(command.Kind, state.ActiveSurfaceId); + state.ActiveSurfaceId = activeSurfaceId; + state.LastCommandId = command.CommandId ?? ""; + state.LastCommand = command.Command ?? ""; + state.UpdatedAtUtc = string.IsNullOrWhiteSpace(command.IssuedAtUtc) + ? DateTimeOffset.UtcNow.ToString("O") + : command.IssuedAtUtc; + await node.MutableDocument(AetheriaStateNode.MainMenuStateKey) + .ReplaceAsync(state) + .ConfigureAwait(false); + } + + private static string ActiveMainMenuSurfaceFor( + AetheriaRuntimeEveCommandKind kind, + string currentSurfaceId) + { + switch (kind) + { + case AetheriaRuntimeEveCommandKind.MainMenuShowSettings: + case AetheriaRuntimeEveCommandKind.MainMenuBackToSettings: + return AetheriaRuntimeMainMenuCommands.SettingsSurfaceId; + case AetheriaRuntimeEveCommandKind.MainMenuShowPlayerSettings: + return AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId; + case AetheriaRuntimeEveCommandKind.MainMenuShowVerseSettings: + return AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId; + case AetheriaRuntimeEveCommandKind.MainMenuShowInputSettings: + case AetheriaRuntimeEveCommandKind.MainMenuOpenRuntimeInputScreen: + return AetheriaRuntimeMainMenuCommands.InputSettingsSurfaceId; + case AetheriaRuntimeEveCommandKind.MainMenuBackToMain: + case AetheriaRuntimeEveCommandKind.MainMenuContinueRun: + case AetheriaRuntimeEveCommandKind.MainMenuNewGame: + case AetheriaRuntimeEveCommandKind.MainMenuQuit: + return AetheriaRuntimeMainMenuCommands.RootSurfaceId; + default: + return NormalizeMainMenuSurfaceId(currentSurfaceId); + } + } + + private static string NormalizeMainMenuSurfaceId(string surfaceId) + { + switch (surfaceId ?? "") + { + case AetheriaRuntimeMainMenuCommands.SettingsSurfaceId: + case AetheriaRuntimeMainMenuCommands.InputSettingsSurfaceId: + case AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId: + case AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId: + return surfaceId; + default: + return AetheriaRuntimeMainMenuCommands.RootSurfaceId; + } + } + private static string Cycle(string current, params string[] values) { if (values.Length == 0) diff --git a/Aetheria.State/AetheriaStateNode.cs b/Aetheria.State/AetheriaStateNode.cs index 0e61c2837..714090d6e 100644 --- a/Aetheria.State/AetheriaStateNode.cs +++ b/Aetheria.State/AetheriaStateNode.cs @@ -259,6 +259,9 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable public static CultRecordKey EveCommandAcceptanceStatusKey { get; } = new("global:aetheria.eve_command_acceptance_status.v1"); + public static CultRecordKey MainMenuStateKey { get; } = + new("global:aetheria.main_menu_state.v1"); + public static CultRecordKey TradeValuePolicyKey { get; } = new(AetheriaTradeValuePolicy.RecordKey); diff --git a/Aetheria.State/Documents/AetheriaEveCommandAcceptanceStatus.cs b/Aetheria.State/Documents/AetheriaEveCommandAcceptanceStatus.cs index c6e4eac2e..5914bd793 100644 --- a/Aetheria.State/Documents/AetheriaEveCommandAcceptanceStatus.cs +++ b/Aetheria.State/Documents/AetheriaEveCommandAcceptanceStatus.cs @@ -69,4 +69,7 @@ public sealed class AetheriaEveCommandAcceptanceStatus [Key(19)] public string[] AccountedCommandIds { get; set; } = []; + + [Key(20)] + public int AppliedMainMenuCommands { get; set; } } diff --git a/Aetheria.State/Documents/AetheriaMainMenuState.cs b/Aetheria.State/Documents/AetheriaMainMenuState.cs new file mode 100644 index 000000000..91b03d344 --- /dev/null +++ b/Aetheria.State/Documents/AetheriaMainMenuState.cs @@ -0,0 +1,26 @@ +using GameCult.Caching; +using MessagePack; + +namespace Aetheria.State.Documents; + +[CultDocument("aetheria.main_menu_state", "aetheria.main_menu_state.v1")] +[CultGlobal] +[MessagePackObject] +public sealed class AetheriaMainMenuState +{ + [Key(0)] + [CultName] + public string Name { get; set; } = "Aetheria main menu state"; + + [Key(1)] + public string ActiveSurfaceId { get; set; } = ""; + + [Key(2)] + public string LastCommandId { get; set; } = ""; + + [Key(3)] + public string LastCommand { get; set; } = ""; + + [Key(4)] + public string UpdatedAtUtc { get; set; } = ""; +} diff --git a/Assets/Editor/AetheriaCultUiCompositionSurfaceWindow.cs b/Assets/Editor/AetheriaCultUiCompositionSurfaceWindow.cs index f3a74e824..feab941cd 100644 --- a/Assets/Editor/AetheriaCultUiCompositionSurfaceWindow.cs +++ b/Assets/Editor/AetheriaCultUiCompositionSurfaceWindow.cs @@ -75,7 +75,7 @@ public sealed class AetheriaCultUiCompositionSurfaceWindow : EditorWindow _providerId = TextInput("Provider Id", DefaultProviderId, controls); _providerKind = TextInput("Provider Kind", DefaultProviderKind, controls); _title = TextInput("Title", "AETHERIA", controls); - _subtitle = TextInput("Subtitle", "TERMINUS", controls); + _subtitle = TextInput("Subtitle", "STARBRIDGE", controls); _note = TextInput("Note", "Live CultUI draft lowered through the shared Eve UI Toolkit path.", controls); _showStatusMetrics = ToggleInput("Status Metrics", true, controls); _useButtonRow = ToggleInput("Button Row", false, controls); @@ -206,7 +206,7 @@ public sealed class AetheriaCultUiCompositionSurfaceWindow : EditorWindow _providerId.value = DefaultProviderId; _providerKind.value = DefaultProviderKind; _title.value = "AETHERIA"; - _subtitle.value = "TERMINUS"; + _subtitle.value = "STARBRIDGE"; _note.value = "Live CultUI draft lowered through the shared Eve UI Toolkit path."; _showStatusMetrics.value = true; _useButtonRow.value = false; diff --git a/Assets/Scripts/UI/MainMenu.cs b/Assets/Scripts/UI/MainMenu.cs index 867de8251..bd2d2cf09 100644 --- a/Assets/Scripts/UI/MainMenu.cs +++ b/Assets/Scripts/UI/MainMenu.cs @@ -183,7 +183,7 @@ public class MainMenu : MonoBehaviour private void ShowPlayerSettingsSurface(bool animateFromRight = true) { RenderMenuSurface( - ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.PlayerSettingsShellSurfaceId), + ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId), HandlePlayerSettingsSurfaceCommand, animateFromRight); } @@ -191,7 +191,7 @@ public class MainMenu : MonoBehaviour private void ShowVerseSettingsSurface(bool animateFromRight = true) { RenderMenuSurface( - ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.VerseSettingsShellSurfaceId), + ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId), HandleVerseSettingsSurfaceCommand, animateFromRight); } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs index 1215b9979..d9899519a 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs @@ -16,7 +16,8 @@ namespace GameCult.Aetheria.State.Verse AetheriaRuntimeDaemonFrameDocument frame, AetheriaRuntimeDaemonHealthDocument health, AetheriaRuntimeDaemonCommandBoundaryDocument commandBoundary, - AetheriaRuntimeStarbridgeSessionSummaryDocument? starbridge = null) + AetheriaRuntimeStarbridgeSessionSummaryDocument? starbridge = null, + string activeMainMenuSurfaceId = AetheriaRuntimeMainMenuCommands.RootSurfaceId) { frame ??= new AetheriaRuntimeDaemonFrameDocument(); health ??= new AetheriaRuntimeDaemonHealthDocument(); @@ -28,10 +29,11 @@ namespace GameCult.Aetheria.State.Verse var entity = FindCurrentEntity(run, zone); var target = FindTargetEntity(zone, entity); var entityName = string.IsNullOrWhiteSpace(entity?.Name) ? "(no current entity)" : entity!.Name; + activeMainMenuSurfaceId = NormalizeMainMenuSurfaceId(activeMainMenuSurfaceId); var surfaceChildren = new List { GravityFieldSurface("aetheria.daemon.game.field"), - MainMenuOverlay("aetheria.daemon.game.main_menu"), + MainMenuOverlay("aetheria.daemon.game.main_menu", activeMainMenuSurfaceId), Node( "aetheria.daemon.game.frame", "card", @@ -363,15 +365,18 @@ namespace GameCult.Aetheria.State.Verse null); } - private static AetheriaRuntimeSurfaceComponent MainMenuOverlay(string id) + private static AetheriaRuntimeSurfaceComponent MainMenuOverlay( + string id, + string activeSurfaceId) { + activeSurfaceId = NormalizeMainMenuSurfaceId(activeSurfaceId); return new AetheriaRuntimeSurfaceComponent( id, "surface.slot", new Dictionary(StringComparer.Ordinal) { ["slotId"] = "mainMenuPanel", - ["documentId"] = AetheriaRuntimeMainMenuCommands.RootSurfaceId, + ["documentId"] = activeSurfaceId, ["schemaId"] = "gamecult.eve.surface.v1", ["presentationKind"] = "menu.overlay" }, @@ -381,7 +386,7 @@ namespace GameCult.Aetheria.State.Verse { new AetheriaRuntimeEmbeddedDocumentSlot( "mainMenuPanel", - AetheriaRuntimeMainMenuCommands.RootSurfaceId, + activeSurfaceId, "gamecult.eve.surface.v1", "menu.overlay") }, @@ -401,6 +406,20 @@ namespace GameCult.Aetheria.State.Verse }); } + private static string NormalizeMainMenuSurfaceId(string surfaceId) + { + switch (surfaceId ?? "") + { + case AetheriaRuntimeMainMenuCommands.SettingsSurfaceId: + case AetheriaRuntimeMainMenuCommands.InputSettingsSurfaceId: + case AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId: + case AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId: + return surfaceId; + default: + return AetheriaRuntimeMainMenuCommands.RootSurfaceId; + } + } + private static AetheriaRuntimeRtsViewportBounds DefaultViewport() { return new AetheriaRuntimeRtsViewportBounds diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveCommandClient.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveCommandClient.cs index b9961bb03..93e9584fc 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveCommandClient.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveCommandClient.cs @@ -245,6 +245,13 @@ namespace GameCult.Aetheria.State.Verse when AetheriaRuntimeTradeValuePolicyCommands.IsKnown(command): envelope = CreateTradeValuePolicyCommand(CommandKindForSurface(request), request, clientId); return true; + case AetheriaRuntimeMainMenuCommands.RootSurfaceId: + case AetheriaRuntimeMainMenuCommands.SettingsSurfaceId: + case AetheriaRuntimeMainMenuCommands.InputSettingsSurfaceId: + case AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId: + case AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId: + envelope = CreateMainMenuCommand(CommandKindForSurface(request), request, clientId); + return envelope.Kind != AetheriaRuntimeEveCommandKind.Unknown; } envelope = null; @@ -417,6 +424,23 @@ namespace GameCult.Aetheria.State.Verse payload: request.Payload); } + private static AetheriaRuntimeEveCommandEnvelope CreateMainMenuCommand( + AetheriaRuntimeEveCommandKind command, + EveSurfaceCommandRequest request, + string clientId) + { + return CreateTypedCommand( + request.SurfaceId ?? AetheriaRuntimeMainMenuCommands.RootSurfaceId, + command, + string.IsNullOrWhiteSpace(clientId) ? request.ClientId ?? "" : clientId, + playerSettings: ReadPlayerSettingsBody(request), + inputSettings: ReadInputSettingsBody(request), + loadoutTemplate: null, + tradeValuePolicy: null, + invocation: request.Operation, + payload: request.Payload); + } + public static AetheriaRuntimeEveCommandDocument ToDocument(AetheriaRuntimeEveCommandEnvelope envelope) { if (envelope == null) throw new ArgumentNullException(nameof(envelope)); @@ -616,6 +640,28 @@ namespace GameCult.Aetheria.State.Verse : AetheriaRuntimeEveCommandKind.Unknown; case AetheriaRuntimeTradeValuePolicyCommands.SurfaceId: return TradeValuePolicyKind(command); + case AetheriaRuntimeMainMenuCommands.RootSurfaceId: + return MainMenuRootKind(command); + case AetheriaRuntimeMainMenuCommands.SettingsSurfaceId: + return MainMenuSettingsKind(command); + case AetheriaRuntimeMainMenuCommands.InputSettingsSurfaceId: + return command == AetheriaRuntimeMainMenuCommands.OpenRuntimeInputScreen + ? AetheriaRuntimeEveCommandKind.MainMenuOpenRuntimeInputScreen + : command == AetheriaRuntimeMainMenuCommands.BackToSettings + ? AetheriaRuntimeEveCommandKind.MainMenuBackToSettings + : InputSettingsKind(command); + case AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId: + return command == AetheriaRuntimeMainMenuCommands.BackToSettings + ? AetheriaRuntimeEveCommandKind.MainMenuBackToSettings + : PlayerSettingsKind(command); + case AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId: + return command == AetheriaRuntimeMainMenuCommands.BackToSettings + ? AetheriaRuntimeEveCommandKind.MainMenuBackToSettings + : command == AetheriaRuntimeVerseHostCommands.Refresh + ? AetheriaRuntimeEveCommandKind.VerseHostRefresh + : command == AetheriaRuntimeVerseHostCommands.CycleVisibility + ? AetheriaRuntimeEveCommandKind.CycleVerseHostVisibility + : AetheriaRuntimeEveCommandKind.Unknown; default: return AetheriaRuntimeEveCommandKind.Unknown; } @@ -671,11 +717,57 @@ namespace GameCult.Aetheria.State.Verse return AetheriaRuntimeTradeValuePolicyCommands.SetQualityExponent; case AetheriaRuntimeEveCommandKind.SetTradeValueTierQuality: return AetheriaRuntimeTradeValuePolicyCommands.SetTierQuality; + case AetheriaRuntimeEveCommandKind.MainMenuContinueRun: + return AetheriaRuntimeMainMenuCommands.ContinueRun; + case AetheriaRuntimeEveCommandKind.MainMenuNewGame: + return AetheriaRuntimeMainMenuCommands.NewGame; + case AetheriaRuntimeEveCommandKind.MainMenuShowSettings: + return AetheriaRuntimeMainMenuCommands.ShowSettings; + case AetheriaRuntimeEveCommandKind.MainMenuQuit: + return AetheriaRuntimeMainMenuCommands.Quit; + case AetheriaRuntimeEveCommandKind.MainMenuShowPlayerSettings: + return AetheriaRuntimeMainMenuCommands.ShowPlayerSettings; + case AetheriaRuntimeEveCommandKind.MainMenuShowVerseSettings: + return AetheriaRuntimeMainMenuCommands.ShowVerseSettings; + case AetheriaRuntimeEveCommandKind.MainMenuShowInputSettings: + return AetheriaRuntimeMainMenuCommands.ShowInputSettings; + case AetheriaRuntimeEveCommandKind.MainMenuBackToMain: + return AetheriaRuntimeMainMenuCommands.BackToMain; + case AetheriaRuntimeEveCommandKind.MainMenuBackToSettings: + return AetheriaRuntimeMainMenuCommands.BackToSettings; + case AetheriaRuntimeEveCommandKind.MainMenuOpenRuntimeInputScreen: + return AetheriaRuntimeMainMenuCommands.OpenRuntimeInputScreen; default: return ""; } } + private static AetheriaRuntimeEveCommandKind MainMenuRootKind(string command) + { + if (command == AetheriaRuntimeMainMenuCommands.ContinueRun) + return AetheriaRuntimeEveCommandKind.MainMenuContinueRun; + if (command == AetheriaRuntimeMainMenuCommands.NewGame) + return AetheriaRuntimeEveCommandKind.MainMenuNewGame; + if (command == AetheriaRuntimeMainMenuCommands.ShowSettings) + return AetheriaRuntimeEveCommandKind.MainMenuShowSettings; + if (command == AetheriaRuntimeMainMenuCommands.Quit) + return AetheriaRuntimeEveCommandKind.MainMenuQuit; + return AetheriaRuntimeEveCommandKind.Unknown; + } + + private static AetheriaRuntimeEveCommandKind MainMenuSettingsKind(string command) + { + if (command == AetheriaRuntimeMainMenuCommands.ShowPlayerSettings) + return AetheriaRuntimeEveCommandKind.MainMenuShowPlayerSettings; + if (command == AetheriaRuntimeMainMenuCommands.ShowVerseSettings) + return AetheriaRuntimeEveCommandKind.MainMenuShowVerseSettings; + if (command == AetheriaRuntimeMainMenuCommands.ShowInputSettings) + return AetheriaRuntimeEveCommandKind.MainMenuShowInputSettings; + if (command == AetheriaRuntimeMainMenuCommands.BackToMain) + return AetheriaRuntimeEveCommandKind.MainMenuBackToMain; + return AetheriaRuntimeEveCommandKind.Unknown; + } + private static AetheriaRuntimeEveCommandKind PlayerSettingsKind(string command) { if (command == AetheriaRuntimePlayerSettingsCommands.Refresh) diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveCommandDocument.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveCommandDocument.cs index 5d0f2807b..348d9275a 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveCommandDocument.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveCommandDocument.cs @@ -33,7 +33,17 @@ namespace GameCult.Aetheria.State.Verse SetTradeValueQualityMinimum = 20, SetTradeValueQualityMaximum = 21, SetTradeValueQualityExponent = 22, - SetTradeValueTierQuality = 23 + SetTradeValueTierQuality = 23, + MainMenuContinueRun = 24, + MainMenuNewGame = 25, + MainMenuShowSettings = 26, + MainMenuQuit = 27, + MainMenuShowPlayerSettings = 28, + MainMenuShowVerseSettings = 29, + MainMenuShowInputSettings = 30, + MainMenuBackToMain = 31, + MainMenuBackToSettings = 32, + MainMenuOpenRuntimeInputScreen = 33 } public sealed class AetheriaRuntimeEveCommandEnvelope diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMainMenuSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMainMenuSurfaceBuilder.cs index b8a991974..cd12f77f9 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMainMenuSurfaceBuilder.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMainMenuSurfaceBuilder.cs @@ -41,17 +41,7 @@ namespace GameCult.Aetheria.State.Verse long version = 1) { return BuildRoot( - stateBoot.TargetLabel, - stateBoot.TargetKind, - stateBoot.TargetSource, - stateBoot.Title, - stateBoot.VerseId, - "unknown", - stateBoot.CultMeshAddress, inGame, - hasAuthoritativeDaemonFrame: false, - daemonRunId: "", - daemonFrameId: -1, gravityField: AetheriaMainMenuGravityField.Default(), updatedAtUtc, version); @@ -68,17 +58,7 @@ namespace GameCult.Aetheria.State.Verse long version = 1) { return BuildRoot( - stateBoot.TargetLabel, - stateBoot.TargetKind, - stateBoot.TargetSource, - verseHost?.Title ?? stateBoot.Title, - verseHost?.VerseId ?? stateBoot.VerseId, - verseHost?.Visibility ?? "unknown", - verseHost?.CultMeshAddress ?? stateBoot.CultMeshAddress, inGame, - hasAuthoritativeDaemonFrame: daemonFrame != null, - daemonFrame?.Run?.RunId ?? "", - daemonFrame?.FrameId ?? -1, gravityField: AetheriaMainMenuGravityField.FromDaemonFrame(daemonFrame), updatedAtUtc, version); @@ -95,17 +75,7 @@ namespace GameCult.Aetheria.State.Verse long version = 1) { return BuildRoot( - stateBoot.TargetLabel, - stateBoot.TargetKind, - stateBoot.TargetSource, - verseHost?.Title ?? stateBoot.Title, - verseHost?.VerseId ?? stateBoot.VerseId, - verseHost?.Visibility ?? "unknown", - verseHost?.CultMeshAddress ?? stateBoot.CultMeshAddress, inGame, - hasAuthoritativeDaemonFrame: sectorMap != null, - sectorMap?.RunId ?? "", - sectorMap?.FrameId ?? -1, gravityField: AetheriaMainMenuGravityField.Default(), updatedAtUtc, version); @@ -141,17 +111,7 @@ namespace GameCult.Aetheria.State.Verse } private static AetheriaRuntimeSurfaceDocument BuildRoot( - string targetLabel, - string targetKind, - string targetSource, - string verseTitle, - string verseId, - string verseVisibility, - string verseCultMeshAddress, bool inGame, - bool hasAuthoritativeDaemonFrame, - string daemonRunId, - long daemonFrameId, AetheriaMainMenuGravityField gravityField, string updatedAtUtc, long version) @@ -195,7 +155,7 @@ namespace GameCult.Aetheria.State.Verse ("font", "100 5.9rem/0.98 Montserrat, sans-serif"), ("color", "rgba(232, 250, 255, 0.94)"), ("whiteSpace", "nowrap"))), - Text($"{AetheriaRuntimeMainMenuCommands.RootSurfaceId}.subtitle", "TERMINUS", "text.subtitle", + Text($"{AetheriaRuntimeMainMenuCommands.RootSurfaceId}.subtitle", "STARBRIDGE", "text.subtitle", Layout(("margin", "0 0 0.35rem 16.8rem")), Style( ("font", "100 2.6rem/1 Montserrat, sans-serif"), @@ -212,7 +172,7 @@ namespace GameCult.Aetheria.State.Verse return new AetheriaRuntimeSurfaceDocument( ProviderId, ProviderKind, - "Aetheria Terminus", + "Aetheria Starbridge", version, updatedAtUtc, new AetheriaRuntimeSurfaceTree( @@ -228,27 +188,6 @@ namespace GameCult.Aetheria.State.Verse }); } - private static string VerseLabel(string verseTitle, string verseId) - { - if (string.IsNullOrWhiteSpace(verseTitle)) - return string.IsNullOrWhiteSpace(verseId) ? "Unknown Verse" : verseId; - - if (string.IsNullOrWhiteSpace(verseId) || string.Equals(verseTitle, verseId, StringComparison.Ordinal)) - return verseTitle; - - return $"{verseTitle} / {verseId}"; - } - - private static string TargetLine(string targetLabel, string targetKind, string targetSource) - { - var label = string.IsNullOrWhiteSpace(targetLabel) ? "Unknown target" : targetLabel; - var kind = string.IsNullOrWhiteSpace(targetKind) ? "unknown" : targetKind; - if (string.IsNullOrWhiteSpace(targetSource)) - return $"{label} ({kind})"; - - return $"{label} ({kind}) / {targetSource}"; - } - public static AetheriaRuntimeSurfaceDocument BuildSettings( string updatedAtUtc, long version = 1)