Make RTS menu use daemon Eve surface

This commit is contained in:
Meta Vens 2026-07-05 16:17:18 +01:00
commit 7c8a12261d
4 changed files with 108 additions and 188 deletions

View file

@ -7,6 +7,7 @@ import {
import type {
AetheriaRtsApi,
AetheriaMenuSurfaceDocument,
AetheriaMenuSurfaceComponent,
AuthorityStatusDocument,
AetheriaRuntimeViewportFeedSnapshot,
BodyView,
@ -59,6 +60,8 @@ let refreshInFlight = false;
let viewportFeedUnsubscribe: (() => void) | null = null;
let eveSurfacePoll: number | null = null;
let latestEveSurfaceKey = "";
let activeMainMenuSurfaceId = "aetheria.main_menu.root";
let renderDaemonEveSurfaceNow: (() => Promise<void>) | null = null;
const mainMenuMode = window.location.hash === "#main-menu";
const legacyViewportMode = window.location.hash === "#legacy-viewport";
@ -99,40 +102,6 @@ function setStatus(text: string): void {
statusEl.textContent = text;
}
async function showMainMenu(surfaceId = "aetheria.main_menu.root"): Promise<void> {
document.body.classList.add("main-menu-mode");
document.body.classList.remove("eve-game-mode");
viewportFeedUnsubscribe?.();
stopEveSurfacePoll();
const surface = await window.aetheriaRts.mainMenuSurface({
surfaceId,
inGame: false,
canOpenRuntimeInputScreen: false,
});
renderMainMenuSurface(surface);
}
function renderMainMenuSurface(surface: AetheriaMenuSurfaceDocument): void {
let host = document.querySelector<HTMLElement>("#main-menu-host");
if (!host) {
host = document.createElement("section");
host.id = "main-menu-host";
host.className = "main-menu-host";
document.body.append(host);
}
renderEveSurface(surface, host, {
body: document.body,
assetUrlResolver: resolveAetheriaAssetUrl,
clientId: "aetheria.rts.electron",
commandSink: intent => handleMenuCommand(intent.command, surface),
documentResolver: resolveEveDocument,
source: "Aetheria Electron",
statusElement: statusEl,
});
wireWindowControls();
}
async function showDaemonEveSurface(): Promise<void> {
document.body.classList.remove("main-menu-mode");
document.body.classList.add("eve-game-mode");
@ -152,10 +121,11 @@ async function showDaemonEveSurface(): Promise<void> {
const surface = await window.aetheriaRts.eveSurface({
recordKey: "eve:surface:aetheria.daemon.game",
});
const surfaceKey = `${surface.providerId}:${surface.surface.id}:${surface.version}:${surface.updatedAtUtc}`;
const loweredSurface = withActiveMainMenuPanel(surface, activeMainMenuSurfaceId);
const surfaceKey = `${surface.providerId}:${surface.surface.id}:${surface.version}:${surface.updatedAtUtc}:${activeMainMenuSurfaceId}`;
if (surfaceKey !== latestEveSurfaceKey) {
latestEveSurfaceKey = surfaceKey;
renderEveSurface(surface, host, {
renderEveSurface(loweredSurface, host, {
body: document.body,
assetUrlResolver: resolveAetheriaAssetUrl,
clientId: "aetheria.rts.electron",
@ -171,6 +141,7 @@ async function showDaemonEveSurface(): Promise<void> {
}
};
renderDaemonEveSurfaceNow = renderLatest;
await renderLatest();
if (eveSurfacePoll == null) {
eveSurfacePoll = window.setInterval(() => {
@ -179,6 +150,70 @@ 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,
@ -291,22 +326,22 @@ function handleMenuCommand(commandOrIntent: string | EveCommandIntent, surface?:
const command = typeof commandOrIntent === "string" ? commandOrIntent : commandOrIntent.command;
switch (command) {
case "aetheria.main_menu.root.show_settings":
void showMainMenu("aetheria.main_menu.settings");
void showMainMenuPanel("aetheria.main_menu.settings");
return;
case "aetheria.main_menu.settings.show_player_settings":
void showMainMenu("aetheria.main_menu.player_settings");
void showMainMenuPanel("aetheria.main_menu.player_settings");
return;
case "aetheria.main_menu.settings.show_verse_settings":
void showMainMenu("aetheria.main_menu.verse_settings");
void showMainMenuPanel("aetheria.main_menu.verse_settings");
return;
case "aetheria.main_menu.settings.show_input_settings":
void showMainMenu("aetheria.main_menu.input_settings");
void showMainMenuPanel("aetheria.main_menu.input_settings");
return;
case "aetheria.main_menu.settings.back_to_settings":
void showMainMenu("aetheria.main_menu.settings");
void showMainMenuPanel("aetheria.main_menu.settings");
return;
case "aetheria.main_menu.settings.back_to_main":
void showMainMenu("aetheria.main_menu.root");
void showMainMenuPanel("aetheria.main_menu.root");
return;
default:
setStatus(`${surface?.title ?? "Aetheria Starbridge"}: ${command || "no command"}`);
@ -841,7 +876,7 @@ function roleText(session: StarbridgeSessionDocument): string {
}
if (mainMenuMode) {
void showMainMenu();
void showDaemonEveSurface();
} else if (legacyViewportMode) {
document.body.classList.remove("main-menu-mode", "eve-game-mode");
updateZoomLabel();

View file

@ -700,34 +700,11 @@ function buildMainMenuSurface(
surfaceId: string,
canOpenRuntimeInputScreen: boolean,
inGame: boolean,
panelOnly = false,
_panelOnly = false,
): AetheriaMenuSurfaceDocument {
const updatedAtUtc = new Date().toISOString();
const activeSurfaceId = normalizeMainMenuSurfaceId(surfaceId);
return panelOnly
? buildMainMenuPanelSurface(activeSurfaceId, canOpenRuntimeInputScreen, inGame, updatedAtUtc)
: buildMainMenuShellSurface(activeSurfaceId, updatedAtUtc);
}
function buildMainMenuShellSurface(
activeSurfaceId: string,
updatedAtUtc: string,
): AetheriaMenuSurfaceDocument {
return surfaceDocument(
"aetheria.main_menu.shell",
"Aetheria Starbridge",
updatedAtUtc,
mainMenuCommands(),
gravitySurface("aetheria.main_menu.shell.gravity"),
surfaceSlot(
"aetheria.main_menu.shell.panel",
"mainMenuPanel",
activeSurfaceId,
eveSurfaceSchemaId,
"menu.overlay",
{ position: "absolute", top: "0", right: "0", bottom: "0", left: "0", width: "100%", height: "100%" },
{ background: "rgba(0,0,0,0)" }),
);
return buildMainMenuPanelSurface(activeSurfaceId, canOpenRuntimeInputScreen, inGame, updatedAtUtc);
}
function buildMainMenuPanelSurface(
@ -858,7 +835,6 @@ function buildMainMenuPanelSurface(
function normalizeMainMenuSurfaceId(surfaceId: string): string {
switch (surfaceId) {
case "aetheria.main_menu.shell":
case "":
return "aetheria.main_menu.root";
case "aetheria.main_menu.root":
@ -872,20 +848,6 @@ function normalizeMainMenuSurfaceId(surfaceId: string): string {
}
}
function mainMenuCommands(): AetheriaMenuSurfaceCommand[] {
return [
command("aetheria.main_menu.root.continue", "Continue"),
command("aetheria.main_menu.root.new_game", "New Game"),
command("aetheria.main_menu.root.show_settings", "Settings"),
command("aetheria.main_menu.root.quit", "Quit"),
command("aetheria.main_menu.settings.show_player_settings", "Player Settings"),
command("aetheria.main_menu.settings.show_verse_settings", "Verse"),
command("aetheria.main_menu.settings.show_input_settings", "Input"),
command("aetheria.main_menu.settings.back_to_main", "Back"),
command("aetheria.main_menu.settings.back_to_settings", "Back"),
];
}
function surfaceDocument(
surfaceId: string,
title: string,
@ -893,7 +855,6 @@ function surfaceDocument(
commands: AetheriaMenuSurfaceCommand[],
...children: AetheriaMenuSurfaceComponent[]
): AetheriaMenuSurfaceDocument {
const isMenuShell = surfaceId === "aetheria.main_menu.shell";
return {
providerId: "aetheria",
providerKind: "game.menu",
@ -907,7 +868,7 @@ function surfaceDocument(
"surface",
{},
{ position: "relative", overflow: "hidden", width: "100%", height: "100vh", minHeight: "100vh" },
{ background: isMenuShell ? "#020606" : "rgba(0,0,0,0)" },
{ background: "rgba(0,0,0,0)" },
...children),
styles: [
{ name: "font.title.family", value: "Montserrat" },
@ -948,26 +909,6 @@ function node(
return { id, kind, props, layout, style, children: normalizedChildren };
}
function surfaceSlot(
id: string,
slotId: string,
documentId: string,
schemaId: string,
presentationKind: string,
layout?: Record<string, string>,
style?: Record<string, string>,
): AetheriaMenuSurfaceComponent {
return {
id,
kind: "surface.slot",
props: { slotId, documentId, schemaId, presentationKind },
layout,
style,
embeddedDocuments: [{ slotId, documentId, schemaId, presentationKind }],
children: [],
};
}
function text(
id: string,
value: string,
@ -1016,8 +957,10 @@ function button(id: string, label: string, commandId: string): AetheriaMenuSurfa
{ minWidth: "0", width: "220px", height: "32px", padding: "0" },
{
background: "rgba(0, 0, 0, 0)",
border: "0",
borderWidth: "0",
borderStyle: "solid",
borderRadius: "0",
boxShadow: "none",
font: "400 1.55rem/32px Ubuntu, sans-serif",
color: "#e8fbff",
@ -1029,43 +972,6 @@ function buttonColumn(id: string, ...children: AetheriaMenuSurfaceComponent[]):
return node(id, "column", {}, { gap: "0.18rem", alignItems: "flex-start" }, { color: "#e8fbff" }, ...children);
}
function gravitySurface(id: string): AetheriaMenuSurfaceComponent {
return node(
id,
"gravity.surface",
{
label: "Aetheria level gravity surface",
viewRadius: "1450",
terrainRadius: "1450",
terrainDepth: "-8",
terrainDepthExponent: "1.2",
terrainWaveFrequency: "0.6",
simulationTimeSeconds: "0",
samplesX: "196",
isolines: "22",
shader: "perlines.gravity-map.v1",
noiseAsset: "cultmesh://aetheria/assets/textures/perlines-nebula",
tintSplatAsset: "cultmesh://aetheria/assets/textures/tint_splat",
bodies: [
"terminus-sun|Sun|0|0|900|-80|3|450|10|2|cultmesh://aetheria/assets/icons/star.terminus-sun|cultmesh://aetheria/assets/textures/tint_splat",
"anchor-moon|Planet|360|-180|260|-22|2.2|180|4|1.4|cultmesh://aetheria/assets/icons/body.anchor-moon|cultmesh://aetheria/assets/textures/tint_splat",
"blueglass|Planet|-540|340|340|-34|2.6|150|5|1.1|cultmesh://aetheria/assets/icons/body.blueglass|cultmesh://aetheria/assets/textures/tint_splat",
"emberhook|Planet|-680|-420|310|-28|2.1|130|4|1.3|cultmesh://aetheria/assets/icons/body.emberhook|cultmesh://aetheria/assets/textures/tint_splat",
"vesper|Gas Giant|780|420|420|-38|2.4|210|6|0.7|cultmesh://aetheria/assets/icons/body.vesper|cultmesh://aetheria/assets/textures/tint_splat",
"latch|Moon|-160|620|210|-18|2.8|95|3|1.6|cultmesh://aetheria/assets/icons/body.latch|cultmesh://aetheria/assets/textures/tint_splat",
].join(";"),
objects: [
"anchor-station|station|Anchor Station|-220|-90|1|0|player|1|1|cultmesh://aetheria/assets/icons/station.anchor-station",
"vanguard-one|ship|Vanguard One|-60|-40|1|0.2|player|1|1|cultmesh://aetheria/assets/icons/ship.vanguard-one",
"wing-two|ship|Wing Two|180|120|-0.4|0.9|player|1|1|cultmesh://aetheria/assets/icons/ship.wing-two",
"torch-three|ship|Torch Three|-160|210|0.6|-0.8|player|1|1|cultmesh://aetheria/assets/icons/ship.torch-three",
"ash-raider|ship|Ash Raider|420|180|-0.8|-0.1|raider|0|1|cultmesh://aetheria/assets/icons/ship.ash-raider",
"derelict-relay|station|Derelict Relay|-260|260|0|1|neutral|0|1|cultmesh://aetheria/assets/icons/station.derelict-relay",
].join(";"),
},
{ position: "absolute", top: "0", right: "0", bottom: "0", left: "0", width: "100%", height: "100%" });
}
function isSurfaceComponent(value: unknown): value is AetheriaMenuSurfaceComponent {
return !!value && typeof value === "object" && "kind" in value && "props" in value && "children" in value;
}

View file

@ -96,10 +96,7 @@ app.whenReady().then(async () => {
throw new Error("AETHERIA_RTS_PEER_CULTMESH_ENDPOINTS has been removed. Configure AETHERIA_RTS_CULTMESH_URI and let Odin/CultMesh discover peer endpoints.");
}
if (rendererView === "main-menu") {
showStartup("Launching Aetheria Menu", "Lowering the CultUI main-menu surface.");
mkdirSync(runtimeRoot, { recursive: true });
} else if (launchLocalDaemon) {
if (launchLocalDaemon) {
await ensureDotnetBuild();
showStartup("Launching Aetheria RTS", "Starting the Aetheria daemon.");
mkdirSync(runtimeRoot, { recursive: true });
@ -139,11 +136,11 @@ app.whenReady().then(async () => {
"aetheria-rts-electron",
{ publicationMode: launchLocalDaemon ? "local" : "remote" });
showStartup("Launching Aetheria RTS", "Waiting for the daemon CultMesh frame.");
await rtsClient.waitForFrame(30000);
if (rendererView === "main-menu") {
await mainWindow.loadFile(rendererIndex, { hash: "main-menu" });
} else {
showStartup("Launching Aetheria RTS", "Waiting for the daemon CultMesh frame.");
await rtsClient.waitForFrame(30000);
await mainWindow.loadFile(rendererIndex);
}
if (electronSmoke) {

View file

@ -367,55 +367,37 @@ namespace GameCult.Aetheria.State.Verse
{
return new AetheriaRuntimeSurfaceComponent(
id,
"column",
new Dictionary<string, string>(StringComparer.Ordinal),
"surface.slot",
new Dictionary<string, string>(StringComparer.Ordinal)
{
["slotId"] = "mainMenuPanel",
["documentId"] = AetheriaRuntimeMainMenuCommands.RootSurfaceId,
["schemaId"] = "gamecult.eve.surface.v1",
["presentationKind"] = "menu.overlay"
},
Array.Empty<AetheriaRuntimeSurfaceComponent>(),
AetheriaRuntimeSurfaceStateBindings.FromProps(new Dictionary<string, string>(StringComparer.Ordinal)),
new[]
{
TextNode(
$"{id}.title",
"AETHERIA",
"text.title",
Layout(("margin", "0 0 -1.35rem 0")),
new Dictionary<string, string>
{
["font"] = "100 5.3rem/0.98 Montserrat, sans-serif",
["color"] = "rgba(232, 250, 255, 0.94)",
["whiteSpace"] = "nowrap"
}),
TextNode(
$"{id}.subtitle",
"STARBRIDGE",
"text.subtitle",
Layout(("margin", "0 0 1.15rem 15.2rem")),
new Dictionary<string, string>
{
["font"] = "100 2.25rem/1 Montserrat, sans-serif",
["color"] = "rgba(232, 250, 255, 0.88)",
["whiteSpace"] = "nowrap"
}),
Node(
$"{id}.actions",
"column",
Array.Empty<(string Key, string Value)>(),
MenuButton($"{id}.continue", "Continue", "aetheria.main_menu.root.continue"),
MenuButton($"{id}.new_game", "New Game", "aetheria.main_menu.root.new_game"),
MenuButton($"{id}.settings", "Settings", "aetheria.main_menu.root.show_settings"),
MenuButton($"{id}.quit", "Quit", "aetheria.main_menu.root.quit"))
new AetheriaRuntimeEmbeddedDocumentSlot(
"mainMenuPanel",
AetheriaRuntimeMainMenuCommands.RootSurfaceId,
"gamecult.eve.surface.v1",
"menu.overlay")
},
AetheriaRuntimeSurfaceStateBindings.FromProps(new Dictionary<string, string>(StringComparer.Ordinal)),
Array.Empty<AetheriaRuntimeEmbeddedDocumentSlot>(),
Layout(
("position", "absolute"),
("left", "6.75rem"),
("top", "5.75rem"),
("width", "34rem"),
("gap", "1rem"),
("alignItems", "flex-start"),
("top", "0"),
("right", "0"),
("bottom", "0"),
("left", "0"),
("width", "100%"),
("height", "100%"),
("zIndex", "4"),
("pointerEvents", "auto")),
new Dictionary<string, string>
{
["color"] = "#e9fbff"
["background"] = "rgba(0,0,0,0)"
});
}