From 7cfc30c7a19f0d1c77faed6a1f747e2f127747bb Mon Sep 17 00:00:00 2001 From: GonnaHetzMe Date: Tue, 7 Apr 2026 22:35:17 +0200 Subject: [PATCH 1/2] feat: add RIFF container support --- .../buildAssets/consumableProvisionIcons.ts | 61 +++++++++++++++++-- .../src/core/blitz/parsePackedSpriteRect.ts | 53 ++++++++++++++++ 2 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 packages/scripts/src/core/blitz/parsePackedSpriteRect.ts diff --git a/packages/scripts/src/buildAssets/consumableProvisionIcons.ts b/packages/scripts/src/buildAssets/consumableProvisionIcons.ts index 5622ddd88..d933ae28e 100644 --- a/packages/scripts/src/buildAssets/consumableProvisionIcons.ts +++ b/packages/scripts/src/buildAssets/consumableProvisionIcons.ts @@ -1,5 +1,6 @@ import sharp from "sharp"; import { AssetUploader } from "../core/github/assetUploader"; +import { parsePackedSpriteRect } from "../core/blitz/parsePackedSpriteRect"; import { vfs } from "./constants"; import { ConsumablesCommon, ProvisionsCommon } from "./definitions"; @@ -57,8 +58,35 @@ export async function consumableProvisionIcons() { .replace(".txt", ""); if (await vfs.resolve(`Data/${configPath}.packed.webp`)) { - const image = sharp(await vfs.file(`Data/${configPath}.packed.webp`)); - const content = await image.trim({ threshold: 100 }).toBuffer(); + const packedBuffer = await vfs.file(`Data/${configPath}.packed.webp`); + const consumablesTexture = sharp(packedBuffer); + const sizes = parsePackedSpriteRect(packedBuffer); + let content: Buffer; + + if (sizes) { + try { + content = await consumablesTexture + .clone() + .extract({ + left: sizes[0], + top: sizes[1], + width: sizes[2], + height: sizes[3], + }) + .toBuffer(); + } catch { + console.warn( + `Failed to extract consumable ${consumable.icon} from RIFF data; falling back to trim...`, + ); + content = await consumablesTexture + .trim({ threshold: 100 }) + .toBuffer(); + } + } else { + content = await consumablesTexture + .trim({ threshold: 100 }) + .toBuffer(); + } await uploader.add({ path: `icons/consumables/${consumable.id}.webp`, @@ -122,8 +150,33 @@ export async function consumableProvisionIcons() { .replace(".txt", ""); if (await vfs.resolve(`Data/${configPath}.packed.webp`)) { - const image = sharp(await vfs.file(`Data/${configPath}.packed.webp`)); - const content = await image.trim({ threshold: 100 }).toBuffer(); + const packedBuffer = await vfs.file(`Data/${configPath}.packed.webp`); + const provisionsTexture = sharp(packedBuffer); + const sizes = parsePackedSpriteRect(packedBuffer); + let content: Buffer; + + if (sizes) { + try { + content = await provisionsTexture + .clone() + .extract({ + left: sizes[0], + top: sizes[1], + width: sizes[2], + height: sizes[3], + }) + .toBuffer(); + } catch { + console.warn( + `Failed to extract provision ${provision.icon} from RIFF data; falling back to trim...`, + ); + content = await provisionsTexture + .trim({ threshold: 100 }) + .toBuffer(); + } + } else { + content = await provisionsTexture.trim({ threshold: 100 }).toBuffer(); + } await uploader.add({ path: `icons/provisions/${provision.id}.webp`, diff --git a/packages/scripts/src/core/blitz/parsePackedSpriteRect.ts b/packages/scripts/src/core/blitz/parsePackedSpriteRect.ts new file mode 100644 index 000000000..e47f0e8e9 --- /dev/null +++ b/packages/scripts/src/core/blitz/parsePackedSpriteRect.ts @@ -0,0 +1,53 @@ +const textDecoder = new TextDecoder(); + +function readFourCC(buffer: Uint8Array, offset: number) { + return textDecoder.decode(buffer.subarray(offset, offset + 4)); +} + +export function parsePackedSpriteRect(buffer: Uint8Array): number[] | null { + if ( + buffer.length < 12 || + readFourCC(buffer, 0) !== "RIFF" || + readFourCC(buffer, 8) !== "WEBP" + ) { + return null; + } + + const view = new DataView( + buffer.buffer, + buffer.byteOffset, + buffer.byteLength, + ); + let offset = 12; + + while (offset + 8 <= buffer.length) { + const chunkId = readFourCC(buffer, offset); + const chunkSize = view.getUint32(offset + 4, true); + const dataStart = offset + 8; + const dataEnd = dataStart + chunkSize; + + if (dataEnd > buffer.length) return null; + + if (chunkId === "extr") { + const lines = textDecoder + .decode(buffer.subarray(dataStart, dataEnd)) + .replace(/\0/g, "") + .split(/\r?\n/) + .map((line) => line.trim().split(/\s+/).map(Number)); + + for (const values of lines) { + if (values.length < 4) continue; + if (!values.slice(0, 4).every(Number.isFinite)) continue; + if (values[2] <= 0 || values[3] <= 0) continue; + + return values; + } + + return null; + } + + offset = dataEnd + (chunkSize % 2); + } + + return null; +} From 504f143ff7269ec31f6235f8677b8642f1b9aa55 Mon Sep 17 00:00:00 2001 From: GonnaHetzMe Date: Tue, 7 Apr 2026 23:15:26 +0200 Subject: [PATCH 2/2] fix: resolve requested changes --- .../buildAssets/consumableProvisionIcons.ts | 88 ++++++++++--------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/packages/scripts/src/buildAssets/consumableProvisionIcons.ts b/packages/scripts/src/buildAssets/consumableProvisionIcons.ts index d933ae28e..a9031bb24 100644 --- a/packages/scripts/src/buildAssets/consumableProvisionIcons.ts +++ b/packages/scripts/src/buildAssets/consumableProvisionIcons.ts @@ -14,6 +14,42 @@ interface Mappings { const listItemsPattern = //g; +async function extractPackedIcon( + texture: sharp.Sharp, + sizes: number[], + context: string, +) { + const [left, top, width, height] = sizes; + const bounds = [left, top, width, height]; + + if (!bounds.every(Number.isInteger)) { + throw new Error( + `Invalid RIFF sprite bounds for ${context}: ${bounds.join(" ")}`, + ); + } + + const metadata = await texture.metadata(); + + if (metadata.width === undefined || metadata.height === undefined) { + throw new Error(`Failed to read image dimensions for ${context}`); + } + + if ( + left < 0 || + top < 0 || + width <= 0 || + height <= 0 || + left + width > metadata.width || + top + height > metadata.height + ) { + throw new Error( + `Out-of-bounds RIFF sprite for ${context}: ${bounds.join(" ")} in ${metadata.width}x${metadata.height}`, + ); + } + + return await texture.clone().extract({ left, top, width, height }).toBuffer(); +} + export async function consumableProvisionIcons() { console.log("Building consumable and provision icons..."); @@ -64,28 +100,13 @@ export async function consumableProvisionIcons() { let content: Buffer; if (sizes) { - try { - content = await consumablesTexture - .clone() - .extract({ - left: sizes[0], - top: sizes[1], - width: sizes[2], - height: sizes[3], - }) - .toBuffer(); - } catch { - console.warn( - `Failed to extract consumable ${consumable.icon} from RIFF data; falling back to trim...`, - ); - content = await consumablesTexture - .trim({ threshold: 100 }) - .toBuffer(); - } + content = await extractPackedIcon( + consumablesTexture, + sizes, + `consumable ${consumable.icon}`, + ); } else { - content = await consumablesTexture - .trim({ threshold: 100 }) - .toBuffer(); + content = await consumablesTexture.trim({ threshold: 80 }).toBuffer(); } await uploader.add({ @@ -156,26 +177,13 @@ export async function consumableProvisionIcons() { let content: Buffer; if (sizes) { - try { - content = await provisionsTexture - .clone() - .extract({ - left: sizes[0], - top: sizes[1], - width: sizes[2], - height: sizes[3], - }) - .toBuffer(); - } catch { - console.warn( - `Failed to extract provision ${provision.icon} from RIFF data; falling back to trim...`, - ); - content = await provisionsTexture - .trim({ threshold: 100 }) - .toBuffer(); - } + content = await extractPackedIcon( + provisionsTexture, + sizes, + `provision ${provision.icon}`, + ); } else { - content = await provisionsTexture.trim({ threshold: 100 }).toBuffer(); + content = await provisionsTexture.trim({ threshold: 80 }).toBuffer(); } await uploader.add({