import type { NetscriptContext } from "./APIWrapper"; import React from "react"; import { killWorkerScript } from "./killWorkerScript"; import { GetAllServers, GetServer } from "../Server/AllServers"; import { Player } from "@player"; import { ScriptDeath } from "./ScriptDeath"; import { formatExp, formatMoney, formatRam, formatThreads } from "../ui/formatNumber"; import { ScriptArg } from "./ScriptArg"; import { RunningScript as IRunningScript, Person as IPerson, Server as IServer } from "@nsdefs"; import { Server } from "../Server/Server"; import { calculateHackingChance, calculateHackingExpGain, calculateHackingTime, calculatePercentMoneyHacked, } from "../Hacking"; import { netscriptCanHack } from "../Hacking/netscriptCanHack"; import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions"; import { currentNodeMults } from "../BitNode/BitNodeMultipliers"; import { CONSTANTS } from "../Constants"; import { influenceStockThroughServerHack } from "../StockMarket/PlayerInfluencing"; import { PortNumber } from "../NetscriptPort"; import { FormulaGang } from "../Gang/formulas/formulas"; import { GangMember } from "../Gang/GangMember"; import { GangMemberTask } from "../Gang/GangMemberTask"; import { RunningScript } from "../Script/RunningScript"; import { toNative } from "../NetscriptFunctions/toNative"; import { ScriptIdentifier } from "./ScriptIdentifier"; import { findRunningScripts, findRunningScriptByPid } from "../Script/ScriptHelpers"; import { arrayToString } from "../utils/helpers/ArrayHelpers"; import { HacknetServer } from "../Hacknet/HacknetServer"; import { BaseServer } from "../Server/BaseServer"; import { RamCostConstants } from "./RamCostGenerator"; import { isPositiveInteger, PositiveInteger, Unknownify, isPositiveNumber, PositiveNumber } from "../types"; import { Engine } from "../engine"; import { resolveFilePath, FilePath } from "../Paths/FilePath"; import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath"; import { CustomBoundary } from "../ui/Components/CustomBoundary"; import { ServerConstants } from "../Server/data/Constants"; import { basicErrorMessage, errorMessage, log } from "./ErrorMessages"; import { assertString, debugType } from "./TypeAssertion"; export const helpers = { string, number, positiveInteger, scriptArgs, runOptions, spawnOptions, argsToString, basicErrorMessage, errorMessage, validateHGWOptions, checkEnvFlags, checkSingularityAccess, netscriptDelay, updateDynamicRam, getServer, scriptIdentifier, hack, portNumber, person, server, gang, gangMember, gangTask, log, filePath, scriptPath, getRunningScript, getRunningScriptsByArgs, getCannotFindRunningScriptErrorMessage, createPublicRunningScript, failOnHacknetServer, }; /** RunOptions with non-optional, type-validated members, for passing between internal functions. */ export interface CompleteRunOptions { threads: PositiveInteger; temporary: boolean; ramOverride?: number; preventDuplicates: boolean; } /** SpawnOptions with non-optional, type-validated members, for passing between internal functions. */ export interface CompleteSpawnOptions extends CompleteRunOptions { spawnDelay: PositiveInteger; } /** HGWOptions with non-optional, type-validated members, for passing between internal functions. */ export interface CompleteHGWOptions { threads: PositiveNumber; stock: boolean; additionalMsec: number; } /** Convert a provided value v for argument argName to string. If it wasn't originally a string or number, throw. */ function string(ctx: NetscriptContext, argName: string, v: unknown): string { if (typeof v === "number") v = v + ""; // cast to string; assertString(ctx, argName, v); return v; } /** Convert provided value v for argument argName to number. Throw if could not convert to a non-NaN number. */ function number(ctx: NetscriptContext, argName: string, v: unknown): number { if (typeof v === "string") { const x = parseFloat(v); if (!isNaN(x)) return x; // otherwise it wasn't even a string representing a number. } else if (typeof v === "number") { if (isNaN(v)) throw errorMessage(ctx, `'${argName}' is NaN.`); return v; } throw errorMessage(ctx, `'${argName}' should be a number. ${debugType(v)}`, "TYPE"); } /** Convert provided value v for argument argName to a positive integer, throwing if it looks like something else. */ function positiveInteger(ctx: NetscriptContext, argName: string, v: unknown): PositiveInteger { const n = number(ctx, argName, v); if (!isPositiveInteger(n)) { throw errorMessage(ctx, `${argName} should be a positive integer, was ${n}`, "TYPE"); } return n; } /** Convert provided value v for argument argName to a positive number, throwing if it looks like something else. */ function positiveNumber(ctx: NetscriptContext, argName: string, v: unknown): PositiveNumber { const n = number(ctx, argName, v); if (!isPositiveNumber(n)) { throw errorMessage(ctx, `${argName} should be a positive number, was ${n}`, "TYPE"); } return n; } /** Returns args back if it is a ScriptArg[]. Throws an error if it is not. */ function scriptArgs(ctx: NetscriptContext, args: unknown) { if (!isScriptArgs(args)) throw errorMessage(ctx, "'args' is not an array of script args", "TYPE"); return args; } function runOptions(ctx: NetscriptContext, threadOrOption: unknown): CompleteRunOptions { const result: CompleteRunOptions = { threads: 1 as PositiveInteger, temporary: false, preventDuplicates: false, }; function checkThreads(threads: unknown, argName: string) { if (threads !== null && threads !== undefined) { result.threads = positiveInteger(ctx, argName, threads); } } if (typeof threadOrOption !== "object" || threadOrOption === null) { checkThreads(threadOrOption, "threads"); return result; } // Safe assertion since threadOrOption type has been narrowed to a non-null object const options = threadOrOption as Unknownify; checkThreads(options.threads, "RunOptions.threads"); result.temporary = !!options.temporary; result.preventDuplicates = !!options.preventDuplicates; if (options.ramOverride !== undefined && options.ramOverride !== null) { result.ramOverride = number(ctx, "RunOptions.ramOverride", options.ramOverride); if (result.ramOverride < RamCostConstants.Base) { throw errorMessage( ctx, `RunOptions.ramOverride must be >= baseCost (${RamCostConstants.Base}), was ${result.ramOverride}`, ); } } return result; } function spawnOptions(ctx: NetscriptContext, threadOrOption: unknown): CompleteSpawnOptions { const result: CompleteSpawnOptions = { spawnDelay: 10000 as PositiveInteger, ...runOptions(ctx, threadOrOption) }; if (typeof threadOrOption !== "object" || !threadOrOption) return result; // Safe assertion since threadOrOption type has been narrowed to a non-null object const { spawnDelay } = threadOrOption as Unknownify; if (spawnDelay !== undefined) result.spawnDelay = positiveInteger(ctx, "spawnDelayMsec", spawnDelay); return result; } /** Convert multiple arguments for tprint or print into a single string. */ function argsToString(args: unknown[]): string { // Reduce array of args into a single output string return args.reduce((out, arg) => { if (arg === null) { return (out += "null"); } if (arg === undefined) { return (out += "undefined"); } const nativeArg = toNative(arg); // Handle Map formatting, since it does not JSON stringify or toString in a helpful way // output is "< Map: key1 => value1; key2 => value2 >" if (nativeArg instanceof Map && [...nativeArg].length) { const formattedMap = [...nativeArg] .map((m) => { return `${m[0]} => ${m[1]}`; }) .join("; "); return (out += `< Map: ${formattedMap} >`); } // Handle Set formatting, since it does not JSON stringify or toString in a helpful way if (nativeArg instanceof Set) { return (out += `< Set: ${[...nativeArg].join("; ")} >`); } if (typeof nativeArg === "object") { return (out += JSON.stringify(nativeArg)); } return (out += `${nativeArg}`); }, "") as string; } function validateHGWOptions(ctx: NetscriptContext, opts: unknown): CompleteHGWOptions { const result: CompleteHGWOptions = { threads: ctx.workerScript.scriptRef.threads, stock: false, additionalMsec: 0, }; if (opts == null) { return result; } if (typeof opts !== "object") { throw errorMessage(ctx, `BasicHGWOptions must be an object if specified, was ${opts}`); } // Safe assertion since threadOrOption type has been narrowed to a non-null object const options = opts as Unknownify; result.stock = !!options.stock; result.additionalMsec = number(ctx, "opts.additionalMsec", options.additionalMsec ?? 0); if (result.additionalMsec < 0) { throw errorMessage(ctx, `additionalMsec must be non-negative, got ${options.additionalMsec}`); } if (result.additionalMsec > 1e9) { throw errorMessage(ctx, `additionalMsec too large (>1e9), got ${options.additionalMsec}`); } const requestedThreads = options.threads; const threads = ctx.workerScript.scriptRef.threads; if (!requestedThreads) { result.threads = (isNaN(threads) || threads < 1 ? 1 : threads) as PositiveInteger; } else { const positiveThreads = positiveNumber(ctx, "opts.threads", requestedThreads); if (positiveThreads > threads) { throw errorMessage( ctx, `Too many threads requested by ${ctx.function}. Requested: ${positiveThreads}. Has: ${threads}.`, ); } result.threads = positiveThreads; } return result; } /** Validate singularity access by throwing an error if the player does not have access. */ function checkSingularityAccess(ctx: NetscriptContext): void { if (Player.bitNodeN !== 4 && Player.sourceFileLvl(4) === 0) { throw errorMessage( ctx, `This singularity function requires Source-File 4 to run. A power up you obtain later in the game. It will be very obvious when and how you can obtain it.`, "API ACCESS", ); } } /** Create an error if a script is dead or if concurrent ns function calls are made */ function checkEnvFlags(ctx: NetscriptContext): void { const ws = ctx.workerScript; if (ws.env.stopFlag) { log(ctx, () => "Failed to run due to script being killed."); throw new ScriptDeath(ws); } if (ws.env.runningFn && ctx.function !== "asleep") { log(ctx, () => "Failed to run due to failed concurrency check."); const err = errorMessage( ctx, `Concurrent calls to Netscript functions are not allowed! Did you forget to await hack(), grow(), or some other promise-returning function? Currently running: ${ws.env.runningFn} tried to run: ${ctx.function}`, "CONCURRENCY", ); killWorkerScript(ws); throw err; } } /** Set a timeout for performing a task, mark the script as busy in the meantime. */ function netscriptDelay(ctx: NetscriptContext, time: number): Promise { const ws = ctx.workerScript; return new Promise(function (resolve, reject) { ws.delay = window.setTimeout(() => { ws.delay = null; ws.delayReject = undefined; ws.env.runningFn = ""; if (ws.env.stopFlag) reject(new ScriptDeath(ws)); else resolve(); }, time); ws.delayReject = reject; ws.env.runningFn = ctx.function; }); } /** Adds to dynamic ram cost when calling new ns functions from a script */ function updateDynamicRam(ctx: NetscriptContext, ramCost: number): void { const ws = ctx.workerScript; const fnName = ctx.function; if (ws.dynamicLoadedFns[fnName]) return; ws.dynamicLoadedFns[fnName] = true; ws.dynamicRamUsage = Math.min(ws.dynamicRamUsage + ramCost, RamCostConstants.Max); // This constant is just a handful of ULPs, and gives protection against // rounding issues without exposing rounding exploits in ramUsage. if (ws.dynamicRamUsage > 1.00000000000001 * ws.scriptRef.ramUsage) { log(ctx, () => "Insufficient static ram available."); const err = errorMessage( ctx, `Dynamic RAM usage calculated to be greater than RAM allocation. This is probably because you somehow circumvented the static RAM calculation. Threads: ${ws.scriptRef.threads} Dynamic RAM Usage: ${formatRam(ws.dynamicRamUsage)} per thread RAM Allocation: ${formatRam(ws.scriptRef.ramUsage)} per thread One of these could be the reason: * Using eval() to get a reference to a ns function \u00a0\u00a0const myScan = eval('ns.scan'); * Using map access to do the same \u00a0\u00a0const myScan = ns['scan']; * Using RunOptions.ramOverride to set a smaller allocation than needed Sorry :(`, "RAM USAGE", ); killWorkerScript(ws); throw err; } } function scriptIdentifier( ctx: NetscriptContext, scriptID: unknown, _hostname: unknown, _args: unknown, ): ScriptIdentifier { const ws = ctx.workerScript; // Provide the pid for the current script if no identifier provided if (scriptID === undefined) return ws.pid; if (typeof scriptID === "number") return scriptID; if (typeof scriptID === "string") { const hostname = _hostname === undefined ? ctx.workerScript.hostname : string(ctx, "hostname", _hostname); const args = _args === undefined ? [] : scriptArgs(ctx, _args); return { scriptname: scriptID, hostname, args, }; } throw errorMessage(ctx, "An unknown type of input was provided as a script identifier.", "TYPE"); } /** * Gets the Server for a specific hostname/ip, throwing an error * if the server doesn't exist. * @param {NetscriptContext} ctx - Context from which getServer is being called. For logging purposes. * @param {string} hostname - Hostname of the server * @returns {BaseServer} The specified server as a BaseServer */ function getServer(ctx: NetscriptContext, hostname: string) { const server = GetServer(hostname); if (server == null || (server.serversOnNetwork.length == 0 && server.hostname != "home")) { const str = hostname === "" ? "'' (empty string)" : "'" + hostname + "'"; throw errorMessage(ctx, `Invalid hostname: ${str}`); } return server; } function isScriptArgs(args: unknown): args is ScriptArg[] { const isScriptArg = (arg: unknown) => typeof arg === "string" || typeof arg === "number" || typeof arg === "boolean"; return Array.isArray(args) && args.every(isScriptArg); } function hack(ctx: NetscriptContext, hostname: string, manual: boolean, opts: unknown): Promise { const ws = ctx.workerScript; const { threads, stock, additionalMsec } = validateHGWOptions(ctx, opts); const server = getServer(ctx, hostname); if (!(server instanceof Server)) { throw errorMessage(ctx, "Cannot be executed on this server."); } // Calculate the hacking time // This is in seconds const hackingTime = calculateHackingTime(server, Player) + additionalMsec / 1000.0; // No root access or skill level too low const canHack = netscriptCanHack(server); if (!canHack.res) { throw errorMessage(ctx, canHack.msg || ""); } log( ctx, () => `Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString( hackingTime * 1000, true, )} (t=${formatThreads(threads)})`, ); return helpers.netscriptDelay(ctx, hackingTime * 1000).then(function () { const hackChance = calculateHackingChance(server, Player); const rand = Math.random(); let expGainedOnSuccess = calculateHackingExpGain(server, Player) * threads; const expGainedOnFailure = expGainedOnSuccess / 4; if (rand < hackChance) { // Success! const percentHacked = calculatePercentMoneyHacked(server, Player); let maxThreadNeeded = Math.ceil(1 / percentHacked); if (isNaN(maxThreadNeeded)) { // Server has a 'max money' of 0 (probably). We'll set this to an arbitrarily large value maxThreadNeeded = 1e6; } let moneyDrained = 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; } let moneyGained = moneyDrained * currentNodeMults.ScriptHackMoneyGain; if (manual) { moneyGained = moneyDrained * currentNodeMults.ManualHackMoney; } Player.gainMoney(moneyGained, "hacking"); ws.scriptRef.onlineMoneyMade += moneyGained; Player.scriptProdSinceLastAug += moneyGained; ws.scriptRef.recordHack(server.hostname, moneyGained, threads); Player.gainHackingExp(expGainedOnSuccess); if (manual) Player.gainIntelligenceExp(0.005); ws.scriptRef.onlineExpGained += expGainedOnSuccess; log( ctx, () => `Successfully hacked '${server.hostname}' for ${formatMoney(moneyGained)} and ${formatExp( expGainedOnSuccess, )} exp (t=${formatThreads(threads)})`, ); server.fortify(ServerConstants.ServerFortifyAmount * Math.min(threads, maxThreadNeeded)); if (stock) { influenceStockThroughServerHack(server, moneyDrained); } if (manual) { server.backdoorInstalled = true; // Manunally check for faction invites Engine.Counters.checkFactionInvitations = 0; Engine.checkCounters(); } return moneyGained; } else { // Player only gains 25% exp for failure? Player.gainHackingExp(expGainedOnFailure); ws.scriptRef.onlineExpGained += expGainedOnFailure; log( ctx, () => `Failed to hack '${server.hostname}'. Gained ${formatExp(expGainedOnFailure)} exp (t=${formatThreads( threads, )})`, ); return 0; } }); } function portNumber(ctx: NetscriptContext, _n: unknown): PortNumber { const n = positiveInteger(ctx, "portNumber", _n); if (n > CONSTANTS.NumNetscriptPorts) { throw errorMessage( ctx, `Trying to use an invalid port: ${n}. Must be less or equal to ${CONSTANTS.NumNetscriptPorts}.`, ); } return n as PortNumber; } function person(ctx: NetscriptContext, p: unknown): IPerson { const fakePerson = { hp: undefined, exp: undefined, mults: undefined, city: undefined, }; const error = missingKey(fakePerson, p); if (error) throw errorMessage(ctx, `person should be a Person.\n${error}`, "TYPE"); return p as IPerson; } function server(ctx: NetscriptContext, s: unknown): IServer { const fakeServer = { hostname: undefined, ip: undefined, sshPortOpen: undefined, ftpPortOpen: undefined, smtpPortOpen: undefined, httpPortOpen: undefined, sqlPortOpen: undefined, hasAdminRights: undefined, cpuCores: undefined, isConnectedTo: undefined, ramUsed: undefined, maxRam: undefined, organizationName: undefined, purchasedByPlayer: undefined, }; const error = missingKey(fakeServer, s); if (error) throw errorMessage(ctx, `server should be a Server.\n${error}`, "TYPE"); return s as IServer; } function missingKey(expect: object, actual: unknown): string | false { if (typeof actual !== "object" || actual === null) { return `Expected to be an object, was ${actual === null ? "null" : typeof actual}.`; } for (const key in expect) { if (!(key in actual)) return `Property ${key} was expected but not present.`; } return false; } function gang(ctx: NetscriptContext, g: unknown): FormulaGang { const error = missingKey({ respect: 0, territory: 0, wantedLevel: 0 }, g); if (error) throw errorMessage(ctx, `gang should be a Gang.\n${error}`, "TYPE"); return g as FormulaGang; } function gangMember(ctx: NetscriptContext, m: unknown): GangMember { const error = missingKey(new GangMember(), m); if (error) throw errorMessage(ctx, `member should be a GangMember.\n${error}`, "TYPE"); return m as GangMember; } function gangTask(ctx: NetscriptContext, t: unknown): GangMemberTask { const error = missingKey(new GangMemberTask("", "", false, false, { hackWeight: 100 }), t); if (error) throw errorMessage(ctx, `task should be a GangMemberTask.\n${error}`, "TYPE"); return t as GangMemberTask; } export function filePath(ctx: NetscriptContext, argName: string, filename: unknown): FilePath { assertString(ctx, argName, filename); const path = resolveFilePath(filename, ctx.workerScript.name); if (path) return path; throw errorMessage(ctx, `Invalid ${argName}, was not a valid path: ${filename}`); } export function scriptPath(ctx: NetscriptContext, argName: string, filename: unknown): ScriptFilePath { const path = filePath(ctx, argName, filename); if (hasScriptExtension(path)) return path; throw errorMessage(ctx, `Invalid ${argName}, must be a script: ${filename}`); } /** * Searches for and returns the RunningScript objects for the specified script. * If the 'fn' argument is not specified, this returns the current RunningScript. * @param fn - Filename of script * @param hostname - Hostname/ip of the server on which the script resides * @param scriptArgs - Running script's arguments * @returns Running scripts identified by the parameters, or empty if no such script * exists, or only the current running script if the first argument 'fn' * is not specified. */ export function getRunningScriptsByArgs( ctx: NetscriptContext, fn: string, hostname: string, scriptArgs: ScriptArg[], ): Map | null { if (!Array.isArray(scriptArgs)) { throw helpers.errorMessage( ctx, "Invalid scriptArgs argument passed into getRunningScriptByArgs().\n" + "This is probably a bug. Please report to game developer", ); } const path = scriptPath(ctx, "filename", fn); // Lookup server to scope search if (hostname == null) { hostname = ctx.workerScript.hostname; } const server = helpers.getServer(ctx, hostname); return findRunningScripts(path, scriptArgs, server); } function getRunningScriptByPid(pid: number): RunningScript | null { for (const server of GetAllServers()) { const runningScript = findRunningScriptByPid(pid, server); if (runningScript) return runningScript; } return null; } function getRunningScript(ctx: NetscriptContext, ident: ScriptIdentifier): RunningScript | null { if (typeof ident === "number") { return getRunningScriptByPid(ident); } else { const scripts = getRunningScriptsByArgs(ctx, ident.scriptname, ident.hostname, ident.args); if (scripts === null) return null; return scripts.values().next().value; } } /** * Helper function for getting the error log message when the user specifies * a nonexistent running script * @param ident - Identifier (pid or identifier object) of script. * @returns Error message to print to logs */ function getCannotFindRunningScriptErrorMessage(ident: ScriptIdentifier): string { if (typeof ident === "number") return `Cannot find running script with pid: ${ident}`; return `Cannot find running script ${ident.scriptname} on server ${ident.hostname} with args: ${arrayToString( ident.args, )}`; } /** * Sanitizes a `RunningScript` to remove sensitive information, making it suitable for * return through an NS function. * @see NS.getRecentScripts * @see NS.getRunningScript * @param runningScript Existing, internal RunningScript * @returns A sanitized, NS-facing copy of the RunningScript */ function createPublicRunningScript(runningScript: RunningScript): IRunningScript { const logProps = runningScript.tailProps; return { args: runningScript.args.slice(), filename: runningScript.filename, logs: runningScript.logs.map((x) => "" + x), 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, tailProperties: !logProps || !logProps.isVisible() ? null : { x: logProps.x, y: logProps.y, width: logProps.width, height: logProps.height, }, title: runningScript.title, threads: runningScript.threads, temporary: runningScript.temporary, }; } /** * 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 */ function failOnHacknetServer(ctx: NetscriptContext, server: BaseServer): boolean { if (server instanceof HacknetServer) { log(ctx, () => `Does not work on Hacknet Servers`); return true; } else { return false; } } // Incrementing value for custom element keys let customElementKey = 0; /** * Wrap a user-provided React Element (or maybe invalid junk) in an Error-catching component, * so the game won't crash and the user gets sensible messages. */ export function wrapUserNode(value: unknown) { return {value}; }