From d91e87b6d6802a85f308a0aabc2fed8843c70de5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 6 Jul 2026 03:21:20 +0100 Subject: [PATCH] Fetch Electron viewports from daemon documents --- .../Electron/aetheria-cultmesh.ts | 67 +- .../Electron/aetheria-rts-local-documents.ts | 720 ++++++------------ Aetheria.State.Daemon/Program.cs | 7 +- Aetheria.State.Verify/Program.cs | 42 +- 4 files changed, 320 insertions(+), 516 deletions(-) diff --git a/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts index c3cf28adf..06783fa09 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts @@ -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 { + 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 { 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; diff --git a/Aetheria.Rts.Web/Electron/aetheria-rts-local-documents.ts b/Aetheria.Rts.Web/Electron/aetheria-rts-local-documents.ts index 72ecc54ae..5e0d2ced7 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-rts-local-documents.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-rts-local-documents.ts @@ -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(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(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(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(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(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(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 = 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(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(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(document[gameViewportSlots.objects])); + const gravityInfluences = readGravityInfluences(list(document[gameViewportSlots.gravityInfluences])); + const bodies = readBodyViews(list(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(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(document[gravityViewportSlots.gravityInfluences])), + bodies: readBodyViews(list(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(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(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(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, 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`; } diff --git a/Aetheria.State.Daemon/Program.cs b/Aetheria.State.Daemon/Program.cs index 7d422ae00..e056d3059 100644 --- a/Aetheria.State.Daemon/Program.cs +++ b/Aetheria.State.Daemon/Program.cs @@ -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; diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 8cc70f8cd..7da46cedf 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -16391,12 +16391,6 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root) [gameViewportDocumentsPath] = new[] { "public IReadOnlyList Projectiles" - }, - [rtsLocalDocumentsPath] = new[] - { - "const projectiles = list(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)) {