diff --git a/src/lib/services/catalog-audit.ts b/src/lib/services/catalog-audit.ts index d2e4aad7..69a55f4d 100644 --- a/src/lib/services/catalog-audit.ts +++ b/src/lib/services/catalog-audit.ts @@ -1,9 +1,13 @@ import { readdir } from "node:fs/promises"; +import path from "node:path"; import { sql } from "drizzle-orm"; import { db } from "@/lib/db"; import { fetchSourceFurnidata } from "@/lib/services/clone-import"; import { listSources } from "@/lib/services/clone-sources"; -import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs"; +import { + getFurniAssetDirs, + getGamedataRoot, +} from "@/lib/services/furni-asset-dirs"; import { applyCatalogSql, generateCatalogSql, @@ -140,12 +144,26 @@ export async function runCatalogAudit( return; } - const [iconFiles, nitroFiles] = await Promise.all([ + const [iconFiles, nitroFiles, badgeGifFiles] = await Promise.all([ readdir(iconDir).catch(() => [] as string[]), readdir(nitroDir).catch(() => [] as string[]), + (async () => { + try { + const gamedataRoot = await getGamedataRoot(); + if (!gamedataRoot) return [] as string[]; + return await readdir(path.join(gamedataRoot, "album1584")).catch( + () => [] as string[], + ); + } catch { + return [] as string[]; + } + })(), ]); const iconSet = new Set(iconFiles); const nitroSet = new Set(nitroFiles); + const badgeSet = new Set( + badgeGifFiles.filter((f) => f.endsWith(".gif")).map((f) => f.slice(0, -4)), + ); onEvent?.({ type: "progress", message: "Loading items_base…" }); @@ -242,7 +260,7 @@ export async function runCatalogAudit( }); // Badge items use .gif files, not .nitro bundles. if ( - !item.item_name.startsWith("badge_") && + !badgeSet.has(item.item_name) && nitroSet.has(`${item.item_name}.nitro`) ) { missingCatalogEntries.push({ @@ -314,7 +332,7 @@ export async function runCatalogAudit( for (let i = 0; i < items.length; i++) { const item = items[i]; // Badge items use .gif files, not .nitro bundles. - if (item.item_name.startsWith("badge_")) continue; + if (badgeSet.has(item.item_name)) continue; if (!nitroSet.has(`${item.item_name}.nitro`)) { missingNitroClassnames.push(item.item_name); issues.push({ @@ -340,11 +358,10 @@ export async function runCatalogAudit( const missingIconClassnames: string[] = []; for (let i = 0; i < items.length; i++) { const item = items[i]; - // Badge items use .gif icons stored as badge__icon.png, - // not _icon.png like regular furni. + // Badge items use .gif icons (stored in album1584), not .png icon files. + if (badgeSet.has(item.item_name)) continue; const iconFileName = `${item.item_name}_icon.png`; - const badgeIconFileName = `badge_${item.item_name}_icon.png`; - if (!iconSet.has(iconFileName) && !iconSet.has(badgeIconFileName)) { + if (!iconSet.has(iconFileName)) { missingIconClassnames.push(item.item_name); issues.push({ type: "missing_icon", diff --git a/src/lib/services/repair-icons.ts b/src/lib/services/repair-icons.ts index c43f1552..65b25149 100644 --- a/src/lib/services/repair-icons.ts +++ b/src/lib/services/repair-icons.ts @@ -12,6 +12,7 @@ import { import { type FurniAssetWriteTargets, getFurniAssetWriteTargets, + getGamedataRoot, } from "@/lib/services/furni-asset-dirs"; import { downloadFile } from "@/lib/services/import/core/download"; import { ensureDirectories } from "./furni-import"; @@ -140,6 +141,23 @@ export async function repairMissingIcons( ...targets.mirrorDirs.map((d) => d.nitroDir), ]); + // Load known badge codes from the album directory so we can skip badge + // items — they use .gif files, not .png icons. + let badgeSet: Set | null = null; + try { + const gamedataRoot = await getGamedataRoot(); + if (gamedataRoot) { + const badgeFiles = await fs + .readdir(path.join(gamedataRoot, "album1584")) + .catch(() => [] as string[]); + badgeSet = new Set( + badgeFiles.filter((f) => f.endsWith(".gif")).map((f) => f.slice(0, -4)), + ); + } + } catch { + badgeSet = null; + } + let items: Array<{ id: number; item_name: string }>; try { const [rows] = (await db.execute(sql` @@ -158,6 +176,11 @@ export async function repairMissingIcons( onEvent?.({ type: "started", total }); const processItem = async (classname: string) => { + // Badge items that only exist as .gif files (in album1584) don't have + // .png icon files to repair. Skip them. + if (badgeSet?.has(classname)) { + return { status: "skipped" } as const; + } const fileName = `${classname}_icon.png`; // Badge items store icons as badge__icon.png (the classname already diff --git a/src/lib/services/repair-nitros.ts b/src/lib/services/repair-nitros.ts index 556dbfdf..df9bed65 100644 --- a/src/lib/services/repair-nitros.ts +++ b/src/lib/services/repair-nitros.ts @@ -11,6 +11,7 @@ import { import { type FurniAssetWriteTargets, getFurniAssetWriteTargets, + getGamedataRoot, } from "@/lib/services/furni-asset-dirs"; import { downloadFile } from "@/lib/services/import/core/download"; import { parseNitroBundle } from "@/lib/services/swf/nitro-builder"; @@ -136,6 +137,23 @@ export async function repairMissingNitros( ...targets.mirrorDirs.map((d) => d.nitroDir), ]); + // Load known badge codes from the album directory so we can skip badge + // items — they use .gif files, not .nitro bundles. + let badgeSet: Set | null = null; + try { + const gamedataRoot = await getGamedataRoot(); + if (gamedataRoot) { + const badgeFiles = await fs + .readdir(path.join(gamedataRoot, "album1584")) + .catch(() => [] as string[]); + badgeSet = new Set( + badgeFiles.filter((f) => f.endsWith(".gif")).map((f) => f.slice(0, -4)), + ); + } + } catch { + badgeSet = null; + } + let items: Array<{ id: number; item_name: string }>; try { const [rows] = (await db.execute(sql` @@ -155,7 +173,7 @@ export async function repairMissingNitros( const processItem = async (classname: string) => { // Badge items use .gif files, not .nitro bundles. - if (classname.startsWith("badge_")) { + if (badgeSet?.has(classname) ?? false) { return { status: "skipped" } as const; } const fileName = `${classname}.nitro`;