NETSCRIPT: More ns Proxy changes (#297)

This commit is contained in:
David Walker 2023-01-05 17:41:24 -08:00 committed by GitHub
parent 3281b785ce
commit 4eef9eec03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 148 additions and 143 deletions

4
package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "bitburner",
"version": "2.2.0",
"version": "2.2.2dev",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "bitburner",
"version": "2.2.0",
"version": "2.2.2dev",
"hasInstallScript": true,
"license": "SEE LICENSE IN license.txt",
"dependencies": {

@ -1,7 +1,7 @@
{
"name": "bitburner",
"license": "SEE LICENSE IN license.txt",
"version": "2.2.0",
"version": "2.2.2dev",
"main": "electron-main.js",
"author": {
"name": "Daniel Xie, Olivier Gagnon, et al."

@ -3,18 +3,14 @@ import type { WorkerScript } from "./WorkerScript";
import { helpers } from "./NetscriptHelpers";
/** Permissive type for the documented API functions */
type APIFn = (...args: any[]) => void;
/** Type for the actual wrapped function given to the player */
type WrappedFn = (...args: unknown[]) => unknown;
type APIFn = (...args: any[]) => unknown;
/** Type for internal, unwrapped ctx function that produces an APIFunction */
type InternalFn<F extends APIFn> = (ctx: NetscriptContext) => ((...args: unknown[]) => ReturnType<F>) & F;
/** Type constraint for an API layer. They must all fit this "shape". */
type GenericAPI = { [key: string]: APIFn | GenericAPI };
// 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 APIFn ? WrappedFn : ExternalAPI<API[key]>;
};
export type InternalAPI<API> = {
[key in keyof API]: API[key] extends APIFn ? InternalFn<API[key]> : InternalAPI<API[key]>;
};
@ -25,70 +21,91 @@ export type NetscriptContext = {
functionPath: string;
};
export function NSProxy<API>(
class NSProxyHandler<API extends GenericAPI> {
ns: API;
ws: WorkerScript;
tree: string[];
additionalData: Record<string, unknown>;
memoed: API = {} as API;
constructor(ws: WorkerScript, ns: API, tree: string[], additionalData: Record<string, unknown>) {
this.ns = ns;
this.ws = ws;
this.tree = tree;
this.additionalData = additionalData;
Object.assign(this.memoed, additionalData);
}
has(__target: unknown, key: string): boolean {
return Reflect.has(this.ns, key) || Reflect.has(this.additionalData, key);
}
ownKeys(__target: unknown): (string | symbol)[] {
return [...Reflect.ownKeys(this.ns), ...Reflect.ownKeys(this.additionalData)];
}
getOwnPropertyDescriptor(__target: unknown, key: keyof API & string): PropertyDescriptor | undefined {
if (!this.has(__target, key)) return undefined;
return { value: this.get(__target, key, this), configurable: true, enumerable: true, writable: false };
}
defineProperty(__target: unknown, __key: unknown, __attrs: unknown): boolean {
throw new TypeError("ns instances are not modifiable!");
}
set(__target: unknown, __key: unknown, __attrs: unknown): boolean {
// Redundant with defineProperty, but we'll be explicit
throw new TypeError("ns instances are not modifiable!");
}
get(__target: unknown, key: keyof API & string, __receiver: any) {
const ours = this.memoed[key];
if (ours) return ours;
const field = this.ns[key];
if (!field) return field;
if (typeof field === "function") {
const arrayPath = [...this.tree, key];
const functionPath = arrayPath.join(".");
const ctx = { workerScript: this.ws, function: key, functionPath };
// Only do the context-binding once, instead of each time the function
// is called.
const func: any = field(ctx);
const wrappedFunction = function (...args: unknown[]): unknown {
// What remains *must* be called every time.
helpers.checkEnvFlags(ctx);
helpers.updateDynamicRam(ctx, getRamCost(...arrayPath));
return func(...args);
};
return ((this.memoed[key] as APIFn) = wrappedFunction);
}
if (typeof field === "object") {
return ((this.memoed[key] as GenericAPI) = NSProxy(this.ws, field as InternalAPI<GenericAPI>, [
...this.tree,
key,
]));
}
console.warn(`Unexpected data while wrapping API.`, "tree:", this.tree, "key:", key, "field:", field);
throw new Error("Error while wrapping netscript API. See console.");
}
}
export function NSProxy<API extends GenericAPI>(
ws: WorkerScript,
ns: InternalAPI<API>,
tree: string[],
additionalData?: Record<string, unknown>,
): ExternalAPI<API> {
const memoed: ExternalAPI<API> = Object.assign({} as ExternalAPI<API>, additionalData ?? {});
const handler = {
has(__target: unknown, key: string) {
return Reflect.has(ns, key);
},
ownKeys(__target: unknown) {
return Reflect.ownKeys(ns);
},
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!");
},
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];
if (!field) return field;
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 check, for removedFunction
helpers.checkEnvFlags(ctx);
helpers.updateDynamicRam(ctx, getRamCost(...tree, key));
return func(...args);
};
return ((memoed[key] as WrappedFn) = wrappedFunction);
}
if (typeof field === "object") {
// TODO unplanned: Make this work generically
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.");
},
};
additionalData: Record<string, unknown> = {},
): API {
const handler = new NSProxyHandler(ws, ns, tree, additionalData);
// We target an empty Object, so that unproxied methods don't do anything.
// We *can't* freeze the target, because it would break invariants on ownKeys.
return new Proxy({}, handler) as ExternalAPI<API>;
return new Proxy({}, handler) as API;
}
/** Specify when a function was removed from the game, and its replacement function. */
export function removedFunction(version: string, replacement: string, replaceMsg?: boolean) {
return (ctx: NetscriptContext) => {
return (ctx: NetscriptContext) => () => {
throw helpers.makeRuntimeErrorMsg(
ctx,
`Function removed in ${version}. ${replaceMsg ? replacement : `Please use ${replacement} instead.`}`,

@ -1,5 +1,4 @@
import { NSFull } from "../NetscriptFunctions";
import { ExternalAPI } from "./APIWrapper";
/**
* The environment in which a script runs. The environment holds
@ -14,5 +13,5 @@ export class Environment {
runningFn = "";
/** Environment variables (currently only Netscript functions) */
vars: ExternalAPI<NSFull> | null = null;
vars: NSFull | null = null;
}

@ -16,7 +16,6 @@ import { GetServer } from "../Server/AllServers";
import { BaseServer } from "../Server/BaseServer";
import { ScriptDeath } from "./ScriptDeath";
import { ScriptArg } from "./ScriptArg";
import { ExternalAPI } from "./APIWrapper";
import { NSFull } from "../NetscriptFunctions";
export class WorkerScript {
@ -84,11 +83,7 @@ export class WorkerScript {
/** Function called when the script ends. */
atExit?: () => void;
constructor(
runningScriptObj: RunningScript,
pid: number,
nsFuncsGenerator?: (ws: WorkerScript) => ExternalAPI<NSFull>,
) {
constructor(runningScriptObj: RunningScript, pid: number, nsFuncsGenerator?: (ws: WorkerScript) => NSFull) {
this.name = runningScriptObj.filename;
this.hostname = runningScriptObj.server;

@ -72,7 +72,7 @@ import { Flags } from "./NetscriptFunctions/Flags";
import { calculateIntelligenceBonus } from "./PersonObjects/formulas/intelligence";
import { CalculateShareMult, StartSharing } from "./NetworkShare/Share";
import { recentScripts } from "./Netscript/RecentScripts";
import { ExternalAPI, InternalAPI, removedFunction, NSProxy } from "./Netscript/APIWrapper";
import { InternalAPI, removedFunction, NSProxy } from "./Netscript/APIWrapper";
import { INetscriptExtra } from "./NetscriptFunctions/Extra";
import { ScriptDeath } from "./Netscript/ScriptDeath";
import { getBitNodeMultipliers } from "./BitNode/BitNode";
@ -1894,7 +1894,7 @@ Object.assign(ns, {
getServerRam: removedFunction("v2.2.0", "getServerMaxRam and getServerUsedRam"),
});
export function NetscriptFunctions(ws: WorkerScript): ExternalAPI<NSFull> {
export function NetscriptFunctions(ws: WorkerScript): NSFull {
return NSProxy(ws, ns, [], { args: ws.args.slice(), pid: ws.pid, enums });
}

@ -7,7 +7,7 @@ import { InternalAPI } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers";
import { Terminal } from "../Terminal";
export interface INetscriptExtra {
export type INetscriptExtra = {
heart: {
break(): number;
};
@ -18,7 +18,7 @@ export interface INetscriptExtra {
rainbow(guess: string): void;
iKnowWhatImDoing(): void;
printRaw(value: React.ReactNode): void;
}
};
export function NetscriptExtra(): InternalAPI<INetscriptExtra> {
return {

@ -83,11 +83,6 @@ 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[]) => {

@ -1,8 +1,7 @@
import { NSFull } from "../NetscriptFunctions";
import { ExternalAPI } from "../Netscript/APIWrapper";
import { AutocompleteData } from "@nsdefs";
export interface ScriptModule {
main?: (ns: ExternalAPI<NSFull>) => unknown;
main?: (ns: NSFull) => unknown;
autocomplete?: (data: AutocompleteData, flags: string[]) => unknown;
}

@ -1000,7 +1000,7 @@ interface NetscriptPort {
* Stock market API
* @public
*/
export interface TIX {
export type TIX = {
/**
* Returns true if the player has access to a WSE Account
* @remarks RAM cost: 0.05 GB
@ -1439,7 +1439,7 @@ export interface TIX {
* @returns True if you successfully purchased it or if you already have access, false otherwise.
*/
purchaseTixApi(): boolean;
}
};
/**
* Singularity API
@ -1448,7 +1448,7 @@ export interface TIX {
* Source-File 4 levels.
* @public
*/
export interface Singularity {
export type Singularity = {
/**
* Backup game save.
* @remarks
@ -2292,7 +2292,7 @@ export interface Singularity {
* @returns - An object representing the current work. Fields depend on the kind of work.
*/
getCurrentWork(): any | null;
}
};
/**
* Hacknet API
@ -2300,7 +2300,7 @@ export interface Singularity {
* Not all these functions are immediately available.
* @public
*/
export interface Hacknet {
export type Hacknet = {
/**
* Get the number of hacknet nodes you own.
* @remarks
@ -2651,7 +2651,7 @@ export interface Hacknet {
* @returns Multiplier.
*/
getTrainingMult(): number;
}
};
/**
* Bladeburner API
@ -2660,7 +2660,7 @@ export interface Hacknet {
* or have Source-File 7 in order to use this API.
* @public
*/
export interface Bladeburner {
export type Bladeburner = {
/**
* List all contracts.
* @remarks
@ -3148,13 +3148,13 @@ export interface Bladeburner {
*
* @returns whether player is a member of bladeburner division. */
inBladeburner(): boolean;
}
};
/**
* Coding Contract API
* @public
*/
export interface CodingContract {
export type CodingContract = {
/**
* Attempts a coding contract, returning a reward string on success or empty string on failure.
* @remarks
@ -3259,7 +3259,7 @@ export interface CodingContract {
* RAM cost: 2 GB
*/
getContractTypes(): string[];
}
};
/**
* Gang API
@ -3267,7 +3267,7 @@ export interface CodingContract {
* If you are not in BitNode-2, then you must have Source-File 2 in order to use this API.
* @public
*/
export interface Gang {
export type Gang = {
/**
* Create a gang.
* @remarks
@ -3526,7 +3526,7 @@ export interface Gang {
* @returns Bonus time for the Gang mechanic in milliseconds.
*/
getBonusTime(): number;
}
};
/**
* Sleeve API
@ -3534,7 +3534,7 @@ export interface Gang {
* If you are not in BitNode-10, then you must have Source-File 10 in order to use this API.
* @public
*/
export interface Sleeve {
export type Sleeve = {
/**
* Get the number of sleeves you own.
* @remarks
@ -3764,7 +3764,7 @@ export interface Sleeve {
* @returns True if the sleeve started working out, false otherwise.
*/
setToBladeburnerAction(sleeveNumber: number, action: string, contract?: string): boolean;
}
};
/**
* Grafting API
@ -3772,7 +3772,7 @@ export interface Sleeve {
* This API requires Source-File 10 to use.
* @public
*/
export interface Grafting {
export type Grafting = {
/**
* Retrieve the grafting cost of an aug.
* @remarks
@ -3819,13 +3819,13 @@ export interface Grafting {
* @throws Will error if called while you are not in New Tokyo.
*/
graftAugmentation(augName: string, focus?: boolean): boolean;
}
};
/**
* Skills formulas
* @public
*/
interface SkillsFormulas {
type SkillsFormulas = {
/**
* Calculate skill level.
* @param exp - experience for that skill
@ -3840,7 +3840,7 @@ interface SkillsFormulas {
* @returns The calculated exp required.
*/
calculateExp(skill: number, skillMult?: number): number;
}
};
/** @public */
interface WorkStats {
@ -3859,7 +3859,7 @@ interface WorkStats {
* Work formulas
* @public
*/
interface WorkFormulas {
type WorkFormulas = {
crimeSuccessChance(person: Person, crimeType: CrimeType | `${CrimeType}`): number;
/** @returns The WorkStats gained when completing one instance of the specified crime. */
crimeGains(person: Person, crimeType: CrimeType | `${CrimeType}`): WorkStats;
@ -3875,13 +3875,13 @@ interface WorkFormulas {
factionGains(person: Person, workType: FactionWorkType | `${FactionWorkType}`, favor: number): WorkStats;
/** @returns The WorkStats applied every game cycle (200ms) by performing the specified company work. */
companyGains(person: Person, companyName: string, workType: JobName | `${JobName}`, favor: number): WorkStats;
}
};
/**
* Reputation formulas
* @public
*/
interface ReputationFormulas {
type ReputationFormulas = {
/**
* Calculate the total required amount of faction reputation to reach a target favor.
* @param favor - target faction favor.
@ -3902,13 +3902,13 @@ interface ReputationFormulas {
* @param player - Player info from {@link NS.getPlayer | getPlayer}
*/
repFromDonation(amount: number, player: Person): number;
}
};
/**
* Hacking formulas
* @public
*/
interface HackingFormulas {
type HackingFormulas = {
/**
* Calculate hack chance.
* (Ex: 0.25 would indicate a 25% chance of success.)
@ -3968,13 +3968,13 @@ interface HackingFormulas {
* @returns The calculated weaken time.
*/
weakenTime(server: Server, player: Person): number;
}
};
/**
* Hacknet Node formulas
* @public
*/
interface HacknetNodesFormulas {
type HacknetNodesFormulas = {
/**
* Calculate money gain rate.
* @param level - level of the node.
@ -4020,13 +4020,13 @@ interface HacknetNodesFormulas {
* @returns An object with all hacknet node constants used by the game.
*/
constants(): HacknetNodeConstants;
}
};
/**
* Hacknet Server formulas
* @public
*/
interface HacknetServersFormulas {
type HacknetServersFormulas = {
/**
* Calculate hash gain rate.
* @param level - level of the server.
@ -4087,13 +4087,13 @@ interface HacknetServersFormulas {
* @returns An object with all hacknet server constants used by the game.
*/
constants(): HacknetServerConstants;
}
};
/**
* Gang formulas
* @public
*/
interface GangFormulas {
type GangFormulas = {
/**
* Calculate the wanted penalty.
* @param gang - Gang info from {@link Gang.getGangInformation | getGangInformation}
@ -4138,7 +4138,7 @@ interface GangFormulas {
* @returns The calculated ascension mult.
*/
ascensionMultiplier(points: number): number;
}
};
/**
* Formulas API
@ -4146,7 +4146,7 @@ interface GangFormulas {
* You need Formulas.exe on your home computer to use this API.
* @public
*/
export interface Formulas {
export type Formulas = {
mockServer(): Server;
mockPlayer(): Player;
mockPerson(): Person;
@ -4164,7 +4164,7 @@ export interface Formulas {
gang: GangFormulas;
/** Work formulas */
work: WorkFormulas;
}
};
/** @public */
interface Fragment {
@ -4189,7 +4189,7 @@ interface ActiveFragment {
* Stanek's Gift API.
* @public
*/
interface Stanek {
type Stanek = {
/**
* Stanek's Gift width.
* @remarks
@ -4295,7 +4295,7 @@ interface Stanek {
* false otherwise.
*/
acceptGift(): boolean;
}
};
/** @public */
interface InfiltrationReward {
@ -4321,7 +4321,7 @@ interface InfiltrationLocation {
* Infiltration API.
* @public
*/
interface Infiltration {
type Infiltration = {
/**
* Get all locations that can be infiltrated.
* @remarks
@ -4338,13 +4338,13 @@ interface Infiltration {
* @returns Infiltration data for given location.
*/
getInfiltration(location: string): InfiltrationLocation;
}
};
/**
* User Interface API.
* @public
*/
interface UserInterface {
type UserInterface = {
/**
* Get the current window size
* @remarks
@ -4427,7 +4427,7 @@ interface UserInterface {
* RAM cost: 0.2 GB
*/
clearTerminal(): void;
}
};
/**
* Collection of all functions passed to scripts
@ -4456,7 +4456,7 @@ interface UserInterface {
* {@link https://bitburner.readthedocs.io/en/latest/netscript/netscriptjs.html| ns2 in-game docs}
* <hr>
*/
export interface NS {
export type NS = {
/**
* Namespace for hacknet functions.
* @remarks RAM cost: 4 GB
@ -6853,7 +6853,7 @@ export interface NS {
getSharePower(): number;
enums: NSEnums;
}
};
// BASE ENUMS
/** @public */
@ -7073,7 +7073,7 @@ export type NSEnums = {
* @public
*/
export interface OfficeAPI {
export type OfficeAPI = {
/**
* Hire an employee.
* @param divisionName - Name of the division
@ -7165,7 +7165,7 @@ export interface OfficeAPI {
* @returns Cost of upgrading the office
*/
getOfficeSizeUpgradeCost(divisionName: string, city: CityName | `${CityName}`, asize: number): number;
}
};
/**
* Corporation Warehouse API
@ -7173,7 +7173,7 @@ export interface OfficeAPI {
* Requires the Warehouse API upgrade from your corporation.
* @public
*/
export interface WarehouseAPI {
export type WarehouseAPI = {
/**
* Set material sell data.
* @param divisionName - Name of the division
@ -7396,13 +7396,13 @@ export interface WarehouseAPI {
* @returns true if warehouse is present, false if not
*/
hasWarehouse(divisionName: string, city: CityName | `${CityName}`): boolean;
}
};
/**
* Corporation API
* @public
*/
export interface Corporation extends WarehouseAPI, OfficeAPI {
export type Corporation = {
/** Returns whether the player has a corporation. Does not require API access.
* @returns whether the player has a corporation */
hasCorporation(): boolean;
@ -7513,7 +7513,8 @@ export interface Corporation extends WarehouseAPI, OfficeAPI {
* Bonus time makes the game progress faster.
* @returns Bonus time for the Corporation mechanic in milliseconds. */
getBonusTime(): number;
}
} & WarehouseAPI &
OfficeAPI;
/** Product rating information
* @public */

@ -7,7 +7,7 @@ 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 { InternalAPI, NetscriptContext } from "src/Netscript/APIWrapper";
import { Singularity } from "@nsdefs";
type PotentiallyAsyncFunction = (arg?: unknown) => { catch?: PotentiallyAsyncFunction };
@ -20,15 +20,10 @@ function getFunction(fn: unknown) {
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");
throw new Error("Invalid ramcost: " + ramEntry);
}
function isRemovedFunction(fn: Function) {
try {
fn();
} catch {
return true;
}
return false;
function isRemovedFunction(ctx: NetscriptContext, fn: (ctx: NetscriptContext) => (...__: unknown[]) => unknown) {
return /REMOVED FUNCTION/.test(fn(ctx) + "");
}
describe("Netscript RAM Calculation/Generation Tests", function () {
@ -112,7 +107,7 @@ describe("Netscript RAM Calculation/Generation Tests", function () {
describe("ns", () => {
function testLayer<API>(
internalLayer: InternalAPI<API>,
externalLayer: ExternalAPI<API>,
externalLayer: API,
ramLayer: RamCostTree<API>,
path: string[],
extraLayerCost: number,
@ -122,18 +117,22 @@ describe("Netscript RAM Calculation/Generation Tests", function () {
const newPath = [...path, key as string];
if (typeof val === "function") {
// Removed functions have no ram cost and should be skipped.
if (isRemovedFunction(val)) return;
if (isRemovedFunction({ workerScript }, val)) return;
const fn = getFunction(externalLayer[key]);
const fnName = newPath.join(".");
if (!(key in ramLayer)) {
throw new Error("Missing ramcost for " + fnName);
}
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 {
else if (typeof val === "object" && key !== "enums") {
//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);
}
// Other things like args, enums, etc. have no cost
}
});
}
@ -149,7 +148,7 @@ describe("Netscript RAM Calculation/Generation Tests", function () {
const singObjects = (
Object.entries(ns.singularity) as [keyof Singularity, InternalAPI<Singularity>[keyof Singularity]][]
)
.filter(([_, v]) => typeof v === "function" && !isRemovedFunction(v))
.filter(([__, v]) => typeof v === "function" && !isRemovedFunction({ workerScript }, v))
.map(([name]) => {
return {
name,