Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
944527e078 | ||
|
|
d176fad4da |
No files matched your search
@@ -0,0 +1,24 @@
|
||||
import { apiError, apiOk } from "@/lib/api";
|
||||
import { withAdmin } from "@/lib/api-handler";
|
||||
import { planGamedataCleanup } from "@/lib/furni/gamedata-dedupe";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
|
||||
/**
|
||||
* Read-only preview of the gamedata cleanup: duplicate FurnitureData rows,
|
||||
* FurnitureData rows whose `.nitro` is gone, and the dangling/duplicated
|
||||
* FigureMap parts plus the FigureData entries nothing points at any more.
|
||||
*
|
||||
* Nothing here writes. The apply endpoint re-derives every decision under its
|
||||
* own lock, so this response is a report for the operator rather than a plan
|
||||
* that is replayed later.
|
||||
*/
|
||||
export const GET = withAdmin({ permission: PERMS.ASSETS_IMPORT }, async () => {
|
||||
try {
|
||||
return apiOk({ plan: await planGamedataCleanup() });
|
||||
} catch (err) {
|
||||
return apiError(
|
||||
`Gamedata cleanup preview failed: ${(err as Error).message}`,
|
||||
500,
|
||||
);
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -520,14 +520,29 @@ function inflateJsonOrGzip(
|
||||
* renamed. `meta.image` is corrected by `createNitroBundle`.
|
||||
*
|
||||
* A bundle that is already lossless WebP is returned untouched, which keeps
|
||||
* the operation idempotent and avoids a pointless re-encode.
|
||||
* the operation idempotent and avoids a pointless re-encode — but only when
|
||||
* its metadata is already consistent. A bundle whose `meta.image` names a
|
||||
* member that is not in the file is still rebuilt, because a client that
|
||||
* cannot find the texture renders nothing and a re-encode is not what fixes
|
||||
* that.
|
||||
*
|
||||
* Throws if the texture claims to be PNG or WebP but cannot actually be
|
||||
* decoded; callers that would rather keep the original should catch.
|
||||
*/
|
||||
export async function toWebpLosslessBundle(bundle: Buffer): Promise<Buffer> {
|
||||
const parsed = parseNitroBundle(bundle);
|
||||
if (isLosslessWebp(parsed.texture)) return bundle;
|
||||
const meta = (parsed.json as { spritesheet?: { meta?: { image?: string } } })
|
||||
.spritesheet?.meta;
|
||||
if (isLosslessWebp(parsed.texture)) {
|
||||
if (meta?.image === parsed.textureFileName) return bundle;
|
||||
// Stale metadata only: rebuild the archive with the pointer corrected
|
||||
// and reuse the existing VP8L bytes rather than re-encoding them.
|
||||
return createNitroBundle(
|
||||
parsed.json,
|
||||
parsed.texture,
|
||||
parsed.textureFileName.replace(/\.(?:png|webp)$/i, ""),
|
||||
);
|
||||
}
|
||||
|
||||
const image = sharp(parsed.texture);
|
||||
const { width = 0, height = 0 } = await image.metadata();
|
||||
|
||||
@@ -223,6 +223,22 @@ describe("normalising a bundle to WebP Lossless", () => {
|
||||
);
|
||||
await expect(toWebpLosslessBundle(broken)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("repairs stale meta.image in a bundle that is already lossless", async () => {
|
||||
// Already VP8L, so there is nothing to re-encode — but the pointer
|
||||
// names a member the archive does not contain, which a re-encode
|
||||
// would not be what fixes.
|
||||
const stale = await bundleWith("webp", "chair.png");
|
||||
expect(isLosslessWebp(parseNitroBundle(stale).texture)).toBe(true);
|
||||
|
||||
const after = parseNitroBundle(await toWebpLosslessBundle(stale));
|
||||
expect(after.textureFileName).toBe("chair.webp");
|
||||
expect(
|
||||
(after.json.spritesheet as { meta: { image: string } }).meta.image,
|
||||
).toBe("chair.webp");
|
||||
// The existing VP8L bytes are reused rather than decoded again.
|
||||
expect(after.texture.equals(parseNitroBundle(stale).texture)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("spritesheet encoding", () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -6651,6 +6651,38 @@
|
||||
"toastAutoCleanDone": "{deleted} oude fake .nitro-bestand(en) opgeruimd ({copies} kopieën). {recent} recente en {broken} kapotte overgeslagen.",
|
||||
"toastAutoCleanNothing": "Niets oud genoeg om te verwijderen ({recent} recente en {broken} kapotte overgeslagen)",
|
||||
"toastManifestDownload": "Scanmanifest gedownload",
|
||||
"gamedataTitle": "Gamedata opruimen",
|
||||
"gamedataHint": "Eerst een voorbeeld bekijken: dit herschrijft FurnitureData.json, FigureMap.json en FigureData.json. Anders dan de tools hierboven verwijdert dit ook rijen die naar niets verwijzen — controleer de tellingen voordat je het uitvoert.",
|
||||
"gamedataScan": "Gamedata scannen",
|
||||
"gamedataScanning": "Scannen…",
|
||||
"gamedataRun": "Geselecteerde uitvoeren",
|
||||
"gamedataRunning": "Opruimen…",
|
||||
"gamedataNoResult": "Nog geen voorbeeld — scan eerst de gamedata JSON-bestanden.",
|
||||
"gamedataCount_duplicates": "Dubbele FurnitureData",
|
||||
"gamedataCount_orphans": "FurnitureData-wezen",
|
||||
"gamedataCount_figureMap": "FigureMap-problemen",
|
||||
"gamedataCount_figureData": "Ongebruikte FigureData",
|
||||
"gamedataPass_dedupeFurnitureData": "FurnitureData dedupliceren",
|
||||
"gamedataPassDesc_dedupeFurnitureData": "Verwijdert herhaalde classnames binnen één sectie en houdt de entry met de hoogste revision.",
|
||||
"gamedataPass_cleanFurnitureData": "FurnitureData opschonen",
|
||||
"gamedataPassDesc_cleanFurnitureData": "Verwijdert entries waarvan het .nitro-bundle in geen enkele map meer staat.",
|
||||
"gamedataPass_cleanFigureMap": "FigureMap opschonen",
|
||||
"gamedataPassDesc_cleanFigureMap": "Verwijdert parts die naar een ontbrekende set in FigureData wijzen, plus exact dubbele parts.",
|
||||
"gamedataPass_cleanFigureData": "FigureData opschonen",
|
||||
"gamedataPassDesc_cleanFigureData": "Verwijdert sets waarnaar geen FigureMap-part verwijst en daarna palettes die geen enkele settype gebruikt. Moet na FigureMap opschonen draaien.",
|
||||
"gamedataNothingSelected": "Selecteer minstens één opruimstap",
|
||||
"gamedataConfirmTitle": "Gamedata JSON-bestanden herschrijven?",
|
||||
"gamedataConfirmDescription": "{passes} stap(pen) draait tegen {duplicates} dubbele groep(en), {orphans} wezen-rij(en), {figureMap} FigureMap-probleem(en) en {figureData} ongebruikte FigureData-entry('s). Rijen worden zonder back-up verwijderd — dit is niet ongedaan te maken.",
|
||||
"gamedataConfirmLabel": "Bestanden herschrijven",
|
||||
"gamedataDifferingWarning": "Minstens één duplicaat is geen exacte kopie van de entry die behouden blijft; de verwijderde rij bevatte dus data die de overblijvende mist.",
|
||||
"gamedataCrossSectionNote": "{count} classname(s) bestaan zowel in roomitemtypes als wallitemtypes. Die worden alleen gerapporteerd — één verwijderen zou het type van een item veranderen.",
|
||||
"gamedataToastDone": "Gamedata-opruiming verwijderde {removed} rij(en)",
|
||||
"gamedataToastNothing": "Gamedata was al schoon",
|
||||
"gamedataMaxRemovalsLabel": "Max. rijen per stap",
|
||||
"gamedataMaxRemovalsHint": "0 gebruikt de standaard van 500. Een stap die meer zou verwijderen stopt en vraagt om bevestiging in plaats van te schrijven.",
|
||||
"gamedataLimitTitle": "{pass} zou {wouldRemove} rij(en) verwijderen, meer dan de limiet van {maxRemovals}.",
|
||||
"gamedataLimitDescription": "Die stap heeft niets geschreven, dus de bestanden zijn ongewijzigd. Controleer eerst de asset-paden — een verkeerde map laat elke rij als wezen zien. De limiet verhogen is een aparte, bewuste uitvoering.",
|
||||
"gamedataLimitOverride": "{wouldRemove} rij(en) toch verwijderen",
|
||||
"historyTitle": "Opruimgeschiedenis",
|
||||
"historyScheduled": "gepland",
|
||||
"historyManual": "handmatig",
|
||||
|
||||
Reference in new issue
Block a user