From 7d43117acfe8d35b6d0184d6bac3aa57d30d0361 Mon Sep 17 00:00:00 2001 From: Alain Bryden <2285037+alainbryden@users.noreply.github.com> Date: Fri, 8 Nov 2024 23:36:50 -0400 Subject: [PATCH] Fix ram-dodging of "Map" values. - Lean into jsonReplacer as a necessary evil to support ram-dodging fancy new types being used by the NS API - make run-command more concise for more deeply-nested objects --- helpers.js | 30 +++++++++++++++++++++--------- run-command.js | 4 +++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/helpers.js b/helpers.js index 146781a3..3720861f 100644 --- a/helpers.js +++ b/helpers.js @@ -209,7 +209,7 @@ export async function getNsDataThroughFile_Custom(ns, fnRun, command, fName = nu // If an error occurs, it will write an empty file to avoid old results being misread. const commandToFile = `let r;try{r=JSON.stringify(\n` + ` ${command}\n` + - `);}catch(e){r="ERROR: "+(typeof e=='string'?e:e?.message??JSON.stringify(e));}\n` + + `, jsonReplacer);}catch(e){r="ERROR: "+(typeof e=='string'?e:e?.message??JSON.stringify(e));}\n` + `const f="${fName}"; if(ns.read(f)!==r) ns.write(f,r,'w')`; // Run the command with auto-retries if it fails const pid = await runCommand_Custom(ns, fnRun, commandToFile, fNameCommand, args, verbose, maxRetries, retryDelayMs, silent); @@ -240,6 +240,11 @@ export function jsonReplacer(key, value) { type: 'bigint', value: value.toString() }; + } else if (value instanceof Map) { + return { + dataType: 'Map', + value: Array.from(value.entries()), + }; } else { return value; } @@ -247,8 +252,13 @@ export function jsonReplacer(key, value) { /** Allows us to deserialize special values created by the above jsonReplacer */ export function jsonReviver(key, value) { - if (value && value.type == 'bigint') - return BigInt(value.value); + if (typeof value === 'object' && value !== null) { + if (value && value.type == 'bigint') + return BigInt(value.value); + else if (value.dataType === 'Map') { + return new Map(value.value); + } + } return value; } @@ -389,7 +399,9 @@ export async function waitForProcessToComplete_Custom(ns, fnIsAlive, pid, verbos /** If the argument is an Error instance, returns it as is, otherwise, returns a new Error instance. */ function asError(error) { - return error instanceof Error ? error : new Error(typeof error === 'string' ? error : JSON.stringify(error)); + return error instanceof Error ? error : + new Error(typeof error === 'string' ? error : + JSON.stringify(error, jsonReplacer)); // TODO: jsonReplacer to support ScriptDeath objects and other custom Bitburner throws } /** Helper to retry something that failed temporarily (can happen when e.g. we temporarily don't have enough RAM to run) @@ -530,20 +542,20 @@ export async function getActiveSourceFiles(ns, includeLevelsFromCurrentBitnode = export async function getActiveSourceFiles_Custom(ns, fnGetNsDataThroughFile, includeLevelsFromCurrentBitnode = true, silent = true) { checkNsInstance(ns, '"getActiveSourceFiles"'); // Find out what source files the user has unlocked - let dictSourceFiles = (/**@returns{{[bitNodeN: number]: number;}}*/() => { currentNode: 0 })(); + let dictSourceFiles = (/**@returns{{[bitNodeN: number]: number;}}*/() => null)(); try { dictSourceFiles = await fnGetNsDataThroughFile(ns, `Object.fromEntries(ns.singularity.getOwnedSourceFiles().map(sf => [sf.n, sf.lvl]))`, '/Temp/getOwnedSourceFiles-asDict.txt', null, null, null, null, silent); } catch { } // If this fails (e.g. presumably due to low RAM or no singularity access), default to an empty dictionary - //ns.tprint(JSON.stringify(dictSourceFiles)); + dictSourceFiles ??= {}; // Try to get reset info - let resetInfo = (/**@returns{ResetInfo}*/() => { currentNode: 0 })(); + let resetInfo = (/**@returns{ResetInfo}*/() => null)(); try { resetInfo = await fnGetNsDataThroughFile(ns, 'ns.getResetInfo()', null, null, null, null, null, silent); } catch { } // As above, suppress any errors and use a fall-back to survive low ram conditions. - //ns.tprint(JSON.stringify(resetInfo)); + resetInfo ??= { currentNode: 0 } // If the user is currently in a given bitnode, they will have its features unlocked. Include these "effective" levels if requested; if (includeLevelsFromCurrentBitnode && resetInfo.currentNode != 0) { @@ -555,7 +567,7 @@ export async function getActiveSourceFiles_Custom(ns, fnGetNsDataThroughFile, in // If any bitNodeOptions were set, it might reduce our source file levels for gameplay purposes, // but the game currently has a bug where getOwnedSourceFiles won't reflect this, so we must do it ourselves. - if ((resetInfo?.bitNodeOptions?.sourceFileOverrides?.length ?? 0) > 0) { + if ((resetInfo?.bitNodeOptions?.sourceFileOverrides?.size ?? 0) > 0) { resetInfo.bitNodeOptions.sourceFileOverrides.forEach((sfLevel, bn) => dictSourceFiles[bn] = sfLevel); // Completely remove keys whose override level is 0 Object.keys(dictSourceFiles).filter(bn => dictSourceFiles[bn] == 0).forEach(bn => delete dictSourceFiles[bn]); diff --git a/run-command.js b/run-command.js index 2168604e..51ba721e 100644 --- a/run-command.js +++ b/run-command.js @@ -31,7 +31,9 @@ export async function main(ns) { command = `{ ${command} }`; } // Wrapping the command in a lambda that can capture and print its output. - command = `ns.tprint(JSON.stringify(await (async () => ${command})() ?? "(no output)", null, 2))`; + command = `ns.tprint(JSON.stringify(await (async () => ${command})() ?? "(no output)", jsonReplacer, 2)` + + // While we're using pretty formatting, "condence" formatting for any objects nested more than 2 layers deep + `.replaceAll(/\\n +/gi,""))`; await ns.write(`/Temp/terminal-command.js`, "", "w"); // Clear the previous command file to avoid a warning about re-using temp script names. This is the one exception. return await runCommand(ns, command, `/Temp/terminal-command.js`, (escaped ? args.slice(1) : undefined), !silent); } \ No newline at end of file