Fix FurnitureData.json spriteId conflict auto-repair

- 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; sqlCount?: number;
applied?: number; applied?: number;
sqlFailed?: number; sqlFailed?: number;
furniData?: { added: number; removedDuplicates: number }; furniData?: {
added: number;
removedDuplicates: number;
removedIdConflicts: number;
};
structure?: { structure?: {
orphanedRemoved: number; orphanedRemoved: number;
orphanedCleaned: number; orphanedCleaned: number;
@@ -175,6 +179,7 @@ export function AuditClient() {
const [furniDataResult, setFurniDataResult] = useState<{ const [furniDataResult, setFurniDataResult] = useState<{
added: number; added: number;
removedDuplicates: number; removedDuplicates: number;
removedIdConflicts: number;
} | null>(null); } | null>(null);
const [structureResult, setStructureResult] = useState<{ const [structureResult, setStructureResult] = useState<{
orphanedRemoved: number; orphanedRemoved: number;
@@ -675,6 +680,15 @@ export function AuditClient() {
</span> </span>
</> </>
)} )}
{furniDataResult.removedIdConflicts > 0 && (
<>
{" · "}
<span className="text-[var(--admin-warning)] font-medium">
{furniDataResult.removedIdConflicts} spriteId conflicts
fixed
</span>
</>
)}
</span> </span>
</div> </div>
)} )}
+6 -1
View File
@@ -56,7 +56,11 @@ export interface AuditEvent {
sqlCount?: number; sqlCount?: number;
applied?: number; applied?: number;
sqlFailed?: number; sqlFailed?: number;
furniData?: { added: number; removedDuplicates: number }; furniData?: {
added: number;
removedDuplicates: number;
removedIdConflicts: number;
};
structure?: { structure?: {
orphanedRemoved: number; orphanedRemoved: number;
orphanedCleaned: number; orphanedCleaned: number;
@@ -715,6 +719,7 @@ export async function runCatalogAudit(
furniData: { furniData: {
added: result.added, added: result.added,
removedDuplicates: result.removedDuplicates, removedDuplicates: result.removedDuplicates,
removedIdConflicts: result.removedIdConflicts,
}, },
}); });
} catch (err) { } catch (err) {
+35 -6
View File
@@ -620,6 +620,7 @@ export async function repairDuplicateClassnames(): Promise<DuplicateClassnameRes
export interface FurniDataRepairResult { export interface FurniDataRepairResult {
added: number; added: number;
removedDuplicates: number; removedDuplicates: number;
removedIdConflicts: number;
} }
function buildMinimalFurniEntry(item: { function buildMinimalFurniEntry(item: {
@@ -658,7 +659,8 @@ function buildMinimalFurniEntry(item: {
/** /**
* Add FurnitureData.json entries for items_base rows that are missing from the * 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( export async function repairFurniData(
items: Array<{ items: Array<{
@@ -668,7 +670,8 @@ export async function repairFurniData(
type: string; type: string;
}>, }>,
): Promise<FurniDataRepairResult> { ): 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 () => { return withFurniDataLock(async () => {
const furniData = (await readFurniData()) as Record< 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>(); const existing = new Set<string>();
for (const section of ["roomitemtypes", "wallitemtypes"] as const) { for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
for (const e of furniData[section]?.furnitype ?? []) { for (const e of furniData[section]?.furnitype ?? []) {
@@ -714,8 +743,8 @@ export async function repairFurniData(
}); });
} }
// 3) Write once if anything changed. // 4) Write once if anything changed.
if (removedDuplicates > 0 || added.length > 0) { if (removedDuplicates > 0 || removedIdConflicts > 0 || added.length > 0) {
for (const { entry, itemType } of added) { for (const { entry, itemType } of added) {
const sectionKey = itemType === "i" ? "wallitemtypes" : "roomitemtypes"; const sectionKey = itemType === "i" ? "wallitemtypes" : "roomitemtypes";
if (!furniData[sectionKey]) { if (!furniData[sectionKey]) {
@@ -725,6 +754,6 @@ export async function repairFurniData(
} }
await writeFurniData(furniData as Record<string, unknown>); await writeFurniData(furniData as Record<string, unknown>);
} }
return { added: added.length, removedDuplicates }; return { added: added.length, removedDuplicates, removedIdConflicts };
}); });
} }