diff --git a/src/ActiveScriptsUI.js b/src/ActiveScriptsUI.js index cb86c0b1a..1cdc66b26 100644 --- a/src/ActiveScriptsUI.js +++ b/src/ActiveScriptsUI.js @@ -1,7 +1,7 @@ import {workerScripts, killWorkerScript} from "./NetscriptWorker"; -import {Player} from "./Player"; -import {getServer} from "./Server"; +import { Player } from "./Player"; +import { getServer } from "./Server/ServerHelpers"; import {numeralWrapper} from "./ui/numeralFormat"; import {dialogBoxCreate} from "../utils/DialogBox"; import {createAccordionElement} from "../utils/uiHelpers/createAccordionElement"; diff --git a/src/Augmentation/AugmentationHelpers.js b/src/Augmentation/AugmentationHelpers.js index b1cd629df..af405e93e 100644 --- a/src/Augmentation/AugmentationHelpers.js +++ b/src/Augmentation/AugmentationHelpers.js @@ -12,9 +12,9 @@ import { addWorkerScript } from "../NetscriptWorker"; import { Player } from "../Player"; import { prestigeAugmentation } from "../Prestige"; import { saveObject } from "../SaveObject"; -import { Script, - RunningScript} from "../Script"; -import { Server } from "../Server"; +import { RunningScript } from "../Script/RunningScript"; +import { Script } from "../Script/Script"; +import { Server } from "../Server/Server"; import { OwnedAugmentationsOrderSetting } from "../Settings/SettingEnums"; import { Settings } from "../Settings/Settings"; diff --git a/src/CodingContractGenerator.js b/src/CodingContractGenerator.js index 8cf9fdda2..135d47863 100644 --- a/src/CodingContractGenerator.js +++ b/src/CodingContractGenerator.js @@ -3,8 +3,8 @@ import { CodingContract, CodingContractTypes } from "./CodingContracts"; import { Factions } from "./Faction/Factions"; import { Player } from "./Player"; -import { GetServerByHostname, - AllServers } from "./Server"; +import { AllServers } from "./Server/AllServers"; +import { GetServerByHostname } from "./Server/ServerHelpers"; import { getRandomInt } from "../utils/helpers/getRandomInt"; diff --git a/src/DarkWeb/DarkWeb.js b/src/DarkWeb/DarkWeb.js index 394e31982..b8a5cdf89 100644 --- a/src/DarkWeb/DarkWeb.js +++ b/src/DarkWeb/DarkWeb.js @@ -1,7 +1,7 @@ import { DarkWebItems } from "./DarkWebItems"; import { Player } from "../Player"; -import { SpecialServerIps } from "../SpecialServerIps"; +import { SpecialServerIps } from "../Server/SpecialServerIps"; import { post } from "../ui/postToTerminal"; import { isValidIPAddress } from "../../utils/helpers/isValidIPAddress"; diff --git a/src/DevMenu.js b/src/DevMenu.js index 03b72f108..95591491c 100644 --- a/src/DevMenu.js +++ b/src/DevMenu.js @@ -8,7 +8,7 @@ import { Company } from "./Company/Company"; import { Programs } from "./Programs/Programs"; import { Factions } from "./Faction/Factions"; import { Player } from "./Player"; -import { AllServers } from "./Server"; +import { AllServers } from "./Server/AllServers"; import { hackWorldDaemon } from "./RedPill"; import { StockMarket, SymbolToStockMap } from "./StockMarket/StockMarket"; diff --git a/src/Fconf.js b/src/Fconf/Fconf.js similarity index 95% rename from src/Fconf.js rename to src/Fconf/Fconf.js index 6f125c26c..f40bcc8ce 100644 --- a/src/Fconf.js +++ b/src/Fconf/Fconf.js @@ -1,16 +1,7 @@ -import {parse, Node} from "../utils/acorn"; -import {dialogBoxCreate} from "../utils/DialogBox"; +import { FconfSettings } from "./FconfSettings"; -var FconfSettings = { - ENABLE_BASH_HOTKEYS: false, - ENABLE_TIMESTAMPS: false, - MAIN_MENU_STYLE: "default", - THEME_BACKGROUND_COLOR: "#000000", - THEME_FONT_COLOR: "#66ff33", - THEME_HIGHLIGHT_COLOR: "#ffffff", - THEME_PROMPT_COLOR: "#f92672", - WRAP_INPUT: false, -} +import { parse, Node } from "../../utils/acorn"; +import { dialogBoxCreate } from "../../utils/DialogBox"; var FconfComments = { ENABLE_BASH_HOTKEYS: "Improved Bash emulation mode. Setting this to 1 enables several\n" + diff --git a/src/Fconf/FconfSettings.ts b/src/Fconf/FconfSettings.ts new file mode 100644 index 000000000..f1c08bc73 --- /dev/null +++ b/src/Fconf/FconfSettings.ts @@ -0,0 +1,10 @@ +export const FconfSettings = { + ENABLE_BASH_HOTKEYS: false, + ENABLE_TIMESTAMPS: false, + MAIN_MENU_STYLE: "default", + THEME_BACKGROUND_COLOR: "#000000", + THEME_FONT_COLOR: "#66ff33", + THEME_HIGHLIGHT_COLOR: "#ffffff", + THEME_PROMPT_COLOR: "#f92672", + WRAP_INPUT: false, +} diff --git a/src/Hacking.js b/src/Hacking.js index 452d38120..893ca3779 100644 --- a/src/Hacking.js +++ b/src/Hacking.js @@ -1,6 +1,6 @@ import { BitNodeMultipliers } from "./BitNode/BitNodeMultipliers"; import { Player } from "./Player"; -import { Server } from "./Server"; +import { Server } from "./Server/Server"; /** * Returns the chance the player has to successfully hack a server diff --git a/src/Location.js b/src/Location.js index b00adc353..33281738f 100644 --- a/src/Location.js +++ b/src/Location.js @@ -11,13 +11,16 @@ import {beginInfiltration} from "./Infiltration"; import {hasBladeburnerSF} from "./NetscriptFunctions"; import {Locations} from "./Locations"; import {Player} from "./Player"; -import {Server, AllServers, AddToAllServers} from "./Server"; +import { AllServers, + AddToAllServers } from "./Server/AllServers"; +import { Server } from "./Server/Server"; import { getPurchaseServerCost, purchaseServer, - purchaseRamForHomeComputer} from "./ServerPurchases"; + purchaseRamForHomeComputer } from "./Server/ServerPurchases"; import {Settings} from "./Settings/Settings"; import { SourceFileFlags } from "./SourceFile/SourceFileFlags"; -import {SpecialServerNames, SpecialServerIps} from "./SpecialServerIps"; +import { SpecialServerNames, + SpecialServerIps } from "./Server/SpecialServerIps"; import {numeralWrapper} from "./ui/numeralFormat"; diff --git a/src/Message/Message.ts b/src/Message/Message.ts new file mode 100644 index 000000000..90e24aa2c --- /dev/null +++ b/src/Message/Message.ts @@ -0,0 +1,32 @@ +import { Reviver, + Generic_toJSON, + Generic_fromJSON } from "../../utils/JSONReviver"; + +export class Message { + // Initializes a Message Object from a JSON save state + static fromJSON(value: any): Message { + return Generic_fromJSON(Message, value.data); + } + + // Name of Message file + filename: string = ""; + + // The text contains in the Message + msg: string = ""; + + // Flag indicating whether this Message has been received by the player + recvd: boolean = false; + + constructor(filename="", msg="") { + this.filename = filename; + this.msg = msg; + this.recvd = false; + } + + // Serialize the current object to a JSON save state + toJSON(): any { + return Generic_toJSON("Message", this); + } +} + +Reviver.constructors.Message = Message; diff --git a/src/Message.js b/src/Message/MessageHelpers.js similarity index 86% rename from src/Message.js rename to src/Message/MessageHelpers.js index 7ebee10fa..4845e6a30 100644 --- a/src/Message.js +++ b/src/Message/MessageHelpers.js @@ -1,34 +1,15 @@ -import { Augmentatation } from "./Augmentation/Augmentation"; -import { Augmentations } from "./Augmentation/Augmentations"; -import { AugmentationNames } from "./Augmentation/data/AugmentationNames"; -import { Programs } from "./Programs/Programs"; -import { inMission } from "./Missions"; -import { Player } from "./Player"; -import { redPillFlag } from "./RedPill"; -import { GetServerByHostname } from "./Server"; -import { Settings } from "./Settings/Settings"; +import { Message } from "./Message"; +import { Augmentatation } from "../Augmentation/Augmentation"; +import { Augmentations } from "../Augmentation/Augmentations"; +import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; +import { Programs } from "../Programs/Programs"; +import { inMission } from "../Missions"; +import { Player } from "../Player"; +import { redPillFlag } from "../RedPill"; +import { GetServerByHostname } from "../Server/ServerHelpers"; +import { Settings } from "../Settings/Settings"; import { dialogBoxCreate, - dialogBoxOpened} from "../utils/DialogBox"; -import {Reviver, Generic_toJSON, - Generic_fromJSON} from "../utils/JSONReviver"; - -/* Message.js */ -function Message(filename="", msg="") { - this.filename = filename; - this.msg = msg; - this.recvd = false; -} - -Message.prototype.toJSON = function() { - return Generic_toJSON("Message", this); -} - - -Message.fromJSON = function(value) { - return Generic_fromJSON(Message, value.data); -} - -Reviver.constructors.Message = Message; + dialogBoxOpened} from "../../utils/DialogBox"; //Sends message to player, including a pop up function sendMessage(msg, forced=false) { diff --git a/src/NetscriptEvaluator.js b/src/NetscriptEvaluator.js index 8e144ac15..c301f2b76 100644 --- a/src/NetscriptEvaluator.js +++ b/src/NetscriptEvaluator.js @@ -3,10 +3,12 @@ import { CONSTANTS } from "./Constants"; import { Player } from "./Player"; import { Environment } from "./NetscriptEnvironment"; import { WorkerScript, addWorkerScript} from "./NetscriptWorker"; -import { Server, getServer} from "./Server"; +import { Server } from "./Server/Server"; +import { getServer } from "./Server/ServerHelpers"; import { Settings } from "./Settings/Settings"; -import { Script, findRunningScript, - RunningScript } from "./Script"; +import { RunningScript } from "./Script/RunningScript"; +import { Script } from "./Script/Script"; +import { findRunningScript } from "./Script/ScriptHelpers"; import { setTimeoutRef } from "./utils/SetTimeoutRef"; diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index e2e585616..38e985350 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -31,20 +31,26 @@ import { joinFaction, import { getCostOfNextHacknetNode, purchaseHacknet } from "./HacknetNode"; import {Locations} from "./Locations"; -import {Message, Messages} from "./Message"; +import { Message } from "./Message/Message"; +import { Messages } from "./Message/MessageHelpers"; import {inMission} from "./Missions"; import {Player} from "./Player"; import { Programs } from "./Programs/Programs"; -import {Script, findRunningScript, RunningScript, - isScriptFilename} from "./Script"; -import {Server, getServer, AddToAllServers, - AllServers, processSingleServerGrowth, - GetServerByHostname, numCycleForGrowth} from "./Server"; +import { Script } from "./Script/Script"; +import { findRunningScript } from "./Script/ScriptHelpers"; +import { isScriptFilename } from "./Script/ScriptHelpersTS"; +import { AllServers, + AddToAllServers } from "./Server/AllServers"; +import { Server } from "./Server/Server"; +import { GetServerByHostname, + getServer, + numCycleForGrowth, + processSingleServerGrowth } from "./Server/ServerHelpers"; import { getPurchaseServerCost, getPurchaseServerLimit, - getPurchaseServerMaxRam } from "./ServerPurchases"; + getPurchaseServerMaxRam } from "./Server/ServerPurchases"; import {Settings} from "./Settings/Settings"; -import {SpecialServerIps} from "./SpecialServerIps"; +import {SpecialServerIps} from "./Server/SpecialServerIps"; import {Stock} from "./StockMarket/Stock"; import {StockMarket, StockSymbols, SymbolToStockMap, initStockMarket, initSymbolToStockMap, buyStock, @@ -306,9 +312,9 @@ function NetscriptFunctions(workerScript) { for (var i = 0; i < server.serversOnNetwork.length; i++) { var entry; if (hostnames) { - entry = server.getServerOnNetwork(i).hostname; + entry = getServerOnNetwork(server, i).hostname; } else { - entry = server.getServerOnNetwork(i).ip; + entry = getServerOnNetwork(server, i).ip; } if (entry == null) { continue; @@ -484,7 +490,7 @@ function NetscriptFunctions(workerScript) { if (workerScript.env.stopFlag) {return Promise.reject(workerScript);} const moneyBefore = server.moneyAvailable <= 0 ? 1 : server.moneyAvailable; server.moneyAvailable += (1 * threads); //It can be grown even if it has no money - var growthPercentage = processSingleServerGrowth(server, 450 * threads); + var growthPercentage = processSingleServerGrowth(server, 450 * threads, Player); const moneyAfter = server.moneyAvailable; workerScript.scriptRef.recordGrow(server.ip, threads); var expGain = calculateHackingExpGain(server) * threads; @@ -513,7 +519,7 @@ function NetscriptFunctions(workerScript) { throw makeRuntimeRejectMsg(workerScript, `Invalid growth argument passed into growthAnalyze: ${growth}. Must be numeric`); } - return numCycleForGrowth(server, Number(growth)); + return numCycleForGrowth(server, Number(growth), Player); }, weaken : function(ip) { if (workerScript.checkingRam) { diff --git a/src/NetscriptWorker.js b/src/NetscriptWorker.js index 936dc82f6..e698166f8 100644 --- a/src/NetscriptWorker.js +++ b/src/NetscriptWorker.js @@ -11,7 +11,7 @@ import {evaluate, isScriptErrorMessage, import {NetscriptFunctions} from "./NetscriptFunctions"; import {executeJSScript} from "./NetscriptJSEvaluator"; import {NetscriptPort} from "./NetscriptPort"; -import {AllServers} from "./Server"; +import { AllServers } from "./Server/AllServers"; import {Settings} from "./Settings/Settings"; import { setTimeoutRef } from "./utils/SetTimeoutRef"; diff --git a/src/PersonObjects/IPlayer.ts b/src/PersonObjects/IPlayer.ts index 948b72e22..c41247a2e 100644 --- a/src/PersonObjects/IPlayer.ts +++ b/src/PersonObjects/IPlayer.ts @@ -20,6 +20,7 @@ export interface IPlayer { city: string; companyName: string; corporation: any; + currentServer: string; factions: string[]; hacknetNodes: any[]; hasWseAccount: boolean; diff --git a/src/Player.js b/src/Player.js index 25c19af5f..ca0c5c0ec 100644 --- a/src/Player.js +++ b/src/Player.js @@ -24,9 +24,11 @@ import {Gang, resetGangs} from "./Gang"; import {Locations} from "./Locations"; import {hasBn11SF, hasWallStreetSF,hasAISF} from "./NetscriptFunctions"; import { Sleeve } from "./PersonObjects/Sleeve/Sleeve"; -import {AllServers, Server, AddToAllServers} from "./Server"; +import { AllServers, + AddToAllServers } from "./Server/AllServers"; +import { Server } from "./Server/Server"; import {Settings} from "./Settings/Settings"; -import {SpecialServerIps, SpecialServerNames} from "./SpecialServerIps"; +import {SpecialServerIps, SpecialServerNames} from "./Server/SpecialServerIps"; import {SourceFiles, applySourceFile} from "./SourceFile"; import { SourceFileFlags } from "./SourceFile/SourceFileFlags"; import Decimal from "decimal.js"; diff --git a/src/Prestige.js b/src/Prestige.js index 3e46c03f1..0f12c1e97 100755 --- a/src/Prestige.js +++ b/src/Prestige.js @@ -16,20 +16,25 @@ import { Factions, import { joinFaction } from "./Faction/FactionHelpers"; import {deleteGangDisplayContent} from "./Gang"; import {Locations} from "./Location"; -import {initMessages, Messages, Message} from "./Message"; +import { Message } from "./Message/Message"; +import { initMessages, + Messages } from "./Message/MessageHelpers"; import {initSingularitySFFlags, hasWallStreetSF}from "./NetscriptFunctions"; import {WorkerScript, workerScripts, prestigeWorkerScripts} from "./NetscriptWorker"; import {Player} from "./Player"; -import {AllServers, AddToAllServers, - initForeignServers, Server, - prestigeAllServers, - prestigeHomeComputer} from "./Server"; +import { AllServers, + AddToAllServers, + prestigeAllServers } from "./Server/AllServers"; +import { Server } from "./Server/Server" +import { initForeignServers, + prestigeHomeComputer } from "./Server/ServerHelpers"; import { updateSourceFileFlags } from "./SourceFile/SourceFileFlags"; -import {SpecialServerIps, SpecialServerIpsMap, - prestigeSpecialServerIps, - SpecialServerNames} from "./SpecialServerIps"; +import { SpecialServerIps, + SpecialServerIpsMap, + prestigeSpecialServerIps, + SpecialServerNames} from "./Server/SpecialServerIps"; import {initStockMarket, initSymbolToStockMap, stockMarketContentCreated, setStockMarketContentCreated} from "./StockMarket/StockMarket"; @@ -89,7 +94,7 @@ function prestigeAugmentation() { } //Re-create foreign servers - initForeignServers(); + initForeignServers(Player.getHomeComputer()); //Darkweb is purchase-able document.getElementById("location-purchase-tor").setAttribute("class", "a-link-button"); @@ -194,7 +199,7 @@ function prestigeSourceFile() { prestigeHomeComputer(homeComp); //Re-create foreign servers - initForeignServers(); + initForeignServers(Player.getHomeComputer()); var srcFile1Owned = false; for (var i = 0; i < Player.sourceFiles.length; ++i) { diff --git a/src/SaveObject.js b/src/SaveObject.js index 5667b1efb..27af3062a 100755 --- a/src/SaveObject.js +++ b/src/SaveObject.js @@ -7,15 +7,18 @@ import {Engine} from "./engine"; import { Factions, loadFactions } from "./Faction/Factions"; import { processPassiveFactionRepGain } from "./Faction/FactionHelpers"; -import {FconfSettings, loadFconf} from "./Fconf"; +import { loadFconf } from "./Fconf/Fconf"; +import { FconfSettings } from "./Fconf/FconfSettings"; import {loadAllGangs, AllGangs} from "./Gang"; import {processAllHacknetNodeEarnings} from "./HacknetNode"; -import {loadMessages, initMessages, Messages} from "./Message"; +import { loadMessages, initMessages, Messages } from "./Message/MessageHelpers"; import {Player, loadPlayer} from "./Player"; -import {loadAllRunningScripts} from "./Script"; -import {AllServers, loadAllServers} from "./Server"; -import {Settings} from "./Settings/Settings"; -import {loadSpecialServerIps, SpecialServerIps} from "./SpecialServerIps"; +import { loadAllRunningScripts } from "./Script/ScriptHelpers"; +import { AllServers, + loadAllServers } from "./Server/AllServers"; +import { Settings } from "./Settings/Settings"; +import { loadSpecialServerIps, + SpecialServerIps } from "./Server/SpecialServerIps"; import {loadStockMarket, StockMarket} from "./StockMarket/StockMarket"; import { setTimeoutRef } from "./utils/SetTimeoutRef"; diff --git a/src/Script.js b/src/Script.js deleted file mode 100755 index 5733ab404..000000000 --- a/src/Script.js +++ /dev/null @@ -1,1037 +0,0 @@ -// Importing this doesn't work for some reason. -const walk = require("acorn/dist/walk"); - -import {CONSTANTS} from "./Constants"; -import {Engine} from "./engine"; -import {FconfSettings, parseFconfSettings} from "./Fconf"; -import {iTutorialSteps, iTutorialNextStep, - ITutorial} from "./InteractiveTutorial"; -import {evaluateImport} from "./NetscriptEvaluator"; -import {NetscriptFunctions} from "./NetscriptFunctions"; -import {addWorkerScript, WorkerScript} from "./NetscriptWorker"; -import {Player} from "./Player"; -import { AceEditor } from "./ScriptEditor/Ace"; -import { CodeMirrorEditor } from "./ScriptEditor/CodeMirror"; -import {AllServers, processSingleServerGrowth} from "./Server"; -import { Settings } from "./Settings/Settings"; -import { EditorSetting } from "./Settings/SettingEnums"; -import {post} from "./ui/postToTerminal"; -import {TextFile} from "./TextFile"; -import {parse, Node} from "../utils/acorn"; -import {Page, routing} from "./ui/navigationTracking"; -import {numeralWrapper} from "./ui/numeralFormat"; -import { setTimeoutRef } from "./utils/SetTimeoutRef"; -import {dialogBoxCreate} from "../utils/DialogBox"; -import {Reviver, Generic_toJSON, - Generic_fromJSON} from "../utils/JSONReviver"; -import {compareArrays} from "../utils/helpers/compareArrays"; -import {createElement} from "../utils/uiHelpers/createElement"; -import {getTimestamp} from "../utils/helpers/getTimestamp"; -import {roundToTwo} from "../utils/helpers/roundToTwo"; - -function isScriptFilename(f) { - return f.endsWith(".js") || f.endsWith(".script") || f.endsWith(".ns"); -} - -var scriptEditorRamCheck = null, scriptEditorRamText = null; -function scriptEditorInit() { - // Wrapper container that holds all the buttons below the script editor - const wrapper = document.getElementById("script-editor-buttons-wrapper"); - if (wrapper == null) { - console.error("Could not find 'script-editor-buttons-wrapper'"); - return false; - } - - // Beautify button - const beautifyButton = createElement("button", { - class: "std-button", - display: "inline-block", - innerText: "Beautify", - clickListener:()=>{ - let editor = getCurrentEditor(); - if (editor != null) { - editor.beautifyScript(); - } - return false; - } - }); - - // Text that displays RAM calculation - scriptEditorRamText = createElement("p", { - display:"inline-block", margin:"10px", id:"script-editor-status-text" - }); - - // Label for checkbox (defined below) - const checkboxLabel = createElement("label", { - for:"script-editor-ram-check", margin:"4px", marginTop: "8px", - innerText:"Dynamic RAM Usage Checker", color:"white", - tooltip:"Enable/Disable the dynamic RAM Usage display. You may " + - "want to disable it for very long scripts because there may be " + - "performance issues" - }); - - // Checkbox for enabling/disabling dynamic RAM calculation - scriptEditorRamCheck = createElement("input", { - type:"checkbox", name:"script-editor-ram-check", id:"script-editor-ram-check", - margin:"4px", marginTop: "8px", - }); - scriptEditorRamCheck.checked = true; - - // Link to Netscript documentation - const documentationButton = createElement("a", { - class: "std-button", - display: "inline-block", - href:"https://bitburner.readthedocs.io/en/latest/index.html", - innerText:"Netscript Documentation", - target:"_blank", - }); - - // Save and Close button - const closeButton = createElement("button", { - class: "std-button", - display: "inline-block", - innerText: "Save & Close (Ctrl/Cmd + b)", - clickListener:()=>{ - saveAndCloseScriptEditor(); - return false; - } - }); - - // Add all buttons to the UI - wrapper.appendChild(beautifyButton); - wrapper.appendChild(closeButton); - wrapper.appendChild(scriptEditorRamText); - wrapper.appendChild(scriptEditorRamCheck); - wrapper.appendChild(checkboxLabel); - wrapper.appendChild(documentationButton); - - // Initialize editors - const initParams = { - saveAndCloseFn: saveAndCloseScriptEditor, - quitFn: Engine.loadTerminalContent, - } - - AceEditor.init(initParams); - CodeMirrorEditor.init(initParams); - - // Setup the selector for which Editor to use - const editorSelector = document.getElementById("script-editor-option-editor"); - if (editorSelector == null) { - console.error(`Could not find DOM Element for editor selector (id=script-editor-option-editor)`); - return false; - } - - for (let i = 0; i < editorSelector.options.length; ++i) { - if (editorSelector.options[i].value === Settings.Editor) { - editorSelector.selectedIndex = i; - break; - } - } - - editorSelector.onchange = () => { - const opt = editorSelector.value; - switch (opt) { - case EditorSetting.Ace: - const codeMirrorCode = CodeMirrorEditor.getCode(); - const codeMirrorFn = CodeMirrorEditor.getFilename(); - AceEditor.create(); - CodeMirrorEditor.setInvisible(); - AceEditor.openScript(codeMirrorFn, codeMirrorCode); - break; - case EditorSetting.CodeMirror: - const aceCode = AceEditor.getCode(); - const aceFn = AceEditor.getFilename(); - CodeMirrorEditor.create(); - AceEditor.setInvisible(); - CodeMirrorEditor.openScript(aceFn, aceCode); - break; - default: - console.error(`Unrecognized Editor Setting: ${opt}`); - return; - } - - Settings.Editor = opt; - } - - editorSelector.onchange(); // Trigger the onchange event handler -} - -export function getCurrentEditor() { - switch (Settings.Editor) { - case EditorSetting.Ace: - return AceEditor; - case EditorSetting.CodeMirror: - return CodeMirrorEditor; - default: - console.log(`Invalid Editor Setting: ${Settings.Editor}`); - throw new Error(`Invalid Editor Setting: ${Settings.Editor}`); - return null; - } -} - -//Updates RAM usage in script -export async function updateScriptEditorContent() { - var filename = document.getElementById("script-editor-filename").value; - if (scriptEditorRamCheck == null || !scriptEditorRamCheck.checked || !isScriptFilename(filename)) { - scriptEditorRamText.innerText = "N/A"; - return; - } - - let code; - try { - code = getCurrentEditor().getCode(); - } catch(e) { - scriptEditorRamText.innerText = "RAM: ERROR"; - return; - } - - var codeCopy = code.repeat(1); - var ramUsage = await calculateRamUsage(codeCopy); - if (ramUsage !== -1) { - scriptEditorRamText.innerText = "RAM: " + numeralWrapper.format(ramUsage, '0.00') + " GB"; - } else { - scriptEditorRamText.innerText = "RAM: Syntax Error"; - } -} - -//Define key commands in script editor (ctrl o to save + close, etc.) -$(document).keydown(function(e) { - if (Settings.DisableHotkeys === true) {return;} - if (routing.isOn(Page.ScriptEditor)) { - //Ctrl + b - if (e.keyCode == 66 && (e.ctrlKey || e.metaKey)) { - e.preventDefault(); - saveAndCloseScriptEditor(); - } - } -}); - -function saveAndCloseScriptEditor() { - var filename = document.getElementById("script-editor-filename").value; - - let code; - try { - code = getCurrentEditor().getCode(); - } catch(e) { - dialogBoxCreate("Something went wrong when trying to save (getCurrentEditor().getCode()). Please report to game developer with details"); - return; - } - - if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) { - //Make sure filename + code properly follow tutorial - if (filename !== "foodnstuff.script") { - dialogBoxCreate("Leave the script name as 'foodnstuff'!"); - return; - } - code = code.replace(/\s/g, ""); - if (code.indexOf("while(true){hack('foodnstuff');}") == -1) { - dialogBoxCreate("Please copy and paste the code from the tutorial!"); - return; - } - - //Save the script - let s = Player.getCurrentServer(); - for (var i = 0; i < s.scripts.length; i++) { - if (filename == s.scripts[i].filename) { - s.scripts[i].saveScript(); - Engine.loadTerminalContent(); - return iTutorialNextStep(); - } - } - - //If the current script does NOT exist, create a new one - let script = new Script(); - script.saveScript(); - s.scripts.push(script); - - return iTutorialNextStep(); - } - - if (filename == "") { - dialogBoxCreate("You must specify a filename!"); - return; - } - - if (checkValidFilename(filename) == false) { - dialogBoxCreate("Script filename can contain only alphanumerics, hyphens, and underscores"); - return; - } - - var s = Player.getCurrentServer(); - if (filename === ".fconf") { - try { - parseFconfSettings(code); - } catch(e) { - dialogBoxCreate(`Invalid .fconf file: ${e}`); - return; - } - } else if (isScriptFilename(filename)) { - //If the current script already exists on the server, overwrite it - for (var i = 0; i < s.scripts.length; i++) { - if (filename == s.scripts[i].filename) { - s.scripts[i].saveScript(); - Engine.loadTerminalContent(); - return; - } - } - - //If the current script does NOT exist, create a new one - var script = new Script(); - script.saveScript(); - s.scripts.push(script); - } else if (filename.endsWith(".txt")) { - for (var i = 0; i < s.textFiles.length; ++i) { - if (s.textFiles[i].fn === filename) { - s.textFiles[i].write(code); - Engine.loadTerminalContent(); - return; - } - } - var textFile = new TextFile(filename, code); - s.textFiles.push(textFile); - } else { - dialogBoxCreate("Invalid filename. Must be either a script (.script) or " + - " or text file (.txt)") - return; - } - Engine.loadTerminalContent(); -} - -//Checks that the string contains only valid characters for a filename, which are alphanumeric, -// underscores, hyphens, and dots -function checkValidFilename(filename) { - var regex = /^[.a-zA-Z0-9_-]+$/; - - if (filename.match(regex)) { - return true; - } - return false; -} - -function Script(fn = "", code = "", server = "") { - this.filename = fn; - this.code = code; - this.ramUsage = 0; - this.server = server; //IP of server this script is on - this.module = ""; - if (this.code !== "") {this.updateRamUsage();} -}; - -//Get the script data from the Script Editor and save it to the object -Script.prototype.saveScript = function() { - if (routing.isOn(Page.ScriptEditor)) { - //Update code and filename - const code = getCurrentEditor().getCode(); - this.code = code.replace(/^\s+|\s+$/g, ''); - - var filename = document.getElementById("script-editor-filename").value; - this.filename = filename; - - //Server - this.server = Player.currentServer; - - //Calculate/update ram usage, execution time, etc. - this.updateRamUsage(); - - this.module = ""; - } -} - -//Updates how much RAM the script uses when it is running. -Script.prototype.updateRamUsage = async function() { - var codeCopy = this.code.repeat(1); - var res = await calculateRamUsage(codeCopy); - if (res !== -1) { - this.ramUsage = roundToTwo(res); - } -} - -// These special strings are used to reference the presence of a given logical -// construct within a user script. -const specialReferenceIF = "__SPECIAL_referenceIf"; -const specialReferenceFOR = "__SPECIAL_referenceFor"; -const specialReferenceWHILE = "__SPECIAL_referenceWhile"; - -// The global scope of a script is registered under this key during parsing. -const memCheckGlobalKey = ".__GLOBAL__"; - -// Calcluates the amount of RAM a script uses. Uses parsing and AST walking only, -// rather than NetscriptEvaluator. This is useful because NetscriptJS code does -// not work under NetscriptEvaluator. -async function parseOnlyRamCalculate(server, code, workerScript) { - try { - // Maps dependent identifiers to their dependencies. - // - // The initial identifier is __SPECIAL_INITIAL_MODULE__.__GLOBAL__. - // It depends on all the functions declared in the module, all the global scopes - // of its imports, and any identifiers referenced in this global scope. Each - // function depends on all the identifiers referenced internally. - // We walk the dependency graph to calculate RAM usage, given that some identifiers - // reference Netscript functions which have a RAM cost. - let dependencyMap = {}; - - // Scripts we've parsed. - const completedParses = new Set(); - - // Scripts we've discovered that need to be parsed. - const parseQueue = []; - - // Parses a chunk of code with a given module name, and updates parseQueue and dependencyMap. - function parseCode(code, moduleName) { - const result = parseOnlyCalculateDeps(code, moduleName); - completedParses.add(moduleName); - - // Add any additional modules to the parse queue; - for (let i = 0; i < result.additionalModules.length; ++i) { - if (!completedParses.has(result.additionalModules[i])) { - parseQueue.push(result.additionalModules[i]); - } - } - - // Splice all the references in. - //Spread syntax not supported in edge, use Object.assign instead - //dependencyMap = {...dependencyMap, ...result.dependencyMap}; - dependencyMap = Object.assign(dependencyMap, result.dependencyMap); - } - - const initialModule = "__SPECIAL_INITIAL_MODULE__"; - parseCode(code, initialModule); - - while (parseQueue.length > 0) { - // Get the code from the server. - const nextModule = parseQueue.shift(); - - let code; - if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) { - try { - const module = await eval('import(nextModule)'); - code = ""; - for (const prop in module) { - if (typeof module[prop] === 'function') { - code += module[prop].toString() + ";\n"; - } - } - } catch(e) { - console.error(`Error dynamically importing module from ${nextModule} for RAM calculations: ${e}`); - return -1; - } - } else { - const script = server.getScript(nextModule.startsWith("./") ? nextModule.slice(2) : nextModule); - if (!script) { - console.warn("Invalid script"); - return -1; // No such script on the server. - } - code = script.code; - } - - parseCode(code, nextModule); - } - - // Finally, walk the reference map and generate a ram cost. The initial set of keys to scan - // are those that start with __SPECIAL_INITIAL_MODULE__. - let ram = CONSTANTS.ScriptBaseRamCost; - const unresolvedRefs = Object.keys(dependencyMap).filter(s => s.startsWith(initialModule)); - const resolvedRefs = new Set(); - while (unresolvedRefs.length > 0) { - const ref = unresolvedRefs.shift(); - - // Check if this is one of the special keys, and add the appropriate ram cost if so. - if (ref === "hacknet" && !resolvedRefs.has("hacknet")) { - ram += CONSTANTS.ScriptHacknetNodesRamCost; - } - if (ref === "document" && !resolvedRefs.has("document")) { - ram += CONSTANTS.ScriptDomRamCost; - } - if (ref === "window" && !resolvedRefs.has("window")) { - ram += CONSTANTS.ScriptDomRamCost; - } - - resolvedRefs.add(ref); - - if (ref.endsWith(".*")) { - // A prefix reference. We need to find all matching identifiers. - const prefix = ref.slice(0, ref.length - 2); - for (let ident of Object.keys(dependencyMap).filter(k => k.startsWith(prefix))) { - for (let dep of dependencyMap[ident] || []) { - if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep); - } - } - } else { - // An exact reference. Add all dependencies of this ref. - for (let dep of dependencyMap[ref] || []) { - if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep); - } - } - - // Check if this ident is a function in the workerscript env. If it is, then we need to - // get its RAM cost. We do this by calling it, which works because the running script - // is in checkingRam mode. - // - // TODO it would be simpler to just reference a dictionary. - try { - function applyFuncRam(func) { - if (typeof func === "function") { - try { - let res; - if (func.constructor.name === "AsyncFunction") { - res = 0; // Async functions will always be 0 RAM - } else { - res = func.apply(null, []); - } - if (typeof res === "number") { - return res; - } - return 0; - } catch(e) { - console.log("ERROR applying function: " + e); - return 0; - } - } else { - return 0; - } - } - - //Special logic for namespaces (Bladeburner, CodingCOntract) - var func; - if (ref in workerScript.env.vars.bladeburner) { - func = workerScript.env.vars.bladeburner[ref]; - } else if (ref in workerScript.env.vars.codingcontract) { - func = workerScript.env.vars.codingcontract[ref]; - } else if (ref in workerScript.env.vars.gang) { - func = workerScript.env.vars.gang[ref]; - } else { - func = workerScript.env.get(ref); - } - ram += applyFuncRam(func); - } catch (error) {continue;} - } - return ram; - - } catch (error) { - // console.info("parse or eval error: ", error); - // This is not unexpected. The user may be editing a script, and it may be in - // a transitory invalid state. - return -1; - } -} - -// Parses one script and calculates its ram usage, for the global scope and each function. -// Returns a cost map and a dependencyMap for the module. Returns a reference map to be joined -// onto the main reference map, and a list of modules that need to be parsed. -function parseOnlyCalculateDeps(code, currentModule) { - const ast = parse(code, {sourceType:"module", ecmaVersion: 8}); - - // Everything from the global scope goes in ".". Everything else goes in ".function", where only - // the outermost layer of functions counts. - const globalKey = currentModule + memCheckGlobalKey; - const dependencyMap = {}; - dependencyMap[globalKey] = new Set(); - - // If we reference this internal name, we're really referencing that external name. - // Filled when we import names from other modules. - let internalToExternal = {}; - - var additionalModules = []; - - // References get added pessimistically. They are added for thisModule.name, name, and for - // any aliases. - function addRef(key, name) { - const s = dependencyMap[key] || (dependencyMap[key] = new Set()); - if (name in internalToExternal) { - s.add(internalToExternal[name]); - } - s.add(currentModule + "." + name); - s.add(name); // For builtins like hack. - } - - //A list of identifiers that resolve to "native Javascript code" - const objectPrototypeProperties = Object.getOwnPropertyNames(Object.prototype); - - // If we discover a dependency identifier, state.key is the dependent identifier. - // walkDeeper is for doing recursive walks of expressions in composites that we handle. - function commonVisitors() { - return { - Identifier: (node, st, walkDeeper) => { - if (objectPrototypeProperties.includes(node.name)) {return;} - addRef(st.key, node.name); - }, - WhileStatement: (node, st, walkDeeper) => { - addRef(st.key, specialReferenceWHILE); - node.test && walkDeeper(node.test, st); - node.body && walkDeeper(node.body, st); - }, - DoWhileStatement: (node, st, walkDeeper) => { - addRef(st.key, specialReferenceWHILE); - node.test && walkDeeper(node.test, st); - node.body && walkDeeper(node.body, st); - }, - ForStatement: (node, st, walkDeeper) => { - addRef(st.key, specialReferenceFOR); - node.init && walkDeeper(node.init, st); - node.test && walkDeeper(node.test, st); - node.update && walkDeeper(node.update, st); - node.body && walkDeeper(node.body, st); - }, - IfStatement: (node, st, walkDeeper) => { - addRef(st.key, specialReferenceIF); - node.test && walkDeeper(node.test, st); - node.consequent && walkDeeper(node.consequent, st); - node.alternate && walkDeeper(node.alternate, st); - }, - MemberExpression: (node, st, walkDeeper) => { - node.object && walkDeeper(node.object, st); - node.property && walkDeeper(node.property, st); - }, - } - } - - //Spread syntax not supported in Edge yet, use Object.assign - /* - walk.recursive(ast, {key: globalKey}, { - ImportDeclaration: (node, st, walkDeeper) => { - const importModuleName = node.source.value; - additionalModules.push(importModuleName); - - // This module's global scope refers to that module's global scope, no matter how we - // import it. - dependencyMap[st.key].add(importModuleName + memCheckGlobalKey); - - for (let i = 0; i < node.specifiers.length; ++i) { - const spec = node.specifiers[i]; - if (spec.imported !== undefined && spec.local !== undefined) { - // We depend on specific things. - internalToExternal[spec.local.name] = importModuleName + "." + spec.imported.name; - } else { - // We depend on everything. - dependencyMap[st.key].add(importModuleName + ".*"); - } - } - }, - FunctionDeclaration: (node, st, walkDeeper) => { - // Don't use walkDeeper, because we are changing the visitor set. - const key = currentModule + "." + node.id.name; - walk.recursive(node, {key: key}, commonVisitors()); - }, - ...commonVisitors() - }); - */ - walk.recursive(ast, {key: globalKey}, Object.assign({ - ImportDeclaration: (node, st, walkDeeper) => { - const importModuleName = node.source.value; - additionalModules.push(importModuleName); - - // This module's global scope refers to that module's global scope, no matter how we - // import it. - dependencyMap[st.key].add(importModuleName + memCheckGlobalKey); - - for (let i = 0; i < node.specifiers.length; ++i) { - const spec = node.specifiers[i]; - if (spec.imported !== undefined && spec.local !== undefined) { - // We depend on specific things. - internalToExternal[spec.local.name] = importModuleName + "." + spec.imported.name; - } else { - // We depend on everything. - dependencyMap[st.key].add(importModuleName + ".*"); - } - } - }, - FunctionDeclaration: (node, st, walkDeeper) => { - // Don't use walkDeeper, because we are changing the visitor set. - const key = currentModule + "." + node.id.name; - walk.recursive(node, {key: key}, commonVisitors()); - }, - }, commonVisitors())); - - return {dependencyMap: dependencyMap, additionalModules: additionalModules}; -} - -async function calculateRamUsage(codeCopy) { - //Create a temporary/mock WorkerScript and an AST from the code - var currServ = Player.getCurrentServer(); - var workerScript = new WorkerScript({ - filename:"foo", - scriptRef: {code:""}, - args:[], - getCode: function() { return ""; } - }); - workerScript.checkingRam = true; //Netscript functions will return RAM usage - workerScript.serverIp = currServ.ip; - - try { - return await parseOnlyRamCalculate(currServ, codeCopy, workerScript); - } catch (e) { - console.log("Failed to parse ram using new method. Falling back.", e); - } - - // Try the old way. - - try { - var ast = parse(codeCopy, {sourceType:"module"}); - } catch(e) { - return -1; - } - - //Search through AST, scanning for any 'Identifier' nodes for functions, or While/For/If nodes - var queue = [], ramUsage = CONSTANTS.ScriptBaseRamCost; - var whileUsed = false, forUsed = false, ifUsed = false; - queue.push(ast); - while (queue.length != 0) { - var exp = queue.shift(); - switch (exp.type) { - case "ImportDeclaration": - //Gets an array of all imported functions as AST expressions - //and pushes them on the queue. - var res = evaluateImport(exp, workerScript, true); - for (var i = 0; i < res.length; ++i) { - queue.push(res[i]); - } - break; - case "BlockStatement": - case "Program": - for (var i = 0; i < exp.body.length; ++i) { - if (exp.body[i] instanceof Node) { - queue.push(exp.body[i]); - } - } - break; - case "WhileStatement": - if (!whileUsed) { - ramUsage += CONSTANTS.ScriptWhileRamCost; - whileUsed = true; - } - break; - case "ForStatement": - if (!forUsed) { - ramUsage += CONSTANTS.ScriptForRamCost; - forUsed = true; - } - break; - case "IfStatement": - if (!ifUsed) { - ramUsage += CONSTANTS.ScriptIfRamCost; - ifUsed = true; - } - break; - case "Identifier": - if (exp.name in workerScript.env.vars) { - var func = workerScript.env.get(exp.name); - if (typeof func === "function") { - try { - var res = func.apply(null, []); - if (typeof res === "number") { - ramUsage += res; - } - } catch(e) { - console.log("ERROR applying function: " + e); - } - } - } - break; - default: - break; - } - - for (var prop in exp) { - if (exp.hasOwnProperty(prop)) { - if (exp[prop] instanceof Node) { - queue.push(exp[prop]); - } - } - } - } - - //Special case: hacknetnodes array - if (codeCopy.includes("hacknet")) { - ramUsage += CONSTANTS.ScriptHacknetNodesRamCost; - } - return ramUsage; -} - -Script.prototype.download = function() { - var filename = this.filename + ".js"; - var file = new Blob([this.code], {type: 'text/plain'}); - if (window.navigator.msSaveOrOpenBlob) {// IE10+ - window.navigator.msSaveOrOpenBlob(file, filename); - } else { // Others - var a = document.createElement("a"), - url = URL.createObjectURL(file); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - setTimeoutRef(function() { - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - }, 0); - } -} - -Script.prototype.toJSON = function() { - return Generic_toJSON("Script", this); -} - - -Script.fromJSON = function(value) { - return Generic_fromJSON(Script, value.data); -} - -Reviver.constructors.Script = Script; - -//Called when the game is loaded. Loads all running scripts (from all servers) -//into worker scripts so that they will start running -function loadAllRunningScripts() { - var total = 0; - let skipScriptLoad = (window.location.href.toLowerCase().indexOf("?noscripts") !== -1); - if (skipScriptLoad) { console.info("Skipping the load of any scripts during startup"); } - for (var property in AllServers) { - if (AllServers.hasOwnProperty(property)) { - var server = AllServers[property]; - - //Reset each server's RAM usage to 0 - server.ramUsed = 0; - - //Reset modules on all scripts - for (var i = 0; i < server.scripts.length; ++i) { - server.scripts[i].module = ""; - } - - if (skipScriptLoad) { - //Start game with no scripts - server.runningScripts.length = 0; - } else { - for (var j = 0; j < server.runningScripts.length; ++j) { - addWorkerScript(server.runningScripts[j], server); - - //Offline production - total += scriptCalculateOfflineProduction(server.runningScripts[j]); - } - } - } - } - - return total; -} - -function scriptCalculateOfflineProduction(runningScriptObj) { - //The Player object stores the last update time from when we were online - var thisUpdate = new Date().getTime(); - var lastUpdate = Player.lastUpdate; - var timePassed = (thisUpdate - lastUpdate) / 1000; //Seconds - - //Calculate the "confidence" rating of the script's true production. This is based - //entirely off of time. We will arbitrarily say that if a script has been running for - //4 hours (14400 sec) then we are completely confident in its ability - var confidence = (runningScriptObj.onlineRunningTime) / 14400; - if (confidence >= 1) {confidence = 1;} - - //Data map: [MoneyStolen, NumTimesHacked, NumTimesGrown, NumTimesWeaken] - - // Grow - for (var ip in runningScriptObj.dataMap) { - if (runningScriptObj.dataMap.hasOwnProperty(ip)) { - if (runningScriptObj.dataMap[ip][2] == 0 || runningScriptObj.dataMap[ip][2] == null) {continue;} - var serv = AllServers[ip]; - if (serv == null) {continue;} - var timesGrown = Math.round(0.5 * runningScriptObj.dataMap[ip][2] / runningScriptObj.onlineRunningTime * timePassed); - console.log(runningScriptObj.filename + " called grow() on " + serv.hostname + " " + timesGrown + " times while offline"); - runningScriptObj.log("Called grow() on " + serv.hostname + " " + timesGrown + " times while offline"); - var growth = processSingleServerGrowth(serv, timesGrown * 450); - runningScriptObj.log(serv.hostname + " grown by " + numeralWrapper.format(growth * 100 - 100, '0.000000%') + " from grow() calls made while offline"); - } - } - - // Money from hacking - var totalOfflineProduction = 0; - for (var ip in runningScriptObj.dataMap) { - if (runningScriptObj.dataMap.hasOwnProperty(ip)) { - if (runningScriptObj.dataMap[ip][0] == 0 || runningScriptObj.dataMap[ip][0] == null) {continue;} - var serv = AllServers[ip]; - if (serv == null) {continue;} - var production = 0.5 * runningScriptObj.dataMap[ip][0] / runningScriptObj.onlineRunningTime * timePassed; - production *= confidence; - if (production > serv.moneyAvailable) { - production = serv.moneyAvailable; - } - totalOfflineProduction += production; - Player.gainMoney(production); - Player.recordMoneySource(production, "hacking"); - console.log(runningScriptObj.filename + " generated $" + production + " while offline by hacking " + serv.hostname); - runningScriptObj.log(runningScriptObj.filename + " generated $" + production + " while offline by hacking " + serv.hostname); - serv.moneyAvailable -= production; - if (serv.moneyAvailable < 0) {serv.moneyAvailable = 0;} - if (isNaN(serv.moneyAvailable)) {serv.moneyAvailable = 0;} - } - } - - // Offline EXP gain - // A script's offline production will always be at most half of its online production. - var expGain = 0.5 * (runningScriptObj.onlineExpGained / runningScriptObj.onlineRunningTime) * timePassed; - expGain *= confidence; - - Player.gainHackingExp(expGain); - - // Update script stats - runningScriptObj.offlineMoneyMade += totalOfflineProduction; - runningScriptObj.offlineRunningTime += timePassed; - runningScriptObj.offlineExpGained += expGain; - - // Fortify a server's security based on how many times it was hacked - for (var ip in runningScriptObj.dataMap) { - if (runningScriptObj.dataMap.hasOwnProperty(ip)) { - if (runningScriptObj.dataMap[ip][1] == 0 || runningScriptObj.dataMap[ip][1] == null) {continue;} - var serv = AllServers[ip]; - if (serv == null) {continue;} - var timesHacked = Math.round(0.5 * runningScriptObj.dataMap[ip][1] / runningScriptObj.onlineRunningTime * timePassed); - console.log(runningScriptObj.filename + " hacked " + serv.hostname + " " + timesHacked + " times while offline"); - runningScriptObj.log("Hacked " + serv.hostname + " " + timesHacked + " times while offline"); - serv.fortify(CONSTANTS.ServerFortifyAmount * timesHacked); - } - } - - // Weaken - for (var ip in runningScriptObj.dataMap) { - if (runningScriptObj.dataMap.hasOwnProperty(ip)) { - if (runningScriptObj.dataMap[ip][3] == 0 || runningScriptObj.dataMap[ip][3] == null) {continue;} - var serv = AllServers[ip]; - if (serv == null) {continue;} - var timesWeakened = Math.round(0.5 * runningScriptObj.dataMap[ip][3] / runningScriptObj.onlineRunningTime * timePassed); - console.log(runningScriptObj.filename + " called weaken() on " + serv.hostname + " " + timesWeakened + " times while offline"); - runningScriptObj.log("Called weaken() on " + serv.hostname + " " + timesWeakened + " times while offline"); - serv.weaken(CONSTANTS.ServerWeakenAmount * timesWeakened); - } - } - - return totalOfflineProduction; -} - -//Returns a RunningScript object matching the filename and arguments on the -//designated server, and false otherwise -function findRunningScript(filename, args, server) { - for (var i = 0; i < server.runningScripts.length; ++i) { - if (server.runningScripts[i].filename == filename && - compareArrays(server.runningScripts[i].args, args)) { - return server.runningScripts[i]; - } - } - return null; -} - -function RunningScript(script, args) { - if (script == null || script == undefined) { return; } - this.filename = script.filename; - this.args = args; - this.server = script.server; //IP Address only - this.ramUsage = script.ramUsage; - - this.logs = []; //Script logging. Array of strings, with each element being a log entry - this.logUpd = false; - - //Stats to display on the Scripts menu, and used to determine offline progress - this.offlineRunningTime = 0.01; //Seconds - this.offlineMoneyMade = 0; - this.offlineExpGained = 0; - this.onlineRunningTime = 0.01; //Seconds - this.onlineMoneyMade = 0; - this.onlineExpGained = 0; - - this.threads = 1; - - // Holds a map of all servers, 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 - this.dataMap = {}; -} - -RunningScript.prototype.getCode = function() { - const server = AllServers[this.server]; - if (server == null) { return ""; } - for (let i = 0; i < server.scripts.length; ++i) { - if (server.scripts[i].filename === this.filename) { - return server.scripts[i].code; - } - } - - return ""; -} - -RunningScript.prototype.getRamUsage = function() { - if (this.ramUsage != null && this.ramUsage > 0) { return this.ramUsage; } // Use cached value - - const server = AllServers[this.server]; - if (server == null) { return 0; } - for (let i = 0; i < server.scripts.length; ++i) { - if (server.scripts[i].filename === this.filename) { - // Cache the ram usage for the next call - this.ramUsage = server.scripts[i].ramUsage; - return this.ramUsage; - } - } - - - return 0; -} - -RunningScript.prototype.log = function(txt) { - if (this.logs.length > Settings.MaxLogCapacity) { - //Delete first element and add new log entry to the end. - //TODO Eventually it might be better to replace this with circular array - //to improve performance - this.logs.shift(); - } - let logEntry = txt; - if (FconfSettings.ENABLE_TIMESTAMPS) { - logEntry = "[" + getTimestamp() + "] " + logEntry; - } - this.logs.push(logEntry); - this.logUpd = true; -} - -RunningScript.prototype.displayLog = function() { - for (var i = 0; i < this.logs.length; ++i) { - post(this.logs[i]); - } -} - -RunningScript.prototype.clearLog = function() { - this.logs.length = 0; -} - -//Update the moneyStolen and numTimesHack maps when hacking -RunningScript.prototype.recordHack = function(serverIp, moneyGained, n=1) { - if (this.dataMap[serverIp] == null || this.dataMap[serverIp].constructor !== Array) { - this.dataMap[serverIp] = [0, 0, 0, 0]; - } - this.dataMap[serverIp][0] += moneyGained; - this.dataMap[serverIp][1] += n; -} - -//Update the grow map when calling grow() -RunningScript.prototype.recordGrow = function(serverIp, n=1) { - if (this.dataMap[serverIp] == null || this.dataMap[serverIp].constructor !== Array) { - this.dataMap[serverIp] = [0, 0, 0, 0]; - } - this.dataMap[serverIp][2] += n; -} - -//Update the weaken map when calling weaken() { -RunningScript.prototype.recordWeaken = function(serverIp, n=1) { - if (this.dataMap[serverIp] == null || this.dataMap[serverIp].constructor !== Array) { - this.dataMap[serverIp] = [0, 0, 0, 0]; - } - this.dataMap[serverIp][3] += n; -} - -RunningScript.prototype.toJSON = function() { - return Generic_toJSON("RunningScript", this); -} - - -RunningScript.fromJSON = function(value) { - return Generic_fromJSON(RunningScript, value.data); -} - -Reviver.constructors.RunningScript = RunningScript; - -export {loadAllRunningScripts, findRunningScript, - RunningScript, Script, scriptEditorInit, isScriptFilename}; diff --git a/src/Script/RamCalculations.d.ts b/src/Script/RamCalculations.d.ts new file mode 100644 index 000000000..0a886c842 --- /dev/null +++ b/src/Script/RamCalculations.d.ts @@ -0,0 +1 @@ +export declare function calculateRamUsage(codeCopy: string): number; diff --git a/src/Script/RamCalculations.js b/src/Script/RamCalculations.js new file mode 100644 index 000000000..643845a8e --- /dev/null +++ b/src/Script/RamCalculations.js @@ -0,0 +1,409 @@ +// Calculate a script's RAM usage +const walk = require("acorn/dist/walk"); // Importing this doesn't work for some reason. + +import { CONSTANTS } from "../Constants"; +import {evaluateImport} from "../NetscriptEvaluator"; +import { WorkerScript } from "../NetscriptWorker"; +import { Player } from "../Player"; +import {parse, Node} from "../../utils/acorn"; + +// These special strings are used to reference the presence of a given logical +// construct within a user script. +const specialReferenceIF = "__SPECIAL_referenceIf"; +const specialReferenceFOR = "__SPECIAL_referenceFor"; +const specialReferenceWHILE = "__SPECIAL_referenceWhile"; + +// The global scope of a script is registered under this key during parsing. +const memCheckGlobalKey = ".__GLOBAL__"; + +// Calcluates the amount of RAM a script uses. Uses parsing and AST walking only, +// rather than NetscriptEvaluator. This is useful because NetscriptJS code does +// not work under NetscriptEvaluator. +async function parseOnlyRamCalculate(server, code, workerScript) { + try { + // Maps dependent identifiers to their dependencies. + // + // The initial identifier is __SPECIAL_INITIAL_MODULE__.__GLOBAL__. + // It depends on all the functions declared in the module, all the global scopes + // of its imports, and any identifiers referenced in this global scope. Each + // function depends on all the identifiers referenced internally. + // We walk the dependency graph to calculate RAM usage, given that some identifiers + // reference Netscript functions which have a RAM cost. + let dependencyMap = {}; + + // Scripts we've parsed. + const completedParses = new Set(); + + // Scripts we've discovered that need to be parsed. + const parseQueue = []; + + // Parses a chunk of code with a given module name, and updates parseQueue and dependencyMap. + function parseCode(code, moduleName) { + const result = parseOnlyCalculateDeps(code, moduleName); + completedParses.add(moduleName); + + // Add any additional modules to the parse queue; + for (let i = 0; i < result.additionalModules.length; ++i) { + if (!completedParses.has(result.additionalModules[i])) { + parseQueue.push(result.additionalModules[i]); + } + } + + // Splice all the references in. + //Spread syntax not supported in edge, use Object.assign instead + //dependencyMap = {...dependencyMap, ...result.dependencyMap}; + dependencyMap = Object.assign(dependencyMap, result.dependencyMap); + } + + const initialModule = "__SPECIAL_INITIAL_MODULE__"; + parseCode(code, initialModule); + + while (parseQueue.length > 0) { + // Get the code from the server. + const nextModule = parseQueue.shift(); + + let code; + if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) { + try { + const module = await eval('import(nextModule)'); + code = ""; + for (const prop in module) { + if (typeof module[prop] === 'function') { + code += module[prop].toString() + ";\n"; + } + } + } catch(e) { + console.error(`Error dynamically importing module from ${nextModule} for RAM calculations: ${e}`); + return -1; + } + } else { + const script = server.getScript(nextModule.startsWith("./") ? nextModule.slice(2) : nextModule); + if (!script) { + console.warn("Invalid script"); + return -1; // No such script on the server. + } + code = script.code; + } + + parseCode(code, nextModule); + } + + // Finally, walk the reference map and generate a ram cost. The initial set of keys to scan + // are those that start with __SPECIAL_INITIAL_MODULE__. + let ram = CONSTANTS.ScriptBaseRamCost; + const unresolvedRefs = Object.keys(dependencyMap).filter(s => s.startsWith(initialModule)); + const resolvedRefs = new Set(); + while (unresolvedRefs.length > 0) { + const ref = unresolvedRefs.shift(); + + // Check if this is one of the special keys, and add the appropriate ram cost if so. + if (ref === "hacknet" && !resolvedRefs.has("hacknet")) { + ram += CONSTANTS.ScriptHacknetNodesRamCost; + } + if (ref === "document" && !resolvedRefs.has("document")) { + ram += CONSTANTS.ScriptDomRamCost; + } + if (ref === "window" && !resolvedRefs.has("window")) { + ram += CONSTANTS.ScriptDomRamCost; + } + + resolvedRefs.add(ref); + + if (ref.endsWith(".*")) { + // A prefix reference. We need to find all matching identifiers. + const prefix = ref.slice(0, ref.length - 2); + for (let ident of Object.keys(dependencyMap).filter(k => k.startsWith(prefix))) { + for (let dep of dependencyMap[ident] || []) { + if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep); + } + } + } else { + // An exact reference. Add all dependencies of this ref. + for (let dep of dependencyMap[ref] || []) { + if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep); + } + } + + // Check if this ident is a function in the workerscript env. If it is, then we need to + // get its RAM cost. We do this by calling it, which works because the running script + // is in checkingRam mode. + // + // TODO it would be simpler to just reference a dictionary. + try { + function applyFuncRam(func) { + if (typeof func === "function") { + try { + let res; + if (func.constructor.name === "AsyncFunction") { + res = 0; // Async functions will always be 0 RAM + } else { + res = func.apply(null, []); + } + if (typeof res === "number") { + return res; + } + return 0; + } catch(e) { + console.log("ERROR applying function: " + e); + return 0; + } + } else { + return 0; + } + } + + //Special logic for namespaces (Bladeburner, CodingCOntract) + var func; + if (ref in workerScript.env.vars.bladeburner) { + func = workerScript.env.vars.bladeburner[ref]; + } else if (ref in workerScript.env.vars.codingcontract) { + func = workerScript.env.vars.codingcontract[ref]; + } else if (ref in workerScript.env.vars.gang) { + func = workerScript.env.vars.gang[ref]; + } else { + func = workerScript.env.get(ref); + } + ram += applyFuncRam(func); + } catch (error) {continue;} + } + return ram; + + } catch (error) { + // console.info("parse or eval error: ", error); + // This is not unexpected. The user may be editing a script, and it may be in + // a transitory invalid state. + return -1; + } +} + +// Parses one script and calculates its ram usage, for the global scope and each function. +// Returns a cost map and a dependencyMap for the module. Returns a reference map to be joined +// onto the main reference map, and a list of modules that need to be parsed. +function parseOnlyCalculateDeps(code, currentModule) { + const ast = parse(code, {sourceType:"module", ecmaVersion: 8}); + + // Everything from the global scope goes in ".". Everything else goes in ".function", where only + // the outermost layer of functions counts. + const globalKey = currentModule + memCheckGlobalKey; + const dependencyMap = {}; + dependencyMap[globalKey] = new Set(); + + // If we reference this internal name, we're really referencing that external name. + // Filled when we import names from other modules. + let internalToExternal = {}; + + var additionalModules = []; + + // References get added pessimistically. They are added for thisModule.name, name, and for + // any aliases. + function addRef(key, name) { + const s = dependencyMap[key] || (dependencyMap[key] = new Set()); + if (name in internalToExternal) { + s.add(internalToExternal[name]); + } + s.add(currentModule + "." + name); + s.add(name); // For builtins like hack. + } + + //A list of identifiers that resolve to "native Javascript code" + const objectPrototypeProperties = Object.getOwnPropertyNames(Object.prototype); + + // If we discover a dependency identifier, state.key is the dependent identifier. + // walkDeeper is for doing recursive walks of expressions in composites that we handle. + function commonVisitors() { + return { + Identifier: (node, st, walkDeeper) => { + if (objectPrototypeProperties.includes(node.name)) {return;} + addRef(st.key, node.name); + }, + WhileStatement: (node, st, walkDeeper) => { + addRef(st.key, specialReferenceWHILE); + node.test && walkDeeper(node.test, st); + node.body && walkDeeper(node.body, st); + }, + DoWhileStatement: (node, st, walkDeeper) => { + addRef(st.key, specialReferenceWHILE); + node.test && walkDeeper(node.test, st); + node.body && walkDeeper(node.body, st); + }, + ForStatement: (node, st, walkDeeper) => { + addRef(st.key, specialReferenceFOR); + node.init && walkDeeper(node.init, st); + node.test && walkDeeper(node.test, st); + node.update && walkDeeper(node.update, st); + node.body && walkDeeper(node.body, st); + }, + IfStatement: (node, st, walkDeeper) => { + addRef(st.key, specialReferenceIF); + node.test && walkDeeper(node.test, st); + node.consequent && walkDeeper(node.consequent, st); + node.alternate && walkDeeper(node.alternate, st); + }, + MemberExpression: (node, st, walkDeeper) => { + node.object && walkDeeper(node.object, st); + node.property && walkDeeper(node.property, st); + }, + } + } + + //Spread syntax not supported in Edge yet, use Object.assign + /* + walk.recursive(ast, {key: globalKey}, { + ImportDeclaration: (node, st, walkDeeper) => { + const importModuleName = node.source.value; + additionalModules.push(importModuleName); + + // This module's global scope refers to that module's global scope, no matter how we + // import it. + dependencyMap[st.key].add(importModuleName + memCheckGlobalKey); + + for (let i = 0; i < node.specifiers.length; ++i) { + const spec = node.specifiers[i]; + if (spec.imported !== undefined && spec.local !== undefined) { + // We depend on specific things. + internalToExternal[spec.local.name] = importModuleName + "." + spec.imported.name; + } else { + // We depend on everything. + dependencyMap[st.key].add(importModuleName + ".*"); + } + } + }, + FunctionDeclaration: (node, st, walkDeeper) => { + // Don't use walkDeeper, because we are changing the visitor set. + const key = currentModule + "." + node.id.name; + walk.recursive(node, {key: key}, commonVisitors()); + }, + ...commonVisitors() + }); + */ + walk.recursive(ast, {key: globalKey}, Object.assign({ + ImportDeclaration: (node, st, walkDeeper) => { + const importModuleName = node.source.value; + additionalModules.push(importModuleName); + + // This module's global scope refers to that module's global scope, no matter how we + // import it. + dependencyMap[st.key].add(importModuleName + memCheckGlobalKey); + + for (let i = 0; i < node.specifiers.length; ++i) { + const spec = node.specifiers[i]; + if (spec.imported !== undefined && spec.local !== undefined) { + // We depend on specific things. + internalToExternal[spec.local.name] = importModuleName + "." + spec.imported.name; + } else { + // We depend on everything. + dependencyMap[st.key].add(importModuleName + ".*"); + } + } + }, + FunctionDeclaration: (node, st, walkDeeper) => { + // Don't use walkDeeper, because we are changing the visitor set. + const key = currentModule + "." + node.id.name; + walk.recursive(node, {key: key}, commonVisitors()); + }, + }, commonVisitors())); + + return {dependencyMap: dependencyMap, additionalModules: additionalModules}; +} + +export async function calculateRamUsage(codeCopy) { + //Create a temporary/mock WorkerScript and an AST from the code + var currServ = Player.getCurrentServer(); + var workerScript = new WorkerScript({ + filename:"foo", + scriptRef: {code:""}, + args:[], + getCode: function() { return ""; } + }); + workerScript.checkingRam = true; //Netscript functions will return RAM usage + workerScript.serverIp = currServ.ip; + + try { + return await parseOnlyRamCalculate(currServ, codeCopy, workerScript); + } catch (e) { + console.log("Failed to parse ram using new method. Falling back.", e); + } + + // Try the old way. + + try { + var ast = parse(codeCopy, {sourceType:"module"}); + } catch(e) { + return -1; + } + + //Search through AST, scanning for any 'Identifier' nodes for functions, or While/For/If nodes + var queue = [], ramUsage = CONSTANTS.ScriptBaseRamCost; + var whileUsed = false, forUsed = false, ifUsed = false; + queue.push(ast); + while (queue.length != 0) { + var exp = queue.shift(); + switch (exp.type) { + case "ImportDeclaration": + //Gets an array of all imported functions as AST expressions + //and pushes them on the queue. + var res = evaluateImport(exp, workerScript, true); + for (var i = 0; i < res.length; ++i) { + queue.push(res[i]); + } + break; + case "BlockStatement": + case "Program": + for (var i = 0; i < exp.body.length; ++i) { + if (exp.body[i] instanceof Node) { + queue.push(exp.body[i]); + } + } + break; + case "WhileStatement": + if (!whileUsed) { + ramUsage += CONSTANTS.ScriptWhileRamCost; + whileUsed = true; + } + break; + case "ForStatement": + if (!forUsed) { + ramUsage += CONSTANTS.ScriptForRamCost; + forUsed = true; + } + break; + case "IfStatement": + if (!ifUsed) { + ramUsage += CONSTANTS.ScriptIfRamCost; + ifUsed = true; + } + break; + case "Identifier": + if (exp.name in workerScript.env.vars) { + var func = workerScript.env.get(exp.name); + if (typeof func === "function") { + try { + var res = func.apply(null, []); + if (typeof res === "number") { + ramUsage += res; + } + } catch(e) { + console.log("ERROR applying function: " + e); + } + } + } + break; + default: + break; + } + + for (var prop in exp) { + if (exp.hasOwnProperty(prop)) { + if (exp[prop] instanceof Node) { + queue.push(exp[prop]); + } + } + } + } + + //Special case: hacknetnodes array + if (codeCopy.includes("hacknet")) { + ramUsage += CONSTANTS.ScriptHacknetNodesRamCost; + } + return ramUsage; +} diff --git a/src/Script/RunningScript.ts b/src/Script/RunningScript.ts new file mode 100644 index 000000000..9da52d47b --- /dev/null +++ b/src/Script/RunningScript.ts @@ -0,0 +1,161 @@ +// 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 { AllServers } from "../Server/AllServers"; +import { Settings } from "../Settings/Settings"; +import { IMap } from "../types"; +import { post } from "../ui/postToTerminal"; + +import { Generic_fromJSON, + Generic_toJSON, + Reviver } from "../../utils/JSONReviver"; +import { getTimestamp } from "../../utils/helpers/getTimestamp"; + +export class RunningScript { + // Initializes a RunningScript Object from a JSON save state + static fromJSON(value: any): RunningScript { + return Generic_fromJSON(RunningScript, value.data); + } + + // 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 + dataMap: IMap = {}; + + // Script filename + filename: string = ""; + + // This script's logs. An array of log entries + logs: string[] = []; + + // Flag indicating whether the logs have been updated since + // the last time the UI was updated + logUpd: boolean = false; + + // Total amount of hacking experience earned from this script when offline + offlineExpGained: number = 0; + + // Total amount of money made by this script when offline + offlineMoneyMade: number = 0; + + // Number of seconds that the script has been running offline + offlineRunningTime: number = 0.01; + + // Total amount of hacking experience earned from this script when online + onlineExpGained: number = 0; + + // Total amount of money made by this script when online + onlineMoneyMade: number = 0; + + // Number of seconds that this script has been running online + onlineRunningTime: number = 0.01; + + // How much RAM this script uses for ONE thread + ramUsage: number = 0; + + // IP of the server on which this script is running + server: string = ""; + + // Number of threads that this script is running with + threads: number = 1; + + constructor(script: Script | null = null, args: any[] = []) { + if (script == null) { return; } + this.filename = script.filename; + this.args = args; + + this.server = script.server; //IP Address only + this.ramUsage = script.ramUsage; + } + + getCode(): string { + const server = AllServers[this.server]; + if (server == null) { return ""; } + for (let i = 0; i < server.scripts.length; ++i) { + if (server.scripts[i].filename === this.filename) { + return server.scripts[i].code; + } + } + + return ""; + } + + getRamUsage(): number { + if (this.ramUsage != null && this.ramUsage > 0) { return this.ramUsage; } // Use cached value + + const server = AllServers[this.server]; + if (server == null) { return 0; } + for (let i = 0; i < server.scripts.length; ++i) { + if (server.scripts[i].filename === this.filename) { + // Cache the ram usage for the next call + this.ramUsage = server.scripts[i].ramUsage; + return this.ramUsage; + } + } + + + return 0; + } + + log(txt: string): void { + if (this.logs.length > Settings.MaxLogCapacity) { + //Delete first element and add new log entry to the end. + //TODO Eventually it might be better to replace this with circular array + //to improve performance + this.logs.shift(); + } + let logEntry = txt; + if (FconfSettings.ENABLE_TIMESTAMPS) { + logEntry = "[" + getTimestamp() + "] " + logEntry; + } + this.logs.push(logEntry); + this.logUpd = true; + } + + displayLog(): void { + for (var i = 0; i < this.logs.length; ++i) { + post(this.logs[i]); + } + } + + clearLog(): void { + this.logs.length = 0; + } + + // Update the moneyStolen and numTimesHack maps when hacking + recordHack(serverIp: string, moneyGained: number, n: number=1) { + if (this.dataMap[serverIp] == null || this.dataMap[serverIp].constructor !== Array) { + this.dataMap[serverIp] = [0, 0, 0, 0]; + } + this.dataMap[serverIp][0] += moneyGained; + this.dataMap[serverIp][1] += n; + } + + // Update the grow map when calling grow() + recordGrow(serverIp: string, n: number=1) { + if (this.dataMap[serverIp] == null || this.dataMap[serverIp].constructor !== Array) { + this.dataMap[serverIp] = [0, 0, 0, 0]; + } + this.dataMap[serverIp][2] += n; + } + + // Update the weaken map when calling weaken() { + recordWeaken(serverIp: string, n: number=1) { + if (this.dataMap[serverIp] == null || this.dataMap[serverIp].constructor !== Array) { + this.dataMap[serverIp] = [0, 0, 0, 0]; + } + this.dataMap[serverIp][3] += n; + } + + // Serialize the current object to a JSON save state + toJSON(): any { + return Generic_toJSON("RunningScript", this); + } +} + +Reviver.constructors.RunningScript = RunningScript; diff --git a/src/Script/Script.ts b/src/Script/Script.ts new file mode 100644 index 000000000..92f854f40 --- /dev/null +++ b/src/Script/Script.ts @@ -0,0 +1,106 @@ +// 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 { IPlayer } from "../PersonObjects/IPlayer"; +import { Page, + routing } from "../ui/navigationTracking"; + +import { setTimeoutRef } from "../utils/SetTimeoutRef"; +import { Generic_fromJSON, + Generic_toJSON, + Reviver } from "../../utils/JSONReviver"; +import { roundToTwo } from "../../utils/helpers/roundToTwo"; + +export class Script { + // Initializes a Script Object from a JSON save state + static fromJSON(value: any): Script { + return Generic_fromJSON(Script, value.data); + } + + // Code for this script + code: string = ""; + + // Filename for the script file + filename: string = ""; + + // The dynamic module generated for this script when it is run. + // This is only applicable for NetscriptJS + module: any = ""; + + // Amount of RAM this Script requres to run + ramUsage: number = 0; + + // IP of server that this script is on. + server: string = ""; + + + constructor(fn: string = "", code: string = "", server: string = "") { + this.filename = fn; + this.code = code; + this.ramUsage = 0; + this.server = server; // IP of server this script is on + this.module = ""; + if (this.code !== "") {this.updateRamUsage();} + }; + + download(): void { + const filename = this.filename + ".js"; + const file = new Blob([this.code], {type: 'text/plain'}); + if (window.navigator.msSaveOrOpenBlob) {// IE10+ + window.navigator.msSaveOrOpenBlob(file, filename); + } else { // Others + var a = document.createElement("a"), + url = URL.createObjectURL(file); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + setTimeoutRef(function() { + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 0); + } + } + + // Save a script FROM THE SCRIPT EDITOR + saveScript(code: string, p: IPlayer): void { + if (routing.isOn(Page.ScriptEditor)) { + //Update code and filename + this.code = code.replace(/^\s+|\s+$/g, ''); + + const filenameElem: HTMLInputElement | null = document.getElementById("script-editor-filename") as HTMLInputElement; + if (filenameElem == null) { + console.error(`Failed to get Script filename DOM element`); + return; + } + this.filename = filenameElem!.value; + + // Server + this.server = p.currentServer; + + //Calculate/update ram usage, execution time, etc. + this.updateRamUsage(); + + this.module = ""; + } + } + + // Updates the script's RAM usage based on its code + async updateRamUsage() { + // TODO Commented this out because I think its unnecessary + // DOuble check/Test + // var codeCopy = this.code.repeat(1); + var res = await calculateRamUsage(this.code); + if (res !== -1) { + this.ramUsage = roundToTwo(res); + } + } + + // Serialize the current object to a JSON save state + toJSON(): any { + return Generic_toJSON("Script", this); + } +} + +Reviver.constructors.Script = Script; diff --git a/src/Script/ScriptHelpers.js b/src/Script/ScriptHelpers.js new file mode 100644 index 000000000..821dce013 --- /dev/null +++ b/src/Script/ScriptHelpers.js @@ -0,0 +1,441 @@ +import { calculateRamUsage } from "./RamCalculations"; +import { isScriptFilename } from "./ScriptHelpersTS"; + +import {CONSTANTS} from "../Constants"; +import {Engine} from "../engine"; +import { parseFconfSettings } from "../Fconf/Fconf"; +import { FconfSettings } from "../Fconf/FconfSettings"; +import {iTutorialSteps, iTutorialNextStep, + ITutorial} from "../InteractiveTutorial"; +import { addWorkerScript } from "../NetscriptWorker"; +import { Player } from "../Player"; +import { AceEditor } from "../ScriptEditor/Ace"; +import { CodeMirrorEditor } from "../ScriptEditor/CodeMirror"; +import { AllServers } from "../Server/AllServers"; +import { processSingleServerGrowth } from "../Server/ServerHelpers"; +import { Settings } from "../Settings/Settings"; +import { EditorSetting } from "../Settings/SettingEnums"; +import {TextFile} from "../TextFile"; + +import {Page, routing} from "../ui/navigationTracking"; +import {numeralWrapper} from "../ui/numeralFormat"; + +import {dialogBoxCreate} from "../../utils/DialogBox"; +import {Reviver, Generic_toJSON, + Generic_fromJSON} from "../../utils/JSONReviver"; +import {compareArrays} from "../../utils/helpers/compareArrays"; +import {createElement} from "../../utils/uiHelpers/createElement"; + +var scriptEditorRamCheck = null, scriptEditorRamText = null; +export function scriptEditorInit() { + // Wrapper container that holds all the buttons below the script editor + const wrapper = document.getElementById("script-editor-buttons-wrapper"); + if (wrapper == null) { + console.error("Could not find 'script-editor-buttons-wrapper'"); + return false; + } + + // Beautify button + const beautifyButton = createElement("button", { + class: "std-button", + display: "inline-block", + innerText: "Beautify", + clickListener:()=>{ + let editor = getCurrentEditor(); + if (editor != null) { + editor.beautifyScript(); + } + return false; + } + }); + + // Text that displays RAM calculation + scriptEditorRamText = createElement("p", { + display:"inline-block", margin:"10px", id:"script-editor-status-text" + }); + + // Label for checkbox (defined below) + const checkboxLabel = createElement("label", { + for:"script-editor-ram-check", margin:"4px", marginTop: "8px", + innerText:"Dynamic RAM Usage Checker", color:"white", + tooltip:"Enable/Disable the dynamic RAM Usage display. You may " + + "want to disable it for very long scripts because there may be " + + "performance issues" + }); + + // Checkbox for enabling/disabling dynamic RAM calculation + scriptEditorRamCheck = createElement("input", { + type:"checkbox", name:"script-editor-ram-check", id:"script-editor-ram-check", + margin:"4px", marginTop: "8px", + }); + scriptEditorRamCheck.checked = true; + + // Link to Netscript documentation + const documentationButton = createElement("a", { + class: "std-button", + display: "inline-block", + href:"https://bitburner.readthedocs.io/en/latest/index.html", + innerText:"Netscript Documentation", + target:"_blank", + }); + + // Save and Close button + const closeButton = createElement("button", { + class: "std-button", + display: "inline-block", + innerText: "Save & Close (Ctrl/Cmd + b)", + clickListener:()=>{ + saveAndCloseScriptEditor(); + return false; + } + }); + + // Add all buttons to the UI + wrapper.appendChild(beautifyButton); + wrapper.appendChild(closeButton); + wrapper.appendChild(scriptEditorRamText); + wrapper.appendChild(scriptEditorRamCheck); + wrapper.appendChild(checkboxLabel); + wrapper.appendChild(documentationButton); + + // Initialize editors + const initParams = { + saveAndCloseFn: saveAndCloseScriptEditor, + quitFn: Engine.loadTerminalContent, + } + + AceEditor.init(initParams); + CodeMirrorEditor.init(initParams); + + // Setup the selector for which Editor to use + const editorSelector = document.getElementById("script-editor-option-editor"); + if (editorSelector == null) { + console.error(`Could not find DOM Element for editor selector (id=script-editor-option-editor)`); + return false; + } + + for (let i = 0; i < editorSelector.options.length; ++i) { + if (editorSelector.options[i].value === Settings.Editor) { + editorSelector.selectedIndex = i; + break; + } + } + + editorSelector.onchange = () => { + const opt = editorSelector.value; + switch (opt) { + case EditorSetting.Ace: + const codeMirrorCode = CodeMirrorEditor.getCode(); + const codeMirrorFn = CodeMirrorEditor.getFilename(); + AceEditor.create(); + CodeMirrorEditor.setInvisible(); + AceEditor.openScript(codeMirrorFn, codeMirrorCode); + break; + case EditorSetting.CodeMirror: + const aceCode = AceEditor.getCode(); + const aceFn = AceEditor.getFilename(); + CodeMirrorEditor.create(); + AceEditor.setInvisible(); + CodeMirrorEditor.openScript(aceFn, aceCode); + break; + default: + console.error(`Unrecognized Editor Setting: ${opt}`); + return; + } + + Settings.Editor = opt; + } + + editorSelector.onchange(); // Trigger the onchange event handler +} + +export function getCurrentEditor() { + switch (Settings.Editor) { + case EditorSetting.Ace: + return AceEditor; + case EditorSetting.CodeMirror: + return CodeMirrorEditor; + default: + console.log(`Invalid Editor Setting: ${Settings.Editor}`); + throw new Error(`Invalid Editor Setting: ${Settings.Editor}`); + return null; + } +} + +//Updates RAM usage in script +export async function updateScriptEditorContent() { + var filename = document.getElementById("script-editor-filename").value; + if (scriptEditorRamCheck == null || !scriptEditorRamCheck.checked || !isScriptFilename(filename)) { + scriptEditorRamText.innerText = "N/A"; + return; + } + + let code; + try { + code = getCurrentEditor().getCode(); + } catch(e) { + scriptEditorRamText.innerText = "RAM: ERROR"; + return; + } + + var codeCopy = code.repeat(1); + var ramUsage = await calculateRamUsage(codeCopy); + if (ramUsage !== -1) { + scriptEditorRamText.innerText = "RAM: " + numeralWrapper.format(ramUsage, '0.00') + " GB"; + } else { + scriptEditorRamText.innerText = "RAM: Syntax Error"; + } +} + +//Define key commands in script editor (ctrl o to save + close, etc.) +$(document).keydown(function(e) { + if (Settings.DisableHotkeys === true) {return;} + if (routing.isOn(Page.ScriptEditor)) { + //Ctrl + b + if (e.keyCode == 66 && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + saveAndCloseScriptEditor(); + } + } +}); + +function saveAndCloseScriptEditor() { + var filename = document.getElementById("script-editor-filename").value; + + let code; + try { + code = getCurrentEditor().getCode(); + } catch(e) { + dialogBoxCreate("Something went wrong when trying to save (getCurrentEditor().getCode()). Please report to game developer with details"); + return; + } + + if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) { + //Make sure filename + code properly follow tutorial + if (filename !== "foodnstuff.script") { + dialogBoxCreate("Leave the script name as 'foodnstuff'!"); + return; + } + code = code.replace(/\s/g, ""); + if (code.indexOf("while(true){hack('foodnstuff');}") == -1) { + dialogBoxCreate("Please copy and paste the code from the tutorial!"); + return; + } + + //Save the script + let s = Player.getCurrentServer(); + for (var i = 0; i < s.scripts.length; i++) { + if (filename == s.scripts[i].filename) { + s.scripts[i].saveScript(getCurrentEditor().getCode(), Player); + Engine.loadTerminalContent(); + return iTutorialNextStep(); + } + } + + //If the current script does NOT exist, create a new one + let script = new Script(); + script.saveScript(getCurrentEditor().getCode(), Player); + s.scripts.push(script); + + return iTutorialNextStep(); + } + + if (filename == "") { + dialogBoxCreate("You must specify a filename!"); + return; + } + + if (checkValidFilename(filename) == false) { + dialogBoxCreate("Script filename can contain only alphanumerics, hyphens, and underscores"); + return; + } + + var s = Player.getCurrentServer(); + if (filename === ".fconf") { + try { + parseFconfSettings(code); + } catch(e) { + dialogBoxCreate(`Invalid .fconf file: ${e}`); + return; + } + } else if (isScriptFilename(filename)) { + //If the current script already exists on the server, overwrite it + for (var i = 0; i < s.scripts.length; i++) { + if (filename == s.scripts[i].filename) { + s.scripts[i].saveScript(getCurrentEditor().getCode(), Player); + Engine.loadTerminalContent(); + return; + } + } + + //If the current script does NOT exist, create a new one + var script = new Script(); + script.saveScript(getCurrentEditor().getCode(), Player); + s.scripts.push(script); + } else if (filename.endsWith(".txt")) { + for (var i = 0; i < s.textFiles.length; ++i) { + if (s.textFiles[i].fn === filename) { + s.textFiles[i].write(code); + Engine.loadTerminalContent(); + return; + } + } + var textFile = new TextFile(filename, code); + s.textFiles.push(textFile); + } else { + dialogBoxCreate("Invalid filename. Must be either a script (.script) or " + + " or text file (.txt)") + return; + } + Engine.loadTerminalContent(); +} + +//Checks that the string contains only valid characters for a filename, which are alphanumeric, +// underscores, hyphens, and dots +function checkValidFilename(filename) { + var regex = /^[.a-zA-Z0-9_-]+$/; + + if (filename.match(regex)) { + return true; + } + return false; +} + +//Called when the game is loaded. Loads all running scripts (from all servers) +//into worker scripts so that they will start running +export function loadAllRunningScripts() { + var total = 0; + let skipScriptLoad = (window.location.href.toLowerCase().indexOf("?noscripts") !== -1); + if (skipScriptLoad) { console.info("Skipping the load of any scripts during startup"); } + for (var property in AllServers) { + if (AllServers.hasOwnProperty(property)) { + var server = AllServers[property]; + + //Reset each server's RAM usage to 0 + server.ramUsed = 0; + + //Reset modules on all scripts + for (var i = 0; i < server.scripts.length; ++i) { + server.scripts[i].module = ""; + } + + if (skipScriptLoad) { + //Start game with no scripts + server.runningScripts.length = 0; + } else { + for (var j = 0; j < server.runningScripts.length; ++j) { + addWorkerScript(server.runningScripts[j], server); + + //Offline production + total += scriptCalculateOfflineProduction(server.runningScripts[j]); + } + } + } + } + + return total; +} + +function scriptCalculateOfflineProduction(runningScriptObj) { + //The Player object stores the last update time from when we were online + var thisUpdate = new Date().getTime(); + var lastUpdate = Player.lastUpdate; + var timePassed = (thisUpdate - lastUpdate) / 1000; //Seconds + + //Calculate the "confidence" rating of the script's true production. This is based + //entirely off of time. We will arbitrarily say that if a script has been running for + //4 hours (14400 sec) then we are completely confident in its ability + var confidence = (runningScriptObj.onlineRunningTime) / 14400; + if (confidence >= 1) {confidence = 1;} + + //Data map: [MoneyStolen, NumTimesHacked, NumTimesGrown, NumTimesWeaken] + + // Grow + for (var ip in runningScriptObj.dataMap) { + if (runningScriptObj.dataMap.hasOwnProperty(ip)) { + if (runningScriptObj.dataMap[ip][2] == 0 || runningScriptObj.dataMap[ip][2] == null) {continue;} + var serv = AllServers[ip]; + if (serv == null) {continue;} + var timesGrown = Math.round(0.5 * runningScriptObj.dataMap[ip][2] / runningScriptObj.onlineRunningTime * timePassed); + console.log(runningScriptObj.filename + " called grow() on " + serv.hostname + " " + timesGrown + " times while offline"); + runningScriptObj.log("Called grow() on " + serv.hostname + " " + timesGrown + " times while offline"); + var growth = processSingleServerGrowth(serv, timesGrown * 450, Player); + runningScriptObj.log(serv.hostname + " grown by " + numeralWrapper.format(growth * 100 - 100, '0.000000%') + " from grow() calls made while offline"); + } + } + + // Money from hacking + var totalOfflineProduction = 0; + for (var ip in runningScriptObj.dataMap) { + if (runningScriptObj.dataMap.hasOwnProperty(ip)) { + if (runningScriptObj.dataMap[ip][0] == 0 || runningScriptObj.dataMap[ip][0] == null) {continue;} + var serv = AllServers[ip]; + if (serv == null) {continue;} + var production = 0.5 * runningScriptObj.dataMap[ip][0] / runningScriptObj.onlineRunningTime * timePassed; + production *= confidence; + if (production > serv.moneyAvailable) { + production = serv.moneyAvailable; + } + totalOfflineProduction += production; + Player.gainMoney(production); + Player.recordMoneySource(production, "hacking"); + console.log(runningScriptObj.filename + " generated $" + production + " while offline by hacking " + serv.hostname); + runningScriptObj.log(runningScriptObj.filename + " generated $" + production + " while offline by hacking " + serv.hostname); + serv.moneyAvailable -= production; + if (serv.moneyAvailable < 0) {serv.moneyAvailable = 0;} + if (isNaN(serv.moneyAvailable)) {serv.moneyAvailable = 0;} + } + } + + // Offline EXP gain + // A script's offline production will always be at most half of its online production. + var expGain = 0.5 * (runningScriptObj.onlineExpGained / runningScriptObj.onlineRunningTime) * timePassed; + expGain *= confidence; + + Player.gainHackingExp(expGain); + + // Update script stats + runningScriptObj.offlineMoneyMade += totalOfflineProduction; + runningScriptObj.offlineRunningTime += timePassed; + runningScriptObj.offlineExpGained += expGain; + + // Fortify a server's security based on how many times it was hacked + for (var ip in runningScriptObj.dataMap) { + if (runningScriptObj.dataMap.hasOwnProperty(ip)) { + if (runningScriptObj.dataMap[ip][1] == 0 || runningScriptObj.dataMap[ip][1] == null) {continue;} + var serv = AllServers[ip]; + if (serv == null) {continue;} + var timesHacked = Math.round(0.5 * runningScriptObj.dataMap[ip][1] / runningScriptObj.onlineRunningTime * timePassed); + console.log(runningScriptObj.filename + " hacked " + serv.hostname + " " + timesHacked + " times while offline"); + runningScriptObj.log("Hacked " + serv.hostname + " " + timesHacked + " times while offline"); + serv.fortify(CONSTANTS.ServerFortifyAmount * timesHacked); + } + } + + // Weaken + for (var ip in runningScriptObj.dataMap) { + if (runningScriptObj.dataMap.hasOwnProperty(ip)) { + if (runningScriptObj.dataMap[ip][3] == 0 || runningScriptObj.dataMap[ip][3] == null) {continue;} + var serv = AllServers[ip]; + if (serv == null) {continue;} + var timesWeakened = Math.round(0.5 * runningScriptObj.dataMap[ip][3] / runningScriptObj.onlineRunningTime * timePassed); + console.log(runningScriptObj.filename + " called weaken() on " + serv.hostname + " " + timesWeakened + " times while offline"); + runningScriptObj.log("Called weaken() on " + serv.hostname + " " + timesWeakened + " times while offline"); + serv.weaken(CONSTANTS.ServerWeakenAmount * timesWeakened); + } + } + + return totalOfflineProduction; +} + +//Returns a RunningScript object matching the filename and arguments on the +//designated server, and false otherwise +export function findRunningScript(filename, args, server) { + for (var i = 0; i < server.runningScripts.length; ++i) { + if (server.runningScripts[i].filename == filename && + compareArrays(server.runningScripts[i].args, args)) { + return server.runningScripts[i]; + } + } + return null; +} diff --git a/src/Script/ScriptHelpersTS.ts b/src/Script/ScriptHelpersTS.ts new file mode 100644 index 000000000..f59b69810 --- /dev/null +++ b/src/Script/ScriptHelpersTS.ts @@ -0,0 +1,4 @@ +// Script helper functions +export function isScriptFilename(f: string) { + return f.endsWith(".js") || f.endsWith(".script") || f.endsWith(".ns"); +} diff --git a/src/Server.js b/src/Server.js deleted file mode 100644 index 6aa27b6f4..000000000 --- a/src/Server.js +++ /dev/null @@ -1,468 +0,0 @@ -import { BitNodeMultipliers } from "./BitNode/BitNodeMultipliers"; -import { CodingContract, - ContractTypes } from "./CodingContracts"; -import { CONSTANTS } from "./Constants"; -import { Script, - isScriptFilename } from "./Script"; -import { Player } from "./Player"; -import { Programs } from "./Programs/Programs"; -import { SpecialServerIps } from "./SpecialServerIps"; -import { TextFile } from "./TextFile"; -import { getRandomInt } from "../utils/helpers/getRandomInt"; -import { createRandomIp, - ipExists } from "../utils/IPAddress"; -import { serverMetadata } from "./data/servers"; -import { Reviver, - Generic_toJSON, - Generic_fromJSON} from "../utils/JSONReviver"; -import {isValidIPAddress} from "../utils/helpers/isValidIPAddress"; - -function Server(params={ip:createRandomIp(), hostname:""}) { - /* Properties */ - //Connection information - this.ip = params.ip ? params.ip : createRandomIp(); - - var hostname = params.hostname; - var i = 0; - var suffix = ""; - while (GetServerByHostname(hostname+suffix) != null) { - //Server already exists - suffix = "-" + i; - ++i; - } - this.hostname = hostname + suffix; - this.organizationName = params.organizationName != null ? params.organizationName : ""; - this.isConnectedTo = params.isConnectedTo != null ? params.isConnectedTo : false; - - //Access information - this.hasAdminRights = params.adminRights != null ? params.adminRights : false; - this.purchasedByPlayer = params.purchasedByPlayer != null ? params.purchasedByPlayer : false; - this.manuallyHacked = false; //Flag that tracks whether or not the server has been hacked at least once - - //RAM, CPU speed and Scripts - this.maxRam = params.maxRam != null ? params.maxRam : 0; //GB - this.ramUsed = 0; - this.cpuCores = 1; //Max of 8, affects hacking times and Hacking Mission starting Cores - - this.scripts = []; - this.runningScripts = []; //Stores RunningScript objects - this.programs = []; - this.messages = []; - this.textFiles = []; - this.contracts = []; - this.dir = 0; //new Directory(this, null, ""); TODO - - /* Hacking information (only valid for "foreign" aka non-purchased servers) */ - this.requiredHackingSkill = params.requiredHackingSkill != null ? params.requiredHackingSkill : 1; - this.moneyAvailable = params.moneyAvailable != null ? params.moneyAvailable * BitNodeMultipliers.ServerStartingMoney : 0; - this.moneyMax = 25 * this.moneyAvailable * BitNodeMultipliers.ServerMaxMoney; - - //Hack Difficulty is synonymous with server security. Base Difficulty = Starting difficulty - this.hackDifficulty = params.hackDifficulty != null ? params.hackDifficulty * BitNodeMultipliers.ServerStartingSecurity : 1; - this.baseDifficulty = this.hackDifficulty; - this.minDifficulty = Math.max(1, Math.round(this.hackDifficulty / 3)); - this.serverGrowth = params.serverGrowth != null ? params.serverGrowth : 1; //Integer from 0 to 100. Affects money increase from grow() - - //The IP's of all servers reachable from this one (what shows up if you run scan/netstat) - // NOTE: Only contains IP and not the Server objects themselves - this.serversOnNetwork = []; - - //Port information, required for porthacking servers to get admin rights - this.numOpenPortsRequired = params.numOpenPortsRequired != null ? params.numOpenPortsRequired : 5; - this.sshPortOpen = false; //Port 22 - this.ftpPortOpen = false; //Port 21 - this.smtpPortOpen = false; //Port 25 - this.httpPortOpen = false; //Port 80 - this.sqlPortOpen = false; //Port 1433 - this.openPortCount = 0; -}; - -Server.prototype.setMaxRam = function(ram) { - this.maxRam = ram; -} - -//The serverOnNetwork array holds the IP of all the servers. This function -//returns the actual Server objects -Server.prototype.getServerOnNetwork = function(i) { - if (i > this.serversOnNetwork.length) { - console.log("Tried to get server on network that was out of range"); - return; - } - return AllServers[this.serversOnNetwork[i]]; -} - -//Given the name of the script, returns the corresponding -//script object on the server (if it exists) -Server.prototype.getScript = function(scriptName) { - for (var i = 0; i < this.scripts.length; i++) { - if (this.scripts[i].filename == scriptName) { - return this.scripts[i]; - } - } - return null; -} - -Server.prototype.capDifficulty = function() { - if (this.hackDifficulty < this.minDifficulty) {this.hackDifficulty = this.minDifficulty;} - if (this.hackDifficulty < 1) {this.hackDifficulty = 1;} - //Place some arbitrarily limit that realistically should never happen unless someone is - //screwing around with the game - if (this.hackDifficulty > 1000000) {this.hackDifficulty = 1000000;} -} - -//Strengthens a server's security level (difficulty) by the specified amount -Server.prototype.fortify = function(amt) { - this.hackDifficulty += amt; - this.capDifficulty(); -} - -Server.prototype.weaken = function(amt) { - this.hackDifficulty -= (amt * BitNodeMultipliers.ServerWeakenRate); - this.capDifficulty(); -} - -// Write to a script file -// Overwrites existing files. Creates new files if the script does not eixst -Server.prototype.writeToScriptFile = function(fn, code) { - var ret = {success: false, overwritten: false}; - if (!isScriptFilename(fn)) { return ret; } - - //Check if the script already exists, and overwrite it if it does - for (let i = 0; i < this.scripts.length; ++i) { - if (fn === this.scripts[i].filename) { - let script = this.scripts[i]; - script.code = code; - script.updateRamUsage(); - script.module = ""; - ret.overwritten = true; - ret.success = true; - return ret; - } - } - - //Otherwise, create a new script - var newScript = new Script(); - newScript.filename = fn; - newScript.code = code; - newScript.updateRamUsage(); - newScript.server = this.ip; - this.scripts.push(newScript); - ret.success = true; - return ret; -} - -// Write to a text file -// Overwrites existing files. Creates new files if the text file does not exist -Server.prototype.writeToTextFile = function(fn, txt) { - var ret = {success: false, overwritten: false}; - if (!fn.endsWith("txt")) { return ret; } - - //Check if the text file already exists, and overwrite if it does - for (let i = 0; i < this.textFiles.length; ++i) { - if (this.textFiles[i].fn === fn) { - ret.overwritten = true; - this.textFiles[i].text = txt; - ret.success = true; - return ret; - } - } - - //Otherwise create a new text file - var newFile = new TextFile(fn, txt); - this.textFiles.push(newFile); - ret.success = true; - return ret; -} - -Server.prototype.addContract = function(contract) { - this.contracts.push(contract); -} - -Server.prototype.removeContract = function(contract) { - if (contract instanceof CodingContract) { - this.contracts = this.contracts.filter((c) => { - return c.fn !== contract.fn; - }); - } else { - this.contracts = this.contracts.filter((c) => { - return c.fn !== contract; - }); - } -} - -Server.prototype.getContract = function(contractName) { - for (const contract of this.contracts) { - if (contract.fn === contractName) { - return contract; - } - } - return null; -} - -//Functions for loading and saving a Server -Server.prototype.toJSON = function() { - return Generic_toJSON("Server", this); -} - -Server.fromJSON = function(value) { - return Generic_fromJSON(Server, value.data); -} - -Reviver.constructors.Server = Server; - -export function initForeignServers() { - /* Create a randomized network for all the foreign servers */ - //Groupings for creating a randomized network - const networkLayers = []; - for (let i = 0; i < 15; i++) { - networkLayers.push([]); - } - - // Essentially any property that is of type 'number | IMinMaxRange' - const propertiesToPatternMatch = [ - "hackDifficulty", - "moneyAvailable", - "requiredHackingSkill", - "serverGrowth" - ]; - - const toNumber = (value) => { - switch (typeof value) { - case 'number': - return value; - case 'object': - return getRandomInt(value.min, value.max); - default: - throw Error(`Do not know how to convert the type '${typeof value}' to a number`); - } - } - - for (const metadata of serverMetadata) { - const serverParams = { - hostname: metadata.hostname, - ip: createRandomIp(), - numOpenPortsRequired: metadata.numOpenPortsRequired, - organizationName: metadata.organizationName - }; - - if (metadata.maxRamExponent !== undefined) { - serverParams.maxRam = Math.pow(2, toNumber(metadata.maxRamExponent)); - } - - for (const prop of propertiesToPatternMatch) { - if (metadata[prop] !== undefined) { - serverParams[prop] = toNumber(metadata[prop]); - } - } - - const server = new Server(serverParams); - for (const filename of (metadata.literature || [])) { - server.messages.push(filename); - } - - if (metadata.specialName !== undefined) { - SpecialServerIps.addIp(metadata.specialName, server.ip); - } - - AddToAllServers(server); - if (metadata.networkLayer !== undefined) { - networkLayers[toNumber(metadata.networkLayer) - 1].push(server); - } - } - - /* Create a randomized network for all the foreign servers */ - const linkComputers = (server1, server2) => { - server1.serversOnNetwork.push(server2.ip); - server2.serversOnNetwork.push(server1.ip); - }; - - const getRandomArrayItem = (arr) => arr[Math.floor(Math.random() * arr.length)]; - - const linkNetworkLayers = (network1, selectServer) => { - for (const server of network1) { - linkComputers(server, selectServer()); - } - }; - - // Connect the first tier of servers to the player's home computer - linkNetworkLayers(networkLayers[0], () => Player.getHomeComputer()); - for (let i = 1; i < networkLayers.length; i++) { - linkNetworkLayers(networkLayers[i], () => getRandomArrayItem(networkLayers[i - 1])); - } -} - -// Returns the number of cycles needed to grow the specified server by the -// specified amount. 'growth' parameter is in decimal form, not percentage -export function numCycleForGrowth(server, growth) { - let ajdGrowthRate = 1 + (CONSTANTS.ServerBaseGrowthRate - 1) / server.hackDifficulty; - if(ajdGrowthRate > CONSTANTS.ServerMaxGrowthRate) { - ajdGrowthRate = CONSTANTS.ServerMaxGrowthRate; - } - - const serverGrowthPercentage = server.serverGrowth / 100; - - const cycles = Math.log(growth)/(Math.log(ajdGrowthRate)*Player.hacking_grow_mult*serverGrowthPercentage); - return cycles; -} - -//Applied server growth for a single server. Returns the percentage growth -export function processSingleServerGrowth(server, numCycles) { - //Server growth processed once every 450 game cycles - const numServerGrowthCycles = Math.max(Math.floor(numCycles / 450), 0); - - //Get adjusted growth rate, which accounts for server security - const growthRate = CONSTANTS.ServerBaseGrowthRate; - var adjGrowthRate = 1 + (growthRate - 1) / server.hackDifficulty; - if (adjGrowthRate > CONSTANTS.ServerMaxGrowthRate) {adjGrowthRate = CONSTANTS.ServerMaxGrowthRate;} - - //Calculate adjusted server growth rate based on parameters - const serverGrowthPercentage = server.serverGrowth / 100; - const numServerGrowthCyclesAdjusted = numServerGrowthCycles * serverGrowthPercentage * BitNodeMultipliers.ServerGrowthRate; - - //Apply serverGrowth for the calculated number of growth cycles - var serverGrowth = Math.pow(adjGrowthRate, numServerGrowthCyclesAdjusted * Player.hacking_grow_mult); - if (serverGrowth < 1) { - console.log("WARN: serverGrowth calculated to be less than 1"); - serverGrowth = 1; - } - - const oldMoneyAvailable = server.moneyAvailable; - server.moneyAvailable *= serverGrowth; - - // in case of data corruption - if (server.moneyMax && isNaN(server.moneyAvailable)) { - server.moneyAvailable = server.moneyMax; - } - - // cap at max - if (server.moneyMax && server.moneyAvailable > server.moneyMax) { - server.moneyAvailable = server.moneyMax; - } - - // if there was any growth at all, increase security - if (oldMoneyAvailable !== server.moneyAvailable) { - //Growing increases server security twice as much as hacking - let usedCycles = numCycleForGrowth(server, server.moneyAvailable / oldMoneyAvailable); - usedCycles = Math.max(0, usedCycles); - server.fortify(2 * CONSTANTS.ServerFortifyAmount * Math.ceil(usedCycles)); - } - return server.moneyAvailable / oldMoneyAvailable; -} - -export function prestigeHomeComputer(homeComp) { - const hasBitflume = homeComp.programs.includes(Programs.BitFlume.name); - - homeComp.programs.length = 0; //Remove programs - homeComp.runningScripts = []; - homeComp.serversOnNetwork = []; - homeComp.isConnectedTo = true; - homeComp.ramUsed = 0; - homeComp.programs.push(Programs.NukeProgram.name); - if (hasBitflume) { homeComp.programs.push(Programs.BitFlume.name); } - - //Update RAM usage on all scripts - homeComp.scripts.forEach(function(script) { - script.updateRamUsage(); - }); - - homeComp.messages.length = 0; //Remove .lit and .msg files - homeComp.messages.push("hackers-starting-handbook.lit"); -} - -//List of all servers that exist in the game, indexed by their ip -let AllServers = {}; - -export function prestigeAllServers() { - for (var member in AllServers) { - delete AllServers[member]; - } - AllServers = {}; -} - -export function loadAllServers(saveString) { - AllServers = JSON.parse(saveString, Reviver); -} - -function SizeOfAllServers() { - var size = 0, key; - for (key in AllServers) { - if (AllServers.hasOwnProperty(key)) size++; - } - return size; -} - -//Add a server onto the map of all servers in the game -export function AddToAllServers(server) { - var serverIp = server.ip; - if (ipExists(serverIp)) { - console.log("IP of server that's being added: " + serverIp); - console.log("Hostname of the server thats being added: " + server.hostname); - console.log("The server that already has this IP is: " + AllServers[serverIp].hostname); - throw new Error("Error: Trying to add a server with an existing IP"); - return; - } - AllServers[serverIp] = server; -} - -//Returns server object with corresponding hostname -// Relatively slow, would rather not use this a lot -export function GetServerByHostname(hostname) { - for (var ip in AllServers) { - if (AllServers.hasOwnProperty(ip)) { - if (AllServers[ip].hostname == hostname) { - return AllServers[ip]; - } - } - } - return null; -} - -//Get server by IP or hostname. Returns null if invalid -export function getServer(s) { - if (!isValidIPAddress(s)) { - return GetServerByHostname(s); - } - if(AllServers[s] !== undefined) { - return AllServers[s]; - } - return null; -} - -//Debugging tool -function PrintAllServers() { - for (var ip in AllServers) { - if (AllServers.hasOwnProperty(ip)) { - console.log("Ip: " + ip + ", hostname: " + AllServers[ip].hostname); - } - } -} - -// Directory object (folders) -function Directory(server, parent, name) { - this.s = server; //Ref to server - this.p = parent; //Ref to parent directory - this.c = []; //Subdirs - this.n = name; - this.d = parent.d + 1; //We'll only have a maximum depth of 3 or something - this.scrs = []; //Holds references to the scripts in server.scripts - this.pgms = []; - this.msgs = []; -} - -Directory.prototype.createSubdir = function(name) { - var subdir = new Directory(this.s, this, name); - -} - -Directory.prototype.getPath = function(name) { - var res = []; - var i = this; - while (i !== null) { - res.unshift(i.n, "/"); - i = i.parent; - } - res.unshift("/"); - return res.join(""); -} - -export {Server, AllServers}; diff --git a/src/Server/AllServers.ts b/src/Server/AllServers.ts new file mode 100644 index 000000000..6d8d91c0b --- /dev/null +++ b/src/Server/AllServers.ts @@ -0,0 +1,132 @@ +import { Server } from "./Server"; +import { SpecialServerIps } from "./SpecialServerIps"; +import { serverMetadata } from "./data/servers"; + +import { IMap } from "../types"; +import { createRandomIp, + ipExists } from "../../utils/IPAddress"; +import { getRandomInt } from "../../utils/helpers/getRandomInt"; +import { Reviver } from "../../utils/JSONReviver"; + +// Map of all Servers that exist in the game +// Key (string) = IP +// Value = Server object +export let AllServers: IMap = {}; + +// Saftely add a Server to the AllServers map +export function AddToAllServers(server: Server): void { + var serverIp = server.ip; + if (ipExists(serverIp)) { + console.log("IP of server that's being added: " + serverIp); + console.log("Hostname of the server thats being added: " + server.hostname); + console.log("The server that already has this IP is: " + AllServers[serverIp].hostname); + throw new Error("Error: Trying to add a server with an existing IP"); + } + AllServers[serverIp] = server; +} + +interface IServerParams { + hackDifficulty?: number; + hostname: string; + ip: string; + maxRam?: number; + moneyAvailable?: number; + numOpenPortsRequired: number; + organizationName: string; + requiredHackingSkill?: number; + serverGrowth?: number; + + [key: string]: any; +} + +export function initForeignServers(homeComputer: Server) { + /* Create a randomized network for all the foreign servers */ + //Groupings for creating a randomized network + const networkLayers: Server[][] = []; + for (let i = 0; i < 15; i++) { + networkLayers.push([]); + } + + // Essentially any property that is of type 'number | IMinMaxRange' + const propertiesToPatternMatch: string[] = [ + "hackDifficulty", + "moneyAvailable", + "requiredHackingSkill", + "serverGrowth" + ]; + + const toNumber = (value: any) => { + switch (typeof value) { + case 'number': + return value; + case 'object': + return getRandomInt(value.min, value.max); + default: + throw Error(`Do not know how to convert the type '${typeof value}' to a number`); + } + } + + for (const metadata of serverMetadata) { + const serverParams: IServerParams = { + hostname: metadata.hostname, + ip: createRandomIp(), + numOpenPortsRequired: metadata.numOpenPortsRequired, + organizationName: metadata.organizationName + }; + + if (metadata.maxRamExponent !== undefined) { + serverParams.maxRam = Math.pow(2, toNumber(metadata.maxRamExponent)); + } + + for (const prop of propertiesToPatternMatch) { + if (metadata[prop] !== undefined) { + serverParams[prop] = toNumber(metadata[prop]); + } + } + + const server = new Server(serverParams); + for (const filename of (metadata.literature || [])) { + server.messages.push(filename); + } + + if (metadata.specialName !== undefined) { + SpecialServerIps.addIp(metadata.specialName, server.ip); + } + + AddToAllServers(server); + if (metadata.networkLayer !== undefined) { + networkLayers[toNumber(metadata.networkLayer) - 1].push(server); + } + } + + /* Create a randomized network for all the foreign servers */ + const linkComputers = (server1: Server, server2: Server) => { + server1.serversOnNetwork.push(server2.ip); + server2.serversOnNetwork.push(server1.ip); + }; + + const getRandomArrayItem = (arr: any[]) => arr[Math.floor(Math.random() * arr.length)]; + + const linkNetworkLayers = (network1: Server[], selectServer: () => Server) => { + for (const server of network1) { + linkComputers(server, selectServer()); + } + }; + + // Connect the first tier of servers to the player's home computer + linkNetworkLayers(networkLayers[0], () => homeComputer); + for (let i = 1; i < networkLayers.length; i++) { + linkNetworkLayers(networkLayers[i], () => getRandomArrayItem(networkLayers[i - 1])); + } +} + +export function prestigeAllServers() { + for (var member in AllServers) { + delete AllServers[member]; + } + AllServers = {}; +} + +export function loadAllServers(saveString: string) { + AllServers = JSON.parse(saveString, Reviver); +} diff --git a/src/Server/Server.ts b/src/Server/Server.ts new file mode 100644 index 000000000..bc3056d14 --- /dev/null +++ b/src/Server/Server.ts @@ -0,0 +1,303 @@ +// Class representing a single generic Server + +// TODO This import is a circular import. Try to fix it in the future +import { GetServerByHostname } from "./ServerHelpers"; + +import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; +import { CodingContract } from "../CodingContracts"; +import { Message } from "../Message/Message"; +import { RunningScript } from "../Script/RunningScript"; +import { Script } from "../Script/Script"; +import { isScriptFilename } from "../Script/ScriptHelpersTS"; +import { TextFile } from "../TextFile"; + +import { createRandomIp } from "../../utils/IPAddress"; +import { Generic_fromJSON, + Generic_toJSON, + Reviver } from "../../utils/JSONReviver"; + +interface IConstructorParams { + adminRights?: boolean; + hackDifficulty?: number; + hostname: string; + ip?: string; + isConnectedTo?: boolean; + maxRam?: number; + moneyAvailable?: number; + numOpenPortsRequired?: number; + organizationName?: string; + purchasedByPlayer?: boolean; + requiredHackingSkill?: number; + serverGrowth?: number; +} + +export class Server { + // Initializes a Server Object from a JSON save state + static fromJSON(value: any): Server { + return Generic_fromJSON(Server, value.data); + } + + // Initial server security level + // (i.e. security level when the server was created) + baseDifficulty: number = 1; + + // Coding Contract files on this server + contracts: CodingContract[] = []; + + // How many CPU cores this server has. Maximum of 8. + // Currently, this only affects hacking missions + cpuCores: number = 1; + + // Flag indicating whether the FTP port is open + ftpPortOpen: boolean = false; + + // Server Security Level + hackDifficulty: number = 1; + + // Flag indicating whether player has admin/root access to this server + hasAdminRights: boolean = false; + + // Hostname. Must be unique + hostname: string = ""; + + // Flag indicating whether HTTP Port is open + httpPortOpen: boolean = false; + + // IP Address. Must be unique + ip: string = ""; + + // Flag indicating whether player is curently connected to this server + isConnectedTo: boolean = false; + + // Flag indicating whether this server has been manually hacked (ie. + // hacked through Terminal) by the player + manuallyHacked: boolean = false; + + // RAM (GB) available on this server + maxRam: number = 0; + + // Message files AND Literature files on this Server + // For Literature files, this array contains only the filename (string) + // For Messages, it contains the actual Message object + // TODO Separate literature files into its own property + messages: (Message | string)[] = []; + + // Minimum server security level that this server can be weakened to + minDifficulty: number = 1; + + // How much money currently resides on the server and can be hacked + moneyAvailable: number = 0; + + // Maximum amount of money that this server can hold + moneyMax: number = 0; + + // Number of open ports required in order to gain admin/root access + numOpenPortsRequired: number = 5; + + // How many ports are currently opened on the server + openPortCount: number = 0; + + // Name of company/faction/etc. that this server belongs to. + // Optional, not applicable to all Servers + organizationName: string = ""; + + // Programs on this servers. Contains only the names of the programs + programs: string[] = []; + + // Flag indicating wehther this is a purchased server + purchasedByPlayer: boolean = false; + + // RAM (GB) used. i.e. unavailable RAM + ramUsed: number = 0; + + // Hacking level required to hack this server + requiredHackingSkill: number = 1; + + // RunningScript files on this server + runningScripts: RunningScript[] = []; + + // Script files on this Server + scripts: Script[] = []; + + // Parameter that affects how effectively this server's money can + // be increased using the grow() Netscript function + serverGrowth: number = 1; + + // Contains the IP Addresses of all servers that are immediately + // reachable from this one + serversOnNetwork: string[] = []; + + // Flag indicating whether SMTP Port is open + smtpPortOpen: boolean = false; + + // Flag indicating whether SQL Port is open + sqlPortOpen: boolean = false; + + // Flag indicating whether the SSH Port is open + sshPortOpen: boolean = false; + + // Text files on this server + textFiles: TextFile[] = []; + + constructor(params: IConstructorParams={hostname: "", ip: createRandomIp() }) { + /* Properties */ + //Connection information + this.ip = params.ip ? params.ip : createRandomIp(); + + var hostname = params.hostname; + var i = 0; + var suffix = ""; + while (GetServerByHostname(hostname+suffix) != null) { + //Server already exists + suffix = "-" + i; + ++i; + } + this.hostname = hostname + suffix; + this.organizationName = params.organizationName != null ? params.organizationName : ""; + this.isConnectedTo = params.isConnectedTo != null ? params.isConnectedTo : false; + + //Access information + this.hasAdminRights = params.adminRights != null ? params.adminRights : false; + this.purchasedByPlayer = params.purchasedByPlayer != null ? params.purchasedByPlayer : false; + + //RAM, CPU speed and Scripts + this.maxRam = params.maxRam != null ? params.maxRam : 0; //GB + + /* Hacking information (only valid for "foreign" aka non-purchased servers) */ + this.requiredHackingSkill = params.requiredHackingSkill != null ? params.requiredHackingSkill : 1; + this.moneyAvailable = params.moneyAvailable != null ? params.moneyAvailable * BitNodeMultipliers.ServerStartingMoney : 0; + this.moneyMax = 25 * this.moneyAvailable * BitNodeMultipliers.ServerMaxMoney; + + //Hack Difficulty is synonymous with server security. Base Difficulty = Starting difficulty + this.hackDifficulty = params.hackDifficulty != null ? params.hackDifficulty * BitNodeMultipliers.ServerStartingSecurity : 1; + this.baseDifficulty = this.hackDifficulty; + this.minDifficulty = Math.max(1, Math.round(this.hackDifficulty / 3)); + this.serverGrowth = params.serverGrowth != null ? params.serverGrowth : 1; //Integer from 0 to 100. Affects money increase from grow() + + //Port information, required for porthacking servers to get admin rights + this.numOpenPortsRequired = params.numOpenPortsRequired != null ? params.numOpenPortsRequired : 5; + }; + + setMaxRam(ram: number): void { + this.maxRam = ram; + } + + // Given the name of the script, returns the corresponding + // script object on the server (if it exists) + getScript(scriptName: string): Script | null { + for (let i = 0; i < this.scripts.length; i++) { + if (this.scripts[i].filename === scriptName) { + return this.scripts[i]; + } + } + + return null; + } + + // Ensures that the server's difficulty (server security) doesn't get too high + capDifficulty(): void { + if (this.hackDifficulty < this.minDifficulty) {this.hackDifficulty = this.minDifficulty;} + if (this.hackDifficulty < 1) {this.hackDifficulty = 1;} + + // Place some arbitrarily limit that realistically should never happen unless someone is + // screwing around with the game + if (this.hackDifficulty > 1000000) {this.hackDifficulty = 1000000;} + } + + // Strengthens a server's security level (difficulty) by the specified amount + fortify(amt: number): void { + this.hackDifficulty += amt; + this.capDifficulty(); + } + + // Lowers the server's security level (difficulty) by the specified amount) + weaken(amt: number): void { + this.hackDifficulty -= (amt * BitNodeMultipliers.ServerWeakenRate); + this.capDifficulty(); + } + + // Write to a script file + // Overwrites existing files. Creates new files if the script does not eixst + writeToScriptFile(fn: string, code: string) { + var ret = {success: false, overwritten: false}; + if (!isScriptFilename(fn)) { return ret; } + + //Check if the script already exists, and overwrite it if it does + for (let i = 0; i < this.scripts.length; ++i) { + if (fn === this.scripts[i].filename) { + let script = this.scripts[i]; + script.code = code; + script.updateRamUsage(); + script.module = ""; + ret.overwritten = true; + ret.success = true; + return ret; + } + } + + //Otherwise, create a new script + const newScript = new Script(); + newScript.filename = fn; + newScript.code = code; + newScript.updateRamUsage(); + newScript.server = this.ip; + this.scripts.push(newScript); + ret.success = true; + return ret; + } + + // Write to a text file + // Overwrites existing files. Creates new files if the text file does not exist + writeToTextFile(fn: string, txt: string) { + var ret = { success: false, overwritten: false }; + if (!fn.endsWith("txt")) { return ret; } + + //Check if the text file already exists, and overwrite if it does + for (let i = 0; i < this.textFiles.length; ++i) { + if (this.textFiles[i].fn === fn) { + ret.overwritten = true; + this.textFiles[i].text = txt; + ret.success = true; + return ret; + } + } + + //Otherwise create a new text file + var newFile = new TextFile(fn, txt); + this.textFiles.push(newFile); + ret.success = true; + return ret; + } + + addContract(contract: CodingContract) { + this.contracts.push(contract); + } + + removeContract(contract: CodingContract) { + if (contract instanceof CodingContract) { + this.contracts = this.contracts.filter((c) => { + return c.fn !== contract.fn; + }); + } else { + this.contracts = this.contracts.filter((c) => { + return c.fn !== contract; + }); + } + } + + getContract(contractName: string) { + for (const contract of this.contracts) { + if (contract.fn === contractName) { + return contract; + } + } + return null; + } + + // Serialize the current object to a JSON save state + toJSON(): any { + return Generic_toJSON("Server", this); + } +} + +Reviver.constructors.Server = Server; diff --git a/src/Server/ServerHelpers.ts b/src/Server/ServerHelpers.ts new file mode 100644 index 000000000..e443deb0c --- /dev/null +++ b/src/Server/ServerHelpers.ts @@ -0,0 +1,125 @@ +import { AllServers } from "./AllServers"; +import { Server } from "./Server"; + +import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; +import { CONSTANTS } from "../Constants"; +import { IPlayer } from "../PersonObjects/IPlayer"; +import { Programs } from "../Programs/Programs"; + +import {isValidIPAddress} from "../../utils/helpers/isValidIPAddress"; + +// Returns the number of cycles needed to grow the specified server by the +// specified amount. 'growth' parameter is in decimal form, not percentage +export function numCycleForGrowth(server: Server, growth: number, p: IPlayer) { + let ajdGrowthRate = 1 + (CONSTANTS.ServerBaseGrowthRate - 1) / server.hackDifficulty; + if(ajdGrowthRate > CONSTANTS.ServerMaxGrowthRate) { + ajdGrowthRate = CONSTANTS.ServerMaxGrowthRate; + } + + const serverGrowthPercentage = server.serverGrowth / 100; + + const cycles = Math.log(growth)/(Math.log(ajdGrowthRate) * p.hacking_grow_mult * serverGrowthPercentage); + return cycles; +} + +//Applied server growth for a single server. Returns the percentage growth +export function processSingleServerGrowth(server: Server, numCycles: number, p: IPlayer) { + //Server growth processed once every 450 game cycles + const numServerGrowthCycles = Math.max(Math.floor(numCycles / 450), 0); + + //Get adjusted growth rate, which accounts for server security + const growthRate = CONSTANTS.ServerBaseGrowthRate; + var adjGrowthRate = 1 + (growthRate - 1) / server.hackDifficulty; + if (adjGrowthRate > CONSTANTS.ServerMaxGrowthRate) {adjGrowthRate = CONSTANTS.ServerMaxGrowthRate;} + + //Calculate adjusted server growth rate based on parameters + const serverGrowthPercentage = server.serverGrowth / 100; + const numServerGrowthCyclesAdjusted = numServerGrowthCycles * serverGrowthPercentage * BitNodeMultipliers.ServerGrowthRate; + + //Apply serverGrowth for the calculated number of growth cycles + let serverGrowth = Math.pow(adjGrowthRate, numServerGrowthCyclesAdjusted * p.hacking_grow_mult); + if (serverGrowth < 1) { + console.log("WARN: serverGrowth calculated to be less than 1"); + serverGrowth = 1; + } + + const oldMoneyAvailable = server.moneyAvailable; + server.moneyAvailable *= serverGrowth; + + // in case of data corruption + if (server.moneyMax && isNaN(server.moneyAvailable)) { + server.moneyAvailable = server.moneyMax; + } + + // cap at max + if (server.moneyMax && server.moneyAvailable > server.moneyMax) { + server.moneyAvailable = server.moneyMax; + } + + // if there was any growth at all, increase security + if (oldMoneyAvailable !== server.moneyAvailable) { + //Growing increases server security twice as much as hacking + let usedCycles = numCycleForGrowth(server, server.moneyAvailable / oldMoneyAvailable, p); + usedCycles = Math.max(0, usedCycles); + server.fortify(2 * CONSTANTS.ServerFortifyAmount * Math.ceil(usedCycles)); + } + return server.moneyAvailable / oldMoneyAvailable; +} + +export function prestigeHomeComputer(homeComp: Server) { + const hasBitflume = homeComp.programs.includes(Programs.BitFlume.name); + + homeComp.programs.length = 0; //Remove programs + homeComp.runningScripts = []; + homeComp.serversOnNetwork = []; + homeComp.isConnectedTo = true; + homeComp.ramUsed = 0; + homeComp.programs.push(Programs.NukeProgram.name); + if (hasBitflume) { homeComp.programs.push(Programs.BitFlume.name); } + + //Update RAM usage on all scripts + homeComp.scripts.forEach(function(script) { + script.updateRamUsage(); + }); + + homeComp.messages.length = 0; //Remove .lit and .msg files + homeComp.messages.push("hackers-starting-handbook.lit"); +} + +//Returns server object with corresponding hostname +// Relatively slow, would rather not use this a lot +export function GetServerByHostname(hostname: string): Server | null { + for (var ip in AllServers) { + if (AllServers.hasOwnProperty(ip)) { + if (AllServers[ip].hostname == hostname) { + return AllServers[ip]; + } + } + } + + return null; +} + +//Get server by IP or hostname. Returns null if invalid +export function getServer(s: string): Server | null { + if (!isValidIPAddress(s)) { + return GetServerByHostname(s); + } + if (AllServers[s] !== undefined) { + return AllServers[s]; + } + + return null; +} + +// Returns the i-th server on the specified server's network +// A Server's serverOnNetwork property holds only the IPs. This function returns +// the actual Server object +export function getServerOnNetwork(server: Server, i: number) { + if (i > server.serversOnNetwork.length) { + console.error("Tried to get server on network that was out of range"); + return; + } + + return AllServers[server.serversOnNetwork[i]]; +} diff --git a/src/ServerPurchases.js b/src/Server/ServerPurchases.js similarity index 82% rename from src/ServerPurchases.js rename to src/Server/ServerPurchases.js index 00535b997..7805206f3 100644 --- a/src/ServerPurchases.js +++ b/src/Server/ServerPurchases.js @@ -2,16 +2,16 @@ * Implements functions for purchasing servers or purchasing more RAM for * the home computer */ -import { BitNodeMultipliers } from "./BitNode/BitNodeMultipliers"; -import { CONSTANTS } from "./Constants"; -import { Player } from "./Player"; -import { Server, - AllServers, - AddToAllServers} from "./Server"; -import { dialogBoxCreate } from "../utils/DialogBox"; -import { createRandomIp } from "../utils/IPAddress"; -import { yesNoTxtInpBoxGetInput } from "../utils/YesNoBox"; -import { isPowerOfTwo } from "../utils/helpers/isPowerOfTwo"; +import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; +import { CONSTANTS } from "../Constants"; +import { Player } from "../Player"; +import { AllServers, + AddToAllServers } from "../Server/AllServers"; +import { Server } from "../Server/Server"; +import { dialogBoxCreate } from "../../utils/DialogBox"; +import { createRandomIp } from "../../utils/IPAddress"; +import { yesNoTxtInpBoxGetInput } from "../../utils/YesNoBox"; +import { isPowerOfTwo } from "../../utils/helpers/isPowerOfTwo"; // Returns the cost of purchasing a server with the given RAM // Returns Infinity for invalid 'ram' arguments diff --git a/src/Server/SpecialServerIps.ts b/src/Server/SpecialServerIps.ts new file mode 100644 index 000000000..121c377cc --- /dev/null +++ b/src/Server/SpecialServerIps.ts @@ -0,0 +1,56 @@ +import { IMap } from "../types"; +import { Reviver, + Generic_toJSON, + Generic_fromJSON } from "../../utils/JSONReviver"; + +/* Holds IP of Special Servers */ +export let SpecialServerNames: IMap = { + FulcrumSecretTechnologies: "Fulcrum Secret Technologies Server", + CyberSecServer: "CyberSec Server", + NiteSecServer: "NiteSec Server", + TheBlackHandServer: "The Black Hand Server", + BitRunnersServer: "BitRunners Server", + TheDarkArmyServer: "The Dark Army Server", + DaedalusServer: "Daedalus Server", + WorldDaemon: "w0r1d_d43m0n", +} + +export class SpecialServerIpsMap { + // Initializes a SpecialServerIpsMap Object from a JSON save state + static fromJSON(value: any): SpecialServerIpsMap { + return Generic_fromJSON(SpecialServerIpsMap, value.data); + } + + [key: string]: Function | string; + + constructor() {} + + addIp(name:string, ip: string) { + this[name] = ip; + } + + // Serialize the current object to a JSON save state + toJSON(): any { + return Generic_toJSON("SpecialServerIpsMap", this); + } +} + +Reviver.constructors.SpecialServerIpsMap = SpecialServerIpsMap; + +export let SpecialServerIps: SpecialServerIpsMap = new SpecialServerIpsMap(); + +export function prestigeSpecialServerIps() { + for (var member in SpecialServerIps) { + delete SpecialServerIps[member]; + } + + SpecialServerIps = new SpecialServerIpsMap(); +} + +export function loadSpecialServerIps(saveString: string) { + SpecialServerIps = JSON.parse(saveString, Reviver); +} + +export function initSpecialServerIps() { + SpecialServerIps = new SpecialServerIpsMap(); +} diff --git a/src/data/servers.ts b/src/Server/data/servers.ts similarity index 99% rename from src/data/servers.ts rename to src/Server/data/servers.ts index 12698a3b8..ec661c458 100644 --- a/src/data/servers.ts +++ b/src/Server/data/servers.ts @@ -81,6 +81,8 @@ interface IServerMetadata { * A "unique" server that has special implications when the player manually hacks it. */ specialName?: string; + + [key: string]: any; } /** diff --git a/src/SpecialServerIps.js b/src/SpecialServerIps.js deleted file mode 100644 index d3175dc01..000000000 --- a/src/SpecialServerIps.js +++ /dev/null @@ -1,50 +0,0 @@ -import {Reviver, Generic_toJSON, - Generic_fromJSON} from "../utils/JSONReviver"; - -/* Holds IP of Special Servers */ -let SpecialServerNames = { - FulcrumSecretTechnologies: "Fulcrum Secret Technologies Server", - CyberSecServer: "CyberSec Server", - NiteSecServer: "NiteSec Server", - TheBlackHandServer: "The Black Hand Server", - BitRunnersServer: "BitRunners Server", - TheDarkArmyServer: "The Dark Army Server", - DaedalusServer: "Daedalus Server", - WorldDaemon: "w0r1d_d43m0n", -} -function SpecialServerIpsMap() {} - -SpecialServerIpsMap.prototype.addIp = function(name, ip) { - this[name] = ip; -} - -SpecialServerIpsMap.prototype.toJSON = function() { - return Generic_toJSON("SpecialServerIpsMap", this); -} - -SpecialServerIpsMap.fromJSON = function(value) { - return Generic_fromJSON(SpecialServerIpsMap, value.data); -} - -Reviver.constructors.SpecialServerIpsMap = SpecialServerIpsMap; - -let SpecialServerIps = new SpecialServerIpsMap(); - -function prestigeSpecialServerIps() { - for (var member in SpecialServerIps) { - delete SpecialServerIps[member]; - } - SpecialServerIps = null; - SpecialServerIps = new SpecialServerIpsMap(); -} - -function loadSpecialServerIps(saveString) { - SpecialServerIps = JSON.parse(saveString, Reviver); -} - -function initSpecialServerIps() { - SpecialServerIps = new SpecialServerIpsMap(); -} - -export {SpecialServerNames, SpecialServerIps, SpecialServerIpsMap, loadSpecialServerIps, - prestigeSpecialServerIps, initSpecialServerIps}; diff --git a/src/Terminal.js b/src/Terminal.js index bb06b6c25..d2744b2c9 100644 --- a/src/Terminal.js +++ b/src/Terminal.js @@ -10,8 +10,9 @@ import { executeDarkwebTerminalCommand, checkIfConnectedToDarkweb } from "./DarkWeb/DarkWeb"; import { DarkWebItems } from "./DarkWeb/DarkWebItems"; import {Engine} from "./engine"; -import {FconfSettings, parseFconfSettings, - createFconf} from "./Fconf"; +import { parseFconfSettings, + createFconf } from "./Fconf/Fconf"; +import { FconfSettings } from "./Fconf/FconfSettings"; import {calculateHackingChance, calculateHackingExpGain, calculatePercentMoneyHacked, @@ -22,18 +23,22 @@ import {TerminalHelpText, HelpTexts} from "./HelpText"; import {iTutorialNextStep, iTutorialSteps, ITutorial} from "./InteractiveTutorial"; import {showLiterature} from "./Literature"; -import {showMessage, Message} from "./Message"; +import { Message } from "./Message/Message"; +import { showMessage } from "./Message/MessageHelpers"; import {killWorkerScript, addWorkerScript} from "./NetscriptWorker"; import {Player} from "./Player"; import {hackWorldDaemon} from "./RedPill"; -import { findRunningScript, - RunningScript, - isScriptFilename } from "./Script"; -import {AllServers, GetServerByHostname, - getServer, Server} from "./Server"; +import { RunningScript } from "./Script/RunningScript"; +import { findRunningScript } from "./Script/ScriptHelpers"; +import { isScriptFilename } from "./Script/ScriptHelpersTS"; +import { AllServers } from "./Server/AllServers"; +import { Server } from "./Server/Server"; +import { GetServerByHostname, + getServer, + getServerOnNetwork } from "./Server/ServerHelpers"; import {Settings} from "./Settings/Settings"; -import {SpecialServerIps, - SpecialServerNames} from "./SpecialServerIps"; +import { SpecialServerIps, + SpecialServerNames } from "./Server/SpecialServerIps"; import {getTextFile} from "./TextFile"; import { setTimeoutRef } from "./utils/SetTimeoutRef"; import {containsAllStrings, @@ -1157,8 +1162,8 @@ let Terminal = { let ip = commandArray[1]; - for (var i = 0; i < Player.getCurrentServer().serversOnNetwork.length; i++) { - if (Player.getCurrentServer().getServerOnNetwork(i).ip == ip || Player.getCurrentServer().getServerOnNetwork(i).hostname == ip) { + for (var i = 0; i < s.serversOnNetwork.length; i++) { + if (getServerOnNetwork(s, i).ip == ip || getServerOnNetwork(s, i).hostname == ip) { Terminal.connectToServer(ip); return; } @@ -1812,11 +1817,13 @@ let Terminal = { postError("Incorrect usage of netstat/scan command. Usage: netstat/scan"); return; } - //Displays available network connections using TCP + + // Displays available network connections using TCP + const currServ = Player.getCurrentServer(); post("Hostname IP Root Access"); - for (let i = 0; i < Player.getCurrentServer().serversOnNetwork.length; i++) { + for (let i = 0; i < currServ.serversOnNetwork.length; i++) { //Add hostname - let entry = Player.getCurrentServer().getServerOnNetwork(i); + let entry = getServerOnNetwork(currServ, i); if (entry == null) { continue; } entry = entry.hostname; @@ -1824,16 +1831,16 @@ let Terminal = { let numSpaces = 21 - entry.length; let spaces = Array(numSpaces+1).join(" "); entry += spaces; - entry += Player.getCurrentServer().getServerOnNetwork(i).ip; + entry += getServerOnNetwork(currServ, i).ip; //Calculate padding and add root access info let hasRoot; - if (Player.getCurrentServer().getServerOnNetwork(i).hasAdminRights) { + if (getServerOnNetwork(currServ, i).hasAdminRights) { hasRoot = 'Y'; } else { hasRoot = 'N'; } - numSpaces = 21 - Player.getCurrentServer().getServerOnNetwork(i).ip.length; + numSpaces = 21 - getServerOnNetwork(currServ, i).ip.length; spaces = Array(numSpaces+1).join(" "); entry += spaces; entry += hasRoot; @@ -1867,7 +1874,7 @@ let Terminal = { visited[s.ip] = 1; } for (var i = s.serversOnNetwork.length-1; i >= 0; --i) { - stack.push(s.getServerOnNetwork(i)); + stack.push(getServerOnNetwork(s, i)); depthQueue.push(d+1); } if (d == 0) {continue;} //Don't print current server diff --git a/src/engine.js b/src/engine.js index bbf24a9f9..de21680f4 100644 --- a/src/engine.js +++ b/src/engine.js @@ -21,14 +21,12 @@ import {CompanyPositions} from "./Company/CompanyP import {initCompanies} from "./Company/Companies"; import { Corporation } from "./Corporation/Corporation"; import {CONSTANTS} from "./Constants"; - - import {createDevMenu, closeDevMenu} from "./DevMenu"; import { Factions, initFactions } from "./Faction/Factions"; import { displayFactionContent, joinFaction, processPassiveFactionRepGain, inviteToFaction } from "./Faction/FactionHelpers"; -import {FconfSettings} from "./Fconf"; +import { FconfSettings } from "./Fconf/FconfSettings"; import {displayLocationContent, initLocationButtons} from "./Location"; import {Locations} from "./Locations"; @@ -36,7 +34,7 @@ import {displayHacknetNodesContent, processAllHacknetNodeEarnings, updateHacknetNodesContent} from "./HacknetNode"; import {iTutorialStart} from "./InteractiveTutorial"; import {initLiterature} from "./Literature"; -import {checkForMessagesToSend, initMessages} from "./Message"; +import { checkForMessagesToSend, initMessages } from "./Message/MessageHelpers"; import {inMission, currMission} from "./Missions"; import {initSingularitySFFlags, hasSingularitySF, hasCorporationSF} from "./NetscriptFunctions"; @@ -54,13 +52,14 @@ import {saveObject, loadGame} from "./SaveObject"; import { getCurrentEditor, loadAllRunningScripts, scriptEditorInit, - updateScriptEditorContent } from "./Script"; -import {AllServers, Server, initForeignServers} from "./Server"; + updateScriptEditorContent } from "./Script/ScriptHelpers"; +import { AllServers } from "./Server/AllServers"; +import { Server } from "./Server/Server"; +import { initForeignServers } from "./Server/ServerHelpers"; import {Settings} from "./Settings/Settings"; import { initSourceFiles, SourceFiles } from "./SourceFile"; import { updateSourceFileFlags } from "./SourceFile/SourceFileFlags"; - -import {SpecialServerIps, initSpecialServerIps} from "./SpecialServerIps"; +import {SpecialServerIps, initSpecialServerIps} from "./Server/SpecialServerIps"; import {StockMarket, StockSymbols, SymbolToStockMap, initStockSymbols, initSymbolToStockMap, stockMarketCycle, @@ -1271,7 +1270,7 @@ const Engine = { Engine.setDisplayElements(); //Sets variables for important DOM elements Engine.start(); //Run main game loop and Scripts loop Player.init(); - initForeignServers(); + initForeignServers(Player.getHomeComputer()); initCompanies(); initFactions(); initAugmentations(); diff --git a/utils/IPAddress.js b/utils/IPAddress.js deleted file mode 100644 index 44fa97156..000000000 --- a/utils/IPAddress.js +++ /dev/null @@ -1,33 +0,0 @@ -import {AllServers} from "../src/Server"; -import {getRandomByte} from "./helpers/getRandomByte"; - -/* Functions to deal with manipulating IP addresses*/ - -//Generate a random IP address -//Will not return an IP address that already exists in the AllServers array -function createRandomIp() { - var ip = getRandomByte(99) + '.' + - getRandomByte(9) + '.' + - getRandomByte(9) + '.' + - getRandomByte(9); - - //If the Ip already exists, recurse to create a new one - if (ipExists(ip)) { - return createRandomIp(); - } - return ip; -} - -//Returns true if the IP already exists in one of the game's servers -function ipExists(ip) { - for (var property in AllServers) { - if (AllServers.hasOwnProperty(property)) { - if (property == ip) { - return true; - } - } - } - return false; -} - -export {createRandomIp, ipExists}; diff --git a/utils/IPAddress.ts b/utils/IPAddress.ts new file mode 100644 index 000000000..948b055eb --- /dev/null +++ b/utils/IPAddress.ts @@ -0,0 +1,25 @@ +import { AllServers } from "../src/Server/AllServers"; +import { getRandomByte } from "./helpers/getRandomByte"; + +/* Functions to deal with manipulating IP addresses*/ + +//Generate a random IP address +//Will not return an IP address that already exists in the AllServers array +export function createRandomIp(): string { + const ip: string = getRandomByte(99) + '.' + + getRandomByte(9) + '.' + + getRandomByte(9) + '.' + + getRandomByte(9); + + // If the Ip already exists, recurse to create a new one + if (ipExists(ip)) { + return createRandomIp(); + } + + return ip; +} + +// Returns true if the IP already exists in one of the game's servers +export function ipExists(ip: string) { + return (AllServers[ip] != null); +}