diablo2-web/scripts/pack-canonical-tbl.ts

230 lines
7.1 KiB
TypeScript

/**
* scripts/pack-canonical-tbl.ts
*
* Extracts canonical Diablo II v1.13c `.tbl` string table entries (ENG & CHI)
* and `ItemStatCost.txt` tooltip formatting metadata (`descfunc`, `descval`,
* `descstrpos`, `descstrneg`, `descstr2`, `dgrp*`) from `samples/d2/` MPQ archives
* (`d2data.mpq`, `d2exp.mpq`, `Patch_D2.mpq`) into `src/data/canonical-tbl-strings.ts`.
*
* Ground Truth References:
* - D2MOO `source/D2Common/src/DataTbls/ItemsTbls.cpp` (`0x6FD55280` .. `0x6FD5E020`)
* - D2Client.dll `sub_6FB23F80` (`descfunc` 1..28 & `dgrp` 1..2)
*/
import * as fs from 'node:fs'
import * as path from 'node:path'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { MountedArchives } from '../src/mpq/mount.ts'
import { loadStringTableChain } from '../src/game/item-names.ts'
import { parseTable } from '../src/game/tables.ts'
async function main() {
const archives = new MountedArchives()
for (const m of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
archives.add(m, await MpqArchive.open(await fileSource(`samples/d2/${m}`)))
}
const eng = await loadStringTableChain(archives, 'ENG')
const chi = await loadStringTableChain(archives, 'CHI')
const readTxt = async (name: string) =>
parseTable(new TextDecoder().decode(await archives.read(`data/global/excel/${name}`)))
const keys = new Set<string>()
const addKey = (k?: string) => {
const t = k?.trim()
if (t && t !== 'Expansion' && !t.startsWith('*')) {
keys.add(t)
}
}
// 1. Base items (weapons.txt, armor.txt, misc.txt)
for (const fn of ['weapons.txt', 'armor.txt', 'misc.txt']) {
const t = await readTxt(fn)
for (const r of t.rows) {
addKey(r['namestr'])
addKey(r['code'])
}
}
// 2. Uniques, SetItems, Sets, Runes, Gems, LowQualityItems
for (const fn of ['UniqueItems.txt', 'SetItems.txt']) {
const t = await readTxt(fn)
for (const r of t.rows) addKey(r['index'])
}
for (const r of (await readTxt('Sets.txt')).rows) {
addKey(r['name'])
addKey(r['index'])
}
for (const r of (await readTxt('Runes.txt')).rows) {
addKey(r['Name'])
}
for (const r of (await readTxt('Gems.txt')).rows) {
addKey(r['name'])
addKey(r['code'])
}
for (const r of (await readTxt('LowQualityItems.txt')).rows) {
addKey(r['Name'])
}
// 3. MagicPrefix, MagicSuffix, AutoMagic, RarePrefix, RareSuffix
for (const fn of ['MagicPrefix.txt', 'MagicSuffix.txt', 'AutoMagic.txt']) {
const t = await readTxt(fn)
for (const r of t.rows) addKey(r['Name'])
}
for (const fn of ['RarePrefix.txt', 'RareSuffix.txt']) {
const t = await readTxt(fn)
for (const r of t.rows) addKey(r['name'])
}
// 4. ItemStatCost.txt description & display group keys
const isc = await readTxt('ItemStatCost.txt')
interface IscDescMeta {
id: number
stat: string
descPriority: number
descFunc: number
descVal: number
descStrPos: string
descStrNeg: string
descStr2: string
dgrp: number
dgrpFunc: number
dgrpVal: number
dgrpStrPos: string
dgrpStrNeg: string
dgrpStr2: string
}
const iscMeta: Record<string, IscDescMeta> = {}
for (const r of isc.rows) {
const stat = r['Stat']?.trim()
if (!stat) continue
for (const col of ['descstrpos', 'descstrneg', 'descstr2', 'dgrpstrpos', 'dgrpstrneg', 'dgrpstr2']) {
addKey(r[col])
}
const descFunc = Number(r['descfunc']?.trim() || 0)
const dgrp = Number(r['dgrp']?.trim() || 0)
if (descFunc > 0 || dgrp > 0) {
iscMeta[stat] = {
id: Number(r['ID']?.trim() || 0),
stat,
descPriority: Number(r['descpriority']?.trim() || 0),
descFunc,
descVal: Number(r['descval']?.trim() || 0),
descStrPos: r['descstrpos']?.trim() || '',
descStrNeg: r['descstrneg']?.trim() || '',
descStr2: r['descstr2']?.trim() || '',
dgrp,
dgrpFunc: Number(r['dgrpfunc']?.trim() || 0),
dgrpVal: Number(r['dgrpval']?.trim() || 0),
dgrpStrPos: r['dgrpstrpos']?.trim() || '',
dgrpStrNeg: r['dgrpstrneg']?.trim() || '',
dgrpStr2: r['dgrpstr2']?.trim() || '',
}
}
}
// 5. CharStats.txt & SkillDesc.txt
for (const r of (await readTxt('CharStats.txt')).rows) {
for (const col of ['StrAllSkills', 'StrSkillTab1', 'StrSkillTab2', 'StrSkillTab3', 'StrClassOnly', 'class']) {
addKey(r[col])
}
}
for (const r of (await readTxt('SkillDesc.txt')).rows) {
addKey(r['str name'])
}
// 6. All ModStr*, strMod*, ItemStats*, Weapon*, StrGem*, GemXp*, ExInsert*, Socketable, quality/gold keys
for (const [k] of eng.dict.entries()) {
if (
k.startsWith('ModStr') ||
k.startsWith('strMod') ||
k.startsWith('ItemStats') ||
k.startsWith('Weapon') ||
k.startsWith('StrGem') ||
k.startsWith('GemXp') ||
k.startsWith('ExInsert') ||
k === 'Socketable' ||
k === 'gld' ||
k === 'Gold' ||
k === 'Hiquality' ||
k === 'Cracked' ||
k === 'Crude' ||
k === 'Damaged' ||
k === 'Low Quality' ||
k === 'Gemmed' ||
k === 'GemmedNormalName'
) {
keys.add(k)
}
}
const sortedKeys = Array.from(keys).sort()
const engOut: Record<string, string> = {}
const chiOut: Record<string, string> = {}
for (const k of sortedKeys) {
const ev = eng.get(k)
if (ev !== undefined) {
engOut[k] = ev
}
const cv = chi.get(k)
if (cv !== undefined) {
chiOut[k] = cv
}
}
// Also include expansionstring.tbl index 2745 ("Ethereal (Cannot be Repaired)") under canonical key 'strethereal'
const expTbl = eng.tables[1]
if (expTbl && expTbl[2745]) {
engOut['strethereal'] = expTbl[2745]!
}
const outPath = path.join(process.cwd(), 'src/data/canonical-tbl-strings.ts')
const fileContent = `/**
* AUTO-GENERATED by \`scripts/pack-canonical-tbl.ts\` from authoritative 1.13c MPQ archives:
* - \`d2data.mpq\` (\`string.tbl\`)
* - \`d2exp.mpq\` (\`expansionstring.tbl\`)
* - \`Patch_D2.mpq\` (\`patchstring.tbl\`)
* - \`data/global/excel/ItemStatCost.txt\`
*
* DO NOT EDIT MANUALLY.
*/
export interface CanonicalIscDescEntry {
readonly id: number
readonly stat: string
readonly descPriority: number
readonly descFunc: number
readonly descVal: number
readonly descStrPos: string
readonly descStrNeg: string
readonly descStr2: string
readonly dgrp: number
readonly dgrpFunc: number
readonly dgrpVal: number
readonly dgrpStrPos: string
readonly dgrpStrNeg: string
readonly dgrpStr2: string
}
export const CANONICAL_ENG_TBL_DICT: Readonly<Record<string, string>> = ${JSON.stringify(engOut, null, 2)}
export const CANONICAL_CHI_TBL_DICT: Readonly<Record<string, string>> = ${JSON.stringify(chiOut, null, 2)}
export const CANONICAL_ISC_DESC_TABLE: Readonly<Record<string, CanonicalIscDescEntry>> = ${JSON.stringify(iscMeta, null, 2)}
`
fs.writeFileSync(outPath, fileContent, 'utf8')
console.log(
`Wrote ${outPath}: ${Object.keys(engOut).length} ENG keys, ${Object.keys(chiOut).length} CHI keys, ${Object.keys(iscMeta).length} ISC entries.`,
)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})