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
@@ -14,6 +14,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 { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
import { ensureDirectories } from "./furni-import";
@@ -137,28 +138,43 @@ 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<string> | null = null;
// Badges and other special items (pets, effects, bots, sticky notes) never
// ship furni `.nitro` bundles — 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 albumBadgeSet: 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(
albumBadgeSet = new Set(
badgeFiles.filter((f) => f.endsWith(".gif")).map((f) => f.slice(0, -4)),
);
}
} catch {
badgeSet = null;
albumBadgeSet = 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?.({
@@ -168,12 +184,24 @@ export async function repairMissingNitros(
return { succeeded: 0, failed: 0, skipped: 0 };
}
// Build the authoritative skip set from the DB, supplemented by album .gifs.
const skipSet = new Set<string>();
for (const item of items) {
if (NO_ASSET_KINDS.has(classifyCatalogItem(item))) {
skipSet.add(item.item_name);
}
}
for (const code of albumBadgeSet ?? []) {
skipSet.add(code);
}
const total = items.length;
onEvent?.({ type: "started", total });
const processItem = async (classname: string) => {
// Badge items use .gif files, not .nitro bundles.
if (badgeSet?.has(classname) ?? false) {
// Special items (badges, pets, effects, bots, sticky notes) don't have
// furni .nitro bundles to repair. Skip them.
if (skipSet.has(classname)) {
return { status: "skipped" } as const;
}
const fileName = `${classname}.nitro`;
@@ -229,6 +257,11 @@ export async function repairMissingNitros(
`${classname.replace(/\*/g, "_")}.nitro`,
]
: []),
// Classnames with dots (`post.it`) are stored on disk with
// underscores (`post_it.nitro`).
...(classname.includes(".")
? [`${classname.replace(/\./g, "_")}.nitro`]
: []),
];
for (const source of sources) {