diff --git a/.gitignore b/.gitignore
index 4e4b9b7b7..ce9e18618 100644
--- a/.gitignore
+++ b/.gitignore
@@ -117,6 +117,20 @@ Aetheria.State.Replica/bin/
Aetheria.State.Replica/obj/
Aetheria.State.Verify/bin/
Aetheria.State.Verify/obj/
+Aetheria.Rts.Web/bin/
+Aetheria.Rts.Web/obj/
+Aetheria.Rts.Web/electron-dist/
+Aetheria.Rts.Web/logs/
+Aetheria.Rts.Web/node_modules/
+Aetheria.Rts.Web/runtime/
+Aetheria.Rts.Web/Aetheria.Rts.Web/logs/
+Aetheria.Rts.Web/*.log
+Aetheria.Rts.Web/wwwroot/*.js
+Aetheria.State.AuthoritySmoke/bin/
+Aetheria.State.AuthoritySmoke/obj/
+Aetheria.State.Freeze/bin/
+Aetheria.State.Freeze/obj/
+Aetheria.State.Daemon/*.log
# JetBrains Rider .idea folder
.idea/
diff --git a/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts b/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts
new file mode 100644
index 000000000..b75d06ea9
--- /dev/null
+++ b/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts
@@ -0,0 +1,278 @@
+//
+// Generated by scripts/generate-rts-bindings.mjs from the RTS CultMesh surface contract.
+
+import type {
+ CultMeshOperationReceipt,
+ CultMeshSurfaceCatalogDiagnostic,
+ CultMeshSurfaceCatalogIndexDiagnostic,
+ CultMeshViewportRequest,
+} from "cultmesh-ts";
+
+export type Viewport = CultMeshViewportRequest;
+
+export type AetheriaRuntimeSetMoveVectorRequest = {
+ actorEntityKey: string;
+ directionX: number;
+ directionY: number;
+ scalar: number;
+ observedFrameId?: number;
+};
+
+export type AetheriaRuntimeSetTargetRequest = {
+ actorEntityKey: string;
+ targetEntityKey: string;
+ observedFrameId?: number;
+};
+
+export type AetheriaRuntimeDaemonCommandReceipt = CultMeshOperationReceipt & {
+ commandId: string;
+};
+
+export type AetheriaRuntimeSelectedObjectRequest = {
+ entityIndex: number;
+};
+
+export type AetheriaRuntimeViewportFeedRequest = {
+ viewport: Viewport;
+ selectedEntityIndex?: number;
+};
+
+export type AetheriaRuntimeViewportFeedSnapshot = {
+ viewport: ViewportResponse;
+ selectedObject: SelectedObjectProjection | null;
+ inventory: InventoryProjection | null;
+ daemonHealth: DaemonHealthProjection;
+ authorityStatus: AuthorityStatusProjection;
+ starbridgeSession: StarbridgeSessionProjection;
+ receivedAtUtc: string;
+ sampleMs: number;
+};
+
+export type ViewportResponse = {
+ schema: string;
+ frameId: number;
+ publishedAtUtc: string;
+ simulationTimeSeconds: number;
+ runId: string;
+ zoneIndex: number;
+ zoneName: string;
+ currentEntityKey: string;
+ viewport: Viewport;
+ controlledEntityIndices: number[];
+ objects: ViewObject[];
+ gravityInfluences: GravityInfluence[];
+ bodies: BodyView[];
+};
+
+export type ObjectsViewportResponse = {
+ schema: string;
+ frameId: number;
+ publishedAtUtc: string;
+ simulationTimeSeconds: number;
+ runId: string;
+ zoneIndex: number;
+ zoneName: string;
+ currentEntityKey: string;
+ viewport: Viewport;
+ controlledEntityIndices: number[];
+ objects: ViewObject[];
+};
+
+export type GravityViewportResponse = {
+ schema: string;
+ frameId: number;
+ publishedAtUtc: string;
+ simulationTimeSeconds: number;
+ runId: string;
+ zoneIndex: number;
+ zoneName: string;
+ viewport: Viewport;
+ gravityInfluences: GravityInfluence[];
+ bodies: BodyView[];
+};
+
+export type ViewObject = {
+ entityIndex: number;
+ entityKey: string;
+ displayName: string;
+ kind: string;
+ factionKey: string;
+ x: number;
+ y: number;
+ z: number;
+ directionX: number;
+ directionY: number;
+ velocityX: number;
+ velocityY: number;
+ controlled: boolean;
+ targetEntityIndex: number;
+ isActive: boolean;
+ visibility: number;
+ status: EntityStatus;
+ inventory: InventoryItem[];
+};
+
+export type EntityStatus = {
+ hull: number;
+ shield: number;
+ heat: number;
+};
+
+export type InventoryItem = {
+ source: string;
+ itemKey: string;
+ quantity: number;
+ quality: number;
+ durability: number;
+ enabled: boolean;
+};
+
+export type SelectedObjectProjection = {
+ schema: string;
+ frameId: number;
+ runId: string;
+ zoneIndex: number;
+ entityIndex: number;
+ selected: ViewObject | null;
+};
+
+export type InventoryProjection = {
+ schema: string;
+ frameId: number;
+ runId: string;
+ zoneIndex: number;
+ entityIndex: number;
+ entityKey: string;
+ items: InventoryItem[];
+ equipment: InventoryItem[];
+ cargo: InventoryItem[];
+};
+
+export type DaemonHealthProjection = {
+ schema: string;
+ daemonId: string;
+ verseId: string;
+ publishedAtUtc: string;
+ statePath: string;
+ frameId: number;
+ observedCommandCount: number;
+ appliedCommandCount: number;
+ rejectedCommandCount: number;
+ status: string;
+ publicationSource: string;
+ transport: string;
+ commandBoundaryPath: string;
+};
+
+export type AuthorityStatusProjection = {
+ schema: string;
+ verseId: string;
+ policyId: string;
+ ruleVersion: string;
+ hostRuntimeId: string;
+ defaultMode: string;
+ updatedAtUtc: string;
+ rules: AuthorityRuleProjection[];
+};
+
+export type AuthorityRuleProjection = {
+ ruleId: string;
+ subjectPrefix: string;
+ claimKinds: string[];
+ mode: string;
+ runtimeIds: string[];
+ leaseScope: string;
+ priority: number;
+};
+
+export type StarbridgeSessionProjection = {
+ schema: string;
+ frameId: number;
+ publishedAtUtc: string;
+ sessionId: string;
+ scenarioId: string;
+ scenarioName: string;
+ runId: string;
+ zoneIndex: number;
+ zoneName: string;
+ phase: string;
+ currentWaveIndex: number;
+ baseStatus: StarbridgeBaseStatusProjection;
+ stationStock: StarbridgeStationStockItemProjection[];
+ waveForecast: StarbridgeWaveForecastProjection[];
+ runtimeRoles: StarbridgeRuntimeRoleProjection[];
+};
+
+export type StarbridgeBaseStatusProjection = {
+ entityKey: string;
+ displayName: string;
+ hull: number;
+ shield: number;
+ heat: number;
+ isActive: boolean;
+};
+
+export type StarbridgeStationStockItemProjection = {
+ itemKey: string;
+ quantity: number;
+ quality: number;
+ durability: number;
+ source: string;
+};
+
+export type StarbridgeWaveForecastProjection = {
+ waveIndex: number;
+ displayName: string;
+ attackerKeys: string[];
+ bossKey: string;
+ recoveredTechnologyKeys: string[];
+};
+
+export type StarbridgeRuntimeRoleProjection = {
+ runtimeId: string;
+ role: string;
+ entityKey: string;
+};
+
+export type GravityInfluence = {
+ bodyKey: string;
+ orbitKey: string;
+ kind: string;
+ x: number;
+ y: number;
+ radius: number;
+ gravityDepth: number;
+ gravityDepthExponent: number;
+ waveRadius: number;
+ waveDepth: number;
+ waveSpeed: number;
+};
+
+export type BodyView = {
+ bodyKey: string;
+ orbitKey: string;
+ name: string;
+ kind: string;
+ x: number;
+ y: number;
+ radius: number;
+ isAsteroidBelt: boolean;
+};
+
+
+export type AetheriaRtsApi = {
+ mapViewport(request: Viewport): Promise;
+ objectsViewport(request: Viewport): Promise;
+ gravityViewport(request: Viewport): Promise;
+ selectedObject(request: AetheriaRuntimeSelectedObjectRequest): Promise;
+ inventory(request: AetheriaRuntimeSelectedObjectRequest): Promise;
+ daemonHealth(): Promise;
+ authorityStatus(): Promise;
+ starbridgeSession(): Promise;
+ watchViewportFeed(request: AetheriaRuntimeViewportFeedRequest, callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void): () => void;
+ setMoveVector(request: AetheriaRuntimeSetMoveVectorRequest): Promise;
+ setTarget(request: AetheriaRuntimeSetTargetRequest): Promise;
+ surfaceCatalog(): Promise;
+ surfaceCatalogIndex(): Promise;
+ health(): Promise;
+};
diff --git a/Aetheria.Rts.Web/Client/app.ts b/Aetheria.Rts.Web/Client/app.ts
new file mode 100644
index 000000000..30aade699
--- /dev/null
+++ b/Aetheria.Rts.Web/Client/app.ts
@@ -0,0 +1,590 @@
+import type {
+ AetheriaRtsApi,
+ AuthorityStatusProjection,
+ AetheriaRuntimeViewportFeedSnapshot,
+ BodyView,
+ DaemonHealthProjection,
+ GravityInfluence,
+ InventoryItem,
+ InventoryProjection,
+ SelectedObjectProjection,
+ StarbridgeSessionProjection,
+ ViewObject,
+ Viewport,
+ ViewportResponse,
+} from "./aetheria-rts-contract.js";
+
+export {};
+
+declare global {
+ interface Window {
+ aetheriaRts: AetheriaRtsApi;
+ }
+}
+
+const canvas = requiredElement("#map");
+const statusEl = requiredElement("#status");
+const zoomLabel = requiredElement("#zoom-label");
+const selectionTitle = requiredElement("#selection-title");
+const selectionDetails = requiredElement("#selection-details");
+const frameDetails = requiredElement("#frame-details");
+const starbridgeDetails = requiredElement("#starbridge-details");
+const runtimeSurfaceDetails = requiredElement("#runtime-surface-details");
+const controlledList = requiredElement("#controlled-list");
+const ctx = requiredContext(canvas);
+
+let zoom = 1;
+let centerX = 0;
+let centerY = 0;
+let selectedEntityIndex = -1;
+let latest: ViewportResponse | null = null;
+let latestSelected: SelectedObjectProjection | null = null;
+let latestInventory: InventoryProjection | null = null;
+let latestDaemonHealth: DaemonHealthProjection | null = null;
+let latestAuthority: AuthorityStatusProjection | null = null;
+let latestStarbridge: StarbridgeSessionProjection | null = null;
+let latestSurfaceIndex: Awaited> | null = null;
+let latestOperationReceipt: string | null = null;
+let latestReceivedAt = 0;
+let latestPollMs = 0;
+let refreshInFlight = false;
+let viewportFeedUnsubscribe: (() => void) | null = null;
+
+function requiredElement(selector: string): TElement {
+ const element = document.querySelector(selector);
+ if (!element) {
+ throw new Error(`Aetheria RTS shell 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;
+}
+
+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 {
+ try {
+ latestSurfaceIndex = await window.aetheriaRts.surfaceCatalogIndex();
+ renderPanels();
+ } catch (error) {
+ runtimeSurfaceDetails.innerHTML = details([
+ ["State", error instanceof Error ? error.message : "unavailable"]
+ ]);
+ }
+}
+
+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();
+ }
+ }
+
+ 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 ?? [])]
+ ])
+ : `StateClick a visible object on the map.`;
+
+ 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)]
+ ])
+ : `StateDiscovering runtime surfaces.`;
+
+ 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)]
+ ])
+ : `StateNo Starbridge session.`;
+
+ controlledList.innerHTML = "";
+ const controlledObjects = latest.objects.filter(object => object.controlled);
+ if (controlledObjects.length === 0) {
+ controlledList.innerHTML = `No controlled units in viewport.`;
+ return;
+ }
+
+ for (const object of controlledObjects) {
+ const row = document.createElement("button");
+ row.type = "button";
+ row.className = "controlled-row";
+ row.innerHTML = `${escapeHtml(object.displayName || object.entityKey)}#${object.entityIndex}`;
+ row.addEventListener("click", () => {
+ selectedEntityIndex = object.entityIndex;
+ centerX = object.x;
+ centerY = object.y;
+ void refresh();
+ });
+ 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]) => `${escapeHtml(key)}${escapeHtml(value)}`)
+ .join("");
+}
+
+function escapeHtml(value: string): string {
+ return value.replace(/[&<>"']/g, character => {
+ switch (character) {
+ case "&": return "&";
+ case "<": return "<";
+ case ">": return ">";
+ case "\"": return """;
+ case "'": return "'";
+ 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 {
+ 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 {
+ 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>,
+ 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("#zoom-in")?.addEventListener("click", () => {
+ zoom = Math.min(12, zoom * 1.4);
+ updateZoomLabel();
+ void refresh();
+});
+
+document.querySelector("#zoom-out")?.addEventListener("click", () => {
+ zoom = Math.max(0.2, zoom / 1.4);
+ updateZoomLabel();
+ void refresh();
+});
+
+document.querySelector("#refresh")?.addEventListener("click", () => {
+ void refresh();
+});
+
+window.addEventListener("resize", () => {
+ render();
+});
+
+function updateZoomLabel(): void {
+ zoomLabel.value = `${zoom.toFixed(1)}x`;
+}
+
+async function refresh(): Promise {
+ 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: StarbridgeSessionProjection): 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: StarbridgeSessionProjection): 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: StarbridgeSessionProjection): string {
+ if (session.runtimeRoles.length === 0) {
+ return "none";
+ }
+
+ return session.runtimeRoles
+ .map(role => `${role.role}:${role.runtimeId}`)
+ .join(", ");
+}
+
+updateZoomLabel();
+void refreshRuntimeSurfaces();
+void refresh();
diff --git a/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts
new file mode 100644
index 000000000..f410c7206
--- /dev/null
+++ b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts
@@ -0,0 +1,443 @@
+import { encode } from "@msgpack/msgpack";
+import { performance } from "node:perf_hooks";
+import { CultMesh } from "cultmesh-ts";
+import type {
+ CultMeshOperationContext,
+ CultMeshQueryContext,
+ CultMeshSurfaceCatalogIndexDiagnostic,
+ CultMeshVerse,
+} from "cultmesh-ts";
+import type {
+ CultNetDocumentPutRawMessage,
+ CultNetPeer,
+} from "cultnet-ts";
+import {
+ AetheriaRtsSchemas,
+ createAetheriaRuntimeRtsOperationHandles,
+ createAetheriaRuntimeRtsVerseFacade,
+ createAetheriaRuntimeRtsQueryHandles,
+ createAetheriaRuntimeRtsStatePointers,
+ describeAetheriaRuntimeRtsLiveFeedSurface,
+ describeAetheriaRuntimeRtsQueryHandles,
+ describeAetheriaRuntimeRtsSurfaceCatalog,
+ type AetheriaRuntimeDaemonCommandReceipt,
+ type AuthorityStatusProjection,
+ type DaemonHealthProjection,
+ type GravityViewportResponse,
+ type InventoryProjection,
+ type ObjectsViewportResponse,
+ type AetheriaRuntimeRtsLiveFeedDiagnostic,
+ type AetheriaRuntimeRtsProjectionDiagnostic,
+ type AetheriaRuntimeRtsSurfaceCatalogDiagnostic,
+ type AetheriaRuntimeRtsOperationHandles,
+ type AetheriaRuntimeRtsVerseFacade,
+ type AetheriaRuntimeRtsStatePointers,
+ type AetheriaRuntimeViewportFeedRequest,
+ type AetheriaRuntimeViewportFeedSnapshot,
+ type RtsSetMoveVectorRequest,
+ type RtsSetTargetRequest,
+ type SelectedObjectProjection,
+ type SelectedObjectRequest,
+ type StarbridgeSessionProjection,
+ type ViewportRequest,
+ type ViewportResponse,
+} from "./aetheria-rts-bindings.js";
+import { AetheriaLocalPublicationReader } from "./aetheria-local-publication-reader.js";
+import { AetheriaRemotePublicationReader } from "./aetheria-remote-publication-reader.js";
+import {
+ projectAuthorityStatus,
+ projectDaemonHealth,
+ projectGravityViewportFromFrame,
+ projectInventoryFromFrame,
+ projectObjectsViewportFromFrame,
+ projectSelectedObjectFromFrame,
+ projectStarbridgeSessionSummary,
+ projectViewportFromFrame,
+} from "./aetheria-rts-local-projection.js";
+
+const connectionId = 0x43554c54;
+
+type AetheriaPublicationReader = {
+ readonly statePathDescription: string;
+ readDaemonFrame(): Promise;
+ readDaemonHealth(): Promise;
+ readAuthorityPolicy(): Promise;
+ readStarbridgeSessionSummary(): Promise;
+ close?(): Promise;
+};
+
+export type AetheriaCultMeshClientOptions = {
+ publicationMode?: "local" | "remote";
+ snapshotTimeoutMs?: number;
+};
+
+export type {
+ BodyView,
+ EntityStatus,
+ GravityViewportResponse,
+ GravityInfluence,
+ InventoryItem,
+ ObjectsViewportResponse,
+ AetheriaRuntimeViewportFeedRequest,
+ AetheriaRuntimeViewportFeedSnapshot,
+ RtsSetMoveVectorRequest,
+ RtsSetTargetRequest,
+ SelectedObjectProjection,
+ SelectedObjectRequest,
+ StarbridgeSessionProjection,
+ ViewObject,
+ ViewportRequest,
+ ViewportResponse,
+} from "./aetheria-rts-bindings.js";
+
+export class AetheriaCultMeshClient {
+ #peer: CultNetPeer | null = null;
+ private readonly verse: CultMeshVerse;
+ private readonly queryVerse: CultMeshVerse;
+ private readonly commandVerse: CultMeshVerse;
+
+ public constructor(
+ private readonly endpoint: string,
+ statePath: string,
+ private readonly runtimeId = "aetheria-rts-electron",
+ options: AetheriaCultMeshClientOptions = {},
+ ) {
+ const publicationMode = options.publicationMode ?? "local";
+ this.publications = publicationMode === "remote"
+ ? new AetheriaRemotePublicationReader(
+ this.endpoint,
+ `${this.runtimeId}-reader`,
+ options.snapshotTimeoutMs)
+ : new AetheriaLocalPublicationReader(statePath);
+ this.verse = CultMesh.verse("aetheria.local", this.runtimeId);
+ this.queryVerse = publicationMode === "remote"
+ ? this.verse.withRoute("network", this.publications.statePathDescription)
+ : this.verse.withRoute("shared-memory", this.publications.statePathDescription);
+ this.commandVerse = this.verse
+ .withRoute("network", this.endpoint)
+ .withClaim("commander-control", { shardId: "aetheria.local" });
+ const executors = {
+ mapViewport: async (request: ViewportRequest) => projectViewportFromFrame(await this.fetchLatestFrameDocument(), request),
+ objectsViewport: async (request: ViewportRequest) => projectObjectsViewportFromFrame(await this.fetchLatestFrameDocument(), request),
+ gravityViewport: async (request: ViewportRequest) => projectGravityViewportFromFrame(await this.fetchLatestFrameDocument(), request),
+ selectedObject: async (request: SelectedObjectRequest) => projectSelectedObjectFromFrame(await this.fetchLatestFrameDocument(), request),
+ inventory: async (request: SelectedObjectRequest) => projectInventoryFromFrame(await this.fetchLatestFrameDocument(), request),
+ daemonHealth: async () => projectDaemonHealth(await this.fetchDaemonHealthDocument()),
+ authorityStatus: async () => projectAuthorityStatus(await this.fetchAuthorityPolicyDocument()),
+ starbridgeSession: async () => projectStarbridgeSessionSummary(await this.fetchStarbridgeSessionSummaryDocument()),
+ };
+ this.queries = createAetheriaRuntimeRtsQueryHandles(
+ executors,
+ this.queryVerse.context.routeHint,
+ {
+ objectsViewport: CultMesh.pollingQueryWatcher(executors.objectsViewport, { intervalMs: 50 }),
+ gravityViewport: CultMesh.pollingQueryWatcher(executors.gravityViewport, { intervalMs: 50 }),
+ selectedObject: CultMesh.pollingQueryWatcher(executors.selectedObject, { intervalMs: 50 }),
+ inventory: CultMesh.pollingQueryWatcher(executors.inventory, { intervalMs: 50 }),
+ daemonHealth: CultMesh.pollingQueryWatcher(executors.daemonHealth, { intervalMs: 250 }),
+ authorityStatus: CultMesh.pollingQueryWatcher(executors.authorityStatus, { intervalMs: 250 }),
+ starbridgeSession: CultMesh.pollingQueryWatcher(executors.starbridgeSession, { intervalMs: 250 }),
+ },
+ );
+ this.statePointers = createAetheriaRuntimeRtsStatePointers(
+ this.queryVerse.context.routeHint,
+ {
+ daemonFrame: async () => this.fetchLatestFrameDocument(),
+ daemonHealth: async () => this.fetchDaemonHealthDocument(),
+ authorityPolicy: async () => this.fetchAuthorityPolicyDocument(),
+ starbridgeSession: async () => this.fetchStarbridgeSessionSummaryDocument(),
+ },
+ );
+ this.operations = createAetheriaRuntimeRtsOperationHandles(
+ (commandId, issuedAtUtc, command, context) =>
+ this.sendCommandDocument(commandId, issuedAtUtc, command, context),
+ );
+ this.aetheria = createAetheriaRuntimeRtsVerseFacade(
+ this.queryVerse.context,
+ this.commandVerse.context,
+ this.queries,
+ this.operations,
+ this.statePointers,
+ );
+ }
+
+ private readonly publications: AetheriaPublicationReader;
+ private readonly queries: ReturnType;
+ private readonly statePointers: AetheriaRuntimeRtsStatePointers;
+ private readonly operations: AetheriaRuntimeRtsOperationHandles;
+ private readonly aetheria: AetheriaRuntimeRtsVerseFacade;
+
+ public async close(): Promise {
+ await this.publications.close?.();
+ this.#peer?.close();
+ this.#peer = null;
+ }
+
+ public async waitForFrame(timeoutMs: number): Promise {
+ const started = Date.now();
+ let lastError = "";
+ while (Date.now() - started < timeoutMs) {
+ try {
+ await this.mapViewport(CultMesh.viewportRequest(CultMesh.rectFromBounds(-1000, -1000, 1000, 1000)));
+ return;
+ } catch (error) {
+ lastError = error instanceof Error ? error.message : String(error);
+ await delay(250);
+ }
+ }
+
+ throw new Error(`Timed out waiting for Aetheria CultMesh frame at ${this.endpoint}. ${lastError}`);
+ }
+
+ public async mapViewport(request: ViewportRequest): Promise {
+ return this.aetheria.zone().viewport.within(request);
+ }
+
+ public async objectsViewport(request: ViewportRequest): Promise {
+ return this.aetheria.zone().objects.visibleWithin(request);
+ }
+
+ public async gravityViewport(request: ViewportRequest): Promise {
+ return this.aetheria.zone().gravity.within(request);
+ }
+
+ public async selectedObject(request: SelectedObjectRequest): Promise {
+ return this.aetheria.selectedObject(request.entityIndex);
+ }
+
+ public async inventory(request: SelectedObjectRequest): Promise {
+ return this.aetheria.inventory(request.entityIndex);
+ }
+
+ public async daemonHealth(): Promise {
+ return this.aetheria.daemon.health();
+ }
+
+ public async authorityStatus(): Promise {
+ return this.aetheria.daemon.authorityStatus();
+ }
+
+ public async starbridgeSession(): Promise {
+ return this.aetheria.daemon.starbridgeSession();
+ }
+
+ public projectionDiagnostics(): Readonly> {
+ return describeAetheriaRuntimeRtsQueryHandles(this.queries);
+ }
+
+ public liveFeedDiagnostics(): Readonly> {
+ return {
+ viewportFeed: describeAetheriaRuntimeRtsLiveFeedSurface(this.createViewportFeed()),
+ };
+ }
+
+ public surfaceCatalogDiagnostics(): AetheriaRuntimeRtsSurfaceCatalogDiagnostic {
+ const catalog = describeAetheriaRuntimeRtsSurfaceCatalog(this.queries, this.operations, this.statePointers);
+ return CultMesh.describeSurfaceCatalog(catalog.catalogId, [
+ ...catalog.surfaces,
+ CultMesh.describeSurface(this.createViewportFeed()),
+ ]);
+ }
+
+ public surfaceCatalogIndexDiagnostics(): CultMeshSurfaceCatalogIndexDiagnostic {
+ return CultMesh.surfaceCatalogIndex(this.surfaceCatalogDiagnostics());
+ }
+
+ public watchViewportFeed(
+ request: AetheriaRuntimeViewportFeedRequest,
+ callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void,
+ ): () => void {
+ return this.createViewportFeed().watch(request, this.queryContext(), callback);
+ }
+
+ public async setMoveVector(request: RtsSetMoveVectorRequest): Promise {
+ return this.aetheria.entity(request.actorEntityKey).pilot.move(
+ CultMesh.vec2(request.directionX, request.directionY),
+ {
+ scalar: request.scalar,
+ observedFrameId: request.observedFrameId,
+ },
+ );
+ }
+
+ public async setTarget(request: RtsSetTargetRequest): Promise {
+ return this.aetheria.entity(request.actorEntityKey).pilot.target(request.targetEntityKey, {
+ observedFrameId: request.observedFrameId,
+ });
+ }
+
+ private async fetchLatestFrameDocument(): Promise {
+ return this.publications.readDaemonFrame();
+ }
+
+ private async fetchDaemonHealthDocument(): Promise {
+ return this.publications.readDaemonHealth();
+ }
+
+ private async fetchAuthorityPolicyDocument(): Promise {
+ return this.publications.readAuthorityPolicy();
+ }
+
+ private async fetchStarbridgeSessionSummaryDocument(): Promise {
+ return this.publications.readStarbridgeSessionSummary();
+ }
+
+ private createViewportFeed() {
+ return CultMesh.liveFeed(
+ "gamecult.aetheria.rts.viewport_feed.v1",
+ async (request, context) => {
+ const objects = await this.queries.objectsViewport.execute(request.viewport, context);
+ return this.createViewportFeedSnapshot(request, objects);
+ },
+ {
+ sources: [
+ ...this.queries.objectsViewport.sources,
+ ...this.queries.gravityViewport.sources,
+ ...this.queries.daemonHealth.sources,
+ ...this.queries.authorityStatus.sources,
+ ...this.queries.starbridgeSession.sources,
+ ],
+ routeHint: this.queryVerse.context.routeHint,
+ watchFeed: (request, context, callback) => {
+ let disposed = false;
+ let sampling = false;
+ const unsubscribe = this.queries.objectsViewport.watch(request.viewport, context, objects => {
+ if (disposed || sampling) {
+ return;
+ }
+
+ sampling = true;
+ void this.createViewportFeedSnapshot(request, objects)
+ .then(snapshot => {
+ if (!disposed) {
+ callback(snapshot);
+ }
+ })
+ .finally(() => {
+ sampling = false;
+ });
+ });
+
+ return () => {
+ disposed = true;
+ unsubscribe();
+ };
+ },
+ },
+ );
+ }
+
+ private async createViewportFeedSnapshot(
+ request: AetheriaRuntimeViewportFeedRequest,
+ objects: ObjectsViewportResponse,
+ ): Promise {
+ const startedAt = performance.now();
+ const selectedEntityIndex = request.selectedEntityIndex ?? -1;
+ const [
+ gravity,
+ daemonHealth,
+ authorityStatus,
+ starbridgeSession,
+ selectedObject,
+ inventory,
+ ] = await Promise.all([
+ this.aetheria.zone().gravity.within(request.viewport),
+ this.aetheria.daemon.health(),
+ this.aetheria.daemon.authorityStatus(),
+ this.aetheria.daemon.starbridgeSession(),
+ selectedEntityIndex >= 0
+ ? this.aetheria.selectedObject(selectedEntityIndex)
+ : Promise.resolve(null),
+ selectedEntityIndex >= 0
+ ? this.aetheria.inventory(selectedEntityIndex)
+ : Promise.resolve(null),
+ ]);
+
+ return {
+ viewport: composeViewport(objects, gravity),
+ selectedObject,
+ inventory,
+ daemonHealth,
+ authorityStatus,
+ starbridgeSession,
+ receivedAtUtc: new Date().toISOString(),
+ sampleMs: performance.now() - startedAt,
+ };
+ }
+
+ private queryContext(): CultMeshQueryContext {
+ return this.queryVerse.queryContext();
+ }
+
+ private async sendCommandDocument(
+ commandId: string,
+ issuedAtUtc: string,
+ command: unknown[],
+ context: CultMeshOperationContext,
+ ): Promise {
+ const message: CultNetDocumentPutRawMessage = {
+ schemaVersion: "cultnet.document_put_raw.v0",
+ messageId: commandId,
+ document: {
+ schemaId: AetheriaRtsSchemas.daemonCommand,
+ recordKey: `daemon:commands:${stableToken(commandId)}:${AetheriaRtsSchemas.daemonCommand}`,
+ storedAt: issuedAtUtc,
+ payloadEncoding: "messagepack",
+ payload: encode(command),
+ sourceRuntimeId: context.runtimeId,
+ sourceRole: "rts-client",
+ tags: ["aetheria-rts"],
+ },
+ };
+ const peer = await this.peer();
+ peer.send(message);
+ await delay(80);
+ }
+
+ private async peer(): Promise {
+ if (this.#peer)
+ return this.#peer;
+ this.#peer = await CultMesh.createRudpPeer(this.runtimeId, connectionId, this.endpoint, {
+ connectTimeoutMs: 2000,
+ maxFragmentBytes: 1200,
+ maxPendingReliablePackets: 512,
+ });
+ this.#peer.on("close", () => {
+ this.#peer = null;
+ });
+ return this.#peer;
+ }
+}
+
+function stableToken(value: string): string {
+ return value
+ .replace(/[^a-zA-Z0-9]+/gu, "-")
+ .replace(/^-+|-+$/gu, "")
+ .replace(/--+/gu, "-")
+ .toLowerCase() || "empty";
+}
+
+function composeViewport(
+ objects: ObjectsViewportResponse,
+ gravity: GravityViewportResponse,
+): ViewportResponse {
+ return {
+ schema: "gamecult.aetheria.rts_viewport.v1",
+ frameId: objects.frameId,
+ publishedAtUtc: objects.publishedAtUtc,
+ simulationTimeSeconds: objects.simulationTimeSeconds,
+ runId: objects.runId,
+ zoneIndex: objects.zoneIndex,
+ zoneName: objects.zoneName,
+ currentEntityKey: objects.currentEntityKey,
+ viewport: objects.viewport,
+ controlledEntityIndices: objects.controlledEntityIndices,
+ objects: objects.objects,
+ gravityInfluences: gravity.gravityInfluences,
+ bodies: gravity.bodies,
+ };
+}
+
+function delay(milliseconds: number): Promise {
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
+}
diff --git a/Aetheria.Rts.Web/Electron/aetheria-local-publication-reader.ts b/Aetheria.Rts.Web/Electron/aetheria-local-publication-reader.ts
new file mode 100644
index 000000000..05f594a23
--- /dev/null
+++ b/Aetheria.Rts.Web/Electron/aetheria-local-publication-reader.ts
@@ -0,0 +1,36 @@
+import { decode } from "@msgpack/msgpack";
+import { SingleFileMessagePackBackingStore } from "cultcache-ts";
+
+export class AetheriaLocalPublicationReader {
+ public constructor(private readonly statePath: string) {}
+
+ public get statePathDescription(): string {
+ return this.statePath;
+ }
+
+ public readDaemonFrame(): Promise {
+ return this.readSingleDocument(`${this.statePath}.daemon.frame.cc`, "gamecult.aetheria.daemon_frame.v1");
+ }
+
+ public readDaemonHealth(): Promise {
+ return this.readSingleDocument(`${this.statePath}.daemon.health.cc`, "gamecult.aetheria.daemon_health.v1");
+ }
+
+ public readAuthorityPolicy(): Promise {
+ return this.readSingleDocument(`${this.statePath}.authority.policy.cc`, "gamecult.aetheria.verse_authority_policy.v1");
+ }
+
+ public readStarbridgeSessionSummary(): Promise {
+ return this.readSingleDocument(
+ `${this.statePath}.daemon.starbridge.session.cc`,
+ "gamecult.aetheria.starbridge_session_summary.v1");
+ }
+
+ private async readSingleDocument(path: string, schemaId: string): Promise {
+ const records = await new SingleFileMessagePackBackingStore(path).pullAll();
+ const record = records.find(candidate => candidate.schemaId === schemaId);
+ if (!record)
+ throw new Error(`Aetheria local publication ${path} did not contain schema ${schemaId}.`);
+ return decode(record.payload);
+ }
+}
diff --git a/Aetheria.Rts.Web/Electron/aetheria-remote-publication-reader.ts b/Aetheria.Rts.Web/Electron/aetheria-remote-publication-reader.ts
new file mode 100644
index 000000000..ae9d725ad
--- /dev/null
+++ b/Aetheria.Rts.Web/Electron/aetheria-remote-publication-reader.ts
@@ -0,0 +1,139 @@
+import { decode } from "@msgpack/msgpack";
+import { CultMesh } from "cultmesh-ts";
+import type { CultNetPeer } from "cultnet-ts";
+import type {
+ CultNetErrorMessage,
+ CultNetSnapshotResponseRawMessage,
+} from "cultnet-ts";
+
+const connectionId = 0x43554c54;
+
+export class AetheriaRemotePublicationReader {
+ #peer: CultNetPeer | null = null;
+
+ public constructor(
+ private readonly endpoint: string,
+ private readonly runtimeId = "aetheria-rts-remote-reader",
+ private readonly timeoutMs = 2_500,
+ ) {}
+
+ public get statePathDescription(): string {
+ return this.endpoint;
+ }
+
+ public async close(): Promise {
+ this.#peer?.close();
+ this.#peer = null;
+ }
+
+ public readDaemonFrame(): Promise {
+ return this.readSingleDocument(
+ "gamecult.aetheria.daemon_frame.v1",
+ "daemon:aetheria.frame.latest.v1");
+ }
+
+ public readDaemonHealth(): Promise {
+ return this.readSingleDocument(
+ "gamecult.aetheria.daemon_health.v1",
+ "daemon:aetheria.health.v1");
+ }
+
+ public readAuthorityPolicy(): Promise {
+ return this.readSingleDocument(
+ "gamecult.aetheria.verse_authority_policy.v1",
+ "global:aetheria.verse_authority_policy.v1");
+ }
+
+ public readStarbridgeSessionSummary(): Promise {
+ return this.readSingleDocument(
+ "gamecult.aetheria.starbridge_session_summary.v1",
+ "daemon:aetheria.starbridge.session.latest.v1");
+ }
+
+ private async readSingleDocument(schemaId: string, recordKey: string): Promise {
+ const response = await this.requestSnapshot([recordKey]);
+ const document = response.documents.find(candidate =>
+ candidate.recordKey === recordKey);
+ if (!document) {
+ throw new Error(`Aetheria remote publication ${this.endpoint} did not return ${schemaId} at ${recordKey}.`);
+ }
+
+ return decode(toBytes(document.payload));
+ }
+
+ private async requestSnapshot(
+ recordKeys: readonly string[],
+ ): Promise {
+ const peer = await this.peer();
+ const messageId = `${this.runtimeId}:snapshot:${Date.now()}:${Math.random().toString(16).slice(2)}`;
+
+ return new Promise((resolve, reject) => {
+ const cleanup = (): void => {
+ clearTimeout(timer);
+ peer.off("message", onMessage);
+ peer.off("invalidMessage", onInvalidMessage);
+ peer.off("error", onError);
+ peer.off("close", onClose);
+ };
+ const rejectWith = (error: Error): void => {
+ cleanup();
+ reject(error);
+ };
+ const onMessage = (message: unknown): void => {
+ const response = message as CultNetSnapshotResponseRawMessage | CultNetErrorMessage;
+ if (response.schemaVersion === "cultnet.error.v0") {
+ cleanup();
+ reject(new Error(response.error));
+ return;
+ }
+
+ if (response.schemaVersion !== "cultnet.snapshot_response_raw.v0" ||
+ response.messageId !== messageId) {
+ return;
+ }
+
+ cleanup();
+ resolve(response);
+ };
+ const onInvalidMessage = (error: Error): void => rejectWith(error);
+ const onError = (error: Error): void => rejectWith(error);
+ const onClose = (): void => rejectWith(new Error("CultMesh peer closed before snapshot response."));
+ const timer = setTimeout(
+ () => rejectWith(new Error(`Timed out waiting for Aetheria remote snapshot ${messageId}.`)),
+ this.timeoutMs);
+
+ peer.on("message", onMessage);
+ peer.on("invalidMessage", onInvalidMessage);
+ peer.on("error", onError);
+ peer.on("close", onClose);
+ peer.sendSnapshotRequest({
+ schemaVersion: "cultnet.snapshot_request.v0",
+ messageId,
+ recordKeys: [...recordKeys],
+ });
+ });
+ }
+
+ private async peer(): Promise {
+ if (this.#peer)
+ return this.#peer;
+
+ this.#peer = await CultMesh.createRudpPeer(this.runtimeId, connectionId, this.endpoint, {
+ connectTimeoutMs: 2_000,
+ maxFragmentBytes: 1200,
+ maxPendingReliablePackets: 512,
+ });
+ this.#peer.on("close", () => {
+ this.#peer = null;
+ });
+ return this.#peer;
+ }
+}
+
+function toBytes(value: unknown): Uint8Array {
+ if (value instanceof Uint8Array)
+ return value;
+ if (Array.isArray(value))
+ return Uint8Array.from(value);
+ throw new Error("CultNet raw document payload was not binary.");
+}
diff --git a/Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts b/Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts
new file mode 100644
index 000000000..7e1ae94f8
--- /dev/null
+++ b/Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts
@@ -0,0 +1,50 @@
+import type { CultMeshViewportRequest } from "cultmesh-ts";
+
+export {
+ AetheriaRtsIpcChannels,
+ AetheriaRtsSchemas,
+ AetheriaRuntimeDaemonCommandKinds,
+ createAetheriaRuntimeRtsOperationHandles,
+ createAetheriaRuntimeRtsQueryHandles,
+ createAetheriaRuntimeRtsStatePointers,
+ createAetheriaRuntimeRtsVerseFacade,
+ describeAetheriaRuntimeRtsLiveFeedSurface,
+ describeAetheriaRuntimeRtsQueryHandles,
+ describeAetheriaRuntimeRtsSurfaceCatalog,
+} from "./aetheria-rts-generated-bindings.js";
+
+export type {
+ AetheriaRuntimeDaemonCommandReceipt,
+ AetheriaRuntimeRtsLiveFeedDiagnostic,
+ AetheriaRuntimeRtsOperationHandles,
+ AetheriaRuntimeRtsProjectionDiagnostic,
+ AetheriaRuntimeRtsStatePointerResolvers,
+ AetheriaRuntimeRtsStatePointers,
+ AetheriaRuntimeRtsSurfaceCatalogDiagnostic,
+ AetheriaRuntimeRtsVerseFacade,
+ AetheriaRuntimeSelectedObjectRequest as SelectedObjectRequest,
+ AetheriaRuntimeSetMoveVectorRequest as RtsSetMoveVectorRequest,
+ AetheriaRuntimeSetTargetRequest as RtsSetTargetRequest,
+ AetheriaRuntimeViewportFeedRequest,
+ AetheriaRuntimeViewportFeedSnapshot,
+ AuthorityRuleProjection,
+ AuthorityStatusProjection,
+ BodyView,
+ DaemonHealthProjection,
+ EntityStatus,
+ GravityInfluence,
+ GravityViewportResponse,
+ InventoryItem,
+ InventoryProjection,
+ ObjectsViewportResponse,
+ SelectedObjectProjection,
+ StarbridgeBaseStatusProjection,
+ StarbridgeRuntimeRoleProjection,
+ StarbridgeSessionProjection,
+ StarbridgeStationStockItemProjection,
+ StarbridgeWaveForecastProjection,
+ ViewObject,
+ ViewportResponse,
+} from "./aetheria-rts-generated-bindings.js";
+
+export type ViewportRequest = CultMeshViewportRequest;
diff --git a/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts b/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts
new file mode 100644
index 000000000..20f90043c
--- /dev/null
+++ b/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts
@@ -0,0 +1,2132 @@
+//
+// Generated by scripts/generate-rts-bindings.mjs from Aetheria C# MessagePack documents.
+
+import { CultMesh } from "cultmesh-ts";
+import type { CultMeshLiveFeedDiagnostic, CultMeshOperationContext, CultMeshOperationReceipt, CultMeshProjectionSource, CultMeshQueryContext, CultMeshQuerySurfaceDiagnostic, CultMeshQueryWatcher, CultMeshRouteHint, CultMeshSurfaceCatalogDiagnostic, CultMeshSurfaceCatalogIndexDiagnostic, CultMeshUnsubscribe, CultMeshVerseContext, CultMeshViewportRequest } from "cultmesh-ts";
+import type { IpcMain } from "electron";
+
+export const AetheriaRtsSchemas = {
+ daemonCommand: "gamecult.aetheria.daemon_command.v1",
+ eveCommand: "gamecult.eve.command.v1",
+ daemonFrame: "gamecult.aetheria.daemon_frame.v1",
+ daemonHealth: "gamecult.aetheria.daemon_health.v1",
+ rtsViewport: "gamecult.aetheria.rts_viewport.v1",
+ objectsViewport: "gamecult.aetheria.objects_viewport.v1",
+ gravityViewport: "gamecult.aetheria.gravity_viewport.v1",
+ currentZone: "gamecult.aetheria.current_zone.v1",
+ currentEntity: "gamecult.aetheria.current_entity.v1",
+ currentDocking: "gamecult.aetheria.current_docking.v1",
+ stationRefit: "gamecult.aetheria.station_refit.v1",
+ sectorMap: "gamecult.aetheria.sector_map.v1",
+ zoneRender: "gamecult.aetheria.zone_render.v1",
+ selectedObject: "gamecult.aetheria.selected_object.v1",
+ inventory: "gamecult.aetheria.inventory.v1",
+ starbridgeScenario: "gamecult.aetheria.starbridge_scenario.v1",
+ starbridgeSession: "gamecult.aetheria.starbridge_session.v1",
+ starbridgeSessionSummary: "gamecult.aetheria.starbridge_session_summary.v1",
+ verseAuthorityPolicy: "gamecult.aetheria.verse_authority_policy.v1",
+ authorityLease: "gamecult.aetheria.authority_lease.v1",
+} as const;
+
+export const AetheriaRuntimeDaemonCommandKinds = {
+ none: 0,
+ ping: 1,
+ setTarget: 2,
+ clearTarget: 3,
+ targetNearest: 4,
+ targetNext: 5,
+ targetPrevious: 6,
+ targetReticle: 7,
+ setMoveVector: 8,
+ setLookDirection: 9,
+ setTractorPower: 10,
+ fireWeaponGroup: 11,
+ setWeaponGroupActive: 12,
+ setWeaponGroupMembership: 13,
+ setBehaviorActive: 14,
+ activateConsumable: 15,
+ sensorPing: 16,
+ setItemEnabled: 17,
+ setItemOverrideShutdown: 18,
+ setThermotoggleTargetTemperature: 19,
+ pickUpLoot: 20,
+ restoreLoadout: 21,
+ dock: 22,
+ dockNearest: 23,
+ undock: 24,
+ interact: 25,
+ setDockedCurrentShip: 26,
+ towToStation: 27,
+ enterWormhole: 28,
+ tradePurchase: 29,
+ transferCargoItem: 30,
+ equipItem: 31,
+ storeItem: 32,
+ toggleHullConductivity: 33,
+ setEntityName: 34,
+ destroyEntity: 35,
+ setOverrideShutdown: 36,
+ setHeatsinksEnabled: 37,
+ setShutdownPerformance: 38,
+ toggleShieldEnabled: 39,
+} as const;
+
+export const AetheriaRuntimeEveCommandKinds = {
+ unknown: 0,
+ catalogRefresh: 1,
+ operationsRefresh: 2,
+ playerSettingsRefresh: 3,
+ setPlayerName: 4,
+ cycleTemperatureUnit: 5,
+ decrementSignificantDigits: 6,
+ incrementSignificantDigits: 7,
+ cycleNebulaQuality: 8,
+ toggleShowAsteroidsInMinimap: 9,
+ inputSettingsRefresh: 10,
+ beginInputCapture: 11,
+ cancelInputCapture: 12,
+ setBindingOverride: 13,
+ toggleActionBar: 14,
+ setActionBarEnabled: 15,
+ saveLoadoutTemplate: 16,
+ verseHostRefresh: 17,
+ cycleVerseHostVisibility: 18,
+ tradeValuePolicyRefresh: 19,
+ setTradeValueQualityMinimum: 20,
+ setTradeValueQualityMaximum: 21,
+ setTradeValueQualityExponent: 22,
+ setTradeValueTierQuality: 23,
+} as const;
+
+export const AetheriaRtsIpcChannels = {
+ mapViewport: "aetheria-rts:map-viewport",
+ objectsViewport: "aetheria-rts:objects-viewport",
+ gravityViewport: "aetheria-rts:gravity-viewport",
+ viewportFeed: "aetheria-rts:viewport-feed",
+ viewportFeedStop: "aetheria-rts:viewport-feed-stop",
+ viewportFeedUpdate: "aetheria-rts:viewport-feed-update",
+ selectedObject: "aetheria-rts:selected-object",
+ inventory: "aetheria-rts:inventory",
+ daemonHealth: "aetheria-rts:daemon-health",
+ authorityStatus: "aetheria-rts:authority-status",
+ starbridgeSession: "aetheria-rts:starbridge-session",
+ setMoveVector: "aetheria-rts:set-move-vector",
+ setTarget: "aetheria-rts:set-target",
+ surfaceCatalog: "aetheria-rts:surface-catalog",
+ surfaceCatalogIndex: "aetheria-rts:surface-catalog-index",
+ health: "aetheria-rts:health",
+} as const;
+
+export type AetheriaRuntimeDaemonCommandDocumentSlot =
+ | "schema"
+ | "commandId"
+ | "clientId"
+ | "issuedAtUtc"
+ | "sessionId"
+ | "observedFrameId"
+ | "kind"
+ | "actorEntityKey"
+ | "targetEntityKey"
+ | "targetZoneIndex"
+ | "equipmentIndex"
+ | "behaviorIndex"
+ | "weaponGroup"
+ | "positionX"
+ | "positionY"
+ | "positionZ"
+ | "directionX"
+ | "directionY"
+ | "scalarValue"
+ | "textValue"
+ | "cargoTransfer"
+ | "tradePurchase"
+ | "lootPickup"
+ | "loadoutRestore"
+ | "equipmentTransfer"
+ | "storeItem"
+ | "authorRuntimeId"
+ | "subjectKey"
+ | "claimKind";
+
+export const aetheriaRuntimeDaemonCommandDocumentSlots: Record = {
+ schema: 0,
+ commandId: 1,
+ clientId: 2,
+ issuedAtUtc: 3,
+ sessionId: 4,
+ observedFrameId: 5,
+ kind: 6,
+ actorEntityKey: 7,
+ targetEntityKey: 8,
+ targetZoneIndex: 9,
+ equipmentIndex: 10,
+ behaviorIndex: 11,
+ weaponGroup: 12,
+ positionX: 13,
+ positionY: 14,
+ positionZ: 15,
+ directionX: 16,
+ directionY: 17,
+ scalarValue: 18,
+ textValue: 19,
+ cargoTransfer: 21,
+ tradePurchase: 22,
+ lootPickup: 23,
+ loadoutRestore: 24,
+ equipmentTransfer: 25,
+ storeItem: 26,
+ authorRuntimeId: 27,
+ subjectKey: 28,
+ claimKind: 29,
+};
+
+export type AetheriaRuntimeEveCommandDocumentSlot =
+ | "schema"
+ | "commandId"
+ | "providerId"
+ | "surfaceId"
+ | "command"
+ | "issuedAtUtc"
+ | "clientId"
+ | "playerSettings"
+ | "loadoutTemplate"
+ | "inputSettings"
+ | "tradeValuePolicy"
+ | "operationId"
+ | "operationSchemaId"
+ | "operationRouteKind"
+ | "operationRouteDescription"
+ | "operationIdempotencyKey"
+ | "payload"
+ | "operation";
+
+export const aetheriaRuntimeEveCommandDocumentSlots: Record = {
+ schema: 0,
+ commandId: 1,
+ providerId: 2,
+ surfaceId: 3,
+ command: 4,
+ issuedAtUtc: 5,
+ clientId: 6,
+ playerSettings: 7,
+ loadoutTemplate: 8,
+ inputSettings: 9,
+ tradeValuePolicy: 10,
+ operationId: 11,
+ operationSchemaId: 12,
+ operationRouteKind: 13,
+ operationRouteDescription: 14,
+ operationIdempotencyKey: 15,
+ payload: 16,
+ operation: 17,
+};
+
+export type AetheriaRuntimeDaemonFrameDocumentSlot =
+ | "schema"
+ | "daemonId"
+ | "sessionId"
+ | "frameId"
+ | "publishedAtUtc"
+ | "simulationTimeSeconds"
+ | "fixedDeltaSeconds"
+ | "isAuthoritative"
+ | "stateSource"
+ | "run"
+ | "capabilities"
+ | "appliedCommandIds"
+ | "rejectedCommandIds"
+ | "accountedCommandIds"
+ | "cumulativeAppliedCommandIds"
+ | "cumulativeRejectedCommandIds"
+ | "importedFactIds"
+ | "rejectedImportedFactIds"
+ | "duplicateImportedFactIds"
+ | "cumulativeImportedFactIds"
+ | "cumulativeRejectedImportedFactIds";
+
+export const aetheriaRuntimeDaemonFrameDocumentSlots: Record = {
+ schema: 0,
+ daemonId: 1,
+ sessionId: 2,
+ frameId: 3,
+ publishedAtUtc: 4,
+ simulationTimeSeconds: 5,
+ fixedDeltaSeconds: 6,
+ isAuthoritative: 7,
+ stateSource: 8,
+ run: 9,
+ capabilities: 10,
+ appliedCommandIds: 11,
+ rejectedCommandIds: 12,
+ accountedCommandIds: 13,
+ cumulativeAppliedCommandIds: 14,
+ cumulativeRejectedCommandIds: 15,
+ importedFactIds: 16,
+ rejectedImportedFactIds: 17,
+ duplicateImportedFactIds: 18,
+ cumulativeImportedFactIds: 19,
+ cumulativeRejectedImportedFactIds: 20,
+};
+
+export type AetheriaRuntimeDaemonHealthDocumentSlot =
+ | "schema"
+ | "daemonId"
+ | "verseId"
+ | "publishedAtUtc"
+ | "statePath"
+ | "frameId"
+ | "observedCommandCount"
+ | "appliedCommandCount"
+ | "rejectedCommandCount"
+ | "status"
+ | "publicationSource"
+ | "transport"
+ | "commandBoundaryPath";
+
+export const aetheriaRuntimeDaemonHealthDocumentSlots: Record = {
+ schema: 0,
+ daemonId: 1,
+ verseId: 2,
+ publishedAtUtc: 3,
+ statePath: 4,
+ frameId: 5,
+ observedCommandCount: 6,
+ appliedCommandCount: 7,
+ rejectedCommandCount: 8,
+ status: 9,
+ publicationSource: 10,
+ transport: 11,
+ commandBoundaryPath: 12,
+};
+
+export type AetheriaRuntimeRtsViewportDocumentSlot =
+ | "schema"
+ | "frameId"
+ | "publishedAtUtc"
+ | "simulationTimeSeconds"
+ | "runId"
+ | "zoneIndex"
+ | "zoneName"
+ | "currentEntityKey"
+ | "viewport"
+ | "controlledEntityIndices"
+ | "objects"
+ | "gravityInfluences"
+ | "bodies";
+
+export const aetheriaRuntimeRtsViewportDocumentSlots: Record = {
+ schema: 0,
+ frameId: 1,
+ publishedAtUtc: 2,
+ simulationTimeSeconds: 3,
+ runId: 4,
+ zoneIndex: 5,
+ zoneName: 6,
+ currentEntityKey: 7,
+ viewport: 8,
+ controlledEntityIndices: 9,
+ objects: 10,
+ gravityInfluences: 11,
+ bodies: 12,
+};
+
+export type AetheriaRuntimeObjectsViewportDocumentSlot =
+ | "schema"
+ | "frameId"
+ | "publishedAtUtc"
+ | "simulationTimeSeconds"
+ | "runId"
+ | "zoneIndex"
+ | "zoneName"
+ | "currentEntityKey"
+ | "viewport"
+ | "controlledEntityIndices"
+ | "objects";
+
+export const aetheriaRuntimeObjectsViewportDocumentSlots: Record = {
+ schema: 0,
+ frameId: 1,
+ publishedAtUtc: 2,
+ simulationTimeSeconds: 3,
+ runId: 4,
+ zoneIndex: 5,
+ zoneName: 6,
+ currentEntityKey: 7,
+ viewport: 8,
+ controlledEntityIndices: 9,
+ objects: 10,
+};
+
+export type AetheriaRuntimeGravityViewportDocumentSlot =
+ | "schema"
+ | "frameId"
+ | "publishedAtUtc"
+ | "simulationTimeSeconds"
+ | "runId"
+ | "zoneIndex"
+ | "zoneName"
+ | "viewport"
+ | "gravityInfluences"
+ | "bodies"
+ | "terrainRadius"
+ | "terrainDepth"
+ | "terrainDepthExponent"
+ | "terrainWaveFrequency";
+
+export const aetheriaRuntimeGravityViewportDocumentSlots: Record = {
+ schema: 0,
+ frameId: 1,
+ publishedAtUtc: 2,
+ simulationTimeSeconds: 3,
+ runId: 4,
+ zoneIndex: 5,
+ zoneName: 6,
+ viewport: 7,
+ gravityInfluences: 8,
+ bodies: 9,
+ terrainRadius: 10,
+ terrainDepth: 11,
+ terrainDepthExponent: 12,
+ terrainWaveFrequency: 13,
+};
+
+export type AetheriaRuntimeCurrentZoneDocumentSlot =
+ | "schema"
+ | "frameId"
+ | "publishedAtUtc"
+ | "simulationTimeSeconds"
+ | "runId"
+ | "zoneIndex"
+ | "zoneName"
+ | "positionX"
+ | "positionY"
+ | "currentEntityKey"
+ | "adjacentZoneIndices";
+
+export const aetheriaRuntimeCurrentZoneDocumentSlots: Record = {
+ schema: 0,
+ frameId: 1,
+ publishedAtUtc: 2,
+ simulationTimeSeconds: 3,
+ runId: 4,
+ zoneIndex: 5,
+ zoneName: 6,
+ positionX: 7,
+ positionY: 8,
+ currentEntityKey: 9,
+ adjacentZoneIndices: 10,
+};
+
+export type AetheriaRuntimeCurrentEntityDocumentSlot =
+ | "schema"
+ | "frameId"
+ | "publishedAtUtc"
+ | "simulationTimeSeconds"
+ | "runId"
+ | "zoneIndex"
+ | "entityKey"
+ | "entityIndex"
+ | "entity"
+ | "status"
+ | "inventory"
+ | "equipment"
+ | "cargo"
+ | "shutdownPerformance"
+ | "hud";
+
+export const aetheriaRuntimeCurrentEntityDocumentSlots: Record = {
+ schema: 0,
+ frameId: 1,
+ publishedAtUtc: 2,
+ simulationTimeSeconds: 3,
+ runId: 4,
+ zoneIndex: 5,
+ entityKey: 6,
+ entityIndex: 7,
+ entity: 8,
+ status: 9,
+ inventory: 10,
+ equipment: 11,
+ cargo: 12,
+ shutdownPerformance: 13,
+ hud: 14,
+};
+
+export type AetheriaRuntimeCurrentDockingDocumentSlot =
+ | "schema"
+ | "frameId"
+ | "publishedAtUtc"
+ | "simulationTimeSeconds"
+ | "runId"
+ | "zoneIndex"
+ | "currentEntityKey"
+ | "currentEntityIndex"
+ | "isDocked"
+ | "dockParentEntityKey"
+ | "dockParentEntityIndex"
+ | "dockingBayIndex"
+ | "dockParent"
+ | "dockParentOrbitKey"
+ | "dockParentParentOrbitKey"
+ | "dockParentParentBodyKey";
+
+export const aetheriaRuntimeCurrentDockingDocumentSlots: Record = {
+ schema: 0,
+ frameId: 1,
+ publishedAtUtc: 2,
+ simulationTimeSeconds: 3,
+ runId: 4,
+ zoneIndex: 5,
+ currentEntityKey: 6,
+ currentEntityIndex: 7,
+ isDocked: 8,
+ dockParentEntityKey: 9,
+ dockParentEntityIndex: 10,
+ dockingBayIndex: 11,
+ dockParent: 12,
+ dockParentOrbitKey: 13,
+ dockParentParentOrbitKey: 14,
+ dockParentParentBodyKey: 15,
+};
+
+export type AetheriaRuntimeStationRefitDocumentSlot =
+ | "schema"
+ | "frameId"
+ | "publishedAtUtc"
+ | "simulationTimeSeconds"
+ | "runId"
+ | "zoneIndex"
+ | "currentEntityKey"
+ | "currentEntityIndex"
+ | "isDocked"
+ | "dockParentEntityKey"
+ | "dockParentEntityIndex"
+ | "dockingBayIndex"
+ | "dockParent"
+ | "availableEntities"
+ | "credits"
+ | "stationStock"
+ | "dockingBays"
+ | "loadoutRestoreOptions"
+ | "cargoTargets";
+
+export const aetheriaRuntimeStationRefitDocumentSlots: Record = {
+ schema: 0,
+ frameId: 1,
+ publishedAtUtc: 2,
+ simulationTimeSeconds: 3,
+ runId: 4,
+ zoneIndex: 5,
+ currentEntityKey: 6,
+ currentEntityIndex: 7,
+ isDocked: 8,
+ dockParentEntityKey: 9,
+ dockParentEntityIndex: 10,
+ dockingBayIndex: 11,
+ dockParent: 12,
+ availableEntities: 13,
+ credits: 14,
+ stationStock: 15,
+ dockingBays: 16,
+ loadoutRestoreOptions: 17,
+ cargoTargets: 18,
+};
+
+export type AetheriaRuntimeStationRefitEntityOptionSlot =
+ | "entityKey"
+ | "entityIndex"
+ | "displayName"
+ | "kind"
+ | "isCurrentEntity"
+ | "isPlayerShip"
+ | "cargoBayCount"
+ | "dockingBayIndex"
+ | "hullItemKey"
+ | "cargoItems";
+
+export const aetheriaRuntimeStationRefitEntityOptionSlots: Record = {
+ entityKey: 0,
+ entityIndex: 1,
+ displayName: 2,
+ kind: 3,
+ isCurrentEntity: 4,
+ isPlayerShip: 5,
+ cargoBayCount: 6,
+ dockingBayIndex: 7,
+ hullItemKey: 8,
+ cargoItems: 9,
+};
+
+export type AetheriaRuntimeStationStockItemSlot =
+ | "itemKey"
+ | "quantity"
+ | "quality"
+ | "durability"
+ | "cargoBayIndex"
+ | "x"
+ | "y"
+ | "price"
+ | "canAfford"
+ | "ownedQuantity";
+
+export const aetheriaRuntimeStationStockItemSlots: Record = {
+ itemKey: 0,
+ quantity: 1,
+ quality: 2,
+ durability: 3,
+ cargoBayIndex: 4,
+ x: 5,
+ y: 6,
+ price: 7,
+ canAfford: 8,
+ ownedQuantity: 9,
+};
+
+export type AetheriaRuntimeSectorMapDocumentSlot =
+ | "schema"
+ | "frameId"
+ | "publishedAtUtc"
+ | "simulationTimeSeconds"
+ | "runId"
+ | "currentZoneIndex"
+ | "entranceZoneIndex"
+ | "exitZoneIndex"
+ | "discoveredZoneIndices"
+ | "zones"
+ | "links"
+ | "isTutorial"
+ | "generationSeed"
+ | "factionRelationships";
+
+export const aetheriaRuntimeSectorMapDocumentSlots: Record = {
+ schema: 0,
+ frameId: 1,
+ publishedAtUtc: 2,
+ simulationTimeSeconds: 3,
+ runId: 4,
+ currentZoneIndex: 5,
+ entranceZoneIndex: 6,
+ exitZoneIndex: 7,
+ discoveredZoneIndices: 8,
+ zones: 9,
+ links: 10,
+ isTutorial: 11,
+ generationSeed: 12,
+ factionRelationships: 13,
+};
+
+export type AetheriaRuntimeSectorMapZoneSlot =
+ | "zoneIndex"
+ | "name"
+ | "x"
+ | "y"
+ | "ownerFactionIndex"
+ | "factionIndices"
+ | "adjacentZoneIndices"
+ | "discovered"
+ | "current"
+ | "entrance"
+ | "exit";
+
+export const aetheriaRuntimeSectorMapZoneSlots: Record = {
+ zoneIndex: 0,
+ name: 1,
+ x: 2,
+ y: 3,
+ ownerFactionIndex: 4,
+ factionIndices: 5,
+ adjacentZoneIndices: 6,
+ discovered: 7,
+ current: 8,
+ entrance: 9,
+ exit: 10,
+};
+
+export type AetheriaRuntimeSectorMapLinkSlot =
+ | "fromZoneIndex"
+ | "toZoneIndex"
+ | "discovered";
+
+export const aetheriaRuntimeSectorMapLinkSlots: Record = {
+ fromZoneIndex: 0,
+ toZoneIndex: 1,
+ discovered: 2,
+};
+
+export type AetheriaRuntimeZoneRenderDocumentSlot =
+ | "schema"
+ | "frameId"
+ | "publishedAtUtc"
+ | "simulationTimeSeconds"
+ | "runId"
+ | "zoneIndex"
+ | "zoneName"
+ | "currentEntityKey"
+ | "zoneRenderRadius"
+ | "credits"
+ | "adjacentZones"
+ | "bodyPoses"
+ | "asteroidBeltPoses"
+ | "wormholeExits"
+ | "droppedPickups"
+ | "entityFacades"
+ | "orbits"
+ | "bodies";
+
+export const aetheriaRuntimeZoneRenderDocumentSlots: Record = {
+ schema: 0,
+ frameId: 1,
+ publishedAtUtc: 2,
+ simulationTimeSeconds: 3,
+ runId: 4,
+ zoneIndex: 5,
+ zoneName: 6,
+ currentEntityKey: 7,
+ zoneRenderRadius: 8,
+ credits: 9,
+ adjacentZones: 12,
+ bodyPoses: 13,
+ asteroidBeltPoses: 14,
+ wormholeExits: 15,
+ droppedPickups: 16,
+ entityFacades: 17,
+ orbits: 18,
+ bodies: 19,
+};
+
+export type AetheriaRuntimeZoneRenderBodyPoseSlot =
+ | "bodyKey"
+ | "orbitKey"
+ | "parentOrbitKey"
+ | "kind"
+ | "centerX"
+ | "centerZ"
+ | "parentCenterX"
+ | "parentCenterZ"
+ | "gravityWaveSpeed";
+
+export const aetheriaRuntimeZoneRenderBodyPoseSlots: Record = {
+ bodyKey: 0,
+ orbitKey: 1,
+ parentOrbitKey: 2,
+ kind: 3,
+ centerX: 4,
+ centerZ: 5,
+ parentCenterX: 6,
+ parentCenterZ: 7,
+ gravityWaveSpeed: 8,
+};
+
+export type AetheriaRuntimeZoneRenderAsteroidBeltPoseSlot =
+ | "bodyKey"
+ | "orbitKey"
+ | "centerX"
+ | "centerZ"
+ | "radius"
+ | "asteroidCount"
+ | "instancePoses";
+
+export const aetheriaRuntimeZoneRenderAsteroidBeltPoseSlots: Record = {
+ bodyKey: 0,
+ orbitKey: 1,
+ centerX: 2,
+ centerZ: 3,
+ radius: 4,
+ asteroidCount: 5,
+ instancePoses: 6,
+};
+
+export type AetheriaRuntimeSelectedObjectDocumentSlot =
+ | "schema"
+ | "frameId"
+ | "runId"
+ | "zoneIndex"
+ | "entityIndex"
+ | "selected";
+
+export const aetheriaRuntimeSelectedObjectDocumentSlots: Record = {
+ schema: 0,
+ frameId: 1,
+ runId: 2,
+ zoneIndex: 3,
+ entityIndex: 4,
+ selected: 5,
+};
+
+export type AetheriaRuntimeInventoryDocumentSlot =
+ | "schema"
+ | "frameId"
+ | "runId"
+ | "zoneIndex"
+ | "entityIndex"
+ | "entityKey"
+ | "items"
+ | "equipment"
+ | "cargo";
+
+export const aetheriaRuntimeInventoryDocumentSlots: Record = {
+ schema: 0,
+ frameId: 1,
+ runId: 2,
+ zoneIndex: 3,
+ entityIndex: 4,
+ entityKey: 5,
+ items: 6,
+ equipment: 7,
+ cargo: 8,
+};
+
+export type AetheriaRuntimeRunCheckpointCommitSlot =
+ | "runId"
+ | "isTutorial"
+ | "entranceZoneIndex"
+ | "exitZoneIndex"
+ | "currentZoneIndex"
+ | "discoveredZoneIndices"
+ | "zones"
+ | "factionRelationships"
+ | "generationSeed"
+ | "currentEntityKey"
+ | "credits";
+
+export const aetheriaRuntimeRunCheckpointCommitSlots: Record = {
+ runId: 0,
+ isTutorial: 1,
+ entranceZoneIndex: 2,
+ exitZoneIndex: 3,
+ currentZoneIndex: 4,
+ discoveredZoneIndices: 5,
+ zones: 6,
+ factionRelationships: 8,
+ generationSeed: 9,
+ currentEntityKey: 10,
+ credits: 11,
+};
+
+export type AetheriaRuntimeZoneSnapshotCommitSlot =
+ | "zoneIndex"
+ | "name"
+ | "positionX"
+ | "positionY"
+ | "adjacentZoneIndices"
+ | "factionIndices"
+ | "ownerFactionIndex"
+ | "entities"
+ | "orbits"
+ | "bodies"
+ | "droppedPickups"
+ | "gravityTerrainRadius"
+ | "gravityTerrainDepth"
+ | "gravityTerrainDepthExponent"
+ | "gravityTerrainBoundaryFog"
+ | "gravityTerrainWaveFrequency"
+ | "simulationTimeSeconds";
+
+export const aetheriaRuntimeZoneSnapshotCommitSlots: Record = {
+ zoneIndex: 0,
+ name: 1,
+ positionX: 2,
+ positionY: 3,
+ adjacentZoneIndices: 4,
+ factionIndices: 5,
+ ownerFactionIndex: 6,
+ entities: 7,
+ orbits: 8,
+ bodies: 9,
+ droppedPickups: 10,
+ gravityTerrainRadius: 11,
+ gravityTerrainDepth: 12,
+ gravityTerrainDepthExponent: 13,
+ gravityTerrainBoundaryFog: 14,
+ gravityTerrainWaveFrequency: 15,
+ simulationTimeSeconds: 16,
+};
+
+export type AetheriaRuntimeEntitySnapshotCommitSlot =
+ | "entityIndex"
+ | "name"
+ | "kind"
+ | "positionX"
+ | "positionY"
+ | "positionZ"
+ | "directionX"
+ | "directionY"
+ | "equipment"
+ | "cargoBays"
+ | "dockingBays"
+ | "childEntityIndices"
+ | "weaponGroups"
+ | "statGrids"
+ | "velocityX"
+ | "velocityY"
+ | "targetEntityIndex"
+ | "isActive"
+ | "heatsinksEnabled"
+ | "overrideShutdown"
+ | "tractorPower"
+ | "heatstroke"
+ | "hypothermia"
+ | "activeConsumables"
+ | "behaviorProgress"
+ | "weaponStates"
+ | "behaviorStates"
+ | "cargoContents"
+ | "dockingBayContents"
+ | "dockingBayAssignments"
+ | "visibility"
+ | "visibilitySourceCount"
+ | "contacts"
+ | "hullItemKey"
+ | "factionKey"
+ | "shutdownPerformance";
+
+export const aetheriaRuntimeEntitySnapshotCommitSlots: Record = {
+ entityIndex: 0,
+ name: 1,
+ kind: 2,
+ positionX: 3,
+ positionY: 4,
+ positionZ: 5,
+ directionX: 6,
+ directionY: 7,
+ equipment: 10,
+ cargoBays: 11,
+ dockingBays: 12,
+ childEntityIndices: 13,
+ weaponGroups: 14,
+ statGrids: 15,
+ velocityX: 16,
+ velocityY: 17,
+ targetEntityIndex: 18,
+ isActive: 19,
+ heatsinksEnabled: 20,
+ overrideShutdown: 21,
+ tractorPower: 22,
+ heatstroke: 23,
+ hypothermia: 24,
+ activeConsumables: 25,
+ behaviorProgress: 26,
+ weaponStates: 27,
+ behaviorStates: 28,
+ cargoContents: 29,
+ dockingBayContents: 30,
+ dockingBayAssignments: 31,
+ visibility: 32,
+ visibilitySourceCount: 33,
+ contacts: 34,
+ hullItemKey: 35,
+ factionKey: 36,
+ shutdownPerformance: 37,
+};
+
+export type AetheriaRuntimeBodySnapshotCommitSlot =
+ | "kind"
+ | "name"
+ | "mass"
+ | "resources"
+ | "bodyRadiusMultiplier"
+ | "gravityRadiusMultiplier"
+ | "gravityDepthMultiplier"
+ | "gravityDepthExponent"
+ | "asteroids"
+ | "gasGiantVisual"
+ | "sunVisual"
+ | "bodyKey"
+ | "orbitKey"
+ | "gravityInfluenceCenterX"
+ | "gravityInfluenceCenterZ"
+ | "gravityInfluenceRadius"
+ | "gravityWellDepth"
+ | "gravityWaveRadius"
+ | "gravityWaveDepth"
+ | "gravityWaveSpeed";
+
+export const aetheriaRuntimeBodySnapshotCommitSlots: Record = {
+ kind: 1,
+ name: 2,
+ mass: 4,
+ resources: 5,
+ bodyRadiusMultiplier: 6,
+ gravityRadiusMultiplier: 7,
+ gravityDepthMultiplier: 8,
+ gravityDepthExponent: 9,
+ asteroids: 10,
+ gasGiantVisual: 11,
+ sunVisual: 12,
+ bodyKey: 13,
+ orbitKey: 14,
+ gravityInfluenceCenterX: 15,
+ gravityInfluenceCenterZ: 16,
+ gravityInfluenceRadius: 17,
+ gravityWellDepth: 18,
+ gravityWaveRadius: 19,
+ gravityWaveDepth: 20,
+ gravityWaveSpeed: 21,
+};
+
+export type AetheriaRuntimeEntityStatGridCommitSlot =
+ | "name"
+ | "width"
+ | "height"
+ | "values";
+
+export const aetheriaRuntimeEntityStatGridCommitSlots: Record = {
+ name: 0,
+ width: 1,
+ height: 2,
+ values: 3,
+};
+
+export type AetheriaRuntimeCargoBayLoadoutCommitSlot =
+ | "items";
+
+export const aetheriaRuntimeCargoBayLoadoutCommitSlots: Record = {
+ items: 0,
+};
+
+export type AetheriaRuntimeLoadoutItemSlotCommitSlot =
+ | "x"
+ | "y"
+ | "item";
+
+export const aetheriaRuntimeLoadoutItemSlotCommitSlots: Record = {
+ x: 0,
+ y: 1,
+ item: 2,
+};
+
+export type AetheriaRuntimeLoadoutItemCommitSlot =
+ | "quality"
+ | "durability"
+ | "quantity"
+ | "enabled"
+ | "itemKey"
+ | "overrideShutdown"
+ | "temperature";
+
+export const aetheriaRuntimeLoadoutItemCommitSlots: Record = {
+ quality: 1,
+ durability: 2,
+ quantity: 3,
+ enabled: 4,
+ itemKey: 5,
+ overrideShutdown: 6,
+ temperature: 7,
+};
+
+export type AetheriaRuntimeRtsViewportBoundsSlot =
+ | "minX"
+ | "minY"
+ | "maxX"
+ | "maxY";
+
+export const aetheriaRuntimeRtsViewportBoundsSlots: Record = {
+ minX: 0,
+ minY: 1,
+ maxX: 2,
+ maxY: 3,
+};
+
+export type AetheriaRuntimeRtsViewportObjectSlot =
+ | "entityIndex"
+ | "entityKey"
+ | "displayName"
+ | "kind"
+ | "factionKey"
+ | "x"
+ | "y"
+ | "z"
+ | "directionX"
+ | "directionY"
+ | "velocityX"
+ | "velocityY"
+ | "controlled"
+ | "targetEntityIndex"
+ | "isActive"
+ | "visibility"
+ | "status"
+ | "inventory";
+
+export const aetheriaRuntimeRtsViewportObjectSlots: Record = {
+ entityIndex: 0,
+ entityKey: 1,
+ displayName: 2,
+ kind: 3,
+ factionKey: 4,
+ x: 5,
+ y: 6,
+ z: 7,
+ directionX: 8,
+ directionY: 9,
+ velocityX: 10,
+ velocityY: 11,
+ controlled: 12,
+ targetEntityIndex: 13,
+ isActive: 14,
+ visibility: 15,
+ status: 16,
+ inventory: 17,
+};
+
+export type AetheriaRuntimeRtsEntityStatusSlot =
+ | "hull"
+ | "shield"
+ | "heat";
+
+export const aetheriaRuntimeRtsEntityStatusSlots: Record = {
+ hull: 0,
+ shield: 1,
+ heat: 2,
+};
+
+export type AetheriaRuntimeRtsInventoryItemSlot =
+ | "source"
+ | "itemKey"
+ | "quantity"
+ | "quality"
+ | "durability"
+ | "enabled"
+ | "sourceIndex"
+ | "x"
+ | "y";
+
+export const aetheriaRuntimeRtsInventoryItemSlots: Record = {
+ source: 0,
+ itemKey: 1,
+ quantity: 2,
+ quality: 3,
+ durability: 4,
+ enabled: 5,
+ sourceIndex: 6,
+ x: 7,
+ y: 8,
+};
+
+export type AetheriaRuntimeRtsGravityInfluenceSlot =
+ | "bodyKey"
+ | "orbitKey"
+ | "kind"
+ | "x"
+ | "y"
+ | "radius"
+ | "gravityDepth"
+ | "gravityDepthExponent"
+ | "waveRadius"
+ | "waveDepth"
+ | "waveSpeed";
+
+export const aetheriaRuntimeRtsGravityInfluenceSlots: Record = {
+ bodyKey: 0,
+ orbitKey: 1,
+ kind: 2,
+ x: 3,
+ y: 4,
+ radius: 5,
+ gravityDepth: 6,
+ gravityDepthExponent: 7,
+ waveRadius: 8,
+ waveDepth: 9,
+ waveSpeed: 10,
+};
+
+export type AetheriaRuntimeRtsBodyViewSlot =
+ | "bodyKey"
+ | "orbitKey"
+ | "name"
+ | "kind"
+ | "x"
+ | "y"
+ | "radius"
+ | "isAsteroidBelt"
+ | "body";
+
+export const aetheriaRuntimeRtsBodyViewSlots: Record = {
+ bodyKey: 0,
+ orbitKey: 1,
+ name: 2,
+ kind: 3,
+ x: 4,
+ y: 5,
+ radius: 6,
+ isAsteroidBelt: 7,
+ body: 8,
+};
+
+export type AetheriaRuntimeStarbridgeScenarioDocumentSlot =
+ | "schema"
+ | "scenarioId"
+ | "displayName"
+ | "startingBaseKey"
+ | "stationStock"
+ | "availableShipKeys"
+ | "waves"
+ | "attackerMixKeys"
+ | "recoveredTechnologyPoolKeys"
+ | "runtimeRoles";
+
+export const aetheriaRuntimeStarbridgeScenarioDocumentSlots: Record = {
+ schema: 0,
+ scenarioId: 1,
+ displayName: 2,
+ startingBaseKey: 3,
+ stationStock: 4,
+ availableShipKeys: 5,
+ waves: 6,
+ attackerMixKeys: 7,
+ recoveredTechnologyPoolKeys: 8,
+ runtimeRoles: 9,
+};
+
+export type AetheriaRuntimeStarbridgeSessionDocumentSlot =
+ | "schema"
+ | "sessionId"
+ | "scenarioId"
+ | "runId"
+ | "baseEntityKey"
+ | "stationEntityKey"
+ | "phase"
+ | "currentWaveIndex"
+ | "runtimeRoles";
+
+export const aetheriaRuntimeStarbridgeSessionDocumentSlots: Record = {
+ schema: 0,
+ sessionId: 1,
+ scenarioId: 2,
+ runId: 3,
+ baseEntityKey: 4,
+ stationEntityKey: 5,
+ phase: 6,
+ currentWaveIndex: 7,
+ runtimeRoles: 8,
+};
+
+export type AetheriaRuntimeStarbridgeSessionSummaryDocumentSlot =
+ | "schema"
+ | "frameId"
+ | "publishedAtUtc"
+ | "sessionId"
+ | "scenarioId"
+ | "scenarioName"
+ | "runId"
+ | "zoneIndex"
+ | "zoneName"
+ | "phase"
+ | "currentWaveIndex"
+ | "baseStatus"
+ | "stationStock"
+ | "waveForecast"
+ | "runtimeRoles";
+
+export const aetheriaRuntimeStarbridgeSessionSummaryDocumentSlots: Record = {
+ schema: 0,
+ frameId: 1,
+ publishedAtUtc: 2,
+ sessionId: 3,
+ scenarioId: 4,
+ scenarioName: 5,
+ runId: 6,
+ zoneIndex: 7,
+ zoneName: 8,
+ phase: 9,
+ currentWaveIndex: 10,
+ baseStatus: 11,
+ stationStock: 12,
+ waveForecast: 13,
+ runtimeRoles: 14,
+};
+
+export type AetheriaRuntimeStarbridgeStationStockItemSlot =
+ | "itemKey"
+ | "quantity"
+ | "quality"
+ | "durability"
+ | "source";
+
+export const aetheriaRuntimeStarbridgeStationStockItemSlots: Record = {
+ itemKey: 0,
+ quantity: 1,
+ quality: 2,
+ durability: 3,
+ source: 4,
+};
+
+export type AetheriaRuntimeStarbridgeWaveDefinitionSlot =
+ | "waveIndex"
+ | "displayName"
+ | "attackerKeys"
+ | "startsAfterSeconds"
+ | "bossKey"
+ | "recoveredTechnologyKeys";
+
+export const aetheriaRuntimeStarbridgeWaveDefinitionSlots: Record = {
+ waveIndex: 0,
+ displayName: 1,
+ attackerKeys: 2,
+ startsAfterSeconds: 3,
+ bossKey: 4,
+ recoveredTechnologyKeys: 5,
+};
+
+export type AetheriaRuntimeStarbridgeWaveForecastSlot =
+ | "waveIndex"
+ | "displayName"
+ | "attackerKeys"
+ | "bossKey"
+ | "recoveredTechnologyKeys";
+
+export const aetheriaRuntimeStarbridgeWaveForecastSlots: Record = {
+ waveIndex: 0,
+ displayName: 1,
+ attackerKeys: 2,
+ bossKey: 3,
+ recoveredTechnologyKeys: 4,
+};
+
+export type AetheriaRuntimeStarbridgeRuntimeRoleSlot =
+ | "runtimeId"
+ | "role"
+ | "entityKey";
+
+export const aetheriaRuntimeStarbridgeRuntimeRoleSlots: Record = {
+ runtimeId: 0,
+ role: 1,
+ entityKey: 2,
+};
+
+export type AetheriaRuntimeStarbridgeBaseStatusSlot =
+ | "entityKey"
+ | "displayName"
+ | "hull"
+ | "shield"
+ | "heat"
+ | "isActive";
+
+export const aetheriaRuntimeStarbridgeBaseStatusSlots: Record = {
+ entityKey: 0,
+ displayName: 1,
+ hull: 2,
+ shield: 3,
+ heat: 4,
+ isActive: 5,
+};
+
+export type AetheriaRuntimeVerseAuthorityPolicyDocumentSlot =
+ | "schema"
+ | "verseId"
+ | "policyId"
+ | "ruleVersion"
+ | "hostRuntimeId"
+ | "defaultMode"
+ | "rules"
+ | "updatedAtUtc"
+ | "deploymentMode"
+ | "runtimeRoles";
+
+export const aetheriaRuntimeVerseAuthorityPolicyDocumentSlots: Record = {
+ schema: 0,
+ verseId: 1,
+ policyId: 2,
+ ruleVersion: 3,
+ hostRuntimeId: 4,
+ defaultMode: 5,
+ rules: 6,
+ updatedAtUtc: 7,
+ deploymentMode: 8,
+ runtimeRoles: 9,
+};
+
+export type AetheriaRuntimeAuthorityRuleSlot =
+ | "ruleId"
+ | "subjectPrefix"
+ | "claimKinds"
+ | "mode"
+ | "runtimeIds"
+ | "leaseScope"
+ | "priority";
+
+export const aetheriaRuntimeAuthorityRuleSlots: Record = {
+ ruleId: 0,
+ subjectPrefix: 1,
+ claimKinds: 2,
+ mode: 3,
+ runtimeIds: 4,
+ leaseScope: 5,
+ priority: 6,
+};
+
+export type AetheriaRuntimeAuthorityRuntimeRoleSlot =
+ | "runtimeId"
+ | "roles";
+
+export const aetheriaRuntimeAuthorityRuntimeRoleSlots: Record = {
+ runtimeId: 0,
+ roles: 1,
+};
+
+export type AetheriaRuntimeAuthorityLeaseDocumentSlot =
+ | "schema"
+ | "leaseId"
+ | "verseId"
+ | "runtimeId"
+ | "subjectPrefix"
+ | "claimKinds"
+ | "validFromUtc"
+ | "expiresAtUtc"
+ | "scope";
+
+export const aetheriaRuntimeAuthorityLeaseDocumentSlots: Record = {
+ schema: 0,
+ leaseId: 1,
+ verseId: 2,
+ runtimeId: 3,
+ subjectPrefix: 4,
+ claimKinds: 5,
+ validFromUtc: 6,
+ expiresAtUtc: 7,
+ scope: 8,
+};
+
+export type ViewportResponse = {
+ schema: string;
+ frameId: number;
+ publishedAtUtc: string;
+ simulationTimeSeconds: number;
+ runId: string;
+ zoneIndex: number;
+ zoneName: string;
+ currentEntityKey: string;
+ viewport: CultMeshViewportRequest;
+ controlledEntityIndices: number[];
+ objects: ViewObject[];
+ gravityInfluences: GravityInfluence[];
+ bodies: BodyView[];
+};
+
+export type ObjectsViewportResponse = {
+ schema: string;
+ frameId: number;
+ publishedAtUtc: string;
+ simulationTimeSeconds: number;
+ runId: string;
+ zoneIndex: number;
+ zoneName: string;
+ currentEntityKey: string;
+ viewport: CultMeshViewportRequest;
+ controlledEntityIndices: number[];
+ objects: ViewObject[];
+};
+
+export type GravityViewportResponse = {
+ schema: string;
+ frameId: number;
+ publishedAtUtc: string;
+ simulationTimeSeconds: number;
+ runId: string;
+ zoneIndex: number;
+ zoneName: string;
+ viewport: CultMeshViewportRequest;
+ gravityInfluences: GravityInfluence[];
+ bodies: BodyView[];
+};
+
+export type ViewObject = {
+ entityIndex: number;
+ entityKey: string;
+ displayName: string;
+ kind: string;
+ factionKey: string;
+ x: number;
+ y: number;
+ z: number;
+ directionX: number;
+ directionY: number;
+ velocityX: number;
+ velocityY: number;
+ controlled: boolean;
+ targetEntityIndex: number;
+ isActive: boolean;
+ visibility: number;
+ status: EntityStatus;
+ inventory: InventoryItem[];
+};
+
+export type EntityStatus = {
+ hull: number;
+ shield: number;
+ heat: number;
+};
+
+export type InventoryItem = {
+ source: string;
+ itemKey: string;
+ quantity: number;
+ quality: number;
+ durability: number;
+ enabled: boolean;
+};
+
+export type SelectedObjectProjection = {
+ schema: string;
+ frameId: number;
+ runId: string;
+ zoneIndex: number;
+ entityIndex: number;
+ selected: ViewObject | null;
+};
+
+export type InventoryProjection = {
+ schema: string;
+ frameId: number;
+ runId: string;
+ zoneIndex: number;
+ entityIndex: number;
+ entityKey: string;
+ items: InventoryItem[];
+ equipment: InventoryItem[];
+ cargo: InventoryItem[];
+};
+
+export type DaemonHealthProjection = {
+ schema: string;
+ daemonId: string;
+ verseId: string;
+ publishedAtUtc: string;
+ statePath: string;
+ frameId: number;
+ observedCommandCount: number;
+ appliedCommandCount: number;
+ rejectedCommandCount: number;
+ status: string;
+ publicationSource: string;
+ transport: string;
+ commandBoundaryPath: string;
+};
+
+export type AuthorityStatusProjection = {
+ schema: string;
+ verseId: string;
+ policyId: string;
+ ruleVersion: string;
+ hostRuntimeId: string;
+ defaultMode: string;
+ updatedAtUtc: string;
+ rules: AuthorityRuleProjection[];
+};
+
+export type AuthorityRuleProjection = {
+ ruleId: string;
+ subjectPrefix: string;
+ claimKinds: string[];
+ mode: string;
+ runtimeIds: string[];
+ leaseScope: string;
+ priority: number;
+};
+
+export type StarbridgeSessionProjection = {
+ schema: string;
+ frameId: number;
+ publishedAtUtc: string;
+ sessionId: string;
+ scenarioId: string;
+ scenarioName: string;
+ runId: string;
+ zoneIndex: number;
+ zoneName: string;
+ phase: string;
+ currentWaveIndex: number;
+ baseStatus: StarbridgeBaseStatusProjection;
+ stationStock: StarbridgeStationStockItemProjection[];
+ waveForecast: StarbridgeWaveForecastProjection[];
+ runtimeRoles: StarbridgeRuntimeRoleProjection[];
+};
+
+export type StarbridgeBaseStatusProjection = {
+ entityKey: string;
+ displayName: string;
+ hull: number;
+ shield: number;
+ heat: number;
+ isActive: boolean;
+};
+
+export type StarbridgeStationStockItemProjection = {
+ itemKey: string;
+ quantity: number;
+ quality: number;
+ durability: number;
+ source: string;
+};
+
+export type StarbridgeWaveForecastProjection = {
+ waveIndex: number;
+ displayName: string;
+ attackerKeys: string[];
+ bossKey: string;
+ recoveredTechnologyKeys: string[];
+};
+
+export type StarbridgeRuntimeRoleProjection = {
+ runtimeId: string;
+ role: string;
+ entityKey: string;
+};
+
+export type GravityInfluence = {
+ bodyKey: string;
+ orbitKey: string;
+ kind: string;
+ x: number;
+ y: number;
+ radius: number;
+ gravityDepth: number;
+ gravityDepthExponent: number;
+ waveRadius: number;
+ waveDepth: number;
+ waveSpeed: number;
+};
+
+export type BodyView = {
+ bodyKey: string;
+ orbitKey: string;
+ name: string;
+ kind: string;
+ x: number;
+ y: number;
+ radius: number;
+ isAsteroidBelt: boolean;
+};
+
+
+export type AetheriaRuntimeSetMoveVectorRequest = {
+ actorEntityKey: string;
+ directionX: number;
+ directionY: number;
+ scalar: number;
+ observedFrameId?: number;
+};
+
+export type AetheriaRuntimeSetTargetRequest = {
+ actorEntityKey: string;
+ targetEntityKey: string;
+ observedFrameId?: number;
+};
+
+export type AetheriaRuntimeDaemonCommandReceipt = CultMeshOperationReceipt & {
+ commandId: string;
+};
+
+export type AetheriaRuntimeSelectedObjectRequest = {
+ entityIndex: number;
+};
+
+export type AetheriaRuntimeViewportFeedRequest = {
+ viewport: CultMeshViewportRequest;
+ selectedEntityIndex?: number;
+};
+
+export type AetheriaRuntimeViewportFeedSnapshot = {
+ viewport: ViewportResponse;
+ selectedObject: SelectedObjectProjection | null;
+ inventory: InventoryProjection | null;
+ daemonHealth: DaemonHealthProjection;
+ authorityStatus: AuthorityStatusProjection;
+ starbridgeSession: StarbridgeSessionProjection;
+ receivedAtUtc: string;
+ sampleMs: number;
+};
+
+export type AetheriaRtsMainClient = {
+ mapViewport(request: CultMeshViewportRequest): Promise;
+ objectsViewport(request: CultMeshViewportRequest): Promise;
+ gravityViewport(request: CultMeshViewportRequest): Promise;
+ selectedObject(request: AetheriaRuntimeSelectedObjectRequest): Promise;
+ inventory(request: AetheriaRuntimeSelectedObjectRequest): Promise;
+ daemonHealth(): Promise;
+ authorityStatus(): Promise;
+ starbridgeSession(): Promise;
+ watchViewportFeed(request: AetheriaRuntimeViewportFeedRequest, callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void): CultMeshUnsubscribe;
+ setMoveVector(request: AetheriaRuntimeSetMoveVectorRequest): Promise;
+ setTarget(request: AetheriaRuntimeSetTargetRequest): Promise;
+ surfaceCatalogDiagnostics(): CultMeshSurfaceCatalogDiagnostic;
+ surfaceCatalogIndexDiagnostics(): CultMeshSurfaceCatalogIndexDiagnostic;
+};
+
+export type AetheriaRtsHealthProvider = () => unknown;
+
+export function registerAetheriaRtsIpcHandlers(
+ ipcMain: IpcMain,
+ getClient: () => AetheriaRtsMainClient,
+ getHealth: AetheriaRtsHealthProvider,
+): void {
+ const viewportFeedSubscriptions = new Map();
+ ipcMain.handle(AetheriaRtsIpcChannels.mapViewport, async (_event, request: CultMeshViewportRequest) =>
+ getClient().mapViewport(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.objectsViewport, async (_event, request: CultMeshViewportRequest) =>
+ getClient().objectsViewport(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.gravityViewport, async (_event, request: CultMeshViewportRequest) =>
+ getClient().gravityViewport(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.selectedObject, async (_event, request: AetheriaRuntimeSelectedObjectRequest) =>
+ getClient().selectedObject(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.inventory, async (_event, request: AetheriaRuntimeSelectedObjectRequest) =>
+ getClient().inventory(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.daemonHealth, async () => getClient().daemonHealth());
+ ipcMain.handle(AetheriaRtsIpcChannels.authorityStatus, async () => getClient().authorityStatus());
+ ipcMain.handle(AetheriaRtsIpcChannels.starbridgeSession, async () => getClient().starbridgeSession());
+ ipcMain.handle(AetheriaRtsIpcChannels.viewportFeed, async (event, request: { subscriptionId: string; feed: AetheriaRuntimeViewportFeedRequest }) => {
+ viewportFeedSubscriptions.get(request.subscriptionId)?.();
+ const unsubscribe = getClient().watchViewportFeed(request.feed, snapshot => {
+ event.sender.send(AetheriaRtsIpcChannels.viewportFeedUpdate, {
+ subscriptionId: request.subscriptionId,
+ snapshot,
+ });
+ });
+ viewportFeedSubscriptions.set(request.subscriptionId, unsubscribe);
+ });
+ ipcMain.handle(AetheriaRtsIpcChannels.viewportFeedStop, (_event, subscriptionId: string) => {
+ viewportFeedSubscriptions.get(subscriptionId)?.();
+ viewportFeedSubscriptions.delete(subscriptionId);
+ });
+ ipcMain.handle(AetheriaRtsIpcChannels.setMoveVector, async (_event, request: AetheriaRuntimeSetMoveVectorRequest) =>
+ getClient().setMoveVector(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.setTarget, async (_event, request: AetheriaRuntimeSetTargetRequest) =>
+ getClient().setTarget(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.surfaceCatalog, () => getClient().surfaceCatalogDiagnostics());
+ ipcMain.handle(AetheriaRtsIpcChannels.surfaceCatalogIndex, () => getClient().surfaceCatalogIndexDiagnostics());
+ ipcMain.handle(AetheriaRtsIpcChannels.health, () => getHealth());
+}
+
+export type AetheriaRuntimeDaemonCommandSender = (
+ commandId: string,
+ issuedAtUtc: string,
+ command: unknown[],
+ context: CultMeshOperationContext,
+) => Promise;
+
+export type AetheriaRuntimeViewportQueryExecutor = (
+ request: CultMeshViewportRequest,
+ context: CultMeshQueryContext,
+) => Promise;
+
+export type AetheriaRuntimeQueryExecutor = (
+ request: TParameters,
+ context: CultMeshQueryContext,
+) => Promise;
+
+export type AetheriaRuntimeRtsQueryExecutors = {
+ mapViewport: AetheriaRuntimeViewportQueryExecutor;
+ objectsViewport: AetheriaRuntimeViewportQueryExecutor;
+ gravityViewport: AetheriaRuntimeViewportQueryExecutor;
+ selectedObject: AetheriaRuntimeQueryExecutor;
+ inventory: AetheriaRuntimeQueryExecutor;
+ daemonHealth: AetheriaRuntimeQueryExecutor;
+ authorityStatus: AetheriaRuntimeQueryExecutor;
+ starbridgeSession: AetheriaRuntimeQueryExecutor;
+};
+
+export type AetheriaRuntimeRtsQueryWatchers = Partial<{
+ mapViewport: CultMeshQueryWatcher;
+ objectsViewport: CultMeshQueryWatcher;
+ gravityViewport: CultMeshQueryWatcher;
+ selectedObject: CultMeshQueryWatcher;
+ inventory: CultMeshQueryWatcher;
+ daemonHealth: CultMeshQueryWatcher;
+ authorityStatus: CultMeshQueryWatcher;
+ starbridgeSession: CultMeshQueryWatcher;
+}>;
+
+export type AetheriaRuntimeRtsProjectionDiagnostic = CultMeshQuerySurfaceDiagnostic;
+
+export type AetheriaRuntimeRtsLiveFeedDiagnostic = CultMeshLiveFeedDiagnostic;
+
+export type AetheriaRuntimeRtsSurfaceCatalogDiagnostic = CultMeshSurfaceCatalogDiagnostic;
+
+export type AetheriaRuntimeRtsOperationHandles = ReturnType;
+
+export type AetheriaRuntimeRtsStatePointers = ReturnType;
+
+export type AetheriaRuntimeRtsStatePointerResolvers = Partial<{
+ daemonFrame: (context: CultMeshQueryContext) => Promise;
+ daemonHealth: (context: CultMeshQueryContext) => Promise;
+ authorityPolicy: (context: CultMeshQueryContext) => Promise;
+ starbridgeSession: (context: CultMeshQueryContext) => Promise;
+}>;
+
+export const AetheriaRuntimeRtsProjectionSources = {
+ daemonFrame: CultMesh.projectionSource("daemon:aetheria.frame.latest.v1", {
+ schemaId: AetheriaRtsSchemas.daemonFrame,
+ description: "latest daemon frame"
+ }),
+ daemonHealth: CultMesh.projectionSource("daemon:aetheria.health.latest.v1", {
+ schemaId: AetheriaRtsSchemas.daemonHealth,
+ description: "latest daemon health"
+ }),
+ authorityPolicy: CultMesh.projectionSource("daemon:aetheria.authority.policy.latest.v1", {
+ schemaId: AetheriaRtsSchemas.verseAuthorityPolicy,
+ description: "latest Verse authority policy"
+ }),
+ starbridgeSession: CultMesh.projectionSource("daemon:aetheria.starbridge.session.latest.v1", {
+ schemaId: AetheriaRtsSchemas.starbridgeSessionSummary,
+ description: "latest Starbridge session summary"
+ })
+} as const;
+
+export function createAetheriaRuntimeRtsStatePointers(
+ routeHint: CultMeshRouteHint = CultMesh.routeHint(),
+ resolvers: AetheriaRuntimeRtsStatePointerResolvers = {},
+) {
+ return {
+ daemonFrame: CultMesh.statePointer(
+ AetheriaRuntimeRtsProjectionSources.daemonFrame.sourceId,
+ async (context) => resolvers.daemonFrame?.(context),
+ undefined,
+ {
+ routeHint,
+ sources: [AetheriaRuntimeRtsProjectionSources.daemonFrame],
+ },
+ ),
+ daemonHealth: CultMesh.statePointer(
+ AetheriaRuntimeRtsProjectionSources.daemonHealth.sourceId,
+ async (context) => resolvers.daemonHealth?.(context),
+ undefined,
+ {
+ routeHint,
+ sources: [AetheriaRuntimeRtsProjectionSources.daemonHealth],
+ },
+ ),
+ authorityPolicy: CultMesh.statePointer(
+ AetheriaRuntimeRtsProjectionSources.authorityPolicy.sourceId,
+ async (context) => resolvers.authorityPolicy?.(context),
+ undefined,
+ {
+ routeHint,
+ sources: [AetheriaRuntimeRtsProjectionSources.authorityPolicy],
+ },
+ ),
+ starbridgeSession: CultMesh.statePointer(
+ AetheriaRuntimeRtsProjectionSources.starbridgeSession.sourceId,
+ async (context) => resolvers.starbridgeSession?.(context),
+ undefined,
+ {
+ routeHint,
+ sources: [AetheriaRuntimeRtsProjectionSources.starbridgeSession],
+ },
+ ),
+ } as const;
+}
+
+export function createAetheriaRuntimeRtsQueryHandles(
+ executors: AetheriaRuntimeRtsQueryExecutors,
+ routeHint: CultMeshRouteHint = CultMesh.routeHint(),
+ watchers: AetheriaRuntimeRtsQueryWatchers = {},
+) {
+ return {
+ mapViewport: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.rtsViewport,
+ [AetheriaRuntimeRtsProjectionSources.daemonFrame],
+ executors.mapViewport,
+ { routeHint, watchProjection: watchers.mapViewport },
+ ).asQuerySurface(),
+ objectsViewport: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.objectsViewport,
+ [AetheriaRuntimeRtsProjectionSources.daemonFrame],
+ executors.objectsViewport,
+ { routeHint, watchProjection: watchers.objectsViewport },
+ ).asQuerySurface(),
+ gravityViewport: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.gravityViewport,
+ [AetheriaRuntimeRtsProjectionSources.daemonFrame],
+ executors.gravityViewport,
+ { routeHint, watchProjection: watchers.gravityViewport },
+ ).asQuerySurface(),
+ selectedObject: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.selectedObject,
+ [AetheriaRuntimeRtsProjectionSources.daemonFrame],
+ executors.selectedObject,
+ { routeHint, watchProjection: watchers.selectedObject },
+ ).asQuerySurface(),
+ inventory: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.inventory,
+ [AetheriaRuntimeRtsProjectionSources.daemonFrame],
+ executors.inventory,
+ { routeHint, watchProjection: watchers.inventory },
+ ).asQuerySurface(),
+ daemonHealth: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.daemonHealth,
+ [AetheriaRuntimeRtsProjectionSources.daemonHealth],
+ executors.daemonHealth,
+ { routeHint, watchProjection: watchers.daemonHealth },
+ ).asQuerySurface(),
+ authorityStatus: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.verseAuthorityPolicy,
+ [AetheriaRuntimeRtsProjectionSources.authorityPolicy],
+ executors.authorityStatus,
+ { routeHint, watchProjection: watchers.authorityStatus },
+ ).asQuerySurface(),
+ starbridgeSession: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.starbridgeSessionSummary,
+ [AetheriaRuntimeRtsProjectionSources.starbridgeSession],
+ executors.starbridgeSession,
+ { routeHint, watchProjection: watchers.starbridgeSession },
+ ).asQuerySurface(),
+ } as const;
+}
+
+export function describeAetheriaRuntimeRtsQueryHandles(
+ handles: ReturnType,
+) {
+ return {
+ mapViewport: describeAetheriaRuntimeRtsQuerySurface(handles.mapViewport),
+ objectsViewport: describeAetheriaRuntimeRtsQuerySurface(handles.objectsViewport),
+ gravityViewport: describeAetheriaRuntimeRtsQuerySurface(handles.gravityViewport),
+ selectedObject: describeAetheriaRuntimeRtsQuerySurface(handles.selectedObject),
+ inventory: describeAetheriaRuntimeRtsQuerySurface(handles.inventory),
+ daemonHealth: describeAetheriaRuntimeRtsQuerySurface(handles.daemonHealth),
+ authorityStatus: describeAetheriaRuntimeRtsQuerySurface(handles.authorityStatus),
+ starbridgeSession: describeAetheriaRuntimeRtsQuerySurface(handles.starbridgeSession),
+ } as const;
+}
+
+export function describeAetheriaRuntimeRtsSurfaceCatalog(
+ handles: ReturnType,
+ operations?: AetheriaRuntimeRtsOperationHandles,
+ statePointers: AetheriaRuntimeRtsStatePointers = createAetheriaRuntimeRtsStatePointers(),
+): AetheriaRuntimeRtsSurfaceCatalogDiagnostic {
+ return CultMesh.describeSurfaceCatalog(
+ "gamecult.aetheria.rts.surfaces.v1",
+ [
+ CultMesh.describeSurface(statePointers.daemonFrame),
+ CultMesh.describeSurface(statePointers.daemonHealth),
+ CultMesh.describeSurface(statePointers.authorityPolicy),
+ CultMesh.describeSurface(statePointers.starbridgeSession),
+ CultMesh.describeSurface(handles.mapViewport),
+ CultMesh.describeSurface(handles.objectsViewport),
+ CultMesh.describeSurface(handles.gravityViewport),
+ CultMesh.describeSurface(handles.selectedObject),
+ CultMesh.describeSurface(handles.inventory),
+ CultMesh.describeSurface(handles.daemonHealth),
+ CultMesh.describeSurface(handles.authorityStatus),
+ CultMesh.describeSurface(handles.starbridgeSession),
+ ...(operations
+ ? [
+ CultMesh.describeSurface(operations.setMoveVector),
+ CultMesh.describeSurface(operations.setTarget),
+ ]
+ : []),
+ ],
+ );
+}
+
+function describeAetheriaRuntimeRtsQuerySurface(surface: {
+ queryId: string;
+ routeHint: CultMeshRouteHint;
+ sources: readonly CultMeshProjectionSource[];
+}): AetheriaRuntimeRtsProjectionDiagnostic {
+ return CultMesh.describeQuerySurface(surface);
+}
+
+export function describeAetheriaRuntimeRtsLiveFeedSurface(surface: {
+ feedId: string;
+ routeHint: CultMeshRouteHint;
+ sources: readonly CultMeshProjectionSource[];
+}): AetheriaRuntimeRtsLiveFeedDiagnostic {
+ return CultMesh.describeLiveFeed(surface);
+}
+
+export function createAetheriaRuntimeRtsOperationHandles(
+ sendCommand: AetheriaRuntimeDaemonCommandSender,
+ createCommandId: () => string = createDefaultCommandId,
+) {
+ return {
+ setMoveVector: CultMesh.operation(
+ "gamecult.aetheria.pilot.set_move_vector.v1",
+ async (request, context) => {
+ const commandId = context.idempotencyKey ?? createCommandId();
+ const issuedAtUtc = new Date().toISOString();
+ await sendCommand(
+ commandId,
+ issuedAtUtc,
+ encodeSetMoveVectorCommand(commandId, issuedAtUtc, context.runtimeId, request),
+ context,
+ );
+ return {
+ commandId,
+ ...CultMesh.operationReceipt("gamecult.aetheria.pilot.set_move_vector.v1", true, { route: context.routeHint }),
+ };
+ },
+ ),
+ setTarget: CultMesh.operation(
+ "gamecult.aetheria.pilot.set_target.v1",
+ async (request, context) => {
+ const commandId = context.idempotencyKey ?? createCommandId();
+ const issuedAtUtc = new Date().toISOString();
+ await sendCommand(
+ commandId,
+ issuedAtUtc,
+ encodeSetTargetCommand(commandId, issuedAtUtc, context.runtimeId, request),
+ context,
+ );
+ return {
+ commandId,
+ ...CultMesh.operationReceipt("gamecult.aetheria.pilot.set_target.v1", true, { route: context.routeHint }),
+ };
+ },
+ ),
+ } as const;
+}
+
+export type AetheriaRuntimeRtsVerseFacade = ReturnType;
+
+export function createAetheriaRuntimeRtsVerseFacade(
+ queryVerse: CultMeshVerseContext,
+ commandVerse: CultMeshVerseContext,
+ queries: ReturnType,
+ operations: AetheriaRuntimeRtsOperationHandles,
+ statePointers: AetheriaRuntimeRtsStatePointers = createAetheriaRuntimeRtsStatePointers(),
+) {
+ const mapViewport = CultMesh.bindQuery(queryVerse, queries.mapViewport);
+ const objectsViewport = CultMesh.bindQuery(queryVerse, queries.objectsViewport);
+ const gravityViewport = CultMesh.bindQuery(queryVerse, queries.gravityViewport);
+ const selectedObject = CultMesh.bindQuery(queryVerse, queries.selectedObject);
+ const inventory = CultMesh.bindQuery(queryVerse, queries.inventory);
+ const daemonHealth = CultMesh.bindQuery(queryVerse, queries.daemonHealth);
+ const authorityStatus = CultMesh.bindQuery(queryVerse, queries.authorityStatus);
+ const starbridgeSession = CultMesh.bindQuery(queryVerse, queries.starbridgeSession);
+ const daemonFramePointer = CultMesh.bindStatePointer(queryVerse, statePointers.daemonFrame);
+ const daemonHealthPointer = CultMesh.bindStatePointer(queryVerse, statePointers.daemonHealth);
+ const authorityPolicyPointer = CultMesh.bindStatePointer(queryVerse, statePointers.authorityPolicy);
+ const starbridgeSessionPointer = CultMesh.bindStatePointer(queryVerse, statePointers.starbridgeSession);
+ const setMoveVector = CultMesh.bindOperation(commandVerse, operations.setMoveVector);
+ const setTarget = CultMesh.bindOperation(commandVerse, operations.setTarget);
+
+ return {
+ zone: (_zoneId = "current") => ({
+ viewport: {
+ within: (request: CultMeshViewportRequest) =>
+ mapViewport.execute(request),
+ watch: (
+ request: CultMeshViewportRequest,
+ callback: (value: ViewportResponse) => void,
+ ) => mapViewport.watch(request, callback),
+ },
+ objects: {
+ visibleWithin: (request: CultMeshViewportRequest) =>
+ objectsViewport.execute(request),
+ watchVisibleWithin: (
+ request: CultMeshViewportRequest,
+ callback: (value: ObjectsViewportResponse) => void,
+ ) => objectsViewport.watch(request, callback),
+ },
+ gravity: {
+ within: (request: CultMeshViewportRequest) =>
+ gravityViewport.execute(request),
+ watchWithin: (
+ request: CultMeshViewportRequest,
+ callback: (value: GravityViewportResponse) => void,
+ ) => gravityViewport.watch(request, callback),
+ },
+ }),
+ entity: (entityKey: string) => ({
+ pilot: {
+ move: (
+ direction: { readonly x: number; readonly y: number },
+ options: { scalar?: number; observedFrameId?: number; commandId?: string } = {},
+ ) => setMoveVector.invoke({
+ actorEntityKey: entityKey,
+ directionX: direction.x,
+ directionY: direction.y,
+ scalar: options.scalar ?? 1,
+ observedFrameId: options.observedFrameId,
+ }, { idempotencyKey: options.commandId }),
+ target: (
+ targetEntityKey: string,
+ options: { observedFrameId?: number; commandId?: string } = {},
+ ) => setTarget.invoke({
+ actorEntityKey: entityKey,
+ targetEntityKey,
+ observedFrameId: options.observedFrameId,
+ }, { idempotencyKey: options.commandId }),
+ },
+ }),
+ selectedObject: (entityIndex: number) =>
+ selectedObject.execute({ entityIndex }),
+ inventory: (entityIndex: number) =>
+ inventory.execute({ entityIndex }),
+ daemon: {
+ state: {
+ frame: () => daemonFramePointer.resolve(),
+ health: () => daemonHealthPointer.resolve(),
+ authorityPolicy: () => authorityPolicyPointer.resolve(),
+ starbridgeSession: () => starbridgeSessionPointer.resolve(),
+ },
+ health: () => daemonHealth.execute(undefined),
+ authorityStatus: () => authorityStatus.execute(undefined),
+ starbridgeSession: () => starbridgeSession.execute(undefined),
+ },
+ } as const;
+}
+
+export function encodeSetMoveVectorCommand(
+ commandId: string,
+ issuedAtUtc: string,
+ runtimeId: string,
+ request: AetheriaRuntimeSetMoveVectorRequest,
+): unknown[] {
+ const command = createBaseCommand(commandId, issuedAtUtc, runtimeId, request.actorEntityKey, request.observedFrameId);
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.kind] = AetheriaRuntimeDaemonCommandKinds.setMoveVector;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.directionX] = request.directionX;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.directionY] = request.directionY;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.scalarValue] = Math.min(1, Math.max(0, request.scalar));
+ return command;
+}
+
+export function encodeSetTargetCommand(
+ commandId: string,
+ issuedAtUtc: string,
+ runtimeId: string,
+ request: AetheriaRuntimeSetTargetRequest,
+): unknown[] {
+ const command = createBaseCommand(commandId, issuedAtUtc, runtimeId, request.actorEntityKey, request.observedFrameId);
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.kind] = AetheriaRuntimeDaemonCommandKinds.setTarget;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.targetEntityKey] = request.targetEntityKey;
+ return command;
+}
+
+function createBaseCommand(
+ commandId: string,
+ issuedAtUtc: string,
+ runtimeId: string,
+ actorEntityKey: string,
+ observedFrameId?: number,
+): unknown[] {
+ const command = new Array(Object.keys(aetheriaRuntimeDaemonCommandDocumentSlots).length).fill(null);
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.schema] = AetheriaRtsSchemas.daemonCommand;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.commandId] = commandId;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.clientId] = runtimeId;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.issuedAtUtc] = issuedAtUtc;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.sessionId] = "local";
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.observedFrameId] = observedFrameId ?? -1;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.kind] = 0;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.actorEntityKey] = actorEntityKey;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.targetEntityKey] = "";
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.targetZoneIndex] = -1;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.equipmentIndex] = -1;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.behaviorIndex] = -1;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.weaponGroup] = -1;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.positionX] = 0;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.positionY] = 0;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.positionZ] = 0;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.directionX] = 0;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.directionY] = 0;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.scalarValue] = 0;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.textValue] = "";
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.authorRuntimeId] = "";
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.subjectKey] = "";
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.claimKind] = "";
+ return command;
+}
+
+function createDefaultCommandId(): string {
+ const cryptoApi = globalThis.crypto as { randomUUID?: () => string } | undefined;
+ return cryptoApi?.randomUUID?.() ?? `command-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
+}
diff --git a/Aetheria.Rts.Web/Electron/aetheria-rts-local-projection.ts b/Aetheria.Rts.Web/Electron/aetheria-rts-local-projection.ts
new file mode 100644
index 000000000..298e2ed7b
--- /dev/null
+++ b/Aetheria.Rts.Web/Electron/aetheria-rts-local-projection.ts
@@ -0,0 +1,445 @@
+import {
+ AetheriaRtsSchemas,
+ aetheriaRuntimeAuthorityRuleSlots as authorityRuleSlots,
+ aetheriaRuntimeBodySnapshotCommitSlots as bodySlots,
+ aetheriaRuntimeCargoBayLoadoutCommitSlots as cargoBaySlots,
+ aetheriaRuntimeDaemonFrameDocumentSlots as frameSlots,
+ aetheriaRuntimeDaemonHealthDocumentSlots as healthSlots,
+ aetheriaRuntimeEntitySnapshotCommitSlots as entitySlots,
+ aetheriaRuntimeEntityStatGridCommitSlots as statGridSlots,
+ aetheriaRuntimeLoadoutItemCommitSlots as itemSlots,
+ aetheriaRuntimeLoadoutItemSlotCommitSlots as itemSlotSlots,
+ aetheriaRuntimeRunCheckpointCommitSlots as runSlots,
+ aetheriaRuntimeStarbridgeBaseStatusSlots as starbridgeBaseSlots,
+ aetheriaRuntimeStarbridgeRuntimeRoleSlots as starbridgeRoleSlots,
+ aetheriaRuntimeStarbridgeSessionSummaryDocumentSlots as starbridgeSummarySlots,
+ aetheriaRuntimeStarbridgeStationStockItemSlots as starbridgeStockSlots,
+ aetheriaRuntimeStarbridgeWaveForecastSlots as starbridgeWaveForecastSlots,
+ aetheriaRuntimeVerseAuthorityPolicyDocumentSlots as authorityPolicySlots,
+ aetheriaRuntimeZoneSnapshotCommitSlots as zoneSlots,
+} from "./aetheria-rts-generated-bindings.js";
+import { cultMeshRectFromBounds, cultMeshViewportRequest } from "cultmesh-ts";
+import type {
+ AuthorityStatusProjection,
+ BodyView,
+ DaemonHealthProjection,
+ GravityViewportResponse,
+ GravityInfluence,
+ InventoryProjection,
+ InventoryItem,
+ ObjectsViewportResponse,
+ SelectedObjectProjection,
+ SelectedObjectRequest,
+ StarbridgeSessionProjection,
+ ViewObject,
+ ViewportRequest,
+ ViewportResponse,
+} from "./aetheria-rts-bindings.js";
+
+export function projectViewportFromFrame(frameDocument: unknown, request: ViewportRequest): ViewportResponse {
+ const objects = projectObjectsViewportFromFrame(frameDocument, request);
+ const gravity = projectGravityViewportFromFrame(frameDocument, request);
+ return {
+ schema: AetheriaRtsSchemas.rtsViewport,
+ frameId: objects.frameId,
+ publishedAtUtc: objects.publishedAtUtc,
+ simulationTimeSeconds: objects.simulationTimeSeconds,
+ runId: objects.runId,
+ zoneIndex: objects.zoneIndex,
+ zoneName: objects.zoneName,
+ currentEntityKey: objects.currentEntityKey,
+ viewport: objects.viewport,
+ controlledEntityIndices: objects.controlledEntityIndices,
+ objects: objects.objects,
+ gravityInfluences: gravity.gravityInfluences,
+ bodies: gravity.bodies,
+ };
+}
+
+export function projectObjectsViewportFromFrame(
+ frameDocument: unknown,
+ request: ViewportRequest,
+): ObjectsViewportResponse {
+ const frame = arr(frameDocument);
+ const run = arr(frame[frameSlots.run]);
+ const zones = list(run[runSlots.zones]);
+ const currentZoneIndex = num(run[runSlots.currentZoneIndex], -1);
+ const zone = zones.find(candidate => num(candidate[zoneSlots.zoneIndex], -1) === currentZoneIndex) ??
+ zones[0] ??
+ [];
+ const runId = str(run[runSlots.runId]) || "local-rts";
+ const viewport = normalizeViewport(request);
+ const entities = list(zone[zoneSlots.entities]);
+ const controlledEntityIndices = entities
+ .filter(isPlayerControlled)
+ .map(entity => num(entity[entitySlots.entityIndex], -1))
+ .filter(index => index >= 0);
+ const controlled = entities.filter(entity => controlledEntityIndices.includes(num(entity[entitySlots.entityIndex], -1)));
+ const objects = entities
+ .filter(entity => entityIntersectsViewport(entity, viewport))
+ .filter(entity => isPlayerControlled(entity) ||
+ controlled.length === 0 ||
+ controlled.some(observer => canSee(observer, entity)))
+ .map(entity => toViewObject(entity, runId, num(zone[zoneSlots.zoneIndex])));
+
+ return {
+ schema: AetheriaRtsSchemas.objectsViewport,
+ frameId: num(frame[frameSlots.frameId]),
+ publishedAtUtc: str(frame[frameSlots.publishedAtUtc]),
+ simulationTimeSeconds: num(frame[frameSlots.simulationTimeSeconds]),
+ runId,
+ zoneIndex: num(zone[zoneSlots.zoneIndex]),
+ zoneName: str(zone[zoneSlots.name]) || `Zone ${num(zone[zoneSlots.zoneIndex])}`,
+ currentEntityKey: str(run[runSlots.currentEntityKey]),
+ viewport,
+ controlledEntityIndices,
+ objects,
+ };
+}
+
+export function projectGravityViewportFromFrame(
+ frameDocument: unknown,
+ request: ViewportRequest,
+): GravityViewportResponse {
+ const frame = arr(frameDocument);
+ const run = arr(frame[frameSlots.run]);
+ const zones = list(run[runSlots.zones]);
+ const currentZoneIndex = num(run[runSlots.currentZoneIndex], -1);
+ const zone = zones.find(candidate => num(candidate[zoneSlots.zoneIndex], -1) === currentZoneIndex) ??
+ zones[0] ??
+ [];
+ const runId = str(run[runSlots.runId]) || "local-rts";
+ const viewport = normalizeViewport(request);
+ const visibleBodies = list(zone[zoneSlots.bodies])
+ .filter(body => gravityInfluenceIntersectsViewport(body, viewport));
+
+ return {
+ schema: AetheriaRtsSchemas.gravityViewport,
+ frameId: num(frame[frameSlots.frameId]),
+ publishedAtUtc: str(frame[frameSlots.publishedAtUtc]),
+ simulationTimeSeconds: num(frame[frameSlots.simulationTimeSeconds]),
+ runId,
+ zoneIndex: num(zone[zoneSlots.zoneIndex]),
+ zoneName: str(zone[zoneSlots.name]) || `Zone ${num(zone[zoneSlots.zoneIndex])}`,
+ viewport,
+ gravityInfluences: visibleBodies.map(toGravityInfluence),
+ bodies: visibleBodies.map(toBodyView),
+ };
+}
+
+export function projectSelectedObjectFromFrame(
+ frameDocument: unknown,
+ request: SelectedObjectRequest,
+): SelectedObjectProjection {
+ const context = frameContext(frameDocument);
+ const entity = context.entities.find(candidate => num(candidate[entitySlots.entityIndex], -1) === request.entityIndex);
+ return {
+ schema: AetheriaRtsSchemas.selectedObject,
+ frameId: context.frameId,
+ runId: context.runId,
+ zoneIndex: context.zoneIndex,
+ entityIndex: request.entityIndex,
+ selected: entity ? toViewObject(entity, context.runId, context.zoneIndex) : null,
+ };
+}
+
+export function projectInventoryFromFrame(frameDocument: unknown, request: SelectedObjectRequest): InventoryProjection {
+ const context = frameContext(frameDocument);
+ const entity = context.entities.find(candidate => num(candidate[entitySlots.entityIndex], -1) === request.entityIndex);
+ const allItems = entity ? inventory(entity) : [];
+ return {
+ schema: AetheriaRtsSchemas.inventory,
+ frameId: context.frameId,
+ runId: context.runId,
+ zoneIndex: context.zoneIndex,
+ entityIndex: request.entityIndex,
+ entityKey: entity ? entityKey(context.runId, context.zoneIndex, request.entityIndex) : "",
+ items: allItems,
+ equipment: allItems.filter(item => item.source === "equipment"),
+ cargo: allItems.filter(item => item.source === "cargo"),
+ };
+}
+
+export function projectDaemonHealth(healthDocument: unknown): DaemonHealthProjection {
+ const health = arr(healthDocument);
+ return {
+ schema: str(health[healthSlots.schema]) || AetheriaRtsSchemas.daemonHealth,
+ daemonId: str(health[healthSlots.daemonId]),
+ verseId: str(health[healthSlots.verseId]),
+ publishedAtUtc: str(health[healthSlots.publishedAtUtc]),
+ statePath: str(health[healthSlots.statePath]),
+ frameId: num(health[healthSlots.frameId]),
+ observedCommandCount: num(health[healthSlots.observedCommandCount]),
+ appliedCommandCount: num(health[healthSlots.appliedCommandCount]),
+ rejectedCommandCount: num(health[healthSlots.rejectedCommandCount]),
+ status: str(health[healthSlots.status]) || "unknown",
+ publicationSource: str(health[healthSlots.publicationSource]),
+ transport: str(health[healthSlots.transport]),
+ commandBoundaryPath: str(health[healthSlots.commandBoundaryPath]),
+ };
+}
+
+export function projectAuthorityStatus(policyDocument: unknown): AuthorityStatusProjection {
+ const policy = arr(policyDocument);
+ return {
+ schema: str(policy[authorityPolicySlots.schema]) || AetheriaRtsSchemas.verseAuthorityPolicy,
+ verseId: str(policy[authorityPolicySlots.verseId]),
+ policyId: str(policy[authorityPolicySlots.policyId]),
+ ruleVersion: str(policy[authorityPolicySlots.ruleVersion]),
+ hostRuntimeId: str(policy[authorityPolicySlots.hostRuntimeId]),
+ defaultMode: str(policy[authorityPolicySlots.defaultMode]),
+ updatedAtUtc: str(policy[authorityPolicySlots.updatedAtUtc]),
+ rules: list(policy[authorityPolicySlots.rules]).map(rule => ({
+ ruleId: str(rule[authorityRuleSlots.ruleId]),
+ subjectPrefix: str(rule[authorityRuleSlots.subjectPrefix]),
+ claimKinds: stringList(rule[authorityRuleSlots.claimKinds]),
+ mode: str(rule[authorityRuleSlots.mode]),
+ runtimeIds: stringList(rule[authorityRuleSlots.runtimeIds]),
+ leaseScope: str(rule[authorityRuleSlots.leaseScope]),
+ priority: num(rule[authorityRuleSlots.priority]),
+ })),
+ };
+}
+
+export function projectStarbridgeSessionSummary(summaryDocument: unknown): StarbridgeSessionProjection {
+ const summary = arr(summaryDocument);
+ return {
+ schema: str(summary[starbridgeSummarySlots.schema]) || AetheriaRtsSchemas.starbridgeSessionSummary,
+ frameId: num(summary[starbridgeSummarySlots.frameId]),
+ publishedAtUtc: str(summary[starbridgeSummarySlots.publishedAtUtc]),
+ sessionId: str(summary[starbridgeSummarySlots.sessionId]) || "starbridge-session",
+ scenarioId: str(summary[starbridgeSummarySlots.scenarioId]) || "starbridge.local",
+ scenarioName: str(summary[starbridgeSummarySlots.scenarioName]) || "Starbridge",
+ runId: str(summary[starbridgeSummarySlots.runId]) || "local-starbridge",
+ zoneIndex: num(summary[starbridgeSummarySlots.zoneIndex]),
+ zoneName: str(summary[starbridgeSummarySlots.zoneName]),
+ phase: str(summary[starbridgeSummarySlots.phase]) || "setup",
+ currentWaveIndex: num(summary[starbridgeSummarySlots.currentWaveIndex]),
+ baseStatus: toStarbridgeBaseStatus(arr(summary[starbridgeSummarySlots.baseStatus])),
+ stationStock: list(summary[starbridgeSummarySlots.stationStock]).map(toStarbridgeStationStockItem),
+ waveForecast: list(summary[starbridgeSummarySlots.waveForecast]).map(toStarbridgeWaveForecast),
+ runtimeRoles: list(summary[starbridgeSummarySlots.runtimeRoles]).map(toStarbridgeRuntimeRole),
+ };
+}
+
+function frameContext(frameDocument: unknown): {
+ frameId: number;
+ runId: string;
+ zoneIndex: number;
+ entities: unknown[][];
+} {
+ const frame = arr(frameDocument);
+ const run = arr(frame[frameSlots.run]);
+ const zones = list(run[runSlots.zones]);
+ const currentZoneIndex = num(run[runSlots.currentZoneIndex], -1);
+ const zone = zones.find(candidate => num(candidate[zoneSlots.zoneIndex], -1) === currentZoneIndex) ??
+ zones[0] ??
+ [];
+ return {
+ frameId: num(frame[frameSlots.frameId]),
+ runId: str(run[runSlots.runId]) || "local-rts",
+ zoneIndex: num(zone[zoneSlots.zoneIndex]),
+ entities: list(zone[zoneSlots.entities]),
+ };
+}
+
+function normalizeViewport(request: ViewportRequest): ViewportRequest {
+ return cultMeshViewportRequest(
+ cultMeshRectFromBounds(request.minX, request.minY, request.maxX, request.maxY),
+ request.controlledEntityIndices,
+ );
+}
+
+function toViewObject(entity: unknown[], runId: string, zoneIndex: number): ViewObject {
+ return {
+ entityIndex: num(entity[entitySlots.entityIndex], -1),
+ entityKey: entityKey(runId, zoneIndex, num(entity[entitySlots.entityIndex], -1)),
+ displayName: str(entity[entitySlots.name]),
+ kind: str(entity[entitySlots.kind]),
+ factionKey: str(entity[entitySlots.factionKey]),
+ x: num(entity[entitySlots.positionX]),
+ y: num(entity[entitySlots.positionZ]),
+ z: num(entity[entitySlots.positionY]),
+ directionX: num(entity[entitySlots.directionX]),
+ directionY: num(entity[entitySlots.directionY]),
+ velocityX: num(entity[entitySlots.velocityX]),
+ velocityY: num(entity[entitySlots.velocityY]),
+ controlled: isPlayerControlled(entity),
+ targetEntityIndex: num(entity[entitySlots.targetEntityIndex], -1),
+ isActive: bool(entity[entitySlots.isActive]),
+ visibility: num(entity[entitySlots.visibility]),
+ status: {
+ hull: stat(entity, "hull"),
+ shield: stat(entity, "shield"),
+ heat: stat(entity, "heat"),
+ },
+ inventory: inventory(entity),
+ };
+}
+
+function toStarbridgeBaseStatus(status: unknown[]): StarbridgeSessionProjection["baseStatus"] {
+ return {
+ entityKey: str(status[starbridgeBaseSlots.entityKey]),
+ displayName: str(status[starbridgeBaseSlots.displayName]),
+ hull: num(status[starbridgeBaseSlots.hull]),
+ shield: num(status[starbridgeBaseSlots.shield]),
+ heat: num(status[starbridgeBaseSlots.heat]),
+ isActive: bool(status[starbridgeBaseSlots.isActive]),
+ };
+}
+
+function toStarbridgeStationStockItem(item: unknown[]): StarbridgeSessionProjection["stationStock"][number] {
+ return {
+ itemKey: str(item[starbridgeStockSlots.itemKey]),
+ quantity: num(item[starbridgeStockSlots.quantity]),
+ quality: num(item[starbridgeStockSlots.quality]),
+ durability: num(item[starbridgeStockSlots.durability]),
+ source: str(item[starbridgeStockSlots.source]) || "station",
+ };
+}
+
+function toStarbridgeWaveForecast(wave: unknown[]): StarbridgeSessionProjection["waveForecast"][number] {
+ return {
+ waveIndex: num(wave[starbridgeWaveForecastSlots.waveIndex]),
+ displayName: str(wave[starbridgeWaveForecastSlots.displayName]),
+ attackerKeys: stringList(wave[starbridgeWaveForecastSlots.attackerKeys]),
+ bossKey: str(wave[starbridgeWaveForecastSlots.bossKey]),
+ recoveredTechnologyKeys: stringList(wave[starbridgeWaveForecastSlots.recoveredTechnologyKeys]),
+ };
+}
+
+function toStarbridgeRuntimeRole(role: unknown[]): StarbridgeSessionProjection["runtimeRoles"][number] {
+ return {
+ runtimeId: str(role[starbridgeRoleSlots.runtimeId]),
+ role: str(role[starbridgeRoleSlots.role]),
+ entityKey: str(role[starbridgeRoleSlots.entityKey]),
+ };
+}
+
+function toGravityInfluence(body: unknown[]): GravityInfluence {
+ return {
+ bodyKey: str(body[bodySlots.bodyKey]),
+ orbitKey: str(body[bodySlots.orbitKey]),
+ kind: str(body[bodySlots.kind]),
+ x: num(body[bodySlots.gravityInfluenceCenterX]),
+ y: num(body[bodySlots.gravityInfluenceCenterZ]),
+ radius: resolveGravityRadius(body),
+ gravityDepth: num(body[bodySlots.gravityWellDepth]),
+ gravityDepthExponent: num(body[bodySlots.gravityDepthExponent]),
+ waveRadius: num(body[bodySlots.gravityWaveRadius]),
+ waveDepth: num(body[bodySlots.gravityWaveDepth]),
+ waveSpeed: num(body[bodySlots.gravityWaveSpeed]),
+ };
+}
+
+function toBodyView(body: unknown[]): BodyView {
+ return {
+ bodyKey: str(body[bodySlots.bodyKey]),
+ orbitKey: str(body[bodySlots.orbitKey]),
+ name: str(body[bodySlots.name]),
+ kind: str(body[bodySlots.kind]),
+ x: num(body[bodySlots.gravityInfluenceCenterX]),
+ y: num(body[bodySlots.gravityInfluenceCenterZ]),
+ radius: Math.max(32, num(body[bodySlots.bodyRadiusMultiplier]) * 70),
+ isAsteroidBelt: str(body[bodySlots.kind]).toLowerCase().includes("asteroid"),
+ };
+}
+
+function inventory(entity: unknown[]): InventoryItem[] {
+ const items: InventoryItem[] = [];
+ for (const slot of list(entity[entitySlots.equipment]))
+ addSlot(items, "equipment", slot);
+ for (const bay of list(entity[entitySlots.cargoContents])) {
+ for (const slot of list(bay[cargoBaySlots.items]))
+ addSlot(items, "cargo", slot);
+ }
+
+ return items.filter(item => item.itemKey.length > 0);
+}
+
+function addSlot(items: InventoryItem[], source: string, slot: unknown[]): void {
+ const item = arr(slot[itemSlotSlots.item]);
+ items.push({
+ source,
+ itemKey: str(item[itemSlots.itemKey]),
+ quantity: num(item[itemSlots.quantity]),
+ quality: num(item[itemSlots.quality]),
+ durability: num(item[itemSlots.durability]),
+ enabled: bool(item[itemSlots.enabled]),
+ });
+}
+
+function entityIntersectsViewport(entity: unknown[], viewport: ViewportRequest): boolean {
+ const x = num(entity[entitySlots.positionX]);
+ const y = num(entity[entitySlots.positionZ]);
+ return x >= viewport.minX && x <= viewport.maxX && y >= viewport.minY && y <= viewport.maxY;
+}
+
+function gravityInfluenceIntersectsViewport(body: unknown[], viewport: ViewportRequest): boolean {
+ const x = num(body[bodySlots.gravityInfluenceCenterX]);
+ const y = num(body[bodySlots.gravityInfluenceCenterZ]);
+ const radius = resolveGravityRadius(body);
+ return x + radius >= viewport.minX &&
+ x - radius <= viewport.maxX &&
+ y + radius >= viewport.minY &&
+ y - radius <= viewport.maxY;
+}
+
+function canSee(observer: unknown[], target: unknown[]): boolean {
+ if (num(observer[entitySlots.entityIndex], -1) === num(target[entitySlots.entityIndex], -2))
+ return true;
+
+ const dx = num(observer[entitySlots.positionX]) - num(target[entitySlots.positionX]);
+ const dy = num(observer[entitySlots.positionZ]) - num(target[entitySlots.positionZ]);
+ const range = Math.max(180, num(observer[entitySlots.visibility]));
+ return dx * dx + dy * dy <= range * range;
+}
+
+function isPlayerControlled(entity: unknown[]): boolean {
+ return str(entity[entitySlots.factionKey]).toLowerCase() === "player";
+}
+
+function stat(entity: unknown[], name: string): number {
+ const grid = list(entity[entitySlots.statGrids])
+ .find(candidate => str(candidate[statGridSlots.name]).toLowerCase() === name.toLowerCase());
+ return grid ? numberList(grid[statGridSlots.values])[0] ?? 0 : 0;
+}
+
+function resolveGravityRadius(body: unknown[]): number {
+ const explicit = num(body[bodySlots.gravityInfluenceRadius]);
+ if (explicit > 0)
+ return explicit;
+ return Math.max(32, num(body[bodySlots.bodyRadiusMultiplier]) * 70);
+}
+
+function entityKey(runId: string, zoneIndex: number, entityIndex: number): string {
+ return `global:aetheria.run_state.${runId}.zone.${zoneIndex}.entity.${entityIndex}.v1`;
+}
+
+function arr(value: unknown): unknown[] {
+ return Array.isArray(value) ? value : [];
+}
+
+function list(value: unknown): T[] {
+ return Array.isArray(value) ? value.filter(Array.isArray) as T[] : [];
+}
+
+function numberList(value: unknown): number[] {
+ return Array.isArray(value) ? value.map(candidate => num(candidate)) : [];
+}
+
+function stringList(value: unknown): string[] {
+ return Array.isArray(value) ? value.map(candidate => str(candidate)).filter(candidate => candidate.length > 0) : [];
+}
+
+function str(value: unknown): string {
+ return typeof value === "string" ? value : "";
+}
+
+function num(value: unknown, fallback = 0): number {
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
+}
+
+function bool(value: unknown): boolean {
+ return value === true;
+}
diff --git a/Aetheria.Rts.Web/Electron/main.ts b/Aetheria.Rts.Web/Electron/main.ts
new file mode 100644
index 000000000..f44576402
--- /dev/null
+++ b/Aetheria.Rts.Web/Electron/main.ts
@@ -0,0 +1,424 @@
+import { app, BrowserWindow, ipcMain, shell } from "electron";
+import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
+import { createWriteStream, existsSync, mkdirSync, writeFileSync } from "node:fs";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+import {
+ AetheriaCultMeshClient,
+} from "./aetheria-cultmesh.js";
+import { registerAetheriaRtsIpcHandlers } from "./aetheria-rts-generated-bindings.js";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const projectRoot = resolve(__dirname, "..");
+const repoRoot = resolve(projectRoot, "..");
+const logsRoot = resolve(projectRoot, "logs");
+const runtimeRoot = resolve(process.env.AETHERIA_RTS_RUNTIME_ROOT ?? resolve(projectRoot, "runtime"));
+const runtimeStatePath = resolve(runtimeRoot, "aetheria-rts.cc");
+const daemonDll = resolve(repoRoot, "Aetheria.State.Daemon", "bin", "Debug", "net10.0", "Aetheria.State.Daemon.dll");
+const rendererIndex = resolve(projectRoot, "wwwroot", "index.html");
+const rtsCultMeshPort = Number.parseInt(process.env.AETHERIA_RTS_CULTMESH_PORT ?? "3076", 10);
+const configuredDaemonEndpoint = process.env.AETHERIA_RTS_DAEMON_ENDPOINT?.trim() ?? "";
+const launchLocalDaemon = configuredDaemonEndpoint.length === 0;
+const rtsCultMeshEndpoint = configuredDaemonEndpoint || `rudp://127.0.0.1:${rtsCultMeshPort}`;
+const rtsVerseId = process.env.AETHERIA_RTS_VERSE_ID?.trim() || "aetheria.local";
+const rtsDaemonId = process.env.AETHERIA_RTS_DAEMON_ID?.trim() || "starfire-rts";
+const rtsCultMeshAdvertiseHost = process.env.AETHERIA_RTS_CULTMESH_ADVERTISE_HOST?.trim() || "127.0.0.1";
+const peerCultMeshEndpoints = parseEndpointList(process.env.AETHERIA_RTS_PEER_CULTMESH_ENDPOINTS);
+const electronSmoke = process.env.AETHERIA_RTS_ELECTRON_SMOKE === "1";
+const electronSmokeResultPath = process.env.AETHERIA_RTS_ELECTRON_SMOKE_RESULT;
+
+let daemonProcess: ChildProcessWithoutNullStreams | null = null;
+let mainWindow: BrowserWindow | null = null;
+let rtsClient: AetheriaCultMeshClient | null = null;
+let isQuitting = false;
+
+app.whenReady().then(async () => {
+ mainWindow = createWindow();
+ registerIpc();
+ showStartup("Preparing Aetheria RTS", "Building daemon if needed.");
+
+ try {
+ if (launchLocalDaemon) {
+ await ensureDotnetBuild();
+ showStartup("Launching Aetheria RTS", "Starting the Aetheria daemon.");
+ mkdirSync(runtimeRoot, { recursive: true });
+ daemonProcess = startDotnet("aetheria-daemon", daemonDll, [
+ "--state",
+ runtimeStatePath,
+ "--verse-id",
+ rtsVerseId,
+ "--daemon-id",
+ rtsDaemonId,
+ "--rts-cultmesh-port",
+ rtsCultMeshPort.toString(),
+ "--rts-cultmesh-advertise-host",
+ rtsCultMeshAdvertiseHost,
+ "--tick-interval-ms",
+ "20",
+ "--fixed-delta-ms",
+ "20",
+ "--api-publication-interval-ms",
+ "100000",
+ ...peerCultMeshEndpoints.flatMap(endpoint => ["--peer-cultmesh-endpoint", endpoint]),
+ ]);
+ } else {
+ showStartup("Connecting Aetheria RTS", `Using daemon ${rtsCultMeshEndpoint}.`);
+ mkdirSync(runtimeRoot, { recursive: true });
+ }
+
+ rtsClient = new AetheriaCultMeshClient(
+ rtsCultMeshEndpoint,
+ runtimeStatePath,
+ "aetheria-rts-electron",
+ { publicationMode: launchLocalDaemon ? "local" : "remote" });
+
+ showStartup("Launching Aetheria RTS", "Waiting for the daemon CultMesh frame.");
+ await rtsClient.waitForFrame(30000);
+ await mainWindow.loadFile(rendererIndex);
+ if (electronSmoke) {
+ const result = await runElectronSmoke(mainWindow);
+ writeElectronSmokeResult({ ok: true, result });
+ console.log(JSON.stringify(result, null, 2));
+ exitElectronSmoke(0);
+ }
+ } catch (error) {
+ await showFailure(error);
+ if (electronSmoke) {
+ writeElectronSmokeResult({
+ ok: false,
+ error: error instanceof Error ? error.stack ?? error.message : String(error),
+ });
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error));
+ exitElectronSmoke(1);
+ }
+ }
+});
+
+app.on("window-all-closed", () => {
+ app.quit();
+});
+
+app.on("before-quit", () => {
+ isQuitting = true;
+ void rtsClient?.close();
+ stopChild(daemonProcess);
+});
+
+function createWindow(): BrowserWindow {
+ const window = new BrowserWindow({
+ width: 1440,
+ height: 900,
+ minWidth: 1000,
+ minHeight: 640,
+ show: !electronSmoke,
+ backgroundColor: "#0b1016",
+ title: "Aetheria RTS",
+ webPreferences: {
+ contextIsolation: true,
+ nodeIntegration: false,
+ preload: resolve(__dirname, "preload.cjs"),
+ },
+ });
+
+ window.webContents.setWindowOpenHandler(({ url }) => {
+ void shell.openExternal(url);
+ return { action: "deny" };
+ });
+
+ return window;
+}
+
+async function runElectronSmoke(window: BrowserWindow): Promise> {
+ const started = Date.now();
+ let lastResult: Record | null = null;
+ while (Date.now() - started < 15000) {
+ lastResult = await window.webContents.executeJavaScript(`
+ (async () => {
+ 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 health = api ? await api.daemonHealth() : null;
+ const authority = api ? await api.authorityStatus() : null;
+ const starbridge = api ? await api.starbridgeSession() : null;
+ const surfaceCatalog = api ? await api.surfaceCatalog() : null;
+ 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,
+ }) : null;
+ return {
+ hasApi: !!api &&
+ typeof api.mapViewport === "function" &&
+ typeof api.objectsViewport === "function" &&
+ typeof api.gravityViewport === "function" &&
+ typeof api.selectedObject === "function" &&
+ typeof api.inventory === "function" &&
+ typeof api.daemonHealth === "function" &&
+ typeof api.authorityStatus === "function" &&
+ typeof api.starbridgeSession === "function" &&
+ typeof api.surfaceCatalog === "function" &&
+ typeof api.surfaceCatalogIndex === "function",
+ status,
+ selectionTitle,
+ selectionDetails,
+ frameDetails,
+ runtimeSurfaceDetails,
+ controlledText,
+ canvasClientWidth: canvas?.clientWidth ?? 0,
+ canvasClientHeight: canvas?.clientHeight ?? 0,
+ canvasWidth: canvas?.width ?? 0,
+ canvasHeight: canvas?.height ?? 0,
+ health,
+ authority,
+ starbridge,
+ surfaceCatalog,
+ surfaceCatalogIndex,
+ moveReceipt
+ };
+ })()
+ `, true) as Record;
+
+ if (isElectronSmokeReady(lastResult))
+ return lastResult;
+ await delay(250);
+ }
+
+ throw new Error(`Aetheria RTS Electron smoke did not reach ready state. Last result: ${JSON.stringify(lastResult)}`);
+}
+
+function isElectronSmokeReady(result: Record): 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 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);
+ 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("shared-memory") &&
+ 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 &&
+ health?.status === "healthy" &&
+ authority?.policyId === "aetheria.trusted-coop.v1" &&
+ starbridge?.scenarioName === "Frontier Fabricator Defense" &&
+ 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;
+}
+
+function stringValue(value: unknown): string {
+ return typeof value === "string" ? value : "";
+}
+
+function numberValue(value: unknown): number {
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
+}
+
+function objectValue(value: unknown): Record | null {
+ return value != null && typeof value === "object" ? value as Record : null;
+}
+
+function arrayValue(value: unknown): unknown[] {
+ return Array.isArray(value) ? value : [];
+}
+
+function writeElectronSmokeResult(result: Record): void {
+ if (!electronSmokeResultPath)
+ return;
+
+ writeFileSync(electronSmokeResultPath, JSON.stringify(result, null, 2), "utf8");
+}
+
+function exitElectronSmoke(exitCode: number): void {
+ isQuitting = true;
+ void rtsClient?.close();
+ rtsClient = null;
+ stopChild(daemonProcess);
+ daemonProcess = null;
+ mainWindow?.destroy();
+ mainWindow = null;
+ setImmediate(() => app.exit(exitCode));
+}
+
+function registerIpc(): void {
+ registerAetheriaRtsIpcHandlers(
+ ipcMain,
+ requireClient,
+ () => ({
+ status: "ok",
+ transport: "cultmesh-rudp",
+ endpoint: rtsCultMeshEndpoint,
+ verseId: rtsVerseId,
+ daemonId: rtsDaemonId,
+ peerEndpoints: peerCultMeshEndpoints,
+ daemonRunning: daemonProcess != null && !daemonProcess.killed,
+ daemonMode: launchLocalDaemon ? "local" : "remote",
+ }));
+}
+
+function parseEndpointList(value: string | undefined): string[] {
+ if (!value)
+ return [];
+
+ return value
+ .split(/[;,]/u)
+ .map(endpoint => endpoint.trim())
+ .filter(endpoint => endpoint.length > 0);
+}
+
+function requireClient(): AetheriaCultMeshClient {
+ if (!rtsClient)
+ throw new Error("Aetheria RTS CultMesh client is not initialized.");
+ return rtsClient;
+}
+
+async function ensureDotnetBuild(): Promise {
+ if (existsSync(daemonDll))
+ return;
+
+ await runProcess("dotnet", ["build", resolve(repoRoot, "Aetheria.State.Daemon", "Aetheria.State.Daemon.csproj")], "Aetheria.State.Daemon.build");
+}
+
+function startDotnet(name: string, dllPath: string, args: string[]): ChildProcessWithoutNullStreams {
+ mkdirSync(logsRoot, { recursive: true });
+ const child = spawn("dotnet", [dllPath, ...args], {
+ cwd: repoRoot,
+ windowsHide: true,
+ });
+ pipeChildLogs(name, child);
+ child.once("exit", (code, signal) => {
+ if (!isQuitting) {
+ void showFailure(new Error(`${name} exited unexpectedly (${signal ?? code ?? "unknown"}). Check ${logsRoot}.`));
+ }
+ });
+ return child;
+}
+
+function runProcess(command: string, args: string[], logName: string): Promise {
+ mkdirSync(logsRoot, { recursive: true });
+ const child = spawn(command, args, {
+ cwd: repoRoot,
+ windowsHide: true,
+ });
+ pipeChildLogs(logName, child);
+ return new Promise((resolvePromise, reject) => {
+ child.once("error", reject);
+ child.once("exit", code => {
+ if (code === 0) {
+ resolvePromise();
+ return;
+ }
+
+ reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code}. Check ${logsRoot}.`));
+ });
+ });
+}
+
+function pipeChildLogs(name: string, child: ChildProcessWithoutNullStreams): void {
+ const stdout = createWriteStream(resolve(logsRoot, `${name}.log`), { flags: "a" });
+ const stderr = createWriteStream(resolve(logsRoot, `${name}.err.log`), { flags: "a" });
+ child.stdout.pipe(stdout);
+ child.stderr.pipe(stderr);
+}
+
+function stopChild(child: ChildProcessWithoutNullStreams | null): void {
+ if (!child || child.killed)
+ return;
+
+ child.kill();
+}
+
+function delay(milliseconds: number): Promise {
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
+}
+
+function showStartup(title: string, detail: string): void {
+ void mainWindow?.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(startupHtml(title, detail))}`);
+}
+
+async function showFailure(error: unknown): Promise {
+ const message = error instanceof Error ? error.message : String(error);
+ await mainWindow?.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(startupHtml("Aetheria RTS failed to start", message))}`);
+}
+
+function startupHtml(title: string, detail: string): string {
+ return `
+
+
+
+ ${escapeHtml(title)}
+
+
+
+
+ ${escapeHtml(title)}
+ ${escapeHtml(detail)}
+
+
+`;
+}
+
+function escapeHtml(value: string): string {
+ return value
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """);
+}
diff --git a/Aetheria.Rts.Web/Electron/preload.cjs b/Aetheria.Rts.Web/Electron/preload.cjs
new file mode 100644
index 000000000..4a0382b59
--- /dev/null
+++ b/Aetheria.Rts.Web/Electron/preload.cjs
@@ -0,0 +1,53 @@
+//
+// Generated by scripts/generate-rts-bindings.mjs from the RTS IPC surface contract.
+
+const { contextBridge, ipcRenderer } = require("electron");
+
+const channels = {
+ mapViewport: "aetheria-rts:map-viewport",
+ objectsViewport: "aetheria-rts:objects-viewport",
+ gravityViewport: "aetheria-rts:gravity-viewport",
+ viewportFeed: "aetheria-rts:viewport-feed",
+ viewportFeedStop: "aetheria-rts:viewport-feed-stop",
+ viewportFeedUpdate: "aetheria-rts:viewport-feed-update",
+ selectedObject: "aetheria-rts:selected-object",
+ inventory: "aetheria-rts:inventory",
+ daemonHealth: "aetheria-rts:daemon-health",
+ authorityStatus: "aetheria-rts:authority-status",
+ starbridgeSession: "aetheria-rts:starbridge-session",
+ setMoveVector: "aetheria-rts:set-move-vector",
+ setTarget: "aetheria-rts:set-target",
+ surfaceCatalog: "aetheria-rts:surface-catalog",
+ surfaceCatalogIndex: "aetheria-rts:surface-catalog-index",
+ health: "aetheria-rts:health",
+};
+
+contextBridge.exposeInMainWorld("aetheriaRts", {
+ mapViewport: request => ipcRenderer.invoke(channels.mapViewport, request),
+ objectsViewport: request => ipcRenderer.invoke(channels.objectsViewport, request),
+ gravityViewport: request => ipcRenderer.invoke(channels.gravityViewport, request),
+ watchViewportFeed: (request, callback) => {
+ const subscriptionId = `viewport-feed-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
+ const listener = (_event, message) => {
+ if (message?.subscriptionId === subscriptionId) {
+ callback(message.snapshot);
+ }
+ };
+ ipcRenderer.on(channels.viewportFeedUpdate, listener);
+ void ipcRenderer.invoke(channels.viewportFeed, { subscriptionId, feed: request });
+ return () => {
+ ipcRenderer.off(channels.viewportFeedUpdate, listener);
+ void ipcRenderer.invoke(channels.viewportFeedStop, subscriptionId);
+ };
+ },
+ selectedObject: request => ipcRenderer.invoke(channels.selectedObject, request),
+ inventory: request => ipcRenderer.invoke(channels.inventory, request),
+ daemonHealth: () => ipcRenderer.invoke(channels.daemonHealth),
+ authorityStatus: () => ipcRenderer.invoke(channels.authorityStatus),
+ starbridgeSession: () => ipcRenderer.invoke(channels.starbridgeSession),
+ setMoveVector: request => ipcRenderer.invoke(channels.setMoveVector, request),
+ setTarget: request => ipcRenderer.invoke(channels.setTarget, request),
+ surfaceCatalog: () => ipcRenderer.invoke(channels.surfaceCatalog),
+ surfaceCatalogIndex: () => ipcRenderer.invoke(channels.surfaceCatalogIndex),
+ health: () => ipcRenderer.invoke(channels.health),
+});
diff --git a/Aetheria.Rts.Web/package-lock.json b/Aetheria.Rts.Web/package-lock.json
new file mode 100644
index 000000000..88fa115a6
--- /dev/null
+++ b/Aetheria.Rts.Web/package-lock.json
@@ -0,0 +1,275 @@
+{
+ "name": "aetheria-rts-web",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "aetheria-rts-web",
+ "version": "0.1.0",
+ "dependencies": {
+ "@msgpack/msgpack": "^3.1.2",
+ "cultcache-ts": "file:../../CultLib/packages/cultcache-ts",
+ "cultmesh-ts": "file:../../CultLib/packages/cultmesh-ts",
+ "cultnet-ts": "file:../../CultLib/packages/cultnet-ts"
+ },
+ "devDependencies": {
+ "@types/node": "^26.0.0",
+ "electron": "^42.4.1",
+ "typescript": "^5.6.3"
+ }
+ },
+ "../../CultLib/packages/cultcache-ts": {
+ "version": "0.13.5",
+ "license": "MIT",
+ "dependencies": {
+ "@msgpack/msgpack": "^3.1.3",
+ "zod": "^3.25.76"
+ },
+ "devDependencies": {
+ "@types/node": "^24.0.0",
+ "typescript": "^5.9.0"
+ }
+ },
+ "../../CultLib/packages/cultmesh-ts": {
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@msgpack/msgpack": "^3.1.2",
+ "cultcache-ts": "file:../cultcache-ts",
+ "cultnet-ts": "file:../cultnet-ts"
+ },
+ "devDependencies": {
+ "@types/node": "^24.0.0",
+ "typescript": "^5.9.0",
+ "zod": "^3.25.76"
+ }
+ },
+ "../../CultLib/packages/cultnet-ts": {
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@msgpack/msgpack": "^3.1.3",
+ "ajv": "^8.17.1",
+ "cultcache-ts": "file:../cultcache-ts"
+ },
+ "devDependencies": {
+ "@types/node": "^24.0.0",
+ "json-schema-to-typescript": "^15.0.4",
+ "typescript": "^5.9.0",
+ "zod": "^3.25.76"
+ }
+ },
+ "node_modules/@electron-internal/extract-zip": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.3.tgz",
+ "integrity": "sha512-OjKpjB7gohtEjZiq6nDx1egqjZJhGPN1iFOIED+NFhB/MMkXw/XRcHjh1DGXKT5z2W9eW7Jy2UKU3gpjvusFTQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@electron/get": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz",
+ "integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "env-paths": "^3.0.0",
+ "graceful-fs": "^4.2.11",
+ "progress": "^2.0.3",
+ "semver": "^7.6.3",
+ "sumchecker": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "optionalDependencies": {
+ "undici": "^7.24.4"
+ }
+ },
+ "node_modules/@msgpack/msgpack": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
+ "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "26.0.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz",
+ "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~8.3.0"
+ }
+ },
+ "node_modules/cultcache-ts": {
+ "resolved": "../../CultLib/packages/cultcache-ts",
+ "link": true
+ },
+ "node_modules/cultmesh-ts": {
+ "resolved": "../../CultLib/packages/cultmesh-ts",
+ "link": true
+ },
+ "node_modules/cultnet-ts": {
+ "resolved": "../../CultLib/packages/cultnet-ts",
+ "link": true
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/electron": {
+ "version": "42.4.1",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-42.4.1.tgz",
+ "integrity": "sha512-8CYHJP5O4wFO+ycoJR98yy907MmPeo+vWXrzjxmGGgRNKqv8pOjjm+wphO0CCgQJnBU7+QUPSJS4QXhbKrO50w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@electron-internal/extract-zip": "^1.0.1",
+ "@electron/get": "^5.0.0",
+ "@types/node": "^24.9.0"
+ },
+ "bin": {
+ "electron": "cli.js",
+ "install-electron": "install.js"
+ },
+ "engines": {
+ "node": ">= 22.12.0"
+ }
+ },
+ "node_modules/electron/node_modules/@types/node": {
+ "version": "24.13.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
+ "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/electron/node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/env-paths": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
+ "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sumchecker": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
+ "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "debug": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
+ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
+ "dev": true,
+ "license": "MIT"
+ }
+ }
+}
diff --git a/Aetheria.Rts.Web/package.json b/Aetheria.Rts.Web/package.json
new file mode 100644
index 000000000..d399daf22
--- /dev/null
+++ b/Aetheria.Rts.Web/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "aetheria-rts-web",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "main": "electron-dist/main.js",
+ "scripts": {
+ "build": "C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.json && C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.electron.json && C:\\Progra~1\\nodejs\\node.exe scripts/copy-electron-preload.mjs",
+ "build:renderer": "C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.json",
+ "build:electron": "C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.electron.json && C:\\Progra~1\\nodejs\\node.exe scripts/copy-electron-preload.mjs",
+ "generate:rts-bindings": "C:\\Progra~1\\nodejs\\node.exe scripts/generate-rts-bindings.mjs",
+ "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: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"
+ },
+ "devDependencies": {
+ "@types/node": "^26.0.0",
+ "electron": "^42.4.1",
+ "typescript": "^5.6.3"
+ },
+ "dependencies": {
+ "@msgpack/msgpack": "^3.1.2",
+ "cultcache-ts": "file:../../CultLib/packages/cultcache-ts",
+ "cultmesh-ts": "file:../../CultLib/packages/cultmesh-ts",
+ "cultnet-ts": "file:../../CultLib/packages/cultnet-ts"
+ }
+}
diff --git a/Aetheria.Rts.Web/scripts/copy-electron-preload.mjs b/Aetheria.Rts.Web/scripts/copy-electron-preload.mjs
new file mode 100644
index 000000000..474429ac7
--- /dev/null
+++ b/Aetheria.Rts.Web/scripts/copy-electron-preload.mjs
@@ -0,0 +1,10 @@
+import { copyFileSync, rmSync } from "node:fs";
+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 output = resolve(root, "electron-dist", "preload.cjs");
+
+rmSync(legacyOutput, { force: true });
+copyFileSync(source, output);
diff --git a/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs b/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs
new file mode 100644
index 000000000..da47f79a9
--- /dev/null
+++ b/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs
@@ -0,0 +1,1273 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const scriptPath = fileURLToPath(import.meta.url);
+const root = resolve(scriptPath, "..", "..");
+const repoRoot = resolve(root, "..");
+
+const sources = [
+ resolve(repoRoot, "Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs"),
+ resolve(repoRoot, "Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveCommandDocument.cs"),
+ resolve(repoRoot, "Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs"),
+ resolve(repoRoot, "Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsViewportDocuments.cs"),
+ resolve(repoRoot, "Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs"),
+ resolve(repoRoot, "Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseAuthorityPolicy.cs"),
+];
+const outputPath = resolve(root, "Electron/aetheria-rts-generated-bindings.ts");
+const rendererContractPath = resolve(root, "Client/aetheria-rts-contract.ts");
+const preloadPath = resolve(root, "Electron/preload.cjs");
+const check = process.argv.includes("--check");
+
+const ipcChannels = [
+ ["mapViewport", "aetheria-rts:map-viewport"],
+ ["objectsViewport", "aetheria-rts:objects-viewport"],
+ ["gravityViewport", "aetheria-rts:gravity-viewport"],
+ ["viewportFeed", "aetheria-rts:viewport-feed"],
+ ["viewportFeedStop", "aetheria-rts:viewport-feed-stop"],
+ ["viewportFeedUpdate", "aetheria-rts:viewport-feed-update"],
+ ["selectedObject", "aetheria-rts:selected-object"],
+ ["inventory", "aetheria-rts:inventory"],
+ ["daemonHealth", "aetheria-rts:daemon-health"],
+ ["authorityStatus", "aetheria-rts:authority-status"],
+ ["starbridgeSession", "aetheria-rts:starbridge-session"],
+ ["setMoveVector", "aetheria-rts:set-move-vector"],
+ ["setTarget", "aetheria-rts:set-target"],
+ ["surfaceCatalog", "aetheria-rts:surface-catalog"],
+ ["surfaceCatalogIndex", "aetheria-rts:surface-catalog-index"],
+ ["health", "aetheria-rts:health"],
+];
+
+const sourceText = sources.map(path => readFileSync(path, "utf8")).join("\n");
+
+const enums = [
+ {
+ exportName: "AetheriaRuntimeDaemonCommandKinds",
+ entries: parseEnum(sourceText, "AetheriaRuntimeDaemonCommandKinds"),
+ },
+ {
+ exportName: "AetheriaRuntimeEveCommandKinds",
+ entries: parseEnum(sourceText, "AetheriaRuntimeEveCommandKind"),
+ },
+];
+const documents = [
+ {
+ exportName: "aetheriaRuntimeDaemonCommandDocument",
+ className: "AetheriaRuntimeDaemonCommandDocument",
+ schemaName: "daemonCommand",
+ },
+ {
+ exportName: "aetheriaRuntimeEveCommandDocument",
+ className: "AetheriaRuntimeEveCommandDocument",
+ schemaName: "eveCommand",
+ },
+ {
+ exportName: "aetheriaRuntimeDaemonFrameDocument",
+ className: "AetheriaRuntimeDaemonFrameDocument",
+ schemaName: "daemonFrame",
+ },
+ {
+ exportName: "aetheriaRuntimeDaemonHealthDocument",
+ className: "AetheriaRuntimeDaemonHealthDocument",
+ schemaName: "daemonHealth",
+ },
+ {
+ exportName: "aetheriaRuntimeRtsViewportDocument",
+ className: "AetheriaRuntimeRtsViewportDocument",
+ schemaName: "rtsViewport",
+ },
+ {
+ exportName: "aetheriaRuntimeObjectsViewportDocument",
+ className: "AetheriaRuntimeObjectsViewportDocument",
+ schemaName: "objectsViewport",
+ },
+ {
+ exportName: "aetheriaRuntimeGravityViewportDocument",
+ className: "AetheriaRuntimeGravityViewportDocument",
+ schemaName: "gravityViewport",
+ },
+ {
+ exportName: "aetheriaRuntimeCurrentZoneDocument",
+ className: "AetheriaRuntimeCurrentZoneDocument",
+ schemaName: "currentZone",
+ },
+ {
+ exportName: "aetheriaRuntimeCurrentEntityDocument",
+ className: "AetheriaRuntimeCurrentEntityDocument",
+ schemaName: "currentEntity",
+ },
+ {
+ exportName: "aetheriaRuntimeCurrentDockingDocument",
+ className: "AetheriaRuntimeCurrentDockingDocument",
+ schemaName: "currentDocking",
+ },
+ {
+ exportName: "aetheriaRuntimeStationRefitDocument",
+ className: "AetheriaRuntimeStationRefitDocument",
+ schemaName: "stationRefit",
+ },
+ {
+ exportName: "aetheriaRuntimeStationRefitEntityOption",
+ className: "AetheriaRuntimeStationRefitEntityOption",
+ },
+ {
+ exportName: "aetheriaRuntimeStationStockItem",
+ className: "AetheriaRuntimeStationStockItem",
+ },
+ {
+ exportName: "aetheriaRuntimeSectorMapDocument",
+ className: "AetheriaRuntimeSectorMapDocument",
+ schemaName: "sectorMap",
+ },
+ {
+ exportName: "aetheriaRuntimeSectorMapZone",
+ className: "AetheriaRuntimeSectorMapZone",
+ },
+ {
+ exportName: "aetheriaRuntimeSectorMapLink",
+ className: "AetheriaRuntimeSectorMapLink",
+ },
+ {
+ exportName: "aetheriaRuntimeZoneRenderDocument",
+ className: "AetheriaRuntimeZoneRenderDocument",
+ schemaName: "zoneRender",
+ },
+ {
+ exportName: "aetheriaRuntimeZoneRenderBodyPose",
+ className: "AetheriaRuntimeZoneRenderBodyPose",
+ },
+ {
+ exportName: "aetheriaRuntimeZoneRenderAsteroidBeltPose",
+ className: "AetheriaRuntimeZoneRenderAsteroidBeltPose",
+ },
+ {
+ exportName: "aetheriaRuntimeSelectedObjectDocument",
+ className: "AetheriaRuntimeSelectedObjectDocument",
+ schemaName: "selectedObject",
+ },
+ {
+ exportName: "aetheriaRuntimeInventoryDocument",
+ className: "AetheriaRuntimeInventoryDocument",
+ schemaName: "inventory",
+ },
+ {
+ exportName: "aetheriaRuntimeRunCheckpointCommit",
+ className: "AetheriaRuntimeRunCheckpointCommit",
+ },
+ {
+ exportName: "aetheriaRuntimeZoneSnapshotCommit",
+ className: "AetheriaRuntimeZoneSnapshotCommit",
+ },
+ {
+ exportName: "aetheriaRuntimeEntitySnapshotCommit",
+ className: "AetheriaRuntimeEntitySnapshotCommit",
+ },
+ {
+ exportName: "aetheriaRuntimeBodySnapshotCommit",
+ className: "AetheriaRuntimeBodySnapshotCommit",
+ },
+ {
+ exportName: "aetheriaRuntimeEntityStatGridCommit",
+ className: "AetheriaRuntimeEntityStatGridCommit",
+ },
+ {
+ exportName: "aetheriaRuntimeCargoBayLoadoutCommit",
+ className: "AetheriaRuntimeCargoBayLoadoutCommit",
+ },
+ {
+ exportName: "aetheriaRuntimeLoadoutItemSlotCommit",
+ className: "AetheriaRuntimeLoadoutItemSlotCommit",
+ },
+ {
+ exportName: "aetheriaRuntimeLoadoutItemCommit",
+ className: "AetheriaRuntimeLoadoutItemCommit",
+ },
+ {
+ exportName: "aetheriaRuntimeRtsViewportBounds",
+ className: "AetheriaRuntimeRtsViewportBounds",
+ },
+ {
+ exportName: "aetheriaRuntimeRtsViewportObject",
+ className: "AetheriaRuntimeRtsViewportObject",
+ },
+ {
+ exportName: "aetheriaRuntimeRtsEntityStatus",
+ className: "AetheriaRuntimeRtsEntityStatus",
+ },
+ {
+ exportName: "aetheriaRuntimeRtsInventoryItem",
+ className: "AetheriaRuntimeRtsInventoryItem",
+ },
+ {
+ exportName: "aetheriaRuntimeRtsGravityInfluence",
+ className: "AetheriaRuntimeRtsGravityInfluence",
+ },
+ {
+ exportName: "aetheriaRuntimeRtsBodyView",
+ className: "AetheriaRuntimeRtsBodyView",
+ },
+ {
+ exportName: "aetheriaRuntimeStarbridgeScenarioDocument",
+ className: "AetheriaRuntimeStarbridgeScenarioDocument",
+ schemaName: "starbridgeScenario",
+ },
+ {
+ exportName: "aetheriaRuntimeStarbridgeSessionDocument",
+ className: "AetheriaRuntimeStarbridgeSessionDocument",
+ schemaName: "starbridgeSession",
+ },
+ {
+ exportName: "aetheriaRuntimeStarbridgeSessionSummaryDocument",
+ className: "AetheriaRuntimeStarbridgeSessionSummaryDocument",
+ schemaName: "starbridgeSessionSummary",
+ },
+ {
+ exportName: "aetheriaRuntimeStarbridgeStationStockItem",
+ className: "AetheriaRuntimeStarbridgeStationStockItem",
+ },
+ {
+ exportName: "aetheriaRuntimeStarbridgeWaveDefinition",
+ className: "AetheriaRuntimeStarbridgeWaveDefinition",
+ },
+ {
+ exportName: "aetheriaRuntimeStarbridgeWaveForecast",
+ className: "AetheriaRuntimeStarbridgeWaveForecast",
+ },
+ {
+ exportName: "aetheriaRuntimeStarbridgeRuntimeRole",
+ className: "AetheriaRuntimeStarbridgeRuntimeRole",
+ },
+ {
+ exportName: "aetheriaRuntimeStarbridgeBaseStatus",
+ className: "AetheriaRuntimeStarbridgeBaseStatus",
+ },
+ {
+ exportName: "aetheriaRuntimeVerseAuthorityPolicyDocument",
+ className: "AetheriaRuntimeVerseAuthorityPolicyDocument",
+ schemaName: "verseAuthorityPolicy",
+ },
+ {
+ exportName: "aetheriaRuntimeAuthorityRule",
+ className: "AetheriaRuntimeAuthorityRule",
+ },
+ {
+ exportName: "aetheriaRuntimeAuthorityRuntimeRole",
+ className: "AetheriaRuntimeAuthorityRuntimeRole",
+ },
+ {
+ exportName: "aetheriaRuntimeAuthorityLeaseDocument",
+ className: "AetheriaRuntimeAuthorityLeaseDocument",
+ schemaName: "authorityLease",
+ },
+].map(document => ({
+ ...document,
+ slots: parseMessagePackSlots(sourceText, document.className),
+ schema: parseCultDocumentSchema(sourceText, document.className),
+}));
+
+const generated = render(enums, documents);
+const rendererContract = renderRendererContract();
+const preload = renderPreload();
+const existing = readExisting(outputPath);
+const existingRendererContract = readExisting(rendererContractPath);
+const existingPreload = readExisting(preloadPath);
+
+if (check) {
+ if (existing !== generated || existingRendererContract !== rendererContract || existingPreload !== preload) {
+ console.error("RTS generated bindings are stale. Run `npm run generate:rts-bindings`.");
+ process.exit(1);
+ }
+} else {
+ writeFileSync(outputPath, generated, "utf8");
+ writeFileSync(rendererContractPath, rendererContract, "utf8");
+ writeFileSync(preloadPath, preload, "utf8");
+}
+
+function readExisting(path) {
+ try {
+ return readFileSync(path, "utf8");
+ } catch {
+ return "";
+ }
+}
+
+function parseEnum(text, enumName) {
+ const body = readTypeBody(text, `enum ${enumName}`);
+ const entries = [];
+ let nextValue = 0;
+ for (const rawLine of body.split(/\r?\n/u)) {
+ const line = rawLine.replace(/\/\/.*$/u, "").trim().replace(/,$/u, "");
+ if (!line)
+ continue;
+
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)(?:\s*=\s*(-?\d+))?$/u.exec(line);
+ if (!match)
+ continue;
+
+ const value = match[2] === undefined ? nextValue : Number.parseInt(match[2], 10);
+ entries.push([toCamelCase(match[1]), value]);
+ nextValue = value + 1;
+ }
+
+ return entries;
+}
+
+function parseMessagePackSlots(text, className) {
+ const body = readTypeBody(text, `class ${className}`);
+ const slots = [];
+ const propertyPattern = /\[Key\((\d+)\)\][\s\S]*?public\s+[A-Za-z0-9_<>,.?[\]\s]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{\s*get;\s*set;\s*\}/gu;
+ for (const match of body.matchAll(propertyPattern)) {
+ slots.push([toCamelCase(match[2]), Number.parseInt(match[1], 10)]);
+ }
+
+ if (slots.length === 0)
+ throw new Error(`No [Key] properties found for ${className}.`);
+
+ return slots.sort((left, right) => left[1] - right[1]);
+}
+
+function parseCultDocumentSchema(text, className) {
+ const classIndex = text.indexOf(`class ${className}`);
+ if (classIndex < 0)
+ throw new Error(`Could not find ${className}.`);
+
+ const prefix = text.slice(Math.max(0, classIndex - 500), classIndex);
+ const matches = [...prefix.matchAll(/\[CultDocument\("([^"]+)",\s*"([^"]+)"\)\]/gu)];
+ return matches.length > 0 ? matches[matches.length - 1][2] : "";
+}
+
+function readTypeBody(text, declaration) {
+ const declarationIndex = text.indexOf(declaration);
+ if (declarationIndex < 0)
+ throw new Error(`Could not find ${declaration}.`);
+
+ const openIndex = text.indexOf("{", declarationIndex);
+ if (openIndex < 0)
+ throw new Error(`Could not find body for ${declaration}.`);
+
+ let depth = 0;
+ for (let index = openIndex; index < text.length; index += 1) {
+ const character = text[index];
+ if (character === "{")
+ depth += 1;
+ if (character === "}") {
+ depth -= 1;
+ if (depth === 0)
+ return text.slice(openIndex + 1, index);
+ }
+ }
+
+ throw new Error(`Could not read body for ${declaration}.`);
+}
+
+function render(enums, documents) {
+ const schemas = documents
+ .filter(document => document.schema)
+ .map(document => ` ${document.schemaName}: "${document.schema}",`)
+ .join("\n");
+ const enumBlocks = enums
+ .map(item => renderEnumBlock(item.exportName, item.entries))
+ .join("\n\n");
+ const slotBlocks = documents
+ .map(document => renderSlotBlock(document.exportName, document.slots))
+ .join("\n\n");
+
+ return `// \n// Generated by scripts/generate-rts-bindings.mjs from Aetheria C# MessagePack documents.\n\nimport { CultMesh } from "cultmesh-ts";\nimport type { CultMeshLiveFeedDiagnostic, CultMeshOperationContext, CultMeshOperationReceipt, CultMeshProjectionSource, CultMeshQueryContext, CultMeshQuerySurfaceDiagnostic, CultMeshQueryWatcher, CultMeshRouteHint, CultMeshSurfaceCatalogDiagnostic, CultMeshSurfaceCatalogIndexDiagnostic, CultMeshUnsubscribe, CultMeshVerseContext, CultMeshViewportRequest } from "cultmesh-ts";\nimport type { IpcMain } from "electron";\n\nexport const AetheriaRtsSchemas = {\n${schemas}\n} as const;\n\n${enumBlocks}\n\nexport const AetheriaRtsIpcChannels = {\n${renderIpcChannelLines()}\n} as const;\n\n${slotBlocks}\n\n${renderProjectionTypes("CultMeshViewportRequest")}\n\n${renderRtsOperations()}\n`;
+}
+
+function renderEnumBlock(exportName, entries) {
+ const lines = entries
+ .map(([name, value]) => ` ${name}: ${value},`)
+ .join("\n");
+ return `export const ${exportName} = {\n${lines}\n} as const;`;
+}
+
+function renderSlotBlock(exportName, slots) {
+ const slotLines = slots.map(([name, value]) => ` ${name}: ${value},`).join("\n");
+ const slotNames = slots.map(([name]) => ` | "${name}"`).join("\n");
+ const typeName = `${toPascalCase(exportName)}Slot`;
+ return `export type ${typeName} =\n${slotNames};\n\nexport const ${exportName}Slots: Record<${typeName}, number> = {\n${slotLines}\n};`;
+}
+
+function toCamelCase(value) {
+ return value.slice(0, 1).toLowerCase() + value.slice(1);
+}
+
+function toPascalCase(value) {
+ return value.slice(0, 1).toUpperCase() + value.slice(1);
+}
+
+function renderRtsOperations() {
+ return `export type AetheriaRuntimeSetMoveVectorRequest = {
+ actorEntityKey: string;
+ directionX: number;
+ directionY: number;
+ scalar: number;
+ observedFrameId?: number;
+};
+
+export type AetheriaRuntimeSetTargetRequest = {
+ actorEntityKey: string;
+ targetEntityKey: string;
+ observedFrameId?: number;
+};
+
+export type AetheriaRuntimeDaemonCommandReceipt = CultMeshOperationReceipt & {
+ commandId: string;
+};
+
+export type AetheriaRuntimeSelectedObjectRequest = {
+ entityIndex: number;
+};
+
+export type AetheriaRuntimeViewportFeedRequest = {
+ viewport: CultMeshViewportRequest;
+ selectedEntityIndex?: number;
+};
+
+export type AetheriaRuntimeViewportFeedSnapshot = {
+ viewport: ViewportResponse;
+ selectedObject: SelectedObjectProjection | null;
+ inventory: InventoryProjection | null;
+ daemonHealth: DaemonHealthProjection;
+ authorityStatus: AuthorityStatusProjection;
+ starbridgeSession: StarbridgeSessionProjection;
+ receivedAtUtc: string;
+ sampleMs: number;
+};
+
+export type AetheriaRtsMainClient = {
+ mapViewport(request: CultMeshViewportRequest): Promise;
+ objectsViewport(request: CultMeshViewportRequest): Promise;
+ gravityViewport(request: CultMeshViewportRequest): Promise;
+ selectedObject(request: AetheriaRuntimeSelectedObjectRequest): Promise;
+ inventory(request: AetheriaRuntimeSelectedObjectRequest): Promise;
+ daemonHealth(): Promise;
+ authorityStatus(): Promise;
+ starbridgeSession(): Promise;
+ watchViewportFeed(request: AetheriaRuntimeViewportFeedRequest, callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void): CultMeshUnsubscribe;
+ setMoveVector(request: AetheriaRuntimeSetMoveVectorRequest): Promise;
+ setTarget(request: AetheriaRuntimeSetTargetRequest): Promise;
+ surfaceCatalogDiagnostics(): CultMeshSurfaceCatalogDiagnostic;
+ surfaceCatalogIndexDiagnostics(): CultMeshSurfaceCatalogIndexDiagnostic;
+};
+
+export type AetheriaRtsHealthProvider = () => unknown;
+
+export function registerAetheriaRtsIpcHandlers(
+ ipcMain: IpcMain,
+ getClient: () => AetheriaRtsMainClient,
+ getHealth: AetheriaRtsHealthProvider,
+): void {
+ const viewportFeedSubscriptions = new Map();
+ ipcMain.handle(AetheriaRtsIpcChannels.mapViewport, async (_event, request: CultMeshViewportRequest) =>
+ getClient().mapViewport(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.objectsViewport, async (_event, request: CultMeshViewportRequest) =>
+ getClient().objectsViewport(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.gravityViewport, async (_event, request: CultMeshViewportRequest) =>
+ getClient().gravityViewport(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.selectedObject, async (_event, request: AetheriaRuntimeSelectedObjectRequest) =>
+ getClient().selectedObject(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.inventory, async (_event, request: AetheriaRuntimeSelectedObjectRequest) =>
+ getClient().inventory(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.daemonHealth, async () => getClient().daemonHealth());
+ ipcMain.handle(AetheriaRtsIpcChannels.authorityStatus, async () => getClient().authorityStatus());
+ ipcMain.handle(AetheriaRtsIpcChannels.starbridgeSession, async () => getClient().starbridgeSession());
+ ipcMain.handle(AetheriaRtsIpcChannels.viewportFeed, async (event, request: { subscriptionId: string; feed: AetheriaRuntimeViewportFeedRequest }) => {
+ viewportFeedSubscriptions.get(request.subscriptionId)?.();
+ const unsubscribe = getClient().watchViewportFeed(request.feed, snapshot => {
+ event.sender.send(AetheriaRtsIpcChannels.viewportFeedUpdate, {
+ subscriptionId: request.subscriptionId,
+ snapshot,
+ });
+ });
+ viewportFeedSubscriptions.set(request.subscriptionId, unsubscribe);
+ });
+ ipcMain.handle(AetheriaRtsIpcChannels.viewportFeedStop, (_event, subscriptionId: string) => {
+ viewportFeedSubscriptions.get(subscriptionId)?.();
+ viewportFeedSubscriptions.delete(subscriptionId);
+ });
+ ipcMain.handle(AetheriaRtsIpcChannels.setMoveVector, async (_event, request: AetheriaRuntimeSetMoveVectorRequest) =>
+ getClient().setMoveVector(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.setTarget, async (_event, request: AetheriaRuntimeSetTargetRequest) =>
+ getClient().setTarget(request));
+ ipcMain.handle(AetheriaRtsIpcChannels.surfaceCatalog, () => getClient().surfaceCatalogDiagnostics());
+ ipcMain.handle(AetheriaRtsIpcChannels.surfaceCatalogIndex, () => getClient().surfaceCatalogIndexDiagnostics());
+ ipcMain.handle(AetheriaRtsIpcChannels.health, () => getHealth());
+}
+
+export type AetheriaRuntimeDaemonCommandSender = (
+ commandId: string,
+ issuedAtUtc: string,
+ command: unknown[],
+ context: CultMeshOperationContext,
+) => Promise;
+
+export type AetheriaRuntimeViewportQueryExecutor = (
+ request: CultMeshViewportRequest,
+ context: CultMeshQueryContext,
+) => Promise;
+
+export type AetheriaRuntimeQueryExecutor = (
+ request: TParameters,
+ context: CultMeshQueryContext,
+) => Promise;
+
+export type AetheriaRuntimeRtsQueryExecutors = {
+ mapViewport: AetheriaRuntimeViewportQueryExecutor;
+ objectsViewport: AetheriaRuntimeViewportQueryExecutor;
+ gravityViewport: AetheriaRuntimeViewportQueryExecutor;
+ selectedObject: AetheriaRuntimeQueryExecutor;
+ inventory: AetheriaRuntimeQueryExecutor;
+ daemonHealth: AetheriaRuntimeQueryExecutor;
+ authorityStatus: AetheriaRuntimeQueryExecutor;
+ starbridgeSession: AetheriaRuntimeQueryExecutor;
+};
+
+export type AetheriaRuntimeRtsQueryWatchers = Partial<{
+ mapViewport: CultMeshQueryWatcher;
+ objectsViewport: CultMeshQueryWatcher;
+ gravityViewport: CultMeshQueryWatcher;
+ selectedObject: CultMeshQueryWatcher;
+ inventory: CultMeshQueryWatcher;
+ daemonHealth: CultMeshQueryWatcher;
+ authorityStatus: CultMeshQueryWatcher;
+ starbridgeSession: CultMeshQueryWatcher;
+}>;
+
+export type AetheriaRuntimeRtsProjectionDiagnostic = CultMeshQuerySurfaceDiagnostic;
+
+export type AetheriaRuntimeRtsLiveFeedDiagnostic = CultMeshLiveFeedDiagnostic;
+
+export type AetheriaRuntimeRtsSurfaceCatalogDiagnostic = CultMeshSurfaceCatalogDiagnostic;
+
+export type AetheriaRuntimeRtsOperationHandles = ReturnType;
+
+export type AetheriaRuntimeRtsStatePointers = ReturnType;
+
+export type AetheriaRuntimeRtsStatePointerResolvers = Partial<{
+ daemonFrame: (context: CultMeshQueryContext) => Promise;
+ daemonHealth: (context: CultMeshQueryContext) => Promise;
+ authorityPolicy: (context: CultMeshQueryContext) => Promise;
+ starbridgeSession: (context: CultMeshQueryContext) => Promise;
+}>;
+
+export const AetheriaRuntimeRtsProjectionSources = {
+ daemonFrame: CultMesh.projectionSource("daemon:aetheria.frame.latest.v1", {
+ schemaId: AetheriaRtsSchemas.daemonFrame,
+ description: "latest daemon frame"
+ }),
+ daemonHealth: CultMesh.projectionSource("daemon:aetheria.health.latest.v1", {
+ schemaId: AetheriaRtsSchemas.daemonHealth,
+ description: "latest daemon health"
+ }),
+ authorityPolicy: CultMesh.projectionSource("daemon:aetheria.authority.policy.latest.v1", {
+ schemaId: AetheriaRtsSchemas.verseAuthorityPolicy,
+ description: "latest Verse authority policy"
+ }),
+ starbridgeSession: CultMesh.projectionSource("daemon:aetheria.starbridge.session.latest.v1", {
+ schemaId: AetheriaRtsSchemas.starbridgeSessionSummary,
+ description: "latest Starbridge session summary"
+ })
+} as const;
+
+export function createAetheriaRuntimeRtsStatePointers(
+ routeHint: CultMeshRouteHint = CultMesh.routeHint(),
+ resolvers: AetheriaRuntimeRtsStatePointerResolvers = {},
+) {
+ return {
+ daemonFrame: CultMesh.statePointer(
+ AetheriaRuntimeRtsProjectionSources.daemonFrame.sourceId,
+ async (context) => resolvers.daemonFrame?.(context),
+ undefined,
+ {
+ routeHint,
+ sources: [AetheriaRuntimeRtsProjectionSources.daemonFrame],
+ },
+ ),
+ daemonHealth: CultMesh.statePointer(
+ AetheriaRuntimeRtsProjectionSources.daemonHealth.sourceId,
+ async (context) => resolvers.daemonHealth?.(context),
+ undefined,
+ {
+ routeHint,
+ sources: [AetheriaRuntimeRtsProjectionSources.daemonHealth],
+ },
+ ),
+ authorityPolicy: CultMesh.statePointer(
+ AetheriaRuntimeRtsProjectionSources.authorityPolicy.sourceId,
+ async (context) => resolvers.authorityPolicy?.(context),
+ undefined,
+ {
+ routeHint,
+ sources: [AetheriaRuntimeRtsProjectionSources.authorityPolicy],
+ },
+ ),
+ starbridgeSession: CultMesh.statePointer(
+ AetheriaRuntimeRtsProjectionSources.starbridgeSession.sourceId,
+ async (context) => resolvers.starbridgeSession?.(context),
+ undefined,
+ {
+ routeHint,
+ sources: [AetheriaRuntimeRtsProjectionSources.starbridgeSession],
+ },
+ ),
+ } as const;
+}
+
+export function createAetheriaRuntimeRtsQueryHandles(
+ executors: AetheriaRuntimeRtsQueryExecutors,
+ routeHint: CultMeshRouteHint = CultMesh.routeHint(),
+ watchers: AetheriaRuntimeRtsQueryWatchers = {},
+) {
+ return {
+ mapViewport: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.rtsViewport,
+ [AetheriaRuntimeRtsProjectionSources.daemonFrame],
+ executors.mapViewport,
+ { routeHint, watchProjection: watchers.mapViewport },
+ ).asQuerySurface(),
+ objectsViewport: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.objectsViewport,
+ [AetheriaRuntimeRtsProjectionSources.daemonFrame],
+ executors.objectsViewport,
+ { routeHint, watchProjection: watchers.objectsViewport },
+ ).asQuerySurface(),
+ gravityViewport: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.gravityViewport,
+ [AetheriaRuntimeRtsProjectionSources.daemonFrame],
+ executors.gravityViewport,
+ { routeHint, watchProjection: watchers.gravityViewport },
+ ).asQuerySurface(),
+ selectedObject: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.selectedObject,
+ [AetheriaRuntimeRtsProjectionSources.daemonFrame],
+ executors.selectedObject,
+ { routeHint, watchProjection: watchers.selectedObject },
+ ).asQuerySurface(),
+ inventory: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.inventory,
+ [AetheriaRuntimeRtsProjectionSources.daemonFrame],
+ executors.inventory,
+ { routeHint, watchProjection: watchers.inventory },
+ ).asQuerySurface(),
+ daemonHealth: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.daemonHealth,
+ [AetheriaRuntimeRtsProjectionSources.daemonHealth],
+ executors.daemonHealth,
+ { routeHint, watchProjection: watchers.daemonHealth },
+ ).asQuerySurface(),
+ authorityStatus: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.verseAuthorityPolicy,
+ [AetheriaRuntimeRtsProjectionSources.authorityPolicy],
+ executors.authorityStatus,
+ { routeHint, watchProjection: watchers.authorityStatus },
+ ).asQuerySurface(),
+ starbridgeSession: CultMesh.projectionRecipe(
+ AetheriaRtsSchemas.starbridgeSessionSummary,
+ [AetheriaRuntimeRtsProjectionSources.starbridgeSession],
+ executors.starbridgeSession,
+ { routeHint, watchProjection: watchers.starbridgeSession },
+ ).asQuerySurface(),
+ } as const;
+}
+
+export function describeAetheriaRuntimeRtsQueryHandles(
+ handles: ReturnType,
+) {
+ return {
+ mapViewport: describeAetheriaRuntimeRtsQuerySurface(handles.mapViewport),
+ objectsViewport: describeAetheriaRuntimeRtsQuerySurface(handles.objectsViewport),
+ gravityViewport: describeAetheriaRuntimeRtsQuerySurface(handles.gravityViewport),
+ selectedObject: describeAetheriaRuntimeRtsQuerySurface(handles.selectedObject),
+ inventory: describeAetheriaRuntimeRtsQuerySurface(handles.inventory),
+ daemonHealth: describeAetheriaRuntimeRtsQuerySurface(handles.daemonHealth),
+ authorityStatus: describeAetheriaRuntimeRtsQuerySurface(handles.authorityStatus),
+ starbridgeSession: describeAetheriaRuntimeRtsQuerySurface(handles.starbridgeSession),
+ } as const;
+}
+
+export function describeAetheriaRuntimeRtsSurfaceCatalog(
+ handles: ReturnType,
+ operations?: AetheriaRuntimeRtsOperationHandles,
+ statePointers: AetheriaRuntimeRtsStatePointers = createAetheriaRuntimeRtsStatePointers(),
+): AetheriaRuntimeRtsSurfaceCatalogDiagnostic {
+ return CultMesh.describeSurfaceCatalog(
+ "gamecult.aetheria.rts.surfaces.v1",
+ [
+ CultMesh.describeSurface(statePointers.daemonFrame),
+ CultMesh.describeSurface(statePointers.daemonHealth),
+ CultMesh.describeSurface(statePointers.authorityPolicy),
+ CultMesh.describeSurface(statePointers.starbridgeSession),
+ CultMesh.describeSurface(handles.mapViewport),
+ CultMesh.describeSurface(handles.objectsViewport),
+ CultMesh.describeSurface(handles.gravityViewport),
+ CultMesh.describeSurface(handles.selectedObject),
+ CultMesh.describeSurface(handles.inventory),
+ CultMesh.describeSurface(handles.daemonHealth),
+ CultMesh.describeSurface(handles.authorityStatus),
+ CultMesh.describeSurface(handles.starbridgeSession),
+ ...(operations
+ ? [
+ CultMesh.describeSurface(operations.setMoveVector),
+ CultMesh.describeSurface(operations.setTarget),
+ ]
+ : []),
+ ],
+ );
+}
+
+function describeAetheriaRuntimeRtsQuerySurface(surface: {
+ queryId: string;
+ routeHint: CultMeshRouteHint;
+ sources: readonly CultMeshProjectionSource[];
+}): AetheriaRuntimeRtsProjectionDiagnostic {
+ return CultMesh.describeQuerySurface(surface);
+}
+
+export function describeAetheriaRuntimeRtsLiveFeedSurface(surface: {
+ feedId: string;
+ routeHint: CultMeshRouteHint;
+ sources: readonly CultMeshProjectionSource[];
+}): AetheriaRuntimeRtsLiveFeedDiagnostic {
+ return CultMesh.describeLiveFeed(surface);
+}
+
+export function createAetheriaRuntimeRtsOperationHandles(
+ sendCommand: AetheriaRuntimeDaemonCommandSender,
+ createCommandId: () => string = createDefaultCommandId,
+) {
+ return {
+ setMoveVector: CultMesh.operation(
+ "gamecult.aetheria.pilot.set_move_vector.v1",
+ async (request, context) => {
+ const commandId = context.idempotencyKey ?? createCommandId();
+ const issuedAtUtc = new Date().toISOString();
+ await sendCommand(
+ commandId,
+ issuedAtUtc,
+ encodeSetMoveVectorCommand(commandId, issuedAtUtc, context.runtimeId, request),
+ context,
+ );
+ return {
+ commandId,
+ ...CultMesh.operationReceipt("gamecult.aetheria.pilot.set_move_vector.v1", true, { route: context.routeHint }),
+ };
+ },
+ ),
+ setTarget: CultMesh.operation(
+ "gamecult.aetheria.pilot.set_target.v1",
+ async (request, context) => {
+ const commandId = context.idempotencyKey ?? createCommandId();
+ const issuedAtUtc = new Date().toISOString();
+ await sendCommand(
+ commandId,
+ issuedAtUtc,
+ encodeSetTargetCommand(commandId, issuedAtUtc, context.runtimeId, request),
+ context,
+ );
+ return {
+ commandId,
+ ...CultMesh.operationReceipt("gamecult.aetheria.pilot.set_target.v1", true, { route: context.routeHint }),
+ };
+ },
+ ),
+ } as const;
+}
+
+export type AetheriaRuntimeRtsVerseFacade = ReturnType;
+
+export function createAetheriaRuntimeRtsVerseFacade(
+ queryVerse: CultMeshVerseContext,
+ commandVerse: CultMeshVerseContext,
+ queries: ReturnType,
+ operations: AetheriaRuntimeRtsOperationHandles,
+ statePointers: AetheriaRuntimeRtsStatePointers = createAetheriaRuntimeRtsStatePointers(),
+) {
+ const mapViewport = CultMesh.bindQuery(queryVerse, queries.mapViewport);
+ const objectsViewport = CultMesh.bindQuery(queryVerse, queries.objectsViewport);
+ const gravityViewport = CultMesh.bindQuery(queryVerse, queries.gravityViewport);
+ const selectedObject = CultMesh.bindQuery(queryVerse, queries.selectedObject);
+ const inventory = CultMesh.bindQuery(queryVerse, queries.inventory);
+ const daemonHealth = CultMesh.bindQuery(queryVerse, queries.daemonHealth);
+ const authorityStatus = CultMesh.bindQuery(queryVerse, queries.authorityStatus);
+ const starbridgeSession = CultMesh.bindQuery(queryVerse, queries.starbridgeSession);
+ const daemonFramePointer = CultMesh.bindStatePointer(queryVerse, statePointers.daemonFrame);
+ const daemonHealthPointer = CultMesh.bindStatePointer(queryVerse, statePointers.daemonHealth);
+ const authorityPolicyPointer = CultMesh.bindStatePointer(queryVerse, statePointers.authorityPolicy);
+ const starbridgeSessionPointer = CultMesh.bindStatePointer(queryVerse, statePointers.starbridgeSession);
+ const setMoveVector = CultMesh.bindOperation(commandVerse, operations.setMoveVector);
+ const setTarget = CultMesh.bindOperation(commandVerse, operations.setTarget);
+
+ return {
+ zone: (_zoneId = "current") => ({
+ viewport: {
+ within: (request: CultMeshViewportRequest) =>
+ mapViewport.execute(request),
+ watch: (
+ request: CultMeshViewportRequest,
+ callback: (value: ViewportResponse) => void,
+ ) => mapViewport.watch(request, callback),
+ },
+ objects: {
+ visibleWithin: (request: CultMeshViewportRequest) =>
+ objectsViewport.execute(request),
+ watchVisibleWithin: (
+ request: CultMeshViewportRequest,
+ callback: (value: ObjectsViewportResponse) => void,
+ ) => objectsViewport.watch(request, callback),
+ },
+ gravity: {
+ within: (request: CultMeshViewportRequest) =>
+ gravityViewport.execute(request),
+ watchWithin: (
+ request: CultMeshViewportRequest,
+ callback: (value: GravityViewportResponse) => void,
+ ) => gravityViewport.watch(request, callback),
+ },
+ }),
+ entity: (entityKey: string) => ({
+ pilot: {
+ move: (
+ direction: { readonly x: number; readonly y: number },
+ options: { scalar?: number; observedFrameId?: number; commandId?: string } = {},
+ ) => setMoveVector.invoke({
+ actorEntityKey: entityKey,
+ directionX: direction.x,
+ directionY: direction.y,
+ scalar: options.scalar ?? 1,
+ observedFrameId: options.observedFrameId,
+ }, { idempotencyKey: options.commandId }),
+ target: (
+ targetEntityKey: string,
+ options: { observedFrameId?: number; commandId?: string } = {},
+ ) => setTarget.invoke({
+ actorEntityKey: entityKey,
+ targetEntityKey,
+ observedFrameId: options.observedFrameId,
+ }, { idempotencyKey: options.commandId }),
+ },
+ }),
+ selectedObject: (entityIndex: number) =>
+ selectedObject.execute({ entityIndex }),
+ inventory: (entityIndex: number) =>
+ inventory.execute({ entityIndex }),
+ daemon: {
+ state: {
+ frame: () => daemonFramePointer.resolve(),
+ health: () => daemonHealthPointer.resolve(),
+ authorityPolicy: () => authorityPolicyPointer.resolve(),
+ starbridgeSession: () => starbridgeSessionPointer.resolve(),
+ },
+ health: () => daemonHealth.execute(undefined),
+ authorityStatus: () => authorityStatus.execute(undefined),
+ starbridgeSession: () => starbridgeSession.execute(undefined),
+ },
+ } as const;
+}
+
+export function encodeSetMoveVectorCommand(
+ commandId: string,
+ issuedAtUtc: string,
+ runtimeId: string,
+ request: AetheriaRuntimeSetMoveVectorRequest,
+): unknown[] {
+ const command = createBaseCommand(commandId, issuedAtUtc, runtimeId, request.actorEntityKey, request.observedFrameId);
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.kind] = AetheriaRuntimeDaemonCommandKinds.setMoveVector;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.directionX] = request.directionX;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.directionY] = request.directionY;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.scalarValue] = Math.min(1, Math.max(0, request.scalar));
+ return command;
+}
+
+export function encodeSetTargetCommand(
+ commandId: string,
+ issuedAtUtc: string,
+ runtimeId: string,
+ request: AetheriaRuntimeSetTargetRequest,
+): unknown[] {
+ const command = createBaseCommand(commandId, issuedAtUtc, runtimeId, request.actorEntityKey, request.observedFrameId);
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.kind] = AetheriaRuntimeDaemonCommandKinds.setTarget;
+ command[aetheriaRuntimeDaemonCommandDocumentSlots.targetEntityKey] = request.targetEntityKey;
+ return command;
+}
+
+function createBaseCommand(
+ commandId: string,
+ issuedAtUtc: string,
+ runtimeId: string,
+ actorEntityKey: string,
+ observedFrameId?: number,
+): unknown[] {
+ const command = new Array