/** * Generates src/game/drlg/drlg-ids.ts from D2MOO's DataTbls/LevelsIds.h (the LvlPrest, LvlSub and * LvlTypes enums), so the DRLG port uses exactly the ids D2MOO compiles against. * * D2MOO is read from D2MOO_SRC (default ~/tmp/D2MOO) and must be at the pinned commit. * * Usage: npx tsx scripts/gen-drlg-ids.ts */ import { execFileSync } from 'node:child_process' import { readFileSync, writeFileSync } from 'node:fs' import { homedir } from 'node:os' import { join, resolve } from 'node:path' const PINNED_COMMIT = '5596f5cb6c5251a0a07c6637d26458b06099d516' const d2mooSrc = resolve(process.env.D2MOO_SRC ?? join(homedir(), 'tmp', 'D2MOO')) const commit = execFileSync('git', ['-C', d2mooSrc, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim() if (commit !== PINNED_COMMIT) { throw new Error(`${d2mooSrc} is at ${commit}, expected ${PINNED_COMMIT}`) } const header = readFileSync(join(d2mooSrc, 'source/D2Common/include/DataTbls/LevelsIds.h'), 'latin1') function parseEnum(name: string): [string, number][] { const match = new RegExp(`enum\\s+${name}\\s*\\{([\\s\\S]*?)\\};`).exec(header) if (!match) throw new Error(`enum ${name} not found`) const body = match[1]! if (/^\s*#/m.test(body)) throw new Error(`enum ${name} has preprocessor directives; handle them explicitly`) const entries: [string, number][] = [] let value = -1 for (const raw of body.replace(/\/\*[\s\S]*?\*\//g, '').split(',')) { const item = raw.replace(/\/\/.*$/gm, '').trim() if (!item) continue const eq = /^(\w+)\s*=\s*(.+)$/.exec(item) if (eq) { const literal = eq[2]!.trim() if (!/^(0x[0-9a-fA-F]+|\d+)$/.test(literal)) throw new Error(`${name}: unsupported initialiser ${item}`) value = Number(literal) entries.push([eq[1]!, value]) } else { if (!/^\w+$/.test(item)) throw new Error(`${name}: unsupported entry ${item}`) value += 1 entries.push([item, value]) } } return entries } const sections: [string, string][] = [ ['D2C_LvlPrestIds', 'LvlPrest.txt row ids (DataTbls/LevelsIds.h D2C_LvlPrestIds).'], ['D2C_LvlSubIds', 'LvlSub.txt types (DataTbls/LevelsIds.h D2C_LvlSubIds).'], ['D2C_LvlTypes', 'LvlTypes.txt ids (DataTbls/LevelsIds.h D2C_LvlTypes).'], ] let out = `/** * GENERATED by scripts/gen-drlg-ids.ts from D2MOO source/D2Common/include/DataTbls/LevelsIds.h * (commit ${PINNED_COMMIT}, MIT License, Copyright (c) 2020-2025 The Phrozen Keep community). * Do not edit by hand. */ ` for (const [name, doc] of sections) { const entries = parseEnum(name) out += `\n// ${doc} ${entries.length} entries.\n` for (const [id, value] of entries) out += `export const ${id} = ${value}\n` } /** Values of `static const [..]... = { ... };` in a D2MOO source file, flattened. */ function extractArray(relPath: string, name: string, expectedLength: number): number[] { const source = readFileSync(join(d2mooSrc, relPath), 'latin1') const match = new RegExp(`static\\s+const\\s+\\w+\\s+${name}\\s*(?:\\[[^\\]]*\\])+\\s*=\\s*\\{([\\s\\S]*?)\\};`).exec(source) if (!match) throw new Error(`${relPath}: array ${name} not found`) const values = match[1]! .replace(/\/\/.*$/gm, '') .replace(/[{}]/g, ',') .split(',') .map(s => s.trim()) .filter(s => s.length > 0) .map(s => { if (!/^(0x[0-9a-fA-F]+|\d+)$/.test(s)) throw new Error(`${relPath}: ${name}: unsupported literal ${s}`) return Number(s) }) if (values.length !== expectedLength) { throw new Error(`${relPath}: ${name} has ${values.length} values, expected ${expectedLength}`) } return values } const arrays: [string, string, string, number, string][] = [ ['source/D2Common/src/Drlg/DrlgPreset.cpp', 'dword_6FDE1180', 'gObjPresetToObjectId', 5 * 150, 'DRLGPRESET_GetObjectIndexFromObjPreset (D2Common.0x6FD859E0): [nAct * 150 + nUnitId].'], ['source/D2Common/src/Drlg/DrlgPreset.cpp', 'nTileTypeMappingTable', 'gTileTypeMappingTable', 42, 'DRLGPRESET_MapTileType (D2Common.0x6FD88850): DS1 version < 7 tile types.'], ['source/D2Common/src/Drlg/DrlgOutdoors.cpp', 'byte_6FDCF958', 'byte_6FDCF958', 256, 'DRLG_OUTDOORS_GenerateDirtPath (D2Common.0x6FD7EFE0): dirt-path neighbourhood -> tile sequence.'], ] for (const [relPath, name, exportName, length, doc] of arrays) { const values = extractArray(relPath, name, length) out += `\n// ${doc}\n// ${relPath}: ${name}\nexport const ${exportName}: readonly number[] = [\n` for (let i = 0; i < values.length; i += 25) out += ` ${values.slice(i, i + 25).join(', ')},\n` out += ']\n' } const target = resolve('src/game/drlg/drlg-ids.ts') writeFileSync(target, out) console.log(`wrote ${target}`)