diff --git a/src/Netscript/APIWrapper.ts b/src/Netscript/APIWrapper.ts new file mode 100644 index 000000000..d3df414c8 --- /dev/null +++ b/src/Netscript/APIWrapper.ts @@ -0,0 +1,153 @@ +import { getRamCost } from "./RamCostGenerator"; +import type { IPort } from "../NetscriptPort"; +import type { BaseServer } from "../Server/BaseServer"; +import type { WorkerScript } from "./WorkerScript"; +import { makeRuntimeRejectMsg } from "../NetscriptEvaluator"; +import { Player } from "../Player"; + +type ExternalFunction = (...args: any[]) => any; +type ExternalAPI = { + [string: string]: ExternalAPI | ExternalFunction; +}; + +type InternalFunction unknown> = (ctx: NetscriptContext) => F; +export type InternalAPI = { + [Property in keyof API]: API[Property] extends ExternalFunction + ? InternalFunction + : API[Property] extends ExternalAPI + ? InternalAPI + : never; +}; + +type WrappedNetscriptFunction = (...args: unknown[]) => unknown; +type WrappedNetscriptAPI = { + readonly [string: string]: WrappedNetscriptAPI | WrappedNetscriptFunction; +}; + +export type NetscriptContext = { + makeRuntimeErrorMsg: (message: string) => string; + log: (message: () => string) => void; + workerScript: WorkerScript; + function: string; + helper: WrappedNetscriptHelpers; +}; + +type NetscriptHelpers = { + updateDynamicRam: (fnName: string, ramCost: number) => void; + makeRuntimeErrorMsg: (caller: string, msg: string) => string; + string: (funcName: string, argName: string, v: unknown) => string; + number: (funcName: string, argName: string, v: unknown) => number; + boolean: (v: unknown) => boolean; + getServer: (hostname: string, callingFnName: string) => BaseServer; + checkSingularityAccess: (func: string) => void; + hack: (hostname: any, manual: any, { threads: requestedThreads, stock }?: any) => Promise; + getValidPort: (funcName: string, port: any) => IPort; +}; + +type WrappedNetscriptHelpers = { + makeRuntimeErrorMsg: (msg: string) => string; + string: (argName: string, v: unknown) => string; + number: (argName: string, v: unknown) => number; + boolean: (v: unknown) => boolean; + getServer: (hostname: string) => BaseServer; + checkSingularityAccess: () => void; + hack: (hostname: any, manual: any, { threads: requestedThreads, stock }?: any) => Promise; + getValidPort: (port: any) => IPort; +}; + +function wrapFunction( + helpers: NetscriptHelpers, + wrappedAPI: any, + workerScript: WorkerScript, + func: (_ctx: NetscriptContext) => (...args: unknown[]) => unknown, + ...tree: string[] +): void { + const functionPath = tree.join("."); + const functionName = tree.pop(); + if (typeof functionName !== "string") { + throw makeRuntimeRejectMsg(workerScript, "Failure occured while wrapping netscript api"); + } + const ctx = { + makeRuntimeErrorMsg: (message: string) => { + return helpers.makeRuntimeErrorMsg(functionPath, message); + }, + log: (message: () => string) => { + workerScript.log(functionPath, message); + }, + workerScript, + function: functionName, + helper: { + makeRuntimeErrorMsg: (msg: string) => helpers.makeRuntimeErrorMsg(functionPath, msg), + string: (argName: string, v: unknown) => helpers.string(functionPath, argName, v), + number: (argName: string, v: unknown) => helpers.number(functionPath, argName, v), + boolean: helpers.boolean, + getServer: (hostname: string) => helpers.getServer(hostname, functionPath), + checkSingularityAccess: () => helpers.checkSingularityAccess(functionName), + hack: helpers.hack, + getValidPort: (port: any) => helpers.getValidPort(functionPath, port), + }, + }; + function wrappedFunction(...args: unknown[]): unknown { + helpers.updateDynamicRam(ctx.function, getRamCost(Player, ...tree, ctx.function)); + return func(ctx)(...args); + } + const parent = getNestedProperty(wrappedAPI, ...tree); + Object.defineProperty(parent, functionName, { + value: wrappedFunction, + writable: true, + enumerable: true, + }); +} + +export function wrapAPI( + helpers: NetscriptHelpers, + wrappedAPI: ExternalAPI, + workerScript: WorkerScript, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + namespace: any, + ...tree: string[] +): WrappedNetscriptAPI { + if (typeof namespace !== "object") throw new Error("Invalid namespace?"); + for (const property of Object.getOwnPropertyNames(namespace)) { + switch (typeof namespace[property]) { + case "function": { + wrapFunction(helpers, wrappedAPI, workerScript, namespace[property], ...tree, property); + break; + } + case "object": { + wrapAPI(helpers, wrappedAPI, workerScript, namespace[property], ...tree, property); + break; + } + default: { + setNestedProperty(wrappedAPI, namespace[property], ...tree, property); + } + } + } + return wrappedAPI; +} + +function setNestedProperty(root: any, value: any, ...tree: string[]): any { + let target = root; + const key = tree.pop(); + if (typeof key !== "string") { + throw new Error("Failure occured while wrapping netscript api (setNestedProperty)"); + } + for (const branch of tree) { + if (target[branch] === undefined) { + target[branch] = {}; + } + target = target[branch]; + } + target[key] = value; +} + +function getNestedProperty(root: any, ...tree: string[]): any { + let target = root; + for (const branch of tree) { + if (target[branch] === undefined) { + target[branch] = {}; + } + target = target[branch]; + } + return target; +} diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts index bc204239a..0ced4545f 100644 --- a/src/NetscriptFunctions.ts +++ b/src/NetscriptFunctions.ts @@ -99,6 +99,7 @@ import { Flags } from "./NetscriptFunctions/Flags"; import { calculateIntelligenceBonus } from "./PersonObjects/formulas/intelligence"; import { CalculateShareMult, StartSharing } from "./NetworkShare/Share"; import { CityName } from "./Locations/data/CityNames"; +import { wrapAPI } from "./Netscript/APIWrapper"; interface NS extends INS { [key: string]: any; @@ -491,7 +492,8 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS { const sleeve = NetscriptSleeve(Player, workerScript, helper); const extra = NetscriptExtra(Player, workerScript, helper); const hacknet = NetscriptHacknet(Player, workerScript, helper); - const stanek = NetscriptStanek(Player, workerScript, helper); + const stanek = wrapAPI(helper, {}, workerScript, NetscriptStanek(Player, workerScript, helper), "stanek") + .stanek as unknown as IStanek; const bladeburner = NetscriptBladeburner(Player, workerScript, helper); const codingcontract = NetscriptCodingContract(Player, workerScript, helper); const corporation = NetscriptCorporation(Player, workerScript, helper); diff --git a/src/NetscriptFunctions/Stanek.ts b/src/NetscriptFunctions/Stanek.ts index 6c55e21bd..2137582ff 100644 --- a/src/NetscriptFunctions/Stanek.ts +++ b/src/NetscriptFunctions/Stanek.ts @@ -2,112 +2,112 @@ import { INetscriptHelper } from "./INetscriptHelper"; import { IPlayer } from "../PersonObjects/IPlayer"; import { WorkerScript } from "../Netscript/WorkerScript"; import { netscriptDelay } from "../NetscriptEvaluator"; -import { getRamCost } from "../Netscript/RamCostGenerator"; import { staneksGift } from "../CotMG/Helper"; import { Fragments, FragmentById } from "../CotMG/Fragment"; import { - Stanek as IStanek, Fragment as IFragment, ActiveFragment as IActiveFragment, + Stanek as IStanek, } from "../ScriptEditor/NetscriptDefinitions"; import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; +import { NetscriptContext, InternalAPI } from "src/Netscript/APIWrapper"; -export function NetscriptStanek(player: IPlayer, workerScript: WorkerScript, helper: INetscriptHelper): IStanek { +export function NetscriptStanek( + player: IPlayer, + workerScript: WorkerScript, + helper: INetscriptHelper, +): InternalAPI { function checkStanekAPIAccess(func: string): void { if (!player.hasAugmentation(AugmentationNames.StaneksGift1, true)) { helper.makeRuntimeErrorMsg(func, "Requires Stanek's Gift installed."); } } - const updateRam = (funcName: string): void => - helper.updateDynamicRam(funcName, getRamCost(player, "stanek", funcName)); - return { - giftWidth: function (): number { - updateRam("giftWidth"); - checkStanekAPIAccess("giftWidth"); - return staneksGift.width(); - }, - giftHeight: function (): number { - updateRam("giftHeight"); - checkStanekAPIAccess("giftHeight"); - return staneksGift.height(); - }, - chargeFragment: function (_rootX: unknown, _rootY: unknown): Promise { - updateRam("chargeFragment"); - const rootX = helper.number("stanek.chargeFragment", "rootX", _rootX); - const rootY = helper.number("stanek.chargeFragment", "rootY", _rootY); - checkStanekAPIAccess("chargeFragment"); - const fragment = staneksGift.findFragment(rootX, rootY); - if (!fragment) - throw helper.makeRuntimeErrorMsg("stanek.chargeFragment", `No fragment with root (${rootX}, ${rootY}).`); - const time = staneksGift.inBonus() ? 200 : 1000; - return netscriptDelay(time, workerScript).then(function () { - const charge = staneksGift.charge(player, fragment, workerScript.scriptRef.threads); - workerScript.log("stanek.chargeFragment", () => `Charged fragment for ${charge} charge.`); - return Promise.resolve(); - }); - }, - fragmentDefinitions: function (): IFragment[] { - updateRam("fragmentDefinitions"); - checkStanekAPIAccess("fragmentDefinitions"); - workerScript.log("stanek.fragmentDefinitions", () => `Returned ${Fragments.length} fragments`); - return Fragments.map((f) => f.copy()); - }, - activeFragments: function (): IActiveFragment[] { - updateRam("activeFragments"); - checkStanekAPIAccess("activeFragments"); - workerScript.log("stanek.activeFragments", () => `Returned ${staneksGift.fragments.length} fragments`); - return staneksGift.fragments.map((af) => { - return { ...af.copy(), ...af.fragment().copy() }; - }); - }, - clearGift: function (): void { - updateRam("clearGift"); - checkStanekAPIAccess("clearGift"); - workerScript.log("stanek.clearGift", () => `Cleared Stanek's Gift.`); - staneksGift.clear(); - }, - canPlaceFragment: function (_rootX: unknown, _rootY: unknown, _rotation: unknown, _fragmentId: unknown): boolean { - updateRam("canPlaceFragment"); - const rootX = helper.number("stanek.canPlaceFragment", "rootX", _rootX); - const rootY = helper.number("stanek.canPlaceFragment", "rootY", _rootY); - const rotation = helper.number("stanek.canPlaceFragment", "rotation", _rotation); - const fragmentId = helper.number("stanek.canPlaceFragment", "fragmentId", _fragmentId); - checkStanekAPIAccess("canPlaceFragment"); - const fragment = FragmentById(fragmentId); - if (!fragment) throw helper.makeRuntimeErrorMsg("stanek.canPlaceFragment", `Invalid fragment id: ${fragmentId}`); - const can = staneksGift.canPlace(rootX, rootY, rotation, fragment); - return can; - }, - placeFragment: function (_rootX: unknown, _rootY: unknown, _rotation: unknown, _fragmentId: unknown): boolean { - updateRam("placeFragment"); - const rootX = helper.number("stanek.placeFragment", "rootX", _rootX); - const rootY = helper.number("stanek.placeFragment", "rootY", _rootY); - const rotation = helper.number("stanek.placeFragment", "rotation", _rotation); - const fragmentId = helper.number("stanek.placeFragment", "fragmentId", _fragmentId); - checkStanekAPIAccess("placeFragment"); - const fragment = FragmentById(fragmentId); - if (!fragment) throw helper.makeRuntimeErrorMsg("stanek.placeFragment", `Invalid fragment id: ${fragmentId}`); - return staneksGift.place(rootX, rootY, rotation, fragment); - }, - getFragment: function (_rootX: unknown, _rootY: unknown): IActiveFragment | undefined { - updateRam("getFragment"); - const rootX = helper.number("stanek.getFragment", "rootX", _rootX); - const rootY = helper.number("stanek.getFragment", "rootY", _rootY); - checkStanekAPIAccess("getFragment"); - const fragment = staneksGift.findFragment(rootX, rootY); - if (fragment !== undefined) return fragment.copy(); - return undefined; - }, - removeFragment: function (_rootX: unknown, _rootY: unknown): boolean { - updateRam("removeFragment"); - const rootX = helper.number("stanek.removeFragment", "rootX", _rootX); - const rootY = helper.number("stanek.removeFragment", "rootY", _rootY); - checkStanekAPIAccess("removeFragment"); - return staneksGift.delete(rootX, rootY); - }, + giftWidth: (_ctx: NetscriptContext) => + function (): number { + checkStanekAPIAccess("giftWidth"); + return staneksGift.width(); + }, + giftHeight: (_ctx: NetscriptContext) => + function (): number { + checkStanekAPIAccess("giftHeight"); + return staneksGift.height(); + }, + chargeFragment: (_ctx: NetscriptContext) => + function (_rootX: unknown, _rootY: unknown): Promise { + const rootX = _ctx.helper.number("rootX", _rootX); + const rootY = _ctx.helper.number("rootY", _rootY); + checkStanekAPIAccess("chargeFragment"); + const fragment = staneksGift.findFragment(rootX, rootY); + if (!fragment) throw _ctx.makeRuntimeErrorMsg(`No fragment with root (${rootX}, ${rootY}).`); + const time = staneksGift.inBonus() ? 200 : 1000; + return netscriptDelay(time, workerScript).then(function () { + const charge = staneksGift.charge(player, fragment, workerScript.scriptRef.threads); + _ctx.log(() => `Charged fragment for ${charge} charge.`); + return Promise.resolve(); + }); + }, + fragmentDefinitions: (_ctx: NetscriptContext) => + function (): IFragment[] { + checkStanekAPIAccess("fragmentDefinitions"); + _ctx.log(() => `Returned ${Fragments.length} fragments`); + return Fragments.map((f) => f.copy()); + }, + activeFragments: (_ctx: NetscriptContext) => + function (): IActiveFragment[] { + checkStanekAPIAccess("activeFragments"); + _ctx.log(() => `Returned ${staneksGift.fragments.length} fragments`); + return staneksGift.fragments.map((af) => { + return { ...af.copy(), ...af.fragment().copy() }; + }); + }, + clearGift: (_ctx: NetscriptContext) => + function (): void { + checkStanekAPIAccess("clearGift"); + _ctx.log(() => `Cleared Stanek's Gift.`); + staneksGift.clear(); + }, + canPlaceFragment: (_ctx: NetscriptContext) => + function (_rootX: unknown, _rootY: unknown, _rotation: unknown, _fragmentId: unknown): boolean { + const rootX = _ctx.helper.number("rootX", _rootX); + const rootY = _ctx.helper.number("rootY", _rootY); + const rotation = _ctx.helper.number("rotation", _rotation); + const fragmentId = _ctx.helper.number("fragmentId", _fragmentId); + checkStanekAPIAccess("canPlaceFragment"); + const fragment = FragmentById(fragmentId); + if (!fragment) throw _ctx.makeRuntimeErrorMsg(`Invalid fragment id: ${fragmentId}`); + const can = staneksGift.canPlace(rootX, rootY, rotation, fragment); + return can; + }, + placeFragment: (_ctx: NetscriptContext) => + function (_rootX: unknown, _rootY: unknown, _rotation: unknown, _fragmentId: unknown): boolean { + const rootX = _ctx.helper.number("rootX", _rootX); + const rootY = _ctx.helper.number("rootY", _rootY); + const rotation = _ctx.helper.number("rotation", _rotation); + const fragmentId = _ctx.helper.number("fragmentId", _fragmentId); + checkStanekAPIAccess("placeFragment"); + const fragment = FragmentById(fragmentId); + if (!fragment) throw _ctx.makeRuntimeErrorMsg(`Invalid fragment id: ${fragmentId}`); + return staneksGift.place(rootX, rootY, rotation, fragment); + }, + getFragment: (_ctx: NetscriptContext) => + function (_rootX: unknown, _rootY: unknown): IActiveFragment | undefined { + const rootX = _ctx.helper.number("rootX", _rootX); + const rootY = _ctx.helper.number("rootY", _rootY); + checkStanekAPIAccess("getFragment"); + const fragment = staneksGift.findFragment(rootX, rootY); + if (fragment !== undefined) return fragment.copy(); + return undefined; + }, + removeFragment: (_ctx: NetscriptContext) => + function (_rootX: unknown, _rootY: unknown): boolean { + const rootX = _ctx.helper.number("rootX", _rootX); + const rootY = _ctx.helper.number("rootY", _rootY); + checkStanekAPIAccess("removeFragment"); + return staneksGift.delete(rootX, rootY); + }, }; }