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;