Reconcile FurnitureData.json with items_base after import
CI / check (push) Successful in 45s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m16s

After every single import, batch import and batch-regen, cross-check all
items_base rows against the local FurnitureData.json: entries whose
spriteId drifted are corrected to the DB id, and missing entries plus
real id conflicts are reported in the API/SSE response. Entries that are
missing entirely are counted (rebuilding them needs SWF/nitro metadata).
This commit is contained in:
openhands committed 2026-08-18 20:43:41 +02:00
1 parent b4021f6b42
commit b4968a9967
4 files changed
+118 -1

No files matched your search

@@ -10,6 +10,7 @@ import {
appendFurniEntriesBatch,
buildFurniEntry,
readFurniData,
reconcileFurniDataWithItemsBase,
} from "@/lib/services/furni-data";
import {
downloadFile,
@@ -265,6 +266,9 @@ export const POST = withAdmin(
// Backfill any missing nitros and icons into the emulator's Gamedata bundle
const assetsSynced = await syncAssetsToGamedataBundle();
// Verify the furnidata matches the DB ids
const furniReconcile = await reconcileFurniDataWithItemsBase();
send({
type: "regen_complete",
succeeded,
@@ -274,6 +278,9 @@ export const POST = withAdmin(
ownershipFixed: ownership.fixed,
nitrosSynced: assetsSynced.copiedNitros,
iconsSynced: assetsSynced.copiedIcons,
furniDataFixedIds: furniReconcile.fixedIds,
furniDataMissing: furniReconcile.missing,
furniDataConflicts: furniReconcile.conflicts,
duration: Date.now() - startTime,
});
+14 -1
View File
@@ -3,7 +3,10 @@ import { apiError } from "@/lib/api-response";
import { PERMS } from "@/lib/permissions";
import { logAudit } from "@/lib/services/audit";
import { getSource } from "@/lib/services/clone-sources";
import { appendFurniEntriesBatch } from "@/lib/services/furni-data";
import {
appendFurniEntriesBatch,
reconcileFurniDataWithItemsBase,
} from "@/lib/services/furni-data";
import {
ensureDirectories,
ensureFurniOwnership,
@@ -219,12 +222,19 @@ export const POST = withAdmin(
let ownershipFixed: string[] = [];
let nitrosSynced: string[] = [];
let iconsSynced: string[] = [];
let furniDataFixedIds = 0;
let furniDataMissing = 0;
let furniDataConflicts = 0;
try {
offerIdsFixed = (await reconcileImportedOfferIds()).fixed;
ownershipFixed = (await ensureFurniOwnership()).fixed;
const assetsSynced = await syncAssetsToGamedataBundle();
nitrosSynced = assetsSynced.copiedNitros;
iconsSynced = assetsSynced.copiedIcons;
const furniReconcile = await reconcileFurniDataWithItemsBase();
furniDataFixedIds = furniReconcile.fixedIds;
furniDataMissing = furniReconcile.missing;
furniDataConflicts = furniReconcile.conflicts;
} catch (err) {
console.warn(
"[import-furni] Post-import reconcile/ownership failed:",
@@ -241,6 +251,9 @@ export const POST = withAdmin(
ownershipFixed,
nitrosSynced,
iconsSynced,
furniDataFixedIds,
furniDataMissing,
furniDataConflicts,
duration: Date.now() - startTime,
});
+13
View File
@@ -9,6 +9,7 @@ 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 {
autoPriceFurni,
CATEGORY_PAGE,
@@ -415,6 +416,9 @@ export const POST = withAdmin(
// Backfill any missing nitros into the emulator's Gamedata bundle
const assetsSynced = await syncAssetsToGamedataBundle();
// Verify the furnidata matches the DB ids
const furniReconcile = await reconcileFurniDataWithItemsBase();
const succeeded = results.filter((r) => r.ok).length;
const failed = results.filter((r) => !r.ok).length;
@@ -427,6 +431,9 @@ export const POST = withAdmin(
ownershipFixed: ownership.fixed,
nitrosSynced: assetsSynced.copiedNitros,
iconsSynced: assetsSynced.copiedIcons,
furniDataFixedIds: furniReconcile.fixedIds,
furniDataMissing: furniReconcile.missing,
furniDataConflicts: furniReconcile.conflicts,
});
}
@@ -485,6 +492,9 @@ export const POST = withAdmin(
// Backfill any missing nitros and icons into the emulator's Gamedata bundle
const assetsSynced = await syncAssetsToGamedataBundle();
// Verify the furnidata matches the DB ids
const furniReconcile = await reconcileFurniDataWithItemsBase();
logAudit({
userId: ctx.session.user.id,
action: "furni_import",
@@ -509,6 +519,9 @@ export const POST = withAdmin(
ownershipFixed: ownership.fixed,
nitrosSynced: assetsSynced.copiedNitros,
iconsSynced: assetsSynced.copiedIcons,
furniDataFixedIds: furniReconcile.fixedIds,
furniDataMissing: furniReconcile.missing,
furniDataConflicts: furniReconcile.conflicts,
spriteCount: result.spriteCount,
warnings: result.warnings,
});
+84
View File
@@ -1,5 +1,6 @@
import { existsSync, promises as fs } from "node:fs";
import path from "node:path";
import { db, ItemsBase } from "@/lib/db";
import {
getGamedataRoot,
getNitroFilesRoot,
@@ -442,3 +443,86 @@ export async function appendFurniEntriesBatch(
await writeFurniData(furniData as Record<string, unknown>);
});
}
export interface FurniDataReconcileResult {
/** Entries whose stale spriteId was corrected to the DB id. */
fixedIds: number;
/** items_base rows with no matching furnitype entry (need re-import). */
missing: number;
/** spriteIds used by a different classname in the file; left untouched. */
conflicts: number;
}
/**
* Cross-check every items_base row against the local FurnitureData.json and
* repair drift:
* - an entry matching the classname but carrying a stale spriteId is updated
* to the DB id (items_base is authoritative);
* - rows missing entirely from the file are counted (they need a re-import
* or batch-regen, since building an entry requires the SWF/nitro metadata);
* - genuine spriteId conflicts (id used by a different classname) are counted
* but never auto-fixed.
*/
export async function reconcileFurniDataWithItemsBase(): Promise<FurniDataReconcileResult> {
return withFurniDataLock(async () => {
const rows = await db
.select({ id: ItemsBase.id, itemName: ItemsBase.itemName })
.from(ItemsBase);
const furniData = (await readFurniData()) as Record<
string,
{ furnitype: Array<Record<string, unknown>> }
>;
const byId = new Map<
number,
{ section: string; index: number; entry: Record<string, unknown> }
>();
const byClass = new Map<
string,
{ section: string; index: number; entry: Record<string, unknown> }
>();
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
const arr = furniData[section]?.furnitype ?? [];
for (let i = 0; i < arr.length; i++) {
const e = arr[i];
const id = Number(e?.id);
if (Number.isFinite(id) && id > 0 && !byId.has(id)) {
byId.set(id, { section, index: i, entry: e });
}
const cn = typeof e?.classname === "string" ? e.classname : "";
if (cn && !byClass.has(cn)) {
byClass.set(cn, { section, index: i, entry: e });
}
}
}
let fixedIds = 0;
let missing = 0;
let conflicts = 0;
for (const row of rows) {
const spriteId = row.id;
const existing = byId.get(spriteId);
if (existing) {
if (existing.entry.classname !== row.itemName) {
conflicts++;
}
continue;
}
const byName = byClass.get(row.itemName);
if (byName) {
byName.entry.id = spriteId;
byId.set(spriteId, byName);
fixedIds++;
} else {
missing++;
}
}
if (fixedIds > 0) {
await writeFurniData(furniData as Record<string, unknown>);
}
return { fixedIds, missing, conflicts };
});
}