Move RTS menu panel authority to daemon

This commit is contained in:
Meta Vens 2026-07-05 18:07:21 +01:00
commit b86635fda5
15 changed files with 361 additions and 195 deletions

View file

@ -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<HTMLElement>("#eve-surface-host");
const statusEl = requiredElement<HTMLElement>("#status");
let latestEveSurfaceKey = "";
let activeMainMenuSurfaceId = "aetheria.main_menu.root";
let renderDaemonEveSurfaceNow: (() => Promise<void>) | null = null;
let eveSurfacePoll: number | null = null;
@ -48,14 +46,13 @@ async function showDaemonEveSurface(): Promise<void> {
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<void> {
}
}
async function showMainMenuPanel(surfaceId: string): Promise<void> {
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<string, string>;
style?: Record<string, string>;
};
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<void> {
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<void> {
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<HTMLButtonElement>("[data-window-control]").forEach(button => {
if (button.dataset.windowControlWired === "true") {

View file

@ -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 = {

View file

@ -1089,10 +1089,24 @@ static async Task PublishDaemonApiDocumentsAsync(
await node.MutableDocument<AetheriaRuntimeStarbridgeSessionSummaryDocument>(AetheriaRuntimeVerseRecordKeys.StarbridgeSessionSummary)
.ReplaceAsync(result.StarbridgeSessionSummary)
.ConfigureAwait(false);
var mainMenuState = await node.MutableDocument<AetheriaMainMenuState>(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<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.DaemonGameSurface)
.ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(result.GameSurface))
.ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(gameSurface))
.ConfigureAwait(false);
}
if (result.GameTuiSurface != null)
await node.MutableDocument<EveSurfaceDocument>(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,

View file

@ -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<AetheriaTradeValuePolicy>(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<AetheriaMainMenuState>(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[]

View file

@ -37,6 +37,7 @@ public static class AetheriaDocumentRegistry
typeof(AetheriaZoneState),
typeof(AetheriaEntitySnapshot),
typeof(AetheriaVerseHostSettings),
typeof(AetheriaMainMenuState),
typeof(AetheriaEveCommandAcceptanceStatus),
typeof(AetheriaRuntimeDaemonProviderAdvertisementDocument),
typeof(AetheriaRuntimeDaemonHealthDocument),

View file

@ -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<AetheriaMainMenuState>(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<AetheriaMainMenuState>(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)

View file

@ -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);

View file

@ -69,4 +69,7 @@ public sealed class AetheriaEveCommandAcceptanceStatus
[Key(19)]
public string[] AccountedCommandIds { get; set; } = [];
[Key(20)]
public int AppliedMainMenuCommands { get; set; }
}

View file

@ -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; } = "";
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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<AetheriaRuntimeSurfaceComponent>
{
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<string, string>(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

View file

@ -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)

View file

@ -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

View file

@ -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)