Merge pull request #614 from danielyxie/active-scripts-ui-and-implementation-rework

Active scripts ui and implementation rework
This commit is contained in:
danielyxie 2019-05-17 13:50:55 -07:00 committed by GitHub
commit 8a00e6e532
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 956 additions and 728 deletions

126
css/activescripts.scss Normal file

@ -0,0 +1,126 @@
@import "theme";
.active-scripts-list {
list-style-type: none;
}
#active-scripts-container {
position: fixed;
padding-top: 10px;
> p {
width: 70%;
margin: 6px;
padding: 4px;
}
.accordion-header {
> pre {
color: white;
}
}
}
.active-scripts-server-header {
background-color: #444;
font-size: $defaultFontSize * 1.25;
color: #fff;
margin: 6px 6px 0 6px;
padding: 6px;
cursor: pointer;
width: 60%;
text-align: left;
border: none;
outline: none;
&:after {
content: '\02795'; /* "plus" sign (+) */
font-size: $defaultFontSize * 0.8125;
color: #fff;
float: right;
margin-left: 5px;
}
&.active, &:hover {
background-color: #555;
}
}
.active-scripts-server-header.active {
&:after {
content: "\2796"; /* "minus" sign (-) */
font-size: $defaultFontSize * 0.8125;
color: #fff;
float: right;
margin-left: 5px;
}
&:hover {
background-color: #666;
}
}
.active-scripts-server-panel {
margin: 0 6px 6px 6px;
padding: 0 6px 6px 6px;
width: 55%;
margin-left: 5%;
display: none;
div, ul, ul > li {
background-color: #555;
}
}
.active-scripts-script-header {
background-color: #555;
border: none;
color: var(--my-font-color);
cursor: pointer;
display: block;
outline: none;
padding: 4px 25px 4px 10px;
position: relative;
text-align: left;
width: auto;
&:after {
content: '\02795'; /* "plus" sign (+) */
font-size: $defaultFontSize * 0.8125;
float: right;
margin-left: 5px;
color: transparent;
text-shadow: 0 0 0 var(--my-font-color);
position: absolute;
bottom: 4px;
}
&.active:after {
content: "\2796"; /* "minus" sign (-) */
}
&:hover,
&.active:hover {
background-color: #666;
}
&.active {
background-color: #555;
}
}
.active-scripts-script-panel {
background-color: #555;
display: none;
font-size: 14px;
margin-bottom: 6px;
padding: 0 18px;
width: auto;
pre, h2, ul, li {
background-color: #555;
width: auto;
color: #fff;
margin-left: 5%;
}
}

@ -18,126 +18,6 @@
position: fixed; position: fixed;
} }
/* Active scripts */
.active-scripts-list {
list-style-type: none;
}
#active-scripts-container {
position: fixed;
padding-top: 10px;
}
#active-scripts-text,
#active-scripts-total-prod {
width: 70%;
margin: 6px;
padding: 4px;
}
.active-scripts-server-header {
background-color: #444;
font-size: $defaultFontSize * 1.25;
color: #fff;
margin: 6px 6px 0 6px;
padding: 6px;
cursor: pointer;
width: 60%;
text-align: left;
border: none;
outline: none;
}
.active-scripts-server-header.active,
.active-scripts-server-header:hover {
background-color: #555;
}
.active-scripts-server-header.active:hover {
background-color: #666;
}
.active-scripts-server-header:after {
content: '\02795'; /* "plus" sign (+) */
font-size: $defaultFontSize * 0.8125;
color: #fff;
float: right;
margin-left: 5px;
}
.active-scripts-server-header.active:after {
content: "\2796"; /* "minus" sign (-) */
font-size: $defaultFontSize * 0.8125;
color: #fff;
float: right;
margin-left: 5px;
}
.active-scripts-server-panel {
margin: 0 6px 6px 6px;
padding: 0 6px 6px 6px;
width: 55%;
margin-left: 5%;
display: none;
}
.active-scripts-server-panel div,
.active-scripts-server-panel ul,
.active-scripts-server-panel ul > li {
background-color: #555;
}
.active-scripts-script-header {
background-color: #555;
color: var(--my-font-color);
padding: 4px 25px 4px 10px;
cursor: pointer;
width: auto;
text-align: left;
border: none;
outline: none;
position: relative;
&:after {
content: '\02795'; /* "plus" sign (+) */
font-size: $defaultFontSize * 0.8125;
float: right;
margin-left: 5px;
color: transparent;
text-shadow: 0 0 0 var(--my-font-color);
position: absolute;
bottom: 4px;
}
&.active:after {
content: "\2796"; /* "minus" sign (-) */
}
&:hover,
&.active:hover {
background-color: #666;
}
&.active {
background-color: #555;
}
}
.active-scripts-script-panel {
padding: 0 18px;
background-color: #555;
width: auto;
display: none;
margin-bottom: 6px;
p, h2, ul, li {
background-color: #555;
width: auto;
color: #fff;
margin-left: 5%;
}
}
/* World */ /* World */
#world-container { #world-container {
position: fixed; position: fixed;

@ -243,8 +243,8 @@ a:visited {
/* Accordion menus (Header with collapsible panel) */ /* Accordion menus (Header with collapsible panel) */
.accordion-header { .accordion-header {
background-color: #444; background-color: #444;
font-size: $defaultFontSize * 1.25;
color: #fff; color: #fff;
font-size: $defaultFontSize * 1.25;
margin: 6px 6px 0 6px; margin: 6px 6px 0 6px;
padding: 4px 6px; padding: 4px 6px;
cursor: pointer; cursor: pointer;

@ -1,337 +0,0 @@
// TODO - Convert this to React
import { workerScripts, killWorkerScript } from "./NetscriptWorker";
import { Player } from "./Player";
import { getServer } from "./Server/ServerHelpers";
import { Page, routing } from "./ui/navigationTracking";
import { numeralWrapper } from "./ui/numeralFormat";
import { dialogBoxCreate } from "../utils/DialogBox";
import { logBoxCreate } from "../utils/LogBox";
import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions";
import { arrayToString } from "../utils/helpers/arrayToString";
import { createProgressBarText } from "../utils/helpers/createProgressBarText";
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
import { roundToTwo } from "../utils/helpers/roundToTwo";
import { createAccordionElement } from "../utils/uiHelpers/createAccordionElement";
import { createElement } from "../utils/uiHelpers/createElement";
import { getElementById } from "../utils/uiHelpers/getElementById";
import { removeChildrenFromElement } from "../utils/uiHelpers/removeChildrenFromElement";
import { removeElement } from "../utils/uiHelpers/removeElement";
/**
* {
* serverName: {
* header: Server Header Element
* panel: Server Panel List (ul) element
* scripts: {
* script id: Ref to Script information
* }
* }
* ...
*/
const ActiveScriptsUI = {};
const ActiveScriptsTasks = []; // Sequentially schedule the creation/deletion of UI elements
const getHeaderHtml = (server) => {
// TODO: calculate the longest hostname length rather than hard coding it
const longestHostnameLength = 18;
const paddedName = `${server.hostname}${" ".repeat(longestHostnameLength)}`.slice(0, Math.max(server.hostname.length, longestHostnameLength));
const barOptions = {
progress: server.ramUsed / server.maxRam,
totalTicks: 30
};
return `${paddedName} ${createProgressBarText(barOptions)}`.replace(/\s/g, ' ');
};
const updateHeaderHtml = (server) => {
const accordion = ActiveScriptsUI[server.hostname];
if (accordion === null || accordion === undefined) {
return;
}
// Convert it to a string, as that's how it's stored it will come out of the data attributes
const ramPercentage = '' + roundToTwo(server.ramUsed / server.maxRam);
if (accordion.header.dataset.ramPercentage !== ramPercentage) {
accordion.header.dataset.ramPercentage = ramPercentage;
accordion.header.innerHTML = getHeaderHtml(server);
}
}
function createActiveScriptsServerPanel(server) {
let hostname = server.hostname;
var activeScriptsList = document.getElementById("active-scripts-list");
let res = createAccordionElement({
hdrText: getHeaderHtml(server)
});
let li = res[0];
var hdr = res[1];
let panel = res[2];
if (ActiveScriptsUI[hostname] != null) {
console.log("WARNING: Tried to create already-existing Active Scripts Server panel. This is most likely fine. It probably means many scripts just got started up on a new server. Aborting");
return;
}
var panelScriptList = createElement("ul");
panel.appendChild(panelScriptList);
activeScriptsList.appendChild(li);
ActiveScriptsUI[hostname] = {
header: hdr,
panel: panel,
panelList: panelScriptList,
scripts: {}, // Holds references to li elements for each active script
scriptHdrs: {}, // Holds references to header elements for each active script
scriptStats: {}, // Holds references to the p elements containing text for each active script
};
return li;
}
/**
* Deletes the info for a particular server (Dropdown header + Panel with all info)
* in the Active Scripts page if it exists
*/
function deleteActiveScriptsServerPanel(server) {
let hostname = server.hostname;
if (ActiveScriptsUI[hostname] == null) {
console.log("WARNING: Tried to delete non-existent Active Scripts Server panel. Aborting");
return;
}
// Make sure it's empty
if (Object.keys(ActiveScriptsUI[hostname].scripts).length > 0) {
console.warn("Tried to delete Active Scripts Server panel that still has scripts. Aborting");
return;
}
removeElement(ActiveScriptsUI[hostname].panel);
removeElement(ActiveScriptsUI[hostname].header);
delete ActiveScriptsUI[hostname];
}
function addActiveScriptsItem(workerscript) {
var server = getServer(workerscript.serverIp);
if (server == null) {
console.warn("Invalid server IP for workerscript in addActiveScriptsItem()");
return;
}
let hostname = server.hostname;
ActiveScriptsTasks.push(function(workerscript, hostname) {
if (ActiveScriptsUI[hostname] == null) {
createActiveScriptsServerPanel(server);
}
// Create the unique identifier (key) for this script
var itemNameArray = ["active", "scripts", hostname, workerscript.name];
for (var i = 0; i < workerscript.args.length; ++i) {
itemNameArray.push(String(workerscript.args[i]));
}
var itemName = itemNameArray.join("-");
let res = createAccordionElement({hdrText:workerscript.name});
let li = res[0];
let hdr = res[1];
let panel = res[2];
hdr.classList.remove("accordion-header");
hdr.classList.add("active-scripts-script-header");
panel.classList.remove("accordion-panel");
panel.classList.add("active-scripts-script-panel");
/**
* Handle the constant elements on the panel that don't change after creation:
* Threads, args, kill/log button
*/
panel.appendChild(createElement("p", {
innerHTML: "Threads: " + workerscript.scriptRef.threads + "<br>" +
"Args: " + arrayToString(workerscript.args)
}));
var panelText = createElement("p", {
innerText: "Loading...",
fontSize: "14px",
});
panel.appendChild(panelText);
panel.appendChild(createElement("br"));
panel.appendChild(createElement("span", {
innerText: "Log",
class: "accordion-button",
margin: "4px",
padding: "4px",
clickListener: () => {
logBoxCreate(workerscript.scriptRef);
return false;
}
}));
panel.appendChild(createElement("span", {
innerText: "Kill Script",
class: "accordion-button",
margin: "4px",
padding: "4px",
clickListener: () => {
killWorkerScript(workerscript.scriptRef, workerscript.scriptRef.server);
dialogBoxCreate("Killing script, may take a few minutes to complete...");
return false;
}
}));
// Append element to list
ActiveScriptsUI[hostname]["panelList"].appendChild(li);
ActiveScriptsUI[hostname].scripts[itemName] = li;
ActiveScriptsUI[hostname].scriptHdrs[itemName] = hdr;
ActiveScriptsUI[hostname].scriptStats[itemName] = panelText;
}.bind(null, workerscript, hostname));
}
function deleteActiveScriptsItem(workerscript) {
ActiveScriptsTasks.push(function(workerscript) {
var server = getServer(workerscript.serverIp);
if (server == null) {
throw new Error("ERROR: Invalid server IP for workerscript. This most likely occurred because " +
"you tried to delete a large number of scripts and also deleted servers at the " +
"same time. It's not a big deal, just save and refresh the game.");
return;
}
let hostname = server.hostname;
if (ActiveScriptsUI[hostname] == null) {
console.log("ERROR: Trying to delete Active Script UI Element with a hostname that cant be found in ActiveScriptsUI: " + hostname);
return;
}
var itemNameArray = ["active", "scripts", server.hostname, workerscript.name];
for (var i = 0; i < workerscript.args.length; ++i) {
itemNameArray.push(String(workerscript.args[i]));
}
var itemName = itemNameArray.join("-");
let li = ActiveScriptsUI[hostname].scripts[itemName];
if (li == null) {
console.log("ERROR: Cannot find Active Script UI element for workerscript: ");
console.log(workerscript);
return;
}
removeElement(li);
delete ActiveScriptsUI[hostname].scripts[itemName];
delete ActiveScriptsUI[hostname].scriptHdrs[itemName];
delete ActiveScriptsUI[hostname].scriptStats[itemName];
if (Object.keys(ActiveScriptsUI[hostname].scripts).length === 0) {
deleteActiveScriptsServerPanel(server);
}
}.bind(null, workerscript));
}
function updateActiveScriptsItems(maxTasks=150) {
/**
* Run tasks that need to be done sequentially (adding items, creating/deleting server panels)
* We'll limit this to 150 at a time for performance (in case someone decides to start a
* bunch of scripts all at once...)
*/
const numTasks = Math.min(maxTasks, ActiveScriptsTasks.length);
for (let i = 0; i < numTasks; ++i) {
let task = ActiveScriptsTasks.shift();
try {
task();
} catch(e) {
exceptionAlert(e);
console.log(task);
}
}
let total = 0;
for (var i = 0; i < workerScripts.length; ++i) {
try {
total += updateActiveScriptsItemContent(workerScripts[i]);
} catch(e) {
exceptionAlert(e);
}
}
if (!routing.isOn(Page.ActiveScripts)) { return total; }
getElementById("active-scripts-total-production-active").innerText = numeralWrapper.formatMoney(total);
getElementById("active-scripts-total-prod-aug-total").innerText = numeralWrapper.formatMoney(Player.scriptProdSinceLastAug);
getElementById("active-scripts-total-prod-aug-avg").innerText = numeralWrapper.formatMoney(Player.scriptProdSinceLastAug / (Player.playtimeSinceLastAug/1000));
return total;
}
function updateActiveScriptsItemContent(workerscript) {
var server = getServer(workerscript.serverIp);
if (server == null) {
console.log("ERROR: Invalid server IP for workerscript in updateActiveScriptsItemContent().");
return;
}
let hostname = server.hostname;
if (ActiveScriptsUI[hostname] == null) {
return; // Hasn't been created yet. We'll skip it
}
updateHeaderHtml(server);
var itemNameArray = ["active", "scripts", server.hostname, workerscript.name];
for (var i = 0; i < workerscript.args.length; ++i) {
itemNameArray.push(String(workerscript.args[i]));
}
var itemName = itemNameArray.join("-");
if (ActiveScriptsUI[hostname].scriptStats[itemName] == null) {
return; // Hasn't been fully added yet. We'll skip it
}
var item = ActiveScriptsUI[hostname].scriptStats[itemName];
// Update the text if necessary. This fn returns the online $/s production
return updateActiveScriptsText(workerscript, item, itemName);
}
function updateActiveScriptsText(workerscript, item, itemName) {
var server = getServer(workerscript.serverIp);
if (server == null) {
console.log("ERROR: Invalid server IP for workerscript for updateActiveScriptsText()");
return;
}
let hostname = server.hostname;
if (ActiveScriptsUI[hostname] == null || ActiveScriptsUI[hostname].scriptHdrs[itemName] == null) {
console.log("ERROR: Trying to update Active Script UI Element with a hostname that cant be found in ActiveScriptsUI: " + hostname);
return;
}
updateHeaderHtml(server);
var onlineMps = workerscript.scriptRef.onlineMoneyMade / workerscript.scriptRef.onlineRunningTime;
// Only update if the item is visible
if (ActiveScriptsUI[hostname].header.classList.contains("active") === false) {return onlineMps;}
if (ActiveScriptsUI[hostname].scriptHdrs[itemName].classList.contains("active") === false) {return onlineMps;}
removeChildrenFromElement(item);
var onlineTime = "Online Time: " + convertTimeMsToTimeElapsedString(workerscript.scriptRef.onlineRunningTime * 1e3);
var offlineTime = "Offline Time: " + convertTimeMsToTimeElapsedString(workerscript.scriptRef.offlineRunningTime * 1e3);
// Online
var onlineTotalMoneyMade = "Total online production: " + numeralWrapper.formatMoney(workerscript.scriptRef.onlineMoneyMade);
var onlineTotalExpEarned = (Array(26).join(" ") + numeralWrapper.formatBigNumber(workerscript.scriptRef.onlineExpGained) + " hacking exp").replace( / /g, "&nbsp;");
var onlineMpsText = "Online production rate: " + numeralWrapper.formatMoney(onlineMps) + " / second";
var onlineEps = workerscript.scriptRef.onlineExpGained / workerscript.scriptRef.onlineRunningTime;
var onlineEpsText = (Array(25).join(" ") + numeralWrapper.formatBigNumber(onlineEps) + " hacking exp / second").replace( / /g, "&nbsp;");
// Offline
var offlineTotalMoneyMade = "Total offline production: " + numeralWrapper.formatMoney(workerscript.scriptRef.offlineMoneyMade);
var offlineTotalExpEarned = (Array(27).join(" ") + numeralWrapper.formatBigNumber(workerscript.scriptRef.offlineExpGained) + " hacking exp").replace( / /g, "&nbsp;");
var offlineMps = workerscript.scriptRef.offlineMoneyMade / workerscript.scriptRef.offlineRunningTime;
var offlineMpsText = "Offline production rate: " + numeralWrapper.formatMoney(offlineMps) + " / second";
var offlineEps = workerscript.scriptRef.offlineExpGained / workerscript.scriptRef.offlineRunningTime;
var offlineEpsText = (Array(26).join(" ") + numeralWrapper.formatBigNumber(offlineEps) + " hacking exp / second").replace( / /g, "&nbsp;");
item.innerHTML = onlineTime + "<br>" + offlineTime + "<br>" + onlineTotalMoneyMade + "<br>" + onlineTotalExpEarned + "<br>" +
onlineMpsText + "<br>" + onlineEpsText + "<br>" + offlineTotalMoneyMade + "<br>" + offlineTotalExpEarned + "<br>" +
offlineMpsText + "<br>" + offlineEpsText + "<br>";
return onlineMps;
}
export {addActiveScriptsItem, deleteActiveScriptsItem, updateActiveScriptsItems};

@ -32,6 +32,11 @@ export class WorkerScript {
*/ */
delay: number | null = null; delay: number | null = null;
/**
* Holds the Promise resolve() function for when the script is "blocked" by an async op
*/
delayResolve?: () => void;
/** /**
* Stores names of all functions that have logging disabled * Stores names of all functions that have logging disabled
*/ */

@ -0,0 +1,6 @@
/**
* Event emitter that triggers when scripts are started/stopped
*/
import { EventEmitter } from "../utils/EventEmitter";
export const WorkerScriptStartStopEventEmitter = new EventEmitter();

@ -0,0 +1,6 @@
/**
* Global pool of all active scripts (scripts that are currently running)
*/
import { WorkerScript } from "./WorkerScript";
export const workerScripts: WorkerScript[] = [];

@ -0,0 +1,127 @@
/**
* Stops an actively-running script (represented by a WorkerScript object)
* and removes it from the global pool of active scripts.
*/
import { WorkerScript } from "./WorkerScript";
import { workerScripts } from "./WorkerScripts";
import { WorkerScriptStartStopEventEmitter } from "./WorkerScriptStartStopEventEmitter";
import { RunningScript } from "../Script/RunningScript";
import { AllServers } from "../Server/AllServers";
import { compareArrays } from "../../utils/helpers/compareArrays";
import { roundToTwo } from "../../utils/helpers/roundToTwo";
export function killWorkerScript(runningScriptObj: RunningScript, serverIp: string): boolean;
export function killWorkerScript(workerScript: WorkerScript): boolean;
export function killWorkerScript(script: RunningScript | WorkerScript, serverIp?: string): boolean {
if (script instanceof WorkerScript) {
script.env.stopFlag = true;
killNetscriptDelay(script);
removeWorkerScript(script);
return true;
} else if (script instanceof RunningScript && typeof serverIp === "string") {
for (let i = 0; i < workerScripts.length; i++) {
if (workerScripts[i].name == script.filename && workerScripts[i].serverIp == serverIp &&
compareArrays(workerScripts[i].args, script.args)) {
workerScripts[i].env.stopFlag = true;
killNetscriptDelay(workerScripts[i]);
removeWorkerScript(workerScripts[i]);
return true;
}
}
return false;
} else {
console.error(`killWorkerScript() called with invalid argument:`);
console.error(script);
return false;
}
}
/**
* Helper function that removes the script being killed from the global pool.
* Also handles other cleanup-time operations
*
* @param {WorkerScript | number} - Identifier for WorkerScript. Either the object itself, or
* its index in the global workerScripts array
*/
function removeWorkerScript(id: WorkerScript | number): void {
// Get a reference to the WorkerScript and its index in the global pool
let workerScript: WorkerScript;
let index: number | null = null;
if (typeof id === "number") {
if (id < 0 || id >= workerScripts.length) {
console.error(`Too high of an index passed into removeWorkerScript(): ${id}`);
return;
}
workerScript = workerScripts[id];
index = id;
} else if (id instanceof WorkerScript) {
workerScript = id;
for (let i = 0; i < workerScripts.length; ++i) {
if (workerScripts[i] == id) {
index = i;
break;
}
}
if (index == null) {
console.error(`Could not find WorkerScript in global pool:`);
console.error(workerScript);
}
} else {
console.error(`Invalid argument passed into removeWorkerScript(): ${id}`);
return;
}
const ip = workerScript.serverIp;
const name = workerScript.name;
// Get the server on which the script runs
const server = AllServers[ip];
if (server == null) {
console.error(`Could not find server on which this script is running: ${ip}`);
return;
}
// Recalculate ram used on that server
server.ramUsed = roundToTwo(server.ramUsed - workerScript.ramUsage);
if (server.ramUsed < 0) {
console.warn(`Server RAM usage went negative (if it's due to floating pt imprecision, it's okay): ${server.ramUsed}`);
server.ramUsed = 0;
}
// Delete the RunningScript object from that server
for (let i = 0; i < server.runningScripts.length; ++i) {
const runningScript = server.runningScripts[i];
if (runningScript.filename === name && compareArrays(runningScript.args, workerScript.args)) {
server.runningScripts.splice(i, 1);
break;
}
}
// Delete script from global pool (workerScripts)
workerScripts.splice(<number>index, 1);
WorkerScriptStartStopEventEmitter.emitEvent();
}
/**
* Helper function that interrupts a script's delay if it is in the middle of a
* timed, blocked operation (like hack(), sleep(), etc.). This allows scripts to
* be killed immediately even if they're in the middle of one of those long operations
*/
function killNetscriptDelay(workerScript: WorkerScript) {
if (workerScript instanceof WorkerScript) {
if (workerScript.delay) {
clearTimeout(workerScript.delay);
if (workerScript.delayResolve) {
workerScript.delayResolve();
}
}
}
}

@ -1,21 +1,9 @@
import { WorkerScript } from "./Netscript/WorkerScript";
import { getServer } from "./Server/ServerHelpers";
import { setTimeoutRef } from "./utils/SetTimeoutRef"; import { setTimeoutRef } from "./utils/SetTimeoutRef";
import { parse, Node } from "../utils/acorn"; import { parse, Node } from "../utils/acorn";
import { isValidIPAddress } from "../utils/helpers/isValidIPAddress"; import { isValidIPAddress } from "../utils/helpers/isValidIPAddress";
import { isString } from "../utils/helpers/isString"; import { isString } from "../utils/helpers/isString";
export function killNetscriptDelay(workerScript) {
if (workerScript instanceof WorkerScript) {
if (workerScript.delay) {
clearTimeout(workerScript.delay);
workerScript.delayResolve();
}
}
}
export function netscriptDelay(time, workerScript) { export function netscriptDelay(time, workerScript) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
workerScript.delay = setTimeoutRef(() => { workerScript.delay = setTimeoutRef(() => {

@ -3,7 +3,6 @@ const vsprintf = require("sprintf-js").vsprintf;
import { getRamCost } from "./Netscript/RamCostGenerator"; import { getRamCost } from "./Netscript/RamCostGenerator";
import { updateActiveScriptsItems } from "./ActiveScriptsUI";
import { Augmentation } from "./Augmentation/Augmentation"; import { Augmentation } from "./Augmentation/Augmentation";
import { Augmentations } from "./Augmentation/Augmentations"; import { Augmentations } from "./Augmentation/Augmentations";
import { import {
@ -120,11 +119,11 @@ import {
} from "./NetscriptBladeburner"; } from "./NetscriptBladeburner";
import * as nsGang from "./NetscriptGang"; import * as nsGang from "./NetscriptGang";
import { import {
workerScripts,
killWorkerScript,
NetscriptPorts, NetscriptPorts,
runScriptFromScript, runScriptFromScript,
} from "./NetscriptWorker"; } from "./NetscriptWorker";
import { killWorkerScript } from "./Netscript/killWorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts";
import { import {
makeRuntimeRejectMsg, makeRuntimeRejectMsg,
netscriptDelay, netscriptDelay,

@ -2,20 +2,17 @@
* Functions for handling WorkerScripts, which are the underlying mechanism * Functions for handling WorkerScripts, which are the underlying mechanism
* that allows for scripts to run * that allows for scripts to run
*/ */
import { killWorkerScript } from "./Netscript/killWorkerScript";
import { WorkerScript } from "./Netscript/WorkerScript"; import { WorkerScript } from "./Netscript/WorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts";
import { WorkerScriptStartStopEventEmitter } from "./Netscript/WorkerScriptStartStopEventEmitter";
import {
addActiveScriptsItem,
deleteActiveScriptsItem,
updateActiveScriptsItems
} from "./ActiveScriptsUI";
import { CONSTANTS } from "./Constants"; import { CONSTANTS } from "./Constants";
import { Engine } from "./engine"; import { Engine } from "./engine";
import { Interpreter } from "./JSInterpreter"; import { Interpreter } from "./JSInterpreter";
import { import {
isScriptErrorMessage, isScriptErrorMessage,
makeRuntimeRejectMsg, makeRuntimeRejectMsg,
killNetscriptDelay
} from "./NetscriptEvaluator"; } from "./NetscriptEvaluator";
import { NetscriptFunctions } from "./NetscriptFunctions"; import { NetscriptFunctions } from "./NetscriptFunctions";
import { executeJSScript } from "./NetscriptJSEvaluator"; import { executeJSScript } from "./NetscriptJSEvaluator";
@ -42,9 +39,7 @@ import { isString } from "../utils/StringHelperFunctions";
const walk = require("acorn/dist/walk"); const walk = require("acorn/dist/walk");
//Array containing all scripts that are running across all servers, to easily run them all // Netscript Ports are instantiated here
export const workerScripts = [];
export const NetscriptPorts = []; export const NetscriptPorts = [];
for (var i = 0; i < CONSTANTS.NumNetscriptPorts; ++i) { for (var i = 0; i < CONSTANTS.NumNetscriptPorts; ++i) {
NetscriptPorts.push(new NetscriptPort()); NetscriptPorts.push(new NetscriptPort());
@ -52,10 +47,9 @@ for (var i = 0; i < CONSTANTS.NumNetscriptPorts; ++i) {
export function prestigeWorkerScripts() { export function prestigeWorkerScripts() {
for (var i = 0; i < workerScripts.length; ++i) { for (var i = 0; i < workerScripts.length; ++i) {
deleteActiveScriptsItem(workerScripts[i]); // TODO Signal event emitter
workerScripts[i].env.stopFlag = true; workerScripts[i].env.stopFlag = true;
} }
updateActiveScriptsItems(5000); //Force UI to update
workerScripts.length = 0; workerScripts.length = 0;
} }
@ -141,7 +135,7 @@ function startNetscript2Script(workerScript) {
} }
function startNetscript1Script(workerScript) { function startNetscript1Script(workerScript) {
var code = workerScript.code; const code = workerScript.code;
workerScript.running = true; workerScript.running = true;
//Process imports //Process imports
@ -413,66 +407,55 @@ function processNetscript1Imports(code, workerScript) {
return res; return res;
} }
// Loop through workerScripts and run every script that is not currently running /**
export function runScriptsLoop() { * Start a script
let scriptDeleted = false; *
* Given a RunningScript object, constructs a corresponding WorkerScript,
* adds it to the global 'workerScripts' pool, and begins executing it.
* @param {RunningScript} runningScriptObj - Script that's being run
* @param {Server} server - Server on which the script is to be run
*/
export function addWorkerScript(runningScriptObj, server) {
const filename = runningScriptObj.filename;
// Delete any scripts that finished or have been killed. Loop backwards bc removing items screws up indexing // Update server's ram usage
for (let i = workerScripts.length - 1; i >= 0; i--) { let threads = 1;
if (workerScripts[i].running == false && workerScripts[i].env.stopFlag == true) { if (runningScriptObj.threads && !isNaN(runningScriptObj.threads)) {
scriptDeleted = true; threads = runningScriptObj.threads;
// Delete script from the runningScripts array on its host serverIp
const ip = workerScripts[i].serverIp;
const name = workerScripts[i].name;
// Recalculate ram used
AllServers[ip].ramUsed = 0;
for (let j = 0; j < workerScripts.length; j++) {
if (workerScripts[j].serverIp !== ip) {
continue;
}
if (j === i) { // not this one
continue;
}
AllServers[ip].ramUsed += workerScripts[j].ramUsage;
}
// Delete script from Active Scripts
deleteActiveScriptsItem(workerScripts[i]);
for (let j = 0; j < AllServers[ip].runningScripts.length; j++) {
if (AllServers[ip].runningScripts[j].filename == name &&
compareArrays(AllServers[ip].runningScripts[j].args, workerScripts[i].args)) {
AllServers[ip].runningScripts.splice(j, 1);
break;
}
}
// Delete script from workerScripts
workerScripts.splice(i, 1);
}
}
if (scriptDeleted) { updateActiveScriptsItems(); } // Force Update
// Run any scripts that haven't been started
for (let i = 0; i < workerScripts.length; i++) {
// If it isn't running, start the script
if (workerScripts[i].running == false && workerScripts[i].env.stopFlag == false) {
let p = null; // p is the script's result promise.
if (workerScripts[i].name.endsWith(".js") || workerScripts[i].name.endsWith(".ns")) {
p = startNetscript2Script(workerScripts[i]);
} else { } else {
p = startNetscript1Script(workerScripts[i]); runningScriptObj.threads = 1;
if (!(p instanceof Promise)) { continue; } }
const ramUsage = roundToTwo(getRamUsageFromRunningScript(runningScriptObj) * threads);
const ramAvailable = server.maxRam - server.ramUsed;
if (ramUsage > ramAvailable) {
dialogBoxCreate(
`Not enough RAM to run script ${runningScriptObj.filename} with args ` +
`${arrayToString(runningScriptObj.args)}. This likely occurred because you re-loaded ` +
`the game and the script's RAM usage increased (either because of an update to the game or ` +
`your changes to the script.)`
);
return;
}
server.ramUsed = roundToTwo(server.ramUsed + ramUsage);
// Create the WorkerScript
const s = new WorkerScript(runningScriptObj, NetscriptFunctions);
s.ramUsage = ramUsage;
// Start the script's execution
let p = null; // Script's resulting promise
if (s.name.endsWith(".js") || s.name.endsWith(".ns")) {
p = startNetscript2Script(s);
} else {
p = startNetscript1Script(s);
if (!(p instanceof Promise)) { return; }
} }
// Once the code finishes (either resolved or rejected, doesnt matter), set its // Once the code finishes (either resolved or rejected, doesnt matter), set its
// running status to false // running status to false
p.then(function(w) { p.then(function(w) {
console.log("Stopping script " + w.name + " because it finished running naturally"); console.log("Stopping script " + w.name + " because it finished running naturally");
w.running = false; killWorkerScript(s);
w.env.stopFlag = true;
w.scriptRef.log("Script finished running"); w.scriptRef.log("Script finished running");
}).catch(function(w) { }).catch(function(w) {
if (w instanceof Error) { if (w instanceof Error) {
@ -502,6 +485,7 @@ export function runScriptsLoop() {
w.scriptRef.log("Script crashed with runtime error"); w.scriptRef.log("Script crashed with runtime error");
} else { } else {
w.scriptRef.log("Script killed"); w.scriptRef.log("Script killed");
return; // Already killed, so stop here
} }
w.running = false; w.running = false;
w.env.stopFlag = true; w.env.stopFlag = true;
@ -513,64 +497,13 @@ export function runScriptsLoop() {
dialogBoxCreate("An unknown script died for an unknown reason. This is a bug please contact game dev"); dialogBoxCreate("An unknown script died for an unknown reason. This is a bug please contact game dev");
console.log(w); console.log(w);
} }
killWorkerScript(s);
}); });
}
}
setTimeoutRef(runScriptsLoop, 3e3); // Add the WorkerScript to the global pool
}
/**
* Queues a script to be killed by setting its stop flag to true. This
* kills and timed/blocking Netscript functions (like hack(), sleep(), etc.) and
* prevents any further execution of Netscript functions.
* The runScriptsLoop() handles the actual deletion of the WorkerScript
*/
export function killWorkerScript(runningScriptObj, serverIp) {
for (var i = 0; i < workerScripts.length; i++) {
if (workerScripts[i].name == runningScriptObj.filename && workerScripts[i].serverIp == serverIp &&
compareArrays(workerScripts[i].args, runningScriptObj.args)) {
workerScripts[i].env.stopFlag = true;
killNetscriptDelay(workerScripts[i]);
return true;
}
}
return false;
}
/**
* Given a RunningScript object, queues that script to be run
*/
export function addWorkerScript(runningScriptObj, server) {
var filename = runningScriptObj.filename;
//Update server's ram usage
var threads = 1;
if (runningScriptObj.threads && !isNaN(runningScriptObj.threads)) {
threads = runningScriptObj.threads;
} else {
runningScriptObj.threads = 1;
}
var ramUsage = roundToTwo(getRamUsageFromRunningScript(runningScriptObj) * threads);
var ramAvailable = server.maxRam - server.ramUsed;
if (ramUsage > ramAvailable) {
dialogBoxCreate("Not enough RAM to run script " + runningScriptObj.filename + " with args " +
arrayToString(runningScriptObj.args) + ". This likely occurred because you re-loaded " +
"the game and the script's RAM usage increased (either because of an update to the game or " +
"your changes to the script.)");
return;
}
server.ramUsed = roundToTwo(server.ramUsed + ramUsage);
//Create the WorkerScript
var s = new WorkerScript(runningScriptObj, NetscriptFunctions);
s.ramUsage = ramUsage;
//Add the WorkerScript to the Active Scripts list
addActiveScriptsItem(s);
//Add the WorkerScript
workerScripts.push(s); workerScripts.push(s);
WorkerScriptStartStopEventEmitter.emitEvent();
return; return;
} }

@ -1,4 +1,3 @@
import { deleteActiveScriptsItem } from "./ActiveScriptsUI";
import { Augmentations } from "./Augmentation/Augmentations"; import { Augmentations } from "./Augmentation/Augmentations";
import { import {
augmentationExists, augmentationExists,

@ -1,5 +1,7 @@
// Class representing a Script instance that is actively running. /**
// A Script can have multiple active instances * Class representing a Script instance that is actively running.
* A Script can have multiple active instances
*/
import { Script } from "./Script"; import { Script } from "./Script";
import { FconfSettings } from "../Fconf/FconfSettings"; import { FconfSettings } from "../Fconf/FconfSettings";
import { Settings } from "../Settings/Settings"; import { Settings } from "../Settings/Settings";
@ -22,10 +24,8 @@ export class RunningScript {
// Script arguments // Script arguments
args: any[] = []; args: any[] = [];
// Holds a map of servers hacked, where server = key and the value for each // Map of [key: server ip] -> Hacking data. Used for offline progress calculations.
// server is an array of four numbers. The four numbers represent: // Hacking data format: [MoneyStolen, NumTimesHacked, NumTimesGrown, NumTimesWeaken]
// [MoneyStolen, NumTimesHacked, NumTimesGrown, NumTimesWeaken]
// This data is used for offline progress
dataMap: IMap<number[]> = {}; dataMap: IMap<number[]> = {};
// Script filename // Script filename

@ -1,6 +1,9 @@
// Class representing a script file /**
// This does NOT represent a script that is actively running and * Class representing a script file.
// being evaluated. See RunningScript for that *
* This does NOT represent a script that is actively running and
* being evaluated. See RunningScript for that
*/
import { calculateRamUsage } from "./RamCalculations"; import { calculateRamUsage } from "./RamCalculations";
import { Page, routing } from "../ui/navigationTracking"; import { Page, routing } from "../ui/navigationTracking";
@ -34,7 +37,6 @@ export class Script {
// IP of server that this script is on. // IP of server that this script is on.
server: string = ""; server: string = "";
constructor(fn: string="", code: string="", server: string="", otherScripts: Script[]=[]) { constructor(fn: string="", code: string="", server: string="", otherScripts: Script[]=[]) {
this.filename = fn; this.filename = fn;
this.code = code; this.code = code;
@ -44,6 +46,9 @@ export class Script {
if (this.code !== "") { this.updateRamUsage(otherScripts); } if (this.code !== "") { this.updateRamUsage(otherScripts); }
}; };
/**
* Download the script as a file
*/
download(): void { download(): void {
const filename = this.filename + ".js"; const filename = this.filename + ".js";
const file = new Blob([this.code], {type: 'text/plain'}); const file = new Blob([this.code], {type: 'text/plain'});
@ -63,10 +68,14 @@ export class Script {
} }
} }
// Save a script FROM THE SCRIPT EDITOR /**
* Save a script from the script editor
* @param {string} code - The new contents of the script
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports
*/
saveScript(code: string, serverIp: string, otherScripts: Script[]): void { saveScript(code: string, serverIp: string, otherScripts: Script[]): void {
if (routing.isOn(Page.ScriptEditor)) { if (routing.isOn(Page.ScriptEditor)) {
//Update code and filename // Update code and filename
this.code = code.replace(/^\s+|\s+$/g, ''); this.code = code.replace(/^\s+|\s+$/g, '');
const filenameElem: HTMLInputElement | null = document.getElementById("script-editor-filename") as HTMLInputElement; const filenameElem: HTMLInputElement | null = document.getElementById("script-editor-filename") as HTMLInputElement;
@ -75,18 +84,16 @@ export class Script {
return; return;
} }
this.filename = filenameElem!.value; this.filename = filenameElem!.value;
// Server
this.server = serverIp; this.server = serverIp;
//Calculate/update ram usage, execution time, etc.
this.updateRamUsage(otherScripts); this.updateRamUsage(otherScripts);
this.module = ""; this.module = "";
} }
} }
// Updates the script's RAM usage based on its code /**
* Calculates and updates the script's RAM usage based on its code
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports
*/
async updateRamUsage(otherScripts: Script[]) { async updateRamUsage(otherScripts: Script[]) {
var res = await calculateRamUsage(this.code, otherScripts); var res = await calculateRamUsage(this.code, otherScripts);
if (res > 0) { if (res > 0) {

@ -53,7 +53,8 @@ import {
import { showLiterature } from "./Literature"; import { showLiterature } from "./Literature";
import { Message } from "./Message/Message"; import { Message } from "./Message/Message";
import { showMessage } from "./Message/MessageHelpers"; import { showMessage } from "./Message/MessageHelpers";
import { killWorkerScript, addWorkerScript } from "./NetscriptWorker"; import { addWorkerScript } from "./NetscriptWorker";
import { killWorkerScript } from "./Netscript/killWorkerScript";
import { Player } from "./Player"; import { Player } from "./Player";
import { hackWorldDaemon } from "./RedPill"; import { hackWorldDaemon } from "./RedPill";
import { RunningScript } from "./Script/RunningScript"; import { RunningScript } from "./Script/RunningScript";

@ -1,9 +1,13 @@
/**
* Game engine. Handles the main game loop as well as the main UI pages
*
* TODO: Separate UI functionality into its own component
*/
import { import {
convertTimeMsToTimeElapsedString, convertTimeMsToTimeElapsedString,
replaceAt replaceAt
} from "../utils/StringHelperFunctions"; } from "../utils/StringHelperFunctions";
import { logBoxUpdateText, logBoxOpened } from "../utils/LogBox"; import { logBoxUpdateText, logBoxOpened } from "../utils/LogBox";
import { updateActiveScriptsItems } from "./ActiveScriptsUI";
import { Augmentations } from "./Augmentation/Augmentations"; import { Augmentations } from "./Augmentation/Augmentations";
import { import {
initAugmentations, initAugmentations,
@ -41,9 +45,9 @@ import { LocationName } from "./Locations/data/LocationNames";
import { LocationRoot } from "./Locations/ui/Root"; import { LocationRoot } from "./Locations/ui/Root";
import { checkForMessagesToSend, initMessages } from "./Message/MessageHelpers"; import { checkForMessagesToSend, initMessages } from "./Message/MessageHelpers";
import { inMission, currMission } from "./Missions"; import { inMission, currMission } from "./Missions";
import { workerScripts } from "./Netscript/WorkerScripts";
import { import {
loadAllRunningScripts, loadAllRunningScripts,
runScriptsLoop,
updateOnlineScriptTimes, updateOnlineScriptTimes,
} from "./NetscriptWorker"; } from "./NetscriptWorker";
import { Player } from "./Player"; import { Player } from "./Player";
@ -87,6 +91,8 @@ import { displayCharacterInfo } from "./ui/displayCharacterInfo";
import { Page, routing } from "./ui/navigationTracking"; import { Page, routing } from "./ui/navigationTracking";
import { numeralWrapper } from "./ui/numeralFormat"; import { numeralWrapper } from "./ui/numeralFormat";
import { setSettingsLabels } from "./ui/setSettingsLabels"; import { setSettingsLabels } from "./ui/setSettingsLabels";
import { ActiveScriptsRoot } from "./ui/ActiveScripts/Root";
import { initializeMainMenuHeaders } from "./ui/MainMenu/Headers"; import { initializeMainMenuHeaders } from "./ui/MainMenu/Headers";
import { initializeMainMenuLinks, MainMenuLinks } from "./ui/MainMenu/Links"; import { initializeMainMenuLinks, MainMenuLinks } from "./ui/MainMenu/Links";
@ -259,7 +265,10 @@ const Engine = {
Engine.hideAllContent(); Engine.hideAllContent();
Engine.Display.activeScriptsContent.style.display = "block"; Engine.Display.activeScriptsContent.style.display = "block";
routing.navigateTo(Page.ActiveScripts); routing.navigateTo(Page.ActiveScripts);
updateActiveScriptsItems(); ReactDOM.render(
<ActiveScriptsRoot p={Player} workerScripts={workerScripts} />,
Engine.Display.activeScriptsContent
)
MainMenuLinks.ActiveScripts.classList.add("active"); MainMenuLinks.ActiveScripts.classList.add("active");
}, },
@ -471,7 +480,10 @@ const Engine = {
Engine.Display.terminalContent.style.display = "none"; Engine.Display.terminalContent.style.display = "none";
Engine.Display.characterContent.style.display = "none"; Engine.Display.characterContent.style.display = "none";
Engine.Display.scriptEditorContent.style.display = "none"; Engine.Display.scriptEditorContent.style.display = "none";
Engine.Display.activeScriptsContent.style.display = "none"; Engine.Display.activeScriptsContent.style.display = "none";
ReactDOM.unmountComponentAtNode(Engine.Display.activeScriptsContent);
clearHacknetNodesUI(); clearHacknetNodesUI();
Engine.Display.createProgramContent.style.display = "none"; Engine.Display.createProgramContent.style.display = "none";
@ -802,13 +814,14 @@ const Engine = {
} }
if (Engine.Counters.updateActiveScriptsDisplay <= 0) { if (Engine.Counters.updateActiveScriptsDisplay <= 0) {
// Always update, but make the interval longer if the page isn't active
updateActiveScriptsItems();
if (routing.isOn(Page.ActiveScripts)) { if (routing.isOn(Page.ActiveScripts)) {
Engine.Counters.updateActiveScriptsDisplay = 5; ReactDOM.render(
} else { <ActiveScriptsRoot p={Player} workerScripts={workerScripts} />,
Engine.Counters.updateActiveScriptsDisplay = 10; Engine.Display.activeScriptsContent
)
} }
Engine.Counters.updateActiveScriptsDisplay = 5;
} }
if (Engine.Counters.updateDisplays <= 0) { if (Engine.Counters.updateDisplays <= 0) {
@ -1515,9 +1528,6 @@ const Engine = {
start: function() { start: function() {
// Run main loop // Run main loop
Engine.idleTimer(); Engine.idleTimer();
// Script-processing loop
runScriptsLoop();
} }
}; };

@ -9,6 +9,7 @@ import "../css/characteroverview.scss";
import "../css/terminal.scss"; import "../css/terminal.scss";
import "../css/scripteditor.scss"; import "../css/scripteditor.scss";
import "../css/codemirror-overrides.scss"; import "../css/codemirror-overrides.scss";
import "../css/activescripts.scss";
import "../css/hacknetnodes.scss"; import "../css/hacknetnodes.scss";
import "../css/menupages.scss"; import "../css/menupages.scss";
import "../css/augmentations.scss"; import "../css/augmentations.scss";

@ -0,0 +1,38 @@
/**
* Root React Component for the "Active Scripts" UI page. This page displays
* and provides information about all of the player's scripts that are currently running
*/
import * as React from "react";
import { ScriptProduction } from "./ScriptProduction";
import { ServerAccordions } from "./ServerAccordions";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { IPlayer } from "../../PersonObjects/IPlayer";
type IProps = {
p: IPlayer;
workerScripts: WorkerScript[];
}
export class ActiveScriptsRoot extends React.Component<IProps> {
constructor(props: IProps) {
super(props);
}
render() {
return (
<>
<p>
This page displays a list of all of your scripts that are currently
running across every machine. It also provides information about each
script's production. The scripts are categorized by the hostname of
the servers on which they are running.
</p>
<ScriptProduction {...this.props} />
<ServerAccordions {...this.props} />
</>
)
}
}

@ -0,0 +1,46 @@
/**
* React Component for displaying the total production and production rate
* of scripts on the 'Active Scripts' UI page
*/
import * as React from "react";
import { numeralWrapper } from "../numeralFormat";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { IPlayer } from "../../PersonObjects/IPlayer";
type IProps = {
p: IPlayer;
workerScripts: WorkerScript[];
}
export function ScriptProduction(props: IProps): React.ReactElement {
const prodRateSinceLastAug = props.p.scriptProdSinceLastAug / (props.p.playtimeSinceLastAug / 1000);
let onlineProduction = 0;
for (const ws of props.workerScripts) {
onlineProduction += (ws.scriptRef.onlineMoneyMade / ws.scriptRef.onlineRunningTime);
}
return (
<p id="active-scripts-total-prod">
Total online production of Active scripts:
<span className="money-gold">
<span id="active-scripts-total-production-active">
{numeralWrapper.formatMoney(onlineProduction)}
</span> / sec
</span><br />
Total online production since last Aug installation:
<span id="active-scripts-total-prod-aug-total" className="money-gold">
{numeralWrapper.formatMoney(props.p.scriptProdSinceLastAug)}
</span>
(<span className="money-gold">
<span id="active-scripts-total-prod-aug-avg" className="money-gold">
{numeralWrapper.formatMoney(prodRateSinceLastAug)}
</span> / sec
</span>)
</p>
)
}

@ -0,0 +1,49 @@
/**
* React Component for rendering the Accordion element for a single
* server in the 'Active Scripts' UI page
*/
import * as React from "react";
import { WorkerScriptAccordion } from "./WorkerScriptAccordion";
import { Accordion } from "../React/Accordion";
import { BaseServer } from "../../Server/BaseServer";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { createProgressBarText } from "../../../utils/helpers/createProgressBarText";
type IProps = {
server: BaseServer;
workerScripts: WorkerScript[];
}
export function ServerAccordion(props: IProps): React.ReactElement {
const server = props.server;
// Accordion's header text
// TODO: calculate the longest hostname length rather than hard coding it
const longestHostnameLength = 18;
const paddedName = `${server.hostname}${" ".repeat(longestHostnameLength)}`.slice(0, Math.max(server.hostname.length, longestHostnameLength));
const barOptions = {
progress: server.ramUsed / server.maxRam,
totalTicks: 30
};
const headerTxt = `${paddedName} ${createProgressBarText(barOptions)}`;
const scripts = props.workerScripts.map((ws) => {
return (
<WorkerScriptAccordion key={`${ws.name}_${ws.args}`} workerScript={ws} />
)
});
return (
<Accordion
headerContent={
<pre>{headerTxt}</pre>
}
panelContent={
<ul>{scripts}</ul>
}
/>
)
}

@ -0,0 +1,109 @@
/**
* React Component for rendering the Accordion elements for all servers
* on which scripts are running
*/
import * as React from "react";
import { ServerAccordion } from "./ServerAccordion";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { WorkerScriptStartStopEventEmitter } from "../../Netscript/WorkerScriptStartStopEventEmitter";
import { getServer } from "../../Server/ServerHelpers";
import { BaseServer } from "../../Server/BaseServer";
// Map of server hostname -> all workerscripts on that server for all active scripts
interface IServerData {
server: BaseServer;
workerScripts: WorkerScript[];
}
interface IServerToScriptsMap {
[key: string]: IServerData;
}
type IProps = {
workerScripts: WorkerScript[];
};
type IState = {
rerenderFlag: boolean;
}
const subscriberId = "ActiveScriptsUI";
export class ServerAccordions extends React.Component<IProps, IState> {
serverToScriptMap: IServerToScriptsMap = {};
constructor(props: IProps) {
super(props);
this.state = {
rerenderFlag: false,
}
this.updateServerToScriptsMap();
this.rerender = this.rerender.bind(this);
}
componentDidMount() {
WorkerScriptStartStopEventEmitter.addSubscriber({
cb: this.rerender,
id: subscriberId,
})
}
componentWillUnmount() {
WorkerScriptStartStopEventEmitter.removeSubscriber(subscriberId);
}
updateServerToScriptsMap(): void {
const map: IServerToScriptsMap = {};
for (const ws of this.props.workerScripts) {
const server = getServer(ws.serverIp);
if (server == null) {
console.warn(`WorkerScript has invalid IP address: ${ws.serverIp}`);
continue;
}
if (map[server.hostname] == null) {
map[server.hostname] = {
server: server,
workerScripts: [],
};
}
map[server.hostname].workerScripts.push(ws);
}
this.serverToScriptMap = map;
}
rerender() {
this.updateServerToScriptsMap();
this.setState((prevState) => {
return { rerenderFlag: !prevState.rerenderFlag }
});
}
render() {
const elems = Object.keys(this.serverToScriptMap).map((serverName) => {
const data = this.serverToScriptMap[serverName];
return (
<ServerAccordion
key={serverName}
server={data.server}
workerScripts={data.workerScripts}
/>
)
});
return (
<ul className="active-scripts-list" id="active-scripts-list">
{elems}
</ul>
)
}
}

@ -0,0 +1,77 @@
/**
* React Component for displaying a single WorkerScript's info as an
* Accordion element
*/
import * as React from "react";
import { numeralWrapper } from "../numeralFormat";
import { Accordion } from "../React/Accordion";
import { AccordionButton } from "../React/AccordionButton";
import { killWorkerScript } from "../../Netscript/killWorkerScript";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { dialogBoxCreate } from "../../../utils/DialogBox";
import { logBoxCreate } from "../../../utils/LogBox";
import { convertTimeMsToTimeElapsedString } from "../../../utils/StringHelperFunctions";
import { arrayToString } from "../../../utils/helpers/arrayToString";
type IProps = {
workerScript: WorkerScript;
}
export function WorkerScriptAccordion(props: IProps): React.ReactElement {
const workerScript = props.workerScript;
const scriptRef = workerScript.scriptRef;
const logClickHandler = logBoxCreate.bind(null, scriptRef);
const killScript = killWorkerScript.bind(null, scriptRef, scriptRef.server);
function killScriptClickHandler() {
killScript();
dialogBoxCreate("Killing script");
}
// Calculations for script stats
const onlineMps = scriptRef.onlineMoneyMade / scriptRef.onlineRunningTime;
const onlineEps = scriptRef.onlineExpGained / scriptRef.onlineRunningTime;
const offlineMps = scriptRef.offlineMoneyMade / scriptRef.offlineRunningTime;
const offlineEps = scriptRef.offlineExpGained / scriptRef.offlineRunningTime;
return (
<Accordion
headerClass="active-scripts-script-header"
headerContent={
<>{props.workerScript.name}</>
}
panelClass="active-scripts-script-panel"
panelContent={
<>
<pre>Threads: {props.workerScript.scriptRef.threads}</pre>
<pre>Args: {arrayToString(props.workerScript.args)}</pre>
<pre>Online Time: {convertTimeMsToTimeElapsedString(scriptRef.onlineRunningTime * 1e3)}</pre>
<pre>Offline Time: {convertTimeMsToTimeElapsedString(scriptRef.offlineRunningTime * 1e3)}</pre>
<pre>Total online production: {numeralWrapper.formatMoney(scriptRef.onlineMoneyMade)}</pre>
<pre>{(Array(26).join(" ") + numeralWrapper.formatBigNumber(scriptRef.onlineExpGained) + " hacking exp")}</pre>
<pre>Online production rate: {numeralWrapper.formatMoney(onlineMps)} / second</pre>
<pre>{(Array(25).join(" ") + numeralWrapper.formatBigNumber(onlineEps) + " hacking exp / second")}</pre>
<pre>Total offline production: {numeralWrapper.formatMoney(scriptRef.offlineMoneyMade)}</pre>
<pre>{(Array(27).join(" ") + numeralWrapper.formatBigNumber(scriptRef.offlineExpGained) + " hacking exp")}</pre>
<pre>Offline production rate: {numeralWrapper.formatMoney(offlineMps)} / second</pre>
<pre>{(Array(26).join(" ") + numeralWrapper.formatBigNumber(offlineEps) + " hacking exp / second")}</pre>
<AccordionButton
onClick={logClickHandler}
text="Log"
/>
<AccordionButton
onClick={killScriptClickHandler}
text="Kill Script"
/>
</>
}
/>
)
}

@ -4,9 +4,12 @@
import * as React from "react"; import * as React from "react";
type IProps = { type IProps = {
headerClass?: string; // Override default class
headerContent: React.ReactElement; headerContent: React.ReactElement;
panelClass?: string; // Override default class
panelContent: React.ReactElement; panelContent: React.ReactElement;
panelInitiallyOpened?: boolean; panelInitiallyOpened?: boolean;
style?: string;
} }
type IState = { type IState = {
@ -44,12 +47,21 @@ export class Accordion extends React.Component<IProps, IState> {
} }
render() { render() {
let className = "accordion-header";
if (typeof this.props.headerClass === "string") {
className = this.props.headerClass;
}
return ( return (
<> <>
<button className={"accordion-header"} onClick={this.handleHeaderClick}> <button className={className} onClick={this.handleHeaderClick}>
{this.props.headerContent} {this.props.headerContent}
</button> </button>
<AccordionPanel opened={this.state.panelOpened} panelContent={this.props.panelContent} /> <AccordionPanel
opened={this.state.panelOpened}
panelClass={this.props.panelClass}
panelContent={this.props.panelContent}
/>
</> </>
) )
} }
@ -57,6 +69,7 @@ export class Accordion extends React.Component<IProps, IState> {
type IPanelProps = { type IPanelProps = {
opened: boolean; opened: boolean;
panelClass?: string; // Override default class
panelContent: React.ReactElement; panelContent: React.ReactElement;
} }
@ -66,8 +79,13 @@ class AccordionPanel extends React.Component<IPanelProps, any> {
} }
render() { render() {
let className = "accordion-panel"
if (typeof this.props.panelClass === "string") {
className = this.props.panelClass;
}
return ( return (
<div className={"accordion-panel"}> <div className={className}>
{this.props.panelContent} {this.props.panelContent}
</div> </div>
) )

@ -0,0 +1,52 @@
/**
* Basic stateless button that uses the 'accordion-button' css class.
* This class has a black background so that it does not clash with the default
* accordion coloring
*/
import * as React from "react";
interface IProps {
addClasses?: string;
disabled?: boolean;
id?: string;
onClick?: (e: React.MouseEvent<HTMLElement>) => any;
style?: object;
text: string;
tooltip?: string;
}
type IInnerHTMLMarkup = {
__html: string;
}
export function AccordionButton(props: IProps): React.ReactElement {
const hasTooltip = props.tooltip != null && props.tooltip !== "";
// TODO Add a disabled class for accordion buttons?
let className = "accordion-button";
if (hasTooltip) {
className += " tooltip";
}
if (typeof props.addClasses === "string") {
className += ` ${props.addClasses}`;
}
// Tooltip will be set using inner HTML
let tooltipMarkup: IInnerHTMLMarkup | null;
if (hasTooltip) {
tooltipMarkup = {
__html: props.tooltip!
}
}
return (
<button className={className} id={props.id} onClick={props.onClick} style={props.style}>
{props.text}
{
hasTooltip &&
<span className={"tooltiptext"} dangerouslySetInnerHTML={tooltipMarkup!}></span>
}
</button>
)
}

50
src/utils/EventEmitter.ts Normal file

@ -0,0 +1,50 @@
/**
* Generic Event Emitter class following a subscribe/publish paradigm.
*/
import { IMap } from "../types";
type cbFn = (...args: any[]) => any;
export interface ISubscriber {
/**
* Callback function that will be run when an event is emitted
*/
cb: cbFn;
/**
* Name/identifier for this subscriber
*/
id: string;
}
export class EventEmitter {
/**
* Map of Subscriber name -> Callback function
*/
subscribers: IMap<cbFn> = {};
constructor(subs?: ISubscriber[]) {
if (Array.isArray(subs)) {
for (const s of subs) {
this.addSubscriber(s);
}
}
}
addSubscriber(s: ISubscriber) {
this.subscribers[s.id] = s.cb;
}
emitEvent(...args: any[]): void {
for (const s in this.subscribers) {
const cb = this.subscribers[s];
cb(args);
}
}
removeSubscriber(id: string) {
delete this.subscribers[id];
}
}

@ -1,68 +0,0 @@
import {killWorkerScript} from "../src/NetscriptWorker";
import {clearEventListeners} from "./uiHelpers/clearEventListeners";
import {arrayToString} from "./helpers/arrayToString";
$(document).keydown(function(event) {
if (logBoxOpened && event.keyCode == 27) {
logBoxClose();
}
});
function logBoxInit() {
var closeButton = document.getElementById("log-box-close");
logBoxClose();
//Close Dialog box
closeButton.addEventListener("click", function() {
logBoxClose();
return false;
});
document.getElementById("log-box-text-header").style.display = "inline-block";
};
document.addEventListener("DOMContentLoaded", logBoxInit, false);
function logBoxClose() {
logBoxOpened = false;
var logBox = document.getElementById("log-box-container");
logBox.style.display = "none";
}
function logBoxOpen() {
logBoxOpened = true;
var logBox = document.getElementById("log-box-container");
logBox.style.display = "block";
}
var logBoxOpened = false;
var logBoxCurrentScript = null;
function logBoxCreate(script) {
logBoxCurrentScript = script;
var killScriptBtn = clearEventListeners("log-box-kill-script");
killScriptBtn.addEventListener("click", ()=>{
killWorkerScript(script, script.server);
return false;
});
document.getElementById('log-box-kill-script').style.display = "inline-block";
logBoxOpen();
document.getElementById("log-box-text-header").innerHTML =
logBoxCurrentScript.filename + " " + arrayToString(logBoxCurrentScript.args) + ":<br><br>";
logBoxCurrentScript.logUpd = true;
logBoxUpdateText();
}
function logBoxUpdateText() {
var txt = document.getElementById("log-box-text");
if (logBoxCurrentScript && logBoxOpened && txt && logBoxCurrentScript.logUpd) {
txt.innerHTML = "";
for (var i = 0; i < logBoxCurrentScript.logs.length; ++i) {
txt.innerHTML += logBoxCurrentScript.logs[i];
txt.innerHTML += "<br>";
}
logBoxCurrentScript.logUpd = false;
}
}
export {logBoxCreate, logBoxUpdateText, logBoxOpened, logBoxCurrentScript};

106
utils/LogBox.ts Normal file

@ -0,0 +1,106 @@
import { killWorkerScript } from "../src/Netscript/killWorkerScript";
import { RunningScript } from "../src/Script/RunningScript";
import { clearEventListeners } from "./uiHelpers/clearEventListeners";
import { arrayToString } from "./helpers/arrayToString";
import { KEY } from "./helpers/keyCodes";
document.addEventListener("keydown", function(event: KeyboardEvent) {
if (logBoxOpened && event.keyCode == KEY.ESC) {
logBoxClose();
}
});
let logBoxContainer: HTMLElement | null;
let textHeader: HTMLElement | null;
let logText: HTMLElement | null;
function logBoxInit(): void {
// Initialize Close button click listener
const closeButton = document.getElementById("log-box-close");
if (closeButton == null) {
console.error(`Could not find LogBox's close button`);
return;
}
closeButton.addEventListener("click", function() {
logBoxClose();
return false;
});
// Initialize text header
textHeader = document.getElementById("log-box-text-header");
if (textHeader instanceof HTMLElement) {
textHeader.style.display = "inline-block";
}
// Initialize references to other DOM elements
logBoxContainer = document.getElementById("log-box-container");
logText = document.getElementById("log-box-text");
logBoxClose();
document.removeEventListener("DOMContentLoaded", logBoxInit);
};
document.addEventListener("DOMContentLoaded", logBoxInit);
function logBoxClose() {
logBoxOpened = false;
if (logBoxContainer instanceof HTMLElement) {
logBoxContainer.style.display = "none";
}
}
function logBoxOpen() {
logBoxOpened = true;
if (logBoxContainer instanceof HTMLElement) {
logBoxContainer.style.display = "block";
}
}
export let logBoxOpened = false;
let logBoxCurrentScript: RunningScript | null = null;
export function logBoxCreate(script: RunningScript) {
logBoxCurrentScript = script;
const killScriptBtn = clearEventListeners("log-box-kill-script");
if (killScriptBtn == null) {
console.error(`Could not find LogBox's 'Kill Script' button`);
return;
}
killScriptBtn.addEventListener("click", () => {
killWorkerScript(script, script.server);
return false;
});
killScriptBtn.style.display = "inline-block";
logBoxOpen();
if (textHeader instanceof HTMLElement) {
textHeader.innerHTML = `${logBoxCurrentScript.filename} ${arrayToString(logBoxCurrentScript.args)}:<br><br>`;
} else {
console.warn(`LogBox's Text Header DOM element is null`);
}
logBoxCurrentScript.logUpd = true;
logBoxUpdateText();
}
export function logBoxUpdateText() {
if (!(logText instanceof HTMLElement)) { return; }
if (logBoxCurrentScript && logBoxOpened && logBoxCurrentScript.logUpd) {
logText.innerHTML = "";
for (let i = 0; i < logBoxCurrentScript.logs.length; ++i) {
logText.innerHTML += logBoxCurrentScript.logs[i];
logText.innerHTML += "<br>";
}
logBoxCurrentScript.logUpd = false;
}
}