diff --git a/autopilot.js b/autopilot.js index eb9ee448..6b1eb7bc 100644 --- a/autopilot.js +++ b/autopilot.js @@ -48,12 +48,13 @@ let reservedPurchase = 0; // Flag to indicate whether we've reservedPurchase mon let reserveForDaedalus = false, daedalusUnavailable = false; // Flags to indicate that we should be keeping 100b cash on hand to earn an invite to Daedalus let lastScriptsCheck = 0; // Last time we got a listing of all running scripts let killScripts = []; // A list of scripts flagged to be restarted due to changes in priority -let dictOwnedSourceFiles = [], unlockedSFs = [], bitnodeMults, nextBn = 0; // Info for the current bitnode +let dictOwnedSourceFiles = [], unlockedSFs = [], nextBn = 0; // Info for the current bitnode let installedAugmentations = [], playerInstalledAugCount = 0, stanekLaunched = false; // Info for the current ascend let daemonStartTime = 0; // The time we personally launched daemon. let installCountdown = 0; // Start of a countdown before we install augmentations. let bnCompletionSuppressed = false; // Flag if we've detected that we've won the BN, but are suppressing a restart let resetInfo = (/**@returns{ResetInfo}*/() => undefined)(); // Information about the current bitnode +let bitNodeMults = (/**@returns{BitNodeMultipliers}*/() => undefined)(); // bitNode multipliers that can be automatically determined after SF-5 // Replacements for player properties deprecated since 2.3.0 function getTimeInAug() { return Date.now() - resetInfo.lastAugReset; } @@ -103,7 +104,7 @@ async function startUp(ns) { // Collect and cache some one-time data const player = await getNsDataThroughFile(ns, 'ns.getPlayer()'); resetInfo = await getNsDataThroughFile(ns, 'ns.getResetInfo()'); - bitnodeMults = await tryGetBitNodeMultipliers(ns); + bitNodeMults = await tryGetBitNodeMultipliers(ns); dictOwnedSourceFiles = await getActiveSourceFiles(ns, false); unlockedSFs = await getActiveSourceFiles(ns, true); try { @@ -191,10 +192,8 @@ async function checkOnDaedalusStatus(ns, player, stocksValue) { return (4 in unlockedSFs) ? await getNsDataThroughFile(ns, 'ns.singularity.joinFaction(ns.args[0])', null, ["Daedalus"]) : log(ns, "INFO: Please manually join the faction 'Daedalus' as soon as possible to proceed", false, 'info'); } - const bitNodeMults = await tryGetBitNodeMultipliers(ns, false) || { DaedalusAugsRequirement: 1 }; - // Note: A change coming soon will convert DaedalusAugsRequirement from a fractional multiplier, to an integer number of augs. This should support both for now. - const reqDaedalusAugs = bitNodeMults.DaedalusAugsRequirement < 2 ? Math.round(30 * bitNodeMults.DaedalusAugsRequirement) : bitNodeMults.DaedalusAugsRequirement; - if (playerInstalledAugCount !== null && playerInstalledAugCount < reqDaedalusAugs) + // See if we have enough augmentations to attempt to join Daedalus + if (playerInstalledAugCount !== null && playerInstalledAugCount < bitNodeMults.DaedalusAugsRequirement) return daedalusUnavailable = true; // Won't be able to unlock daedalus this ascend // If we have sufficient augs and hacking, all we need is the money (100b) const totalWorth = player.money + stocksValue; @@ -597,8 +596,8 @@ async function shouldDelayInstall(ns, player, facmanOutput) { if (!options['disable-wait-for-4s'] && !(await getNsDataThroughFile(ns, `ns.stock.has4SDataTIXAPI()`))) { const totalWorth = player.money + await getStocksValue(ns); const has4S = await getNsDataThroughFile(ns, `ns.stock.has4SData()`); - const totalCost = 25E9 * (bitnodeMults?.FourSigmaMarketDataApiCost || 1) + - (has4S ? 0 : 1E9 * (bitnodeMults?.FourSigmaMarketDataCost || 1)); + const totalCost = 25E9 * bitNodeMults.FourSigmaMarketDataApiCost + + (has4S ? 0 : 1E9 * bitNodeMults.FourSigmaMarketDataCost); const ratio = totalWorth / totalCost; // If we're e.g. 50% of the way there, hold off, regardless of the '--wait-for-4s' setting // TODO: If ratio is > 1, we can afford it - but stockmaster won't buy until it has e.g. 20% more than the cost diff --git a/daemon.js b/daemon.js index a2a4047f..1b320c24 100644 --- a/daemon.js +++ b/daemon.js @@ -124,9 +124,9 @@ let recoveryThreadPadding = 1; // How many multiples to increase the weaken/grow let daemonHost = null; // the name of the host of this daemon, so we don't have to call the function more than once. let hasFormulas = true; let currentTerminalServer = ""; // Periodically updated when intelligence farming, the current connected terminal server. -let dictSourceFiles = (/**@returns{{[bitnode: number]: number;}}*/() => undefined)(); // Available source files -let bitnodeMults = null; // bitnode multipliers that can be automatically determined after SF-5 -let isInBn8 = false; // Flag indicating whether we are in BN8 (where lots of rules change) +let dictSourceFiles = (/**@returns{{[bitNode: number]: number;}}*/() => undefined)(); // Available source files +let bitNodeMults = (/**@returns{BitNodeMultipliers}*/() => undefined)(); +let currentBn = 0, isInBn8 = false; // Flag indicating whether we are in BN8 (where lots of rules change) let haveTixApi = false, have4sApi = false; // Whether we have WSE API accesses let _cachedPlayerInfo = (/**@returns{Player}*/() => undefined)(); // stores multipliers for player abilities and other player info let _ns = (/**@returns{NS}*/() => undefined)(); // Globally available ns reference, for convenience @@ -196,7 +196,7 @@ function reservedMoney(ns) { let playerMoney = ns.getServerMoneyAvailable("home"); if (!ownedCracks.includes("SQLInject.exe") && playerMoney > 200e6) shouldReserve += 250e6; // Start saving at 200m of the 250m required for SQLInject - const fourSigmaCost = (bitnodeMults.FourSigmaMarketDataApiCost * 25000000000); + const fourSigmaCost = (bitNodeMults.FourSigmaMarketDataApiCost * 25000000000); if (!have4sApi && playerMoney >= fourSigmaCost / 2) shouldReserve += fourSigmaCost; // Start saving if we're half-way to buying 4S market access return shouldReserve; @@ -239,7 +239,8 @@ export async function main(ns) { } catch { resetInfo = { currentNode: 1, lastAugReset: Date.now() }; } - isInBn8 = resetInfo.currentNode === 8; // We do some things differently if we're in BN8 (Stock Market BN) + currentBn = resetInfo.currentNode; + isInBn8 = currentBn === 8; // We do some things differently if we're in BN8 (Stock Market BN) dictSourceFiles = await getActiveSourceFiles_Custom(ns, getNsDataThroughFile); log(ns, "The following source files are active: " + JSON.stringify(dictSourceFiles)); @@ -302,7 +303,7 @@ export async function main(ns) { // These scripts are spawned periodically (at some interval) to do their checks, with an optional condition that limits when they should be spawned let shouldUpgradeHacknet = async () => ((await whichServerIsRunning(ns, "hacknet-upgrade-manager.js", false)) === null) && reservedMoney(ns) < ns.getServerMoneyAvailable("home"); // In BN8 (stocks-only bn) and others with hack income disabled, don't waste money on improving hacking infrastructure unless we have plenty of money to spare - let shouldImproveHacking = () => bitnodeMults.ScriptHackMoneyGain != 0 && !isInBn8 || ns.getServerMoneyAvailable("home") > 1e12; + let shouldImproveHacking = () => bitNodeMults.ScriptHackMoneyGain != 0 && !isInBn8 || ns.getServerMoneyAvailable("home") > 1e12; // Note: Periodic script are generally run every 30 seconds, but intervals are spaced out to ensure they aren't all bursting into temporary RAM at the same time. periodicScripts = [ // Buy tor as soon as we can if we haven't already, and all the port crackers (exception: don't buy 2 most expensive port crackers until later if in a no-hack BN) @@ -352,7 +353,7 @@ export async function main(ns) { const allServers = await getNsDataThroughFile(ns, 'scanAllServers(ns)'); await getStaticServerData(ns, allServers); // Gather information about servers that will never change await buildServerList(ns, false, allServers); // create the exhaustive server list - await establishMultipliers(ns); // figure out the various bitnode and player multipliers + await establishMultipliers(ns); // figure out the various bitNode and player multipliers maxTargets = options['initial-max-targets']; if (stockFocus) // Ensure we attempt to target at least all servers that represent stocks if in stock-focus mode maxTargets = Math.max(maxTargets, Object.keys(serverStockSymbols).length); @@ -817,7 +818,7 @@ async function doTargetingLoop(ns) { } // How much a weaken thread is expected to reduce security by -let actualWeakenPotency = () => bitnodeMults.ServerWeakenRate * weakenThreadPotency; +let actualWeakenPotency = () => bitNodeMults.ServerWeakenRate * weakenThreadPotency; // Get a dictionary from retrieving the same infromation for every server name async function getServersDict(ns, command, serverNames) { @@ -948,7 +949,7 @@ class Server { return this._isXpFarming; } serverGrowthPercentage() { - return this.serverGrowth * bitnodeMults.ServerGrowthRate * getPlayerHackingGrowMulti() / 100; + return this.serverGrowth * bitNodeMults.ServerGrowthRate * getPlayerHackingGrowMulti() / 100; } adjustedGrowthRate() { return Math.min(maxGrowthRate, 1 + ((unadjustedGrowthRate - 1) / this.getMinSecurity())); @@ -989,9 +990,9 @@ class Server { } } // Taken from https://github.com/bitburner-official/bitburner-src/blob/dev/src/Hacking.ts#L43 (calculatePercentMoneyHacked) - const playerHackSkill = playerHackSkill(); + const hackLevel = playerHackSkill(); const difficultyMult = (100 - hackDifficulty) / 100; - const skillMult = (playerHackSkill - (this.requiredHackLevel - 1)) / playerHackSkill; + const skillMult = (hackLevel - (this.requiredHackLevel - 1)) / hackLevel; const percentMoneyHacked = (difficultyMult * skillMult * _cachedPlayerInfo.mults.hacking_money * bitNodeMults.ScriptHackMoney) / 240; return this._percentStolenPerHackThread = Math.min(1, Math.max(0, percentMoneyHacked)); } @@ -1249,7 +1250,7 @@ function getFlagsArgs(toolName, target, allowLooping = true) { args.push(stockMode && (toolName == "hack" && shouldManipulateHack[target] || toolName == "grow" && shouldManipulateGrow[target]) ? 1 : 0); if (["hack", "weak"].includes(toolName)) args.push(options['silent-misfires'] || // Optional arg to disable toast warnings about a failed hack if hacking money gain is disabled - (toolName == "hack" && (bitnodeMults.ScriptHackMoneyGain == 0 || isInBn8)) ? 1 : 0); // Disable automatically in BN8 (hack income disabled) + (toolName == "hack" && (bitNodeMults.ScriptHackMoneyGain == 0 || isInBn8)) ? 1 : 0); // Disable automatically in BN8 (hack income disabled) args.push(allowLooping && loopingMode ? 1 : 0); // Argument to indicate whether the cycle should loop perpetually return args; } @@ -1557,7 +1558,7 @@ async function farmHackXp(ns, fractionOfFreeRamToConsume = 1, verbose = false, n singleServerLimit = 0; lastCycleTotalRam = totalServerRam; } - let tryAdvanceMode = bitnodeMults.ScriptHackMoneyGain != 0; // We can't attempt hack-based XP if it's impossible to gain hack income (XP will always be 1/4) + let tryAdvanceMode = bitNodeMults.ScriptHackMoneyGain != 0; // We can't attempt hack-based XP if it's impossible to gain hack income (XP will always be 1/4) let singleServerMode = false; // Start off maximizing hack threads for best targets by spreading their weaken/grow threads to other servers for (let i = 0; i < numTargets; i++) { let lastSchedulingResult; @@ -1868,16 +1869,9 @@ function getHomeProcIsAlive(ns) { async function establishMultipliers(ns) { log(ns, "establishMultipliers"); - - bitnodeMults = (await tryGetBitNodeMultipliers_Custom(ns, getNsDataThroughFile)) || { - // prior to SF-5, bitnodeMults stays null and these mults are set to 1. - ServerGrowthRate: 1, - ServerWeakenRate: 1, - FourSigmaMarketDataApiCost: 1, - ScriptHackMoneyGain: 1 - }; + bitNodeMults = await tryGetBitNodeMultipliers_Custom(ns, getNsDataThroughFile); if (verbose) - log(ns, `Bitnode mults:\n  ${Object.keys(bitnodeMults).filter(k => bitnodeMults[k] != 1.0).map(k => `${k}: ${bitnodeMults[k]}`).join('\n  ')}`); + log(ns, `Bitnode mults:\n  ${Object.keys(bitNodeMults).filter(k => bitNodeMults[k] != 1.0).map(k => `${k}: ${bitNodeMults[k]}`).join('\n  ')}`); } class Tool { diff --git a/faction-manager.js b/faction-manager.js index 2dd1f88d..f546c00f 100644 --- a/faction-manager.js +++ b/faction-manager.js @@ -27,7 +27,8 @@ let playerData = null, bitNode = 0, gangFaction = null; let augsAwaitingInstall, startingPlayerMoney, stockValue = 0; // If the player holds stocks, their liquidation value will be determined let factionNames = [], joinedFactions = [], desiredStatsFilters = [], purchaseFactionDonations = []; let ownedAugmentations = [], simulatedOwnedAugmentations = [], effectiveSourceFiles = [], allAugStats = [], priorityAugs = [], purchaseableAugs = []; -let factionData = {}, augmentationData = {}, bitNodeMults = {}; +let factionData = {}, augmentationData = {}; +let bitNodeMults = (/**@returns{BitNodeMultipliers}*/() => undefined)(); let printToTerminal, ignorePlayerData; let _ns; // Used to avoid passing ns to functions that don't need it except for some logs. @@ -182,11 +183,8 @@ export async function main(ns) { const sort = unshorten(options.sort || desiredStatsFilters[0]); displayFactionSummary(ns, sort, options.u || options.unique, afterFactions, hideSummaryStats); - // Determine the current bitnode multipliers, or default to some sane assumptions if they aren't available (requires SF 5.1) - bitNodeMults = await tryGetBitNodeMultipliers(ns, false) || { - DaedalusAugsRequirement: bitNode == 6 || bitNode == 7 ? 35 : 30, - FactionWorkRepGain: bitNode == 2 ? 0.5 : bitNode == 4 ? 0.75 : bitNode == 13 ? 0.6 : bitNode == 14 ? 0.2 : 1, - }; + // Determine the current bitnode multipliers + bitNodeMults = await tryGetBitNodeMultipliers(ns); // Create the table of all augmentations, and the breakdown of what we can afford await manageUnownedAugmentations(ns, omitAugs); diff --git a/gangs.js b/gangs.js index 92e4b7c0..dc7247f7 100644 --- a/gangs.js +++ b/gangs.js @@ -165,7 +165,7 @@ async function initialize(ns) { // Initialize information about gang members and crimes allTaskNames = await getNsDataThroughFile(ns, 'ns.gang.getTaskNames()') allTaskStats = await getGangInfoDict(ns, allTaskNames, 'getTaskStats'); - multGangSoftcap = (await tryGetBitNodeMultipliers(ns))?.GangSoftcap || 1; + multGangSoftcap = (await tryGetBitNodeMultipliers(ns)).GangSoftcap; myGangMembers = await getNsDataThroughFile(ns, 'ns.gang.getMemberNames()'); const dictMembers = await getGangInfoDict(ns, myGangMembers, 'getMemberInformation'); for (const member of Object.values(dictMembers)) // Initialize the current activity of each member @@ -230,7 +230,7 @@ async function onTerritoryTick(ns, myGangInfo) { // Update gang members in case someone died in a clash myGangMembers = await getNsDataThroughFile(ns, 'ns.gang.getMemberNames()'); - const canRecruit = await getNsDataThroughFile(ns, 'ns.gang.canRecruitMember()'); + const canRecruit = await getNsDataThroughFile(ns, 'ns.gang.canRecruitMember()'); if (canRecruit) await doRecruitMember(ns) // Recruit new members if available const dictMembers = await getGangInfoDict(ns, myGangMembers, 'getMemberInformation'); diff --git a/helpers.js b/helpers.js index a8b43787..5d619027 100644 --- a/helpers.js +++ b/helpers.js @@ -125,7 +125,7 @@ export function getFnIsAliveViaNsPs(ns) { * Retrieve the result of an ns command by executing it in a temporary .js script, writing the result to a file, then shuting it down * Importing incurs a maximum of 1.1 GB RAM (0 GB for ns.read, 1 GB for ns.run, 0.1 GB for ns.isRunning). * Has the capacity to retry if there is a failure (e.g. due to lack of RAM available). Not recommended for performance-critical code. - * @param {NS} ns - The nestcript instance passed to your script's main entry point + * @param {NS} ns The nestcript instance passed to your script's main entry point * @param {string} command - The ns command that should be invoked to get the desired data (e.g. "ns.getServer('home')" ) * @param {string=} fName - (default "/Temp/{command-name}.txt") The name of the file to which data will be written to disk by a temporary process * @param {args=} args - args to be passed in as arguments to command being run as a new script. @@ -154,7 +154,7 @@ function getDefaultCommandFileName(command, ext = '.txt') { * An advanced version of getNsDataThroughFile that lets you pass your own "fnRun" implementation to reduce RAM requirements * Importing incurs no RAM (now that ns.read is free) plus whatever fnRun you provide it * Has the capacity to retry if there is a failure (e.g. due to lack of RAM available). Not recommended for performance-critical code. - * @param {NS} ns - The nestcript instance passed to your script's main entry point + * @param {NS} ns The nestcript instance passed to your script's main entry point * @param {function} fnRun - A single-argument function used to start the new sript, e.g. `ns.run` or `(f,...args) => ns.exec(f, "home", ...args)` * @param {args=} args - args to be passed in as arguments to command being run as a new script. **/ @@ -208,7 +208,7 @@ export async function getNsDataThroughFile_Custom(ns, fnRun, command, fName = nu } /** Evaluate an arbitrary ns command by writing it to a new script and then running or executing it. - * @param {NS} ns - The nestcript instance passed to your script's main entry point + * @param {NS} ns The nestcript instance passed to your script's main entry point * @param {string} command - The ns command that should be invoked to get the desired data (e.g. "ns.getServer('home')" ) * @param {string=} fileName - (default "/Temp/{command-name}.txt") The name of the file to which data will be written to disk by a temporary process * @param {args=} args - args to be passed in as arguments to command being run as a new script. @@ -221,7 +221,7 @@ export async function runCommand(ns, command, fileName, args = [], verbose = fal } const _cachedExports = []; -/** @param {NS} ns - The nestcript instance passed to your script's main entry point +/** @param {NS} ns The nestcript instance passed to your script's main entry point * @returns {string[]} The set of all funciton names exported by this file. */ function getExports(ns) { if (_cachedExports.length > 0) return _cachedExports; @@ -238,7 +238,7 @@ function getExports(ns) { /** * An advanced version of runCommand that lets you pass your own "isAlive" test to reduce RAM requirements (e.g. to avoid referencing ns.isRunning) * Importing incurs 0 GB RAM (assuming fnRun, fnWrite are implemented using another ns function you already reference elsewhere like ns.exec) - * @param {NS} ns - The nestcript instance passed to your script's main entry point + * @param {NS} ns The nestcript instance passed to your script's main entry point * @param {function} fnRun - A single-argument function used to start the new sript, e.g. `ns.run` or `(f,...args) => ns.exec(f, "home", ...args)` * @param {string} command - The ns command that should be invoked to get the desired data (e.g. "ns.getServer('home')" ) * @param {string=} fileName - (default "/Temp/{commandhash}-data.txt") The name of the file to which data will be written to disk by a temporary process @@ -299,11 +299,10 @@ export async function runCommand_Custom(ns, fnRun, command, fileName, args = [], /** * Wait for a process id to complete running * Importing incurs a maximum of 0.1 GB RAM (for ns.isRunning) - * @param {NS} ns - The nestcript instance passed to your script's main entry point - * @param {int} pid - The process id to monitor - * @param {bool=} verbose - (default false) If set to true, pid and result of command are logged. - **/ -export async function waitForProcessToComplete(ns, pid, verbose) { + * @param {NS} ns The nestcript instance passed to your script's main entry point + * @param {number} pid - The process id to monitor + * @param {boolean} verbose - (default false) If set to true, pid and result of command are logged. **/ +export async function waitForProcessToComplete(ns, pid, verbose = false) { checkNsInstance(ns, '"waitForProcessToComplete"'); if (!verbose) disableLogs(ns, ['isRunning']); return await waitForProcessToComplete_Custom(ns, ns.isRunning, pid, verbose); @@ -311,9 +310,10 @@ export async function waitForProcessToComplete(ns, pid, verbose) { /** * An advanced version of waitForProcessToComplete that lets you pass your own "isAlive" test to reduce RAM requirements (e.g. to avoid referencing ns.isRunning) * Importing incurs 0 GB RAM (assuming fnIsAlive is implemented using another ns function you already reference elsewhere like ns.ps) - * @param {NS} ns - The nestcript instance passed to your script's main entry point + * @param {NS} ns The nestcript instance passed to your script's main entry point * @param {(pid: number) => Promise} fnIsAlive - A single-argument function used to start the new sript, e.g. `ns.isRunning` or `pid => ns.ps("home").some(process => process.pid === pid)` - **/ + * @param {number} pid - The process id to monitor + * @param {boolean} verbose - (default false) If set to true, pid and result of command are logged. **/ export async function waitForProcessToComplete_Custom(ns, fnIsAlive, pid, verbose) { checkNsInstance(ns, '"waitForProcessToComplete_Custom"'); if (!verbose) disableLogs(ns, ['sleep']); @@ -344,7 +344,7 @@ function asError(error) { } /** Helper to retry something that failed temporarily (can happen when e.g. we temporarily don't have enough RAM to run) - * @param {NS} ns - The nestcript instance passed to your script's main entry point */ + * @param {NS} ns The nestcript instance passed to your script's main entry point */ export async function autoRetry(ns, fnFunctionThatMayFail, fnSuccessCondition, errorContext = "Success condition not met", maxRetries = 5, initialRetryDelayMs = 50, backoffRate = 3, verbose = false, tprintFatalErrors = true) { checkNsInstance(ns, '"autoRetry"'); @@ -388,7 +388,7 @@ export async function autoRetry(ns, fnFunctionThatMayFail, fnSuccessCondition, e } /** Helper for extracting the error message from an error thrown by the game. - * @param {string|Error} err - A thrown error message or object + * @param {string|Error} err A thrown error message or object */ export function getErrorInfo(err) { return err === undefined || err == null ? "(null error)" : @@ -398,9 +398,11 @@ export function getErrorInfo(err) { } /** Helper to log a message, and optionally also tprint it and toast it - * @param {NS} ns - The nestcript instance passed to your script's main entry point + * @param {NS} ns The nestcript instance passed to your script's main entry point + * @param {string} message The message to display + * @param {boolean} alsoPrintToTerminal Set to true to print not only to the current script's tail file, but to the terminal * @param {""|"success"|"warning"|"error"|"info"} toastStyle - If specified, your log will will also become a toast notification - */ + * @param {int} */ export function log(ns, message = "", alsoPrintToTerminal = false, toastStyle = "", maxToastLength = Number.MAX_SAFE_INTEGER) { checkNsInstance(ns, '"log"'); ns.print(message); @@ -415,7 +417,8 @@ export function log(ns, message = "", alsoPrintToTerminal = false, toastStyle = } /** Helper to get a list of all hostnames on the network - * @param {NS} ns - The nestcript instance passed to your script's main entry point */ + * @param {NS} ns The nestcript instance passed to your script's main entry point + * @returns {string[]}>} **/ export function scanAllServers(ns) { checkNsInstance(ns, '"scanAllServers"'); let discoveredHosts = []; // Hosts (a.k.a. servers) we have scanned @@ -431,15 +434,18 @@ export function scanAllServers(ns) { return discoveredHosts; // The list of scanned hosts should now be the set of all hosts in the game! } -/** @param {NS} ns - * Get a dictionary of active source files, taking into account the current active bitnode as well (optionally disabled). **/ +/** Get a dictionary of active source files, taking into account the current active bitNode as well (optionally disabled). + * @param {NS} ns The nestcript instance passed to your script's main entry point + * @returns {Promise<{[k: number]: number}>} A dictionary keyed by source file number, where the value is the level (between 1 and 3 for all but BN12) **/ export async function getActiveSourceFiles(ns, includeLevelsFromCurrentBitnode = true) { return await getActiveSourceFiles_Custom(ns, getNsDataThroughFile, includeLevelsFromCurrentBitnode); } -/** @param {NS} ns - * @param {(ns: NS, command: string, fName?: string, args?: any, verbose?: any, maxRetries?: number, retryDelayMs?: number) => Promise} fnGetNsDataThroughFile - * getActiveSourceFiles Helper that allows the user to pass in their chosen implementation of getNsDataThroughFile to minimize RAM usage **/ +/** getActiveSourceFiles Helper that allows the user to pass in their chosen implementation of getNsDataThroughFile to minimize RAM usage + * @param {NS} ns The nestcript instance passed to your script's main entry point + * @param {(ns: NS, command: string, fName?: string, args?: any, verbose?: any, maxRetries?: number, retryDelayMs?: number) => Promise} fnGetNsDataThroughFile getActiveSourceFiles Helper that allows the user to pass in their chosen implementation of getNsDataThroughFile to minimize RAM usage + * @param {bool} includeLevelsFromCurrentBitnode Set to true to use the current bitNode number to infer the effective source code level (for purposes of determining what features are unlocked) + * @returns {Promise<{[k: number]: number}>} A dictionary keyed by source file number, where the value is the level (between 1 and 3 for all but BN12) **/ export async function getActiveSourceFiles_Custom(ns, fnGetNsDataThroughFile, includeLevelsFromCurrentBitnode = true) { checkNsInstance(ns, '"getActiveSourceFiles"'); // Find out what source files the user has unlocked @@ -449,36 +455,113 @@ export async function getActiveSourceFiles_Custom(ns, fnGetNsDataThroughFile, in `Object.fromEntries(ns.singularity.getOwnedSourceFiles().map(sf => [sf.n, sf.lvl]))`, '/Temp/owned-source-files.txt'); } catch { dictSourceFiles = {}; } // If this fails (e.g. low RAM), return an empty dictionary - // If the user is currently in a given bitnode, they will have its features unlocked - // TODO: This is true of BN4, but not BN14.2, Check them all! + // If the user is currently in a given bitNode, they will have its features unlocked if (includeLevelsFromCurrentBitnode) { try { const currentNode = (await fnGetNsDataThroughFile(ns, 'ns.getResetInfo()', '/Temp/reset-info.txt')).currentNode; - dictSourceFiles[currentNode] = Math.max((currentNode == 4 ? 3 : 1), dictSourceFiles[currentNode] || 0); + dictSourceFiles[currentNode] = Math.max(3, dictSourceFiles[currentNode] || 0); } catch { /* We are expected to be fault-tolerant in low-ram conditions */ } } return dictSourceFiles; } -/** @param {NS} ns - * Return bitnode multiplers, or null if they cannot be accessed. **/ +/** Return bitNode multiplers, or a best guess based on hard-coded values if they cannot currently be retrieved (no SF5, or insufficient RAM) + * @param {NS} ns The nestcript instance passed to your script's main entry point + * @returns {Promise} the current bitNode multipliers, or a best guess if we do not currently have access. */ export async function tryGetBitNodeMultipliers(ns) { return await tryGetBitNodeMultipliers_Custom(ns, getNsDataThroughFile); } -/** @param {NS} ns - * tryGetBitNodeMultipliers Helper that allows the user to pass in their chosen implementation of getNsDataThroughFile to minimize RAM usage **/ +/** tryGetBitNodeMultipliers Helper that allows the user to pass in their chosen implementation of getNsDataThroughFile to minimize RAM usage + * @param {NS} ns The nestcript instance passed to your script's main entry point + * @param {(ns: NS, command: string, fName?: string, args?: any, verbose?: any, maxRetries?: number, retryDelayMs?: number) => Promise} fnGetNsDataThroughFile getActiveSourceFiles Helper that allows the user to pass in their chosen implementation of getNsDataThroughFile to minimize RAM usage + * @returns {Promise} the current bitNode multipliers, or a best guess if we do not currently have access. */ export async function tryGetBitNodeMultipliers_Custom(ns, fnGetNsDataThroughFile) { checkNsInstance(ns, '"tryGetBitNodeMultipliers"'); let canGetBitNodeMultipliers = false; - try { canGetBitNodeMultipliers = 5 in (await getActiveSourceFiles_Custom(ns, fnGetNsDataThroughFile)); } catch { } - if (!canGetBitNodeMultipliers) return null; - try { return await fnGetNsDataThroughFile(ns, 'ns.getBitNodeMultipliers()', '/Temp/bitnode-multipliers.txt'); } catch { } - return null; + try { + canGetBitNodeMultipliers = 5 in (await getActiveSourceFiles_Custom(ns, fnGetNsDataThroughFile)); + } catch { } + if (canGetBitNodeMultipliers) { + try { + return await fnGetNsDataThroughFile(ns, 'ns.getBitNodeMultipliers()', '/Temp/bitNode-multipliers.txt'); + } catch { } + } + return await getHardCodedBitNodeMultipliers(ns, fnGetNsDataThroughFile); +} + +/** Cheeky hard-coded values stolen from https://github.com/bitburner-official/bitburner-src/blob/dev/src/BitNode/BitNode.tsx#L456 + * so that we essentially can provide bitNode multipliers even without SF-5 or sufficient RAM to request them. + * We still prefer to use the API though, this is just a a fallback, but it may become stale over time. + * @param {NS} ns The nestcript instance passed to your script's main entry point + * @param {(ns: NS, command: string, fName?: string, args?: any, verbose?: any, maxRetries?: number, retryDelayMs?: number) => Promise} fnGetNsDataThroughFile getActiveSourceFiles Helper that allows the user to pass in their chosen implementation of getNsDataThroughFile to minimize RAM usage + * @returns {Promise} a mocked BitNodeMultipliers instance with hard-coded values. */ +async function getHardCodedBitNodeMultipliers(ns, fnGetNsDataThroughFile) { + let bn = 1; + try { bn = (await fnGetNsDataThroughFile(ns, 'ns.getResetInfo()', '/Temp/reset-info.txt')).currentNode; } + catch { /* We are expected to be fault-tolerant in low-ram conditions */ } + return { + AgilityLevelMultiplier: [1, 1, 1, 1, 1, 1, 1, 1, 0.45, 0.4, 1, 1, 0.7, 0.5][bn - 1], + AugmentationMoneyCost: [1, 1, 3, 1, 2, 1, 3, 1, 1, 5, 2, 1, 1, 1.5][bn - 1], + AugmentationRepCost: [1, 1, 3, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1][bn - 1], + BladeburnerRank: [1, 1, 1, 1, 1, 1, 0.6, 0, 0.9, 0.8, 1, 1, 0.45, 0.6][bn - 1], + BladeburnerSkillCost: [1, 1, 1, 1, 1, 1, 2, 1, 1.2, 1, 1, 1, 2, 2][bn - 1], + CharismaLevelMultiplier: [1, 1, 1, 1, 1, 1, 1, 1, 0.45, 0.4, 1, 1, 1, 1][bn - 1], + ClassGymExpGain: [1, 1, 1, 0.5, 1, 1, 1, 1, 1, 1, 1, 1, 0.5, 1][bn - 1], + CodingContractMoney: [1, 1, 1, 1, 1, 1, 1, 0, 1, 0.5, 0.25, 1, 0.4, 1][bn - 1], + CompanyWorkExpGain: [1, 1, 1, 0.5, 1, 1, 1, 1, 1, 1, 1, 1, 0.5, 1][bn - 1], + CompanyWorkMoney: [1, 1, 0.25, 0.1, 1, 0.5, 0.5, 0, 1, 0.5, 0.5, 1, 0.4, 1][bn - 1], + CompanyWorkRepGain: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.2][bn - 1], + CorporationDivisions: [1, 0.9, 1, 1, 0.75, 0.8, 0.8, 0, 0.8, 0.9, 0.9, 0.5, 0.4, 0.8][bn - 1], + CorporationSoftcap: [1, 0.9, 1, 1, 1, 0.9, 0.9, 0, 0.75, 0.9, 0.9, 0.8, 0.4, 0.9][bn - 1], + CorporationValuation: [1, 1, 1, 1, 0.75, 0.2, 0.2, 0, 0.5, 0.5, 0.1, 1, 0.001, 0.4][bn - 1], + CrimeExpGain: [1, 1, 1, 0.5, 1, 1, 1, 1, 1, 1, 1, 1, 0.5, 1][bn - 1], + CrimeMoney: [1, 3, 0.25, 0.2, 0.5, 0.75, 0.75, 0, 0.5, 0.5, 3, 1, 0.4, 0.75][bn - 1], + CrimeSuccessRate: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.4][bn - 1], + DaedalusAugsRequirement: [30, 30, 30, 30, 30, 35, 35, 30, 30, 30, 30, 31, 30, 30][bn - 1], + DefenseLevelMultiplier: [1, 1, 1, 1, 1, 1, 1, 1, 0.45, 0.4, 1, 1, 0.7, 1][bn - 1], + DexterityLevelMultiplier: [1, 1, 1, 1, 1, 1, 1, 1, 0.45, 0.4, 1, 1, 0.7, 0.5][bn - 1], + FactionPassiveRepGain: [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1][bn - 1], + FactionWorkExpGain: [1, 1, 1, 0.5, 1, 1, 1, 1, 1, 1, 1, 1, 0.5, 1][bn - 1], + FactionWorkRepGain: [1, 0.5, 1, 0.75, 1, 1, 1, 1, 1, 1, 1, 1, 0.6, 0.2][bn - 1], + FourSigmaMarketDataApiCost: [1, 1, 1, 1, 1, 1, 2, 1, 4, 1, 4, 1, 10, 1][bn - 1], + FourSigmaMarketDataCost: [1, 1, 1, 1, 1, 1, 2, 1, 5, 1, 4, 1, 10, 1][bn - 1], + GangSoftcap: [1, 1, 0.9, 1, 1, 0.7, 0.7, 0, 0.8, 0.9, 1, 0.8, 0.3, 0.7][bn - 1], + GangUniqueAugs: [1, 1, 0.5, 0.5, 0.5, 0.2, 0.2, 0, 0.25, 0.25, 0.75, 1, 0.1, 0.4][bn - 1], + GoPower: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4][bn - 1], + HackExpGain: [1, 1, 1, 0.4, 0.5, 0.25, 0.25, 1, 0.05, 1, 0.5, 1, 0.1, 1][bn - 1], + HackingLevelMultiplier: [1, 0.8, 0.8, 1, 1, 0.35, 0.35, 1, 0.5, 0.35, 0.6, 1, 0.25, 0.4][bn - 1], + HackingSpeedMultiplier: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.3][bn - 1], + HacknetNodeMoney: [1, 1, 0.25, 0.05, 0.2, 0.2, 0.2, 0, 1, 0.5, 0.1, 1, 0.4, 0.25][bn - 1], + HomeComputerRamCost: [1, 1, 1.5, 1, 1, 1, 1, 1, 5, 1.5, 1, 1, 1, 1][bn - 1], + InfiltrationMoney: [1, 3, 1, 1, 1.5, 0.75, 0.75, 0, 1, 0.5, 2.5, 1, 1, 0.75][bn - 1], + InfiltrationRep: [1, 1, 1, 1, 1.5, 1, 1, 1, 1, 1, 2.5, 1, 1, 1][bn - 1], + ManualHackMoney: [1, 1, 1, 1, 1, 1, 1, 0, 1, 0.5, 1, 1, 1, 1][bn - 1], + PurchasedServerCost: [1, 1, 2, 1, 1, 1, 1, 1, 1, 5, 1, 1, 1, 1][bn - 1], + PurchasedServerSoftcap: [1, 1.3, 1.3, 1.2, 1.2, 2, 2, 4, 1, 1.1, 2, 1, 1.6, 1][bn - 1], + PurchasedServerLimit: [1, 1, 1, 1, 1, 1, 1, 1, 0, 0.6, 1, 1, 1, 1][bn - 1], + PurchasedServerMaxRam: [1, 1, 1, 1, 1, 1, 1, 1, 1, 0.5, 1, 1, 1, 1][bn - 1], + RepToDonateToFaction: [1, 1, 0.5, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1][bn - 1], + ScriptHackMoney: [1, 1, 0.2, 0.2, 0.15, 0.75, 0.5, 0.3, 0.1, 0.5, 1, 1, 0.2, 0.3][bn - 1], + ScriptHackMoneyGain: [1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1][bn - 1], + ServerGrowthRate: [1, 0.8, 0.2, 1, 1, 1, 1, 1, 1, 1, 0.2, 1, 1, 1][bn - 1], + ServerMaxMoney: [1, 0.08, 0.04, 0.1125, 1, 0.2, 0.2, 1, 0.01, 1, 0.01, 1, 0.3375, 0.7][bn - 1], + ServerStartingMoney: [1, 0.4, 0.2, 0.75, 0.5, 0.5, 0.5, 1, 0.1, 1, 0.1, 1, 0.75, 0.5][bn - 1], + ServerStartingSecurity: [1, 1, 1, 1, 2, 1.5, 1.5, 1, 2.5, 1, 1, 1.5, 3, 1.5][bn - 1], + ServerWeakenRate: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1][bn - 1], + StrengthLevelMultiplier: [1, 1, 1, 1, 1, 1, 1, 1, 0.45, 0.4, 1, 1, 0.7, 0.5][bn - 1], + StaneksGiftPowerMultiplier: [1, 2, 0.75, 1.5, 1.3, 0.5, 0.9, 1, 0.5, 0.75, 1, 1, 2, 0.5][bn - 1], + StaneksGiftExtraSize: [0, -6, -2, 0, 0, 2, -1, -99, 2, -3, 0, 1, 1, -1][bn - 1], + WorldDaemonDifficulty: [1, 5, 2, 3, 1.5, 2, 2, 1, 2, 2, 1.5, 1, 3, 5][bn - 1] + } } -/** @param {NS} ns - * Returns the number of instances of the current script running on the specified host. **/ +/** Returns the number of instances of the current script running on the specified host. + * @param {NS} ns The nestcript instance passed to your script's main entry point + * @param {string} onHost - The host to search for the script on + * @param {boolean} warn - Whether to automatically log a warning when there are more than other running instances + * @param {tailOtherInstances} warn - Whether to open the tail window of other running instances so that they can be easily killed + * @returns {Promise} The number of other instance of this script running on this host. */ export async function instanceCount(ns, onHost = "home", warn = true, tailOtherInstances = true) { checkNsInstance(ns, '"alreadyRunning"'); const scriptName = ns.getScriptName(); @@ -495,7 +578,8 @@ export async function instanceCount(ns, onHost = "home", warn = true, tailOtherI } /** Helper function to get all stock symbols, or null if you do not have TIX api access. - * @param {NS} ns */ + * @param {NS} ns The nestcript instance passed to your script's main entry point + * @returns {Promise} array of stock symbols */ export async function getStockSymbols(ns) { return await getNsDataThroughFile(ns, `(() => { try { return ns.stock.getSymbols(); } catch { return null; } })()`, @@ -503,7 +587,8 @@ export async function getStockSymbols(ns) { } /** Helper function to get the total value of stocks using as little RAM as possible. - * @param {NS} ns */ + * @param {NS} ns The nestcript instance passed to your script's main entry point + * @returns {Promise} The current total dollar value of all owned stocks */ export async function getStocksValue(ns) { let stockSymbols = await getStockSymbols(ns); if (stockSymbols == null) return 0; // No TIX API Access @@ -522,8 +607,8 @@ export async function getStocksValue(ns) { - 100000 * (Math.sign(stk.pos[0]) + Math.sign(stk.pos[2])), 0); } -/** @param {NS} ns - * Returns a helpful error message if we forgot to pass the ns instance to a function */ +/** Returns a helpful error message if we forgot to pass the ns instance to a function + * @param {NS} ns The nestcript instance passed to your script's main entry point */ export function checkNsInstance(ns, fnName = "this function") { if (ns === undefined || !ns.print) throw new Error(`The first argument to ${fnName} should be a 'ns' instance.`); return ns; @@ -532,7 +617,7 @@ export function checkNsInstance(ns, fnName = "this function") { /** A helper to parse the command line arguments with a bunch of extra features, such as * - Loading a persistent defaults override from a local config file named after the script. * - Rendering "--help" output without all scripts having to explicitly specify it - * @param {NS} ns + * @param {NS} ns The nestcript instance passed to your script's main entry point * @param {[string, string | number | boolean | string[]][]} argsSchema - Specification of possible command line args. **/ export function getConfiguration(ns, argsSchema) { checkNsInstance(ns, '"getConfig"'); diff --git a/stockmaster.js b/stockmaster.js index 1698b832..1cab1757 100644 --- a/stockmaster.js +++ b/stockmaster.js @@ -31,6 +31,7 @@ const catchUpTickTime = 4000; let lastTick = 0; let sleepInterval = 1000; let resetInfo = (/**@returns{ResetInfo}*/() => undefined)(); // Information about the current bitnode +let bitNodeMults = (/**@returns{BitNodeMultipliers}*/() => undefined)(); let options; const argsSchema = [ @@ -135,11 +136,7 @@ export async function main(ns) { allStockSymbols = await getStockSymbols(ns); allStocks = await initAllStocks(ns); - - let bitnodeMults; - if (5 in dictSourceFiles) bitnodeMults = await tryGetBitNodeMultipliers(ns); - // Assume bitnode mults are 1 if user doesn't have this API access yet - if (!bitnodeMults) bitnodeMults = { FourSigmaMarketDataCost: 1, FourSigmaMarketDataApiCost: 1 }; + bitNodeMults = await tryGetBitNodeMultipliers(ns); if (showMarketSummary) await launchSummaryTail(ns); // Opens a separate script / window to continuously display the Pre4S forecast @@ -163,7 +160,7 @@ export async function main(ns) { const holdings = await refresh(ns, !pre4s, allStocks, myStocks); // Returns total stock value const corpus = holdings + playerStats.money; // Corpus means total stocks + cash const maxHoldings = (1 - fracH) * corpus; // The largest value of stock we could hold without violiating fracH (Fraction to keep as cash) - if (pre4s && !mock && await tryGet4SApi(ns, playerStats, bitnodeMults, corpus * (options['buy-4s-budget'] - fracH) - reserve)) + if (pre4s && !mock && await tryGet4SApi(ns, playerStats, corpus * (options['buy-4s-budget'] - fracH) - reserve)) continue; // Start the loop over if we just bought 4S API access // Be more conservative with our decisions if we don't have 4S data const thresholdToBuy = pre4s ? options['pre-4s-buy-threshold-return'] : options['buy-threshold']; @@ -564,10 +561,10 @@ async function liquidate(ns) { /** @param {NS} ns **/ /** @param {Player} playerStats **/ -async function tryGet4SApi(ns, playerStats, bitnodeMults, budget) { +async function tryGet4SApi(ns, playerStats, budget) { if (await checkAccess(ns, 'has4SDataTIXAPI')) return false; // Only return true if we just bought it - const cost4sData = 1E9 * bitnodeMults.FourSigmaMarketDataCost; - const cost4sApi = 25E9 * bitnodeMults.FourSigmaMarketDataApiCost; + const cost4sData = 1E9 * bitNodeMults.FourSigmaMarketDataCost; + const cost4sApi = 25E9 * bitNodeMults.FourSigmaMarketDataApiCost; const has4S = await checkAccess(ns, 'has4SData'); const totalCost = (has4S ? 0 : cost4sData) + cost4sApi; // Liquidate shares if it would allow us to afford 4S API data @@ -588,11 +585,6 @@ async function tryGet4SApi(ns, playerStats, bitnodeMults, budget) { return true; } else { log(ns, 'ERROR attempting to purchase 4SMarketDataTixApi!', false, 'error'); - if (!(5 in dictSourceFiles)) { // If we do not have access to bitnode multipliers, assume the cost is double and try again later - log(ns, 'INFO: Bitnode mults are not available (SF5) - assuming everything is twice as expensive in the current bitnode.'); - bitnodeMults.FourSigmaMarketDataCost *= 2; - bitnodeMults.FourSigmaMarketDataApiCost *= 2; - } } return false; } diff --git a/work-for-factions.js b/work-for-factions.js index 8a877852..90831108 100644 --- a/work-for-factions.js +++ b/work-for-factions.js @@ -83,8 +83,9 @@ const waitForFactionInviteTime = 30 * 1000; // The game will only issue one new let shouldFocus; // Whether we should focus on work or let it be backgrounded (based on whether "Neuroreceptor Management Implant" is owned, or "--no-focus" is specified) // And a bunch of globals because managing state and encapsulation is hard. let hasFocusPenalty, hasSimulacrum, repToDonate, fulcrummHackReq, notifiedAboutDaedalus, playerInBladeburner; -let bitnodeMultipliers, dictSourceFiles, dictFactionFavors, playerGang, mainLoopStart, scope, numJoinedFactions, lastTravel, crimeCount; +let dictSourceFiles, dictFactionFavors, playerGang, mainLoopStart, scope, numJoinedFactions, lastTravel, crimeCount; let firstFactions, skipFactions, completedFactions, softCompletedFactions, mostExpensiveAugByFaction, mostExpensiveDesiredAugByFaction; +let bitNodeMults = (/**@returns{BitNodeMultipliers}*/() => undefined)(); // Trick to get strong typing in mono export function autocomplete(data, args) { data.flags(argsSchema); @@ -162,17 +163,7 @@ async function loadStartupData(ns) { repToDonate = await getNsDataThroughFile(ns, 'ns.getFavorToDonate()'); const playerInfo = await getPlayerInfo(ns); const allKnownFactions = factions.concat(playerInfo.factions.filter(f => !factions.includes(f))); - bitnodeMultipliers = await tryGetBitNodeMultipliers(ns) || - { // BN mults are used to estimate time to train up stats. Default to 1.0 if unknown - HackingLevelMultiplier: 1.0, - StrengthLevelMultiplier: 1.0, - DefenseLevelMultiplier: 1.0, - DexterityLevelMultiplier: 1.0, - AgilityLevelMultiplier: 1.0, - CharismaLevelMultiplier: 1.0, - ClassGymExpGain: 1.0, - CrimeExpGain: 1.0, - }; + bitNodeMults = await tryGetBitNodeMultipliers(ns); // Get some faction and augmentation information to decide what remains to be purchased dictFactionFavors = await getNsDataThroughFile(ns, dictCommand('ns.singularity.getFactionFavor(o)'), '/Temp/getFactionFavors.txt', allKnownFactions); @@ -406,10 +397,10 @@ async function earnFactionInvite(ns, factionName) { // Establish some helper functions used to determine how fast we can train a stat const title = s => s && s[0].toUpperCase() + s.slice(1); // Annoyingly bitnode multis capitalize the first letter physical stat name const heuristic = (stat, trainingBitnodeMult) => - Math.sqrt(player.mults[stat] * bitnodeMultipliers[`${title(stat)}LevelMultiplier`] * + Math.sqrt(player.mults[stat] * bitNodeMults[`${title(stat)}LevelMultiplier`] * /* */ player.mults[`${stat}_exp`] * trainingBitnodeMult); - const crimeHeuristic = (stat) => heuristic(stat, bitnodeMultipliers.CrimeExpGain); // When training with crime - const classHeuristic = (stat) => heuristic(stat, bitnodeMultipliers.ClassGymExpGain); // When training in university + const crimeHeuristic = (stat) => heuristic(stat, bitNodeMults.CrimeExpGain); // When training with crime + const classHeuristic = (stat) => heuristic(stat, bitNodeMults.ClassGymExpGain); // When training in university // Check which stats need to be trained up requirement = requiredCombatByFaction[factionName]; let deficientStats = !requirement ? [] : physicalStats.map(stat => ({ stat, value: player.skills[stat] })).filter(stat => stat.value < requirement); @@ -430,8 +421,8 @@ async function earnFactionInvite(ns, factionName) { `You can control this with --training-stat-per-multi-threshold. Current sqrt(mult*exp_mult*bn_mult*bn_exp_mult) ` + `should be ~${formatNumberShort(em, 2)}, have ` + deficientStats.map(s => s.stat).map(s => `${s.slice(0, 3)}: sqrt(` + `${formatNumberShort(player.mults[s])}*${formatNumberShort(player.mults[`${s}_exp`])}*` + - `${formatNumberShort(bitnodeMultipliers[`${title(s)}LevelMultiplier`])}*` + - `${formatNumberShort(bitnodeMultipliers.CrimeExpGain)})=${formatNumberShort(crimeHeuristic(s))}`).join(", ")); + `${formatNumberShort(bitNodeMults[`${title(s)}LevelMultiplier`])}*` + + `${formatNumberShort(bitNodeMults.CrimeExpGain)})=${formatNumberShort(crimeHeuristic(s))}`).join(", ")); doCrime = true; // TODO: There could be more efficient ways to gain combat stats than homicide, although at least this serves future crime factions } if (doCrime && options['no-crime']) @@ -459,7 +450,7 @@ async function earnFactionInvite(ns, factionName) { else if (classHeuristic('hacking') < em) return ns.print(`Your combination of Hacking mult (${formatNumberShort(player.mults.hacking)}), exp_mult ` + `(${formatNumberShort(player.mults.hacking_exp)}), and bitnode hacking / study exp mults ` + - `(${formatNumberShort(bitnodeMultipliers.HackingLevelMultiplier)}) / (${formatNumberShort(bitnodeMultipliers.ClassGymExpGain)}) ` + + `(${formatNumberShort(bitNodeMults.HackingLevelMultiplier)}) / (${formatNumberShort(bitNodeMults.ClassGymExpGain)}) ` + `are probably too low to increase hack from ${player.skills.hacking} to ${requirement} in a reasonable amount of time ` + `(${formatNumberShort(classHeuristic('hacking'))} < ${formatNumberShort(em, 2)} - configure with --training-stat-per-multi-threshold)`); let studying = false;