diablo2-web/scripts/verify-listfile.ts

91 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// verify-listfile.ts — 用社区 listfile 把"归档里到底有没有这个成员"变成可判定的事实
//
// 背景:`d2data.mpq`/`Patch_D2.mpq` 都没有 `(listfile)`,Storm 的名字是**加密存的**,所以
// 原先 `listFiles()` 只能对能列目录的归档给出名字。1.13c 的社区 listfile
// (`samples/d2/listfile_113c.txt`,39551 行)补齐了这一点:拿它的每个名字去归档里做
// hash 查找,查到就是真成员。`MpqArchive.open` 的 `listfile` 选项就是为此存在的。
//
// node scripts/verify-listfile.ts [--dir=samples/d2]
//
// 退出码非 0 表示有断言没过。
export {}
import { readFileSync } from 'node:fs'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
/** 归档目录,`--dir=` 可覆盖。 */
const DIR = process.argv.find(argument => argument.startsWith('--dir='))?.slice('--dir='.length) ?? 'samples/d2'
/** listfile 文件(社区提供,1.13c)。 */
const LISTFILE = `${DIR}/listfile_113c.txt`
/** 断言结果。 */
const checks: { name: string; ok: boolean; detail: string }[] = []
/**
* 记录一条断言。
*
* @param name - what was checked.
* @param ok - whether it held.
* @param detail - evidence.
*/
function check(name: string, ok: boolean, detail: string): void {
checks.push({ name, ok, detail })
console.log(`${ok ? 'ok ' : 'FAIL'} ${name} — ${detail}`)
}
/**
* 主流程:读 listfile,逐个归档做名字解析,最后核对物体美术的结论。
*/
async function main(): Promise<void> {
const names = readFileSync(LISTFILE, 'utf8').split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0)
const lower = new Set(names.map(name => name.toLowerCase()))
check('listfile 非空', names.length > 30000, `${String(names.length)} 行`)
check('listfile 去重后无重复', lower.size === names.length, `${String(lower.size)} 个不同名字`)
check('listfile 带 (listfile) 自身', lower.has('(listfile)'), '社区 listfile 保留了归档管理成员')
/** 每个归档期望的最低命中数,来自实测;低于它说明 listfile 或归档换了版本。 */
const expectations: Record<string, { min: number; objects: number }> = {
'Patch_D2.mpq': { min: 200, objects: 0 },
'd2data.mpq': { min: 10700, objects: 2400 },
'd2exp.mpq': { min: 9800, objects: 800 },
}
const resolvedByArchive = new Map<string, string[]>()
for (const [file, want] of Object.entries(expectations)) {
const archive = await MpqArchive.open(await fileSource(`${DIR}/${file}`), { listfile: names })
const found = names.filter(name => archive.find(name) !== undefined)
resolvedByArchive.set(file, found)
const objects = found.filter(name => /objects[\\/]/i.test(name))
console.log(`\n=== ${file} ===`)
check(`${file} 名字命中数 ≥ ${String(want.min)}`, found.length >= want.min,
`${String(found.length)} / ${String(names.length)}`)
check(`${file} objects 成员数 ≈ ${String(want.objects)}`, Math.abs(objects.length - want.objects) <= 20,
`${String(objects.length)} 个 objects 成员`)
}
// 1.13c 的关卡表就在 Patch_D2 里:解析器读的就是这些名字,名字错一个字节就全盘皆错。
const patch = await MpqArchive.open(await fileSource(`${DIR}/Patch_D2.mpq`), { listfile: names })
for (const table of ['Levels.txt', 'LvlMaze.txt', 'LvlPrest.txt', 'LvlSub.txt', 'LvlTypes.txt']) {
const member = `data\\global\\excel\\${table}`
const file = patch.find(member)
check(`Patch_D2 有 ${table}`, file !== undefined, file === undefined ? '未找到' : `${String(file.fileSize)} 字节`)
}
console.log(`\n Patch_D2 里 objects 目录:${String((resolvedByArchive.get('Patch_D2.mpq') ?? []).filter(name => /objects[\\/]/i.test(name)).length)}`
+ '(补丁归档不含可破坏物品美术,这是实测结论而不是猜测)')
// 物体美术的关键结论:哪些 token 在全部归档里根本没有美术。
const all = [...resolvedByArchive.values()].flat()
for (const [token, expected] of [['9b', 4], ['tp', 12], ['ta', 0]] as const) {
const hits = all.filter(name => new RegExp(`objects[\\\\/]${token}[\\\\/]`, 'i').test(name))
check(`token ${token.toUpperCase()} 的成员数 = ${String(expected)}`, hits.length === expected, `${String(hits.length)} 个`)
}
const passed = checks.filter(entry => entry.ok).length
console.log(`\n${String(passed)}/${String(checks.length)} passed`)
if (passed !== checks.length) process.exitCode = 1
}
await main()