const readxml = require("./xml"); const readCht = require("./cht"); const { readFile, writeFile } = require("fs/promises"); const XML = node => { const helper = { get: function(obj, key) { if( !(key in obj) ){ return map => { if( !map ) return XML( obj.children.find( n => n.name==key ) ); else return XML( obj.children.find( n => n.name == key && Object.keys(map).every(k=>n.attributes[k]==map[k]) ) ); } } else return obj[key] } } return node ? new Proxy(node, helper) : null; } const slotmap = (type,size) => { const map = { gba_eeprom_4k:0, gba_yoshiug: 0, gba_eeprom: 0, gba_eeprom_64k: 2, gba_boktai: 2, gba_flash_rtc: 8, gba_flash: 9, gba_flash_512: 9, gba_flash_1m_rtc: 10, gba_flash_1m: 11, gba_sram: 14, gba_drilldoz: 14, gba_wariotws: 14 } return type in map ? (0 == (map[type] & 4) ? (size>0x1000000 ? map[type]+1 : map[type]) : map[type] ) : 15 } const romdesc = data => { const getserial = tr => ["AGB-","AGP-"].includes(tr.slice(0,4).toUpperCase())?tr.slice(4,8).toUpperCase() : ""; const getslot = n => n ? slotmap( n.attributes.value ) : 15 const rom = XML(data); // title let title = rom.info( {name: "alt_title"} ); // description let desc = rom.description(); // serial let serial = rom.info( {name: "serial"} ); // slot let part = rom.part( {name: "cart"} ); return { title: title ? title.attributes.value : desc.children[0], desc: desc.children[0], serial: serial ? getserial(serial.attributes.value) : "", slot: part ? getslot( part.feature({name: "slot"}), part.dataarea().attributes.size ) : 15, hash: part ? part.dataarea().rom().attributes.crc : "", } } const loadroms = async () => { const xmlfile = await readFile("./gba.xml") const xmldata = readxml( xmlfile.toString("utf-8") ); return xmldata.children.map( romdesc ).filter( n => !!n ) } const loadlist = async () => { const datfile = await readFile("./gba.dat"); const records = datfile.toString("utf-8").split("\r\n"); return records.filter( rec => rec.includes(";") ).map( rec => rec.split(";") ); } const trimcheat = (dat, rom, 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, rom, order) ); } const loadcheat = async (rom, order) => { try{ const chtfile = await readFile(`./gba/${order.padStart(4,"0").slice(-4)}.u8`); const cheats = chtfile.toString("utf-8"); if( cheats.indexOf("[GameInfo]") != cheats.lastIndexOf("[GameInfo]") ) rom.cheat = trimcheat( cheats, rom, order ); else rom.cheat = [ readCht( cheats, rom, order ) ]; } catch( e ) { console.log( "bad cheat: %s[order=%d]\n%s", rom.title, order, e.stack ) }; } const format = roms => { roms.sort( (a, b) => a.serial.localeCompare(b.serial) ); const valid = roms.filter( r => r.serial.length == 4 && !!r.cheat ); console.info( `all rom has ${roms.length}, valid rom has ${valid.length}` ); 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[3] ){ r[0] = concat( r[0], enc.encode(val) ) r[1] = concat( r[1], [ r[2] ] ); r[3] = val; } r[2] = r[2] + cheat.length; if( idx+1 == arr.length ) r[1] = concat( r[1], [ r[2] ] ); return r; }, [new Uint8Array(), new Uint16Array(), 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] = getnametable( valid ); const [cheats, expanded] = expandcheat( valid, 8+sers.length+offs.length*2+chtc*8 ); console.info( `name: ${sers.length} cheats: ${chtc}` ); 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.setUint32(4, slen, true); let ret = new Uint8Array( output ); ret.set( sers, serialbase ); ret.set( offs, offsetbase ); ret.set( cheats, cheatbase ); ret.set( expanded, expandbase ); return ret; } const start = async () => { let roms = await loadroms(); let indx = roms.reduce( (r,v) => (v.hash && r.set(v.hash,v), r), new Map() ); let list = await loadlist(); await Promise.all( list.map( game => { if( !indx.has(game[8].toLowerCase()) ) { return Promise.resolve(); } else return loadcheat( indx.get(game[8].toLowerCase()), game[0] ); } ) ); const content = await format( roms ); await writeFile( "gba.acl", content ); } start()