[studio] prefix assets

This commit is contained in:
Devin Zuczek
2026-10-09 01:37:19 -04:00
parent f9beba0dae
commit ef65b505cc
16 changed files with 384 additions and 410 deletions
@@ -0,0 +1,26 @@
-- `unity_asset` gains a `kind`: Rec Room Studio builds store a `main` bundle (the scene the
-- client loads) and, optionally, a `stripped` editor-only bundle per target, and both are
-- rows here now that the studio worker writes this table instead of its own
-- `studio_unity_asset_file` (dropped by the studio migration 0002). The studio rows map
-- platform → target (windows 0, android 2), the R2 key → `filename` and the base64
-- SHA-256 → `hash`.
--
-- Generated from packages/domain/src/unity-assets-db.ts (UNITY_ASSET_SCHEMA_DDL) — keep in
-- sync. `kind` joins the primary key, which SQLite cannot alter in place, so the rows are
-- copied into a new table and KEPT: every existing row is a main bundle.
CREATE TABLE unity_asset_new (
unity_asset_id TEXT NOT NULL,
target INTEGER NOT NULL,
version INTEGER NOT NULL,
kind TEXT NOT NULL DEFAULT 'main',
filename TEXT NOT NULL,
hash TEXT NOT NULL,
PRIMARY KEY (unity_asset_id, target, version, kind)
);
INSERT INTO unity_asset_new (unity_asset_id, target, version, kind, filename, hash)
SELECT unity_asset_id, target, version, 'main', filename, hash FROM unity_asset;
DROP TABLE unity_asset;
ALTER TABLE unity_asset_new RENAME TO unity_asset;
+13 -22
View File
@@ -8,6 +8,7 @@ import {
applyRoomTagEdit,
attachStudioUnityAssets,
autocompleteRoomSearch,
bakedUnityAssets,
banPlayerFromRoom,
canManageRoom,
canManageRoomById,
@@ -53,7 +54,6 @@ import {
getVisitedRooms,
incrementRoomExperience,
inviteRoomRole,
isMissingStudioAssetTable,
isPlayerBannedFromRoom,
isRoomOwner,
MessageType,
@@ -78,9 +78,6 @@ import {
setRoomProgression,
setRoomRole,
setSubRoomPermissions,
sha256HexToBase64,
STUDIO_ASSET_VERSION,
studioAssetTarget,
toggleCheer,
toggleFavorite,
transferRoomOwnership,
@@ -3819,33 +3816,27 @@ const app = new Hono<App>()
const save = saves.find((row) => row.UnityAssetId === unityAssetId)
if (!save) return c.notFound()
let asset
try {
asset = await getStudioUnityAsset(c.env.DB, unityAssetId)
} catch (err) {
if (isMissingStudioAssetTable(err)) return c.notFound()
throw err
}
const asset = await getStudioUnityAsset(c.env.DB, unityAssetId)
if (!asset) return c.notFound()
const mains = asset.files
.filter((file) => file.kind === 'main')
.sort((a, b) => studioAssetTarget(a.platform) - studioAssetTarget(b.platform))
const source = mains.find((file) => file.platform === 'windows') ?? mains[0]
// Main bundles, Windows first; the Windows one (or the first there is) supplies
// the top-level `filename`/`hash`.
const mains = bakedUnityAssets(asset.builds)
const source = mains.find((build) => build.Target === 0) ?? mains[0]
if (!source) return c.notFound()
const savedBy = typeof save.SavedByAccountId === 'number' ? save.SavedByAccountId : 0
return c.json({
unityAssetId: asset.unityAssetId,
createdByAccountId: asset.createdByAccountId || savedBy,
bakedUnityAssets: mains.map((file) => ({
unityAssetId: file.unityAssetId,
target: studioAssetTarget(file.platform),
version: STUDIO_ASSET_VERSION,
filename: file.filename,
bakedUnityAssets: mains.map((build) => ({
unityAssetId: build.UnityAssetId,
target: build.Target,
version: build.Version,
filename: build.Filename,
})),
filename: source.filename,
hash: sha256HexToBase64(source.sha256),
filename: source.Filename,
hash: source.Hash,
})
}
)
+21 -26
View File
@@ -24,8 +24,7 @@ import {
ROOM_SCHEMA_DDL,
ROOM_XP_SCHEMA_DDL,
seedRoomWithSubRooms,
sha256HexToBase64,
STUDIO_UNITY_ASSET_SCHEMA_DDL,
STUDIO_CLOUD_BUILD_SCHEMA_DDL,
SUBROOM_SCHEMA_DDL,
UNITY_ASSET_SCHEMA_DDL,
} from '@repo/domain'
@@ -6006,17 +6005,16 @@ describe('rooms endpoints', () => {
})
})
// A Rec Room Studio build is a unity asset on the save. The game loads the scene from
// `CurrentSave.DataBlob` and the bundles from `UnitySubAssets`. Maker-pen saves stay
// empty arrays, and a missing studio table must not fail the room read.
// A Rec Room Studio build is a unity asset on the save: `unity_asset` rows the studio
// worker wrote. The game loads the scene from `CurrentSave.DataBlob` and the bundles from
// `UnitySubAssets`. Maker-pen saves stay empty arrays, and a missing studio cloud-build
// table must not fail the room read.
describe('studio room bundles', () => {
const ASSET = '11111111-2222-4333-8444-555555555555'
const WIN_HEX = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
const ANDROID_HEX = 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb'
const winName = publicStudioBundleFilename(ASSET, 'windows', 'main')
const androidName = publicStudioBundleFilename(ASSET, 'android', 'main')
const winHash = sha256HexToBase64(WIN_HEX)
const androidHash = sha256HexToBase64(ANDROID_HEX)
const winName = publicStudioBundleFilename(9901, ASSET, 'windows', 'main')
const androidName = publicStudioBundleFilename(9901, ASSET, 'android', 'main')
const winHash = '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='
const androidHash = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs='
const save = (unityAssetId?: string) => ({
UnitySubAssets: [],
@@ -6038,8 +6036,8 @@ describe('studio room bundles', () => {
})
async function reset() {
await env.DB.prepare('DROP TABLE IF EXISTS studio_unity_asset_file').run()
await env.DB.prepare('DROP TABLE IF EXISTS studio_cloud_build').run()
await env.DB.prepare('DELETE FROM unity_asset WHERE unity_asset_id = ?1').bind(ASSET).run()
await env.DB.prepare('DELETE FROM subroom_save WHERE sub_room_id IN (99011, 99021)').run()
await env.DB.prepare('DELETE FROM subroom WHERE room_id IN (9901, 9902)').run()
await env.DB.prepare('DELETE FROM room WHERE room_id IN (9901, 9902)').run()
@@ -6080,7 +6078,7 @@ describe('studio room bundles', () => {
}
async function storeFiles() {
for (const sql of STUDIO_UNITY_ASSET_SCHEMA_DDL) await env.DB.prepare(sql).run()
for (const sql of STUDIO_CLOUD_BUILD_SCHEMA_DDL) await env.DB.prepare(sql).run()
await env.DB.prepare(
`INSERT INTO studio_cloud_build
(cloud_build_id, room_id, sub_room_id, sub_room_data_save_id, unity_asset_id,
@@ -6090,22 +6088,19 @@ describe('studio room bundles', () => {
)
.bind(ASSET)
.run()
const insert = async (platform: string, kind: string, filename: string, sha256: string) => {
await env.DB.prepare(
`INSERT INTO studio_unity_asset_file
(unity_asset_id, platform, kind, filename, sha256, byte_length, r2_key)
VALUES (?1, ?2, ?3, ?4, ?5, 4, ?6)`
const insert = (target: number, kind: 'main' | 'stripped', filename: string, hash: string) =>
putUnityAsset(
env.DB,
{ UnityAssetId: ASSET, Target: target, Version: 1, Filename: filename, Hash: hash },
kind
)
.bind(ASSET, platform, kind, filename, sha256, `studio-room-bundles/${filename}`)
.run()
}
await insert('windows', 'main', winName, WIN_HEX)
await insert('android', 'main', androidName, ANDROID_HEX)
await insert(0, 'main', winName, winHash)
await insert(2, 'main', androidName, androidHash)
await insert(
'windows',
0,
'stripped',
publicStudioBundleFilename(ASSET, 'windows', 'stripped'),
WIN_HEX
publicStudioBundleFilename(9901, ASSET, 'windows', 'stripped'),
winHash
)
}