Reconcile FurnitureData.json with items_base after import
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:
1 parent
b4021f6b42
commit
b4968a9967
4 files changed
+118
-1
No files matched your search
@@ -10,6 +10,7 @@ import {
|
|||||||
appendFurniEntriesBatch,
|
appendFurniEntriesBatch,
|
||||||
buildFurniEntry,
|
buildFurniEntry,
|
||||||
readFurniData,
|
readFurniData,
|
||||||
|
reconcileFurniDataWithItemsBase,
|
||||||
} from "@/lib/services/furni-data";
|
} from "@/lib/services/furni-data";
|
||||||
import {
|
import {
|
||||||
downloadFile,
|
downloadFile,
|
||||||
@@ -265,6 +266,9 @@ export const POST = withAdmin(
|
|||||||
// Backfill any missing nitros and icons into the emulator's Gamedata bundle
|
// Backfill any missing nitros and icons into the emulator's Gamedata bundle
|
||||||
const assetsSynced = await syncAssetsToGamedataBundle();
|
const assetsSynced = await syncAssetsToGamedataBundle();
|
||||||
|
|
||||||
|
// Verify the furnidata matches the DB ids
|
||||||
|
const furniReconcile = await reconcileFurniDataWithItemsBase();
|
||||||
|
|
||||||
send({
|
send({
|
||||||
type: "regen_complete",
|
type: "regen_complete",
|
||||||
succeeded,
|
succeeded,
|
||||||
@@ -274,6 +278,9 @@ export const POST = withAdmin(
|
|||||||
ownershipFixed: ownership.fixed,
|
ownershipFixed: ownership.fixed,
|
||||||
nitrosSynced: assetsSynced.copiedNitros,
|
nitrosSynced: assetsSynced.copiedNitros,
|
||||||
iconsSynced: assetsSynced.copiedIcons,
|
iconsSynced: assetsSynced.copiedIcons,
|
||||||
|
furniDataFixedIds: furniReconcile.fixedIds,
|
||||||
|
furniDataMissing: furniReconcile.missing,
|
||||||
|
furniDataConflicts: furniReconcile.conflicts,
|
||||||
duration: Date.now() - startTime,
|
duration: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import { apiError } from "@/lib/api-response";
|
|||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { logAudit } from "@/lib/services/audit";
|
import { logAudit } from "@/lib/services/audit";
|
||||||
import { getSource } from "@/lib/services/clone-sources";
|
import { getSource } from "@/lib/services/clone-sources";
|
||||||
import { appendFurniEntriesBatch } from "@/lib/services/furni-data";
|
import {
|
||||||
|
appendFurniEntriesBatch,
|
||||||
|
reconcileFurniDataWithItemsBase,
|
||||||
|
} from "@/lib/services/furni-data";
|
||||||
import {
|
import {
|
||||||
ensureDirectories,
|
ensureDirectories,
|
||||||
ensureFurniOwnership,
|
ensureFurniOwnership,
|
||||||
@@ -219,12 +222,19 @@ export const POST = withAdmin(
|
|||||||
let ownershipFixed: string[] = [];
|
let ownershipFixed: string[] = [];
|
||||||
let nitrosSynced: string[] = [];
|
let nitrosSynced: string[] = [];
|
||||||
let iconsSynced: string[] = [];
|
let iconsSynced: string[] = [];
|
||||||
|
let furniDataFixedIds = 0;
|
||||||
|
let furniDataMissing = 0;
|
||||||
|
let furniDataConflicts = 0;
|
||||||
try {
|
try {
|
||||||
offerIdsFixed = (await reconcileImportedOfferIds()).fixed;
|
offerIdsFixed = (await reconcileImportedOfferIds()).fixed;
|
||||||
ownershipFixed = (await ensureFurniOwnership()).fixed;
|
ownershipFixed = (await ensureFurniOwnership()).fixed;
|
||||||
const assetsSynced = await syncAssetsToGamedataBundle();
|
const assetsSynced = await syncAssetsToGamedataBundle();
|
||||||
nitrosSynced = assetsSynced.copiedNitros;
|
nitrosSynced = assetsSynced.copiedNitros;
|
||||||
iconsSynced = assetsSynced.copiedIcons;
|
iconsSynced = assetsSynced.copiedIcons;
|
||||||
|
const furniReconcile = await reconcileFurniDataWithItemsBase();
|
||||||
|
furniDataFixedIds = furniReconcile.fixedIds;
|
||||||
|
furniDataMissing = furniReconcile.missing;
|
||||||
|
furniDataConflicts = furniReconcile.conflicts;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[import-furni] Post-import reconcile/ownership failed:",
|
"[import-furni] Post-import reconcile/ownership failed:",
|
||||||
@@ -241,6 +251,9 @@ export const POST = withAdmin(
|
|||||||
ownershipFixed,
|
ownershipFixed,
|
||||||
nitrosSynced,
|
nitrosSynced,
|
||||||
iconsSynced,
|
iconsSynced,
|
||||||
|
furniDataFixedIds,
|
||||||
|
furniDataMissing,
|
||||||
|
furniDataConflicts,
|
||||||
duration: Date.now() - startTime,
|
duration: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { logAudit } from "@/lib/services/audit";
|
|||||||
import { getCloneList } from "@/lib/services/clone-import";
|
import { getCloneList } from "@/lib/services/clone-import";
|
||||||
import { getSource } from "@/lib/services/clone-sources";
|
import { getSource } from "@/lib/services/clone-sources";
|
||||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||||
|
import { reconcileFurniDataWithItemsBase } from "@/lib/services/furni-data";
|
||||||
import {
|
import {
|
||||||
autoPriceFurni,
|
autoPriceFurni,
|
||||||
CATEGORY_PAGE,
|
CATEGORY_PAGE,
|
||||||
@@ -415,6 +416,9 @@ export const POST = withAdmin(
|
|||||||
// Backfill any missing nitros into the emulator's Gamedata bundle
|
// Backfill any missing nitros into the emulator's Gamedata bundle
|
||||||
const assetsSynced = await syncAssetsToGamedataBundle();
|
const assetsSynced = await syncAssetsToGamedataBundle();
|
||||||
|
|
||||||
|
// Verify the furnidata matches the DB ids
|
||||||
|
const furniReconcile = await reconcileFurniDataWithItemsBase();
|
||||||
|
|
||||||
const succeeded = results.filter((r) => r.ok).length;
|
const succeeded = results.filter((r) => r.ok).length;
|
||||||
const failed = 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,
|
ownershipFixed: ownership.fixed,
|
||||||
nitrosSynced: assetsSynced.copiedNitros,
|
nitrosSynced: assetsSynced.copiedNitros,
|
||||||
iconsSynced: assetsSynced.copiedIcons,
|
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
|
// Backfill any missing nitros and icons into the emulator's Gamedata bundle
|
||||||
const assetsSynced = await syncAssetsToGamedataBundle();
|
const assetsSynced = await syncAssetsToGamedataBundle();
|
||||||
|
|
||||||
|
// Verify the furnidata matches the DB ids
|
||||||
|
const furniReconcile = await reconcileFurniDataWithItemsBase();
|
||||||
|
|
||||||
logAudit({
|
logAudit({
|
||||||
userId: ctx.session.user.id,
|
userId: ctx.session.user.id,
|
||||||
action: "furni_import",
|
action: "furni_import",
|
||||||
@@ -509,6 +519,9 @@ export const POST = withAdmin(
|
|||||||
ownershipFixed: ownership.fixed,
|
ownershipFixed: ownership.fixed,
|
||||||
nitrosSynced: assetsSynced.copiedNitros,
|
nitrosSynced: assetsSynced.copiedNitros,
|
||||||
iconsSynced: assetsSynced.copiedIcons,
|
iconsSynced: assetsSynced.copiedIcons,
|
||||||
|
furniDataFixedIds: furniReconcile.fixedIds,
|
||||||
|
furniDataMissing: furniReconcile.missing,
|
||||||
|
furniDataConflicts: furniReconcile.conflicts,
|
||||||
spriteCount: result.spriteCount,
|
spriteCount: result.spriteCount,
|
||||||
warnings: result.warnings,
|
warnings: result.warnings,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { existsSync, promises as fs } from "node:fs";
|
import { existsSync, promises as fs } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { db, ItemsBase } from "@/lib/db";
|
||||||
import {
|
import {
|
||||||
getGamedataRoot,
|
getGamedataRoot,
|
||||||
getNitroFilesRoot,
|
getNitroFilesRoot,
|
||||||
@@ -442,3 +443,86 @@ export async function appendFurniEntriesBatch(
|
|||||||
await writeFurniData(furniData as Record<string, unknown>);
|
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 };
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user