Remove RTS menu shell shortcuts

This commit is contained in:
Meta Vens 2026-07-05 16:59:06 +01:00
commit e4b155576f
23 changed files with 265 additions and 1532 deletions

View file

@ -343,13 +343,6 @@ export type BodyView = {
};
export type AetheriaMenuSurfaceRequest = {
surfaceId?: string;
inGame?: boolean;
canOpenRuntimeInputScreen?: boolean;
panelOnly?: boolean;
};
export type AetheriaEveSurfaceRequest = {
surfaceId?: string;
recordKey?: string;
@ -424,7 +417,6 @@ export type AetheriaRtsApi = {
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;
windowControl(action: "minimize" | "maximize" | "close"): Promise<void>;

View file

@ -5,21 +5,10 @@ import {
type EveSurfaceComponent,
} from "../node_modules/@gamecult/eve-browser-lowering/dist/index.js";
import type {
AetheriaRtsApi,
AetheriaMenuSurfaceDocument,
AetheriaMenuSurfaceComponent,
AuthorityStatusDocument,
AetheriaRuntimeViewportFeedSnapshot,
BodyView,
DaemonHealthDocument,
GravityInfluence,
InventoryItem,
InventoryDocument,
SelectedObjectDocument,
StarbridgeSessionDocument,
ViewObject,
AetheriaMenuSurfaceDocument,
AetheriaRtsApi,
Viewport,
ViewportResponse,
} from "./aetheria-rts-contract.js";
export {};
@ -30,90 +19,28 @@ declare global {
}
}
const canvas = requiredElement<HTMLCanvasElement>("#map");
const host = requiredElement<HTMLElement>("#eve-surface-host");
const statusEl = requiredElement<HTMLElement>("#status");
const zoomLabel = requiredElement<HTMLOutputElement>("#zoom-label");
const selectionTitle = requiredElement<HTMLElement>("#selection-title");
const selectionDetails = requiredElement<HTMLElement>("#selection-details");
const frameDetails = requiredElement<HTMLElement>("#frame-details");
const starbridgeDetails = requiredElement<HTMLElement>("#starbridge-details");
const runtimeSurfaceDetails = requiredElement<HTMLElement>("#runtime-surface-details");
const controlledList = requiredElement<HTMLElement>("#controlled-list");
const debugSurfaceHost = requiredElement<HTMLElement>("#debug-surface-host");
const ctx = requiredContext(canvas);
let zoom = 1;
let centerX = 0;
let centerY = 0;
let selectedEntityIndex = -1;
let latest: ViewportResponse | null = null;
let latestSelected: SelectedObjectDocument | null = null;
let latestInventory: InventoryDocument | null = null;
let latestDaemonHealth: DaemonHealthDocument | null = null;
let latestAuthority: AuthorityStatusDocument | null = null;
let latestStarbridge: StarbridgeSessionDocument | null = null;
let latestSurfaceIndex: Awaited<ReturnType<AetheriaRtsApi["surfaceCatalogIndex"]>> | null = null;
let latestOperationReceipt: string | null = null;
let latestReceivedAt = 0;
let latestPollMs = 0;
let refreshInFlight = false;
let viewportFeedUnsubscribe: (() => void) | null = null;
let eveSurfacePoll: number | null = null;
let latestEveSurfaceKey = "";
let activeMainMenuSurfaceId = "aetheria.main_menu.root";
let renderDaemonEveSurfaceNow: (() => Promise<void>) | null = null;
const mainMenuMode = window.location.hash === "#main-menu";
const legacyViewportMode = window.location.hash === "#legacy-viewport";
let eveSurfacePoll: number | null = null;
function requiredElement<TElement extends Element>(selector: string): TElement {
const element = document.querySelector<TElement>(selector);
if (!element) {
throw new Error(`Aetheria RTS shell is missing ${selector}.`);
throw new Error(`Aetheria RTS is missing ${selector}.`);
}
return element;
}
function requiredContext(target: HTMLCanvasElement): CanvasRenderingContext2D {
const context = target.getContext("2d");
if (!context) {
throw new Error("Canvas 2D context is unavailable.");
}
return context;
}
const worldSpan = () => 2200 / zoom;
function currentViewport(): Viewport {
const span = worldSpan();
const aspect = Math.max(0.25, canvas.clientWidth / Math.max(1, canvas.clientHeight));
const width = span * aspect;
const height = span;
return {
minX: centerX - width * 0.5,
minY: centerY - height * 0.5,
maxX: centerX + width * 0.5,
maxY: centerY + height * 0.5
};
}
function setStatus(text: string): void {
statusEl.textContent = text;
}
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 () => {
@ -123,19 +50,21 @@ async function showDaemonEveSurface(): Promise<void> {
});
const loweredSurface = withActiveMainMenuPanel(surface, activeMainMenuSurfaceId);
const surfaceKey = `${surface.providerId}:${surface.surface.id}:${surface.version}:${surface.updatedAtUtc}:${activeMainMenuSurfaceId}`;
if (surfaceKey !== latestEveSurfaceKey) {
latestEveSurfaceKey = surfaceKey;
renderEveSurface(loweredSurface, host, {
body: document.body,
assetUrlResolver: resolveAetheriaAssetUrl,
clientId: "aetheria.rts.electron",
commandSink: intent => submitEveCommand(intent),
documentResolver: resolveEveDocument,
source: "Aetheria Daemon",
statusElement: statusEl,
});
wireWindowControls();
if (surfaceKey === latestEveSurfaceKey) {
return;
}
latestEveSurfaceKey = surfaceKey;
renderEveSurface(loweredSurface, host, {
body: document.body,
assetUrlResolver: resolveAetheriaAssetUrl,
clientId: "aetheria.rts.electron",
commandSink: intent => submitEveCommand(intent),
documentResolver: resolveEveDocument,
source: "Aetheria Daemon",
statusElement: statusEl,
});
wireWindowControls();
} catch (error) {
setStatus(error instanceof Error ? error.message : "Aetheria daemon Eve surface unavailable.");
}
@ -219,11 +148,8 @@ async function resolveEveDocument(
component: EveSurfaceComponent,
): Promise<{ document?: unknown; documentId: string; schemaId: string; surface?: AetheriaMenuSurfaceDocument["surface"] } | undefined> {
if (request.schemaId === "gamecult.eve.surface.v1" || request.slotId === "mainMenuPanel") {
const surface = await window.aetheriaRts.mainMenuSurface({
const surface = await window.aetheriaRts.eveSurface({
surfaceId: request.documentId,
inGame: false,
canOpenRuntimeInputScreen: false,
panelOnly: true,
});
return {
documentId: request.documentId,
@ -268,19 +194,22 @@ function viewportFromComponent(component: EveSurfaceComponent): Viewport {
}
function numberProp(value: unknown, fallback: number): number {
if (typeof value === "number" && Number.isFinite(value))
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string") {
const parsed = Number.parseFloat(value);
if (Number.isFinite(parsed))
if (Number.isFinite(parsed)) {
return parsed;
}
}
return fallback;
}
function resolveAetheriaAssetUrl(uri: string): string {
if (!uri.startsWith("resources://"))
if (!uri.startsWith("resources://")) {
return uri;
}
const resourcePath = uri.slice("resources://".length).replace(/^\/+/, "");
const filePath = resourcePath.match(/\.[a-z0-9]+$/i)
@ -289,66 +218,6 @@ function resolveAetheriaAssetUrl(uri: string): string {
return new URL(`../../Assets/Resources/${filePath}`, document.baseURI).href;
}
async function startDebugSurface(): Promise<void> {
try {
renderDebugSurface(await window.aetheriaRts.debugSurface());
window.aetheriaRts.watchDebugSurface(renderDebugSurface);
} catch (error) {
debugSurfaceHost.textContent = error instanceof Error ? error.message : "CultUI debug surface unavailable.";
}
}
function renderDebugSurface(surface: AetheriaMenuSurfaceDocument): void {
renderEveSurface(surface, debugSurfaceHost, {
body: document.body,
assetUrlResolver: resolveAetheriaAssetUrl,
clientId: "aetheria.rts.electron",
commandSink: intent => handleMenuCommand(intent.command, surface),
source: "CultUI Debug",
statusElement: statusEl,
});
}
function wireWindowControls(): void {
document.querySelectorAll<HTMLButtonElement>("[data-window-control]").forEach(button => {
if (button.dataset.windowControlWired === "true")
return;
button.dataset.windowControlWired = "true";
button.addEventListener("click", () => {
const action = button.dataset.windowControl as "minimize" | "maximize" | "close" | undefined;
if (action)
void window.aetheriaRts.windowControl(action);
});
});
}
function handleMenuCommand(commandOrIntent: string | EveCommandIntent, surface?: AetheriaMenuSurfaceDocument): void {
const command = typeof commandOrIntent === "string" ? commandOrIntent : commandOrIntent.command;
switch (command) {
case "aetheria.main_menu.root.show_settings":
void showMainMenuPanel("aetheria.main_menu.settings");
return;
case "aetheria.main_menu.settings.show_player_settings":
void showMainMenuPanel("aetheria.main_menu.player_settings");
return;
case "aetheria.main_menu.settings.show_verse_settings":
void showMainMenuPanel("aetheria.main_menu.verse_settings");
return;
case "aetheria.main_menu.settings.show_input_settings":
void showMainMenuPanel("aetheria.main_menu.input_settings");
return;
case "aetheria.main_menu.settings.back_to_settings":
void showMainMenuPanel("aetheria.main_menu.settings");
return;
case "aetheria.main_menu.settings.back_to_main":
void showMainMenuPanel("aetheria.main_menu.root");
return;
default:
setStatus(`${surface?.title ?? "Aetheria Starbridge"}: ${command || "no command"}`);
return;
}
}
async function submitEveCommand(intent: EveCommandIntent): Promise<void> {
if (intent.command.startsWith("aetheria.main_menu.")) {
handleMenuCommand(intent.command);
@ -370,519 +239,45 @@ async function submitEveCommand(intent: EveCommandIntent): Promise<void> {
}
}
function stopEveSurfacePoll(): void {
if (eveSurfacePoll == null)
return;
window.clearInterval(eveSurfacePoll);
eveSurfacePoll = null;
}
function startViewportFeed(): void {
const viewport = currentViewport();
viewportFeedUnsubscribe?.();
viewportFeedUnsubscribe = window.aetheriaRts.watchViewportFeed({
viewport,
selectedEntityIndex
}, applyViewportSnapshot);
}
function applyViewportSnapshot(snapshot: AetheriaRuntimeViewportFeedSnapshot): void {
latestPollMs = snapshot.sampleMs;
latestReceivedAt = Date.parse(snapshot.receivedAtUtc) || Date.now();
latest = snapshot.viewport;
latestSelected = snapshot.selectedObject;
latestInventory = snapshot.inventory;
latestDaemonHealth = snapshot.daemonHealth;
latestAuthority = snapshot.authorityStatus;
latestStarbridge = snapshot.starbridgeSession;
const controlled = snapshot.viewport.objects.find(object => object.controlled);
if (controlled && selectedEntityIndex < 0) {
selectedEntityIndex = controlled.entityIndex;
centerX = controlled.x;
centerY = controlled.y;
startViewportFeed();
return;
}
setStatus(`${snapshot.viewport.zoneName || `Zone ${snapshot.viewport.zoneIndex}`} frame ${snapshot.viewport.frameId}`);
render();
renderPanels();
}
async function refreshRuntimeSurfaces(): Promise<void> {
try {
latestSurfaceIndex = await window.aetheriaRts.surfaceCatalogIndex();
renderPanels();
} catch (error) {
runtimeSurfaceDetails.innerHTML = details([
["State", error instanceof Error ? error.message : "unavailable"]
]);
function handleMenuCommand(command: string): void {
switch (command) {
case "aetheria.main_menu.root.show_settings":
void showMainMenuPanel("aetheria.main_menu.settings");
return;
case "aetheria.main_menu.settings.show_player_settings":
void showMainMenuPanel("aetheria.main_menu.player_settings");
return;
case "aetheria.main_menu.settings.show_verse_settings":
void showMainMenuPanel("aetheria.main_menu.verse_settings");
return;
case "aetheria.main_menu.settings.show_input_settings":
void showMainMenuPanel("aetheria.main_menu.input_settings");
return;
case "aetheria.main_menu.settings.back_to_settings":
void showMainMenuPanel("aetheria.main_menu.settings");
return;
case "aetheria.main_menu.settings.back_to_main":
void showMainMenuPanel("aetheria.main_menu.root");
return;
default:
setStatus(`Aetheria Starbridge: ${command || "no command"}`);
return;
}
}
function resizeCanvas(): void {
const ratio = Math.max(1, window.devicePixelRatio || 1);
const width = Math.max(1, Math.floor(canvas.clientWidth * ratio));
const height = Math.max(1, Math.floor(canvas.clientHeight * ratio));
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
}
function worldToScreen(x: number, y: number): { x: number; y: number } {
const viewport = currentViewport();
const sx = (x - viewport.minX) / (viewport.maxX - viewport.minX) * canvas.clientWidth;
const sy = canvas.clientHeight - (y - viewport.minY) / (viewport.maxY - viewport.minY) * canvas.clientHeight;
return { x: sx, y: sy };
}
function screenToWorld(x: number, y: number): { x: number; y: number } {
const viewport = currentViewport();
return {
x: viewport.minX + x / Math.max(1, canvas.clientWidth) * (viewport.maxX - viewport.minX),
y: viewport.minY + (1 - y / Math.max(1, canvas.clientHeight)) * (viewport.maxY - viewport.minY)
};
}
function worldRadiusToScreen(radius: number): number {
const viewport = currentViewport();
return radius / (viewport.maxX - viewport.minX) * canvas.clientWidth;
}
function render(): void {
resizeCanvas();
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
drawGrid();
if (!latest) {
return;
}
for (const influence of latest.gravityInfluences) {
drawGravity(influence);
}
for (const body of latest.bodies) {
drawBody(body);
}
for (const object of latest.objects) {
drawObject(object);
}
}
function drawGrid(): void {
const viewport = currentViewport();
const spacing = chooseGridSpacing((viewport.maxX - viewport.minX) / 8);
const startX = Math.floor(viewport.minX / spacing) * spacing;
const startY = Math.floor(viewport.minY / spacing) * spacing;
ctx.lineWidth = 1;
ctx.strokeStyle = "#1b242a";
ctx.beginPath();
for (let x = startX; x <= viewport.maxX; x += spacing) {
const screen = worldToScreen(x, viewport.minY);
ctx.moveTo(screen.x, 0);
ctx.lineTo(screen.x, canvas.clientHeight);
}
for (let y = startY; y <= viewport.maxY; y += spacing) {
const screen = worldToScreen(viewport.minX, y);
ctx.moveTo(0, screen.y);
ctx.lineTo(canvas.clientWidth, screen.y);
}
ctx.stroke();
}
function chooseGridSpacing(target: number): number {
const power = 10 ** Math.floor(Math.log10(Math.max(1, target)));
const scaled = target / power;
if (scaled > 5) return power * 10;
if (scaled > 2) return power * 5;
return power * 2;
}
function drawGravity(influence: GravityInfluence): void {
const screen = worldToScreen(influence.x, influence.y);
const radius = worldRadiusToScreen(influence.radius);
ctx.lineWidth = 1;
ctx.strokeStyle = influence.kind === "Sun" ? "rgba(246, 192, 82, 0.28)" : "rgba(97, 174, 197, 0.24)";
ctx.fillStyle = influence.kind === "Sun" ? "rgba(246, 192, 82, 0.05)" : "rgba(97, 174, 197, 0.04)";
ctx.beginPath();
ctx.arc(screen.x, screen.y, Math.max(1, radius), 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
function drawBody(body: BodyView): void {
const screen = worldToScreen(body.x, body.y);
const radius = Math.max(3, Math.min(28, worldRadiusToScreen(body.radius) * 0.025));
ctx.fillStyle = body.kind === "sun" ? "#f4bc5d" : body.isAsteroidBelt ? "#9d9080" : "#71a8b7";
ctx.beginPath();
ctx.arc(screen.x, screen.y, radius, 0, Math.PI * 2);
ctx.fill();
}
function drawObject(object: ViewObject): void {
const screen = worldToScreen(object.x, object.y);
const selected = object.entityIndex === selectedEntityIndex;
const station = object.kind.toLowerCase().includes("station");
const size = selected ? 9 : object.controlled ? 7 : 5;
ctx.save();
ctx.translate(screen.x, screen.y);
ctx.rotate(Math.atan2(object.directionY, object.directionX));
ctx.fillStyle = object.isActive
? selected ? "#ffffff" : object.controlled ? "#54d18a" : object.factionKey === "raider" ? "#e36b4b" : "#aeb7bd"
: "#56616a";
ctx.strokeStyle = "#0d1013";
ctx.lineWidth = 2;
ctx.beginPath();
if (station) {
ctx.rect(-size, -size, size * 2, size * 2);
} else {
ctx.moveTo(size + 4, 0);
ctx.lineTo(-size, size * 0.7);
ctx.lineTo(-size * 0.45, 0);
ctx.lineTo(-size, -size * 0.7);
}
ctx.closePath();
ctx.stroke();
ctx.fill();
ctx.restore();
if (object.targetEntityIndex >= 0) {
const target = latest?.objects.find(candidate => candidate.entityIndex === object.targetEntityIndex);
if (target) {
const targetScreen = worldToScreen(target.x, target.y);
ctx.strokeStyle = object.controlled ? "rgba(84, 209, 138, 0.42)" : "rgba(227, 107, 75, 0.36)";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(screen.x, screen.y);
ctx.lineTo(targetScreen.x, targetScreen.y);
ctx.stroke();
function wireWindowControls(): void {
document.querySelectorAll<HTMLButtonElement>("[data-window-control]").forEach(button => {
if (button.dataset.windowControlWired === "true") {
return;
}
}
if (selected) {
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(screen.x, screen.y, 14, 0, Math.PI * 2);
ctx.stroke();
}
}
function renderPanels(): void {
if (!latest) {
return;
}
const selected = latestSelected?.selected ?? null;
selectionTitle.textContent = selected?.displayName || "No Selection";
selectionDetails.innerHTML = selected
? details([
["Entity", selected.entityIndex.toString()],
["Key", selected.entityKey],
["Kind", selected.kind || "unknown"],
["Faction", selected.factionKey || "unknown"],
["State", selected.isActive ? "active" : "disabled"],
["Hull", selected.status.hull.toFixed(1)],
["Shield", selected.status.shield.toFixed(1)],
["Heat", selected.status.heat.toFixed(1)],
["Position", `${selected.x.toFixed(1)}, ${selected.y.toFixed(1)}, z ${selected.z.toFixed(1)}`],
["Velocity", `${selected.velocityX.toFixed(2)}, ${selected.velocityY.toFixed(2)}`],
["Target", selected.targetEntityIndex >= 0 ? `#${selected.targetEntityIndex}` : "none"],
["Controlled", selected.controlled ? "yes" : "no"],
["Equipment", inventoryText(latestInventory?.equipment ?? [])],
["Cargo", inventoryText(latestInventory?.cargo ?? [])]
])
: `<dt class="muted">State</dt><dd class="muted">Click a visible object on the map.</dd>`;
frameDetails.innerHTML = details([
["Run", latest.runId],
["Zone", `${latest.zoneIndex}`],
["Frame", latest.frameId.toString()],
["Sim Time", `${latest.simulationTimeSeconds.toFixed(2)}s`],
["Frame Age", `${Math.max(0, Date.now() - latestReceivedAt)}ms`],
["Viewport Poll", `${latestPollMs.toFixed(1)}ms`],
["Visible", latest.objects.length.toString()],
["Daemon", latestDaemonHealth ? `${latestDaemonHealth.status} / ${latestDaemonHealth.transport}` : "unknown"],
["Authority", latestAuthority ? latestAuthority.policyId : "unknown"],
["Mode", latestAuthority ? latestAuthority.defaultMode : "unknown"],
["Last Op", latestOperationReceipt ?? "none"]
]);
runtimeSurfaceDetails.innerHTML = latestSurfaceIndex
? details([
["Catalog", latestSurfaceIndex.catalogId],
["Queries", surfaceListText(latestSurfaceIndex.queries)],
["Live Feeds", surfaceListText(latestSurfaceIndex.liveFeeds)],
["Operations", surfaceListText(latestSurfaceIndex.operations)],
["Pointers", surfaceListText(latestSurfaceIndex.statePointers)],
["Native Views", surfaceListText(latestSurfaceIndex.nativeSliceViews)]
])
: `<dt class="muted">State</dt><dd class="muted">Discovering runtime surfaces.</dd>`;
starbridgeDetails.innerHTML = latestStarbridge
? details([
["Scenario", latestStarbridge.scenarioName || latestStarbridge.scenarioId],
["Phase", latestStarbridge.phase],
["Wave", latestStarbridge.currentWaveIndex.toString()],
["Base", latestStarbridge.baseStatus.displayName || "unknown"],
["Hull", latestStarbridge.baseStatus.hull.toFixed(1)],
["Shield", latestStarbridge.baseStatus.shield.toFixed(1)],
["Heat", latestStarbridge.baseStatus.heat.toFixed(1)],
["Next", waveText(latestStarbridge)],
["Stock", stockText(latestStarbridge)],
["Roles", roleText(latestStarbridge)]
])
: `<dt class="muted">State</dt><dd class="muted">No Starbridge session.</dd>`;
controlledList.innerHTML = "";
const controlledObjects = latest.objects.filter(object => object.controlled);
if (controlledObjects.length === 0) {
controlledList.innerHTML = `<span class="muted">No controlled units in viewport.</span>`;
return;
}
for (const object of controlledObjects) {
const row = document.createElement("button");
row.type = "button";
row.className = "controlled-row";
row.innerHTML = `<span>${escapeHtml(object.displayName || object.entityKey)}</span><span>#${object.entityIndex}</span>`;
row.addEventListener("click", () => {
selectedEntityIndex = object.entityIndex;
centerX = object.x;
centerY = object.y;
void refresh();
button.dataset.windowControlWired = "true";
button.addEventListener("click", () => {
const action = button.dataset.windowControl as "minimize" | "maximize" | "close" | undefined;
if (action) {
void window.aetheriaRts.windowControl(action);
}
});
controlledList.append(row);
}
}
function inventoryText(items: InventoryItem[]): string {
if (items.length === 0) {
return "empty";
}
return items
.map(item => `${item.itemKey} x${item.quantity}`)
.join(", ");
}
function details(rows: Array<[string, string]>): string {
return rows
.map(([key, value]) => `<dt>${escapeHtml(key)}</dt><dd>${escapeHtml(value)}</dd>`)
.join("");
}
function escapeHtml(value: string): string {
return value.replace(/[&<>"']/g, character => {
switch (character) {
case "&": return "&amp;";
case "<": return "&lt;";
case ">": return "&gt;";
case "\"": return "&quot;";
case "'": return "&#39;";
default: return character;
}
});
}
canvas.addEventListener("click", event => {
if (!latest) {
return;
}
const rect = canvas.getBoundingClientRect();
const click = screenToWorld(event.clientX - rect.left, event.clientY - rect.top);
const nearest = nearestObject(click.x, click.y);
if (nearest && worldRadiusToScreen(nearest.distance) < 24) {
selectedEntityIndex = nearest.object.entityIndex;
void refresh();
}
});
canvas.addEventListener("contextmenu", event => {
event.preventDefault();
if (!latest) {
return;
}
const actor = latest.objects.find(object => object.entityIndex === selectedEntityIndex && object.controlled);
if (!actor) {
setStatus("Select an owned pawn or station first.");
return;
}
const rect = canvas.getBoundingClientRect();
const click = screenToWorld(event.clientX - rect.left, event.clientY - rect.top);
const nearest = nearestObject(click.x, click.y);
if (nearest && nearest.object.entityIndex !== actor.entityIndex && worldRadiusToScreen(nearest.distance) < 28) {
void issueTargetCommand(actor, nearest.object);
return;
}
void issueMoveCommand(actor, click.x, click.y);
});
function nearestObject(x: number, y: number): { object: ViewObject; distance: number } | null {
if (!latest) {
return null;
}
let nearest: ViewObject | null = null;
let nearestDistance = Number.POSITIVE_INFINITY;
for (const object of latest.objects) {
const dx = object.x - x;
const dy = object.y - y;
const distance = Math.hypot(dx, dy);
if (distance < nearestDistance) {
nearest = object;
nearestDistance = distance;
}
}
return nearest ? { object: nearest, distance: nearestDistance } : null;
}
async function issueTargetCommand(actor: ViewObject, target: ViewObject): Promise<void> {
const receipt = await window.aetheriaRts.setTarget({
actorEntityKey: actor.entityKey,
targetEntityKey: target.entityKey,
observedFrameId: latest?.frameId
});
latestOperationReceipt = formatOperationReceipt("setTarget", receipt, `${actor.entityKey} -> ${target.entityKey}`);
setStatus(latestOperationReceipt);
await refresh();
}
async function issueMoveCommand(actor: ViewObject, x: number, y: number): Promise<void> {
const dx = x - actor.x;
const dy = y - actor.y;
const length = Math.max(0.001, Math.hypot(dx, dy));
const receipt = await window.aetheriaRts.setMoveVector({
actorEntityKey: actor.entityKey,
directionX: dx / length,
directionY: dy / length,
scalar: 1,
observedFrameId: latest?.frameId
});
latestOperationReceipt = formatOperationReceipt("setMoveVector", receipt, `${actor.entityKey} ${dx.toFixed(1)},${dy.toFixed(1)}`);
setStatus(latestOperationReceipt);
await refresh();
}
function formatOperationReceipt(
label: string,
receipt: Awaited<ReturnType<AetheriaRtsApi["setMoveVector"]>>,
detail: string
): string {
const accepted = receipt.accepted ? "accepted" : "rejected";
return `${label} ${receipt.operationId} ${accepted} via ${receipt.route.kind} ${receipt.commandId} ${detail} @ frame ${latest?.frameId ?? "?"}`;
}
document.querySelector<HTMLButtonElement>("#zoom-in")?.addEventListener("click", () => {
zoom = Math.min(12, zoom * 1.4);
updateZoomLabel();
void refresh();
});
document.querySelector<HTMLButtonElement>("#zoom-out")?.addEventListener("click", () => {
zoom = Math.max(0.2, zoom / 1.4);
updateZoomLabel();
void refresh();
});
document.querySelector<HTMLButtonElement>("#refresh")?.addEventListener("click", () => {
void refresh();
});
window.addEventListener("resize", () => {
render();
});
function updateZoomLabel(): void {
zoomLabel.value = `${zoom.toFixed(1)}x`;
}
async function refresh(): Promise<void> {
if (refreshInFlight) {
return;
}
refreshInFlight = true;
try {
startViewportFeed();
} catch (error) {
setStatus(error instanceof Error ? error.message : "Unable to fetch viewport");
} finally {
refreshInFlight = false;
}
}
function surfaceListText(surfaces: ReadonlyArray<{
readonly surfaceId: string;
readonly routeHint: { readonly kind: string; readonly description?: string };
readonly sources: ReadonlyArray<{ readonly sourceId: string; readonly schemaId?: string }>;
}>): string {
if (surfaces.length === 0) {
return "none";
}
return surfaces
.map(surface => {
const route = surface.routeHint.description
? `${surface.routeHint.kind} ${surface.routeHint.description}`
: surface.routeHint.kind;
const sources = surface.sources.length === 0
? "no sources"
: surface.sources
.map(source => source.schemaId ? `${source.sourceId} (${source.schemaId})` : source.sourceId)
.join(" + ");
return `${surface.surfaceId} -> ${route}; ${sources}`;
})
.join(", ");
}
function stockText(session: StarbridgeSessionDocument): string {
if (session.stationStock.length === 0) {
return "empty";
}
return session.stationStock
.slice(0, 4)
.map(item => `${item.itemKey} x${item.quantity}`)
.join(", ");
}
function waveText(session: StarbridgeSessionDocument): string {
const wave = session.waveForecast[0];
if (!wave) {
return "none";
}
const attackers = wave.attackerKeys.length > 0 ? ` / ${wave.attackerKeys.join(", ")}` : "";
return `${wave.displayName || `Wave ${wave.waveIndex}`}${attackers}`;
}
function roleText(session: StarbridgeSessionDocument): string {
if (session.runtimeRoles.length === 0) {
return "none";
}
return session.runtimeRoles
.map(role => `${role.role}:${role.runtimeId}`)
.join(", ");
}
if (mainMenuMode) {
void showDaemonEveSurface();
} else if (legacyViewportMode) {
document.body.classList.remove("main-menu-mode", "eve-game-mode");
updateZoomLabel();
void startDebugSurface();
void refreshRuntimeSurfaces();
void refresh();
} else {
void showDaemonEveSurface();
}
void showDaemonEveSurface();

View file

@ -96,13 +96,6 @@ export type AetheriaCultMeshDaemonTarget = {
readonly endpoints?: readonly string[];
};
export type AetheriaMenuSurfaceRequest = {
surfaceId?: string;
inGame?: boolean;
canOpenRuntimeInputScreen?: boolean;
panelOnly?: boolean;
};
export type AetheriaEveSurfaceRequest = {
surfaceId?: string;
recordKey?: string;
@ -294,27 +287,27 @@ export class AetheriaCultMeshClient {
}
public async mapViewport(request: ViewportRequest): Promise<ViewportResponse> {
return this.queryExecutors.mapViewport(request);
return this.aetheria.zone().viewport.within(request);
}
public async objectsViewport(request: ViewportRequest): Promise<ObjectsViewportResponse> {
return this.queryExecutors.objectsViewport(request);
return this.aetheria.zone().objects.visibleWithin(request);
}
public async gravityViewport(request: ViewportRequest): Promise<GravityViewportResponse> {
return this.queryExecutors.gravityViewport(request);
return this.aetheria.zone().gravity.within(request);
}
public async renderSplatsViewport(request: ViewportRequest): Promise<RenderSplatsViewportResponse> {
return this.queryExecutors.renderSplatsViewport(request);
return this.aetheria.zone().renderSplats.within(request);
}
public async selectedObject(request: SelectedObjectRequest): Promise<SelectedObjectDocument> {
return this.queryExecutors.selectedObject(request);
return this.aetheria.selectedObject(request.entityIndex);
}
public async inventory(request: SelectedObjectRequest): Promise<InventoryDocument> {
return this.queryExecutors.inventory(request);
return this.aetheria.inventory(request.entityIndex);
}
public async daemonHealth(): Promise<DaemonHealthDocument> {
@ -333,14 +326,6 @@ export class AetheriaCultMeshClient {
return this.aetheria.daemon.assetManifest();
}
public async mainMenuSurface(request: AetheriaMenuSurfaceRequest = {}): Promise<AetheriaMenuSurfaceDocument> {
return buildMainMenuSurface(
request.surfaceId || "aetheria.main_menu.root",
request.canOpenRuntimeInputScreen === true,
request.inGame === true,
request.panelOnly === true);
}
public async eveSurface(request: AetheriaEveSurfaceRequest = {}): Promise<AetheriaMenuSurfaceDocument> {
const recordKey = eveSurfaceRecordKey(request);
const document = CultMesh.documentFromPublication(
@ -696,286 +681,6 @@ function isTransientPublicationReadError(error: unknown): boolean {
message.includes("did not contain schema");
}
function buildMainMenuSurface(
surfaceId: string,
canOpenRuntimeInputScreen: boolean,
inGame: boolean,
_panelOnly = false,
): AetheriaMenuSurfaceDocument {
const updatedAtUtc = new Date().toISOString();
const activeSurfaceId = normalizeMainMenuSurfaceId(surfaceId);
return buildMainMenuPanelSurface(activeSurfaceId, canOpenRuntimeInputScreen, inGame, updatedAtUtc);
}
function buildMainMenuPanelSurface(
surfaceId: string,
canOpenRuntimeInputScreen: boolean,
inGame: boolean,
updatedAtUtc: string,
): AetheriaMenuSurfaceDocument {
if (surfaceId === "aetheria.main_menu.settings") {
return surfaceDocument(
surfaceId,
"Aetheria Settings",
updatedAtUtc,
[
command("aetheria.main_menu.settings.show_player_settings", "Player Settings"),
command("aetheria.main_menu.settings.show_verse_settings", "Verse"),
command("aetheria.main_menu.settings.show_input_settings", "Input"),
command("aetheria.main_menu.settings.back_to_main", "Back"),
],
menuPanel(
surfaceId,
[
text("aetheria.mainMenu.settings.title", "SETTINGS", "text.title", { margin: "0 0 2.4rem 0" }, mainMenuTitleStyle("4.4rem")),
buttonColumn(
"aetheria.mainMenu.settings.actions",
button("aetheria.mainMenu.settings.playerSettings", "Player Settings", "aetheria.main_menu.settings.show_player_settings"),
button("aetheria.mainMenu.settings.verse", "Verse", "aetheria.main_menu.settings.show_verse_settings"),
button("aetheria.mainMenu.settings.input", "Input", "aetheria.main_menu.settings.show_input_settings"),
button("aetheria.mainMenu.settings.back", "Back", "aetheria.main_menu.settings.back_to_main"),
),
],
),
);
}
if (surfaceId === "aetheria.main_menu.input_settings") {
const children = [
text("aetheria.mainMenu.input.title", "INPUT", "text.title", { margin: "0 0 2.2rem 0" }, mainMenuTitleStyle("4.4rem")),
metric("aetheria.mainMenu.input.bindingOverrides", "Binding Overrides", "0"),
metric("aetheria.mainMenu.input.actionBarInputs", "Action-Bar Inputs", "0"),
text(
"aetheria.mainMenu.input.note",
canOpenRuntimeInputScreen && inGame
? "The runtime Eve input screen owns low-level InputSystem rebinding and action-bar input edits."
: "This title shell reports typed player-settings state. Launch a run to open the runtime Eve input screen that owns low-level InputSystem rebinding."),
buttonColumn(
"aetheria.mainMenu.input.actions",
button("aetheria.mainMenu.input.back", "Back", "aetheria.main_menu.settings.back_to_settings"),
),
];
return surfaceDocument(
surfaceId,
"Aetheria Input Settings",
updatedAtUtc,
[command("aetheria.main_menu.settings.back_to_settings", "Back")],
menuPanel(surfaceId, children));
}
if (surfaceId === "aetheria.main_menu.player_settings") {
return surfaceDocument(
surfaceId,
"Aetheria Player Settings",
updatedAtUtc,
[command("aetheria.main_menu.settings.back_to_settings", "Back")],
menuPanel(
surfaceId,
[
text("aetheria.mainMenu.player.title", "PLAYER", "text.title", { margin: "0 0 2.2rem 0" }, mainMenuTitleStyle("4.4rem")),
metric("aetheria.mainMenu.player.temperatureUnit", "Temperature Unit", "Celsius"),
metric("aetheria.mainMenu.player.significantDigits", "Significant Digits", "3"),
metric("aetheria.mainMenu.player.shutdown", "Default Shutdown", "25%"),
buttonColumn(
"aetheria.mainMenu.player.actions",
button("aetheria.mainMenu.player.back", "Back", "aetheria.main_menu.settings.back_to_settings"),
),
]));
}
if (surfaceId === "aetheria.main_menu.verse_settings") {
return surfaceDocument(
surfaceId,
"Aetheria Verse Settings",
updatedAtUtc,
[command("aetheria.main_menu.settings.back_to_settings", "Back")],
menuPanel(
surfaceId,
[
text("aetheria.mainMenu.verse.title", "VERSE", "text.title", { margin: "0 0 2.2rem 0" }, mainMenuTitleStyle("4.4rem")),
metric("aetheria.mainMenu.verse.name", "Name", "Local Aetheria"),
metric("aetheria.mainMenu.verse.id", "Verse", "aetheria.local"),
metric("aetheria.mainMenu.verse.visibility", "Visibility", "local"),
buttonColumn(
"aetheria.mainMenu.verse.actions",
button("aetheria.mainMenu.verse.back", "Back", "aetheria.main_menu.settings.back_to_settings"),
),
]));
}
const actionButtons = [
...(!inGame ? [button("aetheria.main_menu.root.continue", "Continue", "aetheria.main_menu.root.continue")] : []),
button("aetheria.main_menu.root.newGame", "New Game", "aetheria.main_menu.root.new_game"),
button("aetheria.main_menu.root.settings", "Settings", "aetheria.main_menu.root.show_settings"),
button("aetheria.main_menu.root.quit", "Quit", "aetheria.main_menu.root.quit"),
];
return surfaceDocument(
"aetheria.main_menu.root",
"Aetheria Starbridge",
updatedAtUtc,
[
...(!inGame ? [command("aetheria.main_menu.root.continue", "Continue")] : []),
command("aetheria.main_menu.root.new_game", "New Game"),
command("aetheria.main_menu.root.show_settings", "Settings"),
command("aetheria.main_menu.root.quit", "Quit"),
],
menuPanel(
"aetheria.main_menu.root",
[
text("aetheria.main_menu.root.title", "AETHERIA", "text.title", { margin: "0 0 -1.6rem 0" }, mainMenuTitleStyle("5.9rem")),
text("aetheria.main_menu.root.subtitle", "STARBRIDGE", "text.subtitle", { margin: "0 0 0.35rem 16.8rem" }, {
font: "100 2.6rem/1 Montserrat, sans-serif",
color: "rgba(232, 250, 255, 0.9)",
whiteSpace: "nowrap",
}),
buttonColumn("aetheria.main_menu.root.actions", ...actionButtons),
]),
);
}
function normalizeMainMenuSurfaceId(surfaceId: string): string {
switch (surfaceId) {
case "":
return "aetheria.main_menu.root";
case "aetheria.main_menu.root":
case "aetheria.main_menu.settings":
case "aetheria.main_menu.input_settings":
case "aetheria.main_menu.player_settings":
case "aetheria.main_menu.verse_settings":
return surfaceId;
default:
return "aetheria.main_menu.root";
}
}
function surfaceDocument(
surfaceId: string,
title: string,
updatedAtUtc: string,
commands: AetheriaMenuSurfaceCommand[],
...children: AetheriaMenuSurfaceComponent[]
): AetheriaMenuSurfaceDocument {
return {
providerId: "aetheria",
providerKind: "game.menu",
title,
version: 1,
updatedAtUtc,
surface: {
id: surfaceId,
root: node(
`${surfaceId}.root`,
"surface",
{},
{ position: "relative", overflow: "hidden", width: "100%", height: "100vh", minHeight: "100vh" },
{ background: "rgba(0,0,0,0)" },
...children),
styles: [
{ name: "font.title.family", value: "Montserrat" },
{ name: "font.title.style", value: "Thin" },
{ name: "font.title.weight", value: "100" },
{ name: "font.body.family", value: "Ubuntu" },
{ name: "font.body.style", value: "Regular" },
{ name: "font.body.weight", value: "400" },
{
name: "font.web.google",
value: "https://fonts.googleapis.com/css2?family=Montserrat:wght@100&family=Ubuntu:wght@400&display=swap",
},
],
},
commands,
};
}
function command(commandId: string, label: string): AetheriaMenuSurfaceCommand {
return { command: commandId, label, transport: "cultmesh" };
}
function node(
id: string,
kind: string,
props: Record<string, string>,
layoutOrChild?: Record<string, string> | AetheriaMenuSurfaceComponent,
styleOrChild?: Record<string, string> | AetheriaMenuSurfaceComponent,
...children: AetheriaMenuSurfaceComponent[]
): AetheriaMenuSurfaceComponent {
const layout = isSurfaceComponent(layoutOrChild) ? undefined : layoutOrChild;
const style = isSurfaceComponent(styleOrChild) ? undefined : styleOrChild;
const normalizedChildren = [
...(isSurfaceComponent(layoutOrChild) ? [layoutOrChild] : []),
...(isSurfaceComponent(styleOrChild) ? [styleOrChild] : []),
...children,
];
return { id, kind, props, layout, style, children: normalizedChildren };
}
function text(
id: string,
value: string,
kind = "text",
layout?: Record<string, string>,
style?: Record<string, string>,
): AetheriaMenuSurfaceComponent {
return node(id, kind, { value }, layout, style);
}
function metric(id: string, label: string, value: string): AetheriaMenuSurfaceComponent {
return node(id, "metric", { label, value });
}
function menuPanel(id: string, children: AetheriaMenuSurfaceComponent[]): AetheriaMenuSurfaceComponent {
return node(
`${id}.menu`,
"column",
{},
{
position: "relative",
padding: "7.25rem 0 0 6.75rem",
gap: "1.1rem",
width: "44rem",
maxWidth: "calc(100vw - 3rem)",
minHeight: "100vh",
alignItems: "flex-start",
},
{ color: "#e9fbff", background: "rgba(0,0,0,0)" },
...children);
}
function mainMenuTitleStyle(fontSize: string): Record<string, string> {
return {
font: `100 ${fontSize}/0.98 Montserrat, sans-serif`,
color: "rgba(232, 250, 255, 0.94)",
whiteSpace: "nowrap",
};
}
function button(id: string, label: string, commandId: string): AetheriaMenuSurfaceComponent {
return node(
id,
"control.button",
{ label, command: commandId },
{ minWidth: "0", width: "220px", height: "32px", padding: "0" },
{
background: "rgba(0, 0, 0, 0)",
border: "0",
borderWidth: "0",
borderStyle: "solid",
borderRadius: "0",
boxShadow: "none",
font: "400 1.55rem/32px Ubuntu, sans-serif",
color: "#e8fbff",
textAlign: "left",
});
}
function buttonColumn(id: string, ...children: AetheriaMenuSurfaceComponent[]): AetheriaMenuSurfaceComponent {
return node(id, "column", {}, { gap: "0.18rem", alignItems: "flex-start" }, { color: "#e8fbff" }, ...children);
}
function isSurfaceComponent(value: unknown): value is AetheriaMenuSurfaceComponent {
return !!value && typeof value === "object" && "kind" in value && "props" in value && "children" in value;
}
function stableToken(value: string): string {
return value
.replace(/[^a-zA-Z0-9]+/gu, "-")
@ -1090,6 +795,14 @@ function eveSurfaceRecordKey(request: AetheriaEveSurfaceRequest): string {
return "eve:surface:aetheria.daemon.game.tui";
case "aetheria.main_menu.root":
return "eve:surface:aetheria.main_menu.root";
case "aetheria.main_menu.settings":
return "eve:surface:aetheria.main_menu.settings";
case "aetheria.main_menu.input_settings":
return "eve:surface:aetheria.main_menu.input_settings";
case "aetheria.main_menu.player_settings":
return "eve:surface:aetheria.main_menu.player_settings";
case "aetheria.main_menu.verse_settings":
return "eve:surface:aetheria.main_menu.verse_settings";
case "aetheria.inventory.panel":
return "eve:surface:aetheria.inventory.panel";
case "aetheria.inventory.panel.dropdown":

View file

@ -120,7 +120,6 @@ export const AetheriaRtsIpcChannels = {
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",
debugSurfaceWatchStop: "aetheria-rts:debug-surface-watch-stop",
@ -1912,13 +1911,6 @@ export type AetheriaRuntimeViewportFeedSnapshot = {
sampleMs: number;
};
export type AetheriaMenuSurfaceRequest = {
surfaceId?: string;
inGame?: boolean;
canOpenRuntimeInputScreen?: boolean;
panelOnly?: boolean;
};
export type AetheriaEveSurfaceRequest = {
surfaceId?: string;
recordKey?: string;

View file

@ -26,16 +26,12 @@ const rtsCultMeshPort = Number.parseInt(process.env.AETHERIA_RTS_CULTMESH_PORT ?
const rtsVerseId = process.env.AETHERIA_RTS_VERSE_ID?.trim() || "aetheria.local";
const rtsDaemonId = process.env.AETHERIA_RTS_DAEMON_ID?.trim() || "starfire-rts";
const configuredDaemonUri = process.env.AETHERIA_RTS_CULTMESH_URI?.trim() ?? "";
const legacyDaemonEndpoint = process.env.AETHERIA_RTS_DAEMON_ENDPOINT?.trim() ?? "";
const launchLocalDaemon = configuredDaemonUri.length === 0 && legacyDaemonEndpoint.length === 0;
const launchLocalDaemon = configuredDaemonUri.length === 0;
const rtsCultMeshUri = configuredDaemonUri || `cultmesh://aetheria/daemon/${encodeURIComponent(rtsDaemonId)}`;
const rtsCultMeshAdvertiseHost = process.env.AETHERIA_RTS_CULTMESH_ADVERTISE_HOST?.trim() || "127.0.0.1";
const removedResolvedRudpEndpoint = process.env.AETHERIA_RTS_RESOLVED_RUDP_ENDPOINT?.trim() ?? "";
const removedPeerCultMeshEndpoints = process.env.AETHERIA_RTS_PEER_CULTMESH_ENDPOINTS?.trim() ?? "";
const localDaemonRudpEndpoint = launchLocalDaemon ? `rudp://127.0.0.1:${rtsCultMeshPort}` : "";
const electronSmoke = process.env.AETHERIA_RTS_ELECTRON_SMOKE === "1";
const electronSmokeResultPath = process.env.AETHERIA_RTS_ELECTRON_SMOKE_RESULT;
const rendererView = process.env.AETHERIA_RTS_VIEW?.trim() ?? "";
let daemonProcess: ChildProcessWithoutNullStreams | null = null;
let mainWindow: BrowserWindow | null = null;
@ -86,16 +82,6 @@ app.whenReady().then(async () => {
showStartup("Preparing Aetheria RTS", "Building daemon if needed.");
try {
if (legacyDaemonEndpoint) {
throw new Error("AETHERIA_RTS_DAEMON_ENDPOINT has been removed. Configure AETHERIA_RTS_CULTMESH_URI and let Odin/CultMesh resolve the transport.");
}
if (removedResolvedRudpEndpoint) {
throw new Error("AETHERIA_RTS_RESOLVED_RUDP_ENDPOINT has been removed. Configure AETHERIA_RTS_CULTMESH_URI and let Odin/CultMesh resolve the daemon transport.");
}
if (removedPeerCultMeshEndpoints) {
throw new Error("AETHERIA_RTS_PEER_CULTMESH_ENDPOINTS has been removed. Configure AETHERIA_RTS_CULTMESH_URI and let Odin/CultMesh discover peer endpoints.");
}
if (launchLocalDaemon) {
await ensureDotnetBuild();
showStartup("Launching Aetheria RTS", "Starting the Aetheria daemon.");
@ -138,11 +124,7 @@ app.whenReady().then(async () => {
showStartup("Launching Aetheria RTS", "Waiting for the daemon CultMesh frame.");
await rtsClient.waitForFrame(30000);
if (rendererView === "main-menu") {
await mainWindow.loadFile(rendererIndex, { hash: "main-menu" });
} else {
await mainWindow.loadFile(rendererIndex);
}
await mainWindow.loadFile(rendererIndex);
if (electronSmoke) {
const result = await runElectronSmoke(mainWindow);
writeElectronSmokeResult({ ok: true, result });
@ -209,7 +191,7 @@ async function runElectronSmoke(window: BrowserWindow): Promise<Record<string, u
const api = window.aetheriaRts;
const status = document.querySelector("#status")?.textContent ?? "";
const bodyMode = document.body.className;
const eveHostText = document.querySelector("#main-menu-host")?.textContent ?? "";
const eveHostText = document.querySelector("#eve-surface-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;
@ -374,8 +356,6 @@ function registerIpc(): void {
daemonRunning: daemonProcess != null && !daemonProcess.killed,
daemonMode: launchLocalDaemon ? "local" : "remote",
}));
ipcMain.handle("aetheria-rts:main-menu-surface", async (_event, request) =>
requireClient().mainMenuSurface(request));
ipcMain.handle("aetheria-rts:debug-surface", () => readDebugSurface());
ipcMain.handle("aetheria-rts:debug-surface-watch", (event, subscriptionId: string) => {
if (!subscriptionId)

View file

@ -23,7 +23,6 @@ const channels = {
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",
debugSurfaceWatchStop: "aetheria-rts:debug-surface-watch-stop",
@ -63,7 +62,6 @@ contextBridge.exposeInMainWorld("aetheriaRts", {
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 => {
const subscriptionId = `debug-surface-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;

View file

@ -12,7 +12,7 @@
"check:rts-bindings": "C:\\Progra~1\\nodejs\\node.exe scripts/generate-rts-bindings.mjs --check",
"verify:stage7b": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\\verify-stage7b-rts-client.ps1",
"verify:stage7c": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\\verify-stage7c-local-runtime.ps1",
"verify:stage7c:electron": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\\verify-stage7c-electron-shell.ps1",
"verify:stage7c:electron": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\\verify-stage7c-electron-rts.ps1",
"verify:stage7d:remote": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\\verify-stage7d-remote-cultmesh.ps1",
"electron": "npm run build && .\\node_modules\\electron\\dist\\electron.exe .",
"watch": "C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.json --watch"

View file

@ -3,8 +3,8 @@ import { resolve } from "node:path";
const root = resolve(import.meta.dirname, "..");
const source = resolve(root, "Electron", "preload.cjs");
const legacyOutput = resolve(root, "electron-dist", "preload.js");
const previousPreloadOutput = resolve(root, "electron-dist", "preload.js");
const output = resolve(root, "electron-dist", "preload.cjs");
rmSync(legacyOutput, { force: true });
rmSync(previousPreloadOutput, { force: true });
copyFileSync(source, output);

View file

@ -43,7 +43,6 @@ const ipcChannels = [
["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"],
["debugSurfaceWatchStop", "aetheria-rts:debug-surface-watch-stop"],
@ -481,13 +480,6 @@ export type AetheriaRuntimeViewportFeedSnapshot = {
sampleMs: number;
};
export type AetheriaMenuSurfaceRequest = {
surfaceId?: string;
inGame?: boolean;
canOpenRuntimeInputScreen?: boolean;
panelOnly?: boolean;
};
export type AetheriaEveSurfaceRequest = {
surfaceId?: string;
recordKey?: string;
@ -1151,13 +1143,6 @@ export type AetheriaRuntimeViewportFeedSnapshot = {
${renderRtsDocumentTypes("Viewport")}
export type AetheriaMenuSurfaceRequest = {
surfaceId?: string;
inGame?: boolean;
canOpenRuntimeInputScreen?: boolean;
panelOnly?: boolean;
};
export type AetheriaEveSurfaceRequest = {
surfaceId?: string;
recordKey?: string;
@ -1232,7 +1217,6 @@ export type AetheriaRtsApi = {
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;
windowControl(action: "minimize" | "maximize" | "close"): Promise<void>;

View file

@ -50,32 +50,21 @@ foreach ($symbol in $required) {
}
}
if (-not (Get-Content Client/app.ts -Raw).Contains('window.aetheriaRts.surfaceCatalogIndex()')) {
Write-Error "Stage 7B verifier failed: RTS renderer does not read the shared CultMesh surface catalog index."
}
$rendererText = Get-Content Client/app.ts -Raw
if (-not $rendererText.Contains('surface.routeHint.kind') -or -not $rendererText.Contains('surface.sources.length')) {
Write-Error "Stage 7B verifier failed: RTS renderer does not expose CultMesh surface route and source metadata."
if (-not $rendererText.Contains('window.aetheriaRts.eveSurface') -or
-not $rendererText.Contains('renderEveSurface') -or
-not $rendererText.Contains('resolveEveDocument') -or
-not $rendererText.Contains('submitEveCommand')) {
Write-Error "Stage 7B verifier failed: RTS renderer is not lowering daemon Eve through the generated preload API."
}
if (-not $rendererText.Contains('latestOperationReceipt') -or
-not $rendererText.Contains('receipt.commandId') -or
-not $rendererText.Contains('receipt.operationId') -or
-not $rendererText.Contains('receipt.accepted') -or
-not $rendererText.Contains('receipt.route.kind')) {
Write-Error "Stage 7B verifier failed: RTS renderer does not expose CultMesh operation receipt metadata."
}
if (-not (Get-Content wwwroot/index.html -Raw).Contains('runtime-surface-details')) {
Write-Error "Stage 7B verifier failed: RTS renderer does not expose runtime surface diagnostics in the shell."
$indexText = Get-Content wwwroot/index.html -Raw
if (-not $indexText.Contains('eve-surface-host')) {
Write-Error "Stage 7B verifier failed: RTS renderer is not mounting the daemon Eve surface host."
}
$transportLayoutPatterns = @(
'\[[0-9]+\]',
'new Array<unknown>',
'command\[',
'value\[',
'bounds\[',
'viewportSchema',
'commandSchema',
@ -426,17 +415,20 @@ if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for duplicated renderer contract checks."
}
$forbiddenRendererPollingHits = & rg -n 'setInterval|objectsViewport\(viewport\)|gravityViewport\(viewport\)' Client/app.ts 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "Stage 7B verifier failed: renderer still owns the RTS viewport polling loop instead of subscribing to the generated feed.`n$forbiddenRendererPollingHits"
}
if ($LASTEXITCODE -gt 1) {
Write-Error "Stage 7B verifier could not run rg for renderer polling checks."
}
$requiredEveRendererSymbols = @(
'renderEveSurface',
'eve-surface-host',
'window.aetheriaRts.eveSurface',
'resolveEveDocument',
'gamecult.eve.surface.v1',
'aetheria.daemon.game'
)
& rg -q 'watchViewportFeed' Client/app.ts
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: renderer is not subscribing to the generated viewport feed."
foreach ($symbol in $requiredEveRendererSymbols) {
& rg -F -q $symbol Client/app.ts wwwroot/index.html
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7B verifier failed: expected daemon Eve renderer symbol '$symbol' was not found."
}
}
$forbiddenBindingPatterns = @(

View file

@ -5,48 +5,50 @@ Set-Location $root
npm run build
if ($LASTEXITCODE -ne 0) {
Write-Error "Stage 7C Electron shell verifier failed: RTS build failed."
Write-Error "Stage 7C Electron RTS verifier failed: RTS build failed."
}
if (-not (Get-Content wwwroot\index.html -Raw).Contains('runtime-surface-details')) {
Write-Error "Stage 7C Electron shell verifier failed: built renderer markup does not expose runtime surface diagnostics."
if (-not (Get-Content wwwroot\index.html -Raw).Contains('eve-surface-host')) {
Write-Error "Stage 7C Electron RTS verifier failed: built renderer markup does not host the daemon Eve surface."
}
if (-not (Get-Content wwwroot\app.js -Raw).Contains('window.aetheriaRts.surfaceCatalogIndex()')) {
Write-Error "Stage 7C Electron shell verifier failed: built renderer script does not read the CultMesh surface catalog index."
$appText = Get-Content wwwroot\app.js -Raw
if (-not $appText.Contains('window.aetheriaRts.eveSurface') -or
$appText.Contains('legacyViewportMode') -or
$appText.Contains('mainMenuSurface') -or
$appText.Contains('main-menu-mode')) {
Write-Error "Stage 7C Electron RTS verifier failed: renderer is not daemon-Eve-only."
}
$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 shell verifier failed: Electron smoke does not call the preload CultMesh surface catalog APIs."
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.renderSplatsViewport') -or
-not $mainText.Contains('eveFieldSurface') -or
-not $mainText.Contains('embeddedDocuments') -or
-not $mainText.Contains('fog.tint')) {
Write-Error "Stage 7C Electron shell verifier failed: Electron smoke does not verify daemon Eve field document lowering."
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
-not $mainText.Contains('eveReceipt?.commandId') -or
-not $mainText.Contains('eveReceipt?.accepted') -or
-not $mainText.Contains('eveReceipt?.route')) {
Write-Error "Stage 7C Electron shell verifier failed: Electron smoke does not verify typed operation receipts through preload."
Write-Error "Stage 7C Electron RTS verifier failed: Electron smoke does not verify typed operation receipts through preload."
}
if (-not $mainText.Contains('AETHERIA_RTS_RESOLVED_RUDP_ENDPOINT has been removed') -or
-not $mainText.Contains('AETHERIA_RTS_PEER_CULTMESH_ENDPOINTS has been removed') -or
-not $mainText.Contains('AETHERIA_RTS_VERSE_ID') -or
if (-not $mainText.Contains('AETHERIA_RTS_VERSE_ID') -or
-not $mainText.Contains('--verse-id') -or
-not $mainText.Contains('AETHERIA_RTS_DAEMON_ID') -or
-not $mainText.Contains('--daemon-id')) {
Write-Error "Stage 7C Electron shell verifier failed: Electron launcher does not enforce CultMesh URI daemon targeting."
Write-Error "Stage 7C Electron RTS verifier failed: Electron launcher does not configure daemon Verse identity."
}
$electron = Join-Path $root "node_modules\electron\dist\electron.exe"
if (!(Test-Path -LiteralPath $electron)) {
Write-Error "Stage 7C Electron shell verifier failed: electron.exe was not found at $electron."
Write-Error "Stage 7C Electron RTS verifier failed: electron.exe was not found at $electron."
}
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("aetheria-stage7c-electron-" + [Guid]::NewGuid().ToString("N"))
@ -65,16 +67,16 @@ try {
$env:AETHERIA_RTS_ELECTRON_SMOKE_RESULT = $resultPath
$process = Start-Process -FilePath $electron -ArgumentList "." -WorkingDirectory $root -Wait -PassThru
if ($process.ExitCode -ne 0) {
Write-Error "Stage 7C Electron shell verifier failed: Electron exited with code $($process.ExitCode)."
Write-Error "Stage 7C Electron RTS verifier failed: Electron exited with code $($process.ExitCode)."
}
if (!(Test-Path -LiteralPath $resultPath)) {
Write-Error "Stage 7C Electron shell verifier failed: Electron did not write a smoke result."
Write-Error "Stage 7C Electron RTS verifier failed: Electron did not write a smoke result."
}
$result = Get-Content -Raw -LiteralPath $resultPath | ConvertFrom-Json
if ($result.ok -ne $true) {
Write-Error "Stage 7C Electron shell verifier failed: $($result.error)"
Write-Error "Stage 7C Electron RTS verifier failed: $($result.error)"
}
}
finally {
@ -83,10 +85,8 @@ finally {
Remove-Item Env:\AETHERIA_RTS_CULTMESH_PORT -ErrorAction SilentlyContinue
Remove-Item Env:\AETHERIA_RTS_VERSE_ID -ErrorAction SilentlyContinue
Remove-Item Env:\AETHERIA_RTS_DAEMON_ID -ErrorAction SilentlyContinue
Remove-Item Env:\AETHERIA_RTS_PEER_CULTMESH_ENDPOINTS -ErrorAction SilentlyContinue
Remove-Item Env:\AETHERIA_RTS_RESOLVED_RUDP_ENDPOINT -ErrorAction SilentlyContinue
Remove-Item Env:\AETHERIA_RTS_ELECTRON_SMOKE_RESULT -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Host "Stage 7C Electron shell verifier passed."
Write-Host "Stage 7C Electron RTS verifier passed."

View file

@ -8,56 +8,14 @@
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main class="shell">
<section class="map-pane">
<header class="topbar">
<div>
<strong>Aetheria RTS</strong>
<span id="status">Connecting</span>
</div>
<div class="toolbar">
<button id="zoom-out" type="button" title="Zoom out">-</button>
<output id="zoom-label">1x</output>
<button id="zoom-in" type="button" title="Zoom in">+</button>
<button id="refresh" type="button" title="Refresh viewport">Refresh</button>
</div>
</header>
<canvas id="map"></canvas>
</section>
<aside class="side-pane">
<section>
<h1 id="selection-title">No Selection</h1>
<dl id="selection-details"></dl>
</section>
<section>
<h2>Controlled</h2>
<div id="controlled-list"></div>
</section>
<section>
<h2>Starbridge</h2>
<dl id="starbridge-details"></dl>
</section>
<section>
<h2>Frame</h2>
<dl id="frame-details"></dl>
</section>
<section>
<h2>Runtime Surfaces</h2>
<dl id="runtime-surface-details"></dl>
</section>
<section>
<h2>CultUI Debug</h2>
<div id="debug-surface-host" class="debug-surface-host"></div>
</section>
</aside>
</main>
<div id="status" class="status">Connecting</div>
<div class="window-drag-region" aria-hidden="true"></div>
<div class="window-controls" aria-label="Window controls">
<button class="window-control" type="button" data-window-control="minimize" aria-label="Minimize">-</button>
<button class="window-control" type="button" data-window-control="maximize" aria-label="Maximize">[]</button>
<button class="window-control window-control-close" type="button" data-window-control="close" aria-label="Close">x</button>
</div>
<section id="main-menu-host" class="main-menu-host" aria-label="Aetheria main menu"></section>
<main id="eve-surface-host" class="eve-surface-host" aria-label="Aetheria daemon Eve surface"></main>
<script type="module" src="app.js"></script>
</body>
</html>

View file

@ -1,8 +1,8 @@
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #111417;
background: #020909;
color: #e8edf0;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
@ -11,189 +11,49 @@
html,
body {
width: 100%;
height: 100%;
margin: 0;
}
body {
-webkit-app-region: drag;
overflow: hidden;
}
.main-menu-host {
display: none;
}
body.main-menu-mode {
-webkit-app-region: drag;
background: #020606;
}
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 {
.eve-surface-host {
-webkit-app-region: no-drag;
display: block;
width: 100%;
min-height: 100%;
}
body.eve-game-mode .window-drag-region {
.status {
position: fixed;
right: 112px;
bottom: 10px;
z-index: 30;
max-width: min(560px, calc(100vw - 140px));
overflow: hidden;
color: rgba(219, 250, 255, 0.45);
font: 400 11px/1.2 "Ubuntu", system-ui, sans-serif;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none;
}
.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: block;
min-height: 100%;
padding: 0;
background: transparent;
}
body.main-menu-mode .eve-surface {
width: 100%;
min-height: 100vh;
}
body.main-menu-mode .eve-text-title,
body.main-menu-mode .eve-text-subtitle {
width: 720px;
color: rgba(209, 242, 255, 0.96);
font-family: "Montserrat", sans-serif;
font-weight: 100;
line-height: 0.95;
text-align: right;
}
body.main-menu-mode .eve-root-title {
margin: 0 0 2px;
font-size: 86px;
}
body.main-menu-mode .eve-root-subtitle {
margin: 0 0 36px;
font-size: 39px;
}
body.main-menu-mode .eve-text,
body.main-menu-mode .eve-metric,
body.main-menu-mode .eve-control-button {
font-family: "Ubuntu", system-ui, sans-serif;
}
body.main-menu-mode .eve-column {
display: flex;
flex-direction: column;
}
body.main-menu-mode .eve-control-button {
width: 220px;
min-width: 0;
height: 32px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
color: rgba(219, 250, 255, 0.96);
font-size: 24px;
line-height: 32px;
text-align: left;
letter-spacing: 0;
transition: letter-spacing 160ms ease, color 160ms ease;
}
body.main-menu-mode .eve-control-button:hover,
body.main-menu-mode .eve-control-button:focus-visible {
background: transparent;
color: white;
letter-spacing: 2px;
outline: 0;
}
body.main-menu-mode .eve-metric {
display: grid;
grid-template-columns: 180px minmax(0, 1fr);
gap: 8px;
width: 520px;
margin: 0 0 8px;
}
body.main-menu-mode .eve-text:not(.eve-text-title):not(.eve-text-subtitle) {
width: 520px;
margin: 0 0 14px;
color: rgba(219, 250, 255, 0.9);
font-size: 16px;
line-height: 1.25;
}
.window-controls {
display: none;
}
body.main-menu-mode .window-drag-region {
-webkit-app-region: drag;
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 15;
display: block;
height: 44px;
}
body.main-menu-mode .window-controls {
-webkit-app-region: no-drag;
position: fixed;
top: 8px;
@ -203,7 +63,7 @@ body.main-menu-mode .window-controls {
gap: 2px;
}
body.main-menu-mode .window-control {
.window-control {
min-width: 0;
width: 30px;
height: 24px;
@ -217,222 +77,14 @@ body.main-menu-mode .window-control {
line-height: 24px;
}
body.main-menu-mode .window-control:hover,
body.main-menu-mode .window-control:focus-visible {
.window-control:hover,
.window-control:focus-visible {
background: rgba(219, 250, 255, 0.09);
color: rgba(240, 252, 255, 0.95);
outline: 0;
}
body.main-menu-mode .window-control-close:hover,
body.main-menu-mode .window-control-close:focus-visible {
.window-control-close:hover,
.window-control-close:focus-visible {
background: rgba(255, 88, 88, 0.28);
}
button {
min-width: 36px;
height: 32px;
border: 1px solid #41505a;
border-radius: 6px;
background: #20282d;
color: #eef6f8;
font: inherit;
}
button:hover {
background: #2a353b;
}
.shell {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
height: 100%;
min-height: 0;
}
.map-pane {
display: grid;
grid-template-rows: 52px minmax(0, 1fr);
min-width: 0;
min-height: 0;
background: #0f1215;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 0 16px;
border-bottom: 1px solid #263038;
background: #171d21;
}
.topbar span {
display: inline-block;
margin-left: 12px;
color: #9fb0b9;
font-size: 13px;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
}
#zoom-label {
min-width: 52px;
color: #c5d2d7;
text-align: center;
}
#map {
display: block;
width: 100%;
height: 100%;
background: #0d1013;
cursor: crosshair;
}
.side-pane {
display: grid;
grid-auto-rows: min-content;
gap: 18px;
min-height: 0;
overflow: auto;
padding: 18px;
border-left: 1px solid #263038;
background: #171d21;
}
.side-pane section {
min-width: 0;
}
h1,
h2 {
margin: 0 0 10px;
font-size: 15px;
line-height: 1.2;
}
h1 {
font-size: 18px;
}
dl {
display: grid;
grid-template-columns: 96px minmax(0, 1fr);
gap: 7px 10px;
margin: 0;
font-size: 13px;
}
dt {
color: #91a4ad;
}
dd {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
}
#controlled-list {
display: grid;
gap: 8px;
font-size: 13px;
}
.controlled-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 7px 0;
border-bottom: 1px solid #263038;
}
.muted {
color: #91a4ad;
}
.debug-surface-host {
min-width: 0;
}
.debug-surface-host .eve-surface,
.debug-surface-host .eve-column,
.debug-surface-host .eve-form {
display: flex;
min-width: 0;
flex-direction: column;
gap: 8px;
}
.debug-surface-host .eve-row {
display: flex;
min-width: 0;
flex-wrap: wrap;
gap: 8px;
}
.debug-surface-host .eve-text-title {
color: #d8f7ff;
font-size: 16px;
font-weight: 700;
}
.debug-surface-host .eve-text {
color: #b8c7cf;
font-size: 13px;
line-height: 1.35;
}
.debug-surface-host .eve-metric {
display: grid;
grid-template-columns: 86px minmax(0, 1fr);
gap: 6px 8px;
font-size: 12px;
}
.debug-surface-host .eve-metric dt {
color: #91a4ad;
}
.debug-surface-host .eve-metric dd {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
}
.debug-surface-host .eve-control-button {
min-width: 82px;
height: 28px;
border-radius: 4px;
font-size: 12px;
}
@media (max-width: 760px) {
.shell {
grid-template-columns: 1fr;
grid-template-rows: minmax(0, 1fr) 230px;
}
.side-pane {
border-top: 1px solid #263038;
border-left: 0;
}
.topbar {
align-items: flex-start;
flex-direction: column;
height: auto;
padding: 10px 12px;
}
.map-pane {
grid-template-rows: auto minmax(0, 1fr);
}
}

View file

@ -402,6 +402,34 @@ static RudpCultNetSchemaServer StartRtsCultMeshHost(
response,
AetheriaRuntimeVerseRecordKeys.MainMenuSurface.ToString(),
"main-menu").ConfigureAwait(false);
await InjectEveSurfaceSnapshotAsync(
node,
options,
request,
response,
AetheriaRuntimeVerseRecordKeys.MainMenuSettingsSurface.ToString(),
"main-menu-settings").ConfigureAwait(false);
await InjectEveSurfaceSnapshotAsync(
node,
options,
request,
response,
AetheriaRuntimeVerseRecordKeys.MainMenuInputSettingsSurface.ToString(),
"main-menu-input-settings").ConfigureAwait(false);
await InjectEveSurfaceSnapshotAsync(
node,
options,
request,
response,
AetheriaRuntimeVerseRecordKeys.MainMenuPlayerSettingsSurface.ToString(),
"main-menu-player-settings").ConfigureAwait(false);
await InjectEveSurfaceSnapshotAsync(
node,
options,
request,
response,
AetheriaRuntimeVerseRecordKeys.MainMenuVerseSettingsSurface.ToString(),
"main-menu-verse-settings").ConfigureAwait(false);
await InjectEveSurfaceSnapshotAsync(
node,
options,
@ -829,6 +857,10 @@ static Task<EveSurfaceDocument?> ReadEveSurfacePublicationAsync(AetheriaStateNod
"editor" => node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.DaemonEditorSurface).ReadAsync(),
"editor-tui" => node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.DaemonEditorTuiSurface).ReadAsync(),
"main-menu" => node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.MainMenuSurface).ReadAsync(),
"main-menu-settings" => node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.MainMenuSettingsSurface).ReadAsync(),
"main-menu-input-settings" => node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.MainMenuInputSettingsSurface).ReadAsync(),
"main-menu-player-settings" => node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.MainMenuPlayerSettingsSurface).ReadAsync(),
"main-menu-verse-settings" => node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.MainMenuVerseSettingsSurface).ReadAsync(),
"inventory-panel" => node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.InventoryPanelSurface).ReadAsync(),
"inventory-dropdown" => node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.InventoryDropdownSurface).ReadAsync(),
"map-menu" => node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.MapMenuSurface).ReadAsync(),
@ -1122,10 +1154,11 @@ static async Task PublishDaemonMenuSurfacesAsync(
var verseHost = await EnsureVerseHostSettingsAsync(node, options, updatedAtUtc)
.ConfigureAwait(false);
var stateBoot = AetheriaRuntimeStateBoot.Inspect(
new DirectoryInfo(Path.GetDirectoryName(node.StatePath) ?? "."),
node.StatePath);
var mainMenu = AetheriaRuntimeMainMenuSurfaceBuilder.BuildRoot(
AetheriaRuntimeStateBoot.Inspect(
new DirectoryInfo(Path.GetDirectoryName(node.StatePath) ?? "."),
node.StatePath),
stateBoot,
frame,
AetheriaRuntimeVerseHostSettingsDocument.FromSnapshot(new AetheriaRuntimeVerseHostSettingsSnapshot(
verseHost.ServiceId,
@ -1141,6 +1174,30 @@ static async Task PublishDaemonMenuSurfacesAsync(
canOpenRuntimeInputScreen: true,
inGame: true,
updatedAtUtc);
var mainMenuSettings = AetheriaRuntimeMainMenuSurfaceBuilder.BuildSettings(updatedAtUtc);
var mainMenuInputSettings = AetheriaRuntimeMainMenuSurfaceBuilder.BuildInputSettings(
stateBoot,
playerSettings,
canOpenRuntimeInputScreen: true,
inGame: true,
updatedAtUtc);
var mainMenuPlayerSettings = AetheriaRuntimeMainMenuSurfaceBuilder.BuildPlayerSettings(
playerSettings,
updatedAtUtc);
var mainMenuVerseSettings = AetheriaRuntimeMainMenuSurfaceBuilder.BuildVerseSettings(
AetheriaRuntimeClientTargetSurfaceBuilder.Build(
stateBoot,
AetheriaRuntimeVerseHostSettingsDocument.FromSnapshot(new AetheriaRuntimeVerseHostSettingsSnapshot(
verseHost.ServiceId,
verseHost.VerseId,
verseHost.RootVerse,
verseHost.CanonicalService,
verseHost.LocatedService,
verseHost.CultMeshAddress,
verseHost.Title,
verseHost.Visibility,
verseHost.LastUpdatedAtUtc)),
updatedAtUtc));
var inventoryPanel = AetheriaRuntimeInventoryPanelSurfaceBuilder.BuildFromDocuments(
currentEntity,
stationRefit,
@ -1165,6 +1222,18 @@ static async Task PublishDaemonMenuSurfacesAsync(
await node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.MainMenuSurface)
.ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(mainMenu))
.ConfigureAwait(false);
await node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.MainMenuSettingsSurface)
.ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(mainMenuSettings))
.ConfigureAwait(false);
await node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.MainMenuInputSettingsSurface)
.ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(mainMenuInputSettings))
.ConfigureAwait(false);
await node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.MainMenuPlayerSettingsSurface)
.ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(mainMenuPlayerSettings))
.ConfigureAwait(false);
await node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.MainMenuVerseSettingsSurface)
.ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(mainMenuVerseSettings))
.ConfigureAwait(false);
await node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.InventoryPanelSurface)
.ReplaceAsync(AetheriaRuntimeSurfaceDocuments.ToPortableSurface(inventoryPanel))
.ConfigureAwait(false);
@ -1377,6 +1446,10 @@ static async Task PublishOdinSurfaceAnnouncementsAsync(
("aetheria.daemon.editor", "Aetheria Daemon Editor", AetheriaRuntimeVerseRecordKeys.DaemonEditorSurface),
("aetheria.daemon.editor.tui", "Aetheria Daemon Editor TUI", AetheriaRuntimeVerseRecordKeys.DaemonEditorTuiSurface),
("aetheria.main_menu.root", "Main Menu", AetheriaRuntimeVerseRecordKeys.MainMenuSurface),
("aetheria.main_menu.settings", "Main Menu Settings", AetheriaRuntimeVerseRecordKeys.MainMenuSettingsSurface),
("aetheria.main_menu.input_settings", "Main Menu Input Settings", AetheriaRuntimeVerseRecordKeys.MainMenuInputSettingsSurface),
("aetheria.main_menu.player_settings", "Main Menu Player Settings", AetheriaRuntimeVerseRecordKeys.MainMenuPlayerSettingsSurface),
("aetheria.main_menu.verse_settings", "Main Menu Verse Settings", AetheriaRuntimeVerseRecordKeys.MainMenuVerseSettingsSurface),
("aetheria.inventory.panel", "Inventory Panel", AetheriaRuntimeVerseRecordKeys.InventoryPanelSurface),
("aetheria.inventory.panel.dropdown", "Inventory Dropdown", AetheriaRuntimeVerseRecordKeys.InventoryDropdownSurface),
("aetheria.map.zone_details", "Map Menu", AetheriaRuntimeVerseRecordKeys.MapMenuSurface),

View file

@ -1309,7 +1309,6 @@ static void RequireTypedBehaviorBodyKeys(string root)
string.Join("; ", missingBehaviorSymbols));
}
}
static void RequireNoScopedReactiveFacadeWrappers(string root)
{
var providerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "AetheriaUnityRuntimeClientProvider.cs");
@ -3758,7 +3757,7 @@ static void RequireMainMenuSettingsCommands(string root)
var requiredMainMenuSymbols = new[]
{
"ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.PlayerSettingsShellSurfaceId)",
"ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId)",
".RuntimeState(stateBoot, \"unity-main-menu\")",
".MainMenuSurface(surfaceId, CanOpenRuntimeInputScreen(), InGame)",
"AetheriaEveUnitySurfaceHost.RenderRuntime(",
@ -3853,8 +3852,8 @@ static void RequireMainMenuSettingsShellUsesEveSurface(string root)
"RenderMenuSurface(",
"ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.SettingsSurfaceId)",
"ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.InputSettingsSurfaceId)",
"ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.PlayerSettingsShellSurfaceId)",
"ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.VerseSettingsShellSurfaceId)",
"ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId)",
"ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId)",
".MainMenuSurface(surfaceId, CanOpenRuntimeInputScreen(), InGame)",
"HandleSettingsSurfaceCommand(",
"HandleVerseSettingsSurfaceCommand(",
@ -3932,8 +3931,8 @@ static void RequireMainMenuSettingsShellUsesEveSurface(string root)
"public static class AetheriaRuntimeMainMenuCommands",
"BuildSettings(",
"BuildInputSettings(",
"BuildPlayerSettingsShell(",
"BuildVerseSettingsShell(",
"BuildPlayerSettings(",
"BuildVerseSettings(",
"private static AetheriaRuntimeSurfaceDocument BuildRoot(",
"private static AetheriaRuntimeSurfaceDocument BuildInputSettings(",
"var verseLabel = VerseLabel(verseTitle, verseId)",
@ -9520,7 +9519,6 @@ static void RequireClientTargetBootAuthority(string root)
"RuntimeClientTargetFileName = \"aetheria-client.cc\"",
"RuntimeReplicaDirectoryName = \"Verses\"",
"RuntimeStatePathOverrideEnvironmentVariable = \"AETHERIA_STATE_PATH\"",
"LegacyRuntimeStatePathOverrideEnvironmentVariable = \"AETHERIA_EVE_STATE_PATH\"",
"GetClientTargetPath",
"GetReplicaStateFilePath",
"ResolveStatePathOverride"
@ -10359,7 +10357,7 @@ static void RequireVerseSettingsShellAndBridge(string root)
"AetheriaRuntimeMainMenuCommandKind.ShowVerseSettings",
"ShowVerseSettingsSurface(",
"HandleVerseSettingsSurfaceCommand(EveSurfaceCommandRequest request)",
"ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.VerseSettingsShellSurfaceId)",
"ResolveMainMenuSurface(AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId)",
".MainMenuSurface(surfaceId, CanOpenRuntimeInputScreen(), InGame)",
"AetheriaRuntimeMainMenuSurfaceCommands.TryRead(request, out var command)",
"AetheriaRuntimeMainMenuCommandKind.ClientTargetCommand",

View file

@ -18,8 +18,7 @@ game surface, so Unity starts as a lowering surface for the daemon Verse member
without a hand-wired scene object. Set `AETHERIA_EVE_SURFACE_ID` to mount a
different daemon or provider surface such as `aetheria.game.tui`,
`aetheria.daemon.editor`, or `aetheria.daemon.editor.tui`; set
`AETHERIA_STATE_PATH` to override the selected local `.cc` state file (`AETHERIA_EVE_STATE_PATH`
still works as a legacy fallback), or disable the automatic mount with
`AETHERIA_STATE_PATH` to override the selected local `.cc` state file, or disable the automatic mount with
`AETHERIA_DISABLE_EVE_RUNTIME_BOOTSTRAP=true` or
`--aetheria-disable-eve-runtime-bootstrap`. Batchmode disables the bootstrap so
compile and smoke gates do not accidentally create renderer state.

View file

@ -26,7 +26,7 @@ namespace GameCult.Aetheria.State.Verse
private readonly Func<int, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> _zoneDetailsSurface;
private readonly Func<AetheriaRuntimeInventoryPanelSurfaceRequest, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> _inventoryPanelSurface;
private readonly Func<AetheriaRuntimeInventoryDropdownSurfaceRequest, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> _inventoryDropdownSurface;
private readonly Func<string, bool, bool, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> _mainMenuSurface;
private readonly Func<string, bool, bool, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> _mainMenuSurfaceDocument;
private readonly Func<int, CultMeshDocumentHandle<AetheriaRuntimeSelectedObjectDocument>> _selectedObject;
private readonly Func<int, CultMeshDocumentHandle<AetheriaRuntimeInventoryDocument>> _inventory;
private readonly Func<string, CultMeshDocumentHandle<AetheriaRuntimeStarbridgePlayerSeatDocument>> _starbridgePlayerSeat;
@ -65,7 +65,7 @@ namespace GameCult.Aetheria.State.Verse
Func<int, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> zoneDetailsSurface,
Func<AetheriaRuntimeInventoryPanelSurfaceRequest, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> inventoryPanelSurface,
Func<AetheriaRuntimeInventoryDropdownSurfaceRequest, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> inventoryDropdownSurface,
Func<string, bool, bool, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> mainMenuSurface,
Func<string, bool, bool, CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument>> mainMenuSurfaceDocument,
Func<int, CultMeshDocumentHandle<AetheriaRuntimeSelectedObjectDocument>> selectedObject,
Func<int, CultMeshDocumentHandle<AetheriaRuntimeInventoryDocument>> inventory,
CultMeshDocumentHandle<AetheriaRuntimeStarbridgeScenarioDocument> starbridgeScenario,
@ -102,7 +102,7 @@ namespace GameCult.Aetheria.State.Verse
_zoneDetailsSurface = zoneDetailsSurface ?? throw new ArgumentNullException(nameof(zoneDetailsSurface));
_inventoryPanelSurface = inventoryPanelSurface ?? throw new ArgumentNullException(nameof(inventoryPanelSurface));
_inventoryDropdownSurface = inventoryDropdownSurface ?? throw new ArgumentNullException(nameof(inventoryDropdownSurface));
_mainMenuSurface = mainMenuSurface ?? throw new ArgumentNullException(nameof(mainMenuSurface));
_mainMenuSurfaceDocument = mainMenuSurfaceDocument ?? throw new ArgumentNullException(nameof(mainMenuSurfaceDocument));
_selectedObject = selectedObject ?? throw new ArgumentNullException(nameof(selectedObject));
_inventory = inventory ?? throw new ArgumentNullException(nameof(inventory));
StarbridgeScenario = starbridgeScenario ?? throw new ArgumentNullException(nameof(starbridgeScenario));
@ -267,7 +267,7 @@ namespace GameCult.Aetheria.State.Verse
if (string.IsNullOrWhiteSpace(surfaceId))
throw new ArgumentException("Main menu surface id must be non-empty.", nameof(surfaceId));
return _mainMenuSurface(surfaceId, canOpenRuntimeInputScreen, inGame);
return _mainMenuSurfaceDocument(surfaceId, canOpenRuntimeInputScreen, inGame);
}
public CultMeshDocumentHandle<AetheriaRuntimeSelectedObjectDocument> SelectedObject(int zoneIndex)

View file

@ -90,7 +90,7 @@ namespace GameCult.Aetheria.State.Verse
AetheriaRuntimeClientTargetCommands.DiscoverVerses),
Button(
"aetheria.clientTarget.discovery.refresh",
"Refresh Shell",
"Refresh",
AetheriaRuntimeClientTargetCommands.Refresh)));
return new AetheriaRuntimeSurfaceDocument(

View file

@ -16,7 +16,7 @@ namespace GameCult.Aetheria.State.Verse
}
/// <summary>
/// Legacy Unity XZ viewport adapter. New daemon/client query code should use CultMath.rect in Aetheria XY space.
/// Unity XZ viewport boundary for callers that still present the world through Unity axes.
/// </summary>
public readonly struct AetheriaRuntimeXzRect
{

View file

@ -64,10 +64,10 @@ namespace GameCult.Aetheria.State.Verse
Surface(AetheriaRuntimeDaemonEditorSurfaceBuilder.SurfaceId, "Aetheria Daemon Editor", AetheriaRuntimeVerseRecordKeys.DaemonEditorSurface.ToString(), "operator", "Daemon editor and surface inventory.", providerId: "aetheria.daemon", providerKind: "editor.daemon", transport: "cultmesh-record"),
Surface(AetheriaRuntimeDaemonEditorSurfaceBuilder.TuiSurfaceId, "Aetheria Daemon Editor TUI", AetheriaRuntimeVerseRecordKeys.DaemonEditorTuiSurface.ToString(), "agent", "Compact daemon editor and surface inventory.", providerId: "aetheria.daemon", providerKind: "editor.daemon", transport: "cultmesh-record"),
Surface(AetheriaRuntimeMainMenuCommands.RootSurfaceId, "Main Menu", AetheriaRuntimeVerseRecordKeys.MainMenuSurface.ToString(), "player", "Runtime main menu root.", transport: "cultmesh-record"),
Surface(AetheriaRuntimeMainMenuCommands.SettingsSurfaceId, "Main Menu Settings", "", "player", "Runtime main menu settings shell.", status: "client-contextual"),
Surface(AetheriaRuntimeMainMenuCommands.InputSettingsSurfaceId, "Main Menu Input Settings", "", "player", "Main menu input settings handoff shell.", status: "client-contextual"),
Surface(AetheriaRuntimeMainMenuCommands.PlayerSettingsShellSurfaceId, "Main Menu Player Settings", "", "player", "Main menu player settings shell.", status: "client-contextual"),
Surface(AetheriaRuntimeMainMenuCommands.VerseSettingsShellSurfaceId, "Main Menu Verse Settings", "", "player", "Main menu Verse settings shell.", status: "client-contextual"),
Surface(AetheriaRuntimeMainMenuCommands.SettingsSurfaceId, "Main Menu Settings", AetheriaRuntimeVerseRecordKeys.MainMenuSettingsSurface.ToString(), "player", "Runtime main menu settings panel.", transport: "cultmesh-record"),
Surface(AetheriaRuntimeMainMenuCommands.InputSettingsSurfaceId, "Main Menu Input Settings", AetheriaRuntimeVerseRecordKeys.MainMenuInputSettingsSurface.ToString(), "player", "Main menu input settings panel.", transport: "cultmesh-record"),
Surface(AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId, "Main Menu Player Settings", AetheriaRuntimeVerseRecordKeys.MainMenuPlayerSettingsSurface.ToString(), "player", "Main menu player settings panel.", transport: "cultmesh-record"),
Surface(AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId, "Main Menu Verse Settings", AetheriaRuntimeVerseRecordKeys.MainMenuVerseSettingsSurface.ToString(), "player", "Main menu Verse settings panel.", transport: "cultmesh-record"),
Surface(AetheriaRuntimeInputSettingsCommands.SurfaceId, "Input Settings", "", "player", "Runtime input binding surface.", status: "client-contextual"),
Surface(AetheriaRuntimeVerseHostCommands.SurfaceId, "Verse Host Settings", "", "operator", "Verse host visibility and discovery surface.", status: "client-contextual"),
Surface(AetheriaRuntimeLoadoutTemplateCommands.SurfaceId, "Loadout Templates", "", "player", "Loadout template save surface.", status: "client-contextual"),

View file

@ -12,8 +12,8 @@ namespace GameCult.Aetheria.State.Verse
public const string RootSurfaceId = "aetheria.main_menu.root";
public const string SettingsSurfaceId = "aetheria.main_menu.settings";
public const string InputSettingsSurfaceId = "aetheria.main_menu.input_settings";
public const string PlayerSettingsShellSurfaceId = "aetheria.main_menu.player_settings";
public const string VerseSettingsShellSurfaceId = "aetheria.main_menu.verse_settings";
public const string PlayerSettingsSurfaceId = "aetheria.main_menu.player_settings";
public const string VerseSettingsSurfaceId = "aetheria.main_menu.verse_settings";
public const string ContinueRun = "aetheria.main_menu.root.continue";
public const string NewGame = "aetheria.main_menu.root.new_game";
@ -128,14 +128,14 @@ namespace GameCult.Aetheria.State.Verse
version);
}
public static AetheriaRuntimeSurfaceDocument BuildPlayerSettingsShell(
public static AetheriaRuntimeSurfaceDocument BuildPlayerSettings(
AetheriaRuntimePlayerSettingsDocument playerSettings,
string updatedAtUtc,
long version = 1)
{
return WithBackAction(
AetheriaRuntimePlayerSettingsSurfaceBuilder.Build(playerSettings, updatedAtUtc, version),
AetheriaRuntimeMainMenuCommands.PlayerSettingsShellSurfaceId,
AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId,
AetheriaRuntimeMainMenuCommands.BackToSettings,
"Back");
}
@ -295,7 +295,7 @@ namespace GameCult.Aetheria.State.Verse
if (canOpenRuntimeInputScreen)
{
builder.Text(
"The runtime Eve input screen owns low-level InputSystem rebinding and action-bar input edits. This title shell reports typed player-settings state and hands off to that owner.",
"The runtime Eve input screen owns low-level InputSystem rebinding and action-bar input edits. This panel reports typed player-settings state and hands off to that owner.",
"aetheria.mainMenu.input.note");
}
else if (inGame)
@ -307,7 +307,7 @@ namespace GameCult.Aetheria.State.Verse
else
{
builder.Text(
"This title shell reports the typed player-settings state. Launch a run to open the runtime Eve input screen that owns low-level InputSystem rebinding.",
"This panel reports typed player-settings state. Launch a run to open the runtime Eve input screen that owns low-level InputSystem rebinding.",
"aetheria.mainMenu.input.note");
}
@ -328,13 +328,13 @@ namespace GameCult.Aetheria.State.Verse
return AetheriaRuntimeSurfaceDocuments.FromPortableSurface(builder.Build());
}
public static AetheriaRuntimeSurfaceDocument BuildVerseSettingsShell(
public static AetheriaRuntimeSurfaceDocument BuildVerseSettings(
AetheriaRuntimeSurfaceDocument document,
long version = 1)
{
return WithBackAction(
document,
AetheriaRuntimeMainMenuCommands.VerseSettingsShellSurfaceId,
AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId,
AetheriaRuntimeMainMenuCommands.BackToSettings,
"Back");
}
@ -968,10 +968,10 @@ namespace GameCult.Aetheria.State.Verse
return TryReadSettings(operationId, out command);
case AetheriaRuntimeMainMenuCommands.InputSettingsSurfaceId:
return TryReadInputSettings(operationId, out command);
case AetheriaRuntimeMainMenuCommands.PlayerSettingsShellSurfaceId:
return TryReadPlayerSettingsShell(operationId, out command);
case AetheriaRuntimeMainMenuCommands.VerseSettingsShellSurfaceId:
return TryReadVerseSettingsShell(operationId, out command);
case AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId:
return TryReadPlayerSettings(operationId, out command);
case AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId:
return TryReadVerseSettings(operationId, out command);
default:
return false;
}
@ -1037,7 +1037,7 @@ namespace GameCult.Aetheria.State.Verse
}
}
private static bool TryReadPlayerSettingsShell(string commandText, out AetheriaRuntimeMainMenuCommand command)
private static bool TryReadPlayerSettings(string commandText, out AetheriaRuntimeMainMenuCommand command)
{
if (string.Equals(commandText, AetheriaRuntimeMainMenuCommands.BackToSettings, StringComparison.Ordinal))
{
@ -1055,7 +1055,7 @@ namespace GameCult.Aetheria.State.Verse
return false;
}
private static bool TryReadVerseSettingsShell(string commandText, out AetheriaRuntimeMainMenuCommand command)
private static bool TryReadVerseSettings(string commandText, out AetheriaRuntimeMainMenuCommand command)
{
if (string.Equals(commandText, AetheriaRuntimeMainMenuCommands.BackToSettings, StringComparison.Ordinal))
{

View file

@ -21,7 +21,6 @@ namespace GameCult.Aetheria.State.Verse
public const string RuntimeDaemonEditorSurfaceFileSuffix = ".daemon.editor.eve.cc";
public const string RuntimeDaemonEditorTuiSurfaceFileSuffix = ".daemon.editor.tui.cc";
public const string RuntimeStatePathOverrideEnvironmentVariable = "AETHERIA_STATE_PATH";
public const string LegacyRuntimeStatePathOverrideEnvironmentVariable = "AETHERIA_EVE_STATE_PATH";
public const string RuntimeIdOverrideEnvironmentVariable = "AETHERIA_RUNTIME_ID";
public const string UnityRuntimeIdOverrideEnvironmentVariable = "AETHERIA_UNITY_RUNTIME_ID";
@ -114,11 +113,7 @@ namespace GameCult.Aetheria.State.Verse
public static string ResolveStatePathOverride()
{
var configured = System.Environment.GetEnvironmentVariable(RuntimeStatePathOverrideEnvironmentVariable);
if (!string.IsNullOrWhiteSpace(configured))
return configured;
return System.Environment.GetEnvironmentVariable(LegacyRuntimeStatePathOverrideEnvironmentVariable) ?? "";
return System.Environment.GetEnvironmentVariable(RuntimeStatePathOverrideEnvironmentVariable) ?? "";
}
public static string ResolveRuntimeIdOverride()

View file

@ -68,6 +68,18 @@ namespace GameCult.Aetheria.State.Verse
public static CultRecordKey MainMenuSurface { get; } =
new CultRecordKey("eve:surface:aetheria.main_menu.root");
public static CultRecordKey MainMenuSettingsSurface { get; } =
new CultRecordKey("eve:surface:aetheria.main_menu.settings");
public static CultRecordKey MainMenuInputSettingsSurface { get; } =
new CultRecordKey("eve:surface:aetheria.main_menu.input_settings");
public static CultRecordKey MainMenuPlayerSettingsSurface { get; } =
new CultRecordKey("eve:surface:aetheria.main_menu.player_settings");
public static CultRecordKey MainMenuVerseSettingsSurface { get; } =
new CultRecordKey("eve:surface:aetheria.main_menu.verse_settings");
public static CultRecordKey InventoryPanelSurface { get; } =
new CultRecordKey("eve:surface:aetheria.inventory.panel");
@ -802,16 +814,16 @@ namespace GameCult.Aetheria.State.Verse
updatedAtUtc);
}
if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.PlayerSettingsShellSurfaceId, StringComparison.Ordinal))
if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId, StringComparison.Ordinal))
{
return AetheriaRuntimeMainMenuSurfaceBuilder.BuildPlayerSettingsShell(
return AetheriaRuntimeMainMenuSurfaceBuilder.BuildPlayerSettings(
TryCurrentPlayerSettings(),
updatedAtUtc);
}
if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.VerseSettingsShellSurfaceId, StringComparison.Ordinal))
if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId, StringComparison.Ordinal))
{
return AetheriaRuntimeMainMenuSurfaceBuilder.BuildVerseSettingsShell(
return AetheriaRuntimeMainMenuSurfaceBuilder.BuildVerseSettings(
AetheriaRuntimeClientTargetSurfaceBuilder.Build(
stateBoot,
TryCurrentVerseHostSettings(),