65 lines
2.0 KiB
TypeScript
65 lines
2.0 KiB
TypeScript
import { cropRgba, decodePng } from "@/lib/services/imager/png-decode";
|
|
import { encodePng, parseNitroBundle } from "@/lib/services/swf/nitro-builder";
|
|
|
|
interface FurniAsset {
|
|
x?: number;
|
|
y?: number;
|
|
source?: string;
|
|
}
|
|
interface FurniFrame {
|
|
frame: { x: number; y: number; w: number; h: number };
|
|
rotated?: boolean;
|
|
}
|
|
interface FurniNitroJson {
|
|
name?: string;
|
|
assets?: Record<string, FurniAsset>;
|
|
spritesheet?: { frames?: Record<string, FurniFrame> };
|
|
}
|
|
|
|
/**
|
|
* 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`.
|
|
*/
|
|
export function extractFurniIconPng(nitro: Buffer): Buffer | null {
|
|
let json: FurniNitroJson;
|
|
let png: Buffer;
|
|
try {
|
|
const parsed = parseNitroBundle(nitro);
|
|
json = parsed.json as FurniNitroJson;
|
|
png = parsed.png;
|
|
} catch {
|
|
return null;
|
|
}
|
|
const name = json.name;
|
|
if (!name || !json.assets) return null;
|
|
|
|
// The icon asset is conventionally `${name}_icon_a`; fall back to any
|
|
// `${name}_icon*` the bundle exposes.
|
|
const iconKey = json.assets[`${name}_icon_a`]
|
|
? `${name}_icon_a`
|
|
: Object.keys(json.assets).find((k) => k.startsWith(`${name}_icon`));
|
|
if (!iconKey) return 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.
|
|
const pixelAsset = asset.source ?? iconKey;
|
|
const frames = json.spritesheet?.frames ?? {};
|
|
const frame =
|
|
frames[`${name}_${pixelAsset}`] ?? frames[`${name}_${pixelAsset}.png`];
|
|
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 px = cropRgba(sheet.rgba, sheet.width, x, y, w, h);
|
|
return encodePng(w, h, px);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|