63 lines
2.7 KiB
TypeScript
63 lines
2.7 KiB
TypeScript
/**
|
|
* Decode a sprite whose frames each have their own width.
|
|
*
|
|
* Diablo I's tile and cursor sheets are the case single-width auto-detection
|
|
* cannot serve: the inventory cursor sheet is paired with a sidecar list of
|
|
* per-frame widths, and dungeon tile sheets take their widths from the tile
|
|
* definitions. This drives the decoder with such a list against a real member,
|
|
* which is the only way to show the per-frame path actually matches shipped
|
|
* data.
|
|
*
|
|
* Usage: node scripts/verify-widths.ts <archive> <member> <widths-file>
|
|
*/
|
|
import { readFile } from 'node:fs/promises'
|
|
import { MpqArchive } from '../src/mpq/archive.ts'
|
|
import { fileSource } from '../src/mpq/file-source.ts'
|
|
import { decodeSpriteFile } from '../src/formats/cel.ts'
|
|
|
|
const [path, member, widthsPath] = process.argv.slice(2)
|
|
if (path === undefined || member === undefined || widthsPath === undefined) {
|
|
console.error('usage: node scripts/verify-widths.ts <archive> <member> <widths-file>')
|
|
process.exit(2)
|
|
}
|
|
|
|
const widths = (await readFile(widthsPath, 'utf8'))
|
|
.split(/\s+/)
|
|
.filter(part => part !== '')
|
|
.map(part => Number.parseInt(part, 10))
|
|
if (widths.some(width => !Number.isFinite(width) || width <= 0)) {
|
|
console.error('widths file has a non-positive or unparsable entry')
|
|
process.exit(1)
|
|
}
|
|
|
|
const archive = await MpqArchive.open(await fileSource(path))
|
|
const mode = member.toLowerCase().endsWith('.cl2') ? 'cl2' : 'cel'
|
|
const file = archive.find(member)
|
|
if (file === undefined) {
|
|
console.error(`no such member: ${member}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
const data = await archive.read(file)
|
|
const sheet = decodeSpriteFile(data, mode, { widths })
|
|
const frames = sheet.groups.flatMap(group => group.frames)
|
|
const heights = frames.map(frame => frame.height)
|
|
const coverage = frames.map(frame => {
|
|
let opaque = 0
|
|
for (const value of frame.mask) if (value !== 0) opaque += 1
|
|
return opaque
|
|
})
|
|
const empty = coverage.filter(count => count === 0).length
|
|
|
|
console.log(`archive ${path}`)
|
|
console.log(`member ${member} (${String(data.byteLength)} bytes, ${mode})`)
|
|
console.log(`widths ${String(widths.length)} entries from ${widthsPath}`)
|
|
console.log(`groups ${String(sheet.groups.length)}`)
|
|
console.log(`frames ${String(frames.length)} decoded`)
|
|
console.log(`heights ${String(Math.min(...heights))}..${String(Math.max(...heights))}`)
|
|
console.log(`pixels ${String(Math.min(...coverage))}..${String(Math.max(...coverage))} opaque per frame`)
|
|
console.log(`empty ${String(empty)} frames with no opaque pixels`)
|
|
console.log(empty === 0 && frames.length === widths.length
|
|
? 'RESULT every frame decoded and matched its width entry exactly'
|
|
: 'RESULT mismatch — check the width list against the frame count')
|