UNPKG

mcs

Version:

A pre-processor to write Minecraft Functions more efficiently

1,163 lines (1,072 loc) 64.8 kB
/* This will appear at the top/start of the final dist file */ (function() { var mcs = (function() { var mcs = function(input) { if (!input || typeof(input) !== 'string') { var err = 'Error: Object to parse is not a valid string or does not exist.'; console.error(err); return err; } /* Compile the AST into a final JSON object */ function Compiler(exp) { var debug = true, oldDebug = true, addTop = "", inFunc = false, current = [], currentFunc = '', namespace = '', prefix = []; // Environment is used to remember/manage the scope function Environment(parent) { this.vars = Object.create(parent ? parent.vars : null); this.parent = parent; } Environment.prototype = { extend: function() { return new Environment(this); }, lookup: function(name) { var scope = this; while (scope) { if (Object.prototype.hasOwnProperty.call(scope.vars, name)) return scope; scope = scope.parent; } }, get: function(name) { if (name in this.vars) return this.vars[name]; err("Undefined variable " + name); }, set: function(name, value) { var scope = this.lookup(name); // let's not allow defining globals from a nested environment if (!scope && this.parent) err("Undefined variable " + name); return (scope || this).vars[name] = value; }, def: function(name, value) { return this.vars[name] = value; } }; // Declare global/root scope var env = new Environment(); var output = {}; evaluate(exp, env); return output; // Actual logic function err(msg) { if (debug) console.error(msg); throw new Error(msg); } // Apply operator function apply_op(op, a, b) { function num(x) { if (typeof x != "number") err("Expected number but got " + x); return x; } function div(x) { if (num(x) == 0) err("Divide by zero"); return x; } if (op == "+" && (typeof a == "string" || typeof b == "string")) { return a + b; } switch (op) { case "+": return num(a) + num(b); case "-": return num(a) - num(b); case "*": return num(a) * num(b); case "/": return num(a) / div(b); case "%": return num(a) % div(b); case "^": return Math.pow(num(a), num(b)); case "&&": return a !== false && b; case "||": return a !== false ? a : b; case "<": return num(a) < num(b); case ">": return num(a) > num(b); case "<=": return num(a) <= num(b); case ">=": return num(a) >= num(b); case "==": return a === b; case "!=": return a !== b; } err("Can't apply operator " + op); } // Evaluates things quickly // Evaluates things normally except that it doesn't return anything except when the return keyword is used. // Also evaluates commands differently as they are added to the output function quickEvaler(exp, env) { function quickEval(element) { // Don't need to return commands, they get added automatically if (element.type == "command") make_command(env, element); else if (element.type == "return" || element.type == "if") { return evaluate(element, env); } else evaluate(element, env); } // If its more than one line if (exp.type == "prog") { // Loops through every exp for (var i = 0; i < exp.prog.length; i++) { // Get its value, return if needed var x = quickEval(exp.prog[i]); if (x) return x; } } // If its only one line else { // Get its value, return if needed var x = quickEval(exp); if (x) return x; } } // Create a JS macro to evaluate when called function make_macro(env, exp) { if (exp.name == "range") err("Range is a pre-defined macro, please use another name"); function macro() { var names = exp.vars; var scope = env.extend(); for (var i = 0; i < names.length; ++i) scope.def(names[i], i < arguments.length ? arguments[i] : false); var x = quickEvaler(exp.body, scope); return x; } return env.set(exp.name, macro); } // Macros have their name as a reg, so need to separate macros from actual regs function reg_or_macro(env, exp) { oldDebug = debug; try { debug = false; if (exp.value == "range") return range_macro; var possible = env.get(exp.value); debug = oldDebug; return possible; } catch (e) { debug = oldDebug; return exp.value; } } // If/else if/else statements function make_if(env, exp) { var cond = evaluate(exp.cond, env); if (cond !== false) { var x = quickEvaler(exp.then, env.extend()); if (x) return x; } else if (exp.else) { var y = quickEvaler(exp.else, env.extend()); if (y) return y; } return false; } // For loops function make_for(env, exp) { // Create a new env var newEnv = env.extend(); // Evaluate the first param (declaring variable) evaluate(exp.params[0], newEnv); // While the second param is valid while (evaluate(exp.params[1], newEnv)) { // Evaluate the for content evaluate(exp.then, newEnv); // Evaluate the last param (setting/modifying the variable) evaluate(exp.params[2], newEnv); } } // Foreach loops function make_foreach(env, exp) { // Create a new env var newEnv = env.extend(); // Get the array var arr = evaluate(exp.param, env); // Define the variable newEnv.def(exp.variable.value, 0); // Loop through all of them for (var i = 0; i < arr.length; i++) { // Set the variable to the correct value newEnv.set(exp.variable.value, arr[i]); // Evaluate evaluate(exp.then, newEnv); } } // Execute blocks function make_execute(env, exp) { // Evaluate all the values var selector = evaluate(exp.selector, env); var pos1 = evaluate(exp.pos[0], env); var pos2 = evaluate(exp.pos[1], env); var pos3 = evaluate(exp.pos[2], env); // Add prefix prefix.push("execute " + selector + " " + pos1 + " " + pos2 + " " + pos3 + " "); // Evaluate content evaluate(exp.prog, env.extend()); // pop prefix.pop(); } // Strings can also have evals inside them function make_string(env, exp) { // If it's an array, then it contains evals if (Array.isArray(exp.value)) { var final = ""; for (var i = 0; i < exp.value.length; i++) { if (exp.value[i].type == "prog") { var x = evaluate(exp.value[i], env.extend()); final += x; } else final += exp.value[i].value; } return final; } // If it's a basic string else { return exp.value; } } // Relatives can be evaluated function make_relative(env, exp) { if (exp.value && exp.value.length > 0) { var final = "~"; for (var i = 0; i < exp.value.length; i++) { final += evaluate(exp.value[i], env); } return final; } else { return "~"; } } // Selectors can be evaluated function make_selector(env, exp) { // If the selector's value is an array if (Array.isArray(exp.value)) { if (exp.value && exp.value.length > 0) { var final = exp.prefix + "["; for (var i = 0; i < exp.value.length; i++) { // Only compile ivars (already created variables/calls) if (exp.value[i].type == "ivar") { var x = evaluate(exp.value[i], env.extend()); final += x; } else final += exp.value[i].value; } return final + "]"; } else { return exp.prefix; } } else { return exp.value; } } // Create an array function make_array(env, exp) { var arr = {}; var index = 0; exp.value.forEach(function(element) { var obj = evaluate(element, env); arr[index] = obj; index++; }); return arr; } // Get the ivar's value function get_ivar(env, exp) { var ivar = env.get(exp.value); if (exp.index) { var index = evaluate(exp.index, env); if (typeof(index) != "number") err("Array index must be a number"); return ivar[index]; } else return ivar; } // Assign can either be a declaration or a modification function make_assign(env, exp) { // Modify a current variable, use set if (exp.left.type == "ivar") return env.set(exp.left.value, evaluate(exp.right, env)); // Declare a new variable, define (def) else if (exp.left.type == "var") { return env.def(exp.left.value, evaluate(exp.right, env)); } } // Need to compile JSON the way that MC would accept it function make_json(env, exp) { var json = "{"; for (var i = 0; i < exp.value.length; i++) { var jsonToAdd = ""; // if it's a string, add quotes around it if (exp.value[i].type == "str") { jsonToAdd = "\"" + evaluate(exp.value[i], env) + "\""; } // If it's an array, JSONinfy it else if (exp.value[i].type == "array") { var temp = evaluate(exp.value[i], env); jsonToAdd = JSON.stringify(Object.keys(temp).map(function(k) { return temp[k]; })); } // if it's something else, evaluate it, it might be something interesting else { jsonToAdd = evaluate(exp.value[i], env); } json += jsonToAdd; } json += "}"; return json; } // Create a comment and add it function make_comment(env, exp) { addToOutput(currentFunc, exp.value + "\n"); } // Create a command function make_command(env, exp) { var cmd = ""; var lastVal = ""; if (env.parent == null) err("Commands cannot be used in root"); for (var i = 0; i < exp.value.length; i++) { var valueToAdd = evaluate(exp.value[i], env); if (valueToAdd != ":" && lastVal != "" & lastVal != ":") cmd += " "; cmd += valueToAdd; lastVal = valueToAdd; } // Whenever a command is read, add it to the output var prefixToAdd = (prefix && prefix.length > 0) ? prefix.join('') : ''; addToOutput(currentFunc, prefixToAdd + cmd + "\n"); return cmd; } // Programs are anything inside a {} with more than one statement function make_prog(env, exp) { var final = ""; exp.prog.forEach(function(exp) { if (exp.type == "command") { var cmd = evaluate(exp, env); final += cmd + "\n"; } // Need to add returned items because of eval blocks else if (exp.type == "return") { var output = evaluate(exp, env); final += output; } else evaluate(exp, env); }); return final; } // Make a function, evaluate, get out of function function make_func(env, exp) { if (inFunc) err("Cannot declare a function inside another"); inFunc = true; currentFunc = exp.name; evaluate(exp.body, env.extend()); // No need to add anything to the output here, whenever a command is found, it adds it when read currentFunc = ''; inFunc = false; } // Make a group, evaluate inside, get out of group function make_group(env, exp) { if (inFunc) err("Groups cannot be inside functions"); current.push(exp.name); evaluate(exp.body, env.extend()); current.pop(); } function make_setting(env, exp) { if (env.parent != null) err("Settings must be declared in the root"); if (exp.name == "namespace") { if (namespace && namespace != "namespace") err("Cannot declare namespace more than once"); if (namespace && namespace == "_namespace") err("Please declare the namespace BEFORE writing any functions"); namespace = exp.value; } else { err("No setting found with the name " + exp.name); } } // Add the given key-value pair to the output function addToOutput(name, value) { // We need the namespace now, if it doesn't exist, set it! (Use _namespace so that there's less chance of conflict) if (!namespace) namespace = "_namespace"; if (!output[namespace]) output[namespace] = { _type: "namespace" }; // Get the current position to setup our group var curOutput = output[namespace]; // Check whether or not we are in a group if (current) { // Get the current group current.forEach(function(element) { if (!curOutput[element]) curOutput[element] = { _type: "group" }; curOutput = curOutput[element]; }); // If it doesn't exist yet, set instead of add (or else it says undefined at the start) if (curOutput.hasOwnProperty(name)) { curOutput[name].value += value; } else { curOutput[name] = { _type: "function", value: value }; } } // No groups else { // If it doesn't exist yet, set instead of add (or else it says undefined at the start) if (curOutput.hasOwnProperty(name)) { curOutput[name] += value; } else { curOutput[name] = { _type: "function", value: value }; } } } // Whether or not a JSON object is empty function isJSONEmpty(obj) { for (var prop in obj) { if (obj.hasOwnProperty(prop)) return false; } return JSON.stringify(obj) === JSON.stringify({}); } // Evaluates all the tokens and compiles commands function evaluate(exp, env) { switch (exp.type) { case "num": case "bool": case "kw": return exp.value; case "str": return make_string(env, exp); case "eval": return evaluate(exp.value, env.extend()); case "colon": return ":"; case "relative": return make_relative(env, exp); case "selector": return make_selector(env, exp); case "comma": return ","; case "json": return make_json(env, exp); case "reg": return reg_or_macro(env, exp); case "comment": return make_comment(env, exp); case "command": return make_command(env, exp); case "array": return make_array(env, exp); case "ivar": return get_ivar(env, exp); case "assign": return make_assign(env, exp); case "binary": return apply_op(exp.operator, evaluate(exp.left, env), evaluate(exp.right, env)); case "macro": return make_macro(env, exp); case "return": return evaluate(exp.value, env); case "if": return make_if(env, exp); case "for": return make_for(env, exp); case "foreach": return make_foreach(env, exp); case "execute": return make_execute(env, exp); case "function": return make_func(env, exp); case "group": return make_group(env, exp); case "setting": return make_setting(env, exp); case "prog": return make_prog(env, exp); case "call": var macro = evaluate(exp.func, env); return macro.apply(null, exp.args.map(function(arg) { return evaluate(arg, env); })); default: err("Unable to evaluate " + exp.type); } } } /* Advanced Parser InputStream reads characters TokenStream is the lexer (converts everything into tokens) Parser tries to create node structures (AST) out of the tokens Parser based on: http://lisperator.net/pltut/ */ // List of all commands that exist in mc var availableCommands = ["advancement", "ban", "blockdata", "clear", "clone", "debug", "defaultgamemode", "deop", "difficulty", "effect", "enchant", "entitydata", "execute", "fill", "function", "gamemode", "gamerule", "give", "help", "kick", "kill", "list", "locate", "me", "op", "pardon", "particle", "playsound", "publish", "recipe", "reload", "replaceitem", "save", "say", "scoreboard", "seed", "setblock", "setidletimeout", "setmaxplayers", "setworldspawn", "spawnpoint", "spreadplayers", "stats", "stop", "stopsound", "summon", "teleport", "tell", "tellraw", "testfor", "testforblock", "testforblocks", "time", "title", "toggledownfall", "tp", "transferserver", "trigger", "weather", "whitelist", "worldborder", "wsserver", "xp"]; // InputStream (Read input character by character) function InputStream(input) { var pos = 0, line = 1, col = 0, lastVal = null, lastWasNewLineVal = true; return { next: next, peek: peek, eof: eof, croak: croak, last: last, lastWasNewLine: lastWasNewLine }; function next() { // Knows whether or not we switched to a new line if (peek() == "\n") lastWasNewLineVal = true; else if ("\r\t ".indexOf(peek()) == -1) lastWasNewLineVal = false; // Get the last character lastVal = peek(); var ch = input.charAt(pos++); if (ch == "\n") line++, col = 0; else col++; return ch; } function last() { return lastVal; } function lastWasNewLine() { return lastWasNewLineVal; } function peek() { return input.charAt(pos); } function eof() { return peek() == ""; } function croak(msg) { var err = msg + ' at (' + line + ':' + col + ')'; console.error(err); throw new Error(err); } } // Lexer (converts everything into tokens) function TokenStream(input) { var current = null; // List of all keywords that are available var keywords = " function macro group if elseif else return execute true false var for foreach in "; var lastVal = null; return { next: next, peek: peek, eof: eof, croak: input.croak, last: last }; function is_keyword(x) { return keywords.indexOf(" " + x + " ") >= 0; } function is_digit(ch) { return /[0-9]/i.test(ch); } function is_id_start(ch) { return /[a-z0-9_\$]/i.test(ch); } function is_id(ch) { return is_id_start(ch); } function is_ivar(ch) { return /\$[a-z0-9-_]/i.test(ch); } function is_op_char(ch) { return "+-*/%^=&|<>!".indexOf(ch) >= 0; } function is_punc(ch) { return ",;(){}[]".indexOf(ch) >= 0; } function is_whitespace(ch) { return " \t\n\r".indexOf(ch) >= 0; } // Read until the given predicate returns false function read_while(predicate) { var str = ""; while (!input.eof() && predicate(input.peek())) str += input.next(); return str; } function try_number() { input.next(); if (is_digit(input.peek())) { var num = read_number(); num.value *= -1; return num; } input.croak("Can't handle character: " + input.peek()); } function read_number() { var has_dot = false; var number = read_while(function(ch) { if (ch == ".") { if (has_dot) return false; has_dot = true; return true; } return is_digit(ch); }); return { type: "num", value: parseFloat(number) }; } // Read identifiers, can return a keyword, an ivar ($variable), or reg (anything else) function read_ident() { var id = read_while(is_id); var type; if (is_keyword(id)) type = "kw"; else if (is_ivar(id)) type = "ivar" else type = "reg"; return { type: type, value: id }; } function read_escaped(end) { var escaped = false, str = ""; input.next(); while (!input.eof()) { var ch = input.next(); if (escaped) { str += ch; escaped = false; } else if (ch == "\\") { escaped = true; } else if (ch == end) { break; } else { str += ch; } } return str; } /* Evaluation blocks Inside a string, content inside `` will be parsed as if it was normal syntax. This allows for easier variable/macro integration: "math result: `math(1,2) + 2`" rather than "math result: " + (math(1,2) + 2) (Although the second option is still available if you need it). */ function read_evaled(val) { // Don't do it if it doesn't need evaluation if (val.indexOf("`") >= 0) { var evalBlock = false, final = [], str = ""; var arr = val.split(''); for (var i = 0; i < arr.length; i++) { var ch = arr[i]; // Currently in an eval block if (evalBlock) { if (ch == '`') { evalBlock = false; // Parse the whole thing as if it was a full code block var parsedEval = Parser(TokenStream(InputStream(str))); if (parsedEval.prog.length != 0) { for (var x = 0; x < parsedEval.prog.length; x++) { if (parsedEval.prog[x].type == "comment") { input.croak("Comments are not allowed in evaluation blocks"); } else if (parsedEval.prog[x].type == "function") { input.croak("Functions are not allowed in evaluation blocks"); } else if (parsedEval.prog[x].type == "macro") { input.croak("Creating macros is not allowed in evaluation blocks"); } } final.push(parsedEval); } str = ""; } else { str += ch; if (i == arr.length - 1) { if (str) final.push({ type: "str", value: str }); } } } // Don't evalBlock else { if (ch == '`') { evalBlock = true; if (str) final.push({ type: "str", value: str }); str = ""; } else { str += ch; if (i == arr.length - 1) { if (str) final.push({ type: "str", value: str }); } } } } return final; } else { return val; } } function read_string() { return { type: "str", value: read_evaled(read_escaped('"')) }; } function selector_or_setting() { var lastNewLine = input.lastWasNewLine(); input.next(); if (input.peek() == '!') { if (!lastNewLine) input.croak('Settings with "@!" need to start at the begining of a line'); return read_settings(); } else { return read_selector(); } } function read_selector() { var output = read_while(function(ch) { return (!is_whitespace(ch) && ch != ';') }); return { type: 'selector', value: '@' + output }; } function read_settings() { var output = read_while(function(ch) { return ch != "\n" }); return { type: "setting", value: output.replace('\r', '') }; } function read_relative() { var val = read_while(function(ch) { return (!is_whitespace(ch) && ch != ";"); }); return { type: 'relative', value: val }; } function read_colon() { input.next(); return { type: 'colon' }; } // Read comments that need to be added (#) function read_comment() { if (!input.lastWasNewLine()) input.croak('Comments with "#" need to start at the begining of a line'); var output = read_while(function(ch) { return ch != "\n" }); return { type: "comment", value: output.replace('\r', '') }; } function skip_comment() { read_while(function(ch) { return ch != "\n" }); input.next(); } // Check whether or not the line is a // comment, skip it if so function check_comment() { var output = read_while(function(ch) { return ch == "/" }); if (output == '//') { skip_comment(); } else { input.next(); } } // Read the next character, assign tokens function read_next() { read_while(is_whitespace); if (input.eof()) return null; var ch = input.peek(); if (ch == "#") { return read_comment(); } if (ch == "/") { check_comment(); return read_next(); } if (ch == "@") return selector_or_setting(); if (ch == '"') return read_string(); if (ch == "~") return read_relative(); if (ch == ":") return read_colon(); if (is_digit(ch)) return read_number(); if (is_id_start(ch)) return read_ident(); if (is_punc(ch)) return { type: "punc", value: input.next() }; if (is_op_char(ch)) return { type: "op", value: read_while(is_op_char) }; input.croak("Can't handle character: " + ch); } function peek() { return current || (current = read_next()); } function last() { return lastVal; } function next() { lastVal = peek(); var tok = current; current = null; return tok || read_next(); } function eof() { return peek() == null; } } // Parser (actually parses data) var FALSE = { type: "bool", value: false }; function Parser(input) { // Order of math operations, greater means included first var PRECEDENCE = { "=": 1, "||": 2, "&&": 3, "<": 7, ">": 7, "<=": 7, ">=": 7, "==": 7, "!=": 7, "+": 10, "-": 10, "*": 20, "/": 20, "%": 20, "^": 30, }; return parse_toplevel(); function is_punc(ch) { var tok = input.peek(); return tok && tok.type == "punc" && (!ch || tok.value == ch) && tok; } function is_kw(kw) { var tok = input.peek(); return tok && tok.type == "kw" && (!kw || tok.value == kw) && tok; } function is_op(op) { var tok = input.peek(); return tok && tok.type == "op" && (!op || tok.value == op) && tok; } function is_comment() { var tok = input.peek(); return tok && tok.type == "comment"; } function is_reg() { var tok = input.peek(); return tok && tok.type == "reg"; } function skip_punc(ch) { if (is_punc(ch)) input.next(); else input.croak("Expecting punctuation: \"" + ch + "\""); } function skip_comment(ch) { if (is_comment()) parse_comment(); input.next(); } function skip_kw(kw) { if (is_kw(kw)) input.next(); else input.croak("Expecting keyword: \"" + kw + "\""); } function skip_op(op) { if (is_op(op)) input.next(); else input.croak("Expecting operator: \"" + op + "\""); } function skip_comma() { if (is_punc(",")) { input.next(); return { type: "comma" }; } else input.croak("Expecting comma: \"" + JSON.stringify(input.peek()) + "\"") } function unexpected() { input.croak("Unexpected token: " + JSON.stringify(input.peek())); } // Check whether or not to parse this through binary operations function maybe_binary(left, my_prec) { var tok = is_op(); if (tok) { var his_prec = PRECEDENCE[tok.value]; if (his_prec > my_prec) { input.next(); return maybe_binary({ type: tok.value == "=" ? "assign" : "binary", operator: tok.value, left: left, right: maybe_binary(parse_atom(), his_prec) }, my_prec); } } return left; } // Parses through anything between start and stop, with separator, using the given parser function delimited(start, stop, separator, parser) { var a = [], first = true; skip_punc(start); while (!input.eof()) { if (is_punc(stop)) break; if (first) first = false; else if (check_last()) skip_punc(separator); if (is_punc(stop)) break; a.push(parser()); } skip_punc(stop); return a; } function parse_call(func) { return { type: "call", func: func, args: delimited("(", ")", ",", parse_expression), }; } // Variable names can't be ivar nor keyword, check that it's a reg function parse_varname() { var name = input.next(); if (name.type != "ivar") input.croak("Expecting variable name"); return name.value; } // Parse if statements, add elseif if there are some, and add else if there is one function parse_if() { skip_kw("if"); var cond = parse_expression(); var then = parse_expression(); var ret = { type: "if", cond: cond, then: then, }; if (is_kw("else")) { input.next(); ret.else = parse_expression(); } return ret; } // Parse a var declaration function parse_var() { skip_kw("var"); return { type: 'var', value: parse_varname() }; } // Parse a for loop function parse_for() { //input.croak("For loops are currently not supported"); skip_kw("for"); var params = delimited("(", ")", ";", parse_expression); var then = parse_expression(); return { type: "for", params: params, then: then, }; } // Parse a foreach loop function parse_foreach() { skip_kw("foreach"); skip_punc("("); skip_kw("var"); var varName = parse_ivar(); skip_kw("in"); var param = parse_expression(); skip_punc(")"); var then = parse_expression(); return { type: "foreach", variable: varName, param: param, then: then }; } /* Parse a function This can be taken in two ways: 1. actual function declaration ( function name { } ) 2. minecraft function command ( function name [if/unless...] ) Therefore, testing if there are reg arguments following it, if so, it's Option 2. */ function parse_function() { // Skip the function keyword input.next(); // Get the name of the function var name = input.next(); // Check if what's afterwards is a call if (input.peek().type == 'colon') { var obj = { type: "command", value: [{ type: "reg", value: "function" }, name ] }; // Loop through it to add all of the arguments while (!input.eof()) { if (input.peek().type == "kw") obj.value.push(input.next()); else obj.value.push(parse_expression()); if (is_punc(';')) break; else if (input.eof()) skip_punc(';'); } return obj; } else { // It's not a call to a function, parse it as a normal function return { type: "function", name: name.value, body: parse_expression() }; } } // Parsing a group (sub namespaces/folders) function parse_group() { input.next(); return { type: "group", name: input.next().value, body: parse_expression() }; } // Parse a macro, basically a function with parameters function parse_macro() { input.next(); return { type: "macro", name: input.next().value, vars: delimited("(", ")", ",", parse_varname), body: parse_expression() }; } // Return statements