Fetch Electron viewports from daemon documents

This commit is contained in:
Meta Vens 2026-07-06 03:21:20 +01:00
commit d91e87b6d6
4 changed files with 320 additions and 516 deletions

View file

@ -52,13 +52,13 @@ import {
readAuthorityStatusDocument,
readAssetManifestDocument,
readDaemonHealthDocument,
buildGravityViewportDocumentFromFrame,
buildInventoryDocumentFromFrame,
buildObjectsViewportDocumentFromFrame,
buildRenderSplatsViewportDocumentFromFrame,
readGravityViewportDocument,
readObjectsViewportDocument,
readRenderSplatsViewportDocument,
buildSelectedObjectDocumentFromFrame,
readStarbridgeSessionSummaryDocument,
buildViewportDocumentFromFrame,
readViewportDocument,
} from "./aetheria-rts-local-documents.js";
const connectionId = 0x43554c54;
@ -208,10 +208,14 @@ export class AetheriaCultMeshClient {
},
);
const executors = {
mapViewport: async (request: ViewportRequest) => buildViewportDocumentFromFrame(await this.fetchLatestFrameDocument(), request),
objectsViewport: async (request: ViewportRequest) => buildObjectsViewportDocumentFromFrame(await this.fetchLatestFrameDocument(), request),
gravityViewport: async (request: ViewportRequest) => buildGravityViewportDocumentFromFrame(await this.fetchLatestFrameDocument(), request),
renderSplatsViewport: async (request: ViewportRequest) => buildRenderSplatsViewportDocumentFromFrame(await this.fetchLatestFrameDocument(), request),
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) => buildSelectedObjectDocumentFromFrame(await this.fetchLatestFrameDocument(), request),
inventory: async (request: SelectedObjectRequest) => buildInventoryDocumentFromFrame(await this.fetchLatestFrameDocument(), request),
daemonHealth: async () => readDaemonHealthDocument(await this.fetchDaemonHealthDocument()),
@ -481,6 +485,33 @@ export class AetheriaCultMeshClient {
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 fetchPublicationDocument(schemaId: string): Promise<unknown> {
return retryTransientPublicationRead(() => this.publications.latest({ schemaId }, this.queryContext()));
}
@ -694,6 +725,26 @@ function normalizeCultMeshAssetUri(uri: string): string {
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;

View file

@ -4,17 +4,26 @@ import {
aetheriaRuntimeAssetManifestEntrySlots as assetManifestEntrySlots,
aetheriaRuntimeAssetRefSlots as assetRefSlots,
aetheriaRuntimeAuthorityRuleSlots as authorityRuleSlots,
aetheriaRuntimeBodySnapshotCommitSlots as bodySlots,
aetheriaRuntimeCargoBayLoadoutCommitSlots as cargoBaySlots,
aetheriaRuntimeDaemonFrameDocumentSlots as frameSlots,
aetheriaRuntimeDaemonHealthDocumentSlots as healthSlots,
aetheriaRuntimeEntitySnapshotCommitSlots as entitySlots,
aetheriaRuntimeEntityStatGridCommitSlots as statGridSlots,
aetheriaRuntimeGameViewportBoundsSlots as viewportBoundsSlots,
aetheriaRuntimeGameViewportDocumentSlots as gameViewportSlots,
aetheriaRuntimeGameViewportObjectSlots as viewportObjectSlots,
aetheriaRuntimeGravityViewportDocumentSlots as gravityViewportSlots,
aetheriaRuntimeLoadoutItemCommitSlots as itemSlots,
aetheriaRuntimeLoadoutItemSlotCommitSlots as itemSlotSlots,
aetheriaRuntimeProjectileCommitSlots as projectileSlots,
aetheriaRuntimeObjectsViewportDocumentSlots as objectsViewportSlots,
aetheriaRuntimeRenderSplatLayerDefinitionSlots as renderSplatLayerSlots,
aetheriaRuntimeRenderSplatsViewportDocumentSlots as renderSplatsViewportSlots,
aetheriaRuntimeRenderSplatSoaSlots as renderSplatSoaSlots,
aetheriaRuntimeRtsBodyViewSlots as bodyViewSlots,
aetheriaRuntimeRtsEntityStatusSlots as viewportStatusSlots,
aetheriaRuntimeRtsGravityInfluenceSlots as gravityInfluenceSlots,
aetheriaRuntimeRtsInventoryItemSlots as viewportInventoryItemSlots,
aetheriaRuntimeRunCheckpointCommitSlots as runSlots,
aetheriaRuntimeSunVisualCommitSlots as sunVisualSlots,
aetheriaRuntimeStarbridgeBaseStatusSlots as starbridgeBaseSlots,
aetheriaRuntimeStarbridgeRuntimeRoleSlots as starbridgeRoleSlots,
aetheriaRuntimeStarbridgeSessionSummaryDocumentSlots as starbridgeSummarySlots,
@ -30,6 +39,7 @@ import type {
AuthorityStatusDocument,
BodyView,
DaemonHealthDocument,
EntityStatus,
GravityViewportResponse,
GravityInfluence,
InventoryDocument,
@ -46,300 +56,6 @@ import type {
const missingDaemonRunId = "aetheria.run.unknown";
export function buildViewportDocumentFromFrame(frameDocument: unknown, request: ViewportRequest): ViewportResponse {
const objects = buildObjectsViewportDocumentFromFrame(frameDocument, request);
const gravity = buildGravityViewportDocumentFromFrame(frameDocument, request);
return {
schema: AetheriaRtsSchemas.gameViewport,
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 buildObjectsViewportDocumentFromFrame(
frameDocument: unknown,
request: ViewportRequest,
): ObjectsViewportResponse {
const frame = arr(frameDocument);
const run = arr(frame[frameSlots.run]);
const zones = list<unknown[]>(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]) || missingDaemonRunId;
const viewport = normalizeViewport(request);
const entities = list<unknown[]>(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 projectiles = list<unknown[]>(zone[zoneSlots.projectiles]);
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])))
.concat(projectiles
.filter(projectile => projectile[projectileSlots.active] !== false)
.filter(projectile => projectileIntersectsViewport(projectile, viewport))
.map(toProjectileViewObject));
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 buildGravityViewportDocumentFromFrame(
frameDocument: unknown,
request: ViewportRequest,
): GravityViewportResponse {
const frame = arr(frameDocument);
const run = arr(frame[frameSlots.run]);
const zones = list<unknown[]>(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]) || missingDaemonRunId;
const viewport = normalizeViewport(request);
const visibleBodies = list<unknown[]>(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 buildRenderSplatsViewportDocumentFromFrame(
frameDocument: unknown,
request: ViewportRequest,
): RenderSplatsViewportResponse {
const frame = arr(frameDocument);
const run = arr(frame[frameSlots.run]);
const zones = list<unknown[]>(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]) || missingDaemonRunId;
const viewport = normalizeViewport(request);
const layers = defaultRenderSplatLayers();
const layerIndices: ReadonlyMap<string, number> = new Map(layers.map((layer, index) => [layer.layerKey, index] as const));
const builder = new RenderSplatBuilder();
const viewportCenterX = (viewport.minX + viewport.maxX) * 0.5;
const viewportCenterY = (viewport.minY + viewport.maxY) * 0.5;
const viewportHalfX = Math.max(0.0001, (viewport.maxX - viewport.minX) * 0.5);
const viewportHalfY = Math.max(0.0001, (viewport.maxY - viewport.minY) * 0.5);
const terrainDepth = num(zone[zoneSlots.gravityTerrainDepth]);
const terrainWaveFrequency = num(zone[zoneSlots.gravityTerrainWaveFrequency], 1);
if (terrainDepth !== 0) {
builder.add({
layerIndex: requiredLayerIndex(layerIndices, "gravity.height"),
centerX: viewportCenterX,
centerY: viewportCenterY,
halfExtentX: viewportHalfX,
halfExtentY: viewportHalfY,
channel: 1,
falloff: 0,
valueR: -terrainDepth,
valueA: 1,
sourceKey: "environment.gravity_terrain",
sourceKind: 2,
frequencyX: 3,
frequencyY: 3,
animationSpeed: terrainWaveFrequency * 0.025,
sourceFlags: 1,
});
}
builder.add({
layerIndex: requiredLayerIndex(layerIndices, "fog.surface_height"),
centerX: viewportCenterX,
centerY: viewportCenterY,
halfExtentX: viewportHalfX,
halfExtentY: viewportHalfY,
channel: 4,
falloff: 0,
valueR: 1,
valueA: 1,
sourceKey: "environment.fog_surface_height",
sourceKind: 2,
frequencyX: 4,
frequencyY: 4,
animationSpeed: 0.015,
sourceFlags: 1,
});
builder.add({
layerIndex: requiredLayerIndex(layerIndices, "fog.patch_height"),
centerX: viewportCenterX,
centerY: viewportCenterY,
halfExtentX: viewportHalfX,
halfExtentY: viewportHalfY,
channel: 4,
falloff: 0,
valueR: 1,
valueA: 1,
sourceKey: "environment.fog_patch_height",
sourceKind: 2,
frequencyX: 9,
frequencyY: 9,
animationSpeed: 0.02,
sourceFlags: 1,
});
builder.add({
layerIndex: requiredLayerIndex(layerIndices, "fog.patch"),
centerX: viewportCenterX,
centerY: viewportCenterY,
halfExtentX: viewportHalfX,
halfExtentY: viewportHalfY,
channel: 4,
falloff: 0,
valueR: 1,
valueA: 1,
sourceKey: "environment.fog_patch",
sourceKind: 2,
frequencyX: 6,
frequencyY: 6,
animationSpeed: 0.01,
sourceFlags: 1,
});
for (const body of list<unknown[]>(zone[zoneSlots.bodies])) {
if (!gravityInfluenceIntersectsViewport(body, viewport))
continue;
const radius = resolveGravityRadius(body);
const bodyKey = str(body[bodySlots.bodyKey]);
builder.add({
layerIndex: requiredLayerIndex(layerIndices, "gravity.height"),
centerX: num(body[bodySlots.gravityInfluenceCenterX]),
centerY: num(body[bodySlots.gravityInfluenceCenterZ]),
halfExtentX: radius,
halfExtentY: radius,
channel: 1,
falloff: 3,
valueR: num(body[bodySlots.gravityWellDepth]),
valueA: 1,
sourceKey: bodyKey,
});
if (num(body[bodySlots.gravityWaveRadius]) > 0 && num(body[bodySlots.gravityWaveDepth]) !== 0) {
builder.add({
layerIndex: requiredLayerIndex(layerIndices, "gravity.wave"),
centerX: num(body[bodySlots.gravityInfluenceCenterX]),
centerY: num(body[bodySlots.gravityInfluenceCenterZ]),
halfExtentX: num(body[bodySlots.gravityWaveRadius]),
halfExtentY: num(body[bodySlots.gravityWaveRadius]),
channel: 2,
falloff: 2,
valueR: num(body[bodySlots.gravityWaveDepth]),
valueG: num(body[bodySlots.gravityWaveSpeed]),
valueA: 1,
sourceKey: bodyKey,
});
}
if (str(body[bodySlots.kind]).toLowerCase().includes("sun")) {
const sunVisual = arr(body[bodySlots.sunVisual]);
const tintRadius = Math.max(
radius,
Math.max(32, num(body[bodySlots.bodyRadiusMultiplier]) * 70) *
Math.max(0.01, num(sunVisual[sunVisualSlots.lightRadiusMultiplier], 1)),
);
builder.add({
layerIndex: requiredLayerIndex(layerIndices, "fog.tint"),
centerX: num(body[bodySlots.gravityInfluenceCenterX]),
centerY: num(body[bodySlots.gravityInfluenceCenterZ]),
halfExtentX: tintRadius,
halfExtentY: tintRadius,
channel: 4,
falloff: 2,
valueR: num(sunVisual[sunVisualSlots.fogTintColorX]),
valueG: num(sunVisual[sunVisualSlots.fogTintColorY]),
valueB: num(sunVisual[sunVisualSlots.fogTintColorZ]),
valueA: 1,
sourceKey: bodyKey,
});
}
}
for (const entity of list<unknown[]>(zone[zoneSlots.entities])) {
if (!isPlayerControlled(entity))
continue;
const visibility = Math.max(180, num(entity[entitySlots.visibility]));
const x = num(entity[entitySlots.positionX]);
const y = num(entity[entitySlots.positionZ]);
if (x + visibility < viewport.minX ||
x - visibility > viewport.maxX ||
y + visibility < viewport.minY ||
y - visibility > viewport.maxY) {
continue;
}
builder.add({
layerIndex: requiredLayerIndex(layerIndices, "visibility.mask"),
centerX: x,
centerY: y,
halfExtentX: visibility,
halfExtentY: visibility,
channel: 0,
falloff: 2,
valueR: 1,
valueG: 1,
valueB: 1,
valueA: 1,
sourceKey: entityKey(runId, num(zone[zoneSlots.zoneIndex]), num(entity[entitySlots.entityIndex], -1)),
});
}
return {
schema: AetheriaRtsSchemas.renderSplatsViewport,
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,
layers,
splats: builder.build(),
};
}
export function buildSelectedObjectDocumentFromFrame(
frameDocument: unknown,
request: SelectedObjectRequest,
@ -373,6 +89,77 @@ export function buildInventoryDocumentFromFrame(frameDocument: unknown, request:
};
}
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 {
@ -467,13 +254,147 @@ function frameContext(frameDocument: unknown): {
};
}
function normalizeViewport(request: ViewportRequest): ViewportRequest {
function readViewportBounds(value: unknown): ViewportRequest {
const bounds = arr(value);
return cultMeshViewportRequest(
cultMeshRectFromBounds(request.minX, request.minY, request.maxX, request.maxY),
request.controlledEntityIndices,
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 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]), entityIconAsset(str(object[viewportObjectSlots.kind]), bool(object[viewportObjectSlots.controlled]))),
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]), itemIconAsset(itemKey)),
};
}
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]), bodyIconAsset(kind)),
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 toViewObject(entity: unknown[], runId: string, zoneIndex: number): ViewObject {
const kind = str(entity[entitySlots.kind]);
const controlled = isPlayerControlled(entity);
@ -504,34 +425,6 @@ function toViewObject(entity: unknown[], runId: string, zoneIndex: number): View
};
}
function toProjectileViewObject(projectile: unknown[]): ViewObject {
return {
entityIndex: -1,
entityKey: str(projectile[projectileSlots.projectileId]),
displayName: str(projectile[projectileSlots.weaponKind]) || "projectile",
kind: "projectile",
factionKey: str(projectile[projectileSlots.factionKey]),
x: num(projectile[projectileSlots.positionX]),
y: num(projectile[projectileSlots.positionZ]),
z: num(projectile[projectileSlots.positionY]),
directionX: num(projectile[projectileSlots.directionX]),
directionY: num(projectile[projectileSlots.directionY]),
velocityX: num(projectile[projectileSlots.velocityX]),
velocityY: num(projectile[projectileSlots.velocityY]),
controlled: false,
targetEntityIndex: num(projectile[projectileSlots.targetEntityIndex], -1),
isActive: projectile[projectileSlots.active] !== false,
visibility: num(projectile[projectileSlots.radius]),
iconAsset: spriteAsset("map.entity.projectile"),
status: {
hull: num(projectile[projectileSlots.damage]),
shield: 0,
heat: num(projectile[projectileSlots.ageSeconds]),
},
inventory: [],
};
}
function toStarbridgeBaseStatus(status: unknown[]): StarbridgeSessionDocument["baseStatus"] {
return {
entityKey: str(status[starbridgeBaseSlots.entityKey]),
@ -573,38 +466,6 @@ function toStarbridgeRuntimeRole(role: unknown[]): StarbridgeSessionDocument["ru
};
}
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 {
const kind = str(body[bodySlots.kind]);
return {
bodyKey: str(body[bodySlots.bodyKey]),
orbitKey: str(body[bodySlots.orbitKey]),
name: str(body[bodySlots.name]),
kind,
x: num(body[bodySlots.gravityInfluenceCenterX]),
y: num(body[bodySlots.gravityInfluenceCenterZ]),
radius: Math.max(32, num(body[bodySlots.bodyRadiusMultiplier]) * 70),
isAsteroidBelt: kind.toLowerCase().includes("asteroid"),
iconAsset: bodyIconAsset(kind),
iconSize: num(body[bodySlots.iconSize]),
};
}
function inventory(entity: unknown[]): InventoryItem[] {
const items: InventoryItem[] = [];
for (const slot of list<unknown[]>(entity[entitySlots.equipment]))
@ -631,38 +492,6 @@ function addSlot(items: InventoryItem[], source: string, slot: unknown[]): void
});
}
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 projectileIntersectsViewport(projectile: unknown[], viewport: ViewportRequest): boolean {
const x = num(projectile[projectileSlots.positionX]);
const y = num(projectile[projectileSlots.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";
}
@ -673,113 +502,6 @@ function stat(entity: unknown[], name: string): number {
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);
}
type RenderSplatRow = {
layerIndex: number;
centerX: number;
centerY: number;
halfExtentX: number;
halfExtentY: number;
rotationCos?: number;
rotationSin?: number;
channel: number;
falloff: number;
valueR?: number;
valueG?: number;
valueB?: number;
valueA?: number;
sourceKey: string;
sourceKind?: number;
frequencyX?: number;
frequencyY?: number;
phaseX?: number;
phaseY?: number;
animationSpeed?: number;
sourceFlags?: number;
};
class RenderSplatBuilder {
private readonly rows: RenderSplatRow[] = [];
public add(row: RenderSplatRow): void {
this.rows.push(row);
}
public build(): RenderSplatsViewportResponse["splats"] {
return {
count: this.rows.length,
centerX: this.rows.map(row => row.centerX),
centerY: this.rows.map(row => row.centerY),
halfExtentX: this.rows.map(row => row.halfExtentX),
halfExtentY: this.rows.map(row => row.halfExtentY),
rotationCos: this.rows.map(row => row.rotationCos ?? 1),
rotationSin: this.rows.map(row => row.rotationSin ?? 0),
channel: this.rows.map(row => row.channel),
falloff: this.rows.map(row => row.falloff),
valueR: this.rows.map(row => row.valueR ?? 0),
valueG: this.rows.map(row => row.valueG ?? 0),
valueB: this.rows.map(row => row.valueB ?? 0),
valueA: this.rows.map(row => row.valueA ?? 0),
sourceKey: this.rows.map(row => row.sourceKey),
layerIndex: this.rows.map(row => row.layerIndex),
sourceKind: this.rows.map(row => row.sourceKind ?? 0),
frequencyX: this.rows.map(row => row.frequencyX ?? 1),
frequencyY: this.rows.map(row => row.frequencyY ?? 1),
phaseX: this.rows.map(row => row.phaseX ?? 0),
phaseY: this.rows.map(row => row.phaseY ?? 0),
animationSpeed: this.rows.map(row => row.animationSpeed ?? 0),
sourceFlags: this.rows.map(row => row.sourceFlags ?? 0),
};
}
}
function defaultRenderSplatLayers(): RenderSplatsViewportResponse["layers"] {
return [
layer("gravity.height", "Gravity Height", 1, "add", "R16_SFloat"),
layer("gravity.wave", "Gravity Wave", 2, "add", "R16_SFloat"),
layer("visibility.mask", "Visibility Mask", 0, "max", "R16_SFloat"),
layer("fog.surface_height", "Fog Surface Height", 4, "add", "R16_SFloat"),
layer("fog.patch_height", "Fog Patch Height", 4, "add", "R16_SFloat"),
layer("fog.patch", "Fog Patch", 4, "max", "R16_SFloat"),
layer("fog.tint", "Fog Tint", 4, "add", "B10G11R11_UFloatPack32"),
layer("influence.mask", "Influence", 3, "add", "R16_SFloat"),
];
}
function layer(
layerKey: string,
displayName: string,
channel: number,
blendMode: string,
graphicsFormat: string,
): RenderSplatsViewportResponse["layers"][number] {
return {
layerKey,
displayName,
channel,
blendMode,
graphicsFormat,
clearBeforeDraw: true,
clearR: 0,
clearG: 0,
clearB: 0,
clearA: 0,
};
}
function requiredLayerIndex(layers: ReadonlyMap<string, number>, key: string): number {
const index = layers.get(key);
if (index == null)
throw new Error(`Missing render splat layer ${key}.`);
return index;
}
function entityKey(runId: string, zoneIndex: number, entityIndex: number): string {
return `global:aetheria.run_state.${runId}.zone.${zoneIndex}.entity.${entityIndex}.v1`;
}

View file

@ -997,7 +997,12 @@ static bool TryGetManagedViewportRequest(
schemaId = "";
viewport = new AetheriaRuntimeViewportBounds();
var prefix = "";
if (recordKey.StartsWith("aetheria.viewport.objects.", StringComparison.Ordinal))
if (recordKey.StartsWith("aetheria.viewport.map.", StringComparison.Ordinal))
{
prefix = "aetheria.viewport.map.";
schemaId = AetheriaRuntimeDaemonSchemas.GameViewport;
}
else if (recordKey.StartsWith("aetheria.viewport.objects.", StringComparison.Ordinal))
{
prefix = "aetheria.viewport.objects.";
schemaId = AetheriaRuntimeDaemonSchemas.ObjectsViewport;

View file

@ -16391,12 +16391,6 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
[gameViewportDocumentsPath] = new[]
{
"public IReadOnlyList<AetheriaRuntimeProjectileCommit> Projectiles"
},
[rtsLocalDocumentsPath] = new[]
{
"const projectiles = list<unknown[]>(zone[zoneSlots.projectiles])",
".map(toProjectileViewObject)",
"function toProjectileViewObject(projectile: unknown[]): ViewObject"
}
};
var missingProjectileAuthoritySymbols = requiredProjectileAuthoritySymbols
@ -16407,8 +16401,7 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
pair.Key == daemonSimulationSettingsPath ? daemonSimulationSettings :
pair.Key == ymirProjectilePhysicsPath ? ymirProjectilePhysics :
pair.Key == gameDocumentsPath ? gameDocuments :
pair.Key == gameViewportDocumentsPath ? gameViewportDocuments :
rtsLocalDocuments;
gameViewportDocuments;
return pair.Value
.Where(symbol => !text.Contains(symbol, StringComparison.Ordinal))
.Select(symbol => $"{Path.GetRelativePath(root, pair.Key)}: missing {symbol}");
@ -16596,6 +16589,39 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
throw new InvalidOperationException(
"Electron Eve lowering must resolve CultMesh asset refs through the daemon CultMesh CDN protocol bridge.");
}
if (rtsElectronCultMesh.Contains("buildViewportDocumentFromFrame(await this.fetchLatestFrameDocument()", StringComparison.Ordinal) ||
rtsElectronCultMesh.Contains("buildObjectsViewportDocumentFromFrame(await this.fetchLatestFrameDocument()", StringComparison.Ordinal) ||
rtsElectronCultMesh.Contains("buildGravityViewportDocumentFromFrame(await this.fetchLatestFrameDocument()", StringComparison.Ordinal) ||
rtsElectronCultMesh.Contains("buildRenderSplatsViewportDocumentFromFrame(await this.fetchLatestFrameDocument()", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Electron viewport queries must fetch daemon-authored managed viewport documents, not rebuild field/view documents from daemonFrame.");
}
if (rtsLocalDocuments.Contains("buildViewportDocumentFromFrame", StringComparison.Ordinal) ||
rtsLocalDocuments.Contains("buildObjectsViewportDocumentFromFrame", StringComparison.Ordinal) ||
rtsLocalDocuments.Contains("buildGravityViewportDocumentFromFrame", StringComparison.Ordinal) ||
rtsLocalDocuments.Contains("buildRenderSplatsViewportDocumentFromFrame", StringComparison.Ordinal) ||
rtsLocalDocuments.Contains("RenderSplatBuilder", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Electron local document decoding must not keep dead viewport projection builders; daemon managed viewport documents own those fields.");
}
if (!rtsElectronCultMesh.Contains("fetchViewportDocument(AetheriaRtsSchemas.gameViewport, \"aetheria.viewport.map\"", StringComparison.Ordinal) ||
!rtsElectronCultMesh.Contains("fetchViewportDocument(AetheriaRtsSchemas.objectsViewport, \"aetheria.viewport.objects\"", StringComparison.Ordinal) ||
!rtsElectronCultMesh.Contains("fetchViewportDocument(AetheriaRtsSchemas.gravityViewport, \"aetheria.viewport.gravity\"", StringComparison.Ordinal) ||
!rtsElectronCultMesh.Contains("fetchViewportDocument(AetheriaRtsSchemas.renderSplatsViewport, \"aetheria.viewport.render_splats\"", StringComparison.Ordinal) ||
!rtsElectronCultMesh.Contains("managedViewportRecordKey(", StringComparison.Ordinal) ||
!rtsElectronCultMesh.Contains("readRenderSplatsViewportDocument(await this.fetchViewportDocument", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Electron viewport queries no longer use the daemon managed viewport document path.");
}
if (!daemonProgram.Contains("recordKey.StartsWith(\"aetheria.viewport.map.\"", StringComparison.Ordinal) ||
!daemonProgram.Contains("schemaId = AetheriaRuntimeDaemonSchemas.GameViewport", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Daemon snapshot handling must expose the managed map viewport document key beside objects/gravity/render-splats.");
}
if (daemonTickRunner.Contains("AetheriaRuntimeDaemonFrameStore.PublishFrame", StringComparison.Ordinal))
{