diff --git a/src/app/api/admin/import/furni/batch-regen/route.ts b/src/app/api/admin/import/furni/batch-regen/route.ts index 3655fa46..c5789d96 100644 --- a/src/app/api/admin/import/furni/batch-regen/route.ts +++ b/src/app/api/admin/import/furni/batch-regen/route.ts @@ -11,7 +11,12 @@ import { buildFurniEntry, readFurniData, } 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 { rcon } from "@/lib/services/rcon"; import { convertSwfToNitro } from "@/lib/services/swf-to-nitro"; @@ -254,12 +259,20 @@ export const POST = withAdmin( /* 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({ type: "regen_complete", succeeded, failed, skipped, furniDataAdded: furniDataEntries.length, + ownershipFixed: ownership.fixed, + nitrosSynced: nitrosSynced.copied, duration: Date.now() - startTime, }); diff --git a/src/app/api/admin/import/furni/batch/route.ts b/src/app/api/admin/import/furni/batch/route.ts index 60eba761..1e88e357 100644 --- a/src/app/api/admin/import/furni/batch/route.ts +++ b/src/app/api/admin/import/furni/batch/route.ts @@ -9,6 +9,7 @@ import { ensureFurniOwnership, importSingleFurni, reconcileImportedOfferIds, + syncNitrosToGamedataBundle, } from "@/lib/services/furni-import"; import { rcon } from "@/lib/services/rcon"; 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 let offerIdsFixed = 0; let ownershipFixed: string[] = []; + let nitrosSynced: string[] = []; try { offerIdsFixed = (await reconcileImportedOfferIds()).fixed; ownershipFixed = (await ensureFurniOwnership()).fixed; + nitrosSynced = (await syncNitrosToGamedataBundle()).copied; } catch (err) { console.warn( "[import-furni] Post-import reconcile/ownership failed:", @@ -233,6 +236,7 @@ export const POST = withAdmin( warnings: withWarnings, offerIdsFixed, ownershipFixed, + nitrosSynced, duration: Date.now() - startTime, }); diff --git a/src/app/api/admin/import/furni/route.ts b/src/app/api/admin/import/furni/route.ts index 2ea58076..998e6ba5 100644 --- a/src/app/api/admin/import/furni/route.ts +++ b/src/app/api/admin/import/furni/route.ts @@ -22,6 +22,7 @@ import { importSingleFurni, reconcileImportedOfferIds, resetCatalogPageCache, + syncNitrosToGamedataBundle, } from "@/lib/services/furni-import"; import { getOfficialHabboFurnidata } from "@/lib/services/habbo-furnidata-cache"; import { listFurniture } from "@/lib/services/habbofurni"; @@ -411,6 +412,9 @@ export const POST = withAdmin( // Keep asset + furnidata folders readable by the web server 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 failed = results.filter((r) => !r.ok).length; @@ -421,6 +425,7 @@ export const POST = withAdmin( results, offerIdsFixed: reconciled.fixed, ownershipFixed: ownership.fixed, + nitrosSynced: nitrosSynced.copied, }); } @@ -476,6 +481,9 @@ export const POST = withAdmin( // Keep asset + furnidata folders readable by the web server const ownership = await ensureFurniOwnership(); + // Backfill any missing nitros into the emulator's Gamedata bundle + const nitrosSynced = await syncNitrosToGamedataBundle(); + logAudit({ userId: ctx.session.user.id, action: "furni_import", @@ -498,6 +506,7 @@ export const POST = withAdmin( catalogItemId: result.catalogItemId, offerIdsFixed: reconciled.fixed, ownershipFixed: ownership.fixed, + nitrosSynced: nitrosSynced.copied, spriteCount: result.spriteCount, warnings: result.warnings, }); diff --git a/src/lib/services/furni-import.ts b/src/lib/services/furni-import.ts index 850d0ed1..7bf24ae2 100644 --- a/src/lib/services/furni-import.ts +++ b/src/lib/services/furni-import.ts @@ -12,6 +12,7 @@ import { DEFAULT_FURNI_SWF_DIR, getFurniAssetDirs, getFurniAssetWriteTargets, + getGamedataRoot, } from "@/lib/services/furni-asset-dirs"; import { 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 * process-level async mutex so concurrent imports in the same Node process