Support Gitea configuration and safe Catalog Studio furniture completion
This commit is contained in:
1 parent
26f071117b
commit
9f791ba284
22 files changed
+1120
-166
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user