Route Electron assets through CultMesh CDN

This commit is contained in:
Meta Vens 2026-07-06 02:56:48 +01:00
commit edc8a3e6c6
5 changed files with 138 additions and 20 deletions

View file

@ -140,15 +140,11 @@ function numberProp(value: unknown, fallback: number): number {
}
function resolveAetheriaAssetUrl(uri: string): string {
if (!uri.startsWith("resources://")) {
if (!uri.startsWith("cultmesh://")) {
return uri;
}
const resourcePath = uri.slice("resources://".length).replace(/^\/+/, "");
const filePath = resourcePath.match(/\.[a-z0-9]+$/i)
? resourcePath
: `${resourcePath}.png`;
return new URL(`../../Assets/Resources/${filePath}`, document.baseURI).href;
return `aetheria-cdn://asset?uri=${encodeURIComponent(uri)}`;
}
async function submitEveCommand(intent: EveCommandIntent): Promise<void> {

View file

@ -64,6 +64,7 @@ import {
const connectionId = 0x43554c54;
const eveSurfaceSchemaId = "gamecult.eve.surface.v1";
const defaultEveSurfaceRecordKey = "eve:surface:aetheria.daemon.game";
const cultMeshCdnAssetBlobSchemaId = "gamecult.cultmesh.cdn.asset_blob.v1";
type AetheriaPublicationDocumentSpec = CultMeshPublicationDocumentBinding & {
readonly localPath: string;
@ -326,6 +327,33 @@ export class AetheriaCultMeshClient {
return this.aetheria.daemon.assetManifest();
}
public async assetBlob(uri: string): Promise<{ bytes: Uint8Array; mimeType: string }> {
const recordKey = normalizeCultMeshAssetUri(uri);
const document = CultMesh.documentFromPublication(
{
kind: "peer-snapshot",
peer: () => this.peer(),
endpoint: this.resolvedRudpEndpoint(),
},
cultMeshCdnAssetBlobSchemaId,
recordKey,
{
documentId: recordKey,
routeHint: this.queryVerse.context.routeHint,
sourceId: recordKey,
timeoutMs: 1500,
pollMs: 50,
messageIdPrefix: `${this.runtimeId}:asset`,
},
);
const bytes = normalizeAssetBlob(await document.latest(this.queryContext()));
const manifest = await this.assetManifest().catch(() => null);
return {
bytes,
mimeType: resolveAssetMimeType(recordKey, manifest),
};
}
public async eveSurface(request: AetheriaEveSurfaceRequest = {}): Promise<AetheriaMenuSurfaceDocument> {
const recordKey = eveSurfaceRecordKey(request);
const document = CultMesh.documentFromPublication(
@ -658,6 +686,38 @@ function normalizeDaemonTarget(target: string | AetheriaCultMeshDaemonTarget): R
};
}
function normalizeCultMeshAssetUri(uri: string): string {
const value = (uri ?? "").trim();
if (!value.toLowerCase().startsWith("cultmesh://")) {
throw new Error(`Aetheria asset URI must be CultMesh-native: ${value}`);
}
return value;
}
function normalizeAssetBlob(value: unknown): Uint8Array {
if (value instanceof Uint8Array)
return value;
if (Array.isArray(value))
return Uint8Array.from(value.map(entry => typeof entry === "number" ? entry : 0));
throw new Error("Aetheria CultMesh CDN asset blob did not decode to bytes.");
}
function resolveAssetMimeType(recordKey: string, manifest: AssetManifestDocument | null): string {
const entry = manifest?.assets.find(candidate => candidate.ref.uri === recordKey);
return entry?.ref.mimeType?.trim() || mimeTypeFromUri(recordKey);
}
function mimeTypeFromUri(uri: string): string {
const normalized = uri.toLowerCase();
if (normalized.endsWith(".jpg") || normalized.endsWith(".jpeg"))
return "image/jpeg";
if (normalized.endsWith(".svg"))
return "image/svg+xml";
if (normalized.endsWith(".png") || normalized.startsWith("cultmesh://"))
return "image/png";
return "application/octet-stream";
}
async function retryTransientPublicationRead<T>(read: () => Promise<T>): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt < 40; attempt += 1) {

View file

@ -522,7 +522,7 @@ function toProjectileViewObject(projectile: unknown[]): ViewObject {
targetEntityIndex: num(projectile[projectileSlots.targetEntityIndex], -1),
isActive: projectile[projectileSlots.active] !== false,
visibility: num(projectile[projectileSlots.radius]),
iconAsset: spriteAsset("aetheria.asset.sprite.game.projectile", "Sprites/Icons/Lightning Bolt"),
iconAsset: spriteAsset("map.entity.projectile"),
status: {
hull: num(projectile[projectileSlots.damage]),
shield: 0,
@ -796,40 +796,46 @@ function toAssetManifestEntry(entry: unknown[]): AssetManifestDocument["assets"]
function entityIconAsset(kind: string, controlled: boolean): AssetRef {
if (controlled)
return spriteAsset("map.entity.player", "Sprites/Icons/Stroked/Ship");
return spriteAsset("map.entity.player");
const normalized = kind.trim().toLowerCase();
if (normalized.includes("station"))
return spriteAsset("map.entity.station", "Sprites/Icons/station1");
return spriteAsset("map.entity.station");
if (normalized.includes("orbital"))
return spriteAsset("map.entity.orbital", "Sprites/Icons/Stroked/orbital");
return spriteAsset("map.entity.orbital");
if (normalized.includes("projectile"))
return spriteAsset("map.entity.projectile");
return spriteAsset("map.entity.ship", "Sprites/Icons/Stroked/Ship");
return spriteAsset("map.entity.ship");
}
function bodyIconAsset(kind: string): AssetRef {
const normalized = kind.trim().toLowerCase();
if (normalized.includes("sun") || normalized.includes("star"))
return spriteAsset("map.body.sun", "Sprites/Icons/Stroked/Sun");
return spriteAsset("map.body.sun");
if (normalized.includes("asteroid"))
return spriteAsset("map.body.asteroid", "Sprites/Icons/Stroked/Planet");
return spriteAsset("map.body.asteroid");
return spriteAsset("map.body.planet", "Sprites/Icons/Stroked/Planet");
return spriteAsset("map.body.planet");
}
function itemIconAsset(itemKey: string): AssetRef {
const key = itemKey.trim();
return key.length > 0
? textureAsset(`item.${key}.icon`, `item.${key}.icon`)
? textureAsset(`item.${key}.icon`)
: emptyAsset("texture");
}
function spriteAsset(assetKey: string, resourcePath: string): AssetRef {
return asset(assetKey, "sprite", `resources://${resourcePath}`, "resources", "image/*");
function spriteAsset(assetKey: string): AssetRef {
return asset(assetKey, "sprite", cultMeshAssetUri(assetKey), "cultmesh", "image/*");
}
function textureAsset(assetKey: string, resourcePath: string): AssetRef {
return asset(assetKey, "texture", resourcePath.startsWith("resources://") ? resourcePath : `resources://${resourcePath}`, "resources", "image/*");
function textureAsset(assetKey: string): AssetRef {
return asset(assetKey, "texture", cultMeshAssetUri(assetKey), "cultmesh", "image/*");
}
function cultMeshAssetUri(assetKey: string): string {
return `cultmesh://aetheria/assets/${assetKey.trim().replace(/[.\\]+/g, "/").replace(/^\/+|\/+$/g, "")}`;
}
function assetRef(value: unknown[], fallback: AssetRef = emptyAsset()): AssetRef {

View file

@ -1,4 +1,4 @@
import { app, BrowserWindow, ipcMain, shell } from "electron";
import { app, BrowserWindow, ipcMain, protocol, shell } from "electron";
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { createWriteStream, existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
@ -27,6 +27,7 @@ const clientCultMeshAdvertiseHost = process.env.AETHERIA_CLIENT_CULTMESH_ADVERTI
const localDaemonRudpEndpoint = launchLocalDaemon ? `rudp://127.0.0.1:${clientCultMeshPort}` : "";
const electronSmoke = process.env.AETHERIA_ELECTRON_SMOKE === "1";
const electronSmokeResultPath = process.env.AETHERIA_ELECTRON_SMOKE_RESULT;
const assetProtocol = "aetheria-cdn";
let daemonProcess: ChildProcessWithoutNullStreams | null = null;
let mainWindow: BrowserWindow | null = null;
@ -36,6 +37,7 @@ let isQuitting = false;
app.whenReady().then(async () => {
mainWindow = createWindow();
registerIpc();
registerAssetProtocol();
showStartup("Preparing Aetheria Starbridge", "Building daemon if needed.");
try {
@ -336,6 +338,29 @@ function registerIpc(): void {
});
}
function registerAssetProtocol(): void {
protocol.handle(assetProtocol, async request => {
try {
const uri = new URL(request.url).searchParams.get("uri") ?? "";
if (!aetheriaClient) {
return new Response("Aetheria CultMesh client is not initialized.", { status: 503 });
}
const asset = await aetheriaClient.assetBlob(uri);
const body = Uint8Array.from(asset.bytes).buffer as ArrayBuffer;
return new Response(body, {
status: 200,
headers: {
"content-type": asset.mimeType,
"cache-control": "no-store",
},
});
} catch (error) {
return new Response(error instanceof Error ? error.message : String(error), { status: 404 });
}
});
}
function requireClient(): AetheriaCultMeshClient {
if (!aetheriaClient)
throw new Error("Aetheria CultMesh client is not initialized.");

View file

@ -16290,6 +16290,9 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
"AetheriaRuntimeClientTargetStore.cs");
var runtimeClientProviderPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "AetheriaUnityRuntimeClientProvider.cs");
var rtsLocalDocumentsPath = Path.Combine(root, "Aetheria.Rts.Web", "Electron", "aetheria-rts-local-documents.ts");
var rtsElectronMainPath = Path.Combine(root, "Aetheria.Rts.Web", "Electron", "main.ts");
var rtsElectronCultMeshPath = Path.Combine(root, "Aetheria.Rts.Web", "Electron", "aetheria-cultmesh.ts");
var rtsClientAppPath = Path.Combine(root, "Aetheria.Rts.Web", "Client", "app.ts");
var daemonRuntimeDocumentTestsPath = Path.Combine(root, "Assets", "Scripts", "Tests", "DaemonRuntimeDocumentTests.cs");
var authoritySmokePath = Path.Combine(root, "Aetheria.State.AuthoritySmoke", "Program.cs");
var freezePath = Path.Combine(root, "Aetheria.State.Freeze", "Program.cs");
@ -16326,6 +16329,15 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
var rtsLocalDocuments = File.Exists(rtsLocalDocumentsPath)
? File.ReadAllText(rtsLocalDocumentsPath)
: throw new InvalidOperationException("Cannot verify daemon projectile authority; Electron local projection is missing.");
var rtsElectronMain = File.Exists(rtsElectronMainPath)
? File.ReadAllText(rtsElectronMainPath)
: throw new InvalidOperationException("Cannot verify Electron CultMesh CDN asset authority; Electron main process is missing.");
var rtsElectronCultMesh = File.Exists(rtsElectronCultMeshPath)
? File.ReadAllText(rtsElectronCultMeshPath)
: throw new InvalidOperationException("Cannot verify Electron CultMesh CDN asset authority; Electron CultMesh client is missing.");
var rtsClientApp = File.Exists(rtsClientAppPath)
? File.ReadAllText(rtsClientAppPath)
: throw new InvalidOperationException("Cannot verify Electron CultMesh CDN asset authority; renderer app is missing.");
var daemonRuntimeDocumentTests = File.Exists(daemonRuntimeDocumentTestsPath)
? File.ReadAllText(daemonRuntimeDocumentTestsPath)
: throw new InvalidOperationException("Cannot verify daemon projectile authority; daemon runtime document tests are missing.");
@ -16566,6 +16578,25 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
string.Join(", ", staleRuntimeSpecificAssetSources));
}
if (rtsLocalDocuments.Contains("resources://", StringComparison.Ordinal) ||
rtsLocalDocuments.Contains("\"resources\"", StringComparison.Ordinal) ||
rtsClientApp.Contains("Assets/Resources", StringComparison.Ordinal) ||
rtsClientApp.Contains("resources://", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Electron Eve lowering must not synthesize renderer-local Resources asset refs; assets resolve through CultMesh CDN.");
}
if (!rtsClientApp.Contains("aetheria-cdn://asset?uri=", StringComparison.Ordinal) ||
!rtsClientApp.Contains("uri.startsWith(\"cultmesh://\")", StringComparison.Ordinal) ||
!rtsElectronMain.Contains("protocol.handle(assetProtocol", StringComparison.Ordinal) ||
!rtsElectronMain.Contains("aetheriaClient.assetBlob(uri)", StringComparison.Ordinal) ||
!rtsElectronCultMesh.Contains("cultMeshCdnAssetBlobSchemaId", StringComparison.Ordinal) ||
!rtsElectronCultMesh.Contains("public async assetBlob(uri: string)", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Electron Eve lowering must resolve CultMesh asset refs through the daemon CultMesh CDN protocol bridge.");
}
if (daemonTickRunner.Contains("AetheriaRuntimeDaemonFrameStore.PublishFrame", StringComparison.Ordinal))
{
throw new InvalidOperationException(