Updated everything

This commit is contained in:
Zakary Timson 2022-04-20 11:32:10 -04:00
parent 322c5f72d4
commit 3df5f1857c
17 changed files with 8419 additions and 1063 deletions

274
README.md
View File

@ -101,18 +101,6 @@ Commands:
start Start this device as the swarm manager start Start this device as the swarm manager
``` ```
#### Examples
```bash
# Start the manager
run scripts/botnet-manager.js start
# Add a single server to the botnet
run scripts/botnet-manager.js join --device n00dles
# Add all rooted servers to the botnet
run scripts/crawler.js -r /scripts/botnet-manager.js join --device {{TARGET}}
# Distribute & run a script on the entire botnet network
run scripts/botnet-manager.js run /scripts/miner.js n00dles
```
### [connect.js](./scripts/connect.js) ### [connect.js](./scripts/connect.js)
**RAM:** 1.85 GB **RAM:** 1.85 GB
@ -124,26 +112,19 @@ you some time.
Running script with 1 thread(s), pid 1 and args: ["--help"]. Running script with 1 thread(s), pid 1 and args: ["--help"].
/scripts/connect.js: /scripts/connect.js:
Search the network for a device and connect to it. Connect to a server anywhere in the network without a backdoor.
Usage: run connect.js DEVICE Usage: run connect.js [OPTIONS] SERVER
run connect.js --help run connect.js --help
DEVICE Device to connect to SERVER Server to connect to
Options: Options:
-h --help Display this help message -h, --help Display this help message
```
#### Examples
```bash
# Connect to a server without knowing where it is
run scripts/connect.js CSEC
run scripts/connect.js I.I.I.I
``` ```
### [copy.js](./scripts/copy.js) ### [copy.js](./scripts/copy.js)
**RAM:** 3.50 GB **RAM:** 4.20 GB
Scripts often import other scripts requiring multiple `scp` calls before it can be run on a remote machine. This script Scripts often import other scripts requiring multiple `scp` calls before it can be run on a remote machine. This script
will automatically scan the file being copied for imports & recursively scan & copy the dependencies. Plus it has a will automatically scan the file being copied for imports & recursively scan & copy the dependencies. Plus it has a
@ -153,30 +134,25 @@ fancy animated loading bar.
Running script with 1 thread(s), pid 1 and args: ["--help"]. Running script with 1 thread(s), pid 1 and args: ["--help"].
/scripts/copy.js: /scripts/copy.js:
Copy a file/script to a device along with any dependencies. Copy a file & it's dependencies to a server.
Usage: run copy.js [OPTIONS] FILE DEVICE Usage: run copy.js [OPTIONS] FILE SERVER [ARGS]...
run copy.js --help run copy.js --help
FILE File to copy FILE File to copy
DEVICE Device to copy file(s) to SERVER Server to copy file(s) to
ARGS Arguments to start file/script with
Options: Options:
-n --no-deps Skip copying dependencies -c, --cpu Number of CPU threads to start script with, will use maximum if not specified
-s --silent Surpress program output -e, --execute Start script after copying
-h --help Display this help message -n, --no-deps Skip copying dependencies
``` -q, --quite Suppress program output
-h, --help Display this help message
#### Examples
```bash
# Copy the miner script with it's dependencies
run scripts/copy.js /scripts/miner.js n00dles
# Copy without the animated bar & dependencies
run scripts/copy.js -sn /scripts/miner.js n00dles
``` ```
### [crawler.js](./scripts/crawler.js) ### [crawler.js](./scripts/crawler.js)
**RAM:** 4.15 GB **RAM:** 5.80 GB
Mid-game solution to distribute & run scripts across the network. Mid-game solution to distribute & run scripts across the network.
``` ```
@ -184,40 +160,30 @@ Mid-game solution to distribute & run scripts across the network.
Running script with 1 thread(s), pid 1 and args: ["--help"]. Running script with 1 thread(s), pid 1 and args: ["--help"].
/scripts/crawler.js: /scripts/crawler.js:
Search the network for targets to execute a script against. Search the network for servers to execute a script against.
Usage: run crawler.js [OPTIONS] SCRIPT [ARGS]... Usage: run crawler.js [OPTIONS] SCRIPT [ARGS]...
run crawler.js --help run crawler.js --help
SCRIPT Script to copy & execute SCRIPT Script to copy & execute
ARGS Arguments for script. Forward the current target with: {{TARGET}} ARGS Arguments for script. Forward the discovered server with: {{SERVER}}
Options: Options:
-c --cpu Number of CPU threads to use with script -c, --cpu Number of CPU threads to start script with, will use maximum if not specified
-d --depth Depth to scan to, defaults to 3 -d, --depth Depth to scan to, defaults to 3
-k --kill Kill all scripts running on device -k, --kill Kill all running scripts on the server
-l --level Exclude targets with higher hack level, defaults to current hack level --level Skip servers with higher hack level, defaults to current hack level
-e --remote-exec Copy script to remote device & run there -e, --remote-exec Execute script on remote server
-r --rooted Filter to devices that have been rooted -r, --rooted Only servers that have been rooted
-n --not-rooted Filter to devices that have not been rooted -n, --not-rooted Only servers that have not been rooted
-p --ports Exclude targets with too many closed ports -p, --ports Skip servers with too many closed ports
-s --silent Suppress program output -q, --quite Suppress program output
-v --verbose Display the device names in the final report -v, --verbose Display the server names in the final report
-h --help Display this help message -h, --help Display this help message
```
#### Examples
```bash
# Run a command on the local machine targeting discovered devices
run scripts/crawler.js -n /scripts/rootkit.js {{TARGET}}
# Chain the miner to the rootkit to automatically deploy it
run scripts/crawler.js -n /scripts/rootkit.js {{TARGET}} /scripts/miner.js
# Deploy a script on rooted devices
run scripts/crawler.js -re /scripts/miner.js n00dles
``` ```
### [find-target.js](./scripts/find-target.js) ### [find-target.js](./scripts/find-target.js)
**RAM:** 6.00 GB **RAM:** 4.05 GB
A utility to help figure out which server is worth hacking the most. It does this by estimating the financial yield per A utility to help figure out which server is worth hacking the most. It does this by estimating the financial yield per
minute for each server & returns the servers in a sorted list. minute for each server & returns the servers in a sorted list.
@ -226,25 +192,17 @@ minute for each server & returns the servers in a sorted list.
Running script with 1 thread(s), pid 1 and args: ["--help"]. Running script with 1 thread(s), pid 1 and args: ["--help"].
/scripts/find-target.js: /scripts/find-target.js:
Scan the network for the best device(s) to mine. Scan the network for the best servers(s) to hack.
Usage: run find-target.js [OPTIONS] Usage: run find-target.js [OPTIONS]
run find-target.js --help run find-target.js --help
Options: Options:
-c --count Number of devices to return in order from best to worst -c, --count Number of servers to return
-r --rooted Filter to devices that have been rooted -r, --rooted Only servers that have been rooted
-n --not-rooted Filter to devices that have not been rooted -n, --not-rooted Only servers that have not been rooted
-v --verbose Display the estimated income per minute per core -v, --verbose Display the estimated income per minute per core
-h --help Display this help message -h, --help Display this help message
```
#### Examples
```bash
# Rank all the servers on the network
run scripts/find-target.js -v
# Best server currently rooted
run scripts/find-target.js -rc 1
``` ```
### [hacknet-manager.js](./scripts/hacknet-manager.js) ### [hacknet-manager.js](./scripts/hacknet-manager.js)
@ -261,21 +219,13 @@ Buy, upgrade & manage Hacknet nodes automatically. Tail for live updates.
Usage: run hacknet-manager.js [OPTIONS] [LIMIT] Usage: run hacknet-manager.js [OPTIONS] [LIMIT]
run hacknet-manager.js --help run hacknet-manager.js --help
LIMIT Limit the number of nodes the manager will buy, defaults to 8 LIMIT Limit the number of nodes the manager will buy, defaults to 8 or the current number of nodes
Options: Options:
-a --auto-limit Automatically increase the node limit when there is nothing to do -a, --auto-limit Automatically increase the node limit when there is nothing to do
-b --balance Prevent spending bellow point -b, --balance Prevent spending bellow point
-s --sleep Amount of time to wait between purchases, defaults to 1 (second) -s, --sleep Amount of time to wait between purchases, defaults to 1 (second)
-h --help Display this help message -h, --help Display this help message
```
#### Examples
```bash
# Start the manager to 8 nodes & prevent spending while we have less than $1 million
run scripts/hacknet-manager.js -b 1E6 8
# Let the hacknet manage & grow itself
run scripts/hacknet-manager.js -a
``` ```
### [miner.js](./scripts/miner.js) ### [miner.js](./scripts/miner.js)
@ -288,78 +238,54 @@ or they can all target the server with the most money which is more efficient (s
Running script with 1 thread(s), pid 1 and args: ["--help"]. Running script with 1 thread(s), pid 1 and args: ["--help"].
/scripts/miner.js: /scripts/miner.js:
Weaken, Grow, Hack loop to "mine" device for money. Tail for live updates. Buy, upgrade & manage Hacknet nodes automatically. Tail for live updates.
Usage: run miner.js [DEVICE] Usage: run hacknet-manager.js [OPTIONS] [LIMIT]
run miner.js --help run hacknet-manager.js --help
DEVICE Device to mine, defaults to current machine LIMIT Limit the number of nodes the manager will buy, defaults to 8 or the current number of nodes
Options: Options:
-h --help Display this help message -a, --auto-limit Automatically increase the node limit when there is nothing to do
``` -b, --balance Prevent spending bellow point
-s, --sleep Amount of time to wait between purchases, defaults to 1 (second)
#### Examples -h, --help Display this help message
```bash
# Use home to hack another server
run scripts/miner.js n00dles
# Make remote server hack itself
run scripts/rootkit.js noodles /scripts/miner.js
# Make remote server hack another remote server
run scripts/rootkit.js noodles /scripts/miner.js foodnstuff
# Distribute the miner on entire network to hack a single server
run scripts/crawler.js /scripts/rootkit.js {{TARGET}} /scripts/miner.js foodnstuff
``` ```
### [network-graph.js](./scripts/network-graph.js) ### [network-graph.js](./scripts/network-graph.js)
**RAM:** 3.85 GB **RAM:** 3.85 GB
A utility to scan the network & build a visual tree of all the devices. It comes with lots of flags to narrow down the A utility to scan the network & build a visual tree of all the devices. It comes with lots of flags to narrow down the
results. It's useful for figuring out where you are, manually finding targets & discovering the path to a server. results. It's useful for figuring out where you are, manually finding targets & discovering the path to a server &
connecting to it.
``` ```
[home ~/]> run /scripts/network-graph.js --help [home ~/]> run /scripts/network-graph.js --help
Running script with 1 thread(s), pid 1 and args: ["--help"]. Running script with 1 thread(s), pid 1 and args: ["--help"].
/scripts/network-graph.js: /scripts/network-graph.js:
Scan the network for devices and display as an ASCII tree: Scan the network for servers and display as an ASCII tree. Servers with root access are highlighted & bold. Click to
home automatically connect.
├─ n00dles (ROOTED)
| └─ max-hardware (80|1)
| └─ neo-net (50|1)
├─ foodnstuff (ROOTED)
└─ sigma-cosmetics (ROOTED)
Usage: run network-graph.js [OPTIONS] [DEVICE] Usage: run network-graph.js [OPTIONS] [SERVER]
run network-graph.js --help run network-graph.js --help
DEVICE Point to start scan from, defaults to current machine SERVER Point to start scan from, defaults to local server
Options: Options:
-d --depth Depth to scan to -d, --depth Depth to scan to
-f --filter Filter to device matching name -f, --filter Filter to servers matching name
-e --regex Filter to devices matching pattern -e, --regex Filter to servers matching pattern
-r --rooted Filter to devices that have been rooted -l, --level Display the required hack level & number of ports to root: [level|port]
-n --not-rooted Filter to devices that have not been rooted -n, --not-rooted Filter to servers that have not been rooted
-v --verbose Display the required hack level & number of ports to root: (level|port) -r, --rooted Filter to servers that have been rooted
-h --help Display this help message -s, --specs Display the server specifications: {CPU|RAM}
``` -u, --usage Display the server utilization: (USG%)
-v, --verbose Display level, specs & usage in that order: [HL|P] {CPU|RAM} (USG%)
#### Example -h, --help Display this help message
```bash
# Show the entire network
run scripts/network-graph.js -v
# Show servers within 3 hops that still need to be rooted
run scripts/network-graph.js -nvd 3
# Show servers you have rooted
run scripts/network-graph.js -r
# Find a specific server
run scripts/network-graph.js -f CSEC
``` ```
### [rootkit.js](./scripts/rootkit.js) ### [rootkit.js](./scripts/rootkit.js)
**RAM:** 5.05 GB <small>(Can be reduced to 4.80 GB)</small> **RAM:** 4.65 GB
Programs can be commented out to lower the cost of running.
Automatically gains root on the local or remote server. A script can be passed as an additional argument; it will be Automatically gains root on the local or remote server. A script can be passed as an additional argument; it will be
copied and automatically executed with the maximum number of threads after being rooted. copied and automatically executed with the maximum number of threads after being rooted.
@ -368,57 +294,53 @@ copied and automatically executed with the maximum number of threads after being
Running script with 1 thread(s), pid 1 and args: ["--help"]. Running script with 1 thread(s), pid 1 and args: ["--help"].
/scripts/rootkit.js: /scripts/rootkit.js:
Automatically gain root access to a device. A file can also be uploaded & executed. Scan the network for servers and display as an ASCII tree. Servers with root access are highlighted & bold.
Usage: run rootkit.js [OPTIONS] [DEVICE] [SCRIPT] [ARGS]... Usage: run network-graph.js [OPTIONS] [SERVER]
run rootkit.js --help run network-graph.js --help
DEVICE Device to root, defaults to current machine SERVER Point to start scan from, defaults to local server
SCRIPT Script to copy & execute
ARGS Arguments for script. Forward the current target with: {{TARGET}}
Options: Options:
-c --cpu Number of CPU threads to use with script -d, --depth Depth to scan to
-s --silent Surpress program output -f, --filter Filter to servers matching name
-h --help Display this help message -e, --regex Filter to servers matching pattern
``` -l, --level Display the required hack level & number of ports to root: [level|port]
-n, --not-rooted Filter to servers that have not been rooted
#### Examples -r, --rooted Filter to servers that have been rooted
```bash -s, --specs Display the server specifications: {CPU|RAM}
# Hack a remote server -u, --usage Display the server utilization: (USG%)
run scripts/rootkit.js n00dles -v, --verbose Display level, specs & usage in that order: [HL|P] {CPU|RAM} (USG%)
# Start the miner after hacking -h, --help Display this help message
run scripts/rootkit.js n00dles /scripts/miner.js foodnstuff
``` ```
### [server-manager.js (WIP)](./scripts/server-manager.js) ### [server-manager.js (WIP)](./scripts/server-manager.js)
**RAM:** 9.35 GB **RAM:** 11.35 GB
Early game script to handle purchasing and upgrading servers for more computer power. Mid-game script to handle purchasing and upgrading servers for more computer power. You can also set a script to run
automatically after purchase. Useful to chain with `miner.js` or `botnet.js`.
``` ```
[home ~/]> run /scripts/server-manager.js --help [home ~/]> run /scripts/server-manager.js --help
Running script with 1 thread(s), pid 1 and args: ["--help"]. Running script with 1 thread(s), pid 1 and args: ["--help"].
/scripts/server-manager.js: /scripts/server-manager.js:
Automate the buying & upgrading of servers. Automate the buying & upgrading of servers. Automatically starts script after purchase. Tail for live updates.
Usage: run server-manager.js [OPTIONS] Usage: run server-manager.js [OPTIONS] [SCRIPT] [ARGS]...
run server-manager.js --help run server-manager.js --help
SCRIPT Script to copy & execute
ARGS Arguments for script. Forward the discovered server with: {{SERVER}}
Options: Options:
-b, --balance Prevent spending bellow point -b, --balance Prevent spending bellow point
-c, --cpu Number of CPU threads to start script with, will use maximum if not specified
-l, --limit Limit the number of servers that can be purchased, defaults to 25 -l, --limit Limit the number of servers that can be purchased, defaults to 25
-r, --ram Amount of RAM to purchase new servers with, defaults to 8 GB -r, --ram Amount of RAM to purchase new servers with, defaults to 8 GB
-s, --sleep Amount of time to wait between purchases, defaults to 1 (second) -s, --sleep Amount of time to wait between purchases, defaults to 1 (second)
-h, --help Display this help message -h, --help Display this help message
``` ```
#### Examples
```bash
# Start automatically purchasing & upgrading servers
run scripts/server-manager.js --ram 16
```
### [update.js](./scripts/update.js) ### [update.js](./scripts/update.js)
**RAM:** 2.65 GB **RAM:** 2.65 GB
@ -434,18 +356,10 @@ Download the latest script updates from the repository using wget.
Usage: run update.js [OPTIONS] [DEVICE] Usage: run update.js [OPTIONS] [DEVICE]
run update.js --help run update.js --help
DEVICE Device to update, defaults to current machine DEVICE Device to update, defaults to current machine
Options: Options:
--skip-self Skip updating self (for debugging & used internally) --skip-self Skip updating self (for debugging & used internally)
--no-banner Hide the banner (Used internally) --no-banner Hide the banner (Used internally)
-h --help Display this help message -h, --help Display this help message
```
#### Examples
```bash
# Download the scripts to local computer
run scripts/update.js
# Download scripts to a remote computer
run scripts/update.js n00dles
``` ```

7249
index.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import {ArgParser} from '/scripts/lib/arg-parser2'; import {ArgParser} from '/scripts/lib/arg-parser';
import {Logger} from '/scripts/lib/logger'; import {Logger} from '/scripts/lib/logger';
import {copyWithDependencies} from '/scripts/lib/utils'; import {copyWithDependencies} from '/scripts/copy';
class Manager { class Manager {
running; running;
@ -125,32 +125,32 @@ export async function main(ns) {
// Setup // Setup
ns.disableLog('ALL'); ns.disableLog('ALL');
const hostname = ns.getHostname(), portNum = 1; const hostname = ns.getHostname(), portNum = 1;
const argParser = new ArgParser(ns, 'botnet-manager.js', 'Connect & manage a network of devices to launch distributed attacks.', [ const argParser = new ArgParser('botnet-manager.js', 'Connect & manage a network of devices to launch distributed attacks.', [
new ArgParser(ns, 'copy', 'Copy file & dependencies to swarm nodes', [ new ArgParser('copy', 'Copy file & dependencies to swarm nodes', [
{name: 'file', desc: 'File to copy', default: false}, {name: 'file', desc: 'File to copy', default: false},
{name: 'manager', desc: 'Copy to manager node', flags: ['-m', '--manager'], default: false}, {name: 'manager', desc: 'Copy to manager node', flags: ['-m', '--manager'], default: false},
{name: 'noDeps', desc: 'Skip copying dependencies', flags: ['-d', '--no-deps'], default: false}, {name: 'noDeps', desc: 'Skip copying dependencies', flags: ['-d', '--no-deps'], default: false},
{name: 'workers', desc: 'Copy to worker nodes', flags: ['-w', '--workers'], default: false}, {name: 'workers', desc: 'Copy to worker nodes', flags: ['-w', '--workers'], default: false},
]), ]),
new ArgParser(ns, 'join', 'Connect device as a worker node to the swarm', [ new ArgParser('join', 'Connect device as a worker node to the swarm', [
{name: 'device', desc: 'Device to connect, defaults to the current machine', optional: true, default: hostname} {name: 'device', desc: 'Device to connect, defaults to the current machine', optional: true, default: hostname}
]), ]),
new ArgParser(ns, 'kill', 'Kill any scripts running on worker nodes'), new ArgParser('kill', 'Kill any scripts running on worker nodes'),
new ArgParser(ns, 'leave', 'Disconnect worker node from swarm', [ new ArgParser('leave', 'Disconnect worker node from swarm', [
{name: 'device', desc: 'Device to disconnect, defaults to the current machine', optional: true, default: hostname} {name: 'device', desc: 'Device to disconnect, defaults to the current machine', optional: true, default: hostname}
]), ]),
new ArgParser(ns, 'run', 'Copy & run script on all worker nodes', [ new ArgParser('run', 'Copy & run script on all worker nodes', [
{name: 'script', desc: 'Script to copy & execute', type: 'string'}, {name: 'script', desc: 'Script to copy & execute', type: 'string'},
{name: 'args', desc: 'Arguments for script. Forward the current target with: {{TARGET}}', optional: true, extras: true}, {name: 'args', desc: 'Arguments for script. Forward the current target with: {{TARGET}}', optional: true, extras: true},
]), ]),
new ArgParser(ns, 'start', 'Start this device as the swarm manager'), new ArgParser('start', 'Start this device as the swarm manager'),
{name: 'silent', desc: 'Suppress program output', flags: ['-s', '--silent'], default: false}, {name: 'silent', desc: 'Suppress program output', flags: ['-s', '--silent'], default: false},
]); ]);
const args = argParser.parse(ns.args); const args = argParser.parse(ns.args);
// Help // Help
if(args['help'] || args['_error']) if(args['help'] || args['_error'].length)
return ns.tprint(argParser.help(args['help'] ? null : args['_error'], args['_command'])); return ns.tprint(argParser.help(args['help'] ? null : args['_error'][0], args['_command']));
// Run command // Run command
if(args['_command'] == 'start') { // Start botnet manager if(args['_command'] == 'start') { // Start botnet manager

View File

@ -1,39 +1,69 @@
import {ArgError, ArgParser} from '/scripts/lib/arg-parser'; import {ArgParser} from '/scripts/lib/arg-parser';
import {pruneTree, scanNetwork, terminal} from '/scripts/lib/utils'; import {pruneTree, terminal} from '/scripts/lib/utils';
import {scanNetwork} from '/scripts/crawler';
/** /**
* BitBurner autocomplete * Create connection string to get from one server to another.
* @param data {server: string[], txts: string[], scripts: string[], flags: string[]} - Contextual information *
* @param {NS} ns - BitBurner API
* @param {string} end - Server to find path to
* @param {string} start - Starting point, defaults to the local server
* @returns {string} - Connection string: "home;connect n00dles;connect CSEC;"
*/
export function connectionString(ns, end, start = ns.getHostname()) {
let path = pathToServer(ns, end, start);
const homeShortcut = path.indexOf('home');
if(homeShortcut) path = path.slice(homeShortcut);
return path.map((p, i) => p == 'home' && i == 0 ? 'home' : 'connect ' + p).join(';');
}
/**
* Find path from one server to another.
*
* @param {NS} ns - BitBurner API
* @param {string} end - Server to find path to
* @param {string} start - Starting point, defaults to the local server
* @returns {string[]} - Path to get to server
*/
export function pathToServer(ns, end, start = ns.getHostname()) {
if(start == end) return [end];
const [ignore, network] = scanNetwork(ns, start);
pruneTree(network, d => d == end);
let current = network, name, path = [start];
while(name = Object.keys(current)[0]) {
path.push(name);
current = current[name];
}
return path;
}
/**
* Connect to a server anywhere in the network without a backdoor.
*
* @param {NS} ns - BitBurner API
*/
export async function main(ns) {
// Setup
ns.disableLog('ALL');
const argParser = new ArgParser('connect.js', 'Connect to a server anywhere in the network without a backdoor.', [
{name: 'server', desc: 'Server to connect to'}
]);
const args = argParser.parse(ns.args);
// Help
if(args['help'] || args['_error'].length)
return ns.tprint(argParser.help(args['help'] ? null : args['_error'][0], args['_command']));
// Run
await terminal(connectionString(ns, args['server']));
}
/**
* BitBurner autocomplete.
*
* @param {{servers: string[], txts: string[], scripts: string[], flags: string[]}} data - Contextual information
* @returns {string[]} - Pool of autocomplete options * @returns {string[]} - Pool of autocomplete options
*/ */
export function autocomplete(data) { export function autocomplete(data) {
return [...data.servers]; return [...data.servers];
} }
/**
* Search the network for a device and connect to it.
* @param ns {NS} - BitBurner API
*/
export async function main(ns) {
// Setup
ns.disableLog('ALL');
const argParser = new ArgParser('connect.js', 'Search the network for a device and connect to it.', null, [
{name: 'device', desc: 'Device to connect to', default: ns.getHostname(), type: 'string'}
]);
try {
// Run
const args = argParser.parse(ns.args);
const [devices, network] = scanNetwork(ns);
pruneTree(network, d => d == args['device']);
let current = network, name, path = [];
while(name = Object.keys(current)[0]) {
current = current[name];
path.push(name);
}
await terminal('home; ' + path.map(p => `connect ${p}`).join('; '));
} catch(err) {
if(err instanceof ArgError) return ns.tprint(argParser.help(err.message));
throw err;
}
}

View File

@ -1,61 +1,107 @@
import {ArgError, ArgParser} from '/scripts/lib/arg-parser'; import {ArgParser} from '/scripts/lib/arg-parser';
import {copyWithDependencies, progressBar} from '/scripts/lib/utils'; import {maxThreads, progressBar} from '/scripts/lib/utils';
/** /**
* BitBurner autocomplete * Copy a file & it's dependencies to a server.
* @param data {server: string[], txts: string[], scripts: string[], flags: string[]} - Contextual information *
* @param {NS} ns - BitBurner API
* @param {string} src - File to scan & copy
* @param {string} server - Device to copy files to
* @returns {Promise<string[]>} - Array of copied files
*/
export async function copyWithDependencies(ns, src, server) {
const queue = [src], found = [src];
while(queue.length) {
const file = queue.splice(0, 1)[0];
const imports = new RegExp(/from ["']\.?(\/.+)["']/g);
const script = await ns.read(file);
let match;
while((match = imports.exec(script)) != null) {
const path = `${match[1]}.js`;
if(!found.includes(path)) found.push(path);
queue.push(path);
}
}
await ns.scp(found, server);
return found.reverse();
}
/**
* Copy a file & it's dependencies to a server.
*
* @param {NS} ns - BitBurner API
*/
export async function main(ns) {
// Setup
ns.disableLog('ALL');
const argParser = new ArgParser('copy.js', 'Copy a file & it\'s dependencies to a server.', [
{name: 'file', desc: 'File to copy'},
{name: 'server', desc: 'Server to copy file(s) to'},
{name: 'args', desc: 'Arguments to start file/script with', optional: true, extras: true},
{name: 'cpu', desc: 'Number of CPU threads to start script with, will use maximum if not specified', flags: ['-c', '--cpu']},
{name: 'execute', desc: 'Start script after copying', flags: ['-e', '--execute'], default: false},
{name: 'noDeps', desc: 'Skip copying dependencies', flags: ['-n', '--no-deps'], default: false},
{name: 'quite', desc: 'Suppress program output', flags: ['-q', '--quite'], default: false},
]);
const args = argParser.parse(ns.args);
// Help
if(args['help'] || args['_error'].length)
return ns.tprint(argParser.help(args['help'] ? null : args['_error'][0], args['_command']));
// Banner
if(!args['quite']) {
ns.tprint('===================================================');
ns.tprint(`Copying: ${args['server']}`);
ns.tprint('===================================================');
ns.tprint('');
ns.tprint('Copying Files:');
await ns.sleep(500);
}
// Copy files & create download bar
if(args['noDeps']) {
await ns.scp(args['file'], args['server']);
if(!args['quite']) await progressBar(ns, args['file']);
} else {
const files = copyWithDependencies(ns, args['file'], args['server']);
if(!args['quite']) {
for(let file of files) {
await progressBar(ns, file);
}
}
}
// Run the script if requested
if(args['execute']) {
const threads = args['cpu'] || maxThreads(ns, args['file'], args['server']) || 1;
if(!args['quite']) {
ns.tprint('');
ns.tprint(`Executing with ${threads} thread${threads > 1 ? 's' : ''}...`);
await ns.sleep(500);
}
ns.killall(args['server']);
const pid = ns.exec(args['file'], args['server'], threads, args['args']);
if(!args['quite']) {
ns.tprint(!!pid ? 'Done!' : 'Failed to start');
ns.tprint('');
}
}
// Done message
if(!args['quite']) {
ns.tprint('');
ns.tprint('Done!');
ns.tprint('');
}
}
/**
* BitBurner autocomplete.
*
* @param {{servers: string[], txts: string[], scripts: string[], flags: string[]}} data - Contextual information
* @returns {string[]} - Pool of autocomplete options * @returns {string[]} - Pool of autocomplete options
*/ */
export function autocomplete(data) { export function autocomplete(data) {
return [...data.servers, ...data.scripts]; return [...data.servers, ...data.scripts];
} }
/** @param {NS} ns **/
export async function main(ns) {
// Setup
ns.disableLog('ALL');
const argParser = new ArgParser('copy.js', 'Copy a file/script to a device along with any dependencies.', null, [
{name: 'file', desc: 'File to copy', type: 'string'},
{name: 'device', desc: 'Device to copy file(s) to', type: 'string'},
{name: 'noDeps', desc: 'Skip copying dependencies', flags: ['-n', '--no-deps'], type: 'bool'},
{name: 'silent', desc: 'Suppress program output', flags: ['-s', '--silent'], type: 'bool'}
], true);
try {
// Run
const args = argParser.parse(ns.args);
// Banner
if(!args['silent']) {
ns.tprint('===================================================');
ns.tprint(`Copying: ${args['device']}`);
ns.tprint('===================================================');
ns.tprint('');
ns.tprint('Copying Files:');
await ns.sleep(500);
}
// Copy files & create download bar
if(args['noDeps']) {
await ns.scp(args['file'], args['device']);
if(!args['silent']) await progressBar(ns, args['file']);
} else {
const files = await copyWithDependencies(ns, args['file'], args['device']);
if(!args['silent']) {
for(let file of files) {
await progressBar(ns, file);
}
}
}
// Done message
if(!args['silent']) {
ns.tprint('');
ns.tprint('Done!');
ns.tprint('');
}
} catch(err) {
if(err instanceof ArgError) return ns.tprint(argParser.help(err.message));
throw err;
}
}

View File

@ -1,95 +1,117 @@
import {ArgError, ArgParser} from '/scripts/lib/arg-parser'; import {ArgParser} from '/scripts/lib/arg-parser';
import {copyWithDependencies, scanNetwork} from '/scripts/lib/utils'; import {availableExploits} from '/scripts/rootkit';
import {copyWithDependencies} from '/scripts/copy';
import {maxThreads} from "/scripts/lib/utils";
/** /**
* BitBurner autocomplete * Scan the network for servers.
* @param data {server: string[], txts: string[], scripts: string[], flags: string[]} - Contextual information *
* @returns {string[]} - Pool of autocomplete options * @param {NS} ns - BitBurner API
* @param {string} server - Entrypoint to network
* @param {number} maxDepth - Depth to scan to
* @returns {[string[], Object]} - A tuple including an array of discovered servers & a tree of the network
*/ */
export function autocomplete(data) { export function scanNetwork(ns, server = ns.getHostname(), maxDepth = Infinity) {
return [...data.scripts, '{{TARGET}}']; let discovered = [server];
function scan (server, depth = 1) {
if(depth > maxDepth) return {};
const localTargets = ns.scan(server).filter(s => !discovered.includes(s));
discovered = [...discovered, ...localTargets];
return localTargets.reduce((acc, s) => ({...acc, [s]: scan(s, depth + 1)}), {});
}
const network = scan(server);
return [discovered.slice(1), network];
} }
/** /**
* Search the network for targets to execute a script against. * Search the network for servers to execute a script against.
* @param ns {NS} - BitBurner API *
* @param {NS} ns - BitBurner API
*/ */
export async function main(ns) { export async function main(ns) {
// Setup // Setup
ns.disableLog('ALL'); ns.disableLog('ALL');
const argParser = new ArgParser('crawler.js', 'Search the network for targets to execute a script against.', null, [ const argParser = new ArgParser('crawler.js', 'Search the network for servers to execute a script against.', [
{name: 'script', desc: 'Script to copy & execute', type: 'string'}, {name: 'script', desc: 'Script to copy & execute'},
{name: 'args', desc: 'Arguments for script. Forward the current target with: {{TARGET}}', optional: true, extras: true, type: 'string'}, {name: 'args', desc: 'Arguments for script. Forward the discovered server with: {{SERVER}}', optional: true, extras: true, type: 'string'},
{name: 'cpu', desc: 'Number of CPU threads to use with script', flags: ['-c', '--cpu'], type: 'num'}, {name: 'cpu', desc: 'Number of CPU threads to start script with, will use maximum if not specified', flags: ['-c', '--cpu']},
{name: 'depth', desc: 'Depth to scan to, defaults to 3', flags: ['-d', '--depth'], default: Infinity, type: 'num'}, {name: 'depth', desc: 'Depth to scan to, defaults to 3', flags: ['-d', '--depth'], default: Infinity},
{name: 'kill', desc: 'Kill all scripts running on device', flags: ['-k', '--kill'], type: 'bool'}, {name: 'kill', desc: 'Kill all running scripts on the server', flags: ['-k', '--kill'], default: false},
{name: 'level', desc: 'Exclude targets with higher hack level, defaults to current hack level', flags: ['--level'], default: ns.getHackingLevel(), type: 'num'}, {name: 'level', desc: 'Skip servers with higher hack level, defaults to current hack level', flags: ['--level'], default: ns.getHackingLevel()},
{name: 'remoteExec', desc: 'Copy script to remote device & run there', flags: ['-e', '--remote-exec'], type: 'bool'}, {name: 'remoteExec', desc: 'Execute script on remote server', flags: ['-e', '--remote-exec'], default: false},
{name: 'rooted', desc: 'Filter to devices that have been rooted', flags: ['-r', '--rooted'], type: 'bool'}, {name: 'rooted', desc: 'Only servers that have been rooted', flags: ['-r', '--rooted'], default: false},
{name: 'notRooted', desc: 'Filter to devices that have not been rooted', flags: ['-n', '--not-rooted'], type: 'bool'}, {name: 'notRooted', desc: 'Only servers that have not been rooted', flags: ['-n', '--not-rooted'], default: false},
{name: 'ports', desc: 'Exclude targets with too many closed ports', flags: ['-p', '--ports'], default: Infinity, type: 'num'}, {name: 'ports', desc: 'Skip servers with too many closed ports', flags: ['-p', '--ports'], default: availableExploits(ns).length},
{name: 'silent', desc: 'Suppress program output', flags: ['-s', '--silent'], type: 'bool'}, {name: 'quite', desc: 'Suppress program output', flags: ['-q', '--quite'], default: false},
{name: 'verbose', desc: 'Display the device names in the final report', flags: ['-v', '--verbose'], type: 'bool'}, {name: 'verbose', desc: 'Display the server names in the final report', flags: ['-v', '--verbose'], default: false},
], true); ]);
const args = argParser.parse(ns.args);
try { // Help
// Run if(args['help'] || args['_error'].length)
const localhost = ns.getHostname(); return ns.tprint(argParser.help(args['help'] ? null : args['_error'][0], args['_command']));
const args = argParser.parse(ns.args);
const [devices, network] = scanNetwork(ns);
let complete = [], failed = [], skipped = [];
for(let device of devices) {
// Check root status if needed
const rooted = ns.hasRootAccess(device);
if(args['rooted'] && !rooted) continue;
if(args['notRooted'] && rooted) continue;
// Skip invalid devices // Run
if(device == 'home' || args['level'] < ns.getServerRequiredHackingLevel(device) || args['ports'] < ns.getServerNumPortsRequired(device)) { const localhost = ns.getHostname();
skipped.push(device); const [servers, network] = scanNetwork(ns);
continue; let complete = [], failed = [], skipped = [];
} for(let server of servers) {
// Check root status if needed
const rooted = ns.hasRootAccess(server);
if(args['rooted'] && !rooted) continue;
if(args['notRooted'] && rooted) continue;
// Start script // Skip invalid servers
if(args['kill']) ns.killall(device); if(server == 'home' || args['level'] < ns.getServerRequiredHackingLevel(server) || args['ports'] < ns.getServerNumPortsRequired(server)) {
const scriptArgs = args['args'].map(arg => arg.toUpperCase() == '{{TARGET}}' ? device : arg); skipped.push(server);
const [totalRam, usedRam] = ns.getServerRam(args['remoteExec'] ? device : localhost); continue;
const threads = args['cpu'] || ~~((totalRam - usedRam) / ns.getScriptRam(args['script'], localhost)) || 1;
if(args['remoteExec']) await copyWithDependencies(ns, args['script'], device);
const pid = ns.exec(args['script'], args['remoteExec'] ? device : localhost, threads, ...scriptArgs);
if(pid == 0) {
failed.push(device);
continue;
}
// Wait for script to finish if local
if(!args['remoteExec'])
while(ns.scriptRunning(args['script'], localhost)) await ns.sleep(1000);
complete.push(device);
} }
// Output report // Start script
if(!args['silent']) { if(args['kill']) ns.killall(server);
ns.tprint('==================================================='); const scriptArgs = args['args'].map(arg => arg.toUpperCase() == '{{SERVER}}' ? server : arg);
ns.tprint(`Crawler Report: ${complete.length + failed.length + skipped.length} Devices`); const threads = args['cpu'] || maxThreads(ns, args['script'], server) || 1;
ns.tprint('==================================================='); if(args['remoteExec']) await copyWithDependencies(ns, args['script'], server);
if(args['verbose']) { const pid = ns.exec(args['script'], args['remoteExec'] ? server : localhost, threads, ...scriptArgs);
ns.tprint(`Complete (${complete.length}):`); if(pid == 0) {
if(complete.length) ns.tprint(complete.join(', ')); failed.push(server);
ns.tprint(''); continue;
ns.tprint(`Failed (${failed.length}):`); }
if(failed.length) ns.tprint(failed.join(', '));
ns.tprint(''); // Wait for script to finish if local
ns.tprint(`Skipped (${skipped.length}):`); if(!args['remoteExec'])
if(skipped.length) ns.tprint(skipped.join(', ')); while(ns.scriptRunning(args['script'], localhost)) await ns.sleep(1000);
ns.tprint(''); complete.push(server);
} else { }
ns.tprint(`Complete: ${complete.length}\tFailed: ${failed.length}\tSkipped: ${skipped.length}`);
ns.tprint(''); // Output report
} if(!args['quite']) {
ns.tprint('===================================================');
ns.tprint(`Crawler: ${complete.length + failed.length + skipped.length} Servers`);
ns.tprint('===================================================');
if(args['verbose']) {
ns.tprint(`Complete (${complete.length}):`);
if(complete.length) ns.tprint(complete.join(', '));
ns.tprint('');
ns.tprint(`Failed (${failed.length}):`);
if(failed.length) ns.tprint(failed.join(', '));
ns.tprint('');
ns.tprint(`Skipped (${skipped.length}):`);
if(skipped.length) ns.tprint(skipped.join(', '));
ns.tprint('');
} else {
ns.tprint(`Complete: ${complete.length}\tFailed: ${failed.length}\tSkipped: ${skipped.length}`);
ns.tprint('');
} }
} catch(err) {
if(err instanceof ArgError) return ns.tprint(argParser.help(err.message));
throw err;
} }
} }
/**
* BitBurner autocomplete.
*
* @param {{servers: string[], txts: string[], scripts: string[], flags: string[]}} data - Contextual information
* @returns {string[]} - Pool of autocomplete options
*/
export function autocomplete(data) {
return [...data.scripts, ...data.servers, '{{SERVER}}'];
}

View File

@ -1,42 +1,68 @@
import {ArgError, ArgParser} from '/scripts/lib/arg-parser'; import {ArgParser} from '/scripts/lib/arg-parser';
import {bestTarget} from '/scripts/lib/utils'; import {toCurrency} from '/scripts/lib/utils';
import {scanNetwork} from '/scripts/crawler';
/** /**
* Scan the network for the best device(s) to mine. * Sort array of servers based on the potential return/yield.
*
* @param {NS} ns - BitBurner API
* @param {string[]} servers - List of servers to sort based on yield
* @returns {[string, number][]} - Sorted list of servers & their potential yield per minute
*/
export function bestTarget(ns, servers) {
return servers.map(s => [s, serverYield(ns, s)]).sort((a, b) => {
if(a[1] < b[1]) return 1;
if(a[1] > b[1]) return -1;
return 0;
});
}
/**
* Calculate the average return per minute when hacking a server.
*
* **Disclaimer:** Does not take into account security or weaken time.
*
* @param {NS} ns - BitBurner API
* @param {string} server - Server to calculate yield for
* @returns {number} - $/minute
*/
export function serverYield(ns, server) {
return (ns.hackAnalyze(server) * ns.getServerMaxMoney(server))
* ((60 / (ns.getHackTime(server) / 1000)) * ns.hackAnalyzeChance(server));
}
/**
* Scan the network for the best server(s) to hack.
*
* @param ns {NS} - BitBurner API * @param ns {NS} - BitBurner API
* @returns {*} * @returns {*}
*/ */
export function main(ns) { export function main(ns) {
// Setup // Setup
ns.disableLog('ALL'); ns.disableLog('ALL');
const argParser = new ArgParser('find-target.js', 'Scan the network for the best device(s) to mine.', null, [ const argParser = new ArgParser('find-target.js', 'Scan the network for the best servers(s) to hack.',[
{name: 'count', desc: 'Number of devices to return in order from best to worst', flags: ['-c', '--count'], default: Infinity, type: 'number'}, {name: 'count', desc: 'Number of servers to return', flags: ['-c', '--count'], default: Infinity},
{name: 'rooted', desc: 'Filter to devices that have been rooted', flags: ['-r', '--rooted'], type: 'bool'}, {name: 'rooted', desc: 'Only servers that have been rooted', flags: ['-r', '--rooted'], default: false},
{name: 'notRooted', desc: 'Filter to devices that have not been rooted', flags: ['-n', '--not-rooted'], type: 'bool'}, {name: 'notRooted', desc: 'Only servers that have not been rooted', flags: ['-n', '--not-rooted'], default: false},
{name: 'verbose', desc: 'Display the estimated income per minute per core', flags: ['-v', '--verbose'], type: 'bool'}, {name: 'verbose', desc: 'Display the estimated income per minute per core', flags: ['-v', '--verbose'], default: false},
]); ]);
const args = argParser.parse(ns.args);
try { // Help
// Run if(args['help'] || args['_error'].length)
const args = argParser.parse(ns.args); return ns.tprint(argParser.help(args['help'] ? null : args['_error'][0], args['_command']));
// Banner // Banner
ns.tprint('==================================================='); ns.tprint('===================================================');
ns.tprint(`Finding Targets:`); ns.tprint(`Finding Targets:`);
ns.tprint('==================================================='); ns.tprint('===================================================');
// Search & display results // Search & display results
bestTarget(ns).filter((t, i) => !args['count'] || i < args['count']) const [servers, ignore] = scanNetwork(ns);
.filter(t => !args['rooted'] || t.hasAdminRights) bestTarget(ns, servers).map(s => [...s, ns.hasRootAccess(s[0])])
.filter(t => !args['notRooted'] || !t.hasAdminRights) .filter(s => (!args['rooted'] || s[2]) || (!args['notRooted'] || !s[2]))
.map(t => `${t.hostname}${args['verbose'] ? ` (${t.moneyAMinute.toLocaleString('en-US', { .filter((s, i) => i < args['count'])
style: 'currency', .map(s => `${s[0]}${args['verbose'] ? ` (${toCurrency(s[1])})` : ''}`)
currency: 'USD', .forEach((s, i) => ns.tprint(`${i + 1}) ${s}`));
})})` : ''}`) ns.tprint('');
.forEach((t, i) => ns.tprint(`${i + 1}) ${t}`));
ns.tprint('');
} catch(err) {
if(err instanceof ArgError) return ns.tprint(argParser.help(err.message));
throw err;
}
} }

View File

@ -1,94 +1,107 @@
import {ArgError, ArgParser} from '/scripts/lib/arg-parser'; import {ArgParser} from '/scripts/lib/arg-parser';
import {Logger} from '/scripts/lib/logger'; import {Logger} from '/scripts/lib/logger';
import {toCurrency} from '/scripts/lib/utils';
/** /**
* Buy, upgrade & manage Hacknet nodes automatically. * Buy, upgrade & manage Hacknet nodes automatically.
* @params ns {NS} - BitBurner API * Strategy is to buy a new node when ever we can & then resort to the cheapest upgrade. If auto-scale is on a new
* server will be purchased when it becomes the cheapest option.
*
* @params {NS} ns - BitBurner API
*/ */
export async function main(ns) { export async function main(ns) {
// Setup // Setup
ns.disableLog('ALL'); ns.disableLog('ALL');
const argParser = new ArgParser('hacknet-manager.js', 'Buy, upgrade & manage Hacknet nodes automatically. Tail for live updates.', null, [ const argParser = new ArgParser('hacknet-manager.js', 'Buy, upgrade & manage Hacknet nodes automatically. Tail for live updates.', [
{name: 'limit', desc: 'Limit the number of nodes the manager will buy, defaults to 8', optional: true, default: 8, type: 'num'}, {name: 'limit', desc: 'Limit the number of nodes the manager will buy, defaults to 8 or the current number of nodes', optional: true, default: 8},
{name: 'autoLimit', desc: 'Automatically increase the node limit when there is nothing to do', flags: ['-a', '--auto-limit'], type: 'bool'}, {name: 'autoLimit', desc: 'Automatically increase the node limit when there is nothing to do', flags: ['-a', '--auto-limit'], default: false},
{name: 'balance', desc: 'Prevent spending bellow point', flags: ['-b', '--balance'], type: 'num'}, {name: 'balance', desc: 'Prevent spending bellow point', flags: ['-b', '--balance'], default: false},
{name: 'sleep', desc: 'Amount of time to wait between purchases, defaults to 1 (second)', flags: ['-s', '--sleep'], default: 1, type: 'num'} {name: 'sleep', desc: 'Amount of time to wait between purchases, defaults to 1 (second)', flags: ['-s', '--sleep'], default: 1}
]); ]);
let nodeCount = ns.hacknet.numNodes();
const args = argParser.parse(ns.args);
if(nodeCount > args['limit']) args['limit'] = nodeCount;
const logger = new Logger(ns, [() => `Hacknet Manager: ${nodeCount}/${args['limit']}`]);
try { // Help
// Run if(args['help'] || args['_error'].length)
const args = argParser.parse(ns.args); return ns.tprint(argParser.help(args['help'] ? null : args['_error'][0], args['_command']));
let nodeCount = ns.hacknet.numNodes();
const logger = new Logger(ns, [() => `Hacknet Manager: ${nodeCount}/${args['limit']}`]);
while(true) {
const balance = ns.getServerMoneyAvailable('home');
// Check if we should buy a new node // Main loop
if(nodeCount < args['limit'] && balance - ns.hacknet.getPurchaseNodeCost() >= args['balance']) { // noinspection InfiniteLoopJS
nodeCount++; while(true) {
ns.hacknet.purchaseNode(); const balance = ns.getServerMoneyAvailable('home');
logger.log(`Buying Node ${nodeCount}`); const newNodeCost = ns.hacknet.getPurchaseNodeCost();
} else {
// Create an ordered list of nodes by their cheapest upgrade
const upgrades = Array(nodeCount).fill(null)
.map((ignore, i) => ({ // Gather information
index: i,
cacheCost: ns.hacknet.getCacheUpgradeCost(i),
coreCost: ns.hacknet.getCoreUpgradeCost(i),
levelCost: ns.hacknet.getLevelUpgradeCost(i),
ramCost: ns.hacknet.getRamUpgradeCost(i),
...ns.hacknet.getNodeStats(i)
})).map(node => { // Figure out cheapest upgrade
if(node.cacheCost != 0 && node.cacheCost != Infinity && node.cacheCost <= node.coreCost && node.cacheCost <= node.levelCost && node.cacheCost <= node.ramCost) {
node.bestUpgrade = {
name: 'cache',
cost: node.cacheCost,
purchase: () => ns.hacknet.upgradeCache(node.index)
};
} else if(node.coreCost != 0 && node.coreCost != Infinity && node.coreCost <= node.cacheCost && node.coreCost <= node.levelCost && node.coreCost <= node.ramCost) {
node.bestUpgrade = {
name: 'cores',
cost: node.coreCost,
purchase: () => ns.hacknet.upgradeCore(node.index)
};
} else if(node.ramCost != 0 && node.ramCost != Infinity && node.ramCost <= node.cacheCost && node.ramCost <= node.levelCost && node.ramCost <= node.coreCost) {
node.bestUpgrade = {
name: 'ram',
cost: node.ramCost,
purchase: () => ns.hacknet.upgradeRam(node.index)
};
} else {
node.bestUpgrade = {
name: 'level',
cost: node.levelCost,
purchase: () => ns.hacknet.upgradeLevel(node.index)
};
}
return node;
}).sort((a, b) => { // Sort by cheapest upgrade
if(a.bestUpgrade.cost > b.bestUpgrade.cost) return 1;
if(a.bestUpgrade.cost < b.bestUpgrade.cost) return -1;
return 0;
});
// Apply the cheapest upgrade // Check if we should wait to buy a node
if(upgrades.length) { if(nodeCount < args['limit'] && balance - newNodeCost >= args['balance']) {
if(args['autoLimit'] && nodeCount >= args['limit'] && upgrades[0].bestUpgrade.cost == Infinity) { nodeCount++;
args['limit'] = Math.max(nodeCount, args['limit']) + 1; ns.hacknet.purchaseNode();
logger.log(`Increasing node limit to ${args['limit']}`); logger.log(`Node ${nodeCount} - Purchased - ${toCurrency(newNodeCost)}`);
} else if(balance - upgrades[0].bestUpgrade.cost >= args['balance']) { } else {
const cost = Math.round(upgrades[0].bestUpgrade.cost * 100) / 100; // Create an ordered list of nodes by their cheapest upgrade
logger.log(`Node ${upgrades[0].index} - ${upgrades[0].bestUpgrade.name} ${upgrades[0][upgrades[0].bestUpgrade.name] + 1} - $${cost}`); const upgrades = Array(nodeCount).fill(null)
upgrades[0].bestUpgrade.purchase(); .map((ignore, i) => ({ // Gather information
index: i,
cacheCost: ns.hacknet.getCacheUpgradeCost(i, 1),
coreCost: ns.hacknet.getCoreUpgradeCost(i, 1),
levelCost: ns.hacknet.getLevelUpgradeCost(i, 1),
ramCost: ns.hacknet.getRamUpgradeCost(i, 1),
...ns.hacknet.getNodeStats(i)
})).map(node => { // Figure out cheapest upgrade
if(node.cacheCost != 0 && node.cacheCost != Infinity && node.cacheCost <= node.coreCost && node.cacheCost <= node.levelCost && node.cacheCost <= node.ramCost) {
node.bestUpgrade = {
name: 'cache',
cost: node.cacheCost,
purchase: () => ns.hacknet.upgradeCache(node.index,1 )
};
} else if(node.coreCost != 0 && node.coreCost != Infinity && node.coreCost <= node.cacheCost && node.coreCost <= node.levelCost && node.coreCost <= node.ramCost) {
node.bestUpgrade = {
name: 'cores',
cost: node.coreCost,
purchase: () => ns.hacknet.upgradeCore(node.index, 1)
};
} else if(node.ramCost != 0 && node.ramCost != Infinity && node.ramCost <= node.cacheCost && node.ramCost <= node.levelCost && node.ramCost <= node.coreCost) {
node.bestUpgrade = {
name: 'ram',
cost: node.ramCost,
purchase: () => ns.hacknet.upgradeRam(node.index, 1)
};
} else {
node.bestUpgrade = {
name: 'level',
cost: node.levelCost,
purchase: () => ns.hacknet.upgradeLevel(node.index, 1)
};
} }
} return node;
} }).sort((a, b) => { // Sort by cheapest upgrade
if(a.bestUpgrade.cost > b.bestUpgrade.cost) return 1;
if(a.bestUpgrade.cost < b.bestUpgrade.cost) return -1;
return 0;
});
// Check again in 1s // Apply the cheapest upgrade/purchase
await ns.sleep(args['sleep'] * 1000); if((!upgrades.length || newNodeCost < upgrades[0].bestUpgrade.cost) && args['autoLimit'] && nodeCount >= args['limit']) {
args['limit'] = Math.max(nodeCount, args['limit']) + 1;
logger.log(`Increasing node limit to ${args['limit']}`);
} else if(upgrades.length && upgrades[0].bestUpgrade.cost != Infinity) {
logger.log(`Node ${upgrades[0].index} - ${upgrades[0].bestUpgrade.name} ${upgrades[0][upgrades[0].bestUpgrade.name] + 1} - ${toCurrency(upgrades[0].bestUpgrade.cost)}`);
upgrades[0].bestUpgrade.purchase();
}
} }
} catch(err) {
if(err instanceof ArgError) return ns.tprint(argParser.help(err.message)); // Wait & then check again
throw err; await ns.sleep(args['sleep'] * 1000);
} }
} }
/**
* BitBurner autocomplete.
*
* @param {{servers: string[], txts: string[], scripts: string[], flags: string[]}} data - Contextual information
* @returns {string[]} - Pool of autocomplete options
*/
export function autocomplete(data) {
return new Array(10).fill(null).map((ignore, i) => Math.pow(i, 2).toString());
}

View File

@ -1,95 +1,120 @@
export class ArgError extends Error {}
export class ArgParser { export class ArgParser {
/** /**
* Create a unix-like argument parser to extract flags from the argument list. Can also create help messages. * Create a unix-like argument parser to extract flags from the argument list. Can also create help messages.
* @param name {string} - Script name *
* @param desc {string} - Help text desciption * @param {string} name - Script name
* @param examples {string[]} - Help text examples * @param {string} desc - Help description
* @param argList {name: string, desc: string, flags: string[], type: string, default: any}[] - Array of CLI arguments * @param {(ArgParser | {name: string, desc: string, flags?: string[], optional?: boolean, default?: any})[]} argList - Array of CLI arguments
* @param allowUnknown {boolean} - Allow unknown flags * @param {string[]} examples - Additional examples to display
*/ */
constructor(name, desc, examples, argList, allowUnknown = false) { constructor(name, desc, argList = [], examples = []) {
this.name = name ?? 'example.js'; this.name = name;
this.description = desc ?? 'Example description'; this.desc = desc;
this.examples = examples || [`${argList.find(arg => !!arg.flags) ? '[OPTIONS] ' : ''}${argList.filter(arg => !arg.flags).map(arg => (arg.optional ? `[${arg.name.toUpperCase()}]` : arg.name.toUpperCase()) + (arg.extras ? '...' : '')).join(' ')}`];
this.examples.push('--help'); // Arguments
this.argList = argList || []; this.commands = argList.filter(arg => arg instanceof ArgParser);
this.argList.push({name: 'help', desc: 'Display this help message', flags: ['-h', '--help'], type: 'bool'}); this.args = argList.filter(arg => !arg.flags || !arg.flags.length);
this.allowUnknown = allowUnknown; this.flags = argList.filter(arg => !(arg instanceof ArgParser) && arg.flags && arg.flags.length);
this.flags.push({name: 'help', desc: 'Display this help message', flags: ['-h', '--help'], default: false});
this.defaults = argList.reduce((acc, arg) => ({...acc, [arg.name]: arg?.extras ? [] : arg.default ?? null}), {});
// Examples
this.examples = [
...examples,
`[OPTIONS] ${this.args.map(arg => (arg.optional ? `[${arg.name.toUpperCase()}]` : arg.name.toUpperCase()) + (arg.extras ? '...' : '')).join(' ')}`,
this.commands.length ? `[OPTIONS] COMMAND` : null,
`--help ${this.commands.length ? '[COMMAND]' : ''}`
].filter(e => !!e);
} }
/** /**
* Parse an array into an arguments dictionary using the configuration. * Parse an array into an arguments dictionary using the configuration.
* @param args {string[]} - Array of arguments to be parsed *
* @param {string[]} args - Array of arguments to be parsed
* @returns {object} - Dictionary of arguments with defaults applied * @returns {object} - Dictionary of arguments with defaults applied
*/ */
parse(args) { parse(args) {
// Parse arguments // Parse arguments
const queue = [...args], extra = []; let extras = [], parsed = {...this.defaults, '_error': []}, queue = [...args];
const parsed = this.argList.reduce((acc, arg) => ({...acc, [arg.name]: arg.default ?? (arg.type == 'bool' ? false : null)}), {});
// Flags
while(queue.length) { while(queue.length) {
let parse = queue.splice(0, 1)[0]; let arg = queue.splice(0, 1)[0];
if(parse[0] == '-') { if(arg[0] == '-') { // Flags
// Check combined flags // Check for combined shorthand
if(parse[1] != '-' && parse.length > 2) { if(arg[1] != '-' && arg.length > 2) {
parse = `-${parse[1]}`; queue = [...arg.substring(2).split('').map(a => `-${a}`), ...queue];
queue = parse.substring(1).split('').map(a => `-${a}`).concat(queue); arg = `-${arg[1]}`;
} }
// Find & add flag // Find & add flag
const split = parse.split('='); const combined = arg.split('=');
const arg = this.argList.find(arg => arg.flags && arg.flags.includes(split[0] || parse)); const argDef = this.flags.find(flag => flag.flags.includes(combined[0] || arg));
if(arg == null) { if(argDef == null) { // Not found, add to extras
if(!this.allowUnknown) throw new ArgError(`Option unknown: ${parse}`); extras.push(arg);
extra.push(parse);
continue; continue;
} }
if(arg.name == 'help') throw new ArgError('Help'); const value = argDef.default === false ? true : argDef.default === true ? false : queue.splice(queue.findIndex(q => q[0] != '-'), 1)[0] || argDef.default;
const value = arg.type == 'bool' ? true : split[1] || queue.splice(queue.findIndex(q => q[0] != '-'), 1)[0]; if(value == null) parsed['_error'].push(`Option missing value: ${arg.name}`);
if(value == null) throw new ArgError(`Option missing value: ${arg.name}`); parsed[argDef.name] = value;
parsed[arg.name] = value; } else { // Command
} else { const c = this.commands.find(command => command.name == arg);
// Save for required parsing if(!!c) {
extra.push(parse); const parsedCommand = c.parse(queue.splice(0, queue.length));
Object.keys(parsedCommand).forEach(key => {
if(parsed[key] != parsedCommand[key] && parsedCommand[key] == c.defaults[key])
delete parsedCommand[key];
});
parsed = {
...parsed,
...parsedCommand,
_command: c.name
};
} else extras.push(arg); // Not found, add to extras
} }
} }
// Arguments // Arguments
this.argList.filter(arg => !arg.flags && !arg.extras).forEach(arg => { this.args.filter(arg => !arg.extras).forEach(arg => {
if(!arg.optional && !extra.length) throw new ArgError(`Argument missing: ${arg.name.toUpperCase()}`); if(!arg.optional && !extras.length) parsed['_error'].push(`Argument missing: ${arg.name.toUpperCase()}`);
const value = extra.splice(0, 1)[0]; if(extras.length) parsed[arg.name] = extras.splice(0, 1)[0];
if(value != null) parsed[arg.name] = value;
}); });
// Extras // Extras
const extraKey = this.argList.find(arg => arg.extras)?.name || 'extra'; const extraKey = this.args.find(arg => arg.extras)?.name || '_extra';
parsed[extraKey] = extra; parsed[extraKey] = extras;
return parsed; return parsed;
} }
/** /**
* Create help message from the provided description, examples & argument list. * Create help message from the provided description & argument list.
* @param message {string} - Message to display, defaults to the description *
* @param {string} message - Message to display, defaults to the description
* @param {string} command - Command help message to show
* @returns {string} - Help message * @returns {string} - Help message
*/ */
help(msg) { help(message = '', command = '') {
const spacer = (text) => Array(24 - text.length || 1).fill(' ').join('');
// Help with specific command
if(command) {
const argParser = this.commands.find(parser => parser.name == command);
if(!argParser) throw new Error(`${command.toUpperCase()} does not have a help`)
return argParser.help(message);
}
// Description // Description
let message = '\n\n' + (msg && msg.toLowerCase() != 'help' ? msg : this.description); let msg = `\n\n${message || this.desc}`;
// Usage // Examples
if(this.examples.length) message += '\n\nUsage:\t' + this.examples.map(ex => `run ${this.name} ${ex}`).join('\n\t'); msg += '\n\nUsage:\t' + this.examples.map(ex => `run ${this.name} ${ex}`).join('\n\t');
// Arguments // Arguments
const req = this.argList.filter(a => !a.flags); if(this.args.length) msg += '\n\n\t' + this.args
if(req.length) message += '\n\n\t' + req.map(arg => { .map(arg => `${arg.name.toUpperCase()}${spacer(arg.name)}${arg.desc}`)
const padding = 3 - ~~(arg.name.length / 8); .join('\n\t');
return `${arg.name.toUpperCase()}${Array(padding).fill('\t').join('')} ${arg.desc}`;
}).join('\n\t');
// Flags // Flags
const opts = this.argList.filter(a => a.flags); msg += '\n\nOptions:\n\t' + this.flags.map(flag => {
if(opts.length) message += '\n\nOptions:\n\t' + opts.map(a => { const flags = flag.flags.join(', ');
const flgs = a.flags.join(' '); return `${flags}${spacer(flags)}${flag.desc}`;
const padding = 3 - ~~(flgs.length / 8);
return `${flgs}${Array(padding).fill('\t').join('')} ${a.desc}`;
}).join('\n\t'); }).join('\n\t');
// Print final message // Commands
return `${message}\n\n`; if(this.commands.length) msg += '\n\nCommands:\n\t' + this.commands
.map(command => `${command.name}${spacer(command.name)}${command.desc}`)
.join('\n\t');
return `${msg}\n\n`;
} }
} }

View File

@ -1,119 +0,0 @@
export class ArgError extends Error {}
export class ArgParser {
/**
* Create a unix-like argument parser to extract flags from the argument list. Can also create help messages.
* @param name {string} - Script name
* @param desc {string} - Help description
* @param argList {(ArgParser || {name: string, desc: string, flags: string[], optional: boolean, default: boolean})[]} - Array of CLI arguments
* @param examples {string[]} - Additional examples to display
*/
constructor(ns, name, desc, argList = [], examples = []) {
this.ns = ns;
this.name = name;
this.desc = desc;
// Arguments
this.commands = argList.filter(arg => arg instanceof ArgParser);
this.args = argList.filter(arg => !arg.flags || !arg.flags.length);
this.flags = argList.filter(arg => !(arg instanceof ArgParser) && arg.flags && arg.flags.length);
this.flags.push({name: 'help', desc: 'Display this help message', flags: ['-h', '--help'], default: false});
this.defaults = argList.reduce((acc, arg) => ({...acc, [arg.name]: arg?.extras ? [] : arg.default ?? null}), {});
// Examples
this.examples = [
...examples,
`[OPTIONS] ${this.args.map(arg => (arg.optional ? `[${arg.name.toUpperCase()}]` : arg.name.toUpperCase()) + (arg.extras ? '...' : '')).join(' ')}`,
this.commands.length ? `[OPTIONS] COMMAND` : null,
`--help ${this.commands.length ? '[COMMAND]' : ''}`
].filter(e => !!e);
}
/**
* Parse an array into an arguments dictionary using the configuration.
* @param args {string[]} - Array of arguments to be parsed
* @returns {object} - Dictionary of arguments with defaults applied
*/
parse(args = this.ns.args) {
// Parse arguments
let extras = [], parsed = {...this.defaults}, queue = [...args];
while(queue.length) {
let arg = queue.splice(0, 1)[0];
if(arg[0] == '-') { // Flags
// Check for combined shorthand
if(arg[1] != '-' && arg.length > 2) {
queue = [...arg.substring(2).split('').map(a => `-${a}`), ...queue];
arg = `-${arg[1]}`;
}
// Find & add flag
const combined = arg.split('=');
const argDef = this.flags.find(flag => flag.flags.includes(combined[0] || arg));
if(argDef == null) { // Not found, add to extras
extras.push(arg);
continue;
}
const value = argDef.default === false ? true : argDef.default === true ? false : queue.splice(queue.findIndex(q => q[0] != '-'), 1)[0] || argDef.default;
if(value == null) parsed['_error'] = `Option missing value: ${arg.name}`;
parsed[argDef.name] = value;
} else { // Command
const c = this.commands.find(command => command.name == arg);
if(!!c) {
const parsedCommand = c.parse(queue.splice(0, queue.length));
Object.keys(parsedCommand).forEach(key => {
if(parsed[key] != parsedCommand[key] && parsedCommand[key] == c.defaults[key])
delete parsedCommand[key];
});
parsed = {
...parsed,
...parsedCommand,
_command: c.name
};
} else extras.push(arg); // Not found, add to extras
}
}
// Arguments
this.args.filter(arg => !arg.extras).forEach(arg => {
if(!arg.optional && !extras.length) parsed['_error'] = `Argument missing: ${arg.name.toUpperCase()}`;
parsed[arg.name] = extras.splice(0, 1)[0];
});
// Extras
const extraKey = this.args.find(arg => arg.extras)?.name || '_extra';
parsed[extraKey] = extras;
return parsed;
}
/**
* Create help message from the provided description & argument list.
* @param message {string} - Message to display, defaults to the description
* @param command {string} - Command help message to show
* @returns {string} - Help message
*/
help(message = '', command = '') {
const spacer = (text) => Array(24 - text.length || 1).fill(' ').join('');
// Help with specific command
if(command) {
const argParser = this.commands.find(parser => parser.name == command);
if(!argParser) throw new Error(`${command.toUpperCase()} does not have a help`)
return argParser.help(message);
}
// Description
let msg = `\n\n${message || this.desc}`;
// Examples
msg += '\n\nUsage:\t' + this.examples.map(ex => `run ${this.name} ${ex}`).join('\n\t');
// Arguments
if(this.args.length) msg += '\n\n\t' + this.args
.map(arg => `${arg.name.toUpperCase()}${spacer(arg.name)}${arg.desc}`)
.join('\n\t');
// Flags
msg += '\n\nOptions:\n\t' + this.flags.map(flag => {
const flags = flag.flags.join(', ');
return `${flags}${spacer(flags)}${flag.desc}`;
}).join('\n\t');
// Commands
if(this.commands.length) msg += '\n\nCommands:\n\t' + this.commands
.map(command => `${command.name}${spacer(command.name)}${command.desc}`)
.join('\n\t');
this.ns.tprint(`${msg}\n\n`);
}
}

View File

@ -4,8 +4,9 @@ export class Logger {
/** /**
* Create a nicer log with a banner. * Create a nicer log with a banner.
* @param ns {NS} - BitBurner API *
* @param lineFns {Function[]} - Functions to generate a line (Seperated by a linebreak) * @param {NS} ns - BitBurner API
* @param {Function[]} lineFns - Functions to generate a line (Seperated by a linebreak)
*/ */
constructor(ns, lineFns = []) { constructor(ns, lineFns = []) {
this.ns = ns; this.ns = ns;
@ -16,8 +17,9 @@ export class Logger {
} }
/** /**
* Add red error message to logs * Add red error message to logs.
* @param message {string} - Text that will be added *
* @param {string} message - Text that will be added
*/ */
error(message) { this.log(`ERROR: ${message}`); } error(message) { this.log(`ERROR: ${message}`); }
@ -29,7 +31,7 @@ export class Logger {
} }
/** /**
* Print the header using the provided functions * Print the header using the provided functions.
*/ */
header() { header() {
this.lineBreak(); this.lineBreak();
@ -40,8 +42,9 @@ export class Logger {
} }
/** /**
* Add message to the logs * Add message to the logs.
* @param message {string} - Text that will be added *
* @param {string} message - Text that will be added
*/ */
log(message = '') { log(message = '') {
this.ns.clearLog(); this.ns.clearLog();
@ -52,8 +55,9 @@ export class Logger {
} }
/** /**
* Add orange warning to the logs * Add orange warning to the logs.
* @param message {string} - Text that will be added *
* @param {string} message - Text that will be added
*/ */
warn(message) { this.log(`WARN: ${message}`); } warn(message) { this.log(`WARN: ${message}`); }
} }

View File

@ -1,42 +1,60 @@
/** /**
* Scan the entire network for the best device to hack. * Add CSS to DOM.
* @param ns {NS} - BitBurner API *
* @returns {string[]} - Sorted list of targets to hack based on financial return * @param {string} id - An ID so we can make sure we only inject it once
* @param {string} css - CSS to inject
*/ */
export function bestTarget(ns) { export function addCSS(id, css) {
const [devices, network] = scanNetwork(ns, 'home'); const doc = eval('document');
return devices.map(d => ns.getServer(d)).filter(d => d.hasAdminRights).map(d => ({ id = `dynamic-css-${id}`;
...d, const exists = doc.querySelector(`#${id}`);
moneyAMinute: (ns.hackAnalyze(d.hostname) * ns.getServerMaxMoney(d.hostname)) * ((60 / (ns.getHackTime(d.hostname) / 1000)) * ns.hackAnalyzeChance(d.hostname))} if(exists) exists.outerHTML = '';
)).sort((a, b) => { doc.head.insertAdjacentHTML('beforeend', `<style id="${id}">${css}</style`);
if(a.moneyAMinute < b.moneyAMinute) return 1; }
if(a.moneyAMinute > b.moneyAMinute) return -1;
return 0; /**
* Format number to look like a dollar value ($1,000.00).
*
* @param {number} num - Number to format
* @returns {string} - formatted value with dollar sign
*/
export function toCurrency(num) {
return Number(num).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
}); });
} }
/** /**
* Copy a file & scan it for dependencies copying them as well. * Injects HTML into the terminal as a new line.
* @param ns {NS} - BitBurner API *
* @param src {string} - File to scan & copy * **Disclaimer:** React will wipe out anything injected by this function.
* @param device {string} - Device to copy files to *
* @returns {string[]} - Array of coppied files * @param {string} html - HTML to inject into terminal
* @param {boolean} wrap - Wrap in a list-item & paragraph to match default style
*/ */
export async function copyWithDependencies(ns, src, device) { export function htmlPrint(html, wrap = true) {
const queue = [src], found = [src]; setTimeout(() => {
while(queue.length) { const doc = eval('document');
const file = queue.splice(0, 1)[0]; if(wrap) {
const imports = new RegExp(/from ["']\.?(\/.+)["']/g); const liClass = doc.querySelector('#terminal li').classList.value;
const script = await ns.read(file); const pClass = doc.querySelector('#terminal li p').classList.value;
let match; html = `<li class="${liClass}"><p class="${pClass}">${html}</p></li>`
while((match = imports.exec(script)) != null) {
const path = `${match[1]}.js`;
queue.push(path);
found.push(path);
} }
} eval('document').getElementById('terminal').insertAdjacentHTML('beforeend', html)
await ns.scp(found, device); }, 25);
return found.reverse(); }
/**
* Calculate the maximum number of threads a script can be executed with.
*
* @param {NS} ns - BitBurner API
* @param {string} script - Full path to script
* @param {string} server - Server script will run on
* @returns {number} - Number of threads the server will be able to support
*/
export function maxThreads(ns, script, server = ns.getHostname()) {
return ~~(ns.getServerMaxRam(server) / ns.getScriptRam(script, ns.getHostname()))
} }
/** /**
@ -46,10 +64,10 @@ export async function copyWithDependencies(ns, src, device) {
* *
* `/script/test.js [||||||||||----------] 50% (24.2 MB/s)` * `/script/test.js [||||||||||----------] 50% (24.2 MB/s)`
* *
* @param ns {NS} - BitBurner API * @param {NS} ns - BitBurner API
* @param name {string} - Name to display at the begging of bar * @param {string} name - Name to display at the begging of bar
* @param showSpeed {boolean} - Show the speed in the progress bar * @param {boolean} showSpeed - Show the speed in the progress bar
* @param time {number} - Time it takes for bar to fill * @param {number} time - Time it takes for bar to fill
*/ */
export async function progressBar(ns, name, showSpeed = true, time = Math.random() + 0.5) { export async function progressBar(ns, name, showSpeed = true, time = Math.random() + 0.5) {
const text = (percentage, speed) => { const text = (percentage, speed) => {
@ -77,8 +95,9 @@ export async function progressBar(ns, name, showSpeed = true, time = Math.random
/** /**
* **Impure:** Prune tree down to keys which pass function * **Impure:** Prune tree down to keys which pass function
* @param tree {object} - Tree to search *
* @param fn {(key: string) => boolean} - Function to test each key with * @param {Object} tree - Tree to search
* @param {(key: string) => boolean} fn - Function to test each key with
* @returns {boolean} - True if a match was found * @returns {boolean} - True if a match was found
*/ */
export function pruneTree(tree, fn) { export function pruneTree(tree, fn) {
@ -91,56 +110,65 @@ export function pruneTree(tree, fn) {
} }
/** /**
* Scan the network of a given device. * Pause for a random amount of time.
* @param ns {NS} - BitBurner API * @param {number} min - minimum amount of time to wait after printing text
* @param device {string} - Device network that will be scanned * @param {number} max - maximum amount of time to wait after printing text
* @param maxDepth - Depth to scan to
* @returns {[string[], Object]} - A tuple including an array of discovered devices & a tree of the network
*/ */
export function scanNetwork(ns, device = ns.getHostname(), maxDepth = Infinity) { export function randomSleep(min = 0.5, max = 1.5) {
let discovered = [device]; return new Promise(res => setTimeout(res, ~~(Math.random() * (max * 1000 - min * 1000)) + min * 1000));
function scan (device, depth = 1) { }
if(depth > maxDepth) return {};
const localTargets = ns.scan(device).filter(newDevice => !discovered.includes(newDevice)); /**
discovered = [...discovered, ...localTargets]; * Converts function into HTML friendly string with it's arguments. Meant to be used
return localTargets.reduce((acc, device) => ({...acc, [device]: scan(device, depth + 1)}), {}); * with `onclick` to execute code.
} *
const network = scan(device); * @param {Function} fn - function that will be serialized
return [discovered.slice(1), network]; * @param {...any} args - Arguments passed to function
* @returns {string} - Serialized function with arguments: "(function(arg1, arg2, ...) {...})(arg1, arg2, ...)"
*/
export function serializeFunction(fn, ...args) {
let serialized = fn.toString().replace(/function .+\W?\(/, 'function(');
serialized = `(${serialized})(${args.map(a => JSON.stringify(a)).join()})`;
serialized = serialized.replace(/"/g, '&quot;');
serialized = serialized.replace(/'/g, '&#39;');
return serialized;
} }
/** /**
* Print text to the terminal & then delay for a random amount of time to emulate execution time. * Print text to the terminal & then delay for a random amount of time to emulate execution time.
* @param ns {NS} - BitBurner API *
* @param message {string} - Text to display * @param {NS} ns - BitBurner API
* @param min {number} - minimum amount of time to wait after printing text * @param {string} message - Text to display
* @param max {number} - maximum amount of time to wait after printing text * @param {boolean} first - Pause first or wait until text is displayed
* @param {number} min - minimum amount of time to wait after printing text
* @param {number} max - maximum amount of time to wait after printing text
*/ */
export async function slowPrint(ns, message, min = 0.5, max = 1.5) { export async function slowPrint(ns, message, first = false, min = 0.5, max = 0.5) {
const time = ~~(Math.random() * (max * 1000 - min * 1000)) + min * 1000; if(first) await randomSleep(min, max);
ns.tprint(message); ns.tprint(message);
await ns.sleep(time); await randomSleep(min, max);
} }
/** /**
* Write a command to the terminal. * Write a command to the terminal.
* @param command {string} - Command that will be run *
* @returns {Promise<string>} - Command line response * @param {string} command - Command that will be run
* @returns {Promise<string[]>} - Any new output
*/ */
export function terminal(command) { export function terminal(command) {
// Get the terminal // Get the terminal
const terminalInput = document.getElementById("terminal-input"); const doc = eval('document');
const terminalInput = doc.getElementById("terminal-input");
const handler = Object.keys(terminalInput)[1]; const handler = Object.keys(terminalInput)[1];
// Send command // Send command
terminalInput.value = command; // Enter the command terminalInput.value = command; // Enter the command
terminalInput[handler].onChange({target:terminalInput}); // React on change terminalInput[handler].onChange({target: terminalInput}); // React on change
terminalInput[handler].onKeyDown({key: 'Enter', preventDefault: () => null}); // Enter 'keystroke' terminalInput[handler].onKeyDown({key: 'Enter', preventDefault: () => null}); // Enter 'keystroke'
// Return any new terminal output // Return any new terminal output
return new Promise(res => setTimeout(() => { return new Promise(res => setTimeout(() => {
const terminalOutput = Array.from(eval('document') const terminalOutput = Array.from(doc.querySelectorAll('#terminal li p')).map(out => out.innerText);
.querySelectorAll('#terminal li p')).map(out => out.innerText);
const i = terminalOutput.length - terminalOutput.reverse().findIndex(o => o.indexOf(command) != -1); const i = terminalOutput.length - terminalOutput.reverse().findIndex(o => o.indexOf(command) != -1);
res(terminalOutput.reverse().slice(i)); res(terminalOutput.reverse().slice(i));
}, 25)); }, 25));

View File

@ -1,60 +1,65 @@
import {ArgError, ArgParser} from '/scripts/lib/arg-parser'; import {ArgParser} from '/scripts/lib/arg-parser';
import {Logger} from "/scripts/lib/logger"; import {Logger} from '/scripts/lib/logger';
import {toCurrency} from '/scripts/lib/utils';
/** /**
* BitBurner autocomplete * Hack, Grow, Weaken loop to "mine" a server for money. Tail for live updates.
* @param data {server: string[], txts: string[], scripts: string[], flags: string[]} - Contextual information *
* @params {NS} ns - BitBurner API
*/
export async function main(ns) {
// Setup
ns.disableLog('ALL');
const argParser = new ArgParser('miner.js', 'Hack, Grow, Weaken loop to "mine" a server for money. Tail for live updates.', [
{name: 'server', desc: 'Server to mine, defaults to the local server', optional: true, default: ns.getHostname()}
]);
const args = argParser.parse(ns.args);
let maxBalance, balance, minSecurity, security;
maxBalance = ns.getServerMaxMoney(args['server']);
balance = ns.getServerMoneyAvailable(args['server']);
minSecurity = ns.getServerMinSecurityLevel(args['server']) + 2;
security = ns.getServerSecurityLevel(args['server']);
// Help
if(args['help'] || args['_error'].length)
return ns.tprint(argParser.help(args['help'] ? null : args['_error'][0], args['_command']));
// Logger
const logger = new Logger(ns, [
() => `Mining: ${args['server']}`,
() => `Security: ${Math.round(security)}/${minSecurity}\tBalance: \$${Math.round(balance * 100) / 100}`
]);
// Main loop
// noinspection InfiniteLoopJS
while(true) {
// Update information
security = ns.getServerSecurityLevel(args['server']);
balance = ns.getServerMoneyAvailable(args['server']);
// Pick step
if(security > minSecurity) { // Weaken
logger.log('Attacking Security...');
const w = await ns.weaken(args['server']);
logger.log(`Security: -${w}`);
} else if(balance < maxBalance) { // Grow
logger.log('Spoofing Balance...');
const g = await ns.grow(args['server']);
logger.log(`Balance: +${toCurrency(g * balance - balance)}`);
} else { // Hack
logger.log('Hacking Account...');
const h = await ns.hack(args['server']);
logger.log(`Balance: -$${h}`);
}
}
}
/**
* BitBurner autocomplete.
*
* @param {{servers: string[], txts: string[], scripts: string[], flags: string[]}} data - Contextual information
* @returns {string[]} - Pool of autocomplete options * @returns {string[]} - Pool of autocomplete options
*/ */
export function autocomplete(data) { export function autocomplete(data) {
return [...data.servers]; return [...data.servers];
} }
/**
* Weaken, Grow, Hack loop to "mine" target machine for money.
* @params ns {NS} - BitBurner API
*/
export async function main(ns) {
// Setup
ns.disableLog('ALL');
const argParser = new ArgParser('miner.js', 'Weaken, Grow, Hack loop to "mine" device for money. Tail for live updates.', null, [
{name: 'device', desc: 'Device to mine, defaults to current machine', optional: true, default: ns.getHostname(), type: 'string'}
]);
try {
// Run
const args = argParser.parse(ns.args);
let maxBalance, balance, minSecurity, security;
maxBalance = await ns.getServerMaxMoney(args['device']);
balance = await ns.getServerMoneyAvailable(args['device']);
minSecurity = await ns.getServerMinSecurityLevel(args['device']) + 2;
security = await ns.getServerSecurityLevel(args['device']);
const logger = new Logger(ns, [
() => `Mining: ${args['device']}`,
() => `Security: ${Math.round(security)}/${minSecurity}\tBalance: \$${Math.round(balance * 100) / 100}`
]);
while(true) {
// Update information
security = await ns.getServerSecurityLevel(args['device']);
balance = await ns.getServerMoneyAvailable(args['device']);
// Pick step
if(security > minSecurity) { // Weaken
logger.log('Attacking Security...');
const w = await ns.weaken(args['device']);
logger.log(`Security: -${w}`);
} else if(balance < maxBalance) { // Grow
logger.log('Spoofing Balance...');
const g = await ns.grow(args['device']);
logger.log(`Balance: +$${Math.round((g * balance - balance) * 100) / 100}`);
} else { // Hack
logger.log('Hacking Account...');
const h = await ns.hack(args['device']);
logger.log(`Balance: -$${h}`);
}
}
} catch(err) {
if(err instanceof ArgError) return ns.tprint(argParser.help(err.message));
throw err;
}
}

View File

@ -1,63 +1,115 @@
import {ArgError, ArgParser} from '/scripts/lib/arg-parser'; import {ArgParser} from '/scripts/lib/arg-parser';
import {pruneTree, scanNetwork} from '/scripts/lib/utils'; import {addCSS, htmlPrint, pruneTree, serializeFunction, terminal} from '/scripts/lib/utils';
import {connectionString} from '/scripts/connect';
import {scanNetwork} from '/scripts/crawler';
const CSS = `
#terminal a:not([href]):hover {
cursor:pointer;
text-decoration:underline;
}
.srv-fnr { color: #BBBB11; }
.srv-fr {
color: #FFFF44;
font-weight: bold;
}
.srv-nr { color: #11BB11; }
.srv-r {
color: #00FF00;
font-weight: bold;
}`
export const factionServers = ['CSEC', 'avmnite-02h', 'I.I.I.I', 'run4theh111z', 'w0r1d_d43m0n'];
/** /**
* BitBurner autocomplete * Scan the network for servers and display as an ASCII tree. Servers with root access are highlighted & bold.
* @param data {server: string[], txts: string[], scripts: string[], flags: string[]} - Contextual information *
* @param {NS} ns - BitBurner API
*/
export async function main(ns) {
// Setup
ns.disableLog('ALL');
const argParser = new ArgParser('network-graph.js', 'Scan the network for servers and display as an ASCII tree. Servers with root access are highlighted & bold. Click to automatically connect.', [
{name: 'server', desc: 'Point to start scan from, defaults to local server', optional: true, default: ns.getHostname()},
{name: 'depth', desc: 'Depth to scan to', flags: ['-d', '--depth'], default: Infinity},
{name: 'filter', desc: 'Filter to servers matching name', flags: ['-f', '--filter'], default: false},
{name: 'regex', desc: 'Filter to servers matching pattern', flags: ['-e', '--regex'], default: false},
{name: 'level', desc: 'Display the required hack level & number of ports to root: [level|port]', flags: ['-l', '--level'], default: false},
{name: 'notRooted', desc: 'Filter to servers that have not been rooted', flags: ['-n', '--not-rooted'], default: false},
{name: 'rooted', desc: 'Filter to servers that have been rooted', flags: ['-r', '--rooted'], default: false},
{name: 'specs', desc: 'Display the server specifications: {CPU|RAM}', flags: ['-s', '--specs'], default: false},
{name: 'usage', desc: 'Display the server utilization: (USG%)', flags: ['-u', '--usage'], default: false},
{name: 'verbose', desc: 'Display level, specs & usage in that order: [HL|P] {CPU|RAM} (USG%)', flags: ['-v', '--verbose'], default: false},
]);
const args = argParser.parse(ns.args);
/**
* Get the color class for the server.
*
* @param {string} server - Server to figure out color for.
*/
function color(server) {
const rooted = ns.getServer(server).hasAdminRights; // Already using getServer so we might as well keep using it
if(factionServers.includes(server)) return rooted ? 'srv-fr' : 'srv-fnr';
return rooted ? 'srv-r' : 'srv-nr';
}
/**
* Create serialized connection command.
*
* @param {string} server - server to connect to.
*/
function connectFn(server) {
return serializeFunction(terminal, connectionString(ns, server, 'home'));
}
/**
* Iterate tree & convert to ascii.
*
* @param {Object} tree - Tree to parse
* @param {string} spacer - Spacer text for tree formatting
*/
function render(tree, spacer = ' ') {
const nodes = Object.keys(tree);
for(let i = 0; i < nodes.length; i++) {
const server = nodes[i], info = ns.getServer(server);
let stats = '';
if(args['level'] || args['verbose']) stats += ` [${info.requiredHackingSkill}|${info.openPortCount}]`;
if(args['specs'] || args['verbose']) stats += ` {${info.cpuCores}|${info.maxRam}}`;
if(args['usage'] || args['verbose']) stats += ` (${Math.round(info.ramUsed / info.maxRam * 100) || 0}%)`;
const last = i == nodes.length - 1;
const branch = last ? '└─ ' : '├─ ';
htmlPrint(spacer + branch + `<a class="${color(server)}" onclick="${connectFn(server)}">${server + stats}</a>`);
render(tree[server], spacer + (last ? ' ' : '| '));
}
}
// Help
if(args['help'] || args['_error'].length)
return ns.tprint(argParser.help(args['help'] ? null : args['_error'][0], args['_command']));
// Gather network information
const [ignore, network] = scanNetwork(ns, args['server'], args['depth']);
// Add flags filters
if(args['regex']) pruneTree(network, s => RegExp(args['regex']).test(s));
if(args['filter']) pruneTree(network, s => s == args['filter']);
if(args['rooted']) pruneTree(network, s => ns.getServer(s).hasAdminRights); // Already using getServer so we might as well keep using it
if(args['notRooted']) pruneTree(network, s => !ns.getServer(s).hasAdminRights); // Already using getServer so we might as well keep using it
// Output
addCSS('network-graph', CSS);
htmlPrint(`\n<a class="srv-tree-span ${color(args['server'])}" onclick="${connectFn(args['server'])}">${args['server']}</a>`);
render(network);
htmlPrint('\n');
}
/**
* BitBurner autocomplete.
*
* @param {{servers: string[], txts: string[], scripts: string[], flags: string[]}} data - Contextual information
* @returns {string[]} - Pool of autocomplete options * @returns {string[]} - Pool of autocomplete options
*/ */
export function autocomplete(data) { export function autocomplete(data) {
return [...data.servers]; return [...data.servers];
} }
/**
* Scan the network for devices and display as an ASCII tree.
* @param ns {NS} - BitBurner API
*/
export async function main(ns) {
/**
* Iterate tree & print to screen
* @param tree {Object} - Tree to parse
* @param stats {Object} - Pool of stats to pull extra information from
* @param spacer {string} - Spacer text for tree formatting
*/
function render(tree, stats, spacer = ' ') {
Object.keys(tree).forEach((device, i, arr) => {
const deviceStats = stats ? stats[device] : null;
const stat = deviceStats ? ` (${deviceStats.hasAdminRights ? 'ROOTED' : `${deviceStats.requiredHackingSkill}|${deviceStats.numOpenPortsRequired}`})` : '';
const last = i == arr.length - 1;
const branch = last ? '└─ ' : '├─ ';
ns.tprint(spacer + branch + device + stat);
render(tree[device], stats, spacer + (last ? ' ' : '| '));
});
}
// Setup
ns.disableLog('ALL');
const argParser = new ArgParser('network-graph.js', 'Scan the network for devices and display as an ASCII tree:\n home\n ├─ n00dles (ROOTED)\n | └─ max-hardware (80|1)\n | └─ neo-net (50|1)\n ├─ foodnstuff (ROOTED)\n └─ sigma-cosmetics (ROOTED)', null, [
{name: 'device', desc: 'Point to start scan from, defaults to current machine', optional: true, default: ns.getHostname(), type: 'string'},
{name: 'depth', desc: 'Depth to scan to', flags: ['-d', '--depth'], default: Infinity, type: 'num'},
{name: 'filter', desc: 'Filter to device matching name', flags: ['-f', '--filter'], type: 'string'},
{name: 'regex', desc: 'Filter to devices matching pattern', flags: ['-e', '--regex'], type: 'string'},
{name: 'rooted', desc: 'Filter to devices that have been rooted', flags: ['-r', '--rooted'], type: 'bool'},
{name: 'notRooted', desc: 'Filter to devices that have not been rooted', flags: ['-n', '--not-rooted'], type: 'bool'},
{name: 'verbose', desc: 'Display the required hack level & number of ports to root: (level|port)', flags: ['-v', '--verbose'], type: 'bool'},
]);
try {
// Run
const args = argParser.parse(ns.args);
const [devices, network] = scanNetwork(ns, args['device'], args['depth']);
const stats = devices.reduce((acc, d) => ({...acc, [d]: ns.getServer(d)}), {});
if(args['regex']) pruneTree(network, d => RegExp(args['regex']).test(d)); // Regex flag
else if(args['filter']) pruneTree(network, d => d == args['filter']); // Filter flag
if(args['rooted']) pruneTree(network, d => stats[d].hasAdminRights); // Rooted flag
else if(args['notRooted']) pruneTree(network, d => !stats[d].hasAdminRights); // Not rooted flag
ns.tprint(args['device']);
render(network, args['verbose'] ? stats : null);
ns.tprint('');
} catch(err) {
if(err instanceof ArgError) return ns.tprint(argParser.help(err.message));
throw err;
}
}

View File

@ -1,104 +1,121 @@
import {ArgError, ArgParser} from '/scripts/lib/arg-parser'; import {ArgParser} from '/scripts/lib/arg-parser';
import {copyWithDependencies, progressBar, slowPrint} from '/scripts/lib/utils'; import {maxThreads, progressBar, slowPrint} from '/scripts/lib/utils';
import {copyWithDependencies} from '/scripts/copy';
/** /**
* BitBurner autocomplete * Check if exploit is available for use.
* @param data {server: string[], txts: string[], scripts: string[], flags: string[]} - Contextual information *
* @param {NS} ns - BitBurner API
* @returns {string[]} - Available exploits
*/
export function availableExploits(ns) {
return ['BruteSSH.exe', 'FTPCrack.exe', 'relaySMTP.exe', 'HTTPWorm.exe', 'SQLInject.exe']
.filter(e => ns.fileExists(e));
}
/**
* Attempt to root a server.
*
* @param {NS} ns - BitBurner API
* @param {string} server - Server to attempt to root
* @returns {[boolean, string[]]} - Tuple of whether the server was successfully rooted & the exploits which where ran
*/
export function root(ns, server) {
function runExploit(name) {
if(name == 'BruteSSH.exe') ns.brutessh(server);
else if(name == 'FTPCrack.exe') ns.ftpcrack(server);
else if(name == 'relaySMTP.exe') ns.relaysmtp(server);
else if(name == 'HTTPWorm.exe') ns.httpworm(server);
else if(name == 'SQLInject.exe') ns.sqlinject(server);
}
const exploits = availableExploits(ns);
exploits.forEach(e => runExploit(e));
try {
ns.nuke(server)
return [true, exploits];
} catch {
return [false, exploits];
}
}
/**
* Automatically gain root access to a server. A file can also be uploaded & executed.
*
* @param {NS} ns - BitBurner API
*/
export async function main(ns) {
// Setup
ns.disableLog('ALL');
const argParser = new ArgParser('rootkit.js', 'Automatically gain root access to a server. A file can also be uploaded & executed.', [
{name: 'server', desc: 'Server to root, defaults to local server', optional: true, default: ns.getHostname()},
{name: 'script', desc: 'Script to copy & execute', optional: true},
{name: 'args', desc: 'Arguments for script. Forward the discovered server with: {{SERVER}}', optional: true, extras: true},
{name: 'cpu', desc: 'Number of CPU threads to start script with, will use maximum if not specified', flags: ['-c', '--cpu'], default: false},
{name: 'quite', desc: 'Suppress program output', flags: ['-q', '--quite'], default: false},
]);
const args = argParser.parse(ns.args);
if(args['script'] && !args['cpu']) args['cpu'] =
~~(ns.getServerMaxRam(args['server']) / ns.getScriptRam(args['script'], 'home')) || 1;
// Help
if(args['help'] || args['_error'].length)
return ns.tprint(argParser.help(args['help'] ? null : args['_error'][0], args['_command']));
// Banner
if(!args['quite']) {
ns.tprint('===================================================');
ns.tprint(`Rooting: ${args['server']}`);
ns.tprint('===================================================');
}
// Root server if access is restricted
if(!ns.hasRootAccess(args['server'])) {
const [rooted, exploits] = root(ns, args['server']);
if(exploits.length >= 1) await slowPrint(ns, 'Exploiting SSH (*:22)...');
if(exploits.length >= 2) await slowPrint(ns, 'Exploiting FTP (*:24)...');
if(exploits.length >= 3) await slowPrint(ns, 'Exploiting SMTP (*:25)...');
if(exploits.length >= 4) await slowPrint(ns, 'Exploiting HTTP (*:80)...');
if(exploits.length >= 5) await slowPrint(ns, 'Exploiting MSQL (*:3306)...');
ns.tprint(`Root: ${rooted ? 'Success!' : 'Failed'}`);
if(!rooted) ns.exit();
} else {
ns.tprint(`Root: Skipped`);
}
ns.tprint('');
// Start script if required
if(args['script']) {
// Copy script & it's dependencies
const files = await copyWithDependencies(ns, args['script'], args['server']);
if(!args['quite']) {
await ns.sleep(500);
ns.tprint('Copying files:');
for(let file of files) await progressBar(ns, file);
}
// Start the script
const threads = args['cpu'] || maxThreads(ns, args['file'], args['server']) || 1;
if(!args['quite']) {
ns.tprint('');
ns.tprint(`Executing with ${threads} thread${threads > 1 ? 's' : ''}...`);
await ns.sleep(500);
}
ns.killall(args['server']);
const pid = ns.exec(args['file'], args['server'], threads, args['args']);
if(!args['quite']) {
ns.tprint(!!pid ? 'Done!' : 'Failed to start');
ns.tprint('');
}
}
}
/**
* BitBurner autocomplete.
*
* @param {{servers: string[], txts: string[], scripts: string[], flags: string[]}} data - Contextual information
* @returns {string[]} - Pool of autocomplete options * @returns {string[]} - Pool of autocomplete options
*/ */
export function autocomplete(data) { export function autocomplete(data) {
return [...data.servers, ...data.scripts]; return [...data.servers, ...data.scripts];
} }
/**
* Pwn a target server with availible tools. Additionally can copy & execute a script after pwning.
* @param ns {NS} - Bitburner API
*/
export async function main(ns) {
// Setup
ns.disableLog('ALL');
const argParser = new ArgParser('rootkit.js', 'Automatically gain root access to a device. A file can also be uploaded & executed.', null, [
{name: 'device', desc: 'Device to root, defaults to current machine', optional: true, default: ns.getHostname(), type: 'string'},
{name: 'script', desc: 'Script to copy & execute', optional: true, type: 'string'},
{name: 'args', desc: 'Arguments for script. Forward the current target with: {{TARGET}}', optional: true, extras: true, type: 'string'},
{name: 'cpu', desc: 'Number of CPU threads to use with script', flags: ['-c', '--cpu'], type: 'num'},
{name: 'silent', desc: 'Surpress program output', flags: ['-s', '--silent'], type: 'bool'}
], true);
try {
const args = argParser.parse(ns.args);
if(args['script'] && !args['cpu']) args['cpu'] = ~~(ns.getServerMaxRam(args['device']) / ns.getScriptRam(args['script'], 'home')) || 1;
// Banner
if(!args['silent']) {
ns.tprint('===================================================');
ns.tprint(`Rooting: ${args['device']}`);
ns.tprint('===================================================');
}
// Check if we already have root
if(ns.hasRootAccess(args['device'])) {
if(!args['silent']) ns.tprint('Root: Skipped');
} else {
let spacer = false;
try {
// Run exploits
ns.brutessh(args['device']);
if(!args['silent']) {
await slowPrint(ns, `Checking SSH (:22)...`, 0.5, 1.5);
spacer = true;
}
ns.ftpcrack(args['device']);
if(!args['silent']) await slowPrint(ns, `Checking FTP (:24)...`, 0.5, 1.5);
ns.relaysmtp(args['device']);
if(!args['silent']) await slowPrint(ns, `Checking SMTP (:25)...`, 0.5, 1.5);
ns.httpworm(args['device']);
if(!args['silent']) await slowPrint(ns, `Checking HTTP (:80)...`, 0.5, 1.5);
ns.sqlinject(args['device']);
if(!args['silent']) await slowPrint(ns, `Checking SQL (:3306)...`, 0.5, 1.5);
} catch {
} finally {
try {
// Attempt root
if(spacer) ns.tprint('');
ns.nuke(args['device']);
if(!args['silent']) ns.tprint(`Root: Success!`);
} catch {
if(!args['silent']) {
ns.tprint(`Root: Failed`);
ns.tprint('');
}
ns.exit();
}
}
}
ns.tprint('');
if(args['script']) {
// Detect script dependencies & copy everything to target
const files = await copyWithDependencies(ns, args['script'], args['device']);
if(!args['silent']) {
await ns.sleep(500);
ns.tprint('Copying files:');
for(let file of files) await progressBar(ns, file);
}
// Run script
if(!args['silent']) {
ns.tprint('');
ns.tprint(`Executing with ${args['cpu']} thread${args['cpu'] > 1 ? 's' : ''}...`);
await ns.sleep(500);
}
ns.scriptKill(args['script'], args['device']);
const pid = ns.exec(args['script'], args['device'], args['cpu'], ...args['args']
.map(a => a == '{{TARGET}}' ? args['device'] : a));
if(!args['silent']) {
ns.tprint(!!pid ? 'Done!' : 'Failed to start');
ns.tprint('');
}
}
} catch(err) {
if(err instanceof ArgError) return ns.tprint(argParser.help(err.message));
throw err;
}
}

View File

@ -1,9 +1,12 @@
import {ArgParser} from '/scripts/lib/arg-parser2'; import {ArgParser} from '/scripts/lib/arg-parser';
import {Logger} from '/scripts/lib/logger'; import {Logger} from '/scripts/lib/logger';
import {maxThreads, toCurrency} from '/scripts/lib/utils';
import {copyWithDependencies} from "/scripts/copy";
/** /**
* Automate the buying & upgrading of servers. * Automate the buying & upgrading of servers.
* @param ns {NS} - BitBurner API *
* @param {NS} ns - BitBurner API
*/ */
export async function main(ns) { export async function main(ns) {
// Setup // Setup
@ -12,34 +15,48 @@ export async function main(ns) {
const logger = new Logger(ns, [ const logger = new Logger(ns, [
() => `Server Manager: ${servers.length}` () => `Server Manager: ${servers.length}`
]); ]);
const argParser = new ArgParser(ns, 'server-manager.js', 'Automate the buying & upgrading of servers.', [ const argParser = new ArgParser('server-manager.js', 'Automate the buying & upgrading of servers. Automatically starts script after purchase. Tail for live updates.', [
{name: 'script', desc: 'Script to copy & execute', optional: true},
{name: 'args', desc: 'Arguments for script. Forward the discovered server with: {{SERVER}}', optional: true, extras: true},
{name: 'balance', desc: 'Prevent spending bellow point', flags: ['-b', '--balance'], default: 0}, {name: 'balance', desc: 'Prevent spending bellow point', flags: ['-b', '--balance'], default: 0},
{name: 'cpu', desc: 'Number of CPU threads to start script with, will use maximum if not specified', flags: ['-c', '--cpu'], default: false},
{name: 'limit', desc: 'Limit the number of servers that can be purchased, defaults to 25', flags: ['-l', '--limit'], default: 25}, {name: 'limit', desc: 'Limit the number of servers that can be purchased, defaults to 25', flags: ['-l', '--limit'], default: 25},
{name: 'ram', desc: 'Amount of RAM to purchase new servers with, defaults to 8 GB', flags: ['-r', '--ram'], default: 8}, {name: 'ram', desc: 'Amount of RAM to purchase new servers with, defaults to 8 GB', flags: ['-r', '--ram'], default: 8},
{name: 'sleep', desc: 'Amount of time to wait between purchases, defaults to 1 (second)', flags: ['-s', '--sleep'], default: 1} {name: 'sleep', desc: 'Amount of time to wait between purchases, defaults to 1 (second)', flags: ['-s', '--sleep'], default: 1}
]); ]);
const args = argParser.parse(); const args = argParser.parse(ns.args);
const serverPrefix = 'botnet_' const serverPrefix = 'botnet_'
const maxRam = ns.getPurchasedServerMaxRam(); const maxRam = ns.getPurchasedServerMaxRam();
const minRamCost = ns.getPurchasedServerCost(args['ram']); const minRamCost = ns.getPurchasedServerCost(args['ram']);
// Help async function startScript(server) {
if(args['help'] || args['_error']) await copyWithDependencies(ns, args['script'], server);
return argParser.help(args['help'] ? null : args['_error'], args['_command']); const threads = args['cpu'] || maxThreads(ns, args['script'], server) || 1;
const pid = ns.exec(args['script'], server, threads, ...args['args']);
logger.log(`Starting "${args['script']}" with ${threads} thread${threads > 1 ? 's' : ''}`);
logger[pid == -1 ? 'warn' : 'log'](pid == -1 ? 'Done!' : 'Failed to start');
}
// Run // Help
if(args['help'] || args['_error'].length)
return ns.tprint(argParser.help(args['help'] ? null : args['_error'][0], args['_command']));
// Main loop
// noinspection InfiniteLoopJS
while(true) { while(true) {
servers = ns.getPurchasedServers(); servers = ns.getPurchasedServers();
const balance = ns.getServerMoneyAvailable('home'); const balance = ns.getServerMoneyAvailable('home');
let targ
// Purchase new server if we can afford it // Purchase new server if we can afford it
if(servers.length < args['limit'] && balance - minRamCost > args['balance']) { if(servers.length < args['limit'] && balance - minRamCost > args['balance']) {
logger.log(`Buying server (${args['ram']} GB): ${minRamCost.toLocaleString('en-US', {style: 'currency', currency: 'USD'})}`); logger.log(`Buying server (${args['ram']} GB): ${toCurrency(minRamCost)}`);
ns.purchaseServer(`${serverPrefix}${servers.length}`, args['ram']); ns.purchaseServer(`${serverPrefix}${servers.length}`, args['ram']);
// Run the script if requested
if(args['script']) await startScript(`${serverPrefix}${servers.length - 1}`);
} else { // Check for upgrades } else { // Check for upgrades
const upgrades = servers.map(server => { let upgrades = servers.map(server => {
// Calculate next RAM upgrades (must be a power of two: 2, 4, 8, 16, 32...) // Calculate next RAM upgrades (must be a power of two: 2, 4, 8, 16, 32...)
let ram = Math.pow(2, Math.log2(ns.getServerRam(server)[0]) + 1); let ram = Math.pow(2, Math.log2(ns.getServerMaxRam(server)) + 1);
if(ram > maxRam) ram = null; if(ram > maxRam) ram = null;
return { return {
server, server,
@ -47,7 +64,7 @@ export async function main(ns) {
cost: ram ? ns.getPurchasedServerCost(ram) : null cost: ram ? ns.getPurchasedServerCost(ram) : null
} }
}); });
upgrades.sort((a, b) => { // Sort by price upgrades = upgrades.sort((a, b) => { // Sort by price
if(a.cost < b.cost) return 1; if(a.cost < b.cost) return 1;
if(a.cost < b.cost) return -1; if(a.cost < b.cost) return -1;
return 0; return 0;
@ -56,10 +73,13 @@ export async function main(ns) {
// Do the cheapest upgrade if we can afford it // Do the cheapest upgrade if we can afford it
const upgrade = upgrades[0]; const upgrade = upgrades[0];
if(upgrade && !!upgrade.ram && balance - upgrade.cost > args['balance']) { if(upgrade && !!upgrade.ram && balance - upgrade.cost > args['balance']) {
logger.log(`Upgrading ${upgrade.server}: ${upgrade.ram} GB/${upgrade.cost.toLocaleString('en-US', {style: 'currency', currency: 'USD'})}`); logger.log(`Upgrading ${upgrade.server}: ${upgrade.ram} GB / ${toCurrency(upgrade.cost)}`);
ns.killall(upgrade.server); ns.killall(upgrade.server);
ns.deleteServer(upgrade.server); ns.deleteServer(upgrade.server);
ns.purchaseServer(upgrade.server, upgrade.ram); ns.purchaseServer(upgrade.server, upgrade.ram);
// Run the script if requested
if(args['script']) await startScript(upgrade.server);
} }
} }
await ns.sleep(args['sleep'] * 1000); await ns.sleep(args['sleep'] * 1000);

View File

@ -1,96 +1,121 @@
class ArgError extends Error {}
class ArgParser { class ArgParser {
/** /**
* Create a unix-like argument parser to extract flags from the argument list. Can also create help messages. * Create a unix-like argument parser to extract flags from the argument list. Can also create help messages.
* @param name {string} - Script name *
* @param desc {string} - Help text desciption * @param {string} name - Script name
* @param examples {string[]} - Help text examples * @param {string} desc - Help description
* @param argList {name: string, desc: string, flags: string[], type: string, default: any}[] - Array of CLI arguments * @param {(ArgParser | {name: string, desc: string, flags?: string[], optional?: boolean, default?: any})[]} argList - Array of CLI arguments
* @param allowUnknown {boolean} - Allow unknown flags * @param {string[]} examples - Additional examples to display
*/ */
constructor(name, desc, examples, argList, allowUnknown = false) { constructor(name, desc, argList = [], examples = []) {
this.name = name ?? 'example.js'; this.name = name;
this.description = desc ?? 'Example description'; this.desc = desc;
this.examples = examples || [`${argList.find(arg => !!arg.flags) ? '[OPTIONS] ' : ''}${argList.filter(arg => !arg.flags).map(arg => (arg.optional ? `[${arg.name.toUpperCase()}]` : arg.name.toUpperCase()) + (arg.extras ? '...' : '')).join(' ')}`];
this.examples.push('--help'); // Arguments
this.argList = argList || []; this.commands = argList.filter(arg => arg instanceof ArgParser);
this.argList.push({name: 'help', desc: 'Display this help message', flags: ['-h', '--help'], type: 'bool'}); this.args = argList.filter(arg => !arg.flags || !arg.flags.length);
this.allowUnknown = allowUnknown; this.flags = argList.filter(arg => !(arg instanceof ArgParser) && arg.flags && arg.flags.length);
this.flags.push({name: 'help', desc: 'Display this help message', flags: ['-h', '--help'], default: false});
this.defaults = argList.reduce((acc, arg) => ({...acc, [arg.name]: arg?.extras ? [] : arg.default ?? null}), {});
// Examples
this.examples = [
...examples,
`[OPTIONS] ${this.args.map(arg => (arg.optional ? `[${arg.name.toUpperCase()}]` : arg.name.toUpperCase()) + (arg.extras ? '...' : '')).join(' ')}`,
this.commands.length ? `[OPTIONS] COMMAND` : null,
`--help ${this.commands.length ? '[COMMAND]' : ''}`
].filter(e => !!e);
} }
/** /**
* Parse an array into an arguments dictionary using the configuration. * Parse an array into an arguments dictionary using the configuration.
* @param args {string[]} - Array of arguments to be parsed *
* @param {string[]} args - Array of arguments to be parsed
* @returns {object} - Dictionary of arguments with defaults applied * @returns {object} - Dictionary of arguments with defaults applied
*/ */
parse(args) { parse(args) {
// Parse arguments // Parse arguments
const queue = [...args], extra = []; let extras = [], parsed = {...this.defaults, '_error': []}, queue = [...args];
const parsed = this.argList.reduce((acc, arg) => ({...acc, [arg.name]: arg.default ?? (arg.type == 'bool' ? false : null)}), {});
// Flags
while(queue.length) { while(queue.length) {
let parse = queue.splice(0, 1)[0]; let arg = queue.splice(0, 1)[0];
if(parse[0] == '-') { if(arg[0] == '-') { // Flags
// Check combined flags // Check for combined shorthand
if(parse[1] != '-' && parse.length > 2) { if(arg[1] != '-' && arg.length > 2) {
parse = `-${parse[1]}`; queue = [...arg.substring(2).split('').map(a => `-${a}`), ...queue];
queue = parse.substring(1).split('').map(a => `-${a}`).concat(queue); arg = `-${arg[1]}`;
} }
// Find & add flag // Find & add flag
const split = parse.split('='); const combined = arg.split('=');
const arg = this.argList.find(arg => arg.flags && arg.flags.includes(split[0] || parse)); const argDef = this.flags.find(flag => flag.flags.includes(combined[0] || arg));
if(arg == null) { if(argDef == null) { // Not found, add to extras
if(!this.allowUnknown) throw new ArgError(`Option unknown: ${parse}`); extras.push(arg);
extra.push(parse);
continue; continue;
} }
if(arg.name == 'help') throw new ArgError('Help'); const value = argDef.default === false ? true : argDef.default === true ? false : queue.splice(queue.findIndex(q => q[0] != '-'), 1)[0] || argDef.default;
const value = arg.type == 'bool' ? true : split[1] || queue.splice(queue.findIndex(q => q[0] != '-'), 1)[0]; if(value == null) parsed['_error'].push(`Option missing value: ${arg.name}`);
if(value == null) throw new ArgError(`Option missing value: ${arg.name}`); parsed[argDef.name] = value;
parsed[arg.name] = value; } else { // Command
} else { const c = this.commands.find(command => command.name == arg);
// Save for required parsing if(!!c) {
extra.push(parse); const parsedCommand = c.parse(queue.splice(0, queue.length));
Object.keys(parsedCommand).forEach(key => {
if(parsed[key] != parsedCommand[key] && parsedCommand[key] == c.defaults[key])
delete parsedCommand[key];
});
parsed = {
...parsed,
...parsedCommand,
_command: c.name
};
} else extras.push(arg); // Not found, add to extras
} }
} }
// Arguments // Arguments
this.argList.filter(arg => !arg.flags && !arg.extras).forEach(arg => { this.args.filter(arg => !arg.extras).forEach(arg => {
if(!arg.optional && !extra.length) throw new ArgError(`Argument missing: ${arg.name.toUpperCase()}`); if(!arg.optional && !extras.length) parsed['_error'].push(`Argument missing: ${arg.name.toUpperCase()}`);
const value = extra.splice(0, 1)[0]; if(extras.length) parsed[arg.name] = extras.splice(0, 1)[0];
if(value != null) parsed[arg.name] = value;
}); });
// Extras // Extras
const extraKey = this.argList.find(arg => arg.extras)?.name || 'extra'; const extraKey = this.args.find(arg => arg.extras)?.name || '_extra';
parsed[extraKey] = extra; parsed[extraKey] = extras;
return parsed; return parsed;
} }
/** /**
* Create help message from the provided description, examples & argument list. * Create help message from the provided description & argument list.
* @param message {string} - Message to display, defaults to the description *
* @param {string} message - Message to display, defaults to the description
* @param {string} command - Command help message to show
* @returns {string} - Help message * @returns {string} - Help message
*/ */
help(msg) { help(message = '', command = '') {
const spacer = (text) => Array(24 - text.length || 1).fill(' ').join('');
// Help with specific command
if(command) {
const argParser = this.commands.find(parser => parser.name == command);
if(!argParser) throw new Error(`${command.toUpperCase()} does not have a help`)
return argParser.help(message);
}
// Description // Description
let message = '\n\n' + (msg && msg.toLowerCase() != 'help' ? msg : this.description); let msg = `\n\n${message || this.desc}`;
// Usage // Examples
if(this.examples.length) message += '\n\nUsage:\t' + this.examples.map(ex => `run ${this.name} ${ex}`).join('\n\t'); msg += '\n\nUsage:\t' + this.examples.map(ex => `run ${this.name} ${ex}`).join('\n\t');
// Arguments // Arguments
const req = this.argList.filter(a => !a.flags); if(this.args.length) msg += '\n\n\t' + this.args
if(req.length) message += '\n\n\t' + req.map(arg => { .map(arg => `${arg.name.toUpperCase()}${spacer(arg.name)}${arg.desc}`)
const padding = 3 - ~~(arg.name.length / 8); .join('\n\t');
return `${arg.name.toUpperCase()}${Array(padding).fill('\t').join('')} ${arg.desc}`;
}).join('\n\t');
// Flags // Flags
const opts = this.argList.filter(a => a.flags); msg += '\n\nOptions:\n\t' + this.flags.map(flag => {
if(opts.length) message += '\n\nOptions:\n\t' + opts.map(a => { const flags = flag.flags.join(', ');
const flgs = a.flags.join(' '); return `${flags}${spacer(flags)}${flag.desc}`;
const padding = 3 - ~~(flgs.length / 8);
return `${flgs}${Array(padding).fill('\t').join('')} ${a.desc}`;
}).join('\n\t'); }).join('\n\t');
// Print final message // Commands
return `${message}\n\n`; if(this.commands.length) msg += '\n\nCommands:\n\t' + this.commands
.map(command => `${command.name}${spacer(command.name)}${command.desc}`)
.join('\n\t');
return `${msg}\n\n`;
} }
} }
@ -101,10 +126,10 @@ class ArgParser {
* *
* `/script/test.js [||||||||||----------] 50% (24.2 MB/s)` * `/script/test.js [||||||||||----------] 50% (24.2 MB/s)`
* *
* @param ns {NS} - BitBurner API * @param {NS} ns - BitBurner API
* @param name {string} - Name to display at the begging of bar * @param {string} name - Name to display at the begging of bar
* @param showSpeed {boolean} - Show the speed in the progress bar * @param {boolean} showSpeed - Show the speed in the progress bar
* @param time {number} - Time it takes for bar to fill * @param {number} time - Time it takes for bar to fill
*/ */
export async function progressBar(ns, name, showSpeed = true, time = Math.random() + 0.5) { export async function progressBar(ns, name, showSpeed = true, time = Math.random() + 0.5) {
const text = (percentage, speed) => { const text = (percentage, speed) => {
@ -132,10 +157,11 @@ export async function progressBar(ns, name, showSpeed = true, time = Math.random
/** /**
* Print text to the terminal & then delay for a random amount of time to emulate execution time. * Print text to the terminal & then delay for a random amount of time to emulate execution time.
* @param ns {NS} - BitBurner API *
* @param message {string} - Text to display * @param {NS} ns - BitBurner API
* @param min {number} - minimum amount of time to wait after printing text * @param {string} message - Text to display
* @param max {number} - maximum amount of time to wait after printing text * @param {number} min - minimum amount of time to wait after printing text
* @param {number} max - maximum amount of time to wait after printing text
*/ */
async function slowPrint(ns, message, min = 0.5, max = 1.5) { async function slowPrint(ns, message, min = 0.5, max = 1.5) {
const time = ~~(Math.random() * (max * 1000 - min * 1000)) + min * 1000; const time = ~~(Math.random() * (max * 1000 - min * 1000)) + min * 1000;
@ -151,61 +177,59 @@ export async function main(ns) {
// Setup // Setup
ns.disableLog('ALL'); ns.disableLog('ALL');
const updateFile = 'update.js'; const updateFile = 'update.js';
const argParser = new ArgParser(updateFile, 'Download the latest script updates from the repository using wget.', null, [ const argParser = new ArgParser(updateFile, 'Download the latest script updates from the repository using wget.', [
{name: 'device', desc: 'Device to update, defaults to current machine', optional: true, default: ns.getHostname(), type: 'string'}, {name: 'device', desc: 'Device to update, defaults to current machine', optional: true, default: ns.getHostname()},
{name: 'skip-self', desc: 'Skip updating self (for debugging & used internally)', flags: ['--skip-self'], type: 'bool'}, {name: 'skip-self', desc: 'Skip updating self (for debugging & used internally)', flags: ['--skip-self'], default: false},
{name: 'no-banner', desc: 'Hide the banner (Used internally)', flags: ['--no-banner'], type: 'bool'} {name: 'no-banner', desc: 'Hide the banner (Used internally)', flags: ['--no-banner'], default: false}
]); ]);
const args = argParser.parse(ns.args || []);
const src = 'https://gitlab.zakscode.com/ztimson/BitBurner/-/raw/develop/scripts/';
const dest = '/scripts/';
const fileList = [
'lib/arg-parser.js',
'lib/logger.js',
'lib/utils.js',
'botnet-manager.js',
'connect.js',
'copy.js',
'crawler.js',
'find-target.js',
'hacknet-manager.js',
'miner.js',
'network-graph.js',
'rootkit.js'
];
try { // Help
const args = argParser.parse(ns.args || []); if(args['help'] || args['_error'].length)
const src = 'https://gitlab.zakscode.com/ztimson/BitBurner/-/raw/develop/scripts/'; return ns.tprint(argParser.help(args['help'] ? null : args['_error'][0], args['_command']));
const dest = '/scripts/';
const fileList = [
'lib/arg-parser.js',
'lib/logger.js',
'lib/utils.js',
'botnet-manager.js',
'connect.js',
'copy.js',
'crawler.js',
'find-target.js',
'hacknet-manager.js',
'miner.js',
'network-graph.js',
'rootkit.js'
];
if(!args['no-banner']) { if(!args['no-banner']) {
// Banner // Banner
ns.tprint('==================================================='); ns.tprint('===================================================');
ns.tprint(`Updating: ${args['device']}`); ns.tprint(`Updating: ${args['device']}`);
ns.tprint('==================================================='); ns.tprint('===================================================');
}
// Run
if(!args['skip-self']) { // Update self & restart
await slowPrint(ns, 'Updating self:');
await ns.wget(`${src}${updateFile}`, `${dest}${updateFile}`, args['device']);
await progressBar(ns, `${dest}${updateFile}`);
ns.tprint('');
await slowPrint(ns, 'Restarting...');
const pid = ns.run(`${dest}${updateFile}`, 1, args['device'], '--skip-self', '--no-banner');
if(pid == 0) ns.tprint('Failed');
else ns.tprint('Complete');
return await slowPrint(ns, '');
} else { // Update everything else
await slowPrint(ns, 'Downloading scripts:');
for(let file of fileList) {
await ns.wget(`${src}${file}`, `${dest}${file}`, args['device']);
await progressBar(ns, `${dest}${file}`);
} }
ns.tprint('');
// Run ns.tprint('Done!');
if(!args['skip-self']) { // Update self & restart ns.tprint('');
await slowPrint(ns, 'Updating self:');
await ns.wget(`${src}${updateFile}`, `${dest}${updateFile}`, args['device']);
await progressBar(ns, `${dest}${updateFile}`);
ns.tprint('');
await slowPrint(ns, 'Restarting...');
const pid = ns.run(`${dest}${updateFile}`, 1, args['device'], '--skip-self', '--no-banner');
if(pid == 0) ns.tprint('Failed');
else ns.tprint('Complete');
return await slowPrint(ns, '');
} else { // Update everything else
await slowPrint(ns, 'Downloading scripts:');
for(let file of fileList) {
await ns.wget(`${src}${file}`, `${dest}${file}`, args['device']);
await progressBar(ns, `${dest}${file}`);
}
ns.tprint('');
ns.tprint('Done!');
ns.tprint('');
}
} catch(err) {
if(err instanceof ArgError) return ns.tprint(argParser.help(err.message));
throw err;
} }
} }