fix: furniture interaction detection and automatic catalog offer_id rebuild

Interaction detection:
- Trust SWF-derived sit/lay/stand flags only when the logic XML actually
  contains action data (new hasActions metadata); otherwise fall back to
  keyword detection so custom furni without <action> nodes are still
  classified correctly
- Add French/Dutch/German/Spanish/Italian keywords (chaise, banquette,
  stoel, silla, sedia, stuhl, tafel, mesa, ...) to sit/lay/stand detection
  with token-boundary matching to avoid false positives like bedside_table
- Unify interaction_modes_count priority: mechanic fixed modes, then raw
  animation state count, then sit/lay fallback, then keyword default
- Detect mechanic type even when real flags are not used

Catalog integrity:
- Skip duplicate catalog_items inserts in clone and upload imports
- Re-check live catalog rows before applying generated repair SQL
- Add rebuildCatalogOfferIds() which rebuilds every catalog_items.offer_id
  from the local FurnitureData.json (matched by entry id, then classname)
  and run it after every import path (single, batch, batch-regen, clone)
This commit is contained in:
openhands committed 2026-08-21 13:39:21 +02:00
1 parent 652d331402
commit 4b7cb519df
14 files changed
+418 -58

No files matched your search

+19 -6
View File
@@ -379,6 +379,8 @@ export async function cloneSingleFurni(params: {
cansiton: entry.cansiton,
canlayon: entry.canlayon,
canstandon: entry.canstandon,
// Source furnidata flags are curated → authoritative over keywords.
hasActionData: true,
logicType: (clonedNitroJson?.logicType as string) || undefined,
animationStates: nitroAnimationStatesCount(clonedNitroJson),
});
@@ -440,15 +442,26 @@ export async function cloneSingleFurni(params: {
}
// Catalog entry (category sub-page + auto price), serialized id allocation.
// Skip when the item already has a catalog row — re-imports must never
// create a second row for the same item_ids.
try {
const pageId = await getOrCreateCategoryPage(classname, itemType);
const price = autoPriceFurni(classname);
await allocateCatalogItemId(async (nextCatalogId) => {
await db.execute(sql`
const [existingCatalog] = (await db.execute(sql`
SELECT id FROM catalog_items WHERE item_ids = ${String(newId)} LIMIT 1
`)) as unknown as [Array<{ id: number }>, unknown];
if (existingCatalog.length > 0) {
warnings.push(
`catalog entry already exists (#${existingCatalog[0].id}) — skipped duplicate`,
);
} else {
const pageId = await getOrCreateCategoryPage(classname, itemType);
const price = autoPriceFurni(classname);
await allocateCatalogItemId(async (nextCatalogId) => {
await db.execute(sql`
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}, ${String(pageId)}, ${String(newId)}, ${classname}, ${price.credits}, ${price.points}, ${price.pointsType}, 1, 1, ${newId}, '')`);
return nextCatalogId;
});
return nextCatalogId;
});
}
} catch (err) {
logger.warn("[clone-import] catalog entry failed for", {
classname,