diff --git a/src/Netscript/RamCostGenerator.ts b/src/Netscript/RamCostGenerator.ts index 280c3b417..2a8fa9dd7 100644 --- a/src/Netscript/RamCostGenerator.ts +++ b/src/Netscript/RamCostGenerator.ts @@ -1,19 +1,12 @@ import { Player } from "../Player"; +import { NSFull } from "../NetscriptFunctions"; -import { NS as INS } from "../ScriptEditor/NetscriptDefinitions"; -import { INetscriptExtra } from "../NetscriptFunctions/Extra"; - +/** This type assumes any value that isn't an API layer or a function has been omitted (args and enum) */ type RamCostTree = { - [Property in keyof API]: API[Property] extends () => void - ? number | (() => void) - : API[Property] extends object - ? RamCostTree - : never; + [Property in keyof API]: API[Property] extends Function ? number | (() => number) : RamCostTree; }; -// TODO remember to update RamCalculations.js and WorkerScript.js - -// RAM costs for Netscript functions +/** Constants for assigning costs to ns functions */ export const RamCostConstants: Record = { ScriptBaseRamCost: 1.6, ScriptDomRamCost: 25, @@ -120,7 +113,7 @@ const hacknet = { getHashUpgradeLevel: 0, getStudyMult: 0, getTrainingMult: 0, -}; +} as const; // Stock API const stock = { @@ -149,7 +142,7 @@ const stock = { purchase4SMarketDataTixApi: RamCostConstants.ScriptBuySellStockRamCost, purchaseWseAccount: RamCostConstants.ScriptBuySellStockRamCost, purchaseTixApi: RamCostConstants.ScriptBuySellStockRamCost, -}; +} as const; // Singularity API const singularity = { @@ -208,7 +201,7 @@ const singularity = { b1tflum3: SF4Cost(16), destroyW0r1dD43m0n: SF4Cost(32), getCurrentWork: SF4Cost(0.5), -}; +} as const; // Gang API const gang = { @@ -233,7 +226,7 @@ const gang = { setTerritoryWarfare: RamCostConstants.ScriptGangApiBaseRamCost / 2, getChanceToWinClash: RamCostConstants.ScriptGangApiBaseRamCost, getBonusTime: 0, -}; +} as const; // Bladeburner API const bladeburner = { @@ -272,12 +265,12 @@ const bladeburner = { joinBladeburnerFaction: RamCostConstants.ScriptBladeburnerApiBaseRamCost, joinBladeburnerDivision: RamCostConstants.ScriptBladeburnerApiBaseRamCost, getBonusTime: 0, -}; +} as const; const infiltration = { getPossibleLocations: RamCostConstants.ScriptInfiltrationGetLocations, getInfiltration: RamCostConstants.ScriptInfiltrationGetInfiltrations, -}; +} as const; // Coding Contract API const codingcontract = { @@ -286,7 +279,7 @@ const codingcontract = { getData: RamCostConstants.ScriptCodingContractBaseRamCost / 2, getDescription: RamCostConstants.ScriptCodingContractBaseRamCost / 2, getNumTriesRemaining: RamCostConstants.ScriptCodingContractBaseRamCost / 5, -}; +} as const; // Duplicate Sleeve API const sleeve = { @@ -308,7 +301,7 @@ const sleeve = { setToBladeburnerAction: RamCostConstants.ScriptSleeveBaseRamCost, getSleeveAugmentationPrice: RamCostConstants.ScriptSleeveBaseRamCost, getSleeveAugmentationRepReq: RamCostConstants.ScriptSleeveBaseRamCost, -}; +} as const; // Stanek API const stanek = { @@ -323,7 +316,7 @@ const stanek = { getFragment: RamCostConstants.ScriptStanekFragmentAt, removeFragment: RamCostConstants.ScriptStanekDeleteAt, acceptGift: RamCostConstants.ScriptStanekAcceptGift, -}; +} as const; // UI API const ui = { @@ -336,7 +329,7 @@ const ui = { getGameInfo: 0, clearTerminal: 0, windowSize: 0, -}; +} as const; // Grafting API const grafting = { @@ -344,7 +337,7 @@ const grafting = { getAugmentationGraftTime: 3.75, getGraftableAugmentations: 5, graftAugmentation: 7.5, -}; +} as const; const corporation = { getMaterialNames: 0, @@ -412,16 +405,17 @@ const corporation = { hasResearched: 0, setAutoJobAssignment: 0, getOfficeSizeUpgradeCost: 0, -}; +} as const; -const SourceRamCosts = { - args: undefined as unknown as never[], // special use case - enums: undefined as unknown as never, +/** RamCosts guaranteed to match ns structure 1:1 (aside from args and enums). + * An error will be generated if there are missing OR additional ram costs defined. + * To avoid errors, define every function in NetscriptDefinition.d.ts and NetscriptFunctions, + * and have a ram cost associated here. */ +export const RamCosts: RamCostTree> = { corporation, hacknet, stock, singularity, - ...singularity, // singularity is in namespace & toplevel gang, bladeburner, infiltration, @@ -601,13 +595,7 @@ const SourceRamCosts = { factionGains: 0, }, }, -}; - -export const RamCosts: Record = SourceRamCosts; - -// This line in particular is there so typescript typechecks that we are not missing any static ram cost. -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const _typecheck: RamCostTree = SourceRamCosts; +} as const; export function getRamCost(...args: string[]): number { if (args.length === 0) { @@ -615,7 +603,7 @@ export function getRamCost(...args: string[]): number { return 0; } - let curr = RamCosts[args[0]]; + let curr = RamCosts[args[0] as keyof typeof RamCosts]; for (let i = 1; i < args.length; ++i) { if (curr == null) { console.warn(`Invalid function passed to getRamCost: ${args}`); @@ -627,7 +615,7 @@ export function getRamCost(...args: string[]): number { break; } - curr = curr[args[i]]; + curr = curr[args[i] as keyof typeof curr]; } if (typeof curr === "number") { diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts index da7bf0f45..a163fca9f 100644 --- a/src/ScriptEditor/NetscriptDefinitions.d.ts +++ b/src/ScriptEditor/NetscriptDefinitions.d.ts @@ -4408,54 +4408,55 @@ export interface NS { * @remarks RAM cost: 4 GB */ readonly hacknet: Hacknet; + /** - * * Namespace for bladeburner functions. * @remarks RAM cost: 0 GB */ readonly bladeburner: Bladeburner; + /** - * * Namespace for codingcontract functions. * @remarks RAM cost: 0 GB */ readonly codingcontract: CodingContract; + /** - * * Namespace for gang functions. * @remarks RAM cost: 0 GB */ readonly gang: Gang; + /** - * * Namespace for sleeve functions. * @remarks RAM cost: 0 GB */ readonly sleeve: Sleeve; + /** - * * Namespace for stock functions. - * @remarks - * RAM cost: 0 GB + * @remarks RAM cost: 0 GB */ readonly stock: TIX; + /** - * * Namespace for formulas functions. - * @remarks - * RAM cost: 0 GB + * @remarks RAM cost: 0 GB */ readonly formulas: Formulas; + /** * Namespace for stanek functions. * RAM cost: 0 GB */ readonly stanek: Stanek; + /** * Namespace for infiltration functions. * RAM cost: 0 GB */ readonly infiltration: Infiltration; + /** * Namespace for corporation functions. * RAM cost: 1022.4 GB @@ -4476,8 +4477,7 @@ export interface NS { /** * Namespace for grafting functions. - * @remarks - * RAM cost: 0 GB + * @remarks RAM cost: 0 GB */ readonly grafting: Grafting; diff --git a/test/jest/Netscript/RamCalculation.test.ts b/test/jest/Netscript/RamCalculation.test.ts index 47672676d..569abec7b 100644 --- a/test/jest/Netscript/RamCalculation.test.ts +++ b/test/jest/Netscript/RamCalculation.test.ts @@ -42,20 +42,7 @@ describe("Netscript RAM Calculation/Generation Tests", function () { }; const ns = NetscriptFunctions(workerScript as WorkerScript); - /** - * Tests that: - * 1. A function has non-zero RAM cost, or zero if it is flagged as zero cost. - * 2. Running the function properly updates the MockWorkerScript's dynamic RAM calculation - * 3. Running multiple calls of the function does not result in additional RAM cost - * @param {string[]} fnDesc - describes the name of the function being tested, - * including the namespace(s). e.g. ["gang", "getMemberNames"] - */ - function testDynamicRamCost(fnDesc: string[], zero: boolean = false) { - const expected = getRamCost(...fnDesc); - zero ? expect(expected).toEqual(0) : expect(expected).toBeGreaterThan(0); - } - - function dynamicCheck(fnPath: string[], expectedRamCost: number) { + function dynamicCheck(fnPath: string[], expectedRamCost: number, extraLayerCost = 0) { const code = `${fnPath.join(".")}();\n`.repeat(3); const fnName = fnPath[fnPath.length - 1]; @@ -82,10 +69,10 @@ describe("Netscript RAM Calculation/Generation Tests", function () { expect(workerScript.dynamicLoadedFns).toHaveProperty(fnName); expect(workerScript.dynamicRamUsage - ScriptBaseCost).toBeCloseTo(expectedRamCost, 5); - expect(workerScript.dynamicRamUsage).toBeCloseTo(runningScript.ramUsage, 5); + expect(workerScript.dynamicRamUsage).toBeCloseTo(runningScript.ramUsage - extraLayerCost, 5); } - function testFunctionExpectZero(fnPath: string[]) { + function testFunctionExpectZero(fnPath: string[], extraLayerCost = 0) { const wholeFn = `${fnPath.join(".")}()`; describe(wholeFn, () => { it("Zero Ram Static Check", () => { @@ -93,13 +80,13 @@ describe("Netscript RAM Calculation/Generation Tests", function () { expect(ramCost).toEqual(0); const code = wholeFn; const staticCost = calculateRamUsage(code, []).cost; - expect(staticCost).toEqual(ScriptBaseCost); + expect(staticCost).toEqual(ScriptBaseCost + extraLayerCost); }); - it("Zero Ram Dynamic check", () => dynamicCheck(fnPath, 0)); + it("Zero Ram Dynamic check", () => dynamicCheck(fnPath, 0, extraLayerCost)); }); } - function testFunctionExpectNonzero(fnPath: string[]) { + function testFunctionExpectNonzero(fnPath: string[], extraLayerCost = 0) { const wholeFn = `${fnPath.join(".")}()`; const ramCost = getRamCost(...fnPath); describe(wholeFn, () => { @@ -107,13 +94,15 @@ describe("Netscript RAM Calculation/Generation Tests", function () { expect(ramCost).toBeGreaterThan(0); const code = wholeFn; const staticCost = calculateRamUsage(code, []).cost; - expect(staticCost).toBeCloseTo(ramCost + ScriptBaseCost, 5); + expect(staticCost).toBeCloseTo(ramCost + ScriptBaseCost + extraLayerCost, 5); }); - it("Dynamic Check", () => dynamicCheck(fnPath, ramCost)); + it("Dynamic Check", () => dynamicCheck(fnPath, ramCost, extraLayerCost)); }); } + //input type for testSingularityFunctions type singularityData = { fnPath: string[]; baseCost: number }; + function testSingularityFunctions(data: singularityData[]) { const sf4 = Player.sourceFiles[0]; data.forEach(({ fnPath, baseCost }) => { @@ -154,64 +143,89 @@ describe("Netscript RAM Calculation/Generation Tests", function () { [key: string]: NSLayer | unknown[] | (() => unknown); }; type RamLayer = { - [key: string]: number | (() => number); + [key: string]: RamLayer | number | (() => number); }; - function testLayer(nsLayer: NSLayer, ramLayer: RamLayer, path: string[]) { + function testLayer(nsLayer: NSLayer, ramLayer: RamLayer, path: string[], extraLayerCost = 0) { const zeroCostFunctions = Object.entries(nsLayer) .filter(([key, val]) => ramLayer[key] === 0 && typeof val === "function") .map(([key]) => [...path, key]); - zeroCostFunctions.forEach(testFunctionExpectZero); + zeroCostFunctions.forEach((fn) => testFunctionExpectZero(fn, extraLayerCost)); const nonzeroCostFunctions = Object.entries(nsLayer) .filter(([key, val]) => ramLayer[key] > 0 && typeof val === "function") .map(([key]) => [...path, key]); - nonzeroCostFunctions.forEach(testFunctionExpectNonzero); + nonzeroCostFunctions.forEach((fn) => testFunctionExpectNonzero(fn, extraLayerCost)); } - describe("Top level ns functions", function () { + describe("Top level ns functions", () => { const nsScope = ns as unknown as NSLayer; - const ramScope = RamCosts as unknown as RamLayer; + const ramScope = RamCosts; testLayer(nsScope, ramScope, []); }); - describe("TIX API (stock) functions", function () { - const nsScope = ns.stock as unknown as NSLayer; - const ramScope = RamCosts.stock as unknown as RamLayer; - testLayer(nsScope, ramScope, ["stock"]); - }); - - describe("Bladeburner API (bladeburner) functions", function () { + describe("Bladeburner API (bladeburner) functions", () => { const nsScope = ns.bladeburner as unknown as NSLayer; - const ramScope = RamCosts.bladeburner as unknown as RamLayer; + const ramScope = RamCosts.bladeburner; testLayer(nsScope, ramScope, ["bladeburner"]); }); - describe("Gang API (gang) functions", function () { + describe("Corporation API (corporation) functions", () => { + const nsScope = ns.corporation as unknown as NSLayer; + const ramScope = RamCosts.corporation; + testLayer(nsScope, ramScope, ["corporation"], 1024 - ScriptBaseCost); + }); + + describe("TIX API (stock) functions", () => { + const nsScope = ns.stock as unknown as NSLayer; + const ramScope = RamCosts.stock; + testLayer(nsScope, ramScope, ["stock"]); + }); + + describe("Gang API (gang) functions", () => { const nsScope = ns.gang as unknown as NSLayer; - const ramScope = RamCosts.gang as unknown as RamLayer; + const ramScope = RamCosts.gang; testLayer(nsScope, ramScope, ["gang"]); }); - describe("Coding Contract API (codingcontract) functions", function () { + describe("Coding Contract API (codingcontract) functions", () => { const nsScope = ns.codingcontract as unknown as NSLayer; - const ramScope = RamCosts.codingcontract as unknown as RamLayer; + const ramScope = RamCosts.codingcontract; testLayer(nsScope, ramScope, ["codingcontract"]); }); - describe("Sleeve API (sleeve) functions", function () { + describe("Sleeve API (sleeve) functions", () => { const nsScope = ns.sleeve as unknown as NSLayer; - const ramScope = RamCosts.sleeve as unknown as RamLayer; + const ramScope = RamCosts.sleeve; testLayer(nsScope, ramScope, ["sleeve"]); }); //Singularity functions are tested in a different way because they also check SF4 level effect - describe("ns.singularity functions", function () { - const singularityFunctions = Object.entries(RamCosts.singularity).map(([key, val]) => { + describe("ns.singularity functions", () => { + const singularityFunctions = Object.entries(RamCosts.singularity).map(([fnName, ramFn]) => { return { - fnPath: ["singularity", key], - baseCost: (val as () => number)(), + fnPath: ["singularity", fnName], + // This will error if a singularity function is assigned a flat cost instead of using the SF4 function + baseCost: (ramFn as () => number)(), }; }); testSingularityFunctions(singularityFunctions); }); + + //Formulas requires deeper layer penetration + function formulasTest( + newLayer = "formulas", + oldNSLayer = ns as unknown as NSLayer, + oldRamLayer = RamCosts as unknown as RamLayer, + path = ["formulas"], + nsLayer = oldNSLayer[newLayer] as NSLayer, + ramLayer = oldRamLayer[newLayer] as RamLayer, + ) { + testLayer(nsLayer, ramLayer, path); + for (const [key, val] of Object.entries(nsLayer)) { + if (Array.isArray(val) || typeof val === "function" || key === "enums") continue; + // Only other option is an object / new layer + describe(key, () => formulasTest(key, nsLayer, ramLayer, [...path, key])); + } + } + describe("ns.formulas functions", formulasTest); });