From 90ad923672a2ebb4e6080d7167575e32ce27f08a Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:22:32 -0500 Subject: [PATCH] consumable icons --- .../buildAssets/consumableProvisionIcons.ts | 36 ------ .../src/core/blitz/extractPackedIcon.ts | 31 +++++ .../src/core/blitz/parsePackedSpriteRect.ts | 0 .../icons/consumables/[id].webp.ts | 112 ++++++++++++++++++ 4 files changed, 143 insertions(+), 36 deletions(-) create mode 100644 packages/website/src/core/blitz/extractPackedIcon.ts rename packages/{scripts => website}/src/core/blitz/parsePackedSpriteRect.ts (100%) create mode 100644 packages/website/src/pages/[...chunk_api]/icons/consumables/[id].webp.ts diff --git a/packages/scripts/src/buildAssets/consumableProvisionIcons.ts b/packages/scripts/src/buildAssets/consumableProvisionIcons.ts index b30fabdc9..a934a5ad8 100644 --- a/packages/scripts/src/buildAssets/consumableProvisionIcons.ts +++ b/packages/scripts/src/buildAssets/consumableProvisionIcons.ts @@ -14,42 +14,6 @@ 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..."); diff --git a/packages/website/src/core/blitz/extractPackedIcon.ts b/packages/website/src/core/blitz/extractPackedIcon.ts new file mode 100644 index 000000000..b2b1f2ca0 --- /dev/null +++ b/packages/website/src/core/blitz/extractPackedIcon.ts @@ -0,0 +1,31 @@ +import type sharp from "sharp"; + +export async function extractPackedIcon(texture: sharp.Sharp, sizes: number[]) { + const [left, top, width, height] = sizes; + const bounds = [left, top, width, height]; + + if (!bounds.every(Number.isInteger)) { + throw new Error(`Invalid RIFF sprite bounds: ${bounds.join(" ")}`); + } + + const metadata = await texture.metadata(); + + if (metadata.width === undefined || metadata.height === undefined) { + throw new Error(`Failed to read image dimensions`); + } + + 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: ${bounds.join(" ")} in ${metadata.width}x${metadata.height}`, + ); + } + + return await texture.clone().extract({ left, top, width, height }).toBuffer(); +} diff --git a/packages/scripts/src/core/blitz/parsePackedSpriteRect.ts b/packages/website/src/core/blitz/parsePackedSpriteRect.ts similarity index 100% rename from packages/scripts/src/core/blitz/parsePackedSpriteRect.ts rename to packages/website/src/core/blitz/parsePackedSpriteRect.ts diff --git a/packages/website/src/pages/[...chunk_api]/icons/consumables/[id].webp.ts b/packages/website/src/pages/[...chunk_api]/icons/consumables/[id].webp.ts new file mode 100644 index 000000000..8b02390b8 --- /dev/null +++ b/packages/website/src/pages/[...chunk_api]/icons/consumables/[id].webp.ts @@ -0,0 +1,112 @@ +import type { ConsumablesCommon } from "@blitzkit/core"; +import type { APIContext, GetStaticPathsItem } from "astro"; +import sharp from "sharp"; +import { extractPackedIcon } from "../../../../core/blitz/extractPackedIcon"; +import { parsePackedSpriteRect } from "../../../../core/blitz/parsePackedSpriteRect"; +import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths"; +import { vfs } from "../../../../core/blitzkit/vfs"; +import { getStaticPaths as _getStaticPaths } from "../../_index"; + +interface Mappings { + Header: { version: number }; + StyleSheets: { + selectors: string[]; + properties: { "bg-sprite": string }; + }[]; +} + +const listItemsPattern = //g; + +export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => { + const paths: GetStaticPathsItem[] = []; + + const styles = [ + "UI/Styles/Lobby/Inventory/InventoryNormalStyles.yaml", + "UI/Styles/Lobby/Inventory/Event/InventoryBigStyles.yaml", + "UI/Styles/Lobby/Inventory/Event/InventoryNormalStyles.yaml", + "UI/Screens3/Lobby/Inventory/Equipment/EquipmentItemImage.style.yaml", + "UI/Screens/Battle/Styles/BattleEquipmentStyles.yaml", + ]; + const styleSheets = await Promise.all( + styles.map(async (path) => + Object.values((await vfs.yaml(`Data/${path}`)).StyleSheets), + ), + ).then((array) => array.flat()); + + for (const match of ( + await vfs.text(`Data/XML/item_defs/vehicles/common/consumables/list.xml`) + ).matchAll(listItemsPattern)) { + const consumablesCommon = await vfs.xml<{ + root: ConsumablesCommon; + }>(`Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`); + + for (const consumable of Object.values(consumablesCommon.root)) { + const styleSheet = styleSheets.find((styleSheet) => + styleSheet.selectors.some((selector) => + selector.includes(`${consumable.icon} `), + ), + ); + + if (!styleSheet) { + console.warn( + `No style sheet found for consumable ${consumable.icon}; skipping...`, + ); + continue; + } + + const path = styleSheet.properties["bg-sprite"] + .replace("~res:/", "") + .replace(".psd", "") + .replace(".txt", ""); + + paths.push({ + props: { path }, + params: { id: consumable.id }, + }); + } + } + + return paths; +}); + +export async function GET({ props }: APIContext<{ path: string }>) { + let buffer: Buffer; + + if (await vfs.resolve(`Data/${props.path}.packed.webp`)) { + const packedBuffer = await vfs.file(`Data/${props.path}.packed.webp`); + const consumablesTexture = sharp(packedBuffer); + const sizes = parsePackedSpriteRect(packedBuffer); + + if (sizes) { + buffer = await extractPackedIcon(consumablesTexture, sizes); + } else { + buffer = await consumablesTexture.trim({ threshold: 80 }).toBuffer(); + } + } else { + const consumablesTexture = sharp( + await vfs.file( + `${`Data/${props.path}` + .split("/") + .slice(0, -1) + .join("/")}/texture0.packed.webp`, + ), + ); + const sizes = (await vfs.text(`Data/${props.path}.txt`)) + .split("\n")[4] + .split(" ") + .map(Number); + buffer = await consumablesTexture + .clone() + .extract({ + left: sizes[0], + top: sizes[1], + width: sizes[2], + height: sizes[3], + }) + .toBuffer(); + } + + const bytes = new Uint8Array(buffer); + + return new Response(bytes); +}