Backfill missing nitros into the Gamedata bundle on import

After each single import, batch import and batch-regen, copy every
.nitro file that is missing from /var/www/Gamedata/bundled/furniture so
the emulator can render all imported furniture even when the mirror write
was skipped. Reports the copied files in the API/SSE response.
This commit is contained in:
openhands committed 2026-08-18 20:29:30 +02:00
1 parent 99e77d9872
commit bcd24bfca4
4 files changed
+77 -1

No files matched your search

@@ -11,7 +11,12 @@ import {
buildFurniEntry, buildFurniEntry,
readFurniData, readFurniData,
} from "@/lib/services/furni-data"; } from "@/lib/services/furni-data";
import { downloadFile, ensureDirectories } from "@/lib/services/furni-import"; import {
downloadFile,
ensureDirectories,
ensureFurniOwnership,
syncNitrosToGamedataBundle,
} from "@/lib/services/furni-import";
import { getOfficialHabboFurnidata } from "@/lib/services/habbo-furnidata-cache"; import { getOfficialHabboFurnidata } from "@/lib/services/habbo-furnidata-cache";
import { rcon } from "@/lib/services/rcon"; import { rcon } from "@/lib/services/rcon";
import { convertSwfToNitro } from "@/lib/services/swf-to-nitro"; import { convertSwfToNitro } from "@/lib/services/swf-to-nitro";
@@ -254,12 +259,20 @@ export const POST = withAdmin(
/* non-fatal */ /* non-fatal */
} }
// Keep regenerated assets readable by the web server
const ownership = await ensureFurniOwnership();
// Backfill any missing nitros into the emulator's Gamedata bundle
const nitrosSynced = await syncNitrosToGamedataBundle();
send({ send({
type: "regen_complete", type: "regen_complete",
succeeded, succeeded,
failed, failed,
skipped, skipped,
furniDataAdded: furniDataEntries.length, furniDataAdded: furniDataEntries.length,
ownershipFixed: ownership.fixed,
nitrosSynced: nitrosSynced.copied,
duration: Date.now() - startTime, duration: Date.now() - startTime,
}); });
@@ -9,6 +9,7 @@ import {
ensureFurniOwnership, ensureFurniOwnership,
importSingleFurni, importSingleFurni,
reconcileImportedOfferIds, reconcileImportedOfferIds,
syncNitrosToGamedataBundle,
} from "@/lib/services/furni-import"; } from "@/lib/services/furni-import";
import { rcon } from "@/lib/services/rcon"; import { rcon } from "@/lib/services/rcon";
import type { ImportSingleResult } from "@/types/furni"; import type { ImportSingleResult } from "@/types/furni";
@@ -216,9 +217,11 @@ export const POST = withAdmin(
// Reconcile offer_id and fix asset ownership across the whole tree // Reconcile offer_id and fix asset ownership across the whole tree
let offerIdsFixed = 0; let offerIdsFixed = 0;
let ownershipFixed: string[] = []; let ownershipFixed: string[] = [];
let nitrosSynced: string[] = [];
try { try {
offerIdsFixed = (await reconcileImportedOfferIds()).fixed; offerIdsFixed = (await reconcileImportedOfferIds()).fixed;
ownershipFixed = (await ensureFurniOwnership()).fixed; ownershipFixed = (await ensureFurniOwnership()).fixed;
nitrosSynced = (await syncNitrosToGamedataBundle()).copied;
} catch (err) { } catch (err) {
console.warn( console.warn(
"[import-furni] Post-import reconcile/ownership failed:", "[import-furni] Post-import reconcile/ownership failed:",
@@ -233,6 +236,7 @@ export const POST = withAdmin(
warnings: withWarnings, warnings: withWarnings,
offerIdsFixed, offerIdsFixed,
ownershipFixed, ownershipFixed,
nitrosSynced,
duration: Date.now() - startTime, duration: Date.now() - startTime,
}); });
+9
View File
@@ -22,6 +22,7 @@ import {
importSingleFurni, importSingleFurni,
reconcileImportedOfferIds, reconcileImportedOfferIds,
resetCatalogPageCache, resetCatalogPageCache,
syncNitrosToGamedataBundle,
} from "@/lib/services/furni-import"; } from "@/lib/services/furni-import";
import { getOfficialHabboFurnidata } from "@/lib/services/habbo-furnidata-cache"; import { getOfficialHabboFurnidata } from "@/lib/services/habbo-furnidata-cache";
import { listFurniture } from "@/lib/services/habbofurni"; import { listFurniture } from "@/lib/services/habbofurni";
@@ -411,6 +412,9 @@ export const POST = withAdmin(
// Keep asset + furnidata folders readable by the web server // Keep asset + furnidata folders readable by the web server
const ownership = await ensureFurniOwnership(); const ownership = await ensureFurniOwnership();
// Backfill any missing nitros into the emulator's Gamedata bundle
const nitrosSynced = await syncNitrosToGamedataBundle();
const succeeded = results.filter((r) => r.ok).length; const succeeded = results.filter((r) => r.ok).length;
const failed = results.filter((r) => !r.ok).length; const failed = results.filter((r) => !r.ok).length;
@@ -421,6 +425,7 @@ export const POST = withAdmin(
results, results,
offerIdsFixed: reconciled.fixed, offerIdsFixed: reconciled.fixed,
ownershipFixed: ownership.fixed, ownershipFixed: ownership.fixed,
nitrosSynced: nitrosSynced.copied,
}); });
} }
@@ -476,6 +481,9 @@ export const POST = withAdmin(
// Keep asset + furnidata folders readable by the web server // Keep asset + furnidata folders readable by the web server
const ownership = await ensureFurniOwnership(); const ownership = await ensureFurniOwnership();
// Backfill any missing nitros into the emulator's Gamedata bundle
const nitrosSynced = await syncNitrosToGamedataBundle();
logAudit({ logAudit({
userId: ctx.session.user.id, userId: ctx.session.user.id,
action: "furni_import", action: "furni_import",
@@ -498,6 +506,7 @@ export const POST = withAdmin(
catalogItemId: result.catalogItemId, catalogItemId: result.catalogItemId,
offerIdsFixed: reconciled.fixed, offerIdsFixed: reconciled.fixed,
ownershipFixed: ownership.fixed, ownershipFixed: ownership.fixed,
nitrosSynced: nitrosSynced.copied,
spriteCount: result.spriteCount, spriteCount: result.spriteCount,
warnings: result.warnings, warnings: result.warnings,
}); });
+50
View File
@@ -12,6 +12,7 @@ import {
DEFAULT_FURNI_SWF_DIR, DEFAULT_FURNI_SWF_DIR,
getFurniAssetDirs, getFurniAssetDirs,
getFurniAssetWriteTargets, getFurniAssetWriteTargets,
getGamedataRoot,
} from "@/lib/services/furni-asset-dirs"; } from "@/lib/services/furni-asset-dirs";
import { import {
appendFurniEntry, appendFurniEntry,
@@ -346,6 +347,55 @@ export async function ensureFurniOwnership(): Promise<{ fixed: string[] }> {
} }
} }
/**
* Copy every .nitro file that is missing from the emulator's Gamedata
* bundle directory (/var/www/Gamedata/bundled/furniture) so all imported
* furniture stays renderable in-game even when a mirror write was skipped.
*/
export async function syncNitrosToGamedataBundle(): Promise<{
copied: string[];
}> {
if (process.platform === "win32") return { copied: [] };
const gamedataRoot = await getGamedataRoot();
if (!gamedataRoot) return { copied: [] };
const targetDir = path.join(gamedataRoot, "bundled/furniture");
const { nitroDir } = await getFurniAssetDirs();
if (nitroDir === targetDir) return { copied: [] };
if (!existsSync(nitroDir)) return { copied: [] };
try {
await fs.mkdir(targetDir, { recursive: true });
} catch {
return { copied: [] };
}
const copied: string[] = [];
try {
const entries = await fs.readdir(nitroDir);
for (const entry of entries) {
if (!entry.endsWith(".nitro")) continue;
const dest = path.join(targetDir, entry);
if (existsSync(dest)) continue;
try {
await fs.copyFile(path.join(nitroDir, entry), dest);
copied.push(entry);
} catch (err) {
logger.warn("[import-furni] nitro sync copy failed", {
entry,
error: (err as Error).message,
});
}
}
} catch (err) {
logger.warn("[import-furni] nitro sync scan failed", {
error: (err as Error).message,
});
}
return { copied };
}
/** /**
* Allocate the next `catalog_items.id` and run the INSERT through a * Allocate the next `catalog_items.id` and run the INSERT through a
* process-level async mutex so concurrent imports in the same Node process * process-level async mutex so concurrent imports in the same Node process