feat: synthesize size 32 (catalog icon) frame for all furniture .nitro bundles
CI / check (push) Successful in 35s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m18s

AddSize32ToBundle downscales the largest existing sprite (usually size 64)
to generate a 32 visualization, so every bundle — including the
source-provided gamedata furniture — carries a 32 frame without needing the
original SWF. AddSize32AllNitros rewrites every existing .nitro across all
nitro directories (incl. /var/www/Gamedata/bundled/furniture) and is
idempotent (bundles that already have 32 are skipped). Expose it via a Studio
button and the background admin endpoint POST /api/admin/import/furni/add-size32.
This commit is contained in:
openhands committed 2026-08-22 13:31:50 +02:00
1 parent 2996ae3ab0
commit ac7c427c3d
4 files changed
+668 -1

No files matched your search

@@ -0,0 +1,97 @@
import { withAdmin } from "@/lib/api-handler";
import { apiError, apiOk } from "@/lib/api-response";
import { PERMS } from "@/lib/permissions";
import { logAudit } from "@/lib/services/audit";
import { addSize32AllNitros } from "@/lib/services/repair-nitros";
interface Size32Status {
running: boolean;
startedAt: number | null;
finishedAt: number | null;
succeeded: number;
failed: number;
skipped: number;
existing: number;
lastMessage: string | null;
}
const status: Size32Status = {
running: false,
startedAt: null,
finishedAt: null,
succeeded: 0,
failed: 0,
skipped: 0,
existing: 0,
lastMessage: null,
};
function runJob(userId: number) {
status.running = true;
status.startedAt = Date.now();
status.finishedAt = null;
status.succeeded = 0;
status.failed = 0;
status.skipped = 0;
status.existing = 0;
status.lastMessage = "started";
addSize32AllNitros((evt) => {
if (evt.type === "progress") {
if (evt.status === "done") status.succeeded++;
else if (evt.status === "convert-failed" || evt.status === "write-failed")
status.failed++;
else status.skipped++;
if (evt.message)
status.lastMessage = `${evt.classname}: ${evt.status} ${evt.message}`;
} else if (evt.type === "batch_complete") {
status.succeeded = evt.succeeded ?? 0;
status.failed = evt.failed ?? 0;
status.skipped = evt.skipped ?? 0;
status.existing = evt.existing ?? 0;
status.lastMessage = "done";
} else if (evt.type === "error") {
status.lastMessage = evt.message ?? null;
}
})
.then((result) => {
status.succeeded = result.succeeded;
status.failed = result.failed;
status.skipped = result.skipped;
status.existing = result.existing;
status.lastMessage = "done";
status.running = false;
status.finishedAt = Date.now();
})
.catch((err) => {
status.lastMessage = (err as Error).message;
status.running = false;
status.finishedAt = Date.now();
});
logAudit({
userId,
action: "furni_add_size32",
target: "FurniNitro",
targetId: 0,
});
}
export const POST = withAdmin(
{ permission: PERMS.ASSETS_IMPORT },
async (_request, ctx) => {
if (status.running) {
return apiOk({ ...status, alreadyRunning: true });
}
try {
runJob(ctx.session.user.id);
return apiOk({ ...status, started: true });
} catch (err) {
return apiError((err as Error).message, 500);
}
},
);
export const GET = withAdmin({ permission: PERMS.ASSETS_IMPORT }, async () => {
return apiOk({ ...status });
});