mirror of
https://gitee.com/anod/open_agb_firm.git
synced 2025-05-06 13:54:09 +08:00
打包cht文件的工具和原始的cht文件(有加工)
This commit is contained in:
parent
ef72e9772a
commit
4bdfea0d56
184
tools/cheat-builder/build.js
Normal file
184
tools/cheat-builder/build.js
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
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()
|
BIN
tools/cheat-builder/cheats.tar.bz2
Normal file
BIN
tools/cheat-builder/cheats.tar.bz2
Normal file
Binary file not shown.
333
tools/cheat-builder/cht.js
Normal file
333
tools/cheat-builder/cht.js
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
|
||||||
|
const WAIT_LOCK = 1
|
||||||
|
const READ_LOCK = 2
|
||||||
|
const WAIT_HOLE = 3
|
||||||
|
const READ_HOLE = 4
|
||||||
|
const WAIT_KEY = 5
|
||||||
|
const READ_KEY = 6
|
||||||
|
|
||||||
|
const STATE = ["", "WAIT_LOCK", "READ_LOCK", "WAIT_HOLE", "READ_HOLE", "WAIT_KEY", "READ_KEY"]
|
||||||
|
const PASSHOLE = ["text", "off"]
|
||||||
|
|
||||||
|
const read = (cht, info, order) => {
|
||||||
|
cht = cht.replace( /[-]{4,}/g, "" );
|
||||||
|
const space = " ".charCodeAt();
|
||||||
|
const tab = "\t".charCodeAt();
|
||||||
|
const zero = "0".charCodeAt();
|
||||||
|
const nine = "9".charCodeAt();
|
||||||
|
const a = "a".charCodeAt();
|
||||||
|
const f = "f".charCodeAt();
|
||||||
|
const size = cht.length;
|
||||||
|
let state = WAIT_LOCK, token = [], line = 1;
|
||||||
|
|
||||||
|
let locks = [], currlock = null;
|
||||||
|
const token_str = () => { let r = String.fromCharCode.apply( String, token ); return (token.length=0,r);}
|
||||||
|
const error = msg => { throw new SyntaxError(`${msg} at line: ${line} in lock ${currlock?.name}`) }
|
||||||
|
for( let i=0; i <= size; ++i ){
|
||||||
|
const ch = i == size ? "" : cht.charAt(i);
|
||||||
|
switch( ch ){
|
||||||
|
case "[":
|
||||||
|
if( state == WAIT_LOCK || state == READ_HOLE ){
|
||||||
|
state = READ_LOCK;
|
||||||
|
if( currlock ) locks.push( { name: currlock.name, keys: currlock.holes } );
|
||||||
|
currlock = {}
|
||||||
|
}
|
||||||
|
else error(`error occur [ on ${STATE[state]}`)
|
||||||
|
break;
|
||||||
|
case "]":
|
||||||
|
if( state == READ_LOCK ){
|
||||||
|
state = WAIT_HOLE;
|
||||||
|
currlock.name = token_str();
|
||||||
|
currlock.holes = [];
|
||||||
|
if( currlock.name.toLowerCase() == "gameinfo" )
|
||||||
|
i = size; // break;
|
||||||
|
}
|
||||||
|
else error(`error occur ] on ${STATE[state]}`)
|
||||||
|
break;
|
||||||
|
case "\r": case "\n":
|
||||||
|
if( state == WAIT_HOLE ){
|
||||||
|
state = READ_HOLE;
|
||||||
|
}
|
||||||
|
else if( state == READ_KEY ){
|
||||||
|
state = READ_HOLE;
|
||||||
|
if( token.length ) currlock.keys.at(-1).push( token_str() );
|
||||||
|
if( !PASSHOLE.includes(currlock.hole.toLowerCase()) ) {
|
||||||
|
currlock.holes.push( {name: currlock.hole, cmd: currlock.keys} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if( state == READ_LOCK ){
|
||||||
|
error(`error occur newline on ${STATE[state]}`);
|
||||||
|
}
|
||||||
|
else if( state == READ_HOLE ){
|
||||||
|
if( token.length > 0 && token.find( c => c!=space && c!=tab ) ){
|
||||||
|
error(`error occur newline on ${STATE[state]}`);
|
||||||
|
}
|
||||||
|
else token.length = 0;
|
||||||
|
}
|
||||||
|
if( ch == "\n" ) ++line;
|
||||||
|
break;
|
||||||
|
case "=":
|
||||||
|
if( state == READ_HOLE ){
|
||||||
|
state = READ_KEY;
|
||||||
|
currlock.hole = token_str();
|
||||||
|
currlock.keys = [ new Array() ];
|
||||||
|
}
|
||||||
|
else if( state == READ_LOCK ){
|
||||||
|
token.push( ch.charCodeAt() );
|
||||||
|
}
|
||||||
|
else error(`error occur = on ${STATE[state]}`);
|
||||||
|
break;
|
||||||
|
case ",":
|
||||||
|
if( state == READ_KEY ){
|
||||||
|
if( token.length ) currlock.keys.at(-1).push( token_str() );
|
||||||
|
}
|
||||||
|
else if( state == READ_HOLE ){// MULTILINE KEY
|
||||||
|
let {name, cmd} = currlock.holes.pop();
|
||||||
|
currlock.hole = name;
|
||||||
|
currlock.keys = cmd;
|
||||||
|
if( token.length ) currlock.keys.at(-1).push( token_str() );
|
||||||
|
state = READ_KEY;
|
||||||
|
}
|
||||||
|
else if( state == READ_LOCK ){
|
||||||
|
token.push( ch.charCodeAt() );
|
||||||
|
}
|
||||||
|
else error(`error occur , on ${STATE[state]}`);
|
||||||
|
break;
|
||||||
|
case ";":
|
||||||
|
if( state == READ_KEY ){
|
||||||
|
if( token.length ) currlock.keys.at(-1).push( token_str() );
|
||||||
|
currlock.keys.push( new Array() );
|
||||||
|
}
|
||||||
|
else if( state == READ_LOCK ){
|
||||||
|
token.push( ch.charCodeAt() );
|
||||||
|
}
|
||||||
|
else if( state == READ_HOLE ){// MULTILINE KEY
|
||||||
|
let {name, cmd} = currlock.holes.pop();
|
||||||
|
currlock.hole = name;
|
||||||
|
currlock.keys = cmd;
|
||||||
|
if( token.length ) currlock.keys.at(-1).push( token_str() );
|
||||||
|
currlock.keys.push( new Array() );
|
||||||
|
state = READ_KEY;
|
||||||
|
}
|
||||||
|
else error(`error occur ; on ${STATE[state]}`);
|
||||||
|
break;
|
||||||
|
case "": // END OF FILE
|
||||||
|
if( state == READ_KEY ){
|
||||||
|
if( token.length ) currlock.keys.at(-1).push( token_str() );
|
||||||
|
}
|
||||||
|
else if( state == READ_HOLE && token.length == 0 ){
|
||||||
|
// safe
|
||||||
|
}
|
||||||
|
else error(`error occur eof on ${STATE[state]}`)
|
||||||
|
break;
|
||||||
|
case " ":
|
||||||
|
if( state == READ_LOCK || state == READ_HOLE ){
|
||||||
|
token.push( ch.charCodeAt() );
|
||||||
|
}
|
||||||
|
else if( state == READ_KEY ){
|
||||||
|
if( currlock.name.toLowerCase() == "gameinfo" ){
|
||||||
|
token.push( ch.charCodeAt() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if( state == READ_LOCK || state == READ_HOLE ){
|
||||||
|
token.push( ch.charCodeAt() );
|
||||||
|
}
|
||||||
|
else if( state == READ_KEY ){
|
||||||
|
let digit = ch.toLowerCase().charCodeAt();
|
||||||
|
if( (zero <= digit && digit <= nine) ||
|
||||||
|
(a <= digit && digit <= f) ||
|
||||||
|
currlock.name.toLowerCase() == "gameinfo" ||
|
||||||
|
currlock.hole.toLowerCase() == "text" )
|
||||||
|
{
|
||||||
|
token.push( digit );
|
||||||
|
}
|
||||||
|
else error( `error occur ${ch}[${digit}] on ${STATE[state]}` );
|
||||||
|
}
|
||||||
|
else error(`error occur ${ch} on ${STATE[state]}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lade( locks, info, order );
|
||||||
|
}
|
||||||
|
|
||||||
|
const assembleCheat = (list, dup, hole) => {
|
||||||
|
// 建立 地址:数值 的映射关系
|
||||||
|
const fromTaddr = taddr => {
|
||||||
|
const n = parseInt(taddr, 16);
|
||||||
|
if( n==0 || n>0x41FFFFF ) throw new Error( `get a invalid address: ${taddr}` );
|
||||||
|
if( n <= 0x3ffff ) return n | 0x2000000;
|
||||||
|
else if( 0 == (n & 0xf000000) ) return (n & 0xffff) | 0x3000000;
|
||||||
|
else return n;
|
||||||
|
};
|
||||||
|
const addrval = list.reduce( (r, command) => {
|
||||||
|
let [taddr, ...vals] = command;
|
||||||
|
let addr = fromTaddr( taddr );
|
||||||
|
vals.forEach( (val, pos) => {
|
||||||
|
let dest = addr + pos;
|
||||||
|
let value = parseInt(val, 16);
|
||||||
|
if( r.has(dest) && r.get(dest) != value )
|
||||||
|
console.warn( `duplicated address assign ${dest.toString(16)}` );
|
||||||
|
else r.set( dest, value );
|
||||||
|
} );
|
||||||
|
return r;
|
||||||
|
}, new Map() );
|
||||||
|
const ordered = [...addrval].sort( (a,b) => a[0] - b[0] );
|
||||||
|
const blocks = ordered.reduce( (arr, [addr, value]) => {
|
||||||
|
if( dup.has(addr) && dup.get(addr) != hole )
|
||||||
|
console.warn( `conflict address in different hole: orig=${dup.get(addr)} confl=${hole}` );
|
||||||
|
dup.set(addr, hole);
|
||||||
|
if( arr.length == 0 ) {
|
||||||
|
arr.push({addr, value, count: 1});// cnt addr value
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let cur = arr.at(-1);
|
||||||
|
if( cur.value == value ){
|
||||||
|
++cur.count;
|
||||||
|
}
|
||||||
|
else arr.push( {addr, value, count: 1} );
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [] );
|
||||||
|
return Uint32Array.from( blocks.reduce( (r,block)=>[...r, block.addr, (block.count<<8)|block.value], []) )
|
||||||
|
}
|
||||||
|
|
||||||
|
const pack = (entlist, strlist, idxlist, cmdlist, xvalue) => {
|
||||||
|
const align = (n, base) => base * Math.floor( (n+base-1) / base );
|
||||||
|
const strbase = 2 + entlist.length * 4 * 3;
|
||||||
|
const idxbase = strbase + strlist.length;
|
||||||
|
const cmdbase = idxbase + idxlist.length * 2;
|
||||||
|
const size = cmdbase + cmdlist.length*4;
|
||||||
|
let result = new ArrayBuffer( align(size, 32) );
|
||||||
|
|
||||||
|
// entry count
|
||||||
|
let view = new DataView(result);
|
||||||
|
view.setUint16(0, entlist.length, true);
|
||||||
|
|
||||||
|
// entry data
|
||||||
|
entlist.forEach( (entry,index) => {
|
||||||
|
let offset = 2 + index*4*3;
|
||||||
|
view.setUint32( offset, entry[0], true );
|
||||||
|
view.setUint32( offset+4, entry[1] + (entry[0] > 0xffff ? cmdbase : idxbase), true );
|
||||||
|
view.setUint32( offset+8, entry[2], true );
|
||||||
|
});
|
||||||
|
|
||||||
|
// string table
|
||||||
|
let strings = new Uint8Array( result, strbase, strlist.length );
|
||||||
|
strings.set( strlist, 0 );
|
||||||
|
|
||||||
|
// indexes table
|
||||||
|
let indexes = new Uint8Array( result, idxbase, idxlist.length*2 );
|
||||||
|
indexes.set( new Uint8Array(idxlist.buffer), 0 );
|
||||||
|
|
||||||
|
// instruction table
|
||||||
|
let commands = new Uint8Array( result, cmdbase, cmdlist.length*4 );
|
||||||
|
commands.set( new Uint8Array(cmdlist.buffer), 0 );
|
||||||
|
|
||||||
|
return { id: xvalue, bin: new Uint8Array( result ) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const lade = (list, info, order) => {
|
||||||
|
let enttable = []; // 保存hole/key数据
|
||||||
|
|
||||||
|
const makeID = (a,b) => a|b<<16;
|
||||||
|
const makeEntry = (id, str) => ({ id, data: str });
|
||||||
|
|
||||||
|
// collect
|
||||||
|
const collectEntry = (id, keys, list) => {
|
||||||
|
keys.forEach( ({name,cmd}, index) => {
|
||||||
|
list.push( name );
|
||||||
|
enttable.push( makeEntry( makeID(id,index+1), cmd ) );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let rootstr = [];
|
||||||
|
list.forEach( (hole,index) => {
|
||||||
|
rootstr.push( hole.name );
|
||||||
|
|
||||||
|
let holestr = [];
|
||||||
|
collectEntry( index+1, hole.keys, holestr );
|
||||||
|
|
||||||
|
let entry = makeEntry( makeID(index+1,0), holestr );
|
||||||
|
enttable.push( entry );
|
||||||
|
} );
|
||||||
|
enttable.push( makeEntry( makeID(0,0), rootstr ) );
|
||||||
|
|
||||||
|
enttable.sort( (a,b) => a.id - b.id );
|
||||||
|
|
||||||
|
const conv = new TextEncoder();
|
||||||
|
let strtable = Uint8Array.of(); // 保存字符串常量
|
||||||
|
let idxtable = Uint16Array.of(); // 从字符串常量索引出来的字符串列表
|
||||||
|
let cmdtable = Uint32Array.of(); // 保存选项对应指令
|
||||||
|
const addString = list => {
|
||||||
|
return list.map( str => {
|
||||||
|
let code = conv.encode( str );
|
||||||
|
let off = strtable.findIndex( (s,i,t) => {
|
||||||
|
if( code.every( (_,j) => code[j]==t[j+i] ) && strtable[i+code.length]==0 ){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} );
|
||||||
|
if( off < 0 ){
|
||||||
|
off = strtable.length;
|
||||||
|
let nt = new Uint8Array( off+code.length+1 );
|
||||||
|
nt.set( strtable, 0 );
|
||||||
|
nt.set( code, off );
|
||||||
|
strtable = nt;
|
||||||
|
}
|
||||||
|
str = str.toLowerCase();
|
||||||
|
if( str.indexOf("off") >= 0 || str.indexOf("关") >= 0 ){
|
||||||
|
//console.log(`似乎是一个单独开关项[label=${str} file=${order}]`);
|
||||||
|
}
|
||||||
|
return off;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const addStrIndex = list => {
|
||||||
|
if( list.length == 0 ) return 0;
|
||||||
|
|
||||||
|
if( strtable.length > 0xffff ) throw new RangeError(`string table is too big.`);
|
||||||
|
let ret = idxtable.length;
|
||||||
|
let nt = new Uint16Array( ret + list.length );
|
||||||
|
nt.set( idxtable, 0 );
|
||||||
|
nt.set( list, ret );
|
||||||
|
idxtable = nt;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addCommand = list => {
|
||||||
|
let ret = cmdtable.length;
|
||||||
|
let nt = new Uint32Array( ret + list.length );
|
||||||
|
nt.set( cmdtable, 0 );
|
||||||
|
nt.set( list, ret );
|
||||||
|
cmdtable = nt;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( enttable.length > 0xffff ){
|
||||||
|
console.warn(`entry 数量超过上限[file=${order}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pack
|
||||||
|
let holedup = new Map();
|
||||||
|
let entbytelist = enttable.map( entry => {
|
||||||
|
let {id, data} = entry;
|
||||||
|
if( id < 0x10000 ){ // 收集str
|
||||||
|
let idxlist = addString(data.length > 1 ? data : []); // length==1就是开启
|
||||||
|
let location = addStrIndex( idxlist );
|
||||||
|
return Uint32Array.of( id, location, idxlist.length );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let armbytecode = assembleCheat( data, holedup, id & 0xffff );
|
||||||
|
let location = addCommand( armbytecode );
|
||||||
|
return Uint32Array.of( id, location, armbytecode.length );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let n = info.serial.split("").reduce( (r,v)=>(v.charCodeAt() | r<<8), 0 );
|
||||||
|
let xv = cmdtable.reduce( (r,v) => r^v, n );
|
||||||
|
return pack( entbytelist, strtable, idxtable, cmdtable, xv );
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = read;
|
157
tools/cheat-builder/xml.js
Normal file
157
tools/cheat-builder/xml.js
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
|
||||||
|
let cursor, tr;
|
||||||
|
|
||||||
|
const is_space = ch => " \r\t\n\H".includes(ch);
|
||||||
|
|
||||||
|
const eat_space = () => {while( is_space( tr.charAt(cursor) ) ) ++cursor;}
|
||||||
|
|
||||||
|
const match_next = (str, ignore_space) => {
|
||||||
|
let ch = next_char( ignore_space );
|
||||||
|
if( ch != str )
|
||||||
|
throw new SyntaxError(`dismatch symbol, expert ${str}, get ${ch} pos: ${tr.slice(cursor-10, cursor+20)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const next_char = ignore_space => {
|
||||||
|
if( cursor == tr.length )
|
||||||
|
throw new SyntaxError(`invalid end of file`)
|
||||||
|
|
||||||
|
if( !ignore_space )
|
||||||
|
return tr.charAt( cursor++ );
|
||||||
|
else {
|
||||||
|
let ch = ""
|
||||||
|
do{
|
||||||
|
ch = tr.charAt(cursor++);
|
||||||
|
}while( is_space(ch) );
|
||||||
|
return ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const read_token = () => {
|
||||||
|
let chs = []
|
||||||
|
do{
|
||||||
|
let ch = peek_char(1)
|
||||||
|
if( is_space(ch) || ch == "=" || ch == ">" || ch == "/" )
|
||||||
|
break;
|
||||||
|
else chs.push( ch );
|
||||||
|
|
||||||
|
next_char();
|
||||||
|
} while( true );
|
||||||
|
if( chs.length == 0 )
|
||||||
|
throw new SyntaxError(`invalid token without any char`)
|
||||||
|
return chs.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
const peek_char = (n,ignore_space=0) => {
|
||||||
|
let t=cursor;
|
||||||
|
if( t == tr.length )
|
||||||
|
return "";
|
||||||
|
|
||||||
|
if( !ignore_space )
|
||||||
|
return tr.slice( t, t+n );
|
||||||
|
else {
|
||||||
|
let chs = []
|
||||||
|
while( n>0 ){
|
||||||
|
let ch = tr.charAt(t++);
|
||||||
|
if( is_space(ch) )
|
||||||
|
continue;
|
||||||
|
chs.push( ch );
|
||||||
|
n--;
|
||||||
|
}
|
||||||
|
return chs.join("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const read_attr = () => {
|
||||||
|
match_next('"', 1);
|
||||||
|
let chs = [], ch = "";
|
||||||
|
do{
|
||||||
|
ch = next_char();
|
||||||
|
if( ch=="\\" && peek_char(1) == '\"' ){
|
||||||
|
chs.push( next_char() )
|
||||||
|
}
|
||||||
|
else if( ch != '"' ) chs.push( ch );
|
||||||
|
}while( ch != '"' );
|
||||||
|
return chs.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
const read_text = () => {
|
||||||
|
let ch = next_char(1);
|
||||||
|
let chs = [ch];
|
||||||
|
do{
|
||||||
|
ch = peek_char(1);
|
||||||
|
if( ch == "<" ) break;
|
||||||
|
chs.push( next_char() );
|
||||||
|
}while( true )
|
||||||
|
return chs.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const read_node = () => {
|
||||||
|
let token, attr, children;
|
||||||
|
let result = {name: "", attributes: {}, children: []}, node_name = "";
|
||||||
|
match_next( "<", 1 );
|
||||||
|
node_name = read_token();
|
||||||
|
result.name = node_name;
|
||||||
|
|
||||||
|
let peekch = peek_char(1,1);
|
||||||
|
while( peekch != "/" && peekch != ">" ){
|
||||||
|
eat_space();
|
||||||
|
token = read_token();
|
||||||
|
if( peek_char(1) == "=" ){
|
||||||
|
next_char();
|
||||||
|
attr = read_attr();
|
||||||
|
result.attributes[token] = attr;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result.attributes[token] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
peekch = peek_char(1,1);
|
||||||
|
}
|
||||||
|
if( peekch == "/" ){
|
||||||
|
match_next("/",1)
|
||||||
|
match_next(">")
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else if( peekch == ">" ){
|
||||||
|
children = []
|
||||||
|
next_char(1)
|
||||||
|
}
|
||||||
|
else throw new SyntaxError(`should not come here ${peekch}`)
|
||||||
|
|
||||||
|
while( true ){
|
||||||
|
if( peek_char(2,1) == "</") {
|
||||||
|
next_char(1); next_char(1);
|
||||||
|
node_name = read_token();
|
||||||
|
match_next(">",1);
|
||||||
|
if( node_name != result.name )
|
||||||
|
throw new SyntaxError(`dismatch close tag for ${node_name}`)
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
else if( peek_char(1,1) == "<") {
|
||||||
|
children.push( read_node() );
|
||||||
|
}
|
||||||
|
else children.push( read_text() )
|
||||||
|
}
|
||||||
|
|
||||||
|
result.children = children;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trim_comment = tr => {
|
||||||
|
let pos = tr.indexOf("<!--");
|
||||||
|
while( pos >= 0 ){
|
||||||
|
let end = tr.indexOf("-->", pos+4);
|
||||||
|
tr = tr.slice(0, pos) + tr.slice(end+3);
|
||||||
|
pos = tr.indexOf("<!--");
|
||||||
|
}
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = r => {
|
||||||
|
cursor = 0;
|
||||||
|
tr = r.replace(/<\?.*\?>/g, "").replace(/<!DOCTYPE.*>/g, "");
|
||||||
|
tr = trim_comment(tr);
|
||||||
|
return read_node();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = start
|
Loading…
x
Reference in New Issue
Block a user