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

@@ -10,6 +10,7 @@ import { getSource } from "@/lib/services/clone-sources";
import {
appendFurniEntriesBatch,
appendFurniEntry,
rebuildCatalogOfferIds,
reconcileFurniDataWithItemsBase,
} from "@/lib/services/furni-data";
import {
@@ -142,6 +143,7 @@ export const POST = withAdmin(
try {
await reconcileImportedOfferIds();
await reconcileFurniDataWithItemsBase();
await rebuildCatalogOfferIds();
await verifyAndFixInteractionModesCount();
await ensureFurniOwnership();
await syncAssetsToGamedataBundle();
@@ -10,6 +10,7 @@ import {
appendFurniEntriesBatch,
buildFurniEntry,
readFurniData,
rebuildCatalogOfferIds,
reconcileFurniDataWithItemsBase,
} from "@/lib/services/furni-data";
import {
@@ -269,6 +270,9 @@ export const POST = withAdmin(
// Verify the furnidata matches the DB ids
const furniReconcile = await reconcileFurniDataWithItemsBase();
// Rebuild every catalog offer_id from the furnidata
const offerRebuild = await rebuildCatalogOfferIds();
send({
type: "regen_complete",
succeeded,
@@ -281,6 +285,8 @@ export const POST = withAdmin(
furniDataFixedIds: furniReconcile.fixedIds,
furniDataMissing: furniReconcile.missing,
furniDataConflicts: furniReconcile.conflicts,
offerRebuildChecked: offerRebuild.checked,
offerRebuildFixed: offerRebuild.fixed,
duration: Date.now() - startTime,
});
@@ -5,6 +5,7 @@ import { logAudit } from "@/lib/services/audit";
import { getSource } from "@/lib/services/clone-sources";
import {
appendFurniEntriesBatch,
rebuildCatalogOfferIds,
reconcileFurniDataWithItemsBase,
} from "@/lib/services/furni-data";
import {
@@ -227,6 +228,7 @@ export const POST = withAdmin(
let furniDataFixedOfferIds = 0;
let furniDataMissing = 0;
let furniDataConflicts = 0;
let offerRebuildFixed = 0;
let interactionFixed = 0;
try {
offerIdsFixed = (await reconcileImportedOfferIds()).fixed;
@@ -239,6 +241,7 @@ export const POST = withAdmin(
furniDataFixedOfferIds = furniReconcile.fixedOfferIds;
furniDataMissing = furniReconcile.missing;
furniDataConflicts = furniReconcile.conflicts;
offerRebuildFixed = (await rebuildCatalogOfferIds()).fixed;
interactionFixed = (await verifyAndFixInteractionModesCount()).fixed;
} catch (err) {
console.warn(
@@ -260,6 +263,7 @@ export const POST = withAdmin(
furniDataFixedOfferIds,
furniDataMissing,
furniDataConflicts,
offerRebuildFixed,
interactionFixed,
duration: Date.now() - startTime,
});
+17 -1
View File
@@ -9,7 +9,10 @@ import { logAudit } from "@/lib/services/audit";
import { getCloneList } from "@/lib/services/clone-import";
import { getSource } from "@/lib/services/clone-sources";
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
import { reconcileFurniDataWithItemsBase } from "@/lib/services/furni-data";
import {
rebuildCatalogOfferIds,
reconcileFurniDataWithItemsBase,
} from "@/lib/services/furni-data";
import {
autoPriceFurni,
CATEGORY_PAGE,
@@ -171,11 +174,14 @@ export const GET = withAdmin(
// Live furnidata reconcile (without hotel RCON reload)
if (action === "live-reconcile") {
const furniReconcile = await reconcileFurniDataWithItemsBase();
const offerRebuild = await rebuildCatalogOfferIds();
return apiOk({
furniDataFixedIds: furniReconcile.fixedIds,
furniDataFixedOfferIds: furniReconcile.fixedOfferIds,
furniDataMissing: furniReconcile.missing,
furniDataConflicts: furniReconcile.conflicts,
offerRebuildChecked: offerRebuild.checked,
offerRebuildFixed: offerRebuild.fixed,
});
}
@@ -452,6 +458,9 @@ export const POST = withAdmin(
// Verify the furnidata matches the DB ids
const furniReconcile = await reconcileFurniDataWithItemsBase();
// Rebuild every catalog offer_id from the furnidata
const offerRebuild = await rebuildCatalogOfferIds();
// Verify + fix interaction_modes_count once for the whole batch
const verification = await verifyAndFixInteractionModesCount();
@@ -471,6 +480,8 @@ export const POST = withAdmin(
furniDataFixedOfferIds: furniReconcile.fixedOfferIds,
furniDataMissing: furniReconcile.missing,
furniDataConflicts: furniReconcile.conflicts,
offerRebuildChecked: offerRebuild.checked,
offerRebuildFixed: offerRebuild.fixed,
interactionFixed: verification.fixed,
});
}
@@ -533,6 +544,9 @@ export const POST = withAdmin(
// Verify the furnidata matches the DB ids
const furniReconcile = await reconcileFurniDataWithItemsBase();
// Rebuild every catalog offer_id from the furnidata
const offerRebuild = await rebuildCatalogOfferIds();
// Verify + fix interaction_modes_count (single consolidated pass)
const verification = await verifyAndFixInteractionModesCount();
@@ -564,6 +578,8 @@ export const POST = withAdmin(
furniDataFixedOfferIds: furniReconcile.fixedOfferIds,
furniDataMissing: furniReconcile.missing,
furniDataConflicts: furniReconcile.conflicts,
offerRebuildChecked: offerRebuild.checked,
offerRebuildFixed: offerRebuild.fixed,
interactionFixed: verification.fixed,
spriteCount: result.spriteCount,
warnings: result.warnings,