fix: precise item classification in catalog audit and repair
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:
1 parent
b1a1e5125c
commit
d587749b50
5 files changed
+445
-36
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user