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
@@ -17,6 +17,14 @@ import {
|
||||
repairOrphanedCatalog,
|
||||
} from "./catalog-repair";
|
||||
import { ensureDirectories } from "./furni-import";
|
||||
import {
|
||||
assetNameCandidates,
|
||||
type CatalogItemKind,
|
||||
classifyCatalogItem,
|
||||
isBadgeType,
|
||||
NO_ASSET_KINDS,
|
||||
NO_CATALOG_KINDS,
|
||||
} from "./item-kind";
|
||||
import { repairMissingIcons } from "./repair-icons";
|
||||
import { repairMissingNitros } from "./repair-nitros";
|
||||
|
||||
@@ -161,7 +169,10 @@ export async function runCatalogAudit(
|
||||
]);
|
||||
const iconSet = new Set(iconFiles);
|
||||
const nitroSet = new Set(nitroFiles);
|
||||
const badgeSet = new Set(
|
||||
// Badge detection: `items_base.type = 'b'` is authoritative. The album1584
|
||||
// `.gif` listing is kept as a fallback for hotels that store badges with a
|
||||
// non-standard type.
|
||||
const badgeGifSet = new Set(
|
||||
badgeGifFiles.filter((f) => f.endsWith(".gif")).map((f) => f.slice(0, -4)),
|
||||
);
|
||||
|
||||
@@ -172,16 +183,19 @@ export async function runCatalogAudit(
|
||||
item_name: string;
|
||||
public_name: string;
|
||||
type: string;
|
||||
interaction_type: string;
|
||||
}>;
|
||||
try {
|
||||
const [rows] = (await db.execute(sql`
|
||||
SELECT id, item_name, public_name, type FROM items_base ORDER BY id
|
||||
SELECT id, item_name, public_name, type, interaction_type
|
||||
FROM items_base ORDER BY id
|
||||
`)) as unknown as [
|
||||
Array<{
|
||||
id: number;
|
||||
item_name: string;
|
||||
public_name: string;
|
||||
type: string;
|
||||
interaction_type: string;
|
||||
}>,
|
||||
unknown,
|
||||
];
|
||||
@@ -194,6 +208,21 @@ export async function runCatalogAudit(
|
||||
return;
|
||||
}
|
||||
|
||||
// item_name → catalog item kind (badge / pet / effect / bot / sticky / furni).
|
||||
const kindByName = new Map<string, CatalogItemKind>();
|
||||
for (const item of items) {
|
||||
let kind = classifyCatalogItem(item);
|
||||
if (
|
||||
kind === "furni" &&
|
||||
(isBadgeType(item.type) || badgeGifSet.has(item.item_name))
|
||||
) {
|
||||
kind = "badge";
|
||||
}
|
||||
kindByName.set(item.item_name, kind);
|
||||
}
|
||||
const kindOf = (name: string): CatalogItemKind =>
|
||||
kindByName.get(name) ?? "furni";
|
||||
|
||||
onEvent?.({ type: "items_loaded", total: items.length });
|
||||
|
||||
onEvent?.({ type: "progress", message: "Loading catalog_items…" });
|
||||
@@ -250,6 +279,10 @@ export async function runCatalogAudit(
|
||||
const missingCatalogEntries: MissingCatalogEntry[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const kind = kindOf(item.item_name);
|
||||
// Badges and system items (effects, bots, sticky notes) are awarded or
|
||||
// system-managed, not catalog-purchasable — don't flag or auto-add them.
|
||||
if (NO_CATALOG_KINDS.has(kind)) continue;
|
||||
if (!itemIdsWithCatalog.has(item.id)) {
|
||||
issues.push({
|
||||
type: "missing_catalog",
|
||||
@@ -258,11 +291,12 @@ export async function runCatalogAudit(
|
||||
itemId: item.id,
|
||||
message: `"${item.item_name}" (ID ${item.id}) has no catalog_items entry — not purchasable`,
|
||||
});
|
||||
// Badge items use .gif files, not .nitro bundles.
|
||||
if (
|
||||
!badgeSet.has(item.item_name) &&
|
||||
nitroSet.has(`${item.item_name}.nitro`)
|
||||
) {
|
||||
const hasNitro = assetNameCandidates(item.item_name).nitro.some((n) =>
|
||||
nitroSet.has(n),
|
||||
);
|
||||
// Pets ship their assets through the pet pipeline, not as furni
|
||||
// .nitro bundles, so they can get a catalog entry regardless.
|
||||
if (kind === "pet" || hasNitro) {
|
||||
missingCatalogEntries.push({
|
||||
classname: item.item_name,
|
||||
itemId: item.id,
|
||||
@@ -331,9 +365,13 @@ export async function runCatalogAudit(
|
||||
const missingNitroClassnames: string[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
// Badge items use .gif files, not .nitro bundles.
|
||||
if (badgeSet.has(item.item_name)) continue;
|
||||
if (!nitroSet.has(`${item.item_name}.nitro`)) {
|
||||
// Badges, pets, effects, bots and sticky notes never ship a furni
|
||||
// .nitro bundle — skip the asset check for them.
|
||||
if (NO_ASSET_KINDS.has(kindOf(item.item_name))) continue;
|
||||
const hasNitro = assetNameCandidates(item.item_name).nitro.some((n) =>
|
||||
nitroSet.has(n),
|
||||
);
|
||||
if (!hasNitro) {
|
||||
missingNitroClassnames.push(item.item_name);
|
||||
issues.push({
|
||||
type: "missing_nitro",
|
||||
@@ -358,10 +396,13 @@ 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 in album1584), not .png icon files.
|
||||
if (badgeSet.has(item.item_name)) continue;
|
||||
const iconFileName = `${item.item_name}_icon.png`;
|
||||
if (!iconSet.has(iconFileName)) {
|
||||
// Badges use album1584 .gif icons; pets, effects, bots and sticky notes
|
||||
// never ship a furni icon file — skip the asset check for them.
|
||||
if (NO_ASSET_KINDS.has(kindOf(item.item_name))) continue;
|
||||
const hasIcon = assetNameCandidates(item.item_name).icon.some((n) =>
|
||||
iconSet.has(n),
|
||||
);
|
||||
if (!hasIcon) {
|
||||
missingIconClassnames.push(item.item_name);
|
||||
issues.push({
|
||||
type: "missing_icon",
|
||||
@@ -412,7 +453,10 @@ export async function runCatalogAudit(
|
||||
// and drop the ones the repair fixed from the issue list.
|
||||
const stillMissing: string[] = [];
|
||||
for (const classname of missingIconClassnames) {
|
||||
if (!iconSet.has(`${classname}_icon.png`)) {
|
||||
const hasIcon = assetNameCandidates(classname).icon.some((n) =>
|
||||
iconSet.has(n),
|
||||
);
|
||||
if (!hasIcon) {
|
||||
stillMissing.push(classname);
|
||||
}
|
||||
}
|
||||
@@ -467,7 +511,10 @@ export async function runCatalogAudit(
|
||||
// and drop the ones the repair fixed from the issue list.
|
||||
const stillMissing: string[] = [];
|
||||
for (const classname of missingNitroClassnames) {
|
||||
if (!nitroSet.has(`${classname}.nitro`)) {
|
||||
const hasNitro = assetNameCandidates(classname).nitro.some((n) =>
|
||||
nitroSet.has(n),
|
||||
);
|
||||
if (!hasNitro) {
|
||||
stillMissing.push(classname);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user