/** * Classification of `items_base` rows so the catalog audit and repairs know * exactly which items are expected to ship normal furni assets (`.nitro` * bundles + `_icon.png` files) and which ones are special: * * - **badge** — stored with type `b`. Badges use album1584 `.gif` files and * never ship furni `.nitro`/`_icon.png` assets. They are awarded, not * purchased through the catalog. * - **pet** — pet purchase items (`a0 pet`, interaction `pet`). Pet * assets live in the pet asset pipeline (`public/nitro-assets/bundled/pet`), * not the furniture bundle dirs. * - **effect / bot / sticky** — system items with no furniture visuals at all. * - **furni** — everything else; must ship a `.nitro` and `_icon.png`. */ export type CatalogItemKind = | "badge" | "pet" | "effect" | "bot" | "sticky" | "furni"; export interface CatalogItemLike { type: string; item_name: string; interaction_type?: string | null; } /** Badges are stored in `items_base` with type `b`. */ export function isBadgeType(type: string): boolean { return type.trim().toLowerCase() === "b"; } /** Pet purchase items use classnames like `a0 pet0` … `a0 pet80`. */ export function isPetClassname(itemName: string): boolean { return /^a0\s*pet\d+$/i.test(itemName.trim()); } /** Pet purchase items use `pet` as their interaction type. */ export function isPetInteraction(interactionType?: string | null): boolean { return /^pet\d+$/i.test((interactionType ?? "").trim()); } /** Classify an `items_base` row into a catalog item kind. */ export function classifyCatalogItem(item: CatalogItemLike): CatalogItemKind { const name = item.item_name.trim(); if (isBadgeType(item.type)) return "badge"; if (isPetClassname(name) || isPetInteraction(item.interaction_type)) { return "pet"; } if (/^avatar_effect/i.test(name)) return "effect"; if (/^(bot_|rentable_bot_)/i.test(name)) return "bot"; if (/^post[._]?it/i.test(name)) return "sticky"; return "furni"; } /** Kinds that never ship furni `.nitro` / `_icon.png` assets. */ export const NO_ASSET_KINDS: ReadonlySet = new Set([ "badge", "pet", "effect", "bot", "sticky", ]); /** Kinds that are not purchasable furniture and should not get catalog entries. */ export const NO_CATALOG_KINDS: ReadonlySet = new Set([ "badge", "effect", "bot", "sticky", ]); /** * Asset file names for a classname, mirroring how `importSingleFurni` writes * files. Returns `.nitro` and icon candidates (most specific first): * the exact classname, the star-variant base, the star→underscore name and the * dot→underscore name (`post.it` → `post_it.nitro`). */ export function assetNameCandidates(classname: string): { nitro: string[]; icon: string[]; } { const unique = (names: string[]): string[] => { const seen = new Set(); const out: string[] = []; for (const n of names) { if (!n || seen.has(n)) continue; seen.add(n); out.push(n); } return out; }; const starBase = classname.split("*")[0]; const starUnderscore = classname.replace(/\*/g, "_"); const dotUnderscore = classname.replace(/\./g, "_"); return { nitro: unique([classname, starBase, dotUnderscore, starUnderscore]).map( (n) => `${n}.nitro`, ), icon: unique([classname, starUnderscore, dotUnderscore, starBase]).map( (n) => `${n}_icon.png`, ), }; }