diff --git a/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts b/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts index b75d06ea9..83c37ccee 100644 --- a/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts +++ b/Aetheria.Rts.Web/Client/aetheria-rts-contract.ts @@ -44,6 +44,7 @@ export type AetheriaRuntimeViewportFeedSnapshot = { daemonHealth: DaemonHealthProjection; authorityStatus: AuthorityStatusProjection; starbridgeSession: StarbridgeSessionProjection; + assetManifest: AssetManifestProjection; receivedAtUtc: string; sampleMs: number; }; @@ -108,6 +109,7 @@ export type ViewObject = { targetEntityIndex: number; isActive: boolean; visibility: number; + iconAsset: AssetRef; status: EntityStatus; inventory: InventoryItem[]; }; @@ -125,6 +127,7 @@ export type InventoryItem = { quality: number; durability: number; enabled: boolean; + iconAsset: AssetRef; }; export type SelectedObjectProjection = { @@ -218,6 +221,33 @@ export type StarbridgeStationStockItemProjection = { quality: number; durability: number; source: string; + iconAsset: AssetRef; +}; + +export type AssetManifestProjection = { + schema: string; + publishedAtUtc: string; + runId: string; + baseUri: string; + assets: AssetManifestEntry[]; +}; + +export type AssetManifestEntry = { + ref: AssetRef; + sizeBytes: number; + width: number; + height: number; + tags: string[]; +}; + +export type AssetRef = { + assetKey: string; + kind: string; + uri: string; + transport: string; + contentHash: string; + mimeType: string; + metadata: Record; }; export type StarbridgeWaveForecastProjection = { @@ -269,6 +299,7 @@ export type AetheriaRtsApi = { daemonHealth(): Promise; authorityStatus(): Promise; starbridgeSession(): Promise; + assetManifest(): Promise; watchViewportFeed(request: AetheriaRuntimeViewportFeedRequest, callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void): () => void; setMoveVector(request: AetheriaRuntimeSetMoveVectorRequest): Promise; setTarget(request: AetheriaRuntimeSetTargetRequest): Promise; diff --git a/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts index 42baa0670..1e67160d7 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts @@ -24,6 +24,7 @@ import { describeAetheriaRuntimeRtsQueryHandles, describeAetheriaRuntimeRtsSurfaceCatalog, type AetheriaRuntimeDaemonCommandReceipt, + type AssetManifestProjection, type AuthorityStatusProjection, type DaemonHealthProjection, type GravityViewportResponse, @@ -47,6 +48,7 @@ import { } from "./aetheria-rts-bindings.js"; import { projectAuthorityStatus, + projectAssetManifest, projectDaemonHealth, projectGravityViewportFromFrame, projectInventoryFromFrame, @@ -127,6 +129,7 @@ export class AetheriaCultMeshClient { daemonHealth: async () => projectDaemonHealth(await this.fetchDaemonHealthDocument()), authorityStatus: async () => projectAuthorityStatus(await this.fetchAuthorityPolicyDocument()), starbridgeSession: async () => projectStarbridgeSessionSummary(await this.fetchStarbridgeSessionSummaryDocument()), + assetManifest: async () => projectAssetManifest(await this.fetchAssetManifestDocument()), }; this.queries = createAetheriaRuntimeRtsQueryHandles( executors, @@ -139,6 +142,7 @@ export class AetheriaCultMeshClient { daemonHealth: CultMesh.pollingQueryWatcher(executors.daemonHealth, { intervalMs: 250 }), authorityStatus: CultMesh.pollingQueryWatcher(executors.authorityStatus, { intervalMs: 250 }), starbridgeSession: CultMesh.pollingQueryWatcher(executors.starbridgeSession, { intervalMs: 250 }), + assetManifest: CultMesh.pollingQueryWatcher(executors.assetManifest, { intervalMs: 1000 }), }, ); this.documents = createAetheriaRuntimeRtsDocuments( @@ -148,6 +152,7 @@ export class AetheriaCultMeshClient { daemonHealth: async () => this.fetchDaemonHealthDocument(), authorityPolicy: async () => this.fetchAuthorityPolicyDocument(), starbridgeSession: async () => this.fetchStarbridgeSessionSummaryDocument(), + assetManifest: async () => this.fetchAssetManifestDocument(), }, ); this.operations = createAetheriaRuntimeRtsOperationHandles( @@ -223,6 +228,9 @@ export class AetheriaCultMeshClient { return this.aetheria.daemon.starbridgeSession(); } + public async assetManifest(): Promise { + return this.aetheria.daemon.assetManifest(); + } public projectionDiagnostics(): Readonly> { return describeAetheriaRuntimeRtsQueryHandles(this.queries); @@ -285,6 +293,9 @@ export class AetheriaCultMeshClient { return this.fetchPublicationDocument(AetheriaRtsSchemas.starbridgeSessionSummary); } + private async fetchAssetManifestDocument(): Promise { + return this.fetchPublicationDocument(AetheriaRtsSchemas.assetManifest); + } private fetchPublicationDocument(schemaId: string): Promise { return this.publications.latest({ schemaId }, this.queryContext()); @@ -321,6 +332,7 @@ export class AetheriaCultMeshClient { ...this.queries.daemonHealth.sources, ...this.queries.authorityStatus.sources, ...this.queries.starbridgeSession.sources, + ...this.queries.assetManifest.sources, ], routeHint: this.queryVerse.context.routeHint, watchFeed: (request, context, callback) => { @@ -363,6 +375,7 @@ export class AetheriaCultMeshClient { daemonHealth, authorityStatus, starbridgeSession, + assetManifest, selectedObject, inventory, ] = await Promise.all([ @@ -370,6 +383,7 @@ export class AetheriaCultMeshClient { this.aetheria.daemon.health(), this.aetheria.daemon.authorityStatus(), this.aetheria.daemon.starbridgeSession(), + this.aetheria.daemon.assetManifest(), selectedEntityIndex >= 0 ? this.aetheria.selectedObject(selectedEntityIndex) : Promise.resolve(null), @@ -385,6 +399,7 @@ export class AetheriaCultMeshClient { daemonHealth, authorityStatus, starbridgeSession, + assetManifest, receivedAtUtc: new Date().toISOString(), sampleMs: performance.now() - startedAt, }; @@ -514,6 +529,17 @@ function createAetheriaPublicationDocuments( ), localPath: `${statePath}.daemon.starbridge.session.cc`, }, + { + ...CultMesh.publicationDocument( + AetheriaRtsSchemas.assetManifest, + "daemon:aetheria.asset_manifest.latest.v1", + { + documentId: "daemon:aetheria.asset_manifest.latest.v1", + sourceId: "daemon:aetheria.asset_manifest.latest.v1", + }, + ), + localPath: `${statePath}.daemon.assets.cc`, + }, ]; return documents.map(document => mode === "remote" && document.remoteRecordKey diff --git a/Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts b/Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts index 11717d812..5cd3ddfb4 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts @@ -30,6 +30,9 @@ export type { AetheriaRuntimeSetTargetRequest as RtsSetTargetRequest, AetheriaRuntimeViewportFeedRequest, AetheriaRuntimeViewportFeedSnapshot, + AssetManifestEntry, + AssetManifestProjection, + AssetRef, AuthorityRuleProjection, AuthorityStatusProjection, BodyView, diff --git a/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts b/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts index 66da36443..63872cb40 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts @@ -10,6 +10,7 @@ export const AetheriaRtsSchemas = { eveCommand: "gamecult.eve.command.v1", daemonFrame: "gamecult.aetheria.daemon_frame.v1", daemonHealth: "gamecult.aetheria.daemon_health.v1", + assetManifest: "gamecult.aetheria.asset_manifest.v1", rtsViewport: "gamecult.aetheria.rts_viewport.v1", objectsViewport: "gamecult.aetheria.objects_viewport.v1", gravityViewport: "gamecult.aetheria.gravity_viewport.v1", @@ -110,6 +111,7 @@ export const AetheriaRtsIpcChannels = { daemonHealth: "aetheria-rts:daemon-health", authorityStatus: "aetheria-rts:authority-status", starbridgeSession: "aetheria-rts:starbridge-session", + assetManifest: "aetheria-rts:asset-manifest", setMoveVector: "aetheria-rts:set-move-vector", setTarget: "aetheria-rts:set-target", surfaceCatalog: "aetheria-rts:surface-catalog", @@ -299,6 +301,55 @@ export const aetheriaRuntimeDaemonHealthDocumentSlots: Record = { + assetKey: 0, + kind: 1, + uri: 2, + transport: 3, + contentHash: 4, + mimeType: 5, + metadata: 6, +}; + +export type AetheriaRuntimeAssetManifestDocumentSlot = + | "schema" + | "publishedAtUtc" + | "runId" + | "baseUri" + | "assets"; + +export const aetheriaRuntimeAssetManifestDocumentSlots: Record = { + schema: 0, + publishedAtUtc: 1, + runId: 2, + baseUri: 3, + assets: 4, +}; + +export type AetheriaRuntimeAssetManifestEntrySlot = + | "ref" + | "sizeBytes" + | "width" + | "height" + | "tags"; + +export const aetheriaRuntimeAssetManifestEntrySlots: Record = { + ref: 0, + sizeBytes: 1, + width: 2, + height: 3, + tags: 4, +}; + export type AetheriaRuntimeRtsViewportDocumentSlot = | "schema" | "frameId" @@ -1045,7 +1096,8 @@ export type AetheriaRuntimeRtsViewportObjectSlot = | "isActive" | "visibility" | "status" - | "inventory"; + | "inventory" + | "iconAsset"; export const aetheriaRuntimeRtsViewportObjectSlots: Record = { entityIndex: 0, @@ -1066,6 +1118,7 @@ export const aetheriaRuntimeRtsViewportObjectSlots: Record = { source: 0, @@ -1100,6 +1154,7 @@ export const aetheriaRuntimeRtsInventoryItemSlots: Record = { itemKey: 0, @@ -1248,6 +1304,7 @@ export const aetheriaRuntimeStarbridgeStationStockItemSlots: Record; }; export type StarbridgeWaveForecastProjection = { @@ -1632,6 +1718,7 @@ export type AetheriaRuntimeViewportFeedSnapshot = { daemonHealth: DaemonHealthProjection; authorityStatus: AuthorityStatusProjection; starbridgeSession: StarbridgeSessionProjection; + assetManifest: AssetManifestProjection; receivedAtUtc: string; sampleMs: number; }; @@ -1645,6 +1732,7 @@ export type AetheriaRtsMainClient = { daemonHealth(): Promise; authorityStatus(): Promise; starbridgeSession(): Promise; + assetManifest(): Promise; watchViewportFeed(request: AetheriaRuntimeViewportFeedRequest, callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void): CultMeshUnsubscribe; setMoveVector(request: AetheriaRuntimeSetMoveVectorRequest): Promise; setTarget(request: AetheriaRuntimeSetTargetRequest): Promise; @@ -1673,6 +1761,7 @@ export function registerAetheriaRtsIpcHandlers( ipcMain.handle(AetheriaRtsIpcChannels.daemonHealth, async () => getClient().daemonHealth()); ipcMain.handle(AetheriaRtsIpcChannels.authorityStatus, async () => getClient().authorityStatus()); ipcMain.handle(AetheriaRtsIpcChannels.starbridgeSession, async () => getClient().starbridgeSession()); + ipcMain.handle(AetheriaRtsIpcChannels.assetManifest, async () => getClient().assetManifest()); ipcMain.handle(AetheriaRtsIpcChannels.viewportFeed, async (event, request: { subscriptionId: string; feed: AetheriaRuntimeViewportFeedRequest }) => { viewportFeedSubscriptions.get(request.subscriptionId)?.(); const unsubscribe = getClient().watchViewportFeed(request.feed, snapshot => { @@ -1722,6 +1811,7 @@ export type AetheriaRuntimeRtsQueryExecutors = { daemonHealth: AetheriaRuntimeQueryExecutor; authorityStatus: AetheriaRuntimeQueryExecutor; starbridgeSession: AetheriaRuntimeQueryExecutor; + assetManifest: AetheriaRuntimeQueryExecutor; }; export type AetheriaRuntimeRtsQueryWatchers = Partial<{ @@ -1733,6 +1823,7 @@ export type AetheriaRuntimeRtsQueryWatchers = Partial<{ daemonHealth: CultMeshQueryWatcher; authorityStatus: CultMeshQueryWatcher; starbridgeSession: CultMeshQueryWatcher; + assetManifest: CultMeshQueryWatcher; }>; export type AetheriaRuntimeRtsProjectionDiagnostic = CultMeshQuerySurfaceDiagnostic; @@ -1752,6 +1843,7 @@ export type AetheriaRuntimeRtsDocumentResolvers = Partial<{ daemonHealth: (context: CultMeshQueryContext) => Promise; authorityPolicy: (context: CultMeshQueryContext) => Promise; starbridgeSession: (context: CultMeshQueryContext) => Promise; + assetManifest: (context: CultMeshQueryContext) => Promise; }>; export type AetheriaRuntimeRtsStatePointerResolvers = AetheriaRuntimeRtsDocumentResolvers; @@ -1772,6 +1864,10 @@ export const AetheriaRuntimeRtsProjectionSources = { starbridgeSession: CultMesh.projectionSource("daemon:aetheria.starbridge.session.latest.v1", { schemaId: AetheriaRtsSchemas.starbridgeSessionSummary, description: "latest Starbridge session summary" + }), + assetManifest: CultMesh.projectionSource("daemon:aetheria.asset_manifest.latest.v1", { + schemaId: AetheriaRtsSchemas.assetManifest, + description: "latest daemon asset manifest" }) } as const; @@ -1816,6 +1912,15 @@ export function createAetheriaRuntimeRtsDocuments( sources: [AetheriaRuntimeRtsProjectionSources.starbridgeSession], }, ), + assetManifest: CultMesh.document( + AetheriaRuntimeRtsProjectionSources.assetManifest.sourceId, + { schemaId: AetheriaRtsSchemas.assetManifest }, + async (context) => resolvers.assetManifest?.(context), + { + routeHint, + sources: [AetheriaRuntimeRtsProjectionSources.assetManifest], + }, + ), } as const; } @@ -1875,6 +1980,12 @@ export function createAetheriaRuntimeRtsQueryHandles( executors.starbridgeSession, { routeHint, watchProjection: watchers.starbridgeSession }, ).asQuerySurface(), + assetManifest: CultMesh.projectionRecipe( + AetheriaRtsSchemas.assetManifest, + [AetheriaRuntimeRtsProjectionSources.assetManifest], + executors.assetManifest, + { routeHint, watchProjection: watchers.assetManifest }, + ).asQuerySurface(), } as const; } @@ -1890,6 +2001,7 @@ export function describeAetheriaRuntimeRtsQueryHandles( daemonHealth: describeAetheriaRuntimeRtsQuerySurface(handles.daemonHealth), authorityStatus: describeAetheriaRuntimeRtsQuerySurface(handles.authorityStatus), starbridgeSession: describeAetheriaRuntimeRtsQuerySurface(handles.starbridgeSession), + assetManifest: describeAetheriaRuntimeRtsQuerySurface(handles.assetManifest), } as const; } @@ -1905,6 +2017,7 @@ export function describeAetheriaRuntimeRtsSurfaceCatalog( CultMesh.describeSurface(documents.daemonHealth), CultMesh.describeSurface(documents.authorityPolicy), CultMesh.describeSurface(documents.starbridgeSession), + CultMesh.describeSurface(documents.assetManifest), CultMesh.describeSurface(handles.mapViewport), CultMesh.describeSurface(handles.objectsViewport), CultMesh.describeSurface(handles.gravityViewport), @@ -1913,6 +2026,7 @@ export function describeAetheriaRuntimeRtsSurfaceCatalog( CultMesh.describeSurface(handles.daemonHealth), CultMesh.describeSurface(handles.authorityStatus), CultMesh.describeSurface(handles.starbridgeSession), + CultMesh.describeSurface(handles.assetManifest), ...(operations ? [ CultMesh.describeSurface(operations.setMoveVector), @@ -1998,10 +2112,12 @@ export function createAetheriaRuntimeRtsVerseFacade( const daemonHealth = CultMesh.bindQuery(queryVerse, queries.daemonHealth); const authorityStatus = CultMesh.bindQuery(queryVerse, queries.authorityStatus); const starbridgeSession = CultMesh.bindQuery(queryVerse, queries.starbridgeSession); + const assetManifest = CultMesh.bindQuery(queryVerse, queries.assetManifest); const daemonFrameDocument = CultMesh.bindDocument(queryVerse, documents.daemonFrame); const daemonHealthDocument = CultMesh.bindDocument(queryVerse, documents.daemonHealth); const authorityPolicyDocument = CultMesh.bindDocument(queryVerse, documents.authorityPolicy); const starbridgeSessionDocument = CultMesh.bindDocument(queryVerse, documents.starbridgeSession); + const assetManifestDocument = CultMesh.bindDocument(queryVerse, documents.assetManifest); const setMoveVector = CultMesh.bindOperation(commandVerse, operations.setMoveVector); const setTarget = CultMesh.bindOperation(commandVerse, operations.setTarget); @@ -2064,10 +2180,12 @@ export function createAetheriaRuntimeRtsVerseFacade( health: () => daemonHealthDocument.latest(), authorityPolicy: () => authorityPolicyDocument.latest(), starbridgeSession: () => starbridgeSessionDocument.latest(), + assetManifest: () => assetManifestDocument.latest(), }, health: () => daemonHealth.execute(undefined), authorityStatus: () => authorityStatus.execute(undefined), starbridgeSession: () => starbridgeSession.execute(undefined), + assetManifest: () => assetManifest.execute(undefined), }, } as const; } diff --git a/Aetheria.Rts.Web/Electron/aetheria-rts-local-projection.ts b/Aetheria.Rts.Web/Electron/aetheria-rts-local-projection.ts index 298e2ed7b..c049a89bc 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-rts-local-projection.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-rts-local-projection.ts @@ -1,5 +1,8 @@ import { AetheriaRtsSchemas, + aetheriaRuntimeAssetManifestDocumentSlots as assetManifestSlots, + aetheriaRuntimeAssetManifestEntrySlots as assetManifestEntrySlots, + aetheriaRuntimeAssetRefSlots as assetRefSlots, aetheriaRuntimeAuthorityRuleSlots as authorityRuleSlots, aetheriaRuntimeBodySnapshotCommitSlots as bodySlots, aetheriaRuntimeCargoBayLoadoutCommitSlots as cargoBaySlots, @@ -20,6 +23,8 @@ import { } from "./aetheria-rts-generated-bindings.js"; import { cultMeshRectFromBounds, cultMeshViewportRequest } from "cultmesh-ts"; import type { + AssetManifestProjection, + AssetRef, AuthorityStatusProjection, BodyView, DaemonHealthProjection, @@ -222,6 +227,17 @@ export function projectStarbridgeSessionSummary(summaryDocument: unknown): Starb }; } +export function projectAssetManifest(assetManifestDocument: unknown): AssetManifestProjection { + const manifest = arr(assetManifestDocument); + return { + schema: str(manifest[assetManifestSlots.schema]) || AetheriaRtsSchemas.assetManifest, + publishedAtUtc: str(manifest[assetManifestSlots.publishedAtUtc]), + runId: str(manifest[assetManifestSlots.runId]), + baseUri: str(manifest[assetManifestSlots.baseUri]), + assets: list(manifest[assetManifestSlots.assets]).map(toAssetManifestEntry), + }; +} + function frameContext(frameDocument: unknown): { frameId: number; runId: string; @@ -251,11 +267,13 @@ function normalizeViewport(request: ViewportRequest): ViewportRequest { } 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: str(entity[entitySlots.kind]), + kind, factionKey: str(entity[entitySlots.factionKey]), x: num(entity[entitySlots.positionX]), y: num(entity[entitySlots.positionZ]), @@ -264,10 +282,11 @@ function toViewObject(entity: unknown[], runId: string, zoneIndex: number): View directionY: num(entity[entitySlots.directionY]), velocityX: num(entity[entitySlots.velocityX]), velocityY: num(entity[entitySlots.velocityY]), - controlled: isPlayerControlled(entity), + 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"), @@ -289,12 +308,14 @@ function toStarbridgeBaseStatus(status: unknown[]): StarbridgeSessionProjection[ } function toStarbridgeStationStockItem(item: unknown[]): StarbridgeSessionProjection["stationStock"][number] { + const itemKey = str(item[starbridgeStockSlots.itemKey]); return { - itemKey: str(item[starbridgeStockSlots.itemKey]), + itemKey, quantity: num(item[starbridgeStockSlots.quantity]), quality: num(item[starbridgeStockSlots.quality]), durability: num(item[starbridgeStockSlots.durability]), source: str(item[starbridgeStockSlots.source]) || "station", + iconAsset: assetRef(arr(item[starbridgeStockSlots.iconAsset]), itemIconAsset(itemKey)), }; } @@ -359,13 +380,15 @@ function inventory(entity: unknown[]): InventoryItem[] { 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: str(item[itemSlots.itemKey]), + itemKey, quantity: num(item[itemSlots.quantity]), quality: num(item[itemSlots.quality]), durability: num(item[itemSlots.durability]), enabled: bool(item[itemSlots.enabled]), + iconAsset: itemIconAsset(itemKey), }); } @@ -416,6 +439,84 @@ function entityKey(runId: string, zoneIndex: number, entityIndex: number): strin return `global:aetheria.run_state.${runId}.zone.${zoneIndex}.entity.${entityIndex}.v1`; } +function toAssetManifestEntry(entry: unknown[]): AssetManifestProjection["assets"][number] { + return { + ref: assetRef(arr(entry[assetManifestEntrySlots.ref])), + sizeBytes: num(entry[assetManifestEntrySlots.sizeBytes]), + width: num(entry[assetManifestEntrySlots.width]), + height: num(entry[assetManifestEntrySlots.height]), + tags: stringList(entry[assetManifestEntrySlots.tags]), + }; +} + +function entityIconAsset(kind: string, controlled: boolean): AssetRef { + if (controlled) + return spriteAsset("map.entity.player", "Sprites/Map/player"); + + const normalized = kind.trim().toLowerCase(); + if (normalized.includes("station")) + return spriteAsset("map.entity.station", "Sprites/Map/station"); + if (normalized.includes("orbital")) + return spriteAsset("map.entity.orbital", "Sprites/Map/orbital"); + + return spriteAsset("map.entity.ship", "Sprites/Map/ship"); +} + +function itemIconAsset(itemKey: string): AssetRef { + const key = itemKey.trim(); + return key.length > 0 + ? textureAsset(`item.${key}.icon`, `item.${key}.icon`) + : emptyAsset("texture"); +} + +function spriteAsset(assetKey: string, resourcePath: string): AssetRef { + return asset(assetKey, "sprite", `resources://${resourcePath}`, "resources", "image/*"); +} + +function textureAsset(assetKey: string, resourcePath: string): AssetRef { + return asset(assetKey, "texture", resourcePath.startsWith("resources://") ? resourcePath : `resources://${resourcePath}`, "resources", "image/*"); +} + +function assetRef(value: unknown[], fallback: AssetRef = emptyAsset()): AssetRef { + const assetKey = str(value[assetRefSlots.assetKey]); + if (assetKey.length === 0) + return fallback; + + return asset( + assetKey, + str(value[assetRefSlots.kind]), + str(value[assetRefSlots.uri]), + str(value[assetRefSlots.transport]), + str(value[assetRefSlots.mimeType]), + str(value[assetRefSlots.contentHash]), + stringRecord(value[assetRefSlots.metadata]), + ); +} + +function asset( + assetKey: string, + kind: string, + uri: string, + transport: string, + mimeType: string, + contentHash = "", + metadata: Record = {}, +): AssetRef { + return { + assetKey, + kind, + uri, + transport, + contentHash, + mimeType, + metadata, + }; +} + +function emptyAsset(kind = ""): AssetRef { + return asset("", kind, "", "", ""); +} + function arr(value: unknown): unknown[] { return Array.isArray(value) ? value : []; } @@ -432,6 +533,17 @@ function stringList(value: unknown): string[] { return Array.isArray(value) ? value.map(candidate => str(candidate)).filter(candidate => candidate.length > 0) : []; } +function stringRecord(value: unknown): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) + return {}; + + return Object.fromEntries( + Object.entries(value) + .map(([key, candidate]) => [key, str(candidate)]) + .filter(([, candidate]) => candidate.length > 0), + ); +} + function str(value: unknown): string { return typeof value === "string" ? value : ""; } diff --git a/Aetheria.Rts.Web/Electron/main.ts b/Aetheria.Rts.Web/Electron/main.ts index f44576402..9930f3ecd 100644 --- a/Aetheria.Rts.Web/Electron/main.ts +++ b/Aetheria.Rts.Web/Electron/main.ts @@ -147,6 +147,7 @@ async function runElectronSmoke(window: BrowserWindow): Promise ipcRenderer.invoke(channels.daemonHealth), authorityStatus: () => ipcRenderer.invoke(channels.authorityStatus), starbridgeSession: () => ipcRenderer.invoke(channels.starbridgeSession), + assetManifest: () => ipcRenderer.invoke(channels.assetManifest), setMoveVector: request => ipcRenderer.invoke(channels.setMoveVector, request), setTarget: request => ipcRenderer.invoke(channels.setTarget, request), surfaceCatalog: () => ipcRenderer.invoke(channels.surfaceCatalog), diff --git a/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs b/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs index d49a25b03..d5edbcca7 100644 --- a/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs +++ b/Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs @@ -9,6 +9,7 @@ const repoRoot = resolve(root, ".."); const sources = [ resolve(repoRoot, "Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs"), resolve(repoRoot, "Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveCommandDocument.cs"), + resolve(repoRoot, "Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssetDocuments.cs"), resolve(repoRoot, "Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs"), resolve(repoRoot, "Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsViewportDocuments.cs"), resolve(repoRoot, "Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs"), @@ -31,6 +32,7 @@ const ipcChannels = [ ["daemonHealth", "aetheria-rts:daemon-health"], ["authorityStatus", "aetheria-rts:authority-status"], ["starbridgeSession", "aetheria-rts:starbridge-session"], + ["assetManifest", "aetheria-rts:asset-manifest"], ["setMoveVector", "aetheria-rts:set-move-vector"], ["setTarget", "aetheria-rts:set-target"], ["surfaceCatalog", "aetheria-rts:surface-catalog"], @@ -71,6 +73,19 @@ const documents = [ className: "AetheriaRuntimeDaemonHealthDocument", schemaName: "daemonHealth", }, + { + exportName: "aetheriaRuntimeAssetRef", + className: "AetheriaRuntimeAssetRef", + }, + { + exportName: "aetheriaRuntimeAssetManifestDocument", + className: "AetheriaRuntimeAssetManifestDocument", + schemaName: "assetManifest", + }, + { + exportName: "aetheriaRuntimeAssetManifestEntry", + className: "AetheriaRuntimeAssetManifestEntry", + }, { exportName: "aetheriaRuntimeRtsViewportDocument", className: "AetheriaRuntimeRtsViewportDocument", @@ -285,18 +300,12 @@ if (check) { function readExisting(path) { try { - return normalizeGeneratedText(readFileSync(path, "utf8")); + return readFileSync(path, "utf8"); } catch { return ""; } } -function normalizeGeneratedText(text) { - return text - .replace(/^\uFEFF/u, "") - .replace(/\r\n/gu, "\n"); -} - function parseEnum(text, enumName) { const body = readTypeBody(text, `enum ${enumName}`); const entries = []; @@ -438,6 +447,7 @@ export type AetheriaRuntimeViewportFeedSnapshot = { daemonHealth: DaemonHealthProjection; authorityStatus: AuthorityStatusProjection; starbridgeSession: StarbridgeSessionProjection; + assetManifest: AssetManifestProjection; receivedAtUtc: string; sampleMs: number; }; @@ -451,6 +461,7 @@ export type AetheriaRtsMainClient = { daemonHealth(): Promise; authorityStatus(): Promise; starbridgeSession(): Promise; + assetManifest(): Promise; watchViewportFeed(request: AetheriaRuntimeViewportFeedRequest, callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void): CultMeshUnsubscribe; setMoveVector(request: AetheriaRuntimeSetMoveVectorRequest): Promise; setTarget(request: AetheriaRuntimeSetTargetRequest): Promise; @@ -479,6 +490,7 @@ export function registerAetheriaRtsIpcHandlers( ipcMain.handle(AetheriaRtsIpcChannels.daemonHealth, async () => getClient().daemonHealth()); ipcMain.handle(AetheriaRtsIpcChannels.authorityStatus, async () => getClient().authorityStatus()); ipcMain.handle(AetheriaRtsIpcChannels.starbridgeSession, async () => getClient().starbridgeSession()); + ipcMain.handle(AetheriaRtsIpcChannels.assetManifest, async () => getClient().assetManifest()); ipcMain.handle(AetheriaRtsIpcChannels.viewportFeed, async (event, request: { subscriptionId: string; feed: AetheriaRuntimeViewportFeedRequest }) => { viewportFeedSubscriptions.get(request.subscriptionId)?.(); const unsubscribe = getClient().watchViewportFeed(request.feed, snapshot => { @@ -528,6 +540,7 @@ export type AetheriaRuntimeRtsQueryExecutors = { daemonHealth: AetheriaRuntimeQueryExecutor; authorityStatus: AetheriaRuntimeQueryExecutor; starbridgeSession: AetheriaRuntimeQueryExecutor; + assetManifest: AetheriaRuntimeQueryExecutor; }; export type AetheriaRuntimeRtsQueryWatchers = Partial<{ @@ -539,6 +552,7 @@ export type AetheriaRuntimeRtsQueryWatchers = Partial<{ daemonHealth: CultMeshQueryWatcher; authorityStatus: CultMeshQueryWatcher; starbridgeSession: CultMeshQueryWatcher; + assetManifest: CultMeshQueryWatcher; }>; export type AetheriaRuntimeRtsProjectionDiagnostic = CultMeshQuerySurfaceDiagnostic; @@ -558,6 +572,7 @@ export type AetheriaRuntimeRtsDocumentResolvers = Partial<{ daemonHealth: (context: CultMeshQueryContext) => Promise; authorityPolicy: (context: CultMeshQueryContext) => Promise; starbridgeSession: (context: CultMeshQueryContext) => Promise; + assetManifest: (context: CultMeshQueryContext) => Promise; }>; export type AetheriaRuntimeRtsStatePointerResolvers = AetheriaRuntimeRtsDocumentResolvers; @@ -578,6 +593,10 @@ export const AetheriaRuntimeRtsProjectionSources = { starbridgeSession: CultMesh.projectionSource("daemon:aetheria.starbridge.session.latest.v1", { schemaId: AetheriaRtsSchemas.starbridgeSessionSummary, description: "latest Starbridge session summary" + }), + assetManifest: CultMesh.projectionSource("daemon:aetheria.asset_manifest.latest.v1", { + schemaId: AetheriaRtsSchemas.assetManifest, + description: "latest daemon asset manifest" }) } as const; @@ -622,6 +641,15 @@ export function createAetheriaRuntimeRtsDocuments( sources: [AetheriaRuntimeRtsProjectionSources.starbridgeSession], }, ), + assetManifest: CultMesh.document( + AetheriaRuntimeRtsProjectionSources.assetManifest.sourceId, + { schemaId: AetheriaRtsSchemas.assetManifest }, + async (context) => resolvers.assetManifest?.(context), + { + routeHint, + sources: [AetheriaRuntimeRtsProjectionSources.assetManifest], + }, + ), } as const; } @@ -681,6 +709,12 @@ export function createAetheriaRuntimeRtsQueryHandles( executors.starbridgeSession, { routeHint, watchProjection: watchers.starbridgeSession }, ).asQuerySurface(), + assetManifest: CultMesh.projectionRecipe( + AetheriaRtsSchemas.assetManifest, + [AetheriaRuntimeRtsProjectionSources.assetManifest], + executors.assetManifest, + { routeHint, watchProjection: watchers.assetManifest }, + ).asQuerySurface(), } as const; } @@ -696,6 +730,7 @@ export function describeAetheriaRuntimeRtsQueryHandles( daemonHealth: describeAetheriaRuntimeRtsQuerySurface(handles.daemonHealth), authorityStatus: describeAetheriaRuntimeRtsQuerySurface(handles.authorityStatus), starbridgeSession: describeAetheriaRuntimeRtsQuerySurface(handles.starbridgeSession), + assetManifest: describeAetheriaRuntimeRtsQuerySurface(handles.assetManifest), } as const; } @@ -711,6 +746,7 @@ export function describeAetheriaRuntimeRtsSurfaceCatalog( CultMesh.describeSurface(documents.daemonHealth), CultMesh.describeSurface(documents.authorityPolicy), CultMesh.describeSurface(documents.starbridgeSession), + CultMesh.describeSurface(documents.assetManifest), CultMesh.describeSurface(handles.mapViewport), CultMesh.describeSurface(handles.objectsViewport), CultMesh.describeSurface(handles.gravityViewport), @@ -719,6 +755,7 @@ export function describeAetheriaRuntimeRtsSurfaceCatalog( CultMesh.describeSurface(handles.daemonHealth), CultMesh.describeSurface(handles.authorityStatus), CultMesh.describeSurface(handles.starbridgeSession), + CultMesh.describeSurface(handles.assetManifest), ...(operations ? [ CultMesh.describeSurface(operations.setMoveVector), @@ -804,10 +841,12 @@ export function createAetheriaRuntimeRtsVerseFacade( const daemonHealth = CultMesh.bindQuery(queryVerse, queries.daemonHealth); const authorityStatus = CultMesh.bindQuery(queryVerse, queries.authorityStatus); const starbridgeSession = CultMesh.bindQuery(queryVerse, queries.starbridgeSession); + const assetManifest = CultMesh.bindQuery(queryVerse, queries.assetManifest); const daemonFrameDocument = CultMesh.bindDocument(queryVerse, documents.daemonFrame); const daemonHealthDocument = CultMesh.bindDocument(queryVerse, documents.daemonHealth); const authorityPolicyDocument = CultMesh.bindDocument(queryVerse, documents.authorityPolicy); const starbridgeSessionDocument = CultMesh.bindDocument(queryVerse, documents.starbridgeSession); + const assetManifestDocument = CultMesh.bindDocument(queryVerse, documents.assetManifest); const setMoveVector = CultMesh.bindOperation(commandVerse, operations.setMoveVector); const setTarget = CultMesh.bindOperation(commandVerse, operations.setTarget); @@ -870,10 +909,12 @@ export function createAetheriaRuntimeRtsVerseFacade( health: () => daemonHealthDocument.latest(), authorityPolicy: () => authorityPolicyDocument.latest(), starbridgeSession: () => starbridgeSessionDocument.latest(), + assetManifest: () => assetManifestDocument.latest(), }, health: () => daemonHealth.execute(undefined), authorityStatus: () => authorityStatus.execute(undefined), starbridgeSession: () => starbridgeSession.execute(undefined), + assetManifest: () => assetManifest.execute(undefined), }, } as const; } @@ -991,6 +1032,7 @@ export type AetheriaRuntimeViewportFeedSnapshot = { daemonHealth: DaemonHealthProjection; authorityStatus: AuthorityStatusProjection; starbridgeSession: StarbridgeSessionProjection; + assetManifest: AssetManifestProjection; receivedAtUtc: string; sampleMs: number; }; @@ -1006,6 +1048,7 @@ export type AetheriaRtsApi = { daemonHealth(): Promise; authorityStatus(): Promise; starbridgeSession(): Promise; + assetManifest(): Promise; watchViewportFeed(request: AetheriaRuntimeViewportFeedRequest, callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void): () => void; setMoveVector(request: AetheriaRuntimeSetMoveVectorRequest): Promise; setTarget(request: AetheriaRuntimeSetTargetRequest): Promise; @@ -1077,6 +1120,7 @@ export type ViewObject = { targetEntityIndex: number; isActive: boolean; visibility: number; + iconAsset: AssetRef; status: EntityStatus; inventory: InventoryItem[]; }; @@ -1094,6 +1138,7 @@ export type InventoryItem = { quality: number; durability: number; enabled: boolean; + iconAsset: AssetRef; }; export type SelectedObjectProjection = { @@ -1187,6 +1232,33 @@ export type StarbridgeStationStockItemProjection = { quality: number; durability: number; source: string; + iconAsset: AssetRef; +}; + +export type AssetManifestProjection = { + schema: string; + publishedAtUtc: string; + runId: string; + baseUri: string; + assets: AssetManifestEntry[]; +}; + +export type AssetManifestEntry = { + ref: AssetRef; + sizeBytes: number; + width: number; + height: number; + tags: string[]; +}; + +export type AssetRef = { + assetKey: string; + kind: string; + uri: string; + transport: string; + contentHash: string; + mimeType: string; + metadata: Record; }; export type StarbridgeWaveForecastProjection = { @@ -1261,6 +1333,7 @@ function renderPreload() { name === "daemonHealth" || name === "authorityStatus" || name === "starbridgeSession" || + name === "assetManifest" || name === "surfaceCatalog" || name === "surfaceCatalogIndex"; const parameters = hasNoArguments ? "()" : "request"; diff --git a/Aetheria.State.Daemon/Program.cs b/Aetheria.State.Daemon/Program.cs index f27093c79..4940823b9 100644 --- a/Aetheria.State.Daemon/Program.cs +++ b/Aetheria.State.Daemon/Program.cs @@ -167,6 +167,7 @@ static async Task TickAsync( PreRejectedCommandIds = policyRejectedCommandIds, CumulativeAppliedCommandIds = currentFrame?.CumulativeAppliedCommandIds ?? currentFrame?.AppliedCommandIds ?? Array.Empty(), CumulativeRejectedCommandIds = currentFrame?.CumulativeRejectedCommandIds ?? currentFrame?.RejectedCommandIds ?? Array.Empty(), + Catalog = node.OpenRuntimeCatalog(), StarbridgeScenario = starbridgeScenario, StarbridgeSession = starbridgeSession, PublishWitnesses = publishWitnesses, diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 5b0135730..9e59822d6 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -983,7 +983,10 @@ static void RequirePackageSerializerBoundary(string root) "AetheriaRuntimeDaemonDocuments.cs", "AetheriaRuntimeDaemonSoaDocuments.cs", "AetheriaRuntimeRtsViewportDocuments.cs", + "AetheriaRuntimeAssetDocuments.cs", + "AetheriaRuntimeRenderSplatDocuments.cs", "AetheriaRuntimeStarbridgeDocuments.cs", + "AetheriaRuntimeStarbridgePlayerSeatDocuments.cs", "AetheriaRuntimeVerseAuthorityPolicy.cs" }; @@ -1688,9 +1691,9 @@ static void RequireTypedOrbitConsumerKeys(string root) { "private readonly List _daemonBodyViews", "private void SyncDaemonBodyViews()", - "private AetheriaRuntimeGravityViewportDocument _daemonGravityViewport;", - "GravityViewportAsync(viewport)", - "foreach (var bodyView in _daemonGravityViewport?.Bodies ?? Array.Empty())", + "private IReadOnlyList _zoneRenderBodies", + "_zoneRenderBodies = render?.Bodies ?? Array.Empty();", + "foreach (var bodyPose in _zoneRenderBodyPoses ?? Array.Empty())", "ResolveDaemonRenderViewport()", "private AetheriaRuntimeXzRect ResolveDaemonRenderViewport()", "private void UnloadBodyView(string bodyKey)", @@ -2345,7 +2348,7 @@ static void RequireDaemonRenderQueryAuthority(string root) "private string _daemonCurrentEntityKey = \"\";", "private double _daemonSimulationTimeSeconds;", "private readonly List _daemonBodyViews", - "private AetheriaRuntimeGravityViewportDocument _daemonGravityViewport;", + "private IReadOnlyList _zoneRenderBodies", "private readonly List _daemonBodyPoses", "private readonly Dictionary _daemonBodyPosesByBodyKey", "private readonly Dictionary _bodyViewsByBodyKey", @@ -2373,14 +2376,14 @@ static void RequireDaemonRenderQueryAuthority(string root) "public void ApplyZoneRender(AetheriaRuntimeZoneRenderDocument render)", "_daemonCurrentEntityKey = render?.CurrentEntityKey ?? \"\";", "_daemonSimulationTimeSeconds = render?.SimulationTimeSeconds ?? 0;", + "_zoneRenderBodies = render?.Bodies ?? Array.Empty();", "_observedEntityFacadesByDaemonIndex = observedEntityFacadesByDaemonIndex;", "render?.ZoneRenderRadius ?? 2000", "private readonly HashSet _daemonVisibleBodyKeys", "private void SyncDaemonBodyViews()", - "GravityViewportAsync(viewport)", "foreach (var bodyView in _daemonBodyViews)", "if (bodyView.IsAsteroidBelt)", - "foreach (var bodyView in _daemonGravityViewport?.Bodies ?? Array.Empty())", + "foreach (var bodyPose in _zoneRenderBodyPoses ?? Array.Empty())", "ResolveDaemonRenderViewport()", "private AetheriaRuntimeXzRect ResolveDaemonRenderViewport()", "private static AetheriaRuntimeRtsViewportBounds ToViewportBounds(AetheriaRuntimeXzRect viewport)", @@ -2409,7 +2412,9 @@ static void RequireDaemonRenderQueryAuthority(string root) "foreach (var beltPose in _daemonAsteroidBeltPoses)", "beltPose.InstancePoses ?? Array.Empty()", "private void RefreshDaemonContactRows()", - "ZoneContactsAsync()", + ".Aetheria()", + ".ZoneContacts", + ".LatestAsync()", "private void RefreshDaemonCompassMarkers()", "private void RefreshDaemonVisibleEntityInstances()", "PowerPulse(", @@ -2514,7 +2519,9 @@ static void RequireDaemonRenderQueryAuthority(string root) { "private AetheriaRuntimeCurrentEntityDocument _currentEntityDocument;", "ResolveCurrentEntityHudStatus()", - ".CurrentEntityAsync()", + ".Current", + ".Entity", + ".LatestAsync()", "hud.OverrideShutdown", "hud.HeatsinksEnabled", "hud.Heatstroke", @@ -4472,7 +4479,9 @@ static void RequireSectorMapZoneDetailsUseEveSurface(string root) "AetheriaRuntimeZoneDetailsSurfaceBuilder.Project(", "ProjectZoneDetailsSurfaceState(", "private AetheriaClient _client", - "SectorMapAsync()", + ".Aetheria()", + ".SectorMap", + ".LatestAsync()", "ZoneDetailsAsync(zoneIndex)", "OpenRuntimeCatalog()", "PlayerSettingsAsync()", @@ -4646,7 +4655,8 @@ static void RequireRuntimeMenuTabsUseEveSurface(string root) "AetheriaRuntimeMenuTabsSurfaceBuilder.Project(", "new AetheriaRuntimeMenuTabProjectionOption(", "ResolveVisibleTabs(", - "CurrentDockingAsync()", + ".DockingState", + ".Latest()", "ResolveCurrentDocking()?.IsDocked == true", "AetheriaClient", "OpenLocalAsync(", @@ -4748,12 +4758,12 @@ static void RequireRuntimeMenuTabsUseEveSurface(string root) var requiredDockedStoryObserverSymbols = new[] { "TryResolveDockedLocalStory(out _currentLocation)", - "private bool TryResolveCurrentDocking(out AetheriaRuntimeCurrentDockingDocument docking)", + "private bool TryResolveDockingState(out AetheriaClientDockingSnapshot dockingState)", "private bool TryResolveDockedLocalStory(out LocationStory story)", "SetObservedFacadeIndex(AetheriaUnityObservedFacadeIndex observedFacadeIndex)", "_observedFacadeIndex.TryResolveDockingBayByRecordKey(", - "docking.DockParentEntityKey", - "docking.DockingBayIndex", + "dockingState.CurrentDocking.DockParentEntityKey", + "dockingState.CurrentDocking.DockingBayIndex", "dockingBay?.Entity is not OrbitalEntity { Story: { } dockedStory }", "AetheriaRuntimeLocalStorySurfaceBuilder.Build(ProjectStorySurface())", "AetheriaRuntimeLocalStorySurfaceBuilder.Project(", @@ -4761,7 +4771,8 @@ static void RequireRuntimeMenuTabsUseEveSurface(string root) "AetheriaRuntimeLocalStoryCommandKind.Continue", "AetheriaRuntimeLocalStoryCommandKind.Choose", "new AetheriaRuntimeLocalStoryChoiceState(", - "CurrentDockingAsync()", + ".DockingState", + ".Latest()", "unity-runtime-local-story" }; var dockedStoryObserverCorpus = localMenu; @@ -4873,15 +4884,15 @@ static void RequireInventoryShipSettingsUseEveSurface(string root) "ResolveDefaultShutdownPerformance()", "ResolvePlayerSettings()?.DefaultShutdownPerformance", "TryResolveCurrentEntityDocument(out var currentEntity)", - "TryGetTypedCurrentEntityFacade(out var currentEntity)", "SetObservedFacadeIndex(AetheriaUnityObservedFacadeIndex observedFacadeIndex)", - "_observedFacadeIndex.TryResolveEntityByRecordKey", "private AetheriaRuntimeCurrentEntityDocument _shipSettingsCurrentEntity;", "!TryResolveCurrentEntityDocument(out var latestCurrentEntity)", "RequestEntityShutdownPerformance(", "latestCurrentEntity.EntityKey", "(float)latestCurrentEntity.ShutdownPerformance", - "CurrentEntityAsync()" + ".DockingState", + ".Latest()", + "?.CurrentEntity" }; var missingSymbols = requiredSymbols @@ -5589,8 +5600,8 @@ static void RequireTradeCargoSelectorUseEveSurface(string root) "AetheriaRuntimeTradeCargoSelectorSurfaceCommands.TryRead(request, out var command)", "AetheriaRuntimeTradeCargoSelectorCommandKind.Close", "AetheriaRuntimeTradeCargoSelectorCommandKind.Select", - "CurrentDockingAsync()", - "StationRefitAsync()", + ".DockingState", + ".Latest()", "SetTargetCargo(", "selection.EntityKey", "OwnedQuantity", @@ -6148,7 +6159,8 @@ static void RequireInventoryDropdownUseEveSurface(string root) "SetObservedFacadeIndex(AetheriaUnityObservedFacadeIndex observedFacadeIndex)", "_observedFacadeIndex.TryResolveEntityByRecordKey", "_observedFacadeIndex.TryResolveDockingBayByRecordKey", - "CurrentDockingAsync", + ".DockingState", + ".Latest()", "LoadoutRestoreOptions" }; @@ -6214,7 +6226,9 @@ static void RequireInventoryDropdownUseEveSurface(string root) } if (!inventoryMenu.Contains("TryGetTypedCurrentDockingBayFacade(out var dockingBay)", StringComparison.Ordinal) || - !inventoryMenu.Contains("CurrentDockingAsync()", StringComparison.Ordinal) || + !inventoryMenu.Contains(".DockingState", StringComparison.Ordinal) || + !inventoryMenu.Contains(".Latest()", StringComparison.Ordinal) || + !inventoryMenu.Contains("ResolveDockingState()?.CurrentDocking", StringComparison.Ordinal) || !inventoryMenu.Contains("_observedFacadeIndex.TryResolveDockingBayByRecordKey", StringComparison.Ordinal) || inventoryMenu.Contains("GameManager.TryGetObservedDockingBay(", StringComparison.Ordinal) || inventoryMenu.Contains("GameManager.DockingBay", StringComparison.Ordinal)) @@ -6373,12 +6387,11 @@ static void RequireStationRefitDockingBaysUseTypedProjection(string root) var requiredPanelSymbols = new[] { - "TryResolveCurrentDockingBayRow(out _)", "TryResolveCurrentDockingBayRow(out var currentDockingBay)", "AetheriaRuntimeStationDockingBayRow dockingBay", "stationRefit.DockingBays", "row.DockingBayIndex == stationRefit.DockingBayIndex", - "dockingBayRow.DockingBayIndex" + "currentDockingBay.DockingBayIndex" }; var missingPanelSymbols = requiredPanelSymbols .Where(symbol => !inventoryPanel.Contains(symbol, StringComparison.Ordinal)) @@ -6393,11 +6406,10 @@ static void RequireStationRefitDockingBaysUseTypedProjection(string root) var requiredMenuSymbols = new[] { - "TryResolveCurrentDockingBayRow(out var dockingBayRow)", + "TryResolveCurrentDockingBayRow(out AetheriaRuntimeStationDockingBayRow dockingBay)", "AetheriaRuntimeStationDockingBayRow dockingBay", "stationRefit.DockingBays", - "row.DockingBayIndex == stationRefit.DockingBayIndex", - "dockingBayRow.DockingBayIndex" + "row.DockingBayIndex == stationRefit.DockingBayIndex" }; var missingMenuSymbols = requiredMenuSymbols .Where(symbol => !inventoryMenu.Contains(symbol, StringComparison.Ordinal)) @@ -7354,7 +7366,9 @@ static void RequireClientTargetBootAuthority(string root) "CurrentStateBoot()", "LatestSectorMap(AetheriaRuntimeStateBootReport stateBoot)", "AetheriaClient", - ".SectorMapAsync()", + ".Aetheria()", + ".SectorMap", + ".LatestAsync()", "LatestVerseHostSettings(AetheriaRuntimeStateBootReport stateBoot)", "AetheriaState.At(AetheriaUnityRuntimePaths.GameDataDirectory)", ".ClientTarget", @@ -9194,9 +9208,10 @@ static void RequireAetheriaRuntimeVerseClientContract(string root) var clientPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeVerseClient.cs"); var surfaceStatePath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeEveSurfaceState.cs"); var oldSurfaceStatePath = Path.Combine(root, "Aetheria.State", "Documents", "EveSurfaceState.cs"); + var stateReaderPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeStateReader.cs"); var docPath = Path.Combine(root, "docs", "aetheria-verse-client-contract.md"); - var requiredFiles = new[] { clientPath, surfaceStatePath, docPath }; + var requiredFiles = new[] { clientPath, surfaceStatePath, stateReaderPath, docPath }; var missingFiles = requiredFiles .Where(path => !File.Exists(path)) .Select(path => Path.GetRelativePath(root, path)) @@ -9210,6 +9225,7 @@ static void RequireAetheriaRuntimeVerseClientContract(string root) var client = File.ReadAllText(clientPath); var surfaceState = File.ReadAllText(surfaceStatePath); + var stateReader = File.ReadAllText(stateReaderPath); var doc = File.ReadAllText(docPath); if (File.Exists(oldSurfaceStatePath)) @@ -9246,8 +9262,7 @@ static void RequireAetheriaRuntimeVerseClientContract(string root) "VerseHostSettingsAsync()", "GetLoadoutTemplatesAsync()", "CreateEveSurfaceStateRefResolver(", - "AetheriaRuntimeStateReader.TryResolveDaemonStateRef(", - "AetheriaRuntimeStateReader.TryResolveDaemonItemStatRef(", + "AetheriaRuntimeStateReader.CreateEveSurfaceCultMeshStateRefResolver(", "SubmitDaemonCommandAsync(", "SubmitEveCommandAsync(", "AetheriaRuntimeVerseRecordKeys.DaemonCommand(command.CommandId)", @@ -9265,6 +9280,21 @@ static void RequireAetheriaRuntimeVerseClientContract(string root) string.Join(", ", missingClientSymbols)); } + var requiredStateReaderSymbols = new[] + { + "TryResolveDaemonStateRef(", + "TryResolveDaemonItemStatRef(" + }; + var missingStateReaderSymbols = requiredStateReaderSymbols + .Where(symbol => !stateReader.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + if (missingStateReaderSymbols.Length > 0) + { + throw new InvalidOperationException( + "Aetheria runtime state reader is missing typed Eve state-ref resolver symbols: " + + string.Join(", ", missingStateReaderSymbols)); + } + if (client.Contains("public async Task SubmitDaemonCommandAsync(", StringComparison.Ordinal) || client.Contains("public async Task SubmitEveCommandAsync(", StringComparison.Ordinal)) { @@ -9950,7 +9980,9 @@ static void RequireMainMenuContinueRunState(string root) { "LatestSectorMap", "AetheriaClient", - ".SectorMapAsync()", + ".Aetheria()", + ".SectorMap", + ".LatestAsync()", "ContinueGame()", "AetheriaUnityObservedRunProjection.Project(", "SceneManager.LoadScene(\"ARPG\")" @@ -10218,7 +10250,9 @@ static void RequireMainMenuContinueRunState(string root) { "public sealed class AetheriaUnityObservedFrameApplier", "public bool ApplyLatestZoneRender()", - "ZoneRenderAsync()", + ".Aetheria()", + ".ZoneRender", + ".LatestAsync()", "private bool TryRestoreEntityGraphFromZoneRender(", "if (string.IsNullOrWhiteSpace(render.RunId))", "Aetheria zone-render feed does not identify a run id.", @@ -10281,7 +10315,9 @@ static void RequireMainMenuContinueRunState(string root) "private bool TryQueryEntityTarget(", "AetheriaRuntimeZoneContactRow", "AetheriaRuntimeZoneTargetRow", - "ZoneContactsAsync()", + ".Aetheria()", + ".ZoneContacts", + ".LatestAsync()", "_facadeIndex.TryResolveEntityByDaemonIndex(targetEntityIndex, out var targetEntity)" }; var missingObservedTargetQuerySymbols = requiredObservedTargetQuerySymbols @@ -10317,7 +10353,9 @@ static void RequireMainMenuContinueRunState(string root) var requiredObservedDockingSymbols = new[] { "public sealed class AetheriaUnityObservedDockingIndex", - "CurrentDockingAsync()", + ".Aetheria()", + ".DockingState", + ".Latest()", "public bool IsEntityUndocked(Entity entity)", "public bool TryResolveDockingBay(", "out AetheriaRuntimeCurrentDockingDocument docking", @@ -11069,7 +11107,9 @@ static void RequireUnityObserverDoesNotTickLocalSimulation(string root) { "public sealed class AetheriaUnityObservedFrameApplier", "public bool ApplyLatestZoneRender()", - "ZoneRenderAsync()", + ".Aetheria()", + ".ZoneRender", + ".LatestAsync()", "private bool TryRestoreEntityGraphFromZoneRender(", "AetheriaUnityDaemonEntitySnapshotProjector.CreateSnapshots(runId, render.ZoneIndex, render.EntityFacades)", "AetheriaUnityDaemonEntitySnapshotProjector.EntityIndexFromRecordKey(entity.RecordKey)", @@ -11237,7 +11277,9 @@ static void RequireUnityObserverDoesNotTickLocalSimulation(string root) "private bool TryQueryEntityTarget(", "AetheriaRuntimeZoneContactRow", "AetheriaRuntimeZoneTargetRow", - "ZoneContactsAsync()", + ".Aetheria()", + ".ZoneContacts", + ".LatestAsync()", "_facadeIndex.TryResolveEntityByDaemonIndex(targetEntityIndex, out var targetEntity)" }; var missingObservedTargetQuerySymbols = requiredObservedTargetQuerySymbols @@ -11365,15 +11407,18 @@ static void RequireUnityObserverDoesNotTickLocalSimulation(string root) if (!mapRenderer.Contains("private AetheriaClient _client", StringComparison.Ordinal) || !mapRenderer.Contains("ObjectsViewportAsync", StringComparison.Ordinal) || - !mapRenderer.Contains("GravityViewportAsync", StringComparison.Ordinal) || !mapRenderer.Contains("PlayerSettingsAsync()", StringComparison.Ordinal) || !sectorRenderer.Contains("private AetheriaClient _client", StringComparison.Ordinal) || - !sectorRenderer.Contains("SectorMapAsync()", StringComparison.Ordinal) || + !sectorRenderer.Contains(".Aetheria()", StringComparison.Ordinal) || + !sectorRenderer.Contains(".SectorMap", StringComparison.Ordinal) || + !sectorRenderer.Contains(".LatestAsync()", StringComparison.Ordinal) || !sectorRenderer.Contains("ZoneDetailsAsync(zoneIndex)", StringComparison.Ordinal) || !sectorRenderer.Contains("OpenRuntimeCatalog()", StringComparison.Ordinal) || !sectorRenderer.Contains("PlayerSettingsAsync()", StringComparison.Ordinal) || !sectorMap.Contains("private AetheriaClient _client", StringComparison.Ordinal) || - !sectorMap.Contains("SectorMapAsync()", StringComparison.Ordinal) || + !sectorMap.Contains(".Aetheria()", StringComparison.Ordinal) || + !sectorMap.Contains(".SectorMap", StringComparison.Ordinal) || + !sectorMap.Contains(".LatestAsync()", StringComparison.Ordinal) || !sectorRenderer.Contains("AetheriaRuntimeZoneDetailsSurfaceBuilder.ProjectDaemonZone(", StringComparison.Ordinal)) { throw new InvalidOperationException( @@ -12016,7 +12061,9 @@ static void RequireUnityObserverDoesNotTickLocalSimulation(string root) { "TryStartDaemonObservedGame", "LatestSectorMap(stateBoot)", - ".SectorMapAsync()", + ".Aetheria()", + ".SectorMap", + ".LatestAsync()", "AetheriaUnityObservedRunProjection.Project(", "sectorMap.FrameId", "sectorMap.IsTutorial", @@ -12835,7 +12882,9 @@ static void RequireRuntimeStateReaderOwnsUnityStateAcquisition(string root) if (!observedFrameApplier.Contains("private AetheriaRuntimeZoneRenderDocument _lastZoneRender;", StringComparison.Ordinal) || !observedFrameApplier.Contains("ApplyLatestZoneRender()", StringComparison.Ordinal) || - !observedFrameApplier.Contains("ZoneRenderAsync()", StringComparison.Ordinal)) + !observedFrameApplier.Contains(".Aetheria()", StringComparison.Ordinal) || + !observedFrameApplier.Contains(".ZoneRender", StringComparison.Ordinal) || + !observedFrameApplier.Contains(".LatestAsync()", StringComparison.Ordinal)) { throw new InvalidOperationException( "Observed zone-render state acquisition must live behind AetheriaUnityObservedFrameApplier."); @@ -12888,7 +12937,9 @@ static void RequireRuntimeStateReaderOwnsUnityStateAcquisition(string root) if (!mainMenu.Contains("AetheriaClient", StringComparison.Ordinal) || !mainMenu.Contains("LatestSectorMap(AetheriaRuntimeStateBootReport stateBoot)", StringComparison.Ordinal) || - !mainMenu.Contains(".SectorMapAsync()", StringComparison.Ordinal)) + !mainMenu.Contains(".Aetheria()", StringComparison.Ordinal) || + !mainMenu.Contains(".SectorMap", StringComparison.Ordinal) || + !mainMenu.Contains(".LatestAsync()", StringComparison.Ordinal)) { throw new InvalidOperationException( "MainMenu no longer routes sector-map lookup through the shared Aetheria client facade."); @@ -14579,7 +14630,8 @@ static void RequireDockedCurrentShipRequestAuthority(string root) } if (!inventoryPanel.Contains("TryResolveCurrentEntityKey(out var currentEntityKey)", StringComparison.Ordinal) || - !inventoryPanel.Contains("CurrentEntityAsync()", StringComparison.Ordinal) || + !inventoryPanel.Contains(".DockingState", StringComparison.Ordinal) || + !inventoryPanel.Contains("snapshot?.CurrentEntityKey", StringComparison.Ordinal) || inventoryPanel.Contains("GameManager.CurrentEntity", StringComparison.Ordinal)) { throw new InvalidOperationException( diff --git a/Aetheria.State/AetheriaVerseReplica.cs b/Aetheria.State/AetheriaVerseReplica.cs index 4188e7d4d..759fa5414 100644 --- a/Aetheria.State/AetheriaVerseReplica.cs +++ b/Aetheria.State/AetheriaVerseReplica.cs @@ -70,6 +70,25 @@ public static class AetheriaVerseReplica .ConfigureAwait(false); } + public static async Task ApplyRawSnapshotResponseAsync( + CultMeshNode node, + CultNetSnapshotResponseRawMessage response, + bool flush = true) + { + if (node == null) + throw new ArgumentNullException(nameof(node)); + if (response == null) + throw new ArgumentNullException(nameof(response)); + + var applied = await node.Database.Documents + .ApplyRawSnapshotResponseAsync(node.Cache, response) + .ConfigureAwait(false); + if (flush) + await node.FlushAsync().ConfigureAwait(false); + + return applied.Count; + } + public static async Task> FetchScopedDocumentsAsync( string endpoint, IReadOnlyList? schemaIds = null, diff --git a/Aetheria.sln b/Aetheria.sln index cb41bd8e5..7e9eba219 100644 --- a/Aetheria.sln +++ b/Aetheria.sln @@ -1,43 +1,75 @@  -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 11.00 # Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Aetheria.State.Unity", "GameCult.Aetheria.State.Unity.csproj", "{D6094CFD-26D1-83D1-2366-5762CCA2BC33}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp-firstpass", "Assembly-CSharp-firstpass.csproj", "{7c9c511c-1c6f-9c1f-c455-88359f498803}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Aetheria.EveRuntime", "GameCult.Aetheria.EveRuntime.csproj", "{A1A8B65B-CA79-2FA2-3803-5F4833778BDB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp", "Assembly-CSharp.csproj", "{47728ed0-fa53-fa99-9dca-8eb78b1dbfe4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Brokkr.Editor", "GameCult.Brokkr.Editor.csproj", "{F041E9DC-DC28-1D6C-75A3-824162B4A1AE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Aetheria.State.Unity", "GameCult.Aetheria.State.Unity.csproj", "{d6094cfd-26d1-83d1-2366-5762cca2bc33}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CultMath", "CultMath.csproj", "{6F7A47C4-21DC-64EF-0969-96B956A000F4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniRx", "UniRx.csproj", "{99486114-c78b-e9b0-654f-710398497645}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Brokkr", "GameCult.Brokkr.csproj", "{FC28BCBE-9EF4-444F-C1C9-DDE8A3A864AD}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Aetheria.EveRuntime", "GameCult.Aetheria.EveRuntime.csproj", "{a1a8b65b-ca79-2fa2-3803-5f4833778bdb}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Eve.UnityUIToolkit", "GameCult.Eve.UnityUIToolkit.csproj", "{5CDB0413-71D7-C6B3-4AD9-D8E730B55FFB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aetheria.Shared.Unity", "Aetheria.Shared.Unity.csproj", "{31542707-f1f7-33c1-eef6-37982be14f82}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Eve.Surface", "GameCult.Eve.Surface.csproj", "{037DACF0-10F4-20ED-7963-3A1E1AC15B64}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp-Editor-firstpass", "Assembly-CSharp-Editor-firstpass.csproj", "{b690125b-09ac-e423-4873-1648ff205166}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessagePack.SourceDisabled", "MessagePack.SourceDisabled.csproj", "{B916E744-81E2-A0BA-3884-E845F1C5EF88}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp-Editor", "Assembly-CSharp-Editor.csproj", "{6529161a-3c3e-1394-61a8-ab66047d3732}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ink", "Ink.csproj", "{6bae5155-742f-1170-f8b5-ff696c59aad8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Brokkr.Editor", "GameCult.Brokkr.Editor.csproj", "{f041e9dc-dc28-1d6c-75a3-824162b4a1ae}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CultMath", "CultMath.csproj", "{6f7a47c4-21dc-64ef-0969-96b956a000f4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Brokkr", "GameCult.Brokkr.csproj", "{fc28bcbe-9ef4-444f-c1c9-dde8a3a864ad}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests.csproj", "{28568750-512b-4d45-69bf-887882a23da7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Eve.UnityUIToolkit", "GameCult.Eve.UnityUIToolkit.csproj", "{5cdb0413-71d7-c6b3-4ad9-d8e730b55ffb}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Eve.Surface", "GameCult.Eve.Surface.csproj", "{037dacf0-10f4-20ed-7963-3a1e1ac15b64}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessagePack.SourceDisabled", "MessagePack.SourceDisabled.csproj", "{b916e744-81e2-a0ba-3884-e845f1c5ef88}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D6094CFD-26D1-83D1-2366-5762CCA2BC33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D6094CFD-26D1-83D1-2366-5762CCA2BC33}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1A8B65B-CA79-2FA2-3803-5F4833778BDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1A8B65B-CA79-2FA2-3803-5F4833778BDB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F041E9DC-DC28-1D6C-75A3-824162B4A1AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F041E9DC-DC28-1D6C-75A3-824162B4A1AE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6F7A47C4-21DC-64EF-0969-96B956A000F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6F7A47C4-21DC-64EF-0969-96B956A000F4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FC28BCBE-9EF4-444F-C1C9-DDE8A3A864AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FC28BCBE-9EF4-444F-C1C9-DDE8A3A864AD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5CDB0413-71D7-C6B3-4AD9-D8E730B55FFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5CDB0413-71D7-C6B3-4AD9-D8E730B55FFB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {037DACF0-10F4-20ED-7963-3A1E1AC15B64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {037DACF0-10F4-20ED-7963-3A1E1AC15B64}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B916E744-81E2-A0BA-3884-E845F1C5EF88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B916E744-81E2-A0BA-3884-E845F1C5EF88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7c9c511c-1c6f-9c1f-c455-88359f498803}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7c9c511c-1c6f-9c1f-c455-88359f498803}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47728ed0-fa53-fa99-9dca-8eb78b1dbfe4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47728ed0-fa53-fa99-9dca-8eb78b1dbfe4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {d6094cfd-26d1-83d1-2366-5762cca2bc33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {d6094cfd-26d1-83d1-2366-5762cca2bc33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99486114-c78b-e9b0-654f-710398497645}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99486114-c78b-e9b0-654f-710398497645}.Debug|Any CPU.Build.0 = Debug|Any CPU + {a1a8b65b-ca79-2fa2-3803-5f4833778bdb}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {a1a8b65b-ca79-2fa2-3803-5f4833778bdb}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31542707-f1f7-33c1-eef6-37982be14f82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31542707-f1f7-33c1-eef6-37982be14f82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {b690125b-09ac-e423-4873-1648ff205166}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {b690125b-09ac-e423-4873-1648ff205166}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6529161a-3c3e-1394-61a8-ab66047d3732}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6529161a-3c3e-1394-61a8-ab66047d3732}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6bae5155-742f-1170-f8b5-ff696c59aad8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6bae5155-742f-1170-f8b5-ff696c59aad8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {f041e9dc-dc28-1d6c-75a3-824162b4a1ae}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {f041e9dc-dc28-1d6c-75a3-824162b4a1ae}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6f7a47c4-21dc-64ef-0969-96b956a000f4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6f7a47c4-21dc-64ef-0969-96b956a000f4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {fc28bcbe-9ef4-444f-c1c9-dde8a3a864ad}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {fc28bcbe-9ef4-444f-c1c9-dde8a3a864ad}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28568750-512b-4d45-69bf-887882a23da7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28568750-512b-4d45-69bf-887882a23da7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5cdb0413-71d7-c6b3-4ad9-d8e730b55ffb}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5cdb0413-71d7-c6b3-4ad9-d8e730b55ffb}.Debug|Any CPU.Build.0 = Debug|Any CPU + {037dacf0-10f4-20ed-7963-3a1e1ac15b64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {037dacf0-10f4-20ed-7963-3a1e1ac15b64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {b916e744-81e2-a0ba-3884-e845f1c5ef88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {b916e744-81e2-a0ba-3884-e845f1c5ef88}.Debug|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Assets/Scripts/Gameplay/AetheriaDaemonIndirectRenderer.cs b/Assets/Scripts/Gameplay/AetheriaDaemonIndirectRenderer.cs index 6fe240f98..3aa6f2771 100644 --- a/Assets/Scripts/Gameplay/AetheriaDaemonIndirectRenderer.cs +++ b/Assets/Scripts/Gameplay/AetheriaDaemonIndirectRenderer.cs @@ -67,8 +67,8 @@ public sealed class AetheriaDaemonIndirectRenderer : MonoBehaviour foreach (var renderGroup in renderGroups) { _seenGroupIds.Add(renderGroup.GroupId); - if (!assetCatalog.TryResolveMesh(renderGroup.MeshKey, out var mesh) || - !assetCatalog.TryResolveMaterial(renderGroup.MaterialKey, out var material)) + if (!TryResolveMesh(renderGroup, out var mesh) || + !TryResolveMaterial(renderGroup, out var material)) { if (logMissingAssets) { @@ -237,4 +237,28 @@ public sealed class AetheriaDaemonIndirectRenderer : MonoBehaviour _staleGroupIds.Clear(); } + + private bool TryResolveMesh(AetheriaRuntimeDaemonRenderGroupDocument renderGroup, out Mesh mesh) + { + if (renderGroup?.MeshAsset != null && + !string.IsNullOrWhiteSpace(renderGroup.MeshAsset.AssetKey) && + assetCatalog.TryResolveMesh(renderGroup.MeshAsset, out mesh)) + { + return true; + } + + return assetCatalog.TryResolveMesh(renderGroup?.MeshKey, out mesh); + } + + private bool TryResolveMaterial(AetheriaRuntimeDaemonRenderGroupDocument renderGroup, out Material material) + { + if (renderGroup?.MaterialAsset != null && + !string.IsNullOrWhiteSpace(renderGroup.MaterialAsset.AssetKey) && + assetCatalog.TryResolveMaterial(renderGroup.MaterialAsset, out material)) + { + return true; + } + + return assetCatalog.TryResolveMaterial(renderGroup?.MaterialKey, out material); + } } diff --git a/Assets/Scripts/Gameplay/AetheriaDaemonRenderAssetCatalog.cs b/Assets/Scripts/Gameplay/AetheriaDaemonRenderAssetCatalog.cs index 0fb885cfb..46d1b86c8 100644 --- a/Assets/Scripts/Gameplay/AetheriaDaemonRenderAssetCatalog.cs +++ b/Assets/Scripts/Gameplay/AetheriaDaemonRenderAssetCatalog.cs @@ -1,4 +1,5 @@ using System; +using GameCult.Aetheria.State.Verse; using UnityEngine; [CreateAssetMenu( @@ -33,6 +34,11 @@ public sealed class AetheriaDaemonRenderAssetCatalog : ScriptableObject return mesh != null; } + public bool TryResolveMesh(AetheriaRuntimeAssetRef asset, out Mesh mesh) + { + return TryResolveMesh(ResolveLocalKey(asset), out mesh); + } + public bool TryResolveMaterial(string key, out Material material) { material = null; @@ -54,6 +60,22 @@ public sealed class AetheriaDaemonRenderAssetCatalog : ScriptableObject return material != null; } + public bool TryResolveMaterial(AetheriaRuntimeAssetRef asset, out Material material) + { + return TryResolveMaterial(ResolveLocalKey(asset), out material); + } + + public bool TryResolveTexture(AetheriaRuntimeAssetRef asset, out Texture2D texture) + { + texture = null; + var key = ResolveLocalKey(asset); + if (string.IsNullOrWhiteSpace(key)) + return false; + + texture = Resources.Load(NormalizeResourceKey(key)); + return texture != null; + } + private static string NormalizeResourceKey(string key) { const string resourcesPrefix = "resources://"; @@ -65,6 +87,17 @@ public sealed class AetheriaDaemonRenderAssetCatalog : ScriptableObject return key; } + private static string ResolveLocalKey(AetheriaRuntimeAssetRef asset) + { + if (asset == null) + return ""; + + if (string.Equals(asset.Transport, AetheriaRuntimeAssetTransports.Resources, StringComparison.OrdinalIgnoreCase)) + return asset.Uri; + + return string.IsNullOrWhiteSpace(asset.Uri) ? asset.AssetKey : asset.Uri; + } + [Serializable] private struct MeshEntry { diff --git a/Assets/Scripts/Gameplay/AetheriaRenderSplatBuffer.cs b/Assets/Scripts/Gameplay/AetheriaRenderSplatBuffer.cs new file mode 100644 index 000000000..9fab7182f --- /dev/null +++ b/Assets/Scripts/Gameplay/AetheriaRenderSplatBuffer.cs @@ -0,0 +1,137 @@ +using GameCult.Aetheria.State.Verse; +using Unity.Mathematics; +using UnityEngine; + +public sealed class AetheriaRenderSplatBuffer : System.IDisposable +{ + public const string SplatBufferPropertyName = "_AetheriaSplats"; + public const string SplatCountPropertyName = "_AetheriaSplatCount"; + public const string ViewportToClipPropertyName = "_AetheriaViewportToClip"; + public const string ChannelFilterPropertyName = "_AetheriaChannelFilter"; + + private const int SplatStrideBytes = 80; + + private AetheriaRenderSplat[] _splats = System.Array.Empty(); + private GraphicsBuffer _buffer; + private int _capacity; + + public int Count { get; private set; } + public GraphicsBuffer Buffer => _buffer; + public bool HasGpuBuffer => _buffer != null && _buffer.IsValid() && Count > 0; + + public bool Upload(AetheriaRuntimeRenderSplatSoa splats, int channelFilter = -1, int layerFilter = -1) + { + Count = Pack(splats, channelFilter, layerFilter); + EnsureCapacity(Count); + if (Count <= 0) + return false; + + _buffer.SetData(_splats, 0, 0, Count); + return true; + } + + private int Pack(AetheriaRuntimeRenderSplatSoa splats, int channelFilter, int layerFilter) + { + if (splats == null || splats.Count <= 0) + return 0; + + EnsureCpuCapacity(splats.Count); + var count = 0; + for (var i = 0; i < splats.Count; i++) + { + var channel = Read(splats.Channel, i, 0); + if (channelFilter >= 0 && channel != channelFilter) + continue; + + var layerIndex = Read(splats.LayerIndex, i, -1); + if (layerFilter >= 0 && layerIndex != layerFilter) + continue; + + var halfExtentX = (float)Read(splats.HalfExtentX, i, 0); + var halfExtentY = (float)Read(splats.HalfExtentY, i, 0); + if (halfExtentX <= 0 || halfExtentY <= 0) + continue; + + _splats[count++] = new AetheriaRenderSplat + { + CenterHalfExtent = new float4( + (float)Read(splats.CenterX, i, 0), + (float)Read(splats.CenterY, i, 0), + halfExtentX, + halfExtentY), + RotationChannelFalloff = new float4( + (float)Read(splats.RotationCos, i, 1), + (float)Read(splats.RotationSin, i, 0), + channel, + Read(splats.Falloff, i, AetheriaRuntimeRenderSplatFalloffs.Smooth)), + LayerSource = new float4( + layerIndex, + Read(splats.SourceKind, i, AetheriaRuntimeRenderSplatSourceKinds.Constant), + (float)Read(splats.AnimationSpeed, i, 0), + (float)Read(splats.SourceFlags, i, 0)), + SourceFrequencyPhase = new float4( + (float)Read(splats.FrequencyX, i, 1), + (float)Read(splats.FrequencyY, i, 1), + (float)Read(splats.PhaseX, i, 0), + (float)Read(splats.PhaseY, i, 0)), + Value = new float4( + (float)Read(splats.ValueR, i, 0), + (float)Read(splats.ValueG, i, 0), + (float)Read(splats.ValueB, i, 0), + (float)Read(splats.ValueA, i, 1)) + }; + } + + return count; + } + + private void EnsureCpuCapacity(int count) + { + if (_splats.Length >= count) + return; + + _splats = new AetheriaRenderSplat[math.ceilpow2(math.max(1, count))]; + } + + private void EnsureCapacity(int count) + { + if (_buffer != null && _buffer.IsValid() && _capacity >= count) + return; + + _buffer?.Release(); + _buffer = null; + _capacity = 0; + if (count <= 0) + return; + + _capacity = math.ceilpow2(math.max(1, count)); + _buffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, _capacity, SplatStrideBytes); + } + + public void Dispose() + { + _buffer?.Release(); + _buffer = null; + _capacity = 0; + Count = 0; + } + + private static double Read(System.Collections.Generic.IReadOnlyList values, int index, double fallback) + { + return values != null && index >= 0 && index < values.Count ? values[index] : fallback; + } + + private static int Read(System.Collections.Generic.IReadOnlyList values, int index, int fallback) + { + return values != null && index >= 0 && index < values.Count ? values[index] : fallback; + } + + private struct AetheriaRenderSplat + { + public float4 CenterHalfExtent; + public float4 RotationChannelFalloff; + public float4 LayerSource; + public float4 SourceFrequencyPhase; + public float4 Value; + } +} diff --git a/Assets/Scripts/Gameplay/AetheriaRenderSplatBuffer.cs.meta b/Assets/Scripts/Gameplay/AetheriaRenderSplatBuffer.cs.meta new file mode 100644 index 000000000..0d577cb03 --- /dev/null +++ b/Assets/Scripts/Gameplay/AetheriaRenderSplatBuffer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 32e6f17486f34e8c8c5f7640bff319af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Gameplay/AetheriaRenderSplatLayerRenderer.cs b/Assets/Scripts/Gameplay/AetheriaRenderSplatLayerRenderer.cs new file mode 100644 index 000000000..6a7138399 --- /dev/null +++ b/Assets/Scripts/Gameplay/AetheriaRenderSplatLayerRenderer.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using GameCult.Aetheria.State.Verse; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Experimental.Rendering; +using UnityEngine.UI; + +public sealed class AetheriaRenderSplatLayerRenderer : MonoBehaviour +{ + [SerializeField] + private AetheriaRenderSplatRasterizer rasterizer; + + [SerializeField] + private AetheriaRenderSplatLayerBinding[] bindings = + { + new AetheriaRenderSplatLayerBinding + { + LayerKey = AetheriaRuntimeRenderSplatLayerKeys.GravityHeight, + MaterialTextureProperty = "_DetailTex", + GlobalTextureName = "_AetheriaGravityHeight" + }, + new AetheriaRenderSplatLayerBinding + { + LayerKey = AetheriaRuntimeRenderSplatLayerKeys.FogTint, + MaterialTextureProperty = "_DetailTex", + GlobalTextureName = "_NebulaTint" + }, + new AetheriaRenderSplatLayerBinding + { + LayerKey = AetheriaRuntimeRenderSplatLayerKeys.FogSurfaceHeight, + GlobalTextureName = "_NebulaSurfaceHeight", + WidthScale = 0.5f, + HeightScale = 0.5f + }, + new AetheriaRenderSplatLayerBinding + { + LayerKey = AetheriaRuntimeRenderSplatLayerKeys.FogPatchHeight, + GlobalTextureName = "_NebulaPatchHeight", + WidthScale = 0.5f, + HeightScale = 0.5f + }, + new AetheriaRenderSplatLayerBinding + { + LayerKey = AetheriaRuntimeRenderSplatLayerKeys.FogPatch, + GlobalTextureName = "_NebulaPatch", + WidthScale = 0.5f, + HeightScale = 0.5f + }, + new AetheriaRenderSplatLayerBinding + { + LayerKey = AetheriaRuntimeRenderSplatLayerKeys.Influence, + MaterialTextureProperty = "_DetailTex", + GlobalTextureName = "_AetheriaInfluence" + } + }; + + private readonly Dictionary _texturesByLayerKey = + new Dictionary(StringComparer.Ordinal); + + private void Reset() + { + rasterizer = GetComponent(); + } + + public bool TryGetTexture(string layerKey, out RenderTexture texture) + { + return _texturesByLayerKey.TryGetValue(layerKey ?? "", out texture); + } + + public void Render( + AetheriaRuntimeRenderSplatsViewportDocument document, + int width, + int height) + { + if (document == null) + return; + + ResolveRasterizer(); + if (rasterizer == null) + return; + + var layers = document.Layers ?? Array.Empty(); + for (var layerIndex = 0; layerIndex < layers.Count; layerIndex++) + { + var layer = layers[layerIndex]; + if (layer == null || string.IsNullOrWhiteSpace(layer.LayerKey)) + continue; + + foreach (var binding in bindings ?? Array.Empty()) + { + if (binding == null || + !binding.Enabled || + !string.Equals(binding.LayerKey, layer.LayerKey, StringComparison.Ordinal)) + { + continue; + } + + var texture = EnsureTexture(binding, layer, width, height); + rasterizer.RenderLayerToTarget( + document, + texture, + layerIndex, + ResolveMaterialPass(layer.BlendMode), + ResolveClearColor(layer)); + ApplyBinding(binding, texture); + _texturesByLayerKey[layer.LayerKey] = texture; + } + } + } + + private void ResolveRasterizer() + { + if (rasterizer != null) + return; + + rasterizer = GetComponent(); + if (rasterizer == null) + rasterizer = gameObject.AddComponent(); + } + + private RenderTexture EnsureTexture( + AetheriaRenderSplatLayerBinding binding, + AetheriaRuntimeRenderSplatLayerDefinition layer, + int width, + int height) + { + var targetWidth = math.max(1, Mathf.RoundToInt(width * Mathf.Max(0.01f, binding.WidthScale))); + var targetHeight = math.max(1, Mathf.RoundToInt(height * Mathf.Max(0.01f, binding.HeightScale))); + var format = ResolveGraphicsFormat(binding.GraphicsFormatOverride, layer.GraphicsFormat); + var existing = binding.TargetTexture; + if (existing != null && + existing.width == targetWidth && + existing.height == targetHeight && + existing.graphicsFormat == format) + { + return existing; + } + + if (existing != null) + { + existing.Release(); + Destroy(existing); + } + + var descriptor = new RenderTextureDescriptor(targetWidth, targetHeight) + { + depthBufferBits = 0, + graphicsFormat = format, + msaaSamples = 1, + sRGB = false, + useMipMap = binding.UseMipMaps, + autoGenerateMips = binding.UseMipMaps + }; + binding.TargetTexture = new RenderTexture(descriptor) + { + name = $"Aetheria {layer.LayerKey}", + filterMode = binding.FilterMode, + wrapMode = TextureWrapMode.Clamp + }; + binding.TargetTexture.Create(); + return binding.TargetTexture; + } + + private static GraphicsFormat ResolveGraphicsFormat(string bindingOverride, string layerFormat) + { + var value = string.IsNullOrWhiteSpace(bindingOverride) ? layerFormat : bindingOverride; + return Enum.TryParse(value, out GraphicsFormat format) + ? format + : GraphicsFormat.R16_SFloat; + } + + private static int ResolveMaterialPass(string blendMode) + { + switch (blendMode) + { + case AetheriaRuntimeRenderSplatBlendModes.Max: + return 1; + case AetheriaRuntimeRenderSplatBlendModes.Alpha: + return 2; + default: + return 0; + } + } + + private static Color ResolveClearColor(AetheriaRuntimeRenderSplatLayerDefinition layer) + { + return new Color( + (float)layer.ClearR, + (float)layer.ClearG, + (float)layer.ClearB, + (float)layer.ClearA); + } + + private static void ApplyBinding(AetheriaRenderSplatLayerBinding binding, RenderTexture texture) + { + if (binding.Display != null) + { + var material = binding.Display.material; + if (material != null && !string.IsNullOrWhiteSpace(binding.MaterialTextureProperty)) + material.SetTexture(binding.MaterialTextureProperty, texture); + else + binding.Display.material.mainTexture = texture; + } + + if (!string.IsNullOrWhiteSpace(binding.GlobalTextureName)) + Shader.SetGlobalTexture(binding.GlobalTextureName, texture); + } + + private void OnDisable() + { + foreach (var binding in bindings ?? Array.Empty()) + { + if (binding?.TargetTexture == null) + continue; + + binding.TargetTexture.Release(); + Destroy(binding.TargetTexture); + binding.TargetTexture = null; + } + + _texturesByLayerKey.Clear(); + } +} + +[Serializable] +public sealed class AetheriaRenderSplatLayerBinding +{ + public bool Enabled = true; + public string LayerKey = ""; + public Image Display; + public string MaterialTextureProperty = "_DetailTex"; + public string GlobalTextureName = ""; + public string GraphicsFormatOverride = ""; + public float WidthScale = 1.0f; + public float HeightScale = 1.0f; + public bool UseMipMaps; + public FilterMode FilterMode = FilterMode.Bilinear; + [NonSerialized] + public RenderTexture TargetTexture; +} diff --git a/Assets/Scripts/Gameplay/AetheriaRenderSplatLayerRenderer.cs.meta b/Assets/Scripts/Gameplay/AetheriaRenderSplatLayerRenderer.cs.meta new file mode 100644 index 000000000..4621a3b70 --- /dev/null +++ b/Assets/Scripts/Gameplay/AetheriaRenderSplatLayerRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0f3c143f0a844f38a109f2de02ce4423 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Gameplay/AetheriaRenderSplatRasterizer.cs b/Assets/Scripts/Gameplay/AetheriaRenderSplatRasterizer.cs new file mode 100644 index 000000000..ff6aeef92 --- /dev/null +++ b/Assets/Scripts/Gameplay/AetheriaRenderSplatRasterizer.cs @@ -0,0 +1,300 @@ +using GameCult.Aetheria.State.Verse; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Experimental.Rendering; +using UnityEngine.Rendering; + +public sealed class AetheriaRenderSplatRasterizer : MonoBehaviour +{ + [SerializeField] + private Material splatMaterial; + + [SerializeField] + private RenderTexture targetTexture; + + [SerializeField] + private GraphicsFormat targetFormat = GraphicsFormat.R16_SFloat; + + [SerializeField] + private int width = 512; + + [SerializeField] + private int height = 512; + + [SerializeField] + private int channelFilter = -1; + + [SerializeField] + private int layerFilter = -1; + + [SerializeField] + private int materialPass; + + [SerializeField] + private Color clearColor = Color.clear; + + [SerializeField] + private bool clearBeforeDraw = true; + + private readonly AetheriaRenderSplatBuffer _buffer = new AetheriaRenderSplatBuffer(); + private CommandBuffer _commandBuffer; + private RenderTexture _ownedTargetTexture; + private Material _ownedSplatMaterial; + private static readonly int SplatBufferPropertyId = + Shader.PropertyToID(AetheriaRenderSplatBuffer.SplatBufferPropertyName); + private static readonly int SplatCountPropertyId = + Shader.PropertyToID(AetheriaRenderSplatBuffer.SplatCountPropertyName); + private static readonly int ViewportToClipPropertyId = + Shader.PropertyToID(AetheriaRenderSplatBuffer.ViewportToClipPropertyName); + private static readonly int ChannelFilterPropertyId = + Shader.PropertyToID(AetheriaRenderSplatBuffer.ChannelFilterPropertyName); + + public RenderTexture TargetTexture => targetTexture != null ? targetTexture : _ownedTargetTexture; + public int ChannelFilter + { + get => channelFilter; + set => channelFilter = value; + } + + public int LayerFilter + { + get => layerFilter; + set => layerFilter = value; + } + + public int LastDrawnCount { get; private set; } + + public RenderTexture Render( + AetheriaRuntimeRenderSplatsViewportDocument document, + int overrideWidth = 0, + int overrideHeight = 0) + { + if (document == null) + { + LastDrawnCount = 0; + return TargetTexture; + } + + var output = EnsureTarget(overrideWidth, overrideHeight); + var material = ResolveMaterial(); + if (output == null || material == null) + { + LastDrawnCount = 0; + return output; + } + + _buffer.Upload(document.Splats, channelFilter, layerFilter); + LastDrawnCount = _buffer.Count; + if (!_buffer.HasGpuBuffer) + { + if (clearBeforeDraw) + Clear(output); + return output; + } + + _commandBuffer ??= new CommandBuffer { name = "Aetheria Render Splats" }; + _commandBuffer.Clear(); + _commandBuffer.SetRenderTarget(output); + if (clearBeforeDraw) + _commandBuffer.ClearRenderTarget(false, true, clearColor); + + _commandBuffer.SetGlobalBuffer(SplatBufferPropertyId, _buffer.Buffer); + _commandBuffer.SetGlobalInt(SplatCountPropertyId, _buffer.Count); + _commandBuffer.SetGlobalInt(ChannelFilterPropertyId, channelFilter); + _commandBuffer.SetGlobalMatrix(ViewportToClipPropertyId, BuildViewportToClip(document.Viewport)); + _commandBuffer.DrawProcedural( + Matrix4x4.identity, + material, + math.max(0, materialPass), + MeshTopology.Triangles, + 6, + _buffer.Count); + Graphics.ExecuteCommandBuffer(_commandBuffer); + return output; + } + + public RenderTexture Render( + AetheriaRuntimeRenderSplatsViewportDocument document, + int overrideWidth, + int overrideHeight, + int overrideChannelFilter) + { + var previousChannelFilter = channelFilter; + channelFilter = overrideChannelFilter; + try + { + return Render(document, overrideWidth, overrideHeight); + } + finally + { + channelFilter = previousChannelFilter; + } + } + + public RenderTexture RenderLayer( + AetheriaRuntimeRenderSplatsViewportDocument document, + int overrideWidth, + int overrideHeight, + int overrideLayerFilter, + int overrideMaterialPass, + GraphicsFormat overrideTargetFormat, + Color overrideClearColor) + { + var previousLayerFilter = layerFilter; + var previousMaterialPass = materialPass; + var previousTargetFormat = targetFormat; + var previousClearColor = clearColor; + layerFilter = overrideLayerFilter; + materialPass = overrideMaterialPass; + targetFormat = overrideTargetFormat; + clearColor = overrideClearColor; + try + { + return Render(document, overrideWidth, overrideHeight); + } + finally + { + layerFilter = previousLayerFilter; + materialPass = previousMaterialPass; + targetFormat = previousTargetFormat; + clearColor = previousClearColor; + } + } + + public RenderTexture RenderLayerToTarget( + AetheriaRuntimeRenderSplatsViewportDocument document, + RenderTexture output, + int overrideLayerFilter, + int overrideMaterialPass, + Color overrideClearColor) + { + var previousTargetTexture = targetTexture; + var previousLayerFilter = layerFilter; + var previousMaterialPass = materialPass; + var previousClearColor = clearColor; + targetTexture = output; + layerFilter = overrideLayerFilter; + materialPass = overrideMaterialPass; + clearColor = overrideClearColor; + try + { + return Render(document, output != null ? output.width : 0, output != null ? output.height : 0); + } + finally + { + targetTexture = previousTargetTexture; + layerFilter = previousLayerFilter; + materialPass = previousMaterialPass; + clearColor = previousClearColor; + } + } + + private Material ResolveMaterial() + { + if (splatMaterial != null) + return splatMaterial; + + if (_ownedSplatMaterial != null) + return _ownedSplatMaterial; + + var shader = Shader.Find("Aetheria/Render Splats"); + if (shader == null) + return null; + + _ownedSplatMaterial = new Material(shader) + { + hideFlags = HideFlags.DontSave + }; + return _ownedSplatMaterial; + } + + private RenderTexture EnsureTarget(int overrideWidth, int overrideHeight) + { + if (targetTexture != null) + return targetTexture; + + var targetWidth = math.max(1, overrideWidth > 0 ? overrideWidth : width); + var targetHeight = math.max(1, overrideHeight > 0 ? overrideHeight : height); + if (_ownedTargetTexture != null && + _ownedTargetTexture.width == targetWidth && + _ownedTargetTexture.height == targetHeight && + _ownedTargetTexture.graphicsFormat == targetFormat) + { + return _ownedTargetTexture; + } + + ReleaseOwnedTarget(); + var descriptor = new RenderTextureDescriptor(targetWidth, targetHeight) + { + depthBufferBits = 0, + graphicsFormat = targetFormat, + msaaSamples = 1, + sRGB = false, + useMipMap = false, + autoGenerateMips = false + }; + _ownedTargetTexture = new RenderTexture(descriptor) + { + name = "Aetheria Render Splats", + filterMode = FilterMode.Bilinear, + wrapMode = TextureWrapMode.Clamp + }; + _ownedTargetTexture.Create(); + return _ownedTargetTexture; + } + + private void Clear(RenderTexture output) + { + _commandBuffer ??= new CommandBuffer { name = "Aetheria Render Splats" }; + _commandBuffer.Clear(); + _commandBuffer.SetRenderTarget(output); + _commandBuffer.ClearRenderTarget(false, true, clearColor); + Graphics.ExecuteCommandBuffer(_commandBuffer); + } + + private static Matrix4x4 BuildViewportToClip(AetheriaRuntimeRtsViewportBounds viewport) + { + viewport ??= new AetheriaRuntimeRtsViewportBounds(); + var minX = (float)math.min(viewport.MinX, viewport.MaxX); + var minY = (float)math.min(viewport.MinY, viewport.MaxY); + var maxX = (float)math.max(viewport.MinX, viewport.MaxX); + var maxY = (float)math.max(viewport.MinY, viewport.MaxY); + var width = math.max(0.0001f, maxX - minX); + var height = math.max(0.0001f, maxY - minY); + + return new Matrix4x4( + new Vector4(2f / width, 0, 0, 0), + new Vector4(0, 2f / height, 0, 0), + new Vector4(0, 0, 1, 0), + new Vector4(-(maxX + minX) / width, -(maxY + minY) / height, 0, 1)); + } + + private void OnDisable() + { + _buffer.Dispose(); + _commandBuffer?.Release(); + _commandBuffer = null; + DestroyOwnedMaterial(); + ReleaseOwnedTarget(); + LastDrawnCount = 0; + } + + private void DestroyOwnedMaterial() + { + if (_ownedSplatMaterial == null) + return; + + Destroy(_ownedSplatMaterial); + _ownedSplatMaterial = null; + } + + private void ReleaseOwnedTarget() + { + if (_ownedTargetTexture == null) + return; + + _ownedTargetTexture.Release(); + Destroy(_ownedTargetTexture); + _ownedTargetTexture = null; + } +} diff --git a/Assets/Scripts/Gameplay/AetheriaRenderSplatRasterizer.cs.meta b/Assets/Scripts/Gameplay/AetheriaRenderSplatRasterizer.cs.meta new file mode 100644 index 000000000..d111bb5f0 --- /dev/null +++ b/Assets/Scripts/Gameplay/AetheriaRenderSplatRasterizer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f5150857a83849f195312d13b8f4d19b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Gameplay/AetheriaUnityPilotFrameAdapter.cs b/Assets/Scripts/Gameplay/AetheriaUnityPilotFrameAdapter.cs index 339a7bd7c..ef3958342 100644 --- a/Assets/Scripts/Gameplay/AetheriaUnityPilotFrameAdapter.cs +++ b/Assets/Scripts/Gameplay/AetheriaUnityPilotFrameAdapter.cs @@ -36,7 +36,6 @@ public sealed class AetheriaUnityPilotFrameAdapter return; } - var renderSettings = ZoneRenderer?.RenderSettings; TargetPresentation?.Tick(currentEntity, timeSeconds); var look = Input.Player.Look.ReadValue(); @@ -50,13 +49,14 @@ public sealed class AetheriaUnityPilotFrameAdapter _setViewDirection(viewDirection); PilotCommands.RequestLookDirection(viewDirection); - if (renderSettings != null) + if (ZoneRenderer != null) { + var renderSettings = ZoneRenderer.RenderSettings; if (HeatstrokePost != null) - HeatstrokePost.weight = (float)renderSettings.Value.NormalizeHeatstrokePost(currentEntity.Heatstroke); + HeatstrokePost.weight = (float)renderSettings.NormalizeHeatstrokePost(currentEntity.Heatstroke); if (SevereHeatstrokePost != null) SevereHeatstrokePost.weight = - (float)renderSettings.Value.ResolveSevereHeatstrokePostWeight(currentEntity.Heatstroke, timeSeconds); + (float)renderSettings.ResolveSevereHeatstrokePostWeight(currentEntity.Heatstroke, timeSeconds); } if (currentEntity is Ship) diff --git a/Assets/Scripts/Gameplay/AetheriaUnityRenderSplatViewportSource.cs b/Assets/Scripts/Gameplay/AetheriaUnityRenderSplatViewportSource.cs new file mode 100644 index 000000000..90892d56b --- /dev/null +++ b/Assets/Scripts/Gameplay/AetheriaUnityRenderSplatViewportSource.cs @@ -0,0 +1,164 @@ +using System; +using System.IO; +using GameCult.Aetheria.State.Verse; +using UnityEngine; + +public sealed class AetheriaUnityRenderSplatViewportSource : MonoBehaviour +{ + [SerializeField] + private AetheriaRenderSplatRasterizer rasterizer; + + [SerializeField] + private AetheriaRenderSplatLayerRenderer layerRenderer; + + [SerializeField] + private string layerKey = AetheriaRuntimeRenderSplatLayerKeys.GravityHeight; + + [SerializeField] + private Vector2 position; + + [SerializeField] + private Vector2 size = new Vector2(1024, 1024); + + [SerializeField] + private float refreshIntervalSeconds = 0.5f; + + [SerializeField] + private bool renderInLateUpdate = true; + + private AetheriaClient _client; + private string _clientStatePath = ""; + private float _nextRefreshTime; + private AetheriaRuntimeRenderSplatsViewportDocument _document; + + public RenderTexture TargetTexture + { + get + { + if (layerRenderer != null && layerRenderer.TryGetTexture(layerKey, out var texture)) + return texture; + + return rasterizer != null ? rasterizer.TargetTexture : null; + } + } + + private void Reset() + { + rasterizer = GetComponent(); + layerRenderer = GetComponent(); + } + + private void LateUpdate() + { + if (renderInLateUpdate) + RenderLatest(); + } + + public bool RenderLatest() + { + ResolveRenderers(); + if (layerRenderer == null && rasterizer == null) + return false; + + RefreshDocument(false); + if (_document == null) + return false; + + if (layerRenderer != null) + { + layerRenderer.Render(_document, Mathf.RoundToInt(size.x), Mathf.RoundToInt(size.y)); + return true; + } + + rasterizer.Render(_document, Mathf.RoundToInt(size.x), Mathf.RoundToInt(size.y)); + return true; + } + + private void ResolveRenderers() + { + if (layerRenderer == null) + layerRenderer = GetComponent(); + if (rasterizer == null) + rasterizer = GetComponent(); + + if (layerRenderer == null && rasterizer == null) + layerRenderer = gameObject.AddComponent(); + } + + public void SetLayerKey(string value) + { + layerKey = string.IsNullOrWhiteSpace(value) + ? AetheriaRuntimeRenderSplatLayerKeys.GravityHeight + : value; + } + + public void SetViewport(Vector2 center, Vector2 viewportSize) + { + position = center; + size = viewportSize; + RefreshDocument(true); + } + + private void RefreshDocument(bool force) + { + if (!force && Time.unscaledTime < _nextRefreshTime) + return; + + _nextRefreshTime = Time.unscaledTime + Mathf.Max(0.02f, refreshIntervalSeconds); + try + { + _document = ResolveClient() + .RenderSplatsViewportAsync(ResolveViewportBounds()) + .GetAwaiter() + .GetResult(); + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to read Aetheria render splats from local Verse state: {ex.Message}"); + } + } + + private AetheriaRuntimeRtsViewportBounds ResolveViewportBounds() + { + var halfSize = size * 0.5f; + return new AetheriaRuntimeRtsViewportBounds + { + MinX = position.x - halfSize.x, + MinY = position.y - halfSize.y, + MaxX = position.x + halfSize.x, + MaxY = position.y + halfSize.y + }; + } + + private AetheriaClient ResolveClient() + { + var gameDataDirectory = new DirectoryInfo(Path.Combine(Application.dataPath, "..", "GameData")); + var stateBoot = AetheriaRuntimeStateBoot.Inspect(gameDataDirectory); + if (_client != null && string.Equals(_clientStatePath, stateBoot.StateFilePath, StringComparison.Ordinal)) + return _client; + + DisposeClient(); + _client = AetheriaClient + .OpenLocalAsync( + gameDataDirectory, + "unity-render-splat-viewport", + "local", + pullOnOpen: true) + .GetAwaiter() + .GetResult(); + _clientStatePath = stateBoot.StateFilePath; + return _client; + } + + private void OnDestroy() + { + DisposeClient(); + } + + private void DisposeClient() + { + _client?.Dispose(); + _client = null; + _clientStatePath = ""; + } +} diff --git a/Assets/Scripts/Gameplay/AetheriaUnityRenderSplatViewportSource.cs.meta b/Assets/Scripts/Gameplay/AetheriaUnityRenderSplatViewportSource.cs.meta new file mode 100644 index 000000000..431541ce7 --- /dev/null +++ b/Assets/Scripts/Gameplay/AetheriaUnityRenderSplatViewportSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 473b4029da16430aa0c0ef89298070d8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Gameplay/AetheriaUnityTargetPresentation.cs b/Assets/Scripts/Gameplay/AetheriaUnityTargetPresentation.cs index 395ed60e8..b3e01f8cb 100644 --- a/Assets/Scripts/Gameplay/AetheriaUnityTargetPresentation.cs +++ b/Assets/Scripts/Gameplay/AetheriaUnityTargetPresentation.cs @@ -61,12 +61,12 @@ public sealed class AetheriaUnityTargetPresentation _observedHostileIndicatorTargets.Clear(); _observedFriendlyIndicatorTargets.Clear(); - var renderSettings = ZoneRenderer?.RenderSettings; - if (currentEntity != null && renderSettings != null) + if (currentEntity != null && ZoneRenderer != null) { + var renderSettings = ZoneRenderer.RenderSettings; var contacts = ResolveVisibleContacts?.Invoke( currentEntity, - renderSettings.Value.TargetDetectionInfoThreshold, + renderSettings.TargetDetectionInfoThreshold, true) ?? Array.Empty(); foreach (var contact in contacts) @@ -153,10 +153,10 @@ public sealed class AetheriaUnityTargetPresentation float time, bool blinkSpottedTargets) { - var renderSettings = ZoneRenderer?.RenderSettings; - if (renderSettings == null) + if (ZoneRenderer == null) return; + var renderSettings = ZoneRenderer.RenderSettings; foreach (var indicator in indicators) { indicator.Value.gameObject.SetActive(indicator.Key != observedTarget); @@ -168,9 +168,9 @@ public sealed class AetheriaUnityTargetPresentation } var infoGathered = ResolveInfoGathered?.Invoke(indicator.Key, currentEntity) ?? 0f; - indicator.Value.Fill.fillAmount = (float)renderSettings.Value.NormalizeDetectionProgress(infoGathered); + indicator.Value.Fill.fillAmount = (float)renderSettings.NormalizeDetectionProgress(infoGathered); indicator.Value.Fill.enabled = !blinkSpottedTargets || - renderSettings.Value.ResolveTargetSpottedFillEnabled(infoGathered, time); + renderSettings.ResolveTargetSpottedFillEnabled(infoGathered, time); } } diff --git a/Assets/Scripts/UI/Menu/InventoryMenu.cs b/Assets/Scripts/UI/Menu/InventoryMenu.cs index 9e6c86057..aaef18792 100644 --- a/Assets/Scripts/UI/Menu/InventoryMenu.cs +++ b/Assets/Scripts/UI/Menu/InventoryMenu.cs @@ -89,9 +89,8 @@ public class InventoryMenu : MonoBehaviour // { // _destroyItem = false; // }); - var hasDockingIndex = TryResolveObservedDockingIndex(out var dockingIndex); - var hasObservedCurrentEntity = hasDockingIndex && dockingIndex.TryResolveCurrentEntity(out var currentEntity); - var cargo = hasDockingIndex && dockingIndex.TryResolveCurrentDockingBay(out var dockingBay) + var hasObservedCurrentEntity = TryResolveCurrentEntityFacade(out var currentEntity); + var cargo = TryGetTypedCurrentDockingBayFacade(out var dockingBay) ? dockingBay : currentEntity?.CargoBays.FirstOrDefault(); if (cargo!=null) @@ -727,46 +726,81 @@ public class InventoryMenu : MonoBehaviour private bool TryResolveCurrentDockingBayRow(out AetheriaRuntimeStationDockingBayRow dockingBay) { dockingBay = null; - return TryResolveObservedDockingIndex(out var dockingIndex) && - dockingIndex.TryResolveCurrentDockingBayRow(out dockingBay); + var stationRefit = ResolveStationRefit(); + dockingBay = stationRefit?.IsDocked == true && stationRefit.DockingBayIndex >= 0 + ? (stationRefit.DockingBays ?? Array.Empty()) + .FirstOrDefault(row => row != null && row.DockingBayIndex == stationRefit.DockingBayIndex) + : null; + return dockingBay != null; } private bool TryResolveCurrentEntityKey(out string currentEntityKey) { currentEntityKey = ""; - return TryResolveObservedDockingIndex(out var dockingIndex) && - dockingIndex.TryResolveCurrentEntityKey(out currentEntityKey); + currentEntityKey = ResolveDockingState()?.CurrentEntityKey ?? ""; + return !string.IsNullOrWhiteSpace(currentEntityKey); } private bool TryResolveCurrentEntityDocument(out AetheriaRuntimeCurrentEntityDocument currentEntity) { currentEntity = null; - return TryResolveObservedDockingIndex(out var dockingIndex) && - dockingIndex.TryResolveCurrentEntityDocument(out currentEntity); + try + { + currentEntity = ResolveClient() + .Aetheria() + .DockingState + .Latest() + ?.CurrentEntity; + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to read Aetheria current entity for inventory ship settings: {ex.Message}"); + return false; + } + + return currentEntity != null; } private AetheriaRuntimeStationRefitDocument ResolveStationRefit() { - return TryResolveObservedDockingIndex(out var dockingIndex) - ? dockingIndex.ResolveStationRefit() - : null; + return ResolveDockingState()?.StationRefit; } - private bool TryResolveCurrentDocking(out AetheriaRuntimeCurrentDockingDocument docking) + private bool TryResolveCurrentEntityFacade(out Entity currentEntity) { - docking = null; - return TryResolveObservedDockingIndex(out var dockingIndex) && - dockingIndex.TryResolveCurrentDocking(out docking); + currentEntity = null; + var currentEntityKey = ResolveDockingState()?.CurrentEntityKey; + return _observedFacadeIndex != null && + !string.IsNullOrWhiteSpace(currentEntityKey) && + _observedFacadeIndex.TryResolveEntityByRecordKey(currentEntityKey, out currentEntity); } - private bool TryResolveObservedDockingIndex(out AetheriaUnityObservedDockingIndex dockingIndex) + private bool TryGetTypedCurrentDockingBayFacade(out EquippedCargoBay dockingBay) { - dockingIndex = null; - if (_observedFacadeIndex == null) - return false; + dockingBay = null; + var docking = ResolveDockingState()?.CurrentDocking; + return _observedFacadeIndex != null && + docking?.IsDocked == true && + _observedFacadeIndex.TryResolveDockingBayByRecordKey( + docking.DockParentEntityKey, + docking.DockingBayIndex, + out dockingBay); + } - dockingIndex = new AetheriaUnityObservedDockingIndex(ResolveClient, _observedFacadeIndex); - return true; + private AetheriaClientDockingSnapshot ResolveDockingState() + { + try + { + return ResolveClient() + .Aetheria() + .DockingState + .Latest(); + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to read Aetheria docking state for inventory menu: {ex.Message}"); + return null; + } } private bool TryResolveCargoBay(EquippedCargoBay cargoBay, out string entityKey, out int cargoIndex) diff --git a/Assets/Scripts/UI/Menu/InventoryPanel.cs b/Assets/Scripts/UI/Menu/InventoryPanel.cs index 4190a88fa..4250d317c 100644 --- a/Assets/Scripts/UI/Menu/InventoryPanel.cs +++ b/Assets/Scripts/UI/Menu/InventoryPanel.cs @@ -260,9 +260,7 @@ public class InventoryPanel : MonoBehaviour, IPointerClickHandler } return; case AetheriaRuntimeInventoryDropdownSelectionKind.DockingBay: - if (TryResolveObservedDockingIndex(out var dockingIndex) && - dockingIndex.TryResolveCurrentDockingBayRow(out _) && - dockingIndex.TryResolveCurrentDockingBay(out var dockingBay)) + if (TryResolveCurrentDockingBayFacade(out var dockingBay)) { Display(dockingBay); } @@ -1133,29 +1131,53 @@ private void Update() private bool TryResolveCurrentEntityKey(out string currentEntityKey) { currentEntityKey = ""; - return TryResolveObservedDockingIndex(out var dockingIndex) && - dockingIndex.TryResolveCurrentEntityKey(out currentEntityKey); + var snapshot = ResolveDockingState(); + currentEntityKey = snapshot?.CurrentEntityKey ?? ""; + return !string.IsNullOrWhiteSpace(currentEntityKey); } private bool TryResolveCurrentDockingBayRow(out AetheriaRuntimeStationDockingBayRow dockingBay) { dockingBay = null; - return TryResolveObservedDockingIndex(out var dockingIndex) && - dockingIndex.TryResolveCurrentDockingBayRow(out dockingBay); + var stationRefit = ResolveStationRefit(); + dockingBay = stationRefit?.IsDocked == true && stationRefit.DockingBayIndex >= 0 + ? (stationRefit.DockingBays ?? Array.Empty()) + .FirstOrDefault(row => row != null && row.DockingBayIndex == stationRefit.DockingBayIndex) + : null; + return dockingBay != null; } - private bool TryResolveCurrentDocking(out AetheriaRuntimeCurrentDockingDocument docking) + private bool TryResolveCurrentDockingBayFacade(out EquippedCargoBay dockingBay) { - docking = null; - return TryResolveObservedDockingIndex(out var dockingIndex) && - dockingIndex.TryResolveCurrentDocking(out docking); + dockingBay = null; + var docking = ResolveDockingState()?.CurrentDocking; + return _observedFacadeIndex != null && + docking?.IsDocked == true && + _observedFacadeIndex.TryResolveDockingBayByRecordKey( + docking.DockParentEntityKey, + docking.DockingBayIndex, + out dockingBay); } private AetheriaRuntimeStationRefitDocument ResolveStationRefit() { - return TryResolveObservedDockingIndex(out var dockingIndex) - ? dockingIndex.ResolveStationRefit() - : null; + return ResolveDockingState()?.StationRefit; + } + + private AetheriaClientDockingSnapshot ResolveDockingState() + { + try + { + return ResolveClient() + .Aetheria() + .DockingState + .Latest(); + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to read Aetheria docking state for inventory panel: {ex.Message}"); + return null; + } } private bool TryResolveObservedDockingIndex(out AetheriaUnityObservedDockingIndex dockingIndex) diff --git a/Assets/Scripts/UI/Menu/LocalMenu.cs b/Assets/Scripts/UI/Menu/LocalMenu.cs index 0084cc9b2..d2cc5f06a 100644 --- a/Assets/Scripts/UI/Menu/LocalMenu.cs +++ b/Assets/Scripts/UI/Menu/LocalMenu.cs @@ -205,38 +205,36 @@ public class LocalMenu : MonoBehaviour return "Local"; } - private bool TryResolveCurrentDocking(out AetheriaRuntimeCurrentDockingDocument docking) + private bool TryResolveDockingState(out AetheriaClientDockingSnapshot dockingState) { - docking = null; + dockingState = null; try { - docking = ResolveClient() + dockingState = ResolveClient() .Aetheria() - .Current - .Docking - .LatestAsync() - .GetAwaiter() - .GetResult(); + .DockingState + .Latest(); } catch (Exception ex) { - Debug.LogWarning($"Failed to read Aetheria current docking for local story surface: {ex.Message}"); + Debug.LogWarning($"Failed to read Aetheria docking state for local story surface: {ex.Message}"); return false; } - return docking != null; + return dockingState != null; } private bool TryResolveDockedLocalStory(out LocationStory story) { story = null; if (_observedFacadeIndex == null || - !TryResolveCurrentDocking(out var docking) || - !docking.IsDocked || + !TryResolveDockingState(out var dockingState) || + dockingState?.IsDocked != true || + dockingState.CurrentDocking == null || !_observedFacadeIndex.TryResolveDockingBayByRecordKey( - docking.DockParentEntityKey, - docking.DockingBayIndex, + dockingState.CurrentDocking.DockParentEntityKey, + dockingState.CurrentDocking.DockingBayIndex, out var dockingBay) || dockingBay?.Entity is not OrbitalEntity { Story: { } dockedStory }) { diff --git a/Assets/Scripts/UI/Menu/MapRenderer.cs b/Assets/Scripts/UI/Menu/MapRenderer.cs index 291ee4959..a841ca0dd 100644 --- a/Assets/Scripts/UI/Menu/MapRenderer.cs +++ b/Assets/Scripts/UI/Menu/MapRenderer.cs @@ -25,24 +25,27 @@ public class MapRenderer : MonoBehaviour public Camera InfluenceCamera; public Image OverlayDisplay; public Image GravityDisplay; + public Material GravityBackdropMaterial; public Image TintDisplay; public Image InfluenceDisplay; + public AetheriaRenderSplatLayerRenderer SplatLayerRenderer; + public AetheriaDaemonRenderAssetCatalog AssetCatalog; + public RectTransform RtsIconRoot; + public RawImage RtsIconPrototype; public float Scale; public float2 Position; public float IconSize = 1f/128; private RectTransform _rect; private RenderTexture _mapTexture; - private RenderTexture _gravityTexture; - private RenderTexture _tintTexture; - private RenderTexture _influenceTexture; private int2 _size; private bool _init; private AetheriaClient _client; private string _clientStatePath = ""; private AetheriaRuntimeObjectsViewportDocument _objectsViewport; - private AetheriaRuntimeGravityViewportDocument _gravityViewport; + private AetheriaRuntimeRenderSplatsViewportDocument _renderSplatsViewport; private float _nextViewportRefreshTime; + private readonly List _rtsIconPool = new List(); void Start() { @@ -52,10 +55,16 @@ public class MapRenderer : MonoBehaviour private void OnEnable() { _init = true; - MapOverlayCamera.gameObject.SetActive(true); - GravityCamera.gameObject.SetActive(true); - TintCamera.gameObject.SetActive(true); - InfluenceCamera.gameObject.SetActive(true); + SetCameraActive(MapOverlayCamera, false); + SetCameraActive(GravityCamera, false); + SetCameraActive(TintCamera, false); + SetCameraActive(InfluenceCamera, false); + if (OverlayDisplay != null) + OverlayDisplay.gameObject.SetActive(false); + if (GravityBackdropMaterial != null && GravityDisplay != null) + GravityDisplay.material = GravityBackdropMaterial; + EnsureRtsIconRoot(); + SetRtsIconsActive(true); RefreshViewportDocuments(force: true); // If hiding minimap asteroids, turn them back on for the map screen @@ -69,10 +78,13 @@ public class MapRenderer : MonoBehaviour { ReleaseTextures(); } - MapOverlayCamera.gameObject.SetActive(false); - GravityCamera.gameObject.SetActive(false); - TintCamera.gameObject.SetActive(false); - InfluenceCamera.gameObject.SetActive(false); + SetCameraActive(MapOverlayCamera, false); + SetCameraActive(GravityCamera, false); + SetCameraActive(TintCamera, false); + SetCameraActive(InfluenceCamera, false); + if (OverlayDisplay != null) + OverlayDisplay.gameObject.SetActive(false); + SetRtsIconsActive(false); // If hiding minimap asteroids, turn them back off when leaving the map screen if (!ResolveShowAsteroidsInMinimap()) @@ -88,12 +100,6 @@ public class MapRenderer : MonoBehaviour { _mapTexture.Release(); _mapTexture = null; - _gravityTexture.Release(); - _gravityTexture = null; - _tintTexture.Release(); - _tintTexture = null; - _influenceTexture.Release(); - _influenceTexture = null; } void LateUpdate() @@ -108,40 +114,12 @@ public class MapRenderer : MonoBehaviour ReleaseTextures(); } - _mapTexture = new RenderTexture(_size.x, _size.y, 0, RenderTextureFormat.Default); - MapOverlayCamera.targetTexture = _mapTexture; - OverlayDisplay.material.SetTexture("_DetailTex", _mapTexture); - - _gravityTexture = new RenderTexture(_size.x, _size.y, 0, RenderTextureFormat.RFloat); - GravityCamera.targetTexture = _gravityTexture; - GravityDisplay.material.SetTexture("_DetailTex", _gravityTexture); - - _tintTexture = new RenderTexture(_size.x / 2, _size.y / 2, 0, RenderTextureFormat.RGB111110Float); - TintCamera.targetTexture = _tintTexture; - TintDisplay.material.SetTexture("_DetailTex", _tintTexture); - - _influenceTexture = new RenderTexture(_size.x, _size.y, 0, RenderTextureFormat.RFloat); - InfluenceCamera.targetTexture = _influenceTexture; - InfluenceDisplay.material.SetTexture("_DetailTex", _influenceTexture); } - var pos = ((Vector2) Position).Flatland(1); - - MapOverlayCamera.transform.position = pos; - MapOverlayCamera.orthographicSize = _size.y * Scale * .5f; - - GravityCamera.transform.position = pos; - GravityCamera.orthographicSize = _size.y * Scale * .5f; GravityDisplay.material.SetFloat("_Scale", Scale / 2); - - TintCamera.transform.position = pos; - TintCamera.orthographicSize = _size.y * Scale * .5f; - - InfluenceCamera.transform.position = pos; - InfluenceCamera.orthographicSize = _size.y * Scale * .5f; - - ZoneRenderer.SetIconSize(IconSize * Scale); RefreshViewportDocuments(force: false); + RenderSplatLayers(); + RenderRtsIcons(); } private void RefreshViewportDocuments(bool force) @@ -158,8 +136,8 @@ public class MapRenderer : MonoBehaviour .ObjectsViewportAsync(viewport) .GetAwaiter() .GetResult(); - _gravityViewport = client - .GravityViewportAsync(viewport) + _renderSplatsViewport = client + .RenderSplatsViewportAsync(viewport) .GetAwaiter() .GetResult(); @@ -175,6 +153,153 @@ public class MapRenderer : MonoBehaviour } } + private void RenderSplatLayers() + { + if (_renderSplatsViewport == null) + return; + + if (SplatLayerRenderer == null) + SplatLayerRenderer = GetComponent() ?? + gameObject.AddComponent(); + + SplatLayerRenderer.Render(_renderSplatsViewport, _size.x, _size.y); + ApplyLayerTexture(AetheriaRuntimeRenderSplatLayerKeys.GravityHeight, GravityDisplay); + ApplyLayerTexture(AetheriaRuntimeRenderSplatLayerKeys.FogTint, TintDisplay); + ApplyLayerTexture(AetheriaRuntimeRenderSplatLayerKeys.Influence, InfluenceDisplay); + } + + private void ApplyLayerTexture(string layerKey, Image display) + { + if (display == null || + SplatLayerRenderer == null || + !SplatLayerRenderer.TryGetTexture(layerKey, out var texture) || + texture == null) + { + return; + } + + display.material.SetTexture("_DetailTex", texture); + } + + private static void SetCameraActive(Camera camera, bool active) + { + if (camera != null) + camera.gameObject.SetActive(active); + } + + private void RenderRtsIcons() + { + EnsureRtsIconRoot(); + if (RtsIconRoot == null) + return; + + var objects = _objectsViewport?.Objects ?? Array.Empty(); + for (var i = 0; i < objects.Count; i++) + { + var icon = ResolveRtsIcon(i); + ApplyRtsIcon(icon, objects[i]); + } + + for (var i = objects.Count; i < _rtsIconPool.Count; i++) + if (_rtsIconPool[i] != null) + _rtsIconPool[i].gameObject.SetActive(false); + } + + private RawImage ResolveRtsIcon(int index) + { + while (_rtsIconPool.Count <= index) + { + RawImage icon; + if (RtsIconPrototype != null) + { + icon = Instantiate(RtsIconPrototype, RtsIconRoot); + } + else + { + var go = new GameObject("RTS Map Icon", typeof(RectTransform), typeof(CanvasRenderer), typeof(RawImage)); + go.transform.SetParent(RtsIconRoot, false); + icon = go.GetComponent(); + icon.texture = Texture2D.whiteTexture; + icon.raycastTarget = false; + } + + _rtsIconPool.Add(icon); + } + + return _rtsIconPool[index]; + } + + private void ApplyRtsIcon(RawImage icon, AetheriaRuntimeRtsViewportObject obj) + { + if (icon == null || obj == null) + return; + + icon.gameObject.SetActive(true); + if (AssetCatalog != null && AssetCatalog.TryResolveTexture(obj.IconAsset, out var texture)) + icon.texture = texture; + icon.color = ResolveIconColor(obj); + var rectTransform = icon.rectTransform; + var pixelSize = Mathf.Max(4f, IconSize * (obj.Controlled ? 1.25f : 1f)); + rectTransform.anchorMin = rectTransform.anchorMax = new Vector2(0.5f, 0.5f); + rectTransform.pivot = new Vector2(0.5f, 0.5f); + rectTransform.sizeDelta = new Vector2(pixelSize, pixelSize); + rectTransform.anchoredPosition = WorldToMapPosition(obj.X, obj.Y); + rectTransform.localRotation = Quaternion.Euler( + 0, + 0, + Mathf.Atan2((float)obj.DirectionY, (float)obj.DirectionX) * Mathf.Rad2Deg - 90f); + } + + private Vector2 WorldToMapPosition(double worldX, double worldY) + { + var viewport = _objectsViewport?.Viewport ?? ResolveViewportBounds(); + var minX = Math.Min(viewport.MinX, viewport.MaxX); + var maxX = Math.Max(viewport.MinX, viewport.MaxX); + var minY = Math.Min(viewport.MinY, viewport.MaxY); + var maxY = Math.Max(viewport.MinY, viewport.MaxY); + var width = Math.Max(0.0001, maxX - minX); + var height = Math.Max(0.0001, maxY - minY); + var rootRect = RtsIconRoot != null ? RtsIconRoot.rect : new Rect(0, 0, _size.x, _size.y); + var u = (float)((worldX - minX) / width); + var v = (float)((worldY - minY) / height); + return new Vector2( + (u - 0.5f) * rootRect.width, + (v - 0.5f) * rootRect.height); + } + + private static Color ResolveIconColor(AetheriaRuntimeRtsViewportObject obj) + { + var color = obj.Controlled + ? new Color(0.3f, 0.9f, 1.0f, 1.0f) + : Color.HSVToRGB(frac((obj.FactionKey ?? obj.Kind ?? "").GetHashCode() * 0.0137f), 0.55f, 0.95f); + color.a = Mathf.Clamp01(Mathf.Max(0.25f, (float)obj.Visibility)); + return color; + } + + private void EnsureRtsIconRoot() + { + if (RtsIconRoot != null) + return; + + var parent = GravityDisplay != null + ? GravityDisplay.transform.parent + : transform; + var go = new GameObject("RTS Command Icons", typeof(RectTransform)); + go.transform.SetParent(parent, false); + RtsIconRoot = go.GetComponent(); + RtsIconRoot.anchorMin = Vector2.zero; + RtsIconRoot.anchorMax = Vector2.one; + RtsIconRoot.offsetMin = Vector2.zero; + RtsIconRoot.offsetMax = Vector2.zero; + RtsIconRoot.SetAsLastSibling(); + } + + private void SetRtsIconsActive(bool active) + { + if (RtsIconRoot != null) + RtsIconRoot.gameObject.SetActive(active); + } + private AetheriaRuntimeRtsViewportBounds ResolveViewportBounds() { var screenHeight = _size.y <= 0 ? Math.Max(1, Screen.height) : _size.y; diff --git a/Assets/Scripts/UI/Menu/MenuPanel.cs b/Assets/Scripts/UI/Menu/MenuPanel.cs index 5ceb390cd..d935a3ca2 100644 --- a/Assets/Scripts/UI/Menu/MenuPanel.cs +++ b/Assets/Scripts/UI/Menu/MenuPanel.cs @@ -169,17 +169,14 @@ public class MenuPanel : MonoBehaviour .ToArray(); } - private AetheriaRuntimeCurrentDockingDocument ResolveCurrentDocking() + private AetheriaClientDockingSnapshot ResolveCurrentDocking() { try { return ResolveClient() .Aetheria() - .Current - .Docking - .LatestAsync() - .GetAwaiter() - .GetResult(); + .DockingState + .Latest(); } catch (Exception ex) { diff --git a/Assets/Scripts/UI/Menu/SectorMap.cs b/Assets/Scripts/UI/Menu/SectorMap.cs index bd5b7ac19..d6a35781f 100644 --- a/Assets/Scripts/UI/Menu/SectorMap.cs +++ b/Assets/Scripts/UI/Menu/SectorMap.cs @@ -15,6 +15,7 @@ using float2 = Unity.Mathematics.float2; public class SectorMap : MonoBehaviour { public Camera InfluenceCamera; + public AetheriaRenderSplatRasterizer InfluenceSplatRasterizer; public Prototype InfluenceRendererPrototype; public MeshRenderer SectorRenderer; public Prototype ZonePrototype; @@ -216,6 +217,7 @@ public class SectorMap : MonoBehaviour public void Start() { EnsureSectorMapLoaded(); + DisableLegacyInfluenceCamera(); } private void EnsureSectorMapLoaded() @@ -341,33 +343,162 @@ public class SectorMap : MonoBehaviour private void RenderInfluence() { + DisableLegacyInfluenceCamera(); foreach (var pair in _factionMaterials) { - foreach (var zone in _sectorMap?.Zones ?? Array.Empty()) - { - if (!_zoneInstances.TryGetValue(zone.ZoneIndex, out var instance)) - continue; - - var influence = 0f; - if (zone.FactionIndices?.Count > 0) - { - if (zone.FactionIndices.Contains(pair.Key)) - { - influence = 10; - if (zone.OwnerFactionIndex != pair.Key) - influence *= .5f; - } - else influence = -10; - } - - instance.Influence.material.SetFloat("_Depth", influence); - } - - InfluenceCamera.targetTexture = pair.Value.influence; - InfluenceCamera.Render(); + var document = BuildInfluenceSplatDocument(pair.Key); + ResolveInfluenceSplatRasterizer().RenderLayerToTarget( + document, + pair.Value.influence, + 0, + 0, + Color.clear); } } + private AetheriaRenderSplatRasterizer ResolveInfluenceSplatRasterizer() + { + if (InfluenceSplatRasterizer != null) + return InfluenceSplatRasterizer; + + InfluenceSplatRasterizer = GetComponent(); + if (InfluenceSplatRasterizer == null) + InfluenceSplatRasterizer = gameObject.AddComponent(); + return InfluenceSplatRasterizer; + } + + private AetheriaRuntimeRenderSplatsViewportDocument BuildInfluenceSplatDocument(int factionIndex) + { + var zones = (_sectorMap?.Zones ?? Array.Empty()).ToArray(); + var minX = zones.Length > 0 ? zones.Min(zone => zone.X) : -1; + var maxX = zones.Length > 0 ? zones.Max(zone => zone.X) : 1; + var minY = zones.Length > 0 ? zones.Min(zone => zone.Y) : -1; + var maxY = zones.Length > 0 ? zones.Max(zone => zone.Y) : 1; + var padding = Math.Max(IconBackgroundSize * 2, 4); + + var centerX = new List(); + var centerY = new List(); + var halfExtentX = new List(); + var halfExtentY = new List(); + var rotationCos = new List(); + var rotationSin = new List(); + var channel = new List(); + var falloff = new List(); + var valueR = new List(); + var valueG = new List(); + var valueB = new List(); + var valueA = new List(); + var sourceKey = new List(); + var layerIndex = new List(); + var sourceKind = new List(); + var frequencyX = new List(); + var frequencyY = new List(); + var phaseX = new List(); + var phaseY = new List(); + var animationSpeed = new List(); + var sourceFlags = new List(); + + foreach (var zone in zones) + { + if (!_zoneInstances.ContainsKey(zone.ZoneIndex)) + continue; + + var influence = ResolveZoneInfluence(zone, factionIndex); + if (Math.Abs(influence) <= 0.0001f) + continue; + + centerX.Add(zone.X); + centerY.Add(zone.Y); + halfExtentX.Add(Math.Max(IconBackgroundSize, 1)); + halfExtentY.Add(Math.Max(IconBackgroundSize, 1)); + rotationCos.Add(1); + rotationSin.Add(0); + channel.Add(AetheriaRuntimeRenderSplatChannels.Influence); + falloff.Add(AetheriaRuntimeRenderSplatFalloffs.Smooth); + valueR.Add(influence); + valueG.Add(influence); + valueB.Add(influence); + valueA.Add(1); + sourceKey.Add($"sector-zone:{zone.ZoneIndex}:faction:{factionIndex}"); + layerIndex.Add(0); + sourceKind.Add(AetheriaRuntimeRenderSplatSourceKinds.Constant); + frequencyX.Add(0); + frequencyY.Add(0); + phaseX.Add(0); + phaseY.Add(0); + animationSpeed.Add(0); + sourceFlags.Add(0); + } + + return new AetheriaRuntimeRenderSplatsViewportDocument + { + Schema = AetheriaRuntimeDaemonSchemas.RenderSplatsViewport, + Viewport = new AetheriaRuntimeRtsViewportBounds + { + MinX = minX - padding, + MinY = minY - padding, + MaxX = maxX + padding, + MaxY = maxY + padding + }, + Layers = new[] + { + new AetheriaRuntimeRenderSplatLayerDefinition + { + LayerKey = AetheriaRuntimeRenderSplatLayerKeys.Influence, + DisplayName = "Sector Influence", + Channel = AetheriaRuntimeRenderSplatChannels.Influence, + BlendMode = AetheriaRuntimeRenderSplatBlendModes.Add, + GraphicsFormat = "R16_SFloat" + } + }, + Splats = new AetheriaRuntimeRenderSplatSoa + { + Count = centerX.Count, + CenterX = centerX, + CenterY = centerY, + HalfExtentX = halfExtentX, + HalfExtentY = halfExtentY, + RotationCos = rotationCos, + RotationSin = rotationSin, + Channel = channel, + Falloff = falloff, + ValueR = valueR, + ValueG = valueG, + ValueB = valueB, + ValueA = valueA, + SourceKey = sourceKey, + LayerIndex = layerIndex, + SourceKind = sourceKind, + FrequencyX = frequencyX, + FrequencyY = frequencyY, + PhaseX = phaseX, + PhaseY = phaseY, + AnimationSpeed = animationSpeed, + SourceFlags = sourceFlags + } + }; + } + + private static float ResolveZoneInfluence(AetheriaRuntimeSectorMapZone zone, int factionIndex) + { + if (zone.FactionIndices?.Count <= 0) + return 0f; + + if (!zone.FactionIndices.Contains(factionIndex)) + return -10f; + + return zone.OwnerFactionIndex == factionIndex ? 10f : 5f; + } + + private void DisableLegacyInfluenceCamera() + { + if (InfluenceCamera == null) + return; + + InfluenceCamera.targetTexture = null; + InfluenceCamera.gameObject.SetActive(false); + } + private AetheriaClient ResolveClient() { var gameDataDirectory = new DirectoryInfo(Path.Combine(Application.dataPath, "..", "GameData")); diff --git a/Assets/Scripts/UI/Menu/TradeMenu.cs b/Assets/Scripts/UI/Menu/TradeMenu.cs index 4a8d425d8..826f2b2b0 100644 --- a/Assets/Scripts/UI/Menu/TradeMenu.cs +++ b/Assets/Scripts/UI/Menu/TradeMenu.cs @@ -47,6 +47,7 @@ public class TradeMenu : MonoBehaviour private AetheriaRuntimeTradeCargoSelectorSurfaceProjection _cargoSelectorSurfaceProjection; private AetheriaRuntimeStationCargoTargetRow[] _cargoSelectorStationRefitTargets = Array.Empty(); + private AetheriaClientDockingSnapshot _dockingState; private AetheriaRuntimeStationRefitDocument _stationRefit; private AetheriaRuntimeTradeFilterSurfaceProjection _filterSurfaceProjection; private Action[] _rowActionCallbacks = Array.Empty(); @@ -56,8 +57,10 @@ public class TradeMenu : MonoBehaviour private void OnEnable() { - if (!TryResolveCurrentDocking(out var docking) || - !docking.IsDocked || + var dockingState = ResolveDockingState(); + var docking = dockingState?.CurrentDocking; + if (dockingState?.IsDocked != true || + docking == null || string.IsNullOrWhiteSpace(docking.DockParentEntityKey) || docking.DockingBayIndex < 0) { @@ -514,7 +517,8 @@ public class TradeMenu : MonoBehaviour private bool TryResolveCurrentDockingTargetEntityKey(out string targetEntityKey) { targetEntityKey = ""; - if (!TryResolveCurrentDocking(out var docking) || + var docking = ResolveDockingState()?.CurrentDocking; + if (docking == null || !docking.IsDocked || string.IsNullOrWhiteSpace(docking.DockParentEntityKey)) { @@ -525,54 +529,40 @@ public class TradeMenu : MonoBehaviour return true; } - private bool TryResolveCurrentDocking(out AetheriaRuntimeCurrentDockingDocument docking) - { - docking = null; - - try - { - docking = ResolveClient() - .Aetheria() - .Current - .Docking - .LatestAsync() - .GetAwaiter() - .GetResult(); - } - catch (Exception ex) - { - Debug.LogWarning($"Failed to read Aetheria current docking for trade menu: {ex.Message}"); - return false; - } - - return docking != null; - } - private AetheriaRuntimeStationRefitDocument ResolveStationRefit() { if (_stationRefit != null) return _stationRefit; + _stationRefit = ResolveDockingState()?.StationRefit; + return _stationRefit; + } + + private AetheriaClientDockingSnapshot ResolveDockingState() + { + if (_dockingState != null) + return _dockingState; + try { - _stationRefit = ResolveClient() + _dockingState = ResolveClient() .Aetheria() - .StationRefit - .LatestAsync() - .GetAwaiter() - .GetResult(); + .DockingState + .Latest(); + _stationRefit = _dockingState?.StationRefit; } catch (Exception ex) { - Debug.LogWarning($"Failed to read Aetheria station refit projection for trade menu: {ex.Message}"); + Debug.LogWarning($"Failed to read Aetheria docking state for trade menu: {ex.Message}"); return null; } - return _stationRefit; + return _dockingState; } private void InvalidateStationRefit() { + _dockingState = null; _stationRefit = null; } @@ -649,6 +639,7 @@ public class TradeMenu : MonoBehaviour _clientStatePath = ""; _catalog = null; _playerSettings = null; + _dockingState = null; _stationRefit = null; } diff --git a/Assets/Scripts/Zone Display/MapView.cs b/Assets/Scripts/Zone Display/MapView.cs index a2e51c246..eb4d1aad2 100644 --- a/Assets/Scripts/Zone Display/MapView.cs +++ b/Assets/Scripts/Zone Display/MapView.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using GameCult.Aetheria.State.Verse; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.Serialization; @@ -15,6 +16,7 @@ public class MapView : MonoBehaviour public Camera MinimapGravity; public Camera Minimap; public MeshRenderer GravityRenderer; + public AetheriaUnityRenderSplatViewportSource GravitySplatSource; public float ZoomSpeed; [FormerlySerializedAs("SectorRenderer")] public ZoneRenderer ZoneRenderer; @@ -26,7 +28,6 @@ public class MapView : MonoBehaviour private RenderTexture _minimapGravityTexture; private Transform _gravityRendererTransform; private CopyTransform _gravityRendererCopyTransform; - private RenderTexture _fullscreenGravityTexture; private AetheriaInput.UIActions _input; private float _viewDistance = 1024; private float _aspectRatio; @@ -49,9 +50,14 @@ public class MapView : MonoBehaviour _gravityRendererCopyTransform.enabled = false; _previousGravityRendererScale = _gravityRendererTransform.localScale; - _fullscreenGravityTexture = new RenderTexture(Screen.width / 2, Screen.height / 2, 1, RenderTextureFormat.RFloat); - GravityRenderer.material.mainTexture = _fullscreenGravityTexture; - MinimapGravity.targetTexture = _fullscreenGravityTexture; + GravitySplatSource = ResolveGravitySplatSource(); + GravitySplatSource.SetLayerKey(AetheriaRuntimeRenderSplatLayerKeys.GravityHeight); + GravitySplatSource.SetViewport( + new Vector2(_minimapTransform.position.x, _minimapTransform.position.z), + new Vector2(_viewDistance * _aspectRatio * 2, _viewDistance * 2)); + GravitySplatSource.RenderLatest(); + GravityRenderer.material.mainTexture = GravitySplatSource.TargetTexture; + MinimapGravity.targetTexture = null; Minimap.targetTexture = null; SetZoom(); } @@ -61,6 +67,14 @@ public class MapView : MonoBehaviour //ZoneRenderer.ViewDistance = _viewDistance * _aspectRatio; Minimap.orthographicSize = MinimapGravity.orthographicSize = _viewDistance; _gravityRendererTransform.localScale = new Vector3(_viewDistance*_aspectRatio*2, _viewDistance*2, 1); + if (GravitySplatSource != null && _minimapTransform != null) + { + GravitySplatSource.SetViewport( + new Vector2(_minimapTransform.position.x, _minimapTransform.position.z), + new Vector2(_viewDistance * _aspectRatio * 2, _viewDistance * 2)); + GravitySplatSource.RenderLatest(); + GravityRenderer.material.mainTexture = GravitySplatSource.TargetTexture; + } } // public void BindInput(AetheriaInput.UIActions input) @@ -91,6 +105,7 @@ public class MapView : MonoBehaviour _gravityRendererTransform.position = gravPos; Main.transform.position = _minimapTransform.position = _minimapGravityTransform.position = pos; + SetZoom(); } } @@ -106,6 +121,20 @@ public class MapView : MonoBehaviour Minimap.targetTexture = _minimapTexture; MinimapGravity.targetTexture = _minimapGravityTexture; GravityRenderer.material.mainTexture = _minimapGravityTexture; - _fullscreenGravityTexture.Release(); + } + + private AetheriaUnityRenderSplatViewportSource ResolveGravitySplatSource() + { + if (GravitySplatSource != null) + return GravitySplatSource; + + GravitySplatSource = GetComponent(); + if (GravitySplatSource == null) + GravitySplatSource = gameObject.AddComponent(); + + if (GetComponent() == null) + gameObject.AddComponent(); + + return GravitySplatSource; } } diff --git a/Assets/Scripts/Zone Display/VolumeSampling.cs b/Assets/Scripts/Zone Display/VolumeSampling.cs index ef5c025c4..6f39324b5 100644 --- a/Assets/Scripts/Zone Display/VolumeSampling.cs +++ b/Assets/Scripts/Zone Display/VolumeSampling.cs @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ using System; +using GameCult.Aetheria.State.Verse; using UnityEngine; using Unity.Mathematics; using UnityEngine.Experimental.Rendering; @@ -26,6 +27,8 @@ public class VolumeSampling : MonoBehaviour public RenderTexture NebulaPatchHeight; public RenderTexture NebulaPatch; public RenderTexture NebulaTint; + public AetheriaUnityRenderSplatViewportSource SplatViewportSource; + public AetheriaRenderSplatLayerRenderer SplatLayerRenderer; private MeshRenderer _gridMeshRenderer; private float _flowScroll; @@ -42,6 +45,8 @@ public class VolumeSampling : MonoBehaviour if (GridMesh != null) _gridMeshRenderer = GridMesh.GetComponent(); + + ResolveSplatRenderers(); } void Update() @@ -52,6 +57,8 @@ public class VolumeSampling : MonoBehaviour if(GridCamera != null) Shader.SetGlobalVector("_GridTransform", new Vector4(_gridTransform.position.x,_gridTransform.position.z,GridCamera.orthographicSize*2)); + RenderSplatTextures(); + var environment = Settings.DefaultEnvironment; if (GridMesh != null) @@ -106,4 +113,48 @@ public class VolumeSampling : MonoBehaviour Shader.SetKeyword(_keywordFlowSlope, environment.Flow.SlopeAmplitude != 0 || environment.Flow.SwirlAmplitude != 0); Shader.SetKeyword(_keywordNoiseSlope, environment.Noise.SlopeExponent != 0); } + + private void RenderSplatTextures() + { + ResolveSplatRenderers(); + if (SplatViewportSource == null) + return; + + var size = GridCamera != null + ? new Vector2(GridCamera.orthographicSize * 2, GridCamera.orthographicSize * 2) + : new Vector2(1024, 1024); + var center = _gridTransform != null + ? new Vector2(_gridTransform.position.x, _gridTransform.position.z) + : Vector2.zero; + SplatViewportSource.SetViewport(center, size); + SplatViewportSource.RenderLatest(); + + if (SplatLayerRenderer == null) + return; + + NebulaSurfaceHeight = ResolveLayerTexture(AetheriaRuntimeRenderSplatLayerKeys.FogSurfaceHeight, NebulaSurfaceHeight); + NebulaPatchHeight = ResolveLayerTexture(AetheriaRuntimeRenderSplatLayerKeys.FogPatchHeight, NebulaPatchHeight); + NebulaPatch = ResolveLayerTexture(AetheriaRuntimeRenderSplatLayerKeys.FogPatch, NebulaPatch); + NebulaTint = ResolveLayerTexture(AetheriaRuntimeRenderSplatLayerKeys.FogTint, NebulaTint); + } + + private RenderTexture ResolveLayerTexture(string layerKey, RenderTexture fallback) + { + return SplatLayerRenderer.TryGetTexture(layerKey, out var texture) && texture != null + ? texture + : fallback; + } + + private void ResolveSplatRenderers() + { + if (SplatLayerRenderer == null) + SplatLayerRenderer = GetComponent(); + if (SplatLayerRenderer == null) + SplatLayerRenderer = gameObject.AddComponent(); + + if (SplatViewportSource == null) + SplatViewportSource = GetComponent(); + if (SplatViewportSource == null) + SplatViewportSource = gameObject.AddComponent(); + } } diff --git a/Assets/Scripts/Zone Display/ZoneRenderer.cs b/Assets/Scripts/Zone Display/ZoneRenderer.cs index cc12bcc2c..c9d3e0362 100644 --- a/Assets/Scripts/Zone Display/ZoneRenderer.cs +++ b/Assets/Scripts/Zone Display/ZoneRenderer.cs @@ -30,6 +30,7 @@ public class ZoneRenderer : MonoBehaviour public CinemachineVirtualCamera SceneCamera; public Camera[] FogCameras; public Camera[] MinimapCameras; + public bool UseRenderSplatTextureLayers = true; public Material FogMaterial; public float FogFarFadeFraction = .125f; public float FarPlaneDistanceMultiplier = 2; @@ -115,7 +116,8 @@ public class ZoneRenderer : MonoBehaviour Array.Empty(); private IReadOnlyList _zoneRenderAsteroidBeltPoses = Array.Empty(); - private AetheriaRuntimeGravityViewportDocument _daemonGravityViewport; + private IReadOnlyList _zoneRenderBodies = + Array.Empty(); private AetheriaClient _client; private string _clientStatePath = ""; private AetheriaRuntimeCatalogSnapshot _catalog; @@ -146,11 +148,16 @@ public class ZoneRenderer : MonoBehaviour set { _viewDistance = value; - foreach (var camera in FogCameras) - camera.orthographicSize = value; - SceneCamera.m_Lens.FarClipPlane = value * FarPlaneDistanceMultiplier; - FogMaterial.SetFloat("_DepthCeiling", value); - FogMaterial.SetFloat("_DepthBlend", FogFarFadeFraction * value); + foreach (var camera in FogCameras ?? Array.Empty()) + if (camera != null) + camera.orthographicSize = value; + if (SceneCamera != null) + SceneCamera.m_Lens.FarClipPlane = value * FarPlaneDistanceMultiplier; + if (FogMaterial != null) + { + FogMaterial.SetFloat("_DepthCeiling", value); + FogMaterial.SetFloat("_DepthBlend", FogFarFadeFraction * value); + } } } @@ -159,9 +166,11 @@ public class ZoneRenderer : MonoBehaviour set { _minimapDistance = value; - foreach (var camera in MinimapCameras) - camera.orthographicSize = value; - SetIconSize((float)RenderSettings.ResolveMinimapIconSize(value)); + foreach (var camera in MinimapCameras ?? Array.Empty()) + if (camera != null) + camera.orthographicSize = value; + if (RenderSettings != null) + SetIconSize((float)RenderSettings.ResolveMinimapIconSize(value)); // MinimapGravityQuad.transform.localScale = value * 2 * Vector3.one; } } @@ -235,6 +244,7 @@ public class ZoneRenderer : MonoBehaviour _tourTimer = TourSwitchTime; _transposer = SceneCamera.GetCinemachineComponent(); + DisableLegacyTextureCameras(); } public void LoadDaemonZoneView( @@ -244,9 +254,6 @@ public class ZoneRenderer : MonoBehaviour ClearZone(); _observedEntityFacadesByDaemonIndex = observedEntityFacadesByDaemonIndex; ApplyZoneRender(render); - RefreshDaemonBodyPoses(); - RefreshDaemonAsteroidBeltPoses(); - SyncDaemonBodyViews(); RefreshDaemonVisibleEntityInstances(); SyncDaemonEntityInstances(); @@ -260,11 +267,17 @@ public class ZoneRenderer : MonoBehaviour _daemonSimulationTimeSeconds = render?.SimulationTimeSeconds ?? 0; _zoneRenderBodyPoses = render?.BodyPoses ?? Array.Empty(); _zoneRenderAsteroidBeltPoses = render?.AsteroidBeltPoses ?? Array.Empty(); + _zoneRenderBodies = render?.Bodies ?? Array.Empty(); RefreshDaemonContactRows(); var zoneRenderRadius = (float)Math.Max(0, render?.ZoneRenderRadius ?? 2000); SectorBrushes.localScale = zoneRenderRadius * 2 * Vector3.one; - SlimeGravityCamera.orthographicSize = zoneRenderRadius; + if (SlimeGravityCamera != null) + SlimeGravityCamera.orthographicSize = zoneRenderRadius; SlimeRenderer.ZoneRadius = zoneRenderRadius; + DisableLegacyTextureCameras(); + RefreshDaemonBodyPoses(); + RefreshDaemonAsteroidBeltPoses(); + SyncDaemonBodyViews(); _daemonWormholeExits.Clear(); foreach (var exit in render?.WormholeExits ?? Array.Empty()) { @@ -673,30 +686,23 @@ public class ZoneRenderer : MonoBehaviour private void SyncDaemonBodyViews() { - try - { - var viewport = ToViewportBounds(ResolveDaemonRenderViewport()); - var gravity = ResolveClient() - .GravityViewportAsync(viewport) - .GetAwaiter() - .GetResult(); - _daemonGravityViewport = gravity; - } - catch (Exception ex) - { - Debug.LogWarning($"Failed to read Aetheria gravity viewport for zone renderer terrain: {ex.Message}"); - } - _daemonBodyViews.Clear(); - foreach (var bodyView in _daemonGravityViewport?.Bodies ?? Array.Empty()) + var bodySnapshotsByBodyKey = _zoneRenderBodies + .Where(body => body != null && !string.IsNullOrWhiteSpace(body.BodyKey)) + .ToDictionary(body => body.BodyKey, StringComparer.Ordinal); + foreach (var bodyPose in _zoneRenderBodyPoses ?? Array.Empty()) { - if (bodyView?.Body == null || string.IsNullOrWhiteSpace(bodyView.BodyKey)) + if (bodyPose == null || + string.IsNullOrWhiteSpace(bodyPose.BodyKey) || + !bodySnapshotsByBodyKey.TryGetValue(bodyPose.BodyKey, out var body)) + { continue; + } _daemonBodyViews.Add(new AetheriaRuntimeDaemonBodyView( - bodyView.Body, - ToDaemonBodyPose(bodyView), - bodyView.IsAsteroidBelt)); + body, + ToDaemonBodyPose(bodyPose), + IsAsteroidBelt(body))); } var beltPosesByBodyKey = _daemonAsteroidBeltPoses .Where(pose => !string.IsNullOrWhiteSpace(pose.BodyKey)) @@ -739,6 +745,19 @@ public class ZoneRenderer : MonoBehaviour _suns = _bodyViewsByBodyKey.Values.Where(p => p is SunObject).ToArray(); } + private void DisableLegacyTextureCameras() + { + if (!UseRenderSplatTextureLayers) + return; + + foreach (var camera in FogCameras ?? Array.Empty()) + if (camera != null) + camera.gameObject.SetActive(false); + + if (SlimeGravityCamera != null) + SlimeGravityCamera.gameObject.SetActive(false); + } + private AetheriaRuntimeXzRect ResolveDaemonRenderViewport() { var center = PerspectiveEntity?.CultPositionXZ ?? default; @@ -960,42 +979,31 @@ public class ZoneRenderer : MonoBehaviour private double EvaluateGravityTerrainHeight(double positionX, double positionZ) { - var gravity = _daemonGravityViewport; - if (gravity == null) - return 0; - var height = 0.0; - if (gravity.TerrainRadius > 0 && gravity.TerrainDepth != 0) + foreach (var body in _zoneRenderBodies ?? Array.Empty()) { - var distance = Math.Sqrt(positionX * positionX + positionZ * positionZ); - height -= PowerPulse( - distance / (gravity.TerrainRadius * 2.0), - Math.Max(0.0001, gravity.TerrainDepthExponent)) * gravity.TerrainDepth; - } - - foreach (var brush in gravity.GravityInfluences ?? Array.Empty()) - { - if (brush == null) + if (body == null) continue; - var dx = positionX - brush.X; - var dz = positionZ - brush.Y; + var dx = positionX - body.GravityInfluenceCenterX; + var dz = positionZ - body.GravityInfluenceCenterZ; var distance = Math.Sqrt(dx * dx + dz * dz); - if (brush.Radius > 0 && distance < brush.Radius && brush.GravityDepth != 0) + var radius = ResolveGravityRadius(body); + if (radius > 0 && distance < radius && body.GravityWellDepth != 0) { height -= PowerPulse( - distance / brush.Radius, - Math.Max(0.0001, brush.GravityDepthExponent)) * brush.GravityDepth; + distance / radius, + Math.Max(0.0001, body.GravityDepthExponent)) * body.GravityWellDepth; } - if (brush.WaveRadius > 0 && distance < brush.WaveRadius && brush.WaveDepth != 0) + if (body.GravityWaveRadius > 0 && distance < body.GravityWaveRadius && body.GravityWaveDepth != 0) { height -= RadialWaves( - distance / brush.WaveRadius, + distance / body.GravityWaveRadius, 8.0, 1.25, - gravity.TerrainWaveFrequency, - DaemonSimulationTimeSeconds * brush.WaveSpeed) * brush.WaveDepth; + 1.0, + DaemonSimulationTimeSeconds * body.GravityWaveSpeed) * body.GravityWaveDepth; } } @@ -1006,33 +1014,45 @@ public class ZoneRenderer : MonoBehaviour double minimapGravityRange, double maxDepth) { - var gravity = _daemonGravityViewport; - if (gravity == null) - return new AetheriaRuntimeGravityTerrainBand(0, Math.Max(0, maxDepth)); - - var startDepth = PowerPulse( - minimapGravityRange, - Math.Max(0.0001, gravity.TerrainDepthExponent)) * gravity.TerrainDepth; + var terrainDepth = (_zoneRenderBodies ?? Array.Empty()) + .Where(body => body != null) + .Select(body => Math.Max(0, body.GravityWellDepth)) + .DefaultIfEmpty(0) + .Max(); + var startDepth = Math.Min(terrainDepth, Math.Max(0, minimapGravityRange)); return new AetheriaRuntimeGravityTerrainBand( startDepth, - gravity.TerrainDepth - startDepth + maxDepth); + Math.Max(0, terrainDepth - startDepth) + maxDepth); } - private static AetheriaRuntimeDaemonBodyPose ToDaemonBodyPose(AetheriaRuntimeRtsBodyView bodyView) + private static AetheriaRuntimeDaemonBodyPose ToDaemonBodyPose(AetheriaRuntimeZoneRenderBodyPose bodyPose) { - if (bodyView == null) + if (bodyPose == null) return default; return new AetheriaRuntimeDaemonBodyPose( - bodyView.BodyKey, - bodyView.OrbitKey, - "", - bodyView.Kind, - bodyView.X, - bodyView.Y, - 0, - 0, - bodyView.Body?.GravityWaveSpeed ?? 0); + bodyPose.BodyKey, + bodyPose.OrbitKey, + bodyPose.ParentOrbitKey, + bodyPose.Kind, + bodyPose.CenterX, + bodyPose.CenterZ, + bodyPose.ParentCenterX, + bodyPose.ParentCenterZ, + bodyPose.GravityWaveSpeed); + } + + private static bool IsAsteroidBelt(AetheriaRuntimeBodySnapshotCommit body) + { + return (body?.Kind ?? "").IndexOf("asteroid", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static double ResolveGravityRadius(AetheriaRuntimeBodySnapshotCommit body) + { + if (body != null && double.IsFinite(body.GravityInfluenceRadius) && body.GravityInfluenceRadius > 0) + return body.GravityInfluenceRadius; + + return Math.Max(32, (body?.BodyRadiusMultiplier ?? 1) * 70); } private static double PowerPulse(double x, double exponent) diff --git a/Assets/Shaders/AetheriaRenderSplats.shader b/Assets/Shaders/AetheriaRenderSplats.shader new file mode 100644 index 000000000..589d414be --- /dev/null +++ b/Assets/Shaders/AetheriaRenderSplats.shader @@ -0,0 +1,68 @@ +Shader "Aetheria/Render Splats" +{ + Properties + { + _ValueScale("Value Scale", Float) = 1 + } + + SubShader + { + Tags { "RenderType" = "Transparent" "Queue" = "Transparent" } + + Pass + { + Name "Add" + Cull Off + ZWrite Off + ZTest Always + BlendOp Add + Blend One One + + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex Vert + #pragma fragment Frag + #include "UnityCG.cginc" + #define AETHERIA_SPLAT_PASS + #include "AetheriaRenderSplatsCore.hlsl" + ENDHLSL + } + + Pass + { + Name "Max" + Cull Off + ZWrite Off + ZTest Always + BlendOp Max + Blend One One + + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex Vert + #pragma fragment Frag + #include "UnityCG.cginc" + #define AETHERIA_SPLAT_PASS + #include "AetheriaRenderSplatsCore.hlsl" + ENDHLSL + } + + Pass + { + Name "Alpha" + Cull Off + ZWrite Off + ZTest Always + Blend SrcAlpha OneMinusSrcAlpha + + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex Vert + #pragma fragment Frag + #include "UnityCG.cginc" + #define AETHERIA_SPLAT_PASS + #include "AetheriaRenderSplatsCore.hlsl" + ENDHLSL + } + } +} diff --git a/Assets/Shaders/AetheriaRenderSplats.shader.meta b/Assets/Shaders/AetheriaRenderSplats.shader.meta new file mode 100644 index 000000000..ba44374f6 --- /dev/null +++ b/Assets/Shaders/AetheriaRenderSplats.shader.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: a1aa8658e37f433cb08a29ab01f4e8e5 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + preprocessorOverride: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Shaders/AetheriaRenderSplatsCore.hlsl b/Assets/Shaders/AetheriaRenderSplatsCore.hlsl new file mode 100644 index 000000000..5f73e4dab --- /dev/null +++ b/Assets/Shaders/AetheriaRenderSplatsCore.hlsl @@ -0,0 +1,115 @@ +#ifndef AETHERIA_RENDER_SPLATS_CORE_INCLUDED +#define AETHERIA_RENDER_SPLATS_CORE_INCLUDED + +struct AetheriaSplat +{ + float4 centerHalfExtent; + float4 rotationChannelFalloff; + float4 layerSource; + float4 sourceFrequencyPhase; + float4 value; +}; + +StructuredBuffer _AetheriaSplats; +float4x4 _AetheriaViewportToClip; +int _AetheriaSplatCount; +int _AetheriaChannelFilter; +float _ValueScale; + +float hash21(float2 p) +{ + p = frac(p * float2(123.34, 456.21)); + p += dot(p, p + 45.32); + return frac(p.x * p.y); +} + +float valueNoise(float2 p) +{ + float2 i = floor(p); + float2 f = frac(p); + float a = hash21(i); + float b = hash21(i + float2(1, 0)); + float c = hash21(i + float2(0, 1)); + float d = hash21(i + float2(1, 1)); + float2 u = f * f * (3 - 2 * f); + return lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y) * 2 - 1; +} + +struct Varyings +{ + float4 position : SV_POSITION; + float2 localUv : TEXCOORD0; + nointerpolation uint instanceId : TEXCOORD1; +}; + +Varyings Vert(uint vertexId : SV_VertexID, uint instanceId : SV_InstanceID) +{ + static const float2 corners[6] = + { + float2(-1, -1), + float2(1, -1), + float2(1, 1), + float2(-1, -1), + float2(1, 1), + float2(-1, 1) + }; + + AetheriaSplat splat = _AetheriaSplats[instanceId]; + float2 local = corners[vertexId]; + float2 scaled = local * splat.centerHalfExtent.zw; + float c = splat.rotationChannelFalloff.x; + float s = splat.rotationChannelFalloff.y; + float2 world = splat.centerHalfExtent.xy + float2( + scaled.x * c - scaled.y * s, + scaled.x * s + scaled.y * c); + + Varyings output; + output.position = mul(_AetheriaViewportToClip, float4(world, 0, 1)); + output.localUv = local; + output.instanceId = instanceId; + return output; +} + +float ResolveFalloff(float2 localUv, int falloff) +{ + float distance01 = saturate(length(localUv)); + if (falloff == 0) + return 1; + if (falloff == 1) + return saturate(1 - distance01); + if (falloff == 3) + { + float t = smoothstep(0, 1, distance01); + return t; + } + + return 1 - smoothstep(0, 1, distance01); +} + +float4 Frag(Varyings input) : SV_Target +{ + AetheriaSplat splat = _AetheriaSplats[input.instanceId]; + int channel = (int)round(splat.rotationChannelFalloff.z); + if (_AetheriaChannelFilter >= 0 && channel != _AetheriaChannelFilter) + discard; + + int falloff = (int)round(splat.rotationChannelFalloff.w); + float alpha = ResolveFalloff(input.localUv, falloff); + clip(alpha - 0.0001); + + int sourceKind = (int)round(splat.layerSource.y); + float source = 1; + if (sourceKind == 1 || sourceKind == 2) + { + float2 frequency = splat.sourceFrequencyPhase.xy; + float2 phase = splat.sourceFrequencyPhase.zw; + float timeOffset = sourceKind == 2 ? _Time.y * splat.layerSource.z : 0; + source = valueNoise(input.localUv * frequency + phase + timeOffset); + if (splat.layerSource.w != 0) + source = abs(source); + } + + return splat.value * (alpha * source * _ValueScale); +} + +#endif diff --git a/Assets/Shaders/AetheriaRenderSplatsCore.hlsl.meta b/Assets/Shaders/AetheriaRenderSplatsCore.hlsl.meta new file mode 100644 index 000000000..b0399d066 --- /dev/null +++ b/Assets/Shaders/AetheriaRenderSplatsCore.hlsl.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 91df759011f84ed8b003bb7828809e0b +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + preprocessorOverride: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Shaders/Compute/Slime/Slime.cs b/Assets/Shaders/Compute/Slime/Slime.cs index 919854fb9..445601d04 100644 --- a/Assets/Shaders/Compute/Slime/Slime.cs +++ b/Assets/Shaders/Compute/Slime/Slime.cs @@ -106,9 +106,9 @@ public class Slime : MonoBehaviour _trailKernel = SlimeCompute.FindKernel("UpdateTrailPoints"); _geometryKernel = SlimeCompute.FindKernel("CreateTrailGeometry"); - _accumulationTexture = new RenderTexture(TextureSize, TextureSize, 1, RenderTextureFormat.RFloat); + _accumulationTexture = new RenderTexture(TextureSize, TextureSize, 0, RenderTextureFormat.RFloat); _accumulationTexture.enableRandomWrite = true; - _previousAccumulationTexture = new RenderTexture(TextureSize, TextureSize, 1, RenderTextureFormat.RFloat); + _previousAccumulationTexture = new RenderTexture(TextureSize, TextureSize, 0, RenderTextureFormat.RFloat); _previousAccumulationTexture.enableRandomWrite = true; AccumulationMaterial.SetTexture("_MainTex", _accumulationTexture); diff --git a/CultMath.csproj b/CultMath.csproj index 94dcc759d..85e6a7582 100644 --- a/CultMath.csproj +++ b/CultMath.csproj @@ -57,11 +57,11 @@ - + @@ -224,7 +224,7 @@ C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Facades\System.Collections.Specialized.dll - E:\Projects\Brokkr\surfaces\unity\Packages\com.gamecult.brokkr\Plugins\CultMesh\System.ComponentModel.Annotations.dll + C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Facades\System.ComponentModel.Annotations.dll C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Facades\System.ComponentModel.dll @@ -488,7 +488,7 @@ C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Facades\System.Threading.Tasks.dll - E:\Projects\Brokkr\surfaces\unity\Packages\com.gamecult.brokkr\Plugins\CultMesh\System.Threading.Tasks.Extensions.dll + C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Facades\System.Threading.Tasks.Extensions.dll C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Facades\System.Threading.Tasks.Parallel.dll diff --git a/GameCult.Aetheria.State.Unity.csproj b/GameCult.Aetheria.State.Unity.csproj index c701bffbc..f4198f1d1 100644 --- a/GameCult.Aetheria.State.Unity.csproj +++ b/GameCult.Aetheria.State.Unity.csproj @@ -1,4 +1,4 @@ - + latest @@ -48,83 +48,87 @@ + + + + + + - + + - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - + + + + + + + + + + - + + + - + + + + + + - - - - + - - - - + + + + + + + + + + + E:\Projects\Brokkr\surfaces\unity\Packages\com.gamecult.brokkr\Plugins\CultMesh\ConcurrentCollections.dll @@ -265,7 +269,7 @@ C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Facades\System.Collections.Specialized.dll - E:\Projects\Brokkr\surfaces\unity\Packages\com.gamecult.brokkr\Plugins\CultMesh\System.ComponentModel.Annotations.dll + C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Facades\System.ComponentModel.Annotations.dll C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Facades\System.ComponentModel.dll @@ -529,7 +533,7 @@ C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Facades\System.Threading.Tasks.dll - E:\Projects\Brokkr\surfaces\unity\Packages\com.gamecult.brokkr\Plugins\CultMesh\System.Threading.Tasks.Extensions.dll + C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Facades\System.Threading.Tasks.Extensions.dll C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\Facades\System.Threading.Tasks.Parallel.dll @@ -566,14 +570,14 @@ - - {6f7a47c4-21dc-64ef-0969-96b956a000f4} - CultMath - {037dacf0-10f4-20ed-7963-3a1e1ac15b64} GameCult.Eve.Surface + + {6f7a47c4-21dc-64ef-0969-96b956a000f4} + CultMath +