fix(furni): generate scale 32 from bundles with .png frame keys
CI / check (push) Successful in 2m30s
CI / deploy (push) Successful in 1m25s
CI / publish-container (push) Successful in 48s

resolveNitroFrame now matches spritesheet frame keys that carry a .png
suffix or namespaced naming, and isScale/scaleName preserve that suffix.
Broken source sprites (missing frames or references to icon artwork) are
skipped and reported instead of aborting the whole generation, and the
studio UI surfaces the skipped count.
This commit is contained in:
openhands committed 2026-09-12 12:46:17 +02:00
1 parent 0842788a28
commit 4d720ee03c
5 files changed
+119 -29

No files matched your search

+40 -13
View File
@@ -6,10 +6,14 @@ import {
} from "@/lib/services/swf/nitro-builder";
import { object, resolveNitroFrame } from "./nitro-quality";
const scaleName = (name: string) =>
name.replace(/_64_(?=[^_]+_[0-7]_\d+$)/, "_32_");
const scaleName = (name: string) => {
const png = name.endsWith(".png");
const base = png ? name.slice(0, -4) : name;
const renamed = base.replace(/_64_(?=[^_]+_[0-7]_\d+$)/, "_32_");
return png ? `${renamed}.png` : renamed;
};
const isScale = (name: string, size: number) =>
new RegExp(`_${size}_[^_]+_[0-7]_\\d+$`).test(name);
new RegExp(`_${size}_[^_]+_[0-7]_\\d+(\\.png)?$`).test(name);
function halfOffsets(value: unknown): unknown {
if (Array.isArray(value)) return value.map(halfOffsets);
if (!value || typeof value !== "object") return value;
@@ -34,9 +38,13 @@ function emptyScaleDeclaration(viz: Record<string, unknown>): boolean {
);
});
}
export async function generateScale32(
input: Buffer,
): Promise<{ bundle: Buffer; before: string; after: string; count: number }> {
export async function generateScale32(input: Buffer): Promise<{
bundle: Buffer;
before: string;
after: string;
count: number;
skipped: number;
}> {
const { json, png } = parseNitroBundle(input);
const assets = object(json.assets),
sheet = object(json.spritesheet),
@@ -54,7 +62,9 @@ export async function generateScale32(
throw Error("Scale 32 already exists; original artwork is preserved");
const viz = visualizations.find((v) => object(v).size === 64);
const names = Object.keys(assets).filter((n) => isScale(n, 64));
if (!viz || !names.length)
if (!viz)
throw Error("The bundle has no scale 64 visualization to derive from");
if (!names.length)
throw Error("No supported scale 64 furniture sprites were found");
if (names.length > 10000) throw Error("Too many sprites to generate safely");
const dimensions = await sharp(png).metadata();
@@ -68,6 +78,7 @@ export async function generateScale32(
}[] = [];
let before = "";
let pixels = 0;
const skipped = new Set<string>();
for (const name of names) {
const asset = object(assets[name]);
for (const key of ["x", "y"])
@@ -76,10 +87,18 @@ export async function generateScale32(
(typeof asset[key] !== "number" || !Number.isFinite(asset[key]))
)
throw Error("Invalid sprite offset");
const resolved = resolveNitroFrame(json, name);
if (typeof asset.source === "string" && asset.source !== name) {
if (!names.includes(asset.source))
throw Error("A sprite references artwork outside scale 64");
if (!names.includes(asset.source)) {
skipped.add(name);
continue;
}
continue;
}
let resolved: ReturnType<typeof resolveNitroFrame>;
try {
resolved = resolveNitroFrame(json, name);
} catch (error) {
skipped.add(name);
continue;
}
const frame = resolved.frame,
@@ -149,10 +168,17 @@ export async function generateScale32(
])
.png()
.toBuffer();
const generated = new Map<string, boolean>();
for (const name of names)
if (!skipped.has(name)) generated.set(scaleName(name), true);
for (const name of names) {
if (skipped.has(name)) continue;
const asset = object(halfOffsets(assets[name]));
if (typeof asset.source === "string")
asset.source = scaleName(asset.source);
if (typeof asset.source === "string") {
const target = scaleName(asset.source);
if (!generated.has(target)) continue;
asset.source = target;
}
assets[scaleName(name)] = asset;
}
for (const [name, rect] of atlas.frames) {
@@ -188,6 +214,7 @@ export async function generateScale32(
bundle: createNitroBundle(json, combined, String(json.name || "furniture")),
before,
after: `data:image/png;base64,${preview.toString("base64")}`,
count: names.length,
count: images.length,
skipped: skipped.size,
};
}