bitburner-src/src/Netscript/NetscriptWorker.js

942 lines
27 KiB
JavaScript
Raw Normal View History

/* Contains the entire implementation (parser and evaluator) of
* the Netscript language. It needs to be all in one file because the scripts
* are evaluated using Web Workers, which runs code from a single file */
/* Evaluator
* Evaluates the Abstract Syntax Tree for Netscript
* generated by the Parser class
*/
function evaluate(exp, workerScript) {
var env = workerScript.env;
switch (exp.type) {
case "num":
case "str":
case "bool":
return exp.value;
case "var":
return env.get(exp.value);
//Can currently only assign to "var"s
case "assign":
if (exp.left.type != "var")
throw new Error("Cannot assign to " + JSON.stringify(exp.left));
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
var res = evaluate(exp.right, workerScript);
resolve(res);
}, 3000)
});
return p.then(function(expRight) {
return env.set(exp.left.value, expRight);
});
case "binary":
var pLeft = new Promise(function(resolve, reject) {
setTimeout(function() {
var resLeft = evaluate(exp.left, workerScript);
resolve(resLeft);
}, 3000);
});
var pRight = pLeft.then(function(expLeft) {
setTimeout(function() {
var resRight = evaluate(exp.right, workerScript);
resolve([expLeft, resRight]);
}, 3000);
});
return pRight.then(function(args) {
return apply_op(exp.operator,
args[0],
args[1]);
});
//TODO
case "if":
var numConds = exp.cond.length;
var numThens = exp.then.length;
if (numConds == 0 || numThens == 0 || numConds != numThens) {
throw new Error ("Number of conds and thens in if structure don't match (or there are none)");
}
for (var i = 0; i < numConds; i++) {
var cond = evaluate(exp.cond[i], workerScript);
if (cond) return evaluate(exp.then[i], workerScript);
}
//Evaluate else if it exists, snce none of the conditionals
//were true
return exp.else ? evaluate(exp.else, workerScript) : false;
case "for":
var pInit = new Promise(function(resolve, reject) {
setTimeout(function() {
var resInit = evaluate(exp.init, workerScript);
resolve(resInit);
}, 3000);
});
pInit.then(function(expInit) {
setTimeout(function() {
var resCond = evaluate(exp.cond, workerScript);
console.log("Evaluated the conditional");
resolve(resCond);
}, 3000);
})
.then(function(expCond) {
while (expCond) {
var pCode = new Promise(function(resolve, reject) {
setTimeout(function() {
var resCode = evaluate(exp.code, workerScript);
resolve(resCode);
}, 3000);
});
var pPostloop = pCode.then(function(expCode) {
setTimeout(function() {
var resPostloop = evaluate(exp.postloop, workerScript);
resolve(resPostloop);
}, 3000);
});
var pCond = pPostloop.then(function(expPostloop) {
setTimeout(function() {
var resCond = evaluate(exp.cond, workerScript);
resolve(resCond);
}, 3000);
});
pCond.then(function(resCond) {
expCond = resCond;
//Do i need resolve here?
});
}
});
//TODO I don't think I need to return anything..but I might be wrong
break;
case "while":
var pCond = new Promise(function(resolve, reject) {
setTimeout(function() {
var resCond = evaluate(exp.cond, workerScript);
resolve(resCond);
}, 3000);
});
pCond.then(function(expCond) {
while (expCond) {
var pCode = new Promise(function(resolve, reject) {
setTimeout(function() {
var resCode = evaluate(exp.code, workerScript);
resolve(resCode);
}, 3000);
});
pCode.then(function(expCode) {
expCond = evaluate(exp.cond, workerScript);
//Do i need resolve here?
});
}
});
//TODO I don't think I need to return anything..but I might be wrong
break;
case "prog":
var val = false;
exp.prog.forEach(function(exp){
var pExp = new Promise(function(resolve, reject) {
setTimeout(function() {
var resExp = evaluate(exp, workerScript);
resolve(resExp);
}, 3000);
});
pExp.then(function(resExp) {
val = resExp;
});
});
return val;
/* Currently supported function calls:
* hack()
* sleep(N) - sleep N seconds
* print(x) - Prints a variable or constant
*
*/
case "call":
//Define only valid function calls here, like hack() and stuff
//var func = evaluate(exp.func, env);
//return func.apply(null, exp.args.map(function(arg){
// return evaluate(arg, env);
//}));
if (exp.func.value == "hack") {
console.log("Execute hack()");
} else if (exp.func.value == "sleep") {
console.log("Execute sleep()");
} else if (exp.func.value == "print") {
post(evaluate(exp.args[0], workerScript).toString());
}
break;
default:
throw new Error("I don't know how to evaluate " + exp.type);
}
}
function apply_op(op, a, b) {
function num(x) {
if (typeof x != "number")
throw new Error("Expected number but got " + x);
return x;
}
function div(x) {
if (num(x) == 0)
throw new Error("Divide by zero");
return x;
}
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 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;
}
throw new Error("Can't apply operator " + op);
}
/* Environment
* NetScript program environment
*/
function Environment(parent) {
this.vars = Object.create(parent ? parent.vars : null);
this.parent = parent;
}
Environment.prototype = {
//Create a "subscope", which is a new new "sub-environment"
//The subscope is linked to this through its parent variable
extend: function() {
return new Environment(this);
},
//Finds the scope where the variable with the given name is defined
lookup: function(name) {
var scope = this;
while (scope) {
if (Object.prototype.hasOwnProperty.call(scope.vars, name))
return scope;
scope = scope.parent;
}
},
//Get the current value of a variable
get: function(name) {
if (name in this.vars)
return this.vars[name];
throw new Error("Undefined variable " + name);
},
//Sets the value of a variable in any scope
set: function(name, value) {
var scope = this.lookup(name);
// let's not allow defining globals from a nested environment
//
// If scope is null (aka existing variable with name could not be found)
// and this is NOT the global scope, throw error
if (!scope && this.parent)
throw new Error("Undefined variable " + name);
return (scope || this).vars[name] = value;
},
//Creates (or overwrites) a variable in the current scope
def: function(name, value) {
return this.vars[name] = value;
}
};
/* Parser
* Creates Abstract Syntax Tree Nodes
* Operates on a stream of tokens from the Tokenizer
*/
var FALSE = {type: "bool", value: false};
function Parser(input) {
var PRECEDENCE = {
"=": 1,
"||": 2,
"&&": 3,
"<": 7, ">": 7, "<=": 7, ">=": 7, "==": 7, "!=": 7,
"+": 10, "-": 10,
"*": 20, "/": 20, "%": 20,
};
return parse_toplevel();
//Returns true if the next token is a punc type with value ch
function is_punc(ch) {
var tok = input.peek();
return tok && tok.type == "punc" && (!ch || tok.value == ch) && tok;
}
//Returns true if the next token is the kw keyword
function is_kw(kw) {
var tok = input.peek();
return tok && tok.type == "kw" && (!kw || tok.value == kw) && tok;
}
//Returns true if the next token is an op type with the given op value
function is_op(op) {
var tok = input.peek();
return tok && tok.type == "op" && (!op || tok.value == op) && tok;
}
//Checks that the next character is the given punctuation character and throws
//an error if it's not. If it is, skips over it in the input
function checkPuncAndSkip(ch) {
if (is_punc(ch)) input.next();
else input.croak("Expecting punctuation: \"" + ch + "\"");
}
//Checks that the next character is the given keyword and throws an error
//if its not. If it is, skips over it in the input
function checkKeywordAndSkip(kw) {
if (is_kw(kw)) input.next();
else input.croak("Expecting keyword: \"" + kw + "\"");
}
//Checks that the next character is the given operator and throws an error
//if its not. If it is, skips over it in the input
function checkOpAndSkip(op) {
if (is_op(op)) input.next();
else input.croak("Expecting operator: \"" + op + "\"");
}
function unexpected() {
input.croak("Unexpected token: " + JSON.stringify(input.peek()));
}
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;
}
function delimited(start, stop, separator, parser) {
var a = [], first = true;
checkPuncAndSkip(start);
while (!input.eof()) {
if (is_punc(stop)) break;
if (first) first = false; else checkPuncAndSkip(separator);
if (is_punc(stop)) break;
a.push(parser());
}
checkPuncAndSkip(stop);
return a;
}
function parse_call(func) {
return {
type: "call",
func: func,
args: delimited("(", ")", ",", parse_expression),
};
}
function parse_varname() {
var name = input.next();
if (name.type != "var") input.croak("Expecting variable name");
return name.value;
}
/* type: "if",
* cond: [ {"type": "var", "value": "cond1"}, {"type": "var", "value": "cond2"}...]
* then: [ {"type": "var", "value": "then1"}, {"type": "var", "value": "then2"}...]
* else: {"type": "var", "value": "foo"}
*/
function parse_if() {
console.log("Parsing if token");
checkKeywordAndSkip("if");
//Conditional
var cond = parse_expression();
//Body
var then = parse_expression();
var ret = {
type: "if",
cond: [],
then: [],
};
ret.cond.push(cond);
ret.then.push(then);
// Parse all elif branches
while (is_kw("elif")) {
input.next();
var cond = parse_expression();
var then = parse_expression();
ret.cond.push(cond);
ret.then.push(then);
}
// Parse else branch, if it exists
if (is_kw("else")) {
input.next();
ret.else = parse_expression();
}
return ret;
}
/* for (init, cond, postloop) {code;}
*
* type: "for",
* init: assign node,
* cond: var node,
* postloop: assign node
* code: prog node
*/
function parse_for() {
console.log("Parsing for token");
checkKeywordAndSkip("for");
splitExpressions = delimited("(", ")", ";", parse_expression);
console.log("Parsing code in for loop");
code = parse_expression();
if (splitExpressions.length != 3) {
throw new Error("for statement has incorrect number of arugments");
}
//TODO Check type of the init, cond, and postloop nodes
return {
type: "for",
init: splitExpressions[0],
cond: splitExpressions[1],
postloop: splitExpressions[2],
code: code
}
}
/* while (cond) {}
*
* type: "while",
* cond: var node
* code: prog node
*/
function parse_while() {
console.log("Parsing while token");
checkKeywordAndSkip("while");
var cond = parse_expression();
var code = parse_expression();
return {
type: "while",
cond: cond,
code: code
}
}
function parse_bool() {
return {
type : "bool",
value : input.next().value == "true"
};
}
function maybe_call(expr) {
expr = expr();
return is_punc("(") ? parse_call(expr) : expr;
}
function parse_atom() {
return maybe_call(function(){
if (is_punc("(")) {
input.next();
var exp = parse_expression();
checkPuncAndSkip(")");
return exp;
}
if (is_punc("{")) return parse_prog();
if (is_kw("if")) return parse_if();
if (is_kw("for")) return parse_for();
if (is_kw("while")) return parse_while();
//Note, let for loops be function calls (call node types)
if (is_kw("true") || is_kw("false")) return parse_bool();
var tok = input.next();
if (tok.type == "var" || tok.type == "num" || tok.type == "str")
return tok;
unexpected();
});
}
function parse_toplevel() {
var prog = [];
while (!input.eof()) {
prog.push(parse_expression());
if (!input.eof()) checkPuncAndSkip(";");
}
//Return the top level Abstract Syntax Tree, where the top node is a "prog" node
return { type: "prog", prog: prog };
}
function parse_prog() {
console.log("Parsing prog token");
var prog = delimited("{", "}", ";", parse_expression);
if (prog.length == 0) return FALSE;
if (prog.length == 1) return prog[0];
return { type: "prog", prog: prog };
}
function parse_expression() {
return maybe_call(function(){
return maybe_binary(parse_atom(), 0);
});
}
}
/* Tokenizer
* Acts on top of the InputStream class. Takes in a character input stream and and parses it into tokens.
* Tokens can be accessed with peek() and next().
*
* Token types:
* {type: "punc", value: "(" } // punctuation: parens, comma, semicolon etc.
* {type: "num", value: 5 } // numbers (including floats)
* {type: "str", value: "Hello World!" } // strings
* {type: "kw", value: "for/if/" } // keywords, see defs below
* {type: "var", value: "a" } // identifiers/variables
* {type: "op", value: "!=" } // operator characters
* {type: "bool", value: "true" } // Booleans
*
*/
function Tokenizer(input) {
var current = null;
var keywords = " if elif else true false while for ";
return {
next : next,
peek : peek,
eof : eof,
croak : input.croak
}
function is_keyword(x) {
return keywords.indexOf(" " + x + " ") >= 0;
}
function is_digit(ch) {
return /[0-9]/i.test(ch);
}
//An identifier can start with any letter or an underscore
function is_id_start(ch) {
return /[a-z_]/i.test(ch);
}
function is_id(ch) {
return is_id_start(ch) || "?!-<>=0123456789".indexOf(ch) >= 0;
}
function is_op_char(ch) {
return "+-*/%=&|<>!".indexOf(ch) >= 0;
}
function is_punc(ch) {
return ",;(){}[]".indexOf(ch) >= 0;
}
function is_whitespace(ch) {
return " \t\n".indexOf(ch) >= 0;
}
function read_while(predicate) {
var str = "";
while (!input.eof() && predicate(input.peek()))
str += input.next();
return str;
}
function read_number() {
var has_dot = false;
//Reads the number from the input. Checks for only a single decimal point
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) };
}
//This function also checks the identifier against a list of known keywords (defined at the top)
//and will return a kw object rather than identifier if it is one
function read_ident() {
//Identifier must start with a letter or underscore..and can contain anything from ?!-<>=0123456789
var id = read_while(is_id);
return {
type : is_keyword(id) ? "kw" : "var",
value : id
};
}
function read_escaped(end) {
var escaped = false, str = "";
input.next(); //Skip the quotation mark
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;
}
function read_string(ch) {
if (ch == '"') {
return { type: "str", value: read_escaped('"') };
} else if (ch == '\'') {
return { type: "str", value: read_escaped('\'') };
}
}
//Only supports single-line comments right now
function skip_comment() {
read_while(function(ch){ return ch != "\n" });
input.next();
}
//Gets the next token
function read_next() {
//Skip over whitespace
read_while(is_whitespace);
if (input.eof()) return null;
//Peek the next character and decide what to do based on what that
//next character is
var ch = input.peek();
if (ch == "//") {
skip_comment();
return read_next();
}
if (ch == '"' || ch == '\'') return read_string(ch);
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)
}
}
function peek() {
//Returns current token, unless its null in which case it grabs the next one
//and returns it
return current || (current = read_next());
}
function next() {
//The token might have been peaked already, in which case read_next() was already
//called so just return current
var tok = current;
current = null;
return tok || read_next();
}
function eof() {
return peek() == null;
}
}
/* InputStream class. Creates a "stream object" that provides operations to read
* from a string. */
function InputStream(input) {
var pos = 0, line = 1, col = 0;
return {
next : next,
peek : peek,
eof : eof,
croak : croak,
};
function next() {
var ch = input.charAt(pos++);
if (ch == "\n") line++, col = 0; else col++;
return ch;
}
function peek() {
return input.charAt(pos);
}
function eof() {
return peek() == "";
}
function croak(msg) {
throw new Error(msg + " (" + line + ":" + col + ")");
}
}
/* Object that defines the performance/statistics of the script,
* such as how much money it has stolen from every Foreign Server
* and how much exp it has gained for the player.
*
* Every NetscriptWorker has its own ScriptStats object that is passed
* back to the main thread regularly. The main thread then uses the information
* from the object to change the game state. Then, the worker resets the Script Stats
* object and continues. */
function ScriptStats() {
ECorpMoneyHacked = 0;
ECorpTimesHacked = 0;
MegaCorpMoneyHacked = 0;
MegaCorpTimesHacked = 0;
BachmanAndAssociatesMoneyHacked = 0;
BachmanAndAssociatesTimesHacked = 0;
NWOMoneyHacked = 0;
NWOTimesHacked = 0;
ClarkeIncorporatedMoneyHacked = 0;
ClarkeIncorporatedTimesHacked = 0;
OmniTekIncorporatedMoneyHacked = 0;
OmniTekIncorporatedTimesHacked = 0;
FourSigmaMoneyHacked = 0;
FourSigmaTimesHacked = 0;
KuaiGongInternationalMoneyHacked = 0;
KuaiGongInternationalTimesHacked = 0;
FulcrumTechnologiesMoneyHacked = 0;
FulcrumTechnologiesTimesHacked = 0;
FulcrumSecretTechnologiesMoneyHacked = 0;
FulcrumSecretTechnologiesTimesHacked = 0;
StormTechnologiesMoneyHacked = 0;
StormTechnologiesTimesHacked = 0;
DefCommMoneyHacked = 0;
DefCommTimesHacked = 0;
InfoCommMoneyHacked = 0;
InfoCommTimesHacked = 0;
HeliosLabsMoneyHacked = 0;
HeliosLabsTimesHacked = 0;
VitaLifeMoneyHacked = 0;
VitaLifeTimesHacked = 0;
IcarusMicrosystemsMoneyHacked = 0;
IcarusMicrosystemsTimesHacked = 0;
UniversalEnergyMoneyHacked = 0;
UniversalEnergyTimesHacked = 0;
TitanLabsMoneyHacked = 0;
TitanLabsTimesHacked = 0;
MicrodyneTechnologiesMoneyHacked = 0;
MicrodyneTechnologiesTimesHacked = 0;
TaiYangDigitalMoneyHacked = 0;
TaiYangDigitalTimesHacked = 0;
GalacticCybersystemsMoneyHacked = 0;
GalacticCybersystemsTimesHacked = 0;
AeroCorpMoneyHacked = 0;
AeroCorpTimesHacked = 0;
OmniaCybersystemsMoneyHacked = 0;
OmniaCybersystemsTimesHacked = 0;
ZBDefenseMoneyHacked = 0;
ZBDefenseTimesHacked = 0;
AppliedEnergeticsMoneyHacked = 0;
AppliedEnergeticsTimesHacked = 0;
SolarisSpaceSystemsMoneyHacked = 0;
SolarisSpaceSystemsTimesHacked = 0;
DeltaOneMoneyHacked = 0;
DeltaOneTimesHacked = 0;
GlobalPharmaceuticalsMoneyHacked = 0;
GlobalPharmaceuticalsTimesHacked = 0;
NovaMedicalMoneyHacked = 0;
NovaMedicalTimesHacked = 0;
ZeusMedicalMoneyHacked = 0;
ZeusMedicalTimesHacked = 0;
UnitaLifeGroupMoneyHacked = 0;
UnitaLifeGroupTimesHacked = 0;
LexoCorpMoneyHacked = 0;
LexoCorpTimesHacked = 0;
RhoConstructionMoneyHacked = 0;
RhoConstructionTimesHacked = 0;
AlphaEnterprisesMoneyHacked = 0;
AlphaEnterprisesTimesHacked = 0;
AevumPoliceMoneyHacked = 0;
AevumPoliceTimesHacked = 0;
RothmanUniversityMoneyHacked = 0;
RothmanUniversityTimesHacked = 0;
ZBInstituteOfTechnologyMoneyHacked = 0;
ZBInstituteOfTechnologyTimesHacked = 0;
SummitUniversityMoneyHacked = 0;
SummitUniversityTimesHacked = 0;
SysCoreSecuritiesMoneyHacked = 0;
SysCoreSecuritiesTimesHacked = 0;
CatalystVenturesMoneyHacked = 0;
CatalystVenturesTimesHacked = 0;
TheHubMoneyHacked = 0;
TheHubTimesHacked = 0;
CompuTekMoneyHacked = 0;
CompuTekTimesHacked = 0;
NetLinkTechnologiesMoneyHacked = 0;
NetLinkTechnologiesTimesHacked = 0;
JohnsonOrthopedicsMoneyHacked = 0;
JohnsonOrthopedicsTimesHacked = 0;
FoodNStuffMoneyHacked = 0;
FoodNStuffTimesHacked = 0;
SigmaCosmeticsMoneyHacked = 0;
SigmaCosmeticsTimesHacked = 0;
JoesGunsMoneyHacked = 0;
JoesGunsTimesHacked = 0;
Zer0NightclubMoneyHacked = 0;
Zer0NightclubTimesHacked = 0;
NectarNightclubMoneyHacked = 0;
NectarNightclubTimesHacked = 0;
NeoNightclubMoneyHacked = 0;
NeoNightclubTimesHacked = 0;
SilverHelixMoneyHacked = 0;
SilverHelixTimesHacked = 0;
HongFangTeaHouseMoneyHacked = 0;
HongFangTeaHouseTimesHacked = 0;
HaraKiriSushiBarMoneyHacked = 0;
HaraKiriSushiBarTimesHacked = 0;
PhantasyMoneyHacked = 0;
PhantasyTimesHacked = 0;
MaxHardwareMoneyHacked = 0;
MaxHardwareTimesHacked = 0;
OmegaSoftwareMoneyHacked = 0;
OmegaSoftwareTimesHacked = 0;
CrushFitnessGymMoneyHacked = 0;
CrushFitnessGymTimesHacked = 0;
IronGymMoneyHacked = 0;
IronGymTimesHacked = 0;
MilleniumFitnessGymMoneyHacked = 0;
MilleniumFitnessGymTimesHacked = 0;
PowerhouseGymMoneyHacked = 0;
PowerhouseGymTimesHacked = 0;
SnapFitnessGymMoneyHacked = 0;
SnapFitnessGymTimesHacked = 0;
}
ScriptStats.prototype.reset = function() {
for (var key in this) {
if (this.hasOwnProperty(key)) {
this[key] = 0;
}
}
}
/* Actual Worker Code */
function WorkerScript() {
this.name = "";
this.running = false;
this.code = "";
this.env = new Environment();
this.timeout = null;
}
var scriptStats = new ScriptStats();
var workerForeignServers = null;
var workerPlayer = null;
var workerScripts = [];
self.addEventListener('message', msgRecvHandler);
function msgRecvHandler(e) {
/* The first element of the data array from main thread specifies
* what kind of data it is:
* Status Update
* Start Script
* Stop Script
*/
if (e.data.type == "Status Update") {
console.log("Status update received in Script Web Worker");
ForeignServersScript = JSON.parse(e.data.buf1);
PlayerScript = JSON.parse(e.data.buf2);
} else if (e.data.type == "Start Script") {
var s = new WorkerScript();
s.name = e.data.buf1;
s.code = e.data.buf2;
workerScripts.push(s);
} else if (e.data.type == "Stop Script") {
var scriptName = e.data.buf1;
for (var i = 0; i < workerScripts.length; i++) {
if (workerScripts[i].name == scriptName) {
//Stop the script from running and then remove it from workerScripts
clearTimeout(workerScripts[i].timeout);
workerScripts.splice(i, 1);
}
}
}
var ast = Parser(Tokenizer(InputStream(code)));
var globalEnv = new Environment();
evaluate(ast, globalEnv);
}
//Loop through workerScripts and run every script that is not currently running
function runScriptsLoop() {
console.log("runScriptsLoop() iteration");
for (var i = 0; i < workerScripts.length; i++) {
if (workerScripts[i].running == false) {
var ast = Parser(Tokenizer(InputStream(workerScripts[i].code)));
evaluate(ast, workerScripts[i]);
workerScripts[i].running = true;
}
}
setTimeout(runScriptsLoop, 10000);
}
runScriptsLoop();