diablo2-web/scripts/verify-object-lookup.ts

181 lines
8.1 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-object-lookup.ts — 验收"DS1 对象 id → 实际 token/mode"这层查找
//
// 这一层是移植来的(OpenDiablo2 的 object_lookup_record_data.go),所以它必须与**另一个独立
// 来源**对得上才能算数:拿表里的 `objectsTxtId` 去 `Objects.txt` 查 Token,两者应当一致;
// 不一致的地方要**逐条列出来**并写清为什么(实测正好 26 条,全是 `Objects.txt` 写了占位符
// `SS`/`XX`/`SL`/`QO` 而表里是真 token 的情况)。
//
// node scripts/verify-object-lookup.ts [--dir=samples/d2]
//
// 退出码非 0 表示有断言没过。
export {}
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { MountedArchives } from '../src/mpq/mount.ts'
import { loadObjectsTable } from '../src/game/objects.ts'
import type { ObjectsTable } from '../src/game/objects.ts'
import { OBJECT_LOOKUP_WITH_ART, OBJECT_LOOKUP_WITH_ROW, OBJECT_LOOKUP_WITHOUT_ART } from '../src/game/object-lookup-data.ts'
import { lookupObject, objectLookupStats } from '../src/game/object-lookup.ts'
/** 归档目录。 */
const DIR = process.argv.find(argument => argument.startsWith('--dir='))?.slice('--dir='.length) ?? 'samples/d2'
/**
* `Objects.txt` 的 Token 列与查找表不一致、且**以查找表为准**的记录。
*
* 这些行的 `Objects.txt` Token 是占位符(`SS`/`XX`/`SL`/`QO`/`5F`/`6T`/…),而表里是引擎真正
* 使用的 token(例如 act 2 的 jerhyn 是 `JE`)。上一轮"有美术但 Objects.txt 里没有对应行"的
* 那串 token(`5I 5J 5M 5N 5O 9C …`)正是这批。表是这三条来源里唯一的真值,所以这里是白名单
* 而不是失败项;但**必须逐条固定**,任何新增都会让断言失败,逼人重新核对。
*/
const TOKEN_OVERRIDES: readonly string[] = [
'1/110/385:DC',
'1/113/0:29',
'2/16/121:JE',
'2/17/122:JE',
'2/102/133:AZ',
'3/13/194:9C',
'3/93/361:XO',
'3/109/378:HR',
'3/110/379:HR',
'4/19/363:XQ',
'4/46/255:DI',
'4/64/408:98',
'4/65/409:99',
'5/31/419:YO',
'5/33/425:YU',
'5/53/459:XS',
'5/54/460:2N',
'5/55/461:0J',
'5/56/462:0J',
'5/92/509:5M',
'5/103/511:5O',
'5/107/504:5I',
'5/108/505:5J',
'5/111/510:5N',
'5/125/542:XR',
'5/126/543:XR',
]
/** 断言结果。 */
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}`)
}
/**
* 主流程。
*/
async function main(): Promise<void> {
const stats = objectLookupStats()
console.log(`lookup: acts ${stats.acts.join(',')},记录 ${String(stats.rows)},有 token ${String(stats.withArt)},无 token ${String(stats.withoutArt)},有 Objects.txt 行 ${String(stats.withRow)}\n`)
check('五个 act 都有查找表', stats.acts.join(',') === '1,2,3,4,5', stats.acts.join(','))
check('记录数与生成物一致', stats.rows === OBJECT_LOOKUP_WITH_ART + OBJECT_LOOKUP_WITHOUT_ART,
`${String(stats.rows)} = ${String(OBJECT_LOOKUP_WITH_ART)} + ${String(OBJECT_LOOKUP_WITHOUT_ART)}`)
check('有 token 的记录数稳定', stats.withArt === OBJECT_LOOKUP_WITH_ART && stats.withRow === OBJECT_LOOKUP_WITH_ROW,
`withArt=${String(stats.withArt)} withRow=${String(stats.withRow)}`)
check('记录数 = 3,614(3,615 条物体行里有 1 条重复 id 被覆盖)', stats.rows === 3614, `${String(stats.rows)} 条`)
check('无 token 的记录数 = 193(不可见/占位对象)', stats.withoutArt === 193, `${String(stats.withoutArt)} 条`)
// 具体点位:这些名字是从社区表里读出来的,写成断言是为了让"表换了版本"立刻暴露。
const spots: readonly [number, number, string, string, string][] = [
[1, 0, 'FN', 'NU', 'rogue fountain'],
[1, 1, 'TO', 'ON', 'torch 1 tiki'],
[1, 2, 'RB', 'ON', 'Fire, rogue camp'],
[1, 5, 'L1', 'NU', 'Chest, R Large'],
[2, 17, 'JE', 'NU', 'jerhyn'],
[5, 0, 'AO', 'NU', 'act 5 first object'],
]
for (const [act, id, token, mode, label] of spots) {
const entry = lookupObject(act, 2, id)
check(`act ${String(act)} id ${String(id)} → ${token}/${mode}(${label})`,
entry !== null && entry.token === token && entry.mode === mode,
entry === null ? '查不到' : `${entry.token}/${entry.mode}`)
}
check('怪物类型不命中本表', lookupObject(1, 1, 0) === null, 'type 1 走 MonPreset,不是本表')
check('表里没有的 act 返回 null', lookupObject(9, 2, 0) === null, 'act 9 不存在')
// 与 Objects.txt 交叉核对。
const archives = new MountedArchives()
for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
archives.add(name, await MpqArchive.open(await fileSource(`${DIR}/${name}`)))
}
const tables: ObjectsTable = await loadObjectsTable(archives)
let same = 0
let diffToken = 0
let emptyToken = 0
let missingRow = 0
const unexpected: string[] = []
const missingOverrides: string[] = []
const seenOverrides = new Set<string>()
const missingRowTokens: string[] = []
for (const act of stats.acts) {
for (const entry of lookupObjectRows(act)) {
// 表说"没有 token"的记录(193 条)是不可见/占位对象:画不出来是对的。
if (entry.token === '') { emptyToken += 1; continue }
// 表给了 token 却没给行号:靠 token 反查元数据,下面单独断言。
if (entry.objectsTxtId < 0) continue
const row = tables.byId.get(entry.objectsTxtId)
if (row === undefined) {
// 社区表是对着另一版 Objects.txt 做的:有 3 条行号在 1.13c 里不存在。
missingRow += 1
missingRowTokens.push(entry.token)
continue
}
const tableToken = row.token.trim().toUpperCase()
const lookupToken = entry.token.toUpperCase()
if (lookupToken === tableToken) { same += 1; continue }
diffToken += 1
const key = `${String(act)}/${String(entry.ds1Id)}/${String(entry.objectsTxtId)}:${lookupToken}`
seenOverrides.add(key)
if (!TOKEN_OVERRIDES.includes(key)) {
unexpected.push(`${key}(Objects.txt 写的是 "${tableToken}",name "${row.name}")`)
}
}
}
for (const key of TOKEN_OVERRIDES) if (!seenOverrides.has(key)) missingOverrides.push(key)
console.log(`\n 与 Objects.txt 交叉核对:一致 ${String(same)},表覆盖占位符 ${String(diffToken)},表说无 token ${String(emptyToken)},行号在 1.13c 里不存在 ${String(missingRow)}`)
check('不一致的记录恰好是那批占位符 token', unexpected.length === 0,
unexpected.length === 0 ? `${String(TOKEN_OVERRIDES.length)} 条全部对上` : unexpected.slice(0, 5).join(' | '))
check('覆盖条数 = 26', diffToken === TOKEN_OVERRIDES.length, `${String(diffToken)} 条`)
check('白名单没有过期条目', missingOverrides.length === 0,
missingOverrides.length === 0 ? '全部仍然有效' : missingOverrides.slice(0, 5).join(' | '))
check('与 Objects.txt 完全一致的记录数 = 527', same === 527, `${String(same)} 条`)
check('表说无 token 的记录数 = 193', emptyToken === 193, `${String(emptyToken)} 条`)
check('行号缺失的 3 条仍然带真 token', missingRow === 3 && missingRowTokens.sort().join(',') === '7C,PX,PY',
`缺失行号的 token:${missingRowTokens.join(',')}`)
const passed = checks.filter(entry => entry.ok).length
console.log(`\n${String(passed)}/${String(checks.length)} passed`)
if (passed !== checks.length) process.exitCode = 1
}
/**
* 枚举一个 act 的全部记录(`lookupObject` 是单点查询,这里要遍历)。
*
* @param act - act 号.
* @returns the entries of that act.
*/
function lookupObjectRows(act: number): { ds1Id: number; objectsTxtId: number; token: string }[] {
const out: { ds1Id: number; objectsTxtId: number; token: string }[] = []
for (let id = 0; id < 4000; id += 1) {
const entry = lookupObject(act, 2, id)
if (entry !== null) out.push(entry)
}
return out
}
await main()