From 5f34028267a93acba89cfe80daf9362d1dac352c Mon Sep 17 00:00:00 2001 From: dukedotdev Date: Sun, 25 Oct 2026 17:01:42 -0400 Subject: [PATCH] [rooms] load Studio export dependencies (#142) --- apps/rooms/src/rooms.app.ts | 8 ++- apps/rooms/src/test/integration/api.test.ts | 78 +++++++++++++++++++++ packages/domain/src/rooms-db.ts | 24 ++++--- 3 files changed, 101 insertions(+), 9 deletions(-) diff --git a/apps/rooms/src/rooms.app.ts b/apps/rooms/src/rooms.app.ts index d4491c6..4eacd4f 100644 --- a/apps/rooms/src/rooms.app.ts +++ b/apps/rooms/src/rooms.app.ts @@ -962,9 +962,15 @@ function pickBakedBundle( target: number | null ): { filename: string; hash: string | null } | null { if (!Array.isArray(save.UnitySubAssets)) return null + const rootId = typeof save.UnityAssetId === 'string' ? save.UnityAssetId.toLowerCase() : null const rows = save.UnitySubAssets.filter( (item): item is { Target?: unknown; Filename?: unknown; Hash?: unknown } => - typeof item === 'object' && item !== null + typeof item === 'object' && + item !== null && + (rootId === null || + ('UnityAssetId' in item && + typeof item.UnityAssetId === 'string' && + item.UnityAssetId.toLowerCase() === rootId)) ) const chosen = target === null diff --git a/apps/rooms/src/test/integration/api.test.ts b/apps/rooms/src/test/integration/api.test.ts index 54164ca..3361a5c 100644 --- a/apps/rooms/src/test/integration/api.test.ts +++ b/apps/rooms/src/test/integration/api.test.ts @@ -6331,6 +6331,84 @@ describe('studio room bundles', () => { ) } + it('serves partitioned prefab and shared-material exports as save dependencies', async () => { + await reset() + await storeFiles() + const prefab = '00111111-2222-4333-8444-555555555555' + const shared = '00222222-2222-4333-8444-555555555555' + await env.DB.prepare( + "UPDATE subroom_save SET data=json_set(data,'$.ServerData',json(?1)) WHERE sub_room_id=99011" + ) + .bind( + JSON.stringify({ + Exports: [ + { e: 'prefab', u: prefab.toUpperCase() }, + { e: 'shared', u: shared }, + { e: 'duplicate', u: prefab }, + ], + }) + ) + .run() + for (const id of [prefab, shared]) { + for (const target of [0, 2]) { + for (const kind of ['main', 'stripped'] as const) { + await putUnityAsset( + env.DB, + { + UnityAssetId: id, + Target: target, + Version: 5, + Filename: publicStudioBundleFilename( + 9901, + id, + target === 0 ? 'windows' : 'android', + kind + ), + Hash: winHash, + }, + kind + ) + } + } + } + for (const target of [0, 2]) { + const response = await SELF.fetch(`${ORIGIN}/rooms/9901?unityAssetTarget=${target}`) + const room = (await response.json()) as { + SubRooms: Array<{ + CurrentSave: { + SubRoomDataSaveId: number + UnitySubAssets: Array<{ UnityAssetId: string; Target: number }> + } + }> + } + const current = room.SubRooms[0]!.CurrentSave + expect(current.UnitySubAssets.map((asset) => asset.UnityAssetId).sort()).toEqual( + [ASSET, prefab, shared].sort() + ) + expect(current.UnitySubAssets.every((asset) => asset.Target === target)).toBe(true) + const detailResponse = await SELF.fetch( + `${ORIGIN}/rooms/9901/subrooms/99011/saves/${current.SubRoomDataSaveId}?unityAssetTarget=${target}`, + { headers: await bearer('1') } + ) + expect(await detailResponse.json()).toMatchObject({ + UnityAsset: target === 0 ? winName : androidName, + UnitySubAssets: [prefab, shared].map((id) => ({ + UnityAssetId: id, + UnityAsset: publicStudioBundleFilename( + 9901, + id, + target === 0 ? 'windows' : 'android', + 'main' + ), + UnityAssetHash: winHash, + })), + }) + } + await env.DB.prepare('DELETE FROM unity_asset WHERE unity_asset_id IN (?1,?2)') + .bind(prefab, shared) + .run() + }) + it('loads baked bundles onto the room and serves the unity asset', async () => { await reset() diff --git a/packages/domain/src/rooms-db.ts b/packages/domain/src/rooms-db.ts index ed04c6b..c03cc2d 100644 --- a/packages/domain/src/rooms-db.ts +++ b/packages/domain/src/rooms-db.ts @@ -2512,13 +2512,21 @@ export async function attachStudioUnityAssets( db: D1Database, saves: SubRoomDataSave[] ): Promise { - const ids = [ - ...new Set( - saves - .map((save) => save.UnityAssetId) - .filter((id): id is string => typeof id === 'string' && id !== '') - ), - ] + // The cloud build partitions prefab/material exports into separate bundles. + // The main scene keeps external references to them, so the loader must receive + // every export, including shared dependencies, before it instantiates the scene. + const assetIds = (save: SubRoomDataSave): string[] => { + if (typeof save.UnityAssetId !== 'string' || save.UnityAssetId === '') return [] + const serverData = save.ServerData as { Exports?: unknown } | null | undefined + const exports = Array.isArray(serverData?.Exports) ? serverData.Exports : [] + const ids = new Set([save.UnityAssetId.toLowerCase()]) + for (const item of exports) { + const id = (item as { u?: unknown } | null)?.u + if (typeof id === 'string' && id !== '') ids.add(id.toLowerCase()) + } + return [...ids] + } + const ids = [...new Set(saves.flatMap(assetIds))] if (ids.length === 0) return const builds = (await listUnityAssetBuilds(db, ids)).filter((build) => isStudioBundleFilename(build.Filename) @@ -2533,7 +2541,7 @@ export async function attachStudioUnityAssets( for (const save of saves) { const id = save.UnityAssetId if (typeof id !== 'string') continue - const baked = bakedUnityAssets(byAsset.get(id.toLowerCase()) ?? []) + const baked = bakedUnityAssets(assetIds(save).flatMap((assetId) => byAsset.get(assetId) ?? [])) if (baked.length === 0) continue save.UnitySubAssets = baked }