mirror of
https://github.com/skillbert/rsmv.git
synced 2025-12-24 05:57:49 -05:00
clientscript compiler with decent round-trip results
This commit is contained in:
2
generated/clientscript.d.ts
vendored
2
generated/clientscript.d.ts
vendored
@@ -19,6 +19,6 @@ export type clientscript = {
|
||||
opcodedata: {
|
||||
opcode:number,
|
||||
imm:number,
|
||||
imm_obj:number|string|[number,number]|null,
|
||||
imm_obj:number|string|[number,number]|{value:number,label:number}[]|null,
|
||||
}[],
|
||||
};
|
||||
|
||||
17
generated/dbrows.d.ts
vendored
Normal file
17
generated/dbrows.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
// GENERATED DO NOT EDIT
|
||||
// This source data is located at '..\src\opcodes\dbrows.jsonc'
|
||||
// run `npm run filetypes` to rebuild
|
||||
|
||||
export type dbrows = {
|
||||
unk01?: {
|
||||
cols: number,
|
||||
columndata: {
|
||||
id: number,
|
||||
columns: {
|
||||
type: number,
|
||||
value: (string|number)[],
|
||||
}[],
|
||||
}[],
|
||||
} | null
|
||||
table?: number | null
|
||||
};
|
||||
17
generated/dbtables.d.ts
vendored
Normal file
17
generated/dbtables.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
// GENERATED DO NOT EDIT
|
||||
// This source data is located at '..\src\opcodes\dbtables.jsonc'
|
||||
// run `npm run filetypes` to rebuild
|
||||
|
||||
export type dbtables = {
|
||||
unk01?: {
|
||||
cols: number,
|
||||
columndata: {
|
||||
id: number,
|
||||
columns: {
|
||||
type: number,
|
||||
unk: number | null,
|
||||
default: (string|number) | null,
|
||||
}[],
|
||||
}[],
|
||||
} | null
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { clientscriptdata } from "../../generated/clientscriptdata";
|
||||
import { CacheFileSource } from "../cache";
|
||||
import { parse } from "../opdecoder";
|
||||
import { ClientscriptObfuscation, OpcodeInfo, getArgType, getReturnType, prepareClientScript, typeToPrimitive } from "./callibrator";
|
||||
import { clientscriptParser } from "./codeparser";
|
||||
import { binaryOpIds, binaryOpSymbols, branchInstructions, branchInstructionsOrJump, dynamicOps, knownClientScriptOpNames, namedClientScriptOps, variableSources, StackDiff, StackInOut, StackList, StackTypeExt, ClientScriptOp, StackConst, StackType, StackConstants } from "./definitions";
|
||||
|
||||
/**
|
||||
@@ -15,8 +16,20 @@ import { binaryOpIds, binaryOpSymbols, branchInstructions, branchInstructionsOrJ
|
||||
*/
|
||||
//get script names from https://api.runewiki.org/hashes?rev=930
|
||||
|
||||
/**
|
||||
* known compiler differences
|
||||
* - in some situations bunny hop jumps in nested ifs are merged while the jagex compiler doesn't
|
||||
* - default return values for int can be -1 for some specialisations while this compiler doesn't know about those
|
||||
* - this ast tree automatically strips dead code so round trips won't be identical if there dead code
|
||||
* - when a script has no return values but the original code had an explicit return then this compiler won't output that
|
||||
*/
|
||||
|
||||
|
||||
function getSingleChild<T extends AstNode>(op: CodeBlockNode | null | undefined, type: { new(...args: any[]): T }) {
|
||||
if (!op || op.children.length != 1 || !(op.children[0] instanceof type)) { return null; }
|
||||
return op.children[0] as T;
|
||||
}
|
||||
|
||||
function codeIndent(amount: number, linenr = -1, hasquestionmark = false) {
|
||||
// linenr = -1;
|
||||
return (linenr == -1 ? "" : linenr + ":").padEnd(5 + amount * 4, " ") + (hasquestionmark ? "?? " : " ");
|
||||
@@ -27,6 +40,8 @@ function getOpcodeName(calli: ClientscriptObfuscation, op: ClientScriptOp) {
|
||||
return `int${op.imm}`;
|
||||
} else if (op.opcode == namedClientScriptOps.poplocalstring || op.opcode == namedClientScriptOps.pushlocalstring) {
|
||||
return `string${op.imm}`;
|
||||
} else if (op.opcode == namedClientScriptOps.poplocallong || op.opcode == namedClientScriptOps.pushlocallong) {
|
||||
return `long${op.imm}`;
|
||||
} else if (op.opcode == namedClientScriptOps.popvar || op.opcode == namedClientScriptOps.pushvar) {
|
||||
let varmeta = calli.getClientVarMeta(op.imm);
|
||||
if (varmeta) {
|
||||
@@ -44,7 +59,7 @@ function getOpcodeCallCode(calli: ClientscriptObfuscation, op: ClientScriptOp, c
|
||||
if (children.length == 2) {
|
||||
return `(${children[0].getCode(calli, indent)} ${binarysymbol} ${children[1].getCode(calli, indent)})`;
|
||||
} else {
|
||||
return `(${binarysymbol} ${children.map(q => q.getCode(calli, indent))})`;
|
||||
return `(${binarysymbol} ${children.map(q => q.getCode(calli, indent)).join(" ")})`;
|
||||
}
|
||||
}
|
||||
let metastr = "";
|
||||
@@ -202,11 +217,16 @@ export class CodeBlockNode extends AstNode {
|
||||
}
|
||||
getCode(calli: ClientscriptObfuscation, indent: number) {
|
||||
let code = "";
|
||||
if (this.parent) { code += `{\n`; indent++; }
|
||||
if (this.parent && !(this.parent instanceof ClientScriptFunction)) {
|
||||
code += `{\n`;
|
||||
indent++;
|
||||
}
|
||||
for (let child of this.children) {
|
||||
code += `${codeIndent(indent, child.originalindex)}${child.getCode(calli, indent)}\n`;
|
||||
}
|
||||
if (this.parent) { code += `${codeIndent(indent - 1)}}`; }
|
||||
if (this.parent && !(this.parent instanceof ClientScriptFunction)) {
|
||||
code += `${codeIndent(indent - 1)}}`;
|
||||
}
|
||||
return code;
|
||||
}
|
||||
getOpcodes(calli: ClientscriptObfuscation) {
|
||||
@@ -244,7 +264,7 @@ function retargetJumps(calli: ClientscriptObfuscation, code: ClientScriptOp[], f
|
||||
}
|
||||
}
|
||||
|
||||
export class BinaryConditionalOpStatement extends AstNode {
|
||||
export class BranchingStatement extends AstNode {
|
||||
op: ClientScriptOp;
|
||||
knownStackDiff = new StackInOut(new StackList(["int", "int"]), new StackList(["int"]));//TODO not correct, we also use this for longs
|
||||
constructor(opcodeinfo: ClientScriptOp, originalindex: number) {
|
||||
@@ -257,24 +277,24 @@ export class BinaryConditionalOpStatement extends AstNode {
|
||||
}
|
||||
|
||||
getOpcodes(calli: ClientscriptObfuscation) {
|
||||
if (this.children.length != 2) { throw new Error("unexpected"); }
|
||||
let left = this.children[0].getOpcodes(calli);
|
||||
let right = this.children[1].getOpcodes(calli);
|
||||
|
||||
let ops: ClientScriptOp[] = [];
|
||||
if (this.op.opcode == namedClientScriptOps.shorting_or) {
|
||||
//retarget true jumps to true outcome of combined statement
|
||||
retargetJumps(calli, left, 1, right.length + 1);
|
||||
//index 0 [false] will already point to start of right condition
|
||||
} else if (this.op.opcode == namedClientScriptOps.shorting_and) {
|
||||
//retarget the false jumps to one past end [false] of combined statement
|
||||
retargetJumps(calli, left, 0, right.length);
|
||||
//retarget true jumps to start of right statement
|
||||
retargetJumps(calli, left, 1, 0);
|
||||
} else {
|
||||
ops.push({ opcode: this.op.opcode, imm: 1, imm_obj: null });
|
||||
if (this.op.opcode == namedClientScriptOps.shorting_or || this.op.opcode == namedClientScriptOps.shorting_and) {
|
||||
if (this.children.length != 2) { throw new Error("unexpected"); }
|
||||
let left = this.children[0].getOpcodes(calli);
|
||||
let right = this.children[1].getOpcodes(calli);
|
||||
if (this.op.opcode == namedClientScriptOps.shorting_or) {
|
||||
//retarget true jumps to true outcome of combined statement
|
||||
retargetJumps(calli, left, 1, right.length + 1);
|
||||
//index 0 [false] will already point to start of right condition
|
||||
} else {
|
||||
//retarget the false jumps to one past end [false] of combined statement
|
||||
retargetJumps(calli, left, 0, right.length);
|
||||
//retarget true jumps to start of right statement
|
||||
retargetJumps(calli, left, 1, 0);
|
||||
}
|
||||
return [...left, ...right];
|
||||
}
|
||||
return [...left, ...right, ...ops];
|
||||
let op: ClientScriptOp = { opcode: this.op.opcode, imm: 1, imm_obj: null };
|
||||
return this.children.flatMap(q => q.getOpcodes(calli)).concat(op);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +365,8 @@ export class SwitchStatementNode extends AstNode {
|
||||
}
|
||||
|
||||
let defaultblock: CodeBlockNode | null = nodes.find(q => q.originalindex == switchop.originalindex + 1) ?? null;
|
||||
if (defaultblock && defaultblock.children.length == 1 && defaultblock.children[0] instanceof RawOpcodeNode && defaultblock.children[0].opinfo.id == namedClientScriptOps.jump) {
|
||||
let defaultblockjump = getSingleChild(defaultblock, RawOpcodeNode);
|
||||
if (defaultblock && defaultblockjump && defaultblockjump.opinfo.id == namedClientScriptOps.jump) {
|
||||
if (defaultblock.possibleSuccessors.length != 1) { throw new Error("jump successor branch expected"); }
|
||||
defaultblock = defaultblock.possibleSuccessors[0];
|
||||
if (defaultblock.originalindex == endindex) {
|
||||
@@ -374,7 +395,6 @@ export class SwitchStatementNode extends AstNode {
|
||||
res += `${codeIndent(indent + 1)}default:`;
|
||||
res += this.defaultbranch.getCode(calli, indent + 1);
|
||||
}
|
||||
res += `\n`;
|
||||
res += `${codeIndent(indent)}}`;
|
||||
return res;
|
||||
}
|
||||
@@ -382,34 +402,43 @@ export class SwitchStatementNode extends AstNode {
|
||||
let body: ClientScriptOp[] = [];
|
||||
if (this.valueop) { body.push(...this.valueop.getOpcodes(calli)); }
|
||||
let jump = calli.getNamedOp(namedClientScriptOps.jump);
|
||||
let switchop = calli.getNamedOp(namedClientScriptOps.switch);
|
||||
body.push({ opcode: switchop.id, imm: -1, imm_obj: null });//TODO switch map id
|
||||
|
||||
let switchopinfo = calli.getNamedOp(namedClientScriptOps.switch);
|
||||
let switchop: ClientScriptOp = { opcode: switchopinfo.id, imm: -1, imm_obj: [] };
|
||||
let defaultjmp: ClientScriptOp = { opcode: jump.id, imm: -1, imm_obj: null };
|
||||
body.push(switchop);//TODO switch map id
|
||||
let jumpstart = body.length;
|
||||
body.push(defaultjmp);
|
||||
|
||||
//body.push switch
|
||||
let endops: ClientScriptOp[] = [];
|
||||
|
||||
let jumptable: { value: number, label: number }[] = [];
|
||||
let lastblock: CodeBlockNode | null = null;
|
||||
let lastblockindex = 0;
|
||||
for (let i = 0; i < this.branches.length; i++) {
|
||||
let branch = this.branches[i];
|
||||
//TODO write jump table
|
||||
if (branch.block == lastblock) { continue; }
|
||||
lastblock = branch.block;
|
||||
lastblockindex = body.length;
|
||||
body.push(...branch.block.getOpcodes(calli));
|
||||
|
||||
//no jump at last branch
|
||||
if (this.defaultbranch || i != this.branches.length - 1) {
|
||||
//add a jump so the previous branch skips to end (and last branch doesn't)
|
||||
if (branch.block == lastblock) {
|
||||
jumptable.push({ value: branch.value, label: lastblockindex });
|
||||
continue;
|
||||
}
|
||||
if (lastblock) {
|
||||
let jmp: ClientScriptOp = { opcode: jump.id, imm: -1, imm_obj: null };
|
||||
body.push(jmp);
|
||||
endops.push(jmp);
|
||||
}
|
||||
lastblock = branch.block;
|
||||
lastblockindex = body.length - jumpstart;
|
||||
jumptable.push({ value: branch.value, label: lastblockindex });
|
||||
body.push(...branch.block.getOpcodes(calli));
|
||||
}
|
||||
|
||||
if (this.defaultbranch) {
|
||||
if (lastblock) {
|
||||
let jmp: ClientScriptOp = { opcode: jump.id, imm: -1, imm_obj: null };
|
||||
body.push(jmp);
|
||||
endops.push(jmp);
|
||||
}
|
||||
|
||||
defaultjmp.imm = body.length - body.indexOf(defaultjmp) - 1;
|
||||
body.push(...this.defaultbranch.getOpcodes(calli));
|
||||
} else {
|
||||
@@ -422,6 +451,8 @@ export class SwitchStatementNode extends AstNode {
|
||||
op.imm = body.length - index - 1;
|
||||
}
|
||||
|
||||
switchop.imm_obj = jumptable;
|
||||
|
||||
return body;
|
||||
}
|
||||
}
|
||||
@@ -467,7 +498,8 @@ export class IfStatementNode extends AstNode {
|
||||
if (this.falsebranch) {
|
||||
res += `else`;
|
||||
//skip brackets for else if construct
|
||||
if (this.falsebranch instanceof CodeBlockNode && this.falsebranch.children.length == 1 && this.falsebranch.children[0] instanceof IfStatementNode) {
|
||||
let subif = getSingleChild(this.falsebranch, IfStatementNode);
|
||||
if (subif) {
|
||||
res += " " + this.falsebranch.children[0].getCode(calli, indent);
|
||||
} else {
|
||||
res += this.falsebranch.getCode(calli, indent);
|
||||
@@ -481,7 +513,8 @@ export class IfStatementNode extends AstNode {
|
||||
let falsebranch: ClientScriptOp[] = [];
|
||||
if (this.falsebranch) {
|
||||
falsebranch = this.falsebranch.getOpcodes(calli);
|
||||
retargetJumps(calli, truebranch, 0, falsebranch.length)
|
||||
truebranch.push({ opcode: calli.getNamedOp(namedClientScriptOps.jump).id, imm: falsebranch.length, imm_obj: null });
|
||||
// retargetJumps(calli, truebranch, 0, falsebranch.length)
|
||||
}
|
||||
//TODO rerouting true jumps past 2 in order to switch them with false at 1, this is stupid
|
||||
retargetJumps(calli, cond, 0, truebranch.length == 1 ? 2 : truebranch.length);
|
||||
@@ -530,7 +563,7 @@ export class RawOpcodeNode extends AstNode {
|
||||
if (typeof this.op.imm_obj == "string") { return `"${this.op.imm_obj.replace(/(["\\])/g, "\\$1")}"`; }
|
||||
else { return "" + this.op.imm_obj; }
|
||||
}
|
||||
if (this.op.opcode == namedClientScriptOps.pushlocalint || this.op.opcode == namedClientScriptOps.pushlocalstring || this.op.opcode == namedClientScriptOps.pushvar) {
|
||||
if (this.op.opcode == namedClientScriptOps.pushlocalint || this.op.opcode == namedClientScriptOps.poplocallong || this.op.opcode == namedClientScriptOps.pushlocalstring || this.op.opcode == namedClientScriptOps.pushvar) {
|
||||
return getOpcodeName(calli, this.op);
|
||||
}
|
||||
if (this.op.opcode == namedClientScriptOps.joinstring) {
|
||||
@@ -694,7 +727,7 @@ export function translateAst(ast: CodeBlockNode) {
|
||||
//merge variable assign nodes
|
||||
let currentassignnode: VarAssignNode | null = null;
|
||||
for (let node = cursor.goToStart(); node; node = cursor.next()) {
|
||||
let isassign = node instanceof RawOpcodeNode && (node.op.opcode == namedClientScriptOps.poplocalint || node.op.opcode == namedClientScriptOps.poplocalstring || node.op.opcode == namedClientScriptOps.popvar)
|
||||
let isassign = node instanceof RawOpcodeNode && (node.op.opcode == namedClientScriptOps.poplocalint || node.op.opcode == namedClientScriptOps.poplocallong || node.op.opcode == namedClientScriptOps.poplocalstring || node.op.opcode == namedClientScriptOps.popvar)
|
||||
if (isassign) {
|
||||
if (currentassignnode && currentassignnode.parent != node.parent) {
|
||||
throw new Error("ast is expected to be flat at this stage");
|
||||
@@ -766,6 +799,16 @@ function fixControlFlow(ast: AstNode, scriptjson: clientscript) {
|
||||
let cursor = new RewriteCursor(ast);
|
||||
//find if statements
|
||||
for (let node = cursor.goToStart(); node; node = cursor.next()) {
|
||||
if (node instanceof IfStatementNode) {
|
||||
//detect an or statement that wasn't caught before (a bit late, there should be a better way to do this)
|
||||
let subif = getSingleChild(node.falsebranch, IfStatementNode);
|
||||
if (subif && subif.truebranch == node.truebranch) {
|
||||
let combined = new BranchingStatement({ opcode: namedClientScriptOps.shorting_or, imm: 0, imm_obj: null }, node.statement.originalindex);
|
||||
combined.push(node.statement);
|
||||
combined.push(subif.statement);
|
||||
node.setBranches(combined, node.truebranch, subif.falsebranch, subif.ifEndIndex);
|
||||
}
|
||||
}
|
||||
if (node instanceof RawOpcodeNode && branchInstructions.includes(node.opinfo.id)) {
|
||||
let parent = node.parent;
|
||||
if (!(parent instanceof CodeBlockNode) || parent.possibleSuccessors.length != 2) { throw new Error("if op parent is not compatible"); }
|
||||
@@ -774,7 +817,8 @@ function fixControlFlow(ast: AstNode, scriptjson: clientscript) {
|
||||
|
||||
let trueblock = parent.possibleSuccessors[1];
|
||||
let falseblock: CodeBlockNode | null = parent.possibleSuccessors[0];
|
||||
if (falseblock.children.length == 1 && falseblock.children[0] instanceof RawOpcodeNode && falseblock.children[0].opinfo.id == namedClientScriptOps.jump) {
|
||||
let falseblockjump = getSingleChild(falseblock, RawOpcodeNode);
|
||||
if (falseblockjump && falseblockjump.opinfo.id == namedClientScriptOps.jump) {
|
||||
if (falseblock.possibleSuccessors.length != 1) { throw new Error("jump successor branch expected"); }
|
||||
falseblock = falseblock.possibleSuccessors[0];
|
||||
if (falseblock == parent.branchEndNode) {
|
||||
@@ -802,22 +846,22 @@ function fixControlFlow(ast: AstNode, scriptjson: clientscript) {
|
||||
falseblock = newblock;
|
||||
}
|
||||
|
||||
let condnode = new BinaryConditionalOpStatement(node.op, node.originalindex);
|
||||
let condnode = new BranchingStatement(node.op, node.originalindex);
|
||||
condnode.pushList(node.children);
|
||||
|
||||
let grandparent = parent?.parent;
|
||||
if (parent instanceof CodeBlockNode && grandparent instanceof IfStatementNode && grandparent.ifEndIndex == parent.branchEndNode.originalindex) {
|
||||
let isor = grandparent.truebranch == trueblock && grandparent.falsebranch == parent;
|
||||
let isand = grandparent.falsebranch == falseblock && grandparent.truebranch == parent;
|
||||
let isand = condnode.children.length <= 2 && grandparent.falsebranch == falseblock && grandparent.truebranch == parent;
|
||||
if (isor || isand) {
|
||||
if (parent.children.length != 1) {
|
||||
parent.remove(node);
|
||||
condnode.pushList(parent.children);
|
||||
//TODO make some sort of in-line codeblock node for this
|
||||
// console.log("merging if statements while 2nd if wasn't parsed completely, stack will be invalid");
|
||||
parent.remove(node);
|
||||
//TODO make some sort of in-line codeblock node for this
|
||||
// console.log("merging if statements while 2nd if wasn't parsed completely, stack will be invalid");
|
||||
while (parent.children.length != 0) {
|
||||
condnode.unshift(parent.children[0]);
|
||||
}
|
||||
let fakeop: ClientScriptOp = { opcode: isor ? namedClientScriptOps.shorting_or : namedClientScriptOps.shorting_and, imm: 0, imm_obj: null };
|
||||
let combinedcond = new BinaryConditionalOpStatement(fakeop, grandparent.originalindex);
|
||||
let combinedcond = new BranchingStatement(fakeop, grandparent.originalindex);
|
||||
combinedcond.push(grandparent.statement);
|
||||
combinedcond.push(condnode);
|
||||
if (isor) {
|
||||
@@ -855,6 +899,11 @@ function fixControlFlow(ast: AstNode, scriptjson: clientscript) {
|
||||
if (codeblock.originalindex != target) { continue; }
|
||||
if (codeblock.children.at(-1) != ifnode) { throw new Error("unexpected"); }
|
||||
|
||||
//TODO this is silly, there might be more instructions in the enclosing block, make sure these aren't lost
|
||||
//mostly seems to affect expansions of var++ and ++var constructs which are currently not supported
|
||||
for (let i = codeblock.children.length - 2; i >= 0; i--) {
|
||||
ifnode.statement.unshift(codeblock.children[i]);
|
||||
}
|
||||
let originalparent = codeblock.parent;
|
||||
let loopstatement = WhileLoopStatementNode.fromIfStatement(codeblock.originalindex, ifnode);
|
||||
originalparent.replaceChild(codeblock, loopstatement);
|
||||
@@ -1147,6 +1196,46 @@ export function parseClientScriptIm(calli: ClientscriptObfuscation, script: clie
|
||||
}
|
||||
globalThis.parseClientScriptIm = parseClientScriptIm;
|
||||
|
||||
export function astToImJson(calli: ClientscriptObfuscation, func: ClientScriptFunction) {
|
||||
let opdata = func.getOpcodes(calli);
|
||||
let allargs = func.argtype.getStackdiff();
|
||||
let script: clientscript = {
|
||||
byte0: 0,
|
||||
switchsize: -1,
|
||||
switches: [],
|
||||
longargcount: allargs.long,
|
||||
stringargcount: allargs.string,
|
||||
intargcount: allargs.int,
|
||||
locallongcount: allargs.long,
|
||||
localstringcount: allargs.string,
|
||||
localintcount: allargs.int,
|
||||
instructioncount: opdata.length,
|
||||
opcodedata: opdata,
|
||||
}
|
||||
for (let op of opdata) {
|
||||
if (op.opcode == namedClientScriptOps.poplocalint || op.opcode == namedClientScriptOps.pushlocalint) { script.localintcount = Math.max(script.localintcount, op.imm + 1); }
|
||||
if (op.opcode == namedClientScriptOps.poplocallong || op.opcode == namedClientScriptOps.pushlocallong) { script.locallongcount = Math.max(script.locallongcount, op.imm + 1); }
|
||||
if (op.opcode == namedClientScriptOps.poplocalstring || op.opcode == namedClientScriptOps.pushlocalstring) { script.localstringcount = Math.max(script.localstringcount, op.imm + 1); }
|
||||
|
||||
if (op.opcode == namedClientScriptOps.switch) {
|
||||
op.imm = script.switches.push(op.imm_obj as any) - 1;
|
||||
op.imm_obj = null;
|
||||
}
|
||||
}
|
||||
//1+foreach(2+sublen*(4+4))
|
||||
script.switchsize = 1 + script.switches.reduce((a, v) => a + 2 + v.length * (4 + 4), 0);
|
||||
return script;
|
||||
}
|
||||
|
||||
export async function compileClientScript(source: CacheFileSource, code: string) {
|
||||
let calli = await prepareClientScript(source);
|
||||
|
||||
let parseresult = clientscriptParser(calli).runparse(code);
|
||||
if (!parseresult.success) { throw new Error("failed to parse clientscript", { cause: parseresult.failedOn }); }
|
||||
if (parseresult.remaining != "") { throw new Error("failed to parse clientscript, left over: " + parseresult.remaining.slice(0, 100)); }
|
||||
return astToImJson(calli, parseresult.result);
|
||||
}
|
||||
|
||||
export async function renderClientScript(source: CacheFileSource, buf: Buffer, fileid: number) {
|
||||
let calli = await prepareClientScript(source);
|
||||
let script = parse.clientscript.read(buf, source);
|
||||
|
||||
@@ -516,6 +516,9 @@ export class ClientscriptObfuscation {
|
||||
} else if (op.type == "int") {
|
||||
state.buffer.writeInt32BE(v.imm, state.scan);
|
||||
state.scan += 4;
|
||||
} else if (op.type == "tribyte") {
|
||||
state.buffer.writeUIntBE(v.imm, state.scan, 3);
|
||||
state.scan += 3;
|
||||
} else if (op.type == "switch") {
|
||||
if (!("imm_obj" in v)) { throw new Error("imm_obj prop expected"); }
|
||||
state.buffer.writeUInt8(v.imm, state.scan);
|
||||
@@ -531,7 +534,7 @@ export class ClientscriptObfuscation {
|
||||
state.scan += 8;
|
||||
} else if (v.imm == 2) {
|
||||
if (typeof v.imm_obj != "string") { throw new Error("string expected"); }
|
||||
state.buffer.write(v.imm_obj, "latin1");
|
||||
state.buffer.write(v.imm_obj, state.scan, "latin1");
|
||||
state.scan += v.imm_obj.length;
|
||||
state.buffer.writeUint8(0, state.scan);
|
||||
state.scan++;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { has, hasMore, parse, optional, invert, isEnd } from "../libs/yieldparser";
|
||||
import { AstNode, BinaryConditionalOpStatement, CodeBlockNode, FunctionBindNode, IfStatementNode, RawOpcodeNode, VarAssignNode, WhileLoopStatementNode, SwitchStatementNode, ClientScriptFunction } from "./ast";
|
||||
import { AstNode, BranchingStatement, CodeBlockNode, FunctionBindNode, IfStatementNode, RawOpcodeNode, VarAssignNode, WhileLoopStatementNode, SwitchStatementNode, ClientScriptFunction, astToImJson } from "./ast";
|
||||
import { ClientscriptObfuscation } from "./callibrator";
|
||||
import { binaryOpIds, binaryOpSymbols, knownClientScriptOpNames, namedClientScriptOps, variableSources, StackDiff, StackInOut, StackList, StackTypeExt } from "./definitions";
|
||||
import prettyJson from "json-stringify-pretty-compact";
|
||||
|
||||
const whitespace = /^\s*/;
|
||||
function* whitespace() {
|
||||
while (true) {
|
||||
let match = yield [/^\/\/.*$/m, /^\/\*[\s\S]*\*\//, /^\s+/, ""];
|
||||
if (match === "") { break; }
|
||||
}
|
||||
}
|
||||
const newline = /^\s*?\n/;
|
||||
const unmatchable = /$./;
|
||||
const reserverd = "if,while,break,continue,else,switch,strcat,script".split(",");
|
||||
@@ -37,13 +42,14 @@ export function clientscriptParser(deob: ClientscriptObfuscation) {
|
||||
}
|
||||
|
||||
function* stackdiff() {
|
||||
let [match, int, long, string, vararg] = yield (/^\((\d+),(\d+),(\d+),(\d+)\)/);
|
||||
let [match, int, long, string, vararg] = yield (/^\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
|
||||
return new StackDiff(+int, +long, +string, +vararg);
|
||||
}
|
||||
|
||||
function* stacklist() {
|
||||
let items: StackTypeExt[] = [];
|
||||
while (items.length == 0 || (yield has(","))) {
|
||||
yield whitespace;
|
||||
let match = yield [stackdiff, "int", "long", "string", "vararg", ""];
|
||||
if (match == "") {
|
||||
if (items.length == 0) { break; }
|
||||
@@ -308,9 +314,9 @@ export function clientscriptParser(deob: ClientscriptObfuscation) {
|
||||
} else if (m[1] == "int") {
|
||||
let popintop = getopinfo(namedClientScriptOps.pushlocalint);
|
||||
return new RawOpcodeNode(-1, { opcode: popintop.id, imm: varid, imm_obj: null }, popintop);
|
||||
// } else if (m[1] == "long") {
|
||||
// let popintop=getopinfo(namedClientScriptOps.poplocalint);
|
||||
// return new RawOpcodeNode(-1, { opcode: poplongop.id, imm: varid, imm_obj: null }, poplongop);
|
||||
} else if (m[1] == "long") {
|
||||
let poplongop = getopinfo(namedClientScriptOps.pushlocallong);
|
||||
return new RawOpcodeNode(-1, { opcode: poplongop.id, imm: varid, imm_obj: null }, poplongop);
|
||||
} else if (m[1] == "string") {
|
||||
let popstringop = getopinfo(namedClientScriptOps.pushlocalstring);
|
||||
return new RawOpcodeNode(-1, { opcode: popstringop.id, imm: varid, imm_obj: null }, popstringop);
|
||||
@@ -336,6 +342,7 @@ export function clientscriptParser(deob: ClientscriptObfuscation) {
|
||||
let condition = yield valueStatement;
|
||||
yield whitespace;
|
||||
yield ")";
|
||||
yield whitespace;
|
||||
let code = yield codeBlock;
|
||||
return new WhileLoopStatementNode(-1, condition, code);
|
||||
}
|
||||
@@ -352,7 +359,7 @@ export function clientscriptParser(deob: ClientscriptObfuscation) {
|
||||
if (!opid) { throw new Error("unexpected"); }
|
||||
let node: AstNode;
|
||||
if (binaryconditionals.includes(op)) {
|
||||
node = new BinaryConditionalOpStatement({ opcode: opid, imm: 0, imm_obj: null }, -1);
|
||||
node = new BranchingStatement({ opcode: opid, imm: 0, imm_obj: null }, -1);
|
||||
} else {
|
||||
node = new RawOpcodeNode(-1, { opcode: opid, imm: 0, imm_obj: null }, deob.getNamedOp(opid));
|
||||
}
|
||||
@@ -370,7 +377,14 @@ export function clientscriptParser(deob: ClientscriptObfuscation) {
|
||||
}
|
||||
let statements = yield statementlist;
|
||||
yield ")";
|
||||
let node = new BinaryConditionalOpStatement(op, -1);
|
||||
let opid = binaryOpIds.get(op);
|
||||
if (!opid) { throw new Error("unexpected"); }
|
||||
let node: AstNode;
|
||||
if (binaryconditionals.includes(op)) {
|
||||
node = new BranchingStatement({ opcode: opid, imm: 0, imm_obj: null }, -1);
|
||||
} else {
|
||||
node = new RawOpcodeNode(-1, { opcode: opid, imm: 0, imm_obj: null }, deob.getNamedOp(opid));
|
||||
}
|
||||
node.pushList(statements);
|
||||
return node;
|
||||
}
|
||||
@@ -432,23 +446,32 @@ export function clientscriptParser(deob: ClientscriptObfuscation) {
|
||||
|
||||
//TODO remove
|
||||
globalThis.testy = async () => {
|
||||
const fs = require("fs");
|
||||
const fs = require("fs") as typeof import("fs");
|
||||
let codefs = await globalThis.cli("extract -m clientscripttext -i 0-1999");
|
||||
let codefiles = [...codefs.extract.filesMap.values()].map(q => q.data.replace(/^\d+:/gm, "")); 1;
|
||||
let codefiles = [...codefs.extract.filesMap.values()].map(q => q.data.replace(/^\d+:/gm, m => " ".repeat(m.length))); 1;
|
||||
let jsonfs = await globalThis.cli("extract -m clientscript -i 0-1999");
|
||||
jsonfs.extract.filesMap.delete(".schema-clientscript.json");
|
||||
let jsonfiles = [...jsonfs.extract.filesMap.values()];
|
||||
let subtest = (index: number) => {
|
||||
let parseresult = clientscriptParser(globalThis.deob).runparse(codefiles[index]);
|
||||
const deob = globalThis.deob as ClientscriptObfuscation;
|
||||
let parseresult = clientscriptParser(deob).runparse(codefiles[index]);
|
||||
if (!parseresult.success) { return parseresult; }
|
||||
let roundtripped = prettyJson(parseresult.result.getOpcodes(globalThis.deob));
|
||||
let jsondata = JSON.parse(jsonfiles[index].data).opcodedata;
|
||||
jsondata.forEach(q => { delete q.opname });
|
||||
let original = prettyJson(jsondata);
|
||||
let roundtripped = astToImJson(deob, parseresult.result);
|
||||
let jsondata = JSON.parse(jsonfiles[index].data);
|
||||
delete jsondata.$schema;
|
||||
jsondata.opcodedata.forEach(q => { delete q.opname });
|
||||
let original = prettyJson(jsondata.opcodedata);
|
||||
|
||||
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/json1.json", original);
|
||||
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/json2.json", roundtripped);
|
||||
return { roundtripped, original, exact: original == roundtripped };
|
||||
let rawinput = prettyJson(jsondata);
|
||||
let rawroundtrip = prettyJson(roundtripped);
|
||||
|
||||
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/raw1.json", rawinput);
|
||||
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/raw2.json", rawroundtrip);
|
||||
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/json1.json", prettyJson(jsondata.opcodedata));
|
||||
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/json2.json", prettyJson(roundtripped.opcodedata));
|
||||
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/js1.js", codefiles[index]);
|
||||
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/js2.js", parseresult.result.getCode(deob, 0));
|
||||
return { roundtripped, original, exact: rawinput == rawroundtrip };
|
||||
}
|
||||
return { subtest, codefiles, codefs, jsonfs, jsonfiles };
|
||||
}
|
||||
@@ -23,6 +23,8 @@ export const namedClientScriptOps = {
|
||||
poplocalint: 34,
|
||||
pushlocalstring: 35,
|
||||
poplocalstring: 36,
|
||||
pushlocallong: 9102,//TODO find this one
|
||||
poplocallong: 9103,//TODO find this one
|
||||
|
||||
//variable number of args
|
||||
joinstring: 37,
|
||||
@@ -157,7 +159,7 @@ export type ImmediateType = "byte" | "int" | "tribyte" | "switch" | "long" | "st
|
||||
export type ClientScriptOp = {
|
||||
opcode: number,
|
||||
imm: number,
|
||||
imm_obj: string | number | [number, number] | null,
|
||||
imm_obj: string | number | [number, number] | { value: number, label: number }[] | null,
|
||||
opname?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@ export type DecodeState = {
|
||||
}
|
||||
|
||||
export type EncodeState = {
|
||||
scan: number
|
||||
scan: number,
|
||||
endoffset: number,
|
||||
buffer: Buffer,
|
||||
args: Record<string, unknown>
|
||||
}
|
||||
@@ -311,11 +312,13 @@ function structParser(args: unknown[], parent: ChunkParentCallback, typedef: Typ
|
||||
if (typeof value != "object" || !value) { throw new Error("object expected"); }
|
||||
for (let key of keys) {
|
||||
let propvalue = value[key as string];
|
||||
let refarray = refs[key];
|
||||
if (refarray) {
|
||||
propvalue = propvalue ?? 0;
|
||||
for (let ref of refarray) {
|
||||
propvalue = ref.resolve(value, propvalue);
|
||||
if (propvalue == null) {
|
||||
let refarray = refs[key];
|
||||
propvalue ??= 0;
|
||||
if (refarray) {
|
||||
for (let ref of refarray) {
|
||||
propvalue = ref.resolve(value, propvalue);
|
||||
}
|
||||
}
|
||||
}
|
||||
let prop = props[key];
|
||||
@@ -1180,7 +1183,12 @@ const hardcodes: Record<string, (args: unknown[], parent: ChunkParentCallback, t
|
||||
return res;
|
||||
},
|
||||
write(state, v) {
|
||||
throw new Error("not implemented");
|
||||
let oldscan = state.scan;
|
||||
subtype.write(state, v);
|
||||
let len = state.scan - oldscan;
|
||||
state.buffer.copyWithin(state.endoffset - len, oldscan, state.scan);
|
||||
state.scan = oldscan;
|
||||
state.endoffset -= len;
|
||||
},
|
||||
getTypescriptType(indent) {
|
||||
return subtype.getTypescriptType(indent);
|
||||
@@ -1318,7 +1326,7 @@ const hardcodes: Record<string, (args: unknown[], parent: ChunkParentCallback, t
|
||||
return `{\n`
|
||||
+ `${newindent}opcode:number,\n`
|
||||
+ `${newindent}imm:number,\n`
|
||||
+ `${newindent}imm_obj:number|string|[number,number]|null,\n`
|
||||
+ `${newindent}imm_obj:number|string|[number,number]|{value:number,label:number}[]|null,\n`
|
||||
+ `${indent}}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
["struct",
|
||||
["byte0",["match","buildnr",{">458":"ubyte","other":0}]],
|
||||
["switchsize",["match","buildnr",{">=495":["footer",2,"ushort"],"other":0}]],
|
||||
// ["byte0",["match","buildnr",{">458":"ubyte","other":0}]],
|
||||
["byte0","ubyte"],
|
||||
// ["switchsize",["match","buildnr",{">=495":["footer",2,"ushort"],"other":0}]],
|
||||
["switchsize",["footer",2,"ushort"]],
|
||||
["switches",["footer",["ref","switchsize"],
|
||||
["array",["match","buildnr",{">=495":"ubyte","other":0}],["array","ushort",["struct",
|
||||
// ["array",["match","buildnr",{">=495":"ubyte","other":0}],["array","ushort",["struct",
|
||||
["array","ubyte",["array","ushort",["struct",
|
||||
["value","int"],
|
||||
["label","uint"]
|
||||
]]]
|
||||
]],
|
||||
["longargcount",["match","buildnr",{">641":["footer",2,"ushort"],"other":0}]],
|
||||
// ["longargcount",["match","buildnr",{">641":["footer",2,"ushort"],"other":0}]],
|
||||
["longargcount",["footer",2,"ushort"]],
|
||||
["stringargcount",["footer",2,"ushort"]],
|
||||
["intargcount",["footer",2,"ushort"]],
|
||||
["locallongcount",["match","buildnr",{">641":["footer",2,"ushort"],"other":0}]],
|
||||
// ["locallongcount",["match","buildnr",{">641":["footer",2,"ushort"],"other":0}]],
|
||||
["locallongcount",["footer",2,"ushort"]],
|
||||
["localstringcount",["footer",2,"ushort"]],
|
||||
["localintcount",["footer",2,"ushort"]],
|
||||
["instructioncount",["footer",4,"uint"]],
|
||||
|
||||
@@ -62,16 +62,21 @@ export class FileParser<T> {
|
||||
return this.readInternal(state) as T;
|
||||
}
|
||||
|
||||
write(obj: T) {
|
||||
write(obj: T, args?: Record<string, any>) {
|
||||
let state: opcode_reader.EncodeState = {
|
||||
buffer: scratchbuf,
|
||||
scan: 0,
|
||||
endoffset: scratchbuf.byteLength,
|
||||
args: {
|
||||
clientVersion: 1000//TODO
|
||||
clientVersion: 1000,//TODO
|
||||
...args
|
||||
}
|
||||
};
|
||||
this.parser.write(state, obj);
|
||||
if (state.scan > scratchbuf.byteLength) { throw new Error("tried to write file larger than scratchbuffer size"); }
|
||||
if (state.scan > state.endoffset) { throw new Error("tried to write file larger than scratchbuffer size"); }
|
||||
//append footer data to end of normal data
|
||||
state.buffer.copyWithin(state.scan, state.endoffset, scratchbuf.byteLength);
|
||||
state.scan += scratchbuf.byteLength - state.endoffset;
|
||||
//do the weird prototype slice since we need a copy, not a ref
|
||||
let r: Buffer = Uint8Array.prototype.slice.call(scratchbuf, 0, state.scan);
|
||||
//clear it for next use
|
||||
|
||||
@@ -119,7 +119,7 @@ export async function extractCacheFiles(output: ScriptOutput, outdir: ScriptFS,
|
||||
}
|
||||
let logicalid = mode.fileToLogical(source, fileid.index.major, fileid.index.minor, arch[fileid.subindex].fileid);
|
||||
let newfile = await outdir.readFileBuffer(`${args.mode}-${logicalid.join("_")}.${mode.ext}`);
|
||||
arch[fileid.subindex].buffer = mode.write(newfile);
|
||||
arch[fileid.subindex].buffer = await mode.write(newfile, logicalid, source);
|
||||
}
|
||||
await archedited();
|
||||
}
|
||||
@@ -164,7 +164,7 @@ export async function writeCacheFiles(output: ScriptOutput, source: CacheFileSou
|
||||
let arch = getarch(archid.major, archid.minor, mode);
|
||||
|
||||
let raw = await diffdir.readFileBuffer(file);
|
||||
let buf = mode.write(raw);
|
||||
let buf = await mode.write(raw, logicalid, source);
|
||||
arch.files.push({ subid: archid.subid, file: buf });
|
||||
|
||||
continue;
|
||||
|
||||
@@ -16,7 +16,7 @@ import { legacyGroups, legacyMajors } from "../cache/legacycache";
|
||||
import { classicGroups } from "../cache/classicloader";
|
||||
import { renderCutscene } from "./rendercutscene";
|
||||
import { prepareClientScript } from "../clientscript/callibrator";
|
||||
import { renderClientScript } from "../clientscript/ast";
|
||||
import { compileClientScript, renderClientScript } from "../clientscript/ast";
|
||||
import { renderRsInterface } from "./renderrsinterface";
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ export type DecodeMode<T = Buffer | string> = {
|
||||
parser?: FileParser<any>,
|
||||
read(buf: Buffer, fileid: LogicalIndex, source: CacheFileSource): T | Promise<T>,
|
||||
prepareDump(output: ScriptFS, source: CacheFileSource): Promise<void> | void,
|
||||
write(file: Buffer): Buffer,
|
||||
write(file: Buffer, fileid: LogicalIndex, source: CacheFileSource): Buffer | Promise<Buffer>,
|
||||
combineSubs(files: T[]): T
|
||||
} & DecodeLookup;
|
||||
|
||||
@@ -445,12 +445,18 @@ const decodeInterface2: DecodeModeFactory = () => {
|
||||
|
||||
const decodeClientScriptText: DecodeModeFactory = () => {
|
||||
return {
|
||||
ext: "txt",
|
||||
ext: "js",
|
||||
...noArchiveIndex(cacheMajors.clientscript),
|
||||
...throwOnNonSimple,
|
||||
async prepareDump(out, source) { await prepareClientScript(source) },
|
||||
read(buf, fileid, source) {
|
||||
return renderClientScript(source, buf, fileid[0]);
|
||||
},
|
||||
async write(file, fileid, source) {
|
||||
let obj = await compileClientScript(source, file.toString("utf8"));
|
||||
let res = parse.clientscript.write(obj, source.getDecodeArgs());
|
||||
// throw new Error("exit dryrun");
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,7 +640,7 @@ export function FileDisplay(p: { file: UIScriptFile }) {
|
||||
el = <iframe srcDoc={filedata} sandbox="allow-scripts" style={{ width: "95%", height: "95%" }} />;
|
||||
} else {
|
||||
//TODO make this not depend on wether file is Buffer or string
|
||||
//string types so far: txt, json, batch.json
|
||||
//string types so far: js, txt, json, batch.json
|
||||
el = <SimpleTextViewer file={filedata} />;
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user