// Dump sacred.exe's combat-art definition tables (see docs/skill-slots.md). // node tools/arttable.mjs [id...] (ids hex, e.g. 3f7; none: all) import { readFileSync } from "node:fs"; const [exePath, ...ids] = process.argv.slice(2); const file = readFileSync(exePath); const pe = file.readUInt32LE(0x3c); const sections = file.readUInt16LE(pe + 6); const optional = file.readUInt16LE(pe + 20); const imageBase = file.readUInt32LE(pe + 52); const table = pe + 24 + optional; function offset(va) { for (let i = 0; i < sections; i++) { const s = table + i * 40; const vsize = file.readUInt32LE(s + 8), vaddr = file.readUInt32LE(s + 12); const raw = file.readUInt32LE(s + 20), rawSize = file.readUInt32LE(s + 16); const rva = va - imageBase; if (rva >= vaddr && rva < vaddr + vsize) return rva - vaddr < rawSize ? rva - vaddr + raw : -1; } return -1; } const tables = [ { name: "A", base: 0x94ddb0, stride: 0x78, end: 0x950a38 }, { name: "B", base: 0x950a38, stride: 0x94, end: 0x953344 }, ]; const wanted = new Set(ids.map((t) => parseInt(t, 16))); for (const t of tables) { for (let va = t.base; va < t.end; va += t.stride) { const o = offset(va); if (o < 0) { console.log(t.name, va.toString(16), "(no file data: filled at runtime)"); break; } const id = file.readUInt16LE(o); if (wanted.size && !wanted.has(id)) continue; const words = []; for (let k = 0; k < t.stride; k += 4) words.push(file.readUInt32LE(o + k).toString(16).padStart(8, "0")); console.log(`${t.name} ${va.toString(16)} id ${id.toString(16)}: ${words.join(" ")}`); } }