Fetch Electron indexed documents from daemon

This commit is contained in:
Meta Vens 2026-07-06 03:32:45 +01:00
commit 6f91e749cd
4 changed files with 169 additions and 136 deletions

View file

@ -52,11 +52,11 @@ import {
readAuthorityStatusDocument,
readAssetManifestDocument,
readDaemonHealthDocument,
buildInventoryDocumentFromFrame,
readGravityViewportDocument,
readInventoryDocument,
readObjectsViewportDocument,
readRenderSplatsViewportDocument,
buildSelectedObjectDocumentFromFrame,
readSelectedObjectDocument,
readStarbridgeSessionSummaryDocument,
readViewportDocument,
} from "./aetheria-rts-local-documents.js";
@ -216,8 +216,10 @@ export class AetheriaCultMeshClient {
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),
selectedObject: async (request: SelectedObjectRequest) =>
readSelectedObjectDocument(await this.fetchIndexedDocument(AetheriaRtsSchemas.selectedObject, "aetheria.object.selected", request.entityIndex)),
inventory: async (request: SelectedObjectRequest) =>
readInventoryDocument(await this.fetchIndexedDocument(AetheriaRtsSchemas.inventory, "aetheria.inventory", request.entityIndex)),
daemonHealth: async () => readDaemonHealthDocument(await this.fetchDaemonHealthDocument()),
authorityStatus: async () => readAuthorityStatusDocument(await this.fetchAuthorityPolicyDocument()),
starbridgeSession: async () => readStarbridgeSessionSummaryDocument(await this.fetchStarbridgeSessionSummaryDocument()),
@ -512,6 +514,33 @@ export class AetheriaCultMeshClient {
return retryTransientPublicationRead(() => document.latest(this.queryContext()));
}
private async fetchIndexedDocument(
schemaId: string,
recordPrefix: string,
entityIndex: number,
): Promise<unknown> {
const recordKey = `${recordPrefix}.${Math.trunc(entityIndex)}`;
const document = CultMesh.documentFromPublication(
{
kind: "peer-snapshot",
peer: () => this.peer(),
endpoint: this.resolvedRudpEndpoint(),
},
schemaId,
recordKey,
{
documentId: recordKey,
routeHint: this.queryVerse.context.routeHint,
sourceId: recordKey,
timeoutMs: 1500,
pollMs: 50,
messageIdPrefix: `${this.runtimeId}:indexed-document`,
},
);
return retryTransientPublicationRead(() => document.latest(this.queryContext()));
}
private fetchPublicationDocument(schemaId: string): Promise<unknown> {
return retryTransientPublicationRead(() => this.publications.latest({ schemaId }, this.queryContext()));
}

View file

@ -4,17 +4,12 @@ import {
aetheriaRuntimeAssetManifestEntrySlots as assetManifestEntrySlots,
aetheriaRuntimeAssetRefSlots as assetRefSlots,
aetheriaRuntimeAuthorityRuleSlots as authorityRuleSlots,
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,
aetheriaRuntimeInventoryDocumentSlots as inventoryDocumentSlots,
aetheriaRuntimeObjectsViewportDocumentSlots as objectsViewportSlots,
aetheriaRuntimeRenderSplatLayerDefinitionSlots as renderSplatLayerSlots,
aetheriaRuntimeRenderSplatsViewportDocumentSlots as renderSplatsViewportSlots,
@ -23,14 +18,13 @@ import {
aetheriaRuntimeRtsEntityStatusSlots as viewportStatusSlots,
aetheriaRuntimeRtsGravityInfluenceSlots as gravityInfluenceSlots,
aetheriaRuntimeRtsInventoryItemSlots as viewportInventoryItemSlots,
aetheriaRuntimeRunCheckpointCommitSlots as runSlots,
aetheriaRuntimeSelectedObjectDocumentSlots as selectedObjectDocumentSlots,
aetheriaRuntimeStarbridgeBaseStatusSlots as starbridgeBaseSlots,
aetheriaRuntimeStarbridgeRuntimeRoleSlots as starbridgeRoleSlots,
aetheriaRuntimeStarbridgeSessionSummaryDocumentSlots as starbridgeSummarySlots,
aetheriaRuntimeStarbridgeStationStockItemSlots as starbridgeStockSlots,
aetheriaRuntimeStarbridgeWaveForecastSlots as starbridgeWaveForecastSlots,
aetheriaRuntimeVerseAuthorityPolicyDocumentSlots as authorityPolicySlots,
aetheriaRuntimeZoneSnapshotCommitSlots as zoneSlots,
} from "./aetheria-rts-generated-bindings.js";
import { cultMeshRectFromBounds, cultMeshViewportRequest } from "cultmesh-ts";
import type {
@ -47,7 +41,6 @@ import type {
ObjectsViewportResponse,
RenderSplatsViewportResponse,
SelectedObjectDocument,
SelectedObjectRequest,
StarbridgeSessionDocument,
ViewObject,
ViewportRequest,
@ -56,36 +49,30 @@ import type {
const missingDaemonRunId = "aetheria.run.unknown";
export function buildSelectedObjectDocumentFromFrame(
frameDocument: unknown,
request: SelectedObjectRequest,
): SelectedObjectDocument {
const context = frameContext(frameDocument);
const entity = context.entities.find(candidate => num(candidate[entitySlots.entityIndex], -1) === request.entityIndex);
export function readSelectedObjectDocument(selectedObjectDocument: unknown): SelectedObjectDocument {
const document = arr(selectedObjectDocument);
return {
schema: AetheriaRtsSchemas.selectedObject,
frameId: context.frameId,
runId: context.runId,
zoneIndex: context.zoneIndex,
entityIndex: request.entityIndex,
selected: entity ? toViewObject(entity, context.runId, context.zoneIndex) : null,
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 buildInventoryDocumentFromFrame(frameDocument: unknown, request: SelectedObjectRequest): InventoryDocument {
const context = frameContext(frameDocument);
const entity = context.entities.find(candidate => num(candidate[entitySlots.entityIndex], -1) === request.entityIndex);
const allItems = entity ? inventory(entity) : [];
export function readInventoryDocument(inventoryDocument: unknown): InventoryDocument {
const document = arr(inventoryDocument);
return {
schema: AetheriaRtsSchemas.inventory,
frameId: context.frameId,
runId: context.runId,
zoneIndex: context.zoneIndex,
entityIndex: request.entityIndex,
entityKey: entity ? entityKey(context.runId, context.zoneIndex, request.entityIndex) : "",
items: allItems,
equipment: allItems.filter(item => item.source === "equipment"),
cargo: allItems.filter(item => item.source === "cargo"),
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),
};
}
@ -233,27 +220,6 @@ export function readAssetManifestDocument(assetManifestDocument: unknown): Asset
};
}
function frameContext(frameDocument: unknown): {
frameId: number;
runId: string;
zoneIndex: number;
entities: unknown[][];
} {
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] ??
[];
return {
frameId: num(frame[frameSlots.frameId]),
runId: str(run[runSlots.runId]) || missingDaemonRunId,
zoneIndex: num(zone[zoneSlots.zoneIndex]),
entities: list<unknown[]>(zone[zoneSlots.entities]),
};
}
function readViewportBounds(value: unknown): ViewportRequest {
const bounds = arr(value);
return cultMeshViewportRequest(
@ -271,6 +237,10 @@ 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),
@ -395,36 +365,6 @@ function readRenderSplatSoa(value: unknown): RenderSplatsViewportResponse["splat
};
}
function toViewObject(entity: unknown[], runId: string, zoneIndex: number): ViewObject {
const kind = str(entity[entitySlots.kind]);
const controlled = isPlayerControlled(entity);
return {
entityIndex: num(entity[entitySlots.entityIndex], -1),
entityKey: entityKey(runId, zoneIndex, num(entity[entitySlots.entityIndex], -1)),
displayName: str(entity[entitySlots.name]),
kind,
factionKey: str(entity[entitySlots.factionKey]),
x: num(entity[entitySlots.positionX]),
y: num(entity[entitySlots.positionZ]),
z: num(entity[entitySlots.positionY]),
directionX: num(entity[entitySlots.directionX]),
directionY: num(entity[entitySlots.directionY]),
velocityX: num(entity[entitySlots.velocityX]),
velocityY: num(entity[entitySlots.velocityY]),
controlled,
targetEntityIndex: num(entity[entitySlots.targetEntityIndex], -1),
isActive: bool(entity[entitySlots.isActive]),
visibility: num(entity[entitySlots.visibility]),
iconAsset: entityIconAsset(kind, controlled),
status: {
hull: stat(entity, "hull"),
shield: stat(entity, "shield"),
heat: stat(entity, "heat"),
},
inventory: inventory(entity),
};
}
function toStarbridgeBaseStatus(status: unknown[]): StarbridgeSessionDocument["baseStatus"] {
return {
entityKey: str(status[starbridgeBaseSlots.entityKey]),
@ -466,46 +406,6 @@ function toStarbridgeRuntimeRole(role: unknown[]): StarbridgeSessionDocument["ru
};
}
function inventory(entity: unknown[]): InventoryItem[] {
const items: InventoryItem[] = [];
for (const slot of list<unknown[]>(entity[entitySlots.equipment]))
addSlot(items, "equipment", slot);
for (const bay of list<unknown[]>(entity[entitySlots.cargoContents])) {
for (const slot of list<unknown[]>(bay[cargoBaySlots.items]))
addSlot(items, "cargo", slot);
}
return items.filter(item => item.itemKey.length > 0);
}
function addSlot(items: InventoryItem[], source: string, slot: unknown[]): void {
const item = arr(slot[itemSlotSlots.item]);
const itemKey = str(item[itemSlots.itemKey]);
items.push({
source,
itemKey,
quantity: num(item[itemSlots.quantity]),
quality: num(item[itemSlots.quality]),
durability: num(item[itemSlots.durability]),
enabled: bool(item[itemSlots.enabled]),
iconAsset: itemIconAsset(itemKey),
});
}
function isPlayerControlled(entity: unknown[]): boolean {
return str(entity[entitySlots.factionKey]).toLowerCase() === "player";
}
function stat(entity: unknown[], name: string): number {
const grid = list<unknown[]>(entity[entitySlots.statGrids])
.find(candidate => str(candidate[statGridSlots.name]).toLowerCase() === name.toLowerCase());
return grid ? numberList(grid[statGridSlots.values])[0] ?? 0 : 0;
}
function entityKey(runId: string, zoneIndex: number, entityIndex: number): string {
return `global:aetheria.run_state.${runId}.zone.${zoneIndex}.entity.${entityIndex}.v1`;
}
function toAssetManifestEntry(entry: unknown[]): AssetManifestDocument["assets"][number] {
return {
ref: assetRef(arr(entry[assetManifestEntrySlots.ref])),

View file

@ -619,6 +619,46 @@ static RudpCultNetSchemaServer StartClientCultMeshHost(
}
}
if (hasFrame && frame != null && TryGetIndexedGameDocumentRequest(request, out var indexedRecordKey, out var indexedSchemaId, out var indexedEntityIndex))
{
if (string.Equals(indexedSchemaId, AetheriaRuntimeDaemonSchemas.SelectedObject, StringComparison.Ordinal))
{
var selectedObjectPut = node.Database.Documents.CreateRawDocumentPutMessage(
response.MessageId,
new CultRecordHandle<AetheriaRuntimeSelectedObjectDocument>(
new CultRecordKey(indexedRecordKey)),
AetheriaRuntimeGameDocuments.SelectedObject(frame, indexedEntityIndex),
new CultNetDocumentMessageOptions
{
SourceRuntimeId = options.DaemonId,
SourceRole = "aetheria-daemon"
});
response.Documents = response.Documents
.Where(document => !string.Equals(document.SchemaId, selectedObjectPut.Document.SchemaId, StringComparison.Ordinal) ||
!string.Equals(document.RecordKey, selectedObjectPut.Document.RecordKey, StringComparison.Ordinal))
.Concat(new[] { selectedObjectPut.Document })
.ToArray();
}
else if (string.Equals(indexedSchemaId, AetheriaRuntimeDaemonSchemas.Inventory, StringComparison.Ordinal))
{
var inventoryPut = node.Database.Documents.CreateRawDocumentPutMessage(
response.MessageId,
new CultRecordHandle<AetheriaRuntimeInventoryDocument>(
new CultRecordKey(indexedRecordKey)),
AetheriaRuntimeGameDocuments.Inventory(frame, indexedEntityIndex),
new CultNetDocumentMessageOptions
{
SourceRuntimeId = options.DaemonId,
SourceRole = "aetheria-daemon"
});
response.Documents = response.Documents
.Where(document => !string.Equals(document.SchemaId, inventoryPut.Document.SchemaId, StringComparison.Ordinal) ||
!string.Equals(document.RecordKey, inventoryPut.Document.RecordKey, StringComparison.Ordinal))
.Concat(new[] { inventoryPut.Document })
.ToArray();
}
}
await InjectCultMeshCdnAssetSnapshotsAsync(options, request, response).ConfigureAwait(false);
peer.SendCultNet(response);
}
@ -1042,6 +1082,57 @@ static bool TryGetManagedViewportRequest(
return true;
}
static bool TryGetIndexedGameDocumentRequest(
CultNetSnapshotRequestMessage request,
out string recordKey,
out string schemaId,
out int entityIndex)
{
recordKey = "";
schemaId = "";
entityIndex = -1;
var schemaIds = request.SchemaIds ?? Array.Empty<string>();
var allowedSchemas = new[]
{
AetheriaRuntimeDaemonSchemas.SelectedObject,
AetheriaRuntimeDaemonSchemas.Inventory
};
if (schemaIds.Length > 0 && !schemaIds.Any(candidate => allowedSchemas.Contains(candidate, StringComparer.Ordinal)))
return false;
foreach (var candidate in request.RecordKeys ?? Array.Empty<string>())
{
var candidateSchemaId = "";
var prefix = "";
if (candidate.StartsWith("aetheria.object.selected.", StringComparison.Ordinal))
{
prefix = "aetheria.object.selected.";
candidateSchemaId = AetheriaRuntimeDaemonSchemas.SelectedObject;
}
else if (candidate.StartsWith("aetheria.inventory.", StringComparison.Ordinal))
{
prefix = "aetheria.inventory.";
candidateSchemaId = AetheriaRuntimeDaemonSchemas.Inventory;
}
else
{
continue;
}
if (schemaIds.Length > 0 && !schemaIds.Contains(candidateSchemaId, StringComparer.Ordinal))
continue;
if (!int.TryParse(candidate.Substring(prefix.Length), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedEntityIndex))
continue;
recordKey = candidate;
schemaId = candidateSchemaId;
entityIndex = parsedEntityIndex;
return true;
}
return false;
}
static bool TryParseViewportToken(string token, out double value)
{
return double.TryParse(

View file

@ -16592,35 +16592,48 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
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))
rtsElectronCultMesh.Contains("buildRenderSplatsViewportDocumentFromFrame(await this.fetchLatestFrameDocument()", StringComparison.Ordinal) ||
rtsElectronCultMesh.Contains("buildSelectedObjectDocumentFromFrame(await this.fetchLatestFrameDocument()", StringComparison.Ordinal) ||
rtsElectronCultMesh.Contains("buildInventoryDocumentFromFrame(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.");
"Electron queries must fetch daemon-authored managed documents, not rebuild field/view/selection/inventory 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("buildSelectedObjectDocumentFromFrame", StringComparison.Ordinal) ||
rtsLocalDocuments.Contains("buildInventoryDocumentFromFrame", StringComparison.Ordinal) ||
rtsLocalDocuments.Contains("frameContext(frameDocument", 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.");
"Electron local document decoding must not keep dead viewport, selection, or inventory projection builders; daemon managed 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))
!rtsElectronCultMesh.Contains("readRenderSplatsViewportDocument(await this.fetchViewportDocument", StringComparison.Ordinal) ||
!rtsElectronCultMesh.Contains("fetchIndexedDocument(AetheriaRtsSchemas.selectedObject, \"aetheria.object.selected\"", StringComparison.Ordinal) ||
!rtsElectronCultMesh.Contains("fetchIndexedDocument(AetheriaRtsSchemas.inventory, \"aetheria.inventory\"", StringComparison.Ordinal) ||
!rtsElectronCultMesh.Contains("readSelectedObjectDocument(await this.fetchIndexedDocument", StringComparison.Ordinal) ||
!rtsElectronCultMesh.Contains("readInventoryDocument(await this.fetchIndexedDocument", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Electron viewport queries no longer use the daemon managed viewport document path.");
"Electron viewport, selected-object, and inventory queries no longer use the daemon managed document path.");
}
if (!daemonProgram.Contains("recordKey.StartsWith(\"aetheria.viewport.map.\"", StringComparison.Ordinal) ||
!daemonProgram.Contains("schemaId = AetheriaRuntimeDaemonSchemas.GameViewport", StringComparison.Ordinal))
!daemonProgram.Contains("schemaId = AetheriaRuntimeDaemonSchemas.GameViewport", StringComparison.Ordinal) ||
!daemonProgram.Contains("candidate.StartsWith(\"aetheria.object.selected.\"", StringComparison.Ordinal) ||
!daemonProgram.Contains("candidate.StartsWith(\"aetheria.inventory.\"", StringComparison.Ordinal) ||
!daemonProgram.Contains("AetheriaRuntimeGameDocuments.SelectedObject(frame, indexedEntityIndex)", StringComparison.Ordinal) ||
!daemonProgram.Contains("AetheriaRuntimeGameDocuments.Inventory(frame, indexedEntityIndex)", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Daemon snapshot handling must expose the managed map viewport document key beside objects/gravity/render-splats.");
"Daemon snapshot handling must expose managed viewport and indexed selected-object/inventory document keys.");
}
if (daemonTickRunner.Contains("AetheriaRuntimeDaemonFrameStore.PublishFrame", StringComparison.Ordinal))