fix: normalize cross-section duplicate ids in furnidata reconcile

Classnames listed in both roomitemtypes and wallitemtypes keep a canonical copy
with the correct id/offerid while the stray wall copy can carry a legacy wrong
id. reconcileFurniDataWithItemsBase now normalizes those stray copies to the
offerid whenever the offerid maps to an items_base row whose classname matches
exactly, so id === offerid === sprite id holds for every uniquely-mapped
classname. The sandbox rehearsal asserts this invariant and the live database
has been reconciled (76 ids fixed, 0 remaining).
This commit is contained in:
openhands committed 2026-08-20 14:57:40 +02:00
1 parent 3a63152ee3
commit b811086de6
2 files changed
+48

No files matched your search

@@ -230,6 +230,36 @@ describeGuard("catalog studio live repair rehearsal (sandbox)", () => {
const offer = await reconcileImportedOfferIds();
console.log("REHEARSAL offer reconcile:", JSON.stringify(offer));
// Every furnidata entry whose classname maps to a unique items_base row
// must have id === offerid === db id (incl. cross-section copies).
const [uniqueRows] = (await db.execute(sql`
SELECT id, item_name FROM items_base
`)) as unknown as [Array<{ id: number; item_name: string }>, unknown];
const dbById = new Map<number, string>();
for (const r of uniqueRows) dbById.set(Number(r.id), r.item_name);
const byName = new Map<string, number>();
for (const r of uniqueRows)
byName.set(r.item_name, (byName.get(r.item_name) ?? 0) + 1);
const finalFd = JSON.parse(
await fs.readFile(
path.join(gamedataDir, "config", "FurnitureData.json"),
"utf8",
),
) as Record<string, { furnitype: Array<Record<string, unknown>> }>;
let badCrossSection = 0;
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
for (const e of finalFd[section]?.furnitype ?? []) {
const cn = String(e.classname ?? "");
if (byName.get(cn) !== 1) continue;
const offer = Number(e.offerid);
if (!Number.isFinite(offer) || offer <= 0) continue;
if (dbById.get(offer) !== cn) continue;
if (Number(e.id) !== offer) badCrossSection++;
}
}
console.log("REHEARSAL cross-section id mismatches:", badCrossSection);
expect(badCrossSection).toBe(0);
const afterState = await audit();
console.log("REHEARSAL after:", JSON.stringify(afterState));
+18
View File
@@ -536,6 +536,24 @@ export async function reconcileFurniDataWithItemsBase(): Promise<FurniDataReconc
}
}
// Normalize cross-section duplicates (e.g. a wall item listed in both
// roomitemtypes and wallitemtypes). The canonical copy already has the
// correct id/offerid; a stray copy may carry a legacy wrong `id` while
// its offerid still points at the right sprite. Only fix when the
// offerid maps to an items_base row whose classname matches this entry
// exactly, so we never guess.
const dbById = new Map<number, string>();
for (const row of rows) dbById.set(row.id, row.itemName);
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
for (const e of furniData[section]?.furnitype ?? []) {
const offer = entryOfferId(e);
if (offer <= 0 || Number(e.id) === offer) continue;
if (dbById.get(offer) !== e.classname) continue;
e.id = offer;
fixedIds++;
}
}
if (fixedIds > 0 || fixedOfferIds > 0) {
await writeFurniData(furniData as Record<string, unknown>);
}