const readxml = require("./xml"); const readCht = require("./cht"); const fs = require("fs"); const { readFile, writeFile, access } = require("fs/promises"); const loadlist = async () => { const datfile = await readFile("./serial.json"); const datas = JSON.parse(datfile.toString()); return Object.entries(datas).filter(n => n[1] != "????"); } const trimcheat = (dat, info, order) => { let parts = [], start = 0, done = false; while (!done) { let pos = dat.indexOf("[GameInfo]", start); if (pos > 0) { pos = dat.indexOf("[", pos + "[GameInfo]".length); if (pos > 0) { parts.push(dat.slice(start, pos)); start = pos; } else { parts.push(dat.slice(start, dat.length)); done = true; } } else { parts.push(dat.slice(start, dat.length)); done = true; } } return parts.map(tr => readCht(tr, info, order)); } const loadcheat = async (order, serial, title, file) => { try { const chtfile = await readFile(file); const cheats = chtfile.toString("utf-8"); let cheat = null; if (cheats.indexOf("[GameInfo]") != cheats.lastIndexOf("[GameInfo]")) cheat = trimcheat(cheats, { serial }, order); else cheat = [readCht(cheats, { serial }, order)]; return { serial, cheat } } catch (e) { console.log("bad cheat: %s[order=%d]\n%s", title, order, e.stack) return { serial } }; } const format = valid => { valid.sort((a, b) => a.serial.localeCompare(b.serial)); console.info(`valid rom has ${valid.length}`); const enc = new TextEncoder(); const size = Symbol("length"); const concatUnit = 10240; const align = (n, base) => base * Math.floor((n + base - 1) / base); const concat = (a, b) => { let len = (a[size] || 0) + b.length; if (len > a.length) { let n = new (Object.getPrototypeOf(a).constructor)(align(len, concatUnit) * 2); n.set(a, 0); n.set(b, a[size]); n[size] = len; return n; } else { a.set(b, a[size]); a[size] = len; return a; } } const getnametable = list => list.reduce((r, { serial, cheat }, idx, arr) => { const val = serial.slice(0, -1); if (val != r[4]) { if (r[1][size] > 0 && r[2] - r[1].at(r[1][size] - 1) > r[3]) r[3] = r[2] - r[1].at(r[1][size] - 1); r[0] = concat(r[0], enc.encode(val)) r[1] = concat(r[1], [r[2]]); r[4] = val; } r[2] = r[2] + cheat.length; if (idx + 1 == arr.length) { if (r[2] - r[1].at(r[1][size] - 1) > r[3]) r[3] = r[2] - r[1].at(r[1][size] - 1); r[1] = concat(r[1], [r[2]]); } return r; }, [concat(new Uint8Array(concatUnit), []), concat(new Uint16Array(concatUnit), []), 0, 0, ""]); const expandcheat = (list, base) => list.reduce((r, { cheat, serial }) => { const val = serial.charCodeAt(3); return cheat.reduce((r, { id, bin }) => { const off = r[2] + r[1][size]; r[0] = concat(r[0], [val | (off << 3), id]) r[1] = concat(r[1], bin); return r; }, r); }, [concat(new Uint32Array(concatUnit), []), concat(new Uint8Array(concatUnit), []), align(base, 32)]); const linkbuffer = (arr, ...l) => { for (let i of l) { const arrBa = arr[i]; arr[i] = arrBa.slice(0, arrBa[size]); } return arr; } const [sers, offs, chtc, maxl] = linkbuffer(getnametable(valid), 0, 1); const [cheats, expanded] = linkbuffer(expandcheat(valid, 8 + sers.length + offs.length * 2 + chtc * 8), 0, 1); console.info(`name: ${sers.length} cheats: ${chtc} maxl: ${maxl}`); const serialbase = 8; const offsetbase = serialbase + sers.length; const cheatbase = offsetbase + offs.length * 2; const expandbase = align(cheatbase + cheats.length * 4, 32); const total = expandbase + expanded.length; const output = new ArrayBuffer(total); // header: magic number: ACL\1-serial length(u32) const writter = new DataView(output); writter.setUint8(0, "A".charCodeAt()); writter.setUint8(1, "C".charCodeAt()); writter.setUint8(2, "L".charCodeAt()); writter.setUint8(3, 1); const slen = sers.length / 3; writter.setUint16(4, slen, true); writter.setUint16(6, maxl, true); let ret = new Uint8Array(output); ret.set(sers, serialbase); ret.set(new Uint8Array(offs.buffer), offsetbase); ret.set(new Uint8Array(cheats.buffer), cheatbase); ret.set(expanded, expandbase); return ret; } const hasfile = f => { try { fs.accessSync(f); return true; } catch (e) { console.error(e.stack) return false; } } const start = async () => { let list = await loadlist(); let roms = await Promise.all(list.map(game => { const [order, serial, title] = game; const file = `./gba/${order}.u8`; if (hasfile(file)) return loadcheat(order, serial, title ?? order, file); else return Promise.resolve(); })); console.log(`all rom has ${roms.length}`); roms = Object.entries(roms.reduce((r, data) => { if (!data) return r; const { serial, cheat } = data; if (!cheat || cheat.length == 0) return r; if (serial in r) r[serial] = [...r[serial], ...cheat]; else r[serial] = cheat; return r; }, {})).map(([serial, cheat]) => ({ serial, cheat })); const content = await format( roms ); await writeFile( "gba.acl", content ); } start()