fix(catalog): decode WebP bundle textures so furniture icons resolve again
Every write path normalises a bundle's texture to WebP Lossless, so the catalog icon was being read back with a PNG-only decoder. decodePng throws on anything that is not a PNG, the callers caught that and returned null, and the user-visible result was "no icon (not in source or bundle)" for every furniture whose source does not serve a standalone icon. Measured against the production asset tree, all 18,505 bundles were WebP; icon extraction succeeded on 0 of them. src/lib/services/imager/ decode-texture.ts keeps PNG on the dependency-free decoder and routes WebP through sharp, which is already a dependency and already encodes these textures. extractFurniIconPng and getPetIconPng become async; the five call sites (upload, clone import, furni import, icon repair and both icon routes) already awaited their surrounding work. Same root cause, second bug: the spritesheet frame key. Converters disagree on packing — some keep a trailing ".png", and some lowercase the whole key while leaving the bundle name mixed-case, so "LTD_fashionistaf" looks up frame "LTD_fashionistaf_LTD_fashionistaf_icon_a" and never finds "ltd_fashionistaf_ltd_fashionistaf_icon_a". Any mixed-case classname could therefore never match, which is most of the catalogue. findFrame tries the two exact spellings, then falls back to one case-insensitive pass. Extraction now succeeds on 18,483 of 18,505 bundles (99.88%); the 22 remainder are data, not code — 9 bundles ship no icon asset, 11 do not parse. Third: three catalogue icons exist only as .gif while catalogueIconUrl hardcoded .png, so the picker offered icons that could only ever 404, and 291 icons that ship as both formats were listed twice. The API now dedupes per id and the two renderers retry with .gif before falling back to the placeholder, matching what catalog-image-picker already did. Verified live: /gamedata/.../icon_1542.png returns 404 while icon_1542.gif returns 200. Separately, close the last hole in the memory cap. Every script in package.json routes through scripts/with-memory-cap.sh, but invoking the builder directly — from a terminal, an IDE or an agent — skipped the wrapper and ran unbounded, on a host with no swap where the OOM killer picks its victim across the whole machine. next.config.ts now refuses a production build that the wrapper has not marked, before anything allocates. next dev and next start are deliberately unaffected. README gains a Memory-capped commands section covering the per-script ceilings, the backends and the ulimit -v trap, and its stale version and script tables are corrected.
This commit is contained in:
1 parent
6793f77733
commit
43742e8d99
18 files changed
+433
-67
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { cropRgba, decodePng } from "@/lib/services/imager/png-decode";
|
||||
import { decodeTextureRgba } from "@/lib/services/imager/decode-texture";
|
||||
import { cropRgba } from "@/lib/services/imager/png-decode";
|
||||
import { encodePng, parseNitroBundle } from "@/lib/services/swf/nitro-builder";
|
||||
|
||||
interface FurniAsset {
|
||||
@@ -16,19 +17,48 @@ interface FurniNitroJson {
|
||||
spritesheet?: { frames?: Record<string, FurniFrame> };
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate the spritesheet frame for an asset. Try the two exact spellings
|
||||
* first (with and without a trailing `.png`), then one case-insensitive pass
|
||||
* so a converter that lowercased the frame keys still resolves.
|
||||
*/
|
||||
function findFrame(
|
||||
frames: Record<string, FurniFrame>,
|
||||
name: string,
|
||||
pixelAsset: string,
|
||||
): FurniFrame | undefined {
|
||||
const direct =
|
||||
frames[`${name}_${pixelAsset}`] ?? frames[`${name}_${pixelAsset}.png`];
|
||||
if (direct) return direct;
|
||||
|
||||
const wanted = new Set([
|
||||
`${name}_${pixelAsset}`.toLowerCase(),
|
||||
`${name}_${pixelAsset}.png`.toLowerCase(),
|
||||
]);
|
||||
for (const [key, frame] of Object.entries(frames)) {
|
||||
if (wanted.has(key.toLowerCase())) return frame;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the furni catalog icon — the `{name}_icon_a` sprite — from a Nitro
|
||||
* bundle and return it as a standalone PNG, or null if the bundle has no icon
|
||||
* asset / usable frame. Used for sources that embed furni icons inside the
|
||||
* `.nitro` instead of serving a separate `{classname}_icon.png`.
|
||||
*
|
||||
* Async because the texture is usually WebP Lossless, which needs a real codec
|
||||
* to read back out.
|
||||
*/
|
||||
export function extractFurniIconPng(nitro: Buffer): Buffer | null {
|
||||
export async function extractFurniIconPng(
|
||||
nitro: Buffer,
|
||||
): Promise<Buffer | null> {
|
||||
let json: FurniNitroJson;
|
||||
let png: Buffer;
|
||||
let texture: Buffer;
|
||||
try {
|
||||
const parsed = parseNitroBundle(nitro);
|
||||
json = parsed.json as FurniNitroJson;
|
||||
png = parsed.png;
|
||||
texture = parsed.texture;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -44,18 +74,20 @@ export function extractFurniIconPng(nitro: Buffer): Buffer | null {
|
||||
|
||||
const asset = json.assets[iconKey];
|
||||
// An asset may alias another's pixels via `source`; the frame key is the
|
||||
// bundle name prefixed onto the asset name. Different converters pack the
|
||||
// key with or without a trailing `.png`, so try both.
|
||||
// bundle name prefixed onto the asset name. Converters disagree on the
|
||||
// packing: some keep the trailing `.png`, and some lowercase the whole key
|
||||
// while leaving `name` mixed-case (`LTD_fashionistaf` -> key
|
||||
// `ltd_fashionistaf_ltd_fashionistaf_icon_a`). Match exactly first, then
|
||||
// fall back to a case-insensitive lookup so mixed-case classnames work.
|
||||
const pixelAsset = asset.source ?? iconKey;
|
||||
const frames = json.spritesheet?.frames ?? {};
|
||||
const frame =
|
||||
frames[`${name}_${pixelAsset}`] ?? frames[`${name}_${pixelAsset}.png`];
|
||||
const frame = findFrame(frames, name, pixelAsset);
|
||||
if (!frame || frame.rotated) return null;
|
||||
const { x, y, w, h } = frame.frame;
|
||||
if (w <= 0 || h <= 0) return null;
|
||||
|
||||
try {
|
||||
const sheet = decodePng(png);
|
||||
const sheet = await decodeTextureRgba(texture);
|
||||
const px = cropRgba(sheet.rgba, sheet.width, x, y, w, h);
|
||||
return encodePng(w, h, px);
|
||||
} catch {
|
||||
|
||||
Reference in new issue
Block a user