├── common ├── constants.js ├── strings │ ├── constants.js │ ├── stringEncryption.js │ ├── escapeChars.js │ └── StringStream.js ├── util │ ├── assert.js │ ├── clamp.js │ ├── arrays.js │ ├── enumerate.js │ ├── has.js │ └── deepFreeze.js ├── keywords.js ├── numbers │ └── constants.js ├── mapOpsToValue.js ├── operators.js ├── ArrayMap.js └── opcodes.js ├── main.js ├── decompiler ├── DSODisassembler │ ├── DSOToken │ │ ├── DSOToken.js │ │ ├── DSOValueToken.js │ │ ├── DSOReturnToken.js │ │ ├── DSOOpcodeToken.js │ │ ├── DSOOpValueToken.js │ │ └── DSOFuncDeclToken.js │ ├── jumpTypeToMarker.js │ ├── handleFuncDecl.js │ ├── handleMarkers.js │ ├── scanNext.js │ ├── handleMisc.js │ ├── handleSingle.js │ └── DSODisassembler.js ├── DSOParser │ ├── DSONode │ │ ├── precedence.js │ │ ├── DSOReturnNode.js │ │ ├── DSOConstantNode.js │ │ ├── DSOUnaryNode.js │ │ ├── DSOVariableNode.js │ │ ├── DSOCommaCatNode.js │ │ ├── DSOSlotNode.js │ │ ├── DSOPackageNode.js │ │ ├── DSOFuncDeclNode.js │ │ ├── DSOJumpNode.js │ │ ├── DSOAssignNode.js │ │ ├── DSOStrConcatNode.js │ │ ├── DSOFuncCallNode.js │ │ ├── DSOBinaryNode.js │ │ ├── DSOLoopNode.js │ │ ├── DSONode.js │ │ ├── DSOObjectDeclNode.js │ │ └── DSOConditionalNode.js │ ├── parseBinary.js │ ├── parseString.js │ ├── parseAssign.js │ ├── parsePushFrame.js │ ├── parseMisc.js │ ├── parseNext.js │ ├── parseBlock.js │ └── DSOParser.js ├── errors.js ├── opcodes │ ├── associativity.js │ ├── getOpcodeType.js │ ├── operatorToStr.js │ ├── precedence.js │ ├── getOpSize.js │ ├── types.js │ └── subtypes.js ├── DSOCodeGenerator │ ├── generateConstant.js │ ├── generateLoop.js │ ├── generateFuncCall.js │ ├── generateObjectDecl.js │ ├── generateFuncDecl.js │ ├── generateNary.js │ ├── generateConditional.js │ ├── generateAssign.js │ ├── generateConcat.js │ ├── generateStmt.js │ ├── generateExpr.js │ ├── DSOCodeGenerator.js │ └── generateCode.js ├── DSOControlBlock │ ├── DSOJumpInfo.js │ ├── loops.js │ ├── scan.js │ ├── analyzeJumps.js │ └── DSOControlBlock.js ├── DSOLoader │ ├── readString.js │ ├── getTableValue.js │ ├── readNumber.js │ └── DSOLoader.js └── decompiler.js ├── .npmignore ├── package.json ├── .gitignore ├── decomp_v20.mjs ├── webpack.config.js └── README.md /common/constants.js: -------------------------------------------------------------------------------- 1 | const DSO_VERSION = 190; 2 | 3 | 4 | export 5 | { 6 | DSO_VERSION, 7 | }; 8 | -------------------------------------------------------------------------------- /common/strings/constants.js: -------------------------------------------------------------------------------- 1 | const ENCRYPTION_KEY = 'cl3buotro'; 2 | 3 | 4 | export 5 | { 6 | ENCRYPTION_KEY, 7 | }; 8 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime.js'; 2 | 3 | import * as decompiler from './decompiler/decompiler.js'; 4 | 5 | 6 | export { decompiler }; 7 | -------------------------------------------------------------------------------- /decompiler/DSODisassembler/DSOToken/DSOToken.js: -------------------------------------------------------------------------------- 1 | class DSOToken 2 | { 3 | constructor ( type ) 4 | { 5 | this.type = type; 6 | } 7 | } 8 | 9 | 10 | export default DSOToken; 11 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/precedence.js: -------------------------------------------------------------------------------- 1 | const PREC_FUNC_CALL = 0; 2 | const PREC_CONCAT = 5; 3 | const PREC_TERNARY = 13; 4 | const PREC_ASSIGN = 14; 5 | 6 | 7 | export { PREC_FUNC_CALL, PREC_CONCAT, PREC_TERNARY, PREC_ASSIGN }; 8 | -------------------------------------------------------------------------------- /decompiler/DSODisassembler/DSOToken/DSOValueToken.js: -------------------------------------------------------------------------------- 1 | import DSOToken from '~/DSOToken/DSOToken.js'; 2 | 3 | 4 | class DSOValueToken extends DSOToken 5 | { 6 | constructor ( type, value ) 7 | { 8 | super (type); 9 | this.value = value; 10 | } 11 | } 12 | 13 | 14 | export default DSOValueToken; 15 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOReturnNode.js: -------------------------------------------------------------------------------- 1 | import { DSOStmtNode } from '~/DSONode/DSONode.js'; 2 | 3 | 4 | class DSOReturnNode extends DSOStmtNode 5 | { 6 | constructor ( value = null ) 7 | { 8 | super (); 9 | this.value = value; 10 | } 11 | } 12 | 13 | 14 | export default DSOReturnNode; 15 | -------------------------------------------------------------------------------- /common/util/assert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {boolean} test 3 | * @param {string} message 4 | * 5 | * @throws {Error} If `test` is falsy. 6 | */ 7 | const assert = ( test, message ) => 8 | { 9 | if ( !test ) 10 | { 11 | throw new Error (`Assertion Error: ${message}`); 12 | } 13 | }; 14 | 15 | 16 | export default assert; 17 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOConstantNode.js: -------------------------------------------------------------------------------- 1 | import { DSONode } from '~/DSONode/DSONode.js'; 2 | 3 | 4 | class DSOConstantNode extends DSONode 5 | { 6 | constructor ( op, value ) 7 | { 8 | super (); 9 | 10 | this.op = op; 11 | this.value = value; 12 | } 13 | } 14 | 15 | 16 | export default DSOConstantNode; 17 | -------------------------------------------------------------------------------- /common/util/clamp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {number} number 3 | * @param {number} min 4 | * @param {number} max 5 | * 6 | * @returns {number} Number clamped to range [min, max] 7 | */ 8 | const clamp = ( number, min, max ) => 9 | { 10 | return Math.min (Math.max (number, min), max); 11 | }; 12 | 13 | 14 | export default clamp; 15 | -------------------------------------------------------------------------------- /decompiler/DSODisassembler/DSOToken/DSOReturnToken.js: -------------------------------------------------------------------------------- 1 | import DSOToken from '~/DSOToken/DSOToken.js'; 2 | 3 | 4 | class DSOReturnToken extends DSOToken 5 | { 6 | constructor ( returnsValue ) 7 | { 8 | super ('OpcodeReturn'); 9 | this.returnsValue = returnsValue; 10 | } 11 | } 12 | 13 | 14 | export default DSOReturnToken; 15 | -------------------------------------------------------------------------------- /common/keywords.js: -------------------------------------------------------------------------------- 1 | const keywords = new Set ( 2 | [ 3 | 'if', 4 | 'else', 5 | 'package', 6 | 'function', 7 | 'while', 8 | 'for', 9 | 'datablock', 10 | 'new', 11 | 'break', 12 | 'continue', 13 | 'return', 14 | ]); 15 | 16 | 17 | const isKeyword = str => 18 | { 19 | return keywords.has (str); 20 | }; 21 | 22 | 23 | export { isKeyword }; 24 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOUnaryNode.js: -------------------------------------------------------------------------------- 1 | import { DSOExprNode } from '~/DSONode/DSONode.js'; 2 | 3 | 4 | class DSOUnaryNode extends DSOExprNode 5 | { 6 | constructor ( op, expr ) 7 | { 8 | super (); 9 | 10 | this.op = op; 11 | this.expr = expr; 12 | 13 | this.inParens = false; 14 | } 15 | } 16 | 17 | 18 | export default DSOUnaryNode; 19 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOVariableNode.js: -------------------------------------------------------------------------------- 1 | import { DSONode } from '~/DSONode/DSONode.js'; 2 | 3 | 4 | class DSOVariableNode extends DSONode 5 | { 6 | constructor ( varName, arrayExpr = null ) 7 | { 8 | super (); 9 | 10 | this.varName = varName; 11 | this.arrayExpr = arrayExpr; 12 | } 13 | } 14 | 15 | 16 | export default DSOVariableNode; 17 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOCommaCatNode.js: -------------------------------------------------------------------------------- 1 | import { DSOExprNode } from '~/DSONode/DSONode.js'; 2 | 3 | 4 | class DSOCommaCatNode extends DSOExprNode 5 | { 6 | constructor ( left, right ) 7 | { 8 | super (); 9 | 10 | this.left = left; 11 | this.right = right; 12 | 13 | this.inParens = false; 14 | } 15 | } 16 | 17 | 18 | export default DSOCommaCatNode; 19 | -------------------------------------------------------------------------------- /decompiler/DSODisassembler/DSOToken/DSOOpcodeToken.js: -------------------------------------------------------------------------------- 1 | import DSOToken from '~/DSOToken/DSOToken.js'; 2 | 3 | import { getOpcodeSubtype } from '~/decompiler/opcodes/getOpcodeType.js'; 4 | 5 | 6 | class DSOOpcodeToken extends DSOToken 7 | { 8 | constructor ( op ) 9 | { 10 | super (getOpcodeSubtype (op)); 11 | this.op = op; 12 | } 13 | } 14 | 15 | 16 | export default DSOOpcodeToken; 17 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOSlotNode.js: -------------------------------------------------------------------------------- 1 | import { DSONode } from '~/DSONode/DSONode.js'; 2 | 3 | 4 | class DSOSlotNode extends DSONode 5 | { 6 | constructor ( slotName, objectExpr = null, arrayExpr = null ) 7 | { 8 | super (); 9 | 10 | this.slotName = slotName; 11 | this.objectExpr = objectExpr; 12 | this.arrayExpr = arrayExpr; 13 | } 14 | } 15 | 16 | 17 | export default DSOSlotNode; 18 | -------------------------------------------------------------------------------- /decompiler/DSODisassembler/DSOToken/DSOOpValueToken.js: -------------------------------------------------------------------------------- 1 | import DSOToken from '~/DSOToken/DSOToken.js'; 2 | 3 | import { getOpcodeSubtype } from '~/decompiler/opcodes/getOpcodeType.js'; 4 | 5 | 6 | class DSOOpValueToken extends DSOToken 7 | { 8 | constructor ( op, value ) 9 | { 10 | super (getOpcodeSubtype (op)); 11 | 12 | this.op = op; 13 | this.value = value; 14 | } 15 | } 16 | 17 | 18 | export default DSOOpValueToken; 19 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOPackageNode.js: -------------------------------------------------------------------------------- 1 | import { DSOStmtNode } from '~/DSONode/DSONode.js'; 2 | 3 | 4 | class DSOPackageNode extends DSOStmtNode 5 | { 6 | constructor ( name, ...funcNodes ) 7 | { 8 | super (); 9 | 10 | this.name = name; 11 | this.funcNodes = funcNodes; 12 | } 13 | 14 | addFunction ( funcNode ) 15 | { 16 | this.funcNodes.push (funcNode); 17 | } 18 | } 19 | 20 | 21 | export default DSOPackageNode; 22 | -------------------------------------------------------------------------------- /common/numbers/constants.js: -------------------------------------------------------------------------------- 1 | import enumerate from '~/util/enumerate.js'; 2 | 3 | 4 | const SIZE_INT8 = 1; 5 | const SIZE_INT16 = 2; 6 | const SIZE_INT32 = 4; 7 | 8 | const SIZE_F64 = 8; 9 | 10 | const numberTypes = enumerate ( 11 | [ 12 | 'UInt8', 13 | 'UInt16LE', 14 | 'UInt16BE', 15 | 'UInt32LE', 16 | 'UInt32BE', 17 | 'DoubleLE', 18 | 'DoubleBE', 19 | ]); 20 | 21 | 22 | export { SIZE_INT8, SIZE_INT16, SIZE_INT32, SIZE_F64, numberTypes }; 23 | -------------------------------------------------------------------------------- /decompiler/errors.js: -------------------------------------------------------------------------------- 1 | class DSODecompilerError extends Error {} 2 | 3 | class DSOLoaderError extends DSODecompilerError {} 4 | class DSODisassemblerError extends DSODecompilerError {} 5 | class DSOParserError extends DSODecompilerError {} 6 | class DSOCodeGeneratorError extends DSODecompilerError {} 7 | 8 | 9 | export 10 | { 11 | DSODecompilerError, 12 | 13 | DSOLoaderError, 14 | DSODisassemblerError, 15 | DSOParserError, 16 | DSOCodeGeneratorError, 17 | }; 18 | -------------------------------------------------------------------------------- /common/mapOpsToValue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Object} opsets - A key-value collection of opsets. 3 | * @returns {Object} Opcodes mapped to the opset name. 4 | */ 5 | const mapOpsToValue = ( opsets ) => 6 | { 7 | const map = {}; 8 | 9 | for ( let type in opsets ) 10 | { 11 | const opcodes = opsets[type]; 12 | 13 | for ( let op of opcodes ) 14 | { 15 | map[op] = type; 16 | } 17 | } 18 | 19 | return map; 20 | }; 21 | 22 | 23 | export default mapOpsToValue; 24 | -------------------------------------------------------------------------------- /decompiler/DSODisassembler/jumpTypeToMarker.js: -------------------------------------------------------------------------------- 1 | import { has } from '~/util/has.js'; 2 | 3 | 4 | const map = 5 | { 6 | 'conditional': 'MarkerCondEnd', 7 | 'loop': 'MarkerLoopEnd', 8 | 'ifElse': 'MarkerElseEnd', 9 | 'OR': 'MarkerLogicEnd', 10 | 'AND': 'MarkerLogicEnd', 11 | }; 12 | 13 | const jumpTypeToMarker = jumpType => 14 | { 15 | return has (map, jumpType) ? map[jumpType] : null; 16 | }; 17 | 18 | 19 | export default jumpTypeToMarker; 20 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOFuncDeclNode.js: -------------------------------------------------------------------------------- 1 | import { DSOStmtNode } from '~/DSONode/DSONode.js'; 2 | 3 | 4 | class DSOFuncDeclNode extends DSOStmtNode 5 | { 6 | constructor ( funcName, namespace, packageName, args ) 7 | { 8 | super (); 9 | 10 | this.funcName = funcName; 11 | this.namespace = namespace; 12 | this.packageName = packageName; 13 | this.args = args; 14 | 15 | this.body = null; 16 | } 17 | } 18 | 19 | 20 | export default DSOFuncDeclNode; 21 | -------------------------------------------------------------------------------- /decompiler/opcodes/associativity.js: -------------------------------------------------------------------------------- 1 | import { createOpset } from '~/common/opcodes.js'; 2 | 3 | 4 | const associative = createOpset ( 5 | [ 6 | 'OP_ADD', 7 | 'OP_MUL', 8 | 'OP_BITAND', 9 | 'OP_BITOR', 10 | 'OP_XOR', 11 | 'OP_AND', 12 | 'OP_OR', 13 | 'OP_JMPIFNOT_NP', 14 | 'OP_JMPIF_NP', 15 | ]); 16 | 17 | 18 | /** 19 | * @param {integer} op 20 | * @returns {boolean} 21 | */ 22 | const isOpAssociative = op => 23 | { 24 | return associative.has (op); 25 | }; 26 | 27 | 28 | export { isOpAssociative }; 29 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOJumpNode.js: -------------------------------------------------------------------------------- 1 | import { DSOStmtNode } from '~/DSONode/DSONode.js'; 2 | 3 | 4 | class DSOJumpNode extends DSOStmtNode 5 | { 6 | constructor ( sourceIP, destIP ) 7 | { 8 | super (); 9 | 10 | this.sourceIP = sourceIP; 11 | this.destIP = destIP; 12 | } 13 | } 14 | 15 | class DSOElseNode extends DSOJumpNode {} 16 | 17 | class DSOBreakNode extends DSOJumpNode {} 18 | class DSOContinueNode extends DSOJumpNode {} 19 | 20 | 21 | export { DSOJumpNode, DSOElseNode, DSOBreakNode, DSOContinueNode }; 22 | -------------------------------------------------------------------------------- /decompiler/DSODisassembler/DSOToken/DSOFuncDeclToken.js: -------------------------------------------------------------------------------- 1 | import DSOToken from '~/DSOToken/DSOToken.js'; 2 | 3 | 4 | class DSOFuncDeclToken extends DSOToken 5 | { 6 | constructor ( funcName, namespace, packageName ) 7 | { 8 | super ('OpcodeFuncDecl'); 9 | 10 | this.funcName = funcName; 11 | this.namespace = namespace; 12 | this.packageName = packageName; 13 | 14 | this.args = []; 15 | } 16 | 17 | addArgument ( arg ) 18 | { 19 | this.args.push (arg); 20 | } 21 | } 22 | 23 | 24 | export default DSOFuncDeclToken; 25 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOAssignNode.js: -------------------------------------------------------------------------------- 1 | import { DSOExprNode } from '~/DSONode/DSONode.js'; 2 | 3 | import { PREC_ASSIGN } from '~/DSONode/precedence.js'; 4 | 5 | 6 | class DSOAssignNode extends DSOExprNode 7 | { 8 | constructor ( varSlot, valueExpr, operator = null ) 9 | { 10 | super (); 11 | 12 | this.varSlot = varSlot; 13 | this.valueExpr = valueExpr; 14 | this.operator = operator; 15 | } 16 | 17 | getPrecedence () 18 | { 19 | return PREC_ASSIGN; 20 | } 21 | } 22 | 23 | 24 | export default DSOAssignNode; 25 | -------------------------------------------------------------------------------- /common/util/arrays.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Array} array 3 | * @param {boolean} test 4 | * @param {...*} values 5 | */ 6 | const pushIfTrue = ( array, test, ...values ) => 7 | { 8 | if ( test ) 9 | { 10 | array.push (...values); 11 | } 12 | }; 13 | 14 | /** 15 | * @param {Array} array 16 | * @param {Integer} index 17 | * 18 | * @returns {*|null} null if at end of the array. 19 | */ 20 | const arrayPeek = ( array, index ) => 21 | { 22 | return index >= 0 && index < array.length ? array[index] : null; 23 | }; 24 | 25 | 26 | export { pushIfTrue, arrayPeek }; 27 | -------------------------------------------------------------------------------- /common/util/enumerate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string[]} arrayOfStrings 3 | * @param {integer} [startIndex=0] 4 | * 5 | * @returns {Object} 6 | */ 7 | const enumerate = ( arrayOfStrings = null, startIndex = 0 ) => 8 | { 9 | if ( arrayOfStrings === null || !Array.isArray (arrayOfStrings) ) 10 | { 11 | return null; 12 | } 13 | 14 | const enums = {}; 15 | const { length } = arrayOfStrings; 16 | 17 | for ( let i = 0; i < length; i++ ) 18 | { 19 | enums[arrayOfStrings[i]] = startIndex + i; 20 | } 21 | 22 | return Object.freeze (enums); 23 | }; 24 | 25 | 26 | export default enumerate; 27 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOStrConcatNode.js: -------------------------------------------------------------------------------- 1 | import { DSOExprNode } from '~/DSONode/DSONode.js'; 2 | 3 | import { PREC_CONCAT } from '~/DSONode/precedence.js'; 4 | 5 | 6 | class DSOStrConcatNode extends DSOExprNode 7 | { 8 | constructor ( left, right, appendChar = null ) 9 | { 10 | super (); 11 | 12 | this.left = left; 13 | this.right = right; 14 | this.appendChar = appendChar; 15 | } 16 | 17 | isAssociative () 18 | { 19 | return true; 20 | } 21 | 22 | getPrecedence () 23 | { 24 | return PREC_CONCAT; 25 | } 26 | } 27 | 28 | 29 | export default DSOStrConcatNode; 30 | -------------------------------------------------------------------------------- /common/util/has.js: -------------------------------------------------------------------------------- 1 | const { hasOwnProperty } = Object.prototype; 2 | 3 | /** 4 | * Safe wrapper for `object.hasOwnProperty()` 5 | * 6 | * @param {Object} object 7 | * @param {string} key 8 | * 9 | * @returns {boolean} 10 | */ 11 | const has = ( object, key ) => 12 | { 13 | return hasOwnProperty.call (object, key); 14 | }; 15 | 16 | /** 17 | * @param {Object} object 18 | * @param {string} funcName 19 | * 20 | * @returns {boolean} 21 | */ 22 | const hasFunction = ( object, funcName ) => 23 | { 24 | return typeof object[funcName] === 'function'; 25 | } 26 | 27 | 28 | export { has, hasFunction }; 29 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOFuncCallNode.js: -------------------------------------------------------------------------------- 1 | import { DSOExprNode } from '~/DSONode/DSONode.js'; 2 | 3 | import { PREC_FUNC_CALL } from '~/DSONode/precedence.js'; 4 | 5 | 6 | class DSOFuncCallNode extends DSOExprNode 7 | { 8 | constructor ( funcName, namespace, callType, args = [] ) 9 | { 10 | super (); 11 | 12 | this.funcName = funcName; 13 | this.namespace = namespace; 14 | this.callType = callType; 15 | this.args = args; 16 | 17 | this.inParens = false; 18 | } 19 | 20 | getPrecedence () 21 | { 22 | return PREC_FUNC_CALL; 23 | } 24 | } 25 | 26 | 27 | export default DSOFuncCallNode; 28 | -------------------------------------------------------------------------------- /common/util/deepFreeze.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {*} obj 3 | * @returns {*} 4 | */ 5 | const deepFreeze = obj => 6 | { 7 | if ( !Object.isFrozen (obj) ) 8 | { 9 | if ( Array.isArray (obj) ) 10 | { 11 | const { length } = obj; 12 | 13 | for ( let i = 0; i < length; i++ ) 14 | { 15 | deepFreeze (obj[i]); 16 | } 17 | 18 | Object.freeze (obj); 19 | } 20 | else if ( typeof obj === 'object' ) 21 | { 22 | for ( let i in obj ) 23 | { 24 | deepFreeze (obj[i]); 25 | } 26 | 27 | Object.freeze (obj); 28 | } 29 | } 30 | 31 | return obj; 32 | }; 33 | 34 | 35 | export default deepFreeze; 36 | -------------------------------------------------------------------------------- /decompiler/DSOCodeGenerator/generateConstant.js: -------------------------------------------------------------------------------- 1 | import { enums } from '~/common/opcodes.js'; 2 | 3 | const { OP_LOADIMMED_STR, OP_TAG_TO_STR } = enums; 4 | 5 | 6 | const generateConstant = function ( node ) 7 | { 8 | const { op, value } = node; 9 | 10 | let quoteChar = ''; 11 | 12 | if ( op === OP_LOADIMMED_STR ) 13 | { 14 | // Since number literals get converted to strings a lot of the time. 15 | if ( String (parseFloat (value)) !== value ) 16 | { 17 | quoteChar = '"'; 18 | } 19 | } 20 | else if ( op === OP_TAG_TO_STR ) 21 | { 22 | quoteChar = '\''; 23 | } 24 | 25 | return quoteChar + String (value) + quoteChar; 26 | }; 27 | 28 | 29 | export { generateConstant }; 30 | -------------------------------------------------------------------------------- /decompiler/DSODisassembler/handleFuncDecl.js: -------------------------------------------------------------------------------- 1 | import DSOFuncDeclToken from '~/DSOToken/DSOFuncDeclToken.js'; 2 | 3 | 4 | const handleFuncDecl = function () 5 | { 6 | const funcName = this.advanceIdent (); 7 | const namespace = this.advanceIdent (); 8 | const packageName = this.advanceIdent (); 9 | 10 | // has arguments 11 | this.advance (); 12 | 13 | this.funcEndIP = this.advance (); 14 | 15 | const token = new DSOFuncDeclToken (funcName, namespace, packageName); 16 | const numArgs = this.advance (); 17 | 18 | for ( let i = 0; i < numArgs; i++ ) 19 | { 20 | token.addArgument (this.advanceIdent ()); 21 | } 22 | 23 | return token; 24 | }; 25 | 26 | 27 | export { handleFuncDecl }; 28 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOBinaryNode.js: -------------------------------------------------------------------------------- 1 | import assert from '~/util/assert.js'; 2 | 3 | import { DSOExprNode } from '~/DSONode/DSONode.js'; 4 | 5 | import { getOpPrecedence } from '~/decompiler/opcodes/precedence.js'; 6 | import { isOpAssociative } from '~/decompiler/opcodes/associativity.js'; 7 | 8 | 9 | class DSOBinaryNode extends DSOExprNode 10 | { 11 | constructor ( left, right, op ) 12 | { 13 | super (); 14 | 15 | this.left = left; 16 | this.right = right; 17 | this.op = op; 18 | } 19 | 20 | isAssociative () 21 | { 22 | return isOpAssociative (this.op); 23 | } 24 | 25 | getPrecedence () 26 | { 27 | return getOpPrecedence (this.op); 28 | } 29 | } 30 | 31 | 32 | export default DSOBinaryNode; 33 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | lib-cov 13 | 14 | coverage 15 | 16 | .nyc_output 17 | 18 | .grunt 19 | 20 | bower_components 21 | 22 | .lock-wscript 23 | 24 | build/Release 25 | 26 | node_modules 27 | jspm_packages 28 | 29 | typings 30 | 31 | .npm 32 | 33 | .eslintcache 34 | 35 | .node_repl_history 36 | 37 | *.tgz 38 | 39 | .yarn-integrity 40 | 41 | .env 42 | 43 | .next 44 | 45 | *.sublime-project 46 | *.sublime-workspace 47 | 48 | *.cs 49 | *.cs.dso 50 | 51 | *.exe 52 | 53 | .*.swp 54 | ._* 55 | .DS_Store 56 | .git 57 | .hg 58 | .npmrc 59 | .lock-wscript 60 | .svn 61 | .wafpickle-* 62 | config.gypi 63 | CVS 64 | npm-debug.log 65 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOLoopNode.js: -------------------------------------------------------------------------------- 1 | import { DSOStmtNode } from '~/DSONode/DSONode.js'; 2 | 3 | 4 | class DSOLoopNode extends DSOStmtNode 5 | { 6 | constructor ( testExpr, body, initialExpr = null, endExpr = null ) 7 | { 8 | super (); 9 | 10 | if ( initialExpr !== null && endExpr === null ) 11 | { 12 | throw new Error ('Loop has initial expression but no end expression!'); 13 | } 14 | else if ( endExpr !== null && initialExpr === null ) 15 | { 16 | throw new Error ('Loop has end expression but no initial expression!'); 17 | } 18 | 19 | this.testExpr = testExpr; 20 | this.body = body; 21 | this.initialExpr = initialExpr; 22 | this.endExpr = endExpr; 23 | } 24 | } 25 | 26 | 27 | export default DSOLoopNode; 28 | -------------------------------------------------------------------------------- /decompiler/DSOCodeGenerator/generateLoop.js: -------------------------------------------------------------------------------- 1 | const generateLoop = function ( node ) 2 | { 3 | let array; 4 | 5 | if ( node.initialExpr === null ) 6 | { 7 | array = ['while', '(', this.generateExpr (node.testExpr), ')', '{']; 8 | } 9 | else 10 | { 11 | array = 12 | [ 13 | 'for', 14 | '(', 15 | this.generateExpr (node.initialExpr), ';\\', 16 | this.generateExpr (node.testExpr), ';\\', 17 | this.generateExpr (node.endExpr), 18 | ')', 19 | '{', 20 | ]; 21 | } 22 | 23 | const { body } = node; 24 | const { length } = body; 25 | 26 | for ( let i = 0; i < length; i++ ) 27 | { 28 | array.push (this.generateStmt (body[i])); 29 | } 30 | 31 | array.push ('}'); 32 | 33 | return array; 34 | }; 35 | 36 | 37 | export { generateLoop }; 38 | -------------------------------------------------------------------------------- /common/strings/stringEncryption.js: -------------------------------------------------------------------------------- 1 | import { ENCRYPTION_KEY } from '~/common/strings/constants.js'; 2 | 3 | 4 | /** 5 | * Blockland's two-way string encryption. 6 | * 7 | * @param {string} string - Encrypted or unencrypted string -- the opposite will be returned. 8 | * 9 | * @returns {string|null} null if invalid string 10 | */ 11 | const stringEncryption = ( string = null ) => 12 | { 13 | if ( string === null || typeof string !== 'string' ) 14 | { 15 | return null; 16 | } 17 | 18 | let encrypted = ''; 19 | 20 | const { length } = string; 21 | 22 | for ( let i = 0; i < length; i++ ) 23 | { 24 | encrypted += String.fromCharCode (string.charCodeAt (i) ^ ENCRYPTION_KEY.charCodeAt (i % 9)); 25 | } 26 | 27 | return encrypted; 28 | }; 29 | 30 | 31 | export default stringEncryption; 32 | -------------------------------------------------------------------------------- /decompiler/DSOCodeGenerator/generateFuncCall.js: -------------------------------------------------------------------------------- 1 | import { pushIfTrue } from '~/util/arrays.js'; 2 | 3 | 4 | const generateFuncCall = function ( node ) 5 | { 6 | const array = []; 7 | 8 | if ( node.namespace !== null ) 9 | { 10 | if ( node.callType === 1 ) 11 | { 12 | array.push (this.generateParens (node.namespace), '.'); 13 | } 14 | else 15 | { 16 | array.push (node.namespace, '::'); 17 | } 18 | } 19 | 20 | array.push (node.funcName, '('); 21 | 22 | const { args } = node; 23 | const { length } = args; 24 | 25 | for ( let i = 0; i < length; i++ ) 26 | { 27 | array.push (this.generateExpr (args[i])); 28 | 29 | pushIfTrue (array, i < length - 1, ','); 30 | } 31 | 32 | array.push (')'); 33 | 34 | return array; 35 | }; 36 | 37 | 38 | export { generateFuncCall }; 39 | -------------------------------------------------------------------------------- /decompiler/DSOParser/parseBinary.js: -------------------------------------------------------------------------------- 1 | import DSOBinaryNode from '~/DSONode/DSOBinaryNode.js'; 2 | 3 | import assert from '~/util/assert.js'; 4 | 5 | 6 | const parseBinary = function ( op, type ) 7 | { 8 | let left; 9 | let right; 10 | 11 | if ( type === 'OpcodeLogicJump' ) 12 | { 13 | left = this.popNode (); 14 | right = this.parseUntil ('MarkerLogicEnd'); 15 | 16 | assert (right.length === 1, `Binary can only have one right expr (${this.currPos - 1})`); 17 | 18 | right = right[0]; 19 | } 20 | else if ( type === 'OpcodeCompareStr' ) 21 | { 22 | right = this.popNode (); 23 | left = this.popNode (); 24 | } 25 | else 26 | { 27 | left = this.popNode (); 28 | right = this.popNode (); 29 | } 30 | 31 | return new DSOBinaryNode (left, right, op); 32 | }; 33 | 34 | 35 | export { parseBinary }; 36 | -------------------------------------------------------------------------------- /decompiler/DSOControlBlock/DSOJumpInfo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For storing information on jump sources/destinations. 3 | */ 4 | class DSOJumpInfo 5 | { 6 | /** 7 | * @param {integer} sourceIP 8 | * @param {integer} destIP 9 | * @param {DSOControlBlock} block 10 | */ 11 | constructor ( sourceIP, destIP, block ) 12 | { 13 | this.sourceIP = sourceIP; 14 | this.destIP = destIP; 15 | this.block = block; 16 | 17 | // OR, AND, break, continue, or ifElse 18 | this.type = 'ifElse'; 19 | 20 | // Whether the type was not set and just defaulted to ifElse. 21 | this.assumedType = true; 22 | } 23 | 24 | /** 25 | * @param {string} type - OR, AND, break, continue, or ifElse 26 | */ 27 | setType ( type ) 28 | { 29 | this.type = type; 30 | this.assumedType = false; 31 | } 32 | } 33 | 34 | 35 | export default DSOJumpInfo; 36 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSONode.js: -------------------------------------------------------------------------------- 1 | class DSONode 2 | { 3 | constructor () 4 | { 5 | this.inParens = false; 6 | } 7 | 8 | /** 9 | * @returns {boolean} Whether or not it can be part of a return value. 10 | */ 11 | isReturnable () 12 | { 13 | return true; 14 | } 15 | 16 | /** 17 | * @returns {boolean} Whether or not this operation is associative. 18 | */ 19 | isAssociative () 20 | { 21 | return false; 22 | } 23 | 24 | /** 25 | * @returns {integer} Operator precedence. 26 | */ 27 | getPrecedence () 28 | { 29 | return Infinity; 30 | } 31 | } 32 | 33 | class DSOExprNode extends DSONode 34 | { 35 | constructor () 36 | { 37 | super (); 38 | this.inParens = true; 39 | } 40 | } 41 | 42 | class DSOStmtNode extends DSONode 43 | { 44 | isReturnable () 45 | { 46 | return false; 47 | } 48 | } 49 | 50 | 51 | export { DSONode, DSOExprNode, DSOStmtNode }; 52 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOObjectDeclNode.js: -------------------------------------------------------------------------------- 1 | import DSOConstantNode from '~/DSONode/DSOConstantNode.js'; 2 | 3 | import { DSOExprNode } from '~/DSONode/DSONode.js'; 4 | import { enums } from '~/common/opcodes.js'; 5 | 6 | const { OP_LOADIMMED_IDENT } = enums; 7 | 8 | 9 | class DSOObjectDeclNode extends DSOExprNode 10 | { 11 | constructor ( classExpr, nameExpr, args = [] ) 12 | { 13 | super (); 14 | 15 | classExpr.inParens = !(classExpr instanceof DSOConstantNode) || 16 | classExpr.op !== OP_LOADIMMED_IDENT; 17 | 18 | this.classExpr = classExpr; 19 | this.nameExpr = nameExpr; 20 | this.args = args; 21 | 22 | this.parentName = null; 23 | this.isDataBlock = false; 24 | this.placeAtRoot = true; 25 | 26 | this.slots = null; 27 | this.subObjects = null; 28 | 29 | this.inParens = false; 30 | } 31 | } 32 | 33 | 34 | export default DSOObjectDeclNode; 35 | -------------------------------------------------------------------------------- /decompiler/opcodes/getOpcodeType.js: -------------------------------------------------------------------------------- 1 | import { has } from '~/util/has.js'; 2 | 3 | import mapOpsToValue from '~/common/mapOpsToValue.js'; 4 | import opcodeTypes from '~/decompiler/opcodes/types.js'; 5 | import opcodeSubtypes from '~/decompiler/opcodes/subtypes.js'; 6 | 7 | const opToType = mapOpsToValue (opcodeTypes); 8 | const opToSubtype = mapOpsToValue (opcodeSubtypes); 9 | 10 | 11 | /** 12 | * @param {integer} op 13 | * @returns {string|null} null if not found 14 | */ 15 | const getOpcodeType = op => 16 | { 17 | if ( has (opToType, op) ) 18 | { 19 | return opToType[op]; 20 | } 21 | 22 | return null; 23 | }; 24 | 25 | /** 26 | * @param {integer} op 27 | * @returns {string|null} null if not found 28 | */ 29 | const getOpcodeSubtype = op => 30 | { 31 | if ( has (opToSubtype, op) ) 32 | { 33 | return opToSubtype[op]; 34 | } 35 | 36 | return null; 37 | }; 38 | 39 | 40 | export { getOpcodeType, getOpcodeSubtype }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dso.js", 3 | "version": "1.0.0", 4 | "main": "dist/dso.js", 5 | "scripts": { 6 | "publish-package": "webpack && npm pack && npm link && npm publish", 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Electrk/dso.js.git" 12 | }, 13 | "author": "Electrk", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/Electrk/dso.js/issues" 17 | }, 18 | "homepage": "https://github.com/Electrk/dso.js#readme", 19 | "dependencies": { 20 | "@babel/preset-env": "^7.20.2", 21 | "babel-loader": "^8.2.5", 22 | "webpack": "^5.87.0" 23 | }, 24 | "devDependencies": { 25 | "@babel/plugin-transform-regenerator": "^7.10.4", 26 | "regenerator-runtime": "^0.13.7", 27 | "terser-webpack-plugin": "^3.0.7", 28 | "webpack-cli": "^5.1.4" 29 | }, 30 | "description": "A DSO decompiler for Blockland.", 31 | "keywords": [ 32 | "blockland", 33 | "dso", 34 | "decompiler" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /decompiler/DSOParser/parseString.js: -------------------------------------------------------------------------------- 1 | import DSOStrConcatNode from '~/DSONode/DSOStrConcatNode.js'; 2 | import DSOCommaCatNode from '~/DSONode/DSOCommaCatNode.js'; 3 | 4 | import assert from '~/util/assert.js'; 5 | 6 | import { enums } from '~/common/opcodes.js'; 7 | 8 | const { OP_ADVANCE_STR_COMMA } = enums; 9 | 10 | 11 | const parseString = function ( token, controlBlock = this.controlBlock ) 12 | { 13 | const startPos = this.currPos - 1; 14 | const body = this.parseUntil ('OpcodeStringEnd', controlBlock); 15 | 16 | assert (body.length === 1, `Strings can only have one expression (start pos: ${startPos})`); 17 | 18 | if ( token.op === OP_ADVANCE_STR_COMMA ) 19 | { 20 | return new DSOCommaCatNode (this.popNode (), body[0]); 21 | } 22 | 23 | switch ( this.peek ().type ) 24 | { 25 | case 'OpcodeSetVarArr': 26 | case 'OpcodeSetFieldArr': 27 | case 'OpcodeSaveVar': 28 | case 'OpcodeSaveField': 29 | { 30 | return body[0]; 31 | } 32 | } 33 | 34 | return new DSOStrConcatNode (this.popNode (), body[0], token.value); 35 | }; 36 | 37 | 38 | export { parseString }; 39 | -------------------------------------------------------------------------------- /decompiler/opcodes/operatorToStr.js: -------------------------------------------------------------------------------- 1 | import { names, isOpcode } from '~/common/opcodes.js'; 2 | 3 | 4 | const opToStr = 5 | { 6 | 'OP_NOT': '!', 7 | 'OP_NOTF': '!', 8 | 'OP_ONESCOMPLEMENT': '~', 9 | 'OP_NEG': '', 10 | 11 | 'OP_ADD': '+', 12 | 'OP_SUB': '-', 13 | 'OP_DIV': '/', 14 | 'OP_MUL': '*', 15 | 'OP_MOD': '%', 16 | 17 | 'OP_BITAND': '&', 18 | 'OP_BITOR': '|', 19 | 'OP_XOR': '^', 20 | 'OP_SHL': '<<', 21 | 'OP_SHR': '>>', 22 | 23 | 'OP_CMPLT': '<', 24 | 'OP_CMPGR': '>', 25 | 'OP_CMPGE': '>=', 26 | 'OP_CMPLE': '<=', 27 | 'OP_CMPEQ': '==', 28 | 'OP_CMPNE': '!=', 29 | 30 | 'OP_COMPARE_STR': '$=', 31 | 32 | 'OP_OR': '||', 33 | 'OP_AND': '&&', 34 | 'OP_JMPIF_NP': '||', 35 | 'OP_JMPIFNOT_NP': '&&', 36 | }; 37 | 38 | 39 | /** 40 | * @param {integer} op 41 | * @returns {string|null} null if not a valid opcode. 42 | */ 43 | const operatorToStr = op => 44 | { 45 | if ( !isOpcode (op) ) 46 | { 47 | return null; 48 | } 49 | 50 | return opToStr[names[op]]; 51 | }; 52 | 53 | 54 | export default operatorToStr; 55 | -------------------------------------------------------------------------------- /decompiler/DSOCodeGenerator/generateObjectDecl.js: -------------------------------------------------------------------------------- 1 | import { pushIfTrue } from '~/util/arrays.js'; 2 | 3 | 4 | const generateObjectDecl = function ( node ) 5 | { 6 | const { args, slots, subObjects } = node; 7 | 8 | const array = 9 | [ 10 | node.isDataBlock ? 'datablock' : 'new', 11 | this.generateParens (node.classExpr), 12 | 13 | '(', this.generateParens (node.nameExpr), 14 | ]; 15 | 16 | pushIfTrue (array, node.parentName !== '', ':', node.parentName); 17 | 18 | for ( let i = 0; i < args.length; i++ ) 19 | { 20 | array.push (',', this.generateExpr (args[i])); 21 | } 22 | 23 | array.push (')'); 24 | 25 | const hasBody = slots.length > 0 || subObjects.length > 0; 26 | 27 | if ( hasBody ) 28 | { 29 | array.push ('{'); 30 | 31 | for ( let i = 0; i < slots.length; i++ ) 32 | { 33 | array.push (this.generateExpr (slots[i]), ';'); 34 | } 35 | 36 | for ( let i = 0; i < subObjects.length; i++ ) 37 | { 38 | array.push (this.generateExpr (subObjects[i]), ';'); 39 | } 40 | 41 | array.push ('}'); 42 | } 43 | 44 | return array; 45 | }; 46 | 47 | 48 | export { generateObjectDecl }; 49 | -------------------------------------------------------------------------------- /decompiler/opcodes/precedence.js: -------------------------------------------------------------------------------- 1 | import { enums as $ } from '~/common/opcodes.js'; 2 | 3 | 4 | const arr = 5 | [ 6 | [$.OP_NOT, $.OP_NOTF, $.OP_ONESCOMPLEMENT, $.OP_NEG], 7 | [$.OP_MUL, $.OP_DIV, $.OP_MOD], 8 | [$.OP_ADD, $.OP_SUB], 9 | [$.OP_SHL, $.OP_SHR], 10 | [$.OP_COMPARE_STR], 11 | [$.OP_CMPLT, $.OP_CMPLE, $.OP_CMPGR, $.OP_CMPGE], 12 | [$.OP_CMPEQ, $.OP_CMPNE], 13 | [$.OP_BITAND], 14 | [$.OP_XOR], 15 | [$.OP_BITOR], 16 | [$.OP_AND, $.OP_JMPIFNOT_NP], 17 | [$.OP_OR, $.OP_JMPIF_NP], 18 | ]; 19 | 20 | const precedence = new Map (); 21 | 22 | for ( let prec = 0; prec < arr.length; prec++ ) 23 | { 24 | const row = arr[prec]; 25 | 26 | for ( let i = 0; i < row.length; i++ ) 27 | { 28 | // +1 because function calls, member access, etc. are 0 29 | precedence.set (row[i], prec + 1); 30 | } 31 | } 32 | 33 | /** 34 | * @param {integer} op 35 | * @returns {integer} Infinity if no precedence specified. 36 | */ 37 | const getOpPrecedence = op => 38 | { 39 | if ( precedence.has (op) ) 40 | { 41 | return precedence.get (op); 42 | } 43 | 44 | return Infinity; 45 | }; 46 | 47 | 48 | export { getOpPrecedence }; 49 | -------------------------------------------------------------------------------- /decompiler/DSOCodeGenerator/generateFuncDecl.js: -------------------------------------------------------------------------------- 1 | import { pushIfTrue } from '~/util/arrays.js'; 2 | 3 | 4 | const generatePackage = function ( node ) 5 | { 6 | const array = ['package', node.name, '{']; 7 | 8 | const { funcNodes } = node; 9 | const { length } = funcNodes; 10 | 11 | for ( let i = 0; i < length; i++ ) 12 | { 13 | array.push (this.generateStmt (funcNodes[i])); 14 | } 15 | 16 | array.push ('}', ';', '\n'); 17 | 18 | return array; 19 | }; 20 | 21 | const generateFuncDecl = function ( node ) 22 | { 23 | const array = ['function']; 24 | 25 | pushIfTrue (array, node.namespace !== null, node.namespace, '::'); 26 | array.push (node.funcName, '('); 27 | 28 | const { args, body } = node; 29 | 30 | for ( let i = 0; i < args.length; i++ ) 31 | { 32 | array.push (args[i]); 33 | pushIfTrue (array, i < args.length - 1, ','); 34 | } 35 | 36 | array.push (')', '{'); 37 | 38 | for ( let i = 0; i < body.length; i++ ) 39 | { 40 | array.push (this.generateStmt (body[i])); 41 | } 42 | 43 | array.push ('}', '\n'); 44 | 45 | return array; 46 | }; 47 | 48 | 49 | export { generatePackage, generateFuncDecl }; 50 | -------------------------------------------------------------------------------- /decompiler/DSOControlBlock/loops.js: -------------------------------------------------------------------------------- 1 | import { has } from '~/util/has.js'; 2 | 3 | 4 | /** 5 | * Whether or not this control block is in a loop. Not for checking if this block IS a loop. 6 | * 7 | * @returns {boolean} 8 | */ 9 | const isInLoop = function () 10 | { 11 | const { parent } = this; 12 | 13 | if ( parent === null ) 14 | { 15 | return false; 16 | } 17 | 18 | if ( parent.type === 'loop' ) 19 | { 20 | return true; 21 | } 22 | 23 | return parent.isInLoop (); 24 | }; 25 | 26 | /** 27 | * Returns the loop this control block is in, if any, and caches the result. 28 | * 29 | * @returns {DSOControlBlock|null} null if none found. 30 | */ 31 | const getOuterLoop = function () 32 | { 33 | if ( has (this, 'outerLoop') ) 34 | { 35 | return this.outerLoop; 36 | } 37 | 38 | const { parent } = this; 39 | 40 | if ( parent === null ) 41 | { 42 | this.outerLoop = null; 43 | } 44 | else if ( parent.type === 'loop' ) 45 | { 46 | this.outerLoop = parent; 47 | } 48 | else 49 | { 50 | this.outerLoop = parent.getOuterLoop (); 51 | } 52 | 53 | return this.outerLoop; 54 | }; 55 | 56 | 57 | export { isInLoop, getOuterLoop }; 58 | -------------------------------------------------------------------------------- /decompiler/opcodes/getOpSize.js: -------------------------------------------------------------------------------- 1 | import { enums } from '~/common/opcodes.js'; 2 | 3 | import { getOpcodeType, getOpcodeSubtype } from '~/decompiler/opcodes/getOpcodeType.js'; 4 | 5 | const { OP_ADVANCE_STR_APPENDCHAR } = enums; 6 | 7 | 8 | /** 9 | * @param {integer[]} code 10 | * @param {integer} ip 11 | * 12 | * @returns {integer} How many positions it takes up. 0 if invalid opcode. 13 | */ 14 | const getOpSize = ( code, ip ) => 15 | { 16 | const op = code[ip]; 17 | const type = getOpcodeType (op); 18 | const subtype = getOpcodeSubtype (op); 19 | 20 | switch ( type ) 21 | { 22 | case 'OpcodeSinglePrefix': 23 | case 'OpcodeJumpIfNot': 24 | { 25 | return 2; 26 | } 27 | 28 | case 'OpcodeTriplePrefix': 29 | { 30 | return 4; 31 | } 32 | 33 | case 'OpcodeSingle': 34 | case 'OpcodeStringStart': 35 | case 'OpcodeStringEnd': 36 | { 37 | if ( op === OP_ADVANCE_STR_APPENDCHAR ) 38 | { 39 | return 2; 40 | } 41 | 42 | return 1; 43 | } 44 | 45 | case 'OpcodeFuncDecl': 46 | { 47 | return 7 + code[ip + 6]; 48 | } 49 | } 50 | 51 | return 0; 52 | }; 53 | 54 | 55 | export default getOpSize; 56 | -------------------------------------------------------------------------------- /decompiler/DSOCodeGenerator/generateNary.js: -------------------------------------------------------------------------------- 1 | import DSOAssignNode from '~/DSONode/DSOAssignNode.js'; 2 | import DSOBinaryNode from '~/DSONode/DSOBinaryNode.js'; 3 | 4 | import assert from '~/util/assert.js'; 5 | import operatorToStr from '~/decompiler/opcodes/operatorToStr.js'; 6 | 7 | import { enums } from '~/common/opcodes.js'; 8 | 9 | const { OP_COMPARE_STR } = enums; 10 | 11 | 12 | const generateUnary = function ( node ) 13 | { 14 | let operator = operatorToStr (node.op); 15 | 16 | assert (operator !== null, 'Invalid unary operator'); 17 | 18 | const { expr } = node; 19 | 20 | if ( operator === '!' && expr instanceof DSOBinaryNode && expr.op === OP_COMPARE_STR ) 21 | { 22 | return [this.generateBranch (expr, expr.left), '!$=', this.generateBranch (expr, expr.right)]; 23 | } 24 | 25 | return [operator, this.generateParens (expr)]; 26 | }; 27 | 28 | const generateBinary = function ( node ) 29 | { 30 | const operator = operatorToStr (node.op); 31 | 32 | assert (operator !== null, 'Invalid binary operator'); 33 | 34 | return [this.generateBranch (node, node.left), operator, this.generateBranch (node, node.right)]; 35 | }; 36 | 37 | 38 | export { generateUnary, generateBinary }; 39 | -------------------------------------------------------------------------------- /decompiler/DSOCodeGenerator/generateConditional.js: -------------------------------------------------------------------------------- 1 | import assert from '~/util/assert.js'; 2 | 3 | import DSOConditionalNode from '~/DSONode/DSOConditionalNode.js'; 4 | 5 | 6 | const generateTernary = function ( node ) 7 | { 8 | const array = [this.generateExpr (node.testExpr), '?']; 9 | 10 | assert (node.ifBlock.length === 1, 'Ternary if block should have only one node'); 11 | assert (node.elseBlock.length === 1, 'Ternary else block should have only one node'); 12 | 13 | array.push (this.generateExpr (node.ifBlock[0]), ':', this.generateExpr (node.elseBlock[0])); 14 | 15 | return array; 16 | }; 17 | 18 | const generateIfStmt = function ( node ) 19 | { 20 | const array = ['if', '(', this.generateExpr (node.testExpr), ')']; 21 | 22 | array.push ('{', this.generateCodeArray (node.ifBlock), '}'); 23 | 24 | const { elseBlock } = node; 25 | 26 | if ( elseBlock !== null ) 27 | { 28 | array.push ('else'); 29 | 30 | if ( elseBlock.length === 1 && elseBlock[0] instanceof DSOConditionalNode ) 31 | { 32 | array.push (this.generateIfStmt (elseBlock[0])); 33 | } 34 | else 35 | { 36 | array.push ('{', this.generateCodeArray (elseBlock), '}'); 37 | } 38 | } 39 | 40 | return array; 41 | }; 42 | 43 | 44 | export { generateTernary, generateIfStmt }; 45 | -------------------------------------------------------------------------------- /decompiler/DSODisassembler/handleMarkers.js: -------------------------------------------------------------------------------- 1 | import DSOToken from '~/DSOToken/DSOToken.js'; 2 | import DSOValueToken from '~/DSOToken/DSOValueToken.js'; 3 | 4 | import { DSODisassemblerError } from '~/decompiler/errors.js'; 5 | 6 | import jumpTypeToMarker from '~/DSODisassembler/jumpTypeToMarker.js'; 7 | 8 | 9 | const handleMarkers = function ( ip ) 10 | { 11 | if ( ip === this.funcEndIP ) 12 | { 13 | // Drop extra OP_RETURN that's at the end of every function. 14 | this.popToken (); 15 | 16 | this.pushMarker ('MarkerFuncEnd'); 17 | this.funcEndIP = null; 18 | } 19 | 20 | while ( this.currBlock.type !== 'root' && ip === this.currBlock.end ) 21 | { 22 | this.currBlock = this.popBlock () || null; 23 | } 24 | 25 | const { jumpEnds } = this; 26 | 27 | while ( jumpEnds.count (ip) > 0 ) 28 | { 29 | const markerType = jumpTypeToMarker (jumpEnds.pop (ip)); 30 | 31 | this.pushMarker (markerType, ip); 32 | } 33 | 34 | if ( ip === this.currBlock.continuePoint ) 35 | { 36 | this.pushMarker ('MarkerContinuePoint', ip); 37 | } 38 | }; 39 | 40 | const pushMarker = function ( type, ...args ) 41 | { 42 | if ( type === 'MarkerFuncEnd' ) 43 | { 44 | this.pushToken (new DSOToken (type)); 45 | } 46 | else 47 | { 48 | this.pushToken (new DSOValueToken (type, ...args)); 49 | } 50 | }; 51 | 52 | 53 | export { handleMarkers, pushMarker }; 54 | -------------------------------------------------------------------------------- /decompiler/DSOParser/parseAssign.js: -------------------------------------------------------------------------------- 1 | import DSOAssignNode from '~/DSONode/DSOAssignNode.js'; 2 | import DSOVariableNode from '~/DSONode/DSOVariableNode.js'; 3 | import DSOSlotNode from '~/DSONode/DSOSlotNode.js'; 4 | 5 | import { enums } from '~/common/opcodes.js'; 6 | 7 | const { OP_SETCUROBJECT } = enums; 8 | 9 | 10 | const parseAssign = function ( operator = null ) 11 | { 12 | return new DSOAssignNode (this.popNode (), this.popNode (), operator); 13 | }; 14 | 15 | const parseVariable = function ( token, isArray = false ) 16 | { 17 | let varName; 18 | let arrayExpr = null; 19 | 20 | if ( isArray ) 21 | { 22 | arrayExpr = this.popNode (); 23 | varName = this.popNode ().value; 24 | } 25 | else 26 | { 27 | varName = token.value; 28 | } 29 | 30 | this.advanceIfType ('OpcodeLoadVar'); 31 | 32 | return new DSOVariableNode (varName, arrayExpr); 33 | }; 34 | 35 | const parseSlot = function ( op, isArray = false ) 36 | { 37 | let node; 38 | 39 | if ( isArray ) 40 | { 41 | node = this.popNode (); 42 | node.arrayExpr = this.popNode (); 43 | } 44 | else 45 | { 46 | const slotName = this.advance ().value; 47 | node = new DSOSlotNode (slotName, op === OP_SETCUROBJECT ? this.popNode () : null); 48 | } 49 | 50 | this.advanceIfType ('OpcodeLoadField'); 51 | 52 | return node; 53 | }; 54 | 55 | 56 | export { parseAssign, parseVariable, parseSlot }; 57 | -------------------------------------------------------------------------------- /decompiler/DSOCodeGenerator/generateAssign.js: -------------------------------------------------------------------------------- 1 | import assert from '~/util/assert.js'; 2 | import operatorToStr from '~/decompiler/opcodes/operatorToStr.js'; 3 | 4 | import { pushIfTrue } from '~/util/arrays.js'; 5 | 6 | 7 | const generateVariable = function ( node ) 8 | { 9 | if ( node.arrayExpr === null ) 10 | { 11 | return node.varName; 12 | } 13 | 14 | return [node.varName, '[', this.generateExpr (node.arrayExpr), ']']; 15 | }; 16 | 17 | const generateSlot = function ( node ) 18 | { 19 | const array = []; 20 | 21 | if ( node.objectExpr !== null ) 22 | { 23 | array.push (this.generateExpr (node.objectExpr), '.'); 24 | } 25 | 26 | array.push (node.slotName); 27 | 28 | if ( node.arrayExpr !== null ) 29 | { 30 | array.push ('[', this.generateExpr (node.arrayExpr), ']'); 31 | } 32 | 33 | return array; 34 | }; 35 | 36 | const generateAssign = function ( node ) 37 | { 38 | const array = [this.generateExpr (node.varSlot)]; 39 | 40 | if ( node.operator === null ) 41 | { 42 | array.push ('='); 43 | } 44 | else 45 | { 46 | const operator = operatorToStr (node.operator); 47 | 48 | assert (operator !== null, 'Invalid binary operator'); 49 | 50 | array.push (operator + '='); 51 | } 52 | 53 | array.push (this.generateExpr (node.valueExpr)); 54 | 55 | return array; 56 | }; 57 | 58 | 59 | export { generateVariable, generateSlot, generateAssign }; 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Sublime Text files 64 | *.sublime-project 65 | *.sublime-workspace 66 | 67 | # TorqueScript files 68 | *.cs 69 | *.cs.dso 70 | *.gui 71 | *.gui.dso 72 | 73 | # Executable file 74 | *.exe 75 | 76 | # Distribution files 77 | dist/ 78 | -------------------------------------------------------------------------------- /common/strings/escapeChars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Escapes special characters like backslashes, quotes, newlines, as well as 3 | * any control or non-ASCII characters. 4 | * 5 | * @param {string} inputStr 6 | * @returns {string} 7 | */ 8 | const escapeChars = inputStr => 9 | { 10 | inputStr = inputStr.replace (/\\/g, '\\\\').replace (/\t/g, '\\t'). 11 | replace (/\n/g, '\\n').replace (/\r/g, '\\r'). 12 | replace (/'/g, '\\\'').replace (/"/g, '\\"'). 13 | replace (/\cA/g, '\\c0').replace (/\cB/g, '\\c1'). 14 | replace (/\cC/g, '\\c2').replace (/\cD/g, '\\c3'). 15 | replace (/\cE/g, '\\c4').replace (/\cF/g, '\\c5'). 16 | replace (/\cG/g, '\\c6').replace (/\cK/g, '\\c7'). 17 | replace (/\cL/g, '\\c8').replace (/\cN/g, '\\c9'). 18 | replace (/\cO/g, '\\cr').replace (/\cP/g, '\\cp'). 19 | replace (/\cQ/g, '\\co'); 20 | 21 | const { length } = inputStr; 22 | 23 | let escaped = ''; 24 | 25 | for ( let i = 0; i < length; i++ ) 26 | { 27 | const charCode = inputStr.charCodeAt (i); 28 | 29 | if ( charCode < 32 || charCode > 126 ) 30 | { 31 | escaped += `\\x${charCode.toString (16)}`; 32 | } 33 | else 34 | { 35 | escaped += inputStr.charAt (i); 36 | } 37 | } 38 | 39 | return escaped; 40 | }; 41 | 42 | 43 | export default escapeChars; 44 | -------------------------------------------------------------------------------- /decompiler/DSOCodeGenerator/generateConcat.js: -------------------------------------------------------------------------------- 1 | import DSOAssignNode from '~/DSONode/DSOAssignNode.js'; 2 | 3 | import { DSOCodeGeneratorError } from '~/decompiler/errors.js'; 4 | 5 | 6 | const generateCommaCat = function ( node ) 7 | { 8 | return [this.generateExpr (node.left), ',', this.generateExpr (node.right)]; 9 | }; 10 | 11 | const generateStrConcat = function ( node ) 12 | { 13 | const { appendChar } = node; 14 | 15 | let concatOp = '@'; 16 | 17 | if ( appendChar !== null ) 18 | { 19 | switch ( appendChar ) 20 | { 21 | case 32: 22 | { 23 | concatOp = 'SPC'; 24 | break; 25 | } 26 | 27 | case 10: 28 | { 29 | concatOp = 'NL'; 30 | break; 31 | } 32 | 33 | case 9: 34 | { 35 | concatOp = 'TAB'; 36 | break; 37 | } 38 | 39 | default: 40 | { 41 | throw new DSOCodeGeneratorError (`Unsupported concat char \`${appendChar}\``); 42 | } 43 | } 44 | } 45 | 46 | let right; 47 | 48 | if ( this.shouldAddParens (node, node.right) ) 49 | { 50 | right = this.generateParens (node.right); 51 | } 52 | else 53 | { 54 | right = this.generateExpr (node.right); 55 | } 56 | 57 | let left; 58 | 59 | if ( this.shouldAddParens (node, node.left) ) 60 | { 61 | left = this.generateParens (node.left); 62 | } 63 | else 64 | { 65 | left = this.generateExpr (node.left); 66 | } 67 | 68 | return [left, concatOp, right]; 69 | }; 70 | 71 | 72 | export { generateCommaCat, generateStrConcat }; 73 | -------------------------------------------------------------------------------- /decompiler/DSODisassembler/scanNext.js: -------------------------------------------------------------------------------- 1 | import { DSODisassemblerError } from '~/decompiler/errors.js'; 2 | 3 | import { getOpcodeType, getOpcodeSubtype } from '~/decompiler/opcodes/getOpcodeType.js'; 4 | 5 | 6 | const scanNext = function () 7 | { 8 | const { ip } = this; 9 | 10 | const op = this.advance (); 11 | const type = getOpcodeType (op); 12 | const subtype = getOpcodeSubtype (op); 13 | 14 | this.handleMarkers (ip); 15 | 16 | switch ( type ) 17 | { 18 | case 'OpcodeSingle': 19 | case 'OpcodeStringEnd': 20 | { 21 | return this.handleSingle (op, subtype); 22 | } 23 | 24 | case 'OpcodeSinglePrefix': 25 | { 26 | return this.handleSinglePrefix (op, subtype, ip); 27 | } 28 | 29 | case 'OpcodeTriplePrefix': 30 | { 31 | return this.handleTriplePrefix (op, subtype, ip); 32 | } 33 | 34 | case 'OpcodeJumpIfNot': 35 | { 36 | return this.handleJumpIfNot (op, subtype, ip); 37 | } 38 | 39 | case 'OpcodeStringStart': 40 | { 41 | return this.handleStringStart (op, subtype, ip); 42 | } 43 | 44 | case 'OpcodeFuncDecl': 45 | { 46 | return this.handleFuncDecl (); 47 | } 48 | 49 | case 'OpcodeError': 50 | { 51 | throw new DSODisassemblerError (`Invalid opcode ${op} at ${ip}`); 52 | } 53 | 54 | case null: 55 | { 56 | throw new DSODisassemblerError (`Non-existent opcode ${op} at ${ip}`); 57 | } 58 | 59 | default: 60 | { 61 | throw new DSODisassemblerError (`Unhandled opcode ${op} at ${ip}`); 62 | } 63 | } 64 | }; 65 | 66 | 67 | export { scanNext }; 68 | -------------------------------------------------------------------------------- /decompiler/DSOLoader/readString.js: -------------------------------------------------------------------------------- 1 | import stringEncryption from '~/common/strings/stringEncryption.js'; 2 | import escapeChars from '~/common/strings/escapeChars.js'; 3 | 4 | import { DSOLoaderError } from '~/decompiler/errors.js'; 5 | 6 | 7 | const readString = function ( chars = 1, encoding = 'binary' ) 8 | { 9 | if ( chars <= 0 ) 10 | { 11 | throw new DSOLoaderError (`Cannot advance ${chars} chars - must be greater than 0`); 12 | } 13 | 14 | if ( this.currPos + chars >= this.buffer.length ) 15 | { 16 | throw new DSOLoaderError (`Cannot advance ${chars} chars - exceeds buffer length from current index`); 17 | } 18 | 19 | const string = this.buffer.toString (encoding, this.currPos, this.currPos + chars); 20 | 21 | this.advance (chars); 22 | 23 | return string; 24 | }; 25 | 26 | const readStringTable = function () 27 | { 28 | let strings = ''; 29 | 30 | const size = this.readInteger (true); 31 | 32 | if ( size > 0 ) 33 | { 34 | strings = stringEncryption (this.readString (size)); 35 | } 36 | 37 | const table = {}; 38 | 39 | let currString = ''; 40 | let currIndex = 0; 41 | 42 | const { length } = strings; 43 | 44 | for ( let i = 0; i < length; i++ ) 45 | { 46 | const char = strings.charAt (i); 47 | 48 | if ( char === '\0' ) 49 | { 50 | table[currIndex] = currString; 51 | 52 | currString = ''; 53 | currIndex = i + 1; 54 | } 55 | else 56 | { 57 | currString += char; 58 | } 59 | } 60 | 61 | for ( let i in table ) 62 | { 63 | table[i] = escapeChars (table[i]); 64 | } 65 | 66 | return table; 67 | }; 68 | 69 | 70 | export { readString, readStringTable }; 71 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSONode/DSOConditionalNode.js: -------------------------------------------------------------------------------- 1 | import assert from '~/util/assert.js'; 2 | 3 | import { DSONode, DSOStmtNode } from '~/DSONode/DSONode.js'; 4 | 5 | import { PREC_TERNARY } from '~/DSONode/precedence.js'; 6 | 7 | 8 | class DSOConditionalNode extends DSONode 9 | { 10 | constructor ( testExpr, ifBlock, elseBlock = null ) 11 | { 12 | super (); 13 | 14 | this.testExpr = testExpr; 15 | this.ifBlock = ifBlock; 16 | this.elseBlock = elseBlock; 17 | } 18 | 19 | /** 20 | * A gross hack to prevent this from being returned when it's inappropriate to do so. 21 | * 22 | * A better way would be to determine each commands' data type from the beginning and whether 23 | * or not that data type is returnable, but this'll do for now. 24 | */ 25 | isReturnable () 26 | { 27 | const { ifBlock, elseBlock } = this; 28 | 29 | if ( ifBlock.length !== 1 || elseBlock === null || elseBlock.length !== 1 ) 30 | { 31 | return false; 32 | } 33 | 34 | const firstIf = ifBlock[0]; 35 | const firstElse = elseBlock[0]; 36 | 37 | if ( firstIf instanceof DSOStmtNode || firstElse instanceof DSOStmtNode ) 38 | { 39 | return false; 40 | } 41 | 42 | let returnable = true; 43 | 44 | if ( firstIf instanceof DSOConditionalNode ) 45 | { 46 | returnable = firstIf.isReturnable (); 47 | } 48 | 49 | if ( returnable && firstElse instanceof DSOConditionalNode ) 50 | { 51 | returnable = firstElse.isReturnable (); 52 | } 53 | 54 | return returnable; 55 | } 56 | 57 | getPrecedence () 58 | { 59 | return PREC_TERNARY; 60 | } 61 | } 62 | 63 | 64 | export default DSOConditionalNode; 65 | -------------------------------------------------------------------------------- /decompiler/DSOLoader/getTableValue.js: -------------------------------------------------------------------------------- 1 | import { DSOLoaderError } from '~/decompiler/errors.js'; 2 | 3 | import { has } from '~/util/has.js'; 4 | import { enums } from '~/common/opcodes.js'; 5 | 6 | const 7 | { 8 | OP_LOADIMMED_UINT, 9 | OP_LOADIMMED_FLT, 10 | OP_LOADIMMED_STR, 11 | OP_LOADIMMED_IDENT, 12 | OP_TAG_TO_STR, 13 | } 14 | = enums; 15 | 16 | 17 | const getString = function ( index, isFunction = false ) 18 | { 19 | const table = isFunction ? this.functionStringTable : this.globalStringTable; 20 | 21 | return has (table, index) ? table[index] : null; 22 | }; 23 | 24 | const getFloat = function ( index, isFunction = false ) 25 | { 26 | const table = isFunction ? this.functionFloatTable : this.globalFloatTable; 27 | 28 | return has (table, index) ? table[index] : null; 29 | }; 30 | 31 | const getTableValue = function ( op, value, inFunction ) 32 | { 33 | let tableValue = null; 34 | 35 | switch ( op ) 36 | { 37 | case OP_LOADIMMED_STR: 38 | case OP_TAG_TO_STR: 39 | { 40 | tableValue = this.getString (value, inFunction); 41 | break; 42 | } 43 | 44 | case OP_LOADIMMED_IDENT: 45 | { 46 | tableValue = this.getString (value, false); 47 | break; 48 | } 49 | 50 | case OP_LOADIMMED_FLT: 51 | { 52 | tableValue = this.getFloat (value, inFunction); 53 | break; 54 | } 55 | 56 | case OP_LOADIMMED_UINT: 57 | { 58 | tableValue = value; 59 | break; 60 | } 61 | } 62 | 63 | if ( tableValue === null ) 64 | { 65 | throw new DSOLoaderError (`Could not find ${value} (op: ${op} | in func: ${inFunction})`); 66 | } 67 | 68 | return tableValue; 69 | }; 70 | 71 | 72 | export { getString, getFloat, getTableValue }; 73 | -------------------------------------------------------------------------------- /decomp_v20.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import dso_js from './dist/dso.js'; 5 | 6 | const { dso } = dso_js; 7 | const { decompiler } = dso; 8 | 9 | reverse("./dso/server/scripts/allGameScripts.cs.dso"); 10 | reverse("./dso/server/scripts/DamageTypes.cs.dso"); 11 | reverse("./dso/server/scripts/game.cs.dso"); 12 | reverse("./dso/server/defaultMusicList.cs.dso"); 13 | reverse("./dso/server/defaultAddOnList.cs.dso"); 14 | reverse("./dso/server/defaults.cs.dso"); 15 | reverse("./dso/server/init.cs.dso"); 16 | reverse("./dso/server/mainServer.cs.dso"); 17 | 18 | reverse("./dso/client/scripts/allClientScripts.cs.dso"); 19 | reverse("./dso/client/actionMap.cs.dso"); 20 | reverse("./dso/client/audio.cs.dso"); 21 | reverse("./dso/client/canvas.cs.dso"); 22 | reverse("./dso/client/defaults.cs.dso"); 23 | reverse("./dso/client/init.cs.dso"); 24 | reverse("./dso/client/message.cs.dso"); 25 | reverse("./dso/client/mission.cs.dso"); 26 | reverse("./dso/client/missionDownload.cs.dso"); 27 | 28 | 29 | reverse("./dso/client/ui/allClientGuis.gui.dso"); 30 | 31 | reverse("./dso/main.cs.dso"); 32 | 33 | function reverse(dsoPath) { 34 | fs.readFile (dsoPath, ( error, buffer ) => 35 | { 36 | if ( error ) 37 | { 38 | console.error (error); 39 | return; 40 | } 41 | 42 | let codeString; 43 | 44 | try 45 | { 46 | codeString = decompiler.decompileDSO (buffer); 47 | } 48 | catch ( decompilerError ) 49 | { 50 | console.error ('[!] Decompiler Error:', decompilerError); 51 | return; 52 | } 53 | 54 | fs.writeFileSync (path.basename(dsoPath).replace(/\.dso$/, ''), codeString); 55 | console.log("Success!"); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /decompiler/DSODisassembler/handleMisc.js: -------------------------------------------------------------------------------- 1 | import DSOValueToken from '~/DSOToken/DSOValueToken.js'; 2 | import DSOOpValueToken from '~/DSOToken/DSOOpValueToken.js'; 3 | 4 | import assert from '~/util/assert.js'; 5 | 6 | import { DSODisassemblerError } from '~/decompiler/errors.js'; 7 | 8 | import { enums } from '~/common/opcodes.js'; 9 | 10 | const { OP_ADVANCE_STR_APPENDCHAR } = enums; 11 | 12 | 13 | const handleTriplePrefix = function ( op, subtype, ip ) 14 | { 15 | const values = []; 16 | 17 | if ( subtype === 'OpcodeFuncCall' ) 18 | { 19 | values.push (this.advanceIdent (), this.advanceIdent (), this.advance ()); 20 | } 21 | else if ( subtype === 'OpcodeCreateObj' ) 22 | { 23 | values.push (this.advanceIdent (), !!this.advance (), this.advance ()); 24 | } 25 | else 26 | { 27 | throw new DSODisassemblerError (`Unhandled OpcodeTriplePrefix at ${ip}`); 28 | } 29 | 30 | return new DSOOpValueToken (op, values); 31 | }; 32 | 33 | const handleJumpIfNot = function ( op, subtype, ip ) 34 | { 35 | assert (this.currBlock.hasBlock (ip), `Missing control block at ${ip}`); 36 | 37 | this.pushBlock (this.currBlock); 38 | this.currBlock = this.currBlock.getBlock (ip); 39 | 40 | const endIP = this.advance (); 41 | 42 | this.jumpEnds.push (endIP, this.currBlock.type); 43 | 44 | return new DSOValueToken (subtype, [ip, endIP]); 45 | }; 46 | 47 | const handleStringStart = function ( op, subtype, ip ) 48 | { 49 | let appendChar = null; 50 | 51 | if ( op === OP_ADVANCE_STR_APPENDCHAR ) 52 | { 53 | appendChar = this.advance (); 54 | } 55 | 56 | return new DSOOpValueToken (op, appendChar); 57 | }; 58 | 59 | 60 | export { handleTriplePrefix, handleJumpIfNot, handleStringStart }; 61 | -------------------------------------------------------------------------------- /common/operators.js: -------------------------------------------------------------------------------- 1 | const unaryArr = ['!', '', '~']; 2 | const binaryArr = ['+', '-', '*', '/', '%', '^', '&', '|', '<<', '>>']; 3 | const assignArr = ['=', '+=', '-=', '*=', '/=', '%=', '^=', '&=', '|=', '<<=', '>>=']; 4 | const logicArr = ['<', '>', '==', '!=', '<=', '>=', '$=', '!$=', '&&', '||']; 5 | const ternaryArr = ['?', ':']; 6 | const concatArr = ['@', 'SPC', 'TAB', 'NL']; 7 | 8 | const unaryOperators = new Set (unaryArr); 9 | const binaryOperators = new Set (binaryArr); 10 | const assignOperators = new Set (assignArr); 11 | const logicOperators = new Set (logicArr); 12 | const ternaryOperators = new Set (ternaryArr); 13 | const concatOperators = new Set (concatArr); 14 | 15 | const operators = new Set ( 16 | [ 17 | ...unaryArr, 18 | ...binaryArr, 19 | ...assignArr, 20 | ...logicArr, 21 | ...ternaryArr, 22 | ...concatArr, 23 | ]); 24 | 25 | 26 | const isOperator = str => 27 | { 28 | return operators.has (str); 29 | }; 30 | 31 | const isUnaryOperator = str => 32 | { 33 | return unaryOperators.has (str); 34 | }; 35 | 36 | const isBinaryOperator = str => 37 | { 38 | return binaryOperators.has (str); 39 | }; 40 | 41 | const isAssignOperator = str => 42 | { 43 | return assignOperators.has (str); 44 | }; 45 | 46 | const isLogicOperator = str => 47 | { 48 | return logicOperators.has (str); 49 | }; 50 | 51 | const isTernaryOperator = str => 52 | { 53 | return ternaryOperators.has (str); 54 | }; 55 | 56 | const isConcatOperator = str => 57 | { 58 | return concatOperators.has (str); 59 | }; 60 | 61 | 62 | export 63 | { 64 | isOperator, 65 | isUnaryOperator, 66 | isBinaryOperator, 67 | isAssignOperator, 68 | isLogicOperator, 69 | isTernaryOperator, 70 | isConcatOperator, 71 | }; 72 | -------------------------------------------------------------------------------- /decompiler/DSOControlBlock/scan.js: -------------------------------------------------------------------------------- 1 | import assert from '~/util/assert.js'; 2 | import getOpSize from '~/decompiler/opcodes/getOpSize.js'; 3 | 4 | import { getOpcodeSubtype } from '~/decompiler/opcodes/getOpcodeType.js'; 5 | import { enums } from '~/common/opcodes.js'; 6 | 7 | const { OP_JMPIF_NP, OP_JMPIFNOT_NP } = enums; 8 | 9 | 10 | /** 11 | * Initial scan to add jump sources and determine the control block's type. 12 | * 13 | * @param {integer[]} code 14 | */ 15 | const scan = function ( code ) 16 | { 17 | const { end } = this; 18 | 19 | let ip = this.start; 20 | 21 | if ( this.type === 'root' ) 22 | { 23 | this.jumps.clear (); 24 | this.blocks.clear (); 25 | } 26 | else 27 | { 28 | ip += 2; 29 | } 30 | 31 | while ( ip < end ) 32 | { 33 | const op = code[ip]; 34 | const subtype = getOpcodeSubtype (op); 35 | 36 | console.log(`DEBUG: 0x${op < 0x10 ? "0" : ""}${op.toString(16)}, ${subtype}`); 37 | 38 | if ( subtype === 'OpcodeJumpIfNot' ) 39 | { 40 | this.addBlock (ip, code[ip + 1], this).scan (code); 41 | ip = code[ip + 1]; 42 | } 43 | else 44 | { 45 | if ( subtype === 'OpcodeJump' || subtype === 'OpcodeLogicJump' ) 46 | { 47 | const jump = this.addJump (ip, code[ip + 1], this); 48 | 49 | if ( op === OP_JMPIF_NP ) 50 | { 51 | jump.setType ('OR'); 52 | } 53 | else if ( op === OP_JMPIFNOT_NP ) 54 | { 55 | jump.setType ('AND'); 56 | } 57 | } 58 | else if ( subtype === 'OpcodeLoopJump' ) 59 | { 60 | assert (this.type !== 'root', `OP_JMPIF(F) outside of loop at ${ip}`); 61 | 62 | this.type = 'loop'; 63 | } 64 | 65 | const size = getOpSize (code, ip); 66 | 67 | assert (size > 0, `Invalid opcode ${op} at ${ip}`); 68 | 69 | ip += size; 70 | } 71 | } 72 | }; 73 | 74 | 75 | export { scan }; 76 | -------------------------------------------------------------------------------- /decompiler/DSOCodeGenerator/generateStmt.js: -------------------------------------------------------------------------------- 1 | import DSOReturnNode from '~/DSONode/DSOReturnNode.js'; 2 | import DSOConditionalNode from '~/DSONode/DSOConditionalNode.js'; 3 | import DSOLoopNode from '~/DSONode/DSOLoopNode.js'; 4 | import DSOFuncDeclNode from '~/DSONode/DSOFuncDeclNode.js'; 5 | import DSOPackageNode from '~/DSONode/DSOPackageNode.js'; 6 | 7 | import { DSOCodeGeneratorError } from '~/decompiler/errors.js'; 8 | import { DSOStmtNode } from '~/DSONode/DSONode.js'; 9 | 10 | import { DSOBreakNode, DSOContinueNode } from '~/DSONode/DSOJumpNode.js'; 11 | 12 | 13 | const generateStmt = function ( node ) 14 | { 15 | const { constructor } = node; 16 | 17 | switch ( constructor ) 18 | { 19 | case DSOBreakNode: 20 | { 21 | return ['break', ';']; 22 | } 23 | 24 | case DSOContinueNode: 25 | { 26 | return ['continue', ';']; 27 | } 28 | 29 | case DSOReturnNode: 30 | { 31 | const array = ['return']; 32 | 33 | if ( node.value !== null ) 34 | { 35 | array.push (this.generateExpr (node.value)); 36 | } 37 | 38 | array.push (';'); 39 | 40 | return array; 41 | } 42 | 43 | case DSOConditionalNode: 44 | { 45 | return [this.generateIfStmt (node)]; 46 | } 47 | 48 | case DSOLoopNode: 49 | { 50 | return [this.generateLoop (node)]; 51 | } 52 | 53 | case DSOFuncDeclNode: 54 | { 55 | return [this.generateFuncDecl (node)]; 56 | } 57 | 58 | case DSOPackageNode: 59 | { 60 | return [this.generatePackage (node)]; 61 | } 62 | 63 | default: 64 | { 65 | if ( node instanceof DSOStmtNode ) 66 | { 67 | throw new DSOCodeGeneratorError (`Unhandled generator statement \`${name}\``); 68 | } 69 | else 70 | { 71 | return [this.generateExpr (node), ';']; 72 | } 73 | } 74 | } 75 | }; 76 | 77 | 78 | export { generateStmt }; 79 | -------------------------------------------------------------------------------- /decompiler/DSODisassembler/handleSingle.js: -------------------------------------------------------------------------------- 1 | import DSOOpcodeToken from '~/DSOToken/DSOOpcodeToken.js'; 2 | import DSOOpValueToken from '~/DSOToken/DSOOpValueToken.js'; 3 | import DSOReturnToken from '~/DSOToken/DSOReturnToken.js'; 4 | 5 | 6 | const handleSingle = function ( op, subtype ) 7 | { 8 | switch ( subtype ) 9 | { 10 | case 'OpcodeSkip': 11 | case 'OpcodeConvert': 12 | { 13 | return null; 14 | } 15 | 16 | case 'OpcodeReturn': 17 | { 18 | return new DSOReturnToken (true); 19 | } 20 | 21 | case 'OpcodeTypeToNone': 22 | { 23 | const ip = this.ip; 24 | 25 | if ( this.advanceIfSubtype ('OpcodeReturn') ) 26 | { 27 | this.handleMarkers (ip); 28 | return new DSOReturnToken (false); 29 | } 30 | 31 | return null; 32 | } 33 | 34 | default: 35 | { 36 | return new DSOOpcodeToken (op); 37 | } 38 | } 39 | }; 40 | 41 | const handleSinglePrefix = function ( op, subtype, ip ) 42 | { 43 | if ( subtype === 'OpcodeLoopJump' ) 44 | { 45 | this.advance (); 46 | return null; 47 | } 48 | 49 | let value; 50 | 51 | if ( subtype === 'OpcodeLoadImmed' ) 52 | { 53 | value = this.advanceConstant (op); 54 | } 55 | else if ( subtype === 'OpcodeSetCurVar' || subtype === 'OpcodeSetCurField' ) 56 | { 57 | value = this.advanceIdent (); 58 | } 59 | else 60 | { 61 | value = this.advance (); 62 | } 63 | 64 | if ( subtype === 'OpcodeJump' || subtype === 'OpcodeLogicJump' ) 65 | { 66 | const jump = this.currBlock.getJump (ip); 67 | 68 | if ( jump.type !== 'continue' && jump.type !== 'break' ) 69 | { 70 | this.jumpEnds.push (value, jump.type); 71 | } 72 | 73 | // [sourceIP, destIP] 74 | value = [ip, value]; 75 | } 76 | else if ( subtype === 'OpcodeObjSection' ) 77 | { 78 | value = !!value; 79 | } 80 | 81 | return new DSOOpValueToken (op, value); 82 | }; 83 | 84 | 85 | export { handleSingle, handleSinglePrefix }; 86 | -------------------------------------------------------------------------------- /decompiler/decompiler.js: -------------------------------------------------------------------------------- 1 | import DSOLoader from '~/DSOLoader/DSOLoader.js'; 2 | import DSOControlBlock from '~/DSOControlBlock/DSOControlBlock.js'; 3 | import DSODisassembler from '~/DSODisassembler/DSODisassembler.js'; 4 | import DSOParser from '~/DSOParser/DSOParser.js'; 5 | import DSOCodeGenerator from '~/DSOCodeGenerator/DSOCodeGenerator.js'; 6 | 7 | import 8 | { 9 | DSODecompilerError, 10 | DSOLoaderError, 11 | DSODisassemblerError, 12 | DSOParserError, 13 | DSOCodeGeneratorError, 14 | } 15 | from '~/decompiler/errors.js'; 16 | 17 | 18 | /** 19 | * Decompiles a DSO file from a buffer. 20 | * 21 | * For browsers, use the `buffer` npm package. 22 | * For Node.js, use the native Buffer class. 23 | * 24 | * @param {Buffer} buffer 25 | * @param {Object} [options={}] 26 | * 27 | * @returns {string|Array} Either a code string or code array, depending on the options set. 28 | */ 29 | const decompileDSO = ( buffer, options = {} ) => 30 | { 31 | const { outputArray = false } = options; 32 | 33 | const loader = new DSOLoader (buffer); 34 | 35 | loader.read (); 36 | 37 | const controlBlock = new DSOControlBlock (0, loader.code.length); 38 | 39 | controlBlock.scan (loader.code); 40 | controlBlock.analyzeJumps (); 41 | 42 | const disassembler = new DSODisassembler (loader, controlBlock); 43 | const tokens = disassembler.disassemble (); 44 | 45 | const parser = new DSOParser (tokens, controlBlock); 46 | const astNodes = parser.parse (); 47 | 48 | const generator = new DSOCodeGenerator (astNodes); 49 | 50 | if ( outputArray ) 51 | { 52 | return generator.generateCodeArray (); 53 | } 54 | else 55 | { 56 | return generator.generateCode (); 57 | } 58 | }; 59 | 60 | 61 | export 62 | { 63 | decompileDSO, 64 | 65 | DSOLoader, 66 | DSOControlBlock, 67 | DSODisassembler, 68 | DSOParser, 69 | DSOCodeGenerator, 70 | 71 | DSODecompilerError, 72 | DSOLoaderError, 73 | DSODisassemblerError, 74 | DSOParserError, 75 | DSOCodeGeneratorError, 76 | }; 77 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require ('path'); 2 | const webpack = require ('webpack'); 3 | 4 | const TerserPlugin = require ('terser-webpack-plugin'); 5 | 6 | 7 | module.exports = 8 | { 9 | entry: './main.js', 10 | 11 | output: 12 | { 13 | filename: 'dso.js', 14 | path: path.join (__dirname + '/dist'), 15 | 16 | library: 'dso', 17 | libraryTarget: 'commonjs2', 18 | 19 | umdNamedDefine: true, 20 | }, 21 | 22 | mode: 'production', 23 | 24 | module: 25 | { 26 | rules: 27 | [ 28 | { 29 | test: /\.jsx?$/, 30 | exclude: /node_modules/, 31 | loader: 'babel-loader', 32 | options: 33 | { 34 | presets: ['@babel/preset-env'] 35 | } 36 | } 37 | ] 38 | }, 39 | 40 | optimization: 41 | { 42 | // TODO: i'm tired of these weird ass stacktraces 43 | /* minimize: true, 44 | minimizer: 45 | [ 46 | new TerserPlugin ( 47 | { 48 | terserOptions: 49 | { 50 | keep_classnames: true, 51 | keep_fnames: true, 52 | }, 53 | }), 54 | ],*/ 55 | minimize: false, 56 | }, 57 | 58 | resolve: 59 | { 60 | alias: 61 | { 62 | '~/util': path.resolve (__dirname, './common/util/'), 63 | '~/common': path.resolve (__dirname, './common/'), 64 | '~/ArrayMap.js': path.resolve (__dirname, './common/ArrayMap.js'), 65 | '~/decompiler': path.resolve (__dirname, './decompiler/'), 66 | '~/DSOLoader': path.resolve (__dirname, './decompiler/DSOLoader/'), 67 | '~/DSODisassembler': path.resolve (__dirname, './decompiler/DSODisassembler/'), 68 | '~/DSOControlBlock': path.resolve (__dirname, './decompiler/DSOControlBlock/'), 69 | '~/DSOToken': path.resolve (__dirname, './decompiler/DSODisassembler/DSOToken/'), 70 | '~/DSOParser': path.resolve (__dirname, './decompiler/DSOParser/'), 71 | '~/DSONode': path.resolve (__dirname, './decompiler/DSOParser/DSONode/'), 72 | '~/DSOCodeGenerator': path.resolve (__dirname, './decompiler/DSOCodeGenerator/'), 73 | } 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /decompiler/DSOParser/parsePushFrame.js: -------------------------------------------------------------------------------- 1 | import DSOObjectDeclNode from '~/DSONode/DSOObjectDeclNode.js'; 2 | import DSOFuncCallNode from '~/DSONode/DSOFuncCallNode.js'; 3 | 4 | import assert from '~/util/assert.js'; 5 | 6 | 7 | const parsePushFrame = function () 8 | { 9 | const startPos = this.currPos - 1; 10 | const args = []; 11 | 12 | let currToken = this.peek (); 13 | 14 | while ( currToken.type !== 'OpcodeCreateObj' && currToken.type !== 'OpcodeFuncCall' ) 15 | { 16 | const arg = this.parseUntil ('OpcodePush'); 17 | 18 | assert (arg.length <= 1, `Argument has more than one expression at ${startPos}`); 19 | 20 | args.push (arg[0]); 21 | currToken = this.peek (); 22 | } 23 | 24 | this.advance (); 25 | 26 | if ( currToken.type === 'OpcodeCreateObj' ) 27 | { 28 | return this.parseObjectDecl (currToken, args); 29 | } 30 | 31 | return this.parseFuncCall (currToken.value, args); 32 | }; 33 | 34 | const parseObjectDecl = function ( token, args ) 35 | { 36 | const classExpr = args.shift (); 37 | const nameExpr = args.shift (); 38 | 39 | const node = new DSOObjectDeclNode (classExpr, nameExpr, args); 40 | 41 | node.parentName = token.value[0]; 42 | node.isDataBlock = token.value[1]; 43 | 44 | node.slots = this.parseUntil ('OpcodeObjSection', this.controlBlock, false); 45 | node.placeAtRoot = this.advance ().value; 46 | node.subObjects = this.parseUntil ('OpcodeObjSection'); 47 | 48 | if ( node.placeAtRoot ) 49 | { 50 | // If this is a root object, it'll have a preceding 0 for some reason, so we drop that. 51 | this.popNode (); 52 | } 53 | 54 | return node; 55 | }; 56 | 57 | const parseFuncCall = function ( values, args ) 58 | { 59 | const funcName = values[0]; 60 | const callType = values[2]; 61 | 62 | let namespace = values[1]; 63 | 64 | if ( namespace === null && callType === 1 ) 65 | { 66 | namespace = args.shift (); 67 | } 68 | 69 | return new DSOFuncCallNode (funcName, namespace, callType, args); 70 | }; 71 | 72 | 73 | export { parsePushFrame, parseObjectDecl, parseFuncCall }; 74 | -------------------------------------------------------------------------------- /common/strings/StringStream.js: -------------------------------------------------------------------------------- 1 | import clamp from '~/util/clamp.js'; 2 | 3 | 4 | /** 5 | * A class for easily creating multiline strings. 6 | */ 7 | class StringStream 8 | { 9 | /** 10 | * @param {string} [initialValue=""] - Initial string value. 11 | * @param {integer} [intialIndent=0] - Initial indentation amount. 12 | */ 13 | constructor ( initialValue = '', initialIndent = 0 ) 14 | { 15 | this.value = initialValue; 16 | this.indentation = initialIndent; 17 | 18 | this.lines = 0; 19 | } 20 | 21 | /** 22 | * Write to the current string line. 23 | * 24 | * @param {string} [str=""] 25 | */ 26 | write ( str = '' ) 27 | { 28 | this.value += str; 29 | } 30 | 31 | /** 32 | * Write a new string line. 33 | * 34 | * @param {string} [line=""] 35 | */ 36 | writeLine ( line = '' ) 37 | { 38 | if ( this.value !== '' ) 39 | { 40 | this.value += '\n'; 41 | } 42 | 43 | this.writeIndent (); 44 | this.value += line; 45 | 46 | this.lines++; 47 | } 48 | 49 | writeIndent () 50 | { 51 | const { indentation } = this; 52 | 53 | for ( let i = 0; i < indentation; i++ ) 54 | { 55 | this.value += '\t'; 56 | } 57 | } 58 | 59 | /** 60 | * Write string to the current string line with spaces around it. 61 | * 62 | * @param {string} [str=""] 63 | */ 64 | writePadded ( str = '' ) 65 | { 66 | this.value += ` ${str} `; 67 | } 68 | 69 | /** 70 | * Increase the indentation amount. 71 | * 72 | * @param {integer} [amount=1] 73 | */ 74 | indent ( amount = 1 ) 75 | { 76 | this.indentation += amount; 77 | } 78 | 79 | /** 80 | * Decrease the indentation amount. 81 | * 82 | * @param {integer} [amount=1] 83 | */ 84 | unindent ( amount = 1 ) 85 | { 86 | this.indentation = Math.max (0, this.indentation - amount); 87 | } 88 | 89 | /** 90 | * @returns {string} The string value. 91 | */ 92 | toString () 93 | { 94 | return this.value; 95 | } 96 | } 97 | 98 | 99 | export default StringStream; 100 | -------------------------------------------------------------------------------- /decompiler/DSOParser/parseMisc.js: -------------------------------------------------------------------------------- 1 | import DSOPackageNode from '~/DSONode/DSOPackageNode.js'; 2 | import DSOFuncDeclNode from '~/DSONode/DSOFuncDeclNode.js'; 3 | import DSOReturnNode from '~/DSONode/DSOReturnNode.js'; 4 | 5 | import assert from '~/util/assert.js'; 6 | 7 | import { DSOParserError } from '~/decompiler/errors.js'; 8 | import { DSOStmtNode } from '~/DSONode/DSONode.js'; 9 | 10 | import { DSOElseNode, DSOBreakNode, DSOContinueNode } from '~/DSONode/DSOJumpNode.js'; 11 | 12 | 13 | const parseFuncDecl = function ( token ) 14 | { 15 | const { funcName, namespace, packageName, args } = token; 16 | 17 | let node = new DSOFuncDeclNode (funcName, namespace, packageName, args); 18 | node.body = this.parseUntil ('MarkerFuncEnd'); 19 | 20 | if ( packageName !== null ) 21 | { 22 | const prevNode = this.peekNode (); 23 | 24 | if ( prevNode instanceof DSOPackageNode && prevNode.name === packageName ) 25 | { 26 | prevNode.addFunction (node); 27 | node = null; 28 | } 29 | else 30 | { 31 | node = new DSOPackageNode (packageName, node); 32 | } 33 | } 34 | 35 | return node; 36 | }; 37 | 38 | const parseReturn = function ( returnsValue ) 39 | { 40 | const prevNode = this.peekNode (); 41 | 42 | if ( returnsValue && prevNode !== null && prevNode.isReturnable () ) 43 | { 44 | return new DSOReturnNode (this.popNode ()); 45 | } 46 | 47 | return new DSOReturnNode (); 48 | }; 49 | 50 | const parseJump = function ( token ) 51 | { 52 | const { controlBlock } = this; 53 | 54 | const sourceIP = token.value[0]; 55 | const destIP = token.value[1]; 56 | 57 | assert (controlBlock.hasJump (sourceIP), `Jump at ${sourceIP} not found!`); 58 | 59 | const jumpInfo = controlBlock.getJump (sourceIP); 60 | 61 | switch ( jumpInfo.type ) 62 | { 63 | case 'ifElse': return new DSOElseNode (sourceIP, destIP); 64 | case 'break': return new DSOBreakNode (sourceIP, destIP); 65 | case 'continue': return new DSOContinueNode (sourceIP, destIP); 66 | } 67 | 68 | throw new DSOParserError (`Unknown jump type \`${jumpInfo.type}\``); 69 | }; 70 | 71 | 72 | export { parseFuncDecl, parseReturn, parseJump }; 73 | -------------------------------------------------------------------------------- /decompiler/DSOControlBlock/analyzeJumps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Goes through each jump and determines what type it is. 3 | * 4 | * The goal is to determine all the jumps that can only be made by breaks and continues, then 5 | * default the rest to if-elses. 6 | */ 7 | const analyzeJumps = function () 8 | { 9 | if ( this.type !== 'root' ) 10 | { 11 | throw new Error ('analyzeJumps() should only be called on the root block'); 12 | } 13 | 14 | const { jumps } = this; 15 | 16 | for ( let [sourceIP, jump] of jumps ) 17 | { 18 | if ( !jump.assumedType ) 19 | { 20 | continue; 21 | } 22 | 23 | const { destIP, block } = jump; 24 | const { end, parent } = block; 25 | 26 | const outerLoop = block.getOuterLoop (); 27 | 28 | if ( block.type === 'loop' ) 29 | { 30 | if ( destIP === end ) 31 | { 32 | jump.setType ('break'); 33 | } 34 | else 35 | { 36 | // If the OP_JMP is just directly in a loop, it's a continue. 37 | jump.setType ('continue'); 38 | } 39 | } 40 | else if ( outerLoop !== null ) 41 | { 42 | if ( destIP === outerLoop.end ) 43 | { 44 | jump.setType ('break'); 45 | } 46 | else if ( (sourceIP + 2) !== end ) 47 | { 48 | // If the jump is in a conditional, but it's not at the end, it's a continue. 49 | jump.setType ('continue'); 50 | } 51 | else if ( parent !== null && parent.type === 'conditional' && destIP > parent.end ) 52 | { 53 | // If the parent is a conditional, and the destination is higher than its end ip, 54 | // it's a continue. 55 | jump.setType ('continue'); 56 | } 57 | } 58 | 59 | if ( jump.type === 'continue' ) 60 | { 61 | block.setContinuePoint (destIP); 62 | } 63 | } 64 | 65 | // Once the continue point has been determined (if there is one), we can set all jumps to it as 66 | // continues, just in case. 67 | for ( let [sourceIP, jump] of jumps ) 68 | { 69 | const { destIP, block } = jump; 70 | 71 | const outerLoop = block.getOuterLoop (); 72 | 73 | if ( outerLoop !== null && destIP === outerLoop.continuePoint && jump.type === 'ifElse' ) 74 | { 75 | jump.setType ('continue'); 76 | } 77 | } 78 | }; 79 | 80 | 81 | export { analyzeJumps }; 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dso.js 2 | A DSO decompiler for Blockland. 3 | 4 | 5 | ## Installation 6 | 7 | `npm install dso.js` 8 | 9 | 10 | ## Usage 11 | 12 | For client-side, use with the [`buffer`](https://npmjs.com/package/buffer) package. 13 | 14 | For server-side, you can just use the native `Buffer` class. 15 | 16 | 17 | #### Browser Example: 18 | 19 | ```js 20 | import { Buffer } from 'buffer/'; 21 | import { decompiler } from 'dso.js'; 22 | 23 | 24 | // An HTML element with the type "file" and an ID of "fileUpload" 25 | document.getElementById ('fileUpload').onchange = function ( event ) 26 | { 27 | if ( event.target.files.length <= 0 ) 28 | { 29 | return; 30 | } 31 | 32 | const file = event.target.files[0]; 33 | const reader = new FileReader (); 34 | 35 | reader.readAsArrayBuffer (file); 36 | 37 | reader.onload = function ( readerEvent ) 38 | { 39 | const buffer = Buffer.from (readerEvent.target.result); 40 | 41 | let codeString; 42 | 43 | try 44 | { 45 | codeString = decompiler.decompileDSO (buffer); 46 | } 47 | catch ( error ) 48 | { 49 | console.error ('Decompiler Error:', error); 50 | return; 51 | } 52 | 53 | console.log (codeString); 54 | }; 55 | }; 56 | 57 | ``` 58 | 59 | 60 | #### Node.js Example: 61 | 62 | ```js 63 | const fs = require ('fs'); 64 | 65 | const { decompiler } = require ('dso.js'); 66 | 67 | 68 | fs.readFile ('./myFile.cs.dso', ( error, buffer ) => 69 | { 70 | if ( error ) 71 | { 72 | console.error (error); 73 | return; 74 | } 75 | 76 | let codeString; 77 | 78 | try 79 | { 80 | codeString = decompiler.decompileDSO (buffer); 81 | } 82 | catch ( decompilerError ) 83 | { 84 | console.error ('[!] Decompiler Error:', decompilerError); 85 | return; 86 | } 87 | 88 | fs.writeFileSync ('./myFile.cs', codeString); 89 | }); 90 | ``` 91 | 92 | 93 | #### Decompiler Options 94 | 95 | The optional second argument of `decompileDSO` is an options object. 96 | 97 | The only one at the moment is the `outputArray` boolean, which is `false` by default. When set to `true`, it makes `decompileDSO` output a nested code array instead of a string. 98 | 99 | Enable the option like this: 100 | 101 | ```js 102 | decompiler.decompileDSO (buffer, { outputArray: true }); 103 | ``` 104 | -------------------------------------------------------------------------------- /decompiler/DSOParser/parseNext.js: -------------------------------------------------------------------------------- 1 | import DSOConstantNode from '~/DSONode/DSOConstantNode.js'; 2 | import DSOUnaryNode from '~/DSONode/DSOUnaryNode.js'; 3 | 4 | import { DSOParserError } from '~/decompiler/errors.js'; 5 | 6 | 7 | const parseNext = function () 8 | { 9 | const tokenPos = this.currPos; 10 | const token = this.advance (); 11 | 12 | const { type } = token; 13 | 14 | console.log(`DEBUG: ${JSON.stringify(token)} (${type}) @ ${tokenPos}`); 15 | 16 | switch ( type ) 17 | { 18 | case 'OpcodeLoadImmed': 19 | { 20 | return new DSOConstantNode (token.op, token.value); 21 | } 22 | 23 | case 'OpcodeUnary': 24 | { 25 | return new DSOUnaryNode (token.op, this.popNode ()); 26 | } 27 | 28 | case 'OpcodeReturn': 29 | { 30 | return this.parseReturn (token.returnsValue); 31 | } 32 | 33 | case 'OpcodeJump': 34 | { 35 | return this.parseJump (token); 36 | } 37 | 38 | case 'OpcodeJumpIfNot': 39 | { 40 | return this.parseBlock (token); 41 | } 42 | 43 | case 'OpcodeSetCurVar': 44 | case 'OpcodeSetVarArr': 45 | { 46 | return this.parseVariable (token, type === 'OpcodeSetVarArr'); 47 | } 48 | 49 | case 'OpcodeSetCurObject': 50 | case 'OpcodeSetFieldArr': 51 | { 52 | return this.parseSlot (token.op, type === 'OpcodeSetFieldArr'); 53 | } 54 | 55 | case 'OpcodeSaveVar': 56 | case 'OpcodeSaveField': 57 | { 58 | return this.parseAssign (); 59 | } 60 | 61 | case 'OpcodeBinary': 62 | case 'OpcodeCompareStr': 63 | case 'OpcodeLogicJump': 64 | { 65 | if ( this.advanceIfType ('OpcodeSaveVar') || this.advanceIfType ('OpcodeSaveField') ) 66 | { 67 | return this.parseAssign (token.op); 68 | } 69 | 70 | return this.parseBinary (token.op, type); 71 | } 72 | 73 | case 'OpcodeStringStart': 74 | { 75 | return this.parseString (token); 76 | } 77 | 78 | case 'OpcodePushFrame': 79 | { 80 | return this.parsePushFrame (); 81 | } 82 | 83 | case 'OpcodeFuncDecl': 84 | { 85 | return this.parseFuncDecl (token); 86 | } 87 | 88 | case 'OpcodeFuncCall': 89 | { 90 | console.log('DEBUG: not handling function calls!'); 91 | return null; 92 | } 93 | 94 | default: 95 | { 96 | throw new DSOParserError (`Unhandled token type \`${type}\` at ${tokenPos}`); 97 | } 98 | } 99 | }; 100 | 101 | 102 | export { parseNext }; 103 | -------------------------------------------------------------------------------- /common/ArrayMap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A map of arrays. 3 | */ 4 | class ArrayMap 5 | { 6 | constructor () 7 | { 8 | this.map = new Map (); 9 | this.size = 0; 10 | } 11 | 12 | /** 13 | * Pushes a value at the key. 14 | * 15 | * @param {*} key 16 | * @param {*} value 17 | * 18 | * @returns {integer} Number of values now at that key. 19 | */ 20 | push ( key, value ) 21 | { 22 | const { map } = this; 23 | 24 | if ( !map.has (key) ) 25 | { 26 | map.set (key, []); 27 | } 28 | 29 | map.get (key).push (value); 30 | this.size++; 31 | 32 | return map.get (key).length; 33 | } 34 | 35 | /** 36 | * Pops a value from a key. 37 | * 38 | * @param {*} key 39 | * @returns {*} 40 | */ 41 | pop ( key ) 42 | { 43 | const { map } = this; 44 | 45 | let popped = null; 46 | 47 | if ( map.has (key) ) 48 | { 49 | const array = map.get (key); 50 | 51 | popped = array.pop (); 52 | this.size--; 53 | 54 | // Delete empty arrays to prevent potential memory leaks. 55 | if ( array.length <= 0 ) 56 | { 57 | map.delete (key); 58 | } 59 | } 60 | 61 | return popped; 62 | } 63 | 64 | /** 65 | * Deletes all values at a key. 66 | * 67 | * @param {*} key 68 | */ 69 | delete ( key ) 70 | { 71 | const { map } = this; 72 | 73 | if ( map.has (key) ) 74 | { 75 | this.size -= map.get (key).length; 76 | } 77 | 78 | map.delete (key); 79 | } 80 | 81 | /** 82 | * Get all values at a key. 83 | * 84 | * @param {*} key 85 | * @returns {Array|null} An array of all the values, or null if there are no values. 86 | */ 87 | get ( key ) 88 | { 89 | if ( this.map.has (key) ) 90 | { 91 | return this.map.get (key).slice (); 92 | } 93 | 94 | return null; 95 | } 96 | 97 | /** 98 | * @param {*} key 99 | * @returns {boolean} 100 | */ 101 | has ( key, value ) 102 | { 103 | return this.map.has (key); 104 | } 105 | 106 | /** 107 | * Gets number of values at a key. 108 | * 109 | * @param {*} key 110 | * @returns {integer} 111 | */ 112 | count ( key ) 113 | { 114 | return this.map.has (key) ? this.map.get (key).length : 0; 115 | } 116 | 117 | clear () 118 | { 119 | this.map.clear (); 120 | this.size = 0; 121 | } 122 | 123 | *[Symbol.iterator] () 124 | { 125 | const { map } = this; 126 | 127 | for ( let [key, array] of map ) 128 | { 129 | const { length } = array; 130 | 131 | for ( let i = 0; i < length; i++ ) 132 | { 133 | yield [key, array[i]]; 134 | } 135 | } 136 | } 137 | } 138 | 139 | 140 | export default ArrayMap; 141 | -------------------------------------------------------------------------------- /decompiler/DSOParser/parseBlock.js: -------------------------------------------------------------------------------- 1 | import DSOConditionalNode from '~/DSONode/DSOConditionalNode.js'; 2 | import DSOLoopNode from '~/DSONode/DSOLoopNode.js'; 3 | 4 | import assert from '~/util/assert.js'; 5 | 6 | import { DSOElseNode } from '~/DSONode/DSOJumpNode.js'; 7 | 8 | import { DSOParserError } from '~/decompiler/errors.js'; 9 | 10 | 11 | const parseBlock = function ( token ) 12 | { 13 | const { controlBlock } = this; 14 | 15 | const start = token.value[0]; 16 | const end = token.value[1]; 17 | 18 | assert (controlBlock.hasBlock (start), `Control block at ${start} not found!`); 19 | 20 | const subBlock = controlBlock.getBlock (start); 21 | 22 | if ( subBlock.type === 'conditional' ) 23 | { 24 | return this.parseConditional (subBlock); 25 | } 26 | 27 | return this.parseLoop (subBlock); 28 | }; 29 | 30 | const parseConditional = function ( block ) 31 | { 32 | const ifBlock = this.parseUntil ('MarkerCondEnd', block); 33 | 34 | const { length } = ifBlock; 35 | 36 | let elseBlock = null; 37 | 38 | if ( length > 0 ) 39 | { 40 | const last = ifBlock[length - 1]; 41 | 42 | if ( last instanceof DSOElseNode ) 43 | { 44 | ifBlock.pop (); 45 | 46 | elseBlock = this.parseUntil ('MarkerElseEnd', block); 47 | } 48 | } 49 | 50 | return new DSOConditionalNode (this.popNode (), ifBlock, elseBlock); 51 | }; 52 | 53 | const parseLoop = function ( block ) 54 | { 55 | const contPoint = block.continuePoint; 56 | 57 | const endToken = (contPoint === null ? 'MarkerLoopEnd' : 'MarkerContinuePoint'); 58 | const loopBody = this.parseUntil (endToken, block, false); 59 | 60 | const marker = this.advance (); 61 | 62 | const testExpr = this.popNode (); 63 | 64 | let initialExpr = null; 65 | let endExpr = null; 66 | 67 | if ( contPoint === null ) 68 | { 69 | // Drop repeat test expression. 70 | loopBody.pop (); 71 | } 72 | else 73 | { 74 | const destIP = marker.value; 75 | const check = (destIP === contPoint); 76 | 77 | assert (check, `Mistmatched continue points (loop: ${contPoint} jump: ${destIP})`); 78 | 79 | const endBody = this.parseUntil ('MarkerLoopEnd', block); 80 | 81 | // Drop repeat test expression. 82 | endBody.pop (); 83 | 84 | if ( endBody.length == 1 ) 85 | { 86 | initialExpr = this.popNode (); 87 | endExpr = endBody.pop (); 88 | } 89 | else if ( endBody.length > 1 ) 90 | { 91 | throw new DSOParserError (`Too many expressions after continue point ${contPoint}`); 92 | } 93 | } 94 | 95 | return new DSOLoopNode (testExpr, loopBody, initialExpr, endExpr); 96 | }; 97 | 98 | 99 | export { parseBlock, parseConditional, parseLoop }; 100 | -------------------------------------------------------------------------------- /decompiler/DSOCodeGenerator/generateExpr.js: -------------------------------------------------------------------------------- 1 | import DSOConstantNode from '~/DSONode/DSOConstantNode.js'; 2 | import DSOVariableNode from '~/DSONode/DSOVariableNode.js'; 3 | import DSOSlotNode from '~/DSONode/DSOSlotNode.js'; 4 | import DSOUnaryNode from '~/DSONode/DSOUnaryNode.js'; 5 | import DSOBinaryNode from '~/DSONode/DSOBinaryNode.js'; 6 | import DSOAssignNode from '~/DSONode/DSOAssignNode.js'; 7 | import DSOStrConcatNode from '~/DSONode/DSOStrConcatNode.js'; 8 | import DSOCommaCatNode from '~/DSONode/DSOCommaCatNode.js'; 9 | import DSOFuncCallNode from '~/DSONode/DSOFuncCallNode.js'; 10 | import DSOConditionalNode from '~/DSONode/DSOConditionalNode.js'; 11 | import DSOObjectDeclNode from '~/DSONode/DSOObjectDeclNode.js'; 12 | 13 | import { DSOCodeGeneratorError } from '~/decompiler/errors.js'; 14 | 15 | 16 | const generateExpr = function ( node ) 17 | { 18 | const { constructor } = node; 19 | 20 | switch ( constructor ) 21 | { 22 | case DSOConstantNode: 23 | { 24 | return this.generateConstant (node); 25 | } 26 | 27 | case DSOVariableNode: 28 | { 29 | return this.generateVariable (node); 30 | } 31 | 32 | case DSOSlotNode: 33 | { 34 | return this.generateSlot (node); 35 | } 36 | 37 | case DSOUnaryNode: 38 | { 39 | return this.generateUnary (node); 40 | } 41 | 42 | case DSOBinaryNode: 43 | { 44 | return this.generateBinary (node); 45 | } 46 | 47 | case DSOAssignNode: 48 | { 49 | return this.generateAssign (node); 50 | } 51 | 52 | case DSOStrConcatNode: 53 | { 54 | return this.generateStrConcat (node); 55 | } 56 | 57 | case DSOCommaCatNode: 58 | { 59 | return this.generateCommaCat (node); 60 | } 61 | 62 | case DSOFuncCallNode: 63 | { 64 | return this.generateFuncCall (node); 65 | } 66 | 67 | case DSOConditionalNode: 68 | { 69 | return this.generateTernary (node); 70 | } 71 | 72 | case DSOObjectDeclNode: 73 | { 74 | return this.generateObjectDecl (node); 75 | } 76 | 77 | default: 78 | { 79 | throw new DSOCodeGeneratorError (`Unhandled generator expression \`${name}\``); 80 | } 81 | } 82 | }; 83 | 84 | const generateParens = function ( node ) 85 | { 86 | const generated = this.generateExpr (node); 87 | 88 | if ( node.inParens ) 89 | { 90 | return ['(', generated, ')']; 91 | } 92 | 93 | return generated; 94 | }; 95 | 96 | const generateBranch = function ( node, branch ) 97 | { 98 | if ( this.shouldAddParens (node, branch) ) 99 | { 100 | return this.generateParens (branch); 101 | } 102 | 103 | return this.generateExpr (branch); 104 | }; 105 | 106 | 107 | export { generateExpr, generateParens, generateBranch }; 108 | -------------------------------------------------------------------------------- /decompiler/DSOLoader/readNumber.js: -------------------------------------------------------------------------------- 1 | import { DSOLoaderError } from '~/decompiler/errors.js'; 2 | 3 | import 4 | { 5 | SIZE_INT8, 6 | SIZE_INT16, 7 | SIZE_INT32, 8 | 9 | SIZE_F64, 10 | 11 | numberTypes, 12 | } 13 | from '~/common/numbers/constants.js'; 14 | 15 | const { UInt8, UInt16LE, UInt16BE, UInt32LE, UInt32BE, DoubleLE, DoubleBE } = numberTypes; 16 | 17 | 18 | const readNumber = function ( type ) 19 | { 20 | let number; 21 | let size; 22 | 23 | switch ( type ) 24 | { 25 | case UInt8: 26 | { 27 | number = this.buffer.readUInt8 (this.currPos); 28 | size = SIZE_INT8; 29 | 30 | break; 31 | } 32 | 33 | case UInt16LE: 34 | { 35 | number = this.buffer.readUInt16LE (this.currPos); 36 | size = SIZE_INT16; 37 | 38 | break; 39 | } 40 | 41 | case UInt16BE: 42 | { 43 | number = this.buffer.readUInt16BE (this.currPos); 44 | size = SIZE_INT16; 45 | 46 | break; 47 | } 48 | 49 | case UInt32LE: 50 | { 51 | number = this.buffer.readUInt32LE (this.currPos); 52 | size = SIZE_INT32; 53 | 54 | break; 55 | } 56 | 57 | case UInt32BE: 58 | { 59 | number = this.buffer.readUInt32BE (this.currPos); 60 | size = SIZE_INT32; 61 | 62 | break; 63 | } 64 | 65 | case DoubleLE: 66 | { 67 | number = this.buffer.readDoubleLE (this.currPos); 68 | size = SIZE_F64; 69 | 70 | break; 71 | } 72 | 73 | case DoubleBE: 74 | { 75 | number = this.buffer.readDoubleBE (this.currPos); 76 | size = SIZE_F64; 77 | 78 | break; 79 | } 80 | 81 | default: 82 | { 83 | throw new DSOLoaderError (`Invalid number type \`${type}\``); 84 | } 85 | } 86 | 87 | this.advance (size); 88 | 89 | return number; 90 | }; 91 | 92 | const readInteger = function ( littleEndian = true ) 93 | { 94 | if ( littleEndian ) 95 | { 96 | return this.readNumber (UInt32LE); 97 | } 98 | 99 | return this.readNumber (UInt32BE); 100 | }; 101 | 102 | const readFloat = function ( littleEndian = true ) 103 | { 104 | if ( littleEndian ) 105 | { 106 | return this.readNumber (DoubleLE); 107 | } 108 | 109 | return this.readNumber (DoubleBE); 110 | }; 111 | 112 | const readFloatTable = function () 113 | { 114 | const table = []; 115 | const size = this.readInteger (true); 116 | 117 | for ( let i = 0; i < size; i++ ) 118 | { 119 | table.push (this.readFloat ()); 120 | } 121 | 122 | return table; 123 | }; 124 | 125 | const readCodeByte = function () 126 | { 127 | let op = this.readNumber (UInt8); 128 | 129 | if ( op === 0xFF ) 130 | { 131 | op = this.readNumber (UInt32LE); 132 | } 133 | 134 | return op; 135 | }; 136 | 137 | 138 | export { readNumber, readInteger, readFloat, readFloatTable, readCodeByte }; 139 | -------------------------------------------------------------------------------- /decompiler/DSOCodeGenerator/DSOCodeGenerator.js: -------------------------------------------------------------------------------- 1 | import DSOBinaryNode from '~/DSONode/DSOBinaryNode.js'; 2 | 3 | import assert from '~/util/assert.js'; 4 | 5 | import { has } from '~/util/has.js'; 6 | 7 | import * as generateStmt from '~/DSOCodeGenerator/generateStmt.js'; 8 | import * as generateConstant from '~/DSOCodeGenerator/generateConstant.js'; 9 | import * as generateFuncCall from '~/DSOCodeGenerator/generateFuncCall.js'; 10 | import * as generateObjectDecl from '~/DSOCodeGenerator/generateObjectDecl.js'; 11 | import * as generateAssign from '~/DSOCodeGenerator/generateAssign.js'; 12 | import * as generateLoop from '~/DSOCodeGenerator/generateLoop.js'; 13 | import * as generateExpr from '~/DSOCodeGenerator/generateExpr.js'; 14 | import * as generateCode from '~/DSOCodeGenerator/generateCode.js'; 15 | import * as generateNary from '~/DSOCodeGenerator/generateNary.js'; 16 | import * as generateConcat from '~/DSOCodeGenerator/generateConcat.js'; 17 | import * as generateConditional from '~/DSOCodeGenerator/generateConditional.js'; 18 | import * as generateFuncDecl from '~/DSOCodeGenerator/generateFuncDecl.js'; 19 | 20 | 21 | /** 22 | * Generates TorqueScript code from DSONodes. 23 | * 24 | * @usage Create a DSOCodeGenerator instance with an array of DSONodes, then call .generateCode() 25 | */ 26 | class DSOCodeGenerator 27 | { 28 | /** 29 | * @param {DSONode[]} nodes 30 | */ 31 | constructor ( nodes ) 32 | { 33 | this.nodes = nodes; 34 | this.indentation = 0; 35 | } 36 | 37 | /** 38 | * @param {DSONode[]} [nodes=this.nodes] 39 | * @returns {string} 40 | */ 41 | generateCode ( nodes = this.nodes ) 42 | { 43 | return this.generateCodeString (this.generateCodeArray (nodes)); 44 | } 45 | 46 | /** 47 | * @param {DSONode} node1 48 | * @param {DSONode} node2 49 | * 50 | * @returns {boolean} 51 | */ 52 | shouldAddParens ( node1, node2 ) 53 | { 54 | if ( node1.constructor === node2.constructor && node1.isAssociative () && 55 | node2.isAssociative () ) 56 | { 57 | if ( node1 instanceof DSOBinaryNode ) 58 | { 59 | return node1.op !== node2.op; 60 | } 61 | 62 | return false; 63 | } 64 | 65 | return node2.inParens && node2.getPrecedence () >= node1.getPrecedence (); 66 | } 67 | 68 | /** 69 | * @param {Array} array 70 | */ 71 | indent ( array ) 72 | { 73 | array.push (++this.indentation); 74 | } 75 | 76 | /** 77 | * @param {Array} array 78 | */ 79 | unindent ( array ) 80 | { 81 | array.push (--this.indentation); 82 | } 83 | } 84 | 85 | Object.assign (DSOCodeGenerator.prototype, 86 | { 87 | ...generateStmt, ...generateConstant, 88 | ...generateFuncCall, ...generateObjectDecl, 89 | ...generateAssign, ...generateLoop, 90 | ...generateExpr, ...generateCode, 91 | ...generateNary, ...generateConcat, 92 | ...generateConditional, ...generateFuncDecl, 93 | }); 94 | 95 | 96 | export default DSOCodeGenerator; 97 | -------------------------------------------------------------------------------- /decompiler/DSOCodeGenerator/generateCode.js: -------------------------------------------------------------------------------- 1 | import StringStream from '~/common/strings/StringStream.js'; 2 | 3 | import { arrayPeek } from '~/util/arrays.js'; 4 | import { isKeyword } from '~/common/keywords.js'; 5 | 6 | import { isOperator, isUnaryOperator } from '~/common/operators.js'; 7 | 8 | 9 | // Characters that prevent a closing bracket from adding a newline. 10 | const noNewlines = new Set (['}', ')', ']', ',', '.', '?', ':', ';']); 11 | 12 | 13 | const generateCodeArray = function ( nodeArray = this.nodes ) 14 | { 15 | const { length } = nodeArray; 16 | 17 | const array = []; 18 | 19 | for ( let i = 0; i < length; i++ ) 20 | { 21 | array.push (this.generateStmt (nodeArray[i])); 22 | } 23 | 24 | return array; 25 | }; 26 | 27 | const generateCodeString = function ( array = [] ) 28 | { 29 | array = array.flat (Infinity); 30 | 31 | const codeStr = new StringStream (); 32 | 33 | const { length } = array; 34 | 35 | for ( let i = 0; i < length; i++ ) 36 | { 37 | const str = array[i]; 38 | 39 | switch ( str ) 40 | { 41 | case ',': 42 | { 43 | codeStr.write (', '); 44 | break; 45 | } 46 | 47 | case '{': 48 | { 49 | codeStr.writeLine ('{\n'); 50 | codeStr.indent (); 51 | codeStr.writeIndent (); 52 | 53 | break; 54 | } 55 | 56 | case '}': 57 | { 58 | codeStr.unindent (); 59 | codeStr.writeLine ('}'); 60 | 61 | if ( !noNewlines.has (arrayPeek (array, i + 1)) ) 62 | { 63 | codeStr.write ('\n'); 64 | codeStr.writeIndent (); 65 | } 66 | 67 | break; 68 | } 69 | 70 | case ';': 71 | { 72 | codeStr.write (';'); 73 | 74 | if ( arrayPeek (array, i + 1) !== '}' ) 75 | { 76 | codeStr.write ('\n'); 77 | codeStr.writeIndent (); 78 | } 79 | 80 | break; 81 | } 82 | 83 | case ';\\': 84 | { 85 | codeStr.write ('; '); 86 | break; 87 | } 88 | 89 | default: 90 | { 91 | if ( isUnaryOperator (str) && str !== '-' ) 92 | { 93 | if ( str === '' ) 94 | { 95 | codeStr.write ('-'); 96 | } 97 | else 98 | { 99 | codeStr.write (str); 100 | } 101 | } 102 | else if ( isOperator (str) ) 103 | { 104 | codeStr.writePadded (str); 105 | } 106 | else if ( isKeyword (str) ) 107 | { 108 | const peek = arrayPeek (array, i + 1); 109 | 110 | if ( peek === ';' ) 111 | { 112 | codeStr.write (str); 113 | } 114 | else 115 | { 116 | codeStr.write (str + ' '); 117 | } 118 | } 119 | else 120 | { 121 | codeStr.write (str); 122 | 123 | if ( str !== '(' && arrayPeek (array, i + 1) === '(' ) 124 | { 125 | codeStr.write (' '); 126 | } 127 | } 128 | 129 | break; 130 | } 131 | } 132 | } 133 | 134 | return codeStr.toString (); 135 | }; 136 | 137 | 138 | export { generateCodeArray, generateCodeString }; 139 | -------------------------------------------------------------------------------- /decompiler/DSOLoader/DSOLoader.js: -------------------------------------------------------------------------------- 1 | import assert from '~/util/assert.js'; 2 | 3 | import { DSOLoaderError } from '~/decompiler/errors.js'; 4 | 5 | import { DSO_VERSION } from '~/common/constants.js'; 6 | 7 | import * as getTableValue from '~/DSOLoader/getTableValue.js'; 8 | import * as readNumber from '~/DSOLoader/readNumber.js'; 9 | import * as readString from '~/DSOLoader/readString.js'; 10 | 11 | 12 | /** 13 | * Reads raw DSO file buffer and unencrypts string tables -- does not parse opcodes. 14 | * 15 | * @usage Create a DSOLoader instance with a file buffer as the argument, then call .read() 16 | */ 17 | class DSOLoader 18 | { 19 | /** 20 | * @param {Buffer} buffer - File buffer for open DSO file. 21 | */ 22 | constructor ( buffer = null ) 23 | { 24 | if ( buffer === null ) 25 | { 26 | throw new DSOLoaderError ('Missing required argument: `buffer`'); 27 | } 28 | 29 | this.buffer = buffer; 30 | this.currPos = 0; 31 | 32 | this.code = []; 33 | 34 | this.globalStringTable = null; 35 | this.functionStringTable = null; 36 | 37 | this.globalFloatTable = null; 38 | this.functionFloatTable = null; 39 | 40 | this.lineBreakPairs = []; 41 | this.identTable = {}; 42 | } 43 | 44 | read () 45 | { 46 | const fileVersion = this.readInteger (true); 47 | 48 | if ( fileVersion !== DSO_VERSION ) 49 | { 50 | throw new DSOLoaderError (`Invalid DSO version: Expected ${DSO_VERSION}, got ${fileVersion}`); 51 | } 52 | 53 | this.globalStringTable = this.readStringTable (); 54 | this.globalFloatTable = this.readFloatTable (); 55 | this.functionStringTable = this.readStringTable (); 56 | this.functionFloatTable = this.readFloatTable (); 57 | 58 | const codeSize = this.readInteger (true); 59 | const numLineBreakPairs = this.readInteger (true); 60 | 61 | for ( let i = 0; i < codeSize; i++ ) 62 | { 63 | this.code.push (this.readCodeByte ()); 64 | } 65 | 66 | const totalSize = codeSize + numLineBreakPairs * 2; 67 | 68 | for ( let i = codeSize; i < totalSize; i++ ) 69 | { 70 | this.lineBreakPairs.push (this.readInteger (true)); 71 | } 72 | 73 | let numIdent = this.readInteger (true); 74 | 75 | while ( numIdent-- ) 76 | { 77 | let index = this.readInteger (true); 78 | let count = this.readInteger (true); 79 | 80 | while ( count-- ) 81 | { 82 | const ip = this.readInteger (true); 83 | 84 | this.code[ip] = index; 85 | this.identTable[ip] = index; 86 | } 87 | } 88 | } 89 | 90 | advance ( amount = 1 ) 91 | { 92 | if ( amount < 1 ) 93 | { 94 | throw new DSOLoaderError (`Cannot advance ${amount} bytes - must be greater than 0`); 95 | } 96 | 97 | if ( this.currPos + amount > this.buffer.length ) 98 | { 99 | throw new DSOLoaderError (`Cannot advance ${amount} bytes - exceeds buffer length`); 100 | } 101 | 102 | this.currPos += amount; 103 | 104 | return this.buffer[this.currPos - amount]; 105 | } 106 | } 107 | 108 | Object.assign (DSOLoader.prototype, { ...getTableValue, ...readNumber, ...readString }); 109 | 110 | 111 | export default DSOLoader; 112 | -------------------------------------------------------------------------------- /decompiler/opcodes/types.js: -------------------------------------------------------------------------------- 1 | import { createOpset } from '~/common/opcodes.js'; 2 | 3 | 4 | const opcodeTypes = 5 | { 6 | // Opcodes with no additional values. 7 | OpcodeSingle: createOpset ( 8 | [ 9 | 'OP_STR_TO_UINT', 10 | 'OP_STR_TO_FLT', 11 | 'OP_STR_TO_NONE', 12 | 'OP_STR_TO_NONE_2', 13 | 'OP_FLT_TO_UINT', 14 | 'OP_FLT_TO_STR', 15 | 'OP_FLT_TO_NONE', 16 | 'OP_UINT_TO_FLT', 17 | 'OP_UINT_TO_STR', 18 | 'OP_UINT_TO_NONE', 19 | 'OP_NOT', 20 | 'OP_NOTF', 21 | 'OP_ONESCOMPLEMENT', 22 | 'OP_NEG', 23 | 'OP_ADD', 24 | 'OP_SUB', 25 | 'OP_DIV', 26 | 'OP_MUL', 27 | 'OP_XOR', 28 | 'OP_MOD', 29 | 'OP_BITAND', 30 | 'OP_BITOR', 31 | 'OP_CMPLT', 32 | 'OP_CMPGR', 33 | 'OP_CMPGE', 34 | 'OP_CMPLE', 35 | 'OP_CMPEQ', 36 | 'OP_CMPNE', 37 | 'OP_OR', 38 | 'OP_AND', 39 | 'OP_SHR', 40 | 'OP_SHL', 41 | 'OP_COMPARE_STR', 42 | 'OP_SAVEVAR_STR', 43 | 'OP_SAVEVAR_UINT', 44 | 'OP_SAVEVAR_FLT', 45 | 'OP_LOADVAR_STR', 46 | 'OP_LOADVAR_UINT', 47 | 'OP_LOADVAR_FLT', 48 | 'OP_SAVEFIELD_STR', 49 | 'OP_SAVEFIELD_UINT', 50 | 'OP_SAVEFIELD_FLT', 51 | 'OP_LOADFIELD_STR', 52 | 'OP_LOADFIELD_UINT', 53 | 'OP_LOADFIELD_FLT', 54 | 'OP_ADVANCE_STR_NUL', 55 | 'OP_SETCURVAR_ARRAY', 56 | 'OP_SETCURVAR_ARRAY_CREATE', 57 | 'OP_SETCURFIELD_ARRAY', 58 | 'OP_SETCUROBJECT', 59 | 'OP_SETCUROBJECT_NEW', 60 | 'OP_PUSH_FRAME', 61 | 'OP_PUSH', 62 | 'OP_RETURN', 63 | 'OP_BREAK', 64 | 65 | 'OP_NOOP', // rndtrash: no-op 66 | 'OP_ADVANCE_STR_NUL_1', // rndtrash: alias 67 | ]), 68 | 69 | // Opcodes with a single value after them. 70 | OpcodeSinglePrefix: createOpset ( 71 | [ 72 | 'OP_LOADIMMED_UINT', 73 | 'OP_LOADIMMED_FLT', 74 | 'OP_LOADIMMED_STR', 75 | 'OP_LOADIMMED_IDENT', 76 | 'OP_TAG_TO_STR', 77 | 78 | 'OP_JMP', 79 | 80 | 'OP_JMPIF', 81 | 'OP_JMPIFF', 82 | 83 | 'OP_JMPIF_NP', 84 | 'OP_JMPIFNOT_NP', 85 | 86 | 'OP_SETCURVAR', 87 | 'OP_SETCURVAR_CREATE', 88 | 'OP_SETCURFIELD', 89 | 90 | 'OP_ADD_OBJECT', 91 | 'OP_END_OBJECT', 92 | ]), 93 | 94 | // Opcodes with three values after them. 95 | OpcodeTriplePrefix: createOpset ( 96 | [ 97 | 'OP_CALLFUNC', 98 | 'OP_CALLFUNC_RESOLVE', 99 | 'OP_CREATE_OBJECT', 100 | ]), 101 | 102 | // Opcodes that start a string section. 103 | OpcodeStringStart: createOpset ( 104 | [ 105 | 'OP_ADVANCE_STR', 106 | 'OP_ADVANCE_STR_APPENDCHAR', 107 | 'OP_ADVANCE_STR_COMMA', 108 | ]), 109 | 110 | // Opcodes that end a string section. 111 | OpcodeStringEnd: createOpset ( 112 | [ 113 | 'OP_REWIND_STR', 114 | 'OP_TERMINATE_REWIND_STR', 115 | ]), 116 | 117 | // Opcodes that start a loop or conditional. 118 | OpcodeJumpIfNot: createOpset ( 119 | [ 120 | 'OP_JMPIFNOT', 121 | 'OP_JMPIFFNOT', 122 | ]), 123 | 124 | // Special type for the function declaration opcode just because it's so different. 125 | OpcodeFuncDecl: createOpset ( 126 | [ 127 | 'OP_FUNC_DECL', 128 | ]), 129 | 130 | // Opcodes that throw an error should we come across them. 131 | OpcodeError: createOpset ( 132 | [ 133 | 'OP_INVALID', 134 | ]), 135 | }; 136 | 137 | 138 | export default opcodeTypes; 139 | -------------------------------------------------------------------------------- /decompiler/DSOParser/DSOParser.js: -------------------------------------------------------------------------------- 1 | import clamp from '~/util/clamp.js'; 2 | 3 | import * as parseNext from '~/DSOParser/parseNext.js'; 4 | import * as parseBinary from '~/DSOParser/parseBinary.js'; 5 | import * as parseAssign from '~/DSOParser/parseAssign.js'; 6 | import * as parsePushFrame from '~/DSOParser/parsePushFrame.js'; 7 | import * as parseBlock from '~/DSOParser/parseBlock.js'; 8 | import * as parseString from '~/DSOParser/parseString.js'; 9 | import * as parseMisc from '~/DSOParser/parseMisc.js'; 10 | 11 | 12 | /** 13 | * Parses an array DSOTokens into DSONodes. 14 | * 15 | * @usage Create a DSOParser instance with DSOTokens array and DSOControlBlock instance, then 16 | * call .parse() 17 | */ 18 | class DSOParser 19 | { 20 | /** 21 | * @param {DSOToken[]} tokens 22 | * @param {DSOControlBlock} controlBlock 23 | */ 24 | constructor ( tokens, controlBlock ) 25 | { 26 | this.tokens = tokens; 27 | this.controlBlock = controlBlock; 28 | 29 | this.currPos = 0; 30 | this.untilType = null; 31 | this.isRunning = false; 32 | this.nodes = []; 33 | } 34 | 35 | /** 36 | * @param {integer} [start=this.currPos] - The position in the token stream to start at. 37 | * @param {string|null} [untilType=null] - If not null, what type of token to stop at. 38 | */ 39 | parse ( start = this.currPos, untilType = null ) 40 | { 41 | this.currPos = start; 42 | this.untilType = untilType; 43 | 44 | this.isRunning = true; 45 | 46 | while ( this.isRunning && !this.isAtEnd () ) 47 | { 48 | const node = this.parseNext (); 49 | 50 | if ( node !== null ) 51 | { 52 | this.pushNode (node); 53 | } 54 | } 55 | 56 | this.isRunning = false; 57 | 58 | return this.nodes; 59 | } 60 | 61 | parseUntil ( untilType, block = this.controlBlock, skipType = true ) 62 | { 63 | const parser = new DSOParser (this.tokens, block); 64 | const nodes = parser.parse (this.currPos, untilType); 65 | 66 | // +1 if we want to skip past the `untilType` token. 67 | this.seek (skipType ? parser.currPos + 1 : parser.currPos); 68 | 69 | return nodes; 70 | } 71 | 72 | isAtEnd () 73 | { 74 | if ( this.currPos >= this.tokens.length ) 75 | { 76 | return true; 77 | } 78 | 79 | if ( this.untilType !== null && this.peek ().type === this.untilType ) 80 | { 81 | return true; 82 | } 83 | 84 | return false; 85 | } 86 | 87 | seek ( amount ) 88 | { 89 | this.currPos = clamp (amount, 0, this.tokens.length); 90 | } 91 | 92 | peek () 93 | { 94 | return this.tokens[this.currPos]; 95 | } 96 | 97 | advance () 98 | { 99 | return this.tokens[this.currPos++]; 100 | } 101 | 102 | advanceIfType ( type ) 103 | { 104 | if ( this.peek ().type === type ) 105 | { 106 | this.advance (); 107 | return true; 108 | } 109 | 110 | return false; 111 | } 112 | 113 | pushNode ( node ) 114 | { 115 | this.nodes.push (node); 116 | } 117 | 118 | popNode () 119 | { 120 | return this.nodes.pop (); 121 | } 122 | 123 | peekNode () 124 | { 125 | return this.nodes.length > 0 ? this.nodes[this.nodes.length - 1] : null; 126 | } 127 | } 128 | 129 | Object.assign (DSOParser.prototype, 130 | { 131 | ...parseNext, ...parseBinary, 132 | ...parseAssign, ...parsePushFrame, 133 | ...parseBlock, ...parseString, 134 | ...parseMisc, 135 | }); 136 | 137 | 138 | export default DSOParser; 139 | -------------------------------------------------------------------------------- /decompiler/DSODisassembler/DSODisassembler.js: -------------------------------------------------------------------------------- 1 | import ArrayMap from '~/ArrayMap.js'; 2 | import DSOControlBlock from '~/DSOControlBlock/DSOControlBlock.js'; 3 | 4 | import { has } from '~/util/has.js'; 5 | 6 | import { getOpcodeSubtype } from '~/decompiler/opcodes/getOpcodeType.js'; 7 | 8 | import * as scanNext from '~/DSODisassembler/scanNext.js'; 9 | import * as handleSingle from '~/DSODisassembler/handleSingle.js'; 10 | import * as handleMarkers from '~/DSODisassembler/handleMarkers.js'; 11 | import * as handleFuncDecl from '~/DSODisassembler/handleFuncDecl.js'; 12 | import * as handleMisc from '~/DSODisassembler/handleMisc.js'; 13 | 14 | 15 | /** 16 | * Disassembles the DSO code into easily-parsable tokens. 17 | * 18 | * @usage Create a new DSODisassembler instance with DSOLoader and DSOControlBlock instances, 19 | * then call .disassemble() 20 | */ 21 | class DSODisassembler 22 | { 23 | /** 24 | * @param {DSOLoader} loader - DSOLoader with the code, identifier table, etc. 25 | * @param {DSOControlBlock} controlBlock - For data on loops vs. conditionals, jumps, etc. 26 | */ 27 | constructor ( loader, controlBlock ) 28 | { 29 | this.loader = loader; 30 | this.currBlock = controlBlock; 31 | 32 | this.ip = 0; 33 | 34 | this.blockStack = []; 35 | this.jumpEnds = new ArrayMap (); 36 | this.funcEndIP = null; 37 | 38 | this.tokens = []; 39 | } 40 | 41 | /** 42 | * @returns {integer[]} A stream of easily-parsable tokens. 43 | */ 44 | disassemble () 45 | { 46 | // First, we make an initial pass through the code to turn everything into tokens. 47 | while ( !this.isAtEnd () ) 48 | { 49 | const token = this.scanNext (); 50 | 51 | if ( token !== null ) 52 | { 53 | this.pushToken (token); 54 | } 55 | } 56 | 57 | // Drop the final OP_RETURN that's at the end of every file. 58 | this.popToken (); 59 | 60 | return this.tokens; 61 | } 62 | 63 | isAtEnd () 64 | { 65 | return this.ip >= this.code.length; 66 | } 67 | 68 | peek () 69 | { 70 | return this.code[this.ip]; 71 | } 72 | 73 | advance () 74 | { 75 | return this.code[this.ip++]; 76 | } 77 | 78 | advanceIfSubtype ( subtype ) 79 | { 80 | if ( getOpcodeSubtype (this.peek ()) === subtype ) 81 | { 82 | this.advance (); 83 | return true; 84 | } 85 | 86 | return false; 87 | } 88 | 89 | advanceIdent () 90 | { 91 | if ( this.hasIdent (this.ip) ) 92 | { 93 | return this.loader.getString (this.advance (), false); 94 | } 95 | 96 | this.advance (); 97 | return null; 98 | } 99 | 100 | advanceConstant ( op ) 101 | { 102 | const inFunction = this.funcEndIP !== null; 103 | const value = this.advance (); 104 | 105 | return this.loader.getTableValue (op, value, inFunction); 106 | } 107 | 108 | hasIdent ( ip ) 109 | { 110 | return has (this.identTable, ip); 111 | } 112 | 113 | pushToken ( token ) 114 | { 115 | this.tokens.push (token); 116 | } 117 | 118 | popToken () 119 | { 120 | return this.tokens.pop (); 121 | } 122 | 123 | pushBlock ( block ) 124 | { 125 | this.blockStack.push (block); 126 | } 127 | 128 | popBlock () 129 | { 130 | return this.blockStack.pop (); 131 | } 132 | 133 | get code () 134 | { 135 | return this.loader.code; 136 | } 137 | 138 | get identTable () 139 | { 140 | return this.loader.identTable; 141 | } 142 | } 143 | 144 | Object.assign (DSODisassembler.prototype, 145 | { 146 | ...scanNext, 147 | 148 | ...handleSingle, ...handleMarkers, 149 | ...handleFuncDecl, ...handleMisc, 150 | }); 151 | 152 | 153 | export default DSODisassembler; 154 | -------------------------------------------------------------------------------- /decompiler/opcodes/subtypes.js: -------------------------------------------------------------------------------- 1 | import { createOpset } from '~/common/opcodes.js'; 2 | 3 | 4 | const opcodeSubtypes = 5 | { 6 | OpcodeConvert: createOpset ( 7 | [ 8 | 'OP_STR_TO_UINT', 9 | 'OP_STR_TO_FLT', 10 | 11 | 'OP_FLT_TO_UINT', 12 | 'OP_FLT_TO_STR', 13 | 14 | 'OP_UINT_TO_FLT', 15 | 'OP_UINT_TO_STR', 16 | ]), 17 | 18 | OpcodeTypeToNone: createOpset ( 19 | [ 20 | 'OP_STR_TO_NONE', 21 | 'OP_STR_TO_NONE_2', 22 | 'OP_FLT_TO_NONE', 23 | 'OP_UINT_TO_NONE', 24 | ]), 25 | 26 | OpcodeLoadImmed: createOpset ( 27 | [ 28 | 'OP_LOADIMMED_UINT', 29 | 'OP_LOADIMMED_FLT', 30 | 'OP_LOADIMMED_STR', 31 | 'OP_LOADIMMED_IDENT', 32 | 'OP_TAG_TO_STR', 33 | ]), 34 | 35 | OpcodeJump: createOpset ( 36 | [ 37 | 'OP_JMP', 38 | ]), 39 | 40 | OpcodeLoopJump: createOpset ( 41 | [ 42 | 'OP_JMPIF', 43 | 'OP_JMPIFF', 44 | ]), 45 | 46 | OpcodeJumpIfNot: createOpset ( 47 | [ 48 | 'OP_JMPIFNOT', 49 | 'OP_JMPIFFNOT', 50 | ]), 51 | 52 | OpcodeLogicJump: createOpset ( 53 | [ 54 | 'OP_JMPIF_NP', 55 | 'OP_JMPIFNOT_NP', 56 | ]), 57 | 58 | OpcodeUnary: createOpset ( 59 | [ 60 | 'OP_NOT', 61 | 'OP_NOTF', 62 | 'OP_ONESCOMPLEMENT', 63 | 'OP_NEG', 64 | ]), 65 | 66 | OpcodeBinary: createOpset ( 67 | [ 68 | 'OP_ADD', 69 | 'OP_SUB', 70 | 'OP_DIV', 71 | 'OP_MUL', 72 | 'OP_MOD', 73 | 74 | 'OP_BITAND', 75 | 'OP_BITOR', 76 | 'OP_XOR', 77 | 'OP_SHL', 78 | 'OP_SHR', 79 | 80 | 'OP_CMPLT', 81 | 'OP_CMPGR', 82 | 'OP_CMPGE', 83 | 'OP_CMPLE', 84 | 'OP_CMPEQ', 85 | 'OP_CMPNE', 86 | 87 | 'OP_OR', 88 | 'OP_AND', 89 | ]), 90 | 91 | OpcodeCompareStr: createOpset ( 92 | [ 93 | 'OP_COMPARE_STR', 94 | ]), 95 | 96 | OpcodeSaveVar: createOpset ( 97 | [ 98 | 'OP_SAVEVAR_STR', 99 | 'OP_SAVEVAR_UINT', 100 | 'OP_SAVEVAR_FLT', 101 | ]), 102 | 103 | OpcodeLoadVar: createOpset ( 104 | [ 105 | 'OP_LOADVAR_STR', 106 | 'OP_LOADVAR_UINT', 107 | 'OP_LOADVAR_FLT', 108 | ]), 109 | 110 | OpcodeSaveField: createOpset ( 111 | [ 112 | 'OP_SAVEFIELD_STR', 113 | 'OP_SAVEFIELD_UINT', 114 | 'OP_SAVEFIELD_FLT', 115 | ]), 116 | 117 | OpcodeLoadField: createOpset ( 118 | [ 119 | 'OP_LOADFIELD_STR', 120 | 'OP_LOADFIELD_UINT', 121 | 'OP_LOADFIELD_FLT', 122 | ]), 123 | 124 | OpcodeSetCurVar: createOpset ( 125 | [ 126 | 'OP_SETCURVAR', 127 | 'OP_SETCURVAR_CREATE', 128 | ]), 129 | 130 | OpcodeSetCurField: createOpset ( 131 | [ 132 | 'OP_SETCURFIELD', 133 | ]), 134 | 135 | OpcodeSetVarArr: createOpset ( 136 | [ 137 | 'OP_SETCURVAR_ARRAY', 138 | 'OP_SETCURVAR_ARRAY_CREATE', 139 | ]), 140 | 141 | OpcodeSetFieldArr: createOpset ( 142 | [ 143 | 'OP_SETCURFIELD_ARRAY', 144 | ]), 145 | 146 | OpcodeSetCurObject: createOpset ( 147 | [ 148 | 'OP_SETCUROBJECT', 149 | 'OP_SETCUROBJECT_NEW', 150 | ]), 151 | 152 | OpcodeStringStart: createOpset ( 153 | [ 154 | 'OP_ADVANCE_STR', 155 | 'OP_ADVANCE_STR_APPENDCHAR', 156 | 'OP_ADVANCE_STR_COMMA', 157 | ]), 158 | 159 | OpcodeStringEnd: createOpset ( 160 | [ 161 | 'OP_REWIND_STR', 162 | 'OP_TERMINATE_REWIND_STR', 163 | ]), 164 | 165 | OpcodeFuncDecl: createOpset ( 166 | [ 167 | 'OP_FUNC_DECL', 168 | ]), 169 | 170 | OpcodeFuncCall: createOpset ( 171 | [ 172 | 'OP_CALLFUNC', 173 | 'OP_CALLFUNC_RESOLVE', 174 | ]), 175 | 176 | OpcodeCreateObj: createOpset ( 177 | [ 178 | 'OP_CREATE_OBJECT', 179 | ]), 180 | 181 | OpcodeObjSection: createOpset ( 182 | [ 183 | 'OP_ADD_OBJECT', 184 | 'OP_END_OBJECT', 185 | ]), 186 | 187 | OpcodePushFrame: createOpset ( 188 | [ 189 | 'OP_PUSH_FRAME', 190 | ]), 191 | 192 | OpcodePush: createOpset ( 193 | [ 194 | 'OP_PUSH', 195 | ]), 196 | 197 | OpcodeReturn: createOpset ( 198 | [ 199 | 'OP_RETURN', 200 | ]), 201 | 202 | // Opcodes we can skip. 203 | OpcodeSkip: createOpset ( 204 | [ 205 | 'OP_ADVANCE_STR_NUL', 206 | 'OP_ADVANCE_STR_NUL_1', 207 | 'OP_NOOP', 208 | ]), 209 | 210 | // Opcodes that throw an error should we come across them. 211 | OpcodeError: createOpset ( 212 | [ 213 | 'OP_INVALID', 214 | ]), 215 | 216 | OpcodeMisc: createOpset ( 217 | [ 218 | 'OP_BREAK', 219 | ]), 220 | }; 221 | 222 | 223 | export default opcodeSubtypes; 224 | -------------------------------------------------------------------------------- /decompiler/DSOControlBlock/DSOControlBlock.js: -------------------------------------------------------------------------------- 1 | import DSOJumpInfo from '~/DSOControlBlock/DSOJumpInfo.js'; 2 | 3 | import assert from '~/util/assert.js'; 4 | 5 | import * as scan from '~/DSOControlBlock/scan.js'; 6 | import * as loops from '~/DSOControlBlock/loops.js'; 7 | import * as analyzeJumps from '~/DSOControlBlock/analyzeJumps.js'; 8 | 9 | 10 | /** 11 | * For sorting out various code ambiguities (e.g. loops vs. conditionals, what OP_JMPs are, etc.) 12 | * 13 | * @usage Create a DSOControlBlock instance with 0 and code.length as the arguments, then call 14 | * .scan(code) and .analyzeJumps() 15 | */ 16 | class DSOControlBlock 17 | { 18 | /** 19 | * @param {integer} start 20 | * @param {integer} end 21 | * @param {DSOControlBlock|null} [parent=null] 22 | */ 23 | constructor ( start, end, parent = null ) 24 | { 25 | this.start = start; 26 | this.end = end; 27 | this.parent = parent; 28 | this.type = (parent === null ? 'root' : 'conditional'); 29 | 30 | this.jumps = (this.type === 'root' ? new Map () : null); 31 | this.blocks = (this.type === 'root' ? new Map () : null); 32 | this.continuePoint = null; 33 | } 34 | 35 | /** 36 | * @param {integer} start 37 | * @param {integer} end 38 | * @param {string} [type="conditional"] 39 | */ 40 | addBlock ( start, end, parent = null ) 41 | { 42 | let block; 43 | 44 | if ( this.type === 'root' ) 45 | { 46 | block = new DSOControlBlock (start, end, parent); 47 | 48 | this.blocks.set (start, block); 49 | } 50 | else 51 | { 52 | block = this.parent.addBlock (start, end, parent); 53 | } 54 | 55 | return block; 56 | } 57 | 58 | /** 59 | * @param {integer} sourceIP 60 | * @param {integer} destIP 61 | * @param {DSOControlBlock} block 62 | * 63 | * @returns {DSOJumpInfo} 64 | */ 65 | addJump ( sourceIP, destIP, block ) 66 | { 67 | let jump; 68 | 69 | if ( this.type === 'root' ) 70 | { 71 | jump = new DSOJumpInfo (sourceIP, destIP, block); 72 | 73 | this.jumps.set (sourceIP, jump); 74 | } 75 | else 76 | { 77 | jump = this.parent.addJump (sourceIP, destIP, block); 78 | } 79 | 80 | return jump; 81 | } 82 | 83 | /** 84 | * @param {integer} ip 85 | */ 86 | setContinuePoint ( ip ) 87 | { 88 | if ( this.type === 'loop' ) 89 | { 90 | const check = (this.continuePoint === null || ip === this.continuePoint); 91 | 92 | assert (check, `Multiple continue points for control block starting at ${this.start}`); 93 | 94 | this.continuePoint = ip; 95 | } 96 | else 97 | { 98 | const loop = this.getOuterLoop (); 99 | 100 | assert (loop !== null, `Continue point ${ip} not in a loop`); 101 | assert (loop.isInBounds (ip), `Continue point ${ip} past loop end ${loop.end}`); 102 | 103 | loop.setContinuePoint (ip); 104 | } 105 | } 106 | 107 | /** 108 | * @param {integer} start 109 | * @returns {boolean} 110 | */ 111 | hasBlock ( start ) 112 | { 113 | if ( this.type === 'root' ) 114 | { 115 | return this.blocks.has (start); 116 | } 117 | 118 | return this.parent.hasBlock (start); 119 | } 120 | 121 | /** 122 | * @param {integer} sourceIP 123 | * @returns {boolean} 124 | */ 125 | hasJump ( sourceIP ) 126 | { 127 | if ( this.type === 'root' ) 128 | { 129 | return this.jumps.has (sourceIP); 130 | } 131 | 132 | return this.parent.hasJump (sourceIP); 133 | } 134 | 135 | /** 136 | * @param {integer} start 137 | * @returns {DSOControlBlock|null} null if not found 138 | */ 139 | getBlock ( start ) 140 | { 141 | if ( this.type === 'root' ) 142 | { 143 | return this.hasBlock (start) ? this.blocks.get (start) : null; 144 | } 145 | 146 | return this.parent.getBlock (start); 147 | } 148 | 149 | /** 150 | * @param {integer} sourceIP 151 | * @returns {DSOJumpInfo|null} null if not found 152 | */ 153 | getJump ( sourceIP ) 154 | { 155 | if ( this.type === 'root' ) 156 | { 157 | return this.hasJump (sourceIP) ? this.jumps.get (sourceIP) : null; 158 | } 159 | 160 | return this.parent.getJump (sourceIP); 161 | } 162 | 163 | /** 164 | * Whether or not an ip falls within this control block. 165 | * 166 | * @param {integer} ip 167 | * @returns {boolean} 168 | */ 169 | isInBounds ( ip ) 170 | { 171 | return ip >= this.start && ip <= this.end; 172 | } 173 | } 174 | 175 | Object.assign (DSOControlBlock.prototype, { ...scan, ...loops, ...analyzeJumps }); 176 | 177 | 178 | export default DSOControlBlock; 179 | -------------------------------------------------------------------------------- /common/opcodes.js: -------------------------------------------------------------------------------- 1 | import { has } from '~/util/has.js'; 2 | 3 | import enumerate from '~/util/enumerate.js'; 4 | 5 | 6 | const names = 7 | [ 8 | 'OP_LOADVAR_STR', /* 0x00 */ 9 | 'OP_LOADVAR_FLT', /* 0x01 */ 10 | 'OP_SAVEFIELD_FLT', /* 0x02 */ 11 | 'OP_LOADFIELD_FLT', /* 0x03 */ 12 | 'OP_UINT_TO_FLT', /* 0x04 */ 13 | 'OP_ADVANCE_STR_NUL', /* 0x05 */ 14 | 'OP_UINT_TO_STR', /* 0x06 */ 15 | 'OP_UINT_TO_NONE', /* 0x07 */ 16 | 'FILLER1', /* 0x08 */ 17 | 'OP_ADD_OBJECT', /* 0x09 */ 18 | 'FILLER2', /* 0x0A */ 19 | 'OP_CALLFUNC_RESOLVE', /* 0x0B */ 20 | 'OP_FLT_TO_UINT', /* 0x0C */ 21 | 'OP_FLT_TO_STR', /* 0x0D */ 22 | 'FILLER3', /* 0x0E */ 23 | 'OP_LOADVAR_UINT', /* 0x0F */ 24 | 'OP_SAVEVAR_STR', /* 0x10 */ 25 | 'OP_JMPIFNOT', /* 0x11 */ 26 | 'OP_SAVEVAR_FLT', /* 0x12 */ 27 | 'OP_ADVANCE_STR_APPENDCHAR', /* 0x13 */ 28 | 'OP_TERMINATE_REWIND_STR', /* 0x14 */ 29 | 'OP_ADVANCE_STR', /* 0x15 */ 30 | 'OP_CMPLE', /* 0x16 */ 31 | 'OP_SETCURFIELD', /* 0x17 */ 32 | 'OP_SETCURFIELD_ARRAY', /* 0x18 */ 33 | 'OP_JMPIF_NP', /* 0x19 */ 34 | 'OP_JMPIF', /* 0x1A */ 35 | 'OP_JMP', /* 0x1B */ 36 | 'OP_INVALID', /* 0x1C */ 37 | 'OP_BITOR', /* 0x1D */ 38 | 'OP_SHL', /* 0x1E */ 39 | 'OP_SHR', /* 0x1F */ 40 | 'OP_STR_TO_NONE', /* 0x20 */ 41 | 'OP_COMPARE_STR', /* 0x21 */ 42 | 'OP_STR_TO_UINT', /* 0x22 */ 43 | 'OP_SETCUROBJECT', /* 0x23 */ 44 | 'OP_PUSH_FRAME', /* 0x24 */ 45 | 'OP_REWIND_STR', /* 0x25 */ 46 | 'FILLER4', /* 0x26 */ 47 | 'OP_CALLFUNC', /* 0x27 */ 48 | 'OP_MOD', /* 0x28 */ 49 | 'OP_LOADFIELD_UINT', /* 0x29 */ 50 | 'OP_JMPIFFNOT', /* 0x2A */ 51 | 'OP_JMPIFF', /* 0x2B */ 52 | 'OP_SAVEVAR_UINT', /* 0x2C */ 53 | 'OP_SUB', /* 0x2D */ 54 | 'OP_MUL', /* 0x2E */ 55 | 'OP_DIV', /* 0x2F */ 56 | 'OP_NEG', /* 0x30 */ 57 | 'OP_CMPEQ', /* 0x31 */ 58 | 'OP_CMPGR', /* 0x32 */ 59 | 'OP_CMPNE', /* 0x33 */ 60 | 'FILLER5', /* 0x34 */ 61 | 'OP_LOADIMMED_UINT', /* 0x35 */ 62 | 'OP_LOADIMMED_FLT', /* 0x36 */ 63 | 'OP_LOADIMMED_IDENT', /* 0x37 */ 64 | 'OP_TAG_TO_STR', /* 0x38 */ 65 | 'OP_LOADIMMED_STR', /* 0x39 */ 66 | 'OP_STR_TO_FLT', /* 0x3A */ 67 | 'OP_END_OBJECT', /* 0x3B */ 68 | 'OP_CMPLT', /* 0x3C */ 69 | 'OP_BREAK', /* 0x3D */ 70 | 'OP_SETCURVAR_CREATE', /* 0x3E */ 71 | 'OP_SETCUROBJECT_NEW', /* 0x3F */ 72 | 'OP_NOT', /* 0x40 */ 73 | 'OP_NOTF', /* 0x41 */ 74 | 'OP_SETCURVAR', /* 0x42 */ 75 | 'OP_SETCURVAR_ARRAY', /* 0x43 */ 76 | 'OP_ADD', /* 0x44 */ 77 | 'OP_SETCURVAR_ARRAY_CREATE', /* 0x45 */ 78 | 'OP_JMPIFNOT_NP', /* 0x46 */ 79 | 'OP_AND', /* 0x47 */ 80 | 'OP_RETURN', /* 0x48 */ 81 | 'OP_XOR', /* 0x49 */ 82 | 'OP_CMPGE', /* 0x4A */ 83 | 'OP_LOADFIELD_STR', /* 0x4B */ 84 | 'OP_SAVEFIELD_STR', /* 0x4C */ 85 | 'OP_BITAND', /* 0x4D */ 86 | 'OP_ONESCOMPLEMENT', /* 0x4E */ 87 | 'OP_ADVANCE_STR_COMMA', /* 0x4F */ 88 | 'OP_PUSH', /* 0x50 */ 89 | 'OP_FLT_TO_NONE', /* 0x51 */ 90 | 'OP_CREATE_OBJECT', /* 0x52 */ 91 | 'OP_FUNC_DECL', /* 0x53 */ 92 | ]; 93 | 94 | const enums = enumerate (names); 95 | 96 | /** 97 | * Checks if it's a non-filler opcode. 98 | * 99 | * @param {integer} op 100 | * @returns {boolean} 101 | */ 102 | const isOpcode = op => 103 | { 104 | if ( typeof op === 'number' ) 105 | { 106 | return has (names, op) && op !== enums.FILLER1 && op !== enums.FILLER2; 107 | } 108 | 109 | return has (enums, op) && op !== 'FILLER1' && op !== 'FILLER2'; 110 | }; 111 | 112 | /** 113 | * @param {string[]} - opnames 114 | * @returns {Set} 115 | */ 116 | const createOpset = opnames => 117 | { 118 | const opcodes = []; 119 | 120 | const { length } = opnames; 121 | 122 | for ( let i = 0; i < length; i++ ) 123 | { 124 | opcodes.push (enums[opnames[i]]); 125 | } 126 | 127 | return Object.freeze (new Set (opcodes)); 128 | }; 129 | 130 | 131 | export 132 | { 133 | enums, 134 | names, 135 | 136 | isOpcode, 137 | createOpset, 138 | }; 139 | --------------------------------------------------------------------------------