From e9ccf22f54930678d0d2f62871f4b74c469fbc3e Mon Sep 17 00:00:00 2001 From: Brian Beck Date: Sun, 16 Nov 2025 15:59:30 -0800 Subject: [PATCH] add ifl helpers --- scripts/inspect-ifl.ts | 61 ++++++++++++++++++++++++++++++++++++++++++ src/ifl.ts | 15 +++++++++++ src/loaders.ts | 12 +++++++++ 3 files changed, 88 insertions(+) create mode 100644 scripts/inspect-ifl.ts create mode 100644 src/ifl.ts diff --git a/scripts/inspect-ifl.ts b/scripts/inspect-ifl.ts new file mode 100644 index 00000000..ba242474 --- /dev/null +++ b/scripts/inspect-ifl.ts @@ -0,0 +1,61 @@ +import fs from "node:fs"; +import { inspect, parseArgs } from "node:util"; +import { parseImageFrameList } from "@/src/ifl"; +import { getFilePath } from "@/src/manifest"; + +async function run() { + const { values, positionals } = parseArgs({ + allowPositionals: true, + options: { + name: { + type: "string", + short: "n", + }, + list: { + type: "boolean", + short: "l", + }, + }, + }); + + if (values.list) { + if (values.name || positionals[0]) { + console.error("Cannot specify --list (-l) with other options."); + return 1; + } + const manifest = (await import("../public/manifest.json")).default; + const fileNames = Object.keys(manifest); + console.log( + fileNames + .map((f) => f.match(/^textures\/skins\/(.+)\.ifl$/)) + .filter(Boolean) + .map((match) => match[1]) + .join("\n") + ); + return; + } else if ( + (values.name && positionals[0]) || + (!values.name && !positionals[0]) + ) { + console.error( + "Must specify exactly one of --name (-n) or a positional filename." + ); + return 1; + } + + let framesFile = positionals[0]; + if (values.name) { + const resourcePath = `textures/skins/${values.name}.ifl`; + framesFile = getFilePath(resourcePath); + } + const missionScript = fs.readFileSync(framesFile, "utf8"); + console.log( + inspect(parseImageFrameList(missionScript), { + colors: true, + depth: Infinity, + }) + ); +} + +const code = await run(); +process.exit(code ?? 0); diff --git a/src/ifl.ts b/src/ifl.ts new file mode 100644 index 00000000..fcc5a5d0 --- /dev/null +++ b/src/ifl.ts @@ -0,0 +1,15 @@ +export function parseImageFrameList(source: string) { + const lines = source + .split(/(?:\r\n|\r|\n)/g) + .map((line) => line.trim()) + .filter(Boolean); + return lines.map((line) => { + const fileWithCount = line.match(/^(.+)\s(\d+)$/); + if (fileWithCount) { + const frameCount = parseInt(fileWithCount[2], 10); + return { name: fileWithCount[1], frameCount }; + } else { + return { name: line, frameCount: 1 }; + } + }); +} diff --git a/src/loaders.ts b/src/loaders.ts index c9b25ef3..c594430a 100644 --- a/src/loaders.ts +++ b/src/loaders.ts @@ -1,3 +1,4 @@ +import { parseImageFrameList } from "./ifl"; import { getActualResourcePath, getSource } from "./manifest"; import { parseMissionScript } from "./mission"; import { parseTerrainBuffer } from "./terrain"; @@ -45,6 +46,10 @@ export function interiorTextureToUrl(name: string, fallbackUrl?: string) { return getUrlForPath(`textures/${name}.png`, fallbackUrl); } +export function textureFrameToUrl(fileName: string) { + return getUrlForPath(`textures/skins/${fileName}`); +} + export function shapeTextureToUrl(name: string, fallbackUrl?: string) { name = name.replace(/\.\d+$/, ""); return getUrlForPath(`textures/skins/${name}.png`, fallbackUrl); @@ -82,3 +87,10 @@ export async function loadTerrain(fileName: string) { const terrainBuffer = await res.arrayBuffer(); return parseTerrainBuffer(terrainBuffer); } + +export async function loadImageFrameList(iflPath: string) { + const url = getUrlForPath(iflPath); + const res = await fetch(url); + const source = await res.text(); + return parseImageFrameList(source); +}