From fbc48482317c73de150c74ea76f9316fc2370748 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 5 Jul 2026 00:32:49 +0100 Subject: [PATCH] Lower Electron RTS through daemon Eve surface --- .../Client/aetheria-rts-contract.ts | 16 ++ Aetheria.Rts.Web/Client/app.ts | 80 ++++++- .../Electron/aetheria-cultmesh.ts | 212 ++++++++++++++++++ .../Electron/aetheria-rts-bindings.ts | 1 + .../aetheria-rts-generated-bindings.ts | 64 ++++++ Aetheria.Rts.Web/Electron/main.ts | 82 +++---- Aetheria.Rts.Web/Electron/preload.cjs | 4 + .../scripts/generate-rts-bindings.mjs | 80 +++++++ Aetheria.Rts.Web/wwwroot/styles.css | 62 +++++ 9 files changed, 549 insertions(+), 52 deletions(-) diff --git a/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts b/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts index f7ee623ed..d2c3071f9 100644 --- a/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts +++ b/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts @@ -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; +}; + export type AetheriaMenuSurfaceDocument = { providerId: string; providerKind: string; @@ -346,6 +360,8 @@ export type AetheriaRtsApi = { setTarget(request: AetheriaRuntimeSetTargetRequest): Promise; surfaceCatalog(): Promise; surfaceCatalogIndex(): Promise; + eveSurface(request: AetheriaEveSurfaceRequest): Promise; + submitEveCommand(request: AetheriaEveCommandRequest): Promise; mainMenuSurface(request: AetheriaMenuSurfaceRequest): Promise; debugSurface(): Promise; watchDebugSurface(callback: (surface: AetheriaMenuSurfaceDocument) => void): () => void; diff --git a/Aetheria.Rts.Web/Client/app.ts b/Aetheria.Rts.Web/Client/app.ts index 99a485802..996d73e19 100644 --- a/Aetheria.Rts.Web/Client/app.ts +++ b/Aetheria.Rts.Web/Client/app.ts @@ -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(selector: string): TElement { const element = document.querySelector(selector); @@ -96,7 +99,9 @@ function setStatus(text: string): void { async function showMainMenu(surfaceId = "aetheria.main_menu.root"): Promise { 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 { + document.body.classList.remove("main-menu-mode"); + document.body.classList.add("eve-game-mode"); + viewportFeedUnsubscribe?.(); + + let host = document.querySelector("#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 { try { renderDebugSurface(await window.aetheriaRts.debugSurface()); @@ -144,9 +193,6 @@ function renderDebugSurface(surface: AetheriaMenuSurfaceDocument): void { } function wireWindowControls(): void { - if (!mainMenuMode) - return; - document.querySelectorAll("[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 { + 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(); } diff --git a/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts index cefbd035a..989bd4790 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts @@ -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; +}; + export type AetheriaMenuSurfaceDocument = { providerId: string; providerKind: string; @@ -291,6 +308,68 @@ export class AetheriaCultMeshClient { request.inGame === true); } + public async eveSurface(request: AetheriaEveSurfaceRequest = {}): Promise { + 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 { + 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> { 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; + 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 | undefined): Record { + const result: Record = {}; + 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 { + return value && typeof value === "object" && !Array.isArray(value) + ? value as Record + : {}; +} + +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 { + const source = objectValue(value); + const result: Record = {}; + 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 { return new Promise(resolve => setTimeout(resolve, milliseconds)); } diff --git a/Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts b/Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts index 0c39a973e..581996a13 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts @@ -4,6 +4,7 @@ export { AetheriaRtsIpcChannels, AetheriaRtsSchemas, AetheriaRuntimeDaemonCommandKinds, + aetheriaRuntimeEveCommandDocumentSlots, createAetheriaRuntimeRtsDocuments, createAetheriaRuntimeRtsOperationHandles, createAetheriaRuntimeRtsQueryHandles, diff --git a/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts b/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts index 08fbf22d3..905979078 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts @@ -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; +}; + +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; + layout?: Record; + style?: Record; + 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; objectsViewport(request: CultMeshViewportRequest): Promise; @@ -1745,6 +1803,8 @@ export type AetheriaRtsMainClient = { watchViewportFeed(request: AetheriaRuntimeViewportFeedRequest, callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void): CultMeshUnsubscribe; setMoveVector(request: AetheriaRuntimeSetMoveVectorRequest): Promise; setTarget(request: AetheriaRuntimeSetTargetRequest): Promise; + eveSurface(request: AetheriaEveSurfaceRequest): Promise; + submitEveCommand(request: AetheriaEveCommandRequest): Promise; 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()); diff --git a/Aetheria.Rts.Web/Electron/main.ts b/Aetheria.Rts.Web/Electron/main.ts index 943de043d..4980d2bae 100644 --- a/Aetheria.Rts.Web/Electron/main.ts +++ b/Aetheria.Rts.Web/Electron/main.ts @@ -212,12 +212,9 @@ async function runElectronSmoke(window: BrowserWindow): Promise 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 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; @@ -277,44 +272,35 @@ async function runElectronSmoke(window: BrowserWindow): Promise): 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 { diff --git a/Aetheria.Rts.Web/Electron/preload.cjs b/Aetheria.Rts.Web/Electron/preload.cjs index f93b959e0..b6c2430ea 100644 --- a/Aetheria.Rts.Web/Electron/preload.cjs +++ b/Aetheria.Rts.Web/Electron/preload.cjs @@ -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 => { diff --git a/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs b/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs index 7f8a86408..27b14be10 100644 --- a/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs +++ b/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs @@ -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; +}; + +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; + layout?: Record; + style?: Record; + 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; objectsViewport(request: CultMeshViewportRequest): Promise; @@ -471,6 +529,8 @@ export type AetheriaRtsMainClient = { watchViewportFeed(request: AetheriaRuntimeViewportFeedRequest, callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void): CultMeshUnsubscribe; setMoveVector(request: AetheriaRuntimeSetMoveVectorRequest): Promise; setTarget(request: AetheriaRuntimeSetTargetRequest): Promise; + eveSurface(request: AetheriaEveSurfaceRequest): Promise; + submitEveCommand(request: AetheriaEveCommandRequest): Promise; 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; +}; + export type AetheriaMenuSurfaceDocument = { providerId: string; providerKind: string; @@ -1091,6 +1169,8 @@ export type AetheriaRtsApi = { setTarget(request: AetheriaRuntimeSetTargetRequest): Promise; surfaceCatalog(): Promise; surfaceCatalogIndex(): Promise; + eveSurface(request: AetheriaEveSurfaceRequest): Promise; + submitEveCommand(request: AetheriaEveCommandRequest): Promise; mainMenuSurface(request: AetheriaMenuSurfaceRequest): Promise; debugSurface(): Promise; watchDebugSurface(callback: (surface: AetheriaMenuSurfaceDocument) => void): () => void; diff --git a/Aetheria.Rts.Web/wwwroot/styles.css b/Aetheria.Rts.Web/wwwroot/styles.css index a64035faa..878fc3a82 100644 --- a/Aetheria.Rts.Web/wwwroot/styles.css +++ b/Aetheria.Rts.Web/wwwroot/styles.css @@ -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;