[rooms] fix imported unity assets

This commit is contained in:
Devin Zuczek
2026-10-12 21:18:51 -04:00
parent dfe5071878
commit bd15e30bfc
4 changed files with 43 additions and 4 deletions
+10
View File
@@ -287,6 +287,16 @@ inconsistency here without checking the client first.
other implementations set it at all, it names the start of a voice-chat timeout. Other
implementations with timed bans keep their set-time private and serve the remainder on
every read; `banned_at` here is the staff panel's audit trail, not the client's.
- `unity_asset` holds two kinds of bundle under one shape: Rec Room Studio builds (UGC, the
`studio` worker writes them, filenames `studio/<roomId>/…`) and FIRST-PARTY bundles captured
from the official game and loaded by hand (`unity/<roomId>/…`). Only the Studio ones may be
folded onto `CurrentSave.UnitySubAssets` (`rooms`: `GET /rooms/:id`, the save reads) —
`attachStudioUnityAssets` filters on `isStudioBundleFilename`. The 2025 client resolves a
first-party bundle through `POST /unity_assets/baked/bulk` (which serves both kinds); handed
it inline in the five-key `{ Target, Version, Filename, Hash }` shape it takes that instead,
cannot name the file (its decoder wants `UnityAsset`/`UnityAssetHash`), and the room loads
nothing. Every imported first-party room broke when the splice moved from the Studio-only
table to `unity_asset`, with every response looking correct.
- Accessibility is sent as the `RoomAccessibility` enum NAME on
`rooms` `PUT /rooms/:id/subrooms/:sid/accessibility` (`accessibility=Private`), not the
ordinal the room-level `/rooms/:id/accessibility` takes. The enum has five members
+12 -1
View File
@@ -21,6 +21,7 @@ import { bindPlaceholders, chunkForBinds, MAX_BOUND_PARAMS } from './d1-binds'
import { Accessibility, Role } from './enums'
import { countPlayersByRoom } from './presence-db'
import { resyncInstanceCapacity } from './room-instance-db'
import { isStudioBundleFilename } from './studio-unity-assets'
import { bakedUnityAssets, listUnityAssetBuilds } from './unity-assets-db'
import type { UnityAssetBuild } from './unity-assets-db'
@@ -2303,6 +2304,14 @@ async function attachCurrentSaves(
* including their empty arrays; so is a save whose asset has no stored build —
* the scene blob still loads, and the bundle list stays empty.
*
* Only Studio (UGC) builds are folded in — rows whose filename passes
* {@link isStudioBundleFilename}. A FIRST-PARTY bundle loaded by hand under
* `unity/<roomId>/` shares the table but must stay out of this list: the 2025 client
* resolves it through `POST /unity_assets/baked/bulk` (which serves both kinds), and
* handed it inline in this five-key shape it stops asking, cannot name the file, and
* loads nothing. That broke every imported first-party room when the splice moved from
* the Studio-only table to `unity_asset`.
*
* Only main bundles are listed. Stripped bundles stay in the bucket.
*/
export async function attachStudioUnityAssets(
@@ -2317,7 +2326,9 @@ export async function attachStudioUnityAssets(
),
]
if (ids.length === 0) return
const builds = await listUnityAssetBuilds(db, ids)
const builds = (await listUnityAssetBuilds(db, ids)).filter((build) =>
isStudioBundleFilename(build.Filename)
)
// Rows come back with the id lowercased; a save may spell its GUID either way.
const byAsset = new Map<string, UnityAssetBuild[]>()
for (const build of builds) {
+17 -1
View File
@@ -40,6 +40,9 @@ export const STUDIO_ASSET_VERSION = 1
/** The platforms a local Studio build posts, named as the multipart fields name them. */
export type StudioBundlePlatform = 'windows' | 'android'
/** Where every Studio (UGC) bundle filename starts; see {@link publicStudioBundleFilename}. */
const STUDIO_BUNDLE_PREFIX = 'studio/'
/**
* A Studio bundle's `unity_asset.filename` — the name the client downloads, which it
* prefixes with `/room/` (see {@link studioBundleR2Key}). Laid out like the official
@@ -54,7 +57,20 @@ export function publicStudioBundleFilename(
kind: string
): string {
const id = unityAssetId.replace(/-/g, '').toLowerCase()
return `studio/${roomId}/${id}.${platform}.${kind}.assetbundle`
return `${STUDIO_BUNDLE_PREFIX}${roomId}/${id}.${platform}.${kind}.assetbundle`
}
/**
* Whether a `unity_asset` filename is a Rec Room Studio build — UGC the studio worker
* wrote — as opposed to a first-party bundle loaded by hand under the game's own
* `unity/<roomId>/` layout. The two share the table, and only the Studio ones are
* folded onto a save's `UnitySubAssets`: a 2025 client handed a first-party bundle
* inline takes it instead of asking `POST /unity_assets/baked/bulk`, cannot name it
* (its decoder reads `UnityAsset`/`UnityAssetHash`, not `Filename`/`Hash`), and loads
* nothing. The bulk lookup serves both kinds.
*/
export function isStudioBundleFilename(filename: string): boolean {
return filename.startsWith(STUDIO_BUNDLE_PREFIX)
}
/**
+4 -2
View File
@@ -10,8 +10,10 @@
*
* Two things read it. The client asks for main bundles in bulk from
* `POST /unity_assets/baked/bulk` (the `rooms` worker) while loading a room — the step
* Player.log times as `AssetBundle_Download` — and `GET /rooms/{id}` folds the same main
* bundles onto `CurrentSave.UnitySubAssets`; either way the client then downloads
* Player.log times as `AssetBundle_Download` — and `GET /rooms/{id}` folds the STUDIO
* main bundles (filenames under `studio/`, see `isStudioBundleFilename`) onto
* `CurrentSave.UnitySubAssets`. A first-party bundle loaded by hand under `unity/<roomId>/`
* is served by the bulk lookup only; either way the client then downloads
* `/room/<Filename>` from the bucket, and a lookup that answers no row,
* or a row without a `Filename`, simply downloads nothing, with no error anywhere.
*