mirror of
https://gitee.com/anod/open_agb_firm.git
synced 2025-05-06 13:54:09 +08:00
339 lines
12 KiB
JavaScript
339 lines
12 KiB
JavaScript
|
|
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 space = " ".charCodeAt();
|
|
const tab = "\t".charCodeAt();
|
|
const zero = "0".charCodeAt();
|
|
const nine = "9".charCodeAt();
|
|
const a = "a".charCodeAt();
|
|
const f = "f".charCodeAt();
|
|
const conv = new TextEncoder();
|
|
|
|
const findIndex = (t, tr) => {
|
|
const tlen = t.length, rlen = tr.length;
|
|
for( let i=0; i+rlen < tlen; ++i ){
|
|
let eq = true;
|
|
for( j=0; j < rlen; ++j ){
|
|
if( t[i+j] != tr[j] ){
|
|
eq = false;
|
|
break;
|
|
}
|
|
}
|
|
if( eq && t[i+rlen]==0 ) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
const read = (cht, info, order) => {
|
|
cht = cht.replace( /[-]{4,}/g, "" );
|
|
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);
|
|
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]) => {
|
|
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 entrysize = entlist[0].buffer.byteLength;
|
|
const strbase = 2 + entlist.length * entrysize;
|
|
const idxbase = strbase + strlist.buffer.byteLength;
|
|
const cmdbase = idxbase + idxlist.buffer.byteLength;
|
|
const size = cmdbase + cmdlist.buffer.byteLength;;
|
|
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*entrysize;
|
|
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.buffer.byteLength );
|
|
indexes.set( new Uint8Array(idxlist.buffer), 0 );
|
|
|
|
// instruction table
|
|
let commands = new Uint8Array( result, cmdbase, cmdlist.buffer.byteLength );
|
|
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 );
|
|
|
|
let strtable = Uint8Array.of(); // 保存字符串常量
|
|
let idxtable = Uint16Array.of(); // 从字符串常量索引出来的字符串列表
|
|
let cmdtable = Uint32Array.of(); // 保存选项对应指令
|
|
const addString = list => {
|
|
const ut_pushtr = tr => {
|
|
const off = strtable.length;
|
|
let nt = new Uint8Array( off+tr.length+1 );
|
|
nt.set( strtable, 0 );
|
|
nt.set( tr, off );
|
|
strtable = nt;
|
|
return off;
|
|
};
|
|
const ut_addtr = tr => {
|
|
const code = conv.encode( tr );
|
|
const off = findIndex( strtable, code );
|
|
return off < 0 ? ut_pushtr( code ) : off;
|
|
};
|
|
return list.map( ut_addtr );
|
|
}
|
|
|
|
const addStrIndex = list => {
|
|
if( list.length == 0 ) return 0;
|
|
|
|
if( strtable.length > 0xffff ) throw new RangeError(`string table is too big.`);
|
|
let ret = idxtable.buffer.byteLength;
|
|
let nt = new Uint16Array( idxtable.length + list.length );
|
|
nt.set( idxtable, 0 );
|
|
nt.set( list, idxtable.length );
|
|
idxtable = nt;
|
|
return ret;
|
|
}
|
|
|
|
const addCommand = list => {
|
|
let ret = cmdtable.buffer.byteLength;
|
|
let nt = new Uint32Array( cmdtable.length + list.length );
|
|
nt.set( cmdtable, 0 );
|
|
nt.set( list, cmdtable.length );
|
|
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;
|