From 6f91e749cd0dfa2bfac1c0a02379c90975fa2033 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 6 Jul 2026 03:32:45 +0100 Subject: [PATCH] Fetch Electron indexed documents from daemon --- .../Electron/aetheria-cultmesh.ts | 37 ++++- .../Electron/aetheria-rts-local-documents.ts | 150 +++--------------- Aetheria.State.Daemon/Program.cs | 91 +++++++++++ Aetheria.State.Verify/Program.cs | 27 +++- 4 files changed, 169 insertions(+), 136 deletions(-) diff --git a/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts index 06783fa09..99b82a6fa 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts @@ -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 { + 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 { return retryTransientPublicationRead(() => this.publications.latest({ schemaId }, this.queryContext())); } diff --git a/Aetheria.Rts.Web/Electron/aetheria-rts-local-documents.ts b/Aetheria.Rts.Web/Electron/aetheria-rts-local-documents.ts index 5e0d2ced7..2da157f00 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-rts-local-documents.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-rts-local-documents.ts @@ -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(document[inventoryDocumentSlots.items]).map(readViewportInventoryItem), + equipment: list(document[inventoryDocumentSlots.equipment]).map(readViewportInventoryItem), + cargo: list(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(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(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(entity[entitySlots.equipment])) - addSlot(items, "equipment", slot); - for (const bay of list(entity[entitySlots.cargoContents])) { - for (const slot of list(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(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])), diff --git a/Aetheria.State.Daemon/Program.cs b/Aetheria.State.Daemon/Program.cs index e056d3059..b2533fab0 100644 --- a/Aetheria.State.Daemon/Program.cs +++ b/Aetheria.State.Daemon/Program.cs @@ -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( + 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( + 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(); + 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()) + { + 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( diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 7da46cedf..c3ac74a17 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -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))