revert fb8e77bb68
Local Build and Deploy / deploy (push) Successful in 1m11s
Local Build and Deploy / deploy (push) Successful in 1m11s
revert style: clean up code with prettier and eslint
This commit is contained in:
1 parent
27de078f54
commit
e85e4d74ea
378 files changed
+20710
-22428
No files matched your search
+288
-277
@@ -1,13 +1,17 @@
|
||||
import { existsSync, promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { appendFurniEntry, buildFurniEntry, removeFurniEntry } from "@/lib/services/furni-data";
|
||||
import type { HabboItFurniEntry } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { lookupHabboIt } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { existsSync, promises as fs } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { appendFurniEntry, buildFurniEntry, removeFurniEntry } from '@/lib/services/furni-data'
|
||||
import type { HabboItFurniEntry } from '@/lib/services/habbo-furnidata-cache'
|
||||
import { lookupHabboIt } from '@/lib/services/habbo-furnidata-cache'
|
||||
|
||||
// Re-export the download helpers now owned by the shared import core.
|
||||
// Existing furni callers (and furni-repair) keep importing them from here.
|
||||
export { downloadFile, validatePngBytes, validateSwfBytes } from "@/lib/services/import/core/download";
|
||||
export {
|
||||
downloadFile,
|
||||
validatePngBytes,
|
||||
validateSwfBytes,
|
||||
} from '@/lib/services/import/core/download'
|
||||
|
||||
import {
|
||||
DEFAULT_FURNI_ICON_DIR,
|
||||
@@ -15,81 +19,83 @@ import {
|
||||
DEFAULT_FURNI_SWF_DIR,
|
||||
getFurniAssetDirs,
|
||||
getFurniAssetWriteTargets,
|
||||
} from "@/lib/services/furni-asset-dirs";
|
||||
import { downloadFile } from "@/lib/services/import/core/download";
|
||||
import type { ConversionResult } from "@/lib/services/swf-to-nitro";
|
||||
import { convertSwfToNitro, extractIconFromSwf } from "@/lib/services/swf-to-nitro";
|
||||
import type { ImportSingleResult } from "@/types/furni";
|
||||
} from '@/lib/services/furni-asset-dirs'
|
||||
import { downloadFile } from '@/lib/services/import/core/download'
|
||||
import type { ConversionResult } from '@/lib/services/swf-to-nitro'
|
||||
import { convertSwfToNitro, extractIconFromSwf } from '@/lib/services/swf-to-nitro'
|
||||
import type { ImportSingleResult } from '@/types/furni'
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────────────
|
||||
|
||||
// turbopackIgnore hints: this module is reached by App Router routes; without
|
||||
// them Turbopack's NFT tracer marks the whole project as a dependency because
|
||||
// it can't prove the dynamic suffixes (file names) are bounded to the prefix.
|
||||
export const FURNI_ICON_DIR = DEFAULT_FURNI_ICON_DIR;
|
||||
export const FURNI_SWF_DIR = DEFAULT_FURNI_SWF_DIR;
|
||||
export const FURNI_NITRO_DIR = DEFAULT_FURNI_NITRO_DIR;
|
||||
export const FURNI_ICON_DIR = DEFAULT_FURNI_ICON_DIR
|
||||
export const FURNI_SWF_DIR = DEFAULT_FURNI_SWF_DIR
|
||||
export const FURNI_NITRO_DIR = DEFAULT_FURNI_NITRO_DIR
|
||||
|
||||
export const IMPORTED_PAGE_CAPTION_SAVE = "imported_furni";
|
||||
export const IMPORTED_PAGE_CAPTION = "Imported Furniture";
|
||||
export const IMPORTED_PAGE_CAPTION_SAVE = 'imported_furni'
|
||||
export const IMPORTED_PAGE_CAPTION = 'Imported Furniture'
|
||||
|
||||
// Catalog sub-page definitions (key = internal id used for caption_save `imp_<key>`)
|
||||
export const CATEGORY_PAGE: Record<string, { label: string; icon: number; order: number }> = {
|
||||
credits: { label: "Credits & Exchange", icon: 263, order: 1 },
|
||||
rares: { label: "Rares", icon: 11, order: 2 },
|
||||
wired: { label: "Wired", icon: 1, order: 3 },
|
||||
nft: { label: "NFT", icon: 12, order: 4 },
|
||||
seasonal: { label: "Seasonal", icon: 10, order: 5 },
|
||||
music: { label: "Music & Sound", icon: 13, order: 6 },
|
||||
clothing: { label: "Clothing", icon: 3, order: 7 },
|
||||
builders: { label: "Builders Club", icon: 7, order: 8 },
|
||||
walls: { label: "Wall Items", icon: 5, order: 9 },
|
||||
promo: { label: "Promo & Custom", icon: 2, order: 10 },
|
||||
trophies: { label: "Trophies", icon: 6, order: 11 },
|
||||
pets: { label: "Pets", icon: 4, order: 12 },
|
||||
room: { label: "Room & Building", icon: 8, order: 13 },
|
||||
other: { label: "Other", icon: 1, order: 99 },
|
||||
};
|
||||
credits: { label: 'Credits & Exchange', icon: 263, order: 1 },
|
||||
rares: { label: 'Rares', icon: 11, order: 2 },
|
||||
wired: { label: 'Wired', icon: 1, order: 3 },
|
||||
nft: { label: 'NFT', icon: 12, order: 4 },
|
||||
seasonal: { label: 'Seasonal', icon: 10, order: 5 },
|
||||
music: { label: 'Music & Sound', icon: 13, order: 6 },
|
||||
clothing: { label: 'Clothing', icon: 3, order: 7 },
|
||||
builders: { label: 'Builders Club', icon: 7, order: 8 },
|
||||
walls: { label: 'Wall Items', icon: 5, order: 9 },
|
||||
promo: { label: 'Promo & Custom', icon: 2, order: 10 },
|
||||
trophies: { label: 'Trophies', icon: 6, order: 11 },
|
||||
pets: { label: 'Pets', icon: 4, order: 12 },
|
||||
room: { label: 'Room & Building', icon: 8, order: 13 },
|
||||
other: { label: 'Other', icon: 1, order: 99 },
|
||||
}
|
||||
|
||||
// Classname prefix → category key (order matters: first match wins)
|
||||
export const CLASSNAME_RULES: Array<{ test: (cn: string, type: string) => boolean; cat: string }> = [
|
||||
{ test: (cn) => /^CF_/i.test(cn) || /^exe_/i.test(cn) || cn === "credit", cat: "credits" },
|
||||
{ test: (cn) => /^rare_/i.test(cn), cat: "rares" },
|
||||
{ test: (cn) => /^wf_/i.test(cn), cat: "wired" },
|
||||
{ test: (cn) => /^nft_/i.test(cn), cat: "nft" },
|
||||
{
|
||||
test: (cn) => /^(xmas|hween|easter|val_|valentine|hw_|newyear|summer|carnival)/i.test(cn),
|
||||
cat: "seasonal",
|
||||
},
|
||||
{ test: (cn) => /^(trax|jukebox|sound|music)/i.test(cn), cat: "music" },
|
||||
{ test: (cn) => /^(clothing|clothes)/i.test(cn), cat: "clothing" },
|
||||
{ test: (cn) => /^bc_/i.test(cn), cat: "builders" },
|
||||
{ test: (cn) => /^(poster|wall_|window_|wallpaper)/i.test(cn), cat: "walls" },
|
||||
{ test: (_cn, type) => type === "wallitem" || type === "i", cat: "walls" },
|
||||
{ test: (cn) => /^(ads_|cstm_)/i.test(cn), cat: "promo" },
|
||||
{ test: (cn) => /^(trophy|gold_|silver_|bronze_)/i.test(cn), cat: "trophies" },
|
||||
{
|
||||
test: (cn) => /^(pet|dog|cat_|bird|horse|pig|monkey|frog|dragon|bear_|spider|bunny)/i.test(cn),
|
||||
cat: "pets",
|
||||
},
|
||||
{ test: (cn) => /^(room_|floor_|tile_|carpet|rug|door_|gate_|stair)/i.test(cn), cat: "room" },
|
||||
];
|
||||
export const CLASSNAME_RULES: Array<{ test: (cn: string, type: string) => boolean; cat: string }> =
|
||||
[
|
||||
{ test: (cn) => /^CF_/i.test(cn) || /^exe_/i.test(cn) || cn === 'credit', cat: 'credits' },
|
||||
{ test: (cn) => /^rare_/i.test(cn), cat: 'rares' },
|
||||
{ test: (cn) => /^wf_/i.test(cn), cat: 'wired' },
|
||||
{ test: (cn) => /^nft_/i.test(cn), cat: 'nft' },
|
||||
{
|
||||
test: (cn) => /^(xmas|hween|easter|val_|valentine|hw_|newyear|summer|carnival)/i.test(cn),
|
||||
cat: 'seasonal',
|
||||
},
|
||||
{ test: (cn) => /^(trax|jukebox|sound|music)/i.test(cn), cat: 'music' },
|
||||
{ test: (cn) => /^(clothing|clothes)/i.test(cn), cat: 'clothing' },
|
||||
{ test: (cn) => /^bc_/i.test(cn), cat: 'builders' },
|
||||
{ test: (cn) => /^(poster|wall_|window_|wallpaper)/i.test(cn), cat: 'walls' },
|
||||
{ test: (_cn, type) => type === 'wallitem' || type === 'i', cat: 'walls' },
|
||||
{ test: (cn) => /^(ads_|cstm_)/i.test(cn), cat: 'promo' },
|
||||
{ test: (cn) => /^(trophy|gold_|silver_|bronze_)/i.test(cn), cat: 'trophies' },
|
||||
{
|
||||
test: (cn) =>
|
||||
/^(pet|dog|cat_|bird|horse|pig|monkey|frog|dragon|bear_|spider|bunny)/i.test(cn),
|
||||
cat: 'pets',
|
||||
},
|
||||
{ test: (cn) => /^(room_|floor_|tile_|carpet|rug|door_|gate_|stair)/i.test(cn), cat: 'room' },
|
||||
]
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
export async function ensureDirectories(): Promise<void> {
|
||||
const targets = await getFurniAssetWriteTargets();
|
||||
const targets = await getFurniAssetWriteTargets()
|
||||
const dirs = [
|
||||
targets.swfDir,
|
||||
targets.iconDir,
|
||||
targets.nitroDir,
|
||||
...targets.mirrorDirs.flatMap((d) => [d.swfDir, d.iconDir, d.nitroDir]),
|
||||
];
|
||||
]
|
||||
await Promise.all(
|
||||
dirs
|
||||
.filter((dir) => !existsSync(/*turbopackIgnore: true*/ dir))
|
||||
.map((dir) => fs.mkdir(/*turbopackIgnore: true*/ dir, { recursive: true })),
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
async function mirrorFileToDirs(
|
||||
@@ -98,31 +104,31 @@ async function mirrorFileToDirs(
|
||||
dirs: string[],
|
||||
warnings?: string[],
|
||||
): Promise<void> {
|
||||
if (!existsSync(/*turbopackIgnore: true*/ sourcePath)) return;
|
||||
if (!existsSync(/*turbopackIgnore: true*/ sourcePath)) return
|
||||
|
||||
for (const dir of dirs) {
|
||||
const targetPath = path.join(/*turbopackIgnore: true*/ dir, fileName);
|
||||
const sourceKey = process.platform === "win32" ? sourcePath.toLowerCase() : sourcePath;
|
||||
const targetKey = process.platform === "win32" ? targetPath.toLowerCase() : targetPath;
|
||||
if (sourceKey === targetKey) continue;
|
||||
const targetPath = path.join(/*turbopackIgnore: true*/ dir, fileName)
|
||||
const sourceKey = process.platform === 'win32' ? sourcePath.toLowerCase() : sourcePath
|
||||
const targetKey = process.platform === 'win32' ? targetPath.toLowerCase() : targetPath
|
||||
if (sourceKey === targetKey) continue
|
||||
|
||||
try {
|
||||
await fs.mkdir(/*turbopackIgnore: true*/ dir, { recursive: true });
|
||||
await fs.copyFile(/*turbopackIgnore: true*/ sourcePath, /*turbopackIgnore: true*/ targetPath);
|
||||
await fs.mkdir(/*turbopackIgnore: true*/ dir, { recursive: true })
|
||||
await fs.copyFile(/*turbopackIgnore: true*/ sourcePath, /*turbopackIgnore: true*/ targetPath)
|
||||
} catch (err) {
|
||||
warnings?.push(`Mirror copy failed for ${fileName}: ${(err as Error).message}`);
|
||||
warnings?.push(`Mirror copy failed for ${fileName}: ${(err as Error).message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildItemsBaseEnrichmentUpdate(params: {
|
||||
id: number;
|
||||
dims: { x: number; y: number; z: number };
|
||||
cansiton: boolean;
|
||||
canlayon: boolean;
|
||||
canstandon: boolean;
|
||||
allowStack: string;
|
||||
interactionType: string;
|
||||
id: number
|
||||
dims: { x: number; y: number; z: number }
|
||||
cansiton: boolean
|
||||
canlayon: boolean
|
||||
canstandon: boolean
|
||||
allowStack: string
|
||||
interactionType: string
|
||||
}): { sql: string; values: Array<number | string> } {
|
||||
return {
|
||||
sql: `
|
||||
@@ -139,14 +145,14 @@ export function buildItemsBaseEnrichmentUpdate(params: {
|
||||
params.dims.x,
|
||||
params.dims.y,
|
||||
params.dims.z,
|
||||
params.cansiton ? "1" : "0",
|
||||
params.canlayon ? "1" : "0",
|
||||
params.canstandon ? "1" : "0",
|
||||
params.cansiton ? '1' : '0',
|
||||
params.canlayon ? '1' : '0',
|
||||
params.canstandon ? '1' : '0',
|
||||
params.allowStack,
|
||||
params.interactionType,
|
||||
params.id,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Get or create the root "Imported Furniture" parent page (no items directly on this page). */
|
||||
@@ -154,31 +160,31 @@ export async function getOrCreateImportedParentPage(): Promise<number> {
|
||||
const existing = await prisma.catalogPages.findFirst({
|
||||
where: { captionSave: IMPORTED_PAGE_CAPTION_SAVE },
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing) return existing.id;
|
||||
})
|
||||
if (existing) return existing.id
|
||||
|
||||
// Use raw SQL to avoid ENUM mismatch (page_layout, visible, enabled)
|
||||
// All string columns must be '' (not NULL) or the Java emulator may skip the page
|
||||
await prisma.$executeRaw`
|
||||
INSERT INTO catalog_pages (caption_save, caption, page_layout, parent_id, min_rank, order_num, icon_image, enabled, visible, includes, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser)
|
||||
VALUES (${IMPORTED_PAGE_CAPTION_SAVE}, ${IMPORTED_PAGE_CAPTION}, 'default_3x3', -1, '1', 9999, 263, '1', '1', '', '', '', '', '', '', '', '')
|
||||
`;
|
||||
`
|
||||
const created = await prisma.catalogPages.findFirst({
|
||||
where: { captionSave: IMPORTED_PAGE_CAPTION_SAVE },
|
||||
select: { id: true },
|
||||
});
|
||||
})
|
||||
if (!created) {
|
||||
throw new Error('Failed to create "Imported Furniture" parent catalog page');
|
||||
throw new Error('Failed to create "Imported Furniture" parent catalog page')
|
||||
}
|
||||
return created.id;
|
||||
return created.id
|
||||
}
|
||||
|
||||
/** Determine the catalog category for a furniture item based on its classname and type. */
|
||||
export function classifyFurni(classname: string, itemType: string): string {
|
||||
for (const rule of CLASSNAME_RULES) {
|
||||
if (rule.test(classname, itemType)) return rule.cat;
|
||||
if (rule.test(classname, itemType)) return rule.cat
|
||||
}
|
||||
return "other";
|
||||
return 'other'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,44 +198,44 @@ export function classifyFurni(classname: string, itemType: string): string {
|
||||
* Returns { credits, points, pointsType }
|
||||
*/
|
||||
export function autoPriceFurni(classname: string): {
|
||||
credits: number;
|
||||
points: number;
|
||||
pointsType: number;
|
||||
credits: number
|
||||
points: number
|
||||
pointsType: number
|
||||
} {
|
||||
// CF_<number>_xxx → credit furniture, price = the credit value
|
||||
const cfMatch = classname.match(/^CF_(\d+)/i);
|
||||
const cfMatch = classname.match(/^CF_(\d+)/i)
|
||||
if (cfMatch) {
|
||||
return { credits: parseInt(cfMatch[1], 10), points: 0, pointsType: 0 };
|
||||
return { credits: parseInt(cfMatch[1], 10), points: 0, pointsType: 0 }
|
||||
}
|
||||
|
||||
// exe_<number> → exchange items (diamonds/duckets)
|
||||
const exeMatch = classname.match(/^exe_(\d+)/i);
|
||||
const exeMatch = classname.match(/^exe_(\d+)/i)
|
||||
if (exeMatch) {
|
||||
return { credits: parseInt(exeMatch[1], 10), points: 0, pointsType: 0 };
|
||||
return { credits: parseInt(exeMatch[1], 10), points: 0, pointsType: 0 }
|
||||
}
|
||||
|
||||
// Rares
|
||||
if (/^rare_/i.test(classname)) {
|
||||
return { credits: 25, points: 0, pointsType: 0 };
|
||||
return { credits: 25, points: 0, pointsType: 0 }
|
||||
}
|
||||
|
||||
// Wired
|
||||
if (/^wf_/i.test(classname)) {
|
||||
return { credits: 5, points: 0, pointsType: 0 };
|
||||
return { credits: 5, points: 0, pointsType: 0 }
|
||||
}
|
||||
|
||||
// NFT items
|
||||
if (/^nft_/i.test(classname)) {
|
||||
return { credits: 10, points: 0, pointsType: 0 };
|
||||
return { credits: 10, points: 0, pointsType: 0 }
|
||||
}
|
||||
|
||||
// Trophies
|
||||
if (/^(trophy|gold_|silver_|bronze_)/i.test(classname)) {
|
||||
return { credits: 10, points: 0, pointsType: 0 };
|
||||
return { credits: 10, points: 0, pointsType: 0 }
|
||||
}
|
||||
|
||||
// Default
|
||||
return { credits: 3, points: 0, pointsType: 0 };
|
||||
return { credits: 3, points: 0, pointsType: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,66 +245,71 @@ export function autoPriceFurni(classname: string): {
|
||||
* Prisma connection for the whole operation; the in-process mutex is enough
|
||||
* for the single-server deployment and avoids pool-connection gymnastics.
|
||||
*/
|
||||
let catalogIdAllocChain: Promise<unknown> = Promise.resolve();
|
||||
let catalogIdAllocChain: Promise<unknown> = Promise.resolve()
|
||||
|
||||
export async function allocateCatalogItemId<T>(insertFn: (nextId: number) => Promise<T>): Promise<T> {
|
||||
const prev = catalogIdAllocChain;
|
||||
let settle!: () => void;
|
||||
export async function allocateCatalogItemId<T>(
|
||||
insertFn: (nextId: number) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const prev = catalogIdAllocChain
|
||||
let settle!: () => void
|
||||
catalogIdAllocChain = new Promise<void>((r) => {
|
||||
settle = r;
|
||||
});
|
||||
await prev.catch(() => {});
|
||||
settle = r
|
||||
})
|
||||
await prev.catch(() => {})
|
||||
try {
|
||||
const maxIdResult = await prisma.$queryRaw<Array<{ maxId: number | bigint | null }>>`
|
||||
SELECT MAX(id) as maxId FROM catalog_items
|
||||
`;
|
||||
const nextId = Number(maxIdResult[0]?.maxId ?? 0) + 1;
|
||||
return await insertFn(nextId);
|
||||
`
|
||||
const nextId = Number(maxIdResult[0]?.maxId ?? 0) + 1
|
||||
return await insertFn(nextId)
|
||||
} finally {
|
||||
settle();
|
||||
settle()
|
||||
}
|
||||
}
|
||||
|
||||
/** Get or create a category sub-page under the imported parent page. */
|
||||
export async function getOrCreateCategoryPage(classname: string, itemType: string): Promise<number> {
|
||||
const parentId = await getOrCreateImportedParentPage();
|
||||
const catKey = classifyFurni(classname, itemType);
|
||||
const captionSave = `imp_${catKey}`;
|
||||
const catInfo = CATEGORY_PAGE[catKey] || CATEGORY_PAGE.other;
|
||||
export async function getOrCreateCategoryPage(
|
||||
classname: string,
|
||||
itemType: string,
|
||||
): Promise<number> {
|
||||
const parentId = await getOrCreateImportedParentPage()
|
||||
const catKey = classifyFurni(classname, itemType)
|
||||
const captionSave = `imp_${catKey}`
|
||||
const catInfo = CATEGORY_PAGE[catKey] || CATEGORY_PAGE.other
|
||||
|
||||
const existing = await prisma.catalogPages.findFirst({
|
||||
where: { captionSave, parentId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing) return existing.id;
|
||||
})
|
||||
if (existing) return existing.id
|
||||
|
||||
await prisma.$executeRaw`
|
||||
INSERT INTO catalog_pages (caption_save, caption, page_layout, parent_id, min_rank, order_num, icon_image, enabled, visible, includes, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser)
|
||||
VALUES (${captionSave}, ${catInfo.label}, 'default_3x3', ${parentId}, '1', ${catInfo.order}, ${catInfo.icon}, '1', '1', '', '', '', '', '', '', '', '')
|
||||
`;
|
||||
`
|
||||
|
||||
const created = await prisma.catalogPages.findFirst({
|
||||
where: { captionSave, parentId },
|
||||
select: { id: true },
|
||||
});
|
||||
})
|
||||
if (!created) {
|
||||
throw new Error(`Failed to create catalog sub-page "${captionSave}" under parent ${parentId}`);
|
||||
throw new Error(`Failed to create catalog sub-page "${captionSave}" under parent ${parentId}`)
|
||||
}
|
||||
return created.id;
|
||||
return created.id
|
||||
}
|
||||
|
||||
// ── Import Single Furni ─────────────────────────────────────────────
|
||||
|
||||
export async function importSingleFurni(params: {
|
||||
id: number;
|
||||
classname: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
revision: number;
|
||||
category: string;
|
||||
skipFurniDataWrite?: boolean;
|
||||
onProgress?: (status: string) => void;
|
||||
id: number
|
||||
classname: string
|
||||
name: string
|
||||
description: string
|
||||
type: string
|
||||
revision: number
|
||||
category: string
|
||||
skipFurniDataWrite?: boolean
|
||||
onProgress?: (status: string) => void
|
||||
}): Promise<ImportSingleResult> {
|
||||
const {
|
||||
id: originalId,
|
||||
@@ -310,55 +321,55 @@ export async function importSingleFurni(params: {
|
||||
category,
|
||||
skipFurniDataWrite,
|
||||
onProgress,
|
||||
} = params;
|
||||
const warnings: string[] = [];
|
||||
} = params
|
||||
const warnings: string[] = []
|
||||
|
||||
// Check if already exists by classname OR by spriteId (primary key collision).
|
||||
const existsByName = await prisma.itemsBase.findFirst({
|
||||
where: { itemName: classname },
|
||||
select: { id: true },
|
||||
});
|
||||
})
|
||||
if (existsByName) {
|
||||
return {
|
||||
ok: false,
|
||||
warnings,
|
||||
error: `Item "${classname}" already exists (id: ${existsByName.id})`,
|
||||
};
|
||||
}
|
||||
}
|
||||
const existsById = await prisma.itemsBase.findUnique({
|
||||
where: { id: originalId },
|
||||
select: { itemName: true },
|
||||
});
|
||||
})
|
||||
if (existsById) {
|
||||
return {
|
||||
ok: false,
|
||||
warnings,
|
||||
error: `spriteId ${originalId} already used by "${existsById.itemName}"`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const itemType = type === "wallitem" ? "i" : "s";
|
||||
const spriteId = originalId;
|
||||
const itemType = type === 'wallitem' ? 'i' : 's'
|
||||
const spriteId = originalId
|
||||
|
||||
// ── Cross-reference with habbo.it furnidata for enrichment ────────
|
||||
let habboIt: HabboItFurniEntry | null = null;
|
||||
let habboIt: HabboItFurniEntry | null = null
|
||||
try {
|
||||
habboIt = await lookupHabboIt(classname);
|
||||
habboIt = await lookupHabboIt(classname)
|
||||
if (habboIt) {
|
||||
warnings.push(`Enriched from habbo.it: "${habboIt.name}"`);
|
||||
warnings.push(`Enriched from habbo.it: "${habboIt.name}"`)
|
||||
}
|
||||
} catch {
|
||||
// Non-blocking: habbo.it enrichment is optional
|
||||
}
|
||||
|
||||
// Best values: use habbo.it furnidata for enrichment
|
||||
const bestName = name || habboIt?.name || classname;
|
||||
const bestDescription = description || habboIt?.description || "";
|
||||
const bestRevision = revision || habboIt?.revision || 0;
|
||||
const bestCategory = category || habboIt?.category || "unknown";
|
||||
const bestName = name || habboIt?.name || classname
|
||||
const bestDescription = description || habboIt?.description || ''
|
||||
const bestRevision = revision || habboIt?.revision || 0
|
||||
const bestCategory = category || habboIt?.category || 'unknown'
|
||||
|
||||
// ── Insert into DB with enriched data ──────────────────────────────
|
||||
onProgress?.("writing_db");
|
||||
onProgress?.('writing_db')
|
||||
try {
|
||||
// NOTE: live Arcturus `items_base` has no description/revision/rare columns —
|
||||
// those metadata fields are persisted in FurnitureData.json (see entry below),
|
||||
@@ -366,28 +377,28 @@ export async function importSingleFurni(params: {
|
||||
await prisma.$executeRaw`
|
||||
INSERT INTO items_base (id, sprite_id, public_name, item_name, type)
|
||||
VALUES (${spriteId}, ${spriteId}, ${bestName}, ${classname}, ${itemType})
|
||||
`;
|
||||
`
|
||||
} catch (err) {
|
||||
return { ok: false, warnings, error: `DB insert failed: ${(err as Error).message}` };
|
||||
return { ok: false, warnings, error: `DB insert failed: ${(err as Error).message}` }
|
||||
}
|
||||
|
||||
// ── Derive file-safe names ────────────────────────────────────────
|
||||
const rev = revision || 0;
|
||||
const assetTargets = await getFurniAssetWriteTargets();
|
||||
const { swfDir, iconDir, nitroDir } = assetTargets;
|
||||
const starIdx = classname.indexOf("*");
|
||||
const hasColor = starIdx !== -1;
|
||||
const baseClassname = hasColor ? classname.substring(0, starIdx) : classname;
|
||||
const _colorNum = hasColor ? classname.substring(starIdx + 1) : "2";
|
||||
const iconFileName = classname.replace(/\*/g, "_");
|
||||
const safeSwfName = baseClassname;
|
||||
const safeNitroName = baseClassname;
|
||||
const swfPath = path.join(/*turbopackIgnore: true*/ swfDir, `${safeSwfName}.swf`);
|
||||
const nitroPath = path.join(/*turbopackIgnore: true*/ nitroDir, `${safeNitroName}.nitro`);
|
||||
const iconPath = path.join(/*turbopackIgnore: true*/ iconDir, `${iconFileName}_icon.png`);
|
||||
const mirrorSwfDirs = assetTargets.mirrorDirs.map((d) => d.swfDir);
|
||||
const mirrorIconDirs = assetTargets.mirrorDirs.map((d) => d.iconDir);
|
||||
const mirrorNitroDirs = assetTargets.mirrorDirs.map((d) => d.nitroDir);
|
||||
const rev = revision || 0
|
||||
const assetTargets = await getFurniAssetWriteTargets()
|
||||
const { swfDir, iconDir, nitroDir } = assetTargets
|
||||
const starIdx = classname.indexOf('*')
|
||||
const hasColor = starIdx !== -1
|
||||
const baseClassname = hasColor ? classname.substring(0, starIdx) : classname
|
||||
const _colorNum = hasColor ? classname.substring(starIdx + 1) : '2'
|
||||
const iconFileName = classname.replace(/\*/g, '_')
|
||||
const safeSwfName = baseClassname
|
||||
const safeNitroName = baseClassname
|
||||
const swfPath = path.join(/*turbopackIgnore: true*/ swfDir, `${safeSwfName}.swf`)
|
||||
const nitroPath = path.join(/*turbopackIgnore: true*/ nitroDir, `${safeNitroName}.nitro`)
|
||||
const iconPath = path.join(/*turbopackIgnore: true*/ iconDir, `${iconFileName}_icon.png`)
|
||||
const mirrorSwfDirs = assetTargets.mirrorDirs.map((d) => d.swfDir)
|
||||
const mirrorIconDirs = assetTargets.mirrorDirs.map((d) => d.iconDir)
|
||||
const mirrorNitroDirs = assetTargets.mirrorDirs.map((d) => d.nitroDir)
|
||||
|
||||
// ── Download assets in parallel (with retry + validation) ────────
|
||||
// Icon strategy matches Nitro's URL template:
|
||||
@@ -395,104 +406,104 @@ export async function importSingleFurni(params: {
|
||||
// For color variants (classname has `*`): try color-specific first,
|
||||
// fall back to base-classname icon (Nitro reuses base for all colors
|
||||
// when no color-specific icon exists).
|
||||
onProgress?.("downloading");
|
||||
const iconColorAware = classname.replace(/\*/g, "_");
|
||||
onProgress?.('downloading')
|
||||
const iconColorAware = classname.replace(/\*/g, '_')
|
||||
const iconUrl = hasColor
|
||||
? `https://images.habbo.com/dcr/hof_furni/${rev}/${encodeURIComponent(iconColorAware)}_icon.png`
|
||||
: `https://images.habbo.com/dcr/hof_furni/${rev}/${encodeURIComponent(baseClassname)}_icon.png`;
|
||||
: `https://images.habbo.com/dcr/hof_furni/${rev}/${encodeURIComponent(baseClassname)}_icon.png`
|
||||
|
||||
const downloadResults = await Promise.allSettled([
|
||||
downloadFile(iconUrl, iconPath, { validate: "png" }),
|
||||
downloadFile(iconUrl, iconPath, { validate: 'png' }),
|
||||
downloadFile(
|
||||
`https://images.habbo.com/dcr/hof_furni/${rev}/${encodeURIComponent(safeSwfName)}.swf`,
|
||||
swfPath,
|
||||
{ validate: "swf" },
|
||||
{ validate: 'swf' },
|
||||
),
|
||||
]);
|
||||
])
|
||||
|
||||
let iconOk = downloadResults[0].status === "fulfilled" && downloadResults[0].value.ok;
|
||||
const swfOk = downloadResults[1].status === "fulfilled" && downloadResults[1].value.ok;
|
||||
let iconOk = downloadResults[0].status === 'fulfilled' && downloadResults[0].value.ok
|
||||
const swfOk = downloadResults[1].status === 'fulfilled' && downloadResults[1].value.ok
|
||||
|
||||
// If color-specific icon 404'd, try the base-classname icon.
|
||||
if (!iconOk && hasColor) {
|
||||
const dl = await downloadFile(
|
||||
`https://images.habbo.com/dcr/hof_furni/${rev}/${encodeURIComponent(baseClassname)}_icon.png`,
|
||||
iconPath,
|
||||
{ validate: "png" },
|
||||
);
|
||||
if (dl.ok) iconOk = true;
|
||||
{ validate: 'png' },
|
||||
)
|
||||
if (dl.ok) iconOk = true
|
||||
}
|
||||
|
||||
// ── Icon fallback: extract from SWF if download failed ────────────
|
||||
if (!iconOk && (swfOk || existsSync(/*turbopackIgnore: true*/ swfPath))) {
|
||||
try {
|
||||
const swfBuf = await fs.readFile(/*turbopackIgnore: true*/ swfPath);
|
||||
const iconPng = extractIconFromSwf(swfBuf, baseClassname);
|
||||
const swfBuf = await fs.readFile(/*turbopackIgnore: true*/ swfPath)
|
||||
const iconPng = extractIconFromSwf(swfBuf, baseClassname)
|
||||
if (iconPng) {
|
||||
await fs.writeFile(/*turbopackIgnore: true*/ iconPath, iconPng);
|
||||
warnings.push("Icon extracted from SWF (external download failed)");
|
||||
await fs.writeFile(/*turbopackIgnore: true*/ iconPath, iconPng)
|
||||
warnings.push('Icon extracted from SWF (external download failed)')
|
||||
} else {
|
||||
warnings.push("Icon download failed and SWF icon extraction returned nothing");
|
||||
warnings.push('Icon download failed and SWF icon extraction returned nothing')
|
||||
}
|
||||
} catch {
|
||||
warnings.push("Icon download failed and SWF icon extraction also failed");
|
||||
warnings.push('Icon download failed and SWF icon extraction also failed')
|
||||
}
|
||||
} else if (!iconOk) {
|
||||
warnings.push("Icon image download failed");
|
||||
warnings.push('Icon image download failed')
|
||||
}
|
||||
|
||||
if (!swfOk) warnings.push("SWF download failed");
|
||||
if (!swfOk) warnings.push('SWF download failed')
|
||||
|
||||
// ── Convert SWF to Nitro ──────────────────────────────────────────
|
||||
onProgress?.("converting");
|
||||
let conversionResult: ConversionResult | null = null;
|
||||
onProgress?.('converting')
|
||||
let conversionResult: ConversionResult | null = null
|
||||
|
||||
if (existsSync(/*turbopackIgnore: true*/ nitroPath)) {
|
||||
warnings.push("Nitro file already exists (shared across color variants)");
|
||||
warnings.push('Nitro file already exists (shared across color variants)')
|
||||
} else {
|
||||
try {
|
||||
if (existsSync(/*turbopackIgnore: true*/ swfPath)) {
|
||||
const swfBuffer = await fs.readFile(/*turbopackIgnore: true*/ swfPath);
|
||||
conversionResult = convertSwfToNitro(swfBuffer, baseClassname);
|
||||
await fs.writeFile(/*turbopackIgnore: true*/ nitroPath, conversionResult.bundle);
|
||||
const swfBuffer = await fs.readFile(/*turbopackIgnore: true*/ swfPath)
|
||||
conversionResult = convertSwfToNitro(swfBuffer, baseClassname)
|
||||
await fs.writeFile(/*turbopackIgnore: true*/ nitroPath, conversionResult.bundle)
|
||||
if (conversionResult.warnings.length > 0) {
|
||||
warnings.push(...conversionResult.warnings);
|
||||
warnings.push(...conversionResult.warnings)
|
||||
}
|
||||
} else {
|
||||
warnings.push("SWF not available for Nitro conversion");
|
||||
warnings.push('SWF not available for Nitro conversion')
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[import-furni] SWF to Nitro conversion failed:", (err as Error).message);
|
||||
warnings.push(`SWF to Nitro conversion failed: ${(err as Error).message}`);
|
||||
console.warn('[import-furni] SWF to Nitro conversion failed:', (err as Error).message)
|
||||
warnings.push(`SWF to Nitro conversion failed: ${(err as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Rollback: if conversion failed AND no .nitro exists, remove DB record ──
|
||||
if (!existsSync(/*turbopackIgnore: true*/ nitroPath) && !conversionResult) {
|
||||
try {
|
||||
await prisma.$executeRaw`DELETE FROM items_base WHERE id = ${spriteId} AND item_name = ${classname}`;
|
||||
await prisma.$executeRaw`DELETE FROM items_base WHERE id = ${spriteId} AND item_name = ${classname}`
|
||||
} catch {
|
||||
/* best effort cleanup */
|
||||
}
|
||||
return { ok: false, warnings, error: "Import rolled back: no .nitro file could be created" };
|
||||
return { ok: false, warnings, error: 'Import rolled back: no .nitro file could be created' }
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
mirrorFileToDirs(swfPath, `${safeSwfName}.swf`, mirrorSwfDirs, warnings),
|
||||
mirrorFileToDirs(iconPath, `${iconFileName}_icon.png`, mirrorIconDirs, warnings),
|
||||
mirrorFileToDirs(nitroPath, `${safeNitroName}.nitro`, mirrorNitroDirs, warnings),
|
||||
]);
|
||||
])
|
||||
|
||||
// ── Build enriched FurnitureData entry ────────────────────────────
|
||||
const dims =
|
||||
conversionResult?.dimensions ||
|
||||
(habboIt ? { x: habboIt.xdim, y: habboIt.ydim, z: 0 } : { x: 1, y: 1, z: 0 });
|
||||
const meta = conversionResult?.metadata ?? null;
|
||||
(habboIt ? { x: habboIt.xdim, y: habboIt.ydim, z: 0 } : { x: 1, y: 1, z: 0 })
|
||||
const meta = conversionResult?.metadata ?? null
|
||||
|
||||
// Merge interaction flags: prefer .nitro extraction, fallback to habbo.it
|
||||
const cansiton = meta?.cansiton ?? habboIt?.cansiton ?? false;
|
||||
const canlayon = meta?.canlayon ?? habboIt?.canlayon ?? false;
|
||||
const canstandon = meta?.canstandon ?? habboIt?.canstandon ?? false;
|
||||
const cansiton = meta?.cansiton ?? habboIt?.cansiton ?? false
|
||||
const canlayon = meta?.canlayon ?? habboIt?.canlayon ?? false
|
||||
const canstandon = meta?.canstandon ?? habboIt?.canstandon ?? false
|
||||
|
||||
// Derive interaction_type. Priority:
|
||||
// 1. Wired LOGIC items (wf_act_/wf_trg_/wf_cnd_/wf_slc_/wf_xtra_/wf_var_)
|
||||
@@ -504,29 +515,29 @@ export async function importSingleFurni(params: {
|
||||
// wf_floor_switch, …) that didn't match step 2 — use the full
|
||||
// classname as a safe fallback instead of 'default'.
|
||||
// 4. Final fallback: default.
|
||||
const logicType = meta?.logicType || "";
|
||||
let interactionType = "default";
|
||||
const logicType = meta?.logicType || ''
|
||||
let interactionType = 'default'
|
||||
if (/^wf_(act|trg|cnd|slc|xtra|var)_/.test(classname)) {
|
||||
interactionType = classname;
|
||||
} else if (logicType.includes("multistate")) interactionType = "default";
|
||||
else if (logicType.includes("gate")) interactionType = "gate";
|
||||
else if (logicType.includes("teleport")) interactionType = "teleport";
|
||||
else if (logicType.includes("roller")) interactionType = "roller";
|
||||
else if (logicType.includes("dice")) interactionType = "dice";
|
||||
else if (logicType.includes("vendingmachine")) interactionType = "vendingmachine";
|
||||
else if (logicType.includes("firework")) interactionType = "fireworks";
|
||||
else if (logicType.includes("trophy")) interactionType = "trophy";
|
||||
else if (logicType.includes("exchange")) interactionType = "exchange";
|
||||
else if (logicType.includes("habbowheel")) interactionType = "habbowheel";
|
||||
else if (logicType.includes("soundmachine")) interactionType = "jukebox";
|
||||
interactionType = classname
|
||||
} else if (logicType.includes('multistate')) interactionType = 'default'
|
||||
else if (logicType.includes('gate')) interactionType = 'gate'
|
||||
else if (logicType.includes('teleport')) interactionType = 'teleport'
|
||||
else if (logicType.includes('roller')) interactionType = 'roller'
|
||||
else if (logicType.includes('dice')) interactionType = 'dice'
|
||||
else if (logicType.includes('vendingmachine')) interactionType = 'vendingmachine'
|
||||
else if (logicType.includes('firework')) interactionType = 'fireworks'
|
||||
else if (logicType.includes('trophy')) interactionType = 'trophy'
|
||||
else if (logicType.includes('exchange')) interactionType = 'exchange'
|
||||
else if (logicType.includes('habbowheel')) interactionType = 'habbowheel'
|
||||
else if (logicType.includes('soundmachine')) interactionType = 'jukebox'
|
||||
|
||||
if (interactionType === "default" && /^wf_/.test(classname)) {
|
||||
interactionType = classname;
|
||||
warnings.push(`Wired fallback: using full classname "${classname}" as interaction_type`);
|
||||
if (interactionType === 'default' && /^wf_/.test(classname)) {
|
||||
interactionType = classname
|
||||
warnings.push(`Wired fallback: using full classname "${classname}" as interaction_type`)
|
||||
}
|
||||
|
||||
// allow_stack: if it has height and you can put things on it
|
||||
const allowStack = dims.z > 0 ? "1" : "0";
|
||||
const allowStack = dims.z > 0 ? '1' : '0'
|
||||
|
||||
// Update items_base with ALL enriched data
|
||||
try {
|
||||
@@ -538,10 +549,10 @@ export async function importSingleFurni(params: {
|
||||
canstandon,
|
||||
allowStack,
|
||||
interactionType,
|
||||
});
|
||||
await prisma.$executeRawUnsafe(update.sql, ...update.values);
|
||||
})
|
||||
await prisma.$executeRawUnsafe(update.sql, ...update.values)
|
||||
} catch (err) {
|
||||
warnings.push(`items_base dimension update failed: ${(err as Error).message}`);
|
||||
warnings.push(`items_base dimension update failed: ${(err as Error).message}`)
|
||||
}
|
||||
|
||||
// Build enriched FurnitureData.json entry using best available data
|
||||
@@ -556,40 +567,40 @@ export async function importSingleFurni(params: {
|
||||
dims,
|
||||
metadata: meta,
|
||||
habboIt,
|
||||
});
|
||||
})
|
||||
|
||||
// ── Add to FurnitureData.json (or defer for batch) ────────────────
|
||||
if (skipFurniDataWrite) {
|
||||
// Batch mode: return the entry so the caller writes once at the end
|
||||
} else {
|
||||
try {
|
||||
await appendFurniEntry(furniEntry, itemType);
|
||||
await appendFurniEntry(furniEntry, itemType)
|
||||
} catch (err) {
|
||||
console.warn("[import-furni] Failed to update FurnitureData.json:", (err as Error).message);
|
||||
warnings.push("FurnitureData.json update failed");
|
||||
console.warn('[import-furni] Failed to update FurnitureData.json:', (err as Error).message)
|
||||
warnings.push('FurnitureData.json update failed')
|
||||
}
|
||||
}
|
||||
|
||||
// ── Create catalog entry (with duplicate check + category sub-page) ──
|
||||
let catalogItemId: number | null = null;
|
||||
let catalogItemId: number | null = null
|
||||
try {
|
||||
const itemIdStr = String(spriteId);
|
||||
const itemIdStr = String(spriteId)
|
||||
|
||||
// Check if this item already exists anywhere in the catalog
|
||||
const existingCatalog = await prisma.$queryRaw<Array<{ id: number; page_id: string }>>`
|
||||
SELECT id, page_id FROM catalog_items WHERE item_ids = ${itemIdStr} LIMIT 1
|
||||
`;
|
||||
`
|
||||
|
||||
if (existingCatalog.length > 0) {
|
||||
catalogItemId = existingCatalog[0].id;
|
||||
catalogItemId = existingCatalog[0].id
|
||||
warnings.push(
|
||||
`Item already in catalog (catalog_item #${catalogItemId}, page ${existingCatalog[0].page_id}) — skipped duplicate`,
|
||||
);
|
||||
)
|
||||
} else {
|
||||
// Get or create the right category sub-page based on classname
|
||||
const categoryPageId = await getOrCreateCategoryPage(classname, itemType);
|
||||
const pageIdStr = String(categoryPageId);
|
||||
const price = autoPriceFurni(classname);
|
||||
const categoryPageId = await getOrCreateCategoryPage(classname, itemType)
|
||||
const pageIdStr = String(categoryPageId)
|
||||
const price = autoPriceFurni(classname)
|
||||
// catalog_items.id has no AUTO_INCREMENT — serialize ID allocation
|
||||
// across concurrent imports via a MySQL named lock to avoid MAX(id)
|
||||
// TOCTOU that would cause duplicate-key failures.
|
||||
@@ -597,13 +608,13 @@ export async function importSingleFurni(params: {
|
||||
await prisma.$executeRaw`
|
||||
INSERT INTO catalog_items (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata)
|
||||
VALUES (${nextCatalogId}, ${pageIdStr}, ${itemIdStr}, ${classname}, ${price.credits}, ${price.points}, ${price.pointsType}, 1, 1, '-1', '')
|
||||
`;
|
||||
return nextCatalogId;
|
||||
});
|
||||
`
|
||||
return nextCatalogId
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[import-furni] Failed to create catalog entry:", (err as Error).message);
|
||||
warnings.push("Catalog entry creation failed");
|
||||
console.warn('[import-furni] Failed to create catalog entry:', (err as Error).message)
|
||||
warnings.push('Catalog entry creation failed')
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -614,56 +625,56 @@ export async function importSingleFurni(params: {
|
||||
spriteCount: conversionResult?.spriteCount,
|
||||
warnings,
|
||||
furniDataEntry: skipFurniDataWrite ? { entry: furniEntry, itemType } : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Delete helpers ───────────────────────────────────────────────────
|
||||
|
||||
/** Delete a single imported item: DB records, FurnitureData.json entry, and asset files. */
|
||||
export async function deleteImportedItem(classname: string): Promise<{
|
||||
spriteId: number;
|
||||
deletedFiles: string[];
|
||||
errors: string[];
|
||||
spriteId: number
|
||||
deletedFiles: string[]
|
||||
errors: string[]
|
||||
}> {
|
||||
const errors: string[] = [];
|
||||
const errors: string[] = []
|
||||
|
||||
// Look up item in DB
|
||||
const item = await prisma.itemsBase.findFirst({
|
||||
where: { itemName: classname },
|
||||
select: { id: true, type: true },
|
||||
});
|
||||
})
|
||||
|
||||
if (!item) {
|
||||
throw new Error(`Item "${classname}" not found in database`);
|
||||
throw new Error(`Item "${classname}" not found in database`)
|
||||
}
|
||||
|
||||
const spriteId = item.id;
|
||||
const starIdx = classname.indexOf("*");
|
||||
const assetTargets = await getFurniAssetWriteTargets();
|
||||
const { swfDir, iconDir, nitroDir } = assetTargets;
|
||||
const baseClassname = starIdx !== -1 ? classname.substring(0, starIdx) : classname;
|
||||
const iconFileName = classname.replace(/\*/g, "_");
|
||||
const spriteId = item.id
|
||||
const starIdx = classname.indexOf('*')
|
||||
const assetTargets = await getFurniAssetWriteTargets()
|
||||
const { swfDir, iconDir, nitroDir } = assetTargets
|
||||
const baseClassname = starIdx !== -1 ? classname.substring(0, starIdx) : classname
|
||||
const iconFileName = classname.replace(/\*/g, '_')
|
||||
|
||||
// 1. Delete catalog_items
|
||||
try {
|
||||
const itemIdStr = String(spriteId);
|
||||
await prisma.$executeRaw`DELETE FROM catalog_items WHERE item_ids = ${itemIdStr} AND catalog_name = ${classname}`;
|
||||
const itemIdStr = String(spriteId)
|
||||
await prisma.$executeRaw`DELETE FROM catalog_items WHERE item_ids = ${itemIdStr} AND catalog_name = ${classname}`
|
||||
} catch (err) {
|
||||
errors.push(`catalog_items: ${(err as Error).message}`);
|
||||
errors.push(`catalog_items: ${(err as Error).message}`)
|
||||
}
|
||||
|
||||
// 2. Delete items_base
|
||||
try {
|
||||
await prisma.$executeRaw`DELETE FROM items_base WHERE id = ${spriteId} AND item_name = ${classname}`;
|
||||
await prisma.$executeRaw`DELETE FROM items_base WHERE id = ${spriteId} AND item_name = ${classname}`
|
||||
} catch (err) {
|
||||
errors.push(`items_base: ${(err as Error).message}`);
|
||||
errors.push(`items_base: ${(err as Error).message}`)
|
||||
}
|
||||
|
||||
// 3. Remove from FurnitureData.json
|
||||
try {
|
||||
await removeFurniEntry(classname);
|
||||
await removeFurniEntry(classname)
|
||||
} catch (err) {
|
||||
errors.push(`FurnitureData.json: ${(err as Error).message}`);
|
||||
errors.push(`FurnitureData.json: ${(err as Error).message}`)
|
||||
}
|
||||
|
||||
// 4. Delete files (.nitro, .swf, icon)
|
||||
@@ -676,49 +687,49 @@ export async function deleteImportedItem(classname: string): Promise<{
|
||||
path.join(/*turbopackIgnore: true*/ dirs.swfDir, `${baseClassname}.swf`),
|
||||
path.join(/*turbopackIgnore: true*/ dirs.iconDir, `${iconFileName}_icon.png`),
|
||||
]),
|
||||
];
|
||||
]
|
||||
|
||||
const deletedFiles: string[] = [];
|
||||
const deletedFiles: string[] = []
|
||||
for (const filePath of filesToDelete) {
|
||||
if (existsSync(/*turbopackIgnore: true*/ filePath)) {
|
||||
try {
|
||||
await fs.unlink(/*turbopackIgnore: true*/ filePath);
|
||||
deletedFiles.push(path.basename(filePath));
|
||||
await fs.unlink(/*turbopackIgnore: true*/ filePath)
|
||||
deletedFiles.push(path.basename(filePath))
|
||||
} catch (err) {
|
||||
errors.push(`${path.basename(filePath)}: ${(err as Error).message}`);
|
||||
errors.push(`${path.basename(filePath)}: ${(err as Error).message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { spriteId, deletedFiles, errors };
|
||||
return { spriteId, deletedFiles, errors }
|
||||
}
|
||||
|
||||
/** Clean up SWF files that have already been converted to Nitro. */
|
||||
export async function cleanupConvertedSwfs(): Promise<{
|
||||
deleted: number;
|
||||
total: number;
|
||||
remaining: number;
|
||||
errors: string[];
|
||||
deleted: number
|
||||
total: number
|
||||
remaining: number
|
||||
errors: string[]
|
||||
}> {
|
||||
const { swfDir, nitroDir } = await getFurniAssetDirs();
|
||||
const { swfDir, nitroDir } = await getFurniAssetDirs()
|
||||
if (!existsSync(swfDir)) {
|
||||
return { deleted: 0, total: 0, remaining: 0, errors: [] };
|
||||
return { deleted: 0, total: 0, remaining: 0, errors: [] }
|
||||
}
|
||||
|
||||
const swfFiles = (await fs.readdir(swfDir)).filter((f) => f.endsWith(".swf"));
|
||||
let deleted = 0;
|
||||
const errors: string[] = [];
|
||||
const swfFiles = (await fs.readdir(swfDir)).filter((f) => f.endsWith('.swf'))
|
||||
let deleted = 0
|
||||
const errors: string[] = []
|
||||
|
||||
for (const swfFile of swfFiles) {
|
||||
const cn = swfFile.replace(".swf", "");
|
||||
const nitroFilePath = path.join(/*turbopackIgnore: true*/ nitroDir, `${cn}.nitro`);
|
||||
const cn = swfFile.replace('.swf', '')
|
||||
const nitroFilePath = path.join(/*turbopackIgnore: true*/ nitroDir, `${cn}.nitro`)
|
||||
|
||||
if (existsSync(/*turbopackIgnore: true*/ nitroFilePath)) {
|
||||
try {
|
||||
await fs.unlink(path.join(/*turbopackIgnore: true*/ swfDir, swfFile));
|
||||
deleted++;
|
||||
await fs.unlink(path.join(/*turbopackIgnore: true*/ swfDir, swfFile))
|
||||
deleted++
|
||||
} catch (err) {
|
||||
errors.push(`Failed to delete ${swfFile}: ${(err as Error).message}`);
|
||||
errors.push(`Failed to delete ${swfFile}: ${(err as Error).message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -728,5 +739,5 @@ export async function cleanupConvertedSwfs(): Promise<{
|
||||
total: swfFiles.length,
|
||||
remaining: swfFiles.length - deleted,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user