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()