diff --git a/css/activescripts.scss b/css/activescripts.scss new file mode 100644 index 000000000..8dbfbc2e3 --- /dev/null +++ b/css/activescripts.scss @@ -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%; + } +} diff --git a/css/menupages.scss b/css/menupages.scss index b0b45c723..2c94436de 100644 --- a/css/menupages.scss +++ b/css/menupages.scss @@ -18,126 +18,6 @@ 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-container { position: fixed; diff --git a/css/styles.scss b/css/styles.scss index 563a343de..febdf156a 100644 --- a/css/styles.scss +++ b/css/styles.scss @@ -243,8 +243,8 @@ a:visited { /* Accordion menus (Header with collapsible panel) */ .accordion-header { background-color: #444; - font-size: $defaultFontSize * 1.25; color: #fff; + font-size: $defaultFontSize * 1.25; margin: 6px 6px 0 6px; padding: 4px 6px; cursor: pointer; diff --git a/src/ActiveScriptsUI.js b/src/ActiveScriptsUI.js deleted file mode 100644 index 998614fb4..000000000 --- a/src/ActiveScriptsUI.js +++ /dev/null @@ -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 + "
" + - "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, " "); - - 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, " "); - - // 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, " "); - - 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, " "); - - item.innerHTML = onlineTime + "
" + offlineTime + "
" + onlineTotalMoneyMade + "
" + onlineTotalExpEarned + "
" + - onlineMpsText + "
" + onlineEpsText + "
" + offlineTotalMoneyMade + "
" + offlineTotalExpEarned + "
" + - offlineMpsText + "
" + offlineEpsText + "
"; - return onlineMps; -} - -export {addActiveScriptsItem, deleteActiveScriptsItem, updateActiveScriptsItems}; diff --git a/src/Netscript/WorkerScript.ts b/src/Netscript/WorkerScript.ts index 2549c06e0..6b99c43d5 100644 --- a/src/Netscript/WorkerScript.ts +++ b/src/Netscript/WorkerScript.ts @@ -32,6 +32,11 @@ export class WorkerScript { */ 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 */ diff --git a/src/Netscript/WorkerScriptStartStopEventEmitter.ts b/src/Netscript/WorkerScriptStartStopEventEmitter.ts new file mode 100644 index 000000000..8fdb922b8 --- /dev/null +++ b/src/Netscript/WorkerScriptStartStopEventEmitter.ts @@ -0,0 +1,6 @@ +/** + * Event emitter that triggers when scripts are started/stopped + */ +import { EventEmitter } from "../utils/EventEmitter"; + +export const WorkerScriptStartStopEventEmitter = new EventEmitter(); diff --git a/src/Netscript/WorkerScripts.ts b/src/Netscript/WorkerScripts.ts new file mode 100644 index 000000000..50515f413 --- /dev/null +++ b/src/Netscript/WorkerScripts.ts @@ -0,0 +1,6 @@ +/** + * Global pool of all active scripts (scripts that are currently running) + */ +import { WorkerScript } from "./WorkerScript"; + +export const workerScripts: WorkerScript[] = []; diff --git a/src/Netscript/killWorkerScript.ts b/src/Netscript/killWorkerScript.ts new file mode 100644 index 000000000..59abd4e93 --- /dev/null +++ b/src/Netscript/killWorkerScript.ts @@ -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(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(); + } + } + } +} diff --git a/src/NetscriptEvaluator.js b/src/NetscriptEvaluator.js index cad189a8e..323919ad4 100644 --- a/src/NetscriptEvaluator.js +++ b/src/NetscriptEvaluator.js @@ -1,21 +1,9 @@ -import { WorkerScript } from "./Netscript/WorkerScript"; -import { getServer } from "./Server/ServerHelpers"; - import { setTimeoutRef } from "./utils/SetTimeoutRef"; import { parse, Node } from "../utils/acorn"; import { isValidIPAddress } from "../utils/helpers/isValidIPAddress"; 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) { return new Promise(function(resolve, reject) { workerScript.delay = setTimeoutRef(() => { diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index 308054333..394dd0156 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -3,7 +3,6 @@ const vsprintf = require("sprintf-js").vsprintf; import { getRamCost } from "./Netscript/RamCostGenerator"; -import { updateActiveScriptsItems } from "./ActiveScriptsUI"; import { Augmentation } from "./Augmentation/Augmentation"; import { Augmentations } from "./Augmentation/Augmentations"; import { @@ -120,11 +119,11 @@ import { } from "./NetscriptBladeburner"; import * as nsGang from "./NetscriptGang"; import { - workerScripts, - killWorkerScript, NetscriptPorts, runScriptFromScript, } from "./NetscriptWorker"; +import { killWorkerScript } from "./Netscript/killWorkerScript"; +import { workerScripts } from "./Netscript/WorkerScripts"; import { makeRuntimeRejectMsg, netscriptDelay, diff --git a/src/NetscriptWorker.js b/src/NetscriptWorker.js index 9c519e8b1..319693358 100644 --- a/src/NetscriptWorker.js +++ b/src/NetscriptWorker.js @@ -2,20 +2,17 @@ * Functions for handling WorkerScripts, which are the underlying mechanism * that allows for scripts to run */ +import { killWorkerScript } from "./Netscript/killWorkerScript"; 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 { Engine } from "./engine"; import { Interpreter } from "./JSInterpreter"; import { isScriptErrorMessage, makeRuntimeRejectMsg, - killNetscriptDelay } from "./NetscriptEvaluator"; import { NetscriptFunctions } from "./NetscriptFunctions"; import { executeJSScript } from "./NetscriptJSEvaluator"; @@ -42,9 +39,7 @@ import { isString } from "../utils/StringHelperFunctions"; const walk = require("acorn/dist/walk"); -//Array containing all scripts that are running across all servers, to easily run them all -export const workerScripts = []; - +// Netscript Ports are instantiated here export const NetscriptPorts = []; for (var i = 0; i < CONSTANTS.NumNetscriptPorts; ++i) { NetscriptPorts.push(new NetscriptPort()); @@ -52,10 +47,9 @@ for (var i = 0; i < CONSTANTS.NumNetscriptPorts; ++i) { export function prestigeWorkerScripts() { for (var i = 0; i < workerScripts.length; ++i) { - deleteActiveScriptsItem(workerScripts[i]); + // TODO Signal event emitter workerScripts[i].env.stopFlag = true; } - updateActiveScriptsItems(5000); //Force UI to update workerScripts.length = 0; } @@ -141,7 +135,7 @@ function startNetscript2Script(workerScript) { } function startNetscript1Script(workerScript) { - var code = workerScript.code; + const code = workerScript.code; workerScript.running = true; //Process imports @@ -413,164 +407,103 @@ function processNetscript1Imports(code, workerScript) { return res; } -// Loop through workerScripts and run every script that is not currently running -export function runScriptsLoop() { - let scriptDeleted = false; - - // Delete any scripts that finished or have been killed. Loop backwards bc removing items screws up indexing - for (let i = workerScripts.length - 1; i >= 0; i--) { - if (workerScripts[i].running == false && workerScripts[i].env.stopFlag == true) { - scriptDeleted = true; - // 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 { - p = startNetscript1Script(workerScripts[i]); - if (!(p instanceof Promise)) { continue; } - } - - // Once the code finishes (either resolved or rejected, doesnt matter), set its - // running status to false - p.then(function(w) { - console.log("Stopping script " + w.name + " because it finished running naturally"); - w.running = false; - w.env.stopFlag = true; - w.scriptRef.log("Script finished running"); - }).catch(function(w) { - if (w instanceof Error) { - dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer"); - console.error("Evaluating workerscript returns an Error. THIS SHOULDN'T HAPPEN: " + w.toString()); - return; - } else if (w.constructor === Array && w.length === 2 && w[0] === "RETURNSTATEMENT") { - // Script ends with a return statement - console.log("Script returning with value: " + w[1]); - // TODO maybe do something with this in the future - return; - } else if (w instanceof WorkerScript) { - if (isScriptErrorMessage(w.errorMessage)) { - var errorTextArray = w.errorMessage.split("|"); - if (errorTextArray.length != 4) { - console.log("ERROR: Something wrong with Error text in evaluator..."); - console.log("Error text: " + errorText); - return; - } - var serverIp = errorTextArray[1]; - var scriptName = errorTextArray[2]; - var errorMsg = errorTextArray[3]; - - dialogBoxCreate("Script runtime error:
Server Ip: " + serverIp + - "
Script name: " + scriptName + - "
Args:" + arrayToString(w.args) + "
" + errorMsg); - w.scriptRef.log("Script crashed with runtime error"); - } else { - w.scriptRef.log("Script killed"); - } - w.running = false; - w.env.stopFlag = true; - } else if (isScriptErrorMessage(w)) { - dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer"); - console.log("ERROR: Evaluating workerscript returns only error message rather than WorkerScript object. THIS SHOULDN'T HAPPEN: " + w.toString()); - return; - } else { - dialogBoxCreate("An unknown script died for an unknown reason. This is a bug please contact game dev"); - console.log(w); - } - }); - } - } - - setTimeoutRef(runScriptsLoop, 3e3); -} - /** - * 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 + * Start a script + * + * 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) { - var filename = runningScriptObj.filename; + const filename = runningScriptObj.filename; - //Update server's ram usage - var threads = 1; + // Update server's ram usage + let 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; + 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.)"); + 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); + // Create the WorkerScript + const s = new WorkerScript(runningScriptObj, NetscriptFunctions); s.ramUsage = ramUsage; - //Add the WorkerScript to the Active Scripts list - addActiveScriptsItem(s); + // 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; } + } - //Add the WorkerScript - workerScripts.push(s); + // Once the code finishes (either resolved or rejected, doesnt matter), set its + // running status to false + p.then(function(w) { + console.log("Stopping script " + w.name + " because it finished running naturally"); + killWorkerScript(s); + w.scriptRef.log("Script finished running"); + }).catch(function(w) { + if (w instanceof Error) { + dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer"); + console.error("Evaluating workerscript returns an Error. THIS SHOULDN'T HAPPEN: " + w.toString()); + return; + } else if (w.constructor === Array && w.length === 2 && w[0] === "RETURNSTATEMENT") { + // Script ends with a return statement + console.log("Script returning with value: " + w[1]); + // TODO maybe do something with this in the future + return; + } else if (w instanceof WorkerScript) { + if (isScriptErrorMessage(w.errorMessage)) { + var errorTextArray = w.errorMessage.split("|"); + if (errorTextArray.length != 4) { + console.log("ERROR: Something wrong with Error text in evaluator..."); + console.log("Error text: " + errorText); + return; + } + var serverIp = errorTextArray[1]; + var scriptName = errorTextArray[2]; + var errorMsg = errorTextArray[3]; + + dialogBoxCreate("Script runtime error:
Server Ip: " + serverIp + + "
Script name: " + scriptName + + "
Args:" + arrayToString(w.args) + "
" + errorMsg); + w.scriptRef.log("Script crashed with runtime error"); + } else { + w.scriptRef.log("Script killed"); + return; // Already killed, so stop here + } + w.running = false; + w.env.stopFlag = true; + } else if (isScriptErrorMessage(w)) { + dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer"); + console.log("ERROR: Evaluating workerscript returns only error message rather than WorkerScript object. THIS SHOULDN'T HAPPEN: " + w.toString()); + return; + } else { + dialogBoxCreate("An unknown script died for an unknown reason. This is a bug please contact game dev"); + console.log(w); + } + + killWorkerScript(s); + }); + + // Add the WorkerScript to the global pool + workerScripts.push(s); + WorkerScriptStartStopEventEmitter.emitEvent(); return; } diff --git a/src/Prestige.js b/src/Prestige.js index 6e3dc4947..ae875e366 100755 --- a/src/Prestige.js +++ b/src/Prestige.js @@ -1,4 +1,3 @@ -import { deleteActiveScriptsItem } from "./ActiveScriptsUI"; import { Augmentations } from "./Augmentation/Augmentations"; import { augmentationExists, diff --git a/src/Script/RunningScript.ts b/src/Script/RunningScript.ts index be26ddba8..eca819276 100644 --- a/src/Script/RunningScript.ts +++ b/src/Script/RunningScript.ts @@ -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 { FconfSettings } from "../Fconf/FconfSettings"; import { Settings } from "../Settings/Settings"; @@ -22,10 +24,8 @@ export class RunningScript { // Script arguments args: any[] = []; - // Holds a map of servers hacked, where server = key and the value for each - // server is an array of four numbers. The four numbers represent: - // [MoneyStolen, NumTimesHacked, NumTimesGrown, NumTimesWeaken] - // This data is used for offline progress + // Map of [key: server ip] -> Hacking data. Used for offline progress calculations. + // Hacking data format: [MoneyStolen, NumTimesHacked, NumTimesGrown, NumTimesWeaken] dataMap: IMap = {}; // Script filename diff --git a/src/Script/Script.ts b/src/Script/Script.ts index f700b0ef1..cb3200a6c 100644 --- a/src/Script/Script.ts +++ b/src/Script/Script.ts @@ -1,6 +1,9 @@ -// Class representing a script file -// This does NOT represent a script that is actively running and -// being evaluated. See RunningScript for that +/** + * Class representing a script file. + * + * This does NOT represent a script that is actively running and + * being evaluated. See RunningScript for that + */ import { calculateRamUsage } from "./RamCalculations"; import { Page, routing } from "../ui/navigationTracking"; @@ -34,7 +37,6 @@ export class Script { // IP of server that this script is on. server: string = ""; - constructor(fn: string="", code: string="", server: string="", otherScripts: Script[]=[]) { this.filename = fn; this.code = code; @@ -44,6 +46,9 @@ export class Script { if (this.code !== "") { this.updateRamUsage(otherScripts); } }; + /** + * Download the script as a file + */ download(): void { const filename = this.filename + ".js"; 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 { if (routing.isOn(Page.ScriptEditor)) { - //Update code and filename + // Update code and filename this.code = code.replace(/^\s+|\s+$/g, ''); const filenameElem: HTMLInputElement | null = document.getElementById("script-editor-filename") as HTMLInputElement; @@ -75,18 +84,16 @@ export class Script { return; } this.filename = filenameElem!.value; - - // Server this.server = serverIp; - - //Calculate/update ram usage, execution time, etc. this.updateRamUsage(otherScripts); - 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[]) { var res = await calculateRamUsage(this.code, otherScripts); if (res > 0) { diff --git a/src/Terminal.js b/src/Terminal.js index b88832954..12e150742 100644 --- a/src/Terminal.js +++ b/src/Terminal.js @@ -53,7 +53,8 @@ import { import { showLiterature } from "./Literature"; import { Message } from "./Message/Message"; import { showMessage } from "./Message/MessageHelpers"; -import { killWorkerScript, addWorkerScript } from "./NetscriptWorker"; +import { addWorkerScript } from "./NetscriptWorker"; +import { killWorkerScript } from "./Netscript/killWorkerScript"; import { Player } from "./Player"; import { hackWorldDaemon } from "./RedPill"; import { RunningScript } from "./Script/RunningScript"; diff --git a/src/engine.jsx b/src/engine.jsx index 1e8e8731b..20f2ff35b 100644 --- a/src/engine.jsx +++ b/src/engine.jsx @@ -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 { convertTimeMsToTimeElapsedString, replaceAt } from "../utils/StringHelperFunctions"; import { logBoxUpdateText, logBoxOpened } from "../utils/LogBox"; -import { updateActiveScriptsItems } from "./ActiveScriptsUI"; import { Augmentations } from "./Augmentation/Augmentations"; import { initAugmentations, @@ -41,9 +45,9 @@ import { LocationName } from "./Locations/data/LocationNames"; import { LocationRoot } from "./Locations/ui/Root"; import { checkForMessagesToSend, initMessages } from "./Message/MessageHelpers"; import { inMission, currMission } from "./Missions"; +import { workerScripts } from "./Netscript/WorkerScripts"; import { loadAllRunningScripts, - runScriptsLoop, updateOnlineScriptTimes, } from "./NetscriptWorker"; import { Player } from "./Player"; @@ -87,6 +91,8 @@ import { displayCharacterInfo } from "./ui/displayCharacterInfo"; import { Page, routing } from "./ui/navigationTracking"; import { numeralWrapper } from "./ui/numeralFormat"; import { setSettingsLabels } from "./ui/setSettingsLabels"; + +import { ActiveScriptsRoot } from "./ui/ActiveScripts/Root"; import { initializeMainMenuHeaders } from "./ui/MainMenu/Headers"; import { initializeMainMenuLinks, MainMenuLinks } from "./ui/MainMenu/Links"; @@ -259,7 +265,10 @@ const Engine = { Engine.hideAllContent(); Engine.Display.activeScriptsContent.style.display = "block"; routing.navigateTo(Page.ActiveScripts); - updateActiveScriptsItems(); + ReactDOM.render( + , + Engine.Display.activeScriptsContent + ) MainMenuLinks.ActiveScripts.classList.add("active"); }, @@ -471,7 +480,10 @@ const Engine = { Engine.Display.terminalContent.style.display = "none"; Engine.Display.characterContent.style.display = "none"; Engine.Display.scriptEditorContent.style.display = "none"; + Engine.Display.activeScriptsContent.style.display = "none"; + ReactDOM.unmountComponentAtNode(Engine.Display.activeScriptsContent); + clearHacknetNodesUI(); Engine.Display.createProgramContent.style.display = "none"; @@ -802,13 +814,14 @@ const Engine = { } if (Engine.Counters.updateActiveScriptsDisplay <= 0) { - // Always update, but make the interval longer if the page isn't active - updateActiveScriptsItems(); if (routing.isOn(Page.ActiveScripts)) { - Engine.Counters.updateActiveScriptsDisplay = 5; - } else { - Engine.Counters.updateActiveScriptsDisplay = 10; + ReactDOM.render( + , + Engine.Display.activeScriptsContent + ) } + + Engine.Counters.updateActiveScriptsDisplay = 5; } if (Engine.Counters.updateDisplays <= 0) { @@ -1515,9 +1528,6 @@ const Engine = { start: function() { // Run main loop Engine.idleTimer(); - - // Script-processing loop - runScriptsLoop(); } }; diff --git a/src/engineStyle.js b/src/engineStyle.js index c297b37cc..f9895a77a 100644 --- a/src/engineStyle.js +++ b/src/engineStyle.js @@ -9,6 +9,7 @@ import "../css/characteroverview.scss"; import "../css/terminal.scss"; import "../css/scripteditor.scss"; import "../css/codemirror-overrides.scss"; +import "../css/activescripts.scss"; import "../css/hacknetnodes.scss"; import "../css/menupages.scss"; import "../css/augmentations.scss"; diff --git a/src/ui/ActiveScripts/Root.tsx b/src/ui/ActiveScripts/Root.tsx new file mode 100644 index 000000000..2dd6e78d0 --- /dev/null +++ b/src/ui/ActiveScripts/Root.tsx @@ -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 { + constructor(props: IProps) { + super(props); + } + + render() { + return ( + <> +

+ 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. +

+ + + + + ) + } +} diff --git a/src/ui/ActiveScripts/ScriptProduction.tsx b/src/ui/ActiveScripts/ScriptProduction.tsx new file mode 100644 index 000000000..66da02a01 --- /dev/null +++ b/src/ui/ActiveScripts/ScriptProduction.tsx @@ -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 ( +

+ Total online production of Active scripts: + + + {numeralWrapper.formatMoney(onlineProduction)} + / sec +
+ + Total online production since last Aug installation: + + {numeralWrapper.formatMoney(props.p.scriptProdSinceLastAug)} + + + ( + + {numeralWrapper.formatMoney(prodRateSinceLastAug)} + / sec + ) +

+ ) +} diff --git a/src/ui/ActiveScripts/ServerAccordion.tsx b/src/ui/ActiveScripts/ServerAccordion.tsx new file mode 100644 index 000000000..9a0d1c57f --- /dev/null +++ b/src/ui/ActiveScripts/ServerAccordion.tsx @@ -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 ( + + ) + }); + + return ( + {headerTxt} + } + panelContent={ +
    {scripts}
+ } + /> + ) +} diff --git a/src/ui/ActiveScripts/ServerAccordions.tsx b/src/ui/ActiveScripts/ServerAccordions.tsx new file mode 100644 index 000000000..554d69317 --- /dev/null +++ b/src/ui/ActiveScripts/ServerAccordions.tsx @@ -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 { + 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 ( + + ) + }); + + return ( +
    + {elems} +
+ ) + } +} diff --git a/src/ui/ActiveScripts/WorkerScriptAccordion.tsx b/src/ui/ActiveScripts/WorkerScriptAccordion.tsx new file mode 100644 index 000000000..bbf522596 --- /dev/null +++ b/src/ui/ActiveScripts/WorkerScriptAccordion.tsx @@ -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 ( + {props.workerScript.name} + } + panelClass="active-scripts-script-panel" + panelContent={ + <> +
Threads: {props.workerScript.scriptRef.threads}
+
Args: {arrayToString(props.workerScript.args)}
+
Online Time: {convertTimeMsToTimeElapsedString(scriptRef.onlineRunningTime * 1e3)}
+
Offline Time: {convertTimeMsToTimeElapsedString(scriptRef.offlineRunningTime * 1e3)}
+
Total online production: {numeralWrapper.formatMoney(scriptRef.onlineMoneyMade)}
+
{(Array(26).join(" ") + numeralWrapper.formatBigNumber(scriptRef.onlineExpGained) + " hacking exp")}
+
Online production rate: {numeralWrapper.formatMoney(onlineMps)} / second
+
{(Array(25).join(" ") + numeralWrapper.formatBigNumber(onlineEps) + " hacking exp / second")}
+
Total offline production: {numeralWrapper.formatMoney(scriptRef.offlineMoneyMade)}
+
{(Array(27).join(" ") + numeralWrapper.formatBigNumber(scriptRef.offlineExpGained) + " hacking exp")}
+
Offline production rate: {numeralWrapper.formatMoney(offlineMps)} / second
+
{(Array(26).join(" ") + numeralWrapper.formatBigNumber(offlineEps) +  " hacking exp / second")}
+ + + + + } + /> + ) +} diff --git a/src/ui/React/Accordion.tsx b/src/ui/React/Accordion.tsx index 5833f29f7..a90be06f0 100644 --- a/src/ui/React/Accordion.tsx +++ b/src/ui/React/Accordion.tsx @@ -4,9 +4,12 @@ import * as React from "react"; type IProps = { + headerClass?: string; // Override default class headerContent: React.ReactElement; + panelClass?: string; // Override default class panelContent: React.ReactElement; panelInitiallyOpened?: boolean; + style?: string; } type IState = { @@ -44,12 +47,21 @@ export class Accordion extends React.Component { } render() { + let className = "accordion-header"; + if (typeof this.props.headerClass === "string") { + className = this.props.headerClass; + } + return ( <> - - + ) } @@ -57,6 +69,7 @@ export class Accordion extends React.Component { type IPanelProps = { opened: boolean; + panelClass?: string; // Override default class panelContent: React.ReactElement; } @@ -66,8 +79,13 @@ class AccordionPanel extends React.Component { } render() { + let className = "accordion-panel" + if (typeof this.props.panelClass === "string") { + className = this.props.panelClass; + } + return ( -
+
{this.props.panelContent}
) diff --git a/src/ui/React/AccordionButton.tsx b/src/ui/React/AccordionButton.tsx new file mode 100644 index 000000000..f60c6b821 --- /dev/null +++ b/src/ui/React/AccordionButton.tsx @@ -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) => 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 ( + + ) +} diff --git a/src/utils/EventEmitter.ts b/src/utils/EventEmitter.ts new file mode 100644 index 000000000..b513d76d5 --- /dev/null +++ b/src/utils/EventEmitter.ts @@ -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 = {}; + + 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]; + } + +} diff --git a/utils/LogBox.js b/utils/LogBox.js deleted file mode 100644 index 615567610..000000000 --- a/utils/LogBox.js +++ /dev/null @@ -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) + ":

"; - 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 += "
"; - } - logBoxCurrentScript.logUpd = false; - } -} - -export {logBoxCreate, logBoxUpdateText, logBoxOpened, logBoxCurrentScript}; diff --git a/utils/LogBox.ts b/utils/LogBox.ts new file mode 100644 index 000000000..1a38244c6 --- /dev/null +++ b/utils/LogBox.ts @@ -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)}:

`; + } 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 += "
"; + } + logBoxCurrentScript.logUpd = false; + } +}