/* Welcome to part 2. I'll only be commenting on things that have changed from the previous part, so if there's something confusing, be sure to go back and look at part 1 for more detailed explanations. For part 2, we'll be making a protobatcher. Essentially that means we'll be running our previous version in a constant loop. To facilitate this, and because otherwise there wouldn't really be much to this part, we're going to refine the way our scripts communicate with each other using ports. */ import { getServers, copyScripts, checkTarget, isPrepped, prep } from "/S2utils.js"; const TYPES = ["hack", "weaken1", "grow", "weaken2"]; const WORKERS = ["S2tHack.js", "S2tWeaken.js", "S2tGrow.js"]; const SCRIPTS = { hack: "S2tHack.js", weaken1: "S2tWeaken.js", grow: "S2tGrow.js", weaken2: "S2tWeaken.js" }; const COSTS = { hack: 1.7, weaken1: 1.75, grow: 1.75, weaken2: 1.75 }; const OFFSETS = { hack: 0, weaken1: 1, grow: 2, weaken2: 3 }; /* Most of the changes are in the main function, so I've moved it up top. I generally prefer having the main function at the top of the file anyway. */ /** @param {NS} ns */ export async function main(ns) { await ns.sleep(500); // Moving most of our active feeback to the tail window so that batches finishing don't get swept away. ns.disableLog("ALL"); ns.tail(); // Stick the whole script in a loop. That's it, see you in part 3. // Just kidding, there's a bit more to it. let batchCount = 0; while (true) { // Register a port using the script's unique handle. // I like to keep ports strictly coupled to a specific script, but you can use whatever number you like. const dataPort = ns.getPortHandle(ns.pid); dataPort.clear() // Make sure there's no random data left in the port. let target = "n00dles"; const servers = getServers(ns, (server) => { // Don't worry if you don't have Formulas, it's not needed at all here. target = checkTarget(ns, server, target, ns.fileExists("Formulas.exe", "home")); copyScripts(ns, server, WORKERS, true); return ns.hasRootAccess(server); }); //target = "n00dles"; const ramNet = new RamNet(ns, servers); const metrics = new Metrics(ns, target); if (!isPrepped(ns, target)) await prep(ns, metrics, ramNet); optimizeBatch(ns, metrics, ramNet); // The same optimization algorithm works just fine for protobatching. metrics.calculate(ns); const batch = []; batchCount++; for (const type of TYPES) { // We've removed the buffer. You'll see why later. metrics.ends[type] = Date.now() + metrics.wTime + metrics.spacer * OFFSETS[type]; const job = new Job(type, metrics); job.batch = batchCount; // This is a bit of a hack. We'll do it better in the next part. if (!ramNet.assign(job)) { ns.print(`ERROR: Unable to assign ${type}. Dumping debug info:`); ns.print(job); ns.print(metrics); ramNet.printBlocks(ns); return; } batch.push(job); } // We do a bit more during deployment now. for (const job of batch) { job.end += metrics.delay; const jobPid = ns.exec(SCRIPTS[job.type], job.server, { threads: job.threads, temporary: true }, JSON.stringify(job)); if (!jobPid) throw new Error(`Unable to deploy ${job.type}`); // If the exec fails for any reason, error out. /* If a worker deploys late, it will communicate back how late it was, so that the other scripts can adjust. Note that for this we use the *worker's* port instead of our controller's port. It's good practice to make sure your ports have a very narrow focus. */ const tPort = ns.getPortHandle(jobPid); await tPort.nextWrite(); metrics.delay += tPort.read(); } const timer = setInterval(() => { ns.clearLog(); ns.print(`Hacking \$${ns.formatNumber(metrics.maxMoney * metrics.greed)} from ${metrics.target}`) ns.print(`Running batch: ETA ${ns.tFormat(metrics.ends.weaken2 - Date.now())}`); }, 1000); ns.atExit(() => { clearInterval(timer); }); // Wait for the weaken2 worker to report back. For now I've just hardcoded the Job class to tell only // weaken2 to report. This behavior will change later. await dataPort.nextWrite(); dataPort.clear(); // For now we don't actually need the information here, we're just using it for timing. clearInterval(timer); } } class Job { constructor(type, metrics, server = "none") { this.type = type; this.end = metrics.ends[type]; this.time = metrics.times[type]; this.target = metrics.target; this.threads = metrics.threads[type]; this.cost = this.threads * COSTS[type]; this.server = server; this.report = this.type === "weaken2"; // For now, only w2 jobs report. this.port = metrics.port; // This lets the workers know which port to write to. this.batch = 0; // We'll keep track of how many we've run, just because we can. } } /** @param {NS} ns */ class Metrics { constructor(ns, server) { this.target = server; this.maxMoney = ns.getServerMaxMoney(server); this.money = Math.max(ns.getServerMoneyAvailable(server), 1); this.minSec = ns.getServerMinSecurityLevel(server); this.sec = ns.getServerSecurityLevel(server); this.prepped = isPrepped(ns, server); this.chance = 0; this.wTime = 0; this.delay = 0; // The cumulative delays caused by late jobs. this.spacer = 5; this.greed = 0.1; this.depth = 0; // Still not using this. this.times = { hack: 0, weaken1: 0, grow: 0, weaken2: 0 }; this.ends = { hack: 0, weaken1: 0, grow: 0, weaken2: 0 }; this.threads = { hack: 0, weaken1: 0, grow: 0, weaken2: 0 }; this.port = ns.pid; } calculate(ns, greed = this.greed) { const server = this.target; const maxMoney = this.maxMoney; this.money = ns.getServerMoneyAvailable(server); this.sec = ns.getServerSecurityLevel(server); this.wTime = ns.getWeakenTime(server); this.times.weaken1 = this.wTime; this.times.weaken2 = this.wTime; this.times.hack = this.wTime / 4; this.times.grow = this.wTime * 0.8; this.depth = this.wTime / this.spacer * 4; const hPercent = ns.hackAnalyze(server); const amount = maxMoney * greed; const hThreads = Math.max(Math.floor(ns.hackAnalyzeThreads(server, amount)), 1); const tGreed = hPercent * hThreads; const gThreads = Math.ceil(ns.growthAnalyze(server, maxMoney / (maxMoney - maxMoney * tGreed))); this.threads.weaken1 = Math.max(Math.ceil(hThreads * 0.002 / 0.05), 1); this.threads.weaken2 = Math.max(Math.ceil(gThreads * 0.004 / 0.05), 1); this.threads.hack = hThreads; this.threads.grow = gThreads; this.chance = ns.hackAnalyzeChance(server); } } /** @param {NS} ns */ class RamNet { #blocks = []; #minBlockSize = Infinity; #maxBlockSize = 0; #totalRam = 0; #maxRam = 0; #prepThreads = 0; #index = new Map(); constructor(ns, servers) { for (const server of servers) { if (ns.hasRootAccess(server)) { const maxRam = ns.getServerMaxRam(server); const ram = maxRam - ns.getServerUsedRam(server); if (ram >= 1.60) { const block = { server: server, ram: ram }; this.#blocks.push(block); if (ram < this.#minBlockSize) this.#minBlockSize = ram; if (ram > this.#maxBlockSize) this.#maxBlockSize = ram; this.#totalRam += ram; this.#maxRam += maxRam; this.#prepThreads += Math.floor(ram / 1.75); } } } this.#sort(); this.#blocks.forEach((block, index) => this.#index.set(block.server, index)); } #sort() { this.#blocks.sort((x, y) => { if (x.server === "home") return 1; if (y.server === "home") return -1; return x.ram - y.ram; }); } getBlock(server) { if (this.#index.has(server)) { return this.#blocks[this.#index.get(server)]; } else { throw new Error(`Server ${server} not found in RamNet.`); } } get totalRam() { return this.#totalRam; } get maxRam() { return this.#maxRam; } get maxBlockSize() { return this.#maxBlockSize; } get prepThreads() { return this.#prepThreads; } assign(job) { const block = this.#blocks.find(block => block.ram >= job.cost); if (block) { job.server = block.server; block.ram -= job.cost; this.#totalRam -= job.cost; return true; } else return false; } finish(job) { const block = this.getBlock(job.server); block.ram += job.cost; this.#totalRam += job.cost; } cloneBlocks() { return this.#blocks.map(block => ({ ...block })); } printBlocks(ns) { for (const block of this.#blocks) ns.print(block); } } /** * @param {NS} ns * @param {Metrics} metrics * @param {RamNet} ramNet */ export function optimizeBatch(ns, metrics, ramNet) { const maxThreads = ramNet.maxBlockSize / 1.75; const maxMoney = metrics.maxMoney; const hPercent = ns.hackAnalyze(metrics.target); const minGreed = 0.001; const stepValue = 0.001; let greed = 0.99; while (greed > minGreed) { const amount = maxMoney * greed; const hThreads = Math.max(Math.floor(ns.hackAnalyzeThreads(metrics.target, amount)), 1); const tGreed = hPercent * hThreads; const gThreads = Math.ceil(ns.growthAnalyze(metrics.target, maxMoney / (maxMoney - maxMoney * tGreed))); if (Math.max(hThreads, gThreads) <= maxThreads) { const wThreads1 = Math.max(Math.ceil(hThreads * 0.002 / 0.05), 1); const wThreads2 = Math.max(Math.ceil(gThreads * 0.004 / 0.05), 1); const threadCosts = [hThreads * 1.7, wThreads1 * 1.75, gThreads * 1.75, wThreads2 * 1.75]; const pRam = ramNet.cloneBlocks(); let found; for (const cost of threadCosts) { found = false; for (const block of pRam) { if (block.ram < cost) continue; found = true; block.ram -= cost; break; } if (found) continue; break; } if (found) { metrics.greed = greed; metrics.threads = { hack: hThreads, weaken1: wThreads1, grow: gThreads, weaken2: wThreads2 }; return true; } } greed -= stepValue; } throw new Error("Not enough ram to run even a single batch. Something has gone seriously wrong."); }