import { vsprintf, sprintf } from "sprintf-js";
import * as libarg from "arg";

import { getRamCost } from "./Netscript/RamCostGenerator";
import { WorkerScriptStartStopEventEmitter } from "./Netscript/WorkerScriptStartStopEventEmitter";

import { Augmentations } from "./Augmentation/Augmentations";
import { augmentationExists, installAugmentations } from "./Augmentation/AugmentationHelpers";
import { prestigeAugmentation } from "./Prestige";
import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
import { BitNodeMultipliers } from "./BitNode/BitNodeMultipliers";
import { findCrime } from "./Crime/CrimeHelpers";
import { Bladeburner } from "./Bladeburner/Bladeburner";
import { Company } from "./Company/Company";
import { Companies } from "./Company/Companies";
import { CompanyPosition } from "./Company/CompanyPosition";
import { CompanyPositions } from "./Company/CompanyPositions";
import { CONSTANTS } from "./Constants";
import { DarkWebItems } from "./DarkWeb/DarkWebItems";
import {
  NewIndustry,
  NewCity,
  UnlockUpgrade,
  LevelUpgrade,
  IssueDividends,
  SellMaterial,
  SellProduct,
  SetSmartSupply,
  BuyMaterial,
  AssignJob,
  UpgradeOfficeSize,
  ThrowParty,
  PurchaseWarehouse,
  UpgradeWarehouse,
  BuyCoffee,
  HireAdVert,
  MakeProduct,
  Research,
  ExportMaterial,
  CancelExportMaterial,
  SetMaterialMarketTA1,
  SetMaterialMarketTA2,
  SetProductMarketTA1,
  SetProductMarketTA2,
} from "./Corporation/Actions";
import { CorporationUnlockUpgrades } from "./Corporation/data/CorporationUnlockUpgrades";
import { CorporationUpgrades } from "./Corporation/data/CorporationUpgrades";
import {
  calculateHackingChance,
  calculateHackingExpGain,
  calculatePercentMoneyHacked,
  calculateHackingTime,
  calculateGrowTime,
  calculateWeakenTime,
} from "./Hacking";
import { calculateServerGrowth } from "./Server/formulas/grow";
import { Gang } from "./Gang/Gang";
import { AllGangs } from "./Gang/AllGangs";
import { GangMemberTasks } from "./Gang/GangMemberTasks";
import { GangMemberUpgrades } from "./Gang/GangMemberUpgrades";
import { Factions, factionExists } from "./Faction/Factions";
import { joinFaction, purchaseAugmentation } from "./Faction/FactionHelpers";
import { FactionWorkType } from "./Faction/FactionWorkTypeEnum";
import { netscriptCanGrow, netscriptCanHack, netscriptCanWeaken } from "./Hacking/netscriptCanHack";

import {
  getCostOfNextHacknetNode,
  getCostOfNextHacknetServer,
  hasHacknetServers,
  purchaseHacknet,
  purchaseLevelUpgrade,
  purchaseRamUpgrade,
  purchaseCoreUpgrade,
  purchaseCacheUpgrade,
  purchaseHashUpgrade,
  updateHashManagerCapacity,
} from "./Hacknet/HacknetHelpers";
import {
  calculateMoneyGainRate,
  calculateLevelUpgradeCost,
  calculateRamUpgradeCost,
  calculateCoreUpgradeCost,
  calculateNodeCost,
} from "./Hacknet/formulas/HacknetNodes";
import {
  calculateHashGainRate as HScalculateHashGainRate,
  calculateLevelUpgradeCost as HScalculateLevelUpgradeCost,
  calculateRamUpgradeCost as HScalculateRamUpgradeCost,
  calculateCoreUpgradeCost as HScalculateCoreUpgradeCost,
  calculateCacheUpgradeCost as HScalculateCacheUpgradeCost,
  calculateServerCost as HScalculateServerCost,
} from "./Hacknet/formulas/HacknetServers";
import { HacknetNodeConstants, HacknetServerConstants } from "./Hacknet/data/Constants";
import { HacknetServer } from "./Hacknet/HacknetServer";
import { CityName } from "./Locations/data/CityNames";
import { LocationName } from "./Locations/data/LocationNames";
import { Terminal } from "./Terminal";
import { calculateSkill, calculateExp } from "./PersonObjects/formulas/skill";

import { Message } from "./Message/Message";
import { inMission } from "./Missions";
import { Player } from "./Player";
import { Programs } from "./Programs/Programs";
import { Script } from "./Script/Script";
import { findRunningScript, findRunningScriptByPid } from "./Script/ScriptHelpers";
import { isScriptFilename } from "./Script/ScriptHelpersTS";
import { AllServers, AddToAllServers, createUniqueRandomIp } from "./Server/AllServers";
import { RunningScript } from "./Script/RunningScript";
import {
  GetServerByHostname,
  getServer,
  getServerOnNetwork,
  numCycleForGrowth,
  processSingleServerGrowth,
  safetlyCreateUniqueServer,
} from "./Server/ServerHelpers";
import { getPurchaseServerCost, getPurchaseServerLimit, getPurchaseServerMaxRam } from "./Server/ServerPurchases";
import { SpecialServerIps } from "./Server/SpecialServerIps";
import { SourceFileFlags } from "./SourceFile/SourceFileFlags";
import { buyStock, sellStock, shortStock, sellShort } from "./StockMarket/BuyingAndSelling";
import { influenceStockThroughServerHack, influenceStockThroughServerGrow } from "./StockMarket/PlayerInfluencing";
import {
  StockMarket,
  SymbolToStockMap,
  placeOrder,
  cancelOrder,
  displayStockMarketContent,
} from "./StockMarket/StockMarket";
import { getBuyTransactionCost, getSellTransactionGain } from "./StockMarket/StockMarketHelpers";
import { OrderTypes } from "./StockMarket/data/OrderTypes";
import { PositionTypes } from "./StockMarket/data/PositionTypes";
import { StockSymbols } from "./StockMarket/data/StockSymbols";
import { getStockMarket4SDataCost, getStockMarket4STixApiCost } from "./StockMarket/StockMarketCosts";
import { isValidFilePath, removeLeadingSlash } from "./Terminal/DirectoryHelpers";
import { TextFile, getTextFile, createTextFile } from "./TextFile";

import { NetscriptPorts, runScriptFromScript, startWorkerScript } from "./NetscriptWorker";
import { killWorkerScript } from "./Netscript/killWorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts";
import { makeRuntimeRejectMsg, netscriptDelay, resolveNetscriptRequestedThreads } from "./NetscriptEvaluator";
import { Interpreter } from "./JSInterpreter";
import { NetscriptPort } from "./NetscriptPort";
import { SleeveTaskType } from "./PersonObjects/Sleeve/SleeveTaskTypesEnum";
import { findSleevePurchasableAugs } from "./PersonObjects/Sleeve/SleeveHelpers";
import { Exploit } from "./Exploits/Exploit.ts";

import { numeralWrapper } from "./ui/numeralFormat";
import { post } from "./ui/postToTerminal";
import { setTimeoutRef } from "./utils/SetTimeoutRef";
import { is2DArray } from "./utils/helpers/is2DArray";
import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions";

import { logBoxCreate } from "../utils/LogBox";
import { arrayToString } from "../utils/helpers/arrayToString";
import { isString } from "../utils/helpers/isString";

import { createElement } from "../utils/uiHelpers/createElement";
import { createPopup } from "../utils/uiHelpers/createPopup";
import { removeElementById } from "../utils/uiHelpers/removeElementById";

const defaultInterpreter = new Interpreter("", () => undefined);

// the acorn interpreter has a bug where it doesn't convert arrays correctly.
// so we have to more or less copy it here.
function toNative(pseudoObj) {
  if (pseudoObj == null) return null;
  if (
    !pseudoObj.hasOwnProperty("properties") ||
    !pseudoObj.hasOwnProperty("getter") ||
    !pseudoObj.hasOwnProperty("setter") ||
    !pseudoObj.hasOwnProperty("proto")
  ) {
    return pseudoObj; // it wasn't a pseudo object anyway.
  }

  let nativeObj;
  if (pseudoObj.hasOwnProperty("class") && pseudoObj.class === "Array") {
    nativeObj = [];
    const length = defaultInterpreter.getProperty(pseudoObj, "length");
    for (let i = 0; i < length; i++) {
      if (defaultInterpreter.hasProperty(pseudoObj, i)) {
        nativeObj[i] = toNative(defaultInterpreter.getProperty(pseudoObj, i));
      }
    }
  } else {
    // Object.
    nativeObj = {};
    for (var key in pseudoObj.properties) {
      const val = pseudoObj.properties[key];
      nativeObj[key] = toNative(val);
    }
  }
  return nativeObj;
}

function NetscriptFunctions(workerScript) {
  const updateDynamicRam = function (fnName, ramCost) {
    if (workerScript.dynamicLoadedFns[fnName]) {
      return;
    }
    workerScript.dynamicLoadedFns[fnName] = true;

    let threads = workerScript.scriptRef.threads;
    if (typeof threads !== "number") {
      console.warn(`WorkerScript detected NaN for threadcount for ${workerScript.name} on ${workerScript.serverIp}`);
      threads = 1;
    }

    workerScript.dynamicRamUsage += ramCost * threads;
    if (workerScript.dynamicRamUsage > 1.01 * workerScript.ramUsage) {
      throw makeRuntimeRejectMsg(
        workerScript,
        `Dynamic RAM usage calculated to be greater than initial RAM usage on fn: ${fnName}.
        This is probably because you somehow circumvented the static RAM calculation.

        Dynamic RAM Usage: ${numeralWrapper.formatRAM(workerScript.dynamicRamUsage)}
        Static RAM Usage: ${numeralWrapper.formatRAM(workerScript.ramUsage)}

        One of these could be the reason:
        * Using eval() to get a reference to a ns function
        &nbsp;&nbsp;const scan = eval('ns.scan');

        * Using map access to do the same
        &nbsp;&nbsp;const scan = ns['scan'];

        * Saving script in the improper order.
        &nbsp;&nbsp;Increase the cost of an imported script, save it, then run the
        &nbsp;&nbsp;parent. To fix this just re-open & save every script in order
        &nbsp;&nbsp;from most imported to least imported (parent script).

        Sorry :(`,
      );
    }
  };

  /**
   * Gets the Server for a specific hostname/ip, throwing an error
   * if the server doesn't exist.
   * @param {string} ip - Hostname or IP of the server
   * @param {string} callingFnName - Name of calling function. For logging purposes
   * @returns {Server} The specified Server
   */
  const safeGetServer = function (ip, callingFnName = "") {
    var server = getServer(ip);
    if (server == null) {
      throw makeRuntimeErrorMsg(callingFnName, `Invalid IP/hostname: ${ip}`);
    }
    return server;
  };

  /**
   * Searches for and returns the RunningScript object for the specified script.
   * If the 'fn' argument is not specified, this returns the current RunningScript.
   * @param {string} fn - Filename of script
   * @param {string} ip - Hostname/ip of the server on which the script resides
   * @param {any[]} scriptArgs - Running script's arguments
   * @returns {RunningScript}
   *      Running script identified by the parameters, or null if no such script
   *      exists, or the current running script if the first argument 'fn'
   *      is not specified.
   */
  const getRunningScript = function (fn, ip, callingFnName, scriptArgs) {
    if (typeof callingFnName !== "string" || callingFnName === "") {
      callingFnName = "getRunningScript";
    }

    if (!Array.isArray(scriptArgs)) {
      throw makeRuntimeRejectMsg(
        workerScript,
        `Invalid scriptArgs argument passed into getRunningScript() from ${callingFnName}(). ` +
          `This is probably a bug. Please report to game developer`,
      );
    }

    if (fn != null && typeof fn === "string") {
      // Get Logs of another script
      if (ip == null) {
        ip = workerScript.serverIp;
      }
      const server = safeGetServer(ip, callingFnName);

      return findRunningScript(fn, scriptArgs, server);
    }

    // If no arguments are specified, return the current RunningScript
    return workerScript.scriptRef;
  };

  const getRunningScriptByPid = function (pid, callingFnName) {
    if (typeof callingFnName !== "string" || callingFnName === "") {
      callingFnName = "getRunningScriptgetRunningScriptByPid";
    }

    for (const name of Object.keys(AllServers)) {
      const server = AllServers[name];
      const runningScript = findRunningScriptByPid(pid, server);
      if (runningScript) return runningScript;
    }
    return null;
  };

  /**
   * Helper function for getting the error log message when the user specifies
   * a nonexistent running script
   * @param {string} fn - Filename of script
   * @param {string} ip - Hostname/ip of the server on which the script resides
   * @param {any[]} scriptArgs - Running script's arguments
   * @returns {string} Error message to print to logs
   */
  const getCannotFindRunningScriptErrorMessage = function (fn, ip, scriptArgs) {
    if (!Array.isArray(scriptArgs)) {
      scriptArgs = [];
    }

    return `Cannot find running script ${fn} on server ${ip} with args: ${arrayToString(scriptArgs)}`;
  };

  /**
   * Checks if the player has TIX API access. Throws an error if the player does not
   */
  const checkTixApiAccess = function (callingFn = "") {
    if (!Player.hasWseAccount) {
      throw makeRuntimeErrorMsg(callingFn, `You don't have WSE Access! Cannot use ${callingFn}()`);
    }
    if (!Player.hasTixApiAccess) {
      throw makeRuntimeErrorMsg(callingFn, `You don't have TIX API Access! Cannot use ${callingFn}()`);
    }
  };

  /**
   * Gets a stock, given its symbol. Throws an error if the symbol is invalid
   * @param {string} symbol - Stock's symbol
   * @returns {Stock} stock object
   */
  const getStockFromSymbol = function (symbol, callingFn = "") {
    const stock = SymbolToStockMap[symbol];
    if (stock == null) {
      throw makeRuntimeErrorMsg(callingFn, `Invalid stock symbol: '${symbol}'`);
    }

    return stock;
  };

  /**
   * Used to fail a function if the function's target is a Hacknet Server.
   * This is used for functions that should run on normal Servers, but not Hacknet Servers
   * @param {Server} server - Target server
   * @param {string} callingFn - Name of calling function. For logging purposes
   * @returns {boolean} True if the server is a Hacknet Server, false otherwise
   */
  const failOnHacknetServer = function (server, callingFn = "") {
    if (server instanceof HacknetServer) {
      workerScript.log(callingFn, `Does not work on Hacknet Servers`);
      return true;
    } else {
      return false;
    }
  };

  // Utility function to get Hacknet Node object
  const getHacknetNode = function (i, callingFn = "") {
    if (isNaN(i)) {
      throw makeRuntimeErrorMsg(callingFn, "Invalid index specified for Hacknet Node: " + i);
    }
    if (i < 0 || i >= Player.hacknetNodes.length) {
      throw makeRuntimeErrorMsg(callingFn, "Index specified for Hacknet Node is out-of-bounds: " + i);
    }

    if (hasHacknetServers(Player)) {
      const hserver = AllServers[Player.hacknetNodes[i]];
      if (hserver == null) {
        throw makeRuntimeErrorMsg(
          callingFn,
          `Could not get Hacknet Server for index ${i}. This is probably a bug, please report to game dev`,
        );
      }

      return hserver;
    } else {
      return Player.hacknetNodes[i];
    }
  };

  const makeRuntimeErrorMsg = function (caller, msg) {
    const stack = new Error().stack.split("\n").slice(1);
    const scripts = workerScript.getServer().scripts;
    const userstack = [];
    for (const stackline of stack) {
      let filename;
      for (const script of scripts) {
        if (script.url && stackline.includes(script.url)) {
          filename = script.filename;
        }
        for (const dependency of script.dependencies) {
          if (stackline.includes(dependency.url)) {
            filename = dependency.filename;
          }
        }
      }
      if (!filename) continue;

      function parseChromeStackline(line) {
        const lineRe = /.*:(\d+):\d+.*/;
        const funcRe = /.*at (.+) \(.*/;

        const lineMatch = line.match(lineRe);
        const funcMatch = line.match(funcRe);
        if (lineMatch && funcMatch) {
          return { line: lineMatch[1], func: funcMatch[1] };
        }
        return null;
      }
      let call = { line: "-1", func: "unknown" };
      const chromeCall = parseChromeStackline(stackline);
      if (chromeCall) {
        call = chromeCall;
      }

      function parseFirefoxStackline(line) {
        const lineRe = /.*:(\d+):\d+$/;
        const lineMatch = line.match(lineRe);

        const lio = line.lastIndexOf("@");

        if (lineMatch && lio !== -1) {
          return { line: lineMatch[1], func: line.slice(0, lio) };
        }
        return null;
      }

      let firefoxCall = parseFirefoxStackline(stackline);
      if (firefoxCall) {
        call = firefoxCall;
      }

      userstack.push(`${filename}:L${call.line}@${call.func}`);
    }

    workerScript.log(caller, msg);
    let rejectMsg = `${caller}: ${msg}`;
    if (userstack.length !== 0) rejectMsg += `<br><br>Stack:<br>${userstack.join("<br>")}`;
    return makeRuntimeRejectMsg(workerScript, rejectMsg);
  };

  const checkFormulasAccess = function (func, n) {
    if ((SourceFileFlags[5] < 1 && Player.bitNodeN !== 5) || (SourceFileFlags[n] < 1 && Player.bitNodeN !== n)) {
      let extra = "";
      if (n !== 5) {
        extra = ` and Source-File ${n}-1`;
      }
      throw makeRuntimeErrorMsg(`formulas.${func}`, `Requires Source-File 5-1${extra} to run.`);
    }
  };

  const checkSingularityAccess = function (func, n) {
    if (Player.bitNodeN !== 4) {
      if (SourceFileFlags[4] < n) {
        throw makeRuntimeErrorMsg(func, `This singularity function requires Source-File 4-${n} to run.`);
      }
    }
  };

  const checkBladeburnerAccess = function (func, skipjoined = false) {
    const apiAccess =
      Player.bitNodeN === 7 ||
      Player.sourceFiles.some((a) => {
        return a.n === 7;
      });
    if (!apiAccess) {
      const apiDenied = `You do not currently have access to the Bladeburner API. You must either be in BitNode-7 or have Source-File 7.`;
      throw makeRuntimeErrorMsg(`bladeburner.${func}`, apiDenied);
    }
    if (!skipjoined) {
      const bladeburnerAccess = Player.bladeburner instanceof Bladeburner;
      if (!bladeburnerAccess) {
        const bladeburnerDenied = `You must be a member of the Bladeburner division to use this API.`;
        throw makeRuntimeErrorMsg(`bladeburner.${func}`, bladeburnerDenied);
      }
    }
  };

  const checkBladeburnerCity = function (func, city) {
    if (!Player.bladeburner.cities.hasOwnProperty(city)) {
      throw makeRuntimeErrorMsg(`bladeburner.${func}`, `Invalid city: ${city}`);
    }
  };

  const checkSleeveAPIAccess = function (func) {
    if (Player.bitNodeN !== 10 && !SourceFileFlags[10]) {
      throw makeRuntimeErrorMsg(
        `sleeve.${func}`,
        "You do not currently have access to the Sleeve API. This is either because you are not in BitNode-10 or because you do not have Source-File 10",
      );
    }
  };

  const checkSleeveNumber = function (func, sleeveNumber) {
    if (sleeveNumber >= Player.sleeves.length || sleeveNumber < 0) {
      const msg = `Invalid sleeve number: ${sleeveNumber}`;
      workerScript.log(func, msg);
      throw makeRuntimeErrorMsg(`sleeve.${func}`, msg);
    }
  };

  const getCodingContract = function (func, ip, fn) {
    const server = safeGetServer(ip, func);
    const contract = server.getContract(fn);
    if (contract == null) {
      throw makeRuntimeErrorMsg(`codingcontract.${func}`, `Cannot find contract '${fn}' on server '${ip}'`);
    }

    return contract;
  };

  const checkGangApiAccess = function (func) {
    const hasAccess = Player.gang instanceof Gang;
    if (!hasAccess) {
      throw makeRuntimeErrorMsg(`gang.${func}`, `You do not currently have a Gang`);
    }
  };

  const getGangMember = function (func, name) {
    for (const member of Player.gang.members) if (member.name === name) return member;
    throw makeRuntimeErrorMsg(`gang.${func}`, `Invalid gang member: '${name}'`);
  };

  const getGangTask = function (func, name) {
    const task = GangMemberTasks[name];
    if (!task) {
      throw makeRuntimeErrorMsg(`gang.${func}`, `Invalid task: '${name}'`);
    }

    return task;
  };

  const getBladeburnerActionObject = function (func, type, name) {
    const actionId = Player.bladeburner.getActionIdFromTypeAndName(type, name);
    if (!actionId) {
      throw makeRuntimeErrorMsg(`bladeburner.${func}`, `Invalid action type='${type}', name='${name}'`);
    }
    const actionObj = Player.bladeburner.getActionObject(actionId);
    if (!actionObj) {
      throw makeRuntimeErrorMsg(`bladeburner.${func}`, `Invalid action type='${type}', name='${name}'`);
    }

    return actionObj;
  };

  const getCompany = function (func, name) {
    const company = Companies[name];
    if (company == null || !(company instanceof Company)) {
      throw makeRuntimeErrorMsg(func, `Invalid company name: '${name}'`);
    }
    return company;
  };

  const getFaction = function (func, name) {
    if (!factionExists(name)) {
      throw makeRuntimeErrorMsg(func, `Invalid faction name: '${name}`);
    }

    return Factions[name];
  };

  const getAugmentation = function (func, name) {
    if (!augmentationExists(name)) {
      throw makeRuntimeErrorMsg(func, `Invalid augmentation: '${name}'`);
    }

    return Augmentations[name];
  };

  function getDivision(divisionName) {
    const division = Player.corporation.divisions.find((div) => div.name === divisionName);
    if (division === undefined) throw new Error(`No division named '${divisionName}'`);
    return division;
  }

  function getOffice(divisionName, cityName) {
    const division = getDivision(divisionName);
    if (!(cityName in division.offices)) throw new Error(`Invalid city name '${cityName}'`);
    const office = division.offices[cityName];
    if (office === 0) throw new Error(`${division.name} has not expanded to '${cityName}'`);
    return office;
  }

  function getWarehouse(divisionName, cityName) {
    const division = getDivision(divisionName);
    if (!(cityName in division.warehouses)) throw new Error(`Invalid city name '${cityName}'`);
    const warehouse = division.warehouses[cityName];
    if (warehouse === 0) throw new Error(`${division.name} has not expanded to '${cityName}'`);
    return warehouse;
  }

  function getMaterial(divisionName, cityName, materialName) {
    const warehouse = getWarehouse(divisionName, cityName);
    const material = warehouse.materials[materialName];
    if (material === undefined) throw new Error(`Invalid material name: '${materialName}'`);
    return material;
  }

  function getProduct(divisionName, productName) {
    const division = getDivision(divisionName);
    const product = division.products[productName];
    if (product === undefined) throw new Error(`Invalid product name: '${productName}'`);
    return product;
  }

  function getEmployee(divisionName, cityName, employeeName) {
    const office = getOffice(divisionName, cityName);
    const employee = office.employees.find((e) => e.name === employeeName);
    if (employee === undefined) throw new Error(`Invalid employee name: '${employeeName}'`);
    return employee;
  }

  const runAfterReset = function (cbScript = null) {
    //Run a script after reset
    if (cbScript && isString(cbScript)) {
      const home = Player.getHomeComputer();
      for (const script of home.scripts) {
        if (script.filename === cbScript) {
          const ramUsage = script.ramUsage;
          const ramAvailable = home.maxRam - home.ramUsed;
          if (ramUsage > ramAvailable) {
            return; // Not enough RAM
          }
          const runningScriptObj = new RunningScript(script, []); // No args
          runningScriptObj.threads = 1; // Only 1 thread
          startWorkerScript(runningScriptObj, home);
        }
      }
    }
  };

  const hack = function (ip, manual, { threads: requestedThreads, stock } = {}) {
    if (ip === undefined) {
      throw makeRuntimeErrorMsg("hack", "Takes 1 argument.");
    }
    const threads = resolveNetscriptRequestedThreads(workerScript, "hack", requestedThreads);
    const server = getServer(ip);
    if (server == null) {
      throw makeRuntimeErrorMsg("hack", `Invalid IP/hostname: ${ip}.`);
    }

    // Calculate the hacking time
    var hackingTime = calculateHackingTime(server, Player); // This is in seconds

    // No root access or skill level too low
    const canHack = netscriptCanHack(server, Player);
    if (!canHack.res) {
      throw makeRuntimeErrorMsg("hack", canHack.msg);
    }

    workerScript.log(
      "hack",
      `Executing ${ip} in ${convertTimeMsToTimeElapsedString(
        hackingTime * 1000,
        true,
      )} (t=${numeralWrapper.formatThreads(threads)})`,
    );

    return netscriptDelay(hackingTime * 1000, workerScript).then(function () {
      if (workerScript.env.stopFlag) {
        return Promise.reject(workerScript);
      }
      var hackChance = calculateHackingChance(server, Player);
      var rand = Math.random();
      var expGainedOnSuccess = calculateHackingExpGain(server, Player) * threads;
      var expGainedOnFailure = expGainedOnSuccess / 4;
      if (rand < hackChance) {
        // Success!
        const percentHacked = calculatePercentMoneyHacked(server, Player);
        let maxThreadNeeded = Math.ceil((1 / percentHacked) * (server.moneyAvailable / server.moneyMax));
        if (isNaN(maxThreadNeeded)) {
          // Server has a 'max money' of 0 (probably). We'll set this to an arbitrarily large value
          maxThreadNeeded = 1e6;
        }

        let moneyDrained = Math.floor(server.moneyAvailable * percentHacked) * threads;

        // Over-the-top safety checks
        if (moneyDrained <= 0) {
          moneyDrained = 0;
          expGainedOnSuccess = expGainedOnFailure;
        }
        if (moneyDrained > server.moneyAvailable) {
          moneyDrained = server.moneyAvailable;
        }
        server.moneyAvailable -= moneyDrained;
        if (server.moneyAvailable < 0) {
          server.moneyAvailable = 0;
        }

        const moneyGained = moneyDrained * BitNodeMultipliers.ScriptHackMoneyGain;

        Player.gainMoney(moneyGained);
        workerScript.scriptRef.onlineMoneyMade += moneyGained;
        Player.scriptProdSinceLastAug += moneyGained;
        Player.recordMoneySource(moneyGained, "hacking");
        workerScript.scriptRef.recordHack(server.ip, moneyGained, threads);
        Player.gainHackingExp(expGainedOnSuccess);
        workerScript.scriptRef.onlineExpGained += expGainedOnSuccess;
        workerScript.log(
          "hack",
          `Successfully hacked '${server.hostname}' for ${numeralWrapper.formatMoney(
            moneyGained,
          )} and ${numeralWrapper.formatExp(expGainedOnSuccess)} exp (t=${numeralWrapper.formatThreads(threads)})`,
        );
        server.fortify(CONSTANTS.ServerFortifyAmount * Math.min(threads, maxThreadNeeded));
        if (stock) {
          influenceStockThroughServerHack(server, moneyGained);
        }
        if (manual) {
          server.backdoorInstalled = true;
        }
        return Promise.resolve(moneyGained);
      } else {
        // Player only gains 25% exp for failure?
        Player.gainHackingExp(expGainedOnFailure);
        workerScript.scriptRef.onlineExpGained += expGainedOnFailure;
        workerScript.log(
          "hack",
          `Failed to hack '${server.hostname}'. Gained ${numeralWrapper.formatExp(
            expGainedOnFailure,
          )} exp (t=${numeralWrapper.formatThreads(threads)})`,
        );
        return Promise.resolve(0);
      }
    });
  };

  const argsToString = function (args) {
    let out = "";
    for (let arg of args) {
      arg = toNative(arg);
      if (typeof arg === "object") {
        out += JSON.stringify(arg);
        continue;
      }
      out += `${arg}`;
    }

    return out;
  };

  const functions = {
    hacknet: {
      numNodes: function () {
        return Player.hacknetNodes.length;
      },
      maxNumNodes: function () {
        if (hasHacknetServers(Player)) {
          return HacknetServerConstants.MaxServers;
        }
        return Infinity;
      },
      purchaseNode: function () {
        return purchaseHacknet(Player);
      },
      getPurchaseNodeCost: function () {
        if (hasHacknetServers(Player)) {
          return getCostOfNextHacknetServer(Player);
        } else {
          return getCostOfNextHacknetNode(Player);
        }
      },
      getNodeStats: function (i) {
        const node = getHacknetNode(i, "getNodeStats");
        const hasUpgraded = hasHacknetServers(Player);
        const res = {
          name: hasUpgraded ? node.hostname : node.name,
          level: node.level,
          ram: hasUpgraded ? node.maxRam : node.ram,
          cores: node.cores,
          production: hasUpgraded ? node.hashRate : node.moneyGainRatePerSecond,
          timeOnline: node.onlineTimeSeconds,
          totalProduction: hasUpgraded ? node.totalHashesGenerated : node.totalMoneyGenerated,
        };

        if (hasUpgraded) {
          res.cache = node.cache;
          res.hashCapacity = node.hashCapacity;
        }

        return res;
      },
      upgradeLevel: function (i, n) {
        const node = getHacknetNode(i, "upgradeLevel");
        return purchaseLevelUpgrade(Player, node, n);
      },
      upgradeRam: function (i, n) {
        const node = getHacknetNode(i, "upgradeRam");
        return purchaseRamUpgrade(Player, node, n);
      },
      upgradeCore: function (i, n) {
        const node = getHacknetNode(i, "upgradeCore");
        return purchaseCoreUpgrade(Player, node, n);
      },
      upgradeCache: function (i, n) {
        if (!hasHacknetServers(Player)) {
          return false;
        }
        const node = getHacknetNode(i, "upgradeCache");
        const res = purchaseCacheUpgrade(Player, node, n);
        if (res) {
          updateHashManagerCapacity(Player);
        }
        return res;
      },
      getLevelUpgradeCost: function (i, n) {
        const node = getHacknetNode(i, "upgradeLevel");
        return node.calculateLevelUpgradeCost(n, Player.hacknet_node_level_cost_mult);
      },
      getRamUpgradeCost: function (i, n) {
        const node = getHacknetNode(i, "upgradeRam");
        return node.calculateRamUpgradeCost(n, Player.hacknet_node_ram_cost_mult);
      },
      getCoreUpgradeCost: function (i, n) {
        const node = getHacknetNode(i, "upgradeCore");
        return node.calculateCoreUpgradeCost(n, Player.hacknet_node_core_cost_mult);
      },
      getCacheUpgradeCost: function (i, n) {
        if (!hasHacknetServers(Player)) {
          return Infinity;
        }
        const node = getHacknetNode(i, "upgradeCache");
        return node.calculateCacheUpgradeCost(n);
      },
      numHashes: function () {
        if (!hasHacknetServers(Player)) {
          return 0;
        }
        return Player.hashManager.hashes;
      },
      hashCapacity: function () {
        if (!hasHacknetServers(Player)) {
          return 0;
        }
        return Player.hashManager.capacity;
      },
      hashCost: function (upgName) {
        if (!hasHacknetServers(Player)) {
          return Infinity;
        }

        return Player.hashManager.getUpgradeCost(upgName);
      },
      spendHashes: function (upgName, upgTarget) {
        if (!hasHacknetServers(Player)) {
          return false;
        }
        return purchaseHashUpgrade(Player, upgName, upgTarget);
      },
      getHashUpgradeLevel: function (upgName) {
        const level = Player.hashManager.upgrades[upgName];
        if (level === undefined) {
          throw makeRuntimeErrorMsg("hacknet.hashUpgradeLevel", `Invalid Hash Upgrade: ${upgName}`);
        }
        return level;
      },
      getStudyMult: function () {
        if (!hasHacknetServers(Player)) {
          return false;
        }
        return Player.hashManager.getStudyMult();
      },
      getTrainingMult: function () {
        if (!hasHacknetServers(Player)) {
          return false;
        }
        return Player.hashManager.getTrainingMult();
      },
    },
    sprintf: sprintf,
    vsprintf: vsprintf,
    scan: function (ip = workerScript.serverIp, hostnames = true) {
      updateDynamicRam("scan", getRamCost("scan"));
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("scan", `Invalid IP/hostname: ${ip}.`);
      }
      var out = [];
      for (var i = 0; i < server.serversOnNetwork.length; i++) {
        var entry;
        if (hostnames) {
          entry = getServerOnNetwork(server, i).hostname;
        } else {
          entry = getServerOnNetwork(server, i).ip;
        }
        if (entry == null) {
          continue;
        }
        out.push(entry);
      }
      workerScript.log("scan", `returned ${server.serversOnNetwork.length} connections for ${server.hostname}`);
      return out;
    },
    hack: function (ip, { threads: requestedThreads, stock } = {}) {
      updateDynamicRam("hack", getRamCost("hack"));
      return hack(ip, false, { threads: requestedThreads, stock: stock });
    },
    hackAnalyzeThreads: function (ip, hackAmount) {
      updateDynamicRam("hackAnalyzeThreads", getRamCost("hackAnalyzeThreads"));

      // Check argument validity
      const server = safeGetServer(ip, "hackAnalyzeThreads");
      if (isNaN(hackAmount)) {
        throw makeRuntimeErrorMsg(
          workerScript,
          `Invalid growth argument passed into hackAnalyzeThreads: ${hackAmount}. Must be numeric.`,
        );
      }

      if (hackAmount < 0 || hackAmount > server.moneyAvailable) {
        return -1;
      }

      const percentHacked = calculatePercentMoneyHacked(server, Player);

      return hackAmount / Math.floor(server.moneyAvailable * percentHacked);
    },
    hackAnalyzePercent: function (ip) {
      updateDynamicRam("hackAnalyzePercent", getRamCost("hackAnalyzePercent"));

      const server = safeGetServer(ip, "hackAnalyzePercent");

      return calculatePercentMoneyHacked(server, Player) * 100;
    },
    hackChance: function (ip) {
      updateDynamicRam("hackChance", getRamCost("hackChance"));

      const server = safeGetServer(ip, "hackChance");

      return calculateHackingChance(server, Player);
    },
    sleep: function (time) {
      if (time === undefined) {
        throw makeRuntimeErrorMsg("sleep", "Takes 1 argument.");
      }
      workerScript.log("sleep", `Sleeping for ${time} milliseconds`);
      return netscriptDelay(time, workerScript).then(function () {
        return Promise.resolve(true);
      });
    },
    grow: function (ip, { threads: requestedThreads, stock } = {}) {
      updateDynamicRam("grow", getRamCost("grow"));
      const threads = resolveNetscriptRequestedThreads(workerScript, "grow", requestedThreads);
      if (ip === undefined) {
        throw makeRuntimeErrorMsg("grow", "Takes 1 argument.");
      }
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("grow", `Invalid IP/hostname: ${ip}.`);
      }

      const host = getServer(workerScript.serverIp);

      // No root access or skill level too low
      const canHack = netscriptCanGrow(server);
      if (!canHack.res) {
        throw makeRuntimeErrorMsg("grow", canHack.msg);
      }

      var growTime = calculateGrowTime(server, Player);
      workerScript.log(
        "grow",
        `Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString(
          growTime * 1000,
          true,
        )} (t=${numeralWrapper.formatThreads(threads)}).`,
      );
      return netscriptDelay(growTime * 1000, workerScript).then(function () {
        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
        processSingleServerGrowth(server, threads, Player, host.cpuCores);
        const moneyAfter = server.moneyAvailable;
        workerScript.scriptRef.recordGrow(server.ip, threads);
        var expGain = calculateHackingExpGain(server, Player) * threads;
        const logGrowPercent = moneyAfter / moneyBefore - 1;
        workerScript.log(
          "grow",
          `Available money on '${server.hostname}' grown by ${numeralWrapper.formatPercentage(
            logGrowPercent,
            6,
          )}. Gained ${numeralWrapper.formatExp(expGain)} hacking exp (t=${numeralWrapper.formatThreads(threads)}).`,
        );
        workerScript.scriptRef.onlineExpGained += expGain;
        Player.gainHackingExp(expGain);
        if (stock) {
          influenceStockThroughServerGrow(server, moneyAfter - moneyBefore);
        }
        return Promise.resolve(moneyAfter / moneyBefore);
      });
    },
    growthAnalyze: function (ip, growth) {
      updateDynamicRam("growthAnalyze", getRamCost("growthAnalyze"));

      // Check argument validity
      const server = safeGetServer(ip, "growthAnalyze");
      if (typeof growth !== "number" || isNaN(growth) || growth < 1 || !isFinite(growth)) {
        throw makeRuntimeErrorMsg("growthAnalyze", `Invalid argument: growth must be numeric and >= 1, is ${growth}.`);
      }

      return numCycleForGrowth(server, Number(growth), Player);
    },
    weaken: function (ip, { threads: requestedThreads } = {}) {
      updateDynamicRam("weaken", getRamCost("weaken"));
      var threads = resolveNetscriptRequestedThreads(workerScript, "weaken", requestedThreads);
      if (ip === undefined) {
        throw makeRuntimeErrorMsg("weaken", "Takes 1 argument.");
      }
      const server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("weaken", `Invalid IP/hostname: ${ip}`);
      }

      // No root access or skill level too low
      const canHack = netscriptCanWeaken(server);
      if (!canHack.res) {
        throw makeRuntimeErrorMsg("weaken", canHack.msg);
      }

      const weakenTime = calculateWeakenTime(server, Player);
      workerScript.log(
        "weaken",
        `Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString(
          weakenTime * 1000,
          true,
        )} (t=${numeralWrapper.formatThreads(threads)})`,
      );
      return netscriptDelay(weakenTime * 1000, workerScript).then(function () {
        if (workerScript.env.stopFlag) return Promise.reject(workerScript);
        const host = getServer(workerScript.serverIp);
        const coreBonus = 1 + (host.cpuCores - 1) / 16;
        server.weaken(CONSTANTS.ServerWeakenAmount * threads * coreBonus);
        workerScript.scriptRef.recordWeaken(server.ip, threads);
        const expGain = calculateHackingExpGain(server, Player) * threads;
        workerScript.log(
          "weaken",
          `'${server.hostname}' security level weakened to ${server.hackDifficulty}. Gained ${numeralWrapper.formatExp(
            expGain,
          )} hacking exp (t=${numeralWrapper.formatThreads(threads)})`,
        );
        workerScript.scriptRef.onlineExpGained += expGain;
        Player.gainHackingExp(expGain);
        return Promise.resolve(CONSTANTS.ServerWeakenAmount * threads);
      });
    },
    print: function () {
      if (arguments.length === 0) {
        throw makeRuntimeErrorMsg("print", "Takes at least 1 argument.");
      }
      workerScript.print(argsToString(arguments));
    },
    tprint: function () {
      if (arguments.length === 0) {
        throw makeRuntimeErrorMsg("tprint", "Takes at least 1 argument.");
      }
      post(`${workerScript.scriptRef.filename}: ${argsToString(arguments)}`);
    },
    tprintf: function (format, ...args) {
      post(vsprintf(format, args));
    },
    clearLog: function () {
      workerScript.scriptRef.clearLog();
    },
    disableLog: function (fn) {
      if (fn === "ALL") {
        for (fn in possibleLogs) {
          workerScript.disableLogs[fn] = true;
        }
        workerScript.log("disableLog", `Disabled logging for all functions`);
      } else if (possibleLogs[fn] === undefined) {
        throw makeRuntimeErrorMsg("disableLog", `Invalid argument: ${fn}.`);
      } else {
        workerScript.disableLogs[fn] = true;
        workerScript.log("disableLog", `Disabled logging for ${fn}`);
      }
    },
    enableLog: function (fn) {
      if (possibleLogs[fn] === undefined) {
        throw makeRuntimeErrorMsg("enableLog", `Invalid argument: ${fn}.`);
      }
      delete workerScript.disableLogs[fn];
      workerScript.log("enableLog", `Enabled logging for ${fn}`);
    },
    isLogEnabled: function (fn) {
      if (possibleLogs[fn] === undefined) {
        throw makeRuntimeErrorMsg("isLogEnabled", `Invalid argument: ${fn}.`);
      }
      return workerScript.disableLogs[fn] ? false : true;
    },
    getScriptLogs: function (fn, ip, ...scriptArgs) {
      const runningScriptObj = getRunningScript(fn, ip, "getScriptLogs", scriptArgs);
      if (runningScriptObj == null) {
        workerScript.log("getScriptLogs", getCannotFindRunningScriptErrorMessage(fn, ip, scriptArgs));
        return "";
      }

      return runningScriptObj.logs.slice();
    },
    tail: function (fn, ip = workerScript.serverIp, ...scriptArgs) {
      let runningScriptObj;
      if (arguments.length === 0) {
        runningScriptObj = workerScript.scriptRef;
      } else if (typeof fn === "number") {
        runningScriptObj = getRunningScriptByPid(fn, "tail");
      } else {
        runningScriptObj = getRunningScript(fn, ip, "tail", scriptArgs);
      }
      if (runningScriptObj == null) {
        workerScript.log("tail", getCannotFindRunningScriptErrorMessage(fn, ip, scriptArgs));
        return;
      }

      logBoxCreate(runningScriptObj);
    },
    nuke: function (ip) {
      updateDynamicRam("nuke", getRamCost("nuke"));
      if (ip === undefined) {
        throw makeRuntimeErrorMsg("nuke", "Takes 1 argument.");
      }
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("nuke", `Invalid IP/hostname: ${ip}.`);
      }
      if (!Player.hasProgram(Programs.NukeProgram.name)) {
        throw makeRuntimeErrorMsg("nuke", "You do not have the NUKE.exe virus!");
      }
      if (server.openPortCount < server.numOpenPortsRequired) {
        throw makeRuntimeErrorMsg("nuke", "Not enough ports opened to use NUKE.exe virus.");
      }
      if (server.hasAdminRights) {
        workerScript.log("nuke", `Already have root access to '${server.hostname}'.`);
      } else {
        server.hasAdminRights = true;
        workerScript.log("nuke", `Executed NUKE.exe virus on '${server.hostname}' to gain root access.`);
      }
      return true;
    },
    brutessh: function (ip) {
      updateDynamicRam("brutessh", getRamCost("brutessh"));
      if (ip === undefined) {
        throw makeRuntimeErrorMsg("brutessh", "Takes 1 argument.");
      }
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("brutessh", `Invalid IP/hostname: ${ip}.`);
      }
      if (!Player.hasProgram(Programs.BruteSSHProgram.name)) {
        throw makeRuntimeErrorMsg("brutessh", "You do not have the BruteSSH.exe program!");
      }
      if (!server.sshPortOpen) {
        workerScript.log("brutessh", `Executed BruteSSH.exe on '${server.hostname}' to open SSH port (22).`);
        server.sshPortOpen = true;
        ++server.openPortCount;
      } else {
        workerScript.log("brutessh", `SSH Port (22) already opened on '${server.hostname}'.`);
      }
      return true;
    },
    ftpcrack: function (ip) {
      updateDynamicRam("ftpcrack", getRamCost("ftpcrack"));
      if (ip === undefined) {
        throw makeRuntimeErrorMsg("ftpcrack", "Takes 1 argument.");
      }
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("ftpcrack", `Invalid IP/hostname: ${ip}.`);
      }
      if (!Player.hasProgram(Programs.FTPCrackProgram.name)) {
        throw makeRuntimeErrorMsg("ftpcrack", "You do not have the FTPCrack.exe program!");
      }
      if (!server.ftpPortOpen) {
        workerScript.log("ftpcrack", `Executed FTPCrack.exe on '${server.hostname}' to open FTP port (21).`);
        server.ftpPortOpen = true;
        ++server.openPortCount;
      } else {
        workerScript.log("ftpcrack", `FTP Port (21) already opened on '${server.hostname}'.`);
      }
      return true;
    },
    relaysmtp: function (ip) {
      updateDynamicRam("relaysmtp", getRamCost("relaysmtp"));
      if (ip === undefined) {
        throw makeRuntimeErrorMsg("relaysmtp", "Takes 1 argument.");
      }
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("relaysmtp", `Invalid IP/hostname: ${ip}.`);
      }
      if (!Player.hasProgram(Programs.RelaySMTPProgram.name)) {
        throw makeRuntimeErrorMsg("relaysmtp", "You do not have the relaySMTP.exe program!");
      }
      if (!server.smtpPortOpen) {
        workerScript.log("relaysmtp", `Executed relaySMTP.exe on '${server.hostname}' to open SMTP port (25).`);
        server.smtpPortOpen = true;
        ++server.openPortCount;
      } else {
        workerScript.log("relaysmtp", `SMTP Port (25) already opened on '${server.hostname}'.`);
      }
      return true;
    },
    httpworm: function (ip) {
      updateDynamicRam("httpworm", getRamCost("httpworm"));
      if (ip === undefined) {
        throw makeRuntimeErrorMsg("httpworm", "Takes 1 argument");
      }
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("httpworm", `Invalid IP/hostname: ${ip}`);
      }
      if (!Player.hasProgram(Programs.HTTPWormProgram.name)) {
        throw makeRuntimeErrorMsg("httpworm", "You do not have the HTTPWorm.exe program!");
      }
      if (!server.httpPortOpen) {
        workerScript.log("httpworm", `Executed HTTPWorm.exe on '${server.hostname}' to open HTTP port (80).`);
        server.httpPortOpen = true;
        ++server.openPortCount;
      } else {
        workerScript.log("httpworm", `HTTP Port (80) already opened on '${server.hostname}'.`);
      }
      return true;
    },
    sqlinject: function (ip) {
      updateDynamicRam("sqlinject", getRamCost("sqlinject"));
      if (ip === undefined) {
        throw makeRuntimeErrorMsg("sqlinject", "Takes 1 argument.");
      }
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("sqlinject", `Invalid IP/hostname: ${ip}`);
      }
      if (!Player.hasProgram(Programs.SQLInjectProgram.name)) {
        throw makeRuntimeErrorMsg("sqlinject", "You do not have the SQLInject.exe program!");
      }
      if (!server.sqlPortOpen) {
        workerScript.log("sqlinject", `Executed SQLInject.exe on '${server.hostname}' to open SQL port (1433).`);
        server.sqlPortOpen = true;
        ++server.openPortCount;
      } else {
        workerScript.log("sqlinject", `SQL Port (1433) already opened on '${server.hostname}'.`);
      }
      return true;
    },
    run: function (scriptname, threads = 1) {
      updateDynamicRam("run", getRamCost("run"));
      if (scriptname === undefined) {
        throw makeRuntimeErrorMsg("run", "Usage: run(scriptname, [numThreads], [arg1], [arg2]...)");
      }
      if (isNaN(threads) || threads <= 0) {
        throw makeRuntimeErrorMsg("run", `Invalid thread count. Must be numeric and > 0, is ${threads}`);
      }
      var argsForNewScript = [];
      for (var i = 2; i < arguments.length; ++i) {
        argsForNewScript.push(arguments[i]);
      }
      var scriptServer = getServer(workerScript.serverIp);
      if (scriptServer == null) {
        throw makeRuntimeErrorMsg("run", "Could not find server. This is a bug. Report to dev.");
      }

      return runScriptFromScript("run", scriptServer, scriptname, argsForNewScript, workerScript, threads);
    },
    exec: function (scriptname, ip, threads = 1) {
      updateDynamicRam("exec", getRamCost("exec"));
      if (scriptname === undefined || ip === undefined) {
        throw makeRuntimeErrorMsg("exec", "Usage: exec(scriptname, server, [numThreads], [arg1], [arg2]...)");
      }
      if (isNaN(threads) || threads <= 0) {
        throw makeRuntimeErrorMsg("exec", `Invalid thread count. Must be numeric and > 0, is ${threads}`);
      }
      var argsForNewScript = [];
      for (var i = 3; i < arguments.length; ++i) {
        argsForNewScript.push(arguments[i]);
      }
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("exec", `Invalid IP/hostname: ${ip}`);
      }
      return runScriptFromScript("exec", server, scriptname, argsForNewScript, workerScript, threads);
    },
    spawn: function (scriptname, threads) {
      updateDynamicRam("spawn", getRamCost("spawn"));
      if (!scriptname || !threads) {
        throw makeRuntimeErrorMsg("spawn", "Usage: spawn(scriptname, threads)");
      }

      const spawnDelay = 10;
      setTimeoutRef(() => {
        if (isNaN(threads) || threads <= 0) {
          throw makeRuntimeErrorMsg("spawn", `Invalid thread count. Must be numeric and > 0, is ${threads}`);
        }
        var argsForNewScript = [];
        for (var i = 2; i < arguments.length; ++i) {
          argsForNewScript.push(arguments[i]);
        }
        var scriptServer = getServer(workerScript.serverIp);
        if (scriptServer == null) {
          throw makeRuntimeErrorMsg("spawn", "Could not find server. This is a bug. Report to dev");
        }

        return runScriptFromScript("spawn", scriptServer, scriptname, argsForNewScript, workerScript, threads);
      }, spawnDelay * 1e3);

      workerScript.log("spawn", `Will execute '${scriptname}' in ${spawnDelay} seconds`);

      workerScript.running = false; // Prevent workerScript from "finishing execution naturally"
      if (killWorkerScript(workerScript)) {
        workerScript.log("spawn", "Exiting...");
      }
    },
    kill: function (filename, ip, ...scriptArgs) {
      updateDynamicRam("kill", getRamCost("kill"));

      let res;
      const killByPid = typeof filename === "number";
      if (killByPid) {
        // Kill by pid
        res = killWorkerScript(filename);
      } else {
        // Kill by filename/ip
        if (filename === undefined || ip === undefined) {
          throw makeRuntimeErrorMsg("kill", "Usage: kill(scriptname, server, [arg1], [arg2]...)");
        }

        const server = safeGetServer(ip);
        const runningScriptObj = getRunningScript(filename, ip, "kill", scriptArgs);
        if (runningScriptObj == null) {
          workerScript.log("kill", getCannotFindRunningScriptErrorMessage(filename, ip, scriptArgs));
          return false;
        }

        res = killWorkerScript(runningScriptObj, server.ip);
      }

      if (res) {
        if (killByPid) {
          workerScript.log("kill", `Killing script with PID ${filename}`);
        } else {
          workerScript.log("kill", `Killing '${filename}' on '${ip}' with args: ${arrayToString(scriptArgs)}.`);
        }
        return true;
      } else {
        if (killByPid) {
          workerScript.log("kill", `No script with PID ${filename}`);
        } else {
          workerScript.log("kill", `No such script '${filename}' on '${ip}' with args: ${arrayToString(scriptArgs)}`);
        }
        return false;
      }
    },
    killall: function (ip = workerScript.serverIp) {
      updateDynamicRam("killall", getRamCost("killall"));
      if (ip === undefined) {
        throw makeRuntimeErrorMsg("killall", "Takes 1 argument");
      }
      const server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("killall", `Invalid IP/hostname: ${ip}`);
      }
      const scriptsRunning = server.runningScripts.length > 0;
      for (let i = server.runningScripts.length - 1; i >= 0; --i) {
        killWorkerScript(server.runningScripts[i], server.ip, false);
      }
      WorkerScriptStartStopEventEmitter.emitEvent();
      workerScript.log(
        "killall",
        `Killing all scripts on '${server.hostname}'. May take a few minutes for the scripts to die.`,
      );

      return scriptsRunning;
    },
    exit: function () {
      workerScript.running = false; // Prevent workerScript from "finishing execution naturally"
      if (killWorkerScript(workerScript)) {
        workerScript.log("exit", "Exiting...");
      } else {
        workerScript.log("exit", "Failed. This is a bug. Report to dev.");
      }
    },
    scp: function (scriptname, ip1, ip2) {
      updateDynamicRam("scp", getRamCost("scp"));
      if (arguments.length !== 2 && arguments.length !== 3) {
        throw makeRuntimeErrorMsg("scp", "Takes 2 or 3 arguments");
      }
      if (scriptname && scriptname.constructor === Array) {
        // Recursively call scp on all elements of array
        var res = false;
        scriptname.forEach(function (script) {
          if (NetscriptFunctions(workerScript).scp(script, ip1, ip2)) {
            res = true;
          }
        });
        return res;
      }

      // Invalid file type
      if (!isValidFilePath(scriptname)) {
        throw makeRuntimeErrorMsg("scp", `Invalid filename: '${scriptname}'`);
      }

      // Invalid file name
      if (!scriptname.endsWith(".lit") && !isScriptFilename(scriptname) && !scriptname.endsWith("txt")) {
        throw makeRuntimeErrorMsg("scp", "Only works for .script, .lit, and .txt files");
      }

      var destServer, currServ;

      if (ip2 != null) {
        // 3 Argument version: scriptname, source, destination
        if (scriptname === undefined || ip1 === undefined || ip2 === undefined) {
          throw makeRuntimeErrorMsg("scp", "Takes 2 or 3 arguments");
        }
        destServer = getServer(ip2);
        if (destServer == null) {
          throw makeRuntimeErrorMsg("scp", `Invalid IP/hostname: ${ip2}`);
        }

        currServ = getServer(ip1);
        if (currServ == null) {
          throw makeRuntimeErrorMsg("scp", `Invalid IP/hostname: ${ip1}`);
        }
      } else if (ip1 != null) {
        // 2 Argument version: scriptname, destination
        if (scriptname === undefined || ip1 === undefined) {
          throw makeRuntimeErrorMsg("scp", "Takes 2 or 3 arguments");
        }
        destServer = getServer(ip1);
        if (destServer == null) {
          throw makeRuntimeErrorMsg("scp", `Invalid IP/hostname: ${ip1}`);
        }

        currServ = getServer(workerScript.serverIp);
        if (currServ == null) {
          throw makeRuntimeErrorMsg("scp", "Could not find server ip for this script. This is a bug. Report to dev.");
        }
      } else {
        throw makeRuntimeErrorMsg("scp", "Takes 2 or 3 arguments");
      }

      // Scp for lit files
      if (scriptname.endsWith(".lit")) {
        var found = false;
        for (var i = 0; i < currServ.messages.length; ++i) {
          if (!(currServ.messages[i] instanceof Message) && currServ.messages[i] == scriptname) {
            found = true;
            break;
          }
        }

        if (!found) {
          workerScript.log("scp", `File '${scriptname}' does not exist.`);
          return false;
        }

        for (var i = 0; i < destServer.messages.length; ++i) {
          if (destServer.messages[i] === scriptname) {
            workerScript.log("scp", `File '${scriptname}' copied over to '${destServer.hostname}'.`);
            return true; // Already exists
          }
        }
        destServer.messages.push(scriptname);
        workerScript.log("scp", `File '${scriptname}' copied over to '${destServer.hostname}'.`);
        return true;
      }

      // Scp for text files
      if (scriptname.endsWith(".txt")) {
        var found = false,
          txtFile;
        for (var i = 0; i < currServ.textFiles.length; ++i) {
          if (currServ.textFiles[i].fn === scriptname) {
            found = true;
            txtFile = currServ.textFiles[i];
            break;
          }
        }

        if (!found) {
          workerScript.log("scp", `File '${scriptname}' does not exist.`);
          return false;
        }

        for (var i = 0; i < destServer.textFiles.length; ++i) {
          if (destServer.textFiles[i].fn === scriptname) {
            // Overwrite
            destServer.textFiles[i].text = txtFile.text;
            workerScript.log("scp", `File '${scriptname}' copied over to '${destServer.hostname}'.`);
            return true;
          }
        }
        var newFile = new TextFile(txtFile.fn, txtFile.text);
        destServer.textFiles.push(newFile);
        workerScript.log("scp", `File '${scriptname}' copied over to '${destServer.hostname}'.`);
        return true;
      }

      // Scp for script files
      let sourceScript = null;
      for (let i = 0; i < currServ.scripts.length; ++i) {
        if (scriptname == currServ.scripts[i].filename) {
          sourceScript = currServ.scripts[i];
          break;
        }
      }
      if (sourceScript == null) {
        workerScript.log("scp", `File '${scriptname}' does not exist.`);
        return false;
      }

      // Overwrite script if it already exists
      for (let i = 0; i < destServer.scripts.length; ++i) {
        if (scriptname == destServer.scripts[i].filename) {
          workerScript.log("scp", `WARNING: File '${scriptname}' overwritten on '${destServer.hostname}'`);
          const oldScript = destServer.scripts[i];
          // If it's the exact same file don't actually perform the
          // copy to avoid recompiling uselessly. Players tend to scp
          // liberally.
          if (oldScript.code === sourceScript.code) return true;
          oldScript.code = sourceScript.code;
          oldScript.ramUsage = sourceScript.ramUsage;
          oldScript.markUpdated();
          return true;
        }
      }

      // Create new script if it does not already exist
      const newScript = new Script(scriptname);
      newScript.code = sourceScript.code;
      newScript.ramUsage = sourceScript.ramUsage;
      newScript.server = destServer.ip;
      destServer.scripts.push(newScript);
      workerScript.log("scp", `File '${scriptname}' copied over to '${destServer.hostname}'.`);
      return true;
    },
    ls: function (ip, grep) {
      updateDynamicRam("ls", getRamCost("ls"));
      if (ip === undefined) {
        throw makeRuntimeErrorMsg("ls", "Usage: ls(ip/hostname, [grep filter])");
      }
      const server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("ls", `Invalid IP/hostname: ${ip}`);
      }

      // Get the grep filter, if one exists
      let filter = false;
      if (arguments.length >= 2) {
        filter = grep.toString();
      }

      const allFiles = [];
      for (let i = 0; i < server.programs.length; i++) {
        if (filter) {
          if (server.programs[i].includes(filter)) {
            allFiles.push(server.programs[i]);
          }
        } else {
          allFiles.push(server.programs[i]);
        }
      }
      for (let i = 0; i < server.scripts.length; i++) {
        if (filter) {
          if (server.scripts[i].filename.includes(filter)) {
            allFiles.push(server.scripts[i].filename);
          }
        } else {
          allFiles.push(server.scripts[i].filename);
        }
      }
      for (let i = 0; i < server.messages.length; i++) {
        if (filter) {
          if (server.messages[i] instanceof Message) {
            if (server.messages[i].filename.includes(filter)) {
              allFiles.push(server.messages[i].filename);
            }
          } else if (server.messages[i].includes(filter)) {
            allFiles.push(server.messages[i]);
          }
        } else {
          if (server.messages[i] instanceof Message) {
            allFiles.push(server.messages[i].filename);
          } else {
            allFiles.push(server.messages[i]);
          }
        }
      }

      for (var i = 0; i < server.textFiles.length; i++) {
        if (filter) {
          if (server.textFiles[i].fn.includes(filter)) {
            allFiles.push(server.textFiles[i].fn);
          }
        } else {
          allFiles.push(server.textFiles[i].fn);
        }
      }

      for (var i = 0; i < server.contracts.length; ++i) {
        if (filter) {
          if (server.contracts[i].fn.includes(filter)) {
            allFiles.push(server.contracts[i].fn);
          }
        } else {
          allFiles.push(server.contracts[i].fn);
        }
      }

      // Sort the files alphabetically then print each
      allFiles.sort();
      return allFiles;
    },
    ps: function (ip = workerScript.serverIp) {
      updateDynamicRam("ps", getRamCost("ps"));
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("ps", `Invalid IP/hostname: ${ip}`);
      }
      const processes = [];
      for (const i in server.runningScripts) {
        const script = server.runningScripts[i];
        processes.push({
          filename: script.filename,
          threads: script.threads,
          args: script.args.slice(),
          pid: script.pid,
        });
      }
      return processes;
    },
    hasRootAccess: function (ip) {
      updateDynamicRam("hasRootAccess", getRamCost("hasRootAccess"));
      if (ip === undefined) {
        throw makeRuntimeErrorMsg("hasRootAccess", "Takes 1 argument");
      }
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("hasRootAccess", `Invalid IP/hostname: ${ip}`);
      }
      return server.hasAdminRights;
    },
    getIp: function () {
      updateDynamicRam("getIp", getRamCost("getIp"));
      var scriptServer = getServer(workerScript.serverIp);
      if (scriptServer == null) {
        throw makeRuntimeErrorMsg("getIp", "Could not find server. This is a bug. Report to dev.");
      }
      return scriptServer.ip;
    },
    getHostname: function () {
      updateDynamicRam("getHostname", getRamCost("getHostname"));
      var scriptServer = getServer(workerScript.serverIp);
      if (scriptServer == null) {
        throw makeRuntimeErrorMsg(workerScript, "Could not find server. This is a bug. Report to dev.");
      }
      return scriptServer.hostname;
    },
    getHackingLevel: function () {
      updateDynamicRam("getHackingLevel", getRamCost("getHackingLevel"));
      Player.updateSkillLevels();
      workerScript.log("getHackingLevel", `returned ${Player.hacking_skill}`);
      return Player.hacking_skill;
    },
    getHackingMultipliers: function () {
      updateDynamicRam("getHackingMultipliers", getRamCost("getHackingMultipliers"));
      return {
        chance: Player.hacking_chance_mult,
        speed: Player.hacking_speed_mult,
        money: Player.hacking_money_mult,
        growth: Player.hacking_grow_mult,
      };
    },
    getHacknetMultipliers: function () {
      updateDynamicRam("getHacknetMultipliers", getRamCost("getHacknetMultipliers"));
      return {
        production: Player.hacknet_node_money_mult,
        purchaseCost: Player.hacknet_node_purchase_cost_mult,
        ramCost: Player.hacknet_node_ram_cost_mult,
        coreCost: Player.hacknet_node_core_cost_mult,
        levelCost: Player.hacknet_node_level_cost_mult,
      };
    },
    getBitNodeMultipliers: function () {
      updateDynamicRam("getBitNodeMultipliers", getRamCost("getBitNodeMultipliers"));
      if (SourceFileFlags[5] <= 0 && Player.bitNodeN !== 5) {
        throw makeRuntimeErrorMsg("getBitNodeMultipliers", "Requires Source-File 5 to run.");
      }
      let copy = Object.assign({}, BitNodeMultipliers);
      return copy;
    },
    getServer: function (ip) {
      updateDynamicRam("getServer", getRamCost("getServer"));
      if (SourceFileFlags[5] <= 0 && Player.bitNodeN !== 5) {
        throw makeRuntimeErrorMsg("getServer", "Requires Source-File 5 to run.");
      }
      const server = safeGetServer(ip, "getServer");
      const copy = Object.assign({}, server);
      // These fields should be hidden.
      copy.contracts = undefined;
      copy.messages = undefined;
      copy.runningScripts = undefined;
      copy.scripts = undefined;
      copy.textFiles = undefined;
      copy.programs = undefined;
      copy.serversOnNetwork = undefined;
      return copy;
    },
    getServerMoneyAvailable: function (ip) {
      updateDynamicRam("getServerMoneyAvailable", getRamCost("getServerMoneyAvailable"));
      const server = safeGetServer(ip, "getServerMoneyAvailable");
      if (failOnHacknetServer(server, "getServerMoneyAvailable")) {
        return 0;
      }
      if (server.hostname == "home") {
        // Return player's money
        workerScript.log(
          "getServerMoneyAvailable",
          `returned player's money: ${numeralWrapper.formatMoney(Player.money.toNumber())}`,
        );
        return Player.money.toNumber();
      }
      workerScript.log(
        "getServerMoneyAvailable",
        `returned ${numeralWrapper.formatMoney(server.moneyAvailable)} for '${server.hostname}'`,
      );
      return server.moneyAvailable;
    },
    getServerSecurityLevel: function (ip) {
      updateDynamicRam("getServerSecurityLevel", getRamCost("getServerSecurityLevel"));
      const server = safeGetServer(ip, "getServerSecurityLevel");
      if (failOnHacknetServer(server, "getServerSecurityLevel")) {
        return 1;
      }
      workerScript.log(
        "getServerSecurityLevel",
        `returned ${numeralWrapper.formatServerSecurity(server.hackDifficulty, 3)} for '${server.hostname}'`,
      );
      return server.hackDifficulty;
    },
    getServerBaseSecurityLevel: function (ip) {
      updateDynamicRam("getServerBaseSecurityLevel", getRamCost("getServerBaseSecurityLevel"));
      const server = safeGetServer(ip, "getServerBaseSecurityLevel");
      if (failOnHacknetServer(server, "getServerBaseSecurityLevel")) {
        return 1;
      }
      workerScript.log(
        "getServerBaseSecurityLevel",
        `returned ${numeralWrapper.formatServerSecurity(server.baseDifficulty, 3)} for '${server.hostname}'`,
      );
      return server.baseDifficulty;
    },
    getServerMinSecurityLevel: function (ip) {
      updateDynamicRam("getServerMinSecurityLevel", getRamCost("getServerMinSecurityLevel"));
      const server = safeGetServer(ip, "getServerMinSecurityLevel");
      if (failOnHacknetServer(server, "getServerMinSecurityLevel")) {
        return 1;
      }
      workerScript.log(
        "getServerMinSecurityLevel",
        `returned ${numeralWrapper.formatServerSecurity(server.minDifficulty, 3)} for ${server.hostname}`,
      );
      return server.minDifficulty;
    },
    getServerRequiredHackingLevel: function (ip) {
      updateDynamicRam("getServerRequiredHackingLevel", getRamCost("getServerRequiredHackingLevel"));
      const server = safeGetServer(ip, "getServerRequiredHackingLevel");
      if (failOnHacknetServer(server, "getServerRequiredHackingLevel")) {
        return 1;
      }
      workerScript.log(
        "getServerRequiredHackingLevel",
        `returned ${numeralWrapper.formatSkill(server.requiredHackingSkill, 0)} for '${server.hostname}'`,
      );
      return server.requiredHackingSkill;
    },
    getServerMaxMoney: function (ip) {
      updateDynamicRam("getServerMaxMoney", getRamCost("getServerMaxMoney"));
      const server = safeGetServer(ip, "getServerMaxMoney");
      if (failOnHacknetServer(server, "getServerMaxMoney")) {
        return 0;
      }
      workerScript.log(
        "getServerMaxMoney",
        `returned ${numeralWrapper.formatMoney(server.moneyMax)} for '${server.hostname}'`,
      );
      return server.moneyMax;
    },
    getServerGrowth: function (ip) {
      updateDynamicRam("getServerGrowth", getRamCost("getServerGrowth"));
      const server = safeGetServer(ip, "getServerGrowth");
      if (failOnHacknetServer(server, "getServerGrowth")) {
        return 1;
      }
      workerScript.log("getServerGrowth", `returned ${server.serverGrowth} for '${server.hostname}'`);
      return server.serverGrowth;
    },
    getServerNumPortsRequired: function (ip) {
      updateDynamicRam("getServerNumPortsRequired", getRamCost("getServerNumPortsRequired"));
      const server = safeGetServer(ip, "getServerNumPortsRequired");
      if (failOnHacknetServer(server, "getServerNumPortsRequired")) {
        return 5;
      }
      workerScript.log("getServerNumPortsRequired", `returned ${server.numOpenPortsRequired} for '${server.hostname}'`);
      return server.numOpenPortsRequired;
    },
    getServerRam: function (ip) {
      updateDynamicRam("getServerRam", getRamCost("getServerRam"));
      const server = safeGetServer(ip, "getServerRam");
      workerScript.log(
        "getServerRam",
        `returned [${numeralWrapper.formatRAM(server.maxRam, 2)}, ${numeralWrapper.formatRAM(server.ramUsed, 2)}]`,
      );
      return [server.maxRam, server.ramUsed];
    },
    getServerMaxRam: function (ip) {
      updateDynamicRam("getServerMaxRam", getRamCost("getServerMaxRam"));
      const server = safeGetServer(ip, "getServerMaxRam");
      workerScript.log("getServerMaxRam", `returned ${numeralWrapper.formatRAM(server.maxRam, 2)}`);
      return server.maxRam;
    },
    getServerUsedRam: function (ip) {
      updateDynamicRam("getServerUsedRam", getRamCost("getServerUsedRam"));
      const server = safeGetServer(ip, "getServerUsedRam");
      workerScript.log("getServerUsedRam", `returned ${numeralWrapper.formatRAM(server.ramUsed, 2)}`);
      return server.ramUsed;
    },
    serverExists: function (ip) {
      updateDynamicRam("serverExists", getRamCost("serverExists"));
      return getServer(ip) !== null;
    },
    fileExists: function (filename, ip = workerScript.serverIp) {
      updateDynamicRam("fileExists", getRamCost("fileExists"));
      if (filename === undefined) {
        throw makeRuntimeErrorMsg("fileExists", "Usage: fileExists(scriptname, [server])");
      }
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("fileExists", `Invalid IP/hostname: ${ip}`);
      }
      for (var i = 0; i < server.scripts.length; ++i) {
        if (filename == server.scripts[i].filename) {
          return true;
        }
      }
      for (var i = 0; i < server.programs.length; ++i) {
        if (filename.toLowerCase() == server.programs[i].toLowerCase()) {
          return true;
        }
      }
      for (var i = 0; i < server.messages.length; ++i) {
        if (!(server.messages[i] instanceof Message) && filename.toLowerCase() === server.messages[i]) {
          return true;
        }
      }
      var txtFile = getTextFile(filename, server);
      if (txtFile != null) {
        return true;
      }
      return false;
    },
    isRunning: function (fn, ip = workerScript.serverIp, ...scriptArgs) {
      updateDynamicRam("isRunning", getRamCost("isRunning"));
      if (fn === undefined || ip === undefined) {
        throw makeRuntimeErrorMsg("isRunning", "Usage: isRunning(scriptname, server, [arg1], [arg2]...)");
      }
      if (typeof fn === "number") {
        return getRunningScriptByPid(fn, "isRunning") != null;
      } else {
        return getRunningScript(fn, ip, "isRunning", scriptArgs) != null;
      }
    },
    getStockSymbols: function () {
      updateDynamicRam("getStockSymbols", getRamCost("getStockSymbols"));
      checkTixApiAccess("getStockSymbols");
      return Object.values(StockSymbols);
    },
    getStockPrice: function (symbol) {
      updateDynamicRam("getStockPrice", getRamCost("getStockPrice"));
      checkTixApiAccess("getStockPrice");
      const stock = getStockFromSymbol(symbol, "getStockPrice");

      return stock.price;
    },
    getStockAskPrice: function (symbol) {
      updateDynamicRam("getStockAskPrice", getRamCost("getStockAskPrice"));
      checkTixApiAccess("getStockAskPrice");
      const stock = getStockFromSymbol(symbol, "getStockAskPrice");

      return stock.getAskPrice();
    },
    getStockBidPrice: function (symbol) {
      updateDynamicRam("getStockBidPrice", getRamCost("getStockBidPrice"));
      checkTixApiAccess("getStockBidPrice");
      const stock = getStockFromSymbol(symbol, "getStockBidPrice");

      return stock.getBidPrice();
    },
    getStockPosition: function (symbol) {
      updateDynamicRam("getStockPosition", getRamCost("getStockPosition"));
      checkTixApiAccess("getStockPosition");
      var stock = SymbolToStockMap[symbol];
      if (stock == null) {
        throw makeRuntimeErrorMsg("getStockPosition", `Invalid stock symbol: ${symbol}`);
      }
      return [stock.playerShares, stock.playerAvgPx, stock.playerShortShares, stock.playerAvgShortPx];
    },
    getStockMaxShares: function (symbol) {
      updateDynamicRam("getStockMaxShares", getRamCost("getStockMaxShares"));
      checkTixApiAccess("getStockMaxShares");
      const stock = getStockFromSymbol(symbol, "getStockMaxShares");

      return stock.maxShares;
    },
    getStockPurchaseCost: function (symbol, shares, posType) {
      updateDynamicRam("getStockPurchaseCost", getRamCost("getStockPurchaseCost"));
      checkTixApiAccess("getStockPurchaseCost");
      const stock = getStockFromSymbol(symbol, "getStockPurchaseCost");
      shares = Math.round(shares);

      let pos;
      const sanitizedPosType = posType.toLowerCase();
      if (sanitizedPosType.includes("l")) {
        pos = PositionTypes.Long;
      } else if (sanitizedPosType.includes("s")) {
        pos = PositionTypes.Short;
      } else {
        return Infinity;
      }

      const res = getBuyTransactionCost(stock, shares, pos);
      if (res == null) {
        return Infinity;
      }

      return res;
    },
    getStockSaleGain: function (symbol, shares, posType) {
      updateDynamicRam("getStockSaleGain", getRamCost("getStockSaleGain"));
      checkTixApiAccess("getStockSaleGain");
      const stock = getStockFromSymbol(symbol, "getStockSaleGain");
      shares = Math.round(shares);

      let pos;
      const sanitizedPosType = posType.toLowerCase();
      if (sanitizedPosType.includes("l")) {
        pos = PositionTypes.Long;
      } else if (sanitizedPosType.includes("s")) {
        pos = PositionTypes.Short;
      } else {
        return 0;
      }

      const res = getSellTransactionGain(stock, shares, pos);
      if (res == null) {
        return 0;
      }

      return res;
    },
    buyStock: function (symbol, shares) {
      updateDynamicRam("buyStock", getRamCost("buyStock"));
      checkTixApiAccess("buyStock");
      const stock = getStockFromSymbol(symbol, "buyStock");
      const res = buyStock(stock, shares, workerScript, {
        rerenderFn: displayStockMarketContent,
      });
      return res ? stock.price : 0;
    },
    sellStock: function (symbol, shares) {
      updateDynamicRam("sellStock", getRamCost("sellStock"));
      checkTixApiAccess("sellStock");
      const stock = getStockFromSymbol(symbol, "sellStock");
      const res = sellStock(stock, shares, workerScript, {
        rerenderFn: displayStockMarketContent,
      });

      return res ? stock.price : 0;
    },
    shortStock: function (symbol, shares) {
      updateDynamicRam("shortStock", getRamCost("shortStock"));
      checkTixApiAccess("shortStock");
      if (Player.bitNodeN !== 8) {
        if (SourceFileFlags[8] <= 1) {
          throw makeRuntimeErrorMsg(
            shortStock,
            "You must either be in BitNode-8 or you must have Source-File 8 Level 2.",
          );
        }
      }
      const stock = getStockFromSymbol(symbol, "shortStock");
      const res = shortStock(stock, shares, workerScript, {
        rerenderFn: displayStockMarketContent,
      });

      return res ? stock.price : 0;
    },
    sellShort: function (symbol, shares) {
      updateDynamicRam("sellShort", getRamCost("sellShort"));
      checkTixApiAccess("sellShort");
      if (Player.bitNodeN !== 8) {
        if (SourceFileFlags[8] <= 1) {
          throw makeRuntimeErrorMsg(
            "sellShort",
            "You must either be in BitNode-8 or you must have Source-File 8 Level 2.",
          );
        }
      }
      const stock = getStockFromSymbol(symbol, "sellShort");
      const res = sellShort(stock, shares, workerScript, {
        rerenderFn: displayStockMarketContent,
      });

      return res ? stock.price : 0;
    },
    placeOrder: function (symbol, shares, price, type, pos) {
      updateDynamicRam("placeOrder", getRamCost("placeOrder"));
      checkTixApiAccess("placeOrder");
      if (Player.bitNodeN !== 8) {
        if (SourceFileFlags[8] <= 2) {
          throw makeRuntimeErrorMsg(
            "placeOrder",
            "You must either be in BitNode-8 or you must have Source-File 8 Level 3.",
          );
        }
      }
      const stock = getStockFromSymbol(symbol, "placeOrder");

      let orderType, orderPos;
      ltype = type.toLowerCase();
      if (ltype.includes("limit") && ltype.includes("buy")) {
        orderType = OrderTypes.LimitBuy;
      } else if (ltype.includes("limit") && ltype.includes("sell")) {
        orderType = OrderTypes.LimitSell;
      } else if (ltype.includes("stop") && ltype.includes("buy")) {
        orderType = OrderTypes.StopBuy;
      } else if (ltype.includes("stop") && ltype.includes("sell")) {
        orderType = OrderTypes.StopSell;
      } else {
        throw makeRuntimeErrorMsg("placeOrder", `Invalid order type: ${type}`);
      }

      lpos = pos.toLowerCase();
      if (lpos.includes("l")) {
        orderPos = PositionTypes.Long;
      } else if (lpos.includes("s")) {
        orderPos = PositionTypes.Short;
      } else {
        throw makeRuntimeErrorMsg("placeOrder", `Invalid position type: ${pos}`);
      }

      return placeOrder(stock, shares, price, orderType, orderPos, workerScript);
    },
    cancelOrder: function (symbol, shares, price, type, pos) {
      updateDynamicRam("cancelOrder", getRamCost("cancelOrder"));
      checkTixApiAccess("cancelOrder");
      if (Player.bitNodeN !== 8) {
        if (SourceFileFlags[8] <= 2) {
          throw makeRuntimeErrorMsg(
            "cancelOrder",
            "You must either be in BitNode-8 or you must have Source-File 8 Level 3.",
          );
        }
      }
      const stock = getStockFrom(symbol, "cancelOrder");
      if (isNaN(shares) || isNaN(price)) {
        throw makeRuntimeErrorMsg(
          "cancelOrder",
          `Invalid shares or price. Must be numeric. shares=${shares}, price=${price}`,
        );
      }
      var orderType, orderPos;
      ltype = type.toLowerCase();
      if (ltype.includes("limit") && ltype.includes("buy")) {
        orderType = OrderTypes.LimitBuy;
      } else if (ltype.includes("limit") && ltype.includes("sell")) {
        orderType = OrderTypes.LimitSell;
      } else if (ltype.includes("stop") && ltype.includes("buy")) {
        orderType = OrderTypes.StopBuy;
      } else if (ltype.includes("stop") && ltype.includes("sell")) {
        orderType = OrderTypes.StopSell;
      } else {
        throw makeRuntimeErrorMsg("cancelOrder", `Invalid order type: ${type}`);
      }

      lpos = pos.toLowerCase();
      if (lpos.includes("l")) {
        orderPos = PositionTypes.Long;
      } else if (lpos.includes("s")) {
        orderPos = PositionTypes.Short;
      } else {
        throw makeRuntimeErrorMsg("cancelOrder", `Invalid position type: ${pos}`);
      }
      var params = {
        stock: stock,
        shares: shares,
        price: price,
        type: orderType,
        pos: orderPos,
      };
      return cancelOrder(params, workerScript);
    },
    getOrders: function () {
      updateDynamicRam("getOrders", getRamCost("getOrders"));
      checkTixApiAccess("getOrders");
      if (Player.bitNodeN !== 8) {
        if (SourceFileFlags[8] <= 2) {
          throw makeRuntimeErrorMsg(workerScript, "You must either be in BitNode-8 or have Source-File 8 Level 3.");
        }
      }

      const orders = {};

      const stockMarketOrders = StockMarket["Orders"];
      for (let symbol in stockMarketOrders) {
        const orderBook = stockMarketOrders[symbol];
        if (orderBook.constructor === Array && orderBook.length > 0) {
          orders[symbol] = [];
          for (let i = 0; i < orderBook.length; ++i) {
            orders[symbol].push({
              shares: orderBook[i].shares,
              price: orderBook[i].price,
              type: orderBook[i].type,
              position: orderBook[i].pos,
            });
          }
        }
      }

      return orders;
    },
    getStockVolatility: function (symbol) {
      updateDynamicRam("getStockVolatility", getRamCost("getStockVolatility"));
      if (!Player.has4SDataTixApi) {
        throw makeRuntimeErrorMsg("getStockVolatility", "You don't have 4S Market Data TIX API Access!");
      }
      const stock = getStockFromSymbol(symbol, "getStockVolatility");

      return stock.mv / 100; // Convert from percentage to decimal
    },
    getStockForecast: function (symbol) {
      updateDynamicRam("getStockForecast", getRamCost("getStockForecast"));
      if (!Player.has4SDataTixApi) {
        throw makeRuntimeErrorMsg("getStockForecast", "You don't have 4S Market Data TIX API Access!");
      }
      const stock = getStockFromSymbol(symbol, "getStockForecast");

      var forecast = 50;
      stock.b ? (forecast += stock.otlkMag) : (forecast -= stock.otlkMag);
      return forecast / 100; // Convert from percentage to decimal
    },
    purchase4SMarketData: function () {
      updateDynamicRam("purchase4SMarketData", getRamCost("purchase4SMarketData"));
      checkTixApiAccess("purchase4SMarketData");

      if (Player.has4SData) {
        workerScript.log("purchase4SMarketData", "Already purchased 4S Market Data.");
        return true;
      }

      if (Player.money.lt(getStockMarket4SDataCost())) {
        workerScript.log("purchase4SMarketData", "Not enough money to purchase 4S Market Data.");
        return false;
      }

      Player.has4SData = true;
      Player.loseMoney(getStockMarket4SDataCost());
      workerScript.log("purchase4SMarketData", "Purchased 4S Market Data");
      displayStockMarketContent();
      return true;
    },
    purchase4SMarketDataTixApi: function () {
      updateDynamicRam("purchase4SMarketDataTixApi", getRamCost("purchase4SMarketDataTixApi"));
      checkTixApiAccess("purchase4SMarketDataTixApi");

      if (Player.has4SDataTixApi) {
        workerScript.log("purchase4SMarketDataTixApi", "Already purchased 4S Market Data TIX API");
        return true;
      }

      if (Player.money.lt(getStockMarket4STixApiCost())) {
        workerScript.log("purchase4SMarketDataTixApi", "Not enough money to purchase 4S Market Data TIX API");
        return false;
      }

      Player.has4SDataTixApi = true;
      Player.loseMoney(getStockMarket4STixApiCost());
      workerScript.log("purchase4SMarketDataTixApi", "Purchased 4S Market Data TIX API");
      displayStockMarketContent();
      return true;
    },
    getPurchasedServerLimit: function () {
      updateDynamicRam("getPurchasedServerLimit", getRamCost("getPurchasedServerLimit"));

      return getPurchaseServerLimit();
    },
    getPurchasedServerMaxRam: function () {
      updateDynamicRam("getPurchasedServerMaxRam", getRamCost("getPurchasedServerMaxRam"));

      return getPurchaseServerMaxRam();
    },
    getPurchasedServerCost: function (ram) {
      updateDynamicRam("getPurchasedServerCost", getRamCost("getPurchasedServerCost"));

      const cost = getPurchaseServerCost(ram);
      if (cost === Infinity) {
        workerScript.log("getPurchasedServerCost", `Invalid argument: ram='${ram}'`);
        return Infinity;
      }

      return cost;
    },
    purchaseServer: function (hostname, ram) {
      updateDynamicRam("purchaseServer", getRamCost("purchaseServer"));
      var hostnameStr = String(hostname);
      hostnameStr = hostnameStr.replace(/\s+/g, "");
      if (hostnameStr == "") {
        workerScript.log("purchaseServer", `Invalid argument: hostname='${hostnameStr}'`);
        return "";
      }

      if (Player.purchasedServers.length >= getPurchaseServerLimit()) {
        workerScript.log(
          "purchaseServer",
          `You have reached the maximum limit of ${getPurchaseServerLimit()} servers. You cannot purchase any more.`,
        );
        return "";
      }

      const cost = getPurchaseServerCost(ram);
      if (cost === Infinity) {
        workerScript.log("purchaseServer", `Invalid argument: ram='${ram}'`);
        return "";
      }

      if (Player.money.lt(cost)) {
        workerScript.log(
          "purchaseServer",
          `Not enough money to purchase server. Need ${numeralWrapper.formatMoney(cost)}`,
        );
        return "";
      }
      var newServ = safetlyCreateUniqueServer({
        ip: createUniqueRandomIp(),
        hostname: hostnameStr,
        organizationName: "",
        isConnectedTo: false,
        adminRights: true,
        purchasedByPlayer: true,
        maxRam: ram,
      });
      AddToAllServers(newServ);

      Player.purchasedServers.push(newServ.ip);
      var homeComputer = Player.getHomeComputer();
      homeComputer.serversOnNetwork.push(newServ.ip);
      newServ.serversOnNetwork.push(homeComputer.ip);
      Player.loseMoney(cost);
      workerScript.log(
        "purchaseServer",
        `Purchased new server with hostname '${newServ.hostname}' for ${numeralWrapper.formatMoney(cost)}`,
      );
      return newServ.hostname;
    },
    deleteServer: function (hostname) {
      updateDynamicRam("deleteServer", getRamCost("deleteServer"));
      var hostnameStr = String(hostname);
      hostnameStr = hostnameStr.replace(/\s\s+/g, "");
      var server = GetServerByHostname(hostnameStr);
      if (server == null) {
        workerScript.log("deleteServer", `Invalid argument: hostname='${hostnameStr}'`);
        return false;
      }

      if (!server.purchasedByPlayer || server.hostname === "home") {
        workerScript.log("deleteServer", "Cannot delete non-purchased server.");
        return false;
      }

      var ip = server.ip;

      // Can't delete server you're currently connected to
      if (server.isConnectedTo) {
        workerScript.log("deleteServer", "You are currently connected to the server you are trying to delete.");
        return false;
      }

      // A server cannot delete itself
      if (ip === workerScript.serverIp) {
        workerScript.log("deleteServer", "Cannot delete the server this script is running on.");
        return false;
      }

      // Delete all scripts running on server
      if (server.runningScripts.length > 0) {
        workerScript.log(
          "deleteServer",
          `Cannot delete server '${server.hostname}' because it still has scripts running.`,
        );
        return false;
      }

      // Delete from player's purchasedServers array
      var found = false;
      for (var i = 0; i < Player.purchasedServers.length; ++i) {
        if (ip == Player.purchasedServers[i]) {
          found = true;
          Player.purchasedServers.splice(i, 1);
          break;
        }
      }

      if (!found) {
        workerScript.log(
          "deleteServer",
          `Could not identify server ${server.hostname} as a purchased server. This is a bug. Report to dev.`,
        );
        return false;
      }

      // Delete from all servers
      delete AllServers[ip];

      // Delete from home computer
      found = false;
      var homeComputer = Player.getHomeComputer();
      for (var i = 0; i < homeComputer.serversOnNetwork.length; ++i) {
        if (ip == homeComputer.serversOnNetwork[i]) {
          homeComputer.serversOnNetwork.splice(i, 1);
          workerScript.log("deleteServer", `Deleted server '${hostnameStr}`);
          return true;
        }
      }
      // Wasn't found on home computer
      workerScript.log(
        "deleteServer",
        `Could not find server ${server.hostname} as a purchased server. This is a bug. Report to dev.`,
      );
      return false;
    },
    getPurchasedServers: function (hostname = true) {
      updateDynamicRam("getPurchasedServers", getRamCost("getPurchasedServers"));
      var res = [];
      Player.purchasedServers.forEach(function (ip) {
        if (hostname) {
          var server = getServer(ip);
          if (server == null) {
            throw makeRuntimeErrorMsg("getPurchasedServers", "Could not find server. This is a bug. Report to dev.");
          }
          res.push(server.hostname);
        } else {
          res.push(ip);
        }
      });
      return res;
    },
    write: function (port, data = "", mode = "a") {
      updateDynamicRam("write", getRamCost("write"));
      if (!isNaN(port)) {
        // Write to port
        // Port 1-10
        port = Math.round(port);
        if (port < 1 || port > CONSTANTS.NumNetscriptPorts) {
          throw makeRuntimeErrorMsg(
            "write",
            `Trying to write to invalid port: ${port}. Only ports 1-${CONSTANTS.NumNetscriptPorts} are valid.`,
          );
        }
        var port = NetscriptPorts[port - 1];
        if (port == null || !(port instanceof NetscriptPort)) {
          throw makeRuntimeErrorMsg("write", `Could not find port: ${port}. This is a bug. Report to dev.`);
        }
        return port.write(data);
      } else if (isString(port)) {
        // Write to script or text file
        let fn = port;
        if (!isValidFilePath(fn)) {
          throw makeRuntimeErrorMsg("write", `Invalid filepath: ${fn}`);
        }

        if (fn.lastIndexOf("/") === 0) {
          fn = removeLeadingSlash(fn);
        }

        // Coerce 'data' to be a string
        try {
          data = String(data);
        } catch (e) {
          throw makeRuntimeErrorMsg("write", `Invalid data (${e}). Data being written must be convertible to a string`);
        }

        const server = workerScript.getServer();
        if (server == null) {
          throw makeRuntimeErrorMsg("write", "Error getting Server. This is a bug. Report to dev.");
        }
        if (isScriptFilename(fn)) {
          // Write to script
          let script = workerScript.getScriptOnServer(fn);
          if (script == null) {
            // Create a new script
            script = new Script(fn, data, server.ip, server.scripts);
            server.scripts.push(script);
            return true;
          }
          mode === "w" ? (script.code = data) : (script.code += data);
          script.updateRamUsage(server.scripts);
          script.markUpdated();
        } else {
          // Write to text file
          let txtFile = getTextFile(fn, server);
          if (txtFile == null) {
            txtFile = createTextFile(fn, data, server);
            return true;
          }
          if (mode === "w") {
            txtFile.write(data);
          } else {
            txtFile.append(data);
          }
        }
        return true;
      } else {
        throw makeRuntimeErrorMsg("write", `Invalid argument: ${port}`);
      }
    },
    tryWrite: function (port, data = "") {
      updateDynamicRam("tryWrite", getRamCost("tryWrite"));
      if (!isNaN(port)) {
        port = Math.round(port);
        if (port < 1 || port > CONSTANTS.NumNetscriptPorts) {
          throw makeRuntimeErrorMsg(
            "tryWrite",
            `Invalid port: ${port}. Only ports 1-${CONSTANTS.NumNetscriptPorts} are valid.`,
          );
        }
        var port = NetscriptPorts[port - 1];
        if (port == null || !(port instanceof NetscriptPort)) {
          throw makeRuntimeErrorMsg("tryWrite", `Could not find port: ${port}. This is a bug. Report to dev.`);
        }
        return port.tryWrite(data);
      } else {
        throw makeRuntimeErrorMsg("tryWrite", `Invalid argument: ${port}`);
      }
    },
    read: function (port) {
      updateDynamicRam("read", getRamCost("read"));
      if (!isNaN(port)) {
        // Read from port
        // Port 1-10
        port = Math.round(port);
        if (port < 1 || port > CONSTANTS.NumNetscriptPorts) {
          throw makeRuntimeErrorMsg(
            "read",
            `Invalid port: ${port}. Only ports 1-${CONSTANTS.NumNetscriptPorts} are valid.`,
          );
        }
        var port = NetscriptPorts[port - 1];
        if (port == null || !(port instanceof NetscriptPort)) {
          throw makeRuntimeErrorMsg("read", `Could not find port: ${port}. This is a bug. Report to dev.`);
        }
        return port.read();
      } else if (isString(port)) {
        // Read from script or text file
        let fn = port;
        let server = getServer(workerScript.serverIp);
        if (server == null) {
          throw makeRuntimeErrorMsg("read", "Error getting Server. This is a bug. Report to dev.");
        }
        if (isScriptFilename(fn)) {
          // Read from script
          let script = workerScript.getScriptOnServer(fn);
          if (script == null) {
            return "";
          }
          return script.code;
        } else {
          // Read from text file
          let txtFile = getTextFile(fn, server);
          if (txtFile !== null) {
            return txtFile.text;
          } else {
            return "";
          }
        }
      } else {
        throw makeRuntimeErrorMsg("read", `Invalid argument: ${port}`);
      }
    },
    peek: function (port) {
      updateDynamicRam("peek", getRamCost("peek"));
      if (isNaN(port)) {
        throw makeRuntimeErrorMsg(
          "peek",
          `Invalid argument. Must be a port number between 1 and ${CONSTANTS.NumNetscriptPorts}, is ${port}`,
        );
      }
      port = Math.round(port);
      if (port < 1 || port > CONSTANTS.NumNetscriptPorts) {
        throw makeRuntimeErrorMsg(
          "peek",
          `Invalid argument. Must be a port number between 1 and ${CONSTANTS.NumNetscriptPorts}, is ${port}`,
        );
      }
      var port = NetscriptPorts[port - 1];
      if (port == null || !(port instanceof NetscriptPort)) {
        throw makeRuntimeErrorMsg("peek", `Could not find port: ${port}. This is a bug. Report to dev.`);
      }
      return port.peek();
    },
    clear: function (port) {
      updateDynamicRam("clear", getRamCost("clear"));
      if (!isNaN(port)) {
        // Clear port
        port = Math.round(port);
        if (port < 1 || port > CONSTANTS.NumNetscriptPorts) {
          throw makeRuntimeErrorMsg(
            "clear",
            `Trying to clear invalid port: ${port}. Only ports 1-${CONSTANTS.NumNetscriptPorts} are valid`,
          );
        }
        var port = NetscriptPorts[port - 1];
        if (port == null || !(port instanceof NetscriptPort)) {
          throw makeRuntimeErrorMsg("clear", `Could not find port: ${port}. This is a bug. Report to dev.`);
        }
        return port.clear();
      } else if (isString(port)) {
        // Clear text file
        var fn = port;
        var server = getServer(workerScript.serverIp);
        if (server == null) {
          throw makeRuntimeErrorMsg("clear", "Error getting Server. This is a bug. Report to dev.");
        }
        var txtFile = getTextFile(fn, server);
        if (txtFile != null) {
          txtFile.write("");
        }
      } else {
        throw makeRuntimeErrorMsg("clear", `Invalid argument: ${port}`);
      }
      return 0;
    },
    getPortHandle: function (port) {
      updateDynamicRam("getPortHandle", getRamCost("getPortHandle"));
      if (isNaN(port)) {
        throw makeRuntimeErrorMsg(
          "getPortHandle",
          `Invalid port: ${port} Must be an integer between 1 and ${CONSTANTS.NumNetscriptPorts}.`,
        );
      }
      port = Math.round(port);
      if (port < 1 || port > CONSTANTS.NumNetscriptPorts) {
        throw makeRuntimeErrorMsg(
          "getPortHandle",
          `Invalid port: ${port}. Only ports 1-${CONSTANTS.NumNetscriptPorts} are valid.`,
        );
      }
      var port = NetscriptPorts[port - 1];
      if (port == null || !(port instanceof NetscriptPort)) {
        throw makeRuntimeErrorMsg("getPortHandle", `Could not find port: ${port}. This is a bug. Report to dev.`);
      }
      return port;
    },
    rm: function (fn, ip) {
      updateDynamicRam("rm", getRamCost("rm"));

      if (ip == null || ip === "") {
        ip = workerScript.serverIp;
      }
      const s = safeGetServer(ip, "rm");

      const status = s.removeFile(fn);
      if (!status.res) {
        workerScript.log("rm", status.msg);
      }

      return status.res;
    },
    scriptRunning: function (scriptname, ip) {
      updateDynamicRam("scriptRunning", getRamCost("scriptRunning"));
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("scriptRunning", `Invalid IP/hostname: ${ip}`);
      }
      for (var i = 0; i < server.runningScripts.length; ++i) {
        if (server.runningScripts[i].filename == scriptname) {
          return true;
        }
      }
      return false;
    },
    scriptKill: function (scriptname, ip) {
      updateDynamicRam("scriptKill", getRamCost("scriptKill"));
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("scriptKill", `Invalid IP/hostname: ${ip}`);
      }
      var suc = false;
      for (var i = 0; i < server.runningScripts.length; ++i) {
        if (server.runningScripts[i].filename == scriptname) {
          killWorkerScript(server.runningScripts[i], server.ip);
          suc = true;
        }
      }
      return suc;
    },
    getScriptName: function () {
      return workerScript.name;
    },
    getScriptRam: function (scriptname, ip = workerScript.serverIp) {
      updateDynamicRam("getScriptRam", getRamCost("getScriptRam"));
      var server = getServer(ip);
      if (server == null) {
        throw makeRuntimeErrorMsg("getScriptRam", `Invalid IP/hostname: ${ip}`);
      }
      for (var i = 0; i < server.scripts.length; ++i) {
        if (server.scripts[i].filename == scriptname) {
          return server.scripts[i].ramUsage;
        }
      }
      return 0;
    },
    getRunningScript: function (fn, ip) {
      updateDynamicRam("getRunningScript", getRamCost("getRunningScript"));

      let runningScript;
      if (arguments.length === 0) {
        runningScript = workerScript.scriptRef;
      } else if (typeof fn === "number") {
        runningScript = getRunningScriptByPid(fn, "getRunningScript");
      } else {
        const scriptArgs = [];
        for (var i = 2; i < arguments.length; ++i) {
          scriptArgs.push(arguments[i]);
        }
        runningScript = getRunningScript(fn, ip, "getRunningScript", scriptArgs);
      }
      if (runningScript === null) return null;
      return {
        args: runningScript.args.slice(),
        filename: runningScript.filename,
        logs: runningScript.logs.slice(),
        offlineExpGained: runningScript.offlineExpGained,
        offlineMoneyMade: runningScript.offlineMoneyMade,
        offlineRunningTime: runningScript.offlineRunningTime,
        onlineExpGained: runningScript.onlineExpGained,
        onlineMoneyMade: runningScript.onlineMoneyMade,
        onlineRunningTime: runningScript.onlineRunningTime,
        pid: runningScript.pid,
        ramUsage: runningScript.ramUsage,
        server: runningScript.server,
        threads: runningScript.threads,
      };
    },
    getHackTime: function (ip) {
      updateDynamicRam("getHackTime", getRamCost("getHackTime"));
      const server = safeGetServer(ip, "getHackTime");
      if (failOnHacknetServer(server, "getHackTime")) {
        return Infinity;
      }

      return calculateHackingTime(server, Player); // Returns seconds
    },
    getGrowTime: function (ip) {
      updateDynamicRam("getGrowTime", getRamCost("getGrowTime"));
      const server = safeGetServer(ip, "getGrowTime");
      if (failOnHacknetServer(server, "getGrowTime")) {
        return Infinity;
      }

      return calculateGrowTime(server, Player); // Returns seconds
    },
    getWeakenTime: function (ip) {
      updateDynamicRam("getWeakenTime", getRamCost("getWeakenTime"));
      const server = safeGetServer(ip, "getWeakenTime");
      if (failOnHacknetServer(server, "getWeakenTime")) {
        return Infinity;
      }

      return calculateWeakenTime(server, Player); // Returns seconds
    },
    getScriptIncome: function (scriptname, ip) {
      updateDynamicRam("getScriptIncome", getRamCost("getScriptIncome"));
      if (arguments.length === 0) {
        var res = [];

        // First element is total income of all currently running scripts
        let total = 0;
        for (const script of workerScripts.values()) {
          total += script.scriptRef.onlineMoneyMade / script.scriptRef.onlineRunningTime;
        }
        res.push(total);

        // Second element is total income you've earned from scripts since you installed Augs
        res.push(Player.scriptProdSinceLastAug / (Player.playtimeSinceLastAug / 1000));
        return res;
      } else {
        // Get income for a particular script
        var server = getServer(ip);
        if (server == null) {
          throw makeRuntimeErrorMsg("getScriptIncome", `Invalid IP/hostnamed: ${ip}`);
        }
        var argsForScript = [];
        for (var i = 2; i < arguments.length; ++i) {
          argsForScript.push(arguments[i]);
        }
        var runningScriptObj = findRunningScript(scriptname, argsForScript, server);
        if (runningScriptObj == null) {
          workerScript.log(
            "getScriptIncome",
            `No such script '${scriptname}' on '${server.hostname}' with args: ${arrayToString(argsForScript)}`,
          );
          return -1;
        }
        return runningScriptObj.onlineMoneyMade / runningScriptObj.onlineRunningTime;
      }
    },
    getScriptExpGain: function (scriptname, ip) {
      updateDynamicRam("getScriptExpGain", getRamCost("getScriptExpGain"));
      if (arguments.length === 0) {
        var total = 0;
        for (const ws of workerScripts.values()) {
          total += ws.scriptRef.onlineExpGained / ws.scriptRef.onlineRunningTime;
        }
        return total;
      } else {
        // Get income for a particular script
        var server = getServer(ip);
        if (server == null) {
          throw makeRuntimeErrorMsg("getScriptExpGain", `Invalid IP/hostnamed: ${ip}`);
        }
        var argsForScript = [];
        for (var i = 2; i < arguments.length; ++i) {
          argsForScript.push(arguments[i]);
        }
        var runningScriptObj = findRunningScript(scriptname, argsForScript, server);
        if (runningScriptObj == null) {
          workerScript.log(
            "getScriptExpGain",
            `No such script '${scriptname}' on '${server.hostname}' with args: ${arrayToString(argsForScript)}`,
          );
          return -1;
        }
        return runningScriptObj.onlineExpGained / runningScriptObj.onlineRunningTime;
      }
    },
    nFormat: function (n, format) {
      if (isNaN(n) || isNaN(parseFloat(n)) || typeof format !== "string") {
        return "";
      }

      return numeralWrapper.format(parseFloat(n), format);
    },
    tFormat: function (milliseconds, milliPrecision = false) {
      return convertTimeMsToTimeElapsedString(milliseconds, milliPrecision);
    },
    getTimeSinceLastAug: function () {
      updateDynamicRam("getTimeSinceLastAug", getRamCost("getTimeSinceLastAug"));
      return Player.playtimeSinceLastAug;
    },
    prompt: function (txt) {
      if (!isString(txt)) {
        txt = JSON.stringify(txt);
      }

      // The id for this popup will consist of the first 20 characters of the prompt string..
      // Thats hopefully good enough to be unique
      const popupId = `prompt-popup-${txt.slice(0, 20)}`;
      const textElement = createElement("p", { innerHTML: txt });

      return new Promise(function (resolve) {
        const yesBtn = createElement("button", {
          class: "popup-box-button",
          innerText: "Yes",
          clickListener: () => {
            removeElementById(popupId);
            resolve(true);
          },
        });

        const noBtn = createElement("button", {
          class: "popup-box-button",
          innerText: "No",
          clickListener: () => {
            removeElementById(popupId);
            resolve(false);
          },
        });

        createPopup(popupId, [textElement, yesBtn, noBtn]);
      });
    },
    wget: async function (url, target, ip = workerScript.serverIp) {
      if (!isScriptFilename(target) && !target.endsWith(".txt")) {
        workerScript.log("wget", `Invalid target file: '${target}'. Must be a script or text file.`);
        return Promise.resolve(false);
      }
      var s = safeGetServer(ip, "wget");
      return new Promise(function (resolve) {
        $.get(
          url,
          function (data) {
            let res;
            if (isScriptFilename(target)) {
              res = s.writeToScriptFile(target, data);
            } else {
              res = s.writeToTextFile(target, data);
            }
            if (!res.success) {
              workerScript.log("wget", "Failed.");
              return resolve(false);
            }
            if (res.overwritten) {
              workerScript.log("wget", `Successfully retrieved content and overwrote '${target}' on '${ip}'`);
              return resolve(true);
            }
            workerScript.log("wget", `Successfully retrieved content to new file '${target}' on '${ip}'`);
            return resolve(true);
          },
          "text",
        ).fail(function (e) {
          workerScript.log("wget", JSON.stringify(e));
          return resolve(false);
        });
      });
    },
    getFavorToDonate: function () {
      updateDynamicRam("getFavorToDonate", getRamCost("getFavorToDonate"));
      return Math.floor(CONSTANTS.BaseFavorToDonate * BitNodeMultipliers.RepToDonateToFaction);
    },

    /* Singularity Functions */
    universityCourse: function (universityName, className) {
      updateDynamicRam("universityCourse", getRamCost("universityCourse"));
      checkSingularityAccess("universityCourse", 1);
      if (inMission) {
        workerScript.log("universityCourse", "You are in the middle of a mission.");
        return;
      }
      if (Player.isWorking) {
        var txt = Player.singularityStopWork();
        workerScript.log("universityCourse", txt);
      }

      var costMult, expMult;
      switch (universityName.toLowerCase()) {
        case LocationName.AevumSummitUniversity.toLowerCase():
          if (Player.city != CityName.Aevum) {
            workerScript.log(
              "universityCourse",
              "You cannot study at 'Summit University' because you are not in 'Aevum'.",
            );
            return false;
          }
          Player.gotoLocation(LocationName.AevumSummitUniversity);
          costMult = 4;
          expMult = 3;
          break;
        case LocationName.Sector12RothmanUniversity.toLowerCase():
          if (Player.city != CityName.Sector12) {
            workerScript.log(
              "universityCourse",
              "You cannot study at 'Rothman University' because you are not in 'Sector-12'.",
            );
            return false;
          }
          Player.location = LocationName.Sector12RothmanUniversity;
          costMult = 3;
          expMult = 2;
          break;
        case LocationName.VolhavenZBInstituteOfTechnology.toLowerCase():
          if (Player.city != CityName.Volhaven) {
            workerScript.log(
              "universityCourse",
              "You cannot study at 'ZB Institute of Technology' because you are not in 'Volhaven'.",
            );
            return false;
          }
          Player.location = LocationName.VolhavenZBInstituteOfTechnology;
          costMult = 5;
          expMult = 4;
          break;
        default:
          workerScript.log("universityCourse", `Invalid university name: '${universityName}'.`);
          return false;
      }

      var task;
      switch (className.toLowerCase()) {
        case "Study Computer Science".toLowerCase():
          task = CONSTANTS.ClassStudyComputerScience;
          break;
        case "Data Structures".toLowerCase():
          task = CONSTANTS.ClassDataStructures;
          break;
        case "Networks".toLowerCase():
          task = CONSTANTS.ClassNetworks;
          break;
        case "Algorithms".toLowerCase():
          task = CONSTANTS.ClassAlgorithms;
          break;
        case "Management".toLowerCase():
          task = CONSTANTS.ClassManagement;
          break;
        case "Leadership".toLowerCase():
          task = CONSTANTS.ClassLeadership;
          break;
        default:
          workerScript.log("universityCourse", `Invalid class name: ${className}.`);
          return false;
      }
      Player.startClass(costMult, expMult, task);
      workerScript.log("universityCourse", `Started ${task} at ${universityName}`);
      return true;
    },

    gymWorkout: function (gymName, stat) {
      updateDynamicRam("gymWorkout", getRamCost("gymWorkout"));
      checkSingularityAccess("gymWorkout", 1);
      if (inMission) {
        workerScript.log("gymWorkout", "You are in the middle of a mission.");
        return;
      }
      if (Player.isWorking) {
        var txt = Player.singularityStopWork();
        workerScript.log("gymWorkout", txt);
      }
      var costMult, expMult;
      switch (gymName.toLowerCase()) {
        case LocationName.AevumCrushFitnessGym.toLowerCase():
          if (Player.city != CityName.Aevum) {
            workerScript.log("gymWorkout", "You cannot workout at 'Crush Fitness' because you are not in 'Aevum'.");
            return false;
          }
          Player.location = LocationName.AevumCrushFitnessGym;
          costMult = 3;
          expMult = 2;
          break;
        case LocationName.AevumSnapFitnessGym.toLowerCase():
          if (Player.city != CityName.Aevum) {
            workerScript.log("gymWorkout", "You cannot workout at 'Snap Fitness' because you are not in 'Aevum'.");
            return false;
          }
          Player.location = LocationName.AevumSnapFitnessGym;
          costMult = 10;
          expMult = 5;
          break;
        case LocationName.Sector12IronGym.toLowerCase():
          if (Player.city != CityName.Sector12) {
            workerScript.log("gymWorkout", "You cannot workout at 'Iron Gym' because you are not in 'Sector-12'.");
            return false;
          }
          Player.location = LocationName.Sector12IronGym;
          costMult = 1;
          expMult = 1;
          break;
        case LocationName.Sector12PowerhouseGym.toLowerCase():
          if (Player.city != CityName.Sector12) {
            workerScript.log(
              "gymWorkout",
              "You cannot workout at 'Powerhouse Gym' because you are not in 'Sector-12'.",
            );
            return false;
          }
          Player.location = LocationName.Sector12PowerhouseGym;
          costMult = 20;
          expMult = 10;
          break;
        case LocationName.VolhavenMilleniumFitnessGym.toLowerCase():
          if (Player.city != CityName.Volhaven) {
            workerScript.log(
              "gymWorkout",
              "You cannot workout at 'Millenium Fitness Gym' because you are not in 'Volhaven'.",
            );
            return false;
          }
          Player.location = LocationName.VolhavenMilleniumFitnessGym;
          costMult = 7;
          expMult = 4;
          break;
        default:
          workerScript.log("gymWorkout", `Invalid gym name: ${gymName}. gymWorkout() failed`);
          return false;
      }

      switch (stat.toLowerCase()) {
        case "strength".toLowerCase():
        case "str".toLowerCase():
          Player.startClass(costMult, expMult, CONSTANTS.ClassGymStrength);
          break;
        case "defense".toLowerCase():
        case "def".toLowerCase():
          Player.startClass(costMult, expMult, CONSTANTS.ClassGymDefense);
          break;
        case "dexterity".toLowerCase():
        case "dex".toLowerCase():
          Player.startClass(costMult, expMult, CONSTANTS.ClassGymDexterity);
          break;
        case "agility".toLowerCase():
        case "agi".toLowerCase():
          Player.startClass(costMult, expMult, CONSTANTS.ClassGymAgility);
          break;
        default:
          workerScript.log("gymWorkout", `Invalid stat: ${stat}.`);
          return false;
      }
      workerScript.log("gymWorkout", `Started training ${stat} at ${gymName}`);
      return true;
    },

    travelToCity: function (cityname) {
      updateDynamicRam("travelToCity", getRamCost("travelToCity"));
      checkSingularityAccess("travelToCity", 1);

      switch (cityname) {
        case CityName.Aevum:
        case CityName.Chongqing:
        case CityName.Sector12:
        case CityName.NewTokyo:
        case CityName.Ishima:
        case CityName.Volhaven:
          if (Player.money.lt(CONSTANTS.TravelCost)) {
            throw makeRuntimeErrorMsg("travelToCity", "Not enough money to travel.");
          }
          Player.loseMoney(CONSTANTS.TravelCost);
          Player.city = cityname;
          workerScript.log("travelToCity", `Traveled to ${cityname}`);
          return true;
        default:
          workerScript.log("travelToCity", `Invalid city name: '${cityname}'.`);
          return false;
      }
    },

    purchaseTor: function () {
      updateDynamicRam("purchaseTor", getRamCost("purchaseTor"));
      checkSingularityAccess("purchaseTor", 1);

      if (SpecialServerIps["Darkweb Server"] != null) {
        workerScript.log("purchaseTor", "You already have a TOR router!");
        return false;
      }

      if (Player.money.lt(CONSTANTS.TorRouterCost)) {
        workerScript.log("purchaseTor", "You cannot afford to purchase a Tor router.");
        return false;
      }
      Player.loseMoney(CONSTANTS.TorRouterCost);

      var darkweb = safetlyCreateUniqueServer({
        ip: createUniqueRandomIp(),
        hostname: "darkweb",
        organizationName: "",
        isConnectedTo: false,
        adminRights: false,
        purchasedByPlayer: false,
        maxRam: 1,
      });
      AddToAllServers(darkweb);
      SpecialServerIps.addIp("Darkweb Server", darkweb.ip);

      Player.getHomeComputer().serversOnNetwork.push(darkweb.ip);
      darkweb.serversOnNetwork.push(Player.getHomeComputer().ip);
      Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain);
      workerScript.log("purchaseTor", "You have purchased a Tor router!");
      return true;
    },
    purchaseProgram: function (programName) {
      updateDynamicRam("purchaseProgram", getRamCost("purchaseProgram"));
      checkSingularityAccess("purchaseProgram", 1);

      if (SpecialServerIps["Darkweb Server"] == null) {
        workerScript.log("purchaseProgram", "You do not have the TOR router.");
        return false;
      }

      programName = programName.toLowerCase();

      let item = null;
      for (const key in DarkWebItems) {
        const i = DarkWebItems[key];
        if (i.program.toLowerCase() == programName) {
          item = i;
        }
      }

      if (item == null) {
        workerScript.log("purchaseProgram", `Invalid program name: '${programName}.`);
        return false;
      }

      if (Player.money.lt(item.price)) {
        workerScript.log(
          "purchaseProgram",
          `Not enough money to purchase '${item.program}'. Need ${numeralWrapper.formatMoney(item.price)}`,
        );
        return false;
      }

      if (Player.hasProgram(item.program)) {
        workerScript.log("purchaseProgram", `You already have the '${item.program}' program`);
        return true;
      }

      Player.loseMoney(item.price);
      Player.getHomeComputer().programs.push(item.program);
      workerScript.log(
        "purchaseProgram",
        `You have purchased the '${item.program}' program. The new program can be found on your home computer.`,
      );
      return true;
    },
    getCurrentServer: function () {
      updateDynamicRam("getCurrentServer", getRamCost("getCurrentServer"));
      checkSingularityAccess("getCurrentServer", 1);
      return Player.getCurrentServer().hostname;
    },
    connect: function (hostname) {
      updateDynamicRam("connect", getRamCost("connect"));
      checkSingularityAccess("connect", 1);
      if (!hostname) {
        throw makeRuntimeErrorMsg("connect", `Invalid hostname: '${hostname}'`);
      }

      let target = getServer(hostname);
      if (target == null) {
        throw makeRuntimeErrorMsg("connect", `Invalid hostname: '${hostname}'`);
        return;
      }

      if (hostname === "home") {
        Player.getCurrentServer().isConnectedTo = false;
        Player.currentServer = Player.getHomeComputer().ip;
        Player.getCurrentServer().isConnectedTo = true;
        Terminal.currDir = "/";
        Terminal.resetTerminalInput(true);
        return true;
      }

      const server = Player.getCurrentServer();
      for (let i = 0; i < server.serversOnNetwork.length; i++) {
        const other = getServerOnNetwork(server, i);
        if (other.ip == hostname || other.hostname == hostname) {
          Player.getCurrentServer().isConnectedTo = false;
          Player.currentServer = target.ip;
          Player.getCurrentServer().isConnectedTo = true;
          Terminal.currDir = "/";
          Terminal.resetTerminalInput(true);
          return true;
        }
      }

      return false;
    },
    manualHack: function () {
      updateDynamicRam("manualHack", getRamCost("manualHack"));
      checkSingularityAccess("manualHack", 1);
      const server = Player.getCurrentServer();
      return hack(server.hostname, true);
    },
    installBackdoor: function () {
      updateDynamicRam("installBackdoor", getRamCost("installBackdoor"));
      checkSingularityAccess("installBackdoor", 1);
      const server = Player.getCurrentServer();
      const installTime = (calculateHackingTime(server, Player) / 4) * 1000;

      // No root access or skill level too low
      const canHack = netscriptCanHack(server, Player);
      if (!canHack.res) {
        throw makeRuntimeErrorMsg("installBackdoor", canHack.msg);
      }

      workerScript.log(
        "installBackdoor",
        `Installing backdoor on '${server.hostname}' in ${convertTimeMsToTimeElapsedString(installTime, true)}`,
      );

      return netscriptDelay(installTime, workerScript).then(function () {
        if (workerScript.env.stopFlag) {
          return Promise.reject(workerScript);
        }
        workerScript.log("installBackdoor", `Successfully installed backdoor on '${server.hostname}'`);

        server.backdoorInstalled = true;
        return Promise.resolve();
      });
    },
    getStats: function () {
      updateDynamicRam("getStats", getRamCost("getStats"));
      checkSingularityAccess("getStats", 1);
      workerScript.log("getStats", `getStats is deprecated, please use getPlayer`);

      return {
        hacking: Player.hacking_skill,
        strength: Player.strength,
        defense: Player.defense,
        dexterity: Player.dexterity,
        agility: Player.agility,
        charisma: Player.charisma,
        intelligence: Player.intelligence,
      };
    },
    getCharacterInformation: function () {
      updateDynamicRam("getCharacterInformation", getRamCost("getCharacterInformation"));
      checkSingularityAccess("getCharacterInformation", 1);
      workerScript.log("getCharacterInformation", `getCharacterInformation is deprecated, please use getPlayer`);

      return {
        bitnode: Player.bitNodeN,
        city: Player.city,
        factions: Player.factions.slice(),
        hp: Player.hp,
        jobs: Object.keys(Player.jobs),
        jobTitles: Object.values(Player.jobs),
        maxHp: Player.max_hp,
        mult: {
          agility: Player.agility_mult,
          agilityExp: Player.agility_exp_mult,
          companyRep: Player.company_rep_mult,
          crimeMoney: Player.crime_money_mult,
          crimeSuccess: Player.crime_success_mult,
          defense: Player.defense_mult,
          defenseExp: Player.defense_exp_mult,
          dexterity: Player.dexterity_mult,
          dexterityExp: Player.dexterity_exp_mult,
          factionRep: Player.faction_rep_mult,
          hacking: Player.hacking_mult,
          hackingExp: Player.hacking_exp_mult,
          strength: Player.strength_mult,
          strengthExp: Player.strength_exp_mult,
          workMoney: Player.work_money_mult,
        },
        timeWorked: Player.timeWorked,
        tor: SpecialServerIps.hasOwnProperty("Darkweb Server"),
        workHackExpGain: Player.workHackExpGained,
        workStrExpGain: Player.workStrExpGained,
        workDefExpGain: Player.workDefExpGained,
        workDexExpGain: Player.workDexExpGained,
        workAgiExpGain: Player.workAgiExpGained,
        workChaExpGain: Player.workChaExpGained,
        workRepGain: Player.workRepGained,
        workMoneyGain: Player.workMoneyGained,
        hackingExp: Player.hacking_exp,
        strengthExp: Player.strength_exp,
        defenseExp: Player.defense_exp,
        dexterityExp: Player.dexterity_exp,
        agilityExp: Player.agility_exp,
        charismaExp: Player.charisma_exp,
      };
    },
    getPlayer: function () {
      updateDynamicRam("getPlayer", getRamCost("getPlayer"));

      const data = {
        hacking_skill: Player.hacking_skill,
        hp: Player.hp,
        max_hp: Player.max_hp,
        strength: Player.strength,
        defense: Player.defense,
        dexterity: Player.dexterity,
        agility: Player.agility,
        charisma: Player.charisma,
        intelligence: Player.intelligence,
        hacking_chance_mult: Player.hacking_chance_mult,
        hacking_speed_mult: Player.hacking_speed_mult,
        hacking_money_mult: Player.hacking_money_mult,
        hacking_grow_mult: Player.hacking_grow_mult,
        hacking_exp: Player.hacking_exp,
        strength_exp: Player.strength_exp,
        defense_exp: Player.defense_exp,
        dexterity_exp: Player.dexterity_exp,
        agility_exp: Player.agility_exp,
        charisma_exp: Player.charisma_exp,
        hacking_mult: Player.hacking_mult,
        strength_mult: Player.strength_mult,
        defense_mult: Player.defense_mult,
        dexterity_mult: Player.dexterity_mult,
        agility_mult: Player.agility_mult,
        charisma_mult: Player.charisma_mult,
        hacking_exp_mult: Player.hacking_exp_mult,
        strength_exp_mult: Player.strength_exp_mult,
        defense_exp_mult: Player.defense_exp_mult,
        dexterity_exp_mult: Player.dexterity_exp_mult,
        agility_exp_mult: Player.agility_exp_mult,
        charisma_exp_mult: Player.charisma_exp_mult,
        company_rep_mult: Player.company_rep_mult,
        faction_rep_mult: Player.faction_rep_mult,
        numPeopleKilled: Player.numPeopleKilled,
        money: Player.money.toNumber(),
        city: Player.city,
        location: Player.location,
        crime_money_mult: Player.crime_money_mult,
        crime_success_mult: Player.crime_success_mult,
        isWorking: Player.isWorking,
        workType: Player.workType,
        currentWorkFactionName: Player.currentWorkFactionName,
        currentWorkFactionDescription: Player.currentWorkFactionDescription,
        workHackExpGainRate: Player.workHackExpGainRate,
        workStrExpGainRate: Player.workStrExpGainRate,
        workDefExpGainRate: Player.workDefExpGainRate,
        workDexExpGainRate: Player.workDexExpGainRate,
        workAgiExpGainRate: Player.workAgiExpGainRate,
        workChaExpGainRate: Player.workChaExpGainRate,
        workRepGainRate: Player.workRepGainRate,
        workMoneyGainRate: Player.workMoneyGainRate,
        workMoneyLossRate: Player.workMoneyLossRate,
        workHackExpGained: Player.workHackExpGained,
        workStrExpGained: Player.workStrExpGained,
        workDefExpGained: Player.workDefExpGained,
        workDexExpGained: Player.workDexExpGained,
        workAgiExpGained: Player.workAgiExpGained,
        workChaExpGained: Player.workChaExpGained,
        workRepGained: Player.workRepGained,
        workMoneyGained: Player.workMoneyGained,
        createProgramName: Player.createProgramName,
        createProgramReqLvl: Player.createProgramReqLvl,
        className: Player.className,
        crimeType: Player.crimeType,
        work_money_mult: Player.work_money_mult,
        hacknet_node_money_mult: Player.hacknet_node_money_mult,
        hacknet_node_purchase_cost_mult: Player.hacknet_node_purchase_cost_mult,
        hacknet_node_ram_cost_mult: Player.hacknet_node_ram_cost_mult,
        hacknet_node_core_cost_mult: Player.hacknet_node_core_cost_mult,
        hacknet_node_level_cost_mult: Player.hacknet_node_level_cost_mult,
        hasWseAccount: Player.hasWseAccount,
        hasTixApiAccess: Player.hasTixApiAccess,
        has4SData: Player.has4SData,
        has4SDataTixApi: Player.has4SDataTixApi,
        bladeburner_max_stamina_mult: Player.bladeburner_max_stamina_mult,
        bladeburner_stamina_gain_mult: Player.bladeburner_stamina_gain_mult,
        bladeburner_analysis_mult: Player.bladeburner_analysis_mult,
        bladeburner_success_chance_mult: Player.bladeburner_success_chance_mult,
        bitNodeN: Player.bitNodeN,
        totalPlaytime: Player.totalPlaytime,
        playtimeSinceLastAug: Player.playtimeSinceLastAug,
        playtimeSinceLastBitnode: Player.playtimeSinceLastBitnode,
        jobs: {},
        factions: Player.factions.slice(),
        tor: SpecialServerIps.hasOwnProperty("Darkweb Server"),
      };
      Object.assign(data.jobs, Player.jobs);
      return data;
    },
    hospitalize: function () {
      updateDynamicRam("hospitalize", getRamCost("hospitalize"));
      checkSingularityAccess("hospitalize", 1);
      return Player.hospitalize();
    },
    isBusy: function () {
      updateDynamicRam("isBusy", getRamCost("isBusy"));
      checkSingularityAccess("isBusy", 1);
      return Player.isWorking || inMission;
    },
    stopAction: function () {
      updateDynamicRam("stopAction", getRamCost("stopAction"));
      checkSingularityAccess("stopAction", 1);
      if (Player.isWorking) {
        var txt = Player.singularityStopWork();
        workerScript.log("stopAction", txt);
        return true;
      }
      return false;
    },
    upgradeHomeRam: function () {
      updateDynamicRam("upgradeHomeRam", getRamCost("upgradeHomeRam"));
      checkSingularityAccess("upgradeHomeRam", 2);

      // Check if we're at max RAM
      const homeComputer = Player.getHomeComputer();
      if (homeComputer.maxRam >= CONSTANTS.HomeComputerMaxRam) {
        workerScript.log("upgradeHomeRam", `Your home computer is at max RAM.`);
        return false;
      }

      const cost = Player.getUpgradeHomeRamCost();
      if (Player.money.lt(cost)) {
        workerScript.log("upgradeHomeRam", `You don't have enough money. Need ${numeralWrapper.formatMoney(cost)}`);
        return false;
      }

      homeComputer.maxRam *= 2;
      Player.loseMoney(cost);

      Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain);
      workerScript.log(
        "upgradeHomeRam",
        `Purchased additional RAM for home computer! It now has ${homeComputer.maxRam}GB of RAM.`,
      );
      return true;
    },
    getUpgradeHomeRamCost: function () {
      updateDynamicRam("getUpgradeHomeRamCost", getRamCost("getUpgradeHomeRamCost"));
      checkSingularityAccess("getUpgradeHomeRamCost", 2);

      return Player.getUpgradeHomeRamCost();
    },
    workForCompany: function (companyName) {
      updateDynamicRam("workForCompany", getRamCost("workForCompany"));
      checkSingularityAccess("workForCompany", 2);

      // Sanitize input
      if (companyName == null) {
        companyName = Player.companyName;
      }

      // Make sure its a valid company
      if (companyName == null || companyName === "" || !(Companies[companyName] instanceof Company)) {
        workerScript.log("workForCompany", `Invalid company: '${companyName}'`);
        return false;
      }

      // Make sure player is actually employed at the comapny
      if (!Object.keys(Player.jobs).includes(companyName)) {
        workerScript.log("workForCompany", `You do not have a job at '${companyName}'`);
        return false;
      }

      // Cant work while in a mission
      if (inMission) {
        workerScript.log("workForCompany", "You are in the middle of a mission.");
        return false;
      }

      // Check to make sure company position data is valid
      const companyPositionName = Player.jobs[companyName];
      const companyPosition = CompanyPositions[companyPositionName];
      if (companyPositionName === "" || !(companyPosition instanceof CompanyPosition)) {
        workerScript.log("workForCompany", "You do not have a job");
        return false;
      }

      if (Player.isWorking) {
        var txt = Player.singularityStopWork();
        workerScript.log("workForCompany", txt);
      }

      if (companyPosition.isPartTimeJob()) {
        Player.startWorkPartTime(companyName);
      } else {
        Player.startWork(companyName);
      }
      workerScript.log("workForCompany", `Began working at '${Player.companyName}' as a '${companyPositionName}'`);
      return true;
    },
    applyToCompany: function (companyName, field) {
      updateDynamicRam("applyToCompany", getRamCost("applyToCompany"));
      checkSingularityAccess("applyToCompany", 2);
      getCompany("applyToCompany", companyName);

      Player.location = companyName;
      var res;
      switch (field.toLowerCase()) {
        case "software":
          res = Player.applyForSoftwareJob(true);
          break;
        case "software consultant":
          res = Player.applyForSoftwareConsultantJob(true);
          break;
        case "it":
          res = Player.applyForItJob(true);
          break;
        case "security engineer":
          res = Player.applyForSecurityEngineerJob(true);
          break;
        case "network engineer":
          res = Player.applyForNetworkEngineerJob(true);
          break;
        case "business":
          res = Player.applyForBusinessJob(true);
          break;
        case "business consultant":
          res = Player.applyForBusinessConsultantJob(true);
          break;
        case "security":
          res = Player.applyForSecurityJob(true);
          break;
        case "agent":
          res = Player.applyForAgentJob(true);
          break;
        case "employee":
          res = Player.applyForEmployeeJob(true);
          break;
        case "part-time employee":
          res = Player.applyForPartTimeEmployeeJob(true);
          break;
        case "waiter":
          res = Player.applyForWaiterJob(true);
          break;
        case "part-time waiter":
          res = Player.applyForPartTimeWaiterJob(true);
          break;
        default:
          workerScript.log("applyToCompany", `Invalid job: '${field}'.`);
          return false;
      }
      // The Player object's applyForJob function can return string with special error messages
      if (isString(res)) {
        workerScript.log("applyToCompany", res);
        return false;
      }
      if (res) {
        workerScript.log(
          "applyToCompany",
          `You were offered a new job at '${companyName}' as a '${Player.jobs[companyName]}'`,
        );
      } else {
        workerScript.log(
          "applyToCompany",
          `You failed to get a new job/promotion at '${companyName}' in the '${field}' field.`,
        );
      }
      return res;
    },
    getCompanyRep: function (companyName) {
      updateDynamicRam("getCompanyRep", getRamCost("getCompanyRep"));
      checkSingularityAccess("getCompanyRep", 2);
      const company = getCompany("getCompanyRep", companyName);
      return company.playerReputation;
    },
    getCompanyFavor: function (companyName) {
      updateDynamicRam("getCompanyFavor", getRamCost("getCompanyFavor"));
      checkSingularityAccess("getCompanyFavor", 2);
      const company = getCompany("getCompanyFavor", companyName);
      return company.favor;
    },
    getCompanyFavorGain: function (companyName) {
      updateDynamicRam("getCompanyFavorGain", getRamCost("getCompanyFavorGain"));
      checkSingularityAccess("getCompanyFavorGain", 2);
      const company = getCompany("getCompanyFavorGain", companyName);
      return company.getFavorGain()[0];
    },
    checkFactionInvitations: function () {
      updateDynamicRam("checkFactionInvitations", getRamCost("checkFactionInvitations"));
      checkSingularityAccess("checkFactionInvitations", 2);
      // Make a copy of Player.factionInvitations
      return Player.factionInvitations.slice();
    },
    joinFaction: function (name) {
      updateDynamicRam("joinFaction", getRamCost("joinFaction"));
      checkSingularityAccess("joinFaction", 2);
      getFaction("joinFaction", name);

      if (!Player.factionInvitations.includes(name)) {
        workerScript.log("joinFaction", `You have not been invited by faction '${name}'`);
        return false;
      }
      const fac = Factions[name];
      joinFaction(fac);

      // Update Faction Invitation list to account for joined + banned factions
      for (let i = 0; i < Player.factionInvitations.length; ++i) {
        if (Player.factionInvitations[i] == name || Factions[Player.factionInvitations[i]].isBanned) {
          Player.factionInvitations.splice(i, 1);
          i--;
        }
      }
      Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain);
      workerScript.log("joinFaction", `Joined the '${name}' faction.`);
      return true;
    },
    workForFaction: function (name, type) {
      updateDynamicRam("workForFaction", getRamCost("workForFaction"));
      checkSingularityAccess("workForFaction", 2);
      getFaction("workForFaction", name);

      // if the player is in a gang and the target faction is any of the gang faction, fail
      if (Player.inGang() && AllGangs[name] !== undefined) {
        workerScript.log("workForFaction", `Faction '${name}' does not offer work at the moment.`);
        return;
      }

      if (inMission) {
        workerScript.log("workForFaction", "You are in the middle of a mission.");
        return;
      }

      if (!Player.factions.includes(name)) {
        workerScript.log("workForFaction", `You are not a member of '${name}'`);
        return false;
      }

      if (Player.isWorking) {
        const txt = Player.singularityStopWork();
        workerScript.log("workForFaction", txt);
      }

      var fac = Factions[name];
      // Arrays listing factions that allow each time of work
      var hackAvailable = [
        "Illuminati",
        "Daedalus",
        "The Covenant",
        "ECorp",
        "MegaCorp",
        "Bachman & Associates",
        "Blade Industries",
        "NWO",
        "Clarke Incorporated",
        "OmniTek Incorporated",
        "Four Sigma",
        "KuaiGong International",
        "Fulcrum Secret Technologies",
        "BitRunners",
        "The Black Hand",
        "NiteSec",
        "Chongqing",
        "Sector-12",
        "New Tokyo",
        "Aevum",
        "Ishima",
        "Volhaven",
        "Speakers for the Dead",
        "The Dark Army",
        "The Syndicate",
        "Silhouette",
        "Netburners",
        "Tian Di Hui",
        "CyberSec",
      ];
      var fdWkAvailable = [
        "Illuminati",
        "Daedalus",
        "The Covenant",
        "ECorp",
        "MegaCorp",
        "Bachman & Associates",
        "Blade Industries",
        "NWO",
        "Clarke Incorporated",
        "OmniTek Incorporated",
        "Four Sigma",
        "KuaiGong International",
        "The Black Hand",
        "Chongqing",
        "Sector-12",
        "New Tokyo",
        "Aevum",
        "Ishima",
        "Volhaven",
        "Speakers for the Dead",
        "The Dark Army",
        "The Syndicate",
        "Silhouette",
        "Tetrads",
        "Slum Snakes",
      ];
      var scWkAvailable = [
        "ECorp",
        "MegaCorp",
        "Bachman & Associates",
        "Blade Industries",
        "NWO",
        "Clarke Incorporated",
        "OmniTek Incorporated",
        "Four Sigma",
        "KuaiGong International",
        "Fulcrum Secret Technologies",
        "Chongqing",
        "Sector-12",
        "New Tokyo",
        "Aevum",
        "Ishima",
        "Volhaven",
        "Speakers for the Dead",
        "The Syndicate",
        "Tetrads",
        "Slum Snakes",
        "Tian Di Hui",
      ];

      switch (type.toLowerCase()) {
        case "hacking":
        case "hacking contracts":
        case "hackingcontracts":
          if (!hackAvailable.includes(fac.name)) {
            workerScript.log("workForFaction", `Faction '${fac.name}' do not need help with hacking contracts.`);
            return false;
          }
          Player.startFactionHackWork(fac);
          workerScript.log("workForFaction", `Started carrying out hacking contracts for '${fac.name}'`);
          return true;
        case "field":
        case "fieldwork":
        case "field work":
          if (!fdWkAvailable.includes(fac.name)) {
            workerScript.log("workForFaction", `Faction '${fac.name}' do not need help with field missions.`);
            return false;
          }
          Player.startFactionFieldWork(fac);
          workerScript.log("workForFaction", `Started carrying out field missions for '${fac.name}'`);
          return true;
        case "security":
        case "securitywork":
        case "security work":
          if (!scWkAvailable.includes(fac.name)) {
            workerScript.log("workForFaction", `Faction '${fac.name}' do not need help with security work.`);
            return false;
          }
          Player.startFactionSecurityWork(fac);
          workerScript.log("workForFaction", `Started carrying out security work for '${fac.name}'`);
          return true;
        default:
          workerScript.log("workForFaction", `Invalid work type: '${type}`);
      }
      return true;
    },
    getFactionRep: function (name) {
      updateDynamicRam("getFactionRep", getRamCost("getFactionRep"));
      checkSingularityAccess("getFactionRep", 2);
      const faction = getFaction("getFactionRep", name);
      return faction.playerReputation;
    },
    getFactionFavor: function (name) {
      updateDynamicRam("getFactionFavor", getRamCost("getFactionFavor"));
      checkSingularityAccess("getFactionFavor", 2);
      const faction = getFaction("getFactionFavor", name);
      return faction.favor;
    },
    getFactionFavorGain: function (name) {
      updateDynamicRam("getFactionFavorGain", getRamCost("getFactionFavorGain"));
      checkSingularityAccess("getFactionFavorGain", 2);
      const faction = getFaction("getFactionFavorGain", name);
      return faction.getFavorGain()[0];
    },
    donateToFaction: function (name, amt) {
      updateDynamicRam("donateToFaction", getRamCost("donateToFaction"));
      checkSingularityAccess("donateToFaction", 3);
      const faction = getFaction("donateToFaction", name);

      if (typeof amt !== "number" || amt <= 0) {
        workerScript.log("donateToFaction", `Invalid donation amount: '${amt}'.`);
        return false;
      }
      if (Player.money.lt(amt)) {
        workerScript.log(
          "donateToFaction",
          `You do not have enough money to donate ${numeralWrapper.formatMoney(amt)} to '${name}'`,
        );
        return false;
      }
      const repNeededToDonate = Math.round(CONSTANTS.BaseFavorToDonate * BitNodeMultipliers.RepToDonateToFaction);
      if (faction.favor < repNeededToDonate) {
        workerScript.log(
          "donateToFaction",
          `You do not have enough favor to donate to this faction. Have ${faction.favor}, need ${repNeededToDonate}`,
        );
        return false;
      }
      const repGain = (amt / CONSTANTS.DonateMoneyToRepDivisor) * Player.faction_rep_mult;
      faction.playerReputation += repGain;
      Player.loseMoney(amt);
      workerScript.log(
        "donateToFaction",
        `${numeralWrapper.formatMoney(amt)} donated to '${name}' for ${numeralWrapper.formatReputation(
          repGain,
        )} reputation`,
      );
      return true;
    },
    createProgram: function (name) {
      updateDynamicRam("createProgram", getRamCost("createProgram"));
      checkSingularityAccess("createProgram", 3);

      if (inMission) {
        workerScript.log("createProgram", "You are in the middle of a mission.");
        return;
      }
      if (Player.isWorking) {
        var txt = Player.singularityStopWork();
        workerScript.log("createProgram", txt);
      }

      name = name.toLowerCase();

      let p = null;
      for (const key in Programs) {
        if (Programs[key].name.toLowerCase() == name) {
          p = Programs[key];
        }
      }

      if (p == null) {
        workerScript.log("createProgram", `The specified program does not exist: '${name}`);
        return false;
      }

      if (Player.hasProgram(p.name)) {
        workerScript.log("createProgram", `You already have the '${p.name}' program`);
        return false;
      }

      if (!p.create.req(Player)) {
        workerScript.log(
          "createProgram",
          `Hacking level is too low to create '${p.name}' (level ${p.create.level} req)`,
        );
        return false;
      }

      Player.startCreateProgramWork(p.name, p.create.time, p.create.level);
      workerScript.log("createProgram", `Began creating program: '${name}'`);
      return true;
    },
    commitCrime: function (crimeRoughName) {
      updateDynamicRam("commitCrime", getRamCost("commitCrime"));
      checkSingularityAccess("commitCrime", 3);
      if (inMission) {
        workerScript.log("commitCrime", "You are in the middle of a mission.");
        return;
      }
      if (Player.isWorking) {
        const txt = Player.singularityStopWork();
        workerScript.log("commitCrime", txt);
      }

      // Set Location to slums
      Player.gotoLocation(LocationName.Slums);

      const crime = findCrime(crimeRoughName.toLowerCase());
      if (crime == null) {
        // couldn't find crime
        throw makeRuntimeErrorMsg("commitCrime", `Invalid crime: '${crimeRoughName}'`);
      }
      workerScript.log("commitCrime", `Attempting to commit ${crime.name}...`);
      return crime.commit(Player, 1, { workerscript: workerScript });
    },
    getCrimeChance: function (crimeRoughName) {
      updateDynamicRam("getCrimeChance", getRamCost("getCrimeChance"));
      checkSingularityAccess("getCrimeChance", 3);

      const crime = findCrime(crimeRoughName.toLowerCase());
      if (crime == null) {
        throw makeRuntimeErrorMsg("getCrimeChance", `Invalid crime: ${crimeRoughName}`);
      }

      return crime.successRate(Player);
    },
    getCrimeStats: function (crimeRoughName) {
      updateDynamicRam("getCrimeStats", getRamCost("getCrimeStats"));
      checkSingularityAccess("getCrimeStats", 3);

      const crime = findCrime(crimeRoughName.toLowerCase());
      if (crime == null) {
        throw makeRuntimeErrorMsg("getCrimeStats", `Invalid crime: ${crimeRoughName}`);
      }

      return Object.assign({}, crime);
    },
    getOwnedAugmentations: function (purchased = false) {
      updateDynamicRam("getOwnedAugmentations", getRamCost("getOwnedAugmentations"));
      checkSingularityAccess("getOwnedAugmentations", 3);
      var res = [];
      for (var i = 0; i < Player.augmentations.length; ++i) {
        res.push(Player.augmentations[i].name);
      }
      if (purchased) {
        for (var i = 0; i < Player.queuedAugmentations.length; ++i) {
          res.push(Player.queuedAugmentations[i].name);
        }
      }
      return res;
    },
    getOwnedSourceFiles: function () {
      updateDynamicRam("getOwnedSourceFiles", getRamCost("getOwnedSourceFiles"));
      checkSingularityAccess("getOwnedSourceFiles", 3);
      let res = [];
      for (let i = 0; i < Player.sourceFiles.length; ++i) {
        res.push({
          n: Player.sourceFiles[i].n,
          lvl: Player.sourceFiles[i].lvl,
        });
      }
      return res;
    },
    getAugmentationsFromFaction: function (facname) {
      updateDynamicRam("getAugmentationsFromFaction", getRamCost("getAugmentationsFromFaction"));
      checkSingularityAccess("getAugmentationsFromFaction", 3);
      const faction = getFaction("getAugmentationsFromFaction", facname);

      // If player has a gang with this faction, return all augmentations.
      if (Player.hasGangWith(facname)) {
        const res = [];
        for (const augName in Augmentations) {
          const aug = Augmentations[augName];
          if (!aug.isSpecial) {
            res.push(augName);
          }
        }

        return res;
      }

      return faction.augmentations.slice();
    },
    getAugmentationCost: function (name) {
      updateDynamicRam("getAugmentationCost", getRamCost("getAugmentationCost"));
      checkSingularityAccess("getAugmentationCost", 3);
      const aug = getAugmentation("getAugmentationCost", name);
      return [aug.baseRepRequirement, aug.baseCost];
    },
    getAugmentationPrereq: function (name) {
      updateDynamicRam("getAugmentationPrereq", getRamCost("getAugmentationPrereq"));
      checkSingularityAccess("getAugmentationPrereq", 3);
      const aug = getAugmentation("getAugmentationPrereq", name);
      return aug.prereqs.slice();
    },
    getAugmentationPrice: function (name) {
      updateDynamicRam("getAugmentationPrice", getRamCost("getAugmentationPrice"));
      checkSingularityAccess("getAugmentationPrice", 3);
      const aug = getAugmentation("getAugmentationPrice", name);
      return aug.baseCost;
    },
    getAugmentationRepReq: function (name) {
      updateDynamicRam("getAugmentationRepReq", getRamCost("getAugmentationRepReq"));
      checkSingularityAccess("getAugmentationRepReq", 3);
      const aug = getAugmentation("getAugmentationRepReq", name);
      return aug.baseRepRequirement;
    },
    getAugmentationStats: function (name) {
      updateDynamicRam("getAugmentationStats", getRamCost("getAugmentationStats"));
      checkSingularityAccess("getAugmentationStats", 3);
      const aug = getAugmentation("getAugmentationStats", name);
      return Object.assign({}, aug.mults);
    },
    purchaseAugmentation: function (faction, name) {
      updateDynamicRam("purchaseAugmentation", getRamCost("purchaseAugmentation"));
      checkSingularityAccess("purchaseAugmentation", 3);
      const fac = getFaction("purchaseAugmentation", faction);
      const aug = getAugmentation("purchaseAugmentation", name);

      let augs = [];
      if (Player.hasGangWith(faction)) {
        for (const augName in Augmentations) {
          const tempAug = Augmentations[augName];
          if (!tempAug.isSpecial) {
            augs.push(augName);
          }
        }
      } else {
        augs = fac.augmentations;
      }

      if (!augs.includes(name)) {
        workerScript.log("purchaseAugmentation", `Faction '${faction}' does not have the '${name}' augmentation.`);
        return false;
      }

      const isNeuroflux = aug.name === AugmentationNames.NeuroFluxGovernor;
      if (!isNeuroflux) {
        for (let j = 0; j < Player.queuedAugmentations.length; ++j) {
          if (Player.queuedAugmentations[j].name === aug.name) {
            workerScript.log("purchaseAugmentation", `You already have the '${name}' augmentation.`);
            return false;
          }
        }
        for (let j = 0; j < Player.augmentations.length; ++j) {
          if (Player.augmentations[j].name === aug.name) {
            workerScript.log("purchaseAugmentation", `You already have the '${name}' augmentation.`);
            return false;
          }
        }
      }

      if (fac.playerReputation < aug.baseRepRequirement) {
        workerScript.log("purchaseAugmentation", `You do not have enough reputation with '${fac.name}'.`);
        return false;
      }

      const res = purchaseAugmentation(aug, fac, true);
      workerScript.log("purchaseAugmentation", res);
      if (isString(res) && res.startsWith("You purchased")) {
        Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain);
        return true;
      } else {
        return false;
      }
    },
    softReset: function (cbScript) {
      updateDynamicRam("softReset", getRamCost("softReset"));
      checkSingularityAccess("softReset", 3);

      workerScript.log("softReset", "Soft resetting. This will cause this script to be killed");
      setTimeoutRef(() => {
        prestigeAugmentation();
        runAfterReset(cbScript);
      }, 0);

      // Prevent workerScript from "finishing execution naturally"
      workerScript.running = false;
      killWorkerScript(workerScript);
    },
    installAugmentations: function (cbScript) {
      updateDynamicRam("installAugmentations", getRamCost("installAugmentations"));
      checkSingularityAccess("installAugmentations", 3);

      if (Player.queuedAugmentations.length === 0) {
        workerScript.log("installAugmentations", "You do not have any Augmentations to be installed.");
        return false;
      }
      Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain);
      workerScript.log("installAugmentations", "Installing Augmentations. This will cause this script to be killed");
      setTimeoutRef(() => {
        installAugmentations();
        runAfterReset(cbScript);
      }, 0);

      workerScript.running = false; // Prevent workerScript from "finishing execution naturally"
      killWorkerScript(workerScript);
    },

    // Gang API
    gang: {
      createGang: function (faction) {
        updateDynamicRam("createGang", getRamCost("gang", "createGang"));
        // this list is copied from Faction/ui/Root.tsx
        const GangNames = [
          "Slum Snakes",
          "Tetrads",
          "The Syndicate",
          "The Dark Army",
          "Speakers for the Dead",
          "NiteSec",
          "The Black Hand",
        ];
        if (!Player.canAccessGang() || !GangNames.includes(faction)) return false;
        if (Player.inGang()) return false;
        if (!Player.factions.includes(faction)) return false;

        const isHacking = faction === "NiteSec" || faction === "The Black Hand";
        Player.startGang(faction, isHacking);
        return true;
      },
      inGang: function () {
        updateDynamicRam("inGang", getRamCost("gang", "inGang"));
        return Player.inGang();
      },
      getMemberNames: function () {
        updateDynamicRam("getMemberNames", getRamCost("gang", "getMemberNames"));
        checkGangApiAccess("getMemberNames");
        return Player.gang.members.map((member) => member.name);
      },
      getGangInformation: function () {
        updateDynamicRam("getGangInformation", getRamCost("gang", "getGangInformation"));
        checkGangApiAccess("getGangInformation");
        return {
          faction: Player.gang.facName,
          isHacking: Player.gang.isHackingGang,
          moneyGainRate: Player.gang.moneyGainRate,
          power: Player.gang.getPower(),
          respect: Player.gang.respect,
          respectGainRate: Player.gang.respectGainRate,
          territory: Player.gang.getTerritory(),
          territoryClashChance: Player.gang.territoryClashChance,
          territoryWarfareEngaged: Player.gang.territoryWarfareEngaged,
          wantedLevel: Player.gang.wanted,
          wantedLevelGainRate: Player.gang.wantedGainRate,
        };
      },
      getOtherGangInformation: function () {
        updateDynamicRam("getOtherGangInformation", getRamCost("gang", "getOtherGangInformation"));
        checkGangApiAccess("getOtherGangInformation");
        const cpy = {};
        for (const gang in AllGangs) {
          cpy[gang] = Object.assign({}, AllGangs[gang]);
        }

        return cpy;
      },
      getMemberInformation: function (name) {
        updateDynamicRam("getMemberInformation", getRamCost("gang", "getMemberInformation"));
        checkGangApiAccess("getMemberInformation");
        const member = getGangMember("getMemberInformation", name);
        return {
          name: member.name,
          task: member.task,
          earnedRespect: member.earnedRespect,
          hack: member.hack,
          str: member.str,
          def: member.def,
          dex: member.dex,
          agi: member.agi,
          cha: member.cha,

          hack_exp: member.hack_exp,
          str_exp: member.str_exp,
          def_exp: member.def_exp,
          dex_exp: member.dex_exp,
          agi_exp: member.agi_exp,
          cha_exp: member.cha_exp,

          hack_mult: member.hack_mult,
          str_mult: member.str_mult,
          def_mult: member.def_mult,
          dex_mult: member.dex_mult,
          agi_mult: member.agi_mult,
          cha_mult: member.cha_mult,

          hack_asc_mult: member.calculateAscensionMult(member.hack_asc_points),
          str_asc_mult: member.calculateAscensionMult(member.str_asc_points),
          def_asc_mult: member.calculateAscensionMult(member.def_asc_points),
          dex_asc_mult: member.calculateAscensionMult(member.dex_asc_points),
          agi_asc_mult: member.calculateAscensionMult(member.agi_asc_points),
          cha_asc_mult: member.calculateAscensionMult(member.cha_asc_points),

          hack_asc_points: member.hack_asc_points,
          str_asc_points: member.str_asc_points,
          def_asc_points: member.def_asc_points,
          dex_asc_points: member.dex_asc_points,
          agi_asc_points: member.agi_asc_points,
          cha_asc_points: member.cha_asc_points,

          upgrades: member.upgrades.slice(),
          augmentations: member.augmentations.slice(),
        };
      },
      canRecruitMember: function () {
        updateDynamicRam("canRecruitMember", getRamCost("gang", "canRecruitMember"));
        checkGangApiAccess("canRecruitMember");
        return Player.gang.canRecruitMember();
      },
      recruitMember: function (name) {
        updateDynamicRam("recruitMember", getRamCost("gang", "recruitMember"));
        checkGangApiAccess("recruitMember");
        const recruited = Player.gang.recruitMember(name);
        if (recruited) {
          workerScript.log("recruitMember", `Successfully recruited Gang Member '${name}'`);
        } else {
          workerScript.log("recruitMember", `Failed to recruit Gang Member '${name}'`);
        }

        return recruited;
      },
      getTaskNames: function () {
        updateDynamicRam("getTaskNames", getRamCost("gang", "getTaskNames"));
        checkGangApiAccess("getTaskNames");
        const tasks = Player.gang.getAllTaskNames();
        tasks.unshift("Unassigned");
        return tasks;
      },
      setMemberTask: function (memberName, taskName) {
        updateDynamicRam("setMemberTask", getRamCost("gang", "setMemberTask"));
        checkGangApiAccess("setMemberTask");
        const member = getGangMember("setMemberTask", memberName);
        const success = member.assignToTask(taskName);
        if (success) {
          workerScript.log("setMemberTask", `Successfully assigned Gang Member '${memberName}' to '${taskName}' task`);
        } else {
          workerScript.log(
            "setMemberTask",
            `Failed to assign Gang Member '${memberName}' to '${taskName}' task. '${memberName}' is now Unassigned`,
          );
        }

        return success;
      },
      getTaskStats: function (taskName) {
        updateDynamicRam("getTaskStats", getRamCost("gang", "getTaskStats"));
        checkGangApiAccess("getTaskStats");
        const task = getGangTask("getTaskStats", taskName);
        const copy = Object.assign({}, task);
        copy.territory = Object.assign({}, task.territory);
        return copy;
      },
      getEquipmentNames: function () {
        updateDynamicRam("getEquipmentNames", getRamCost("gang", "getEquipmentNames"));
        checkGangApiAccess("getEquipmentNames");
        return Object.keys(GangMemberUpgrades);
      },
      getEquipmentCost: function (equipName) {
        updateDynamicRam("getEquipmentCost", getRamCost("gang", "getEquipmentCost"));
        checkGangApiAccess("getEquipmentCost");
        const upg = GangMemberUpgrades[equipName];
        if (upg === null) return Infinity;
        return Player.gang.getUpgradeCost(upg);
      },
      getEquipmentType: function (equipName) {
        updateDynamicRam("getEquipmentType", getRamCost("gang", "getEquipmentType"));
        checkGangApiAccess("getEquipmentType");
        const upg = GangMemberUpgrades[equipName];
        if (upg == null) return "";
        return upg.getType();
      },
      getEquipmentStats: function (equipName) {
        updateDynamicRam("getEquipmentStats", getRamCost("gang", "getEquipmentStats"));
        checkGangApiAccess("getEquipmentStats");
        const equipment = GangMemberUpgrades[equipName];
        if (!equipment) {
          throw makeRuntimeErrorMsg("getEquipmentStats", `Invalid equipment: ${equipName}`);
        }
        return Object.assign({}, equipment.mults);
      },
      purchaseEquipment: function (memberName, equipName) {
        updateDynamicRam("purchaseEquipment", getRamCost("gang", "purchaseEquipment"));
        checkGangApiAccess("purchaseEquipment");
        const member = getGangMember("purchaseEquipment", memberName);
        const equipment = GangMemberUpgrades[equipName];
        if (!equipment) return false;
        const res = member.buyUpgrade(equipment, Player, Player.gang);
        if (res) {
          workerScript.log("purchaseEquipment", `Purchased '${equipName}' for Gang member '${memberName}'`);
        } else {
          workerScript.log("purchaseEquipment", `Failed to purchase '${equipName}' for Gang member '${memberName}'`);
        }

        return res;
      },
      ascendMember: function (name) {
        updateDynamicRam("ascendMember", getRamCost("gang", "ascendMember"));
        checkGangApiAccess("ascendMember");
        const member = getGangMember("ascendMember", name);
        if (!member.canAscend()) return;
        return Player.gang.ascendMember(member, workerScript);
      },
      setTerritoryWarfare: function (engage) {
        updateDynamicRam("setTerritoryWarfare", getRamCost("gang", "setTerritoryWarfare"));
        checkGangApiAccess("setTerritoryWarfare");
        if (engage) {
          Player.gang.territoryWarfareEngaged = true;
          workerScript.log("setTerritoryWarfare", "Engaging in Gang Territory Warfare");
        } else {
          Player.gang.territoryWarfareEngaged = false;
          workerScript.log("setTerritoryWarfare", "Disengaging in Gang Territory Warfare");
        }
      },
      getChanceToWinClash: function (otherGang) {
        updateDynamicRam("getChanceToWinClash", getRamCost("gang", "getChanceToWinClash"));
        checkGangApiAccess("getChanceToWinClash");
        if (AllGangs[otherGang] == null) {
          throw makeRuntimeErrorMsg(`gang.${getChanceToWinClash}`, `Invalid gang: ${otherGang}`);
        }

        const playerPower = AllGangs[Player.gang.facName].power;
        const otherPower = AllGangs[otherGang].power;

        return playerPower / (otherPower + playerPower);
      },
      getBonusTime: function () {
        updateDynamicRam("getBonusTime", getRamCost("gang", "getBonusTime"));
        checkGangApiAccess("getBonusTime");
        return Math.round(Player.gang.storedCycles / 5);
      },
    }, // end gang namespace

    // Bladeburner API
    bladeburner: {
      getContractNames: function () {
        updateDynamicRam("getContractNames", getRamCost("bladeburner", "getContractNames"));
        checkBladeburnerAccess("getContractNames");
        return Player.bladeburner.getContractNamesNetscriptFn();
      },
      getOperationNames: function () {
        updateDynamicRam("getOperationNames", getRamCost("bladeburner", "getOperationNames"));
        checkBladeburnerAccess("getOperationNames");
        return Player.bladeburner.getOperationNamesNetscriptFn();
      },
      getBlackOpNames: function () {
        updateDynamicRam("getBlackOpNames", getRamCost("bladeburner", "getBlackOpNames"));
        checkBladeburnerAccess("getBlackOpNames");
        return Player.bladeburner.getBlackOpNamesNetscriptFn();
      },
      getBlackOpRank: function (name = "") {
        updateDynamicRam("getBlackOpRank", getRamCost("bladeburner", "getBlackOpRank"));
        checkBladeburnerAccess("getBlackOpRank");
        const action = getBladeburnerActionObject("getBlackOpRank", "blackops", name);
        return action.reqdRank;
      },
      getGeneralActionNames: function () {
        updateDynamicRam("getGeneralActionNames", getRamCost("bladeburner", "getGeneralActionNames"));
        checkBladeburnerAccess("getGeneralActionNames");
        return Player.bladeburner.getGeneralActionNamesNetscriptFn();
      },
      getSkillNames: function () {
        updateDynamicRam("getSkillNames", getRamCost("bladeburner", "getSkillNames"));
        checkBladeburnerAccess("getSkillNames");
        return Player.bladeburner.getSkillNamesNetscriptFn();
      },
      startAction: function (type = "", name = "") {
        updateDynamicRam("startAction", getRamCost("bladeburner", "startAction"));
        checkBladeburnerAccess("startAction");
        try {
          return Player.bladeburner.startActionNetscriptFn(Player, type, name, workerScript);
        } catch (e) {
          throw makeRuntimeErrorMsg("bladeburner.startAction", e);
        }
      },
      stopBladeburnerAction: function () {
        updateDynamicRam("stopBladeburnerAction", getRamCost("bladeburner", "stopBladeburnerAction"));
        checkBladeburnerAccess("stopBladeburnerAction");
        return Player.bladeburner.resetAction();
      },
      getCurrentAction: function () {
        updateDynamicRam("getCurrentAction", getRamCost("bladeburner", "getCurrentAction"));
        checkBladeburnerAccess("getCurrentAction");
        return Player.bladeburner.getTypeAndNameFromActionId(Player.bladeburner.action);
      },
      getActionTime: function (type = "", name = "") {
        updateDynamicRam("getActionTime", getRamCost("bladeburner", "getActionTime"));
        checkBladeburnerAccess("getActionTime");
        try {
          return Player.bladeburner.getActionTimeNetscriptFn(Player, type, name, workerScript);
        } catch (e) {
          throw makeRuntimeErrorMsg("bladeburner.getActionTime", e);
        }
      },
      getActionEstimatedSuccessChance: function (type = "", name = "") {
        updateDynamicRam(
          "getActionEstimatedSuccessChance",
          getRamCost("bladeburner", "getActionEstimatedSuccessChance"),
        );
        checkBladeburnerAccess("getActionEstimatedSuccessChance");
        try {
          return Player.bladeburner.getActionEstimatedSuccessChanceNetscriptFn(Player, type, name, workerScript);
        } catch (e) {
          throw makeRuntimeErrorMsg("bladeburner.getActionEstimatedSuccessChance", e);
        }
      },
      getActionRepGain: function (type = "", name = "", level) {
        updateDynamicRam("getActionRepGain", getRamCost("bladeburner", "getActionRepGain"));
        checkBladeburnerAccess("getActionRepGain");
        const action = getBladeburnerActionObject("getActionRepGain", type, name);
        let rewardMultiplier;
        if (level == null || isNaN(level)) {
          rewardMultiplier = Math.pow(action.rewardFac, action.level - 1);
        } else {
          rewardMultiplier = Math.pow(action.rewardFac, level - 1);
        }

        return action.rankGain * rewardMultiplier * BitNodeMultipliers.BladeburnerRank;
      },
      getActionCountRemaining: function (type = "", name = "") {
        updateDynamicRam("getActionCountRemaining", getRamCost("bladeburner", "getActionCountRemaining"));
        checkBladeburnerAccess("getActionCountRemaining");
        try {
          return Player.bladeburner.getActionCountRemainingNetscriptFn(type, name, workerScript);
        } catch (e) {
          throw makeRuntimeErrorMsg("bladeburner.getActionCountRemaining", e);
        }
      },
      getActionMaxLevel: function (type = "", name = "") {
        updateDynamicRam("getActionMaxLevel", getRamCost("bladeburner", "getActionMaxLevel"));
        checkBladeburnerAccess("getActionMaxLevel");
        const action = getBladeburnerActionObject("getActionMaxLevel", type, name);
        return action.maxLevel;
      },
      getActionCurrentLevel: function (type = "", name = "") {
        updateDynamicRam("getActionCurrentLevel", getRamCost("bladeburner", "getActionCurrentLevel"));
        checkBladeburnerAccess("getActionCurrentLevel");
        const action = getBladeburnerActionObject("getActionCurrentLevel", type, name);
        return action.level;
      },
      getActionAutolevel: function (type = "", name = "") {
        updateDynamicRam("getActionAutolevel", getRamCost("bladeburner", "getActionAutolevel"));
        checkBladeburnerAccess("getActionAutolevel");
        const action = getBladeburnerActionObject("getActionCurrentLevel", type, name);
        return action.autoLevel;
      },
      setActionAutolevel: function (type = "", name = "", autoLevel = true) {
        updateDynamicRam("setActionAutolevel", getRamCost("bladeburner", "setActionAutolevel"));
        checkBladeburnerAccess("setActionAutolevel");
        const action = getBladeburnerActionObject("setActionAutolevel", type, name);
        action.autoLevel = autoLevel;
      },
      setActionLevel: function (type = "", name = "", level = 1) {
        updateDynamicRam("setActionLevel", getRamCost("bladeburner", "setActionLevel"));
        checkBladeburnerAccess("setActionLevel");
        const action = getBladeburnerActionObject("setActionLevel", type, name);
        if (level < 1 || level > action.maxLevel) {
          throw makeRuntimeErrorMsg(
            "bladeburner.setActionLevel",
            `Level must be between 1 and ${action.maxLevel}, is ${level}`,
          );
        }
        action.level = level;
      },
      getRank: function () {
        updateDynamicRam("getRank", getRamCost("bladeburner", "getRank"));
        checkBladeburnerAccess("getRank");
        return Player.bladeburner.rank;
      },
      getSkillPoints: function () {
        updateDynamicRam("getSkillPoints", getRamCost("bladeburner", "getSkillPoints"));
        checkBladeburnerAccess("getSkillPoints");
        return Player.bladeburner.skillPoints;
      },
      getSkillLevel: function (skillName = "") {
        updateDynamicRam("getSkillLevel", getRamCost("bladeburner", "getSkillLevel"));
        checkBladeburnerAccess("getSkillLevel");
        try {
          return Player.bladeburner.getSkillLevelNetscriptFn(skillName, workerScript);
        } catch (e) {
          throw makeRuntimeErrorMsg("bladeburner.getSkillLevel", e);
        }
      },
      getSkillUpgradeCost: function (skillName = "") {
        updateDynamicRam("getSkillUpgradeCost", getRamCost("bladeburner", "getSkillUpgradeCost"));
        checkBladeburnerAccess("getSkillUpgradeCost");
        try {
          return Player.bladeburner.getSkillUpgradeCostNetscriptFn(skillName, workerScript);
        } catch (e) {
          throw makeRuntimeErrorMsg("bladeburner.getSkillUpgradeCost", e);
        }
      },
      upgradeSkill: function (skillName) {
        updateDynamicRam("upgradeSkill", getRamCost("bladeburner", "upgradeSkill"));
        checkBladeburnerAccess("upgradeSkill");
        try {
          return Player.bladeburner.upgradeSkillNetscriptFn(skillName, workerScript);
        } catch (e) {
          throw makeRuntimeErrorMsg("bladeburner.upgradeSkill", e);
        }
      },
      getTeamSize: function (type = "", name = "") {
        updateDynamicRam("getTeamSize", getRamCost("bladeburner", "getTeamSize"));
        checkBladeburnerAccess("getTeamSize");
        try {
          return Player.bladeburner.getTeamSizeNetscriptFn(type, name, workerScript);
        } catch (e) {
          throw makeRuntimeErrorMsg("bladeburner.getTeamSize", e);
        }
      },
      setTeamSize: function (type = "", name = "", size) {
        updateDynamicRam("setTeamSize", getRamCost("bladeburner", "setTeamSize"));
        checkBladeburnerAccess("setTeamSize");
        try {
          return Player.bladeburner.setTeamSizeNetscriptFn(type, name, size, workerScript);
        } catch (e) {
          throw makeRuntimeErrorMsg("bladeburner.setTeamSize", e);
        }
      },
      getCityEstimatedPopulation: function (cityName) {
        updateDynamicRam("getCityEstimatedPopulation", getRamCost("bladeburner", "getCityEstimatedPopulation"));
        checkBladeburnerAccess("getCityEstimatedPopulation");
        checkBladeburnerCity("getCityEstimatedPopulation", cityName);
        return Player.bladeburner.cities[cityName].popEst;
      },
      getCityEstimatedCommunities: function (cityName) {
        updateDynamicRam("getCityEstimatedCommunities", getRamCost("bladeburner", "getCityEstimatedCommunities"));
        checkBladeburnerAccess("getCityEstimatedCommunities");
        checkBladeburnerCity("getCityEstimatedCommunities", cityName);
        return Player.bladeburner.cities[cityName].commsEst;
      },
      getCityChaos: function (cityName) {
        updateDynamicRam("getCityChaos", getRamCost("bladeburner", "getCityChaos"));
        checkBladeburnerAccess("getCityChaos");
        checkBladeburnerCity("getCityChaos", cityName);
        return Player.bladeburner.cities[cityName].chaos;
      },
      getCity: function () {
        updateDynamicRam("getCity", getRamCost("bladeburner", "getCity"));
        checkBladeburnerAccess("getCityChaos");
        return Player.bladeburner.city;
      },
      switchCity: function (cityName) {
        updateDynamicRam("switchCity", getRamCost("bladeburner", "switchCity"));
        checkBladeburnerAccess("switchCity");
        checkBladeburnerCity("switchCity", cityName);
        return (Player.bladeburner.city = cityName);
      },
      getStamina: function () {
        updateDynamicRam("getStamina", getRamCost("bladeburner", "getStamina"));
        checkBladeburnerAccess("getStamina");
        return [Player.bladeburner.stamina, Player.bladeburner.maxStamina];
      },
      joinBladeburnerFaction: function () {
        updateDynamicRam("joinBladeburnerFaction", getRamCost("bladeburner", "joinBladeburnerFaction"));
        checkBladeburnerAccess("joinBladeburnerFaction", true);
        return Player.bladeburner.joinBladeburnerFactionNetscriptFn(workerScript);
      },
      joinBladeburnerDivision: function () {
        updateDynamicRam("joinBladeburnerDivision", getRamCost("bladeburner", "joinBladeburnerDivision"));
        checkBladeburnerAccess("joinBladeburnerDivision", true);
        if (Player.bitNodeN === 7 || SourceFileFlags[7] > 0) {
          if (Player.bitNodeN === 8) {
            return false;
          }
          if (Player.bladeburner instanceof Bladeburner) {
            return true; // Already member
          } else if (
            Player.strength >= 100 &&
            Player.defense >= 100 &&
            Player.dexterity >= 100 &&
            Player.agility >= 100
          ) {
            Player.bladeburner = new Bladeburner(Player);
            workerScript.log("joinBladeburnerDivision", "You have been accepted into the Bladeburner division");

            const worldHeader = document.getElementById("world-menu-header");
            if (worldHeader instanceof HTMLElement) {
              worldHeader.click();
              worldHeader.click();
            }

            return true;
          } else {
            workerScript.log(
              "joinBladeburnerDivision",
              "You do not meet the requirements for joining the Bladeburner division",
            );
            return false;
          }
        }
      },
      getBonusTime: function () {
        updateDynamicRam("getBonusTime", getRamCost("bladeburner", "getBonusTime"));
        checkBladeburnerAccess("getBonusTime");
        return Math.round(Player.bladeburner.storedCycles / 5);
      },
    }, // End Bladeburner

    // Hi, if you're reading this you're a bit nosy.
    // There's a corporation API but it's very imbalanced right now.
    // It's here so players can test with if they want.
    corporation: {
      expandIndustry: function (industryName, divisionName) {
        NewIndustry(Player.corporation, industryName, divisionName);
      },
      expandCity: function (divisionName, cityName) {
        const division = getDivision(divisionName);
        NewCity(Player.corporation, division, cityName);
      },
      unlockUpgrade: function (upgradeName) {
        const upgrade = Object.values(CorporationUnlockUpgrades).find((upgrade) => upgrade[2] === upgradeName);
        if (upgrade === undefined) throw new Error(`No upgrade named '${upgradeName}'`);
        UnlockUpgrade(Player.corporation, upgrade);
      },
      levelUpgrade: function (upgradeName) {
        const upgrade = Object.values(CorporationUpgrades).find((upgrade) => upgrade[4] === upgradeName);
        if (upgrade === undefined) throw new Error(`No upgrade named '${upgradeName}'`);
        LevelUpgrade(Player.corporation, upgrade);
      },
      issueDividends: function (percent) {
        IssueDividends(Player.corporation, percent);
      },
      sellMaterial: function (divisionName, cityName, materialName, amt, price) {
        const material = getMaterial(divisionName, cityName, materialName);
        SellMaterial(material, amt, price);
      },
      sellProduct: function (divisionName, cityName, productName, amt, price, all) {
        const product = getProduct(divisionName, productName);
        SellProduct(product, cityName, amt, price, all);
      },
      discontinueProduct: function (divisionName, productName) {
        getDivision(divisionName).discontinueProduct(getProduct(divisionName, productName));
      },
      setSmartSupply: function (divisionName, cityName, enabled) {
        const warehouse = getWarehouse(divisionName, cityName);
        SetSmartSupply(warehouse, enabled);
      },
      setSmartSupplyUseLeftovers: function () {},
      buyMaterial: function (divisionName, cityName, materialName, amt) {
        const material = getMaterial(divisionName, cityName, materialName);
        BuyMaterial(material, amt);
      },
      employees: function (divisionName, cityName) {
        const office = getOffice(divisionName, cityName);
        return office.employees.map((e) => Object.assign({}, e));
      },
      assignJob: function (divisionName, cityName, employeeName, job) {
        const employee = getEmployee(divisionName, cityName, employeeName);
        AssignJob(employee, job);
      },
      hireEmployee: function (divisionName, cityName) {
        const office = getOffice(divisionName, cityName);
        office.hireRandomEmployee();
      },
      upgradeOfficeSize: function (divisionName, cityName, size) {
        const office = getOffice(divisionName, cityName);
        UpgradeOfficeSize(Player.corporation, office, size);
      },
      throwParty: function (divisionName, cityName, costPerEmployee) {
        const office = getOffice(divisionName, cityName);
        ThrowParty(Player.corporation, office, costPerEmployee);
      },
      purchaseWarehouse: function (divisionName, cityName) {
        PurchaseWarehouse(Player.corporation, getDivision(divisionName), cityName);
      },
      upgradeWarehouse: function (divisionName, cityName) {
        UpgradeWarehouse(Player.corporation, getDivision(divisionName), getWarehouse(divisionName, cityName));
      },
      buyCoffee: function (divisionName, cityName) {
        BuyCoffee(Player.corporation, getDivision(divisionName), getOffice(divisionName, cityName));
      },
      hireAdVert: function (divisionName) {
        HireAdVert(Player.corporation, getDivision(divisionName), getOffice(divisionName, "Sector-12"));
      },
      makeProduct: function (divisionName, cityName, productName, designInvest, marketingInvest) {
        MakeProduct(
          Player.corporation,
          getDivision(divisionName),
          cityName,
          productName,
          designInvest,
          marketingInvest,
        );
      },
      research: function (divisionName, researchName) {
        Research(getDivision(divisionName), researchName);
      },
      exportMaterial: function (sourceDivision, sourceCity, targetDivision, targetCity, materialName, amt) {
        ExportMaterial(targetDivision, targetCity, getMaterial(sourceDivision, sourceCity, materialName), amt + "");
      },
      cancelExportMaterial: function (sourceDivision, sourceCity, targetDivision, targetCity, materialName, amt) {
        CancelExportMaterial(
          targetDivision,
          targetCity,
          getMaterial(sourceDivision, sourceCity, materialName),
          amt + "",
        );
      },
      setMaterialMarketTA1: function (divisionName, cityName, materialName, on) {
        SetMaterialMarketTA1(getMaterial(divisionName, cityName, materialName), on);
      },
      setMaterialMarketTA2: function (divisionName, cityName, materialName, on) {
        SetMaterialMarketTA2(getMaterial(divisionName, cityName, materialName), on);
      },
      setProductMarketTA1: function (divisionName, productName, on) {
        SetProductMarketTA1(getProduct(divisionName, productName), on);
      },
      setProductMarketTA2: function (divisionName, productName, on) {
        SetProductMarketTA2(getProduct(divisionName, productName), on);
      },
      // If you modify these objects you will affect them for real, it's not
      // copies.
      getDivision: function (divisionName) {
        return getDivision(divisionName);
      },
      getOffice: function (divisionName, cityName) {
        return getOffice(divisionName, cityName);
      },
      getWarehouse: function (divisionName, cityName) {
        return getWarehouse(divisionName, cityName);
      },
      getMaterial: function (divisionName, cityName, materialName) {
        return getMaterial(divisionName, cityName, materialName);
      },
      getProduct: function (divisionName, productName) {
        return getProduct(divisionName, productName);
      },
      getEmployee: function (divisionName, cityName, employeeName) {
        return getEmployee(divisionName, cityName, employeeName);
      },
    }, // End Corporation API

    // Coding Contract API
    codingcontract: {
      attempt: function (answer, fn, ip = workerScript.serverIp, { returnReward } = {}) {
        updateDynamicRam("attempt", getRamCost("codingcontract", "attempt"));
        const contract = getCodingContract("attempt", ip, fn);

        // Convert answer to string. If the answer is a 2D array, then we have to
        // manually add brackets for the inner arrays
        if (is2DArray(answer)) {
          let answerComponents = [];
          for (let i = 0; i < answer.length; ++i) {
            answerComponents.push(["[", answer[i].toString(), "]"].join(""));
          }

          answer = answerComponents.join(",");
        } else {
          answer = String(answer);
        }

        const serv = safeGetServer(ip, "codingcontract.attempt");
        if (contract.isSolution(answer)) {
          const reward = Player.gainCodingContractReward(contract.reward, contract.getDifficulty());
          workerScript.log("attempt", `Successfully completed Coding Contract '${fn}'. Reward: ${reward}`);
          serv.removeContract(fn);
          return returnReward ? reward : true;
        } else {
          ++contract.tries;
          if (contract.tries >= contract.getMaxNumTries()) {
            workerScript.log("attempt", `Coding Contract attempt '${fn}' failed. Contract is now self-destructing`);
            serv.removeContract(fn);
          } else {
            workerScript.log(
              "attempt",
              `Coding Contract attempt '${fn}' failed. ${
                contract.getMaxNumTries() - contract.tries
              } attempts remaining.`,
            );
          }

          return returnReward ? "" : false;
        }
      },
      getContractType: function (fn, ip = workerScript.serverIp) {
        updateDynamicRam("getContractType", getRamCost("codingcontract", "getContractType"));
        const contract = getCodingContract("getContractType", ip, fn);
        return contract.getType();
      },
      getData: function (fn, ip = workerScript.serverIp) {
        updateDynamicRam("getData", getRamCost("codingcontract", "getData"));
        const contract = getCodingContract("getData", ip, fn);
        const data = contract.getData();
        if (data.constructor === Array) {
          // For two dimensional arrays, we have to copy the internal arrays using
          // slice() as well. As of right now, no contract has arrays that have
          // more than two dimensions
          const copy = data.slice();
          for (let i = 0; i < copy.length; ++i) {
            if (data[i].constructor === Array) {
              copy[i] = data[i].slice();
            }
          }

          return copy;
        } else {
          return data;
        }
      },
      getDescription: function (fn, ip = workerScript.serverIp) {
        updateDynamicRam("getDescription", getRamCost("codingcontract", "getDescription"));
        const contract = getCodingContract("getDescription", ip, fn);
        return contract.getDescription();
      },
      getNumTriesRemaining: function (fn, ip = workerScript.serverIp) {
        updateDynamicRam("getNumTriesRemaining", getRamCost("codingcontract", "getNumTriesRemaining"));
        const contract = getCodingContract("getNumTriesRemaining", ip, fn);
        return contract.getMaxNumTries() - contract.tries;
      },
    }, // End coding contracts

    // Duplicate Sleeve API
    sleeve: {
      getNumSleeves: function () {
        updateDynamicRam("getNumSleeves", getRamCost("sleeve", "getNumSleeves"));
        checkSleeveAPIAccess("getNumSleeves");
        return Player.sleeves.length;
      },
      setToShockRecovery: function (sleeveNumber = 0) {
        updateDynamicRam("setToShockRecovery", getRamCost("sleeve", "setToShockRecovery"));
        checkSleeveAPIAccess("setToShockRecovery");
        checkSleeveNumber("setToShockRecovery", sleeveNumber);
        return Player.sleeves[sleeveNumber].shockRecovery(Player);
      },
      setToSynchronize: function (sleeveNumber = 0) {
        updateDynamicRam("setToSynchronize", getRamCost("sleeve", "setToSynchronize"));
        checkSleeveAPIAccess("setToSynchronize");
        checkSleeveNumber("setToSynchronize", sleeveNumber);
        return Player.sleeves[sleeveNumber].synchronize(Player);
      },
      setToCommitCrime: function (sleeveNumber = 0, crimeName = "") {
        updateDynamicRam("setToCommitCrime", getRamCost("sleeve", "setToCommitCrime"));
        checkSleeveAPIAccess("setToCommitCrime");
        checkSleeveNumber("setToCommitCrime", sleeveNumber);
        return Player.sleeves[sleeveNumber].commitCrime(Player, crimeName);
      },
      setToUniversityCourse: function (sleeveNumber = 0, universityName = "", className = "") {
        updateDynamicRam("setToUniversityCourse", getRamCost("sleeve", "setToUniversityCourse"));
        checkSleeveAPIAccess("setToUniversityCourse");
        checkSleeveNumber("setToUniversityCourse", sleeveNumber);
        return Player.sleeves[sleeveNumber].takeUniversityCourse(Player, universityName, className);
      },
      travel: function (sleeveNumber = 0, cityName = "") {
        updateDynamicRam("travel", getRamCost("sleeve", "travel"));
        checkSleeveAPIAccess("travel");
        checkSleeveNumber("travel", sleeveNumber);
        return Player.sleeves[sleeveNumber].travel(Player, cityName);
      },
      setToCompanyWork: function (sleeveNumber = 0, companyName = "") {
        updateDynamicRam("setToCompanyWork", getRamCost("sleeve", "setToCompanyWork"));
        checkSleeveAPIAccess("setToCompanyWork");
        checkSleeveNumber("setToCompanyWork", sleeveNumber);

        // Cannot work at the same company that another sleeve is working at
        for (let i = 0; i < Player.sleeves.length; ++i) {
          if (i === sleeveNumber) {
            continue;
          }
          const other = Player.sleeves[i];
          if (other.currentTask === SleeveTaskType.Company && other.currentTaskLocation === companyName) {
            throw makeRuntimeErrorMsg(
              "sleeve.setToFactionWork",
              `Sleeve ${sleeveNumber} cannot work for company ${companyName} because Sleeve ${i} is already working for them.`,
            );
          }
        }

        return Player.sleeves[sleeveNumber].workForCompany(Player, companyName);
      },
      setToFactionWork: function (sleeveNumber = 0, factionName = "", workType = "") {
        updateDynamicRam("setToFactionWork", getRamCost("sleeve", "setToFactionWork"));
        checkSleeveAPIAccess("setToFactionWork");
        checkSleeveNumber("setToFactionWork", sleeveNumber);

        // Cannot work at the same faction that another sleeve is working at
        for (let i = 0; i < Player.sleeves.length; ++i) {
          if (i === sleeveNumber) {
            continue;
          }
          const other = Player.sleeves[i];
          if (other.currentTask === SleeveTaskType.Faction && other.currentTaskLocation === factionName) {
            throw makeRuntimeErrorMsg(
              "sleeve.setToFactionWork",
              `Sleeve ${sleeveNumber} cannot work for faction ${factionName} because Sleeve ${i} is already working for them.`,
            );
          }
        }

        return Player.sleeves[sleeveNumber].workForFaction(Player, factionName, workType);
      },
      setToGymWorkout: function (sleeveNumber = 0, gymName = "", stat = "") {
        updateDynamicRam("setToGymWorkout", getRamCost("sleeve", "setToGymWorkout"));
        checkSleeveAPIAccess("setToGymWorkout");
        checkSleeveNumber("setToGymWorkout", sleeveNumber);

        return Player.sleeves[sleeveNumber].workoutAtGym(Player, gymName, stat);
      },
      getSleeveStats: function (sleeveNumber = 0) {
        updateDynamicRam("getSleeveStats", getRamCost("sleeve", "getSleeveStats"));
        checkSleeveAPIAccess("getSleeveStats");
        checkSleeveNumber("getSleeveStats", sleeveNumber);

        const sl = Player.sleeves[sleeveNumber];
        return {
          shock: 100 - sl.shock,
          sync: sl.sync,
          hacking_skill: sl.hacking_skill,
          strength: sl.strength,
          defense: sl.defense,
          dexterity: sl.dexterity,
          agility: sl.agility,
          charisma: sl.charisma,
        };
      },
      getTask: function (sleeveNumber = 0) {
        updateDynamicRam("getTask", getRamCost("sleeve", "getTask"));
        checkSleeveAPIAccess("getTask");
        checkSleeveNumber("getTask", sleeveNumber);

        const sl = Player.sleeves[sleeveNumber];
        return {
          task: SleeveTaskType[sl.currentTask],
          crime: sl.crimeType,
          location: sl.currentTaskLocation,
          gymStatType: sl.gymStatType,
          factionWorkType: FactionWorkType[sl.factionWorkType],
        };
      },
      getInformation: function (sleeveNumber = 0) {
        updateDynamicRam("getInformation", getRamCost("sleeve", "getInformation"));
        checkSleeveAPIAccess("getInformation");
        checkSleeveNumber("getInformation", sleeveNumber);

        const sl = Player.sleeves[sleeveNumber];
        return {
          city: sl.city,
          hp: sl.hp,
          jobs: Object.keys(Player.jobs), // technically sleeves have the same jobs as the player.
          jobTitle: Object.values(Player.jobs),
          maxHp: sl.max_hp,
          tor: SpecialServerIps.hasOwnProperty("Darkweb Server"), // There's no reason not to give that infomation here as well. Worst case scenario it isn't used.

          mult: {
            agility: sl.agility_mult,
            agilityExp: sl.agility_exp_mult,
            companyRep: sl.company_rep_mult,
            crimeMoney: sl.crime_money_mult,
            crimeSuccess: sl.crime_success_mult,
            defense: sl.defense_mult,
            defenseExp: sl.defense_exp_mult,
            dexterity: sl.dexterity_mult,
            dexterityExp: sl.dexterity_exp_mult,
            factionRep: sl.faction_rep_mult,
            hacking: sl.hacking_mult,
            hackingExp: sl.hacking_exp_mult,
            strength: sl.strength_mult,
            strengthExp: sl.strength_exp_mult,
            workMoney: sl.work_money_mult,
          },

          timeWorked: sl.currentTaskTime,
          earningsForSleeves: {
            workHackExpGain: sl.earningsForSleeves.hack,
            workStrExpGain: sl.earningsForSleeves.str,
            workDefExpGain: sl.earningsForSleeves.def,
            workDexExpGain: sl.earningsForSleeves.dex,
            workAgiExpGain: sl.earningsForSleeves.agi,
            workChaExpGain: sl.earningsForSleeves.cha,
            workMoneyGain: sl.earningsForSleeves.money,
          },
          earningsForPlayer: {
            workHackExpGain: sl.earningsForPlayer.hack,
            workStrExpGain: sl.earningsForPlayer.str,
            workDefExpGain: sl.earningsForPlayer.def,
            workDexExpGain: sl.earningsForPlayer.dex,
            workAgiExpGain: sl.earningsForPlayer.agi,
            workChaExpGain: sl.earningsForPlayer.cha,
            workMoneyGain: sl.earningsForPlayer.money,
          },
          earningsForTask: {
            workHackExpGain: sl.earningsForTask.hack,
            workStrExpGain: sl.earningsForTask.str,
            workDefExpGain: sl.earningsForTask.def,
            workDexExpGain: sl.earningsForTask.dex,
            workAgiExpGain: sl.earningsForTask.agi,
            workChaExpGain: sl.earningsForTask.cha,
            workMoneyGain: sl.earningsForTask.money,
          },
          workRepGain: sl.getRepGain(Player),
        };
      },
      getSleeveAugmentations: function (sleeveNumber = 0) {
        updateDynamicRam("getSleeveAugmentations", getRamCost("sleeve", "getSleeveAugmentations"));
        checkSleeveAPIAccess("getSleeveAugmentations");
        checkSleeveNumber("getSleeveAugmentations", sleeveNumber);

        const augs = [];
        for (let i = 0; i < Player.sleeves[sleeveNumber].augmentations.length; i++) {
          augs.push(Player.sleeves[sleeveNumber].augmentations[i].name);
        }
        return augs;
      },
      getSleevePurchasableAugs: function (sleeveNumber = 0) {
        updateDynamicRam("getSleevePurchasableAugs", getRamCost("sleeve", "getSleevePurchasableAugs"));
        checkSleeveAPIAccess("getSleevePurchasableAugs");
        checkSleeveNumber("getSleevePurchasableAugs", sleeveNumber);

        const purchasableAugs = findSleevePurchasableAugs(Player.sleeves[sleeveNumber], Player);
        const augs = [];
        for (let i = 0; i < purchasableAugs.length; i++) {
          const aug = purchasableAugs[i];
          augs.push({
            name: aug.name,
            cost: aug.startingCost,
          });
        }

        return augs;
      },
      purchaseSleeveAug: function (sleeveNumber = 0, augName = "") {
        updateDynamicRam("purchaseSleeveAug", getRamCost("sleeve", "purchaseSleeveAug"));
        checkSleeveAPIAccess("purchaseSleeveAug");
        checkSleeveNumber("purchaseSleeveAug", sleeveNumber);

        const aug = Augmentations[augName];
        if (!aug) {
          throw makeRuntimeErrorMsg("sleeve.purchaseSleeveAug", `Invalid aug: ${augName}`);
        }

        return Player.sleeves[sleeveNumber].tryBuyAugmentation(Player, aug);
      },
    }, // End sleeve
    formulas: {
      basic: {
        calculateSkill: function (exp, mult = 1) {
          checkFormulasAccess("basic.calculateSkill", 5);
          return calculateSkill(exp, mult);
        },
        calculateExp: function (skill, mult = 1) {
          checkFormulasAccess("basic.calculateExp", 5);
          return calculateExp(skill, mult);
        },
        hackChance: function (server, player) {
          checkFormulasAccess("basic.hackChance", 5);
          return calculateHackingChance(server, player);
        },
        hackExp: function (server, player) {
          checkFormulasAccess("basic.hackExp", 5);
          return calculateHackingExpGain(server, player);
        },
        hackPercent: function (server, player) {
          checkFormulasAccess("basic.hackPercent", 5);
          return calculatePercentMoneyHacked(server, player);
        },
        growPercent: function (server, threads, player, cores = 1) {
          checkFormulasAccess("basic.growPercent", 5);
          return calculateServerGrowth(server, threads, player, cores);
        },
        hackTime: function (server, player) {
          checkFormulasAccess("basic.hackTime", 5);
          return calculateHackingTime(server, player);
        },
        growTime: function (server, player) {
          checkFormulasAccess("basic.growTime", 5);
          return calculateGrowTime(server, player);
        },
        weakenTime: function (server, player) {
          checkFormulasAccess("basic.weakenTime", 5);
          return calculateWeakenTime(server, player);
        },
      },
      hacknetNodes: {
        moneyGainRate: function (level, ram, cores, mult = 1) {
          checkFormulasAccess("hacknetNodes.moneyGainRate", 5);
          return calculateMoneyGainRate(level, ram, cores, mult);
        },
        levelUpgradeCost: function (startingLevel, extraLevels = 1, costMult = 1) {
          checkFormulasAccess("hacknetNodes.levelUpgradeCost", 5);
          return calculateLevelUpgradeCost(startingLevel, extraLevels, costMult);
        },
        ramUpgradeCost: function (startingRam, extraLevels = 1, costMult = 1) {
          checkFormulasAccess("hacknetNodes.ramUpgradeCost", 5);
          return calculateRamUpgradeCost(startingRam, extraLevels, costMult);
        },
        coreUpgradeCost: function (startingCore, extraCores = 1, costMult = 1) {
          checkFormulasAccess("hacknetNodes.coreUpgradeCost", 5);
          return calculateCoreUpgradeCost(startingCore, extraCores, costMult);
        },
        hacknetNodeCost: function (n, mult) {
          checkFormulasAccess("hacknetNodes.hacknetNodeCost", 5);
          return calculateNodeCost(n, mult);
        },
        constants: function () {
          checkFormulasAccess("hacknetNodes.constants", 5);
          return Object.assign({}, HacknetNodeConstants);
        },
      },
      hacknetServers: {
        hashGainRate: function (level, ramUsed, maxRam, cores, mult = 1) {
          checkFormulasAccess("hacknetServers.hashGainRate", 9);
          return HScalculateHashGainRate(level, ramUsed, maxRam, cores, mult);
        },
        levelUpgradeCost: function (startingLevel, extraLevels = 1, costMult = 1) {
          checkFormulasAccess("hacknetServers.levelUpgradeCost", 9);
          return HScalculateLevelUpgradeCost(startingLevel, extraLevels, costMult);
        },
        ramUpgradeCost: function (startingRam, extraLevels = 1, costMult = 1) {
          checkFormulasAccess("hacknetServers.ramUpgradeCost", 9);
          return HScalculateRamUpgradeCost(startingRam, extraLevels, costMult);
        },
        coreUpgradeCost: function (startingCore, extraCores = 1, costMult = 1) {
          checkFormulasAccess("hacknetServers.coreUpgradeCost", 9);
          return HScalculateCoreUpgradeCost(startingCore, extraCores, costMult);
        },
        cacheUpgradeCost: function (startingCache, extraCache = 1, costMult = 1) {
          checkFormulasAccess("hacknetServers.cacheUpgradeCost", 9);
          return HScalculateCacheUpgradeCost(startingCache, extraCache, costMult);
        },
        hashUpgradeCost: function (upgName, level) {
          checkFormulasAccess("hacknetServers.hashUpgradeCost", 9);
          const upg = Player.hashManager.getUpgrade(upgName);
          if (!upg) {
            throw makeRuntimeErrorMsg(
              "formulas.hacknetServers.calculateHashUpgradeCost",
              `Invalid Hash Upgrade: ${upgName}`,
            );
          }
          return upg.getCost(level);
        },
        hacknetServerCost: function (n, mult) {
          checkFormulasAccess("hacknetServers.hacknetServerCost", 9);
          return HScalculateServerCost(n, mult);
        },
        constants: function () {
          checkFormulasAccess("hacknetServers.constants", 9);
          return Object.assign({}, HacknetServerConstants);
        },
      },
    }, // end formulas
    heart: {
      // Easter egg function
      break: function () {
        return Player.karma;
      },
    },
    exploit: function () {
      Player.giveExploit(Exploit.UndocumentedFunctionCall);
    },
    bypass: function (doc) {
      // reset both fields first
      doc.completely_unused_field = undefined;
      document.completely_unused_field = undefined;
      // set one to true and check that it affected the other.
      document.completely_unused_field = true;
      if (doc.completely_unused_field && workerScript.ramUsage === 1.6) {
        Player.giveExploit(Exploit.Bypass);
      }
      doc.completely_unused_field = undefined;
      document.completely_unused_field = undefined;
    },
    flags: function (data) {
      data = toNative(data);
      // We always want the help flag.
      const args = {};

      for (const d of data) {
        let t = String;
        if (typeof d[1] === "number") {
          t = Number;
        } else if (typeof d[1] === "boolean") {
          t = Boolean;
        } else if (Array.isArray(d[1])) {
          t = [String];
        }
        const numDashes = d[0].length > 1 ? 2 : 1;
        args["-".repeat(numDashes) + d[0]] = t;
      }
      const ret = libarg(args, { argv: workerScript.args });
      for (const d of data) {
        if (!ret.hasOwnProperty("--" + d[0]) || !ret.hasOwnProperty("-" + d[0])) ret[d[0]] = d[1];
      }
      for (const key of Object.keys(ret)) {
        if (!key.startsWith("-")) continue;
        const value = ret[key];
        delete ret[key];
        const numDashes = key.length === 2 ? 1 : 2;
        ret[key.slice(numDashes)] = value;
      }
      return ret;
    },
  };

  function getFunctionNames(obj) {
    const functionNames = [];
    for (const [key, value] of Object.entries(obj)) {
      if (typeof value == "function") {
        functionNames.push(key);
      } else if (typeof value == "object") {
        functionNames.push(...getFunctionNames(value));
      }
    }
    return functionNames;
  }

  const possibleLogs = Object.fromEntries([...getFunctionNames(functions)].map((a) => [a, true]));

  return functions;
} // End NetscriptFunction()

export { NetscriptFunctions };
