Lower Electron RTS through daemon Eve surface

This commit is contained in:
Meta Vens 2026-07-05 00:32:49 +01:00
commit fbc4848231
9 changed files with 549 additions and 52 deletions

View file

@ -297,6 +297,20 @@ export type AetheriaMenuSurfaceRequest = {
canOpenRuntimeInputScreen?: boolean;
};
export type AetheriaEveSurfaceRequest = {
surfaceId?: string;
recordKey?: string;
};
export type AetheriaEveCommandRequest = {
providerId?: string;
surfaceId?: string;
command?: string;
clientId?: string;
issuedAtUtc?: string;
payload?: Record<string, unknown>;
};
export type AetheriaMenuSurfaceDocument = {
providerId: string;
providerKind: string;
@ -346,6 +360,8 @@ export type AetheriaRtsApi = {
setTarget(request: AetheriaRuntimeSetTargetRequest): Promise<AetheriaRuntimeDaemonCommandReceipt>;
surfaceCatalog(): Promise<CultMeshSurfaceCatalogDiagnostic>;
surfaceCatalogIndex(): Promise<CultMeshSurfaceCatalogIndexDiagnostic>;
eveSurface(request: AetheriaEveSurfaceRequest): Promise<AetheriaMenuSurfaceDocument>;
submitEveCommand(request: AetheriaEveCommandRequest): Promise<AetheriaRuntimeDaemonCommandReceipt>;
mainMenuSurface(request: AetheriaMenuSurfaceRequest): Promise<AetheriaMenuSurfaceDocument>;
debugSurface(): Promise<AetheriaMenuSurfaceDocument>;
watchDebugSurface(callback: (surface: AetheriaMenuSurfaceDocument) => void): () => void;

View file

@ -55,7 +55,10 @@ let latestReceivedAt = 0;
let latestPollMs = 0;
let refreshInFlight = false;
let viewportFeedUnsubscribe: (() => void) | null = null;
let eveSurfacePoll: number | null = null;
let latestEveSurfaceKey = "";
const mainMenuMode = window.location.hash === "#main-menu";
const legacyViewportMode = window.location.hash === "#legacy-viewport";
function requiredElement<TElement extends Element>(selector: string): TElement {
const element = document.querySelector<TElement>(selector);
@ -96,7 +99,9 @@ function setStatus(text: string): void {
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,
@ -124,6 +129,50 @@ function renderMainMenuSurface(surface: AetheriaMenuSurfaceDocument): void {
wireWindowControls();
}
async function showDaemonEveSurface(): Promise<void> {
document.body.classList.remove("main-menu-mode");
document.body.classList.add("eve-game-mode");
viewportFeedUnsubscribe?.();
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);
}
host.setAttribute("aria-label", "Aetheria daemon Eve surface");
const renderLatest = async () => {
try {
const surface = await window.aetheriaRts.eveSurface({
recordKey: "eve:surface:aetheria.daemon.game",
});
const surfaceKey = `${surface.providerId}:${surface.surface.id}:${surface.version}:${surface.updatedAtUtc}`;
if (surfaceKey !== latestEveSurfaceKey) {
latestEveSurfaceKey = surfaceKey;
renderEveSurface(surface, host, {
body: document.body,
clientId: "aetheria.rts.electron",
commandSink: intent => submitEveCommand(intent),
source: "Aetheria Daemon",
statusElement: statusEl,
});
wireWindowControls();
}
} catch (error) {
setStatus(error instanceof Error ? error.message : "Aetheria daemon Eve surface unavailable.");
}
};
await renderLatest();
if (eveSurfacePoll == null) {
eveSurfacePoll = window.setInterval(() => {
void renderLatest();
}, 250);
}
}
async function startDebugSurface(): Promise<void> {
try {
renderDebugSurface(await window.aetheriaRts.debugSurface());
@ -144,9 +193,6 @@ function renderDebugSurface(surface: AetheriaMenuSurfaceDocument): void {
}
function wireWindowControls(): void {
if (!mainMenuMode)
return;
document.querySelectorAll<HTMLButtonElement>("[data-window-control]").forEach(button => {
if (button.dataset.windowControlWired === "true")
return;
@ -180,6 +226,29 @@ function handleMenuCommand(commandOrIntent: string | EveCommandIntent, surface:
}
}
async function submitEveCommand(intent: EveCommandIntent): Promise<void> {
try {
const receipt = await window.aetheriaRts.submitEveCommand({
providerId: intent.providerId,
surfaceId: intent.surfaceId,
command: intent.command,
clientId: intent.clientId,
issuedAtUtc: intent.issuedAt,
payload: intent.payload,
});
setStatus(`${intent.command}: ${receipt.accepted ? "submitted" : "rejected"}`);
} catch (error) {
setStatus(error instanceof Error ? error.message : `Failed to submit ${intent.command}.`);
}
}
function stopEveSurfacePoll(): void {
if (eveSurfacePoll == null)
return;
window.clearInterval(eveSurfacePoll);
eveSurfacePoll = null;
}
function startViewportFeed(): void {
const viewport = currentViewport();
viewportFeedUnsubscribe?.();
@ -680,9 +749,12 @@ function roleText(session: StarbridgeSessionDocument): string {
if (mainMenuMode) {
void showMainMenu();
} else {
} else if (legacyViewportMode) {
document.body.classList.remove("main-menu-mode", "eve-game-mode");
updateZoomLabel();
void startDebugSurface();
void refreshRuntimeSurfaces();
void refresh();
} else {
void showDaemonEveSurface();
}

View file

@ -16,6 +16,7 @@ import type {
} from "cultnet-ts";
import {
AetheriaRtsSchemas,
aetheriaRuntimeEveCommandDocumentSlots,
createAetheriaRuntimeRtsDocuments,
createAetheriaRuntimeRtsOperationHandles,
createAetheriaRuntimeRtsVerseHandles,
@ -59,6 +60,8 @@ import {
} from "./aetheria-rts-local-documents.js";
const connectionId = 0x43554c54;
const eveSurfaceSchemaId = "gamecult.eve.surface.v1";
const defaultEveSurfaceRecordKey = "eve:surface:aetheria.daemon.game";
type AetheriaPublicationDocumentSpec = CultMeshPublicationDocumentBinding & {
readonly localPath: string;
@ -84,6 +87,20 @@ export type AetheriaMenuSurfaceRequest = {
canOpenRuntimeInputScreen?: boolean;
};
export type AetheriaEveSurfaceRequest = {
surfaceId?: string;
recordKey?: string;
};
export type AetheriaEveCommandRequest = {
providerId?: string;
surfaceId?: string;
command?: string;
clientId?: string;
issuedAtUtc?: string;
payload?: Record<string, unknown>;
};
export type AetheriaMenuSurfaceDocument = {
providerId: string;
providerKind: string;
@ -291,6 +308,68 @@ export class AetheriaCultMeshClient {
request.inGame === true);
}
public async eveSurface(request: AetheriaEveSurfaceRequest = {}): Promise<AetheriaMenuSurfaceDocument> {
const recordKey = eveSurfaceRecordKey(request);
const document = CultMesh.documentFromPublication(
{
kind: "peer-snapshot",
peer: () => this.peer(),
endpoint: this.resolvedRudpEndpoint(),
},
eveSurfaceSchemaId,
recordKey,
{
documentId: recordKey,
routeHint: this.queryVerse.context.routeHint,
sourceId: recordKey,
timeoutMs: 1500,
pollMs: 50,
messageIdPrefix: `${this.runtimeId}:eve-surface`,
},
);
return normalizeEveSurfaceDocument(await document.latest(this.queryContext()));
}
public async submitEveCommand(request: AetheriaEveCommandRequest): Promise<AetheriaRuntimeDaemonCommandReceipt> {
const issuedAtUtc = request?.issuedAtUtc || new Date().toISOString();
const commandId = `${this.runtimeId}:eve:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
const command: unknown[] = [];
command[aetheriaRuntimeEveCommandDocumentSlots.schema] = AetheriaRtsSchemas.eveCommand;
command[aetheriaRuntimeEveCommandDocumentSlots.commandId] = commandId;
command[aetheriaRuntimeEveCommandDocumentSlots.providerId] = request?.providerId ?? "";
command[aetheriaRuntimeEveCommandDocumentSlots.surfaceId] = request?.surfaceId ?? "";
command[aetheriaRuntimeEveCommandDocumentSlots.command] = request?.command ?? "";
command[aetheriaRuntimeEveCommandDocumentSlots.issuedAtUtc] = issuedAtUtc;
command[aetheriaRuntimeEveCommandDocumentSlots.clientId] = request?.clientId || this.runtimeId;
command[aetheriaRuntimeEveCommandDocumentSlots.payload] = normalizeCommandPayload(request?.payload);
const message: CultNetDocumentPutRawMessage = {
schemaVersion: "cultnet.document_put_raw.v0",
messageId: commandId,
document: {
schemaId: AetheriaRtsSchemas.eveCommand,
recordKey: `daemon:eve-commands:${stableToken(commandId)}:${AetheriaRtsSchemas.eveCommand}`,
storedAt: issuedAtUtc,
payloadEncoding: "messagepack",
payload: encode(command),
sourceRuntimeId: this.runtimeId,
sourceRole: "rts-client",
tags: ["aetheria-rts", "eve-command"],
},
};
const peer = await this.peer();
peer.send(message);
await delay(80);
return {
commandId,
operationId: commandId,
accepted: true,
route: this.commandVerse.context.routeHint,
diagnostic: "submitted Eve surface command",
};
}
public queryDiagnostics(): Readonly<Record<string, AetheriaRuntimeRtsQueryDiagnostic>> {
return describeAetheriaRuntimeRtsQueryHandles(this.queries);
}
@ -896,6 +975,139 @@ function createAetheriaPublicationDocuments(
: document);
}
function eveSurfaceRecordKey(request: AetheriaEveSurfaceRequest): string {
if (request?.recordKey && request.recordKey.trim())
return request.recordKey.trim();
switch (request?.surfaceId) {
case "aetheria.game":
case "aetheria.daemon.game":
return "eve:surface:aetheria.daemon.game";
case "aetheria.game.tui":
case "aetheria.daemon.game.tui":
return "eve:surface:aetheria.daemon.game.tui";
case "aetheria.main_menu.root":
return "eve:surface:aetheria.main_menu.root";
case "aetheria.inventory.panel":
return "eve:surface:aetheria.inventory.panel";
case "aetheria.inventory.panel.dropdown":
return "eve:surface:aetheria.inventory.panel.dropdown";
case "aetheria.map.zone_details":
return "eve:surface:aetheria.map.zone_details";
case "aetheria.trade.menu":
return "eve:surface:aetheria.trade.menu";
default:
return defaultEveSurfaceRecordKey;
}
}
function normalizeEveSurfaceDocument(document: unknown): AetheriaMenuSurfaceDocument {
const portableSlots = Array.isArray(document) &&
typeof document[1] === "string" &&
document[1] === eveSurfaceSchemaId;
const providerSlot = portableSlots ? 2 : 0;
const providerKindSlot = portableSlots ? 3 : 1;
const titleSlot = portableSlots ? 4 : 2;
const versionSlot = portableSlots ? 5 : 3;
const updatedAtSlot = portableSlots ? 6 : 4;
const surfaceSlot = portableSlots ? 7 : 5;
const commandsSlot = portableSlots ? 8 : 6;
const surface = readField(document, "surface", surfaceSlot);
const surfaceId = stringOr(readField(surface, "id", 0), "aetheria.surface.missing");
return {
providerId: stringOr(readField(document, "providerId", providerSlot), "aetheria.daemon"),
providerKind: stringOr(readField(document, "providerKind", providerKindSlot), "daemon"),
title: stringOr(readField(document, "title", titleSlot), surfaceId),
version: numberOr(readField(document, "version", versionSlot), 0),
updatedAtUtc: stringOr(readField(document, "updatedAtUtc", updatedAtSlot), ""),
surface: {
id: surfaceId,
root: normalizeEveComponent(readField(surface, "root", 1), `${surfaceId}.root`),
styles: arrayValue(readField(surface, "styles", 2))
.map(token => ({
name: stringOr(readField(token, "name", 0), ""),
value: stringOr(readField(token, "value", 1), ""),
})),
},
commands: arrayValue(readField(document, "commands", commandsSlot))
.map(command => {
const operation = readField(command, "operation", 0);
const routeHint = readField(operation, "routeHint", 3);
return {
command: stringOr(readField(operation, "operationId", 0), ""),
label: stringOr(readField(operation, "label", 1), ""),
transport: stringOr(readField(routeHint, "description", 1), "cultmesh"),
};
})
.filter(command => command.command.length > 0),
};
}
function normalizeEveComponent(component: unknown, fallbackId: string): AetheriaMenuSurfaceComponent {
return {
id: stringOr(readField(component, "id", 0), fallbackId),
kind: stringOr(readField(component, "kind", 1), "surface"),
props: stringRecord(readField(component, "props", 2)),
layout: stringRecord(readField(component, "layout", 6)),
style: stringRecord(readField(component, "style", 7)),
children: arrayValue(readField(component, "children", 3))
.map((child, index) => normalizeEveComponent(child, `${fallbackId}.${index}`)),
};
}
function readField(value: unknown, property: string, index: number): unknown {
if (Array.isArray(value))
return value[index];
if (value && typeof value === "object") {
const record = value as Record<string, unknown>;
if (property in record)
return record[property];
const pascal = property.charAt(0).toUpperCase() + property.slice(1);
if (pascal in record)
return record[pascal];
}
return undefined;
}
function arrayValue(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function normalizeCommandPayload(payload: Record<string, unknown> | undefined): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(payload ?? {})) {
if (value == null)
continue;
result[key] = typeof value === "string" ? value : String(value);
}
return result;
}
function objectValue(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
function stringOr(value: unknown, fallback: string): string {
return typeof value === "string" ? value : fallback;
}
function numberOr(value: unknown, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function stringRecord(value: unknown): Record<string, string> {
const source = objectValue(value);
const result: Record<string, string> = {};
for (const [key, entry] of Object.entries(source)) {
if (entry == null)
continue;
result[key] = typeof entry === "string" ? entry : String(entry);
}
return result;
}
function delay(milliseconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}

View file

@ -4,6 +4,7 @@ export {
AetheriaRtsIpcChannels,
AetheriaRtsSchemas,
AetheriaRuntimeDaemonCommandKinds,
aetheriaRuntimeEveCommandDocumentSlots,
createAetheriaRuntimeRtsDocuments,
createAetheriaRuntimeRtsOperationHandles,
createAetheriaRuntimeRtsQueryHandles,

View file

@ -116,6 +116,8 @@ export const AetheriaRtsIpcChannels = {
setTarget: "aetheria-rts:set-target",
surfaceCatalog: "aetheria-rts:surface-catalog",
surfaceCatalogIndex: "aetheria-rts:surface-catalog-index",
eveSurface: "aetheria-rts:eve-surface",
submitEveCommand: "aetheria-rts:submit-eve-command",
mainMenuSurface: "aetheria-rts:main-menu-surface",
debugSurface: "aetheria-rts:debug-surface",
debugSurfaceWatch: "aetheria-rts:debug-surface-watch",
@ -1732,6 +1734,62 @@ export type AetheriaRuntimeViewportFeedSnapshot = {
sampleMs: number;
};
export type AetheriaMenuSurfaceRequest = {
surfaceId?: string;
inGame?: boolean;
canOpenRuntimeInputScreen?: boolean;
};
export type AetheriaEveSurfaceRequest = {
surfaceId?: string;
recordKey?: string;
};
export type AetheriaEveCommandRequest = {
providerId?: string;
surfaceId?: string;
command?: string;
clientId?: string;
issuedAtUtc?: string;
payload?: Record<string, unknown>;
};
export type AetheriaMenuSurfaceDocument = {
providerId: string;
providerKind: string;
title: string;
version: number;
updatedAtUtc: string;
surface: AetheriaMenuSurfaceTree;
commands: AetheriaMenuSurfaceCommand[];
};
export type AetheriaMenuSurfaceTree = {
id: string;
root: AetheriaMenuSurfaceComponent;
styles: AetheriaMenuStyleToken[];
};
export type AetheriaMenuSurfaceComponent = {
id: string;
kind: string;
props: Record<string, string>;
layout?: Record<string, string>;
style?: Record<string, string>;
children: AetheriaMenuSurfaceComponent[];
};
export type AetheriaMenuStyleToken = {
name: string;
value: string;
};
export type AetheriaMenuSurfaceCommand = {
command: string;
label: string;
transport: string;
};
export type AetheriaRtsMainClient = {
mapViewport(request: CultMeshViewportRequest): Promise<ViewportResponse>;
objectsViewport(request: CultMeshViewportRequest): Promise<ObjectsViewportResponse>;
@ -1745,6 +1803,8 @@ export type AetheriaRtsMainClient = {
watchViewportFeed(request: AetheriaRuntimeViewportFeedRequest, callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void): CultMeshUnsubscribe;
setMoveVector(request: AetheriaRuntimeSetMoveVectorRequest): Promise<AetheriaRuntimeDaemonCommandReceipt>;
setTarget(request: AetheriaRuntimeSetTargetRequest): Promise<AetheriaRuntimeDaemonCommandReceipt>;
eveSurface(request: AetheriaEveSurfaceRequest): Promise<AetheriaMenuSurfaceDocument>;
submitEveCommand(request: AetheriaEveCommandRequest): Promise<AetheriaRuntimeDaemonCommandReceipt>;
surfaceCatalogDiagnostics(): CultMeshSurfaceCatalogDiagnostic;
surfaceCatalogIndexDiagnostics(): CultMeshSurfaceCatalogIndexDiagnostic;
};
@ -1789,6 +1849,10 @@ export function registerAetheriaRtsIpcHandlers(
getClient().setMoveVector(request));
ipcMain.handle(AetheriaRtsIpcChannels.setTarget, async (_event, request: AetheriaRuntimeSetTargetRequest) =>
getClient().setTarget(request));
ipcMain.handle(AetheriaRtsIpcChannels.eveSurface, async (_event, request: AetheriaEveSurfaceRequest) =>
getClient().eveSurface(request));
ipcMain.handle(AetheriaRtsIpcChannels.submitEveCommand, async (_event, request: AetheriaEveCommandRequest) =>
getClient().submitEveCommand(request));
ipcMain.handle(AetheriaRtsIpcChannels.surfaceCatalog, () => getClient().surfaceCatalogDiagnostics());
ipcMain.handle(AetheriaRtsIpcChannels.surfaceCatalogIndex, () => getClient().surfaceCatalogIndexDiagnostics());
ipcMain.handle(AetheriaRtsIpcChannels.health, () => getHealth());

View file

@ -212,12 +212,9 @@ async function runElectronSmoke(window: BrowserWindow): Promise<Record<string, u
await new Promise(resolve => setTimeout(resolve, 100));
const api = window.aetheriaRts;
const status = document.querySelector("#status")?.textContent ?? "";
const selectionTitle = document.querySelector("#selection-title")?.textContent ?? "";
const selectionDetails = document.querySelector("#selection-details")?.textContent ?? "";
const frameDetails = document.querySelector("#frame-details")?.textContent ?? "";
const runtimeSurfaceDetails = document.querySelector("#runtime-surface-details")?.textContent ?? "";
const controlledText = document.querySelector("#controlled-list")?.textContent ?? "";
const canvas = document.querySelector("#map");
const bodyMode = document.body.className;
const eveHostText = document.querySelector("#main-menu-host")?.textContent ?? "";
const eveSurface = api ? await api.eveSurface({ recordKey: "eve:surface:aetheria.daemon.game" }) : null;
const health = api ? await api.daemonHealth() : null;
const authority = api ? await api.authorityStatus() : null;
const starbridge = api ? await api.starbridgeSession() : null;
@ -226,12 +223,12 @@ async function runElectronSmoke(window: BrowserWindow): Promise<Record<string, u
const surfaceCatalogIndex = api ? await api.surfaceCatalogIndex() : null;
const viewport = api ? await api.mapViewport({ minX: -5000, minY: -5000, maxX: 5000, maxY: 5000 }) : null;
const actor = viewport?.objects?.find(object => object.controlled) ?? viewport?.objects?.[0] ?? null;
const moveReceipt = api && actor ? await api.setMoveVector({
actorEntityKey: actor.entityKey,
directionX: 1,
directionY: 0,
scalar: 0.1,
observedFrameId: viewport.frameId,
const eveReceipt = api ? await api.submitEveCommand({
providerId: "aetheria.daemon",
surfaceId: "aetheria.game",
command: "sensor_ping",
clientId: "aetheria-rts-electron-smoke",
payload: {},
}) : null;
return {
hasApi: !!api &&
@ -245,24 +242,22 @@ async function runElectronSmoke(window: BrowserWindow): Promise<Record<string, u
typeof api.starbridgeSession === "function" &&
typeof api.assetManifest === "function" &&
typeof api.surfaceCatalog === "function" &&
typeof api.surfaceCatalogIndex === "function",
typeof api.surfaceCatalogIndex === "function" &&
typeof api.eveSurface === "function" &&
typeof api.submitEveCommand === "function",
status,
selectionTitle,
selectionDetails,
frameDetails,
runtimeSurfaceDetails,
controlledText,
canvasClientWidth: canvas?.clientWidth ?? 0,
canvasClientHeight: canvas?.clientHeight ?? 0,
canvasWidth: canvas?.width ?? 0,
canvasHeight: canvas?.height ?? 0,
bodyMode,
eveHostText,
eveSurface,
health,
authority,
starbridge,
assetManifest,
surfaceCatalog,
surfaceCatalogIndex,
moveReceipt
viewport,
actor,
eveReceipt
};
})()
`, true) as Record<string, unknown>;
@ -277,44 +272,35 @@ async function runElectronSmoke(window: BrowserWindow): Promise<Record<string, u
function isElectronSmokeReady(result: Record<string, unknown>): boolean {
const status = stringValue(result.status);
const selectionTitle = stringValue(result.selectionTitle);
const selectionDetails = stringValue(result.selectionDetails);
const frameDetails = stringValue(result.frameDetails);
const runtimeSurfaceDetails = stringValue(result.runtimeSurfaceDetails);
const controlledText = stringValue(result.controlledText);
const bodyMode = stringValue(result.bodyMode);
const eveHostText = stringValue(result.eveHostText);
const eveSurface = objectValue(result.eveSurface);
const health = objectValue(result.health);
const authority = objectValue(result.authority);
const starbridge = objectValue(result.starbridge);
const surfaceCatalog = objectValue(result.surfaceCatalog);
const surfaceCatalogIndex = objectValue(result.surfaceCatalogIndex);
const moveReceipt = objectValue(result.moveReceipt);
const viewport = objectValue(result.viewport);
const actor = objectValue(result.actor);
const eveReceipt = objectValue(result.eveReceipt);
return result.hasApi === true &&
status.includes("frame") &&
selectionTitle.length > 0 &&
selectionTitle !== "No Selection" &&
selectionDetails.includes("Equipment") &&
selectionDetails.includes("Cargo") &&
frameDetails.includes("Daemon") &&
frameDetails.includes("Authority") &&
runtimeSurfaceDetails.includes("network") &&
runtimeSurfaceDetails.includes("daemon:aetheria.frame.latest.v1") &&
runtimeSurfaceDetails.includes("gamecult.aetheria.pilot.set_move_vector.v1") &&
controlledText.length > 0 &&
numberValue(result.canvasClientWidth) > 0 &&
numberValue(result.canvasClientHeight) > 0 &&
numberValue(result.canvasWidth) > 0 &&
numberValue(result.canvasHeight) > 0 &&
status.includes("Aetheria Daemon") &&
bodyMode.includes("eve-game-mode") &&
eveHostText.includes("Daemon Frame") &&
eveHostText.includes("Typed Command Boundary") &&
objectValue(eveSurface?.surface)?.id === "aetheria.game" &&
health?.status === "healthy" &&
authority?.policyId === "aetheria.trusted-coop.v1" &&
starbridge?.scenarioName === "Frontier Fabricator Defense" &&
arrayValue(viewport?.objects).length > 0 &&
stringValue(actor?.entityKey).length > 0 &&
surfaceCatalog?.catalogId === "gamecult.aetheria.rts.surfaces.v1" &&
arrayValue(surfaceCatalogIndex?.queries).length > 0 &&
arrayValue(surfaceCatalogIndex?.operations).some(surface =>
objectValue(surface)?.surfaceId === "gamecult.aetheria.pilot.set_move_vector.v1") &&
stringValue(moveReceipt?.commandId).length > 0 &&
stringValue(moveReceipt?.operationId) === "gamecult.aetheria.pilot.set_move_vector.v1" &&
objectValue(moveReceipt?.route)?.kind === "network" &&
moveReceipt?.accepted === true;
stringValue(eveReceipt?.commandId).length > 0 &&
objectValue(eveReceipt?.route)?.kind === "network" &&
eveReceipt?.accepted === true;
}
function stringValue(value: unknown): string {

View file

@ -20,6 +20,8 @@ const channels = {
setTarget: "aetheria-rts:set-target",
surfaceCatalog: "aetheria-rts:surface-catalog",
surfaceCatalogIndex: "aetheria-rts:surface-catalog-index",
eveSurface: "aetheria-rts:eve-surface",
submitEveCommand: "aetheria-rts:submit-eve-command",
mainMenuSurface: "aetheria-rts:main-menu-surface",
debugSurface: "aetheria-rts:debug-surface",
debugSurfaceWatch: "aetheria-rts:debug-surface-watch",
@ -57,6 +59,8 @@ contextBridge.exposeInMainWorld("aetheriaRts", {
setTarget: request => ipcRenderer.invoke(channels.setTarget, request),
surfaceCatalog: () => ipcRenderer.invoke(channels.surfaceCatalog),
surfaceCatalogIndex: () => ipcRenderer.invoke(channels.surfaceCatalogIndex),
eveSurface: request => ipcRenderer.invoke(channels.eveSurface, request),
submitEveCommand: request => ipcRenderer.invoke(channels.submitEveCommand, request),
mainMenuSurface: request => ipcRenderer.invoke(channels.mainMenuSurface, request),
debugSurface: () => ipcRenderer.invoke(channels.debugSurface),
watchDebugSurface: callback => {

View file

@ -37,6 +37,8 @@ const ipcChannels = [
["setTarget", "aetheria-rts:set-target"],
["surfaceCatalog", "aetheria-rts:surface-catalog"],
["surfaceCatalogIndex", "aetheria-rts:surface-catalog-index"],
["eveSurface", "aetheria-rts:eve-surface"],
["submitEveCommand", "aetheria-rts:submit-eve-command"],
["mainMenuSurface", "aetheria-rts:main-menu-surface"],
["debugSurface", "aetheria-rts:debug-surface"],
["debugSurfaceWatch", "aetheria-rts:debug-surface-watch"],
@ -458,6 +460,62 @@ export type AetheriaRuntimeViewportFeedSnapshot = {
sampleMs: number;
};
export type AetheriaMenuSurfaceRequest = {
surfaceId?: string;
inGame?: boolean;
canOpenRuntimeInputScreen?: boolean;
};
export type AetheriaEveSurfaceRequest = {
surfaceId?: string;
recordKey?: string;
};
export type AetheriaEveCommandRequest = {
providerId?: string;
surfaceId?: string;
command?: string;
clientId?: string;
issuedAtUtc?: string;
payload?: Record<string, unknown>;
};
export type AetheriaMenuSurfaceDocument = {
providerId: string;
providerKind: string;
title: string;
version: number;
updatedAtUtc: string;
surface: AetheriaMenuSurfaceTree;
commands: AetheriaMenuSurfaceCommand[];
};
export type AetheriaMenuSurfaceTree = {
id: string;
root: AetheriaMenuSurfaceComponent;
styles: AetheriaMenuStyleToken[];
};
export type AetheriaMenuSurfaceComponent = {
id: string;
kind: string;
props: Record<string, string>;
layout?: Record<string, string>;
style?: Record<string, string>;
children: AetheriaMenuSurfaceComponent[];
};
export type AetheriaMenuStyleToken = {
name: string;
value: string;
};
export type AetheriaMenuSurfaceCommand = {
command: string;
label: string;
transport: string;
};
export type AetheriaRtsMainClient = {
mapViewport(request: CultMeshViewportRequest): Promise<ViewportResponse>;
objectsViewport(request: CultMeshViewportRequest): Promise<ObjectsViewportResponse>;
@ -471,6 +529,8 @@ export type AetheriaRtsMainClient = {
watchViewportFeed(request: AetheriaRuntimeViewportFeedRequest, callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void): CultMeshUnsubscribe;
setMoveVector(request: AetheriaRuntimeSetMoveVectorRequest): Promise<AetheriaRuntimeDaemonCommandReceipt>;
setTarget(request: AetheriaRuntimeSetTargetRequest): Promise<AetheriaRuntimeDaemonCommandReceipt>;
eveSurface(request: AetheriaEveSurfaceRequest): Promise<AetheriaMenuSurfaceDocument>;
submitEveCommand(request: AetheriaEveCommandRequest): Promise<AetheriaRuntimeDaemonCommandReceipt>;
surfaceCatalogDiagnostics(): CultMeshSurfaceCatalogDiagnostic;
surfaceCatalogIndexDiagnostics(): CultMeshSurfaceCatalogIndexDiagnostic;
};
@ -515,6 +575,10 @@ export function registerAetheriaRtsIpcHandlers(
getClient().setMoveVector(request));
ipcMain.handle(AetheriaRtsIpcChannels.setTarget, async (_event, request: AetheriaRuntimeSetTargetRequest) =>
getClient().setTarget(request));
ipcMain.handle(AetheriaRtsIpcChannels.eveSurface, async (_event, request: AetheriaEveSurfaceRequest) =>
getClient().eveSurface(request));
ipcMain.handle(AetheriaRtsIpcChannels.submitEveCommand, async (_event, request: AetheriaEveCommandRequest) =>
getClient().submitEveCommand(request));
ipcMain.handle(AetheriaRtsIpcChannels.surfaceCatalog, () => getClient().surfaceCatalogDiagnostics());
ipcMain.handle(AetheriaRtsIpcChannels.surfaceCatalogIndex, () => getClient().surfaceCatalogIndexDiagnostics());
ipcMain.handle(AetheriaRtsIpcChannels.health, () => getHealth());
@ -1042,6 +1106,20 @@ export type AetheriaMenuSurfaceRequest = {
canOpenRuntimeInputScreen?: boolean;
};
export type AetheriaEveSurfaceRequest = {
surfaceId?: string;
recordKey?: string;
};
export type AetheriaEveCommandRequest = {
providerId?: string;
surfaceId?: string;
command?: string;
clientId?: string;
issuedAtUtc?: string;
payload?: Record<string, unknown>;
};
export type AetheriaMenuSurfaceDocument = {
providerId: string;
providerKind: string;
@ -1091,6 +1169,8 @@ export type AetheriaRtsApi = {
setTarget(request: AetheriaRuntimeSetTargetRequest): Promise<AetheriaRuntimeDaemonCommandReceipt>;
surfaceCatalog(): Promise<CultMeshSurfaceCatalogDiagnostic>;
surfaceCatalogIndex(): Promise<CultMeshSurfaceCatalogIndexDiagnostic>;
eveSurface(request: AetheriaEveSurfaceRequest): Promise<AetheriaMenuSurfaceDocument>;
submitEveCommand(request: AetheriaEveCommandRequest): Promise<AetheriaRuntimeDaemonCommandReceipt>;
mainMenuSurface(request: AetheriaMenuSurfaceRequest): Promise<AetheriaMenuSurfaceDocument>;
debugSurface(): Promise<AetheriaMenuSurfaceDocument>;
watchDebugSurface(callback: (surface: AetheriaMenuSurfaceDocument) => void): () => void;

View file

@ -36,6 +36,68 @@ body.main-menu-mode .shell {
display: none;
}
body.eve-game-mode {
-webkit-app-region: drag;
background: #020909;
}
body.eve-game-mode .shell {
display: none;
}
body.eve-game-mode .main-menu-host {
-webkit-app-region: no-drag;
display: block;
min-height: 100%;
}
body.eve-game-mode .window-drag-region {
-webkit-app-region: drag;
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 15;
display: block;
height: 44px;
}
body.eve-game-mode .window-controls {
-webkit-app-region: no-drag;
position: fixed;
top: 8px;
right: 10px;
z-index: 20;
display: flex;
gap: 2px;
}
body.eve-game-mode .window-control {
min-width: 0;
width: 30px;
height: 24px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
color: rgba(219, 250, 255, 0.5);
font-family: "Ubuntu", system-ui, sans-serif;
font-size: 12px;
line-height: 24px;
}
body.eve-game-mode .window-control:hover,
body.eve-game-mode .window-control:focus-visible {
background: rgba(219, 250, 255, 0.09);
color: rgba(240, 252, 255, 0.95);
outline: 0;
}
body.eve-game-mode .window-control-close:hover,
body.eve-game-mode .window-control-close:focus-visible {
background: rgba(255, 88, 88, 0.28);
}
body.main-menu-mode .main-menu-host {
-webkit-app-region: no-drag;
display: flex;