Backfill icons too in the Gamedata bundle sync
CI / check (push) Successful in 38s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m28s

Extend the post-import backfill to also copy missing _icon.png files
into /var/www/Gamedata/icons (in addition to nitros into
bundled/furniture) so all imported furniture shows an icon in-game.
Report copied nitros and icons separately in the API/SSE responses.
This commit is contained in:
openhands committed 2026-08-18 20:35:48 +02:00
1 parent bcd24bfca4
commit b4021f6b42
4 files changed
+99 -54

No files matched your search

@@ -15,7 +15,7 @@ import {
downloadFile, downloadFile,
ensureDirectories, ensureDirectories,
ensureFurniOwnership, ensureFurniOwnership,
syncNitrosToGamedataBundle, syncAssetsToGamedataBundle,
} 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 { rcon } from "@/lib/services/rcon"; import { rcon } from "@/lib/services/rcon";
@@ -262,8 +262,8 @@ export const POST = withAdmin(
// Keep regenerated assets readable by the web server // Keep regenerated assets readable by the web server
const ownership = await ensureFurniOwnership(); const ownership = await ensureFurniOwnership();
// Backfill any missing nitros into the emulator's Gamedata bundle // Backfill any missing nitros and icons into the emulator's Gamedata bundle
const nitrosSynced = await syncNitrosToGamedataBundle(); const assetsSynced = await syncAssetsToGamedataBundle();
send({ send({
type: "regen_complete", type: "regen_complete",
@@ -272,7 +272,8 @@ export const POST = withAdmin(
skipped, skipped,
furniDataAdded: furniDataEntries.length, furniDataAdded: furniDataEntries.length,
ownershipFixed: ownership.fixed, ownershipFixed: ownership.fixed,
nitrosSynced: nitrosSynced.copied, nitrosSynced: assetsSynced.copiedNitros,
iconsSynced: assetsSynced.copiedIcons,
duration: Date.now() - startTime, duration: Date.now() - startTime,
}); });
@@ -9,7 +9,7 @@ import {
ensureFurniOwnership, ensureFurniOwnership,
importSingleFurni, importSingleFurni,
reconcileImportedOfferIds, reconcileImportedOfferIds,
syncNitrosToGamedataBundle, syncAssetsToGamedataBundle,
} 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";
@@ -218,10 +218,13 @@ export const POST = withAdmin(
let offerIdsFixed = 0; let offerIdsFixed = 0;
let ownershipFixed: string[] = []; let ownershipFixed: string[] = [];
let nitrosSynced: string[] = []; let nitrosSynced: string[] = [];
let iconsSynced: string[] = [];
try { try {
offerIdsFixed = (await reconcileImportedOfferIds()).fixed; offerIdsFixed = (await reconcileImportedOfferIds()).fixed;
ownershipFixed = (await ensureFurniOwnership()).fixed; ownershipFixed = (await ensureFurniOwnership()).fixed;
nitrosSynced = (await syncNitrosToGamedataBundle()).copied; const assetsSynced = await syncAssetsToGamedataBundle();
nitrosSynced = assetsSynced.copiedNitros;
iconsSynced = assetsSynced.copiedIcons;
} catch (err) { } catch (err) {
console.warn( console.warn(
"[import-furni] Post-import reconcile/ownership failed:", "[import-furni] Post-import reconcile/ownership failed:",
@@ -237,6 +240,7 @@ export const POST = withAdmin(
offerIdsFixed, offerIdsFixed,
ownershipFixed, ownershipFixed,
nitrosSynced, nitrosSynced,
iconsSynced,
duration: Date.now() - startTime, duration: Date.now() - startTime,
}); });
+8 -6
View File
@@ -22,7 +22,7 @@ import {
importSingleFurni, importSingleFurni,
reconcileImportedOfferIds, reconcileImportedOfferIds,
resetCatalogPageCache, resetCatalogPageCache,
syncNitrosToGamedataBundle, syncAssetsToGamedataBundle,
} 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";
@@ -413,7 +413,7 @@ export const POST = withAdmin(
const ownership = await ensureFurniOwnership(); const ownership = await ensureFurniOwnership();
// Backfill any missing nitros into the emulator's Gamedata bundle // Backfill any missing nitros into the emulator's Gamedata bundle
const nitrosSynced = await syncNitrosToGamedataBundle(); const assetsSynced = await syncAssetsToGamedataBundle();
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;
@@ -425,7 +425,8 @@ export const POST = withAdmin(
results, results,
offerIdsFixed: reconciled.fixed, offerIdsFixed: reconciled.fixed,
ownershipFixed: ownership.fixed, ownershipFixed: ownership.fixed,
nitrosSynced: nitrosSynced.copied, nitrosSynced: assetsSynced.copiedNitros,
iconsSynced: assetsSynced.copiedIcons,
}); });
} }
@@ -481,8 +482,8 @@ 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 // Backfill any missing nitros and icons into the emulator's Gamedata bundle
const nitrosSynced = await syncNitrosToGamedataBundle(); const assetsSynced = await syncAssetsToGamedataBundle();
logAudit({ logAudit({
userId: ctx.session.user.id, userId: ctx.session.user.id,
@@ -506,7 +507,8 @@ 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, nitrosSynced: assetsSynced.copiedNitros,
iconsSynced: assetsSynced.copiedIcons,
spriteCount: result.spriteCount, spriteCount: result.spriteCount,
warnings: result.warnings, warnings: result.warnings,
}); });
+80 -42
View File
@@ -347,53 +347,91 @@ export async function ensureFurniOwnership(): Promise<{ fixed: string[] }> {
} }
} }
/** /** Copy a single missing file from source dir into target dir; logs on failure. */
* Copy every .nitro file that is missing from the emulator's Gamedata async function copyIfMissing(
* bundle directory (/var/www/Gamedata/bundled/furniture) so all imported sourceDir: string,
* furniture stays renderable in-game even when a mirror write was skipped. targetDir: string,
*/ entry: string,
export async function syncNitrosToGamedataBundle(): Promise<{ copied: string[],
copied: string[]; ): Promise<void> {
}> { const dest = path.join(targetDir, entry);
if (process.platform === "win32") return { copied: [] }; if (existsSync(dest)) return;
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 { try {
await fs.mkdir(targetDir, { recursive: true }); await fs.copyFile(path.join(sourceDir, entry), dest);
} catch { copied.push(entry);
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) { } catch (err) {
logger.warn("[import-furni] nitro sync scan failed", { logger.warn("[import-furni] gamedata bundle sync copy failed", {
entry,
error: (err as Error).message, error: (err as Error).message,
}); });
} }
return { copied }; }
/** Scan a source dir and copy every file that is missing from the target dir. */
async function syncDirMissingFiles(
sourceDir: string,
targetDir: string,
predicate: (entry: string) => boolean,
): Promise<string[]> {
if (!existsSync(sourceDir)) return [];
try {
await fs.mkdir(targetDir, { recursive: true });
} catch {
return [];
}
const copied: string[] = [];
try {
const entries = await fs.readdir(sourceDir);
for (const entry of entries) {
if (!predicate(entry)) continue;
await copyIfMissing(sourceDir, targetDir, entry, copied);
}
} catch (err) {
logger.warn("[import-furni] gamedata bundle sync scan failed", {
error: (err as Error).message,
});
}
return copied;
}
/**
* Backfill any nitro and icon files missing from the emulator's Gamedata
* bundle (/var/www/Gamedata/bundled/furniture and /var/www/Gamedata/icons)
* so all imported furniture stays renderable in-game even when a mirror
* write was skipped.
*/
export async function syncAssetsToGamedataBundle(): Promise<{
copiedNitros: string[];
copiedIcons: string[];
}> {
if (process.platform === "win32")
return { copiedNitros: [], copiedIcons: [] };
const gamedataRoot = await getGamedataRoot();
if (!gamedataRoot) return { copiedNitros: [], copiedIcons: [] };
const { nitroDir, iconDir } = await getFurniAssetDirs();
const copiedNitros: string[] = [];
const copiedIcons: string[] = [];
const nitroTarget = path.join(gamedataRoot, "bundled/furniture");
if (nitroDir !== nitroTarget) {
const copied = await syncDirMissingFiles(nitroDir, nitroTarget, (entry) =>
entry.endsWith(".nitro"),
);
copiedNitros.push(...copied);
}
const iconTarget = path.join(gamedataRoot, "icons");
if (iconDir !== iconTarget) {
const copied = await syncDirMissingFiles(iconDir, iconTarget, (entry) =>
entry.endsWith("_icon.png"),
);
copiedIcons.push(...copied);
}
return { copiedNitros, copiedIcons };
} }
/** /**