consumable icons

This commit is contained in:
tresabhi committed 2026-08-02 20:22:32 -05:00
1 parent 59108d7637
commit 90ad923672
4 files changed
+143 -36

No files matched your search

@@ -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();
}
@@ -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;
}
@@ -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 = /<items path="(.+)\.xml"\/>/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<Mappings>(`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);
}