forked from alainbryden/bitburner-scripts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
daemon.js
1937 lines (1822 loc) · 139 KB
/
daemon.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
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import {
formatMoney, formatRam, formatDuration, formatDateTime, formatNumber,
hashCode, disableLogs, log, getFilePath, getConfiguration,
getNsDataThroughFile_Custom, runCommand_Custom, waitForProcessToComplete_Custom,
tryGetBitNodeMultipliers_Custom, getActiveSourceFiles_Custom,
getFnRunViaNsExec, autoRetry
} from './helpers.js'
// daemon.js has histocially been the central orchestrator of almost every script in the game.
// Only recently has it been "enslaved" to an even higher-level orchestrator: autopilot.js
// Its primary job is to manage hacking servers for income, but it also manages launching
// a myriad of helper scripts to take advantage of other game mechanics (such as solving coding contraacts)
// NOTE: This is the the oldest piece of code in the repo and is a mess of global properties and
// functions scattered all over the place. I'll try to clean it up and organize it better over time
// but my appologies if you are trying to read it. Other scripts should serve as better examples.
// These parameters are meant to let you tweak the script's behaviour from the command line (without altering source code)
let options;
const argsSchema = [
['h', false], // Do nothing but hack, no prepping (drains servers to 0 money, if you want to do that for some reason)
['hack-only', false], // Same as above
['s', true], // Enable Stock Manipulation. This is now true for default, but left as a valid argument for backwards-compatibility.
['stock-manipulation', true], // Same as above
['disable-stock-manipulation', false], // You must now opt *out* of stock-manipulation mode by enabling this flag.
['stock-manipulation-focus', false], // Stocks are main source of income - kill any scripts that would do them harm (TODO: Enable automatically in BN8)
['v', false], // Detailed logs about batch scheduling / tuning
['verbose', false], // Same as above
['o', false], // Good for debugging, run the main targettomg loop once then stop, with some extra logs
['run-once', false], // Same as above
['x', false], // Focus on a strategy that produces the most hack EXP rather than money
['xp-only', false], // Same as above
['n', false], // Can toggle on using hacknet nodes for extra hacking ram (at the expense of hash production)
['use-hacknet-nodes', false], // Same as above (kept for backwards compatibility, but these are now called hacknet-servers)
['use-hacknet-servers', false], // Same as above, but the game recently renamed these
['spend-hashes-for-money-when-under', 10E6], // (Default 10m) Convert 4 hashes to money whenever we're below this amount
['disable-spend-hashes', false], // An easy way to set the above to a very large negative number, thus never spending hashes for Money
['silent-misfires', false], // Instruct remote scripts not to alert when they misfire
['initial-max-targets', 2], // Initial number of servers to target / prep (TODO: Scale this as BN progression increases)
['max-steal-percentage', 0.75], // Don't steal more than this in case something goes wrong with timing or scheduling, it's hard to recover from
['cycle-timing-delay', 16000], // Time
['queue-delay', 1000], // Delay before the first script begins, to give time for all scripts to be scheduled
['max-batches', 40], // Maximum overlapping cycles to schedule in advance. Note that once scheduled, we must wait for all batches to complete before we can schedule more
['i', false], // Farm intelligence with manual hack.
['reserved-ram', 32], // Keep this much home RAM free when scheduling hack/grow/weaken cycles on home.
['looping-mode', false], // Set to true to attempt to schedule perpetually-looping tasks.
['recovery-thread-padding', 1], // Multiply the number of grow/weaken threads needed by this amount to automatically recover more quickly from misfires.
['share', false], // Enable sharing free ram to increase faction rep gain (enabled automatically once RAM is sufficient)
['no-share', false], // Disable sharing free ram to increase faction rep gain
['share-cooldown', 5000], // Wait before attempting to schedule more share threads (e.g. to free RAM to be freed for hack batch scheduling first)
['share-max-utilization', 0.8], // Set to 1 if you don't care to leave any RAM free after sharing. Will use up to this much of the available RAM
['no-tail-windows', false], // Set to true to prevent the default behaviour of opening a tail window for certain launched scripts. (Doesn't affect scripts that open their own tail windows)
['initial-study-time', 10], // Seconds. Set to 0 to not do any studying at startup. By default, if early in an augmentation, will start with a little study to boost hack XP
['initial-hack-xp-time', 10], // Seconds. Set to 0 to not do any hack-xp grinding at startup. By default, if early in an augmentation, will start with a little study to boost hack XP
['disable-script', []], // The names of scripts that you do not want run by our scheduler
['run-script', []], // The names of additional scripts that you want daemon to run on home
];
export function autocomplete(data, args) {
data.flags(argsSchema);
const lastFlag = args.length > 1 ? args[args.length - 2] : null;
if (lastFlag == "--disable-script" || lastFlag == "--run-script")
return data.scripts;
return [];
}
// --- CONSTANTS ---
// track how costly (in security) a growth/hacking thread is.
const growthThreadHardening = 0.004;
const hackThreadHardening = 0.002;
// initial potency of weaken threads before multipliers
const weakenThreadPotency = 0.05;
// unadjusted server growth rate, this is way more than what you actually get
const unadjustedGrowthRate = 1.03;
// max server growth rate, growth rates higher than this are throttled.
const maxGrowthRate = 1.0035;
// The name given to purchased servers (should match what's in host-manager.js)
const purchasedServersName = "daemon";
// The maximum current total RAM utilization before we stop attempting to schedule work for the next less profitable server. Can be used to reserve capacity.
const maxUtilization = 0.95;
const lowUtilizationThreshold = 0.80; // The counterpart - low utilization, which leads us to ramp up targets
// If we have plenty of resources after targeting all possible servers, we can start to grow/weaken servers above our hack level - up to this utilization
const maxUtilizationPreppingAboveHackLevel = 0.75;
// Maximum number of milliseconds the main targeting loop should run before we take a break until the next loop
const maxLoopTime = 1000; //ms
// --- VARS ---
// DISCLAIMER: Take any values you see assigned here with a grain of salt. Due to oddities in how Bitburner runs scripts,
// global state can be shared between multiple instances of the same script. As such, many of these values must
// be reset in the main method of this script (and if they aren't it's likely to manifest as a bug.)
let loopInterval = 1000; //ms
// the number of milliseconds to delay the grow execution after theft to ensure it doesn't trigger too early and have no effect.
// For timing reasons the delay between each step should be *close* 1/4th of this number, but there is some imprecision
let cycleTimingDelay = 0; // (Set in command line args)
let queueDelay = 0; // (Set in command line args) The delay that it can take for a script to start, used to pessimistically schedule things in advance
let maxBatches = 0; // (Set in command line args) The max number of batches this daemon will spool up to avoid running out of IRL ram (TODO: Stop wasting RAM by scheduling batches so far in advance. e.g. Grind XP while waiting for cycle start!)
let maxTargets = 0; // (Set in command line args) Initial value, will grow if there is an abundance of RAM
let maxPreppingAtMaxTargets = 3; // The max servers we can prep when we're at our current max targets and have spare RAM
// Allows some home ram to be reserved for ad-hoc terminal script running and when home is explicitly set as the "preferred server" for starting a helper
let homeReservedRam = 0; // (Set in command line args)
let allHostNames = (/**@returns {string[]}*/() => [])(); // simple name array of servers that have been discovered
let _allServers = (/**@returns{Server[]}*/() => [])(); // Array of Server objects - our internal model of servers for hacking
// Lists of tools (external scripts) run
let hackTools, asynchronousHelpers, periodicScripts;
// toolkit var for remembering the names and costs of the scripts we use the most
let toolsByShortName = (/**@returns{{[id: string]: Tool;}}*/() => undefined)(); // Dictionary of tools keyed by tool short name
let allHelpersRunning = false; // Tracks whether all long-lived helper scripts have been launched
let studying = false; // Whether we're currently studying
// Command line Flags
let hackOnly = false; // "-h" command line arg - don't grow or shrink, just hack (a.k.a. scrapping mode)
let stockMode = false; // "-s" command line arg - hack/grow servers in a way that boosts our current stock positions
let stockFocus = false; // If true, stocks are main source of income - kill any scripts that would do them harm
let xpOnly = false; // "-x" command line arg - focus on a strategy that produces the most hack EXP rather than money
let verbose = false; // "-v" command line arg - Detailed logs about batch scheduling / tuning
let runOnce = false; // "-o" command line arg - Good for debugging, run the main targettomg loop once then stop
let useHacknetNodes = false; // "-n" command line arg - Can toggle using hacknet nodes for extra hacking ram
let loopingMode = false;
let recoveryThreadPadding = 1; // How many multiples to increase the weaken/grow threads to recovery from misfires automatically (useful when RAM is abundant and timings are tight)
let daemonHost = null; // the name of the host of this daemon, so we don't have to call the function more than once.
let hasFormulas = true;
let currentTerminalServer = ""; // Periodically updated when intelligence farming, the current connected terminal server.
let dictSourceFiles = (/**@returns{{[bitnode: number]: number;}}*/() => undefined)(); // Available source files
let bitnodeMults = null; // bitnode multipliers that can be automatically determined after SF-5
let isInBn8 = false; // Flag indicating whether we are in BN8 (where lots of rules change)
let haveTixApi = false, have4sApi = false; // Whether we have WSE API accesses
let _cachedPlayerInfo = (/**@returns{Player}*/() => undefined)(); // stores multipliers for player abilities and other player info
let _ns = (/**@returns{NS}*/() => undefined)(); // Globally available ns reference, for convenience
// Property to avoid log churn if our status hasn't changed since the last loop
let lastUpdate = "";
let lastUpdateTime = Date.now();
let lowUtilizationIterations = 0;
let highUtilizationIterations = 0;
let lastShareTime = 0; // Tracks when share was last invoked so we can respect the configured share-cooldown
let allTargetsPrepped = false;
/** Ram-dodge getting updated player info. Note that this is the only async routine called in the main loop.
* If latency or ram instability is an issue, you may wish to try uncommenting the direct request.
* @param {NS} ns
* @returns {Promise<Player>} */
async function getPlayerInfo(ns) {
// return _cachedPlayerInfo = ns.getPlayer();
return _cachedPlayerInfo = await getNsDataThroughFile(ns, `ns.getPlayer()`);
}
function playerHackSkill() { return _cachedPlayerInfo.skills.hacking; }
function getPlayerHackingGrowMulti() { return _cachedPlayerInfo.mults.hacking_grow; };
/** @param {NS} ns
* @returns {Promise<{ type: "COMPANY"|"FACTION"|"CLASS"|"CRIME", cyclesWorked: number, crimeType: string, classType: string, location: string, companyName: string, factionName: string, factionWorkType: string }>} */
async function getCurrentWorkInfo(ns) {
return (await getNsDataThroughFile(ns, 'ns.singularity.getCurrentWork()')) ?? {};
}
/** Helper to check if a file exists.
* A helper is used so that we have the option of exploring alternative implementations that cost less/no RAM.
* @param {NS} ns */
function doesFileExist(ns, filename, hostname = undefined) {
// Fast (and free) - for local files, try to read the file and ensure it's not empty
if ((hostname === undefined || hostname === daemonHost) && !filename.endsWith('.exe'))
return ns.read(filename) != '';
return ns.fileExists(filename, hostname);
}
/** Helper to check which of a set of files exist on a remote server in a single batch ram-dodging request
* @param {NS} ns
* @param {string[]} filenames
* @returns {Promise<boolean[]>} */
async function filesExist(ns, filenames, hostname = undefined) {
return await getNsDataThroughFile(ns, `ns.args.slice(1).map(f => ns.fileExists(f, ns.args[0]))`,
'/Temp/files-exist.txt', [hostname ?? daemonHost, ...filenames])
}
let psCache = (/**@returns{{[serverName: string]: ProcessInfo[];}}*/() => [])();
/** PS can get expensive, and we use it a lot so we cache this for the duration of a loop
* @param {NS} ns
* @returns {ProcessInfo[]} */
function processList(ns, serverName, canUseCache = true) {
const cachedResult = psCache[serverName];
const processList = canUseCache && cachedResult !== undefined ? cachedResult :
// Note: We experimented with ram-dodging `ps`, but there's so much data involed that serializing/deserializing generates a lot of latency
//(psCache[serverName] = await getNsDataThroughFile(ns, 'ns.ps(ns.args[0])', null, [serverName]));
(psCache[serverName] = ns.ps(serverName));
return processList;
}
// Returns the amount of money we should currently be reserving. Dynamically adapts to save money for a couple of big purchases on the horizon
function reservedMoney(ns) {
let shouldReserve = Number(ns.read("reserve.txt") || 0);
let playerMoney = ns.getServerMoneyAvailable("home");
if (!ownedCracks.includes("SQLInject.exe") && playerMoney > 200e6)
shouldReserve += 250e6; // Start saving at 200m of the 250m required for SQLInject
const fourSigmaCost = (bitnodeMults.FourSigmaMarketDataApiCost * 25000000000);
if (!have4sApi && playerMoney >= fourSigmaCost / 2)
shouldReserve += fourSigmaCost; // Start saving if we're half-way to buying 4S market access
return shouldReserve;
}
// script entry point
/** @param {NS} ns **/
export async function main(ns) {
daemonHost = "home"; // ns.getHostname(); // get the name of this node (realistically, will always be home)
const runOptions = getConfiguration(ns, argsSchema);
if (!runOptions) return;
// Ensure no other copies of this script are running (they share memory)
const scriptName = ns.getScriptName();
const competingDaemons = processList(ns, "home").filter(s => s.filename == scriptName && s.pid != ns.pid);
if (competingDaemons.length > 0) { // We expect only 1, due to this logic, but just in case, generalize the code below to support multiple.
const daemonPids = competingDaemons.map(p => p.pid);
log(ns, `Info: Restarting another '${scriptName}' instance running on home (pid: ${daemonPids} args: ` +
`[${competingDaemons[0].args.join(", ")}]) with new args ([${ns.args.join(", ")}])...`, true)
const killPid = await killProcessIds(ns, daemonPids);
await waitForProcessToComplete_Custom(ns, getHomeProcIsAlive(ns), killPid);
await ns.sleep(loopInterval); // The game can be slow to kill scripts, give it an extra bit of time.
}
_ns = ns;
disableLogs(ns, ['getServerMaxRam', 'getServerUsedRam', 'getServerMoneyAvailable', 'getServerGrowth', 'getServerSecurityLevel', 'exec', 'scan', 'sleep']);
// Reset global vars on startup since they persist in memory in certain situations (such as on Augmentation)
lastUpdate = "";
lastUpdateTime = Date.now();
maxTargets = 2;
lowUtilizationIterations = highUtilizationIterations = 0;
allHostNames = [], _allServers = [], psCache = [];
// Get information about the player's current stats (also populates a cache)
const playerInfo = await getPlayerInfo(ns);
// Try to get "resetInfo", with a fallback for a failed dynamic call (i.e. low-ram conditions)
let resetInfo;
try {
resetInfo = await getNsDataThroughFile(ns, `ns.getResetInfo()`);
} catch {
resetInfo = { currentNode: 1, lastAugReset: Date.now() };
}
isInBn8 = resetInfo.currentNode === 8; // We do some things differently if we're in BN8 (Stock Market BN)
dictSourceFiles = await getActiveSourceFiles_Custom(ns, getNsDataThroughFile);
log(ns, "The following source files are active: " + JSON.stringify(dictSourceFiles));
// Process configuration
options = runOptions;
hackOnly = options.h || options['hack-only'];
xpOnly = options.x || options['xp-only'];
stockMode = (options.s || options['stock-manipulation'] || options['stock-manipulation-focus']) && !options['disable-stock-manipulation'];
stockFocus = options['stock-manipulation-focus'] && !options['disable-stock-manipulation'];
useHacknetNodes = options.n || options['use-hacknet-nodes'] || options['use-hacknet-servers'];
verbose = options.v || options['verbose'];
runOnce = options.o || options['run-once'];
loopingMode = options['looping-mode'];
recoveryThreadPadding = options['recovery-thread-padding'];
// Log which flaggs are active
if (hackOnly) log(ns, '-h - Hack-Only mode activated!');
if (xpOnly) log(ns, '-x - Hack XP Grinding mode activated!');
if (useHacknetNodes) log(ns, '-n - Using hacknet nodes to run scripts!');
if (verbose) log(ns, '-v - Verbose logging activated!');
if (runOnce) log(ns, '-o - Run-once mode activated!');
if (stockMode) log(ns, 'Stock market manipulation mode is active (now enabled by default)');
if (!stockMode) log(ns, "--disable-stock-manipulation - Stock manipulation has been disabled.");
if (stockFocus) log(ns, '--stock-manipulation-focus - Stock market manipulation is the main priority');
if (loopingMode) {
log(ns, '--looping-mode - scheduled remote tasks will loop themselves');
cycleTimingDelay = 0;
queueDelay = 0;
if (recoveryThreadPadding == 1) recoveryThreadPadding = 10;
if (stockMode) stockFocus = true; // Need to actively kill scripts that go against stock because they will live forever
}
cycleTimingDelay = options['cycle-timing-delay'];
queueDelay = options['queue-delay'];
maxBatches = options['max-batches'];
homeReservedRam = options['reserved-ram']
// These scripts are started once and expected to run forever (or terminate themselves when no longer needed)
const openTailWindows = !options['no-tail-windows'];
const reqRam = (ram) => ns.getServerMaxRam("home") >= ram; // To avoid wasting precious RAM, many scripts don't launch unless we have more than a certain amount
asynchronousHelpers = [
{ name: "stats.js", shouldRun: () => reqRam(64) }, // Adds stats not usually in the HUD
{ name: "stockmaster.js", shouldRun: () => reqRam(64), args: openTailWindows ? ["--show-market-summary"] : [], tail: openTailWindows }, // Start our stockmaster
{ name: "hacknet-upgrade-manager.js", shouldRun: () => reqRam(64), args: ["-c", "--max-payoff-time", "1h"] }, // Kickstart hash income by buying everything with up to 1h payoff time immediately
{ name: "spend-hacknet-hashes.js", args: [], shouldRun: () => reqRam(64) && 9 in dictSourceFiles }, // Always have this running to make sure hashes aren't wasted
{ name: "sleeve.js", tail: openTailWindows, shouldRun: () => 10 in dictSourceFiles }, // Script to create manage our sleeves for us
{ name: "gangs.js", tail: openTailWindows, shouldRun: () => reqRam(64) && 2 in dictSourceFiles }, // Script to create manage our gang for us
{
name: "work-for-factions.js", args: ['--fast-crimes-only', '--no-coding-contracts'], // Singularity script to manage how we use our "focus" work.
shouldRun: () => 4 in dictSourceFiles && reqRam(256 / (2 ** dictSourceFiles[4]) && !studying) // Higher SF4 levels result in lower RAM requirements
},
{ // Script to manage bladeburner for us. Run automatically if not disabled and bladeburner API is available
name: "bladeburner.js", tail: openTailWindows,
shouldRun: () => !options['disable-script'].includes('bladeburner.js') && 7 in dictSourceFiles && !isInBn8
},
];
asynchronousHelpers.forEach(helper => helper.name = getFilePath(helper.name));
// Add any additional scripts to be run provided by --run-script arguments
options['run-script'].forEach(s => asynchronousHelpers.push({ name: s }));
asynchronousHelpers.forEach(helper => helper.isLaunched = false);
asynchronousHelpers.forEach(helper => helper.requiredServer = "home"); // All helpers should be launched at home since they use tempory scripts, and we only reserve ram on home
// These scripts are spawned periodically (at some interval) to do their checks, with an optional condition that limits when they should be spawned
let shouldUpgradeHacknet = async () => ((await whichServerIsRunning(ns, "hacknet-upgrade-manager.js", false)) === null) && reservedMoney(ns) < ns.getServerMoneyAvailable("home");
// In BN8 (stocks-only bn) and others with hack income disabled, don't waste money on improving hacking infrastructure unless we have plenty of money to spare
let shouldImproveHacking = () => bitnodeMults.ScriptHackMoneyGain != 0 && !isInBn8 || ns.getServerMoneyAvailable("home") > 1e12;
// Note: Periodic script are generally run every 30 seconds, but intervals are spaced out to ensure they aren't all bursting into temporary RAM at the same time.
periodicScripts = [
// Buy tor as soon as we can if we haven't already, and all the port crackers (exception: don't buy 2 most expensive port crackers until later if in a no-hack BN)
{ interval: 25000, name: "/Tasks/tor-manager.js", shouldRun: () => 4 in dictSourceFiles && !allHostNames.includes("darkweb") },
{ interval: 26000, name: "/Tasks/program-manager.js", shouldRun: () => 4 in dictSourceFiles && ownedCracks.length != 5 },
{ interval: 27000, name: "/Tasks/contractor.js", requiredServer: "home" }, // Periodically look for coding contracts that need solving
// Buy every hacknet upgrade with up to 4h payoff if it is less than 10% of our current money or 8h if it is less than 1% of our current money.
{ interval: 28000, name: "hacknet-upgrade-manager.js", shouldRun: shouldUpgradeHacknet, args: () => ["-c", "--max-payoff-time", "4h", "--max-spend", ns.getServerMoneyAvailable("home") * 0.1] },
{ interval: 28500, name: "hacknet-upgrade-manager.js", shouldRun: shouldUpgradeHacknet, args: () => ["-c", "--max-payoff-time", "8h", "--max-spend", ns.getServerMoneyAvailable("home") * 0.01] },
// Buy upgrades regardless of payoff if they cost less than 0.1% of our money
{ interval: 29000, name: "hacknet-upgrade-manager.js", shouldRun: shouldUpgradeHacknet, args: () => ["-c", "--max-payoff-time", "1E100h", "--max-spend", ns.getServerMoneyAvailable("home") * 0.001] },
{
interval: 30000, name: "/Tasks/ram-manager.js", args: () => ['--budget', 0.5, '--reserve', reservedMoney(ns)], // Spend about 50% of un-reserved cash on home RAM upgrades (permanent) when they become available
shouldRun: () => 4 in dictSourceFiles && shouldImproveHacking() // Only trigger if hack income is important
},
{ // Periodically check for new faction invites and join if deemed useful to be in that faction. Also determines how many augs we could afford if we installed right now
interval: 31000, name: "faction-manager.js", requiredServer: "home", args: ['--verbose', 'false'],
// Don't start auto-joining factions until we're holding 1 billion (so coding contracts returning money is probably less critical) or we've joined one already
shouldRun: () => 4 in dictSourceFiles && (_cachedPlayerInfo.factions.length > 0 || ns.getServerMoneyAvailable("home") > 1e9) &&
(ns.getServerMaxRam("home") >= 128 / (2 ** dictSourceFiles[4])) // Uses singularity functions, and higher SF4 levels result in lower RAM requirements
},
{ // Periodically look to purchase new servers, but note that these are often not a great use of our money (hack income isn't everything) so we may hold-back.
interval: 32000, name: "host-manager.js", requiredServer: "home",
// Funky heuristic warning: I find that new players with fewer SF levels under their belt are obsessed with hack income from servers,
// but established players end up finding auto-purchased hosts annoying - so now the % of money we spend shrinks as SF levels grow.
args: () => ['--reserve-percent', Math.min(0.9, 0.1 * Object.values(dictSourceFiles).reduce((t, v) => t + v, 0)), '--absolute-reserve', reservedMoney(ns), '--utilization-trigger', '0'],
shouldRun: () => {
if (!shouldImproveHacking()) return false; // Skip if hack income is not important in this BN or at this time
let utilization = getTotalNetworkUtilization(); // Utilization-based heuristics for when we likely could use more RAM for hacking
return utilization >= maxUtilization || utilization > 0.80 && maxTargets < 20 || utilization > 0.50 && maxTargets < 5;
}
},
// Check if any new servers can be backdoored. If there are many, this can eat up a lot of RAM, so make this the last script scheduled at startup.
{ interval: 33000, name: "/Tasks/backdoor-all-servers.js", requiredServer: "home", shouldRun: () => 4 in dictSourceFiles },
];
periodicScripts.forEach(tool => tool.name = getFilePath(tool.name));
hackTools = [
{ name: "/Remote/weak-target.js", shortName: "weak", threadSpreadingAllowed: true },
{ name: "/Remote/grow-target.js", shortName: "grow" },
{ name: "/Remote/hack-target.js", shortName: "hack" },
{ name: "/Remote/manualhack-target.js", shortName: "manualhack" },
{ name: "/Remote/share.js", shortName: "share", threadSpreadingAllowed: true },
];
hackTools.forEach(tool => tool.name = getFilePath(tool.name));
await buildToolkit(ns, [...asynchronousHelpers, ...periodicScripts, ...hackTools]); // build toolkit
const allServers = await getNsDataThroughFile(ns, 'scanAllServers(ns)');
await getStaticServerData(ns, allServers); // Gather information about servers that will never change
await buildServerList(ns, false, allServers); // create the exhaustive server list
await establishMultipliers(ns); // figure out the various bitnode and player multipliers
maxTargets = options['initial-max-targets'];
if (stockFocus) // Ensure we attempt to target at least all servers that represent stocks if in stock-focus mode
maxTargets = Math.max(maxTargets, Object.keys(serverStockSymbols).length);
// If we ascended less than 10 minutes ago, start with some study and/or XP cycles to quickly restore hack XP
const timeSinceLastAug = Date.now() - resetInfo.lastAugReset;
const shouldKickstartHackXp = (playerHackSkill() < 500 && timeSinceLastAug < 600000);
studying = shouldKickstartHackXp ? true : false; // Flag will be used to prevent focus-stealing scripts from running until we're done studying.
// Start helper scripts and run periodic scripts for the first time to e.g. buy tor and any hack tools available to us (we will continue studying briefly while this happens)
await runStartupScripts(ns);
await runPeriodicScripts(ns);
if (shouldKickstartHackXp) await kickstartHackXp(ns);
// Start the main targetting loop
await doTargetingLoop(ns);
}
/** @param {NS} ns
* Gain a hack XP early after a new Augmentation by studying a bit, then doing a bit of XP grinding */
async function kickstartHackXp(ns) {
let startedStudying = false;
try {
if (4 in dictSourceFiles && options['initial-study-time'] > 0) {
// The safe/cheap thing to do is to study for free at the local university in our current town
// The most effective thing is to study Algorithms at ZB university in Aevum.
// Depending on our money, try to do the latter.
try {
const studyTime = options['initial-study-time'];
log(ns, `INFO: Studying for ${studyTime} seconds to kickstart hack XP and speed up initial cycle times. (set --initial-study-time 0 to disable this step.)`);
const money = ns.getServerMoneyAvailable("home")
const { CityName, LocationName, UniversityClassType } = ns.enums
if (money >= 200000) { // If we can afford to travel, we're probably far enough along that it's worthwhile going to Volhaven where ZB university is.
log(ns, `INFO: Travelling to Volhaven for best study XP gain rate.`);
await getNsDataThroughFile(ns, `ns.singularity.travelToCity(ns.args[0])`, null, [CityName.Volhaven]);
}
const playerInfo = await getPlayerInfo(ns); // Update player stats to be certain of our new location.
const university = playerInfo.city == CityName.Sector12 ? LocationName.Sector12RothmanUniversity :
playerInfo.city == CityName.Aevum ? LocationName.AevumSummitUniversity :
playerInfo.city == CityName.Volhaven ? LocationName.VolhavenZBInstituteOfTechnology : null;
if (!university)
log(ns, `WARN: Cannot study, because you are in city ${playerInfo.city} which has no known university, and you cannot afford to travel to another city.`, false, 'warning');
else {
const course = playerInfo.city == CityName.Sector12 ? UniversityClassType.computerScience : UniversityClassType.algorithms; // Assume if we are still in Sector-12 we are poor and should only take the free course
log(ns, `INFO: Studying "${course}" at "${university}" because we are in city "${playerInfo.city}".`);
startedStudying = await getNsDataThroughFile(ns, `ns.singularity.universityCourse(ns.args[0], ns.args[1], ns.args[2])`, null, [university, course, false]);
if (startedStudying)
await ns.sleep(studyTime * 1000); // Wait for studies to affect Hack XP. This will often greatly reduce time-to-hack/grow/weaken, and avoid a slow first cycle
else
log(ns, `WARNING: Failed to study to kickstart hack XP: ns.singularity.universityCourse("${university}", "${course}", false) returned "false".`, false, 'warning');
}
} catch (err) { log(ns, `WARNING: Caught error while trying to study to kickstart hack XP: ${typeof err === 'string' ? err : err.message || JSON.stringify(err)}`, false, 'warning'); }
}
// Immediately attempt to root initially-accessible targets before attempting any XP cycles
for (const server of getAllServers().filter(s => !s.hasRoot() && s.canCrack()))
await doRoot(ns, server);
// Before starting normal hacking, fire a couple hack XP-focused cycle using a chunk of free RAM to further boost RAM
if (!xpOnly) {
let maxXpCycles = 10000; // Avoid an infinite loop if something goes wrong
const maxXpTime = options['initial-hack-xp-time'];
const start = Date.now();
const xpTarget = getBestXPFarmTarget();
const minCycleTime = xpTarget.timeToWeaken();
if (minCycleTime > maxXpTime * 1000)
return log(ns, `INFO: Skipping XP cycle because the best target (${xpTarget.name}) time to weaken (${formatDuration(minCycleTime)})` +
` is greater than the configured --initial-hack-xp-time of ${maxXpTime} seconds.`);
log(ns, `INFO: Running Hack XP-focused cycles for ${maxXpTime} seconds to further boost hack XP and speed up main hack cycle times. (set --initial-hack-xp-time 0 to disable this step.)`);
while (maxXpCycles-- > 0 && Date.now() - start < maxXpTime * 1000) {
let cycleTime = await farmHackXp(ns, 1, verbose, 1);
if (cycleTime)
await ns.sleep(cycleTime);
else
return log(ns, 'WARNING: Failed to schedule an XP cycle', false, 'warning');
log(ns, `INFO: Hacked ${xpTarget.name} for ${cycleTime.toFixed(1)}ms, (${Date.now() - start}ms total) of ${maxXpTime * 1000}ms`);
}
}
} catch {
log(ns, 'WARNING: Encountered an error while trying to kickstart hack XP (low RAM issues perhaps?)', false, 'warning');
} finally {
// Ensure we stop studying (in case no other running scripts end up stealing focus, so we don't keep studying forever)
if (startedStudying) await getNsDataThroughFile(ns, `ns.singularity.stopAction()`);
studying = false; // This will allow work-for-faction to launch
}
}
/** Check running status of scripts on servers
* @param {NS} ns
* @returns {Promise<string>} */
async function whichServerIsRunning(ns, scriptName, canUseCache = true) {
for (const server of getAllServers())
if (processList(ns, server.name, canUseCache).some(process => process.filename === scriptName))
return server.name;
return null;
}
/** Helper to kick off external scripts
* @param {NS} ns
* @returns {Promise<boolean>} true if all scripts have been launched */
async function runStartupScripts(ns) {
let launched = 0;
for (const helper of asynchronousHelpers) {
if (!helper.isLaunched && (helper.shouldRun === undefined || (await helper.shouldRun()))) {
if (launched > 0) await ns.sleep(200); // Sleep a short while between each script being launched, so they aren't all fighting for temp RAM at the same time.
helper.isLaunched = await tryRunTool(ns, getTool(helper))
if (helper.isLaunched) launched++;
}
}
// if every helper is launched already return "true" so we can skip doing this each cycle going forward.
return asynchronousHelpers.reduce((allLaunched, tool) => allLaunched && tool.isLaunched, true);
}
/** Checks whether it's time for any scheduled tasks to run
* @param {NS} ns */
async function runPeriodicScripts(ns) {
let launched = 0;
for (const task of periodicScripts) {
let tool = getTool(task);
if ((Date.now() - (task.lastRun || 0) >= task.interval) && (task.shouldRun === undefined || (await task.shouldRun()))) {
task.lastRun = Date.now()
if (launched > 0) await ns.sleep(11); // Sleep a short while between each script being launched, so they aren't all fighting for temp RAM at the same time.
if (await tryRunTool(ns, tool))
launched++;
}
}
// Super-early aug, if we are poor, spend hashes as soon as we get them for a quick cash injection. (Only applies if we have hacknet servers)
if (9 in dictSourceFiles && !options['disable-spend-hashes'] // See if we have a hacknet, and spending hashes for money isn't disabled
&& ns.getServerMoneyAvailable("home") < options['spend-hashes-for-money-when-under'] // Only if money is below the configured threshold
&& (ns.getServerMaxRam("home") - ns.getServerUsedRam("home")) >= 5.6) { // Ensure we have spare RAM to run this temp script
await runCommand(ns, `0; if(ns.hacknet.spendHashes("Sell for Money")) ns.toast('Sold 4 hashes for \$1M', 'success')`, '/Temp/sell-hashes-for-money.js');
}
}
// Helper that gets the either invokes a function that returns a value, or returns the value as-is if it is not a function.
const funcResultOrValue = fnOrVal => (fnOrVal instanceof Function ? fnOrVal() : fnOrVal);
/** Returns true if the tool is running (including if it was already running), false if it could not be run.
* @param {NS} ns
* @param {Tool} tool */
async function tryRunTool(ns, tool) {
if (options['disable-script'].includes(tool.name)) {
if (verbose) log(ns, `Tool ${tool.name} was not launched as it was specified with --disable-script`);
return false;
}
if (!doesFileExist(ns, tool.name)) {
log(ns, `ERROR: Tool ${tool.name} was not found on ${daemonHost}`, true, 'error');
return false;
}
let runningOnServer = await whichServerIsRunning(ns, tool.name);
if (runningOnServer != null) {
if (verbose) log(ns, `INFO: Tool ${tool.name} is already running on server ${runningOnServer}.`);
return true;
}
const args = funcResultOrValue(tool.args) || []; // Support either a static args array, or a function returning the args.
const runResult = await arbitraryExecution(ns, tool, 1, args, tool.requiredServer || "home");
if (runResult) {
runningOnServer = await whichServerIsRunning(ns, tool.name, false);
if (verbose) log(ns, `Ran tool: ${tool.name} ` + (args.length > 0 ? `with args ${JSON.stringify(args)} ` : '') + (runningOnServer ? `on server ${runningOnServer}.` : 'but it shut down right away.'));
if (tool.tail === true && runningOnServer) {
log(ns, `Tailing Tool: ${tool.name} on server ${runningOnServer}` + (args.length > 0 ? ` with args ${JSON.stringify(args)}` : ''));
ns.tail(tool.name, runningOnServer, ...args);
//tool.tail = false; // Avoid popping open additional tail windows in the future
}
return true;
} else
log(ns, `WARNING: Tool cannot be run (insufficient RAM? REQ: ${formatRam(tool.cost)} FREE: ${formatRam(ns.getServerMaxRam("home") - ns.getServerUsedRam("home"))}): ${tool.name}`, false, 'warning');
return false;
}
/** Workaround a current bitburner bug by yeilding briefly to the game after executing something.
* @param {NS} ns
* @param {String} script - Filename of script to execute.
* @param {int} host - Hostname of the target server on which to execute the script.
* @param {int} numThreads - Optional thread count for new script. Set to 1 by default. Will be rounded to nearest integer.
* @param args - Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the third argument numThreads must be filled in with a value.
* @returns — Returns the PID of a successfully started script, and 0 otherwise.
* Workaround a current bitburner bug by yeilding briefly to the game after executing something. **/
async function exec(ns, script, host, numThreads, ...args) {
// Try to run the script with auto-retry if it fails to start
// It doesn't make sense to auto-retry hack tools, only add error handling to other scripts
if (hackTools.some(h => h.name === script))
return ns.exec(script, host, numThreads, ...args);
// Otherwise, run with auto-retry to handle e.g. temporary ram issues
const pid = await autoRetry(ns, async () => {
const p = ns.exec(script, host, numThreads, ...args)
return p;
}, p => p !== 0, () => new Error(`Failed to exec ${script} on ${host} with ${numThreads} threads. ` +
`This is likely due to having insufficient RAM. Args were: [${args}]`),
undefined, undefined, undefined, verbose, verbose);
return pid; // Caller is responsible for handling errors if final pid returned is 0 (indicating failure)
}
/** @param {NS} ns
* @param {Server} server
* Execute an external script that roots a server, and wait for it to complete. **/
async function doRoot(ns, server) {
if (verbose) log(ns, `Rooting Server ${server.name}`);
const pid = await exec(ns, getFilePath('/Tasks/crack-host.js'), daemonHost, 1, server.name);
await waitForProcessToComplete_Custom(ns, getHomeProcIsAlive(ns), pid);
server.resetCaches(); // If rooted status was cached, we must now reset it
}
// Main targeting loop
/** @param {NS} ns **/
async function doTargetingLoop(ns) {
log(ns, "doTargetingLoop");
let loops = -1;
//var isHelperListLaunched = false; // Uncomment this and related code to keep trying to start helpers
do {
loops++;
if (loops > 0) await ns.sleep(loopInterval);
try {
let start = Date.now();
psCache = []; // Clear the cache of the process list we update once per loop
await buildServerList(ns, true); // Check if any new servers have been purchased by the external host_manager process
await updatePortCrackers(ns); // Check if any new port crackers have been purchased
await getPlayerInfo(ns); // Force an update of _cachedPlayerInfo
// Run some auxilliary processes that ease the ram burden of this daemon and add additional functionality (like managing hacknet or buying servers)
await runPeriodicScripts(ns);
if (stockMode) await updateStockPositions(ns); // In stock market manipulation mode, get our current position in all stocks
const targetingOrder = await getAllServersByTargetOrder();
if (loops % 60 == 0) { // For more expensive updates, only do these every so often
// If we have not yet launched all helpers (e.g. awaiting more home ram, or TIX API to be purchased) see if any are now ready to be run
if (!allHelpersRunning) allHelpersRunning = await runStartupScripts(ns);
// Pull additional data about servers that infrequently changes
await refreshDynamicServerData(ns, allHostNames);
// Occassionally print our current targetting order (todo, make this controllable with a flag or custom UI?)
if (verbose && loops % 600 == 0) {
const targetsLog = 'Targetting Order:\n ' + targetingOrder.filter(s => s.shouldHack()).map(s =>
`${s.isPrepped() ? '*' : ' '} ${s.canHack() ? '✓' : 'X'} Money: ${formatMoney(s.getMoney(), 4)} of ${formatMoney(s.getMaxMoney(), 4)} ` +
`(${formatMoney(s.getMoneyPerRamSecond(), 4)}/ram.sec), Sec: ${formatNumber(s.getSecurity(), 3)} of ${formatNumber(s.getMinSecurity(), 3)}, ` +
`TTW: ${formatDuration(s.timeToWeaken())}, Hack: ${s.requiredHackLevel} - ${s.name}` +
(!stockMode || !serverStockSymbols[s.name] ? '' : ` Sym: ${serverStockSymbols[s.name]} Owned: ${serversWithOwnedStock.includes(s.name)} ` +
`Manip: ${shouldManipulateGrow[s.name] ? "grow" : shouldManipulateHack[s.name] ? "hack" : '(disabled)'}`))
.join('\n ');
log(ns, targetsLog);
ns.write("/Temp/targets.txt", targetsLog, "w");
}
}
// Processed servers will be split into various lists for generating a summary at the end
/**@returns{Server[]}*/const n = () => []; // Trick to initialize new arrays with a strong type
const prepping = n(), preppedButNotTargeting = n(), targeting = n(), notRooted = n(), cantHack = n(),
cantHackButPrepped = n(), cantHackButPrepping = n(), noMoney = n(), failed = n(), skipped = n();
let lowestUnhackable = 99999;
// Hack: We can get stuck and never improve if we don't try to prep at least one server to improve our future targeting options.
// So get the first un-prepped server that is within our hacking level, and move it to the front of the list.
let firstUnpreppedServerIndex = -1;
for (var i = 0; i < targetingOrder.length; i++) {
const s = targetingOrder[i];
if (s.shouldHack() && s.canHack() && !s.isPrepped() && !(await s.isTargeting())) {
firstUnpreppedServerIndex = i; // Note: Can't use array.findIndex due to await.
break;
}
}
if (firstUnpreppedServerIndex !== -1 && !stockMode)
targetingOrder.unshift(targetingOrder.splice(firstUnpreppedServerIndex, 1)[0]);
// If this gets set to true, the loop will continue (e.g. to gather information), but no more work will be scheduled
var workCapped = false;
// Function to assess whether we've hit some cap that should prevent us from scheduling any more work
let isWorkCapped = () => workCapped = workCapped || failed.length > 0 // Scheduling fails when there's insufficient RAM. We've likely encountered a "soft cap" on ram utilization e.g. due to fragmentation
|| getTotalNetworkUtilization() >= maxUtilization // "hard cap" on ram utilization, can be used to reserve ram or reduce the rate of encountering the "soft cap"
|| targeting.length >= maxTargets // variable cap on the number of simultaneous targets
|| (targeting.length + prepping.length) >= (maxTargets + maxPreppingAtMaxTargets); // Only allow a couple servers to be prepped in advance when at max-targets
// check for servers that need to be rooted
// simultaneously compare our current target to potential targets
for (var i = 0; i < targetingOrder.length; i++) {
if ((Date.now() - start) >= maxLoopTime) { // To avoid lagging the game, completely break out of the loop if we start to run over
skipped.push(...targetingOrder.slice(i));
workCapped = true;
break;
}
const server = targetingOrder[i];
server.resetCaches(); // For each new loop, reset any cached properties
// Attempt to root any servers that are not yet rooted
if (!server.hasRoot() && server.canCrack())
await doRoot(ns, server);
// Check whether we can / should attempt any actions on this server
if (!server.shouldHack()) { // Ignore servers we own (bought servers / home / no money)
noMoney.push(server);
} else if (!server.hasRoot()) { // Can't do anything to servers we have not yet cracked
notRooted.push(server);
} else if (!server.canHack()) { // Note servers above our Hack skill. We can prep them a little if we have spare RAM at the end.
cantHack.push(server);
lowestUnhackable = Math.min(lowestUnhackable, server.requiredHackLevel);
// New logic allows for unhackable servers to be prepping. Keep tabs on how many we have of each
if (server.isPrepped())
cantHackButPrepped.push(server);
else if (await server.isPrepping())
cantHackButPrepping.push(server);
} else if (await server.isTargeting()) { // Note servers already being targeted from a prior loop
targeting.push(server); // TODO: Switch to continuously queing batches in the seconds leading up instead of far in advance with large delays
} else if (await server.isPrepping()) { // Note servers already being prepped from a prior loop
prepping.push(server);
} else if (isWorkCapped() || xpOnly) { // Various conditions for which we'll postpone any additional work on servers
if (xpOnly && (((nextXpCycleEnd[server.name] || 0) > start - 10000) || (await server.isXpFarming())))
targeting.push(server); // A server counts as "targeting" if in XP mode and its due to be farmed or was in the past 10 seconds
else
skipped.push(server);
} else if (!hackOnly && true == await prepServer(ns, server)) { // Returns true if prepping, false if prepping failed, null if prepped
if (server.previouslyPrepped)
log(ns, `WARNING ${server.prepRegressions++}: Server was prepped, but now at security: ${formatNumber(server.getSecurity())} ` +
`(min ${formatNumber(server.getMinSecurity())}) money: ${formatMoney(server.getMoney(), 3)} (max ${formatMoney(server.getMaxMoney(), 3)}). ` +
`Prior cycle: ${server.previousCycle}. ETA now (Hack ${playerHackSkill()}) is ${formatDuration(server.timeToWeaken())}`, true, 'warning');
prepping.push(server); // Perform weakening and initial growth until the server is "perfected" (unless in hack-only mode)
} else if (!hackOnly && !server.isPrepped()) { // If prepServer returned false or null. Check ourselves whether it is prepped
log(ns, 'Prep failed for "' + server.name + '" (RAM Utilization: ' + (getTotalNetworkUtilization() * 100).toFixed(2) + '%)');
failed.push(server);
} else if (targeting.length >= maxTargets) { // Hard cap on number of targets, changes with utilization
server.previouslyPrepped = true;
preppedButNotTargeting.push(server);
} else { // Otherwise, server is prepped at min security & max money and ready to target
var performanceSnapshot = optimizePerformanceMetrics(ns, server); // Adjust the percentage to steal for optimal scheduling
if (server.actualPercentageToSteal() === 0) { // Not enough RAM for even one hack thread of this next-best target.
failed.push(server);
} else if (true == await performScheduling(ns, server, performanceSnapshot)) { // once conditions are optimal, fire barrage after barrage of cycles in a schedule
targeting.push(server);
} else {
log(ns, 'Targeting failed for "' + server.name + '" (RAM Utilization: ' + (getTotalNetworkUtilization() * 100).toFixed(2) + '%)');
failed.push(server);
}
}
// Hack: Quickly ramp up our max-targets without waiting for the next loop if we are far below the low-utilization threshold
if (lowUtilizationIterations >= 5 && targeting.length == maxTargets && maxTargets < allHostNames.length - noMoney.length) {
let network = getNetworkStats();
let utilizationPercent = network.totalUsedRam / network.totalMaxRam;
if (utilizationPercent < lowUtilizationThreshold / 2) maxTargets++;
}
}
// Mini-loop for servers that we can't hack yet, but might have access to soon, we can at least prep them.
if (!isWorkCapped() && cantHack.length > 0 && !hackOnly && !xpOnly) {
// Prep in order of soonest to become available to us
cantHack.sort(function (a, b) {
var diff = a.requiredHackLevel - b.requiredHackLevel;
return diff != 0.0 ? diff : b.getMoneyPerRamSecond() - a.getMoneyPerRamSecond(); // Break ties by sorting by max-money
});
// Try to prep them all unless one of our capping rules are hit
// TODO: Something was not working right here (might be working now that prep code is fixed) so we can probably start prepping more than 1 server again.
for (var j = 0; j < 1 /*cantHack.length*/; j++) {
const server = cantHack[j];
if (isWorkCapped()) break;
if (cantHackButPrepped.includes(server) || cantHackButPrepping.includes(server))
continue;
var prepResult = await prepServer(ns, server);
if (prepResult == true) {
cantHackButPrepping.push(server);
} else if (prepResult == null) {
cantHackButPrepped.push(server);
} else {
log(ns, 'Pre-Prep failed for "' + server.name + '" with ' + server.requiredHackLevel +
' hack requirement (RAM Utilization: ' + (getTotalNetworkUtilization() * 100).toFixed(2) + '%)');
failed.push(server);
break;
}
}
}
let network = getNetworkStats();
let utilizationPercent = network.totalUsedRam / network.totalMaxRam;
highUtilizationIterations = utilizationPercent >= maxUtilization ? highUtilizationIterations + 1 : 0;
lowUtilizationIterations = utilizationPercent <= lowUtilizationThreshold ? lowUtilizationIterations + 1 : 0;
// If we've been at low utilization for longer than the cycle of all our targets, we can add a target
let intervalsPerTargetCycle = targeting.length == 0 ? 120 :
Math.ceil((targeting.reduce((max, t) => Math.max(max, t.timeToWeaken()), 0) + cycleTimingDelay) / loopInterval);
//log(ns, `intervalsPerTargetCycle: ${intervalsPerTargetCycle} lowUtilizationIterations: ${lowUtilizationIterations} loopInterval: ${loopInterval}`);
if (lowUtilizationIterations > intervalsPerTargetCycle && skipped.length > 0) {
maxTargets++;
log(ns, `Increased max targets to ${maxTargets} since utilization (${formatNumber(utilizationPercent * 100, 3)}%) has been quite low for ${lowUtilizationIterations} iterations.`);
lowUtilizationIterations = 0; // Reset the counter of low-utilization iterations
} else if (highUtilizationIterations > 60) { // Decrease max-targets by 1 ram utilization is too high (prevents scheduling efficient cycles)
maxTargets -= 1;
log(ns, `Decreased max targets to ${maxTargets} since utilization has been > ${formatNumber(maxUtilization * 100, 3)}% for 60 iterations and scheduling failed.`);
highUtilizationIterations = 0; // Reset the counter of high-utilization iterations
}
maxTargets = Math.max(maxTargets, targeting.length - 1, 1); // Ensure that after a restart, maxTargets start off with no less than 1 fewer max targets
allTargetsPrepped = skipped.length == 0 && prepping.length == 0;
// If there is still unspent utilization, we can use a chunk of it it to farm XP
if (xpOnly) { // If all we want to do is gain hack XP
let time = await farmHackXp(ns, 1.00, verbose);
loopInterval = Math.min(1000, time || 1000); // Wake up earlier if we're almost done an XP cycle
} else if (!isWorkCapped() && lowUtilizationIterations > 10) {
let expectedRunTime = getBestXPFarmTarget().timeToHack();
let freeRamToUse = (expectedRunTime < loopInterval) ? // If expected runtime is fast, use as much RAM as we want, it'll all be free by our next loop.
1 - (1 - lowUtilizationThreshold) / (1 - utilizationPercent) : // Take us just up to the threshold for 'lowUtilization' so we don't cause unecessary server purchases
1 - (1 - maxUtilizationPreppingAboveHackLevel - 0.05) / (1 - utilizationPercent); // Otherwise, leave more room (e.g. for scheduling new batches.)
await farmHackXp(ns, freeRamToUse, verbose && (expectedRunTime > 10000 || lowUtilizationIterations % 10 == 0), 1);
}
// Use any unspent RAM on share if we are currently working for a faction
const maxShareUtilization = options['share-max-utilization']
if (failed.length <= 0 && utilizationPercent < maxShareUtilization && // Only share RAM if we have succeeded in all hack cycle scheduling and have RAM to space
(Date.now() - lastShareTime) > options['share-cooldown'] && // Respect the share rate-limit if configured to leave gaps for scheduling
!options['no-share'] && (options['share'] || network.totalMaxRam > 1024)) // If not explicitly enabled or disabled, auto-enable share at 1TB of network RAM
{
// Figure out if the player is currently working (no point in RAM share if we aren't currently working for a faction)
let workInfo = await getCurrentWorkInfo(ns);
if (workInfo.type == "FACTION") {
let shareTool = getTool("share");
let maxThreads = shareTool.getMaxThreads(); // This many threads would use up 100% of the (1-utilizationPercent)% RAM remaining
if (xpOnly) maxThreads -= Math.floor(getServerByName('home').ramAvailable() / shareTool.cost); // Reserve home ram entirely for XP cycles when in xpOnly mode
network = getNetworkStats(); // Update network stats since they may have changed after scheduling xp cycles above
utilizationPercent = network.totalUsedRam / network.totalMaxRam;
let shareThreads = Math.floor(maxThreads * (maxShareUtilization - utilizationPercent) / (1 - utilizationPercent)); // Ensure we don't take utilization above (1-maxShareUtilization)%
if (shareThreads > 0) {
if (verbose) log(ns, `Creating ${shareThreads.toLocaleString('en')} share threads to improve faction rep gain rates. Using ${formatRam(shareThreads * 4)} of ${formatRam(network.totalMaxRam)} ` +
`(${(400 * shareThreads / network.totalMaxRam).toFixed(1)}%) of all RAM). Final utilization will be ${(100 * (4 * shareThreads + network.totalUsedRam) / network.totalMaxRam).toFixed(1)}%`);
await arbitraryExecution(ns, getTool('share'), shareThreads, [Date.now()], null, true) // Note: Need a unique argument to facilitate multiple parallel share scripts on the same server
lastShareTime = Date.now();
}
} //else log(ns, `Not Sharing. (Not working for faction. Work is ${JSON.stringify(workInfo)})`);
} //else log(ns, `Not Sharing. workCapped: ${isWorkCapped()} utilizationPercent: ${utilizationPercent} maxShareUtilization: ${maxShareUtilization} cooldown: ${formatDuration(Date.now() - lastShareTime)} networkRam: ${network.totalMaxRam}`);
// Log some status updates
let keyUpdates = `Of ${allHostNames.length} total servers:\n > ${noMoney.length} were ignored (owned or no money)`;
if (notRooted.length > 0 || ownedCracks.length < 5)
keyUpdates += `, ${notRooted.length} are not rooted (missing ${crackNames.filter(c => !ownedCracks.includes(c)).join(', ')})`;
if (cantHack.length > 0)
keyUpdates += `\n > ${cantHack.length} cannot be hacked (${cantHackButPrepping.length} prepping, ` +
`${cantHackButPrepped.length} prepped, next unlock at Hack ${lowestUnhackable})`;
if (preppedButNotTargeting.length > 0)
keyUpdates += `\n > ${preppedButNotTargeting.length} are prepped but are not a priority target`;
if (skipped.length > 0)
keyUpdates += `\n > ${skipped.length} were skipped for now (time, RAM, or target + prepping cap reached)`;
if (failed.length > 0)
keyUpdates += `\n > ${failed.length} servers failed to be scheduled (insufficient RAM?).`;
keyUpdates += `\n > Targeting: ${targeting.length} servers, Prepping: ${prepping.length + cantHackButPrepping.length}`;
if (xpOnly)
keyUpdates += `\n > Grinding XP from ${targeting.map(s => s.name).join(", ")}`;
// To reduce log spam, only log if some key status changes, or if it's been a minute
if (keyUpdates != lastUpdate || (Date.now() - lastUpdateTime) > 60000) {
log(ns, (lastUpdate = keyUpdates) +
'\n > RAM Utilization: ' + formatRam(Math.ceil(network.totalUsedRam)) + ' of ' + formatRam(network.totalMaxRam) + ' (' + (utilizationPercent * 100).toFixed(1) + '%) ' +
`for ${lowUtilizationIterations || highUtilizationIterations} its, Max Targets: ${maxTargets}, Loop Took: ${Date.now() - start}ms`);
lastUpdateTime = Date.now();
}
//log(ns, 'Prepping: ' + prepping.map(s => s.name).join(', '))
//log(ns, 'targeting: ' + targeting.map(s => s.name).join(', '))
} catch (err) {
// Sometimes a script is shut down by throwing an object contianing internal game script info. Detect this and exit silently
if (err?.env?.stopFlag) return;
// Note netscript errors are raised as a simple string (no message property)
var errorMessage = typeof err === 'string' ? err : err.message || JSON.stringify(err);
if (err?.stack) errorMessage += '\n' + err.stack;
log(ns, `WARNING: daemon.js Caught an error in the targeting loop: ${errorMessage}`, true, 'warning');
continue;
}
} while (!runOnce);
}
// How much a weaken thread is expected to reduce security by
let actualWeakenPotency = () => bitnodeMults.ServerWeakenRate * weakenThreadPotency;
// Get a dictionary from retrieving the same infromation for every server name
async function getServersDict(ns, command, serverNames) {
return await getNsDataThroughFile(ns, `Object.fromEntries(ns.args.map(server => [server, ns.${command}(server)]))`,
`/Temp/${command}-all.txt`, serverNames);
}
let dictInitialServerInfos = (/**@returns{{[serverName: string]: number;}}*/() => undefined)();
let dictServerRequiredHackinglevels = (/**@returns{{[serverName: string]: number;}}*/() => undefined)();
let dictServerNumPortsRequired = (/**@returns{{[serverName: string]: number;}}*/() => undefined)();
let dictServerMinSecurityLevels = (/**@returns{{[serverName: string]: number;}}*/() => undefined)();
let dictServerMaxMoney = (/**@returns{{[serverName: string]: number;}}*/() => undefined)();
let dictServerProfitInfo = (/**@returns{{[serverName: string]: {gainRate: number, expRate: number}}}*/() => undefined)();
let dictServerGrowths = (/**@returns{{[serverName: string]: number;}}*/() => undefined)();
/** Gathers up arrays of server data via external request to have the data written to disk.
* @param {NS} ns */
async function getStaticServerData(ns, serverNames) {
dictServerRequiredHackinglevels = await getServersDict(ns, 'getServerRequiredHackingLevel', serverNames);
dictServerNumPortsRequired = await getServersDict(ns, 'getServerNumPortsRequired', serverNames);
dictServerGrowths = await getServersDict(ns, 'getServerGrowth', serverNames);
// The "GetServer" object result is now required to use the formulas API (due to type checking that the parameter is a valid "server" instance)
// TODO: See if in the future they add a "ns.formulas.dummyServer()" function or similar, then we no longer need this.
// TODO: Iff this becomes permanent, might as well get other static server data from the resulting server objectswhy is it that every keystroke
dictInitialServerInfos = await getServersDict(ns, 'getServer', serverNames);
await refreshDynamicServerData(ns, serverNames);
}
/** Refresh data that might change over time, but for which having precice up-to-date information isn't critical.
* @param {NS} ns **/
async function refreshDynamicServerData(ns, serverNames) {
if (verbose) log(ns, "refreshDynamicServerData");
// Min Security / Max Money can be affected by Hashnet purchases, so we should update this occasionally
dictServerMinSecurityLevels = await getServersDict(ns, 'getServerMinSecurityLevel', serverNames);
dictServerMaxMoney = await getServersDict(ns, 'getServerMaxMoney', serverNames);
// Get the information about the relative profitability of each server (affects targetting order)
const pid = await exec(ns, getFilePath('analyze-hack.js'), daemonHost, 1, '--all', '--silent');
await waitForProcessToComplete_Custom(ns, getHomeProcIsAlive(ns), pid);
const analyzeHackResult = dictServerProfitInfo = ns.read('/Temp/analyze-hack.txt');
if (!analyzeHackResult)
log(ns, "WARNING: analyze-hack info unavailable. Will use fallback approach.");
else
dictServerProfitInfo = Object.fromEntries(JSON.parse(analyzeHackResult).map(s => [s.hostname, s]));
// Determine whether we have purchased stock API accesses yet (affects reserving and attempts to manipulate stock markets)
haveTixApi = haveTixApi || await getNsDataThroughFile(ns, `ns.stock.hasTIXAPIAccess()`);
have4sApi = have4sApi || await getNsDataThroughFile(ns, `ns.stock.has4SDataTIXAPI()`);
// If required, determine the current terminal server (used when intelligence farming)
if (options.i)
currentTerminalServer = getServerByName(await getNsDataThroughFile(ns, 'ns.singularity.getCurrentServer()'));
}
class Server {
/** @param {NS} ns
* @param {string} node - a.k.a host / server **/
constructor(ns, node) {
this.ns = ns;
this.name = node;
this.server = dictInitialServerInfos[node];
this.requiredHackLevel = dictServerRequiredHackinglevels[node];
this.portsRequired = dictServerNumPortsRequired[node];
this.serverGrowth = dictServerGrowths[node];
this.percentageToSteal = 1.0 / 16.0; // This will get tweaked automatically based on RAM available and the relative value of this server
this.previouslyPrepped = false;
this.prepRegressions = 0;
this.previousCycle = null;
this._isPrepped = null;
this._isPrepping = null;
this._isTargeting = null;
this._isXpFarming = null;
this._percentStolenPerHackThread = null;
this._hasRootCached = null; // Once we get root, we never lose it, so we can stop asking
}
resetCaches() {
// Reset any caches that can change over time
this._isPrepped = this._isPrepping = this._isTargeting = this._isXpFarming =
this._percentStolenPerHackThread = null;
// Once true - Does not need to be reset, because once rooted, this fact will never change
if (this._hasRootCached == false) this._hasRootCached = null;
}
getMinSecurity() { return dictServerMinSecurityLevels[this.name] ?? 0; } // Servers not in our dictionary were purchased, and so undefined is okay
getMaxMoney() { return dictServerMaxMoney[this.name] ?? 0; }
getMoneyPerRamSecond() { return dictServerProfitInfo ? dictServerProfitInfo[this.name]?.gainRate ?? 0 : (dictServerMaxMoney[this.name] ?? 0); }
getExpPerSecond() { return dictServerProfitInfo ? dictServerProfitInfo[this.name]?.expRate ?? 0 : (1 / dictServerMinSecurityLevels[this.name] ?? 0); }
getMoney() { return this.ns.getServerMoneyAvailable(this.name); }
getSecurity() { return this.ns.getServerSecurityLevel(this.name); }
canCrack() { return ownedCracks.length >= this.portsRequired; }
canHack() { return this.requiredHackLevel <= playerHackSkill(); }
shouldHack() {
return this.getMaxMoney() > 0 && this.name !== "home" && !this.name.startsWith('hacknet-server-') && !this.name.startsWith('hacknet-node-') &&
!this.name.startsWith(purchasedServersName); // Hack, but beats wasting 2.25 GB on ns.getPurchasedServers()
}
// "Prepped" means current security is at the minimum, and current money is at the maximum
isPrepped() {
if (this._isPrepped != null) return this._isPrepped;
let currentSecurity = this.getSecurity();
let currentMoney = this.getMoney();
// Logic for whether we consider the server "prepped" (tolerate a 1% discrepancy)
this._isPrepped = (currentSecurity == 0 || ((this.getMinSecurity() / currentSecurity) >= 0.99)) &&
(this.getMaxMoney() != 0 && ((currentMoney / this.getMaxMoney()) >= 0.99) || stockFocus /* Only prep security in stock-focus mode */);
return this._isPrepped;
}
// Function to tell if the sever is running any tools, with optional filtering criteria on the tool being run
async isSubjectOfRunningScript(filter, useCache = true, count = false) {
let total = 0;
for (const hostname of allHostNames) // For each server that could be running scripts (TODO: Maintain a smaller list of only servers with more than 1.6GB RAM)
for (const process of processList(this.ns, hostname, useCache)) // For all scripts on the server
// Does the script's arguments suggest it is targetting this server and matches the filter criteria?
if (process.args.length > 0 && process.args[0] == this.name && (!filter || filter(process))) {
if (count)
total++;
else
return true;
}
return count ? total : false;
}
async isPrepping(useCache = true) {
this._isPrepping ??= await this.isSubjectOfRunningScript(process => process.args.length > 4 && process.args[4] == "prep", useCache);
return this._isPrepping;
}
async isTargeting(useCache = true) {
this._isTargeting ??= await this.isSubjectOfRunningScript(process => process.args.length > 4 && process.args[4].startsWith('Batch'), useCache);
return this._isTargeting;
}
async isXpFarming(useCache = true) {
this._isXpFarming ??= await this.isSubjectOfRunningScript(process => process.args.length > 4 && process.args[4] == 'FarmXP', useCache);
return this._isXpFarming;
}
serverGrowthPercentage() {
return this.serverGrowth * bitnodeMults.ServerGrowthRate * getPlayerHackingGrowMulti() / 100;
}
adjustedGrowthRate() {
return Math.min(maxGrowthRate, 1 + ((unadjustedGrowthRate - 1) / this.getMinSecurity()));
}
actualServerGrowthRate() {
return Math.pow(this.adjustedGrowthRate(), this.serverGrowthPercentage());
}
// this is the target growth coefficient *immediately*
targetGrowthCoefficient() {
return this.getMaxMoney() / Math.max(this.getMoney(), 1);
}
// this is the target growth coefficient per cycle, based on theft
targetGrowthCoefficientAfterTheft() {
return 1 / (1 - (this.getHackThreadsNeeded() * this.percentageStolenPerHackThread()));
}
cyclesNeededForGrowthCoefficient() {
return Math.log(this.targetGrowthCoefficient()) / Math.log(this.adjustedGrowthRate());
}
cyclesNeededForGrowthCoefficientAfterTheft() {
return Math.log(this.targetGrowthCoefficientAfterTheft()) / Math.log(this.adjustedGrowthRate());
}
percentageStolenPerHackThread() {
if (this._percentStolenPerHackThread !== null) return this._percentStolenPerHackThread;
if (hasFormulas) {
try {
// Mock the properties required to determine the hackPercent at minimum security
this.server.hackDifficulty = this.getMinSecurity();
this.server.requiredHackingSkill = this.requiredHackLevel;
return this._percentStolenPerHackThread =
this.ns.formulas.hacking.hackPercent(this.server, _cachedPlayerInfo); // hackAnalyzePercent(this.name) / 100;
} catch {
hasFormulas = false;
}
}
return this._percentStolenPerHackThread =
Math.min(1, Math.max(0, (((100 - Math.min(100, this.getMinSecurity())) / 100) *
((playerHackSkill() - (this.requiredHackLevel - 1)) / playerHackSkill()) / 240)));
}
actualPercentageToSteal() {
return this.getHackThreadsNeeded() * this.percentageStolenPerHackThread();
}
getHackThreadsNeeded() {
// Force rounding of low-precision digits before taking the floor, to avoid double imprecision throwing us way off.
return Math.floor((this.percentageToSteal / this.percentageStolenPerHackThread()).toPrecision(14));
}
getGrowThreadsNeeded() {
return Math.min(this.getMaxMoney(),
// TODO: Not true! Worst case is 1$ per thread and *then* it multiplies. We can return a much lower number here.
Math.ceil((this.cyclesNeededForGrowthCoefficient() / this.serverGrowthPercentage()).toPrecision(14)));
}
getWeakenThreadsNeeded() {
return Math.ceil(((this.getSecurity() - this.getMinSecurity()) / actualWeakenPotency()).toPrecision(14));
}
getGrowThreadsNeededAfterTheft() {
return Math.min(this.getMaxMoney(),
Math.ceil((this.cyclesNeededForGrowthCoefficientAfterTheft() / this.serverGrowthPercentage() * recoveryThreadPadding).toPrecision(14)));
}