Aetheria/Aetheria.Rts.Web/Electron/aetheria-rts-local-documents.ts

495 lines
22 KiB
TypeScript

import {
AetheriaRtsSchemas,
aetheriaRuntimeAssetManifestDocumentSlots as assetManifestSlots,
aetheriaRuntimeAssetManifestEntrySlots as assetManifestEntrySlots,
aetheriaRuntimeAssetRefSlots as assetRefSlots,
aetheriaRuntimeAuthorityRuleSlots as authorityRuleSlots,
aetheriaRuntimeDaemonHealthDocumentSlots as healthSlots,
aetheriaRuntimeGameViewportBoundsSlots as viewportBoundsSlots,
aetheriaRuntimeGameViewportDocumentSlots as gameViewportSlots,
aetheriaRuntimeGameViewportObjectSlots as viewportObjectSlots,
aetheriaRuntimeGravityViewportDocumentSlots as gravityViewportSlots,
aetheriaRuntimeInventoryDocumentSlots as inventoryDocumentSlots,
aetheriaRuntimeObjectsViewportDocumentSlots as objectsViewportSlots,
aetheriaRuntimeRenderSplatLayerDefinitionSlots as renderSplatLayerSlots,
aetheriaRuntimeRenderSplatsViewportDocumentSlots as renderSplatsViewportSlots,
aetheriaRuntimeRenderSplatSoaSlots as renderSplatSoaSlots,
aetheriaRuntimeRtsBodyViewSlots as bodyViewSlots,
aetheriaRuntimeRtsEntityStatusSlots as viewportStatusSlots,
aetheriaRuntimeRtsGravityInfluenceSlots as gravityInfluenceSlots,
aetheriaRuntimeRtsInventoryItemSlots as viewportInventoryItemSlots,
aetheriaRuntimeSelectedObjectDocumentSlots as selectedObjectDocumentSlots,
aetheriaRuntimeStarbridgeBaseStatusSlots as starbridgeBaseSlots,
aetheriaRuntimeStarbridgeRuntimeRoleSlots as starbridgeRoleSlots,
aetheriaRuntimeStarbridgeSessionSummaryDocumentSlots as starbridgeSummarySlots,
aetheriaRuntimeStarbridgeStationStockItemSlots as starbridgeStockSlots,
aetheriaRuntimeStarbridgeWaveForecastSlots as starbridgeWaveForecastSlots,
aetheriaRuntimeVerseAuthorityPolicyDocumentSlots as authorityPolicySlots,
} from "./aetheria-rts-generated-bindings.js";
import { cultMeshRectFromBounds, cultMeshViewportRequest } from "cultmesh-ts";
import type {
AssetManifestDocument,
AssetRef,
AuthorityStatusDocument,
BodyView,
DaemonHealthDocument,
EntityStatus,
GravityViewportResponse,
GravityInfluence,
InventoryDocument,
InventoryItem,
ObjectsViewportResponse,
RenderSplatsViewportResponse,
SelectedObjectDocument,
StarbridgeSessionDocument,
ViewObject,
ViewportRequest,
ViewportResponse,
} from "./aetheria-rts-bindings.js";
const missingDaemonRunId = "aetheria.run.unknown";
export function readSelectedObjectDocument(selectedObjectDocument: unknown): SelectedObjectDocument {
const document = arr(selectedObjectDocument);
return {
schema: str(document[selectedObjectDocumentSlots.schema]) || AetheriaRtsSchemas.selectedObject,
frameId: num(document[selectedObjectDocumentSlots.frameId]),
runId: str(document[selectedObjectDocumentSlots.runId]) || missingDaemonRunId,
zoneIndex: num(document[selectedObjectDocumentSlots.zoneIndex]),
entityIndex: num(document[selectedObjectDocumentSlots.entityIndex], -1),
selected: readNullableViewportObject(document[selectedObjectDocumentSlots.selected]),
};
}
export function readInventoryDocument(inventoryDocument: unknown): InventoryDocument {
const document = arr(inventoryDocument);
return {
schema: str(document[inventoryDocumentSlots.schema]) || AetheriaRtsSchemas.inventory,
frameId: num(document[inventoryDocumentSlots.frameId]),
runId: str(document[inventoryDocumentSlots.runId]) || missingDaemonRunId,
zoneIndex: num(document[inventoryDocumentSlots.zoneIndex]),
entityIndex: num(document[inventoryDocumentSlots.entityIndex], -1),
entityKey: str(document[inventoryDocumentSlots.entityKey]),
items: list<unknown[]>(document[inventoryDocumentSlots.items]).map(readViewportInventoryItem),
equipment: list<unknown[]>(document[inventoryDocumentSlots.equipment]).map(readViewportInventoryItem),
cargo: list<unknown[]>(document[inventoryDocumentSlots.cargo]).map(readViewportInventoryItem),
};
}
export function readViewportDocument(viewportDocument: unknown): ViewportResponse {
const document = arr(viewportDocument);
const objects = readViewportObjects(list<unknown[]>(document[gameViewportSlots.objects]));
const gravityInfluences = readGravityInfluences(list<unknown[]>(document[gameViewportSlots.gravityInfluences]));
const bodies = readBodyViews(list<unknown[]>(document[gameViewportSlots.bodies]));
return {
schema: str(document[gameViewportSlots.schema]) || AetheriaRtsSchemas.gameViewport,
frameId: num(document[gameViewportSlots.frameId]),
publishedAtUtc: str(document[gameViewportSlots.publishedAtUtc]),
simulationTimeSeconds: num(document[gameViewportSlots.simulationTimeSeconds]),
runId: str(document[gameViewportSlots.runId]) || missingDaemonRunId,
zoneIndex: num(document[gameViewportSlots.zoneIndex]),
zoneName: str(document[gameViewportSlots.zoneName]),
currentEntityKey: str(document[gameViewportSlots.currentEntityKey]),
viewport: readViewportBounds(document[gameViewportSlots.viewport]),
controlledEntityIndices: numberList(document[gameViewportSlots.controlledEntityIndices]),
objects,
gravityInfluences,
bodies,
};
}
export function readObjectsViewportDocument(viewportDocument: unknown): ObjectsViewportResponse {
const document = arr(viewportDocument);
return {
schema: str(document[objectsViewportSlots.schema]) || AetheriaRtsSchemas.objectsViewport,
frameId: num(document[objectsViewportSlots.frameId]),
publishedAtUtc: str(document[objectsViewportSlots.publishedAtUtc]),
simulationTimeSeconds: num(document[objectsViewportSlots.simulationTimeSeconds]),
runId: str(document[objectsViewportSlots.runId]) || missingDaemonRunId,
zoneIndex: num(document[objectsViewportSlots.zoneIndex]),
zoneName: str(document[objectsViewportSlots.zoneName]),
currentEntityKey: str(document[objectsViewportSlots.currentEntityKey]),
viewport: readViewportBounds(document[objectsViewportSlots.viewport]),
controlledEntityIndices: numberList(document[objectsViewportSlots.controlledEntityIndices]),
objects: readViewportObjects(list<unknown[]>(document[objectsViewportSlots.objects])),
};
}
export function readGravityViewportDocument(viewportDocument: unknown): GravityViewportResponse {
const document = arr(viewportDocument);
return {
schema: str(document[gravityViewportSlots.schema]) || AetheriaRtsSchemas.gravityViewport,
frameId: num(document[gravityViewportSlots.frameId]),
publishedAtUtc: str(document[gravityViewportSlots.publishedAtUtc]),
simulationTimeSeconds: num(document[gravityViewportSlots.simulationTimeSeconds]),
runId: str(document[gravityViewportSlots.runId]) || missingDaemonRunId,
zoneIndex: num(document[gravityViewportSlots.zoneIndex]),
zoneName: str(document[gravityViewportSlots.zoneName]),
viewport: readViewportBounds(document[gravityViewportSlots.viewport]),
gravityInfluences: readGravityInfluences(list<unknown[]>(document[gravityViewportSlots.gravityInfluences])),
bodies: readBodyViews(list<unknown[]>(document[gravityViewportSlots.bodies])),
};
}
export function readRenderSplatsViewportDocument(viewportDocument: unknown): RenderSplatsViewportResponse {
const document = arr(viewportDocument);
return {
schema: str(document[renderSplatsViewportSlots.schema]) || AetheriaRtsSchemas.renderSplatsViewport,
frameId: num(document[renderSplatsViewportSlots.frameId]),
publishedAtUtc: str(document[renderSplatsViewportSlots.publishedAtUtc]),
simulationTimeSeconds: num(document[renderSplatsViewportSlots.simulationTimeSeconds]),
runId: str(document[renderSplatsViewportSlots.runId]) || missingDaemonRunId,
zoneIndex: num(document[renderSplatsViewportSlots.zoneIndex]),
zoneName: str(document[renderSplatsViewportSlots.zoneName]),
viewport: readViewportBounds(document[renderSplatsViewportSlots.viewport]),
layers: list<unknown[]>(document[renderSplatsViewportSlots.layers]).map(readRenderSplatLayer),
splats: readRenderSplatSoa(document[renderSplatsViewportSlots.splats]),
};
}
export function readDaemonHealthDocument(healthDocument: unknown): DaemonHealthDocument {
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 readAuthorityStatusDocument(policyDocument: unknown): AuthorityStatusDocument {
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<unknown[]>(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 readStarbridgeSessionSummaryDocument(summaryDocument: unknown): StarbridgeSessionDocument {
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<unknown[]>(summary[starbridgeSummarySlots.stationStock]).map(toStarbridgeStationStockItem),
waveForecast: list<unknown[]>(summary[starbridgeSummarySlots.waveForecast]).map(toStarbridgeWaveForecast),
runtimeRoles: list<unknown[]>(summary[starbridgeSummarySlots.runtimeRoles]).map(toStarbridgeRuntimeRole),
};
}
export function readAssetManifestDocument(assetManifestDocument: unknown): AssetManifestDocument {
const manifest = arr(assetManifestDocument);
return {
schema: str(manifest[assetManifestSlots.schema]) || AetheriaRtsSchemas.assetManifest,
publishedAtUtc: str(manifest[assetManifestSlots.publishedAtUtc]),
runId: str(manifest[assetManifestSlots.runId]),
baseUri: str(manifest[assetManifestSlots.baseUri]),
assets: list<unknown[]>(manifest[assetManifestSlots.assets]).map(toAssetManifestEntry),
};
}
function readViewportBounds(value: unknown): ViewportRequest {
const bounds = arr(value);
return cultMeshViewportRequest(
cultMeshRectFromBounds(
num(bounds[viewportBoundsSlots.minX]),
num(bounds[viewportBoundsSlots.minY]),
num(bounds[viewportBoundsSlots.maxX]),
num(bounds[viewportBoundsSlots.maxY]),
),
[],
);
}
function readViewportObjects(objects: unknown[][]): ViewObject[] {
return objects.map(readViewportObject);
}
function readNullableViewportObject(value: unknown): ViewObject | null {
return Array.isArray(value) ? readViewportObject(value) : null;
}
function readViewportObject(object: unknown[]): ViewObject {
return {
entityIndex: num(object[viewportObjectSlots.entityIndex], -1),
entityKey: str(object[viewportObjectSlots.entityKey]),
displayName: str(object[viewportObjectSlots.displayName]),
kind: str(object[viewportObjectSlots.kind]),
factionKey: str(object[viewportObjectSlots.factionKey]),
x: num(object[viewportObjectSlots.x]),
y: num(object[viewportObjectSlots.y]),
z: num(object[viewportObjectSlots.z]),
directionX: num(object[viewportObjectSlots.directionX]),
directionY: num(object[viewportObjectSlots.directionY]),
velocityX: num(object[viewportObjectSlots.velocityX]),
velocityY: num(object[viewportObjectSlots.velocityY]),
controlled: bool(object[viewportObjectSlots.controlled]),
targetEntityIndex: num(object[viewportObjectSlots.targetEntityIndex], -1),
isActive: object[viewportObjectSlots.isActive] !== false,
visibility: num(object[viewportObjectSlots.visibility]),
iconAsset: assetRef(arr(object[viewportObjectSlots.iconAsset])),
status: readViewportStatus(object[viewportObjectSlots.status]),
inventory: list<unknown[]>(object[viewportObjectSlots.inventory]).map(readViewportInventoryItem),
};
}
function readViewportStatus(value: unknown): EntityStatus {
const status = arr(value);
return {
hull: num(status[viewportStatusSlots.hull]),
shield: num(status[viewportStatusSlots.shield]),
heat: num(status[viewportStatusSlots.heat]),
};
}
function readViewportInventoryItem(value: unknown[]): InventoryItem {
const itemKey = str(value[viewportInventoryItemSlots.itemKey]);
return {
source: str(value[viewportInventoryItemSlots.source]),
itemKey,
quantity: num(value[viewportInventoryItemSlots.quantity]),
quality: num(value[viewportInventoryItemSlots.quality]),
durability: num(value[viewportInventoryItemSlots.durability]),
enabled: value[viewportInventoryItemSlots.enabled] !== false,
iconAsset: assetRef(arr(value[viewportInventoryItemSlots.iconAsset])),
};
}
function readGravityInfluences(influences: unknown[][]): GravityInfluence[] {
return influences.map(influence => ({
bodyKey: str(influence[gravityInfluenceSlots.bodyKey]),
orbitKey: str(influence[gravityInfluenceSlots.orbitKey]),
kind: str(influence[gravityInfluenceSlots.kind]),
x: num(influence[gravityInfluenceSlots.x]),
y: num(influence[gravityInfluenceSlots.y]),
radius: num(influence[gravityInfluenceSlots.radius]),
gravityDepth: num(influence[gravityInfluenceSlots.gravityDepth]),
gravityDepthExponent: num(influence[gravityInfluenceSlots.gravityDepthExponent]),
waveRadius: num(influence[gravityInfluenceSlots.waveRadius]),
waveDepth: num(influence[gravityInfluenceSlots.waveDepth]),
waveSpeed: num(influence[gravityInfluenceSlots.waveSpeed]),
}));
}
function readBodyViews(bodies: unknown[][]): BodyView[] {
return bodies.map(body => {
const kind = str(body[bodyViewSlots.kind]);
return {
bodyKey: str(body[bodyViewSlots.bodyKey]),
orbitKey: str(body[bodyViewSlots.orbitKey]),
name: str(body[bodyViewSlots.name]),
kind,
x: num(body[bodyViewSlots.x]),
y: num(body[bodyViewSlots.y]),
radius: num(body[bodyViewSlots.radius]),
isAsteroidBelt: bool(body[bodyViewSlots.isAsteroidBelt]),
body: num(body[bodyViewSlots.body]),
iconAsset: assetRef(arr(body[bodyViewSlots.iconAsset])),
iconSize: num(body[bodyViewSlots.iconSize]),
};
});
}
function readRenderSplatLayer(layer: unknown[]): RenderSplatsViewportResponse["layers"][number] {
return {
layerKey: str(layer[renderSplatLayerSlots.layerKey]),
displayName: str(layer[renderSplatLayerSlots.displayName]),
channel: num(layer[renderSplatLayerSlots.channel]),
blendMode: str(layer[renderSplatLayerSlots.blendMode]),
graphicsFormat: str(layer[renderSplatLayerSlots.graphicsFormat]),
clearBeforeDraw: bool(layer[renderSplatLayerSlots.clearBeforeDraw]),
clearR: num(layer[renderSplatLayerSlots.clearR]),
clearG: num(layer[renderSplatLayerSlots.clearG]),
clearB: num(layer[renderSplatLayerSlots.clearB]),
clearA: num(layer[renderSplatLayerSlots.clearA]),
};
}
function readRenderSplatSoa(value: unknown): RenderSplatsViewportResponse["splats"] {
const splats = arr(value);
return {
count: num(splats[renderSplatSoaSlots.count]),
centerX: numberList(splats[renderSplatSoaSlots.centerX]),
centerY: numberList(splats[renderSplatSoaSlots.centerY]),
halfExtentX: numberList(splats[renderSplatSoaSlots.halfExtentX]),
halfExtentY: numberList(splats[renderSplatSoaSlots.halfExtentY]),
rotationCos: numberList(splats[renderSplatSoaSlots.rotationCos]),
rotationSin: numberList(splats[renderSplatSoaSlots.rotationSin]),
channel: numberList(splats[renderSplatSoaSlots.channel]),
falloff: numberList(splats[renderSplatSoaSlots.falloff]),
valueR: numberList(splats[renderSplatSoaSlots.valueR]),
valueG: numberList(splats[renderSplatSoaSlots.valueG]),
valueB: numberList(splats[renderSplatSoaSlots.valueB]),
valueA: numberList(splats[renderSplatSoaSlots.valueA]),
sourceKey: stringList(splats[renderSplatSoaSlots.sourceKey]),
layerIndex: numberList(splats[renderSplatSoaSlots.layerIndex]),
sourceKind: numberList(splats[renderSplatSoaSlots.sourceKind]),
frequencyX: numberList(splats[renderSplatSoaSlots.frequencyX]),
frequencyY: numberList(splats[renderSplatSoaSlots.frequencyY]),
phaseX: numberList(splats[renderSplatSoaSlots.phaseX]),
phaseY: numberList(splats[renderSplatSoaSlots.phaseY]),
animationSpeed: numberList(splats[renderSplatSoaSlots.animationSpeed]),
sourceFlags: numberList(splats[renderSplatSoaSlots.sourceFlags]),
};
}
function toStarbridgeBaseStatus(status: unknown[]): StarbridgeSessionDocument["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[]): StarbridgeSessionDocument["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",
iconAsset: assetRef(arr(item[starbridgeStockSlots.iconAsset])),
};
}
function toStarbridgeWaveForecast(wave: unknown[]): StarbridgeSessionDocument["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[]): StarbridgeSessionDocument["runtimeRoles"][number] {
return {
runtimeId: str(role[starbridgeRoleSlots.runtimeId]),
role: str(role[starbridgeRoleSlots.role]),
entityKey: str(role[starbridgeRoleSlots.entityKey]),
};
}
function toAssetManifestEntry(entry: unknown[]): AssetManifestDocument["assets"][number] {
return {
ref: assetRef(arr(entry[assetManifestEntrySlots.ref])),
sizeBytes: num(entry[assetManifestEntrySlots.sizeBytes]),
width: num(entry[assetManifestEntrySlots.width]),
height: num(entry[assetManifestEntrySlots.height]),
tags: stringList(entry[assetManifestEntrySlots.tags]),
};
}
function assetRef(value: unknown[]): AssetRef {
const assetKey = str(value[assetRefSlots.assetKey]);
if (assetKey.length === 0)
return emptyAsset();
return asset(
assetKey,
str(value[assetRefSlots.kind]),
str(value[assetRefSlots.uri]),
str(value[assetRefSlots.transport]),
str(value[assetRefSlots.mimeType]),
str(value[assetRefSlots.contentHash]),
stringRecord(value[assetRefSlots.metadata]),
);
}
function asset(
assetKey: string,
kind: string,
uri: string,
transport: string,
mimeType: string,
contentHash = "",
metadata: Record<string, string> = {},
): AssetRef {
return {
assetKey,
kind,
uri,
transport,
contentHash,
mimeType,
metadata,
};
}
function emptyAsset(kind = ""): AssetRef {
return asset("", kind, "", "", "");
}
function arr(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function list<T extends unknown[]>(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 stringRecord(value: unknown): Record<string, string> {
if (!value || typeof value !== "object" || Array.isArray(value))
return {};
return Object.fromEntries(
Object.entries(value)
.map(([key, candidate]) => [key, 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;
}