* handle enums differently
* Enums are frozen and fed directly to the proxy
* Enums are not included in the NSFull definition, allowing samekeys for RamCostTree<API>, InternalAPI<API>, and ExternalAPI<API>
* 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.)
* Fixed an overview issue where the bars could display inaccurately.
Update changelog and bump version to 2.2.1
This commit is contained in:
Snarling 2023-01-04 09:23:20 -05:00 committed by omuretsu
parent 5f18b87323
commit 31bf0c43d1
8 changed files with 124 additions and 132 deletions

@ -3,20 +3,25 @@
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.
* Fixed an issue where the overview skill bars could be displayed inaccurately based on player multipliers.
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 +29,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 +71,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:

@ -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,26 @@ 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.
* Fixed an issue where the overview skill bars could be displayed inaccurately based on player multipliers.
v2.2.0 - Jan 2 2023 Development Reboot
@ -253,8 +262,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 +305,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"

@ -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<string, Record<string, string>>;
/** 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<F extends APIFn> = (ctx: NetscriptContext) => ((...args: unknown[]) => ReturnType<F>) & 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<API> = {
[key in keyof API]: API[key] extends Enums
? Enums
: key extends "args"
? ScriptArg[] // "args" required to be ScriptArg[]
: API[key] extends APIFn
? WrappedFn
: ExternalAPI<API[key]>;
[key in keyof API]: API[key] extends APIFn ? WrappedFn : ExternalAPI<API[key]>;
};
export type InternalAPI<API> = {
[key in keyof API]: API[key] extends Enums
? API[key] & Enums
: key extends "args"
? ScriptArg[]
: API[key] extends APIFn
? InternalFn<API[key]>
: InternalAPI<API[key]>;
[key in keyof API]: API[key] extends APIFn ? InternalFn<API[key]> : InternalAPI<API[key]>;
};
export type NetscriptContext = {
@ -54,40 +40,41 @@ export function NSProxy<API>(
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<unknown>, [...tree, key]));
return ((memoed[key] as ExternalAPI<API[keyof API]>) = NSProxy(ws, field as InternalAPI<API[keyof API]>, [
...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.");

@ -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<API> = Omit<
{
[Property in keyof API]: API[Property] extends () => unknown ? number | (() => number) : RamCostTree<API[Property]>;
},
"enums"
>;
/** The API does not include enums, args, or pid. */
export type RamCostTree<API> = {
[key in keyof API]: API[key] extends () => unknown ? number | (() => number) : RamCostTree<API[key]>;
};
/** Constants for assigning costs to ns functions */
export const RamCostConstants = {

@ -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<Omit<NS & INetscriptExtra, "pid" | "args">>;
export type NSFull = Readonly<Omit<NS & INetscriptExtra, "pid" | "args" | "enums">>;
export const ns: InternalAPI<NSFull> = {
enums,
singularity: NetscriptSingularity(),
gang: NetscriptGang(),
bladeburner: NetscriptBladeburner(),
@ -1894,7 +1895,7 @@ Object.assign(ns, {
});
export function NetscriptFunctions(ws: WorkerScript): ExternalAPI<NSFull> {
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]));

@ -83,6 +83,11 @@ async function startNetscript1Script(workerScript: WorkerScript): Promise<void>
type BasicObject = Record<string, any>;
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[]) => {

@ -75,11 +75,11 @@ const formattedVals: Record<RowName, () => string> = {
const skillMultUpdaters: Record<SkillRowName, () => number> = {
//Used by skill bars to calculate the mult
Hack: () => Player.mults.hacking * BitNodeMultipliers.HackingLevelMultiplier,
Str: () => Player.mults.hacking * BitNodeMultipliers.HackingLevelMultiplier,
Def: () => Player.mults.hacking * BitNodeMultipliers.HackingLevelMultiplier,
Dex: () => Player.mults.hacking * BitNodeMultipliers.HackingLevelMultiplier,
Agi: () => Player.mults.hacking * BitNodeMultipliers.HackingLevelMultiplier,
Cha: () => Player.mults.hacking * BitNodeMultipliers.HackingLevelMultiplier,
Str: () => Player.mults.strength * BitNodeMultipliers.StrengthLevelMultiplier,
Def: () => Player.mults.defense * BitNodeMultipliers.DefenseLevelMultiplier,
Dex: () => Player.mults.dexterity * BitNodeMultipliers.DexterityLevelMultiplier,
Agi: () => Player.mults.agility * BitNodeMultipliers.AgilityLevelMultiplier,
Cha: () => Player.mults.charisma * BitNodeMultipliers.CharismaLevelMultiplier,
Int: () => 1,
};

@ -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<API>(ramEntry: RamCostTree<API>[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<API>(
internalLayer: InternalAPI<API>,
externalLayer: ExternalAPI<API>,
ramLayer: RamCostTree<API>,
path: string[],
extraLayerCost: number,
) {
describe(path[path.length - 1] ?? "Base ns layer", () => {
for (const [key, val] of Object.entries(internalLayer) as [keyof API, InternalAPI<API>[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<unknown>, 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<Singularity>[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<Singularity>(ramCostSingularity[name]),
};
});
const lvlToMult: Record<number, number> = { 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);
});
});
}
});