├── .gitattributes ├── .github └── workflows │ └── inclusive_language.yml ├── .gitignore ├── LICENSE ├── README.md ├── Remote ├── grow-target.js ├── hack-target.js ├── manualhack-target.js ├── share.js └── weak-target.js ├── Tasks ├── backdoor-all-servers.js ├── backdoor-all-servers.js.backdoor-one.js ├── contractor.js ├── contractor.js.solver.js ├── crack-host.js ├── program-manager.js ├── ram-manager.js ├── run-with-delay.js ├── tor-manager.js └── write-file.js ├── analyze-hack.js ├── ascend.js ├── autopilot.js ├── bladeburner.js ├── casino.js ├── cleanup.js ├── crime.js ├── daemon.js ├── dev-console.js ├── dump-ns-namespace.js ├── faction-manager.js ├── farm-intelligence.js ├── gangs.js ├── git-pull.js ├── go.js ├── grep.js ├── hacknet-upgrade-manager.js ├── helpers.js ├── host-manager.js ├── kill-all-scripts.js ├── optimize-stanek.js ├── optimize-stanek.js.og.js ├── reserve.js ├── reserve.txt ├── run-command.js ├── scan.js ├── sleeve.js ├── spend-hacknet-hashes.js ├── stanek.js ├── stanek.js.create.js ├── stats.js ├── stockmaster.js ├── sync-scripts.js └── work-for-factions.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | * -crlf -------------------------------------------------------------------------------- /.github/workflows/inclusive_language.yml: -------------------------------------------------------------------------------- 1 | name: woke 2 | # Controls when the workflow will run 3 | on: 4 | # Triggers the workflow on push or pull request events but only for the "main" branch 5 | push: 6 | branches: [ "main" ] 7 | 8 | pull_request: 9 | branches: [ "main" ] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | jobs: 15 | woke: 16 | name: woke 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: woke 23 | uses: get-woke/woke-action@v0 24 | with: 25 | # Cause the check to fail on any broke rules 26 | fail-on-error: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.txt 3 | .vscode/*.* 4 | *.ts 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alain Bryden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Insight's Scripts 2 | Welcome to Insight's Bitburner scripts - one of the Bitburner scripts of all time. Hosted on my personal github because all the best hackers dox themselves. 3 | 4 | # Downloading the whole repository 5 | 6 | If you manually `nano git-pull.js` from the terminal and copy the [contents of that script](https://raw.githubusercontent.com/alainbryden/bitburner-scripts/main/git-pull.js), you should be able to run it once and download the rest of the files I use. Early-game, many will be useless because they are only enabled by late-game features, but they shouldn't give you too many problems just being there. 7 | 8 | # Running scripts 9 | 10 | If you `run autopilot.js` from the terminal, it will start several other scripts. 11 | 12 | You can think of this as the "master orchestrator" script. It will kick off `daemon.js` (your primary hacking script), which in turn kicks off several other helper-scripts. It will monitor your progress throughout the game, and take special actions when it can. I don't want to spoil too much for those new to the game, but it's worth mentioning that `SF4` is not required, but is highly-recommended to get the full benefit of this script. 13 | 14 | Most scripts can also be run on their own, but are primarily designed to be orchestrated by `autopilot.js` or `daemon.js`. 15 | 16 | ## Manually run scripts 17 | 18 | Some scripts are meant to be manually run as needed. Most scripts take arguments to tweak or customize their behaviour based on your preferences or special circumstance. More on this [below](#customizing-script-behaviour-basic). 19 | Run scripts with the `--help` flag to get a list of their arguments, default values, and a brief description of each: 20 | ![image](https://user-images.githubusercontent.com/2285037/166085058-952b0805-cf4e-4548-8829-1e1ebeb5428b.png) 21 | You will also see an error-version of this dialog if you make a mistake in how you run the script. 22 | 23 | If you have personal preference and wish to "permanently" change the configuration of one of my scripts, you can do so without sacrificing your ability to "git-pull.js" the latest - simply [create a custom `config.txt`](https://github.com/alainbryden/bitburner-scripts/edit/main/README.md#config-files) file for the script. 24 | 25 | _Note:_ `autopilot.js` (and in turn, `daemon.js`) will already run many instances of scripts with default arguments. If you wish to run them with special arguments, you must either kill the default version or simply run scripts with your desired arguments **before** starting daemon.js. Daemon.js will only start scripts that are not already running (regardless of the arguments of the currently running instance.) 26 | 27 | ## Brief description of Scripts 28 | 29 | Here are scripts that you may want to manually run, roughly in the order in which you'll want to experiment with them: 30 | 31 | - `git-pull.js` - Hopefully you used this to download the scripts. Run it whenever you want to update. 32 | - `scan.js` - Shows you the entire server network and important information about each server. A nice replacement for the built-in `scan` and/or `scan-analyze` commands, with support for unlimited depth. 33 | - `autopilot.js` - Plays the game for you (more or less). 34 | - `daemon.js` - Automates hacking and infrastructure, and kicking off various scripts to take advantage of other mechanics in the game as you unlock them. 35 | - `casino.js` - The first time you run this may come as a surprise, it will play blackjack and reload the game if it loses (automated save-scumming). Once you win 10b, you cannot enter the casino any more. Great way to boost your progress once you make the initial 200k needed to travel to Aevum and use the casino. For best performance, run `kill-all-scripts.js` before you run this, since other running scripts slow down the game's load time. 36 | - `reserve.js` - A simple way to reserve money across all scripts, in case you wanted to be certain to save up for something. e.g. `run reserve.js 200k` will reserve the $200,000 needed to get `casino.js` going. 37 | - `kill-all-scripts.js` - Kills all scripts running on home and remote servers, and also removes files that were copied to remote servers. 38 | - `faction-manager.js` - (Requires SF4) Run this periodically to find out how many augmentations you can currently afford. There are many command line options available to tweak the sort of augmentations you wish to prioritize. Run with `--purchase` to pull the trigger if you're ready to ascend. 39 | - `work-for-factions.js` - (Requires SF4) Daemon.js will start a version of this to make sure your "focus" work goes to good use, but often you'll want to run with your own arguments to specify what kind of work you want to be doing, depending on your goals for the current BitNode. 40 | - `crime.js` - (Requires SF4) While `work-for-factions.js` will do crime as-needed, you can use this instead to do nothing but crime. 41 | - `ascend.js` - (Requires SF4) A nearly-fully-automated way to ascend. Takes care of all the things you may or may not have known you wanted to do before installing augmentations and resetting. 42 | - `spend-hacknet-hashes.js` - (Requires SF9) Many scripts will launch this automatically, but you can start your own instances to focus on purchasing the hash upgrades you want in your current situation. Many aliases for this exist below. 43 | - `farm-intelligence.js` - (Requires SF4, SF5) Contains a script that can execute one or more of the best known methods to farm intelligence experience. 44 | - Note that the current best method (soft reset loop) is most effective if you delete all scripts except this one (and helpers.js which it relies on) before running. You can do this quickly by modifying cleanup.js to run on all files instead of just /Temp/. You then would have to restore scripts by nano'ing git-pull as when you started out. 45 | - `cleanup.js` - Use this to clear out your temp folder (which contains hundreds of miniature scripts generated by the main scripts). Useful to reduce your save file size before exporting. 46 | - `grep.js` - Use this to search one or all files for certain text. Handy if you are trying to figure out e.g. what script spend hashes, or care about the TIX api. 47 | - `run-command.js` - Useful for testing a bit of code from the terminal without having to create a new script. Creating the alias `alias do="run run-command.js"` makes this extra useful. e.g. `do ns.getPlayer()` will print all the player's info to the terminal. `do ns.getServer('joesguns')` will print all info about that server to the terminal. 48 | 49 | If you want more information about any script, try reading the source. I do my best to document things clearly. If it's not clear, feel free to raise an issue. 50 | 51 | ## Customizing Script Behaviour (Basic) 52 | Most scripts are designed to be configured via command line arguments. (Such as using `run host-manager.js --min-ram-exponent 8` to ensure no servers are purchased with less than 2^8 GB of RAM) 53 | 54 | Default behaviours are to try to "balance" priorities and give most things an equal share of budget / RAM, but this isn't always ideal, especially in bitnodes that cripple one aspect of the game or the other. You can `nano` to view the script and see what the command line options are, or type e.g. `daemon.js --` (dash dash) and hit `` to get a pop-up auto-completion list. (Make sure your mouse cursor is over the terminal for the auto-complete to appear.) 55 | 56 | Near the top of the initializer for `daemon.js`, there are a list of external scripts that are spawned initially, and periodically. Some of these can be commented out if you would rather not have that script run automatically (for example `work-for-factions` if you would like to manually choose how to spend your "focus" times.) Once you've downloaded this file, you should customize it with the default options you like, and comment out the external scripts you don't want to run. 57 | 58 | ## Aliases 59 | 60 | You may find it useful to set up one or more aliases with the default options you like rather than editing the file itself. (Pro-tip, aliases support tab-auto-completion). I personally use the following aliases: 61 | 62 | - `alias git-pull="run git-pull.js"` 63 | - Makes auto-updating just a little easier. 64 | - `alias start="run autopilot.js"` 65 | - `alias stop="home; kill autopilot.js ; kill daemon.js ; run kill-all-scripts.js"` 66 | - Quick way to start/stop the system. I personally now use `auto` instead of `start` for this alias (auto => autopilot.js). 67 | - `alias sscan="home; run scan.js"` 68 | - Makes it a little quicker to run this custom-scan routine, which shows the entire network, stats about servers, and provides handy links for jumping to servers or backdooring them. 69 | - `alias do="run run-command.js"` 70 | - This lets you run ns commands from the terminal, such as `do ns.getPlayer()`, `do Object.keys(ns)` or `do ns.getServerMoneyAvailable('n00dles')` 71 | - `alias reserve="run reserve.js"` 72 | - Doesn't save many keystrokes, but worth highlighting this script. You can run e.g. `reserve 100m` to globally reserve this much money. All scripts with an auto-spend component should respect this amount and leave it unspent. This is useful if e.g. you're saving up to buy something (SQLInject.exe, a big server, the next home RAM upgrade), saving money to spend at the casino, etc... 73 | - `alias liquidate="home; run stockmaster.js --liquidate; run spend-hacknet-hashes.js --liquidate;"` 74 | - Quickly sell all your stocks and hacknet hashes for money so that you can spend it (useful before resetting) 75 | - `alias facman="run faction-manager.js"` 76 | - Quickly see what augmentations you can afford to purchase. Then use `facman --purchase` to pull the trigger. 77 | - `alias buy-daemons="run host-manager.js --run-continuously --reserve-percent 0 --min-ram-exponent 19 --utilization-trigger 0 --tail"` 78 | - This is an example of how to use host-manager to buy servers for you. In this example, we are willing to spend all our current money (--reserve-percent 0) if it means buying a server with 2^19 GB ram or more (--min-ram-exponent), even if our scripts aren't using any RAM on the network (--utilization-trigger 0), 79 | - `alias spend-on-ram="run Tasks/ram-manager.js --reserve 0 --budget 1 --tail"` 80 | - `alias spend-on-gangs="run gangs.js --reserve 0 --augmentations-budget 1 --equipment-budget 1 --tail"` 81 | - `alias spend-on-sleeves="run sleeve.js --aug-budget 1 --min-aug-batch 1 --buy-cooldown 0 --reserve 0 --tail"` 82 | - Useful to run one or more of these (in your own priority order) after you've spent all you can on augmentations, before resetting. 83 | - `alias spend-on-hacknet="run hacknet-upgrade-manager.js --interval 10 --max-payoff-time 8888h --continuous --tail"` 84 | - Essentially spends a lot of money upgrading the hacknet. If it doesn't spend enough, increase the --max-payoff-time even more. 85 | - `alias hashes-to-bladeburner="run spend-hacknet-hashes.js --spend-on Exchange_for_Bladeburner_Rank --spend-on Exchange_for_Bladeburner_SP --liquidate --tail"` 86 | - `alias hashes-to-corp-money="run spend-hacknet-hashes.js --spend-on Sell_for_Corporation_Funds --liquidate --tail"` 87 | - `alias hashes-to-corp-research="run spend-hacknet-hashes.js --spend-on Exchange_for_Corporation_Research --liquidate --tail"` 88 | - `alias hashes-to-corp="run spend-hacknet-hashes.js --spend-on Sell_for_Corporation_Funds --spend-on Exchange_for_Corporation_Research --liquidate --tail"` 89 | - `alias hashes-to-hack-server="run spend-hacknet-hashes.js --liquidate --spend-on Increase_Maximum_Money --spend-on Reduce_Minimum_Security --spend-on-server"` 90 | - Useful to set up hashes to automatically get spent on one or more things as you can afford them. Omit --liquidate if you want to save up hashes to spend yourself, and only want to spend them when you reach capacity to avoid wasting them. 91 | - `alias stock="run stockmaster.js --fracH 0.001 --fracB 0.1 --show-pre-4s-forecast --noisy --tail --reserve 100000000"` 92 | - Useful in e.g. BN8 to invest all cash in the stock market, and closely track progress. _(Also reserves 100m to play blackjack at the casino so you can build up cash quickly. Pro-tip: Save if you win, and just reload (or soft-reset if you hate save-scumming) when you lose it all to get your money back.)_ 93 | - `alias crime="run crime.js --tail --fast-crimes-only"` 94 | - Start an auto-crime loop. (Requires SF4 a.k.a. Singularity access, like so many of my scripts.) 95 | - `alias work="run work-for-factions.js --fast-crimes-only"` 96 | - Auto-work for factions. Will also do crime loops as deemed necessary. (Note, daemon will start this automatically as well) 97 | - `alias invites="run work-for-factions.js --fast-crimes-only --get-invited-to-every-faction --prioritize-invites --no-coding-contracts"` 98 | - Tries to join as many factions as possible, regardless of whether you have un-purchased augmentations from them. 99 | - `alias xp="run daemon.js -vx --tail --no-share"` 100 | - Runs daemon in a way that focuses on earning hack XP income as quickly as possible. Only practical when you have a lot of home-ram. 101 | - `alias start-tight="run daemon.js --looping-mode --recovery-thread-padding 30 --cycle-timing-delay 2000 --queue-delay 10 --stock-manipulation-focus --tail --silent-misfires --initial-max-targets 64"` 102 | - Let this be a hint as to how customizable some of these scripts are (without editing the source code). The above alias is powerful when you are end-of-bn and your hacking skill is very high (8000+), so hack/grow/weaken times are very fast (milliseconds). You can greatly increase productivity and reduce lag by switching to this `--looping-mode` which creates long-lived hack/grow/weaken scripts that run in a loop. This, in addition to the tighter cycle-timing makes them more vulnerable to misfiring (completing out-of-order), but adding recovery thread padding (a multiple on the number of grow/weaken threads to use) can quickly recover from misfires. Note that if you don't yet have enough home-ram to support such a high recovery-thread multiple, you can start lower (5 or 10) then buy more home ram and work your way up. 103 | - `alias ascend="run ascend.js --install-augmentations"` 104 | - A good way to finish your node. I personally prioritize augmentations when resetting, because I have all SF bonuses unlocked, but until you have SF11.3 for aug cost reduction, you may want to use the `--prioritize-home-ram` flag which prioritizes upgrading home RAM as much as possible before buying as many augmentations as possible. 105 | 106 | ## Config Files 107 | 108 | Persistent Custom Configurations (script.js.config.txt files) can be specified to override the default args specified by the "args schema" in each script. 109 | 110 | The order in which argument values are determined are: 111 | 1. Arguments provided at the command line (or in the alias) take priority 112 | 2. If no override is provided at the command line, any value in the config file is used. 113 | 3. If no config file value is present, the default in the source (argsSchema) is used. 114 | - Note that some defaults are set to `null` in the args schema to be overridden with more complex defaulting behaviour elsewhere in the script. 115 | 116 | ### Format Specifications 117 | The file should have the name `some-script-name.js.config.txt` (i.e. append `.config.txt` to the name of the script you are configuring) 118 | 119 | Your config file should either of the following two formats 120 | 1. A dictionary e.g.: `{ "string-opt": "value", "num-opt": 123, "array-opt": ["one", "two"] }` 121 | 2. An array of dict entries (2-element arrays) e.g.: `[ ["string-opt", "value"], ["num-opt", 123], ["array-opt", ["one", "two"]] ]` + 122 | 123 | You are welcome to use line breaks and spacing to make things more human readable, so long as it is able to be parsed by JSON.parse (when in doubt, built it in code and generate it with JSON.stringify). 124 | 125 | ## Customizing Script Code (Advanced) 126 | 127 | I encourage you to make a fork and customize scripts to your own needs / liking. Please don't make a PR back to me unless you truly think it's something all would benefit from. If you fork the repository, you can update the `git-pull.js` source to include your github account as the default, or set an alias that specifies this via command-line (e.g. `alias git-pull="run git-pull.js --github mygitusername --repository bitburner-scripts`). This way you can auto-update from your fork and only merge my latest changes when you're ready. 128 | 129 | 130 | # Disclaimer 131 | 132 | This is my own repository of scripts for playing Bitburner. 133 | I often go to some lengths to make them generic and customizable, but am by no means providing these scripts as a "service" to the Bitburner community. 134 | It's meant as an easy way for me to share code with friends, and track changes and bugs in my scripts. 135 | 136 | - If you wish to use my scripts or copy from them, feel free! 137 | - If you think you found a bug in them and want to let me know, awesome! 138 | - Please don't be insulted if you make a feature request, bug report, or pull request that I decline to act on. 139 | While I do like my work to be helpful to others and re-used, I am only willing to put so much effort into customizing it to others' specific needs or whims. 140 | You should fork the code, and start tweaking it the way you want it to behave. That's more in the spirit of the game! 141 | 142 | Hit up the Bitburner Discord with any questions: 143 | - Invite to Bitburner Disccord: https://discord.com/invite/TFc3hKD 144 | - Link to the channel for these scripts: [Bitburner#alains-scripts](https://discord.com/channels/415207508303544321/935667531111342200) 145 | 146 | Many helpful folks in there are familiar with my scripts or ones similar to them and can address your questions and concerns far quicker than I can. 147 | -------------------------------------------------------------------------------- /Remote/grow-target.js: -------------------------------------------------------------------------------- 1 | /** @param {NS} ns 2 | * Wait until an appointed time and then execute a grow. */ 3 | export async function main(ns) { 4 | // Destructure the arguments (default values should never be used and should just provide type hints) 5 | const [ 6 | /*args[0]*/ target = "", 7 | /*args[1]*/ start_time = 0, 8 | /*args[2]*/ duration = 0, 9 | /*args[3]*/ description = "", 10 | /*args[4]*/ manipulateStock = false, 11 | /*args[5]*/ silentMisfires = false, 12 | /*args[6]*/ loopingMode = false 13 | ] = ns.args; 14 | 15 | // We may need to sleep before we start the operation to align ourselves properly with other batch cycle (HGW) operations 16 | let sleepDuration = start_time - Date.now(); 17 | if (sleepDuration < 0) { 18 | if (!silentMisfires) 19 | ns.toast(`Misfire: Grow started ${-sleepDuration} ms too late. ${JSON.stringify(ns.args)}`, 'warning'); 20 | sleepDuration = 0; 21 | } 22 | // We use the "additionalMsec" option to bundle the initial sleep time we require with the built-in operation timer 23 | const hgwOptions = { 24 | stock: manipulateStock, 25 | additionalMsec: sleepDuration 26 | } 27 | 28 | // In looping mode, we want increase the run time to match the time-to-weaken, so that we fire once per cycle 29 | if (loopingMode) 30 | hgwOptions.additionalMsec += duration * 0.25 // (duration*4/3.2 (time-to-weaken) - duration) 31 | 32 | let firstLoop = true; 33 | do { 34 | const growPct = await ns.grow(target, hgwOptions); 35 | // If enabled, warn of any misfires 36 | if (growPct == 0 && !silentMisfires) 37 | ns.toast(`Misfire: Grow achieved no growth. ${JSON.stringify(ns.args)}`, 'warning'); 38 | // (looping mode only) After the first loop, remove the initial sleep time used to align our start with other HGW operations 39 | if (firstLoop) { 40 | hgwOptions.additionalMsec -= sleepDuration; 41 | firstLoop = false; 42 | } 43 | } while (loopingMode); // Keep going only if we were started in "looping mode" 44 | } -------------------------------------------------------------------------------- /Remote/hack-target.js: -------------------------------------------------------------------------------- 1 | /** @param {NS} ns 2 | * Wait until an appointed time and then execute a hack. */ 3 | export async function main(ns) { 4 | // Destructure the arguments (default values should never be used and should just provide type hints) 5 | const [ 6 | /*args[0]*/ target = "", 7 | /*args[1]*/ start_time = 0, 8 | /*args[2]*/ duration = 0, 9 | /*args[3]*/ description = "", 10 | /*args[4]*/ manipulateStock = false, 11 | /*args[5]*/ silentMisfires = false, 12 | /*args[6]*/ loopingMode = false 13 | ] = ns.args; 14 | 15 | // We may need to sleep before we start the operation to align ourselves properly with other batch cycle (HGW) operations 16 | let sleepDuration = start_time - Date.now(); 17 | if (sleepDuration < 0) { 18 | if (!silentMisfires) 19 | ns.toast(`Misfire: Hack started ${-sleepDuration} ms too late. ${JSON.stringify(ns.args)}`, 'warning'); 20 | sleepDuration = 0; 21 | } 22 | // We use the "additionalMsec" option to bundle the initial sleep time we require with the built-in operation timer 23 | const hgwOptions = { 24 | stock: manipulateStock, 25 | additionalMsec: sleepDuration 26 | } 27 | // In looping mode, we want increase the run time to match the time-to-weaken, so that we fire once per cycle 28 | if (loopingMode) 29 | hgwOptions.additionalMsec += duration * 3.0 // (duration * 4.0 (time-to-weaken) - duration) 30 | 31 | let firstLoop = true; 32 | do { 33 | const stolen = await ns.hack(target, hgwOptions); 34 | // If enabled, warn of any misfires 35 | if (stolen == 0 && !silentMisfires) 36 | ns.toast(`Misfire: Hack stole 0 money. ${JSON.stringify(ns.args)}`, 'warning'); 37 | // (looping mode only) After the first loop, remove the initial sleep time used to align our start with other HGW operations 38 | if (firstLoop) { 39 | hgwOptions.additionalMsec -= sleepDuration; 40 | firstLoop = false; 41 | } 42 | } while (loopingMode); // Keep going only if we were started in "looping mode" 43 | } -------------------------------------------------------------------------------- /Remote/manualhack-target.js: -------------------------------------------------------------------------------- 1 | /** @param {NS} ns 2 | * Wait until an appointed time and then execute a manual hack. */ 3 | export async function main(ns) { 4 | //args[0: target, 1: desired start time, 2: expected end, 3: expected duration, 4: description, 5: manipulate stock (N/A ignored), 6: disable toast warnings, 7: loop] 5 | const sleepDuration = ns.args.length > 1 ? ns.args[1] - Date.now() : 0; 6 | const expectedDuration = ns.args.length > 3 ? ns.args[3] : 0; 7 | const manipulateStock = ns.args.length > 5 && ns.args[5] ? true : false; 8 | const disableToastWarnings = ns.args.length > 6 ? ns.args[6] : false; 9 | const loop = ns.args.length > 7 ? ns.args[7] : false; 10 | let cycleTime = expectedDuration * 4; 11 | if (cycleTime < 100) cycleTime = Math.max(1, Math.min(5, cycleTime * 2)); // For fast hacking loops, inject a delay on hack in case grow/weaken are running a bit slow. 12 | if (sleepDuration > 0) 13 | await ns.sleep(sleepDuration); 14 | do { 15 | if (!await ns.singularity.manualHack() && !disableToastWarnings) 16 | ns.toast(`Warning, hack stole 0 money. Might be a misfire. ${JSON.stringify(ns.args)}`, 'warning'); 17 | if (loop) await ns.sleep(cycleTime - expectedDuration); 18 | } while (loop); 19 | } -------------------------------------------------------------------------------- /Remote/share.js: -------------------------------------------------------------------------------- 1 | /** @param {NS} ns **/ 2 | export async function main(ns) { 3 | await ns.share() 4 | } -------------------------------------------------------------------------------- /Remote/weak-target.js: -------------------------------------------------------------------------------- 1 | /** @param {NS} ns 2 | * Wait until an appointed time and then execute a weaken. */ 3 | export async function main(ns) { 4 | // Destructure the arguments (default values should never be used and should just provide type hints) 5 | const [ 6 | /*args[0]*/ target = "", 7 | /*args[1]*/ start_time = 0, 8 | /*args[2]*/ duration = 0, 9 | /*args[3]*/ description = "", 10 | // Note, unlike Grow / Hack, no stock manipulation arg here. 11 | /*args[4]*/ silentMisfires = false, 12 | /*args[5]*/ loopingMode = false 13 | ] = ns.args; 14 | 15 | // We may need to sleep before we start the operation to align ourselves properly with other batch cycle (HGW) operations 16 | let sleepDuration = start_time - Date.now(); 17 | if (sleepDuration < 0) { 18 | if (!silentMisfires) 19 | ns.toast(`Misfire: Weaken started ${-sleepDuration} ms too late. ${JSON.stringify(ns.args)}`, 'warning'); 20 | sleepDuration = 0; 21 | } 22 | // We use the "additionalMsec" option to bundle the initial sleep time we require with the built-in operation timer 23 | const hgwOptions = { 24 | additionalMsec: sleepDuration 25 | } 26 | 27 | let firstLoop = true; 28 | do { 29 | const weakAmt = await ns.weaken(target, hgwOptions); 30 | // If enabled, warn of any misfires 31 | if (weakAmt == 0 && !silentMisfires) 32 | ns.toast(`Misfire: Weaken achieved no security reduction. ${JSON.stringify(ns.args)}`, 'warning'); 33 | // (looping mode only) After the first loop, remove the initial sleep time used to align our start with other HGW operations 34 | if (firstLoop) { 35 | hgwOptions.additionalMsec = 0; 36 | firstLoop = false; 37 | } 38 | } while (loopingMode); // Keep going only if we were started in "looping mode" 39 | } -------------------------------------------------------------------------------- /Tasks/backdoor-all-servers.js: -------------------------------------------------------------------------------- 1 | import { getNsDataThroughFile, getFilePath, getConfiguration, instanceCount, log, getErrorInfo } from '../helpers.js' 2 | 3 | const argsSchema = [ 4 | ['spawn-delay', 50], // Delay to allow time for `installBackdoor` to start running before a we connect back to 'home' and optionally start backdooring the next server 5 | ['reserved-home-ram', 22], // Don't spawn additional backdoor scripts if home free ram dips below this amount (each parallel backdoor consumes 3.6 GB) 6 | ]; 7 | 8 | export function autocomplete(data, args) { 9 | data.flags(argsSchema); 10 | return []; 11 | } 12 | 13 | /** Scan all servers, backdoor anything that can be backdoored, and leave a file to indicate it's been done 14 | * Requires: SF-4.1 **/ 15 | /** @param {NS} ns **/ 16 | export async function main(ns) { 17 | let notAtHome = false; 18 | try { 19 | const options = getConfiguration(ns, argsSchema); 20 | 21 | // Prevent multiple instances of this script from being started 22 | if (await instanceCount(ns, "home", false, false) > 1) 23 | return log(ns, 'Another instance is already running. Shutting down...'); 24 | 25 | const spawnDelay = options['spawn-delay']; 26 | 27 | const servers = ["home"]; 28 | const routes = { home: ["home"] }; 29 | const myHackingLevel = await getNsDataThroughFile(ns, 'ns.getHackingLevel()'); 30 | // Scan all servers and keep track of the path to get to them 31 | ns.disableLog("scan"); 32 | for (let i = 0, j; i < servers.length; i++) 33 | for (j of (await getNsDataThroughFile(ns, `ns.scan(ns.args[0])`, null, [servers[i]]))) 34 | if (!servers.includes(j)) servers.push(j), routes[j] = routes[servers[i]].slice(), routes[j].push(j); 35 | 36 | // Get the required hacking level of each server 37 | const dictRequiredHackingLevels = await getNsDataThroughFile(ns, 38 | `Object.fromEntries(ns.args.map(server => [server, ns.getServerRequiredHackingLevel(server)]))`, 39 | '/Temp/getServerRequiredHackingLevel-all.txt', servers); 40 | // Get the root status for each server 41 | const dictRootAccess = await getNsDataThroughFile(ns, 42 | `Object.fromEntries(ns.args.map(server => [server, ns.hasRootAccess(server)]))`, 43 | '/Temp/hasRootAccess-all.txt', servers); 44 | 45 | // Filter out servers that cannot or should not be hacked / backdoored 46 | let hackableServers = servers.filter(s => s != "home" && !s.includes("hacknet-") && !s.includes("daemon")) /*or whatever you name your purchased servers*/ 47 | ns.print(`${hackableServers.length} not-owned servers on the network.`); 48 | ns.print(`${hackableServers.filter(s => dictRootAccess[s]).length} servers are currently rooted.`); 49 | ns.print(`${hackableServers.filter(s => myHackingLevel > dictRequiredHackingLevels[s]).length} servers are within our hack level (${myHackingLevel}).`); 50 | ns.print(`${hackableServers.filter(s => myHackingLevel > dictRequiredHackingLevels[s] && dictRootAccess[s]).length} rooted servers are within our hack level (${myHackingLevel})`); 51 | 52 | // Get the set of servers that do not yet have a backdoor installed 53 | let toBackdoor = await getNsDataThroughFile(ns, 54 | `ns.args.filter(server => !ns.getServer(server).backdoorInstalled)`, 55 | '/Temp/getServers-where-not-backdoorInstalled.txt', hackableServers); 56 | let count = toBackdoor.length; 57 | // Early exit condition if there are no servers left to backdoor 58 | ns.print(`${count} servers have yet to be backdoored.`); 59 | if (count == 0) return; 60 | 61 | // Early exit condition if there are no servers we can currently backdoor 62 | ns.print(`${toBackdoor.filter(s => dictRootAccess[s]).length} of ${count} servers to backdoor are currently rooted.`); 63 | toBackdoor = toBackdoor.filter(s => myHackingLevel > dictRequiredHackingLevels[s]); 64 | ns.print(`${toBackdoor.length} of ${count} servers to backdoor are within our hack level (${myHackingLevel}).`); 65 | toBackdoor = toBackdoor.filter(s => dictRootAccess[s]); 66 | ns.print(`${toBackdoor.length} of ${count} servers to be backdoored are rooted and within our hack level (${myHackingLevel})`); 67 | if (toBackdoor.length == 0) return; 68 | 69 | // Sort servers by lowest required hacking level (fastest to backdoor) 70 | toBackdoor.sort((a, b) => dictRequiredHackingLevels[a] - dictRequiredHackingLevels[b]) 71 | ns.print(`Servers will be backdoored in the following order:\n` + toBackdoor.join(', ')); 72 | 73 | // Collect information about any servers still being backdoored (from a prior run), so we can skip them 74 | let scriptPath = getFilePath('/Tasks/backdoor-all-servers.js.backdoor-one.js'); 75 | let serversBeingBackdoored = await getNsDataThroughFile(ns, 76 | 'ns.ps().filter(script => script.filename == ns.args[0]).map(script => script.args[0])', 77 | '/Temp/servers-being-backdoored.txt', [scriptPath]); 78 | 79 | for (const server of toBackdoor) { 80 | if (serversBeingBackdoored.includes(server)) { 81 | log(ns, `INFO: Server already beeing backdoored: ${server}`); 82 | continue; 83 | } 84 | 85 | // If we're running low on home ram, don't spawn any more backdoor scripts 86 | const homeFreeRam = await getNsDataThroughFile(ns, 87 | 'ns.getServerMaxRam(ns.args[0]) - ns.getServerUsedRam(ns.args[0])', 88 | '/Temp/getServerFreeRam.txt', ["home"]); 89 | if (homeFreeRam < options['reserved-home-ram']) 90 | return log(ns, `WARNING: Home is low on RAM, will skip backdooring remaining servers.`); 91 | 92 | ns.print(`Hopping to ${server}`); 93 | notAtHome = true; // Set a flag to get us back home if we encounter an error 94 | const success = await getNsDataThroughFile(ns, 95 | 'ns.args.reduce((success, hop) => success && ns.singularity.connect(hop), true)', 96 | '/Temp/singularity-connect-hop-to-server.txt', routes[server]); 97 | if (!success) 98 | log(ns, `ERROR: Failed to hop to server ${server}. Backdoor probably won't work...`, true, 'error'); 99 | if (server === "w0r1d_d43m0n") { 100 | ns.alert("Ready to hack w0r1d_d43m0n!"); 101 | log(ns, "INFO: Sleeping forever to avoid multiple instances navigating to w0r1d_d43m0n."); 102 | while (true) await ns.sleep(10000); // Sleep forever so the script isn't run multiple times to create multiple overlapping alerts 103 | } 104 | ns.print(`Installing backdoor on "${server}"...`); 105 | // Kick off a separate script that will run backdoor before we connect to home. 106 | var pid = ns.run(scriptPath, { temporary: true }, server); 107 | if (pid === 0) 108 | return log(ns, `WARN: Couldn't initiate a new backdoor of "${server}" (insufficient RAM?). Will try again later.`, false, 'warning'); 109 | await ns.sleep(spawnDelay); // Wait some time for the external backdoor script to initiate its backdoor of the current connected server 110 | const backAtHome = await getNsDataThroughFile(ns, 'ns.singularity.connect(ns.args[0])', null, ["home"]); 111 | if (backAtHome) 112 | notAtHome = false; 113 | } 114 | } 115 | catch (err) { 116 | log(ns, `ERROR: ${ns.getScriptName()} Caught an unexpected error:\n${getErrorInfo(err)}`, false, 'error'); 117 | } finally { 118 | // Try to clean-up by re-connecting to home before we shut down 119 | if (notAtHome) 120 | await getNsDataThroughFile(ns, 'ns.singularity.connect(ns.args[0])', null, ["home"]); 121 | } 122 | }; -------------------------------------------------------------------------------- /Tasks/backdoor-all-servers.js.backdoor-one.js: -------------------------------------------------------------------------------- 1 | /** @param {NS} ns **/ 2 | export async function main(ns) { 3 | let target = ns.args.length > 0 ? ns.args[0] : '(unspecified server)'; 4 | try { 5 | await ns.singularity.installBackdoor(); 6 | ns.toast(`Backdoored ${target}`, 'success'); 7 | } 8 | catch (err) { 9 | ns.tprint(`Error while running backdoor (intended for ${target}): ${String(err)}`); 10 | throw (err); 11 | } 12 | } -------------------------------------------------------------------------------- /Tasks/contractor.js: -------------------------------------------------------------------------------- 1 | import { instanceCount, getFilePath, getNsDataThroughFile, disableLogs, log } from '../helpers.js' 2 | const scriptSolver = getFilePath("/Tasks/contractor.js.solver.js"); 3 | 4 | /** @param {NS} ns **/ 5 | export async function main(ns) { 6 | // Prevent multiple instances of this script from being started 7 | if (await instanceCount(ns, "home", false, false) > 1) 8 | return log(ns, 'Another instance is already running. Shutting down...'); 9 | 10 | disableLogs(ns, ["scan"]); 11 | ns.print("Getting server list..."); 12 | const servers = await getNsDataThroughFile(ns, 'scanAllServers(ns)'); 13 | ns.print(`Got ${servers.length} servers. Searching for contracts on each...`); 14 | // Retrieve all contracts and convert them to objects with the required information to solve 15 | const contractsDb = servers.map(hostname => ({ hostname, contracts: ns.ls(hostname, '.cct') })) 16 | .filter(o => o.contracts.length > 0) 17 | .map(o => o.contracts.map(contract => ({ contract, hostname: o.hostname }))).flat(); 18 | if (contractsDb.length == 0) 19 | return ns.print("Found no contracts to solve."); 20 | 21 | // Spawn temporary scripts to gather the remainder of contract data required for solving 22 | ns.print(`Found ${contractsDb.length} contracts to solve. Gathering contract data via separate scripts..."`); 23 | const serializedContractDb = JSON.stringify(contractsDb); 24 | let contractsDictCommand = async (command, tempName) => await getNsDataThroughFile(ns, 25 | `Object.fromEntries(JSON.parse(ns.args[0]).map(c => [c.contract, ${command}]))`, tempName, [serializedContractDb]); 26 | let dictContractTypes = await contractsDictCommand('ns.codingcontract.getContractType(c.contract, c.hostname)', '/Temp/contract-types.txt'); 27 | let dictContractDataStrings = await contractsDictCommand('JSON.stringify(ns.codingcontract.getData(c.contract, c.hostname), jsonReplacer)', '/Temp/contract-data-stringified.txt'); 28 | contractsDb.forEach(c => c.type = dictContractTypes[c.contract]); 29 | contractsDb.forEach(c => c.dataJson = dictContractDataStrings[c.contract]); 30 | 31 | // Let this script die to free up ram, and start up a new script (after a delay) that will solve all these contracts using the minimum ram footprint of 11.6 GB 32 | ns.run(getFilePath('/Tasks/run-with-delay.js'), { temporary: true }, scriptSolver, 1, JSON.stringify(contractsDb)); 33 | } -------------------------------------------------------------------------------- /Tasks/crack-host.js: -------------------------------------------------------------------------------- 1 | /** @param {NS} ns 2 | * Blindly try to open all ports and crack the specified target, regardless of owned tools. */ 3 | export async function main(ns) { 4 | const target = ns.args[0]; 5 | try { ns.brutessh(target); } catch { } 6 | try { ns.ftpcrack(target); } catch { } 7 | try { ns.relaysmtp(target); } catch { } 8 | try { ns.httpworm(target); } catch { } 9 | try { ns.sqlinject(target); } catch { } 10 | try { ns.nuke(target); } catch { } 11 | } -------------------------------------------------------------------------------- /Tasks/program-manager.js: -------------------------------------------------------------------------------- 1 | /** @param {NS} ns 2 | * the purpose of the program-manager is to buy all the programs 3 | * from the darkweb we can afford so we don't have to do it manually 4 | * or write them ourselves. Like tor-manager, this script dies a natural death 5 | * once all programs are bought. **/ 6 | export async function main(ns) { 7 | const programNames = ["BruteSSH.exe", "FTPCrack.exe", "relaySMTP.exe", "HTTPWorm.exe", "SQLInject.exe"]; 8 | const interval = 2000; 9 | 10 | const keepRunning = ns.args.length > 0 && ns.args[0] == "-c"; 11 | if (!keepRunning) 12 | ns.print(`program-manager will run once. Run with argument "-c" to run continuously.`) 13 | 14 | do { 15 | let foundMissingProgram = false; 16 | for (const prog of programNames) { 17 | if (!ns.fileExists(prog, "home") && ns.singularity.purchaseProgram(prog)) 18 | ns.toast(`Purchased ${prog}`, 'success'); 19 | else if (keepRunning) 20 | foundMissingProgram = true; 21 | } 22 | if (keepRunning && foundMissingProgram) 23 | await ns.sleep(interval); 24 | } while (keepRunning && foundMissingProgram); 25 | } -------------------------------------------------------------------------------- /Tasks/ram-manager.js: -------------------------------------------------------------------------------- 1 | import { formatMoney, formatRam, getConfiguration, getNsDataThroughFile, log } from '../helpers.js' 2 | 3 | const max_ram = 2 ** 30; 4 | const argsSchema = [ 5 | ['budget', 0.2], // Spend up to this much of current cash on ram upgrades per tick (Default is high, because these are permanent for the rest of the BN) 6 | ['reserve', null], // Reserve this much cash before determining spending budgets (defaults to contents of reserve.txt if not specified) 7 | ]; 8 | 9 | export function autocomplete(data, _) { 10 | data.flags(argsSchema); 11 | return []; 12 | } 13 | 14 | /** @param {NS} ns **/ 15 | export async function main(ns) { 16 | const options = getConfiguration(ns, argsSchema); 17 | if (!options) return; // Invalid options, or ran in --help mode. 18 | const reserve = (options['reserve'] != null ? options['reserve'] : Number(ns.read("reserve.txt") || 0)); 19 | const money = await getNsDataThroughFile(ns, `ns.getServerMoneyAvailable(ns.args[0])`, null, ["home"]); 20 | let spendable = Math.min(money - reserve, money * options.budget); 21 | if (isNaN(spendable)) 22 | return log(ns, `ERROR: One of the arguments could not be parsed as a number: ${JSON.stringify(options)}`, true, 'error'); 23 | // Quickly buy as many upgrades as we can within the budget 24 | do { 25 | let cost = await getNsDataThroughFile(ns, `ns.singularity.getUpgradeHomeRamCost()`); 26 | let currentRam = await getNsDataThroughFile(ns, `ns.getServerMaxRam(ns.args[0])`, null, ["home"]); 27 | if (cost >= Number.MAX_VALUE || currentRam == max_ram) 28 | return log(ns, `INFO: We're at max home RAM (${formatRam(currentRam)})`); 29 | const nextRam = currentRam * 2; 30 | const upgradeDesc = `home RAM from ${formatRam(currentRam)} to ${formatRam(nextRam)} (cost: ${formatMoney(cost)})`; 31 | if (spendable < cost) 32 | return log(ns, `Money we're allowed to spend (${formatMoney(spendable)}) is less than the cost (${formatMoney(cost)}) to upgrade ${upgradeDesc}`); 33 | if (!(await getNsDataThroughFile(ns, `ns.singularity.upgradeHomeRam()`))) 34 | return log(ns, `ERROR: Failed to upgrade ${upgradeDesc} thinking we could afford it ` + 35 | `(cash: ${formatMoney(money)} budget: ${formatMoney(spendable)})`, true, 'error'); 36 | // Otherwise, we've successfully upgraded home ram. 37 | log(ns, `SUCCESS: Upgraded ${upgradeDesc}`, true, 'success'); 38 | const newMaxRam = await getNsDataThroughFile(ns, `ns.getServerMaxRam(ns.args[0])`, null, ["home"]); 39 | if (nextRam != newMaxRam) 40 | log(ns, `WARNING: Expected to upgrade ${upgradeDesc}, but new home ram is ${newMaxRam}`, true, 'warning'); 41 | // Only loop again if we successfully upgraded home ram, to see if we can upgrade further 42 | spendable -= cost; 43 | await ns.sleep(100); // On the off-chance we have an infinite loop bug, this makes us killable. 44 | } while (spendable > 0) 45 | } -------------------------------------------------------------------------------- /Tasks/run-with-delay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {NS} ns 3 | * Similar to ns.spawn, but can be run for cheaper (1GB for ns.run vs 2GB for ns.spawn), the delay can be shorter, 4 | * and you have the option to *not* shut down the current script, but instead continue execution. 5 | **/ 6 | export async function main(ns) { 7 | var scriptpath = ns.args[0]; // Name of script to run is arg 0 8 | var delay = ns.args[1]; // Delay time is arg 1 9 | // Any additional args are forwarded to the script being run 10 | var forwardedArgs = ns.args.length > 2 ? ns.args.slice(2) : []; 11 | await ns.sleep(delay || 100); 12 | var pid = ns.run(scriptpath, { temporary: true }, ...forwardedArgs); 13 | if (!pid) 14 | ns.tprint(`Failed to spawn "${scriptpath}" with args: ${forwardedArgs} (bad file name or insufficient RAM?)`); 15 | } -------------------------------------------------------------------------------- /Tasks/tor-manager.js: -------------------------------------------------------------------------------- 1 | /** @param {NS} ns 2 | * the purpose of tor-manager is to buy the TOR router ASAP 3 | * so that another script can buy the port breakers. This script 4 | * dies a natural death once tor is bought. **/ 5 | export async function main(ns) { 6 | const interval = 2000; 7 | 8 | var keepRunning = ns.args.length > 0 && ns.args[0] == "-c"; 9 | if (!keepRunning) 10 | ns.print(`tor-manager will run once. Run with argument "-c" to run continuously.`) 11 | 12 | let hasTor = () => ns.scan("home").includes("darkweb"); 13 | if (hasTor()) 14 | return ns.print('Player already has Tor'); 15 | do { 16 | if (hasTor()) { 17 | ns.toast(`Purchased the Tor router!`, 'success'); 18 | break; 19 | } 20 | ns.singularity.purchaseTor(); 21 | if (keepRunning) 22 | await ns.sleep(interval); 23 | } 24 | while (keepRunning); 25 | } -------------------------------------------------------------------------------- /Tasks/write-file.js: -------------------------------------------------------------------------------- 1 | /** @param {NS} ns 2 | * A way to write a new file from some args data **/ 3 | export function main(ns) { 4 | if (ns.args.length == 0) return ns.tprint("You must run this script with the arguments to pass to ns.write") 5 | if (ns.args.length == 2) // Default to "w" (overwrite mode) 6 | ns.args.push("w") 7 | return ns.write(...ns.args); 8 | } -------------------------------------------------------------------------------- /analyze-hack.js: -------------------------------------------------------------------------------- 1 | import { getConfiguration, disableLogs, formatMoney as importedFormatMoney, formatDuration, scanAllServers } from './helpers.js' 2 | 3 | const argsSchema = [ 4 | ['all', false], // Set to true to report on all servers, not just the ones within our hack level 5 | ['silent', false], // Set to true to disable outputting the best servers to the terminal 6 | ['at-hack-level', 0], // Simulate expected gains when the player reaches the specified hack level. 0 means use the player's current hack level. 7 | ['hack-percent', -1], // Compute gains when hacking a certain percentage of each server's money. -1 estimates hack percentage based on current ram available, capped at 98% 8 | ['include-hacknet-ram', false], // Whether to include hacknet servers' RAM when computing current ram available 9 | ['disable-formulas-api', false], // Disables use of the formulas API even if it is available (useful for debugging the fallback logic used when formulas is unavailable) 10 | ]; 11 | 12 | export function autocomplete(data, args) { 13 | data.flags(argsSchema); 14 | return []; 15 | } 16 | 17 | /** @param {NS} ns **/ 18 | export async function main(ns) { 19 | const options = getConfiguration(ns, argsSchema); 20 | if (!options) return; // Invalid options, or ran in --help mode. 21 | disableLogs(ns, ["scan", "sleep"]); 22 | 23 | let serverNames = [""]; // Provide a type hint to the IDE 24 | serverNames = scanAllServers(ns); 25 | 26 | var weaken_ram = 1.75; 27 | var grow_ram = 1.75; 28 | var hack_ram = 1.7; 29 | 30 | var hack_percent = options['hack-percent'] / 100; 31 | var use_est_hack_percent = false; 32 | if (options['hack-percent'] == -1) { 33 | use_est_hack_percent = true; 34 | } else { 35 | hack_percent = options['hack-percent'] / 100; 36 | if (hack_percent <= 0 || hack_percent >= 1) { 37 | ns.tprint("hack-percent out of range (0-100)"); 38 | return; 39 | } 40 | } 41 | 42 | var player = ns.getPlayer(); 43 | //ns.print(JSON.stringify(player)); 44 | 45 | if (options['at-hack-level']) player.skills.hacking = options['at-hack-level']; 46 | let servers = serverNames.map(ns.getServer); 47 | // Compute the total RAM available to us on all servers (e.g. for running hacking scripts) 48 | var ram_total = servers.reduce(function (total, server) { 49 | if (!server.hasAdminRights || (server.hostname.startsWith('hacknet') && !options['include-hacknet-ram'])) return total; 50 | return total + server.maxRam; 51 | }, 0); 52 | 53 | // Override the imported formatMoney to handle amounts less than 0.01: 54 | let formatMoney = (amt) => amt > 0.01 ? importedFormatMoney(amt) : '$' + amt.toPrecision(3); 55 | 56 | /** Helper to compute server gain/exp rates at a specific hacking level 57 | * @param {Server} server 58 | * @param {Player} player */ 59 | function getRatesAtHackLevel(server, player, hackLevel) { 60 | let theoreticalGainRate, cappedGainRate, expRate; 61 | let useFormulas = !options['disable-formulas-api']; 62 | if (useFormulas) { 63 | // Temporarily change the hack level on the player object to the requested level 64 | const real_player_hack_skill = player.skills.hacking; 65 | player.skills.hacking = hackLevel; 66 | // Assume we will have wekened the server to min-security and taken it to max money before targetting 67 | server.hackDifficulty = server.minDifficulty; 68 | server.moneyAvailable = server.moneyMax; 69 | try { 70 | // Compute the cost (ram*seconds) for each tool 71 | const weakenCost = weaken_ram * ns.formulas.hacking.weakenTime(server, player); 72 | const growCost = grow_ram * ns.formulas.hacking.growTime(server, player) + weakenCost * 0.004 / 0.05; 73 | const hackCost = hack_ram * ns.formulas.hacking.hackTime(server, player) + weakenCost * 0.002 / 0.05; 74 | 75 | // Compute the growth and hack gain rates 76 | const growGain = Math.log(ns.formulas.hacking.growPercent(server, 1, player, 1)); 77 | const hackGain = ns.formulas.hacking.hackPercent(server, player); 78 | // If hack gain is less than this minimum (very high BN12 levels?) We must coerce it to some minimum value to avoid NAN results. 79 | const minHackGain = 1e-10; 80 | if (hackGain <= minHackGain) 81 | ns.print(`WARN: hackGain is ${hackGain.toPrecision(3)}. Coercing it to the minimum value ${minHackGain} (${server.hostname})`); 82 | server.estHackPercent = Math.max(minHackGain, Math.min(0.98, 83 | Math.min(ram_total * hackGain / hackCost, 1 - 1 / Math.exp(ram_total * growGain / growCost)))); // TODO: I think these might be off by a factor of 2x 84 | if (use_est_hack_percent) hack_percent = server.estHackPercent; 85 | const grows_per_cycle = -Math.log(1 - hack_percent) / growGain; 86 | const hacks_per_cycle = hack_percent / hackGain; 87 | const hackProfit = server.moneyMax * hack_percent * ns.formulas.hacking.hackChance(server, player); 88 | // Compute the relative monetary gain 89 | theoreticalGainRate = hackProfit / (growCost * grows_per_cycle + hackCost * hacks_per_cycle) * 1000 /* Convert per-millisecond rate to per-second */; 90 | expRate = ns.formulas.hacking.hackExp(server, player) * (1 + 0.002 / 0.05) / (hackCost) * 1000; 91 | // The practical cap on revenue is based on your hacking scripts. For my hacking scripts this is about 20% per second, adjust as needed 92 | // No idea why we divide by ram_total - Basically ensures that as our available RAM gets larger, the sort order merely becomes "by server max money" 93 | cappedGainRate = Math.min(theoreticalGainRate, hackProfit / ram_total); 94 | ns.print(`At hack level ${hackLevel} and steal ${(hack_percent * 100).toPrecision(3)}%: ` + 95 | `Theoretical ${formatMoney(theoreticalGainRate)}, Limit: ${formatMoney(hackProfit / ram_total)}, Exp: ${expRate.toPrecision(3)}, ` + 96 | `Hack Chance: ${(ns.formulas.hacking.hackChance(server, player) * 100).toPrecision(3)}% (${server.hostname})`); 97 | } 98 | catch { // Formulas API unavailable? 99 | useFormulas = false; 100 | } finally { 101 | player.skills.hacking = real_player_hack_skill; // Restore the real hacking skill if we changed it temporarily 102 | } 103 | } 104 | // Solution for when formulas API is disabled or unavailable 105 | if (!useFormulas) { 106 | // Fall-back to returning a "gain rates" based purely on current hack time (i.e. ignoring the RAM associated with required grow/weaken threads) 107 | let timeToHack = ns.getWeakenTime(server.hostname) / 4.0; 108 | // Realistically, batching scripts run on carefully timed intervals (e.g. batches scheduled no less than 200 ms apart). 109 | // So for very small time-to-weakens, we use a "capped" gain rate based on a more achievable number of hacks per second. 110 | let cappedTimeToHack = Math.max(timeToHack, 200) 111 | // the server computes experience gain based on the server's base difficulty. To get a rate, we divide that by the timeToWeaken 112 | let relativeExpGain = 3 + server.minDifficulty * 0.3; // Ignore HackExpGain mults since they affect all servers equally 113 | server.estHackPercent = 1; // Our simple calculations below are based on 100% of server money on every server. 114 | [theoreticalGainRate, cappedGainRate, expRate] = [server.moneyMax / timeToHack, server.moneyMax / cappedTimeToHack, relativeExpGain / timeToHack]; 115 | ns.print(`Without formulas.exe, based on max money ${formatMoney(server.moneyMax)} and hack-time ${formatDuration(timeToHack)} (capped at ${formatDuration(cappedTimeToHack)})): ` + 116 | `Theoretical ${formatMoney(theoreticalGainRate)}, Limit: ${formatMoney(cappedGainRate)}, Exp: ${expRate.toPrecision(3)} (${server.hostname})`); 117 | } 118 | return [theoreticalGainRate, cappedGainRate, expRate]; 119 | } 120 | 121 | ns.print(`All? ${options['all']} Player hack: ${player.skills.hacking} Ram total: ${ram_total}`); 122 | //ns.print(`\n` + servers.map(s => `${s.hostname} bought: ${s.purchasedByPlayer} moneyMax: ${s.moneyMax} admin: ${s.hasAdminRights} hack: ${s.requiredHackingSkill}`).join('\n')); 123 | 124 | // Filter down to the list of servers we wish to report on 125 | servers = servers.filter(server => !server.purchasedByPlayer && (server.moneyMax || 0) > 0 && 126 | (options['all'] || server.hasAdminRights && server.requiredHackingSkill <= player.skills.hacking)); 127 | 128 | // First address the servers within our hacking level 129 | const unlocked_servers = servers.filter(s => s.requiredHackingSkill <= player.skills.hacking) 130 | .map(function (server) { 131 | [server.theoreticalGainRate, server.gainRate, server.expRate] = getRatesAtHackLevel(server, player, player.skills.hacking); 132 | return server; 133 | }); 134 | // The best server's gain rate will be used to pro-rate the relative gain of servers that haven't been unlocked yet (if they were unlocked at this level) 135 | const best_unlocked_server = unlocked_servers.sort((a, b) => b.gainRate - a.gainRate)[0]; 136 | ns.print("Best unlocked server: ", best_unlocked_server.hostname, " with ", formatMoney(best_unlocked_server.gainRate), " per ram-second"); 137 | // Compute locked server's gain rates (pro rated back to the current player's hack level) 138 | const locked_servers = servers.filter(s => s.requiredHackingSkill > player.skills.hacking).sort((a, b) => a.requiredHackingSkill - b.requiredHackingSkill) 139 | .map(function (server) { 140 | // We will need to fake the hacking skill to get the numbers for when this server will first be unlocked, but to keep the comparison 141 | // fair, we will need to scale down the gain by the amount current best server gains now, verses what it would gain at that hack level. 142 | const [bestUnlockedScaledGainRate, _, bestUnlockedScaledExpRate] = getRatesAtHackLevel(best_unlocked_server, player, server.requiredHackingSkill); 143 | const gainRateScaleFactor = bestUnlockedScaledGainRate ? best_unlocked_server.theoreticalGainRate / bestUnlockedScaledGainRate : 1; 144 | const expRateScaleFactor = bestUnlockedScaledExpRate ? best_unlocked_server.expRate / bestUnlockedScaledExpRate : 1; 145 | const [theoreticalGainRate, cappedGainRate, expRate] = getRatesAtHackLevel(server, player, server.requiredHackingSkill); 146 | // Apply the scaling factors, as well as the same cap as above 147 | server.theoreticalGainRate = theoreticalGainRate * gainRateScaleFactor; 148 | server.expRate = expRate * expRateScaleFactor; 149 | server.gainRate = Math.min(server.theoreticalGainRate, cappedGainRate); 150 | ns.print(`${server.hostname}: Scaled theoretical gain by ${gainRateScaleFactor.toPrecision(3)} to ${formatMoney(server.theoreticalGainRate)} ` + 151 | `(capped at ${formatMoney(cappedGainRate)}) and exp by ${expRateScaleFactor.toPrecision(3)} to ${server.expRate.toPrecision(3)}`); 152 | return server; 153 | }) || []; 154 | // Combine the lists, sort, and display a summary. 155 | const server_eval = unlocked_servers.concat(locked_servers); 156 | const best_server = server_eval.sort((a, b) => b.gainRate - a.gainRate)[0]; 157 | if (!options['silent']) 158 | ns.tprint("Best server: ", best_server.hostname, " with ", formatMoney(best_server.gainRate), " per ram-second"); 159 | 160 | // Print all servers by best to work hack money value 161 | let order = 1; 162 | let serverListByGain = `Servers in order of best to worst hack money at Hack ${player.skills.hacking}:`; 163 | for (const server of server_eval) 164 | serverListByGain += `\n ${order++} ${server.hostname}, with ${formatMoney(server.gainRate)} per ram-second while stealing ` + 165 | `${(server.estHackPercent * 100).toPrecision(3)}% (unlocked at hack ${server.requiredHackingSkill})`; 166 | ns.print(serverListByGain); 167 | 168 | // Reorder servers by exp and sort by best to work hack experience gain rate 169 | var best_exp_server = server_eval.sort(function (a, b) { 170 | return b.expRate - a.expRate; 171 | })[0]; 172 | if (!options['silent']) 173 | ns.tprint("Best exp server: ", best_exp_server.hostname, " with ", best_exp_server.expRate, " exp per ram-second"); 174 | order = 1; 175 | let serverListByExp = `Servers in order of best to worst hack exp at Hack ${player.skills.hacking}:`; 176 | for (let i = 0; i < Math.min(5, server_eval.length); i++) 177 | serverListByExp += `\n ${order++} ${server_eval[i].hostname}, with ${server_eval[i].expRate.toPrecision(3)} exp per ram-second`; 178 | ns.print(serverListByExp); 179 | 180 | ns.write('/Temp/analyze-hack.txt', JSON.stringify(server_eval.map(s => ({ 181 | hostname: s.hostname, 182 | gainRate: s.gainRate, 183 | expRate: s.expRate 184 | }))), "w"); 185 | // Below is stats for hacknet servers - uncomment at cost of 4 GB Ram 186 | /* 187 | var hacknet_nodes = [...(function* () { 188 | var n = ns.hacknet.numNodes(); 189 | for (var i = 0; i < n; i++) { 190 | var server = ns.hacknet.getNodeStats(i); 191 | server.gainRate = 1000000 / 4 * server.production / server.ram; 192 | yield server; 193 | } 194 | })()]; 195 | var best_hacknet_node = hacknet_nodes.sort(function (a, b) { 196 | return b.gainRate - a.gainRate; 197 | })[0]; 198 | if (best_hacknet_node) ns.tprint("Best hacknet node: ", best_hacknet_node.name, " with $", best_hacknet_node.gainRate, " per ram-second"); 199 | */ 200 | } -------------------------------------------------------------------------------- /ascend.js: -------------------------------------------------------------------------------- 1 | import { 2 | log, getConfiguration, getFilePath, runCommand, waitForProcessToComplete, getNsDataThroughFile, 3 | getActiveSourceFiles, getStockSymbols 4 | } from './helpers.js' 5 | 6 | const argsSchema = [ 7 | ['install-augmentations', false], // By default, augs will only be purchased. Set this flag to install (a.k.a reset) 8 | /* OR */['reset', false], // An alias for the above flag, does the same thing. 9 | ['allow-soft-reset', false], // If set to true, allows ascend.js to invoke a **soft** reset (installs no augs) when no augs are affordable. This is useful e.g. when ascending rapidly to grind hacknet hash upgrades. 10 | ['skip-staneks-gift', false], // By default, we get stanek's gift before our first install (except in BN8). If set to true, skip this step. 11 | /* Deprecated */['bypass-stanek-warning', false], // (Obsoleted by the above option) Used to warn you if you were installing augs without accepting stanek's gift 12 | // Spawn this script after installing augmentations (Note: Args not supported by the game) 13 | ['on-reset-script', null], // By default, will start with `stanek.js` if you have stanek's gift, otherwise `daemon.js`. 14 | ['ticks-to-wait-for-additional-purchases', 10], // Don't reset until we've gone this many game ticks without any new purchases being made (10 * 200ms (game tick time) ~= 2 seconds) 15 | ['max-wait-time', 60000], // The maximum number of milliseconds we'll wait for external scripts to purchase whatever permanent upgrades they can before we ascend anyway. 16 | ['prioritize-home-ram', false], // If set to true, will spend as much money as possible on upgrading home RAM before buying augmentations 17 | /* Deprecated */['prioritize-augmentations', true], // (Legacy flag, now ignored - left for backwards compatibility) 18 | ]; 19 | 20 | export function autocomplete(data, args) { 21 | data.flags(argsSchema); 22 | const lastFlag = args.length > 1 ? args[args.length - 2] : null; 23 | if (["--on-reset-script"].includes(lastFlag)) 24 | return data.scripts; 25 | return []; 26 | } 27 | 28 | /** @param {NS} ns 29 | * This script is meant to do all the things best done when ascending (in a generally ideal order) **/ 30 | export async function main(ns) { 31 | const options = getConfiguration(ns, argsSchema); 32 | if (!options) return; // Invalid options, or ran in --help mode. 33 | let dictSourceFiles = await getActiveSourceFiles(ns); // Find out what source files the user has unlocked 34 | if (!(4 in dictSourceFiles)) 35 | return log(ns, "ERROR: You cannot automate installing augmentations until you have unlocked singularity access (SF4).", true, 'error'); 36 | ns.disableLog('sleep'); 37 | if (options['prioritize-augmentations']) 38 | log(ns, "INFO: The --prioritize-augmentations flag is deprecated, as this is now the default behaviour. Use --prioritize-home-ram to get back the old behaviour.") 39 | 40 | // Kill every script except this one, since it can interfere with out spending 41 | let pid = await runCommand(ns, `ns.ps().filter(s => s.filename != ns.args[0]).forEach(s => ns.kill(s.pid));`, 42 | '/Temp/kill-everything-but.js', [ns.getScriptName()]); 43 | await waitForProcessToComplete(ns, pid, true); // Wait for the script to shut down, indicating it has shut down other scripts 44 | 45 | // Stop the current action so that we're no longer spending money (if training) and can collect rep earned (if working) 46 | await getNsDataThroughFile(ns, 'ns.singularity.stopAction()'); 47 | 48 | // Clear any global reserve so that all money can be spent 49 | await ns.write("reserve.txt", 0, "w"); 50 | 51 | // STEP 1: Liquidate Stocks and (SF9) Hacknet Hashes 52 | log(ns, 'Sell stocks and hashes...', true, 'info'); 53 | ns.run(getFilePath('spend-hacknet-hashes.js'), 1, '--liquidate'); 54 | 55 | // If we do not have tix api access, we cannot automate checking on or selling stocks, so skip this 56 | const hasTixApiAccess = await getNsDataThroughFile(ns, 'ns.stock.hasTIXAPIAccess()'); 57 | if (hasTixApiAccess) { 58 | const stkSymbols = await getStockSymbols(ns); 59 | const countOwnedStocks = async () => await getNsDataThroughFile(ns, `ns.args.map(sym => ns.stock.getPosition(sym))` + 60 | `.reduce((t, stk) => t + (stk[0] + stk[2] > 0 ? 1 : 0), 0)`, '/Temp/owned-stocks.txt', stkSymbols); 61 | let ownedStocks; 62 | do { 63 | log(ns, `INFO: Waiting for ${ownedStocks} owned stocks to be sold...`, false, 'info'); 64 | pid = ns.run(getFilePath('stockmaster.js'), 1, '--liquidate'); 65 | if (pid) await waitForProcessToComplete(ns, pid, true); 66 | else log(ns, `ERROR: Failed to run "stockmaster.js --liquidate" to sell ${ownedStocks} owned stocks. Will try again soon...`, false, 'true'); 67 | await ns.sleep(1000); 68 | ownedStocks = await countOwnedStocks(); 69 | } while (ownedStocks > 0); 70 | } 71 | 72 | // STEP 2: Buy Home RAM Upgrades (more important than squeezing in a few extra augs) 73 | const spendOnHomeRam = async () => { 74 | log(ns, 'Try Upgrade Home RAM...', true, 'info'); 75 | pid = ns.run(getFilePath('Tasks/ram-manager.js'), 1, '--reserve', '0', '--budget', '0.8'); 76 | await waitForProcessToComplete(ns, pid, true); // Wait for the script to shut down, indicating it has bought all it can. 77 | }; 78 | if (options['prioritize-home-ram']) await spendOnHomeRam(); 79 | 80 | // STEP 3: (SF13) STANEK'S GIFT 81 | // There is now an API to accept stanek's gift without resorting to exploits. We must do this before installing augs for the first time 82 | if (13 in dictSourceFiles) { 83 | // By feature request: Auto-skip stanek in BN8 (requires a separate API check to get current BN) 84 | let isInBn8 = 8 === (await getNsDataThroughFile(ns, `ns.getResetInfo()`)).currentNode; 85 | 86 | if (options['skip-staneks-gift']) 87 | log(ns, 'INFO: --skip-staneks-gift was set, we will not accept it.'); 88 | else if (isInBn8) { 89 | log(ns, 'INFO: Stanek\'s gift is useless in BN8, setting the --skip-staneks-gift argument automatically.'); 90 | options['skip-staneks-gift'] = true; 91 | } else { 92 | log(ns, 'Accepting Stanek\'s Gift (if this is the first reset)...', true, 'info'); 93 | const haveStanek = await getNsDataThroughFile(ns, `ns.stanek.acceptGift()`); 94 | if (haveStanek) log(ns, 'INFO: Confirmed that we have Stanek\'s Gift', true, 'info'); 95 | else { 96 | log(ns, 'WARNING: It looks like we can\'t get Stanek\'s Gift. (Did you manually purchase some augmentations?)', true, 'warning'); 97 | options['skip-staneks-gift'] = true; // Nothing we can do, no point in failing our augmentation install 98 | } 99 | } 100 | } 101 | 102 | // STEP 4: Buy as many desired augmentations as possible 103 | log(ns, 'Purchasing augmentations...', true, 'info'); 104 | const facmanArgs = ['--purchase', '-v']; 105 | if (options['skip-staneks-gift']) { 106 | log(ns, 'INFO: Sending the --ignore-stanek argument to faction-manager.js') 107 | facmanArgs.push('--ignore-stanek'); 108 | } 109 | pid = ns.run(getFilePath('faction-manager.js'), 1, ...facmanArgs); 110 | await waitForProcessToComplete(ns, pid, true); // Wait for the script to shut down, indicating it is done. 111 | 112 | // If we are not slated to install any augmentations, ABORT 113 | // Get owned + purchased augmentations, then installed augmentations. Ensure there's a difference 114 | let purchasedAugmentations = await getNsDataThroughFile(ns, 'ns.singularity.getOwnedAugmentations(true)', '/Temp/player-augs-purchased.txt'); 115 | let installedAugmentations = await getNsDataThroughFile(ns, 'ns.singularity.getOwnedAugmentations()', '/Temp/player-augs-installed.txt'); 116 | let noAugsToInstall = purchasedAugmentations.length == installedAugmentations.length; 117 | if (noAugsToInstall && !options['allow-soft-reset']) 118 | return log(ns, `ERROR: See above faction-manager.js logs - there are no new purchased augs. ` + 119 | `Specify --allow-soft-reset to proceed without any purchased augs.`, true, 'error'); 120 | 121 | // STEP 2 (If Deferred): Upgrade home RAM after purchasing augmentations if this option was set. 122 | if (!options['prioritize-home-ram']) await spendOnHomeRam(); 123 | 124 | // STEP 5: Try to Buy 4S data / API if we haven't already and can afford it (although generally stockmaster.js would have bought these if it could) 125 | log(ns, 'Checking on Stock Market upgrades...', true, 'info'); 126 | await getNsDataThroughFile(ns, 'ns.stock.purchaseWseAccount()'); 127 | let hasStockApi = await getNsDataThroughFile(ns, 'ns.stock.purchaseTixApi()'); 128 | if (hasStockApi) { 129 | await getNsDataThroughFile(ns, 'ns.stock.purchase4SMarketData()'); 130 | await getNsDataThroughFile(ns, 'ns.stock.purchase4SMarketDataTixApi()'); 131 | } 132 | 133 | // STEP 6: (SF10) Buy whatever sleeve upgrades we can afford 134 | if (10 in dictSourceFiles) { 135 | log(ns, 'Try Upgrade Sleeves...', true, 'info'); 136 | ns.run(getFilePath('sleeve.js'), 1, '--reserve', '0', '--aug-budget', '1', '--min-aug-batch', '1', '--buy-cooldown', '0', '--disable-training'); 137 | await ns.sleep(500); // Give it time to make its initial purchases. Note that we do not block on the process shutting down - it will keep running. 138 | } 139 | 140 | // STEP 7: (SF2) Buy whatever gang equipment we can afford 141 | if (2 in dictSourceFiles) { 142 | log(ns, 'Try Upgrade Gangs...', true, 'info'); 143 | ns.run(getFilePath('gangs.js'), 1, '--reserve', '0', '--augmentations-budget', '1', '--equipment-budget', '1'); 144 | await ns.sleep(500); // Give it time to make its initial purchases. Note that we do not block on the process shutting down - it will keep running. 145 | } 146 | 147 | // STEP 8: Buy whatever home CPU upgrades we can afford 148 | log(ns, 'Try Upgrade Home Cores...', true, 'info'); 149 | pid = await runCommand(ns, `while(ns.singularity.upgradeHomeCores()); { await ns.sleep(10); }`, '/Temp/upgrade-home-ram.js'); 150 | await waitForProcessToComplete(ns, pid, true); // Wait for the script to shut down, indicating it has bought all it can. 151 | 152 | // STEP 9: Join every faction we've been invited to (gives a little INT XP) 153 | let invites = await getNsDataThroughFile(ns, 'ns.singularity.checkFactionInvitations()'); 154 | if (invites.length > 0) { 155 | pid = await runCommand(ns, 'ns.args.forEach(f => ns.singularity.joinFaction(f))', '/Temp/join-factions.js', invites); 156 | await waitForProcessToComplete(ns, pid, true); 157 | } 158 | 159 | // TODO: If in corporation, and buyback shares is available, buy as many as we can afford 160 | 161 | // STEP 10: WAIT: For money to stop decreasing, so we know that external scripts have bought what they could. 162 | log(ns, 'Waiting for purchasing to stop...', true, 'info'); 163 | let money = 0, lastMoney = 0, ticksWithoutPurchases = 0; 164 | const maxWait = Date.now() + options['max-wait-time']; 165 | while (ticksWithoutPurchases < options['ticks-to-wait-for-additional-purchases'] && (Date.now() < maxWait)) { 166 | const start = Date.now(); // Used to wait for the game to tick. 167 | const refreshMoney = async () => money = 168 | await getNsDataThroughFile(ns, `ns.getServerMoneyAvailable(ns.args[0])`, null, ["home"]); 169 | while ((Date.now() - start <= 200) && lastMoney == await refreshMoney()) 170 | await ns.sleep(10); // Wait for game to tick (money to change) - might happen sooner than 200ms 171 | ticksWithoutPurchases = money < lastMoney ? 0 : ticksWithoutPurchases + 1; 172 | lastMoney = money; 173 | } 174 | 175 | // TODO STEP 11: Accept any outstanding faction invitations, and claim our +1 free favour if available. 176 | /* 177 | const factionInvites = ns.singularity.checkFactionInvitations() 178 | if (factionInvites.length > 0) 179 | factionInvites.forEach(factionName => ns.singularity.joinFaction(factionName)); 180 | if (ns.singularity.exportGameBonus()) 181 | ns.singularity.exportGame(); 182 | // TODO: No way to close the pop-up save dialog, which is a deal-breaker for me. 183 | */ 184 | 185 | // STEP 4 REDUX: If somehow we have money left over and can afford some junk augs that weren't on our desired list, grab them too 186 | log(ns, 'Seeing if we can afford any other augmentations...', true, 'info'); 187 | facmanArgs.push('--stat-desired', '_'); // Means buy any aug with any stats 188 | pid = ns.run(getFilePath('faction-manager.js'), 1, ...facmanArgs); 189 | await waitForProcessToComplete(ns, pid, true); // Wait for the script to shut down, indicating it is done. 190 | 191 | // Clean up our temp folder - it's good to do this once in a while to reduce the save footprint 192 | // As well as to ensure that data written out on this bitnode don't confuse scripts in the next one. 193 | await waitForProcessToComplete(ns, ns.run(getFilePath('cleanup.js')), true); 194 | 195 | // FINALLY: If configured, soft reset 196 | if (options.reset || options['install-augmentations']) { 197 | log(ns, '\nCatch you on the flippity-flip\n', true, 'success'); 198 | await ns.sleep(1000); // Pause for effect? 199 | const resetScript = options['on-reset-script'] ?? 200 | // Default script (if none is specified) is stanek.js if we have it (which in turn will spawn daemon.js when done) 201 | (purchasedAugmentations.includes(`Stanek's Gift - Genesis`) ? getFilePath('stanek.js') : getFilePath('daemon.js')); 202 | if (noAugsToInstall) 203 | await runCommand(ns, `ns.singularity.softReset(ns.args[0])`, null, [resetScript]); 204 | else 205 | await runCommand(ns, `ns.singularity.installAugmentations(ns.args[0])`, null, [resetScript]); 206 | } else 207 | log(ns, `SUCCESS: Ready to ascend. In the future, you can run with --reset (or --install-augmentations) ` + 208 | `to actually perform the reset automatically.`, true, 'success'); 209 | } -------------------------------------------------------------------------------- /cleanup.js: -------------------------------------------------------------------------------- 1 | /** @param {NS} ns **/ 2 | export async function main(ns) { 3 | for (let file of ns.ls('home', 'Temp/')) 4 | ns.print((ns.rm(file) ? "Removed " : "Failed to remove ") + file); 5 | } -------------------------------------------------------------------------------- /crime.js: -------------------------------------------------------------------------------- 1 | import { instanceCount, getNsDataThroughFile, formatDuration, formatNumberShort, tail } from './helpers.js' 2 | import { crimeForKillsKarmaStats } from './work-for-factions.js' 3 | 4 | const crimes = ["Shoplift", "Rob Store", "Mug", "Larceny", "Deal Drugs", "Bond Forgery", "Traffick Arms", "Homicide", "Grand Theft Auto", "Kidnap", "Assassination", "Heist"] 5 | const argFastCrimesOnly = "--fast-crimes-only"; 6 | export function autocomplete() { return crimes.concat(argFastCrimesOnly); } 7 | 8 | /** @param {NS} ns **/ 9 | export async function main(ns) { 10 | if (await instanceCount(ns) > 1) return; // Prevent multiple instances of this script from being started, even with different args. 11 | ns.disableLog('sleep'); 12 | let crime = ns.args.length == 0 ? undefined : ns.args.join(" "); // Need to join in case the crime has a space in it - it will be treated as two args 13 | tail(ns); 14 | if (!crime || ns.args.includes(argFastCrimesOnly)) // More sophisticated auto-scaling crime logic 15 | await crimeForKillsKarmaStats(ns, 0, 0, Number.MAX_SAFE_INTEGER, ns.args.includes(argFastCrimesOnly)); 16 | else // Simple crime loop for the specified crime 17 | await legacyAutoCrime(ns, crime); 18 | } 19 | 20 | /** @param {NS} ns **/ 21 | async function legacyAutoCrime(ns, crime = "Mug") { 22 | let interval = 100; 23 | while (true) { 24 | let maxBusyLoops = 100; 25 | while ((await getNsDataThroughFile(ns, `ns.singularity.isBusy()`)) && maxBusyLoops-- > 0) { 26 | await ns.sleep(interval); 27 | ns.print("Waiting to no longer be busy..."); 28 | } 29 | if (maxBusyLoops <= 0) { 30 | ns.tprint("User have been busy for too long. auto-crime.js exiting..."); 31 | return; 32 | } 33 | tail(ns); // Force a tail window open when auto-criming, or else it's very difficult to stop if it was accidentally closed. 34 | let wait = 10 + (await getNsDataThroughFile(ns, 'ns.singularity.commitCrime(ns.args[0])', null, [crime])); 35 | ns.print(`Karma: ${formatNumberShort(ns.heart.break())} Committing crime \"${crime}\" and sleeping for ${formatDuration(wait)}...`); 36 | await ns.sleep(wait); 37 | } 38 | } -------------------------------------------------------------------------------- /dev-console.js: -------------------------------------------------------------------------------- 1 | /** @param {NS} ns */ 2 | export async function main(ns) { 3 | globalThis.webpack_require ?? webpackChunkbitburner.push([[-1], {}, w => globalThis.webpack_require = w]); 4 | Object.keys(webpack_require.m).forEach(k => Object.values(webpack_require(k)).forEach(p => p?.toPage?.('Dev'))); 5 | } -------------------------------------------------------------------------------- /dump-ns-namespace.js: -------------------------------------------------------------------------------- 1 | import { runCommand } from './helpers.js' 2 | 3 | export function autocomplete(data, args) { 4 | return [ 5 | "bladeburner", "codingcontract", "corporation", "enums", "formulas", "gang", "grafting", "hacknet", 6 | "infiltration", "singularity", "sleeve", "stanek", "stock", "ui" 7 | ]; 8 | } 9 | 10 | /** Intended to help me explore the NS namespace by dumping properties and function results. 11 | * @param {NS} ns */ 12 | export async function main(ns) { 13 | const obj = ns.args.length > 0 ? ns[ns.args[0]] : ns; 14 | const strObj = ns.args.length > 0 ? `ns.${ns.args[0]}` : 'ns'; 15 | // Print all keys 16 | ns.tprint(Object.keys(obj)); 17 | // Attempt to print the contents of all keys that are either properties or parameterless function calls. 18 | // TODO: Need a denylist of functions that should not be called because they will screw with the current game 19 | // (e.g. softReset, ui.resetTheme, stopAction, etc...) 20 | for (const k of Object.keys(obj)) { 21 | const strMember = `${strObj}.${k}` 22 | await runCommand(ns, `try { 23 | const member = ${strMember}; 24 | if(typeof member === 'function') 25 | ns.tprint('${strMember}(): ' + '(function)'); // JSON.stringify(member())); // Turns out running arbitrary functions has consequences 26 | else 27 | ns.tprint('${strMember}: ' + JSON.stringify(member)); 28 | } catch { /* Ignore failures when calling functions that require parameters */ }`); 29 | } 30 | } -------------------------------------------------------------------------------- /farm-intelligence.js: -------------------------------------------------------------------------------- 1 | import { log, waitForProcessToComplete, formatDuration, getFilePath } from './helpers.js' 2 | 3 | /** @param {NS} ns */ 4 | export async function main(ns) { 5 | const timeSinceLastAug = Date.now() - ns.getResetInfo().lastAugReset; 6 | if (timeSinceLastAug > 20 * 60 * 1000) { 7 | return log(ns, `WARNING: It's been ${formatDuration(timeSinceLastAug)} since your last reset. ` + 8 | `For your protection, we will not soft-reset. Either install augs or soft-reset manually ` + 9 | `once before running this script.`, true); 10 | } else if (timeSinceLastAug > 5000) { 11 | log(ns, `Resetting to get a list of instantly-available invites...`, true); 12 | return ns.singularity.softReset(ns.getScriptName()); 13 | } 14 | const invites = ns.singularity.checkFactionInvitations(); 15 | if (invites.length < 10) 16 | return log(ns, `WARNING: You only have invites to join ${invites.length} factions. ` + 17 | `For best results, you should get invited to all 10 megacorp factions before running this script. ` + 18 | `You can achieve this by running:\n` + 19 | `run work-for-factions.js --get-invited-to-every-faction --invites-only \n` + 20 | `or just edit out this check if you're sure you want to proceed.`, true); 21 | await waitForProcessToComplete(ns, ns.run(getFilePath('cleanup.js'))); 22 | // Prepare a very small script that will accept all invites in a tight loop. 23 | const tempFile = '/Temp/farm-intelligence.js'; 24 | await ns.write(tempFile, `export async function main(ns) { 25 | ns.disableLog('ALL'); 26 | ${JSON.stringify(ns.singularity.checkFactionInvitations())}.forEach(f => ns.singularity.joinFaction(f)); 27 | ns.singularity.softReset('${tempFile}'); 28 | }`, "w"); 29 | ns.run(tempFile); 30 | log(ns, `SUCCESS: Beginning soft-reset loop. It may look like nothing's happening, but watch your intelligence stat...`, true, 'success'); 31 | } -------------------------------------------------------------------------------- /git-pull.js: -------------------------------------------------------------------------------- 1 | let options; 2 | const argsSchema = [ 3 | ['github', 'alainbryden'], 4 | ['repository', 'bitburner-scripts'], 5 | ['branch', 'main'], 6 | ['download', []], // By default, all supported files in the repository will be downloaded. Override with just a subset of files here 7 | ['new-file', []], // If a repository listing fails, only files returned by ns.ls() will be downloaded. You can add additional files to seek out here. 8 | ['subfolder', ''], // Can be set to download to a sub-folder that is not part of the remote repository structure 9 | ['extension', ['.js', '.ns', '.txt', '.script']], // Files to download by extension 10 | ['omit-folder', ['Temp/']], // Folders to omit when getting a list of files to update (TODO: This may be obsolete now that we get a list of files from github itself.) 11 | ]; 12 | 13 | export function autocomplete(data, args) { 14 | data.flags(argsSchema); 15 | const lastFlag = args.length > 1 ? args[args.length - 2] : null; 16 | if (["--download", "--subfolder", "--omit-folder"].includes(lastFlag)) 17 | return data.scripts; 18 | return []; 19 | } 20 | 21 | /** @param {NS} ns 22 | * Will try to download a fresh version of every file on the current server. 23 | * You are responsible for: 24 | * - Backing up your save / scripts first (try `download *` in the terminal) 25 | * - Ensuring you have no local changes that you don't mind getting overwritten **/ 26 | export async function main(ns) { 27 | options = ns.flags(argsSchema); 28 | // Once upon a time, the game API required folders to have a leading slash 29 | // As of 2.3.1, not only is this no longer needed, but it can break the game. 30 | options.subfolder = options.subfolder ? trimSlash(options.subfolder) : // Remove leading slash from any user-specified folder 31 | ns.getScriptName().substring(0, ns.getScriptName().lastIndexOf('/')); // Default to the current folder 32 | const baseUrl = `raw.githubusercontent.com/${options.github}/${options.repository}/${options.branch}/`; 33 | const filesToDownload = options['new-file'].concat(options.download.length > 0 ? options.download : await repositoryListing(ns)); 34 | for (const localFilePath of filesToDownload) { 35 | let fullLocalFilePath = pathJoin(options.subfolder, localFilePath); 36 | const remoteFilePath = `https://` + pathJoin(baseUrl, localFilePath); 37 | ns.print(`Trying to update "${fullLocalFilePath}" from ${remoteFilePath} ...`); 38 | if (await ns.wget(`${remoteFilePath}?ts=${new Date().getTime()}`, fullLocalFilePath) && rewriteFileForSubfolder(ns, fullLocalFilePath)) 39 | ns.tprint(`SUCCESS: Updated "${fullLocalFilePath}" to the latest from ${remoteFilePath}`); 40 | else 41 | ns.tprint(`WARNING: "${fullLocalFilePath}" was not updated. (Currently running, or not located at ${remoteFilePath}?)`) 42 | } 43 | ns.tprint(`INFO: Pull complete. If you have any questions or issues, create an issue on github or join the ` + 44 | `Bitburner Discord channel "#Insight's-scripts": https://discord.com/channels/415207508303544321/935667531111342200`); 45 | // Remove any temp files / scripts from the prior version 46 | ns.run(pathJoin(options.subfolder, `cleanup.js`)); 47 | } 48 | 49 | /** Removes leading and trailing slashes from the specified string */ 50 | function trimSlash(s) { 51 | // Once upon a time, the game API required folders to have a leading slash 52 | // As of 2.3.1, not only is this no longer needed, but it can break the game. 53 | if (s.startsWith('/')) 54 | s = s.slice(1); 55 | if (s.endsWith('/')) 56 | s = s.slice(0, -1); 57 | return s; 58 | } 59 | 60 | /** Joins all arguments as components in a path, e.g. pathJoin("foo", "bar", "/baz") = "foo/bar/baz" **/ 61 | function pathJoin(...args) { 62 | return trimSlash(args.filter(s => !!s).join('/').replace(/\/\/+/g, '/')); 63 | } 64 | 65 | /** @param {NS} ns 66 | * Rewrites a file with path substitions to handle downloading to a subfolder. **/ 67 | export function rewriteFileForSubfolder(ns, path) { 68 | if (!options.subfolder || path.includes('git-pull.js')) 69 | return true; 70 | let contents = ns.read(path); 71 | // Replace subfolder reference in helpers.js getFilePath: 72 | contents = contents.replace(`const subfolder = ''`, `const subfolder = '${options.subfolder}/'`); 73 | // Replace any imports, which can't use getFilePath, but only if they don't specify a relative path (../) 74 | contents = contents.replace(/from '(\.\/)?((?!\.\.\/).*)'/g, `from '${pathJoin(options.subfolder, '$2')}'`); 75 | ns.write(path, contents, 'w'); 76 | return true; 77 | } 78 | 79 | /** @param {NS} ns 80 | * Gets a list of files to download, either from the github repository (if supported), or using a local directory listing **/ 81 | async function repositoryListing(ns, folder = '') { 82 | // Note: Limit of 60 free API requests per day, don't over-do it 83 | const listUrl = `https://api.github.com/repos/${options.github}/${options.repository}/contents/${folder}?ref=${options.branch}` 84 | let response = null; 85 | try { 86 | response = await fetch(listUrl); // Raw response 87 | // Expect an array of objects: [{path:"", type:"[file|dir]" },{...},...] 88 | response = await response.json(); // Deserialized 89 | // Sadly, we must recursively retrieve folders, which eats into our 60 free API requests per day. 90 | const folders = response.filter(f => f.type == "dir").map(f => f.path); 91 | let files = response.filter(f => f.type == "file").map(f => f.path) 92 | .filter(f => options.extension.some(ext => f.endsWith(ext))); 93 | ns.print(`The following files exist at ${listUrl}\n${files.join(", ")}`); 94 | for (const folder of folders) 95 | files = files.concat((await repositoryListing(ns, folder)) 96 | .map(f => `/${f}`)); // Game requires folders to have a leading slash 97 | return files; 98 | } catch (error) { 99 | if (folder !== '') throw error; // Propagate the error if this was a recursive call. 100 | ns.tprint(`WARNING: Failed to get a repository listing (GitHub API request limit of 60 reached?): ${listUrl}` + 101 | `\nResponse Contents (if available): ${JSON.stringify(response ?? '(N/A)')}\nError: ${String(error)}`); 102 | // Fallback, assume the user already has a copy of all files in the repo, and use it as a directory listing 103 | return ns.ls('home').filter(name => options.extension.some(ext => f.endsWith(ext)) && 104 | !options['omit-folder'].some(dir => name.startsWith(dir))); 105 | } 106 | } -------------------------------------------------------------------------------- /grep.js: -------------------------------------------------------------------------------- 1 | // Achievement Unlocked: grep grep.js grep 2 | const usage = "Usage: run grep.js [] \n" + 3 | "- If run with one argument, searches all files for occurrences of that text.\n" + 4 | "- If run with two arguments, the first argument is the name of the file to search.\n" + 5 | "- If you wish to search all files for text with a space in it, wrap it in quotes."; 6 | export function autocomplete(data, args) { 7 | const lastFlag = args.length > 1 ? args[args.length - 2] : null; 8 | return lastFlag ? [] : data.scripts; // For the first argument, auto-complete a list of all files 9 | } 10 | /** @param {NS} ns */ 11 | export async function main(ns) { 12 | const args = ns.args; 13 | if (args.length == 0) 14 | return ns.tprint(`INFO: Searches for text in files.\n${usage}`) 15 | const search = args.length == 1 ? args[0] : args.slice(1, args.length).join(" "); 16 | // Two or more arguments, treat the first argument as a file name 17 | if (args.length > 1) { 18 | const fileName = args[0]; 19 | const contents = ns.read(fileName); 20 | if (!contents) return ns.tprint(`ERROR: File not found: "${fileName}".\n${usage}`); 21 | const output = searchRows(contents, search, fileName); 22 | return ns.tprint(output.length > 0 ? output.join("\n") : 23 | `Search string "${search}" not found in file ${fileName}`); 24 | } 25 | // Otherwise, search all files 26 | const files = ns.ls("home"); 27 | const allOutput = files.flatMap(fileName => searchRows(ns.read(fileName), search, fileName)); 28 | ns.tprint(allOutput.length > 0 ? allOutput.join("\n") : 29 | `Search string "${search}" not found in any of the ${files.length} files on "home".`); 30 | } 31 | /** Helper to search a single file's output */ 32 | function searchRows(text, search, fileName) { 33 | const output = text.split("\n").map((row, i) => [row, i]) 34 | .filter(([row, _]) => row.includes(search)) 35 | .map(([row, i]) => `${i + 1}`.padStart(3) + `: ${row}`) 36 | if (output.length > 0 && fileName) 37 | output.unshift(`Found ${output.length} occurrences of the string "${search}" in file ${fileName}:`); 38 | return output; 39 | } -------------------------------------------------------------------------------- /hacknet-upgrade-manager.js: -------------------------------------------------------------------------------- 1 | import { getConfiguration, disableLogs, formatDuration, formatMoney, } from './helpers.js' 2 | 3 | let haveHacknetServers = true; // Cached flag after detecting whether we do (or don't) have hacknet servers 4 | const argsSchema = [ 5 | ['max-payoff-time', '1h'], // Controls how far to upgrade hacknets. Can be a number of seconds, or an expression of minutes/hours (e.g. '123m', '4h') 6 | ['time', null], // alias for max-payoff-time 7 | ['c', false], // Set to true to run continuously, otherwise, it runs once 8 | ['continuous', false], 9 | ['interval', 1000], // Rate at which the program purchases upgrades when running continuously 10 | ['max-spend', Number.MAX_VALUE], // The maximum amount of money to spend on upgrades 11 | ['toast', false], // Set to true to toast purchases 12 | ['reserve', null], // Reserve this much cash (defaults to contents of reserve.txt if not specified) 13 | 14 | ]; 15 | 16 | export function autocomplete(data, _) { 17 | data.flags(argsSchema); 18 | return []; 19 | } 20 | 21 | /** @param {NS} ns **/ 22 | export async function main(ns) { 23 | const options = getConfiguration(ns, argsSchema); 24 | if (!options) return; // Invalid options, or ran in --help mode. 25 | const continuous = options.c || options.continuous; 26 | const interval = options.interval; 27 | let maxSpend = options["max-spend"]; 28 | let maxPayoffTime = options['time'] || options['max-payoff-time']; 29 | // A little string parsing to be more user friendly 30 | if (maxPayoffTime && String(maxPayoffTime).endsWith("m")) 31 | maxPayoffTime = Number.parseFloat(maxPayoffTime.replace("m", "")) * 60 32 | else if (maxPayoffTime && String(maxPayoffTime).endsWith("h")) 33 | maxPayoffTime = Number.parseFloat(maxPayoffTime.replace("h", "")) * 3600 34 | else 35 | maxPayoffTime = Number.parseFloat(maxPayoffTime); 36 | disableLogs(ns, ['sleep', 'getServerUsedRam', 'getServerMoneyAvailable']); 37 | setStatus(ns, `Starting hacknet-upgrade-manager with purchase payoff time limit of ${formatDuration(maxPayoffTime * 1000)} and ` + 38 | (maxSpend == Number.MAX_VALUE ? 'no spending limit' : `a spend limit of ${formatMoney(maxSpend)}`) + 39 | `. Current fleet: ${ns.hacknet.numNodes()} nodes...`); 40 | do { 41 | try { 42 | const moneySpent = upgradeHacknet(ns, maxSpend, maxPayoffTime, options); 43 | // Using this method, we cannot know for sure that we don't have hacknet servers until we have purchased one 44 | if (haveHacknetServers && ns.hacknet.numNodes() > 0 && ns.hacknet.hashCapacity() == 0) 45 | haveHacknetServers = false; 46 | if (maxSpend && moneySpent === false) { 47 | setStatus(ns, `Spending limit reached. Breaking...`); 48 | break; // Hack, but we return a non-number (false) when we've bought all we can for the current config 49 | } 50 | maxSpend -= moneySpent; 51 | } 52 | catch (err) { 53 | setStatus(ns, `WARNING: hacknet-upgrade-manager.js Caught (and suppressed) an unexpected error in the main loop:\n` + 54 | (typeof err === 'string' ? err : err.message || JSON.stringify(err)), false, 'warning'); 55 | } 56 | if (continuous) await ns.sleep(interval); 57 | } while (continuous); 58 | } 59 | 60 | let lastUpgradeLog = ""; 61 | function setStatus(ns, logMessage) { 62 | if (logMessage != lastUpgradeLog) ns.print(lastUpgradeLog = logMessage); 63 | } 64 | 65 | // Will buy the most effective hacknet upgrade, so long as it will pay for itself in the next {payoffTimeSeconds} seconds. 66 | /** @param {NS} ns **/ 67 | export function upgradeHacknet(ns, maxSpend, maxPayoffTimeSeconds = 3600 /* 3600 sec == 1 hour */, options) { 68 | const currentHacknetMult = ns.getPlayer().mults.hacknet_node_money; 69 | // Get the lowest cache level, we do not consider upgrading the cache level of servers above this until all have the same cache level 70 | const minCacheLevel = [...Array(ns.hacknet.numNodes()).keys()].reduce((min, i) => Math.min(min, ns.hacknet.getNodeStats(i).cache), Number.MAX_VALUE); 71 | // Note: Formulas API has a hashGainRate which should agree with these calcs, but this way they're available even without the formulas API 72 | const upgrades = [{ name: "none", cost: 0 }, { 73 | name: "level", upgrade: ns.hacknet.upgradeLevel, cost: i => ns.hacknet.getLevelUpgradeCost(i, 1), nextValue: nodeStats => nodeStats.level + 1, 74 | addedProduction: nodeStats => nodeStats.production * ((nodeStats.level + 1) / nodeStats.level - 1) 75 | }, { 76 | name: "ram", upgrade: ns.hacknet.upgradeRam, cost: i => ns.hacknet.getRamUpgradeCost(i, 1), nextValue: nodeStats => nodeStats.ram * 2, 77 | addedProduction: nodeStats => nodeStats.production * 0.07 78 | }, { 79 | name: "cores", upgrade: ns.hacknet.upgradeCore, cost: i => ns.hacknet.getCoreUpgradeCost(i, 1), nextValue: nodeStats => nodeStats.cores + 1, 80 | addedProduction: nodeStats => nodeStats.production * ((nodeStats.cores + 5) / (nodeStats.cores + 4) - 1) 81 | }, { 82 | name: "cache", upgrade: ns.hacknet.upgradeCache, cost: i => ns.hacknet.getCacheUpgradeCost(i, 1), nextValue: nodeStats => nodeStats.cache + 1, 83 | addedProduction: nodeStats => nodeStats.cache > minCacheLevel || !haveHacknetServers ? 0 : nodeStats.production * 0.01 / nodeStats.cache // Note: Does not actually give production, but it has "worth" to us so we can buy more things 84 | }]; 85 | // Find the best upgrade we can make to an existing node 86 | let nodeToUpgrade = -1; 87 | let bestUpgrade; 88 | let bestUpgradePayoff = 0; // Hashes per second per dollar spent. Bigger is better. 89 | let cost = 0; 90 | let upgradedValue = 0; 91 | let worstNodeProduction = Number.MAX_VALUE; // Used to how productive a newly purchased node might be 92 | for (var i = 0; i < ns.hacknet.numNodes(); i++) { 93 | let nodeStats = ns.hacknet.getNodeStats(i); 94 | if (haveHacknetServers) { // When a hacknet server runs scripts, nodeStats.production lags behind what it should be for current ram usage. Get the "raw" rate 95 | try { nodeStats.production = ns.formulas.hacknetServers.hashGainRate(nodeStats.level, 0, nodeStats.ram, nodeStats.cores, currentHacknetMult); } 96 | catch { /* If we do not have the formulas API yet, we cannot account for this and must simply fall-back to using the production reported by the node */ } 97 | } 98 | worstNodeProduction = Math.min(worstNodeProduction, nodeStats.production); 99 | for (let up = 1; up < upgrades.length; up++) { 100 | let currentUpgradeCost = upgrades[up].cost(i); 101 | let payoff = upgrades[up].addedProduction(nodeStats) / currentUpgradeCost; // Production (Hashes per second) per dollar spent 102 | if (payoff > bestUpgradePayoff) { 103 | nodeToUpgrade = i; 104 | bestUpgrade = upgrades[up]; 105 | bestUpgradePayoff = payoff; 106 | cost = currentUpgradeCost; 107 | upgradedValue = upgrades[up].nextValue(nodeStats); 108 | } 109 | } 110 | } 111 | // Compare this to the cost of adding a new node. This is an imperfect science. We are paying to unlock the ability to buy all the same upgrades our 112 | // other nodes have - all of which have been deemed worthwhile. Not knowing the sum total that will have to be spent to reach that same production, 113 | // the "most optimistic" case is to treat "price" of all that production to be just the cost of this server, but this is **very** optimistic. 114 | // In practice, the cost of new hacknodes scales steeply enough that this should come close to being true (cost of server >> sum of cost of upgrades) 115 | let newNodeCost = ns.hacknet.getPurchaseNodeCost(); 116 | let newNodePayoff = ns.hacknet.numNodes() == ns.hacknet.maxNumNodes() ? 0 : worstNodeProduction / newNodeCost; 117 | let shouldBuyNewNode = newNodePayoff > bestUpgradePayoff; 118 | if (newNodePayoff == 0 && bestUpgradePayoff == 0) { 119 | setStatus(ns, `All upgrades have no value (is hashNet income disabled in this BN?)`); 120 | return false; // As long as maxSpend doesn't change, we will never purchase another upgrade 121 | } 122 | // If specified, only buy upgrades that will pay for themselves in {payoffTimeSeconds}. 123 | const hashDollarValue = haveHacknetServers ? 2.5e5 : 1; // Dollar value of one hash-per-second (0.25m dollars per production). 124 | let payoffTimeSeconds = 1 / (hashDollarValue * (shouldBuyNewNode ? newNodePayoff : bestUpgradePayoff)); 125 | if (shouldBuyNewNode) cost = newNodeCost; 126 | 127 | // Prepare info about the next uprade. Whether we end up purchasing or not, we will display this info. 128 | let strPurchase = (shouldBuyNewNode ? `a new node "hacknet-node-${ns.hacknet.numNodes()}"` : 129 | `hacknet-node-${nodeToUpgrade} ${bestUpgrade.name} ${upgradedValue}`) + ` for ${formatMoney(cost)}`; 130 | let strPayoff = `production ${((shouldBuyNewNode ? newNodePayoff : bestUpgradePayoff) * cost).toPrecision(3)} payoff time: ${formatDuration(1000 * payoffTimeSeconds)}` 131 | if (cost > maxSpend) { 132 | setStatus(ns, `The next best purchase would be ${strPurchase}, but the cost exceeds the spending limit (${formatMoney(maxSpend)})`); 133 | return false; // Shut-down. As long as maxSpend doesn't change, we will never purchase another upgrade 134 | } 135 | if (payoffTimeSeconds > maxPayoffTimeSeconds) { 136 | setStatus(ns, `The next best purchase would be ${strPurchase}, but the ${strPayoff} is worse than the limit (${formatDuration(1000 * maxPayoffTimeSeconds)})`); 137 | return false; // Shut-down. As long as maxPayoffTimeSeconds doesn't change, we will never purchase another upgrade 138 | } 139 | const reserve = (options['reserve'] != null ? options['reserve'] : Number(ns.read("reserve.txt") || 0)); 140 | const playerMoney = ns.getPlayer().money; 141 | if (cost > playerMoney - reserve) { 142 | setStatus(ns, `The next best purchase would be ${strPurchase}, but the cost exceeds the our ` + 143 | `current available funds` + (reserve == 0 ? '.' : ` (after reserving ${formatMoney(reserve)}).`)); 144 | return 0; // 145 | } 146 | let success = shouldBuyNewNode ? ns.hacknet.purchaseNode() !== -1 : bestUpgrade.upgrade(nodeToUpgrade, 1); 147 | if (success && options.toast) ns.toast(`Purchased ${strPurchase}`, 'success'); 148 | setStatus(ns, success ? `Purchased ${strPurchase} with ${strPayoff}` : `Insufficient funds to purchase the next best upgrade: ${strPurchase}`); 149 | return success ? cost : 0; 150 | } -------------------------------------------------------------------------------- /host-manager.js: -------------------------------------------------------------------------------- 1 | import { log, getConfiguration, instanceCount, getNsDataThroughFile, formatMoney, formatRam, formatDuration } from './helpers.js' 2 | 3 | // The purpose of the host manager is to buy the best servers it can 4 | // until it thinks RAM is underutilized enough that you don't need to anymore. 5 | 6 | const purchasedServerName = "daemon"; // The name to give all purchased servers. Also used to determine which servers were purchased 7 | let maxPurchasableServerRamExponent; // The max server ram you can buy as an exponent (power of 2). Typically 1 petabyte (2^20), but less in some BNs 8 | let maxPurchasedServers; // The max number of servers you can have in your farm. Typically 25, but can be less in some BNs 9 | let costByRamExponent = {}; // A dictionary of how much each server size costs, prepped in advance. 10 | 11 | // The following globals are set via command line arguments specified below, along with their defaults 12 | let keepRunning = false; 13 | let minRamExponent; 14 | let absReservedMoney; 15 | let pctReservedMoney; 16 | let budget; 17 | 18 | let options; 19 | const argsSchema = [ 20 | ['c', false], // Set to true to run continuously 21 | ['run-continuously', false], // Long-form alias for above flag 22 | ['interval', 10000], // Update interval (in milliseconds) when running continuously 23 | ['min-ram-exponent', 5], // the minimum amount of ram to purchase 24 | ['utilization-trigger', 0.80], // the percentage utilization that will trigger an attempted purchase 25 | ['absolute-reserve', null], // Set to reserve a fixed amount of money. Defaults to the contents of reserve.txt on home 26 | ['reserve-percent', 0.9], // Set to reserve a percentage of home money 27 | ['reserve-by-time', false], // Experimental exponential decay by time in the run. Starts willing to spend lots of money, falls off over time. 28 | ['reserve-by-time-decay-factor', 0.2], // Controls how quickly our % reserve increases from --reserve-percent to 100% over time. For example, if --reserve-percent is set to 0.05 (allow spending 95% of money), time to reduce spending to ~25% of money is ~6hrs at 0.2, ~4hrs at 0.3, ~2hrs at 0.5 29 | ['budget', Number.POSITIVE_INFINITY], // Yet another way to control spending, this budget will not be exceeded, regardless of player owned money. Reserves are still respected. 30 | ['allow-worse-purchases', false], // Set to true to allow purchase of servers worse than our current best purchased server 31 | ['compare-to-home-threshold', 0.25], // Do not bother buying servers unless they are at least this big compared to current home RAM 32 | ['compare-to-network-ram-threshold', 0.02], // Do not bother buying servers unless they are at least this big compared to total network RAM 33 | ]; 34 | 35 | export function autocomplete(data, _) { 36 | data.flags(argsSchema); 37 | return []; 38 | } 39 | 40 | /** Note: In addition to this script's RAM footprint (2.7 GB last this was updated) 41 | * This script requires 3.85 GB of DYNAMIC RAM (2.25 GB (for purchaseServer) + 1.6 GB Base Cost) used by getNsDataThroughFile) 42 | * @param {NS} ns **/ 43 | export async function main(ns) { 44 | const runOptions = getConfiguration(ns, argsSchema); 45 | if (!runOptions || await instanceCount(ns) > 1) return; // Prevent multiple instances of this script from being started, even with different args. 46 | options = runOptions; // We don't set the global "options" until we're sure this is the only running instance 47 | ns.disableLog('ALL') 48 | 49 | // Get the maximum number of purchased servers in this bitnode 50 | maxPurchasedServers = await getNsDataThroughFile(ns, 'ns.getPurchasedServerLimit()'); 51 | log(ns, `INFO: Max purchasable servers has been detected as ${maxPurchasedServers.toFixed(0)}.`); 52 | if (maxPurchasedServers == 0) 53 | return log(ns, `INFO: Shutting down due to host purchasing being disabled in this BN...`); 54 | 55 | // Get the maximum size of purchased servers in this bitnode 56 | const purchasedServerMaxRam = await getNsDataThroughFile(ns, 'ns.getPurchasedServerMaxRam()'); 57 | maxPurchasableServerRamExponent = Math.log2(purchasedServerMaxRam); 58 | log(ns, `INFO: Max purchasable RAM has been detected as 2^${maxPurchasableServerRamExponent} (${formatRam(2 ** maxPurchasableServerRamExponent)}).`); 59 | 60 | // Gather one-time info in advance about how much RAM each size of server costs (Up to 2^30 to be future-proof, but we expect everything abouve 2^20 to be Infinity) 61 | costByRamExponent = await getNsDataThroughFile(ns, 'Object.fromEntries([...Array(30).keys()].map(i => [i, ns.getPurchasedServerCost(2**i)]))', '/Temp/host-costs.txt'); 62 | 63 | keepRunning = options.c || options['run-continuously']; 64 | pctReservedMoney = options['reserve-percent']; 65 | minRamExponent = options['min-ram-exponent']; 66 | budget = options['budget']; 67 | // Log the command line options, for new users who don't know why certain decisions are/aren't being made 68 | if (minRamExponent > maxPurchasableServerRamExponent) { 69 | log(ns, `WARN: --min-ram-exponent was set to ${minRamExponent} (${formatRam(2 ** minRamExponent)}), ` + 70 | `but the maximum server RAM in this BN is ${maxPurchasableServerRamExponent} (${formatRam(2 ** maxPurchasableServerRamExponent)}), ` + 71 | `so the minimum has been lowered accordingly.`); 72 | minRamExponent = maxPurchasableServerRamExponent; 73 | } else 74 | log(ns, `INFO: --min-ram-exponent is set to ${minRamExponent}: New servers will only be purchased ` + 75 | `if we can afford 2^${minRamExponent} (${formatRam(2 ** minRamExponent)}) or more in size.`); 76 | log(ns, `INFO: --compare-to-home-threshold is set to ${options['compare-to-home-threshold'] * 100}%: ` + 77 | `New servers are deemed "not worthwhile" unless they are at least this big compared to your home server.`); 78 | log(ns, `INFO: --compare-to-network-ram-threshold is set to ${options['compare-to-network-ram-threshold'] * 100}%: ` + 79 | `New servers are deemed "not worthwhile" unless they are this big compared to total ram on the entire network.`); 80 | log(ns, `INFO: --utilization-trigger is set to ${options['utilization-trigger'] * 100}%: ` + 81 | `New servers will only be purchased when more than this much RAM is in use across the entire network.`); 82 | if (options['reserve-by-time']) 83 | log(ns, `INFO: --reserve-by-time is active! This community-contributed option will spend more of your money on servers early on, and less later on.`); 84 | else 85 | log(ns, `INFO: --reserve-percent is set to ${pctReservedMoney * 100}%: ` + 86 | `This means we will spend no more than ${((1 - pctReservedMoney) * 100).toFixed(1)}% of current Money on a new server.`); 87 | // Start the main loop (or run once) 88 | if (!keepRunning) 89 | log(ns, `host-manager will run once. Run with argument "-c" to run continuously.`) 90 | do { 91 | absReservedMoney = options['absolute-reserve'] != null ? options['absolute-reserve'] : Number(ns.read("reserve.txt") || 0); 92 | await tryToBuyBestServerPossible(ns); 93 | if (keepRunning) 94 | await ns.sleep(options['interval']); 95 | } while (keepRunning); 96 | } 97 | 98 | // Logging system to only print a log if it is different from the last log printed. 99 | let lastStatus = ""; 100 | function setStatus(ns, logMessage) { 101 | return logMessage != lastStatus ? ns.print(lastStatus = logMessage) : false; 102 | } 103 | 104 | /** @param {NS} ns 105 | * Attempts to buy a server at or better than your home machine. **/ 106 | async function tryToBuyBestServerPossible(ns) { 107 | // Scan the set of all servers on the network that we own (or rooted) to get a sense of current RAM utilization 108 | let rootedServers = await getNsDataThroughFile(ns, 'scanAllServers(ns).filter(s => ns.hasRootAccess(s))', '/Temp/rooted-servers.txt'); 109 | // Gether the list of all purchased servers. 110 | let purchasedServers = null; 111 | try { purchasedServers = await getNsDataThroughFile(ns, 'ns.getPurchasedServers()', null, null, null, 3, 5, /* silent errors */ true); } catch { /* Ignore */ } 112 | if (purchasedServers == null) // Early game, if we have insufficient RAM (2.25 GB getPurchasedServers + 1.6 base cost), we can fall-back to guessing based on their name 113 | purchasedServers = rootedServers.filter(s => s.startsWith(purchasedServerName)); 114 | // If some of the servers are hacknet servers, and they aren't being used for scripts, ignore the RAM they have available 115 | // with the assumption that these are reserved for generating hashes 116 | const likelyHacknet = rootedServers.filter(s => s.startsWith("hacknet-node-") || s.startsWith('hacknet-server-')); 117 | if (likelyHacknet.length > 0) { 118 | const totalHacknetUsedRam = likelyHacknet.reduce((t, s) => t + ns.getServerUsedRam(s), 0); 119 | if (totalHacknetUsedRam == 0) { 120 | rootedServers = rootedServers.filter(s => !likelyHacknet.includes(s)); 121 | log(ns, `Removing ${likelyHacknet.length} hacknet servers from RAM statistics since they are not being utilized.`) 122 | } else if (!keepRunning) 123 | log(ns, `We are currently using ${formatRam(totalHacknetUsedRam)} of hacknet RAM, so including hacknet in our utilization stats.`) 124 | } 125 | 126 | const totalMaxRam = rootedServers.reduce((t, s) => t + ns.getServerMaxRam(s), 0); 127 | const totalUsedRam = rootedServers.reduce((t, s) => t + ns.getServerUsedRam(s), 0); 128 | if (options['utilization-trigger'] > 0) { 129 | const utilizationRate = totalUsedRam / totalMaxRam; 130 | setStatus(ns, `Using ${Math.round(totalUsedRam).toLocaleString('en')}/${formatRam(totalMaxRam)} (` + 131 | `${(utilizationRate * 100).toFixed(1)}%) across ${rootedServers.length} servers ` + 132 | `(Triggers at ${options['utilization-trigger'] * 100}%, ${purchasedServers.length} bought so far)`); 133 | // If utilization is below target. We don't need another server. 134 | if (utilizationRate < options['utilization-trigger']) 135 | return; 136 | } 137 | 138 | 139 | // Check for other reasons not to go ahead with the purchase 140 | let prefix = 'Host-manager wants to buy another server, but '; 141 | 142 | // Determine our budget for spending money on home RAM 143 | let cashMoney = await getNsDataThroughFile(ns, `ns.getServerMoneyAvailable(ns.args[0])`, null, ["home"]); 144 | if (options['reserve-by-time']) { // Option to vary pctReservedMoney by time since augment. 145 | // Decay factor of 0.2 = Starts willing to spend 95% of our money, backing down to ~75% at 1 hour, ~60% at 2 hours, ~25% at 6 hours, and ~10% at 10 hours. 146 | // Decay factor of 0.3 = Starts willing to spend 95% of our money, backing down to ~66% at 1 hour, ~45% at 2 hours, ~23% at 4 hours, ~10% at 6 hours 147 | // Decay factor of 0.5 = Starts willing to spend 95% of our money, then halving every hour (to ~48% at 1 hour, ~24% at 2 hours, ~12% at 3 hours, etc) 148 | const timeSinceLastAug = Date.now() - (await getNsDataThroughFile(ns, 'ns.getResetInfo()')).lastAugReset; 149 | const t = timeSinceLastAug / (60 * 60 * 1000); // Time since last aug, in hours. 150 | const decayFactor = options['reserve-by-time-decay-factor']; 151 | pctReservedMoney = 1.0 - (1.0 - options['reserve-percent']) * Math.pow(1 - decayFactor, t); 152 | if (!keepRunning) 153 | log(ns, `After spending ${formatDuration(timeSinceLastAug)} in this augmentation, reserve % has grown from ` + 154 | `${(options['reserve-percent'] * 100).toFixed(1)}% to ${(pctReservedMoney * 100).toFixed(1)}%`); 155 | } 156 | 157 | let spendableMoney = Math.min(budget, cashMoney * (1.0 - pctReservedMoney), cashMoney - absReservedMoney); 158 | if (spendableMoney <= 0.01) { 159 | if (!keepRunning) // Show a more detailed log if we aren't running continuously 160 | 161 | return setStatus(ns, `${prefix}all player cash (${formatMoney(cashMoney)}) is currently reserved (budget: ${formatMoney(budget)}, ` + 162 | `abs reserve: ${formatMoney(absReservedMoney)}, ` + 163 | `% reserve: ${(pctReservedMoney * 100).toFixed(1)}% (${formatMoney(cashMoney * (1.0 - pctReservedMoney))}))`); 164 | // Otherwise the "status" log we show should remain relatively consistent so we aren't spamming e.g. changes in player money in --continuous mode 165 | return setStatus(ns, `${prefix}all player cash is currently reserved (budget: ${formatMoney(budget)}, ` + 166 | `abs reserve: ${formatMoney(absReservedMoney)}, % reserve: ${(pctReservedMoney * 100).toFixed(1)}%)`); 167 | } 168 | 169 | // Determine the most ram we can buy with our current money 170 | let exponentLevel = 1; 171 | for (; exponentLevel < maxPurchasableServerRamExponent; exponentLevel++) 172 | if (costByRamExponent[exponentLevel + 1] > spendableMoney) 173 | break; 174 | let cost = costByRamExponent[exponentLevel]; 175 | let maxRamPossibleToBuy = Math.pow(2, exponentLevel); 176 | 177 | // Don't buy if it would put us below our reserve (shouldn't happen, since we calculated how much to buy based on reserve amount) 178 | if (spendableMoney < cost) 179 | return setStatus(ns, `${prefix}spendableMoney (${formatMoney(spendableMoney)}) is less than the cost ` + 180 | `of even the cheapest server (${formatMoney(cost)} for ${formatRam(2 ** exponentLevel)})`); 181 | // Don't buy if we can't afford our configured --min-ram-exponent 182 | if (exponentLevel < minRamExponent) 183 | return setStatus(ns, `${prefix}The highest ram exponent we can afford (2^${exponentLevel} for ${formatMoney(cost)}) on our budget ` + 184 | `of ${formatMoney(spendableMoney)} is less than the --min-ram-exponent (2^${minRamExponent} for ${formatMoney(costByRamExponent[minRamExponent])})`); 185 | // Under some conditions, we consider the new server "not worthwhile". but only if it isn't the biggest possible server we can buy 186 | if (exponentLevel < maxPurchasableServerRamExponent) { 187 | // Abort if our home server is more than x times bettter (rough guage of how much we 'need' Daemon RAM at the current stage of the game?) 188 | const homeThreshold = options['compare-to-home-threshold']; 189 | // Unless we're looking at buying the maximum purchasable server size - in which case we can do no better 190 | if (maxRamPossibleToBuy < ns.getServerMaxRam("home") * homeThreshold) 191 | return setStatus(ns, `${prefix}the most RAM we can buy (${formatRam(maxRamPossibleToBuy)}) on our budget of ${formatMoney(spendableMoney)} ` + 192 | `is less than --compare-to-home-threshold (${homeThreshold}) x home RAM (${formatRam(ns.getServerMaxRam("home"))})`); 193 | // Abort if purchasing this server wouldn't improve our total RAM by more than x% (ensures we buy in meaningful increments) 194 | const networkThreshold = options['compare-to-network-ram-threshold']; 195 | if (maxRamPossibleToBuy / totalMaxRam < networkThreshold) 196 | return setStatus(ns, `${prefix}the most RAM we can buy (${formatRam(maxRamPossibleToBuy)}) on our budget of ${formatMoney(spendableMoney)} ` + 197 | `is less than --compare-to-network-ram-threshold (${networkThreshold}) x total network RAM (${formatRam(totalMaxRam)})`); 198 | } 199 | 200 | // Collect information about other previoulsy purchased servers 201 | const maxPurchasableServerRam = Math.pow(2, maxPurchasableServerRamExponent); 202 | const ramByServer = Object.fromEntries(purchasedServers.map(server => [server, ns.getServerMaxRam(server)])); 203 | let [worstServerName, worstServerRam] = purchasedServers.reduce(([minS, minR], s) => 204 | ramByServer[s] < minR ? [s, ramByServer[s]] : [minS, minR], [null, maxPurchasableServerRam]); 205 | let [bestServerName, bestServerRam] = purchasedServers.reduce(([maxS, maxR], s) => 206 | ramByServer[s] > maxR ? [s, ramByServer[s]] : [maxS, maxR], [null, 0]); 207 | 208 | // Abort if our worst previously-purchased server is better than the one we're looking to buy (ensures we buy in sane increments of capacity) 209 | if (worstServerName != null && maxRamPossibleToBuy < worstServerRam && !options['allow-worse-purchases']) 210 | return setStatus(ns, `${prefix}the most RAM we can buy (${formatRam(maxRamPossibleToBuy)}) on our budget of ` + 211 | `${formatMoney(spendableMoney)} is less than our worst purchased server ${worstServerName}'s RAM ${formatRam(worstServerRam)}`); 212 | // Only buy new servers as good as or better than our best bought server (anything less is deemed a regression in value) 213 | if (bestServerRam != null && maxRamPossibleToBuy < bestServerRam && !options['allow-worse-purchases']) 214 | return setStatus(ns, `${prefix}the most RAM we can buy (${formatRam(maxRamPossibleToBuy)}) on our budget of ` + 215 | `${formatMoney(spendableMoney)} is less than our previously purchased server ${bestServerName} RAM ${formatRam(bestServerRam)}`); 216 | 217 | let purchasedServer, 218 | isUpgrade = false 219 | // if we're at capacity, check to see if we can improve the current worst purchased server. If so, upgrade it. 220 | if (purchasedServers.length >= maxPurchasedServers) { 221 | if (worstServerRam == maxPurchasableServerRam) { 222 | keepRunning = false; 223 | return setStatus(ns, `INFO: We are at the max number of servers ${maxPurchasedServers}, ` + 224 | `and all have the maximum possible RAM (${formatRam(maxPurchasableServerRam)}).`); 225 | } 226 | 227 | cost -= costByRamExponent[Math.log2(worstServerRam)] 228 | isUpgrade = true 229 | purchasedServer = (await getNsDataThroughFile(ns, `ns.upgradePurchasedServer(ns.args[0], ns.args[1])`, null, 230 | [worstServerName, maxRamPossibleToBuy])) ? worstServerName : ""; 231 | } else { 232 | purchasedServer = await getNsDataThroughFile(ns, `ns.purchaseServer(ns.args[0], ns.args[1])`, null, 233 | [purchasedServerName, maxRamPossibleToBuy]); 234 | } 235 | if (!purchasedServer) 236 | setStatus(ns, `${prefix}Could not ${isUpgrade ? 'upgrade' : 'purchase'} a server with ${formatRam(maxRamPossibleToBuy)} ` + 237 | `RAM for ${formatMoney(cost)} with a budget of ${formatMoney(spendableMoney)}.`); 238 | else { 239 | log(ns, `SUCCESS: ${isUpgrade ? 'Upgraded' : 'Purchased'} server ${purchasedServer} with ${formatRam(maxRamPossibleToBuy)} ` + 240 | `RAM for ${formatMoney(cost)} (budget was ${formatMoney(spendableMoney)})`, true, 'success'); 241 | budget -= cost; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /kill-all-scripts.js: -------------------------------------------------------------------------------- 1 | import { scanAllServers } from './helpers.js' 2 | 3 | // the purpose of cascade kill is to kill all scripts running on any server in the game 4 | // but saving the host that you run it on for last (so that it doesn't kill itself prematurely) 5 | /** @param {NS} ns **/ 6 | export async function main(ns) { 7 | var startingNode = ns.getHostname(); 8 | const serverList = scanAllServers(ns); 9 | 10 | // Send the kill command to all servers 11 | for (const server of serverList) { 12 | // skip if this host, we save it for last 13 | if (server == startingNode) 14 | continue; 15 | 16 | // skip if not running anything 17 | if (ns.ps(server) === 0) 18 | continue; 19 | 20 | // kill all scripts 21 | ns.killall(server); 22 | } 23 | 24 | // idle for things to die 25 | for (const server of serverList) { 26 | // skip if this host, we save it for last 27 | if (server == startingNode) 28 | continue; 29 | // idle until they're dead, this is to avoid killing the cascade before it's finished. 30 | while (ns.ps(server) > 0) { 31 | await ns.sleep(20); 32 | } 33 | // Remove script files the daemon would have copied over (in case we update the source) 34 | for (let file of ns.ls(server, '.js')) 35 | ns.rm(file, server) 36 | } 37 | 38 | // wait to kill these. This kills itself, obviously. 39 | ns.killall(startingNode); 40 | } -------------------------------------------------------------------------------- /optimize-stanek.js: -------------------------------------------------------------------------------- 1 | const FragmentType = { 2 | HackingChance: 2, 3 | HackingSpeed: 3, 4 | HackingMoney: 4, 5 | HackingGrow: 5, 6 | Hacking: 6, 7 | Strength: 7, 8 | Defense: 8, 9 | Dexterity: 9, 10 | Agility: 10, 11 | Charisma: 11, 12 | HacknetMoney: 12, 13 | HacknetCost: 13, 14 | Rep: 14, 15 | WorkMoney: 15, 16 | Crime: 16, 17 | Bladeburner: 17, 18 | Booster: 18, 19 | } 20 | 21 | const FragmentId = { 22 | Hacking1: 0, 23 | Hacking2: 1, 24 | HackingSpeed: 5, 25 | HackingMoney: 6, 26 | HackingGrow: 7, 27 | Strength: 10, 28 | Defense: 12, 29 | Dexterity: 14, 30 | Agility: 16, 31 | Charisma: 18, 32 | HacknetMoney: 20, 33 | HacknetCost: 21, 34 | Rep: 25, 35 | WorkMoney: 27, 36 | Crime: 28, 37 | Bladeburner: 30, 38 | //Booster1: 100, 39 | //Booster2: 101, 40 | //Booster3: 102, 41 | //Booster4: 103, 42 | //Booster5: 104, 43 | //Booster6: 105, 44 | //Booster7: 106, 45 | //Booster8: 107, 46 | }; 47 | 48 | let planStatsCount = 0; 49 | let planBoostersCount = 0; 50 | 51 | /** @typedef {{ key: number, fragment: Fragment, x: number; y: number; rot: number; 52 | * coords: [number, number][]; adjacent: [number, number][]; 53 | * adjacentBoosters: Int16Array; adjacentStats: Int16Array; 54 | * overlapWithBoosters: Int16Array; overlapWithStats: Int16Array }} Placement */ 55 | /** @typedef {{ stats: Placement[]; boosters: Placement[] }} Plan */ 56 | 57 | export function autocomplete(data, args) { 58 | return [...Object.keys(FragmentType)]; 59 | } 60 | 61 | /** @param {NS} ns */ 62 | export async function main(ns) { 63 | /* 64 | if (ns.args.length == 0) { 65 | tlog(ns, "ERROR", "At least one fragment type required"); 66 | return; 67 | } 68 | if (!ns.args.every(arg => Object.keys(FragmentType).includes(arg))) { 69 | tlog("ERROR", "Invalid fragment type(s): %s", 70 | ns.args.filter(arg => !Object.keys(FragmentType).includes(arg))); 71 | return; 72 | }*/ 73 | 74 | // 1. Set up priority order of stat fragments to include 75 | const targetIds = [ 76 | FragmentId.Rep, FragmentId.Hacking2, FragmentId.Hacking1, // Basics, always want 77 | FragmentId.HackingSpeed, // Priority 2, improve hack EXP gain and income? 78 | FragmentId.HacknetMoney, FragmentId.HacknetCost, // Priority 3, hacknet good for lots of things? 79 | FragmentId.HackingGrow, FragmentId.HackingMoney // Priority 4, improves growth, income for RAM from hacking? 80 | //etc... 81 | ]; 82 | const allFragments = ns.stanek.fragmentDefinitions(); 83 | const statFrags = allFragments.filter(frag => targetIds.includes(frag.id)); 84 | const boosterFrags = allFragments.filter(frag => frag.type == FragmentType.Booster); 85 | 86 | // 2. Pick dimensions (why not pick many!) 87 | //const height = 6; //ns.stanek.giftHeight() 88 | //const width = 6; //ns.stanek.giftWidth(); // NOTE: Width is always the same, or one more than height. 89 | for (let height = 3; height <= 5; height++) 90 | for (let width = height; width <= height + 1; width++) { 91 | const [score, plan] = await planFragments(ns, width, height, statFrags, boosterFrags); 92 | ns.tprint(score); 93 | const strFragments = []; 94 | // Output the layout so you can stick it in a database 95 | for (const elem of [...plan.stats, ...plan.boosters]) 96 | strFragments.push(`{"id":${elem.fragment.id},"x":${elem.x},"y":${elem.y},"rotation":${elem.rot}}`); 97 | ns.tprint(`\n{"height": ${height}, "width": ${width}, "fragments": [\n ${strFragments.join(",\n ")}\n]}`); 98 | } 99 | } 100 | 101 | /** @param {NS} ns */ 102 | function tlog(ns, prefix, format, ...args) { 103 | ns.tprintf(prefix + ": " + format, ...args); 104 | } 105 | 106 | 107 | /** @param {NS} ns 108 | * @param {number} width 109 | * @param {number} height 110 | * @param {Fragment[]} statFrags 111 | * @param {Fragment[]} boosterFrags */ 112 | async function planFragments(ns, width, height, statFrags, boosterFrags) { 113 | const t0 = performance.now(); 114 | /** @type {Placement[]} */ 115 | const placements = []; 116 | /** @type {Placement[]} */ 117 | const statPlacements = []; 118 | /** @type {Placement[]} */ 119 | const boosterPlacements = []; 120 | /** @type {Map} */ 121 | const statFragsPlacements = new Map(statFrags.map(frag => [frag.id, []])); 122 | /** @type {Map} */ 123 | const boosterFragsPlacements = new Map(boosterFrags.map(frag => [frag.id, []])); 124 | /** @type {number[][][]} */ 125 | //const overlapping = [...new Array(width)].map(() => [...new Array(height)].map(() => [])); 126 | 127 | let statSeqn = 0, boosterSeqn = 0; 128 | for (let x = 0; x < width; x++) { 129 | await ns.sleep(0); // Don't hang the game 130 | for (let y = 0; y < height; y++) { 131 | for (let rot = 0; rot < 4; rot++) { 132 | for (const frag of [...statFrags, ...boosterFrags]) { 133 | const coords = coverage(x, y, rot, frag) 134 | if (coords.every(([x, y]) => x < width && y < height)) { 135 | const key = frag.type == FragmentType.Booster ? boosterSeqn++ : statSeqn++; //`${frag.id}@${x}-${y}-${rot}`; 136 | const placement = { 137 | key, fragment: frag, x, y, rot, 138 | coords, adjacent: adjacents(width, height, coords) 139 | }; 140 | 141 | placements.push(placement); 142 | if (frag.type == FragmentType.Booster) { 143 | boosterPlacements[key] = placement; 144 | boosterFragsPlacements.get(frag.id).push(placement); 145 | } 146 | else { 147 | statPlacements[key] = placement; 148 | statFragsPlacements.get(frag.id).push(placement); 149 | } 150 | 151 | //coords.forEach(([x, y]) => overlapping[x][y].push(key)); 152 | } 153 | } 154 | } 155 | } 156 | } 157 | ns.tprint(`Placements: ${placements.length}`) 158 | 159 | // Canonise coordinate arrays so we can use equality comparisons on them 160 | const canonicalCoords = [...new Array(width)].map((_, x) => [...new Array(height)].map((_, y) => [x, y])); 161 | for (const placement of placements) { 162 | placement.coords = placement.coords.map(([x, y]) => canonicalCoords[x][y]); 163 | placement.adjacent = placement.adjacent.map(([x, y]) => canonicalCoords[x][y]); 164 | } 165 | 166 | // Pre-compute all adjacencies 167 | for (const placement of placements) { 168 | placement.adjacentBoosters = []; 169 | placement.adjacentStats = []; 170 | placement.overlapWithBoosters = []; 171 | placement.overlapWithStats = []; 172 | for (const other of boosterPlacements) { 173 | if (placement.coords.some(coord => other.adjacent.includes(coord))) 174 | placement.adjacentBoosters.push(other.key); 175 | if (placement.coords.some(coord => other.coords.includes(coord))) { 176 | placement.overlapWithBoosters.push(other.key); 177 | } 178 | } 179 | for (const other of statPlacements) { 180 | if (placement.coords.some(coord => other.adjacent.includes(coord))) 181 | placement.adjacentStats.push(other.key); 182 | if (placement.coords.some(coord => other.coords.includes(coord))) { 183 | placement.overlapWithStats.push(other.key); 184 | } 185 | } 186 | } 187 | 188 | // Turn arrays to fixed type, now that we know their contents 189 | for (const placement of placements) { 190 | placement.adjacentBoosters = Int16Array.from(placement.adjacentBoosters); 191 | placement.adjacentStats = Int16Array.from(placement.adjacentStats); 192 | placement.overlapWithBoosters = Int16Array.from(placement.overlapWithBoosters); 193 | placement.overlapWithStats = Int16Array.from(placement.overlapWithStats); 194 | } 195 | 196 | // Exclude rotational symmetries from search by only using 197 | // - rot 0 placements if the board is square 198 | // - rot 0 and rot 1 placements if the board is non-square 199 | // of the first fragment 200 | // Select the stat fragment with most potential placements as the first fragment, 201 | // to get the biggest reduction of search space 202 | const statFragsKeys = [...statFrags] 203 | //.sort((a, b) => statFragsPlacements.get(b.id).length - statFragsPlacements.get(a.id).length) 204 | .map(frag => statFragsPlacements.get(frag.id).map(placement => placement.key)); 205 | statFragsKeys[0] = statFragsKeys[0].filter(key => 206 | width == height ? statPlacements[key].rot == 0 : (statPlacements[key].rot == 0 || statPlacements[key].rot == 1)); 207 | 208 | /// Compute stat fragment layout that maximises potential stat-booster fragment adjacencies 209 | const blockedStats0 = new Uint8Array(statPlacements.length); 210 | const blockedBoosters0 = new Uint8Array(boosterPlacements.length); 211 | const boosterStatAdjacencies0 = new Uint8Array(boosterPlacements.length); 212 | const plan0 = { stats: [], boosters: [] }; 213 | const bestResult0 = [-Infinity, { stats: [...plan0.stats], boosters: [...plan0.boosters] }]; 214 | 215 | planStatsCount = 0; 216 | planBoostersCount = 0; 217 | const t1 = performance.now(); 218 | const [score, plan] = await planStats(ns, statPlacements, boosterPlacements, statFragsKeys, 219 | blockedStats0, plan0, bestResult0, blockedBoosters0, boosterStatAdjacencies0); 220 | const t2 = performance.now(); 221 | 222 | tlog(ns, "DEBUG", "Computed Stanek plan. Prep work %.3fmsec, layout search %.3fmsec, %d planStats calls, %d planBoosters calls", 223 | t1 - t0, t2 - t1, planStatsCount, planBoostersCount); 224 | 225 | return [score, plan]; 226 | } 227 | 228 | /** @param {NS} ns 229 | * @param {Placement[]} statPlacements 230 | * @param {Placement[]} boosterPlacements 231 | * @param {number[][]} statFragsKeys - the remaining desired fragment ids to be placed on the board 232 | * @param {Uint8Array} blockedStats 233 | * @param {Plan} plan 234 | * @param {[number, Plan]} bestResult 235 | * @param {Uint8Array} blockedBoosters 236 | * @param {Uint8Array} boosterStatAdjacencies 237 | * @return {[number, Plan, Uint8Array, Uint8Array]} */ 238 | async function planStats(ns, statPlacements, boosterPlacements, statFragsKeys, blockedStats, plan, bestResult, blockedBoosters, boosterStatAdjacencies) { 239 | planStatsCount++; 240 | if (planStatsCount % 100000 == 0) 241 | await ns.sleep(0); // Don't hang the game 242 | 243 | let [currentBestScore, _] = bestResult; 244 | currentBestScore = currentBestScore || 0; 245 | 246 | // If at least one fragment has been placed, see what the best score is we can get by adding boosters 247 | if (plan.stats.length > 0) { 248 | // Mark boosters that are not blocked, but also not adjacent to a stat fragment as unavailable 249 | // and count the remaining available boosters 250 | let availableBoostersCount = 0; 251 | for (let i = 0; i < blockedBoosters.length; i++) { 252 | if (boosterStatAdjacencies[i] === 0) // No adjacent stat fragments => block 253 | blockedBoosters[i]++; 254 | else if (blockedBoosters[i] === 0) // Has adjacent stat fragments, and not blocked 255 | availableBoostersCount++; 256 | } 257 | 258 | const [boosterScore, boosterPlan] = planBoosters(plan, boosterPlacements, boosterStatAdjacencies, 259 | blockedBoosters, availableBoostersCount, 0, bestResult); 260 | 261 | // Undo changes 262 | for (let i = 0; i < blockedBoosters.length; i++) 263 | if (boosterStatAdjacencies[i] === 0) 264 | blockedBoosters[i]--; 265 | 266 | if (boosterScore || 0 > currentBestScore) { 267 | bestResult = [boosterScore, boosterPlan]; 268 | currentBestScore = boosterScore; 269 | } 270 | } 271 | // If there are fragments left to place, recurse to see if we can improve the score by placing more 272 | if (statFragsKeys.length > 0) { 273 | for (const key of statFragsKeys[0]) { 274 | if (blockedStats[key] !== 0) continue; 275 | const placement = statPlacements[key]; 276 | const adjacentBoosters = placement.adjacentBoosters; 277 | const overlapWithBoosters = placement.overlapWithBoosters; 278 | const overlapWithStats = placement.overlapWithStats; 279 | 280 | // Add the fragment placement to plan and update usability in-place to account for the new blocks 281 | plan.stats.push(placement); 282 | for (let i = 0; i < overlapWithStats.length; i++) 283 | blockedStats[overlapWithStats[i]]++; 284 | for (let i = 0; i < overlapWithBoosters.length; i++) 285 | blockedBoosters[overlapWithBoosters[i]]++; 286 | for (let i = 0; i < adjacentBoosters.length; i++) 287 | boosterStatAdjacencies[adjacentBoosters[i]]++; 288 | 289 | // Find and score best plan that includes this fragment placement 290 | const [bestPlanScore, bestPlan] = await planStats(ns, statPlacements, boosterPlacements, statFragsKeys.slice(1), 291 | blockedStats, plan, bestResult, blockedBoosters, boosterStatAdjacencies); 292 | if (bestPlanScore || 0 > currentBestScore) 293 | bestResult = [bestPlanScore, bestPlan]; 294 | 295 | // Undo the changes 296 | plan.stats.pop(); 297 | for (let i = 0; i < overlapWithStats.length; i++) 298 | blockedStats[overlapWithStats[i]]--; 299 | for (let i = 0; i < overlapWithBoosters.length; i++) 300 | blockedBoosters[overlapWithBoosters[i]]--; 301 | for (let i = 0; i < adjacentBoosters.length; i++) 302 | boosterStatAdjacencies[adjacentBoosters[i]]--; 303 | } 304 | } 305 | 306 | return bestResult; 307 | } 308 | 309 | /** @param {Plan} plan 310 | * @param {Placement[]} boosterPlacements 311 | * @param {Uint8Array} boosterStatAdjacencies 312 | * @param {Uint8Array} blockedBoosters 313 | * @param {number} availableCount 314 | * @param {number} startIdx 315 | * @param {[number, Plan]} bestResult 316 | * @return {[number, Plan]} */ 317 | function planBoosters(plan, boosterPlacements, boosterStatAdjacencies, blockedBoosters, availableCount, startIdx, bestResult) { 318 | planBoostersCount++; 319 | if (availableCount == 0) { 320 | const { stats, boosters } = plan; 321 | 322 | let score = 0; 323 | for (let i = 0; i < boosters.length; i++) 324 | score += boosterStatAdjacencies[boosters[i].key]; 325 | score = stats.length * (1 + 0.1 * score); // piecesPlaced*(1+0.1*numAdjacencies) 326 | 327 | if (score > bestResult[0]) 328 | return [score, { stats: [...stats], boosters: [...boosters] }]; // Clone plan 329 | else 330 | return bestResult; 331 | } 332 | 333 | for (let i = startIdx; i < blockedBoosters.length; i++) { 334 | if (blockedBoosters[i] !== 0) continue; 335 | const placement = boosterPlacements[i]; 336 | const overlapWithBoosters = placement.overlapWithBoosters; 337 | 338 | // Add the fragment placement to plan and update usability in-place to account for the new blocks 339 | plan.boosters.push(placement); 340 | for (let j = 0; j < overlapWithBoosters.length; j++) 341 | if ((blockedBoosters[overlapWithBoosters[j]]++) === 0) availableCount--; // Placement became blocked? 342 | 343 | // Find and score best plan that includes this fragment placement 344 | bestResult = planBoosters(plan, boosterPlacements, boosterStatAdjacencies, blockedBoosters, availableCount, i + 1, bestResult); 345 | 346 | // Undo the changes 347 | plan.boosters.pop(); 348 | for (let j = 0; j < overlapWithBoosters.length; j++) 349 | if ((--blockedBoosters[overlapWithBoosters[j]]) === 0) availableCount++; // Placement became free? 350 | } 351 | 352 | return bestResult; 353 | } 354 | 355 | /** @param {number} x0 356 | * @param {number} y0 357 | * @param {number} rotation 358 | * @param {Fragment} fragment 359 | * @return {[number, number][]} */ 360 | function coverage(x0, y0, rotation, fragment) { 361 | let shape = fragment.shape; 362 | for (let i = 0; i < rotation; i++) 363 | shape = shape[0].map((_, y) => shape.map((_, x) => shape[shape.length - 1 - x][y])); 364 | 365 | return shape.map((row, y) => row.map((filled, x) => filled ? [x0 + x, y0 + y] : undefined)) 366 | .flat() 367 | .filter(elem => elem != undefined); 368 | } 369 | 370 | /** @param {number} width 371 | * @param {number} height 372 | * @param {[number, number][]} coords 373 | * @return {[number, number][]} */ 374 | function adjacents(width, height, coords) { 375 | const adjacent = [...new Array(width)].map(() => [...new Array(height)].map(() => false)); 376 | // Mark grid squares adjacent to shape member squares 377 | for (const [x, y] of coords) { 378 | if (x - 1 >= 0) adjacent[x - 1][y] = true; 379 | if (x + 1 < width) adjacent[x + 1][y] = true; 380 | if (y - 1 >= 0) adjacent[x][y - 1] = true; 381 | if (y + 1 < height) adjacent[x][y + 1] = true; 382 | } 383 | // Strip out the shape squares themselves 384 | for (const [x, y] of coords) 385 | adjacent[x][y] = false; 386 | 387 | return adjacent.map((col, x) => col.map((is, y) => is ? [x, y] : undefined)) 388 | .flat() 389 | .filter(elem => elem != undefined); 390 | } 391 | -------------------------------------------------------------------------------- /optimize-stanek.js.og.js: -------------------------------------------------------------------------------- 1 | const FragmentType = { 2 | HackingChance: 2, 3 | HackingSpeed: 3, 4 | HackingMoney: 4, 5 | HackingGrow: 5, 6 | Hacking: 6, 7 | Strength: 7, 8 | Defense: 8, 9 | Dexterity: 9, 10 | Agility: 10, 11 | Charisma: 11, 12 | HacknetMoney: 12, 13 | HacknetCost: 13, 14 | Rep: 14, 15 | WorkMoney: 15, 16 | Crime: 16, 17 | Bladeburner: 17, 18 | Booster: 18, 19 | } 20 | 21 | const FragmentId = { 22 | Hacking1: 0, 23 | Hacking2: 1, 24 | HackingSpeed: 5, 25 | HackingMoney: 6, 26 | HackingGrow: 7, 27 | Strength: 10, 28 | Defense: 12, 29 | Dexterity: 14, 30 | Agility: 16, 31 | Charisma: 18, 32 | HacknetMoney: 20, 33 | HacknetCost: 21, 34 | Rep: 25, 35 | WorkMoney: 27, 36 | Crime: 28, 37 | Bladeburner: 30, 38 | //Booster1: 100, 39 | //Booster2: 101, 40 | //Booster3: 102, 41 | //Booster4: 103, 42 | //Booster5: 104, 43 | //Booster6: 105, 44 | //Booster7: 106, 45 | //Booster8: 107, 46 | }; 47 | 48 | let planStatsCount = 0; 49 | let planBoostersCount = 0; 50 | 51 | /** @typedef {{ key: number, fragment: Fragment, x: number; y: number; rot: number; 52 | * coords: [number, number][]; adjacent: [number, number][]; 53 | * adjacentBoosters: Int16Array; adjacentStats: Int16Array; 54 | * overlapWithBoosters: Int16Array; overlapWithStats: Int16Array }} Placement */ 55 | /** @typedef {{ stats: Placement[]; boosters: Placement[] }} Plan */ 56 | 57 | export function autocomplete(data, args) { 58 | return [...Object.keys(FragmentId)]; 59 | } 60 | 61 | /** @param {NS} ns */ 62 | export async function main(ns) { 63 | /* 64 | if (ns.args.length == 0) { 65 | tlog(ns, "ERROR", "At least one fragment type required"); 66 | return; 67 | } 68 | if (!ns.args.every(arg => Object.keys(FragmentType).includes(arg))) { 69 | tlog("ERROR", "Invalid fragment type(s): %s", 70 | ns.args.filter(arg => !Object.keys(FragmentType).includes(arg))); 71 | return; 72 | }*/ 73 | 74 | // 1. Set up priority order of stat fragments to include 75 | const targetIds = [FragmentId.Rep, FragmentId.Hacking2]; 76 | const allFragments = ns.stanek.fragmentDefinitions(); 77 | const statFrags = allFragments.filter(frag => targetIds.includes(frag.id)); 78 | const boosterFrags = allFragments.filter(frag => frag.type == FragmentType.Booster); 79 | 80 | // 2. Pick dimensions (why not pick many!) 81 | const height = 3; //ns.stanek.giftHeight() 82 | const width = 3; //ns.stanek.giftWidth(); // NOTE: Width is always the same, or one more than height. 83 | const [score, plan] = await planFragments(ns, width, height, statFrags, boosterFrags); 84 | ns.tprint(score); 85 | const strFragments = []; 86 | // Output the layout so you can stick it in a database 87 | for (const elem of [...plan.stats, ...plan.boosters]) 88 | strFragments.push(`{"id":${elem.fragment.id},"x":${elem.x},"y":${elem.y},"rotation":${elem.rot}}`); 89 | ns.tprint(`\n{"height": ${height}, "width": ${width}, "fragments": [\n ${strFragments.join(",\n ")}\n]}`); 90 | } 91 | 92 | /** @param {NS} ns */ 93 | function tlog(ns, prefix, format, ...args) { 94 | ns.tprintf(prefix + ": " + format, ...args); 95 | } 96 | 97 | /** @param {NS} ns 98 | * @param {number} width 99 | * @param {number} height 100 | * @param {Fragment[]} statFrags 101 | * @param {Fragment[]} boosterFrags */ 102 | async function planFragments(ns, width, height, statFrags, boosterFrags) { 103 | const t0 = performance.now(); 104 | /** @type {Placement[]} */ 105 | const placements = []; 106 | /** @type {Placement[]} */ 107 | const statPlacements = []; 108 | /** @type {Placement[]} */ 109 | const boosterPlacements = []; 110 | /** @type {Map} */ 111 | const statFragsPlacements = new Map(statFrags.map(frag => [frag.id, []])); 112 | /** @type {Map} */ 113 | const boosterFragsPlacements = new Map(boosterFrags.map(frag => [frag.id, []])); 114 | /** @type {number[][][]} */ 115 | //const overlapping = [...new Array(width)].map(() => [...new Array(height)].map(() => [])); 116 | 117 | let statSeqn = 0, boosterSeqn = 0; 118 | for (const frag of [...statFrags, ...boosterFrags]) { 119 | for (const { rot, mask } of rotations(frag)) { 120 | for (let x = 0; x < width; x++) { 121 | for (let y = 0; y < height; y++) { 122 | const coords = mask.map(([x0, y0]) => [x0 + x, y0 + y]); 123 | if (coords.every(([x, y]) => x < width && y < height)) { 124 | const key = frag.type == FragmentType.Booster ? boosterSeqn++ : statSeqn++; //`${frag.id}@${x}-${y}-${rot}`; 125 | const placement = { 126 | key, fragment: frag, x, y, rot, 127 | coords, adjacent: adjacents(width, height, coords) 128 | }; 129 | 130 | placements.push(placement); 131 | if (frag.type == FragmentType.Booster) { 132 | boosterPlacements[key] = placement; 133 | boosterFragsPlacements.get(frag.id).push(placement); 134 | } 135 | else { 136 | statPlacements[key] = placement; 137 | statFragsPlacements.get(frag.id).push(placement); 138 | } 139 | 140 | //coords.forEach(([x, y]) => overlapping[x][y].push(key)); 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | // Canonise coordinate arrays so we can use equality comparisons on them 148 | const canonicalCoords = [...new Array(width)].map((_, x) => [...new Array(height)].map((_, y) => [x, y])); 149 | for (const placement of placements) { 150 | placement.coords = placement.coords.map(([x, y]) => canonicalCoords[x][y]); 151 | placement.adjacent = placement.adjacent.map(([x, y]) => canonicalCoords[x][y]); 152 | } 153 | 154 | // Pre-compute all adjacencies 155 | for (const placement of placements) { 156 | placement.adjacentBoosters = []; 157 | placement.adjacentStats = []; 158 | placement.overlapWithBoosters = []; 159 | placement.overlapWithStats = []; 160 | for (const other of boosterPlacements) { 161 | if (placement.coords.some(coord => other.adjacent.includes(coord))) 162 | placement.adjacentBoosters.push(other.key); 163 | if (placement.coords.some(coord => other.coords.includes(coord))) { 164 | placement.overlapWithBoosters.push(other.key); 165 | } 166 | } 167 | for (const other of statPlacements) { 168 | if (placement.coords.some(coord => other.adjacent.includes(coord))) 169 | placement.adjacentStats.push(other.key); 170 | if (placement.coords.some(coord => other.coords.includes(coord))) { 171 | placement.overlapWithStats.push(other.key); 172 | } 173 | } 174 | } 175 | 176 | // Turn arrays to fixed type, now that we know their contents 177 | for (const placement of placements) { 178 | placement.adjacentBoosters = Int16Array.from(placement.adjacentBoosters); 179 | placement.adjacentStats = Int16Array.from(placement.adjacentStats); 180 | placement.overlapWithBoosters = Int16Array.from(placement.overlapWithBoosters); 181 | placement.overlapWithStats = Int16Array.from(placement.overlapWithStats); 182 | } 183 | 184 | // Exclude rotational symmetries from search by only using 185 | // - rot 0 placements if the board is square 186 | // - rot 0 and rot 1 placements if the board is non-square 187 | // of the first fragment 188 | // Select the stat fragment with most potential placements as the first fragment, 189 | // to get the biggest reduction of search space 190 | const statFragsKeys = [...statFrags] 191 | .sort((a, b) => statFragsPlacements.get(b.id).length - statFragsPlacements.get(a.id).length) 192 | .map(frag => statFragsPlacements.get(frag.id).map(placement => placement.key)); 193 | statFragsKeys[0] = statFragsKeys[0].filter(key => 194 | width == height ? statPlacements[key].rot == 0 : (statPlacements[key].rot == 0 || statPlacements[key].rot == 1)); 195 | 196 | /// Compute stat fragment layout that maximises potential stat-booster fragment adjacencies 197 | const blockedStats0 = new Uint8Array(statPlacements.length); 198 | const blockedBoosters0 = new Uint8Array(boosterPlacements.length); 199 | const boosterStatAdjacencies0 = new Uint8Array(boosterPlacements.length); 200 | const plan0 = { stats: [], boosters: [] }; 201 | const bestResult0 = [-Infinity, { stats: [...plan0.stats], boosters: [...plan0.boosters] }]; 202 | 203 | planStatsCount = 0; 204 | planBoostersCount = 0; 205 | const t1 = performance.now(); 206 | const [score, plan] = planStats(ns, statPlacements, boosterPlacements, statFragsKeys, 207 | blockedStats0, blockedBoosters0, boosterStatAdjacencies0, plan0, bestResult0); 208 | const t2 = performance.now(); 209 | 210 | tlog(ns, "DEBUG", "Computed Stanek plan. Prep work %.3fmsec, layout search %.3fmsec, %d planStats calls, %d planBoosters calls", 211 | t1 - t0, t2 - t1, planStatsCount, planBoostersCount); 212 | 213 | return [score, plan]; 214 | } 215 | 216 | /** @param {NS} ns 217 | * @param {Placement[]} statPlacements 218 | * @param {Placement[]} boosterPlacements 219 | * @param {number[][]} statFragsKeys 220 | * @param {Uint8Array} blockedStats 221 | * @param {Plan} plan 222 | * @param {[number, Plan]} bestResult 223 | * @param {Uint8Array} blockedBoosters 224 | * @param {Uint8Array} boosterStatAdjacencies 225 | * @return {[number, Plan, Uint8Array, Uint8Array]} */ 226 | function planStats(ns, statPlacements, boosterPlacements, statFragsKeys, blockedStats, blockedBoosters, boosterStatAdjacencies, plan, bestResult) { 227 | planStatsCount++; 228 | if (statFragsKeys.length == 0) { 229 | // Mark boosters that are not blocked, but also not adjacent to a stat fragment as unavailable 230 | // and count the remaining available boosters 231 | let availableBoostersCount = 0; 232 | for (let i = 0; i < blockedBoosters.length; i++) { 233 | if (boosterStatAdjacencies[i] === 0) // No adjacent stat fragments => block 234 | blockedBoosters[i]++; 235 | else if (blockedBoosters[i] === 0) // Has adjacent stat fragments, and not blocked 236 | availableBoostersCount++; 237 | } 238 | 239 | const result = planBoosters(plan, boosterPlacements, boosterStatAdjacencies, 240 | blockedBoosters, availableBoostersCount, 0, bestResult); 241 | 242 | // Undo changes 243 | for (let i = 0; i < blockedBoosters.length; i++) 244 | if (boosterStatAdjacencies[i] === 0) 245 | blockedBoosters[i]--; 246 | 247 | return result; 248 | } 249 | 250 | for (const key of statFragsKeys[0]) { 251 | if (blockedStats[key] !== 0) continue; 252 | const placement = statPlacements[key]; 253 | const adjacentBoosters = placement.adjacentBoosters; 254 | const overlapWithBoosters = placement.overlapWithBoosters; 255 | const overlapWithStats = placement.overlapWithStats; 256 | 257 | // Add the fragment placement to plan and update usability in-place to account for the new blocks 258 | plan.stats.push(placement); 259 | for (let i = 0; i < overlapWithStats.length; i++) 260 | blockedStats[overlapWithStats[i]]++; 261 | for (let i = 0; i < overlapWithBoosters.length; i++) 262 | blockedBoosters[overlapWithBoosters[i]]++; 263 | for (let i = 0; i < adjacentBoosters.length; i++) 264 | boosterStatAdjacencies[adjacentBoosters[i]]++; 265 | 266 | // Find and score best plan that includes this fragment placement 267 | bestResult = planStats(ns, statPlacements, boosterPlacements, statFragsKeys.slice(1), 268 | blockedStats, blockedBoosters, boosterStatAdjacencies, plan, bestResult); 269 | 270 | // Undo the changes 271 | plan.stats.pop(); 272 | for (let i = 0; i < overlapWithStats.length; i++) 273 | blockedStats[overlapWithStats[i]]--; 274 | for (let i = 0; i < overlapWithBoosters.length; i++) 275 | blockedBoosters[overlapWithBoosters[i]]--; 276 | for (let i = 0; i < adjacentBoosters.length; i++) 277 | boosterStatAdjacencies[adjacentBoosters[i]]--; 278 | } 279 | 280 | return bestResult; 281 | } 282 | 283 | /** @param {Plan} plan 284 | * @param {Placement[]} boosterPlacements 285 | * @param {Uint8Array} boosterStatAdjacencies 286 | * @param {Uint8Array} blockedBoosters 287 | * @param {number} availableCount 288 | * @param {number} startIdx 289 | * @param {[number, Plan]} bestResult 290 | * @return {[number, Plan]} */ 291 | function planBoosters(plan, boosterPlacements, boosterStatAdjacencies, blockedBoosters, availableCount, startIdx, bestResult) { 292 | planBoostersCount++; 293 | if (availableCount == 0) { 294 | const { stats, boosters } = plan; 295 | 296 | let score = 0; 297 | for (let i = 0; i < boosters.length; i++) 298 | score += boosterStatAdjacencies[boosters[i].key]; 299 | 300 | if (score > bestResult[0]) 301 | return [score, { stats: [...stats], boosters: [...boosters] }]; // Clone plan 302 | else 303 | return bestResult; 304 | } 305 | 306 | for (let i = startIdx; i < blockedBoosters.length; i++) { 307 | if (blockedBoosters[i] !== 0) continue; 308 | const placement = boosterPlacements[i]; 309 | const overlapWithBoosters = placement.overlapWithBoosters; 310 | 311 | // Add the fragment placement to plan and update usability in-place to account for the new blocks 312 | plan.boosters.push(placement); 313 | for (let j = 0; j < overlapWithBoosters.length; j++) 314 | if ((blockedBoosters[overlapWithBoosters[j]]++) === 0) availableCount--; // Placement became blocked? 315 | 316 | // Find and score best plan that includes this fragment placement 317 | bestResult = planBoosters(plan, boosterPlacements, boosterStatAdjacencies, blockedBoosters, availableCount, i + 1, bestResult); 318 | 319 | // Undo the changes 320 | plan.boosters.pop(); 321 | for (let j = 0; j < overlapWithBoosters.length; j++) 322 | if ((--blockedBoosters[overlapWithBoosters[j]]) === 0) availableCount++; // Placement became free? 323 | } 324 | 325 | return bestResult; 326 | } 327 | 328 | /** @param {Fragment} fragment 329 | * @return {{ rot: number; mask: [number, number][]}[]} */ 330 | function rotations(fragment) { 331 | function shapeEq(s1, s2) { 332 | if (s1.length != s2.length) 333 | return false; 334 | for (let i = 0; i < s1.length; i++) { 335 | if (s1[i].length != s2[i].length) 336 | return false; 337 | for (let j = 0; j < s1[i].length; j++) 338 | if (s1[i][j] != s2[i][j]) 339 | return false; 340 | } 341 | return true; 342 | } 343 | 344 | let shape = fragment.shape; 345 | const rotMasks = [{ rot: 0, mask: shape }]; 346 | for (let i = 1; i < 4; i++) { 347 | shape = shape[0].map((_, y) => shape.map((_, x) => shape[shape.length - 1 - x][y])); 348 | if (!rotMasks.some(({ mask }) => shapeEq(shape, mask))) 349 | rotMasks.push({ rot: i, mask: shape.map(row => [...row]) }); 350 | } 351 | 352 | for (const rotMask of rotMasks) 353 | rotMask.mask = rotMask.mask.map((row, y) => row.map((filled, x) => filled ? [x, y] : undefined)) 354 | .flat() 355 | .filter(elem => elem != undefined); 356 | 357 | return rotMasks; 358 | } 359 | 360 | /** @param {number} width 361 | * @param {number} height 362 | * @param {[number, number][]} coords 363 | * @return {[number, number][]} */ 364 | function adjacents(width, height, coords) { 365 | const adjacent = [...new Array(width)].map(() => [...new Array(height)].map(() => false)); 366 | // Mark grid squares adjacent to shape member squares 367 | for (const [x, y] of coords) { 368 | if (x - 1 >= 0) adjacent[x - 1][y] = true; 369 | if (x + 1 < width) adjacent[x + 1][y] = true; 370 | if (y - 1 >= 0) adjacent[x][y - 1] = true; 371 | if (y + 1 < height) adjacent[x][y + 1] = true; 372 | } 373 | // Strip out the shape squares themselves 374 | for (const [x, y] of coords) 375 | adjacent[x][y] = false; 376 | 377 | return adjacent.map((col, x) => col.map((is, y) => is ? [x, y] : undefined)) 378 | .flat() 379 | .filter(elem => elem != undefined); 380 | } -------------------------------------------------------------------------------- /reserve.js: -------------------------------------------------------------------------------- 1 | import { parseShortNumber } from './helpers.js' 2 | /** @param {NS} ns **/ 3 | export async function main(ns) { 4 | let parsed = parseShortNumber(ns.args[0]); 5 | await ns.write('reserve.txt', parsed, "w"); 6 | ns.tprint(`Set to reserve ${parsed.toLocaleString('en')}`); 7 | } 8 | -------------------------------------------------------------------------------- /reserve.txt: -------------------------------------------------------------------------------- 1 | 0 -------------------------------------------------------------------------------- /run-command.js: -------------------------------------------------------------------------------- 1 | import { runCommand } from './helpers.js' 2 | 3 | const escapeChars = ['"', "'", "`"]; 4 | 5 | /** @param {NS} ns 6 | * The argument can consist of multiple commands to run. The output of the first command will automatically be printed 7 | * unless a subsequent command includes '; output = ...' - in which case that result will be printed instead. **/ 8 | export async function main(ns) { 9 | let args = ns.args; 10 | if (args.length == 0) 11 | return ns.tprint("You must run this script with an argument that is the code to test.") 12 | // Special first argument of -s will result in "silent" mode - do not output the result in the success case 13 | let silent = false; 14 | if (args.includes('-s')) { 15 | silent = true; 16 | args = args.slice(args.indexOf('-s'), 1); 17 | } 18 | const firstArg = String(args[0]); 19 | const escaped = escapeChars.some(c => firstArg.startsWith(c) && firstArg.endsWith(c)); 20 | let command = args == escaped ? args[0] : args.join(" "); // If args weren't escaped, join them together 21 | // To avoid confusion, strip out any trailing spaces / semicolons 22 | command = command.trim(); 23 | if (command.endsWith(';')) command = command.slice(0, -1); 24 | // If the command appears to contian multiple statements, cleverly (and perhaps perilously) 25 | // see if we can inject a return statement so that we can get the return value of the last statement 26 | if (command.includes(';')) { 27 | const lastStatement = command.lastIndexOf(';'); 28 | if (!command.slice(lastStatement + 1).trim().startsWith('return')) 29 | command = command.slice(0, lastStatement + 1) + `return ` + command.slice(lastStatement + 1); 30 | // Create a scope around multi-statement commands so they can be used in a lambda 31 | command = `{ ${command} }`; 32 | } 33 | // Wrapping the command in a lambda that can capture and print its output. 34 | command = `ns.tprint(JSON.stringify(await (async () => ${command})() ?? "(no output)", jsonReplacer, 2)` + 35 | // While we're using pretty formatting, "condence" formatting for any objects nested more than 2 layers deep 36 | `.replaceAll(/\\n +/gi,""))`; 37 | await ns.write(`/Temp/terminal-command.js`, "", "w"); // Clear the previous command file to avoid a warning about re-using temp script names. This is the one exception. 38 | return await runCommand(ns, command, `/Temp/terminal-command.js`, (escaped ? args.slice(1) : undefined), !silent); 39 | } -------------------------------------------------------------------------------- /scan.js: -------------------------------------------------------------------------------- 1 | import { getConfiguration, formatRam, formatMoney, formatNumber } from './helpers.js' 2 | 3 | const argsSchema = [ 4 | ['hide-stats', false], // Set to false to hide detailed server statistics (RAM, max money, etc...) 5 | ]; 6 | 7 | export function autocomplete(data, _) { 8 | data.flags(argsSchema); 9 | return []; 10 | } 11 | 12 | /** 13 | * @param {NS} ns 14 | * @returns interactive server map 15 | */ 16 | export function main(ns) { 17 | const options = getConfiguration(ns, argsSchema); 18 | const showStats = !options['hide-stats']; 19 | const factionServers = ["CSEC", "avmnite-02h", "I.I.I.I", "run4theh111z", "w0r1d_d43m0n", "fulcrumassets"]; 20 | const css = ` `; 36 | const doc = eval("document"); 37 | const terminalInput = doc.getElementById("terminal-input"); 38 | if (!terminalInput) throw new Error("This script must be run while the terminal is visible."); 39 | const terminalEventHandlerKey = Object.keys(terminalInput)[1]; 40 | 41 | function terminalInsert(html) { 42 | const term = doc.getElementById("terminal"); 43 | if (!term) throw new Error("This script must be run while the terminal is visible."); 44 | term.insertAdjacentHTML('beforeend', `
  • ${html}
  • `); 45 | } 46 | async function setNavCommand(inputValue) { 47 | terminalInput.value = inputValue 48 | terminalInput[terminalEventHandlerKey].onChange({ target: terminalInput }) 49 | terminalInput.focus() 50 | await terminalInput[terminalEventHandlerKey].onKeyDown({ key: 'Enter', preventDefault: () => 0 }) 51 | } 52 | 53 | const myHackLevel = ns.getHackingLevel(); 54 | 55 | function getServerInfo(serverName) { 56 | // Costs 2 GB. If you can't don't need backdoor links, uncomment and use the alternate implementations below 57 | return ns.getServer(serverName) 58 | /* return { 59 | requiredHackingSkill: ns.getServerRequiredHackingLevel(serverName), 60 | hasAdminRights: ns.hasRootAccess(serverName), 61 | purchasedByPlayer: serverName.includes('daemon') || serverName.includes('hacknet'), 62 | backdoorInstalled: true // No way of knowing without ns.getServer 63 | // TODO: Other things needed if showStats is true 64 | } */ 65 | } 66 | function createServerEntry(serverName) { 67 | const server = getServerInfo(serverName); 68 | const requiredHackLevel = server.requiredHackingSkill; 69 | const rooted = server.hasAdminRights; 70 | const canHack = requiredHackLevel <= myHackLevel; 71 | const shouldBackdoor = !server.backdoorInstalled && canHack && serverName != 'home' && rooted && !server.purchasedByPlayer; 72 | const contracts = ns.ls(serverName, ".cct"); 73 | return `` 74 | + `${serverName}` 76 | + (server.purchasedByPlayer ? '' : ` (${requiredHackLevel})`) 77 | + `${(shouldBackdoor ? ` [backdoor]` : '')}` 78 | + ` ${contracts.map(c => `@`).join('')}` 79 | + (showStats ? ` ... ` + 80 | `Money: ` + ((server.moneyMax ?? 0 > 0 ? `${formatMoney(server.moneyAvailable ?? 0, 4, 1).padStart(7)} / ` : '') + 81 | `${formatMoney(server.moneyMax ?? 0, 4, 1).padStart(7)} `).padEnd(18) + 82 | `Sec: ${formatNumber(server.hackDifficulty ?? 0, 0, 0).padStart(3)}/${formatNumber(server.minDifficulty ?? 0, 0, 0)} `.padEnd(13) + 83 | `RAM: ${formatRam(server.maxRam ?? 0).replace(' ', '').padStart(6)}` + ( 84 | server.maxRam ?? 0 > 0 ? ` (${formatNumber(server.ramUsed * 100.0 / server.maxRam, 0, 1)}% used)` : '') + 85 | `` : '') 86 | + "" 87 | } 88 | function buildOutput(parent = servers[0], prefix = ["\n"]) { 89 | let output = prefix.join("") + createServerEntry(parent); 90 | if (showStats) { // Roughly right-align server stats if enabled 91 | const expectedLength = parent.length + (2 * prefix.length) + (output.includes('backdoor') ? 11 : 0) + 92 | (output.match(/@/g) || []).length + (((output.match(/\(\d+\)/g) || [{ length: -1 }])[0].length) + 1); 93 | output = output.replace('...', '.'.repeat(Math.max(1, 60 - expectedLength))); 94 | } 95 | for (let i = 0; i < servers.length; i++) { 96 | if (parentByIndex[i] != parent) continue; 97 | const newPrefix = prefix.slice(); 98 | const appearsAgain = parentByIndex.slice(i + 1).includes(parentByIndex[i]); 99 | const lastElementIndex = newPrefix.length - 1; 100 | 101 | newPrefix.push(appearsAgain ? "├╴" : "└╴"); 102 | newPrefix[lastElementIndex] = newPrefix[lastElementIndex].replace("├╴", "│ ").replace("└╴", " "); 103 | output += buildOutput(servers[i], newPrefix); 104 | } 105 | return output; 106 | } 107 | function ordering(serverA, serverB) { 108 | // Sort servers with a smaller sub-tree to the top. Hack: This is O(N^2) but it's easier to 109 | // re-compute the entire subtree here than to figure out what the heck is going on in buildOutput above. 110 | let orderNumber = treeDepth[serverA] - treeDepth[serverB]; 111 | // Sort servers with fewer connections towards the top. 112 | if (orderNumber == 0) orderNumber = ns.scan(serverA).length - ns.scan(serverB).length; 113 | // Purchased servers to the very top 114 | if (orderNumber == 0) 115 | orderNumber = getServerInfo(serverB).purchasedByPlayer - getServerInfo(serverA).purchasedByPlayer; 116 | // Hack: compare just the first 2 chars to keep purchased servers in order purchased 117 | if (orderNumber == 0) 118 | orderNumber = serverA.slice(0, 2).toLowerCase().localeCompare(serverB.slice(0, 2).toLowerCase()); 119 | return orderNumber; 120 | } 121 | 122 | // refresh css (in case it changed) 123 | doc.getElementById("scanCSS")?.remove(); 124 | doc.head.insertAdjacentHTML('beforeend', css); 125 | 126 | // Scan once to determine big the subtree of each node is (purely for sorting purposes) 127 | let treeDepth = {}; 128 | function getTreeDepth(server, stack = []) { 129 | stack.push(server); 130 | const connectedServers = ns.scan(server).filter(s => !stack.includes(s)); 131 | if (connectedServers.length == 0) 132 | return treeDepth[server] = 0; 133 | const maxSubTreeDepth = 1 + Math.max(...connectedServers.map(s => getTreeDepth(s, stack))); 134 | return treeDepth[server] = maxSubTreeDepth; 135 | } 136 | getTreeDepth("home"); 137 | 138 | // Loop over the servers connected to home in the desired sort order to collect display info 139 | let servers = ["home"]; 140 | let parentByIndex = [""]; 141 | let routes = { home: "home" }; // Command to connect to this server 142 | for (let server of servers) 143 | for (let oneScanResult of ns.scan(server).sort(ordering)) 144 | if (!servers.includes(oneScanResult)) { 145 | const backdoored = getServerInfo(oneScanResult)?.backdoorInstalled; 146 | servers.push(oneScanResult); 147 | parentByIndex.push(server); 148 | routes[oneScanResult] = backdoored ? "connect " + oneScanResult : routes[server] + ";connect " + oneScanResult; 149 | } 150 | 151 | // Build the output and insert it into the terminal 152 | terminalInsert(`
    ${buildOutput()}
    `); 153 | doc.querySelectorAll(".serverscan.new .server").forEach(serverEntry => serverEntry 154 | .addEventListener('click', setNavCommand.bind(null, routes[serverEntry.childNodes[0].nodeValue]))); 155 | doc.querySelectorAll(".serverscan.new .backdoor").forEach(backdoorButton => backdoorButton 156 | .addEventListener('click', setNavCommand.bind(null, routes[backdoorButton.parentNode.childNodes[0].childNodes[0].nodeValue] + ";backdoor"))); 157 | doc.querySelector(".serverscan.new").classList.remove("new"); 158 | } 159 | -------------------------------------------------------------------------------- /stanek.js: -------------------------------------------------------------------------------- 1 | import { 2 | log, disableLogs, getFilePath, getConfiguration, formatNumberShort, formatRam, 3 | getNsDataThroughFile, waitForProcessToComplete, getActiveSourceFiles, instanceCount, unEscapeArrayArgs, 4 | tail 5 | } from './helpers.js' 6 | 7 | // Name of the external script that will be created and called to generate charges 8 | const chargeScript = "/Temp/stanek.js.charge.js"; 9 | let awakeningRep = 1E6, serenityRep = 100E6; // Base reputation cost - can be scaled by bitnode multipliers 10 | 11 | const argsSchema = [ 12 | ['reserved-ram', 32], // Don't use this RAM 13 | ['reserved-ram-ideal', 64], // Leave this amount of RAM free if it represents less than 5% of available RAM 14 | ['max-charges', 120], // Stop charging when all fragments have this many charges (diminishing returns - num charges is ^0.07 ) 15 | // By default, starting an augmentation with stanek.js will still spawn daemon.js, but will instruct it not to schedule any hack cycles against home by 'reserving' all its RAM 16 | // TODO: Set these defaults in some way that the user can explicitly specify that they want to run **no** startup script and **no** completion script 17 | ['on-startup-script', null], // Spawn this script when stanek is launched 18 | ['on-startup-script-args', []], // Args for the above 19 | // When stanek completes, it will run daemon.js again (which will terminate the initial ram-starved daemon that is running) 20 | ['on-completion-script', null], // Spawn this script when max-charges is reached 21 | ['on-completion-script-args', []], // Optional args to pass to the script when launched 22 | ['no-tail', false], // By default, keeps a tail window open, because it's pretty important to know when this script is running (can't use home for anything else) 23 | ['reputation-threshold', 0.2], // By default, if we are this close to the rep needed for an unowned stanek upgrade (e.g. "Stanek's Gift - Serenity"), we will keep charging despite the 'max-charges' setting 24 | ]; 25 | 26 | export function autocomplete(data, args) { 27 | data.flags(argsSchema); 28 | return []; 29 | } 30 | 31 | let options, currentServer, maxCharges, idealReservedRam, chargeAttempts, sf4Level, shouldContinueForAug; 32 | 33 | /** Maximizes charge on stanek fragments based on current home RAM. 34 | * NOTE: You should have no other scripts running on home while you do this to get the best peak charge possible 35 | * Stanek stats benefit more from charges with a high avg RAM used per charge, rather than just more charges. 36 | * @param {NS} ns **/ 37 | export async function main(ns) { 38 | const runOptions = getConfiguration(ns, argsSchema); 39 | if (!runOptions || await instanceCount(ns) > 1) return; // Prevent multiple instances of this script from being started, even with different args. 40 | options = runOptions; // We don't set the global "options" until we're sure this is the only running instance 41 | disableLogs(ns, ['sleep', 'run', 'getServerMaxRam', 'getServerUsedRam']) 42 | 43 | // Validate whether we can run 44 | if ((await getActiveFragments(ns)).length == 0) { 45 | // Try to run our helper script to set up the grid 46 | const pid = ns.run(getFilePath('stanek.js.create.js')); 47 | if (pid) await waitForProcessToComplete(ns, pid); 48 | else log(ns, "ERROR while attempting to run stanek.js.create.js (pid was 0)"); 49 | // Verify that this worked. 50 | if ((await getActiveFragments(ns)).length == 0) 51 | return log(ns, "ERROR: You must manually populate your stanek grid with your desired fragments before you run this script to charge them.", true, 'error'); 52 | } 53 | 54 | currentServer = await getNsDataThroughFile(ns, `ns.getHostname()`); 55 | maxCharges = options['max-charges']; // Don't bother adding charges beyond this amount 56 | idealReservedRam = 32; // Reserve this much RAM, if it wouldnt make a big difference anyway. Leaves room for other temp-scripts to spawn. 57 | let startupScript = options['on-startup-script']; 58 | let startupArgs = unEscapeArrayArgs(options['on-startup-script-args']); 59 | if (startupScript) { 60 | // If so configured, launch the start-up script to run alongside stanek and let it consume the RAM it needs before initiating stanek loops. 61 | if (ns.run(startupScript, 1, ...startupArgs)) { 62 | log(ns, `INFO: Stanek.js is launching accompanying 'on-startup-script': ${startupScript}...`, false, 'info'); 63 | await ns.sleep(1000); // Give time for the accompanying script to start up and consume its required RAM footprint. 64 | } else 65 | log(ns, `WARNING: Stanek.js has started successfully, but failed to launch accompanying 'on-startup-script': ${startupScript}...`, false, 'warning'); 66 | } 67 | chargeAttempts = {}; // We keep track of how many times we've charged each segment, to work around a placement bug where fragments can overlap, and then don't register charge 68 | 69 | const chargeScriptBody = "export async function main(ns) { await ns.stanek.chargeFragment(ns.args[0], ns.args[1]); }"; 70 | const checkOnChargeScript = () => { // We must use this periodically since cleanup might be run while we're charging. 71 | // Check if our charge script exists. If not, we can create it (facilitates copying stanek.js to a new server to run) 72 | if (ns.read(chargeScript) != chargeScriptBody) 73 | ns.write(chargeScript, chargeScriptBody, "w"); 74 | } 75 | 76 | // Check what augs we own and establish the theshold to continue grinding REP if we're close to one. 77 | const ownedSourceFiles = await getActiveSourceFiles(ns); 78 | sf4Level = ownedSourceFiles[4] || 0; 79 | shouldContinueForAug = () => false; 80 | if (sf4Level == 0) { 81 | log(ns, `INFO: SF4 required to get owned faction rep and augmentation info. Ignoring the --reputation-threshold setting.`); 82 | } else { 83 | const ownedAugmentations = await getNsDataThroughFile(ns, `ns.singularity.getOwnedAugmentations(true)`, '/Temp/player-augs-purchased.txt'); 84 | const [strAwakening, strSerenity] = ["Stanek's Gift - Awakening", "Stanek's Gift - Serenity"]; 85 | const [awakeningOwned, serenityOwned] = [ownedAugmentations.includes(strAwakening), ownedAugmentations.includes(strSerenity)]; 86 | if (!awakeningOwned || !serenityOwned) { 87 | [awakeningRep, serenityRep] = await getNsDataThroughFile(ns, 88 | `[${[strAwakening, strSerenity].map(a => `ns.singularity.getAugmentationRepReq(\"${a}\")`)}]`, 89 | '/Temp/stanek-aug-rep-reqs.txt'); 90 | log(ns, `INFO: Stanek Augmentations Rep Requirements are Awakening: ${formatNumberShort(awakeningRep)}, ` + 91 | `Serenity: ${formatNumberShort(serenityRep)} (--reputation-threshold = ${options['reputation-threshold']})`); 92 | } 93 | shouldContinueForAug = (currentRep) => // return true if currentRep is high enough that we should keep grinding for the next unowned aug 94 | !awakeningOwned && options['reputation-threshold'] * awakeningRep <= currentRep && currentRep < awakeningRep || 95 | !serenityOwned && options['reputation-threshold'] * serenityRep <= currentRep && currentRep < serenityRep 96 | } 97 | 98 | // Start the main stanek loop 99 | let lastLoopSuccessful = true; 100 | while (true) { 101 | await ns.sleep(lastLoopSuccessful ? 10 : 1000); // Only sleep a short while between charges if things are going well 102 | lastLoopSuccessful = false; 103 | try { 104 | if (!options['no-tail']) tail(ns); // Keep a tail window open unless otherwise configured 105 | checkOnChargeScript(); 106 | const fragmentsToCharge = await getFragmentsToCharge(ns); 107 | if (fragmentsToCharge === undefined) continue; 108 | if (fragmentsToCharge.length == 0) break; // All fragments at max desired charge 109 | lastLoopSuccessful = await tryChargeAllFragments(ns, fragmentsToCharge); 110 | } 111 | catch (err) { 112 | log(ns, `WARNING: stanek.js Caught (and suppressed) an unexpected error in the main loop:\n` + 113 | (typeof err === 'string' ? err : err.message || JSON.stringify(err)), false, 'warning'); 114 | } 115 | } 116 | log(ns, `SUCCESS: All stanek fragments at desired charge ${maxCharges}`, true, 'success'); 117 | 118 | // Run the completion script before shutting down 119 | let completionScript = options['on-completion-script']; 120 | let completionArgs = unEscapeArrayArgs(options['on-completion-script-args']); 121 | if (completionScript) { 122 | if (ns.run(completionScript, 1, ...completionArgs)) { 123 | log(ns, `INFO: Stanek.js shutting down and launching ${completionScript}...`, false, 'info'); 124 | if (!options['no-tail']) 125 | ns.closeTail(); // Close the tail window if we opened it 126 | } else 127 | log(ns, `WARNING: Stanek.js shutting down, but failed to launch ${completionScript}...`, false, 'warning'); 128 | } 129 | } 130 | 131 | /** Get Fragments to Charge 132 | * @param {NS} ns 133 | * @returns {Promise} whether all fragments were charged successfully **/ 134 | async function getFragmentsToCharge(ns) { 135 | // Make sure we have the latest information about all fragments 136 | let fragments = await getActiveFragments(ns); 137 | if (fragments.length == 0) { 138 | log(ns, "ERROR: Stanek fragments were cleared. You must re-populate the grid before charging can continue.", true, 'error'); 139 | return undefined; 140 | } 141 | // If we have SF4, get our updated faction rep, and determine if we should continue past --max-charges to earn rep for the next augmentation 142 | const churchRep = sf4Level ? await getNsDataThroughFile(ns, 'ns.singularity.getFactionRep(ns.args[0])', null, ["Church of the Machine God"]) : 0; 143 | const shouldContinue = shouldContinueForAug(churchRep); 144 | 145 | // Collect information about each fragment's charge status, and prepare a status update 146 | let fragmentSummary = ''; 147 | let minCharges = Number.MAX_SAFE_INTEGER; 148 | for (const fragment of fragments) { 149 | fragmentSummary += `Fragment ${String(fragment.id).padStart(2)} at [${fragment.x},${fragment.y}] ` + 150 | (fragment.id < 100 ? `Peak: ${formatNumberShort(fragment.highestCharge)} Charges: ${fragment.numCharge.toFixed(1)}` : 151 | `(booster, no charge effect)`) + `\n`; 152 | if (fragment.numCharge == 0 && (chargeAttempts[fragment.id] || 0) > 0) { // Ignore fragments that aren't accepting charge. 153 | if (chargeAttempts[fragment.id] == 1 && fragment.id < 100) { // First time we do this, log a message 154 | log(ns, `WARNING: Detected that fragment ${fragment.id} at [${fragment.x},${fragment.y}] is not accepting charge nano (root overlaps with another segment root?)`, true, 'warning'); 155 | chargeAttempts[fragment.id] = 2; // Hack: We will never try to charge this fragment again. Abuse this dict value so we don't see htis log again. 156 | } 157 | } else if (fragment.id < 100) 158 | minCharges = Math.min(minCharges, fragment.numCharge) // Track the least-charged fragment (ignoring fragments that take no charge) 159 | } 160 | minCharges = Math.ceil(minCharges); // Fractional charges now occur. Round these up. 161 | if (minCharges >= maxCharges && !shouldContinue && fragments.some(f => (chargeAttempts[f.id] || 0) > 0)) 162 | return []; // Max charges reached 163 | // We will only charge non-booster fragments, and fragments that aren't stuck at 0 charge 164 | const fragmentsToCharge = fragments.filter(f => f.id < 100 && ((chargeAttempts[f.id] || 0) < 2 || f.numCharge > 0)); 165 | // Log a status update 166 | log(ns, `Charging ${fragmentsToCharge.length}/${fragments.length} fragments ` + (!shouldContinue ? `to ${maxCharges}` : `until faction has ` + 167 | formatNumberShort(churchRep < awakeningRep ? awakeningRep : serenityRep) + ` rep (currently at ${formatNumberShort(churchRep)})`) + 168 | `. Curent charges:\n${fragmentSummary}`); 169 | return fragmentsToCharge; 170 | } 171 | 172 | /** Try to charge all the specified fragments using available ram 173 | * @param {NS} ns 174 | * @returns {Promise} whether all fragments were charged successfully **/ 175 | async function tryChargeAllFragments(ns, fragmentsToCharge) { 176 | // Charge each fragment one at a time 177 | for (const fragment of fragmentsToCharge) { 178 | let availableRam = ns.getServerMaxRam(currentServer) - ns.getServerUsedRam(currentServer); 179 | let reservedRam = (idealReservedRam / availableRam < 0.05) ? options['reserved-ram-ideal'] : options['reserved-ram']; 180 | const threads = Math.floor((availableRam - reservedRam) / 2.0); 181 | if (threads <= 0) { 182 | log(ns, `WARNING: Insufficient free RAM on ${currentServer} to charge Stanek ` + 183 | `(${formatRam(availableRam)} free - ${formatRam(reservedRam)} reserved). Will try again later...`); 184 | continue; 185 | } 186 | const pid = ns.run(chargeScript, { threads: threads, temporary: true }, fragment.x, fragment.y); 187 | if (!pid) { 188 | log(ns, `WARNING: Failed to charge Stanek with ${threads} threads thinking there was ${formatRam(availableRam)} free on ${currentServer}. ` + 189 | `Check if another script is fighting stanek.js for RAM. Will try again later...`); 190 | continue; 191 | } 192 | await waitForProcessToComplete(ns, pid); 193 | chargeAttempts[fragment.id] = 1 + (chargeAttempts[fragment.id] || 0); 194 | } 195 | } 196 | 197 | /** Get the current active stanek fragments 198 | * @param {NS} ns 199 | * @returns {Promise} **/ 200 | async function getActiveFragments(ns) { 201 | return await getNsDataThroughFile(ns, 'ns.stanek.activeFragments()'); 202 | } -------------------------------------------------------------------------------- /stanek.js.create.js: -------------------------------------------------------------------------------- 1 | import { log, getConfiguration, getNsDataThroughFile } from './helpers.js' 2 | 3 | const argsSchema = [ 4 | ['clear', false], // If set to true, will clear whatever layout is already there and create a new one 5 | ['force-width', null], // Force the layout less than or equal to the specified width 6 | ['force-height', null], // Force the layout less than or equal to the specified height 7 | ]; 8 | export function autocomplete(data, args) { 9 | data.flags(argsSchema); 10 | return []; 11 | } 12 | 13 | /** @param {NS} ns */ 14 | export async function main(ns) { 15 | const options = getConfiguration(ns, argsSchema); 16 | if (!options) return; 17 | 18 | // Check if stanek was previously placed 19 | if (!options['clear']) { 20 | const fragments = await getNsDataThroughFile(ns, 'ns.stanek.activeFragments()'); 21 | if (fragments.length > 0) 22 | return log(ns, `WARNING: Nothing to do, you've already populated Stanek's Gift. Exiting...`, true); 23 | } 24 | 25 | // Find the saved layout that best matches 26 | const height = options['force-height'] || await getNsDataThroughFile(ns, 'ns.stanek.giftHeight()'); 27 | const width = options['force-width'] || await getNsDataThroughFile(ns, 'ns.stanek.giftWidth()'); 28 | const usableLayouts = layouts.filter(l => l.height <= height && l.width <= width); 29 | const bestLayout = usableLayouts.sort((l1, l2) => // Use the layout with the least amount of unused rows/columns 30 | (height - l1.height + width - l1.width) - (height - l2.height + width - l2.width))[0]; 31 | log(ns, `Best layout found for current Stanek grid dimentions (height: ${height} width: ${width}) ` + 32 | `has height: ${bestLayout.height} width: ${bestLayout.width} fragments: ${bestLayout.fragments.length}`); 33 | 34 | // Clear any prior layout if enabled 35 | if (options['clear']) { 36 | await getNsDataThroughFile(ns, 'ns.stanek.clearGift() || true', '/Temp/stanek-clearGift.txt'); 37 | log(ns, 'Cleared any existing stanek layout.'); 38 | } 39 | 40 | // If we're in a bladeburner BN, and there's no bladeburner piece in our selected layout (id=30), 41 | // replace the identically shaped hacking multi piece with this one. 42 | const has0butNot30 = bestLayout.fragments.some(p => p.id == 0) && !bestLayout.fragments.some(p => p.id == 30); 43 | if (has0butNot30) { 44 | const bitnodeN = (await getNsDataThroughFile(ns, 'ns.getResetInfo()')).currentNode;; 45 | if (bitnodeN == 6 || bitnodeN == 7) { 46 | log(ns, `We're in a bladeburner node, replacing a hack piece with the bladeburner piece.`); 47 | bestLayout.fragments.find(p => p.id == 0).id = 30; 48 | } 49 | } 50 | 51 | // Place the layout 52 | log(ns, `Placing ${bestLayout.fragments.length} fragments:\n` + JSON.stringify(bestLayout.fragments)); 53 | const result = await getNsDataThroughFile(ns, 54 | 'JSON.parse(ns.args[0]).reduce((t, f) => ns.stanek.placeFragment(f.x, f.y, f.rotation, f.id) && t, true)', 55 | '/Temp/stanek-placeFragments.txt', [JSON.stringify(bestLayout.fragments)]); 56 | if (result) 57 | log(ns, `SUCCESS: Placed ${bestLayout.fragments.length} Stanek fragments.`, true, 'success'); 58 | else 59 | log(ns, `ERROR: Failed to place one or more fragments. The layout may be invalid.`, true, 'error'); 60 | } 61 | 62 | // DISCLAIMER: These layouts are mostly hack focused, but bring in additional important stats as there is room 63 | const layouts = [ // NOTE: Width appears to be always the same as, or one more than height. 64 | { 65 | "height": 2, "width": 3, "fragments": [ // BN 13.1 is this small 66 | { "id": 0, "x": 0, "y": 0, "rotation": 0 } // Hacking Mult 67 | ] 68 | }, { 69 | "height": 3, "width": 3, "fragments": [ 70 | { "id": 1, "x": 0, "y": 0, "rotation": 3 }, // Hacking Mult 71 | { "id": 25, "x": 1, "y": 0, "rotation": 3 }, // Reputation 72 | ] 73 | }, { 74 | "height": 3, "width": 4, "fragments": [ // Note: Possible to fit 3 fragments, see "alternative layouts" below 75 | { "id": 0, "x": 0, "y": 0, "rotation": 1 }, // Hacking Mult 76 | { "id": 1, "x": 2, "y": 0, "rotation": 1 } // Hacking Mult 77 | ] 78 | }, { 79 | "height": 4, "width": 4, "fragments": [ // Note: Possible to fit 4 fragments, but have to sacrifice a hacking mult piece 80 | { "id": 0, "x": 0, "y": 0, "rotation": 0 }, // Hacking Mult 81 | { "id": 1, "x": 0, "y": 2, "rotation": 0 }, // Hacking Mult 82 | { "id": 25, "x": 2, "y": 0, "rotation": 3 } // Reputation 83 | ] 84 | }, { 85 | "height": 4, "width": 5, "fragments": [ 86 | { "id": 0, "x": 0, "y": 0, "rotation": 0 }, // Hacking Mult 87 | { "id": 1, "x": 0, "y": 2, "rotation": 0 }, // Hacking Mult 88 | { "id": 25, "x": 3, "y": 1, "rotation": 3 }, // Reputation 89 | { "id": 104, "x": 2, "y": 0, "rotation": 0 }, // Booster *new* 90 | ] 91 | }, { 92 | "height": 5, "width": 5, "fragments": [ 93 | { "id": 0, "x": 0, "y": 0, "rotation": 0 }, // Hacking Mult 94 | { "id": 1, "x": 1, "y": 2, "rotation": 0 }, // Hacking Mult 95 | { "id": 25, "x": 3, "y": 2, "rotation": 3 }, // Reputation 96 | { "id": 105, "x": 0, "y": 2, "rotation": 1 }, // Booster 97 | { "id": 100, "x": 2, "y": 0, "rotation": 0 }, // Booster *new* 98 | ] 99 | }, { 100 | // NOTE: Things get pretty subjective after this. Should we prioritize boosting hacking multi or adding more stats? 101 | // I've decided to start by adding in Hacking Speed, Hacknet Production + Cost as 3 stats more important than just more boost 102 | "height": 5, "width": 6, "fragments": [ 103 | { "id": 0, "x": 3, "y": 0, "rotation": 0 }, // Hacking Mult 104 | { "id": 1, "x": 3, "y": 3, "rotation": 0 }, // Hacking Mult 105 | { "id": 5, "x": 4, "y": 1, "rotation": 1 }, // Hacking Speed *new* 106 | { "id": 20, "x": 0, "y": 4, "rotation": 0 }, // Hacknet Production *new* 107 | { "id": 21, "x": 0, "y": 1, "rotation": 0 }, // Hacknet Cost Reduction *new* 108 | { "id": 25, "x": 0, "y": 0, "rotation": 2 }, // Reputation 109 | { "id": 102, "x": 0, "y": 2, "rotation": 2 } // Booster 110 | ] 111 | }, { 112 | "height": 6, "width": 6, "fragments": [ 113 | { "id": 0, "x": 0, "y": 2, "rotation": 0 }, // Hacking Mult 114 | { "id": 1, "x": 2, "y": 2, "rotation": 1 }, // Hacking Mult 115 | { "id": 5, "x": 3, "y": 3, "rotation": 1 }, // Hacking Speed 116 | { "id": 20, "x": 5, "y": 2, "rotation": 1 }, // Hacknet Production 117 | { "id": 21, "x": 0, "y": 0, "rotation": 0 }, // Hacknet Cost Reduction 118 | { "id": 25, "x": 3, "y": 0, "rotation": 2 }, // Reputation 119 | { "id": 103, "x": 0, "y": 4, "rotation": 2 }, // Booster 120 | { "id": 104, "x": 2, "y": 0, "rotation": 1 } // Booster *new* 121 | ] 122 | }, { // Special thanks to @Ansopedi (a.k.a. Zoëkeeper) for solving for this layout 123 | "height": 6, "width": 7, "fragments": [ 124 | { "id": 0, "x": 3, "y": 2, "rotation": 1 }, // Hacking Mult 125 | { "id": 1, "x": 1, "y": 3, "rotation": 0 }, // Hacking Mult 126 | { "id": 5, "x": 4, "y": 1, "rotation": 1 }, // Hacking Speed 127 | { "id": 6, "x": 0, "y": 0, "rotation": 0 }, // Hack power *new* 128 | { "id": 7, "x": 4, "y": 0, "rotation": 2 }, // Grow power *new* 129 | { "id": 20, "x": 6, "y": 2, "rotation": 1 }, // Hacknet Production 130 | { "id": 21, "x": 0, "y": 4, "rotation": 0 }, // Hacknet Cost Reduction 131 | { "id": 25, "x": 0, "y": 1, "rotation": 1 }, // Reputation 132 | { "id": 101, "x": 2, "y": 4, "rotation": 2 }, // Booster 133 | { "id": 102, "x": 1, "y": 1, "rotation": 0 }, // Booster 134 | ] 135 | }, { // Note: Late BN12, as Stanek gets bigger, Bladeburner also becomes a faster win condition, so we start adding those stats 136 | "height": 7, "width": 7, "fragments": [ 137 | { "id": 0, "x": 1, "y": 5, "rotation": 2 }, // Hacking Mult 138 | { "id": 1, "x": 3, "y": 3, "rotation": 0 }, // Hacking Mult 139 | { "id": 5, "x": 0, "y": 4, "rotation": 3 }, // Hacking Speed 140 | { "id": 6, "x": 0, "y": 0, "rotation": 1 }, // Hack power 141 | { "id": 7, "x": 1, "y": 1, "rotation": 1 }, // Grow power 142 | { "id": 20, "x": 1, "y": 0, "rotation": 2 }, // Hacknet Production 143 | { "id": 21, "x": 3, "y": 1, "rotation": 0 }, // Hacknet Cost Reduction 144 | { "id": 25, "x": 5, "y": 4, "rotation": 3 }, // Reputation 145 | { "id": 30, "x": 3, "y": 5, "rotation": 2 }, // Bladeburner Stats *new* 146 | { "id": 101, "x": 5, "y": 0, "rotation": 3 }, // Booster 147 | { "id": 106, "x": 1, "y": 2, "rotation": 3 }, // Booster 148 | ] 149 | }, { 150 | "height": 7, "width": 8, "fragments": [ 151 | { "id": 0, "x": 4, "y": 1, "rotation": 0 }, // Hacking Mult 152 | { "id": 1, "x": 4, "y": 4, "rotation": 3 }, // Hacking Mult 153 | { "id": 5, "x": 0, "y": 2, "rotation": 0 }, // Hacking Speed 154 | { "id": 6, "x": 3, "y": 0, "rotation": 2 }, // Hack power 155 | { "id": 7, "x": 2, "y": 0, "rotation": 0 }, // Grow power 156 | { "id": 14, "x": 0, "y": 3, "rotation": 1 }, // Dexterity *new* 157 | { "id": 16, "x": 5, "y": 5, "rotation": 2 }, // Agility *new* 158 | { "id": 20, "x": 0, "y": 6, "rotation": 0 }, // Hacknet Production 159 | { "id": 21, "x": 0, "y": 0, "rotation": 0 }, // Hacknet Cost Reduction 160 | { "id": 25, "x": 6, "y": 0, "rotation": 3 }, // Reputation 161 | { "id": 30, "x": 2, "y": 4, "rotation": 0 }, // Bladeburner Stats 162 | { "id": 103, "x": 4, "y": 3, "rotation": 0 }, // Booster 163 | { "id": 105, "x": 1, "y": 2, "rotation": 0 }, // Booster 164 | ] 165 | }, { // Adds Charisma, which even a small boost makes a huge difference (hours) in grinding company rep 166 | // TODO: Consider adding charisma boosts a little earlier on in the prior 2 layouts. 167 | "height": 8, "width": 8, "fragments": [ // ~BN 12.50 168 | { "id": 0, "x": 3, "y": 0, "rotation": 0 }, // Hacking Mult 169 | { "id": 1, "x": 2, "y": 2, "rotation": 1 }, // Hacking Mult 170 | { "id": 5, "x": 0, "y": 0, "rotation": 3 }, // Hacking Speed 171 | { "id": 6, "x": 7, "y": 2, "rotation": 1 }, // Hack power 172 | { "id": 7, "x": 4, "y": 5, "rotation": 3 }, // Grow power 173 | { "id": 14, "x": 3, "y": 4, "rotation": 3 }, // Dexterity 174 | { "id": 16, "x": 5, "y": 1, "rotation": 1 }, // Agility 175 | { "id": 18, "x": 6, "y": 5, "rotation": 1 }, // Charisma *new* 176 | { "id": 20, "x": 0, "y": 3, "rotation": 3 }, // Hacknet Production 177 | { "id": 21, "x": 6, "y": 0, "rotation": 0 }, // Hacknet Cost Reduction 178 | { "id": 25, "x": 2, "y": 5, "rotation": 3 }, // Reputation 179 | { "id": 30, "x": 0, "y": 6, "rotation": 0 }, // Bladeburner Stats 180 | { "id": 101, "x": 1, "y": 2, "rotation": 3 }, // Booster 181 | { "id": 105, "x": 4, "y": 2, "rotation": 1 }, // Booster 182 | { "id": 106, "x": 1, "y": 0, "rotation": 1 }, // Booster *new* (Thanks @aeroleo) 183 | ] 184 | }, { // Took a minute and found a way to cram Defense and Strength in 185 | "height": 8, "width": 9, "fragments": [ 186 | { "id": 0, "x": 4, "y": 1, "rotation": 0 }, // Hacking Mult 187 | { "id": 1, "x": 4, "y": 4, "rotation": 0 }, // Hacking Mult 188 | { "id": 5, "x": 0, "y": 2, "rotation": 0 }, // Hacking Speed 189 | { "id": 6, "x": 3, "y": 0, "rotation": 2 }, // Hack power 190 | { "id": 7, "x": 2, "y": 0, "rotation": 0 }, // Grow power 191 | { "id": 10, "x": 4, "y": 6, "rotation": 2 }, // Strength *new* 192 | { "id": 12, "x": 6, "y": 5, "rotation": 0 }, // Defense *new* 193 | { "id": 14, "x": 1, "y": 5, "rotation": 1 }, // Dexterity 194 | { "id": 16, "x": 7, "y": 0, "rotation": 3 }, // Agility 195 | { "id": 18, "x": 3, "y": 4, "rotation": 1 }, // Charisma 196 | { "id": 20, "x": 0, "y": 3, "rotation": 3 }, // Hacknet Production 197 | { "id": 21, "x": 0, "y": 0, "rotation": 0 }, // Hacknet Cost Reduction 198 | { "id": 25, "x": 4, "y": 3, "rotation": 2 }, // Reputation 199 | { "id": 30, "x": 2, "y": 5, "rotation": 1 }, // Bladeburner Stats 200 | { "id": 101, "x": 6, "y": 2, "rotation": 1 }, // Booster 201 | { "id": 105, "x": 1, "y": 2, "rotation": 0 } // Booster 202 | ] 203 | }, { // Ample Space ~ BN 12.85 to get more boosts on all stats 204 | "height": 9, "width": 9, "fragments": [ 205 | { "id": 0, "x": 4, "y": 1, "rotation": 0 }, // Hacking Mult 206 | { "id": 1, "x": 4, "y": 4, "rotation": 0 }, // Hacking Mult 207 | { "id": 5, "x": 0, "y": 2, "rotation": 0 }, // Hacking Speed 208 | { "id": 6, "x": 4, "y": 0, "rotation": 0 }, // Hack power 209 | { "id": 7, "x": 2, "y": 0, "rotation": 0 }, // Grow power 210 | { "id": 10, "x": 7, "y": 2, "rotation": 1 }, // Strength 211 | { "id": 12, "x": 5, "y": 7, "rotation": 0 }, // Defense 212 | { "id": 14, "x": 1, "y": 5, "rotation": 1 }, // Dexterity 213 | { "id": 16, "x": 5, "y": 6, "rotation": 0 }, // Agility 214 | { "id": 18, "x": 3, "y": 4, "rotation": 1 }, // Charisma 215 | { "id": 20, "x": 0, "y": 3, "rotation": 3 }, // Hacknet Production 216 | { "id": 21, "x": 0, "y": 0, "rotation": 0 }, // Hacknet Cost Reduction 217 | { "id": 25, "x": 4, "y": 3, "rotation": 2 }, // Reputation 218 | { "id": 30, "x": 2, "y": 5, "rotation": 1 }, // Bladeburner Stats 219 | { "id": 101, "x": 1, "y": 7, "rotation": 2 }, // Booster *new* 220 | { "id": 101, "x": 7, "y": 5, "rotation": 1 }, // Booster 221 | { "id": 105, "x": 1, "y": 2, "rotation": 0 }, // Booster 222 | { "id": 105, "x": 6, "y": 0, "rotation": 0 } // Booster *new* 223 | ] 224 | } 225 | 226 | ]; 227 | 228 | // Not used for anything, but captures our rough priorities when designing the above layouts 229 | const priorities = [ 230 | { id: 25, weight: 13.0 }, /* Faction Rep */ 231 | { id: 0, weight: 12.0 }, /* Hack Mult */ 232 | { id: 1, weight: 11.0 }, /* Hack Mult */ 233 | // Generally prefer adding one of these stats over triple-boosting the above 234 | { id: 5, weight: 1.15 }, /* Hack Speed */ 235 | { id: 20, weight: 1.14 }, /* Hacknet Prod */ 236 | { id: 21, weight: 1.13 }, /* Hacknet Cost */ 237 | { id: 6, weight: 1.12 }, /* Hack Power */ 238 | { id: 7, weight: 1.11 }, /* Grow Power */ 239 | { id: 30, weight: 1.10 }, /* Bladeburner */ 240 | { id: 16, weight: 1.09 }, /* Agi */ 241 | { id: 14, weight: 1.08 }, /* Dex */ 242 | // Generally prefer additional boost over the below 243 | { id: 28, weight: 0.99 }, /* Crime Money */ 244 | { id: 18, weight: 0.98 }, /* Cha */ 245 | { id: 10, weight: 0.97 }, /* Str */ 246 | { id: 12, weight: 0.96 }, /* Def */ 247 | { id: 28, weight: 0.95 }, /* Work Money */ 248 | ] 249 | 250 | // Not used, but these alternative layouts favour fitting more stat pieces vs. boosting most important stats, use if you please 251 | const alternativeLayouts = [ 252 | { 253 | "height": 3, "width": 4, "fragments": [ 254 | { "id": 0, "x": 1, "y": 0, "rotation": 0 }, // Hacking Chance 255 | { "id": 25, "x": 0, "y": 0, "rotation": 1 }, // Reputation 256 | { "id": 28, "x": 1, "y": 1, "rotation": 0 }, // Crime Money 257 | ] 258 | }, { 259 | "height": 4, "width": 4, "fragments": [ 260 | { "id": 0, "x": 0, "y": 2, "rotation": 2 }, // Hacking Chance 261 | { "id": 7, "x": 2, "y": 1, "rotation": 3 }, // Grow power 262 | { "id": 25, "x": 0, "y": 0, "rotation": 1 }, // Reputation 263 | { "id": 30, "x": 1, "y": 0, "rotation": 0 }, // Bladeburner 264 | ] 265 | }, { 266 | "height": 6, "width": 6, "fragments": [ 267 | { "id": 0, "x": 0, "y": 2, "rotation": 0 }, // Hacking Chance 268 | { "id": 1, "x": 0, "y": 4, "rotation": 0 }, // Hacking Chance 269 | { "id": 5, "x": 2, "y": 1, "rotation": 0 }, // Hacking Speed 270 | { "id": 6, "x": 2, "y": 0, "rotation": 0 }, // Hack power 271 | { "id": 7, "x": 2, "y": 3, "rotation": 2 }, // Grow power 272 | { "id": 20, "x": 5, "y": 1, "rotation": 1 }, // Hacknet Production 273 | { "id": 21, "x": 0, "y": 0, "rotation": 0 }, // Hacknet Cost Reduction 274 | { "id": 25, "x": 3, "y": 4, "rotation": 0 }, // Reputation 275 | ] 276 | } 277 | ] -------------------------------------------------------------------------------- /stats.js: -------------------------------------------------------------------------------- 1 | import { 2 | log, disableLogs, instanceCount, getConfiguration, getNsDataThroughFile, getActiveSourceFiles, 3 | getStocksValue, formatNumberShort, formatMoney, formatRam, getFilePath 4 | } from './helpers.js' 5 | 6 | const argsSchema = [ 7 | ['show-peoplekilled', false], 8 | ['hide-stocks', false], 9 | ['hide-RAM-utilization', false], 10 | ]; 11 | 12 | export function autocomplete(data, args) { 13 | data.flags(argsSchema); 14 | return []; 15 | } 16 | 17 | let doc, hook0, hook1; 18 | let playerInBladeburner = false, nodeMap = {} 19 | 20 | /** @param {NS} ns **/ 21 | export async function main(ns) { 22 | const options = getConfiguration(ns, argsSchema); 23 | if (!options || (await instanceCount(ns)) > 1) return; // Prevent multiple instances of this script from being started, even with different args. 24 | 25 | const dictSourceFiles = await getActiveSourceFiles(ns, false); // Find out what source files the user has unlocked 26 | let resetInfo = await getNsDataThroughFile(ns, 'ns.getResetInfo()'); 27 | const bitNode = resetInfo.currentNode; 28 | disableLogs(ns, ['sleep']); 29 | 30 | // Globals need to reset at startup. Otherwise, they can survive e.g. flumes and new BNs and return stale results 31 | playerInBladeburner = false; 32 | nodeMap = {}; 33 | doc = eval('document'); 34 | hook0 = doc.getElementById('overview-extra-hook-0'); 35 | hook1 = doc.getElementById('overview-extra-hook-1'); 36 | 37 | // Hook script exit to clean up after ourselves. 38 | ns.atExit(() => hook1.innerHTML = hook0.innerHTML = "") 39 | 40 | addCSS(doc); 41 | 42 | prepareHudElements(await getHudData(ns, bitNode, dictSourceFiles, options)) 43 | 44 | // Main stats update loop 45 | while (true) { 46 | try { 47 | const hudData = await getHudData(ns, bitNode, dictSourceFiles, options) 48 | 49 | // update HUD elements with info collected above. 50 | for (const [header, show, formattedValue, toolTip] of hudData) { 51 | // Ensure values are never shown glued to the header by adding a non-breaking space to the left of each. 52 | const paddedValue = formattedValue == null ? null : ' ' + formattedValue?.trim(); 53 | updateHudElement(header, show, paddedValue, toolTip) 54 | } 55 | } catch (err) { 56 | // Might run out of ram from time to time, since we use it dynamically 57 | log(ns, `WARNING: stats.js Caught (and suppressed) an unexpected error in the main loop. Update Skipped:\n` + 58 | (typeof err === 'string' ? err : err.message || JSON.stringify(err)), false, 'warning'); 59 | } 60 | await ns.sleep(1000); 61 | } 62 | } 63 | 64 | /** Creates the new UI elements which we will be adding custom HUD data to. 65 | * @param {HudRowConfig[]} hudData */ 66 | function prepareHudElements(hudData) { 67 | const newline = (id, txt, toolTip = "") => { 68 | const p = doc.createElement("p"); 69 | p.className = "tooltip hidden"; 70 | const text = doc.createElement("span"); 71 | text.textContent = txt; 72 | p.appendChild(text); 73 | const tooltip = doc.createElement("span"); 74 | p.appendChild(tooltip); 75 | tooltip.textContent = toolTip; 76 | tooltip.className = "tooltiptext"; 77 | nodeMap[id] = [text, tooltip, p] 78 | return p; 79 | } 80 | 81 | for (const [header, _, value, toolTip] of hudData) { 82 | const id = makeID(header) 83 | hook0.appendChild(newline(id + "-title", header.padEnd(9, " "), toolTip)) 84 | hook1.appendChild(newline(id + "-value", value, toolTip)) 85 | } 86 | } 87 | 88 | function makeID(header) { 89 | return header.replace(" ", "") ?? "empty-header" 90 | } 91 | 92 | /** Creates the new UI elements which we will be adding custom HUD data to. 93 | * @param {string} header - The stat name which appears in the first column of the custom HUD row 94 | * @param {boolean} visible - Indicates whether the current HUD row should be displayed (true) or hidden (false) 95 | * @param {string} value - The value to display in the second column of the custom HUD row 96 | * @param {string} toolTip - The tooltip to display if the user hovers over this HUD row with their cursor. */ 97 | function updateHudElement(header, visible, value, toolTip) { 98 | const id = makeID(header), 99 | valId = id + "-value", 100 | titleId = id + "-title", 101 | maybeUpdate = (id, index, value) => { 102 | if (nodeMap[id][index].textContent != value) 103 | nodeMap[id][index].textContent = value 104 | } 105 | 106 | if (visible) { 107 | maybeUpdate(valId, 0, value) 108 | maybeUpdate(valId, 1, toolTip) 109 | maybeUpdate(titleId, 1, toolTip) 110 | nodeMap[titleId][2].classList.remove("hidden") 111 | nodeMap[valId][2].classList.remove("hidden") 112 | } else { 113 | nodeMap[titleId][2].classList.add("hidden") 114 | nodeMap[valId][2].classList.add("hidden") 115 | } 116 | } 117 | 118 | /** @param {NS} ns 119 | * @param {number} bitNode the current bitnode the user is in 120 | * @param {{[k: number]: number}} dictSourceFiles The source files the user has unlocked so far 121 | * @param {(string | boolean)[][]} options The run configuration of this script. 122 | * @typedef {string} header - The stat name which appears in the first column of the custom HUD row 123 | * @typedef {boolean} show - Indicates whether the current HUD row should be displayed (true) or hidden (false) 124 | * @typedef {string} formattedValue - The value to display in the second column of the custom HUD row 125 | * @typedef {string} toolTip - The tooltip to display if the user hovers over this HUD row with their cursor. 126 | * @typedef {[header, show, formattedValue, toolTip]} HudRowConfig The configuration for a custom row displayed in the HUD 127 | * @returns {Promise} **/ 128 | async function getHudData(ns, bitNode, dictSourceFiles, options) { 129 | const hudData = (/**@returns {HudRowConfig[]}*/() => [])(); 130 | 131 | // Show what bitNode we're currently playing in 132 | { 133 | const val = ["BitNode", true, `${bitNode}.${1 + (dictSourceFiles[bitNode] || 0)}`, 134 | `Detected as being one more than your current owned SF level (${dictSourceFiles[bitNode] || 0}) in the current bitnode (${bitNode}).`] 135 | hudData.push(val) 136 | } 137 | 138 | // Show Hashes 139 | { 140 | const val1 = ["Hashes"]; 141 | const val2 = [" "]; // Blank line placeholder for when hashes are being liquidated 142 | if (9 in dictSourceFiles || 9 == bitNode) { // Section not relevant if you don't have access to hacknet servers 143 | const hashes = await getNsDataThroughFile(ns, '[ns.hacknet.numHashes(), ns.hacknet.hashCapacity()]', '/Temp/hash-stats.txt') 144 | if (hashes[1] > 0) { 145 | val1.push(true, `${formatNumberShort(hashes[0], 3, 1)}/${formatNumberShort(hashes[1], 3, 1)}`, 146 | `Current Hashes ${hashes[0].toLocaleString('en')} / Current Hash Capacity ${hashes[1].toLocaleString('en')}`) 147 | // Detect and notify the HUD if we are liquidating hashes (selling them as quickly as possible) 148 | const spendHashesScript = getFilePath('spend-hacknet-hashes.js'); 149 | const liquidatingHashes = await getNsDataThroughFile(ns, 150 | `ns.ps('home').filter(p => p.filename == ns.args[0] && (p.args.includes('--liquidate') || p.args.includes('-l')))`, 151 | '/Temp/hash-liquidation-scripts.txt', [spendHashesScript]); 152 | if (liquidatingHashes.length > 0) 153 | val2.push(true, "Liquidating", `You have a script running that is selling hashes as quickly as possible ` + 154 | `(PID ${liquidatingHashes[0].pid}: ${spendHashesScript} ${liquidatingHashes[0].args.join(' ')})`); 155 | } 156 | } 157 | if (val1.length < 2) val1.push(false); 158 | if (val2.length < 2) val2.push(false); 159 | hudData.push(val1, val2) 160 | } 161 | 162 | { 163 | const val = ["Stock"] 164 | // Show Stocks (only if stockmaster.js isn't already doing the same) 165 | if (!options['hide-stocks'] && !doc.getElementById("stock-display-1")) { 166 | const stkPortfolio = await getStocksValue(ns); 167 | // Also, don't bother showing a section for stock if we aren't holding anything 168 | if (stkPortfolio > 0) val.push(true, formatMoney(stkPortfolio)) 169 | else val.push(false) 170 | } else val.push(false) 171 | hudData.push(val) 172 | } 173 | 174 | // Show total instantaneous script income and experience per second (values provided directly by the game) 175 | const totalScriptInc = await getNsDataThroughFile(ns, 'ns.getTotalScriptIncome()'); 176 | const totalScriptExp = await getNsDataThroughFile(ns, 'ns.getTotalScriptExpGain()'); 177 | hudData.push(["Scr Inc", true, formatMoney(totalScriptInc[0], 3, 2) + '/sec', "Total 'instantaneous' income per second being earned across all scripts running on all servers."]); 178 | hudData.push(["Scr Exp", true, formatNumberShort(totalScriptExp, 3, 2) + '/sec', "Total 'instantaneous' hack experience per second being earned across all scripts running on all servers."]); 179 | 180 | // Show reserved money 181 | { 182 | const val = ["Reserve"] 183 | const reserve = Number(ns.read("reserve.txt") || 0); 184 | if (reserve > 0) { 185 | val.push(true, formatNumberShort(reserve, 3, 2), "Most scripts will leave this much money unspent. Remove with `run reserve.js 0`"); 186 | } else val.push(false) 187 | hudData.push(val) 188 | } 189 | 190 | // needed for gang and karma 191 | const gangInfo = await getGangInfo(ns); 192 | 193 | // Show gang income and territory 194 | { 195 | const val1 = ["Gang Inc"] 196 | const val2 = ["Territory"] 197 | // Gang income is only relevant once gangs are unlocked 198 | if ((2 in dictSourceFiles || 2 == bitNode) && gangInfo) { 199 | // Add Gang Income 200 | val1.push(true, formatMoney(gangInfo.moneyGainRate * 5, 3, 2) + '/sec', 201 | `Gang (${gangInfo.faction}) income per second while doing tasks.` + 202 | `\nIncome: ${formatMoney(gangInfo.moneyGainRate * 5)}/sec (${formatMoney(gangInfo.moneyGainRate)}/tick)` + 203 | ` Respect: ${formatNumberShort(gangInfo.respect)} (${formatNumberShort(gangInfo.respectGainRate)}/tick)` + 204 | `\nNote: If you see 0, your gang may all be temporarily set to training or territory warfare.`); 205 | // Add Gang Territory 206 | val2.push(true, formatNumberShort(gangInfo.territory * 100, 4, 2) + "%", 207 | `How your gang is currently doing in territory warfare. Starts at 14.29%\n` + 208 | `Gang: ${gangInfo.faction} ${gangInfo.isHacking ? "(Hacking)" : "(Combat)"} ` + 209 | `Power: ${gangInfo.power.toLocaleString('en')} Clash ${gangInfo.territoryWarfareEngaged ? "enabled" : "disabled"} ` + 210 | `(${(gangInfo.territoryClashChance * 100).toFixed(0)}% chance)`); 211 | } else { 212 | val1.push(false) 213 | val2.push(false) 214 | } 215 | hudData.push(val1, val2) 216 | } 217 | 218 | // Show Karma if we're not in a gang yet 219 | { 220 | const val = ["Karma"] 221 | const karma = ns.heart.break(); 222 | // Don't spoiler Karma if they haven't started doing crime yet 223 | if (karma <= -9 224 | // If in a gang, you know you have oodles of bad Karma. Save some space 225 | && !gangInfo) { 226 | let karmaShown = formatNumberShort(karma, 3, 2); 227 | if (2 in dictSourceFiles && 2 != bitNode && !gangInfo) karmaShown += '/54k'; // Display karma needed to unlock gangs ouside of BN2 228 | val.push(true, karmaShown, "After Completing BN2, you need -54,000 Karma in other BNs to start a gang. You also need a tiny amount to join some factions. The most is -90 for 'The Syndicate'"); 229 | } else val.push(false) 230 | hudData.push(val) 231 | } 232 | 233 | // Show number of kills if explicitly enabled 234 | { 235 | const val = ["Kills"] 236 | if (options['show-peoplekilled']) { 237 | const playerInfo = await getNsDataThroughFile(ns, 'ns.getPlayer()'); 238 | const numPeopleKilled = playerInfo.numPeopleKilled; 239 | val.push(true, formatSixSigFigs(numPeopleKilled), "Count of successful Homicides. Note: The most kills you need is 30 for 'Speakers for the Dead'"); 240 | } else val.push(false) 241 | hudData.push(val) 242 | } 243 | 244 | // Show Bladeburner Rank and Skill Points 245 | { 246 | const val1 = ["BB Rank"] 247 | const val2 = ["BB SP"] 248 | // Bladeburner API unlocked 249 | if ((7 in dictSourceFiles || 7 == bitNode) 250 | // Check if we're in bladeburner. Once we find we are, we don't have to check again. 251 | && (playerInBladeburner = playerInBladeburner || await getNsDataThroughFile(ns, 'ns.bladeburner.inBladeburner()'))) { 252 | const bbRank = await getNsDataThroughFile(ns, 'ns.bladeburner.getRank()'); 253 | const bbSP = await getNsDataThroughFile(ns, 'ns.bladeburner.getSkillPoints()'); 254 | val1.push(true, formatSixSigFigs(bbRank), "Your current bladeburner rank"); 255 | val2.push(true, formatSixSigFigs(bbSP), "Your current unspent bladeburner skill points"); 256 | } else { 257 | val1.push(false) 258 | val2.push(false) 259 | } 260 | hudData.push(val1, val2) 261 | } 262 | 263 | // Show various server / RAM utilization stats 264 | { 265 | const val1 = ["Servers"] 266 | const val2 = ["Home RAM"] 267 | const val3 = ["All RAM"] 268 | if (!options['hide-RAM-utilization']) { 269 | const servers = await getAllServersInfo(ns); 270 | const hnServers = servers.filter(s => s.hostname.startsWith("hacknet-server-") || s.hostname.startsWith("hacknet-node-")); 271 | const nRooted = servers.filter(s => s.hasAdminRights).length; 272 | const nPurchased = servers.filter(s => s.hostname != "home" && s.purchasedByPlayer).length; // "home" counts as purchased by the game 273 | // Add Server count. 274 | val1.push(true, `${servers.length}/${nRooted}/${nPurchased}`, `The number of servers on the network (${servers.length}) / ` + 275 | `number rooted (${nRooted}) / number purchased ` + (hnServers.length > 0 ? 276 | `(${nPurchased - hnServers.length} servers + ${hnServers.length} hacknet servers)` : `(${nPurchased})`)); 277 | const home = servers.find(s => s.hostname == "home"); 278 | // Add Home RAM and Utilization 279 | val2.push(true, `${formatRam(home.maxRam)} ${(100 * home.ramUsed / home.maxRam).toFixed(1)}%`, 280 | `Shows total home RAM (and current utilization %)\nDetails: ${home.cpuCores} cores and using ` + 281 | `${formatRam(home.ramUsed, true)} of ${formatRam(home.maxRam, true)} (${formatRam(home.maxRam - home.ramUsed, true)} free)`); 282 | // If the user has any scripts running on hacknet servers, assume they want them included in the main "total available RAM" stat 283 | const includeHacknet = hnServers.some(s => s.ramUsed > 0); 284 | const fileredServers = servers.filter(s => s.hasAdminRights && !hnServers.includes(s)); 285 | const [sMax, sUsed] = fileredServers.reduce(([tMax, tUsed], s) => [tMax + s.maxRam, tUsed + s.ramUsed], [0, 0]); 286 | const [hMax, hUsed] = hnServers.reduce(([tMax, tUsed], s) => [tMax + s.maxRam, tUsed + s.ramUsed], [0, 0]); 287 | const [tMax, tUsed] = [sMax + hMax, sUsed + hUsed]; 288 | let statText = includeHacknet ? 289 | `${formatRam(tMax)} ${(100 * tUsed / tMax).toFixed(1)}%` : 290 | `${formatRam(sMax)} ${(100 * sUsed / sMax).toFixed(1)}%`; 291 | let toolTip = `Shows the sum-total RAM and utilization across all rooted hosts on the network` + (9 in dictSourceFiles || 9 == bitNode ? 292 | (includeHacknet ? "\n(including hacknet servers, because you have scripts running on them)" : " (excluding hacknet servers)") : "") + 293 | `\nUsing ${formatRam(tUsed, true)} of ${formatRam(tMax, true)} (${formatRam(tMax - tUsed, true)} free) across all servers`; 294 | if (hMax > 0) toolTip += 295 | `\nUsing ${formatRam(sUsed, true)} of ${formatRam(sMax, true)} (${formatRam(sMax - sUsed, true)} free) excluding hacknet` + 296 | `\nUsing ${formatRam(hUsed, true)} of ${formatRam(hMax, true)} (${formatRam(hMax - hUsed, true)} free) of hacknet servers`; 297 | // Add Total Network RAM and Utilization 298 | val3.push(true, statText, toolTip); 299 | } else { 300 | val1.push(false) 301 | val2.push(false) 302 | val3.push(false) 303 | } 304 | hudData.push(val1, val2, val3) 305 | } 306 | 307 | // Show current share power 308 | { 309 | const val = ["Share Pwr"] 310 | const sharePower = await getNsDataThroughFile(ns, 'ns.getSharePower()'); 311 | // Bitburner bug: Trace amounts of share power sometimes left over after we stop sharing 312 | if (sharePower > 1.0001) { 313 | val.push(true, formatNumberShort(sharePower, 3, 2), 314 | "Uses RAM to boost faction reputation gain rate while working for factions (tapers off at ~1.65) " + 315 | "\nRun `daemon.js` with the `--no-share` flag to disable."); 316 | } else val.push(false) 317 | hudData.push(val) 318 | } 319 | 320 | return hudData 321 | } 322 | 323 | /** @param {number} value 324 | * @returns {string} The number formatted as a string with up to 6 significant digits, but no more than the specified number of decimal places. */ 325 | function formatSixSigFigs(value, minDecimalPlaces = 0, maxDecimalPlaces = 0) { 326 | return value >= 1E7 ? formatNumberShort(value, 6, 3) : 327 | value.toLocaleString(undefined, { minimumFractionDigits: minDecimalPlaces, maximumFractionDigits: maxDecimalPlaces }); 328 | } 329 | 330 | /** @param {NS} ns 331 | * @returns {Promise} Gang information, if we're in a gang, or False */ 332 | async function getGangInfo(ns) { 333 | return await getNsDataThroughFile(ns, 'ns.gang.inGang() ? ns.gang.getGangInformation() : false', '/Temp/gang-stats.txt') 334 | } 335 | 336 | /** @param {NS} ns 337 | * @returns {Promise} **/ 338 | async function getAllServersInfo(ns) { 339 | const serverNames = await getNsDataThroughFile(ns, 'scanAllServers(ns)'); 340 | return await getNsDataThroughFile(ns, 'ns.args.map(ns.getServer)', '/Temp/getServers.txt', serverNames); 341 | } 342 | 343 | /** Inject the CSS that controls how custom HUD elements are displayed. */ 344 | function addCSS(doc) { 345 | let priorCss = doc.getElementById("statsCSS"); 346 | if (priorCss) priorCss.parentNode.removeChild(priorCss); // Remove old CSS to facilitate tweaking css above 347 | // Hopefully this logic remains valid for detecting which element is the HUD draggable window 348 | const hudParent = doc.getElementsByClassName(`MuiCollapse-root`)[0].parentElement; 349 | if (hudParent) hudParent.style.zIndex = 1E4; // Tail windows start around 1500, this should keep the HUD above them 350 | doc.head.insertAdjacentHTML('beforeend', css(hudParent ? eval('window').getComputedStyle(hudParent) : null)); 351 | } 352 | const css = (rootStyle) => ``; 365 | -------------------------------------------------------------------------------- /sync-scripts.js: -------------------------------------------------------------------------------- 1 | // This is a proof-of-concept script that can continuously push changes to scripts on your home server to all other servers. 2 | // Run this script once to push the latest version of your scripts any other servers that have a copy. 3 | // Warning: If you keep try to edit and save a file while this script is running, it will probably crash your game the first time you save a file. 4 | const loopingMode = false; 5 | const home = "home"; 6 | 7 | /** @param {NS} ns */ 8 | export async function main(ns) { 9 | let scan = (server, parent) => ns.scan(server) 10 | .map(newServer => newServer != parent ? scan(newServer, server) : server).flat(); 11 | ["scan", "scp"].forEach(log => ns.disableLog(log)); 12 | const serverList = scan(home); 13 | do { 14 | const fileList = ns.ls(home); 15 | const latestContents = Object.fromEntries(fileList.map(s => [s, ns.read(s)])); 16 | for (const server of serverList.filter(s => s != home)) { 17 | const serverFiles = ns.ls(server); // What files does the server have 18 | for (const file of serverFiles.filter(s => fileList.includes(s))) { 19 | await ns.scp(file, home, server); // No way to read a remote file, so we have to temporarily copy it home 20 | if (ns.read(file) != latestContents[file]) { // Remote file was out of date. 21 | ns.print(`The file ${file} was out of date on ${server}. Updating...`); 22 | await ns.write(file, latestContents[file], "w"); // Restore original home file 23 | await ns.scp(file, server, home); // Update the remote copy 24 | const runningInstances = ns.ps(server).filter(p => p.filename == file); 25 | runningInstances.forEach(p => { // Restart any running instances 26 | ns.print(`Restarting script ${file} on ${server} (was running with pid ${p.pid})...`); 27 | ns.kill(p.pid); 28 | ns.exec(p.filename, server, p.threads, ...p.args); 29 | }) 30 | } 31 | } 32 | } 33 | if (loopingMode) await ns.sleep(1000); 34 | } while (loopingMode); 35 | } --------------------------------------------------------------------------------