367 lines
11 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, tlen) => {
const rlen = tr.length, limit = tlen - rlen;
for (let i = 0; i < limit; ++i) {
if (t[i + rlen] != 0) continue;
let eq = true, j = 0, k = i + j;
for (; j < rlen; ++j, ++k) {
if (t[k] != tr[j]) {
eq = false;
break;
}
}
if (eq) 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 = new Uint8Array(1024); // 保存字符串常量
let idxtable = new Uint16Array(128); // 从字符串常量索引出来的字符串列表
let cmdtable = new Uint32Array(4096); // 保存选项对应指令
const size = {"str": 0, "cmd": 0, idx: 0};
const addString = list => {
const ut_pushtr = tr => {
const off = size.str;
const len = off + tr.length + 1;
if (len > strtable.length) {
let nt = new Uint8Array(Math.min(strtable.length * 2, off + tr.length + 1024));
nt.set(strtable, 0);
nt.set(tr, off);
strtable = nt;
}
else {
strtable.set(tr, off);
}
size.str = len;
return off;
};
const ut_addtr = tr => {
const code = conv.encode(tr);
const off = findIndex(strtable, code, size.str);
return off < 0 ? ut_pushtr(code) : off;
};
return list.map(ut_addtr);
}
const addStrIndex = list => {
if (list.length == 0) return 0;
if (size.str > 0xffff) throw new RangeError(`string table is too big.`);
const ret = size.idx;
const len = ret + list.length;
if( len > idxtable.length )
{
let nt = new Uint16Array( Math.max( len+128, idxtable.length * 2 ) );
nt.set(idxtable, 0);
nt.set(list, ret);
idxtable = nt;
}
else {
idxtable.set( list, ret );
}
size.idx = len;
return ret * 2;
}
const addCommand = list => {
const ret = size.cmd;
const len = ret + list.length;
if (len > cmdtable.length) {
let n = new Uint32Array(Math.max(cmdtable.length * 8, len));
n.set(cmdtable, 0);
n.set(list, ret);
cmdtable = n;
}
else {
cmdtable.set(list, ret);
}
size.cmd = len;
return ret * 4;
}
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 = id == 0 ? addString( data ) : 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);
}
});
cmdtable = cmdtable.slice(0, size.cmd);
strtable = strtable.slice(0, size.str);
idxtable = idxtable.slice(0, size.idx);
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;