From b90849e9fdbdaeb1091490f3470d42fa12b9cfb6 Mon Sep 17 00:00:00 2001 From: Snarling <84951833+Snarling@users.noreply.github.com> Date: Wed, 4 Jan 2023 09:23:20 -0500 Subject: [PATCH] v2.2.1 (#293) * handle enums differently * Enums are frozen and fed directly to the proxy * Enums are not included in the NSFull definition, allowing samekeys for RamCostTree, InternalAPI, and ExternalAPI * Rewrote a lot of the ramcalc test, with better typing thanks to the samekeys above * Fix ns1 for proxy (args, pid, and enums after above changes were not being added to ns1 scripts.) Update changelog and bump version to 2.2.1 --- doc/source/changelog.rst | 19 +-- src/Constants.ts | 27 +++-- src/Netscript/APIWrapper.ts | 47 +++----- src/Netscript/RamCostGenerator.ts | 11 +- src/NetscriptFunctions.ts | 7 +- src/NetscriptWorker.ts | 5 + test/jest/Netscript/RamCalculation.test.ts | 127 ++++++++++----------- 7 files changed, 116 insertions(+), 127 deletions(-) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 472293f1b..9d6b9b6dd 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -3,20 +3,24 @@ Changelog ========= -v2.2.0 - Jan 2 2023 Development Reboot --------------------------------------- +v2.2.1 Hotfixes +--------------- -Hotfixes: +Hotfix / bugfix: +* (@d0sboots) Implemented a new API wrapping solution that prevents the need for binding functions to ns when placing them in a new variable, but maintains and perhaps improves upon the performance gains from the previous v2.2.0 changes. * Fixed some issues with savegames failing to load, or causing the main engine loop to stall after load. * Fixed an issue where .script files were not receiving the correct args when ran * Fixed an issue with sleeve HP calculation * Possible fix for MathJax "Typesetting Failed" errors -* The Faction Work XP fix listed below was also added during hotfixes. -* There was an issue with Corporations decaying their employees to 0 stats, even though the minimum was supposed to be 5. Fixed, and raised minimum to 10. +* There was an issue with Corporations decaying their employees to 0 stats, even though the minimum was supposed to be 5. Moved the variable storing the min decay value to corporation constants, and raised it to 10. * Regenerated documentation at https://github.com/bitburner-official/bitburner-src/blob/dev/markdown/bitburner.ns.md due to corporation changes related to min decay stats. +* Faction XP was unintentionally providing 20x the experience gain as it did prior to v2.0. This caused faction work to exceed gym/university as the optimal way to gain experience. Values have been reduced to only about 2x what they were prior to v2.0, and they are no longer better than gym/university. +v2.2.0 - Jan 2 2023 Development Reboot +-------------------------------------- + Dev notes * The previous main developer, hydroflame, is stepping back from this project for the foreseeable future. To facilitate this, we've moved the repo to a new location at https://github.com/bitburner-official/bitburner-src. @@ -24,7 +28,7 @@ Dev notes BREAKING API CHANGES: -* (ns2 only) ns functions use the 'this' value from ns: if you move the function to its own variable off of ns, it needs to be bound to ns. The internal changes that make this necessary led to very large performance gains for running many scripts at once. e.g.: +* No longer applicable as of v2.2.1! (ns2 only) ns functions use the 'this' value from ns: if you move the function to its own variable off of ns, it needs to be bound to ns. The internal changes that make this necessary led to very large performance gains for running many scripts at once. e.g.: const tprint1 = ns.tprint; // This doesn't work and will error out when calling tprint1(); @@ -66,9 +70,6 @@ TUTORIAL * Removed NS1/NS2 selection. Tutorial now only references .js files (NS1 is essentially deprecated) (@Mughur) * Fix Ram Text (by @jaculler) -FACTION WORK -* Faction XP was unintentionally providing 20x the experience gain as it did prior to v2.0. This caused faction work to exceed gym/university as the optimal way to gain experience. Values have been reduced to only about 2x what they were prior to v2.0, and they are no longer better than gym/university. - NETSCRIPT * Base NS API: diff --git a/src/Constants.ts b/src/Constants.ts index d43d82624..4d61ec2b3 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -88,8 +88,8 @@ export const CONSTANTS: { Donations: number; // number of blood/plasma/palette donation the dev have verified., boosts NFG LatestUpdate: string; } = { - VersionString: "2.2.0", - VersionNumber: 28, + VersionString: "2.2.1", + VersionNumber: 29, // Speed (in ms) at which the main loop is updated _idleSpeed: 200, @@ -232,17 +232,24 @@ export const CONSTANTS: { Donations: 41, LatestUpdate: ` - v2.2.0 Hotfixes: - + v2.2.1 Hotfixes + + Hotfix / bugfix: + * (@d0sboots) Implemented a new API wrapping solution that prevents the need for binding functions to ns when placing + them in a new variable, but maintains and perhaps improves upon the performance gains from the previous v2.2.0 + changes. * Fixed some issues with savegames failing to load, or causing the main engine loop to stall after load. * Fixed an issue where .script files were not receiving the correct args when ran * Fixed an issue with sleeve HP calculation * Possible fix for MathJax "Typesetting Failed" errors - * The Faction Work XP fix listed below was also added during hotfixes. * There was an issue with Corporations decaying their employees to 0 stats, even though the minimum was supposed to be 5. Moved the variable storing the min decay value to corporation constants, and raised it to 10. * Regenerated documentation at https://github.com/bitburner-official/bitburner-src/blob/dev/markdown/bitburner.ns.md due to corporation changes related to min decay stats. + * Faction XP was unintentionally providing 20x the experience gain as it did prior to v2.0. This caused faction work + to exceed gym/university as the optimal way to gain experience. Values have been reduced to only about 2x what + they were prior to v2.0, and they are no longer better than gym/university. + v2.2.0 - Jan 2 2023 Development Reboot @@ -253,8 +260,9 @@ export const CONSTANTS: { removed functions will provide an error guiding you to the new replacement function to use instead. BREAKING API CHANGES: - * (ns2 only) ns functions use the 'this' value from ns: if you move the function to its own variable off of ns, it - needs to be bound to ns. e.g.: + * No longer applicable as of v2.2.1! + Prior to v2.2.1: (ns2 only) ns functions use the 'this' value from ns: if you move the function to its own + variable off of ns, it needs to be bound to ns. e.g.: const tprint1 = ns.tprint; // This doesn't work and will error out when calling tprint1(); const tprint = ns.tprint.bind(ns); // This works because the 'this' value is preserved. The internal changes that make this necessary led to very large performance gains for running many scripts at once. @@ -295,11 +303,6 @@ export const CONSTANTS: { * Removed NS1/NS2 selection. Tutorial now only references .js files (NS1 is essentially deprecated) (@Mughur) * Fix Ram Text (by @jaculler) - FACTION WORK - * Faction XP was unintentionally providing 20x the experience gain as it did prior to v2.0. This caused faction work - to exceed gym/university as the optimal way to gain experience. Values have been reduced to only about 2x what - they were prior to v2.0, and they are no longer better than gym/university. - NETSCRIPT * Added ns.pid property to access a script's PID without a function call. (@jeek) * Much faster API wrapping on script launch. (@d0sboots) To support this, ns functions need to keep their "this" diff --git a/src/Netscript/APIWrapper.ts b/src/Netscript/APIWrapper.ts index fa40b04c2..ac0e5d78c 100644 --- a/src/Netscript/APIWrapper.ts +++ b/src/Netscript/APIWrapper.ts @@ -1,11 +1,7 @@ import { getRamCost } from "./RamCostGenerator"; import type { WorkerScript } from "./WorkerScript"; import { helpers } from "./NetscriptHelpers"; -import { ScriptArg } from "./ScriptArg"; -import { cloneDeep } from "lodash"; -/** Generic type for an enums object */ -type Enums = Record>; /** Permissive type for the documented API functions */ type APIFn = (...args: any[]) => void; /** Type for the actual wrapped function given to the player */ @@ -13,24 +9,14 @@ type WrappedFn = (...args: unknown[]) => unknown; /** Type for internal, unwrapped ctx function that produces an APIFunction */ type InternalFn = (ctx: NetscriptContext) => ((...args: unknown[]) => ReturnType) & F; +// args, enums, and pid are excluded from the API for typing purposes via the definition of NSFull. +// They do in fact exist on the external API (but are absent on the internal API and ramcost tree) export type ExternalAPI = { - [key in keyof API]: API[key] extends Enums - ? Enums - : key extends "args" - ? ScriptArg[] // "args" required to be ScriptArg[] - : API[key] extends APIFn - ? WrappedFn - : ExternalAPI; + [key in keyof API]: API[key] extends APIFn ? WrappedFn : ExternalAPI; }; export type InternalAPI = { - [key in keyof API]: API[key] extends Enums - ? API[key] & Enums - : key extends "args" - ? ScriptArg[] - : API[key] extends APIFn - ? InternalFn - : InternalAPI; + [key in keyof API]: API[key] extends APIFn ? InternalFn : InternalAPI; }; export type NetscriptContext = { @@ -54,40 +40,41 @@ export function NSProxy( ownKeys(__target: unknown) { return Reflect.ownKeys(ns); }, - getOwnPropertyDescriptor(__target: unknown, key: string) { + getOwnPropertyDescriptor(__target: unknown, key: keyof API & string) { if (!Reflect.has(ns, key)) return undefined; return { value: this.get(__target, key, this), configurable: true, enumerable: true, writable: false }; }, defineProperty(__target: unknown, __key: unknown, __attrs: unknown) { throw new TypeError("ns instances are not modifiable!"); }, - get(__target: unknown, key: string, __receiver: any) { - const ours = memoed[key as keyof API]; + set(__target: unknown, __key: unknown, __attrs: unknown) { + throw new TypeError("ns instances are not modifiable!"); + }, + get(__target: unknown, key: keyof API & string, __receiver: any) { + const ours = memoed[key]; if (ours) return ours; - const field = ns[key as keyof API]; + const field = ns[key]; if (!field) return field; - if (key === "enums") { - const enumObj = Object.freeze(cloneDeep(field as Enums)); - for (const member of Object.values(enumObj)) Object.freeze(member); - return ((memoed[key as keyof API] as Enums) = enumObj); - } if (typeof field === "function") { const arrayPath = [...tree, key]; const functionPath = arrayPath.join("."); const wrappedFunction = function (...args: unknown[]): unknown { const ctx = { workerScript: ws, function: key, functionPath }; - const func = field(ctx); //Allows throwing before ram chack + const func = field(ctx); //Allows throwing before ram check, for removedFunction helpers.checkEnvFlags(ctx); helpers.updateDynamicRam(ctx, getRamCost(...tree, key)); return func(...args); }; - return ((memoed[key as keyof API] as WrappedFn) = wrappedFunction); + return ((memoed[key] as WrappedFn) = wrappedFunction); } if (typeof field === "object") { // TODO unplanned: Make this work generically - return ((memoed[key as keyof API] as unknown) = NSProxy(ws, field as InternalAPI, [...tree, key])); + return ((memoed[key] as ExternalAPI) = NSProxy(ws, field as InternalAPI, [ + ...tree, + key, + ])); } console.warn(`Unexpected data while wrapping API.`, "tree:", tree, "key:", key, "field:", field); throw new Error("Error while wrapping netscript API. See console."); diff --git a/src/Netscript/RamCostGenerator.ts b/src/Netscript/RamCostGenerator.ts index f7d1e3bd5..d66f9b345 100644 --- a/src/Netscript/RamCostGenerator.ts +++ b/src/Netscript/RamCostGenerator.ts @@ -1,13 +1,10 @@ import { Player } from "@player"; import { NSFull } from "../NetscriptFunctions"; -/** This type assumes any value that isn't an API layer or a function has been omitted (enum) */ -type RamCostTree = Omit< - { - [Property in keyof API]: API[Property] extends () => unknown ? number | (() => number) : RamCostTree; - }, - "enums" ->; +/** The API does not include enums, args, or pid. */ +export type RamCostTree = { + [key in keyof API]: API[key] extends () => unknown ? number | (() => number) : RamCostTree; +}; /** Constants for assigning costs to ns functions */ export const RamCostConstants = { diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts index f049bb565..edd9529fd 100644 --- a/src/NetscriptFunctions.ts +++ b/src/NetscriptFunctions.ts @@ -91,11 +91,12 @@ export const enums: NSEnums = { ToastVariant, UniversityClassType, }; +for (const val of Object.values(enums)) Object.freeze(val); +Object.freeze(enums); -export type NSFull = Readonly>; +export type NSFull = Readonly>; export const ns: InternalAPI = { - enums, singularity: NetscriptSingularity(), gang: NetscriptGang(), bladeburner: NetscriptBladeburner(), @@ -1894,7 +1895,7 @@ Object.assign(ns, { }); export function NetscriptFunctions(ws: WorkerScript): ExternalAPI { - return NSProxy(ws, ns, [], { args: ws.args.slice(), pid: ws.pid }); + return NSProxy(ws, ns, [], { args: ws.args.slice(), pid: ws.pid, enums }); } const possibleLogs = Object.fromEntries([...getFunctionNames(ns, "")].map((a) => [a, true])); diff --git a/src/NetscriptWorker.ts b/src/NetscriptWorker.ts index 95b048862..cf9189db7 100644 --- a/src/NetscriptWorker.ts +++ b/src/NetscriptWorker.ts @@ -83,6 +83,11 @@ async function startNetscript1Script(workerScript: WorkerScript): Promise type BasicObject = Record; const wrappedNS = NetscriptFunctions(workerScript); function wrapNS1Layer(int: Interpreter, intLayer: unknown, nsLayer = wrappedNS as BasicObject) { + if (nsLayer === wrappedNS) { + int.setProperty(intLayer, "args", int.nativeToPseudo(nsLayer.args)); + int.setProperty(intLayer, "enums", int.nativeToPseudo(nsLayer.enums)); + int.setProperty(intLayer, "pid", nsLayer.pid); + } for (const [name, entry] of Object.entries(nsLayer)) { if (typeof entry === "function") { const wrapper = async (...args: unknown[]) => { diff --git a/test/jest/Netscript/RamCalculation.test.ts b/test/jest/Netscript/RamCalculation.test.ts index e06acf73c..6baf6c9d0 100644 --- a/test/jest/Netscript/RamCalculation.test.ts +++ b/test/jest/Netscript/RamCalculation.test.ts @@ -1,24 +1,26 @@ import { Player } from "../../../src/Player"; import { NetscriptFunctions } from "../../../src/NetscriptFunctions"; -import { RamCosts, getRamCost, RamCostConstants } from "../../../src/Netscript/RamCostGenerator"; +import { RamCosts, getRamCost, RamCostConstants, RamCostTree } from "../../../src/Netscript/RamCostGenerator"; import { Environment } from "../../../src/Netscript/Environment"; import { RunningScript } from "../../../src/Script/RunningScript"; import { Script } from "../../../src/Script/Script"; import { WorkerScript } from "../../../src/Netscript/WorkerScript"; import { calculateRamUsage } from "../../../src/Script/RamCalculations"; +import { ns } from "../../../src/NetscriptFunctions"; +import { ExternalAPI, InternalAPI } from "src/Netscript/APIWrapper"; +import { Singularity } from "@nsdefs"; type PotentiallyAsyncFunction = (arg?: unknown) => { catch?: PotentiallyAsyncFunction }; -type NSLayer = { - [key: string]: NSLayer | PotentiallyAsyncFunction; -}; -type RamLayer = { - [key: string]: number | (() => number) | RamLayer; -}; -function grabCost(ramLayer: RamLayer, fullPath: string[]) { - const ramEntry = ramLayer[fullPath[fullPath.length - 1]]; - const expectedRam = typeof ramEntry === "function" ? ramEntry() : ramEntry; - if (typeof expectedRam !== "number") throw new Error(`There was no defined ram cost for ${fullPath.join(".")}().`); - return expectedRam; + +/** Get a potentiallyAsyncFunction from a layer of the external ns */ +function getFunction(fn: unknown) { + if (typeof fn !== "function") throw new Error("Expected a function at this location."); + return fn as PotentiallyAsyncFunction; +} +function grabCost(ramEntry: RamCostTree[keyof API]) { + if (typeof ramEntry === "function") return ramEntry(); + if (typeof ramEntry === "number") return ramEntry; + throw new Error("Invalid ramcost"); } function isRemovedFunction(fn: Function) { try { @@ -63,7 +65,7 @@ describe("Netscript RAM Calculation/Generation Tests", function () { ramUsage: scriptRef.ramUsage, scriptRef, }; - const ns = NetscriptFunctions(workerScript as WorkerScript); + const nsExternal = NetscriptFunctions(workerScript as WorkerScript); function combinedRamCheck( fn: PotentiallyAsyncFunction, @@ -91,7 +93,7 @@ describe("Netscript RAM Calculation/Generation Tests", function () { env: new Environment(), dynamicLoadedFns: {}, }); - workerScript.env.vars = ns; + workerScript.env.vars = nsExternal; // Run the function through the workerscript's args if (typeof fn === "function") { @@ -108,67 +110,60 @@ describe("Netscript RAM Calculation/Generation Tests", function () { } describe("ns", () => { - Object.entries(NetscriptFunctions(workerScript) as unknown as NSLayer).forEach(([key, val]) => { - if (key === "args" || key === "enums") return; - if (typeof val === "function") { - // Removed functions have no ram cost and should be skipped. - if (isRemovedFunction(val)) return; - const expectedRam = grabCost(RamCosts, [key]); - it(`${key}()`, () => combinedRamCheck(val.bind(ns), [key], expectedRam)); - } - //The only other option should be an NSLayer - const extraLayerCost = { hacknet: 4 }[key] ?? 0; // Currently only hacknet has a layer cost. - testLayer(val as NSLayer, RamCosts[key as keyof typeof RamCosts] as RamLayer, [key], extraLayerCost); - }); + function testLayer( + internalLayer: InternalAPI, + externalLayer: ExternalAPI, + ramLayer: RamCostTree, + path: string[], + extraLayerCost: number, + ) { + describe(path[path.length - 1] ?? "Base ns layer", () => { + for (const [key, val] of Object.entries(internalLayer) as [keyof API, InternalAPI[keyof API]][]) { + const newPath = [...path, key as string]; + if (typeof val === "function") { + // Removed functions have no ram cost and should be skipped. + if (isRemovedFunction(val)) return; + const fn = getFunction(externalLayer[key]); + const fnName = newPath.join("."); + const expectedRam = grabCost(ramLayer[key]); + it(`${fnName}()`, () => combinedRamCheck(fn, newPath, expectedRam, extraLayerCost)); + } + //A layer should be the only other option. Hacknet is currently the only layer with a layer cost. + else { + //hacknet is currently the only layer with a layer cost. + const layerCost = key === "hacknet" ? 4 : 0; + testLayer(val as InternalAPI, externalLayer[key], ramLayer[key], newPath, layerCost); + } + } + }); + } + testLayer(ns, nsExternal, RamCosts, [], 0); }); - function testLayer(nsLayer: NSLayer, ramLayer: RamLayer, path: string[], extraLayerCost: number) { - // nsLayer is the layer on the main, unstamped wrappedNS object. The actualLayer is needed to check correct stamping. - const actualLayer = path.reduce((prev, curr) => prev[curr], ns as any); //todo: do this typesafely? - describe(path[path.length - 1], () => { - Object.entries(nsLayer).forEach(([key, val]) => { - const newPath = [...path, key]; - if (typeof val === "function") { - // Removed functions have no ram cost and should be skipped. - if (isRemovedFunction(val)) return; - const fnName = newPath.join("."); - const expectedRam = grabCost(ramLayer, newPath); - it(`${fnName}()`, () => combinedRamCheck(val.bind(actualLayer), newPath, expectedRam, extraLayerCost)); - } - //Skip enums layers - else if (key === "enums") return; - //A layer should be the only other option. - else testLayer(val, ramLayer[key] as RamLayer, newPath, 0); - }); - }); - } - describe("Singularity multiplier checks", () => { + // Checks were already done above for SF4.3 having normal ramcost. sf4.lvl = 3; - const singFunctions = Object.entries(NetscriptFunctions(workerScript).singularity).filter( - ([__, val]) => typeof val === "function", - ); - const singObjects = singFunctions - .filter((fn) => !isRemovedFunction(fn)) - .map(([key, val]) => { + const lvlToMult = { 0: 16, 1: 16, 2: 4 }; + const externalSingularity = nsExternal.singularity; + const ramCostSingularity = RamCosts.singularity; + const singObjects = ( + Object.entries(ns.singularity) as [keyof Singularity, InternalAPI[keyof Singularity]][] + ) + .filter(([_, v]) => typeof v === "function" && !isRemovedFunction(v)) + .map(([name]) => { return { - name: key, - fn: val.bind(ns.singularity), - baseRam: grabCost(RamCosts.singularity, ["singularity", key]), + name, + baseRam: grabCost(ramCostSingularity[name]), }; }); - const lvlToMult: Record = { 0: 16, 1: 16, 2: 4 }; - for (const lvl of [0, 1, 2]) { + for (const lvl of [0, 1, 2] as const) { it(`SF4.${lvl} check for x${lvlToMult[lvl]} costs`, () => { sf4.lvl = lvl; - singObjects.forEach((obj) => - combinedRamCheck( - obj.fn as PotentiallyAsyncFunction, - ["singularity", obj.name], - obj.baseRam * lvlToMult[lvl], - 0, - ), - ); + const expectedMult = lvlToMult[lvl]; + singObjects.forEach(({ name, baseRam }) => { + const fn = getFunction(externalSingularity[name]); + combinedRamCheck(fn, ["singularity", name], baseRam * expectedMult); + }); }); } });