-
Notifications
You must be signed in to change notification settings - Fork 273
/
Copy pathdaemon.js
2369 lines (2225 loc) · 180 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, formatNumberShort,
hashCode, disableLogs, log, getFilePath, getConfiguration,
getNsDataThroughFile_Custom, runCommand_Custom, waitForProcessToComplete_Custom,
tryGetBitNodeMultipliers_Custom, getActiveSourceFiles_Custom,
getFnRunViaNsExec, tail, autoRetry, getErrorInfo
} from './helpers.js'
// daemon.js has histocially been the central orchestrator of almost every script in the game.
// Only recently has it been "indentured" 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 = [
// Behaviour-changing flags
['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)
['s', true], // (obsolete) Enable Stock Manipulation. This is now true for default, but left as a valid argument for backwards-compatibility.
['stock-manipulation', true], // (obsolete) 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
['x', false], // Focus on a strategy that produces the most hack EXP rather than money
['xp-only', false], // Same as above
['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
['reserved-ram', 32], // Keep this much home RAM free when scheduling hack/grow/weaken cycles on home.
['double-reserve-threshold', 512], // in GB of RAM. Double our home RAM reserve once there is this much home max RAM.
['share', undefined], // Enable sharing free ram to increase faction rep gain (by default, is 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
['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
['max-purchased-server-spend', 0.25], // Percentage of total hack income earnings we're willing to re-invest in new hosts (extra RAM in the current aug only)
// Batch script fine-tuning flags
['initial-max-targets', undefined], // Initial number of servers to target / prep (default is 2 + 1 for every 500 TB of RAM on the network)
['cycle-timing-delay', 4000], // (ms) Length of a hack cycle. The smaller this is, the more batches (HWGW) we can schedule before the first cycle fires, but the greater the chance of a misfire
['queue-delay', 1000], // (ms) Delay before the first script begins, to give time for all scripts to be scheduled
['recovery-thread-padding', 1], // Multiply the number of grow/weaken threads needed by this amount to automatically recover more quickly from misfires.
['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 mor
['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 frome
['looping-mode', false], // Set to true to attempt to schedule perpetually-looping tasks.
// Special-situation flags
['i', false], // Farm intelligence with manual hack.
// Debugging flags
['silent-misfires', false], // Instruct remote scripts not to alert when they misfire
['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)
['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
['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
];
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 [];
}
// script entry point
/** @param {NS} ns **/
export async function main(ns) {
// --- 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 name of the server to try running scripts on if home RAM is <= 16GB (early BN1)
const backupServerName = 'harakiri-sushi'; // Somewhat arbitrarily chosen. It's one of several servers with 16GB which requires no open ports to crack.
// 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
let homeServer = (/**@returns{Server}*/() => [])(); // Quick access to the home server object.
// Lists of tools (external scripts) run
let hackTools, asynchronousHelpers, periodicScripts;
// Helper dict 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 = (/**@returns{BitNodeMultipliers}*/() => undefined)();
let bitNodeN = 1; // The bitnode we're in
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 moneySources = (/**@returns{MoneySources}*/() => undefined)(); // Cache of player income/expenses by category
// 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.
* @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
* @returns {Promise<boolean>} */
async function doesFileExist(ns, fileName, hostname = undefined) {
// Fast (and free) - for local files, try to read the file and ensure it's not empty
hostname ??= daemonHost;
if (hostname === daemonHost && !fileName.endsWith('.exe'))
return ns.read(fileName) != '';
// return ns.fileExists(fileName, hostname);
// TODO: If the approach below causes too much latency, we may wish to cease ram dodging and revert to the simple method above.
const targetServer = getServerByName(hostname); // Each server object should have a cache of files on that server.
if (!targetServer) // If the servers are not yet set up, use the fallback approach (filesExist)
return await filesExist(ns, [fileName], hostname);
return await targetServer.hasFile(fileName);
}
/** 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
* @param {string} serverName
* @returns {ProcessInfo[]} All processes running on this server. */
function processList(ns, serverName, canUseCache = true) {
let psResult = null;
if (canUseCache)
psResult = psCache[serverName];
// Note: We experimented with ram-dodging `ps`, but there's so much data involed that serializing/deserializing generates a lot of latency
//psResult ??= await getNsDataThroughFile(ns, 'ns.ps(ns.args[0])', null, [serverName]));
psResult ??= psCache[serverName] = ns.ps(serverName);
return psResult;
}
/** Get the players own money
* @param {NS} ns
* @returns {number} */
function getPlayerMoney(ns) {
return ns.getServerMoneyAvailable("home");
}
/** Returns the amount of money we should currently be reserving. Dynamically adapts to save money for a couple of big purchases on the horizon
* @param {NS} ns
* @returns {number} */
function reservedMoney(ns) {
let shouldReserve = Number(ns.read("reserve.txt") || 0);
let playerMoney = getPlayerMoney(ns);
// Conserve money if we get close to affording the last hack tool
if (!ownedCracks.includes("SQLInject.exe") && playerMoney > 200e6)
shouldReserve += 250e6; // Start saving at 200m of the 250m required for SQLInject
// Conserve money if we're close to being able to afford the Stock Market 4s API
const fourSigmaCost = (bitNodeMults.FourSigmaMarketDataApiCost * 25000000000);
if (!have4sApi && playerMoney >= fourSigmaCost / 2)
shouldReserve += fourSigmaCost; // Start saving if we're half-way to buying 4S market access
// Conserve money if we're in BN10 and nearing the cost of the last last sleeve
if (bitNodeN == 10 && playerMoney >= 10e15) // 10q - 10% the cost of the last sleeve
shouldReserve = 100e15; // 100q, the cost of the 6th sleeve from The Covenant
return shouldReserve;
}
/** @param {NS} ns **/
async function startup(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, daemonHost, false /* Important! Don't use the (global shared) cache. */)
.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: Killing 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.
}
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)
// TODO: Can probably get rid of all of this now that the entire script is wrapped in the main function.
lastUpdate = "";
lastUpdateTime = Date.now();
maxTargets = 2;
lowUtilizationIterations = highUtilizationIterations = 0;
allHostNames = [], _allServers = [], homeServer = null;
resetServerSortCache();
ownedCracks = [];
psCache = {};
// XpMode Related Caches
singleServerLimit = 0, lastCycleTotalRam = 0; // Cache of total ram on the server to check whether we should attempt to lift the above restriction.
targetsByExp = [], jobHostMappings = {}, farmXpReentryLock = [], nextXpCycleEnd = [];
loopsHackThreadsByServer = {}, loopsByServer_Grow = {}, loopsByServer_Weaken = {};
// Stock mode related caches
serversWithOwnedStock = [], shouldManipulateGrow = [], shouldManipulateHack = [];
failedStockUpdates = 0;
// 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() };
}
bitNodeN = resetInfo.currentNode;
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'];
cycleTimingDelay = options['cycle-timing-delay'];
queueDelay = options['queue-delay'];
maxBatches = options['max-batches'];
homeReservedRam = options['reserved-ram']
maxTargets = options['initial-max-targets'] ?? 0;
if (stockFocus) { // If the user explicitly requested to focus on stocks, ensure we start with as many targets as there are stock symbols
maxTargets = Math.max(maxTargets, Object.keys(serverStockSymbols).length);
log(ns, `Defaulting --initial-max-targets to ${maxTargets} so that we may manipulate every stock (due to --stock-manipulation-focus flag)`);
}
// 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; // Default if not specified (TODO: Improve timings so we don't need so much padding)
if (stockMode) stockFocus = true; // Need to actively kill scripts that go against stock because they will live forever
}
if (xpOnly && !options['no-share']) {
options['no-share'] = true;
log(ns, '--no-share has been implied by -x (--xp-only)');
}
// These scripts are started once and expected to run forever (or terminate themselves when no longer needed)
const openTailWindows = !options['no-tail-windows'];
if (openTailWindows) log(ns, 'Opening tail windows for helper scripts (run with --no-tail-windows to disable)');
await establishMultipliers(ns); // figure out the various bitNode and player multipliers
// Helper to determine whether we meed a given home RAM requirement (To avoid wasting precious early-BN RAM, many scripts don't launch unless we have more than a certain amount)
const reqRam = (ram) => homeServer.totalRam(/*ignoreReservedRam:*/true) >= ram;
// Helper to decide whether we should launch one of the hacknet upgrade manager scripts.
const shouldUpgradeHacknet = () =>
bitNodeMults.HacknetNodeMoney > 0 && // Ensure hacknet is not disabled in this BN
reqRam(Math.min(64, homeReservedRam + 6.1)) && // These scripts consume 6.1 GB and keep running a long time, so we want to ensure we have more than the home reservered RAM amount available if home reserved RAM is a small number
getPlayerMoney(ns) > reservedMoney(ns); // Player money exceeds the reserve (otherwise it will sit there buying nothing)
// ASYNCHRONOUS HELPERS
// Set up "asynchronous helpers" - standalone scripts to manage certain aspacts of the game. daemon.js launches each of these once when ready (but not again if they are shut down)
asynchronousHelpers = [
{ name: "stats.js", shouldRun: () => reqRam(64), shouldTail: false }, // Adds stats not usually in the HUD (nice to have)
{ name: "go.js", shouldRun: () => reqRam(64), minRamReq: 20.2 }, // Play go.js (various multipliers, but large dynamic ram requirements)
{ name: "stockmaster.js", shouldRun: () => reqRam(64), args: openTailWindows ? ["--show-market-summary"] : [] }, // Start our stockmaster
{ name: "hacknet-upgrade-manager.js", shouldRun: () => shouldUpgradeHacknet(), args: ["-c", "--max-payoff-time", "1h", "--interval", "0"], shouldTail: false }, // One-time kickstart of hash income by buying everything with up to 1h payoff time immediately
{ name: "spend-hacknet-hashes.js", shouldRun: () => reqRam(64) && 9 in dictSourceFiles, args: [], shouldTail: false }, // Always have this running to make sure hashes aren't wasted
{ name: "sleeve.js", shouldRun: () => reqRam(64) && 10 in dictSourceFiles }, // Script to create manage our sleeves for us
{ name: "gangs.js", 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
},
{
name: "bladeburner.js", // Script to manage bladeburner for us. Run automatically if not disabled and bladeburner API is available
shouldRun: () => !options['disable-script'].includes('bladeburner.js') && reqRam(64)
&& 7 in dictSourceFiles && bitNodeMults.BladeburnerRank != 0 // Don't run bladeburner in BN's where it can't rank up (currently just BN8)
},
];
// Add any additional scripts to be run provided by --run-script arguments
options['run-script'].forEach(s => asynchronousHelpers.push({ name: s }));
// Set these helper functions to not be marked as "temporary" when they are run (save their execution state)
asynchronousHelpers.forEach(helper => helper.runOptions = { temporary: false });
asynchronousHelpers.forEach(helper => helper.isLaunched = false);
asynchronousHelpers.forEach(helper => helper.ignoreReservedRam = true);
if (openTailWindows) // Tools should be tailed unless they explicitly opted out in the config above
asynchronousHelpers.forEach(helper => helper.shouldTail ??= true);
// PERIODIC SCRIPTS
// These scripts are spawned periodically (at some interval) to do their checks, with an optional condition that limits when they should be spawned
// 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 && !allHostNames.includes("darkweb") },
{ interval: 26000, name: "/Tasks/program-manager.js", shouldRun: () => 4 in dictSourceFiles && ownedCracks.length != 5 },
{ interval: 27000, name: "/Tasks/contractor.js", minRamReq: 14.2 }, // 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", getPlayerMoney(ns) * 0.1] },
{ interval: 28500, name: "hacknet-upgrade-manager.js", shouldRun: shouldUpgradeHacknet, args: () => ["-c", "--max-payoff-time", "8h", "--max-spend", getPlayerMoney(ns) * 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", getPlayerMoney(ns) * 0.001] },
{ // Spend about 50% of un-reserved cash on home RAM upgrades (permanent) when they become available
interval: 30000, name: "/Tasks/ram-manager.js", args: () => ['--budget', 0.5, '--reserve', reservedMoney(ns)],
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", 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 || getPlayerMoney(ns) > 1e9) &&
reqRam(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", minRamReq: 6.55,
// Restrict spending on new servers (i.e. temporary RAM for the current augmentation only) to be a % of total earned hack income.
shouldRun: () => shouldImproveHacking() && getHostManagerBudget() > 0,
args: () => ['--budget', getHostManagerBudget(), '--absolute-reserve', reservedMoney(ns),
// Mechanic to reserve more of our money the longer we've been in the BN. Starts at 0%, after 24h we should be reserving 92%.
'--reserve-by-time', true, '--reserve-by-time-decay-factor', 0.1, '--reserve-percent', 0,
'--utilization-trigger', '0'], // Disable utilization-based restrictions on purchasing RAM
},
// 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", shouldRun: () => 4 in dictSourceFiles && playerHackSkill() > 10 }, // Don't do this until we reach hack level 10. If we backdoor too early, it's very slow and eats up RAM for a long time,
];
periodicScripts.forEach(tool => tool.ignoreReservedRam = true);
if (verbose) // In verbose mode, have periodic sripts persist their logs.
periodicScripts.forEach(tool => tool.runOptions = { temporary: false });
// HACK TOOLS (run with many threads)
hackTools = [
{ name: "/Remote/weak-target.js", shortName: "weak", threadSpreadingAllowed: true },
{ name: "/Remote/grow-target.js", shortName: "grow" }, // Don't want to split because of security hardening after each fire, reducing success chance of next chunk. Also, a minor reduction in gains due to loss of thread count in base money added before exponential growth.
{ name: "/Remote/hack-target.js", shortName: "hack" }, // Don't want to split because of security hardening, as above.
{ name: "/Remote/manualhack-target.js", shortName: "manualhack" },
{ name: "/Remote/share.js", shortName: "share", threadSpreadingAllowed: true },
];
hackTools.forEach(tool => tool.ignoreReservedRam = false);
await buildToolkit(ns, [...asynchronousHelpers, ...periodicScripts, ...hackTools]); // build toolkit
await buildServerList(ns, false); // create the exhaustive server list
// 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 && reqRam(16)); // RamReq ensures we don't attempt this in BN1.1
studying = shouldKickstartHackXp ? true : false; // Flag will be used to prevent focus-stealing scripts from running until we're done studying.
// Immediately crack all servers we can to maximize RAM available on the first loop
for (const server of getAllServers())
if (!server.hasRoot() && server.canCrack())
await doRoot(ns, server);
if (shouldKickstartHackXp) {
// 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);
await kickstartHackXp(ns);
}
// Default the initial maximum number of targets of none was specified.
if (maxTargets == 0) {
const networkStats = getNetworkStats();
maxTargets = 2 + Math.round(networkStats.totalMaxRam / (500 * 1024));
log(ns, `Defaulting --initial-max-targets to ${maxTargets} since total ram available is ${formatRam(networkStats.totalMaxRam)}`);
}
// Start the main targetting loop
await doTargetingLoop(ns);
}
/** Periodic scripts helper function: In bitnodes with hack income disabled, don't waste money on improving hacking infrastructure */
function shouldImproveHacking() {
return 0 != (bitNodeMults.ScriptHackMoneyGain * bitNodeMults.ScriptHackMoney) || // Check for disabled hack-income
getPlayerMoney(ns) > 1e12 || // If we have sufficient money, we may consider improving hack infrastructure (to earn hack exp more quickly)
bitNodeN === 8 // The exception is in BN8, we still want lots of hacking to take place to manipulate stocks, which requires this infrastructure (TODO: Strike a balance between spending on this stuff and leaving money for stockmaster.js)
}
/** Periodic scripts helper function: Get how much we're willing to spend on new servers (host-manager.js budget) */
function getHostManagerBudget() {
const serverSpend = -(moneySources?.sinceInstall?.servers ?? 0); // This is given as a negative number (profit), we invert it to get it as a positive expense amount
const budget = Math.max(0,
// Ensure the total amount of money spent on new servers is less than the configured max spend amount
options['max-purchased-server-spend'] * (moneySources?.sinceInstall?.hacking ?? 0) - serverSpend,
// Special-case support: In some BNs hack income is severely penalized (or zero) but earning hack exp is still useful.
// To support these, always allow a small percentage (0.1%) of our total earnings (including other income sources) to be spent on servers
(moneySources?.sinceInstall?.total ?? 0) * 0.001 - serverSpend);
//log(ns, `Math.max(0, ${options['max-purchased-server-spend']} * (${formatMoney(moneySources?.sinceInstall?.hacking)} ?? 0) - ${formatMoney(serverSpend)}, ` +
// `(${formatMoney(moneySources?.sinceInstall?.total)} ?? 0) * 0.001 - ${formatMoney(serverSpend)}) = ${formatMoney(budget)}`);
return budget;
}
/** @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 = getPlayerMoney(ns)
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: ${getErrorInfo(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
* @param {string} scriptName
* @returns {[string, pid]} */
function whichServerIsRunning(ns, scriptName, canUseCache = true) {
for (const server of getAllServers()) {
const psList = processList(ns, server.name, canUseCache);
const matches = psList.filter(p => p.filename == scriptName);
if (matches.length >= 1)
return [server.name, matches[0].pid];
}
return [null, 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 script of asynchronousHelpers.filter(s => !s.isLaunched)) {
if (!(await tryRunTool(ns, getTool(script))))
continue; // We may have chosen not to run the script for a number of reasons. Proceed to the next one.
if (++launched > 1) await ns.sleep(1); // If we successfully launch more than 1 script at a time, yeild execution a moment to give them a chance to complete, so many aren't all fighting for temp RAM at the same time.
script.isLaunched = true;
}
// 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 script of periodicScripts) {
// Only run this tool if it's been more than <task.interval> milliseconds since it was last run
const timeSinceLastRun = Date.now() - (script.lastRun || 0);
if (timeSinceLastRun <= script.interval) continue;
script.lastRun = Date.now(); // Update the last run date whether we successfully ran it or not
if (await tryRunTool(ns, getTool(script))) // Try to run the task
if (++launched > 1) await ns.sleep(1); // If we successfully launch more than 1 script at a time, yeild execution a moment to give them a chance to complete, so many aren't all fighting for temp RAM at the same time.
}
// Hack: this doesn't really belong here, but is essentially a "temp script" we periodically run when needed
// 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
if (homeServer.getMoney() < options['spend-hashes-for-money-when-under'] // Only if money is below the configured threshold
&& homeServer.ramAvailable(/*ignoreReservedRam:*/true) >= 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)) { // Ensure the script hasn't been disabled
if (verbose) log(ns, `Tool ${tool.name} was not launched as it was specified with --disable-script`);
return false;
}
if (tool.shouldRun != null && !(await tool.shouldRun())) { // Check the script's own conditions for being run
if (verbose) log(ns, `INFO: Tool ${tool.name} was not launched as its shouldRun() function returned false.`);
return false;
}
if (!(await doesFileExist(ns, tool.name))) { // Ensure the script exists
log(ns, `ERROR: Tool ${tool.name} was not found on ${daemonHost}`, true, 'error');
return false;
}
let [runningOnServer, runningPid] = whichServerIsRunning(ns, tool.name, false);
if (runningOnServer != null) { // Ensure the script isn't already running
if (verbose) log(ns, `INFO: Tool ${tool.name} is already running on server ${runningOnServer} as pid ${runningPid}.`);
return true;
}
// If all criteria pass, launch the script on home, or wherever we have space for it.
const args = funcResultOrValue(tool.args) || []; // Support either a static args array, or a function returning the args.
const lowHomeRam = homeServer.totalRam(true) < 32; // Special-case. In early BN1.1, when home RAM is <32 GB, allow certain scripts to be run on any host
const runResult = lowHomeRam ?
(await arbitraryExecution(ns, tool, 1, args, getServerByName(backupServerName).hasRoot() ? backupServerName : daemonHost)) :
(await exec(ns, tool.name, daemonHost, tool.runOptions, ...args));
if (runResult) {
[runningOnServer, runningPid] = whichServerIsRunning(ns, tool.name, false);
//if (verbose)
log(ns, `INFO: Ran tool: ${tool.name} ` + (args.length > 0 ? `with args ${JSON.stringify(args)} ` : '') +
(runningPid ? `on server ${runningOnServer} (pid ${runningPid}).` : 'but it shut down right away.'));
if (tool.shouldTail == true && runningPid) {
log(ns, `Tailing Tool: ${tool.name}` + (args.length > 0 ? ` with args ${JSON.stringify(args)}` : '') + ` on server ${runningOnServer} (pid ${runningPid})`);
tail(ns, runningPid);
//tool.shouldTail = false; // Avoid popping open additional tail windows in the future
}
return true;
} else {
const errHost = getServerByName(daemonHost);
log(ns, `WARN: Tool could not be run on ${lowHomeRam ? "any host" : errHost} at this time (likely due to insufficient RAM. Requires: ${formatRam(tool.cost)} ` +
(lowHomeRam ? '' : `FREE: ${formatRam(errHost.ramAvailable(/*ignoreReservedRam:*/true))})`) + `: ${tool.name} [${args}]`, false, lowHomeRam ? undefined : 'warning');
}
return false;
}
/** Wrapper for ns.exec which automatically retries if there is a failure.
* @param {NS} ns
* @param {string} script - Filename of script to execute.
* @param {string?} hostname - Hostname of the target server on which to execute the script.
* @param {number|RunOptions?} numThreadsOrOptions - Optional thread count or RunOptions. Default is { threads: 1, temporary: true }
* @param {any} 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, hostname = null, numThreadsOrOptions = null, ...args) {
// Defaults
hostname ??= daemonHost;
numThreadsOrOptions ??= { threads: 1, temporary: true };
let fnRunScript = () => ns.exec(script, hostname, numThreadsOrOptions, ...args);
// Wrap the script execution in an 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 fnRunScript();
// Otherwise, run with auto-retry to handle e.g. temporary ram issues
let p;
const pid = await autoRetry(ns, async () => {
p = fnRunScript();
return p;
}, p => {
if (p == 0) log(ns, `WARNING: pid = ${p} after trying to exec ${script} on ${hostname}. Trying again...`, false, "warning");
return p !== 0;
}, () => new Error(`Failed to exec ${script} on ${hostname}. ` +
`This is likely due to having insufficient RAM.\nArgs 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, { temporary: true }, 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;
//let 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 updateCachedServerData(ns); // Update server data that only needs to be refreshed once per loop
await updatePortCrackers(ns); // Check if any new port crackers have been purchased
await getPlayerInfo(ns); // Force an update of _cachedPlayerInfo
if (!allHelpersRunning && loops % 60 == 0) // If we have not yet launched all helpers see if any are now ready to be run (launch may have been postponed while e.g. awaiting more home ram, or TIX API to be purchased)
allHelpersRunning = await runStartupScripts(ns);
// 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, as it affects targetting order
// For early players, change behaviour slightly
const homeRam = homeServer.totalRam(true);
let targetingOrder = await getAllServersByTargetOrder(homeRam); // Sort the targets in the order we should prioritize spending RAM on them
if (!(4 in dictSourceFiles) && homeRam < 64) {
// Until the user buys the first home RAM upgrade, prioritize just one target, so that we see fast results.
if (homeRam == 8) // Note: getAllServersByTargetOrder should be sorting by
maxTargets = maxPreppingAtMaxTargets = 1;
// Periodically provide a hint to buy more home RAM asap
if (loops % 600 == 0)
log(ns, `Reminder: Daemon.js can do a lot more if you have more Home RAM. Right now, you must buy this yourself.` +
`\n Head to the "City", visit [alpha ent.] (or other Tech store), and purchase at least 64 GB as soon as possible!` +
`\n Also be sure to purchase TOR and run "buy -a" from the terminal until you own all hack tools.`, true, 'info');
}
if (loops % 60 == 0) { // For more expensive updates, only do these every so often
// Pull additional data about servers that infrequently changes
await refreshDynamicServerData(ns);
// 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: (* = prepped, ✓ = hackable)\n ' + targetingOrder.filter(s => s.shouldHack()).map(s =>
`${s.isPrepped() ? '*' : ' '} ${s.canHack() ? '✓' : 'X'}` +
` Money: ${formatMoney(s.getMoney(), 4)} of ${formatMoney(s.getMaxMoney(), 4)} ` +
// In Hack Exp mode, show estimated hack exp earned per second, otherwise show money per RAM-second.
(xpOnly ? `Exp: ${formatNumberShort(s.getExpPerSecond(), 4)}/sec` : `(${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}` +
// In stock mode, show any associated stock symbol and whether we have shares to dictate stock manipulation direction
(!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
const n = (/**@returns{Server[]}*/() => []); // 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;
let maxPossibleTargets = targetingOrder.filter(s => s.shouldHack()).length;
// 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 (let 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
let 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 (let 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
let 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 < maxPossibleTargets) {
let network = getNetworkStats();
let utilizationPercent = network.totalUsedRam / network.totalMaxRam;
if (utilizationPercent < lowUtilizationThreshold / 2) {
maxTargets++;
log(ns, `Increased max targets to ${maxTargets} since utilization (${formatNumber(utilizationPercent * 100, 3)}%) ` +
`is less than ${lowUtilizationThreshold * 50}% after scheduling the first ${maxTargets - 1} targets.`);
}
}
}
// 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) {
const 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 (let j = 0; j < 1 /*cantHack.length*/; j++) {
const server = cantHack[j];
if (isWorkCapped()) break;
if (cantHackButPrepped.includes(server) || cantHackButPrepping.includes(server))
continue;
const 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 max hack cycle out of all our targets, we can add a target.
//
// TODO: Make better use of RAM by prepping more targets. Try not scheduling batches way in advance with a sleep, but instead
// witholding batches until they're closer to when they need to be kicked off.
// We can add logic to kill lower priority tasks using RAM (such as share, and scripts targetting low priority targets)
// if necessary to free up ram for new high-priority target batches.
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) {
// Increase max targets if to make use of additional RAM
let actionTaken = null;
if (skipped.length > 0 && maxTargets < maxPossibleTargets) {
maxTargets++;
actionTaken = `Increased max targets to ${maxTargets}`;
} else if (maxTargets >= maxPossibleTargets && recoveryThreadPadding < 10) {
// If we're already targetting every host and we have RAM to spare, increase the recovery padding
// to speed up our recovering from misfires (at the cost of "wasted" ram on every batch)
recoveryThreadPadding = Math.min(10, recoveryThreadPadding * 1.5);
actionTaken = `Increased recovery thread padding to ${formatNumber(recoveryThreadPadding, 2, 1)}`;
}
if (actionTaken) {
log(ns, `${actionTaken} 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)
if (maxTargets > 1) {
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
}
if (targeting.length - 1 > maxTargets) { // Ensure that after a restart, maxTargets start off with no less than 1 fewer max targets
maxTargets = targeting.length - 1;
log(ns, `Increased max targets to ${maxTargets} since we had previous scripts targetting ${targeting.length} servers at startup.`);
}
allTargetsPrepped = 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
// Take note of any new exp targets for our summary, since these those targets aren't tracked in this main loop
for (let server of Object.keys(nextXpCycleEnd).filter(n => nextXpCycleEnd[n] > start && skipped.some(s => s.name == n)).map(n => getServerByName(n))) {
targeting.push(server);
skipped.splice(skipped.findIndex(s => s.name == server.name), 1);
}
} 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['share'] !== false && options['no-share'] !== true &&
(options['share'] === true || 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)
// Getting work info requires sinularity (SF4) - if we don't have it yet, we can still share, but we can only assume we're currently doing faction work.
let workInfo = 4 in dictSourceFiles ? await getCurrentWorkInfo(ns) : { "type": "FACTION" };
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 += targeting.length > 0 ? `\n > Grinding XP from ${targeting.map(s => s.name).join(", ")}` :
prepping.length > 0 ? `\n > Prepping to grind XP from ${prepping.map(s => s.name).join(", ")}` :
'\nERROR: In --xp-mode, but doing nothing!';
// 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 containing internal game script info. Detect this and exit silently
if (err?.env?.stopFlag) return;
log(ns, `WARNING: daemon.js Caught an error in the targeting loop: ${getErrorInfo(err)}`, true, 'warning');
continue;
}
} while (!runOnce);