/** * 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() 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 = {} 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 = {} const chiOut: Record = {} 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> = ${JSON.stringify(engOut, null, 2)} export const CANONICAL_CHI_TBL_DICT: Readonly> = ${JSON.stringify(chiOut, null, 2)} export const CANONICAL_ISC_DESC_TABLE: Readonly> = ${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) })