forked from alainbryden/bitburner-scripts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
helpers.js
334 lines (305 loc) · 20.1 KB
/
helpers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
/**
* Return a formatted representation of the monetary amount using scale symbols (e.g. $6.50M)
* @param {number} num - The number to format
* @param {number=} maxSignificantFigures - (default: 6) The maximum significant figures you wish to see (e.g. 123, 12.3 and 1.23 all have 3 significant figures)
* @param {number=} maxDecimalPlaces - (default: 3) The maximum decimal places you wish to see, regardless of significant figures. (e.g. 12.3, 1.2, 0.1 all have 1 decimal)
**/
export function formatMoney(num, maxSignificantFigures = 6, maxDecimalPlaces = 3) {
let numberShort = formatNumberShort(num, maxSignificantFigures, maxDecimalPlaces);
return num >= 0 ? "$" + numberShort : numberShort.replace("-", "-$");
}
const symbols = ["", "k", "m", "b", "t", "q", "Q", "s", "S", "o", "n", "e33", "e36", "e39"];
/**
* Return a formatted representation of the monetary amount using scale sympols (e.g. 6.50M)
* @param {number} num - The number to format
* @param {number=} maxSignificantFigures - (default: 6) The maximum significant figures you wish to see (e.g. 123, 12.3 and 1.23 all have 3 significant figures)
* @param {number=} maxDecimalPlaces - (default: 3) The maximum decimal places you wish to see, regardless of significant figures. (e.g. 12.3, 1.2, 0.1 all have 1 decimal)
**/
export function formatNumberShort(num, maxSignificantFigures = 6, maxDecimalPlaces = 3) {
if (Math.abs(num) > 10 ** (3 * symbols.length)) // If we've exceeded our max symbol, switch to exponential notation
return num.toExponential(Math.min(maxDecimalPlaces, maxSignificantFigures - 1));
for (var i = 0, sign = Math.sign(num), num = Math.abs(num); num >= 1000 && i < symbols.length; i++) num /= 1000;
// TODO: A number like 9.999 once rounted to show 3 sig figs, will become 10.00, which is now 4 sig figs.
return ((sign < 0) ? "-" : "") + num.toFixed(Math.max(0, Math.min(maxDecimalPlaces, maxSignificantFigures - Math.floor(1 + Math.log10(num))))) + symbols[i];
}
/** Convert a shortened number back into a value */
export function parseShortNumber(text = "0") {
let parsed = Number(text);
if (!isNaN(parsed)) return parsed;
for (const sym of symbols.slice(1))
if (text.toLowerCase().endsWith(sym))
return Number.parseFloat(text.slice(0, text.length - sym.length)) * Math.pow(10, 3 * symbols.indexOf(sym));
return Number.NaN;
}
/**
* Return a number formatted with the specified number of significatnt figures or decimal places, whichever is more limiting.
* @param {number} num - The number to format
* @param {number=} minSignificantFigures - (default: 6) The minimum significant figures you wish to see (e.g. 123, 12.3 and 1.23 all have 3 significant figures)
* @param {number=} minDecimalPlaces - (default: 3) The minimum decimal places you wish to see, regardless of significant figures. (e.g. 12.3, 1.2, 0.1 all have 1 decimal)
**/
export function formatNumber(num, minSignificantFigures = 3, minDecimalPlaces = 1) {
return num == 0.0 ? num : num.toFixed(Math.max(minDecimalPlaces, Math.max(0, minSignificantFigures - Math.ceil(Math.log10(num)))));
}
/** Formats some RAM amount as a round number of GB with thousands separators e.g. `1,028 GB` */
export function formatRam(num) { return `${Math.round(num).toLocaleString()} GB`; }
/** Return a datatime in ISO format */
export function formatDateTime(datetime) { return datetime.toISOString(); }
/** Format a duration (in milliseconds) as e.g. '1h 21m 6s' for big durations or e.g '12.5s' / '23ms' for small durations */
export function formatDuration(duration) {
if (duration < 1000) return `${duration.toFixed(0)}ms`
const portions = [];
const msInHour = 1000 * 60 * 60;
const hours = Math.trunc(duration / msInHour);
if (hours > 0) {
portions.push(hours + 'h');
duration -= (hours * msInHour);
}
const msInMinute = 1000 * 60;
const minutes = Math.trunc(duration / msInMinute);
if (minutes > 0) {
portions.push(minutes + 'm');
duration -= (minutes * msInMinute);
}
let seconds = (duration / 1000.0)
// Include millisecond precision if we're on the order of seconds
seconds = (hours == 0 && minutes == 0) ? seconds.toPrecision(3) : seconds.toFixed(0);
if (seconds > 0) {
portions.push(seconds + 's');
duration -= (minutes * 1000);
}
return portions.join(' ');
}
/** Generate a hashCode for a string that is pretty unique most of the time */
export function hashCode(s) { return s.split("").reduce(function (a, b) { a = ((a << 5) - a) + b.charCodeAt(0); return a & a }, 0); }
/** @param {NS} ns **/
export function disableLogs(ns, listOfLogs) { ['disableLog'].concat(...listOfLogs).forEach(log => checkNsInstance(ns, '"disableLogs"').disableLog(log)); }
/** Joins all arguments as components in a path, e.g. pathJoin("foo", "bar", "/baz") = "foo/bar/baz" **/
export function pathJoin(...args) {
return args.filter(s => !!s).join('/').replace(/\/\/+/g, '/');
}
/** Gets the path for the given local file, taking into account optional subfolder relocation via git-pull.js **/
export function getFilePath(file) {
const subfolder = ''; // git-pull.js optionally modifies this when downloading
return pathJoin(subfolder, file);
}
// FUNCTIONS THAT PROVIDE ALTERNATIVE IMPLEMENTATIONS TO EXPENSIVE NS FUNCTIONS
// VARIATIONS ON NS.RUN
/** @param {NS} ns
* Use where a function is required to run a script and you have already referenced ns.run in your script **/
export function getFnRunViaNsRun(ns) { return checkNsInstance(ns, '"getFnRunViaNsRun"').run; }
/** @param {NS} ns
* Use where a function is required to run a script and you have already referenced ns.exec in your script **/
export function getFnRunViaNsExec(ns, host = "home") {
checkNsInstance(ns, '"getFnRunViaNsExec"');
return function (scriptPath, ...args) { return ns.exec(scriptPath, host, ...args); }
}
// VARIATIONS ON NS.ISRUNNING
/** @param {NS} ns
* Use where a function is required to run a script and you have already referenced ns.run in your script */
export function getFnIsAliveViaNsIsRunning(ns) { return checkNsInstance(ns, '"getFnIsAliveViaNsIsRunning"').isRunning; }
/** @param {NS} ns
* Use where a function is required to run a script and you have already referenced ns.exec in your script */
export function getFnIsAliveViaNsPs(ns) {
checkNsInstance(ns, '"getFnIsAliveViaNsPs"');
return function (pid, host) { return ns.ps(host).some(process => process.pid === pid); }
}
/**
* Retrieve the result of an ns command by executing it in a temporary .js script, writing the result to a file, then shuting it down
* Importing incurs a maximum of 1.1 GB RAM (0 GB for ns.read, 1 GB for ns.run, 0.1 GB for ns.isRunning).
* Has the capacity to retry if there is a failure (e.g. due to lack of RAM available). Not recommended for performance-critical code.
* @param {NS} ns - The nestcript instance passed to your script's main entry point
* @param {string} command - The ns command that should be invoked to get the desired data (e.g. "ns.getServer('home')" )
* @param {string=} fName - (default "/Temp/{commandhash}-data.txt") The name of the file to which data will be written to disk by a temporary process
* @param {bool=} verbose - (default false) If set to true, pid and result of command are logged.
**/
export async function getNsDataThroughFile(ns, command, fName, verbose = false, maxRetries = 5, retryDelayMs = 50) {
checkNsInstance(ns, '"getNsDataThroughFile"');
if (!verbose) disableLogs(ns, ['run', 'isRunning']);
return await getNsDataThroughFile_Custom(ns, ns.run, ns.isRunning, command, fName, verbose, maxRetries, retryDelayMs);
}
/**
* An advanced version of getNsDataThroughFile that lets you pass your own "fnRun" and "fnIsAlive" implementations to reduce RAM requirements
* Importing incurs no RAM (now that ns.read is free) plus whatever fnRun / fnIsAlive you provide it
* Has the capacity to retry if there is a failure (e.g. due to lack of RAM available). Not recommended for performance-critical code.
* @param {NS} ns - The nestcript instance passed to your script's main entry point
* @param {function} fnRun - A single-argument function used to start the new sript, e.g. `ns.run` or `(f,...args) => ns.exec(f, "home", ...args)`
* @param {function} fnIsAlive - A single-argument function used to start the new sript, e.g. `ns.isRunning` or `pid => ns.ps("home").some(process => process.pid === pid)`
**/
export async function getNsDataThroughFile_Custom(ns, fnRun, fnIsAlive, command, fName, verbose = false, maxRetries = 5, retryDelayMs = 50) {
checkNsInstance(ns, '"getNsDataThroughFile_Custom"');
if (!verbose) disableLogs(ns, ['read']);
const commandHash = hashCode(command);
fName = fName || `/Temp/${commandHash}-data.txt`;
const fNameCommand = (fName || `/Temp/${commandHash}-command`) + '.js'
// Prepare a command that will write out a new file containing the results of the command
// unless it already exists with the same contents (saves time/ram to check first)
// If an error occurs, it will write an empty file to avoid old results being misread.
const commandToFile = `let result="";try{result=JSON.stringify(
${command}
);}catch{} const f="${fName}"; if(ns.read(f)!=result) await ns.write(f,result,'w')`;
// Run the command with auto-retries if it fails
const pid = await runCommand_Custom(ns, fnRun, commandToFile, fNameCommand, false, maxRetries, retryDelayMs);
// Wait for the process to complete
await waitForProcessToComplete_Custom(ns, fnIsAlive, pid, verbose);
if (verbose) ns.print(`Process ${pid} is done. Reading the contents of ${fName}...`);
// Read the file, with auto-retries if it fails
const fileData = await autoRetry(ns, () => ns.read(fName), f => f !== undefined && f !== "",
() => `ns.read('${fName}') somehow returned undefined or an empty string`,
maxRetries, retryDelayMs, undefined, verbose);
if (verbose) ns.print(`Read the following data for command ${command}:\n${fileData}`);
return JSON.parse(fileData); // Deserialize it back into an object/array and return
}
/** Evaluate an arbitrary ns command by writing it to a new script and then running or executing it.
* @param {NS} ns - The nestcript instance passed to your script's main entry point
* @param {string} command - The ns command that should be invoked to get the desired data (e.g. "ns.getServer('home')" )
* @param {string=} fileName - (default "/Temp/{commandhash}-data.txt") The name of the file to which data will be written to disk by a temporary process
* @param {bool=} verbose - (default false) If set to true, the evaluation result of the command is printed to the terminal
* @param {...args} args - args to be passed in as arguments to command being run as a new script.
*/
export async function runCommand(ns, command, fileName, verbose = false, maxRetries = 5, retryDelayMs = 50, ...args) {
checkNsInstance(ns, '"runCommand"');
if (!verbose) disableLogs(ns, ['run', 'asleep']);
return await runCommand_Custom(ns, ns.run, command, fileName, verbose, maxRetries, retryDelayMs, ...args);
}
/**
* An advanced version of runCommand that lets you pass your own "isAlive" test to reduce RAM requirements (e.g. to avoid referencing ns.isRunning)
* Importing incurs 0 GB RAM (assuming fnRun, fnWrite are implemented using another ns function you already reference elsewhere like ns.exec)
* @param {NS} ns - The nestcript instance passed to your script's main entry point
* @param {function} fnRun - A single-argument function used to start the new sript, e.g. `ns.run` or `(f,...args) => ns.exec(f, "home", ...args)`
**/
export async function runCommand_Custom(ns, fnRun, command, fileName, verbose = false, maxRetries = 5, retryDelayMs = 50, ...args) {
checkNsInstance(ns, '"runCommand_Custom"');
let script = `import { formatMoney, formatNumberShort, formatDuration, parseShortNumber, scanAllServers } fr` + `om '${getFilePath('helpers.js')}'\n` +
`export async function main(ns) { try { ` +
(verbose ? `let output = ${command}; ns.tprint(output)` : command) +
`; } catch(err) { ns.tprint(String(err)); throw(err); } }`;
fileName = fileName || `/Temp/${hashCode(command)}-command.js`;
// To improve performance and save on garbage collection, we can skip writing this exact same script was previously written (common for repeatedly-queried data)
if (ns.read(fileName) != script) await ns.write(fileName, script, "w");
return await autoRetry(ns, () => fnRun(fileName, ...args), temp_pid => temp_pid !== 0,
() => `Run command returned no pid.\n Destination: ${fileName}\n Command: ${command}\nEnsure you have sufficient free RAM to run this temporary script.`,
maxRetries, retryDelayMs, undefined, verbose);
}
/**
* Wait for a process id to complete running
* Importing incurs a maximum of 0.1 GB RAM (for ns.isRunning)
* @param {NS} ns - The nestcript instance passed to your script's main entry point
* @param {int} pid - The process id to monitor
* @param {bool=} verbose - (default false) If set to true, pid and result of command are logged.
**/
export async function waitForProcessToComplete(ns, pid, verbose) {
checkNsInstance(ns, '"waitForProcessToComplete"');
if (!verbose) disableLogs(ns, ['isRunning']);
return await waitForProcessToComplete_Custom(ns, ns.isRunning, pid, verbose);
}
/**
* An advanced version of waitForProcessToComplete that lets you pass your own "isAlive" test to reduce RAM requirements (e.g. to avoid referencing ns.isRunning)
* Importing incurs 0 GB RAM (assuming fnIsAlive is implemented using another ns function you already reference elsewhere like ns.ps)
* @param {NS} ns - The nestcript instance passed to your script's main entry point
* @param {function} fnIsAlive - A single-argument function used to start the new sript, e.g. `ns.isRunning` or `pid => ns.ps("home").some(process => process.pid === pid)`
**/
export async function waitForProcessToComplete_Custom(ns, fnIsAlive, pid, verbose) {
checkNsInstance(ns, '"waitForProcessToComplete_Custom"');
if (!verbose) disableLogs(ns, ['asleep']);
// Wait for the PID to stop running (cheaper than e.g. deleting (rm) a possibly pre-existing file and waiting for it to be recreated)
for (var retries = 0; retries < 1000; retries++) {
if (!fnIsAlive(pid)) break; // Script is done running
if (verbose && retries % 100 === 0) ns.print(`Waiting for pid ${pid} to complete... (${retries})`);
await ns.asleep(10);
}
// Make sure that the process has shut down and we haven't just stopped retrying
if (fnIsAlive(pid)) {
let errorMessage = `run-command pid ${pid} is running much longer than expected. Max retries exceeded.`;
ns.print(errorMessage);
throw errorMessage;
}
}
/** Helper to retry something that failed temporarily (can happen when e.g. we temporarily don't have enough RAM to run)
* @param {NS} ns - The nestcript instance passed to your script's main entry point */
export async function autoRetry(ns, fnFunctionThatMayFail, fnSuccessCondition, errorContext = "Success condition not met",
maxRetries = 5, initialRetryDelayMs = 50, backoffRate = 3, verbose = false) {
checkNsInstance(ns, '"autoRetry"');
let retryDelayMs = initialRetryDelayMs;
while (maxRetries-- > 0) {
try {
const result = await fnFunctionThatMayFail()
if (!fnSuccessCondition(result)) throw typeof errorContext === 'string' ? errorContext : errorContext();
return result;
}
catch (error) {
const fatal = maxRetries === 0;
const errorLog = `${fatal ? 'FAIL' : 'WARN'}: (${maxRetries} retries remaining): ${String(error)}`
log(ns, errorLog, fatal, !verbose ? undefined : (fatal ? 'error' : 'warning'))
if (fatal) throw error;
await ns.asleep(retryDelayMs);
retryDelayMs *= backoffRate;
}
}
}
/** Helper to log a message, and optionally also tprint it and toast it
* @param {NS} ns - The nestcript instance passed to your script's main entry point */
export function log(ns, message = "", alsoPrintToTerminal = false, toastStyle = "", maxToastLength = 100) {
checkNsInstance(ns, '"log"');
ns.print(message);
if (alsoPrintToTerminal) ns.tprint(message);
if (toastStyle) ns.toast(message.length <= maxToastLength ? message : message.substring(0, maxToastLength - 3) + "...", toastStyle);
return message;
}
/** Helper to get a list of all hostnames on the network
* @param {NS} ns - The nestcript instance passed to your script's main entry point */
export function scanAllServers(ns) {
checkNsInstance(ns, '"scanAllServers"');
let discoveredHosts = []; // Hosts (a.k.a. servers) we have scanned
let hostsToScan = ["home"]; // Hosts we know about, but have no yet scanned
let infiniteLoopProtection = 9999; // In case you mess with this code, this should save you from getting stuck
while (hostsToScan.length > 0 && infiniteLoopProtection-- > 0) { // Loop until the list of hosts to scan is empty
let hostName = hostsToScan.pop(); // Get the next host to be scanned
for (const connectedHost of ns.scan(hostName)) // "scan" (list all hosts connected to this one)
if (!discoveredHosts.includes(connectedHost)) // If we haven't already scanned this host
hostsToScan.push(connectedHost); // Add it to the queue of hosts to be scanned
discoveredHosts.push(hostName); // Mark this host as "scanned"
}
return discoveredHosts; // The list of scanned hosts should now be the set of all hosts in the game!
}
/** @param {NS} ns
* Get a dictionary of active source files, taking into account the current active bitnode as well. **/
export async function getActiveSourceFiles(ns, includeLevelsFromCurrentBitnode = true) {
return await getActiveSourceFiles_Custom(ns, getNsDataThroughFile, includeLevelsFromCurrentBitnode);
}
/** @param {NS} ns
* getActiveSourceFiles Helper that allows the user to pass in their chosen implementation of getNsDataThroughFile to minimize RAM usage **/
export async function getActiveSourceFiles_Custom(ns, fnGetNsDataThroughFile, includeLevelsFromCurrentBitnode = true) {
checkNsInstance(ns, '"getActiveSourceFiles"');
let tempFile = '/Temp/owned-source-files.txt';
// Find out what source files the user has unlocked
let dictSourceFiles;
try { await fnGetNsDataThroughFile(ns, `Object.fromEntries(ns.getOwnedSourceFiles().map(sf => [sf.n, sf.lvl]))`, tempFile); } catch { }
if (!dictSourceFiles) { // Bit of a hack, but if RAM is so low that this fails, we can fallback to using an older version of this file, and even assuming we have no source files.
dictSourceFiles = ns.read(tempFile)
dictSourceFiles = dictSourceFiles ? JSON.parse(dictSourceFiles) : {};
}
// If the user is currently in a given bitnode, they will have its features unlocked
if (includeLevelsFromCurrentBitnode) {
const bitNodeN = (await fnGetNsDataThroughFile(ns, 'ns.getPlayer()', '/Temp/player-info.txt')).bitNodeN;
dictSourceFiles[bitNodeN] = Math.max(3, dictSourceFiles[bitNodeN] || 0);
}
return dictSourceFiles;
}
/** @param {NS} ns
* Return bitnode multiplers, or null if they cannot be accessed. **/
export async function tryGetBitNodeMultipliers(ns) {
return await tryGetBitNodeMultipliers_Custom(ns, getNsDataThroughFile);
}
/** @param {NS} ns
* tryGetBitNodeMultipliers Helper that allows the user to pass in their chosen implementation of getNsDataThroughFile to minimize RAM usage **/
export async function tryGetBitNodeMultipliers_Custom(ns, fnGetNsDataThroughFile) {
checkNsInstance(ns, '"tryGetBitNodeMultipliers"');
let canGetBitNodeMultipliers = false;
try { canGetBitNodeMultipliers = 5 in (await getActiveSourceFiles_Custom(ns, fnGetNsDataThroughFile)); } catch { }
if (!canGetBitNodeMultipliers) return null;
try { return await fnGetNsDataThroughFile(ns, 'ns.getBitNodeMultipliers()', '/Temp/bitnode-multipliers.txt'); } catch { }
return null;
}
/** @param {NS} ns
* Returns a helpful error message if we forgot to pass the ns instance to a function */
export function checkNsInstance(ns, fnName = "this function") { if (!ns.print) throw `The first argument to ${fnName} should be a 'ns' instance.`; return ns; }