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;