diablo2-web/scripts/scratch-audit-dt1-inventory.ts

84 lines
3.0 KiB
TypeScript

/**
* AUDIT TOOL (read-only, scratch): enumerate the REAL DT1 tile inventory.
*
* For every LevelType listed in /tmp/dt1_list.txt, open the DT1 straight out of
* the MPQs and dump every tile record's (orientation, mainIndex, subIndex,
* rarityFrameIndex). This is the authoritative ground truth for which
* (style, sequence) pairs legally exist -- AutoMap.txt is only a proxy because
* it lists tiles that have an automap glyph, not the full tile set.
*
* DT1 tile record is 96 bytes (Paul Siramy layout, corroborated by the port's own
* src/formats/dt1.ts which reads subTileFlags at +40 and block pointers at +72/76/80):
* +20 int32 orientation (0 == floor)
* +24 int32 mainIndex (== "style")
* +28 int32 subIndex (== "sequence")
* +32 int32 rarityFrameIndex
*
* Output: TSV to stdout -> levelType, dt1File, orientation, mainIndex, subIndex, rarity
*/
import { readFileSync, writeFileSync } from 'node:fs'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
const D2 = '/usr/local/google/home/taodao/d2-data'
// Search order matters: Patch_D2 overrides base archives.
const archives = []
for (const name of ['Patch_D2.mpq', 'd2exp.mpq', 'd2data.mpq']) {
archives.push({ name, ar: await MpqArchive.open(await fileSource(`${D2}/${name}`)) })
}
const i32 = (b: Uint8Array, o: number): number =>
new DataView(b.buffer, b.byteOffset, b.byteLength).getInt32(o, true)
const lines = readFileSync('/tmp/dt1_list.txt', 'utf8').trim().split('\n')
const out: string[] = ['levelType\tdt1\torientation\tmainIndex\tsubIndex\trarity']
const missing: string[] = []
let totalTiles = 0
for (const line of lines) {
const [ltRaw, rel] = line.split('\t')
if (ltRaw === undefined || rel === undefined) continue
const internal = `data\\global\\tiles\\${rel.replace(/\//g, '\\')}`
let data: Uint8Array | undefined
let from = ''
for (const { name, ar } of archives) {
const f = ar.find(internal)
if (f !== undefined) {
data = await ar.read(f)
from = name
break
}
}
if (data === undefined) {
missing.push(`${ltRaw}\t${rel}`)
continue
}
const major = i32(data, 0)
const minor = i32(data, 4)
if (major !== 7 || minor !== 6) {
missing.push(`${ltRaw}\t${rel}\tBAD VERSION ${major}.${minor}`)
continue
}
const tileCount = i32(data, 268)
const tileStart = i32(data, 272)
for (let k = 0; k < tileCount; k += 1) {
const at = tileStart + k * 96
if (at + 96 > data.byteLength) break
out.push(
`${ltRaw}\t${rel}\t${i32(data, at + 20)}\t${i32(data, at + 24)}\t` +
`${i32(data, at + 28)}\t${i32(data, at + 32)}`,
)
totalTiles += 1
}
console.error(` [${from.padEnd(12)}] lt=${ltRaw.padStart(2)} ${rel} -> ${tileCount} tiles`)
}
writeFileSync('/tmp/dt1_tiles.tsv', out.join('\n') + '\n')
console.error(`\nwrote /tmp/dt1_tiles.tsv : ${totalTiles} tile records`)
if (missing.length > 0) {
console.error(`\nMISSING / UNREADABLE (${missing.length}):`)
for (const m of missing) console.error(` ${m}`)
}