From 59e783f6a03317044f8e598a5b561c0bf659df2a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 2 Jul 2026 21:00:36 +0100 Subject: [PATCH] Publish daemon menu Eve surfaces --- Aetheria.State.Daemon/Program.cs | 183 ++++++++++++++++++ .../AetheriaRuntimeCultCacheDocumentStore.cs | 4 +- .../AetheriaRuntimeEveSurfaceCatalog.cs | 9 +- ...theriaRuntimeEveSurfaceDocumentMetadata.cs | 56 ++++++ .../Runtime/AetheriaRuntimeVerseClient.cs | 15 ++ 5 files changed, 261 insertions(+), 6 deletions(-) create mode 100644 Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceDocumentMetadata.cs diff --git a/Aetheria.State.Daemon/Program.cs b/Aetheria.State.Daemon/Program.cs index aebdad066..68c7be7ac 100644 --- a/Aetheria.State.Daemon/Program.cs +++ b/Aetheria.State.Daemon/Program.cs @@ -795,6 +795,189 @@ static async Task PublishDaemonApiDocumentsAsync( await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.DaemonEditorTuiSurface) .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(result.EditorTuiSurface)) .ConfigureAwait(false); + + await PublishDaemonMenuSurfacesAsync(node, result.Frame).ConfigureAwait(false); +} + +static async Task PublishDaemonMenuSurfacesAsync( + AetheriaStateNode node, + AetheriaRuntimeDaemonFrameDocument frame) +{ + if (frame == null) + return; + + var updatedAtUtc = string.IsNullOrWhiteSpace(frame.PublishedAtUtc) + ? DateTimeOffset.UtcNow.ToString("O") + : frame.PublishedAtUtc; + var catalog = AetheriaRuntimeCatalogStore.OpenReadOnly(node.StatePath); + var loadoutTemplates = AetheriaRuntimeCatalogStore.ReadLoadoutTemplates(node.StatePath); + var playerSettings = await ReadRuntimePlayerSettingsAsync(node).ConfigureAwait(false); + var currentEntity = AetheriaRuntimeRtsDocuments.CurrentEntity(frame); + var stationRefit = AetheriaRuntimeRtsDocuments.StationRefit(frame, loadoutTemplates, catalog); + var dropdownRequest = new AetheriaRuntimeInventoryDropdownSurfaceRequest + { + CurrentView = "Current Entity", + DisplayedEntityKey = currentEntity.EntityKey, + DisplayedCargoEntityKey = currentEntity.EntityKey, + DisplayedCargoIndex = 0, + CanSaveLoadout = !string.IsNullOrWhiteSpace(currentEntity.EntityKey) + }; + var inventoryRequest = new AetheriaRuntimeInventoryPanelSurfaceRequest + { + ViewTitle = string.IsNullOrWhiteSpace(currentEntity.Entity?.DisplayName) + ? "Current Entity Inventory" + : currentEntity.Entity.DisplayName, + DisplayedEntityKey = currentEntity.EntityKey, + DisplayedEntityIndex = currentEntity.EntityIndex, + DisplayedCargoEntityKey = currentEntity.EntityKey, + DisplayedCargoEntityIndex = currentEntity.EntityIndex, + DisplayedCargoIndex = 0 + }; + var inventory = currentEntity.EntityIndex < 0 + ? new AetheriaRuntimeInventoryDocument() + : AetheriaRuntimeRtsDocuments.Inventory(frame, currentEntity.EntityIndex); + + var mainMenu = AetheriaRuntimeMainMenuSurfaceBuilder.BuildRoot( + AetheriaRuntimeStateBoot.Inspect( + new DirectoryInfo(Path.GetDirectoryName(node.StatePath) ?? "."), + node.StatePath), + playerSettings, + canOpenRuntimeInputScreen: true, + inGame: true, + updatedAtUtc); + var inventoryPanel = AetheriaRuntimeInventoryPanelSurfaceBuilder.BuildFromDocuments( + currentEntity, + stationRefit, + inventory, + catalog, + playerSettings, + inventoryRequest, + AetheriaRuntimeVerseRecordKeys.InventoryDropdownSurface.ToString(), + updatedAtUtc); + var inventoryDropdown = AetheriaRuntimeInventoryDropdownSurfaceBuilder.BuildFromDocuments( + stationRefit, + dropdownRequest, + updatedAtUtc); + var mapMenu = AetheriaRuntimeZoneDetailsSurfaceBuilder.BuildFromDocuments( + AetheriaRuntimeRtsDocuments.ZoneDetails(frame, currentEntity.ZoneIndex), + AetheriaRuntimeRtsDocuments.SectorMap(frame), + catalog, + playerSettings, + updatedAtUtc); + var tradeMenu = BuildTradeMenuSurface(stationRefit, catalog, updatedAtUtc, frame.FrameId); + + await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.MainMenuSurface) + .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(mainMenu)) + .ConfigureAwait(false); + await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.InventoryPanelSurface) + .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(inventoryPanel)) + .ConfigureAwait(false); + await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.InventoryDropdownSurface) + .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(inventoryDropdown)) + .ConfigureAwait(false); + await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.MapMenuSurface) + .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(mapMenu)) + .ConfigureAwait(false); + await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.TradeMenuSurface) + .ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(tradeMenu)) + .ConfigureAwait(false); +} + +static async Task ReadRuntimePlayerSettingsAsync(AetheriaStateNode node) +{ + var settings = await node.MutableDocument(AetheriaStateNode.PlayerSettingsKey) + .ReadAsync() + .ConfigureAwait(false); + settings ??= new AetheriaPlayerSettings(); + return new AetheriaRuntimePlayerSettingsDocument + { + PlayerName = settings.PlayerName ?? "", + TutorialPassed = settings.TutorialPassed, + TemperatureUnit = settings.Gameplay?.TemperatureUnit ?? "", + SignificantDigits = settings.Gameplay?.SignificantDigits ?? 3, + DefaultShutdownPerformance = settings.Gameplay?.DefaultShutdownPerformance ?? 0, + NebulaQuality = settings.Graphics?.NebulaQuality ?? "", + ShowAsteroidsInMinimap = settings.Graphics?.ShowAsteroidsInMinimap ?? false, + BindingOverrides = Array.Empty(), + ActionBarInputs = settings.Input?.ActionBarInputs ?? Array.Empty() + }; +} + +static AetheriaRuntimeSurfaceDocument BuildTradeMenuSurface( + AetheriaRuntimeStationRefitDocument stationRefit, + AetheriaRuntimeCatalogSnapshot catalog, + string updatedAtUtc, + long version) +{ + stationRefit ??= new AetheriaRuntimeStationRefitDocument(); + var rows = (stationRefit.StationStock ?? Array.Empty()) + .Take(12) + .Select((item, index) => + { + var typedItem = catalog?.FindItem(item.ItemKey); + return SurfaceLeaf( + $"aetheria.trade.menu.stock.{index}", + "row", + ("item", typedItem?.Name ?? item.ItemKey), + ("key", item.ItemKey ?? ""), + ("qty", item.Quantity.ToString(CultureInfo.InvariantCulture)), + ("price", item.Price.ToString("N0", CultureInfo.InvariantCulture)), + ("owned", item.OwnedQuantity.ToString(CultureInfo.InvariantCulture)), + ("afford", item.CanAfford ? "yes" : "no")); + }) + .DefaultIfEmpty(SurfaceLeaf( + "aetheria.trade.menu.stock.empty", + "text", + ("value", "No station stock is available in the current daemon frame."))) + .ToArray(); + + return new AetheriaRuntimeSurfaceDocument( + providerId: "aetheria.daemon", + providerKind: "trade.menu", + title: "Trade Menu", + version: version, + updatedAtUtc: updatedAtUtc ?? "", + surface: new AetheriaRuntimeSurfaceTree( + "aetheria.trade.menu", + SurfaceNode( + "aetheria.trade.menu.root", + "surface", + Array.Empty<(string Key, string Value)>(), + SurfaceNode( + "aetheria.trade.menu.summary", + "card", + new[] { ("title", "Station Trade") }, + SurfaceLeaf("aetheria.trade.menu.summary.docked", "metric", ("label", "Docked"), ("value", stationRefit.IsDocked ? "yes" : "no")), + SurfaceLeaf("aetheria.trade.menu.summary.station", "metric", ("label", "Station"), ("value", stationRefit.DockParent?.DisplayName ?? stationRefit.DockParentEntityKey ?? "")), + SurfaceLeaf("aetheria.trade.menu.summary.credits", "metric", ("label", "Credits"), ("value", stationRefit.Credits.ToString("N0", CultureInfo.InvariantCulture)))), + SurfaceNode( + "aetheria.trade.menu.stock", + "card", + new[] { ("title", "Station Stock") }, + rows)), + Array.Empty()), + commands: Array.Empty()); +} + +static AetheriaRuntimeSurfaceComponent SurfaceLeaf( + string id, + string kind, + params (string Key, string Value)[] props) +{ + return SurfaceNode(id, kind, props, Array.Empty()); +} + +static AetheriaRuntimeSurfaceComponent SurfaceNode( + string id, + string kind, + (string Key, string Value)[] props, + params AetheriaRuntimeSurfaceComponent[] children) +{ + return new AetheriaRuntimeSurfaceComponent( + id, + kind, + props.ToDictionary(prop => prop.Key, prop => prop.Value), + children); } static async Task AcceptEveCommandsAsync(AetheriaStateNode node, AetheriaDaemonHostOptions options) diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCultCacheDocumentStore.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCultCacheDocumentStore.cs index 850c42e2a..90275e81c 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCultCacheDocumentStore.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCultCacheDocumentStore.cs @@ -64,7 +64,7 @@ namespace GameCult.Aetheria.State.Verse payload); } - private static byte[] WriteRuntimeSurfaceDocument(AetheriaRuntimeSurfaceDocument document) + internal static byte[] WriteRuntimeSurfaceDocument(AetheriaRuntimeSurfaceDocument document) { var buffer = new ArrayBufferWriter(); var writer = new MessagePackWriter(buffer); @@ -173,7 +173,7 @@ namespace GameCult.Aetheria.State.Verse } } - private static AetheriaRuntimeSurfaceDocument ReadRuntimeSurfaceDocument(byte[] payload) + internal static AetheriaRuntimeSurfaceDocument ReadRuntimeSurfaceDocument(byte[] payload) { var reader = new MessagePackReader(payload); var fields = reader.ReadArrayHeader(); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs index 48b76e5df..278d90057 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs @@ -63,7 +63,7 @@ namespace GameCult.Aetheria.State.Verse Surface(AetheriaRuntimeDaemonGameSurfaceBuilder.TuiSurfaceId, "Aetheria Daemon TUI", AetheriaRuntimeVerseRecordKeys.DaemonGameTuiSurface.ToString(), "agent", "Compact daemon game surface for dense inspection.", providerId: "aetheria.daemon", providerKind: "game.daemon", transport: "cultmesh-record"), Surface(AetheriaRuntimeDaemonEditorSurfaceBuilder.SurfaceId, "Aetheria Daemon Editor", AetheriaRuntimeVerseRecordKeys.DaemonEditorSurface.ToString(), "operator", "Daemon editor and surface inventory.", providerId: "aetheria.daemon", providerKind: "editor.daemon", transport: "cultmesh-record"), Surface(AetheriaRuntimeDaemonEditorSurfaceBuilder.TuiSurfaceId, "Aetheria Daemon Editor TUI", AetheriaRuntimeVerseRecordKeys.DaemonEditorTuiSurface.ToString(), "agent", "Compact daemon editor and surface inventory.", providerId: "aetheria.daemon", providerKind: "editor.daemon", transport: "cultmesh-record"), - Surface(AetheriaRuntimeMainMenuCommands.RootSurfaceId, "Main Menu", "", "player", "Runtime main menu root.", status: "client-contextual"), + Surface(AetheriaRuntimeMainMenuCommands.RootSurfaceId, "Main Menu", AetheriaRuntimeVerseRecordKeys.MainMenuSurface.ToString(), "player", "Runtime main menu root.", transport: "cultmesh-record"), Surface(AetheriaRuntimeMainMenuCommands.SettingsSurfaceId, "Main Menu Settings", "", "player", "Runtime main menu settings shell.", status: "client-contextual"), Surface(AetheriaRuntimeMainMenuCommands.InputSettingsSurfaceId, "Main Menu Input Settings", "", "player", "Main menu input settings handoff shell.", status: "client-contextual"), Surface(AetheriaRuntimeMainMenuCommands.PlayerSettingsShellSurfaceId, "Main Menu Player Settings", "", "player", "Main menu player settings shell.", status: "client-contextual"), @@ -73,12 +73,13 @@ namespace GameCult.Aetheria.State.Verse Surface(AetheriaRuntimeLoadoutTemplateCommands.SurfaceId, "Loadout Templates", "", "player", "Loadout template save surface.", status: "client-contextual"), Surface(AetheriaRuntimeClientTargetCommands.SurfaceId, "Client Target", "", "operator", "Local client target and Verse discovery surface.", status: "client-contextual"), Surface(AetheriaRuntimeLocalStorySurfaceBuilder.SurfaceId, "Local Story", "", "player", "Local runtime story/menu surface.", status: "client-contextual"), - Surface(AetheriaRuntimeZoneDetailsSurfaceBuilder.SurfaceId, "Zone Details", "", "player", "Selected zone details surface.", status: "contextual"), - Surface(AetheriaRuntimeInventoryPanelSurfaceBuilder.SurfaceId, "Inventory Panel", "", "player", "Current inventory panel surface.", status: "contextual"), - Surface(AetheriaRuntimeInventoryDropdownSurfaceBuilder.SurfaceId, "Inventory Dropdown", "", "player", "Inventory navigation dropdown surface.", status: "contextual"), + Surface(AetheriaRuntimeZoneDetailsSurfaceBuilder.SurfaceId, "Map Menu", AetheriaRuntimeVerseRecordKeys.MapMenuSurface.ToString(), "player", "Current-zone map details surface.", transport: "cultmesh-record"), + Surface(AetheriaRuntimeInventoryPanelSurfaceBuilder.SurfaceId, "Inventory Panel", AetheriaRuntimeVerseRecordKeys.InventoryPanelSurface.ToString(), "player", "Current inventory panel surface.", transport: "cultmesh-record"), + Surface(AetheriaRuntimeInventoryDropdownSurfaceBuilder.SurfaceId, "Inventory Dropdown", AetheriaRuntimeVerseRecordKeys.InventoryDropdownSurface.ToString(), "player", "Inventory navigation dropdown surface.", transport: "cultmesh-record"), Surface(AetheriaRuntimeShipSettingsSurfaceBuilder.SurfaceId, "Current Ship Settings", "", "player", "Current ship settings surface.", status: "contextual"), Surface(AetheriaRuntimeCargoItemDetailsSurfaceBuilder.SurfaceId, "Cargo Item Details", "", "player", "Selected cargo item details surface.", status: "contextual"), Surface(AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.SurfaceId, "Equipped Item Details", "", "player", "Selected equipped item details surface.", status: "contextual"), + Surface("aetheria.trade.menu", "Trade Menu", AetheriaRuntimeVerseRecordKeys.TradeMenuSurface.ToString(), "player", "Daemon-derived station stock trade menu.", transport: "cultmesh-record"), Surface(AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.SurfaceId, "Trade Cargo Selector", "", "player", "Trade cargo target selector surface.", status: "contextual"), Surface(AetheriaRuntimeTradeInteractionSurfaceBuilder.FilterSurfaceId, "Trade Filter Selector", "", "player", "Trade filter selector surface.", status: "contextual"), Surface(AetheriaRuntimeTradeInteractionSurfaceBuilder.RowActionSurfaceId, "Trade Row Actions", "", "player", "Trade row action selector surface.", status: "contextual"), diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceDocumentMetadata.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceDocumentMetadata.cs new file mode 100644 index 000000000..5a9d197c5 --- /dev/null +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceDocumentMetadata.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using GameCult.Caching; +using GameCult.Eve.Surface; + +[assembly: CultGeneratedDocumentMetadataProvider(typeof(GameCult.Aetheria.State.Verse.AetheriaRuntimeEveSurfaceDocumentMetadataProvider))] + +namespace GameCult.Aetheria.State.Verse +{ + public sealed class AetheriaRuntimeEveSurfaceDocumentMetadataProvider : ICultGeneratedDocumentMetadataProvider + { + public IEnumerable GetDocumentDefinitions() + { + yield return new CultGeneratedDocumentDefinition( + typeof(EveSurfaceDocument), + "gamecult.eve.surface", + "gamecult.eve.surface.v1", + isGlobal: false, + nameMember: null, + nameAccessor: null, + serializePayload: value => AetheriaRuntimeCultCacheDocumentStore.WriteRuntimeSurfaceDocument( + AetheriaRuntimeSurfaceDocuments.FromPortableSurface((EveSurfaceDocument)value)), + deserializePayload: payload => AetheriaRuntimeSurfaceDocuments.ToPortableSurface( + AetheriaRuntimeCultCacheDocumentStore.ReadRuntimeSurfaceDocument(payload)), + indexAccessors: Array.Empty(), + members: new[] + { + Member("Type", 0, "string"), + Member("Schema", 1, "string"), + Member("ProviderId", 2, "string"), + Member("ProviderKind", 3, "string"), + Member("Title", 4, "string"), + Member("Version", 5, "int64"), + Member("UpdatedAtUtc", 6, "string"), + Member("Surface", 7, "object"), + Member("Commands", 8, "array") + }); + } + + private static CultGeneratedDocumentMemberDefinition Member( + string name, + int slot, + string typeName) + { + return new CultGeneratedDocumentMemberDefinition( + name, + slot, + typeName, + isReference: false, + isMany: false, + targetSchemaName: null, + isName: false, + indexAlias: null); + } + } +} diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs index 4f6cd43b6..4c164a932 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs @@ -65,6 +65,21 @@ namespace GameCult.Aetheria.State.Verse public static CultRecordKey DaemonEditorTuiSurface { get; } = new CultRecordKey("eve:surface:aetheria.daemon.editor.tui"); + public static CultRecordKey MainMenuSurface { get; } = + new CultRecordKey("eve:surface:aetheria.main_menu.root"); + + public static CultRecordKey InventoryPanelSurface { get; } = + new CultRecordKey("eve:surface:aetheria.inventory.panel"); + + public static CultRecordKey InventoryDropdownSurface { get; } = + new CultRecordKey("eve:surface:aetheria.inventory.panel.dropdown"); + + public static CultRecordKey MapMenuSurface { get; } = + new CultRecordKey("eve:surface:aetheria.map.zone_details"); + + public static CultRecordKey TradeMenuSurface { get; } = + new CultRecordKey("eve:surface:aetheria.trade.menu"); + public static CultRecordKey DaemonCommand(string commandId) { return new CultRecordKey($"daemon:commands:{StableToken(commandId)}:gamecult.aetheria.daemon_command.v1");