feat(catalog): store furniture bundles as .hab instead of .nitro
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 29s
CI / tests-unit (push) Failing after 1m37s
CI / tests-integration (push) Successful in 1m37s
CI / tests-ui (push) Successful in 2m17s
CI / preflight (push) Skipped
CI / deploy (push) Skipped

Every bundle the CMS writes — upload, clone, sync, repair and the pet /
effect / figure importers — now lands as `<classname>.hab`, the extension
this deployment's renderer asks for. `.hab` and `.nitro` are the same
container, so an upload of either extension is accepted.

Resolution goes through one module, src/lib/furni/bundle-file.ts, so
nothing has to know the extension twice. Every existence check probes
`.hab` first and falls back to `.nitro`: the on-disk asset set is still
predominantly `.nitro`, and without the fallback Studio would report every
imported item as missing and the cleanup scan would classify 18k live
bundles as fake leftovers. Downloads are unchanged — Habbo's CDN and every
configured clone source still serve `.nitro`, so the conversion happens on
write, not on request.

Deliberately unchanged: the staged-attachment store in furni-attachment.ts
keys on a UUID and never reaches the client, so renaming it would break
in-flight recovery jobs.

Adds scripts/migrate-nitro-to-hab.ts to rename the existing asset set. It
refuses to run without --dry-run or --yes, never overwrites an existing
.hab, never deletes, and is idempotent.

Note: renderer-config.json lives outside this repo and was patched to
.hab separately; that file is served with a 30-day max-age, so returning
clients need a cms-client cache purge to pick the change up.
This commit is contained in:
openhands committed 2026-10-10 17:09:36 +02:00
1 parent fdb7af7ef5
commit adffac7360
46 files changed
+1001 -210

No files matched your search

+9 -5
View File
@@ -37,6 +37,10 @@ const CONFIG_FILES = [
"ProductData.json",
];
const TABLES = ["items_base", "catalog_pages", "catalog_items"] as const;
// The catalog export must ship both bundle extensions for every asset type:
// `.hab` is what imports write now (furniture, pets, effects and figures all
// go through it), `.nitro` is what the existing asset set is made of.
const BUNDLE_FILE_RE = /\.(?:nitro|hab)$/i;
const identifier = (name: string) => `\`${name.replaceAll("`", "``")}\``;
export async function createCatalogSnapshot(
@@ -75,13 +79,13 @@ export async function createCatalogSnapshot(
await addTree(
path.join(getPublicAssetRoot(), "public/nitro-assets/bundled"),
"Gamedata/bundled",
/\.nitro$/i,
BUNDLE_FILE_RE,
);
if (gamedata) {
await addTree(
path.join(gamedata, "bundled"),
"Gamedata/bundled",
/\.nitro$/i,
BUNDLE_FILE_RE,
);
await addTree(
path.join(gamedata, "icons"),
@@ -94,18 +98,18 @@ export async function createCatalogSnapshot(
/\.(png|gif|webp|jpe?g)$/i,
);
}
await addTree(furni.nitroDir, "Gamedata/bundled/furniture", /\.nitro$/i);
await addTree(furni.nitroDir, "Gamedata/bundled/furniture", BUNDLE_FILE_RE);
await addTree(furni.iconDir, "Gamedata/icons", /\.(png|gif|webp|jpe?g)$/i);
for (const type of ["figure", "effect"]) {
const configured = await siteSettings.get(`${type}_nitro_dir`, "");
if (configured?.trim())
await addTree(configured, `Gamedata/bundled/${type}`, /\.nitro$/i);
await addTree(configured, `Gamedata/bundled/${type}`, BUNDLE_FILE_RE);
}
// Pets currently write directly to the CMS directory.
await addTree(
path.join(getPublicAssetRoot(), "public/nitro-assets/bundled/pet"),
"Gamedata/bundled/pet",
/\.nitro$/i,
BUNDLE_FILE_RE,
);
const configRoot = gamedata
? path.join(gamedata, "config")