feat(nitro-cleanup): dedupe FurnitureData and clean dangling figure entries
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 36s
CI / tests-integration (push) Successful in 1m58s
CI / tests-unit (push) Successful in 2m16s
CI / tests-ui (push) Successful in 3m8s
CI / preflight (push) Skipped
CI / deploy (push) Failing after 2m30s

Add a gamedata cleanup to the Nitro Cleanup panel: a read-only preview
plus an apply run that dedupes FurnitureData classnames and removes
rows and figure entries that reference nothing.

Three passes run in a fixed order, because cleanFigureMap has to precede
cleanFigureData: dropping the part that points at a set is what makes
that set unreferenced.

A pass refuses to write when it would delete more than maxRemovals rows
(default 500) and reports the reason, a wrong asset directory otherwise
turns every row into an orphan and one call would empty the file. Passes
that would act on empty input (no libraries, no sets) treat that as a
missing file rather than as a reason to delete everything. Every write
copies the file to a timestamped backup first, so a pass that turns out
to be wrong can be undone by hand.

The plan reads FurnitureData once and hands the parsed copy to both
furniture passes; the file is tens of megabytes in a real deployment.
This commit is contained in:
openhands committed 2026-09-27 17:14:30 +02:00
1 parent d176fad4da
commit 944527e078
7 files changed
+2163 -1

No files matched your search

+32
View File
@@ -6645,6 +6645,38 @@
"toastAutoCleanDone": "Auto-cleaned {deleted} old fake .nitro file(s) ({copies} copies). Skipped {recent} recent + {broken} broken.",
"toastAutoCleanNothing": "Nothing old enough to remove (skipped {recent} recent, {broken} broken)",
"toastManifestDownload": "Scan manifest downloaded",
"gamedataTitle": "Gamedata cleanup",
"gamedataHint": "Preview first: this rewrites FurnitureData.json, FigureMap.json and FigureData.json. Unlike the asset tools above it removes rows that reference nothing — review the counts before running it.",
"gamedataScan": "Preview gamedata",
"gamedataScanning": "Scanning…",
"gamedataRun": "Run selected",
"gamedataRunning": "Cleaning…",
"gamedataNoResult": "No preview yet — scan the gamedata JSON files first.",
"gamedataCount_duplicates": "Duplicate FurnitureData",
"gamedataCount_orphans": "FurnitureData orphans",
"gamedataCount_figureMap": "FigureMap issues",
"gamedataCount_figureData": "FigureData unused",
"gamedataPass_dedupeFurnitureData": "Dedupe FurnitureData",
"gamedataPassDesc_dedupeFurnitureData": "Removes repeated classnames inside one section, keeping the entry with the highest revision.",
"gamedataPass_cleanFurnitureData": "Clean FurnitureData",
"gamedataPassDesc_cleanFurnitureData": "Removes entries whose .nitro bundle is missing from every directory.",
"gamedataPass_cleanFigureMap": "Clean FigureMap",
"gamedataPassDesc_cleanFigureMap": "Removes parts pointing at a set missing from FigureData, plus exact duplicate parts.",
"gamedataPass_cleanFigureData": "Clean FigureData",
"gamedataPassDesc_cleanFigureData": "Removes sets no FigureMap part references, then palettes no set type uses. Must run after Clean FigureMap.",
"gamedataNothingSelected": "Select at least one cleanup pass",
"gamedataConfirmTitle": "Rewrite gamedata JSON files?",
"gamedataConfirmDescription": "{passes} pass(es) will run against {duplicates} duplicate group(s), {orphans} orphan row(s), {figureMap} FigureMap issue(s) and {figureData} unused FigureData entr(ies). Rows are removed without a backup — this cannot be undone.",
"gamedataConfirmLabel": "Rewrite files",
"gamedataDifferingWarning": "At least one duplicate is not an exact copy of the entry that is kept, so the removed row held data the survivor does not have.",
"gamedataCrossSectionNote": "{count} classname(s) exist in both roomitemtypes and wallitemtypes. Those are reported only — removing one would change an item's type.",
"gamedataToastDone": "Gamedata cleanup removed {removed} row(s)",
"gamedataToastNothing": "Gamedata was already clean",
"gamedataMaxRemovalsLabel": "Max rows per pass",
"gamedataMaxRemovalsHint": "0 uses the default of 500. A pass that would remove more stops and asks instead of writing.",
"gamedataLimitTitle": "{pass} would remove {wouldRemove} rows, above the limit of {maxRemovals}.",
"gamedataLimitDescription": "Nothing was written by that pass, so the files are unchanged. Check the asset paths first — a wrong directory makes every row look orphaned. Raising the limit is a separate, deliberate run.",
"gamedataLimitOverride": "Remove {wouldRemove} rows anyway",
"historyTitle": "Auto-clean history",
"historyScheduled": "scheduled",
"historyManual": "manual",