Lower Electron RTS through daemon Eve surface
This commit is contained in:
parent
63e53a7d66
commit
fbc4848231
9 changed files with 549 additions and 52 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export {
|
|||
AetheriaRtsIpcChannels,
|
||||
AetheriaRtsSchemas,
|
||||
AetheriaRuntimeDaemonCommandKinds,
|
||||
aetheriaRuntimeEveCommandDocumentSlots,
|
||||
createAetheriaRuntimeRtsDocuments,
|
||||
createAetheriaRuntimeRtsOperationHandles,
|
||||
createAetheriaRuntimeRtsQueryHandles,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue