forked from alainbryden/bitburner-scripts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
daemon.js
1732 lines (1627 loc) · 122 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,
scanAllServers, hashCode, disableLogs, log as logHelper, getFilePath,
getNsDataThroughFile_Custom, runCommand_Custom, waitForProcessToComplete_Custom,
tryGetBitNodeMultipliers_Custom, getActiveSourceFiles_Custom,
getFnRunViaNsExec, getFnIsAliveViaNsPs, autoRetry
} from './helpers.js'
// the purpose of the daemon is: it's our global starting point.
// it handles several aspects of the game, primarily hacking for money.
// since it requires a robust "execute arbitrarily" functionality
// it serves as the launching point for all the helper scripts we need.
// this list has been steadily growing as time passes.
/*jshint loopfunc:true */
// --- 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;
// Pad weaken thread counts to account for undershooting. (Shouldn't happen. And if this is a timing issue, padding won't help)
const weakenThreadPadding = 0; //0.01;
// 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
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; // (Set in command line args)
let queueDelay; // (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; // (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; // (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; // (Set in command line args)
// --- VARS ---
// some ancillary scripts that run asynchronously, we utilize the startup/execute capabilities of this daemon to run when able
let asynchronousHelpers = [];
let periodicScripts = [];
// The primary tools copied around and used for hacking
let hackTools = [];
// toolkit var for remembering the names and costs of the scripts we use the most
let tools = [];
let toolsByShortName = []; // Dictionary keyed by tool short name
let allHelpersRunning = false; // Tracks whether all long-lived helper scripts have been launched
// 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)
// simple name array of servers that have been added
let addedServerNames = [];
// complex arrays of servers with relevant properties, one is sorted for ram available, the other is for money
let serverListByFreeRam = [];
let serverListByMaxRam = [];
let serverListByTargetOrder = [];
let _ns = null; // Globally available ns reference, for convenience
let daemonHost = null; // the name of the host of this daemon, so we don't have to call the function more than once.
let playerStats = null; // stores ultipliers for player abilities and other player info
let hasFormulas = true;
let currentTerminalServer; // Periodically updated when intelligence farming, the current connected terminal server.
let dictSourceFiles; // Available source files
let bitnodeMults = null; // bitnode multipliers that can be automatically determined after SF-5
// 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;
// Replacements / wrappers for various NS calls to let us keep track of them in one place and consolidate where possible
let log = (...args) => logHelper(_ns, ...args);
async function updatePlayerStats() { return playerStats = await getNsDataThroughFile(_ns, `ns.getPlayer()`, '/Temp/player-info.txt'); }
function playerHackSkill() { return playerStats.hacking; }
function getPlayerHackingGrowMulti() { return playerStats.hacking_grow_mult };
//let playerMoney = () => playerStats.money;
function doesFileExist(filename, hostname = undefined) { return _ns.fileExists(filename, hostname); }
let psCache = [];
/** @param {NS} ns
* PS can get expensive, and we use it a lot so we cache this for the duration of a loop */
function ps(ns, server, canUseCache = true) {
const cachedResult = psCache[server];
return canUseCache && cachedResult ? cachedResult : (psCache[server] = ns.ps(server));
}
// Returns true if we're at a point where we want to save money for a big purchase on the horizon
function shouldReserveMoney() {
let playerMoney = _ns.getServerMoneyAvailable("home");
if (!doesFileExist("SQLInject.exe", "home")) {
if (playerMoney > 20000000)
return true; // Start saving at 200m of the 250m required for SQLInject
} else if (!playerStats.has4SDataTixApi) {
if (playerMoney >= (bitnodeMults.FourSigmaMarketDataApiCost * 25000000000) / 2)
return true; // Start saving if we're half-way to buying 4S market access
}
return false;
}
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', false], // Enable Stock Manipulation
['stock-manipulation', false], // Same as above
['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
['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],
['looping-mode', false], // Set to true to attempt to schedule perpetually-looping tasks.
['recovery-thread-padding', 1],
['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
];
export function autocomplete(data, args) {
data.flags(argsSchema);
return [];
}
// script entry point
/** @param {NS} ns **/
export async function main(ns) {
_ns = ns;
disableLogs(ns, ['getServerMaxRam', 'getServerUsedRam', 'getServerMoneyAvailable', 'getServerGrowth', 'getServerSecurityLevel', 'exec', 'scan', 'asleep']);
daemonHost = "home"; // ns.getHostname(); // get the name of this node (realistically, will always be home)
// Ensure no other copies of this script are running (they share memory)
const scriptName = ns.getScriptName();
const competingDaemons = ns.ps("home").filter(s => s.filename == scriptName && JSON.stringify(s.args) != JSON.stringify(ns.args));
if (competingDaemons.length > 0) {
const strDaemonPids = JSON.stringify(competingDaemons.map(p => p.pid));
log(`WARN: Detected ${competingDaemons.length} other '${scriptName}' instance is running at home (pids: ${strDaemonPids}) - shutting it down...`, true, 'warning')
const killPid = await runCommand(ns, `${strDaemonPids}.forEach(ns.kill)`, '/Temp/kill-daemons.js');
await waitForProcessToComplete_Custom(ns, getFnIsAliveViaNsPs(ns), killPid);
}
// Reset global vars on startup since they persist in memory in certain situations (such as on Augmentation)
lastUpdate = "";
lastUpdateTime = Date.now();
maxTargets = 2;
lowUtilizationIterations = 0;
highUtilizationIterations = 0;
serverListByFreeRam = [];
serverListByTargetOrder = [];
serverListByMaxRam = [];
addedServerNames = [];
tools = [];
toolsByShortName = [];
psCache = [];
await updatePlayerStats();
dictSourceFiles = await getActiveSourceFiles_Custom(ns, getNsDataThroughFile);
log("The following source files are active: " + JSON.stringify(dictSourceFiles));
// Process command line args (if any)
options = ns.flags(argsSchema);
hackOnly = options.h || options['hack-only'];
xpOnly = options.x || options['xp-only'];
stockMode = options.s || options['stock-manipulation'] || options['stock-manipulation-focus'];
stockFocus = options['stock-manipulation-focus'];
useHacknetNodes = options.n || options['use-hacknet-nodes'];
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('-h - Hack-Only mode activated!');
if (xpOnly) log('-x - Hack XP Grinding mode activated!');
if (stockMode) log('-s - Stock market manipulation mode activated!');
if (stockMode && !playerStats.hasTixApiAccess) log("WARNING: Ran with '--stock-manipulation' flag, but this will have no effect until you buy access to the stock market API then restart or manually run stockmaster.js");
if (stockFocus) log('--stock-manipulation-focus - Stock market manipulation is the main priority');
if (useHacknetNodes) log('-n - Using hacknet nodes to run scripts!');
if (verbose) log('-v - Verbose logging activated!');
if (runOnce) log('-o - Run-once mode activated!');
if (loopingMode) {
log('--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'];
asynchronousHelpers = [
{ name: "stats.js", shouldRun: () => ns.getServerMaxRam("home") >= 64 /* Don't waste precious RAM */ }, // Adds stats not usually in the HUD
{ name: "stockmaster.js", args: ["--show-market-summary"], tail: openTailWindows, shouldRun: () => playerStats.hasTixApiAccess }, // Start our stockmaster if we have the required stockmarket access
{ name: "hacknet-upgrade-manager.js", 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: ["-v"], shouldRun: () => 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: () => 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 && (ns.getServerMaxRam("home") >= 128 / (2 ** dictSourceFiles[4])) // Higher SF4 levels result in lower RAM requirements
},
];
asynchronousHelpers.forEach(helper => helper.name = getFilePath(helper.name));
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 = () => !shouldReserveMoney() && (whichServerIsRunning(ns, "hacknet-upgrade-manager.js", false) === null);
// 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 && playerStats.bitNodeN != 8 || 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
{ interval: 25000, name: "/Tasks/tor-manager.js", shouldRun: () => 4 in dictSourceFiles && !addedServerNames.includes("darkweb") },
{ interval: 26000, name: "/Tasks/program-manager.js", shouldRun: () => 4 in dictSourceFiles && getNumPortCrackers() != 5 && (getNumPortCrackers() < 3 || shouldImproveHacking()) },
{ 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: 29000, name: "hacknet-upgrade-manager.js", shouldRun: shouldUpgradeHacknet, args: () => ["-c", "--max-payoff-time", "8h", "--max-spend", ns.getServerMoneyAvailable("home") * 0.01] },
{
interval: 30000, name: "/Tasks/ram-manager.js", args: ['--budget', '0.25',], // Spend about 25% of un-reserved cash on home RAM upgrades (permanent) when they become available
shouldRun: () => 4 in dictSourceFiles && dictSourceFiles[4] >= 2 && !shouldReserveMoney() && shouldImproveHacking() // Only trigger if we have SF4, not saving for anything, and hack income is important
},
{ // Periodically check for new faction invites and join if deemed useful to be in that faction
interval: 31000, name: "faction-manager.js", requiredServer: "home", args: ['--join-only'],
// 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 && (playerStats.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",
args: () => ['--reserve-by-time', '--utilization-trigger', '0', '--reserve-percent', '0.9'], // Spend up to 10% of our money on temporary (only for this aug) servers
shouldRun: () => {
if (shouldReserveMoney() || !shouldImproveHacking()) return false; // Skip if we're saving up, or 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); // build toolkit
await getStaticServerData(ns, scanAllServers(ns)); // Gather information about servers that will never change
buildServerList(ns); // create the exhaustive server list
await establishMultipliers(ns); // figure out the various bitnode and player multipliers
maxTargets = stockFocus ? Object.keys(serverStockSymbols).length : options['initial-max-targets']; // Ensure we immediately attempt to target all servers that represent stocks if in stock-focus mode
if (playerHackSkill() < 500 && playerStats.playtimeSinceLastAug < 600000)
await kickstartHackXp(ns); // If we ascended less than 10 minutes ago, start with some study to quickly restore hack XP
allHelpersRunning = hackOnly ? true : await runStartupScripts(ns); // Start helper scripts
// 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) {
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(`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")
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.
await getNsDataThroughFile(ns, `ns.travelToCity("Volhaven")`, '/Temp/travel-to-city.txt');
await updatePlayerStats(); // Update player stats to be certain of our new location.
const university = playerStats.city == "Sector-12" ? "Rothman University" : playerStats.city == "Aevum" ? "Summit University" : playerStats.city == "Volhaven" ? "ZB Institute of Technology" : null;
if (!university)
log(`INFO: Cannot study, because you are in city ${playerStats.city} which has no known university, and you cannot afford to travel to another city.`);
else {
const course = playerStats.city == "Sector-12" ? "Study Computer Science" : "Algorithms"; // Assume if we are still in Sector-12 we are poor and should only take the free course
await getNsDataThroughFile(ns, `ns.universityCourse('${university}', '${course}')`, '/Temp/study-for-hack-xp.txt');
await ns.asleep(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
}
} catch { log('WARNING: Failed to study to kickstart hack XP', false, 'warning'); }
}
// 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 runPeriodicScripts(ns);
// Immediately attempt to root initially-accessible targets before attempting any XP cycles
for (const server of serverListByTargetOrder.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 = 10;
const maxXpTime = options['initial-hack-xp-time'];
const start = Date.now();
log(`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)
await ns.asleep((await farmHackXp(ns, 1, verbose, 1)) || loopInterval);
}
}
// Check running status of scripts on servers
function whichServerIsRunning(ns, scriptName, canUseCache = true) {
for (const server of serverListByFreeRam)
if (ps(ns, server.name, canUseCache).some(process => process.filename === scriptName))
return server.name;
return null;
}
/** @param {NS} ns
* Helper to kick off external scripts **/
async function runStartupScripts(ns) {
let launched = 0;
for (const helper of asynchronousHelpers) {
if (launched > 0) await ns.asleep(200); // Sleep a short while between each script being launched, so they aren't all fighting for temp RAM at the same time.
if (!helper.isLaunched && (helper.shouldRun === undefined || helper.shouldRun())) {
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);
}
/** @param {NS} ns
* Checks whether it's time for any scheduled tasks to run **/
async function runPeriodicScripts(ns) {
let launched = 0;
for (const task of periodicScripts) {
if (launched > 0) await ns.asleep(200); // Sleep a short while between each script being launched, so they aren't all fighting for temp RAM at the same time.
let tool = getTool(task);
if ((Date.now() - (task.lastRun || 0) >= task.interval) && (task.shouldRun === undefined || task.shouldRun())) {
task.lastRun = Date.now()
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 && ns.getServerMoneyAvailable("home") < 10000000 && (ns.getServerMaxRam("home") - ns.getServerUsedRam("home")) >= 5.6) {
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 **/
async function tryRunTool(ns, tool) {
if (!doesFileExist(tool.name)) {
log(`ERROR: Tool ${tool.name} was not found on ${daemonHost}`, true, 'error');
return false;
}
let runningOnServer = whichServerIsRunning(ns, tool.name);
if (runningOnServer != null) {
if (verbose) log(`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"); // TODO: Allow actually requiring a server
if (runResult) {
runningOnServer = whichServerIsRunning(ns, tool.name, false);
if (verbose) log(`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(`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(`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;
}
let dictScriptsRun = {}; // Keep a cache of every script run on every host, and sleep if it's our first run (to work around a bitburner bug)
/** 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) {
// The Bitburner "does not have a main function" bug (https://github.com/danielyxie/bitburner/issues/1714) appears to only happen the first time a script is called after the game has been started, or the script has been saved / copied
const key = `${host}|${script}`;
const firstRun = !(key in dictScriptsRun);
dictScriptsRun[key] = true;
// Try to run the script with auto-retry if it fails to start
const pid = await autoRetry(ns, async () => {
const p = ns.exec(script, host, numThreads, ...args)
if (firstRun) await ns.asleep(5); // Reports have come in that putting a brief sleep after the calls to exec works around the issue
return p;
}, p => p !== 0, () => `Attempt to exec ${script} on ${host} returned no pid.\nYou may be too low on RAM, or the script may be invalid.`);
return pid; // Caller is responsible for handling errors if final pid returned is 0 (indicating failure)
}
/** @param {NS} ns
* Execute an external script that roots a server, and wait for it to complete. **/
async function doRoot(ns, server) {
const pid = await exec(ns, getFilePath('/Tasks/crack-host.js'), 'home', 1, server.name);
await waitForProcessToComplete_Custom(ns, getFnIsAliveViaNsPs(ns), pid);
}
// Main targeting loop
/** @param {NS} ns **/
async function doTargetingLoop(ns) {
log("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.asleep(loopInterval); // Use asleep to avoid an error if another daemon is spawned to kill this one while it's asleep.
try {
var start = Date.now();
psCache = []; // Clear the cache of the process list we update once per loop
buildServerList(ns, true); // Check if any new servers have been purchased by the external host_manager process
await updatePlayerStats(); // Update player info
// 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
sortServerList("targeting"); // Update the order in which we ought to target servers
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, addedServerNames);
// 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 ' + serverListByTargetOrder.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(targetsLog);
await ns.write("/Temp/targets.txt", targetsLog, "w");
}
}
var prepping = [];
var preppedButNotTargeting = [];
var targeting = [];
var notRooted = [];
var cantHack = [];
var cantHackButPrepped = [];
var cantHackButPrepping = [];
var noMoney = [];
var failed = [];
var skipped = [];
var 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.
var firstUnpreppedServerIndex = serverListByTargetOrder.findIndex(s => s.shouldHack() && s.canHack() && !s.isPrepped() && !s.isTargeting())
if (firstUnpreppedServerIndex !== -1 && !stockMode)
serverListByTargetOrder.unshift(serverListByTargetOrder.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 < serverListByTargetOrder.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 = skipped.concat(serverListByTargetOrder.slice(i));
workCapped = true;
break;
}
const server = serverListByTargetOrder[i];
// 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 (server.isPrepping())
cantHackButPrepping.push(server);
} else if (server.isTargeting()) { // Note servers already being targeted from a prior loop
targeting.push(server); // TODO: While targeting, we should keep queuing more batches
} else if (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) || 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(`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('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(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('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 < serverListByTargetOrder.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 is not working right here, so until we figure it out, never look at more than the first unhackable server.
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('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(`intervalsPerTargetCycle: ${intervalsPerTargetCycle} lowUtilizationIterations: ${lowUtilizationIterations} loopInterval: ${loopInterval}`);
if (lowUtilizationIterations > intervalsPerTargetCycle && skipped.length > 0) {
maxTargets++;
log(`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(`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 = getXPFarmTarget().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
playerStats.isWorking && playerStats.workType == "Working for Faction" && // No point in sharing RAM if we aren't currently working for a faction.
(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
{
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(`Creating ${shareThreads.toLocaleString()} 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 multiple parallel share scripts on the same server
lastShareTime = Date.now();
}
} // else log(`Not Sharing. workCapped: ${isWorkCapped()} utilizationPercent: ${utilizationPercent} maxShareUtilization: ${maxShareUtilization} cooldown: ${formatDuration(Date.now() - lastShareTime)} networkRam: ${network.totalMaxRam}`);
// Log some status updates
let keyUpdates = `Of ${serverListByFreeRam.length} total servers:\n > ${noMoney.length} were ignored (owned or no money)`;
if (notRooted.length > 0)
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((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('Prepping: ' + prepping.map(s => s.name).join(', '))
//log('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);
log(`WARNING: Caught an error in the targeting loop: ${errorMessage}`, true, 'warning');
// Catch errors that appear to be caused by deleted servers, and remove the server from our lists.
const expectedDeletedHostPhrase = "Invalid IP/hostname: ";
let expectedErrorPhraseIndex = errorMessage.indexOf(expectedDeletedHostPhrase);
if (expectedErrorPhraseIndex == -1) continue;
let start = expectedErrorPhraseIndex + expectedDeletedHostPhrase.length;
let lineBreak = errorMessage.indexOf('<br>', start);
let deletedHostName = errorMessage.substring(start, lineBreak);
log('INFO: The server "' + deletedHostName + '" appears to have been deleted. Removing it from our lists', false, 'info');
removeServerByName(deletedHostName);
}
} while (!runOnce);
}
// How much a weaken thread is expected to reduce security by
let actualWeakenPotency = () => bitnodeMults.ServerWeakenRate * weakenThreadPotency * (1 - weakenThreadPadding);
// Dictionaries of static server information
let serversDictCommand = (servers, command) => `Object.fromEntries(${JSON.stringify(servers)}.map(server => [server, ${command}]))`;
let dictServerRequiredHackinglevels;
let dictServerNumPortsRequired;
let dictServerMinSecurityLevels;
let dictServerMaxMoney;
let dictServerProfitInfo;
// Gathers up arrays of server data via external request to have the data written to disk.
async function getStaticServerData(ns, serverNames) {
dictServerRequiredHackinglevels = await getNsDataThroughFile(ns, serversDictCommand(serverNames, 'ns.getServerRequiredHackingLevel(server)'), '/Temp/servers-hack-req.txt');
dictServerNumPortsRequired = await getNsDataThroughFile(ns, serversDictCommand(serverNames, 'ns.getServerNumPortsRequired(server)'), '/Temp/servers-num-ports.txt');
await refreshDynamicServerData(ns, serverNames);
}
/** @param {NS} ns **/
async function refreshDynamicServerData(ns, serverNames) {
dictServerMinSecurityLevels = await getNsDataThroughFile(ns, serversDictCommand(serverNames, 'ns.getServerMinSecurityLevel(server)'), '/Temp/servers-security.txt');
dictServerMaxMoney = await getNsDataThroughFile(ns, serversDictCommand(serverNames, 'ns.getServerMaxMoney(server)'), '/Temp/servers-max-money.txt');
// Get the information about the relative profitability of each server
const pid = await exec(ns, getFilePath('analyze-hack.js'), 'home', 1, '--all', '--silent');
await waitForProcessToComplete_Custom(ns, getFnIsAliveViaNsPs(ns), pid);
dictServerProfitInfo = ns.read('/Temp/analyze-hack.txt');
if (!dictServerProfitInfo) return log(ns, "WARN: analyze-hack info unavailable.");
dictServerProfitInfo = Object.fromEntries(JSON.parse(dictServerProfitInfo).map(s => [s.hostname, s]));
//ns.print(dictServerProfitInfo);
if (options.i)
currentTerminalServer = getServerByName(await getNsDataThroughFile(ns, 'ns.getCurrentServer()', '/Temp/terminal-server.txt'));
}
/** @param {NS} ns **/
function buildServerObject(ns, node) {
return {
ns: ns,
name: node,
requiredHackLevel: dictServerRequiredHackinglevels[node],
portsRequired: dictServerNumPortsRequired[node],
getMinSecurity: () => dictServerMinSecurityLevels[node] ?? 0, // Servers not in our dictionary were purchased, and so undefined is okay
getMaxMoney: () => dictServerMaxMoney[node] ?? 0,
getMoneyPerRamSecond: () => dictServerProfitInfo ? dictServerProfitInfo[node]?.gainRate ?? 0 : (dictServerMaxMoney[node] ?? 0),
getExpPerSecond: () => dictServerProfitInfo ? dictServerProfitInfo[node]?.expRate ?? 0 : (1 / dictServerMinSecurityLevels[node] ?? 0),
percentageToSteal: 1.0 / 16.0, // This will get tweaked automatically based on RAM available and the relative value of this server
getMoney: function () { return this.ns.getServerMoneyAvailable(this.name); },
getSecurity: function () { return this.ns.getServerSecurityLevel(this.name); },
canCrack: function () { return getNumPortCrackers() >= this.portsRequired; },
canHack: function () { return this.requiredHackLevel <= playerHackSkill(); },
shouldHack: function () {
return this.getMaxMoney() > 0 && this.name !== "home" && !this.name.startsWith('hacknet-node-') &&
!this.name.startsWith(purchasedServersName); // Hack, but beats wasting 2.25 GB on ns.getPurchasedServers()
},
previouslyPrepped: false,
prepRegressions: 0,
previousCycle: null,
// "Prepped" means current security is at the minimum, and current money is at the maximum
isPrepped: function () {
let currentSecurity = this.getSecurity();
let currentMoney = this.getMoney();
// Logic for whether we consider the server "prepped" (tolerate a 1% discrepancy)
let 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 isPrepped;
},
// Function to tell if the sever is running any tools, with optional filtering criteria on the tool being run
isSubjectOfRunningScript: function (filter, useCache = true, count = false) {
const toolNames = hackTools.map(t => t.name);
let total = 0;
// then figure out if the servers are running the other 2, that means prep
for (const hostname of addedServerNames)
for (const process of ps(ns, hostname, useCache))
if (toolNames.includes(process.filename) && process.args[0] == this.name && (!filter || filter(process))) {
if (count) total++; else return true;
}
return count ? total : false;
},
isPrepping: function (useCache = true) {
return this.isSubjectOfRunningScript(process => process.args.length > 4 && process.args[4] == "prep", useCache);
},
isTargeting: function (useCache = true) {
return this.isSubjectOfRunningScript(process => process.args.length > 4 && process.args[4].includes('Batch'), useCache);
},
isXpFarming: function (useCache = true) {
return this.isSubjectOfRunningScript(process => process.args.length > 4 && process.args[4].includes('FarmXP'), useCache);
},
serverGrowthPercentage: function () {
return this.ns.getServerGrowth(this.name) * bitnodeMults.ServerGrowthRate * getPlayerHackingGrowMulti() / 100;
},
adjustedGrowthRate: function () { return Math.min(maxGrowthRate, 1 + ((unadjustedGrowthRate - 1) / this.getMinSecurity())); },
actualServerGrowthRate: function () {
return Math.pow(this.adjustedGrowthRate(), this.serverGrowthPercentage());
},
// this is the target growth coefficient *immediately*
targetGrowthCoefficient: function () {
return this.getMaxMoney() / Math.max(this.getMoney(), 1);
},
// this is the target growth coefficient per cycle, based on theft
targetGrowthCoefficientAfterTheft: function () {
return 1 / (1 - (this.getHackThreadsNeeded() * this.percentageStolenPerHackThread()));
},
cyclesNeededForGrowthCoefficient: function () {
return Math.log(this.targetGrowthCoefficient()) / Math.log(this.adjustedGrowthRate());
},
cyclesNeededForGrowthCoefficientAfterTheft: function () {
return Math.log(this.targetGrowthCoefficientAfterTheft()) / Math.log(this.adjustedGrowthRate());
},
percentageStolenPerHackThread: function () {
if (hasFormulas) {
try {
let server = {
hackDifficulty: this.getMinSecurity(),
requiredHackingSkill: this.requiredHackLevel
}
return ns.formulas.hacking.hackPercent(server, playerStats); // hackAnalyzePercent(this.name) / 100;
} catch {
hasFormulas = false;
}
}
return Math.min(1, Math.max(0, (((100 - Math.min(100, this.getMinSecurity())) / 100) *
((playerHackSkill() - (this.requiredHackLevel - 1)) / playerHackSkill()) / 240)));
},
actualPercentageToSteal: function () {
return this.getHackThreadsNeeded() * this.percentageStolenPerHackThread();
},
getHackThreadsNeeded: function () {
// 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: function () {
return Math.min(this.getMaxMoney(), // Worse case (0 money on server) we get 1$ per thread
Math.ceil((this.cyclesNeededForGrowthCoefficient() / this.serverGrowthPercentage()).toPrecision(14)));
},
getGrowThreadsNeededAfterTheft: function () {
return Math.min(this.getMaxMoney(), // Worse case (0 money on server) we get 1$ per thread
Math.ceil((this.cyclesNeededForGrowthCoefficientAfterTheft() / this.serverGrowthPercentage()).toPrecision(14)));
},
getWeakenThreadsNeededAfterTheft: function () {
return Math.ceil((this.getHackThreadsNeeded() * hackThreadHardening / actualWeakenPotency()).toPrecision(14));
},
getWeakenThreadsNeededAfterGrowth: function () {
return Math.ceil((this.getGrowThreadsNeededAfterTheft() * growthThreadHardening / actualWeakenPotency()).toPrecision(14));
},
// Once we get root, we never lose it, so we can stop asking
_hasRootCached: false,
hasRoot: function () { return this._hasRootCached || (this._hasRootCached = this.ns.hasRootAccess(this.name)); },
isHost: function () { return this.name == daemonHost; },
totalRam: function () {
var maxRam = this.ns.getServerMaxRam(this.name);
if (this.name == "home") maxRam = Math.max(0, maxRam - homeReservedRam); // Complete HACK: but for most planning purposes, we want to pretend home has less ram to leave room for temp scripts to run
return maxRam;
},
usedRam: function () {
var usedRam = this.ns.getServerUsedRam(this.name);
// TODO: Uncertain whether reserved ram is best done by pretending home has less RAM, or pretending it has more ram in use.
//if (this.name == "home")
// usedRam = Math.min(this.totalRam(), usedRam + homeReservedRam);
return usedRam;
},
ramAvailable: function () { return this.totalRam() - this.usedRam(); },
growDelay: function () { return this.timeToWeaken() - this.timeToGrow() + cycleTimingDelay; },
hackDelay: function () { return this.timeToWeaken() - this.timeToHack(); },
timeToWeaken: function () { return this.ns.getWeakenTime(this.name); },
timeToGrow: function () { return this.ns.getGrowTime(this.name); },
timeToHack: function () { return this.ns.getHackTime(this.name); },
weakenThreadsNeeded: function () { return Math.ceil(((this.getSecurity() - this.getMinSecurity()) / actualWeakenPotency()).toPrecision(14)); }
};
}
// Helpers to get slices of info / cumulative stats across all rooted servers
function getNetworkStats() {
const rootedServers = serverListByMaxRam.filter(server => server.hasRoot());
const listOfServersFreeRam = rootedServers.map(s => s.ramAvailable()).filter(ram => ram > 1.6); // Servers that can't run a script don't count
const totalMaxRam = rootedServers.map(s => s.totalRam()).reduce((a, b) => a + b, 0);
const totalFreeRam = Math.max(0, listOfServersFreeRam.reduce((a, b) => a + b, 0)); // Hack, free ram can be negative due to "pretending" reserved home ram doesn't exist. Clip to 0
return {
listOfServersFreeRam: listOfServersFreeRam,
totalMaxRam: totalMaxRam,
totalFreeRam: totalFreeRam,
totalUsedRam: totalMaxRam - totalFreeRam,
// The money we could make if we took 100% from every currently hackable server, to help us guage how relatively profitable each server is
//totalMaxMoney: rootedServers.filter(s => s.canHack() && s.shouldHack()).map(s => s.getMaxMoney()).reduce((a, b) => a + b, 0)
};
}
// Simpler function to get current total percentage of ram used across the network
function getTotalNetworkUtilization() {
const utilizationStats = getNetworkStats();
return utilizationStats.totalUsedRam / utilizationStats.totalMaxRam;
}
// return a "performance snapshot" (Ram required for the cycle) to compare against optimal, or another snapshot
// TODO: Better gaugue of performance is money stolen per (RAM * time) cost - we can schedule as many cycles as we want if done smart
function getPerformanceSnapshot(currentTarget, networkStats) {
// The total RAM cost of running one weaken/hack/grow cycle to steal `currentTarget.percentageToSteal` of `currentTarget.money`
const weaken1Cost = currentTarget.getWeakenThreadsNeededAfterTheft() * getTool("weak").cost;
const weaken2Cost = currentTarget.getWeakenThreadsNeededAfterGrowth() * getTool("weak").cost;
const growCost = currentTarget.getGrowThreadsNeededAfterTheft() * getTool("grow").cost;
const hackCost = currentTarget.getHackThreadsNeeded() * getTool("hack").cost;
// Simulate how many times we could schedule this batch given current server ram availability
// (and hope that whatever executes the tasks in this batch is clever enough to slot them in as such (TODO: simulate using our actual executor logic?)
const jobs = [weaken1Cost, weaken2Cost, growCost, hackCost].sort((a, b) => b - a); // Sort jobs largest to smallest
const simulatedRemainingRam = networkStats.listOfServersFreeRam.slice();
var maxScheduled = -1;
var canScheduleAnother = true;
while (canScheduleAnother && maxScheduled++ <= maxBatches) {
for (const job of jobs) {
// Find a free slot for this job, starting with largest servers as the scheduler tends to do
const freeSlot = simulatedRemainingRam.sort((a, b) => b - a).findIndex(ram => ram >= job);
if (freeSlot === -1)
canScheduleAnother = false;
else
simulatedRemainingRam[freeSlot] -= job;
}
}
return {
percentageToSteal: currentTarget.actualPercentageToSteal(),
canBeScheduled: maxScheduled > 0,
// Given our timing delay, **approximately** how many cycles can we initiate before the first batch's first task fires?
// TODO: Do a better job of calculating this *outside* of the performance snapshot, and only calculate it once.
optimalPacedCycles: Math.min(maxBatches, Math.max(1, Math.floor(((currentTarget.timeToWeaken()) / cycleTimingDelay).toPrecision(14))
- 1)), // Fudge factor, this isnt an exact scuence
// Given RAM availability, how many cycles could we schedule across all hosts?
maxCompleteCycles: Math.max(maxScheduled - 1, 1) // Fudge factor. The executor isn't perfect
};
}
// Produce a summary string containing information about a hack batch for a given target configuration
let getTargetSummary = currentTarget =>
`(H:${currentTarget.getHackThreadsNeeded()} W:${currentTarget.getWeakenThreadsNeededAfterTheft()} ` +
`G:${currentTarget.getGrowThreadsNeededAfterTheft()} W²:${currentTarget.getWeakenThreadsNeededAfterGrowth()}) ` +
(stockMode && shouldManipulateGrow[currentTarget.name] ? 'with grow stock ' : stockMode && shouldManipulateHack[currentTarget.name] ? 'with hack stock ' : '') +
`to steal ${formatNumber(currentTarget.actualPercentageToSteal() * 100)}% ` +
`(${formatMoney(currentTarget.actualPercentageToSteal() * currentTarget.getMaxMoney(), 3, 1)}) ` +
`ETA: ${formatDuration(currentTarget.timeToWeaken())} at Hack ${playerHackSkill()} (${currentTarget.name})`;
// Adjusts the "percentage to steal" for a target based on its respective cost and the current network RAM available
function optimizePerformanceMetrics(currentTarget) {
const maxAdjustments = 1000;
const start = Date.now();
const networkStats = getNetworkStats();
const percentPerHackThread = currentTarget.percentageStolenPerHackThread();
const oldHackThreads = currentTarget.getHackThreadsNeeded();
const oldActualPercentageToSteal = currentTarget.percentageToSteal = currentTarget.actualPercentageToSteal();
if (percentPerHackThread >= 1) {
currentTarget.percentageToSteal = percentPerHackThread;
currentTarget.percentageToSteal = 1;
return getPerformanceSnapshot(currentTarget, networkStats);
}
let lastAdjustmentSign = 1;
let attempts = 0;
let increment = Math.ceil((0.01 / percentPerHackThread).toPrecision(14)); // Initialize the adjustment increment to be the number of hack threads to steal roughly 1%
let newHackThreads = oldHackThreads;
currentTarget.percentageToSteal = Math.max(currentTarget.percentageToSteal, percentPerHackThread); // If the initial % to steal is below the minimum, raise it
// Make adjustments to the number of hack threads until we zero in on the best amount
while (++attempts < maxAdjustments) {
var performanceSnapshot = getPerformanceSnapshot(currentTarget, networkStats);
const adjustment = analyzeSnapshot(performanceSnapshot, currentTarget, networkStats, increment);
if (runOnce && verbose)
log(`Adjustment ${attempts} (increment ${increment}): ${adjustment} to ${newHackThreads} hack threads ` +
`(from ${formatNumber(currentTarget.actualPercentageToSteal() * 100)}% or ${currentTarget.getHackThreadsNeeded()} hack threads)`);
if (adjustment === 0.00 && increment == 1) break; // We've zeroed in on the exact number of hack threads we want
if (adjustment === 0.00 || Math.sign(adjustment) != lastAdjustmentSign) { // Each time we change the direction of adjustments, slow the adjustment rate
increment = Math.max(1, Math.floor((increment / 2.0).toPrecision(14)));
lastAdjustmentSign = adjustment === 0.00 ? lastAdjustmentSign : Math.sign(adjustment);
}
newHackThreads = Math.max(newHackThreads + adjustment, 0); // Adjust the percentage to steal with pefect precision by actually adjusting the number of hack threads
currentTarget.percentageToSteal = Math.max(0, newHackThreads * percentPerHackThread);
}
if (attempts >= maxAdjustments || verbose && currentTarget.actualPercentageToSteal() != oldActualPercentageToSteal) {
log(`Tuned % to steal from ${formatNumber(oldActualPercentageToSteal * 100)}% (${oldHackThreads} threads) to ` +
`${formatNumber(currentTarget.actualPercentageToSteal() * 100)}% (${currentTarget.getHackThreadsNeeded()} threads) ` +
`(${currentTarget.name}) Iterations: ${attempts} Took: ${Date.now() - start} ms`);
}
if (verbose && currentTarget.actualPercentageToSteal() == 0) {
currentTarget.percentageToSteal = percentPerHackThread;
log(`Insufficient RAM for min cycle: ${getTargetSummary(currentTarget)}`);
currentTarget.percentageToSteal = 0.0;
}
if (currentTarget.percentageToSteal != 0 && (currentTarget.actualPercentageToSteal() == 0 ||
Math.abs(currentTarget.actualPercentageToSteal() - currentTarget.percentageToSteal) / currentTarget.percentageToSteal > 0.5))
log(`WARNING: Big difference between %ToSteal (${formatNumber(currentTarget.percentageToSteal * 100)}%) ` +
`and actual%ToSteal (${formatNumber(currentTarget.actualPercentageToSteal() * 100)}%) after ${attempts} attempts. ` +
`Min is: ${formatNumber(currentTarget.percentageStolenPerHackThread() * 100)}%`, false, 'warning');
return performanceSnapshot;
}
// Suggests an adjustment to the percentage to steal based on how much ram would be consumed if attempting the current percentage.