164 lines
5.6 KiB
JavaScript

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("./list.dat");
const records = readxml( datfile.toString("utf-8") );
const inform = rec => [rec.attributes.name.slice(0,4), rec.children[1].attributes.serial||"????", rec.attributes.name.slice(7)]
return records.children.filter( rec => rec.name=="game" && rec.attributes.name.charAt()!="x" ).map( inform );*/
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}` );
valid.forEach( ({serial, cheat}) => console.debug(`${cheat.length} in ${serial}`) );
const enc = new TextEncoder();
const align = (n, base) => base * Math.floor( (n+base-1) / base );
const concat = (a,b) => {
const n = new ( Object.getPrototypeOf(a).constructor )( a.length+b.length );
n.set( a, 0 )
n.set( b, a.length );
return n;
}
const getnametable = list => list.reduce( (r,{serial, cheat}, idx, arr) => {
const val = serial.slice(0, -1);
if( val != r[4] ){
if( r[1].length > 0 && r[2] - r[1].at(-1) > r[3] )
r[3] = r[2] - r[1].at(-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(-1) > r[3] )
r[3] = r[2] - r[1].at(-1);
r[1] = concat( r[1], [ r[2] ] );
}
return r;
}, [new Uint8Array(), new Uint16Array(), 0, 0, ""] );
const expandcheat = (list, base) => list.reduce( (r, {cheat, serial}) => {
const val = serial.charCodeAt(3);
const [c, e] = cheat.reduce( (r,{id, bin}) => {
const off = r[2] + r[1].length;
r[0] = concat( r[0], [val | ( off<<3 ), id] )
r[1] = concat( r[1], bin );
return r;
}, r );
return [c, e, r[2]];
}, [new Uint32Array(), new Uint8Array(), align(base, 32)] );
const [sers, offs, chtc, maxl] = getnametable( valid );
const [cheats, expanded] = expandcheat( valid, 8+sers.length+offs.length*2+chtc*8 );
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()