fix: precise item classification in catalog audit and repair
CI / check (push) Successful in 23s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m3s

Detect badges by items_base.type='b' (authoritative, album gifs as
fallback), recognize pet/animals (a0 pet<N>, pet<N> interaction) and
system items (effects, bots, sticky notes), and resolve asset names for
dot/star classname variants so the audit no longer floods with false
missing nitro/icon errors and repair skips non-furni items.
This commit is contained in:
openhands committed 2026-08-04 11:07:04 +02:00
1 parent b1a1e5125c
commit d587749b50
5 files changed
+445 -36

No files matched your search

+43 -10
View File
@@ -15,6 +15,7 @@ import {
getGamedataRoot,
} from "@/lib/services/furni-asset-dirs";
import { downloadFile } from "@/lib/services/import/core/download";
import { classifyCatalogItem, NO_ASSET_KINDS } from "@/lib/services/item-kind";
import { ensureDirectories } from "./furni-import";
const REPAIR_CONCURRENCY = 6;
@@ -141,28 +142,44 @@ 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<string> | null = null;
// Badges and other special items (pets, effects, bots, sticky notes) never
// ship furni `_icon.png` files — resolve the skip set from the DB so badge
// detection is precise (`items_base.type = 'b'`) instead of relying only on
// which .gif files happen to exist in the album directory.
let skipSet: Set<string> | null = null;
try {
const gamedataRoot = await getGamedataRoot();
if (gamedataRoot) {
const badgeFiles = await fs
.readdir(path.join(gamedataRoot, "album1584"))
.catch(() => [] as string[]);
badgeSet = new Set(
const albumBadgeSet = new Set(
badgeFiles.filter((f) => f.endsWith(".gif")).map((f) => f.slice(0, -4)),
);
skipSet = albumBadgeSet;
}
} catch {
badgeSet = null;
skipSet = null;
}
let items: Array<{ id: number; item_name: string }>;
let items: Array<{
id: number;
item_name: string;
type: string;
interaction_type: string;
}>;
try {
const [rows] = (await db.execute(sql`
SELECT id, item_name FROM items_base ORDER BY id
`)) as unknown as [Array<{ id: number; item_name: string }>, unknown];
SELECT id, item_name, type, interaction_type FROM items_base ORDER BY id
`)) as unknown as [
Array<{
id: number;
item_name: string;
type: string;
interaction_type: string;
}>,
unknown,
];
items = rows;
} catch (err) {
onEvent?.({
@@ -172,13 +189,24 @@ export async function repairMissingIcons(
return { succeeded: 0, failed: 0, skipped: 0 };
}
// Build the authoritative skip set from the DB, supplemented by album .gifs.
const dbSkipSet = new Set<string>();
for (const item of items) {
if (NO_ASSET_KINDS.has(classifyCatalogItem(item))) {
dbSkipSet.add(item.item_name);
}
}
for (const code of skipSet ?? []) {
dbSkipSet.add(code);
}
const total = items.length;
onEvent?.({ type: "started", total });
const processItem = async (classname: string) => {
// Badge items that only exist as .gif files (in album1584) don't have
// Special items (badges, pets, effects, bots, sticky notes) don't have
// .png icon files to repair. Skip them.
if (badgeSet?.has(classname)) {
if (dbSkipSet.has(classname)) {
return { status: "skipped" } as const;
}
const fileName = `${classname}_icon.png`;
@@ -269,6 +297,11 @@ export async function repairMissingIcons(
`${classname.replace(/\*/g, "_")}_icon.png`,
]
: []),
// Classnames with dots (`post.it`) are stored on disk with
// underscores (`post_it_icon.png`).
...(classname.includes(".")
? [`${classname.replace(/\./g, "_")}_icon.png`]
: []),
];
for (const iconName of iconCandidates) {