-
Notifications
You must be signed in to change notification settings - Fork 271
/
host-manager.js
239 lines (215 loc) · 17.6 KB
/
host-manager.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
import { log, getConfiguration, instanceCount, getNsDataThroughFile, formatMoney, formatRam, formatDuration } from './helpers.js'
// The purpose of the host manager is to buy the best servers it can
// until it thinks RAM is underutilized enough that you don't need to anymore.
const purchasedServerName = "daemon"; // The name to give all purchased servers. Also used to determine which servers were purchased
let maxPurchasableServerRamExponent; // The max server ram you can buy as an exponent (power of 2). Typically 1 petabyte (2^20), but less in some BNs
let maxPurchasedServers; // The max number of servers you can have in your farm. Typically 25, but can be less in some BNs
let costByRamExponent = {}; // A dictionary of how much each server size costs, prepped in advance.
// The following globals are set via command line arguments specified below, along with their defaults
let keepRunning = false;
let minRamExponent;
let absReservedMoney;
let pctReservedMoney;
let options;
const argsSchema = [
['c', false], // Set to true to run continuously
['run-continuously', false], // Long-form alias for above flag
['interval', 10000], // Update interval (in milliseconds) when running continuously
['min-ram-exponent', 5], // the minimum amount of ram to purchase
['utilization-trigger', 0.80], // the percentage utilization that will trigger an attempted purchase
['absolute-reserve', null], // Set to reserve a fixed amount of money. Defaults to the contents of reserve.txt on home
['reserve-percent', 0.9], // Set to reserve a percentage of home money
['reserve-by-time', false], // Experimental exponential decay by time in the run. Starts willing to spend lots of money, falls off over time.
['reserve-by-time-decay-factor', 0.2], // Controls how quickly our % reserve increases from --reserve-percent to 100% over time. For example, if --reserve-percent is set to 0.05 (allow spending 95% of money), time to reduce spending to ~25% of money is ~6hrs at 0.2, ~4hrs at 0.3, ~2hrs at 0.5
['budget', Number.POSITIVE_INFINITY], // Yet another way to control spending, this budget will not be exceeded, regardless of player owned money. Reserves are still respected.
['allow-worse-purchases', false], // Set to true to allow purchase of servers worse than our current best purchased server
['compare-to-home-threshold', 0.25], // Do not bother buying servers unless they are at least this big compared to current home RAM
['compare-to-network-ram-threshold', 0.02], // Do not bother buying servers unless they are at least this big compared to total network RAM
];
export function autocomplete(data, _) {
data.flags(argsSchema);
return [];
}
/** Note: In addition to this script's RAM footprint (2.7 GB last this was updated)
* This script requires 3.85 GB of DYNAMIC RAM (2.25 GB (for purchaseServer) + 1.6 GB Base Cost) used by getNsDataThroughFile)
* @param {NS} ns **/
export async function main(ns) {
const runOptions = getConfiguration(ns, argsSchema);
if (!runOptions || await instanceCount(ns) > 1) return; // Prevent multiple instances of this script from being started, even with different args.
options = runOptions; // We don't set the global "options" until we're sure this is the only running instance
ns.disableLog('ALL')
// Get the maximum number of purchased servers in this bitnode
maxPurchasedServers = await getNsDataThroughFile(ns, 'ns.getPurchasedServerLimit()');
log(ns, `INFO: Max purchasable servers has been detected as ${maxPurchasedServers.toFixed(0)}.`);
if (maxPurchasedServers == 0)
return log(ns, `INFO: Shutting down due to host purchasing being disabled in this BN...`);
// Get the maximum size of purchased servers in this bitnode
const purchasedServerMaxRam = await getNsDataThroughFile(ns, 'ns.getPurchasedServerMaxRam()');
maxPurchasableServerRamExponent = Math.log2(purchasedServerMaxRam);
log(ns, `INFO: Max purchasable RAM has been detected as 2^${maxPurchasableServerRamExponent} (${formatRam(2 ** maxPurchasableServerRamExponent)}).`);
// Gather one-time info in advance about how much RAM each size of server costs (Up to 2^30 to be future-proof, but we expect everything abouve 2^20 to be Infinity)
costByRamExponent = await getNsDataThroughFile(ns, 'Object.fromEntries([...Array(30).keys()].map(i => [i, ns.getPurchasedServerCost(2**i)]))', '/Temp/host-costs.txt');
keepRunning = options.c || options['run-continuously'];
pctReservedMoney = options['reserve-percent'];
minRamExponent = options['min-ram-exponent'];
// Log the command line options, for new users who don't know why certain decisions are/aren't being made
if (minRamExponent > maxPurchasableServerRamExponent) {
log(ns, `WARN: --min-ram-exponent was set to ${minRamExponent} (${formatRam(2 ** minRamExponent)}), ` +
`but the maximum server RAM in this BN is ${maxPurchasableServerRamExponent} (${formatRam(2 ** maxPurchasableServerRamExponent)}), ` +
`so the minimum has been lowered accordingly.`);
minRamExponent = maxPurchasableServerRamExponent;
} else
log(ns, `INFO: --min-ram-exponent is set to ${minRamExponent}: New servers will only be purchased ` +
`if we can afford 2^${minRamExponent} (${formatRam(2 ** minRamExponent)}) or more in size.`);
log(ns, `INFO: --compare-to-home-threshold is set to ${options['compare-to-home-threshold'] * 100}%: ` +
`New servers are deemed "not worthwhile" unless they are at least this big compared to your home server.`);
log(ns, `INFO: --compare-to-network-ram-threshold is set to ${options['compare-to-network-ram-threshold'] * 100}%: ` +
`New servers are deemed "not worthwhile" unless they are this big compared to total ram on the entire network.`);
log(ns, `INFO: --utilization-trigger is set to ${options['utilization-trigger'] * 100}%: ` +
`New servers will only be purchased when more than this much RAM is in use across the entire network.`);
if (options['reserve-by-time'])
log(ns, `INFO: --reserve-by-time is active! This community-contributed option will spend more of your money on servers early on, and less later on.`);
else
log(ns, `INFO: --reserve-percent is set to ${pctReservedMoney * 100}%: ` +
`This means we will spend no more than ${((1 - pctReservedMoney) * 100).toFixed(1)}% of current Money on a new server.`);
// Start the main loop (or run once)
if (!keepRunning)
log(ns, `host-manager will run once. Run with argument "-c" to run continuously.`)
do {
absReservedMoney = options['absolute-reserve'] != null ? options['absolute-reserve'] : Number(ns.read("reserve.txt") || 0);
await tryToBuyBestServerPossible(ns);
if (keepRunning)
await ns.sleep(options['interval']);
} while (keepRunning);
}
// Logging system to only print a log if it is different from the last log printed.
let lastStatus = "";
function setStatus(ns, logMessage) {
return logMessage != lastStatus ? ns.print(lastStatus = logMessage) : false;
}
/** @param {NS} ns
* Attempts to buy a server at or better than your home machine. **/
async function tryToBuyBestServerPossible(ns) {
// Scan the set of all servers on the network that we own (or rooted) to get a sense of current RAM utilization
let rootedServers = await getNsDataThroughFile(ns, 'scanAllServers(ns).filter(s => ns.hasRootAccess(s))', '/Temp/rooted-servers.txt');
// Gether the list of all purchased servers.
let purchasedServers = null;
try { purchasedServers = await getNsDataThroughFile(ns, 'ns.getPurchasedServers()', null, null, null, 3, 5, /* silent errors */ true); } catch { /* Ignore */ }
if (purchasedServers == null) // Early game, if we have insufficient RAM (2.25 GB getPurchasedServers + 1.6 base cost), we can fall-back to guessing based on their name
purchasedServers = rootedServers.filter(s => s.startsWith(purchasedServerName));
// If some of the servers are hacknet servers, and they aren't being used for scripts, ignore the RAM they have available
// with the assumption that these are reserved for generating hashes
const likelyHacknet = rootedServers.filter(s => s.startsWith("hacknet-node-") || s.startsWith('hacknet-server-'));
if (likelyHacknet.length > 0) {
const totalHacknetUsedRam = likelyHacknet.reduce((t, s) => t + ns.getServerUsedRam(s), 0);
if (totalHacknetUsedRam == 0) {
rootedServers = rootedServers.filter(s => !likelyHacknet.includes(s));
log(ns, `Removing ${likelyHacknet.length} hacknet servers from RAM statistics since they are not being utilized.`)
} else if (!keepRunning)
log(ns, `We are currently using ${formatRam(totalHacknetUsedRam)} of hacknet RAM, so including hacknet in our utilization stats.`)
}
const totalMaxRam = rootedServers.reduce((t, s) => t + ns.getServerMaxRam(s), 0);
const totalUsedRam = rootedServers.reduce((t, s) => t + ns.getServerUsedRam(s), 0);
if (options['utilization-trigger'] > 0) {
const utilizationRate = totalUsedRam / totalMaxRam;
setStatus(ns, `Using ${Math.round(totalUsedRam).toLocaleString('en')}/${formatRam(totalMaxRam)} (` +
`${(utilizationRate * 100).toFixed(1)}%) across ${rootedServers.length} servers ` +
`(Triggers at ${options['utilization-trigger'] * 100}%, ${purchasedServers.length} bought so far)`);
// If utilization is below target. We don't need another server.
if (utilizationRate < options['utilization-trigger'])
return;
}
// Check for other reasons not to go ahead with the purchase
let prefix = 'Host-manager wants to buy another server, but ';
// Determine our budget for spending money on home RAM
let cashMoney = await getNsDataThroughFile(ns, `ns.getServerMoneyAvailable(ns.args[0])`, null, ["home"]);
if (options['reserve-by-time']) { // Option to vary pctReservedMoney by time since augment.
// Decay factor of 0.2 = Starts willing to spend 95% of our money, backing down to ~75% at 1 hour, ~60% at 2 hours, ~25% at 6 hours, and ~10% at 10 hours.
// Decay factor of 0.3 = Starts willing to spend 95% of our money, backing down to ~66% at 1 hour, ~45% at 2 hours, ~23% at 4 hours, ~10% at 6 hours
// Decay factor of 0.5 = Starts willing to spend 95% of our money, then halving every hour (to ~48% at 1 hour, ~24% at 2 hours, ~12% at 3 hours, etc)
const timeSinceLastAug = Date.now() - (await getNsDataThroughFile(ns, 'ns.getResetInfo()')).lastAugReset;
const t = timeSinceLastAug / (60 * 60 * 1000); // Time since last aug, in hours.
const decayFactor = options['reserve-by-time-decay-factor'];
pctReservedMoney = 1.0 - (1.0 - options['reserve-percent']) * Math.pow(1 - decayFactor, t);
if (!keepRunning)
log(ns, `After spending ${formatDuration(timeSinceLastAug)} in this augmentatin, reserve % has grown from ` +
`${(options['reserve-percent'] * 100).toFixed(1)}% to ${(pctReservedMoney * 100).toFixed(1)}%`);
}
let budget = options['budget'];
let spendableMoney = Math.min(budget, cashMoney * (1.0 - pctReservedMoney), cashMoney - absReservedMoney);
if (spendableMoney <= 0.01) {
if (!keepRunning) // Show a more detailed log if we aren't running continuously
return setStatus(ns, `${prefix}all player cash (${formatMoney(cashMoney)}) is currently reserved (budget: ${formatMoney(budget)}, ` +
`abs reserve: ${formatMoney(absReservedMoney)}, ` +
`% reserve: ${(pctReservedMoney * 100).toFixed(1)}% (${formatMoney(cashMoney * (1.0 - pctReservedMoney))}))`);
// Otherwise the "status" log we show should remain relatively consistent so we aren't spamming e.g. changes in player money in --continuous mode
return setStatus(ns, `${prefix}all player cash is currently reserved (budget: ${formatMoney(budget)}, ` +
`abs reserve: ${formatMoney(absReservedMoney)}, % reserve: ${(pctReservedMoney * 100).toFixed(1)}%)`);
}
// Determine the most ram we can buy with our current money
let exponentLevel = 1;
for (; exponentLevel < maxPurchasableServerRamExponent; exponentLevel++)
if (costByRamExponent[exponentLevel + 1] > spendableMoney)
break;
let cost = costByRamExponent[exponentLevel];
let maxRamPossibleToBuy = Math.pow(2, exponentLevel);
// Don't buy if it would put us below our reserve (shouldn't happen, since we calculated how much to buy based on reserve amount)
if (spendableMoney < cost)
return setStatus(ns, `${prefix}spendableMoney (${formatMoney(spendableMoney)}) is less than the cost ` +
`of even the cheapest server (${formatMoney(cost)} for ${formatRam(2 ** exponentLevel)})`);
// Don't buy if we can't afford our configured --min-ram-exponent
if (exponentLevel < minRamExponent)
return setStatus(ns, `${prefix}The highest ram exponent we can afford (2^${exponentLevel} for ${formatMoney(cost)}) on our budget ` +
`of ${formatMoney(spendableMoney)} is less than the --min-ram-exponent (2^${minRamExponent} for ${formatMoney(costByRamExponent[minRamExponent])})`);
// Under some conditions, we consider the new server "not worthwhile". but only if it isn't the biggest possible server we can buy
if (exponentLevel < maxPurchasableServerRamExponent) {
// Abort if our home server is more than x times bettter (rough guage of how much we 'need' Daemon RAM at the current stage of the game?)
const homeThreshold = options['compare-to-home-threshold'];
// Unless we're looking at buying the maximum purchasable server size - in which case we can do no better
if (maxRamPossibleToBuy < ns.getServerMaxRam("home") * homeThreshold)
return setStatus(ns, `${prefix}the most RAM we can buy (${formatRam(maxRamPossibleToBuy)}) on our budget of ${formatMoney(spendableMoney)} ` +
`is less than --compare-to-home-threshold (${homeThreshold}) x home RAM (${formatRam(ns.getServerMaxRam("home"))})`);
// Abort if purchasing this server wouldn't improve our total RAM by more than x% (ensures we buy in meaningful increments)
const networkThreshold = options['compare-to-network-ram-threshold'];
if (maxRamPossibleToBuy / totalMaxRam < networkThreshold)
return setStatus(ns, `${prefix}the most RAM we can buy (${formatRam(maxRamPossibleToBuy)}) on our budget of ${formatMoney(spendableMoney)} ` +
`is less than --compare-to-network-ram-threshold (${networkThreshold}) x total network RAM (${formatRam(totalMaxRam)})`);
}
// Collect information about other previoulsy purchased servers
const maxPurchasableServerRam = Math.pow(2, maxPurchasableServerRamExponent);
const ramByServer = Object.fromEntries(purchasedServers.map(server => [server, ns.getServerMaxRam(server)]));
let [worstServerName, worstServerRam] = purchasedServers.reduce(([minS, minR], s) =>
ramByServer[s] < minR ? [s, ramByServer[s]] : [minS, minR], [null, maxPurchasableServerRam]);
let [bestServerName, bestServerRam] = purchasedServers.reduce(([maxS, maxR], s) =>
ramByServer[s] > maxR ? [s, ramByServer[s]] : [maxS, maxR], [null, 0]);
// Abort if our worst previously-purchased server is better than the one we're looking to buy (ensures we buy in sane increments of capacity)
if (worstServerName != null && maxRamPossibleToBuy < worstServerRam && !options['allow-worse-purchases'])
return setStatus(ns, `${prefix}the most RAM we can buy (${formatRam(maxRamPossibleToBuy)}) on our budget of ` +
`${formatMoney(spendableMoney)} is less than our worst purchased server ${worstServerName}'s RAM ${formatRam(worstServerRam)}`);
// Only buy new servers as good as or better than our best bought server (anything less is deemed a regression in value)
if (bestServerRam != null && maxRamPossibleToBuy < bestServerRam && !options['allow-worse-purchases'])
return setStatus(ns, `${prefix}the most RAM we can buy (${formatRam(maxRamPossibleToBuy)}) on our budget of ` +
`${formatMoney(spendableMoney)} is less than our previously purchased server ${bestServerName} RAM ${formatRam(bestServerRam)}`);
let purchasedServer,
isUpgrade = false
// if we're at capacity, check to see if we can do better better than the current worst purchased server. If so, upgrade it.
if (purchasedServers.length >= maxPurchasedServers) {
if (worstServerRam == maxPurchasableServerRam) {
keepRunning = false;
return setStatus(ns, `INFO: We are at the max number of servers ${maxPurchasedServers}, ` +
`and all have the maximum possible RAM (${formatRam(maxPurchasableServerRam)}).`);
}
cost -= costByRamExponent[Math.log2(worstServerRam)]
isUpgrade = true
purchasedServer = (await getNsDataThroughFile(ns, `ns.upgradePurchasedServer(ns.args[0], ns.args[1])`, null,
[worstServerName, maxRamPossibleToBuy])) ? worstServerName : "";
} else {
purchasedServer = await getNsDataThroughFile(ns, `ns.purchaseServer(ns.args[0], ns.args[1])`, null,
[purchasedServerName, maxRamPossibleToBuy]);
}
if (!purchasedServer)
setStatus(ns, `${prefix}Could not ${isUpgrade ? 'upgrade' : 'purchase'} a server with ${formatRam(maxRamPossibleToBuy)} RAM for ${formatMoney(cost)} ` +
`with a budget of ${formatMoney(spendableMoney)}. This is either a bug, or we in a SF.9`);
else
log(ns, `SUCCESS: ${isUpgrade ? 'Upgraded' : 'Purchased'} server ${purchasedServer} with ${formatRam(maxRamPossibleToBuy)} RAM for ${formatMoney(cost)}`, true, 'success');
}