mirror of
https://gitee.com/anod/open_agb_firm.git
synced 2025-05-06 05:44:11 +08:00
176 lines
5.7 KiB
JavaScript
176 lines
5.7 KiB
JavaScript
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()
|