Fix FurnitureData.json spriteId conflict auto-repair
CI / check (push) Successful in 1m21s
CI / release (push) Skipped
CI / deploy (push) Successful in 2m17s

- Added spriteId conflict resolution to repairFurniData(): keeps first classname per id, removes conflicting entries
- Returns new removedIdConflicts count in repair result
- Updated audit event type and client UI to display removedIdConflicts
- Updated catalog-audit.ts event type and client state type
- When 'Repair FurnitureData.json' is checked, it now fixes id conflicts automatically
This commit is contained in:
openhands committed 2026-08-14 18:23:00 +02:00
1 parent 3b6ebe7bdc
commit f89b8a6adf
3 files changed
+56 -8

No files matched your search

+15 -1
View File
@@ -86,7 +86,11 @@ interface AuditEvent {
sqlCount?: number;
applied?: number;
sqlFailed?: number;
furniData?: { added: number; removedDuplicates: number };
furniData?: {
added: number;
removedDuplicates: number;
removedIdConflicts: number;
};
structure?: {
orphanedRemoved: number;
orphanedCleaned: number;
@@ -175,6 +179,7 @@ export function AuditClient() {
const [furniDataResult, setFurniDataResult] = useState<{
added: number;
removedDuplicates: number;
removedIdConflicts: number;
} | null>(null);
const [structureResult, setStructureResult] = useState<{
orphanedRemoved: number;
@@ -675,6 +680,15 @@ export function AuditClient() {
</span>
</>
)}
{furniDataResult.removedIdConflicts > 0 && (
<>
{" · "}
<span className="text-[var(--admin-warning)] font-medium">
{furniDataResult.removedIdConflicts} spriteId conflicts
fixed
</span>
</>
)}
</span>
</div>
)}
+6 -1
View File
@@ -56,7 +56,11 @@ export interface AuditEvent {
sqlCount?: number;
applied?: number;
sqlFailed?: number;
furniData?: { added: number; removedDuplicates: number };
furniData?: {
added: number;
removedDuplicates: number;
removedIdConflicts: number;
};
structure?: {
orphanedRemoved: number;
orphanedCleaned: number;
@@ -715,6 +719,7 @@ export async function runCatalogAudit(
furniData: {
added: result.added,
removedDuplicates: result.removedDuplicates,
removedIdConflicts: result.removedIdConflicts,
},
});
} catch (err) {
+35 -6
View File
@@ -620,6 +620,7 @@ export async function repairDuplicateClassnames(): Promise<DuplicateClassnameRes
export interface FurniDataRepairResult {
added: number;
removedDuplicates: number;
removedIdConflicts: number;
}
function buildMinimalFurniEntry(item: {
@@ -658,7 +659,8 @@ function buildMinimalFurniEntry(item: {
/**
* Add FurnitureData.json entries for items_base rows that are missing from the
* file, and remove duplicate classnames (keeping the first occurrence).
* file, remove duplicate classnames, and resolve spriteId conflicts
* (keeping the first classname per id, removing conflicting ones).
*/
export async function repairFurniData(
items: Array<{
@@ -668,7 +670,8 @@ export async function repairFurniData(
type: string;
}>,
): Promise<FurniDataRepairResult> {
if (items.length === 0) return { added: 0, removedDuplicates: 0 };
if (items.length === 0)
return { added: 0, removedDuplicates: 0, removedIdConflicts: 0 };
return withFurniDataLock(async () => {
const furniData = (await readFurniData()) as Record<
@@ -697,7 +700,33 @@ export async function repairFurniData(
}
}
// 2) Add entries for items_base rows missing from the file.
// 2) Resolve spriteId conflicts (same id used by different classnames) — keep first, remove others.
let removedIdConflicts = 0;
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
const sec = furniData[section];
if (!sec?.furnitype) continue;
const idToClassname = new Map<number, string>();
const deduped = sec.furnitype.filter((e) => {
const id = Number(e?.id);
const classname = typeof e.classname === "string" ? e.classname : "";
if (!Number.isFinite(id) || id <= 0 || !classname) return true;
const existing = idToClassname.get(id);
if (existing && existing !== classname) {
// Conflict: same id used by different classname — keep first, remove this
removedIdConflicts++;
return false;
}
if (!existing) {
idToClassname.set(id, classname);
}
return true;
});
if (deduped.length !== sec.furnitype.length) {
furniData[section].furnitype = deduped;
}
}
// 3) Add entries for items_base rows missing from the file.
const existing = new Set<string>();
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
for (const e of furniData[section]?.furnitype ?? []) {
@@ -714,8 +743,8 @@ export async function repairFurniData(
});
}
// 3) Write once if anything changed.
if (removedDuplicates > 0 || added.length > 0) {
// 4) Write once if anything changed.
if (removedDuplicates > 0 || removedIdConflicts > 0 || added.length > 0) {
for (const { entry, itemType } of added) {
const sectionKey = itemType === "i" ? "wallitemtypes" : "roomitemtypes";
if (!furniData[sectionKey]) {
@@ -725,6 +754,6 @@ export async function repairFurniData(
}
await writeFurniData(furniData as Record<string, unknown>);
}
return { added: added.length, removedDuplicates };
return { added: added.length, removedDuplicates, removedIdConflicts };
});
}