Support Gitea configuration and safe Catalog Studio furniture completion
CI / check (push) Successful in 1m31s
CI / deploy (push) Successful in 1m18s
CI / e2e (push) Successful in 21s

This commit is contained in:
Simo committed 2026-09-05 13:33:40 +02:00
1 parent 26f071117b
commit 9f791ba284
22 files changed
+1120 -166

No files matched your search

+46 -22
View File
@@ -510,6 +510,7 @@ export async function importSingleFurni(params: {
category: string;
skipFurniDataWrite?: boolean;
updateExisting?: boolean;
repairExisting?: boolean;
onProgress?: (status: string) => void;
/** Per-source SWF download base URL (e.g. "https://virtualc.nl/dcr"). */
sourceSwfBaseUrl?: string;
@@ -527,6 +528,7 @@ export async function importSingleFurni(params: {
category,
skipFurniDataWrite,
updateExisting,
repairExisting,
onProgress,
sourceSwfBaseUrl,
nitroBaseUrl: sourceNitroBaseUrl,
@@ -536,18 +538,22 @@ export async function importSingleFurni(params: {
// Check if already exists by classname OR by spriteId (primary key collision).
const [existsByName] = await db
.select({ id: ItemsBase.id })
.select({
id: ItemsBase.id,
spriteId: ItemsBase.spriteId,
type: ItemsBase.type,
})
.from(ItemsBase)
.where(eq(ItemsBase.itemName, classname))
.limit(1);
if (existsByName && !updateExisting) {
if (existsByName && !updateExisting && !repairExisting) {
return {
ok: false,
warnings,
error: `Item "${classname}" already exists (id: ${existsByName.id})`,
};
}
if (existsByName && existsByName.id !== originalId) {
if (existsByName && existsByName.id !== originalId && !repairExisting) {
// The classname already occupies a different spriteId. Reassigning the
// id would orphan every reference (catalog item_ids, player items,
// furnidata), so this can never be an "update".
@@ -557,10 +563,14 @@ export async function importSingleFurni(params: {
error: `Item "${classname}" already exists with id ${existsByName.id} — cannot reassign to spriteId ${originalId}`,
};
}
const itemId = repairExisting && existsByName ? existsByName.id : originalId;
const spriteId =
repairExisting && existsByName ? existsByName.spriteId : originalId;
const preservingExisting = !!(repairExisting && existsByName);
const [existsById] = await db
.select({ itemName: ItemsBase.itemName })
.from(ItemsBase)
.where(eq(ItemsBase.id, originalId))
.where(eq(ItemsBase.id, itemId))
.limit(1);
if (existsById && (!existsByName || existsById.itemName !== classname)) {
// The spriteId is owned by a different classname. Even with
@@ -575,17 +585,20 @@ export async function importSingleFurni(params: {
const isUpdate = !!existsByName;
// ── Check if spriteId already used in FurnitureData.json ──
const takenBy = await findFurniDataIdConflict(originalId, classname);
if (takenBy && !updateExisting) {
const takenBy = await findFurniDataIdConflict(spriteId, classname);
if (takenBy) {
return {
ok: false,
warnings,
error: `spriteId ${originalId} already used by "${takenBy}" in FurnitureData.json`,
error: `spriteId ${spriteId} already used by "${takenBy}" in FurnitureData.json`,
};
}
const itemType = type === "wallitem" ? "i" : "s";
const spriteId = originalId;
const itemType = preservingExisting
? existsByName.type
: type === "wallitem"
? "i"
: "s";
// ── Cross-reference with configured official Habbo furnidata ──────
let officialHabbo: OfficialHabboFurniEntry | null = null;
@@ -608,7 +621,11 @@ export async function importSingleFurni(params: {
// ── Insert into DB with enriched data ──────────────────────────────
onProgress?.("writing_db");
try {
if (isUpdate) {
if (preservingExisting) {
warnings.push(
`Existing furniture #${itemId}: SQL settings and catalog offers preserved`,
);
} else if (isUpdate) {
// NOTE: live Arcturus `items_base` has no description/revision/rare columns —
// those metadata fields are persisted in FurnitureData.json (see entry below),
// not in the DB. Only refresh the columns this import manages.
@@ -704,8 +721,12 @@ export async function importSingleFurni(params: {
// Download icon + SWF concurrently (independent assets). Each tries its
// candidate URLs in order until one succeeds.
const [iconOkResult, swfOk] = await Promise.all([
tryDownloadCandidates(iconUrls, iconPath, "png"),
tryDownloadCandidates(swfUrls, swfPath, "swf"),
preservingExisting && existsSync(iconPath)
? Promise.resolve(true)
: tryDownloadCandidates(iconUrls, iconPath, "png"),
preservingExisting && existsSync(swfPath)
? Promise.resolve(true)
: tryDownloadCandidates(swfUrls, swfPath, "swf"),
]);
let iconOk = iconOkResult;
@@ -799,16 +820,19 @@ export async function importSingleFurni(params: {
}
}
try {
await db.execute(
sql`DELETE FROM items_base WHERE id = ${spriteId} AND item_name = ${classname}`,
);
if (!isUpdate)
await db.execute(
sql`DELETE FROM items_base WHERE id = ${itemId} AND item_name = ${classname}`,
);
} catch {
/* best effort cleanup */
}
return {
ok: false,
warnings,
error: "Import rolled back: no .nitro file could be created",
error: isUpdate
? "No .nitro file could be created; existing furniture was preserved"
: "Import rolled back: no .nitro file could be created",
};
}
@@ -924,7 +948,7 @@ export async function importSingleFurni(params: {
// Update items_base with ALL enriched data
try {
const update = buildItemsBaseEnrichmentUpdate({
id: spriteId,
id: itemId,
dims,
cansiton: autoInteraction.canSit,
canlayon: autoInteraction.canLay,
@@ -933,7 +957,7 @@ export async function importSingleFurni(params: {
interactionType,
interactionModesCount,
});
await db.execute(update);
if (!preservingExisting) await db.execute(update);
} catch (err) {
warnings.push(
`items_base dimension update failed: ${(err as Error).message}`,
@@ -972,16 +996,16 @@ export async function importSingleFurni(params: {
let catalogItemId: number | null = null;
let catalogUpdated = false;
try {
const itemIdStr = String(spriteId);
const itemIdStr = String(itemId);
// Check if this item already exists anywhere in the catalog
const [existingCatalog] = (await db.execute(sql`
SELECT id, page_id FROM catalog_items WHERE item_ids = ${itemIdStr} LIMIT 1
SELECT id, page_id FROM catalog_items WHERE FIND_IN_SET(${itemIdStr}, REPLACE(item_ids, ';', ',')) > 0 LIMIT 1
`)) as unknown as [Array<{ id: number; page_id: string }>, unknown];
if (existingCatalog.length > 0) {
catalogItemId = existingCatalog[0].id;
if (updateExisting) {
if (updateExisting && !preservingExisting) {
// Update existing catalog entry: price, page, etc.
const categoryPageId = await getOrCreateCategoryPage(
classname,
@@ -1025,7 +1049,7 @@ export async function importSingleFurni(params: {
return {
ok: true,
itemId: spriteId,
itemId,
catalogItemId,
catalogUpdated,
dimensions: dims,