Aetheria/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts

857 lines
30 KiB
TypeScript
Raw Normal View History

2026-06-25 18:09:33 +01:00
import { encode } from "@msgpack/msgpack";
import { performance } from "node:perf_hooks";
import { CultMesh } from "cultmesh-ts";
import type {
CultMeshDocumentCatalog,
CultMeshDocumentPublicationSource,
CultMeshPublicationDocumentBinding,
2026-06-25 18:09:33 +01:00
CultMeshOperationContext,
CultMeshQueryContext,
CultMeshSurfaceCatalogIndexDiagnostic,
CultMeshVerse,
} from "cultmesh-ts";
import type {
CultNetDocumentPutRawMessage,
CultNetPeer,
} from "cultnet-ts";
import {
AetheriaRtsSchemas,
createAetheriaRuntimeGameDocuments,
createAetheriaRuntimeGameOperationHandles,
createAetheriaRuntimeGameVerseHandles,
createAetheriaRuntimeGameQueryHandles,
describeAetheriaRuntimeGameLiveFeedSurface,
describeAetheriaRuntimeGameQueryHandles,
describeAetheriaRuntimeGameSurfaceCatalog,
2026-06-25 18:09:33 +01:00
type AetheriaRuntimeDaemonCommandReceipt,
2026-06-30 19:14:49 +01:00
type AssetManifestDocument,
type AuthorityStatusDocument,
type DaemonHealthDocument,
2026-06-25 18:09:33 +01:00
type GravityViewportResponse,
2026-06-30 19:14:49 +01:00
type InventoryDocument,
2026-06-25 18:09:33 +01:00
type ObjectsViewportResponse,
2026-07-05 01:17:27 +01:00
type RenderSplatsViewportResponse,
type AetheriaRuntimeGameLiveFeedDiagnostic,
type AetheriaRuntimeGameQueryDiagnostic,
type AetheriaRuntimeGameSurfaceCatalogDiagnostic,
type AetheriaRuntimeGameOperationHandles,
type AetheriaRuntimeGameVerseHandles,
type AetheriaRuntimeGameDocuments,
2026-06-25 18:09:33 +01:00
type AetheriaRuntimeViewportFeedRequest,
type AetheriaRuntimeViewportFeedSnapshot,
type RtsSetMoveVectorRequest,
type RtsSetTargetRequest,
2026-06-30 19:14:49 +01:00
type SelectedObjectDocument,
2026-06-25 18:09:33 +01:00
type SelectedObjectRequest,
2026-06-30 19:14:49 +01:00
type StarbridgeSessionDocument,
2026-06-25 18:09:33 +01:00
type ViewportRequest,
type ViewportResponse,
} from "./aetheria-rts-bindings.js";
import {
2026-06-30 19:14:49 +01:00
readAuthorityStatusDocument,
readAssetManifestDocument,
readDaemonHealthDocument,
readGravityViewportDocument,
readInventoryDocument,
readObjectsViewportDocument,
readRenderSplatsViewportDocument,
readSelectedObjectDocument,
2026-06-30 19:14:49 +01:00
readStarbridgeSessionSummaryDocument,
readViewportDocument,
2026-06-30 19:14:49 +01:00
} from "./aetheria-rts-local-documents.js";
2026-06-25 18:09:33 +01:00
const connectionId = 0x43554c54;
const cultMeshCdnAssetBlobSchemaId = "gamecult.cultmesh.cdn.asset_blob.v1";
2026-06-25 18:09:33 +01:00
type AetheriaPublicationDocumentSpec = CultMeshPublicationDocumentBinding & {
readonly localPath: string;
readonly remoteRecordKey?: string;
2026-06-25 18:09:33 +01:00
};
type AetheriaRuntimeGameQueryExecutors = {
2026-07-05 15:12:29 +01:00
mapViewport: (request: ViewportRequest) => Promise<ViewportResponse>;
objectsViewport: (request: ViewportRequest) => Promise<ObjectsViewportResponse>;
gravityViewport: (request: ViewportRequest) => Promise<GravityViewportResponse>;
renderSplatsViewport: (request: ViewportRequest) => Promise<RenderSplatsViewportResponse>;
selectedObject: (request: SelectedObjectRequest) => Promise<SelectedObjectDocument>;
inventory: (request: SelectedObjectRequest) => Promise<InventoryDocument>;
daemonHealth: () => Promise<DaemonHealthDocument>;
authorityStatus: () => Promise<AuthorityStatusDocument>;
starbridgeSession: () => Promise<StarbridgeSessionDocument>;
assetManifest: () => Promise<AssetManifestDocument>;
};
2026-06-25 18:09:33 +01:00
export type AetheriaCultMeshClientOptions = {
publicationMode?: "local" | "remote";
snapshotTimeoutMs?: number;
};
export type AetheriaCultMeshDaemonTarget = {
readonly uri: string;
readonly peerId?: string;
readonly verseId?: string;
readonly role?: string;
readonly endpoints?: readonly string[];
};
2026-06-25 18:09:33 +01:00
export type {
BodyView,
EntityStatus,
GravityViewportResponse,
GravityInfluence,
InventoryItem,
ObjectsViewportResponse,
AetheriaRuntimeViewportFeedRequest,
AetheriaRuntimeViewportFeedSnapshot,
RtsSetMoveVectorRequest,
RtsSetTargetRequest,
2026-06-30 19:14:49 +01:00
SelectedObjectDocument,
2026-06-25 18:09:33 +01:00
SelectedObjectRequest,
2026-06-30 19:14:49 +01:00
StarbridgeSessionDocument,
2026-06-25 18:09:33 +01:00
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;
private readonly daemonTarget: Required<AetheriaCultMeshDaemonTarget>;
2026-06-25 18:09:33 +01:00
public constructor(
daemonTarget: string | AetheriaCultMeshDaemonTarget,
2026-06-25 18:09:33 +01:00
statePath: string,
private readonly runtimeId = "aetheria-electron-client",
2026-06-25 18:09:33 +01:00
options: AetheriaCultMeshClientOptions = {},
) {
this.daemonTarget = normalizeDaemonTarget(daemonTarget);
2026-06-25 18:09:33 +01:00
const publicationMode = options.publicationMode ?? "local";
this.publicationDescription = publicationMode === "remote" ? this.daemonTarget.uri : statePath;
2026-06-25 18:09:33 +01:00
this.verse = CultMesh.verse("aetheria.local", this.runtimeId);
this.queryVerse = publicationMode === "remote"
? this.verse.withRoute("network", this.publicationDescription)
: this.verse.withRoute("shared-memory", this.publicationDescription);
2026-06-25 18:09:33 +01:00
this.commandVerse = this.verse
.withRoute("network", this.daemonTarget.uri)
2026-06-25 18:09:33 +01:00
.withClaim("commander-control", { shardId: "aetheria.local" });
this.publications = CultMesh.documentsFromPublication(
this.createPublicationSource(publicationMode),
createAetheriaPublicationDocuments(statePath, publicationMode),
{
routeHint: this.queryVerse.context.routeHint,
timeoutMs: options.snapshotTimeoutMs,
pollMs: 50,
messageIdPrefix: `${this.runtimeId}:snapshot`,
},
);
2026-06-25 18:09:33 +01:00
const executors = {
mapViewport: async (request: ViewportRequest) =>
readViewportDocument(await this.fetchViewportDocument(AetheriaRtsSchemas.gameViewport, "aetheria.viewport.map", request)),
objectsViewport: async (request: ViewportRequest) =>
readObjectsViewportDocument(await this.fetchViewportDocument(AetheriaRtsSchemas.objectsViewport, "aetheria.viewport.objects", request)),
gravityViewport: async (request: ViewportRequest) =>
readGravityViewportDocument(await this.fetchViewportDocument(AetheriaRtsSchemas.gravityViewport, "aetheria.viewport.gravity", request)),
renderSplatsViewport: async (request: ViewportRequest) =>
readRenderSplatsViewportDocument(await this.fetchViewportDocument(AetheriaRtsSchemas.renderSplatsViewport, "aetheria.viewport.render_splats", request)),
selectedObject: async (request: SelectedObjectRequest) =>
readSelectedObjectDocument(await this.fetchIndexedDocument(AetheriaRtsSchemas.selectedObject, "aetheria.object.selected", request.entityIndex)),
inventory: async (request: SelectedObjectRequest) =>
readInventoryDocument(await this.fetchIndexedDocument(AetheriaRtsSchemas.inventory, "aetheria.inventory", request.entityIndex)),
2026-06-30 19:14:49 +01:00
daemonHealth: async () => readDaemonHealthDocument(await this.fetchDaemonHealthDocument()),
authorityStatus: async () => readAuthorityStatusDocument(await this.fetchAuthorityPolicyDocument()),
starbridgeSession: async () => readStarbridgeSessionSummaryDocument(await this.fetchStarbridgeSessionSummaryDocument()),
assetManifest: async () => readAssetManifestDocument(await this.fetchAssetManifestDocument()),
2026-06-25 18:09:33 +01:00
};
2026-07-05 15:12:29 +01:00
this.queryExecutors = executors;
this.queries = createAetheriaRuntimeGameQueryHandles(
2026-06-25 18:09:33 +01:00
executors,
this.queryVerse.context.routeHint,
{
objectsViewport: CultMesh.pollingQueryWatcher(executors.objectsViewport, { intervalMs: 50 }),
gravityViewport: CultMesh.pollingQueryWatcher(executors.gravityViewport, { intervalMs: 50 }),
2026-07-05 01:17:27 +01:00
renderSplatsViewport: CultMesh.pollingQueryWatcher(executors.renderSplatsViewport, { intervalMs: 50 }),
2026-06-25 18:09:33 +01:00
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 }),
assetManifest: CultMesh.pollingQueryWatcher(executors.assetManifest, { intervalMs: 1000 }),
2026-06-25 18:09:33 +01:00
},
);
this.documents = createAetheriaRuntimeGameDocuments(
2026-06-25 18:09:33 +01:00
this.queryVerse.context.routeHint,
{
daemonFrame: async () => this.fetchLatestFrameDocument(),
daemonHealth: async () => this.fetchDaemonHealthDocument(),
authorityPolicy: async () => this.fetchAuthorityPolicyDocument(),
starbridgeSession: async () => this.fetchStarbridgeSessionSummaryDocument(),
assetManifest: async () => this.fetchAssetManifestDocument(),
2026-06-25 18:09:33 +01:00
},
);
this.operations = createAetheriaRuntimeGameOperationHandles(
2026-06-25 18:09:33 +01:00
(commandId, issuedAtUtc, command, context) =>
this.sendCommandDocument(commandId, issuedAtUtc, command, context),
);
this.aetheria = createAetheriaRuntimeGameVerseHandles(
2026-06-25 18:09:33 +01:00
this.queryVerse.context,
this.commandVerse.context,
this.queries,
this.operations,
2026-06-27 15:26:20 +01:00
this.documents,
2026-06-25 18:09:33 +01:00
);
}
private readonly publicationDescription: string;
private readonly publications: CultMeshDocumentCatalog;
private readonly queryExecutors: AetheriaRuntimeGameQueryExecutors;
private readonly queries: ReturnType<typeof createAetheriaRuntimeGameQueryHandles>;
private readonly documents: AetheriaRuntimeGameDocuments;
private readonly operations: AetheriaRuntimeGameOperationHandles;
private readonly aetheria: AetheriaRuntimeGameVerseHandles;
2026-06-25 18:09:33 +01:00
public async close(): Promise<void> {
this.#peer?.close();
this.#peer = null;
}
public async waitForFrame(timeoutMs: number): Promise<void> {
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.daemonTarget.uri}. ${lastError}`);
2026-06-25 18:09:33 +01:00
}
public async mapViewport(request: ViewportRequest): Promise<ViewportResponse> {
2026-07-05 16:59:06 +01:00
return this.aetheria.zone().viewport.within(request);
2026-06-25 18:09:33 +01:00
}
public async objectsViewport(request: ViewportRequest): Promise<ObjectsViewportResponse> {
2026-07-05 16:59:06 +01:00
return this.aetheria.zone().objects.visibleWithin(request);
2026-06-25 18:09:33 +01:00
}
public async gravityViewport(request: ViewportRequest): Promise<GravityViewportResponse> {
2026-07-05 16:59:06 +01:00
return this.aetheria.zone().gravity.within(request);
2026-06-25 18:09:33 +01:00
}
2026-07-05 01:17:27 +01:00
public async renderSplatsViewport(request: ViewportRequest): Promise<RenderSplatsViewportResponse> {
2026-07-05 16:59:06 +01:00
return this.aetheria.zone().renderSplats.within(request);
2026-07-05 01:17:27 +01:00
}
2026-06-30 19:14:49 +01:00
public async selectedObject(request: SelectedObjectRequest): Promise<SelectedObjectDocument> {
2026-07-05 16:59:06 +01:00
return this.aetheria.selectedObject(request.entityIndex);
2026-06-25 18:09:33 +01:00
}
2026-06-30 19:14:49 +01:00
public async inventory(request: SelectedObjectRequest): Promise<InventoryDocument> {
2026-07-05 16:59:06 +01:00
return this.aetheria.inventory(request.entityIndex);
2026-06-25 18:09:33 +01:00
}
2026-06-30 19:14:49 +01:00
public async daemonHealth(): Promise<DaemonHealthDocument> {
2026-06-25 18:09:33 +01:00
return this.aetheria.daemon.health();
}
2026-06-30 19:14:49 +01:00
public async authorityStatus(): Promise<AuthorityStatusDocument> {
2026-06-25 18:09:33 +01:00
return this.aetheria.daemon.authorityStatus();
}
2026-06-30 19:14:49 +01:00
public async starbridgeSession(): Promise<StarbridgeSessionDocument> {
2026-06-25 18:09:33 +01:00
return this.aetheria.daemon.starbridgeSession();
}
2026-06-30 19:14:49 +01:00
public async assetManifest(): Promise<AssetManifestDocument> {
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 queryDiagnostics(): Readonly<Record<string, AetheriaRuntimeGameQueryDiagnostic>> {
return describeAetheriaRuntimeGameQueryHandles(this.queries);
2026-06-25 18:09:33 +01:00
}
public liveFeedDiagnostics(): Readonly<Record<string, AetheriaRuntimeGameLiveFeedDiagnostic>> {
2026-06-25 18:09:33 +01:00
return {
viewportFeed: describeAetheriaRuntimeGameLiveFeedSurface(this.createViewportFeed()),
2026-06-25 18:09:33 +01:00
};
}
public surfaceCatalogDiagnostics(): AetheriaRuntimeGameSurfaceCatalogDiagnostic {
const catalog = describeAetheriaRuntimeGameSurfaceCatalog(this.queries, this.operations, this.documents);
2026-06-25 18:09:33 +01:00
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<AetheriaRuntimeDaemonCommandReceipt> {
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<AetheriaRuntimeDaemonCommandReceipt> {
return this.aetheria.entity(request.actorEntityKey).pilot.target(request.targetEntityKey, {
observedFrameId: request.observedFrameId,
});
}
private async fetchLatestFrameDocument(): Promise<unknown> {
return this.fetchPublicationDocument(AetheriaRtsSchemas.daemonFrame);
2026-06-25 18:09:33 +01:00
}
private async fetchDaemonHealthDocument(): Promise<unknown> {
return this.fetchPublicationDocument(AetheriaRtsSchemas.daemonHealth);
2026-06-25 18:09:33 +01:00
}
private async fetchAuthorityPolicyDocument(): Promise<unknown> {
return this.fetchPublicationDocument(AetheriaRtsSchemas.verseAuthorityPolicy);
2026-06-25 18:09:33 +01:00
}
private async fetchStarbridgeSessionSummaryDocument(): Promise<unknown> {
return this.fetchPublicationDocument(AetheriaRtsSchemas.starbridgeSessionSummary);
}
private async fetchAssetManifestDocument(): Promise<unknown> {
return this.fetchPublicationDocument(AetheriaRtsSchemas.assetManifest);
}
private async fetchViewportDocument(
schemaId: string,
recordPrefix: string,
request: ViewportRequest,
): Promise<unknown> {
const recordKey = managedViewportRecordKey(recordPrefix, request);
const document = CultMesh.documentFromPublication(
{
kind: "peer-snapshot",
peer: () => this.peer(),
endpoint: this.resolvedRudpEndpoint(),
},
schemaId,
recordKey,
{
documentId: recordKey,
routeHint: this.queryVerse.context.routeHint,
sourceId: recordKey,
timeoutMs: 1500,
pollMs: 50,
messageIdPrefix: `${this.runtimeId}:viewport`,
},
);
return retryTransientPublicationRead(() => document.latest(this.queryContext()));
}
private async fetchIndexedDocument(
schemaId: string,
recordPrefix: string,
entityIndex: number,
): Promise<unknown> {
const recordKey = `${recordPrefix}.${Math.trunc(entityIndex)}`;
const document = CultMesh.documentFromPublication(
{
kind: "peer-snapshot",
peer: () => this.peer(),
endpoint: this.resolvedRudpEndpoint(),
},
schemaId,
recordKey,
{
documentId: recordKey,
routeHint: this.queryVerse.context.routeHint,
sourceId: recordKey,
timeoutMs: 1500,
pollMs: 50,
messageIdPrefix: `${this.runtimeId}:indexed-document`,
},
);
return retryTransientPublicationRead(() => document.latest(this.queryContext()));
}
private fetchPublicationDocument(schemaId: string): Promise<unknown> {
2026-07-05 13:48:28 +01:00
return retryTransientPublicationRead(() => this.publications.latest({ schemaId }, this.queryContext()));
}
private createPublicationSource(
mode: "local" | "remote",
): (binding: CultMeshPublicationDocumentBinding) => CultMeshDocumentPublicationSource {
if (mode === "remote") {
return () => ({
kind: "peer-snapshot",
peer: () => this.peer(),
endpoint: this.resolvedRudpEndpoint(),
});
}
return binding => ({
kind: "single-file",
path: (binding as AetheriaPublicationDocumentSpec).localPath,
});
2026-06-25 18:09:33 +01:00
}
private createViewportFeed() {
return CultMesh.liveFeed<AetheriaRuntimeViewportFeedRequest, AetheriaRuntimeViewportFeedSnapshot>(
"gamecult.aetheria.viewport_feed.v1",
2026-06-25 18:09:33 +01:00
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,
...this.queries.assetManifest.sources,
2026-06-25 18:09:33 +01:00
],
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<AetheriaRuntimeViewportFeedSnapshot> {
const startedAt = performance.now();
const selectedEntityIndex = request.selectedEntityIndex ?? -1;
const [
gravity,
daemonHealth,
authorityStatus,
starbridgeSession,
assetManifest,
2026-06-25 18:09:33 +01:00
selectedObject,
inventory,
] = await Promise.all([
2026-07-05 15:12:29 +01:00
this.queryExecutors.gravityViewport(request.viewport),
this.queryExecutors.daemonHealth(),
this.queryExecutors.authorityStatus(),
this.queryExecutors.starbridgeSession(),
this.queryExecutors.assetManifest(),
2026-06-25 18:09:33 +01:00
selectedEntityIndex >= 0
2026-07-05 15:12:29 +01:00
? this.queryExecutors.selectedObject({ entityIndex: selectedEntityIndex })
2026-06-25 18:09:33 +01:00
: Promise.resolve(null),
selectedEntityIndex >= 0
2026-07-05 15:12:29 +01:00
? this.queryExecutors.inventory({ entityIndex: selectedEntityIndex })
2026-06-25 18:09:33 +01:00
: Promise.resolve(null),
]);
return {
viewport: composeViewport(objects, gravity),
selectedObject,
inventory,
daemonHealth,
authorityStatus,
starbridgeSession,
assetManifest,
2026-06-25 18:09:33 +01:00
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<void> {
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: "aetheria-client",
tags: ["aetheria"],
2026-06-25 18:09:33 +01:00
},
};
const peer = await this.peer();
peer.send(message);
await delay(80);
}
private async peer(): Promise<CultNetPeer> {
if (this.#peer)
return this.#peer;
const peers = CultMesh.createPeerCatalog();
const leases = CultMesh.createAuthorityLeaseCatalog();
const leaseId = `${this.daemonTarget.peerId}:authority`;
peers.upsert({
peerId: this.daemonTarget.peerId,
verseId: this.daemonTarget.verseId,
endpoints: this.daemonTarget.endpoints,
roles: [this.daemonTarget.role],
authorityLeaseId: leaseId,
});
leases.upsert({
leaseId,
verseId: this.daemonTarget.verseId,
peerId: this.daemonTarget.peerId,
roles: [this.daemonTarget.role],
validFrom: new Date(Date.now() - 60_000),
expiresAt: new Date(Date.now() + 60_000),
});
this.#peer = await CultMesh.createRudpPeerForAuthorizedPeer(
this.runtimeId,
connectionId,
peers,
leases,
this.daemonTarget.verseId,
this.daemonTarget.role,
{
2026-06-25 18:09:33 +01:00
connectTimeoutMs: 2000,
maxFragmentBytes: 1200,
maxPendingReliablePackets: 512,
},
);
2026-06-25 18:09:33 +01:00
this.#peer.on("close", () => {
this.#peer = null;
});
return this.#peer;
}
private resolvedRudpEndpoint(): string {
const endpoint = this.daemonTarget.endpoints.find(value => value.toLowerCase().startsWith("rudp://"));
if (!endpoint) {
throw new Error(`Daemon ${this.daemonTarget.uri} has no resolved RUDP transport endpoint. Resolve it through Odin/CultMesh before opening a transport peer.`);
}
return endpoint;
}
}
function normalizeDaemonTarget(target: string | AetheriaCultMeshDaemonTarget): Required<AetheriaCultMeshDaemonTarget> {
const value = typeof target === "string"
? { uri: target }
: target;
if (!value.uri.trim()) {
throw new Error("Aetheria CultMesh daemon target URI is required.");
}
if (value.uri.toLowerCase().startsWith("rudp://")) {
throw new Error("Aetheria daemon targets must be CultMesh URIs; raw RUDP endpoints belong in resolved peer-card endpoints.");
}
return {
uri: value.uri,
peerId: value.peerId?.trim() || "aetheria-daemon",
verseId: value.verseId?.trim() || "aetheria.local",
role: value.role?.trim() || "aetheria-daemon",
endpoints: value.endpoints ?? [],
};
2026-06-25 18:09:33 +01:00
}
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 managedViewportRecordKey(prefix: string, request: ViewportRequest): string {
const minX = Math.min(finiteNumber(request.minX), finiteNumber(request.maxX));
const minY = Math.min(finiteNumber(request.minY), finiteNumber(request.maxY));
const maxX = Math.max(finiteNumber(request.minX), finiteNumber(request.maxX));
const maxY = Math.max(finiteNumber(request.minY), finiteNumber(request.maxY));
return `${prefix}.${viewportToken(minX)}.${viewportToken(minY)}.${viewportToken(maxX)}.${viewportToken(maxY)}`;
}
function viewportToken(value: number): string {
const rounded = Math.round(value * 1000) / 1000;
return rounded
.toString()
.replace("-", "n")
.replace(".", "p");
}
function finiteNumber(value: unknown): number {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
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";
}
2026-07-05 13:48:28 +01:00
async function retryTransientPublicationRead<T>(read: () => Promise<T>): Promise<T> {
let lastError: unknown;
2026-07-05 15:12:29 +01:00
for (let attempt = 0; attempt < 40; attempt += 1) {
2026-07-05 13:48:28 +01:00
try {
return await read();
} catch (error) {
lastError = error;
if (!isTransientPublicationReadError(error))
throw error;
2026-07-05 15:12:29 +01:00
await delay(25 + attempt * 8);
2026-07-05 13:48:28 +01:00
}
}
throw lastError;
}
function isTransientPublicationReadError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
2026-07-05 15:12:29 +01:00
return message.includes("EBUSY") ||
message.includes("EPERM") ||
message.includes("ENOENT") ||
message.includes("did not contain schema");
2026-07-05 13:48:28 +01:00
}
2026-06-25 18:09:33 +01:00
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.game_viewport.v1",
2026-06-25 18:09:33 +01:00
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 createAetheriaPublicationDocuments(
statePath: string,
mode: "local" | "remote",
): readonly AetheriaPublicationDocumentSpec[] {
const documents: readonly AetheriaPublicationDocumentSpec[] = [
{
...CultMesh.publicationDocument(
AetheriaRtsSchemas.daemonFrame,
"daemon:aetheria.frame.latest.v1",
{
documentId: "daemon:aetheria.frame.latest.v1",
sourceId: "daemon:aetheria.frame.latest.v1",
},
),
2026-06-30 19:14:49 +01:00
localPath: statePath,
},
{
...CultMesh.publicationDocument(
AetheriaRtsSchemas.daemonHealth,
"daemon:aetheria.health.latest.v1",
{
documentId: "daemon:aetheria.health.latest.v1",
sourceId: "daemon:aetheria.health.latest.v1",
},
),
2026-06-30 19:14:49 +01:00
localPath: statePath,
remoteRecordKey: "daemon:aetheria.health.v1",
},
{
...CultMesh.publicationDocument(
AetheriaRtsSchemas.verseAuthorityPolicy,
"daemon:aetheria.authority.policy.latest.v1",
{
documentId: "daemon:aetheria.authority.policy.latest.v1",
sourceId: "daemon:aetheria.authority.policy.latest.v1",
},
),
2026-06-30 19:14:49 +01:00
localPath: statePath,
remoteRecordKey: "global:aetheria.verse_authority_policy.v1",
},
{
...CultMesh.publicationDocument(
AetheriaRtsSchemas.starbridgeSessionSummary,
"daemon:aetheria.starbridge.session.latest.v1",
{
documentId: "daemon:aetheria.starbridge.session.latest.v1",
sourceId: "daemon:aetheria.starbridge.session.latest.v1",
},
),
2026-06-30 19:14:49 +01:00
localPath: statePath,
},
{
...CultMesh.publicationDocument(
AetheriaRtsSchemas.assetManifest,
"daemon:aetheria.asset_manifest.latest.v1",
{
documentId: "daemon:aetheria.asset_manifest.latest.v1",
sourceId: "daemon:aetheria.asset_manifest.latest.v1",
},
),
2026-06-30 19:14:49 +01:00
localPath: statePath,
},
];
return documents.map(document => mode === "remote" && document.remoteRecordKey
? {
...document,
recordKey: document.remoteRecordKey,
}
: document);
}
function readField(value: unknown, property: string, index: number): unknown {
if (Array.isArray(value))
return value[index];
if (value && typeof value === "object") {
const record = value as Record<string, unknown>;
if (property in record)
return record[property];
const pascal = property.charAt(0).toUpperCase() + property.slice(1);
if (pascal in record)
return record[pascal];
}
return undefined;
}
function arrayValue(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function normalizeCommandPayload(payload: Record<string, unknown> | undefined): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(payload ?? {})) {
if (value == null)
continue;
result[key] = typeof value === "string" ? value : String(value);
}
return result;
}
function objectValue(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
function stringOr(value: unknown, fallback: string): string {
return typeof value === "string" ? value : fallback;
}
function numberOr(value: unknown, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function stringRecord(value: unknown): Record<string, string> {
const source = objectValue(value);
const result: Record<string, string> = {};
for (const [key, entry] of Object.entries(source)) {
if (entry == null)
continue;
result[key] = typeof entry === "string" ? entry : String(entry);
}
return result;
}
2026-06-25 18:09:33 +01:00
function delay(milliseconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}