From 8975d7ebbb9f9db899626bd3f0b7be00cf0d59af Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 10 Jul 2026 07:41:03 +0100 Subject: [PATCH] Consume generic EveElectron host --- Aetheria.Rts.Web/Client/app.ts | 17 +++-- Aetheria.Rts.Web/Electron/main.ts | 63 +++++-------------- Aetheria.Rts.Web/Electron/preload.cjs | 18 ++++-- Aetheria.Rts.Web/package-lock.json | 18 ++++++ Aetheria.Rts.Web/package.json | 1 + .../scripts/generate-rts-bindings.mjs | 23 ++++++- .../scripts/verify-stage7c-electron-rts.ps1 | 10 +-- 7 files changed, 86 insertions(+), 64 deletions(-) diff --git a/Aetheria.Rts.Web/Client/app.ts b/Aetheria.Rts.Web/Client/app.ts index 30e9997d9..1bbbaf210 100644 --- a/Aetheria.Rts.Web/Client/app.ts +++ b/Aetheria.Rts.Web/Client/app.ts @@ -14,6 +14,13 @@ export {}; declare global { interface Window { aetheriaRts: AetheriaRtsApi; + eveProvider: { + providerAdvertisement: AetheriaRtsApi["eveProviderAdvertisement"]; + surface: AetheriaRtsApi["eveSurface"]; + document: AetheriaRtsApi["eveDocument"]; + submitCommand: AetheriaRtsApi["submitEveCommand"]; + windowControl: AetheriaRtsApi["windowControl"]; + }; } } @@ -38,8 +45,8 @@ async function showDaemonEveSurface(): Promise { host.setAttribute("aria-label", "Aetheria daemon Eve surface"); const requestedSurfaceId = new URLSearchParams(location.search).get("surface") || ""; const providerHost = new EveBrowserProviderHost(host, { - providerAdvertisement: () => window.aetheriaRts.eveProviderAdvertisement(), - surface: surface => window.aetheriaRts.eveSurface(surfaceRequest(surface)), + providerAdvertisement: () => window.eveProvider.providerAdvertisement(), + surface: surface => window.eveProvider.surface(surfaceRequest(surface)), submitCommand: submitEveCommand, resolveDocument: resolveEveDocument, resolveAssetUrl: resolveAetheriaAssetUrl, @@ -64,7 +71,7 @@ async function resolveEveDocument( request: EveEmbeddedDocumentRequest, component: EveSurfaceComponent, ): ReturnType { - return window.aetheriaRts.eveDocument({ + return window.eveProvider.document({ ...request, context: { viewport: viewportFromComponent(component) }, }); @@ -103,7 +110,7 @@ function resolveAetheriaAssetUrl(uri: string): string { async function submitEveCommand(intent: EveCommandIntent): Promise { try { - const receipt = await window.aetheriaRts.submitEveCommand({ + const receipt = await window.eveProvider.submitCommand({ providerId: intent.providerId, surfaceId: intent.surfaceId, command: intent.command, @@ -126,7 +133,7 @@ function wireWindowControls(): void { button.addEventListener("click", () => { const action = button.dataset.windowControl as "minimize" | "maximize" | "close" | undefined; if (action) { - void window.aetheriaRts.windowControl(action); + void window.eveProvider.windowControl(action); } }); }); diff --git a/Aetheria.Rts.Web/Electron/main.ts b/Aetheria.Rts.Web/Electron/main.ts index 07e236381..22cd6ea31 100644 --- a/Aetheria.Rts.Web/Electron/main.ts +++ b/Aetheria.Rts.Web/Electron/main.ts @@ -8,6 +8,7 @@ import { AetheriaCultMeshClient, } from "./aetheria-cultmesh.js"; import { registerAetheriaRtsIpcHandlers } from "./aetheria-rts-generated-bindings.js"; +import { createEveElectronWindow, registerEveWindowControls } from "@gamecult/eve-electron"; const __dirname = dirname(fileURLToPath(import.meta.url)); const projectRoot = resolve(__dirname, ".."); @@ -116,29 +117,12 @@ app.on("before-quit", () => { }); function createWindow(): BrowserWindow { - const window = new BrowserWindow({ - width: 1440, - height: 900, - minWidth: 1000, - minHeight: 640, + return createEveElectronWindow({ show: !electronSmoke, backgroundColor: "#0b1016", title: "Aetheria Starbridge", - frame: false, - autoHideMenuBar: true, - webPreferences: { - contextIsolation: true, - nodeIntegration: false, - preload: resolve(__dirname, "preload.cjs"), - }, - }); - - window.webContents.setWindowOpenHandler(({ url }) => { - void shell.openExternal(url); - return { action: "deny" }; - }); - - return window; + preload: resolve(__dirname, "preload.cjs"), + }, { BrowserWindow, shell }); } async function runElectronSmoke(window: BrowserWindow): Promise> { @@ -149,14 +133,15 @@ async function runElectronSmoke(window: BrowserWindow): Promise { await new Promise(resolve => setTimeout(resolve, 100)); const api = window.aetheriaRts; + const eveProvider = window.eveProvider; const status = document.querySelector("#status")?.textContent ?? ""; const bodyMode = document.body.className; const eveHostText = document.querySelector("#eve-surface-host")?.textContent ?? ""; const worldScene = document.querySelector(".cultui-world-scene"); const worldEntityCount = worldScene?.querySelectorAll(".cultui-world-entity").length ?? 0; const controlledWorldEntityCount = worldScene?.querySelectorAll('.cultui-world-entity[data-controlled="true"]').length ?? 0; - const providerAdvertisement = api ? await api.eveProviderAdvertisement() : null; - const eveSurface = api ? await api.eveSurface({ recordKey: "eve:surface:aetheria.daemon.game" }) : null; + const providerAdvertisement = eveProvider ? await eveProvider.providerAdvertisement() : null; + const eveSurface = eveProvider ? await eveProvider.surface({ 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; @@ -177,7 +162,7 @@ async function runElectronSmoke(window: BrowserWindow): Promise component.kind === "field.surface2d" || component.kind === "gravity.surface"); - const eveReceipt = api ? await api.submitEveCommand({ + const eveReceipt = eveProvider ? await eveProvider.submitCommand({ providerId: "aetheria.daemon", surfaceId: "aetheria.game", command: "sensor_ping", @@ -198,10 +183,11 @@ async function runElectronSmoke(window: BrowserWindow): Promise { - const window = BrowserWindow.fromWebContents(event.sender); - if (!window) - return; - - switch (action) { - case "minimize": - window.minimize(); - return; - case "maximize": - if (window.isMaximized()) - window.unmaximize(); - else - window.maximize(); - return; - case "close": - window.close(); - return; - default: - throw new Error(`Unknown window control '${action}'.`); - } - }); + registerEveWindowControls(ipcMain, BrowserWindow, "aetheria-rts:window-control"); } function registerAssetProtocol(): void { diff --git a/Aetheria.Rts.Web/Electron/preload.cjs b/Aetheria.Rts.Web/Electron/preload.cjs index 753f10581..5327321a3 100644 --- a/Aetheria.Rts.Web/Electron/preload.cjs +++ b/Aetheria.Rts.Web/Electron/preload.cjs @@ -2,6 +2,7 @@ // Generated by scripts/generate-rts-bindings.mjs from the RTS IPC surface contract. const { contextBridge, ipcRenderer } = require("electron"); +const { installEveProviderBridge } = require("@gamecult/eve-electron/preload"); const channels = { mapViewport: "aetheria-rts:map-viewport", @@ -58,10 +59,17 @@ 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), - eveProviderAdvertisement: request => ipcRenderer.invoke(channels.eveProviderAdvertisement, request), - eveDocument: request => ipcRenderer.invoke(channels.eveDocument, request), - submitEveCommand: request => ipcRenderer.invoke(channels.submitEveCommand, request), - windowControl: request => ipcRenderer.invoke(channels.windowControl, request), health: () => ipcRenderer.invoke(channels.health), }); + +installEveProviderBridge({ + contextBridge, + ipcRenderer, + channels: { + providerAdvertisement: channels.eveProviderAdvertisement, + surface: channels.eveSurface, + document: channels.eveDocument, + submitCommand: channels.submitEveCommand, + windowControl: channels.windowControl, + }, +}); diff --git a/Aetheria.Rts.Web/package-lock.json b/Aetheria.Rts.Web/package-lock.json index bc9ff5968..b5e0a7e9c 100644 --- a/Aetheria.Rts.Web/package-lock.json +++ b/Aetheria.Rts.Web/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@gamecult/eve-browser-lowering": "file:../../Eve/packages/eve-browser-lowering", + "@gamecult/eve-electron": "file:../../EveElectron", "@msgpack/msgpack": "^3.1.2", "cultcache-ts": "file:../../CultLib/packages/cultcache-ts", "cultmesh-ts": "file:../../CultLib/packages/cultmesh-ts", @@ -68,6 +69,19 @@ "typescript": "^5.6.3" } }, + "../../EveElectron": { + "name": "@gamecult/eve-electron", + "version": "0.1.0", + "license": "MPL-2.0", + "peerDependencies": { + "electron": ">=30" + }, + "peerDependenciesMeta": { + "electron": { + "optional": true + } + } + }, "node_modules/@electron-internal/extract-zip": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.3.tgz", @@ -103,6 +117,10 @@ "resolved": "../../Eve/packages/eve-browser-lowering", "link": true }, + "node_modules/@gamecult/eve-electron": { + "resolved": "../../EveElectron", + "link": true + }, "node_modules/@msgpack/msgpack": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", diff --git a/Aetheria.Rts.Web/package.json b/Aetheria.Rts.Web/package.json index 66bdeea64..82b8b9e81 100644 --- a/Aetheria.Rts.Web/package.json +++ b/Aetheria.Rts.Web/package.json @@ -25,6 +25,7 @@ "dependencies": { "@msgpack/msgpack": "^3.1.2", "@gamecult/eve-browser-lowering": "file:../../Eve/packages/eve-browser-lowering", + "@gamecult/eve-electron": "file:../../EveElectron", "cultcache-ts": "file:../../CultLib/packages/cultcache-ts", "cultmesh-ts": "file:../../CultLib/packages/cultmesh-ts", "cultnet-ts": "file:../../CultLib/packages/cultnet-ts" diff --git a/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs b/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs index d0591fbab..4a84f7cc4 100644 --- a/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs +++ b/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs @@ -1591,9 +1591,17 @@ function renderIpcChannelLines() { function renderPreload() { const channelLines = renderIpcChannelLines(); + const eveProviderMethods = new Set([ + "eveSurface", + "eveProviderAdvertisement", + "eveDocument", + "submitEveCommand", + "windowControl", + ]); const apiLines = ipcChannels .filter(([name]) => name !== "viewportFeedUpdate" && - name !== "viewportFeedStop") + name !== "viewportFeedStop" && + !eveProviderMethods.has(name)) .map(([name]) => { if (name === "viewportFeed") { return ` watchViewportFeed: (request, callback) => { @@ -1628,6 +1636,7 @@ function renderPreload() { // Generated by scripts/generate-rts-bindings.mjs from the RTS IPC surface contract. const { contextBridge, ipcRenderer } = require("electron"); +const { installEveProviderBridge } = require("@gamecult/eve-electron/preload"); const channels = { ${channelLines} @@ -1636,5 +1645,17 @@ ${channelLines} contextBridge.exposeInMainWorld("aetheriaRts", { ${apiLines} }); + +installEveProviderBridge({ + contextBridge, + ipcRenderer, + channels: { + providerAdvertisement: channels.eveProviderAdvertisement, + surface: channels.eveSurface, + document: channels.eveDocument, + submitCommand: channels.submitEveCommand, + windowControl: channels.windowControl, + }, +}); `; } diff --git a/Aetheria.Rts.Web/scripts/verify-stage7c-electron-rts.ps1 b/Aetheria.Rts.Web/scripts/verify-stage7c-electron-rts.ps1 index df670c133..e40dba1fe 100644 --- a/Aetheria.Rts.Web/scripts/verify-stage7c-electron-rts.ps1 +++ b/Aetheria.Rts.Web/scripts/verify-stage7c-electron-rts.ps1 @@ -13,9 +13,11 @@ if (-not (Get-Content wwwroot\index.html -Raw).Contains('eve-surface-host')) { } $appText = Get-Content wwwroot\app.js -Raw -if (-not $appText.Contains('window.aetheriaRts.eveProviderAdvertisement') -or +if (-not $appText.Contains('window.eveProvider.providerAdvertisement') -or -not $appText.Contains('EveBrowserProviderHost') -or - -not $appText.Contains('window.aetheriaRts.eveDocument') -or + -not $appText.Contains('window.eveProvider.document') -or + $appText.Contains('window.aetheriaRts.eveProviderAdvertisement') -or + $appText.Contains('window.aetheriaRts.eveDocument') -or $appText.Contains('eve:surface:aetheria.daemon.game') -or $appText.Contains('gamecult.aetheria.gravity_viewport.v1') -or $appText.Contains('gamecult.aetheria.render_splats_viewport.v1') -or @@ -34,7 +36,7 @@ $mainText = Get-Content Electron\main.ts -Raw if (-not $mainText.Contains('await api.surfaceCatalog()') -or -not $mainText.Contains('await api.surfaceCatalogIndex()')) { Write-Error "Stage 7C Electron RTS verifier failed: Electron smoke does not call the preload CultMesh surface catalog APIs." } -if (-not $mainText.Contains('await api.eveProviderAdvertisement()')) { +if (-not $mainText.Contains('await eveProvider.providerAdvertisement()')) { Write-Error "Stage 7C Electron RTS verifier failed: Electron smoke does not verify provider advertisement discovery through preload." } @@ -45,7 +47,7 @@ if (-not $mainText.Contains('await api.renderSplatsViewport') -or Write-Error "Stage 7C Electron RTS verifier failed: Electron smoke does not verify daemon Eve field document lowering." } -if (-not $mainText.Contains('await api.submitEveCommand') -or +if (-not $mainText.Contains('await eveProvider.submitCommand') -or -not $mainText.Contains('eveReceipt?.commandId') -or -not $mainText.Contains('eveReceipt?.accepted') -or -not $mainText.Contains('eveReceipt?.route')) {