diff --git a/src/Achievements/Achievements.ts b/src/Achievements/Achievements.ts index 81c535eb3..b32a6e1f2 100644 --- a/src/Achievements/Achievements.ts +++ b/src/Achievements/Achievements.ts @@ -66,11 +66,11 @@ function bitNodeFinishedState(): boolean { } function hasAccessToSF(player: PlayerObject, bn: number): boolean { - return player.bitNodeN === bn || player.sourceFiles.some((a) => a.n === bn); + return player.bitNodeN === bn || player.sourceFileLvl(bn) > 0; } function knowsAboutBitverse(player: PlayerObject): boolean { - return player.sourceFiles.some((a) => a.n === 1); + return player.sourceFiles.size > 0; } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -274,7 +274,7 @@ export const achievements: Record = { NS2: { ...achievementData["NS2"], Icon: "ns2", - Condition: () => Player.getHomeComputer().scripts.some((s) => s.filename.endsWith(".js")), + Condition: () => [...Player.getHomeComputer().scripts.values()].some((s) => s.filename.endsWith(".js")), }, FROZE: { ...achievementData["FROZE"], @@ -317,7 +317,7 @@ export const achievements: Record = { SCRIPTS_30: { ...achievementData["SCRIPTS_30"], Icon: "folders", - Condition: () => Player.getHomeComputer().scripts.length >= 30, + Condition: () => Player.getHomeComputer().scripts.size >= 30, }, KARMA_1000000: { ...achievementData["KARMA_1000000"], @@ -342,7 +342,7 @@ export const achievements: Record = { SCRIPT_32GB: { ...achievementData["SCRIPT_32GB"], Icon: "bigcost", - Condition: () => Player.getHomeComputer().scripts.some((s) => (s.ramUsage ?? 0) >= 32), + Condition: () => [...Player.getHomeComputer().scripts.values()].some((s) => (s.ramUsage ?? 0) >= 32), }, FIRST_HACKNET_NODE: { ...achievementData["FIRST_HACKNET_NODE"], diff --git a/src/Augmentation/ui/OwnedSourceFiles.tsx b/src/Augmentation/ui/OwnedSourceFiles.tsx deleted file mode 100644 index 54d434f20..000000000 --- a/src/Augmentation/ui/OwnedSourceFiles.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/** - * React Component for displaying a list of the player's Source-Files - * on the Augmentations UI - */ -import * as React from "react"; - -import { Player } from "@player"; -import { Settings } from "../../Settings/Settings"; -import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums"; -import { SourceFiles } from "../../SourceFile/SourceFiles"; - -import { SourceFileAccordion } from "../../ui/React/SourceFileAccordion"; - -export function OwnedSourceFiles(): React.ReactElement { - const sourceSfs = Player.sourceFiles.slice(); - - if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) { - sourceSfs.sort((sf1, sf2) => { - return sf1.n - sf2.n; - }); - } - - return ( - <> - {sourceSfs.map((e) => { - const srcFileKey = "SourceFile" + e.n; - const sfObj = SourceFiles[srcFileKey]; - if (sfObj == null) { - console.error(`Invalid source file number: ${e.n}`); - return null; - } - - return ; - })} - - ); -} diff --git a/src/Augmentation/ui/SourceFiles.tsx b/src/Augmentation/ui/SourceFiles.tsx index e969b46b2..a791918cc 100644 --- a/src/Augmentation/ui/SourceFiles.tsx +++ b/src/Augmentation/ui/SourceFiles.tsx @@ -71,27 +71,24 @@ const getMaxLevel = (sfObj: SourceFile | SfMinus1): string | number => { }; export function SourceFilesElement(): React.ReactElement { - const sourceSfs = Player.sourceFiles.slice(); + const sourceFilesCopy = new Map(Player.sourceFiles); const exploits = Player.exploits; // Create a fake SF for -1, if "owned" if (exploits.length > 0) { - sourceSfs.unshift({ - n: -1, - lvl: exploits.length, - }); + sourceFilesCopy.set(-1, exploits.length); } + const sfList = [...sourceFilesCopy]; if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) { - sourceSfs.sort((sf1, sf2) => { - return sf1.n - sf2.n; - }); + sfList.sort(([n1, __lvl1], [n2, __lvl2]) => n1 - n2); } - if (sourceSfs.length === 0) { + if (sfList.length === 0) { return <>; } - const [selectedSf, setSelectedSf] = useState(sourceSfs[0]); + const firstEle = sfList[0]; + const [selectedSf, setSelectedSf] = useState({ n: firstEle[0], lvl: firstEle[1] }); return ( @@ -104,8 +101,8 @@ export function SourceFilesElement(): React.ReactElement { sx={{ height: 400, overflowY: "scroll", borderRight: `1px solid ${Settings.theme.welllight}` }} disablePadding > - {sourceSfs.map((e, i) => { - const sfObj = safeGetSf(e.n); + {sfList.map(([n, lvl], i) => { + const sfObj = safeGetSf(n); if (!sfObj) return; const maxLevel = getMaxLevel(sfObj); @@ -113,8 +110,8 @@ export function SourceFilesElement(): React.ReactElement { return ( setSelectedSf(e)} - selected={selectedSf.n === e.n} + onClick={() => setSelectedSf({ n, lvl })} + selected={selectedSf.n === n} sx={{ py: 0 }} > {sfObj.name}} secondary={ - Level {e.lvl} / {maxLevel} + Level {lvl} / {maxLevel} } /> diff --git a/src/DevMenu/ui/SourceFiles.tsx b/src/DevMenu/ui/SourceFiles.tsx index ac8dfb3d2..c3e5bc7f8 100644 --- a/src/DevMenu/ui/SourceFiles.tsx +++ b/src/DevMenu/ui/SourceFiles.tsx @@ -7,7 +7,6 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; -import { PlayerOwnedSourceFile } from "../../SourceFile/PlayerOwnedSourceFile"; import { Player } from "@player"; import ButtonGroup from "@mui/material/ButtonGroup"; @@ -21,20 +20,10 @@ export function SourceFiles(): React.ReactElement { Player.hacknetNodes = []; } if (sfLvl === 0) { - Player.sourceFiles = Player.sourceFiles.filter((sf) => sf.n !== sfN); + Player.sourceFiles.delete(sfN); return; } - - if (!Player.sourceFiles.some((sf) => sf.n === sfN)) { - Player.sourceFiles.push(new PlayerOwnedSourceFile(sfN, sfLvl)); - return; - } - - for (let i = 0; i < Player.sourceFiles.length; i++) { - if (Player.sourceFiles[i].n === sfN) { - Player.sourceFiles[i].lvl = sfLvl; - } - } + Player.sourceFiles.set(sfN, sfLvl); }; } diff --git a/src/Diagnostic/FileDiagnosticModal.tsx b/src/Diagnostic/FileDiagnosticModal.tsx index 02c0a9696..d21cab4cb 100644 --- a/src/Diagnostic/FileDiagnosticModal.tsx +++ b/src/Diagnostic/FileDiagnosticModal.tsx @@ -15,16 +15,17 @@ import Accordion from "@mui/material/Accordion"; import AccordionSummary from "@mui/material/AccordionSummary"; import AccordionDetails from "@mui/material/AccordionDetails"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { ServerName } from "../Types/strings"; interface IServerProps { - hostname: string; + hostname: ServerName; } function ServerAccordion(props: IServerProps): React.ReactElement { const server = GetServer(props.hostname); if (server === null) throw new Error(`server '${props.hostname}' should not be null`); let totalSize = 0; - for (const f of server.scripts) { + for (const f of server.scripts.values()) { totalSize += f.code.length; } @@ -43,7 +44,7 @@ function ServerAccordion(props: IServerProps): React.ReactElement { const files: File[] = []; - for (const f of server.scripts) { + for (const f of server.scripts.values()) { files.push({ name: f.filename, size: f.code.length }); } diff --git a/src/Electron.tsx b/src/Electron.tsx index d919bb4f9..055e395aa 100644 --- a/src/Electron.tsx +++ b/src/Electron.tsx @@ -75,7 +75,7 @@ function initWebserver(): void { return { res: true, data: { - files: home.scripts.map((script) => ({ + files: [...home.scripts.values()].map((script) => ({ filename: script.filename, code: script.code, ramUsage: script.ramUsage, diff --git a/src/Locations/ui/SpecialLocation.tsx b/src/Locations/ui/SpecialLocation.tsx index dd358e2e8..ebf9d0b86 100644 --- a/src/Locations/ui/SpecialLocation.tsx +++ b/src/Locations/ui/SpecialLocation.tsx @@ -93,7 +93,7 @@ export function SpecialLocation(props: IProps): React.ReactElement { function EatNoodles(): void { SnackbarEvents.emit("You ate some delicious noodles and feel refreshed", ToastVariant.SUCCESS, 2000); N00dles(); // This is the true power of the noodles. - if (Player.sourceFiles.length > 0) Player.giveExploit(Exploit.N00dles); + if (Player.sourceFiles.size > 0) Player.giveExploit(Exploit.N00dles); if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) { Player.exp.intelligence *= 1.0000000000000002; } diff --git a/src/Message/MessageHelpers.tsx b/src/Message/MessageHelpers.tsx index d70defb95..4f854575d 100644 --- a/src/Message/MessageHelpers.tsx +++ b/src/Message/MessageHelpers.tsx @@ -80,7 +80,7 @@ function checkForMessagesToSend(): void { } //If the daemon can be hacked, send the player icarus.msg if (Player.skills.hacking >= worldDaemon.requiredHackingSkill) { - sendMessage(redpill, Player.sourceFiles.length === 0); + sendMessage(redpill, Player.sourceFiles.size === 0); } //If the daemon cannot be hacked, send the player truthgazer.msg a single time. else if (!recvd(truthGazer)) { diff --git a/src/Netscript/WorkerScript.ts b/src/Netscript/WorkerScript.ts index 24cf51eb3..e9613a58d 100644 --- a/src/Netscript/WorkerScript.ts +++ b/src/Netscript/WorkerScript.ts @@ -96,16 +96,11 @@ export class WorkerScript { if (server == null) { throw new Error(`WorkerScript constructed with invalid server ip: ${this.hostname}`); } - let found = false; - for (let i = 0; i < server.scripts.length; ++i) { - if (server.scripts[i].filename === this.name) { - found = true; - this.code = server.scripts[i].code; - } - } - if (!found) { + const script = server.scripts.get(this.name); + if (!script) { throw new Error(`WorkerScript constructed with invalid script filename: ${this.name}`); } + this.code = script.code; this.scriptRef = runningScriptObj; this.args = runningScriptObj.args.slice(); this.env = new Environment(); @@ -127,16 +122,14 @@ export class WorkerScript { */ getScript(): Script | null { const server = this.getServer(); - for (let i = 0; i < server.scripts.length; ++i) { - if (server.scripts[i].filename === this.name) { - return server.scripts[i]; - } + const script = server.scripts.get(this.name); + if (!script) { + console.error( + "Failed to find underlying Script object in WorkerScript.getScript(). This probably means somethings wrong", + ); + return null; } - - console.error( - "Failed to find underlying Script object in WorkerScript.getScript(). This probably means somethings wrong", - ); - return null; + return script; } /** @@ -144,17 +137,7 @@ export class WorkerScript { * or null if it cannot be found */ getScriptOnServer(fn: string, server: BaseServer): Script | null { - if (server == null) { - server = this.getServer(); - } - - for (let i = 0; i < server.scripts.length; ++i) { - if (server.scripts[i].filename === fn) { - return server.scripts[i]; - } - } - - return null; + return server.scripts.get(fn) ?? null; } shouldLog(fn: string): boolean { diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts index 50b4a5fc0..4b81a34ba 100644 --- a/src/NetscriptFunctions.ts +++ b/src/NetscriptFunctions.ts @@ -36,7 +36,7 @@ import { } from "./Server/ServerPurchases"; import { Server } from "./Server/Server"; import { influenceStockThroughServerGrow } from "./StockMarket/PlayerInfluencing"; -import { areFilesEqual, isValidFilePath, removeLeadingSlash } from "./Terminal/DirectoryHelpers"; +import { isValidFilePath, removeLeadingSlash } from "./Terminal/DirectoryHelpers"; import { TextFile, getTextFile, createTextFile } from "./TextFile"; import { runScriptFromScript } from "./NetscriptWorker"; import { killWorkerScript } from "./Netscript/killWorkerScript"; @@ -877,7 +877,7 @@ export const ns: InternalAPI = { } // Scp for script files - const sourceScript = sourceServ.scripts.find((script) => script.filename === file); + const sourceScript = sourceServ.scripts.get(file); if (!sourceScript) { helpers.log(ctx, () => `File '${file}' does not exist.`); noFailures = false; @@ -885,7 +885,7 @@ export const ns: InternalAPI = { } // Overwrite script if it already exists - const destScript = destServer.scripts.find((script) => script.filename === file); + const destScript = destServer.scripts.get(file); if (destScript) { if (destScript.code === sourceScript.code) { helpers.log(ctx, () => `Identical file '${file}' was already on '${destServer?.hostname}'`); @@ -900,7 +900,7 @@ export const ns: InternalAPI = { // Create new script if it does not already exist const newScript = new Script(file, sourceScript.code, destServer.hostname); - destServer.scripts.push(newScript); + destServer.scripts.set(file, newScript); helpers.log(ctx, () => `File '${file}' copied over to '${destServer?.hostname}'.`); } @@ -915,7 +915,7 @@ export const ns: InternalAPI = { ...server.contracts.map((contract) => contract.fn), ...server.messages, ...server.programs, - ...server.scripts.map((script) => script.filename), + ...server.scripts.keys(), ...server.textFiles.map((textFile) => textFile.filename), ]; @@ -1147,11 +1147,7 @@ export const ns: InternalAPI = { const filename = helpers.string(ctx, "filename", _filename); const hostname = helpers.string(ctx, "hostname", _hostname); const server = helpers.getServer(ctx, hostname); - for (let i = 0; i < server.scripts.length; ++i) { - if (filename == server.scripts[i].filename) { - return true; - } - } + if (server.scripts.has(filename)) return true; for (let i = 0; i < server.programs.length; ++i) { if (filename.toLowerCase() == server.programs[i].toLowerCase()) { return true; @@ -1366,47 +1362,45 @@ export const ns: InternalAPI = { } return writePort(portNumber, data); }, - write: - (ctx) => - (_filename, _data = "", _mode = "a") => { - let fn = helpers.string(ctx, "handle", _filename); - const data = helpers.string(ctx, "data", _data); - const mode = helpers.string(ctx, "mode", _mode); - if (!isValidFilePath(fn)) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filepath: ${fn}`); + write: (ctx) => (_filename, _data, _mode) => { + let filename = helpers.string(ctx, "handle", _filename); + const data = helpers.string(ctx, "data", _data ?? ""); + const mode = helpers.string(ctx, "mode", _mode ?? "a"); + if (!isValidFilePath(filename)) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filepath: ${filename}`); - if (fn.lastIndexOf("/") === 0) fn = removeLeadingSlash(fn); + if (filename.lastIndexOf("/") === 0) filename = removeLeadingSlash(filename); - const server = helpers.getServer(ctx, ctx.workerScript.hostname); + const server = helpers.getServer(ctx, ctx.workerScript.hostname); - if (isScriptFilename(fn)) { - // Write to script - let script = ctx.workerScript.getScriptOnServer(fn, server); - if (script == null) { - // Create a new script - script = new Script(fn, String(data), server.hostname); - server.scripts.push(script); - return; - } - mode === "w" ? (script.code = String(data)) : (script.code += data); - // Set ram to null so a recalc is performed the next time ram usage is needed - script.invalidateModule(); + if (isScriptFilename(filename)) { + // Write to script + let script = ctx.workerScript.getScriptOnServer(filename, server); + if (!script) { + // Create a new script + script = new Script(filename, String(data), server.hostname); + server.scripts.set(filename, script); return; - } else { - // Write to text file - if (!fn.endsWith(".txt")) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: ${fn}`); - const txtFile = getTextFile(fn, server); - if (txtFile == null) { - createTextFile(fn, String(data), server); - return; - } - if (mode === "w") { - txtFile.write(String(data)); - } else { - txtFile.append(String(data)); - } } + mode === "w" ? (script.code = data) : (script.code += data); + // Set ram to null so a recalc is performed the next time ram usage is needed + script.invalidateModule(); return; - }, + } else { + // Write to text file + if (!filename.endsWith(".txt")) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: ${filename}`); + const txtFile = getTextFile(filename, server); + if (txtFile == null) { + createTextFile(filename, String(data), server); + return; + } + if (mode === "w") { + txtFile.write(String(data)); + } else { + txtFile.append(String(data)); + } + } + return; + }, tryWritePort: (ctx) => (_portNumber, data) => { const portNumber = helpers.portNumber(ctx, _portNumber); if (typeof data !== "string" && typeof data !== "number") { @@ -1515,21 +1509,19 @@ export const ns: InternalAPI = { getScriptName: (ctx) => () => { return ctx.workerScript.name; }, - getScriptRam: - (ctx) => - (_scriptname, _hostname = ctx.workerScript.hostname) => { - const scriptname = helpers.string(ctx, "scriptname", _scriptname); - const hostname = helpers.string(ctx, "hostname", _hostname); - const server = helpers.getServer(ctx, hostname); - const script = server.scripts.find((serverScript) => areFilesEqual(serverScript.filename, scriptname)); - if (!script) return 0; - const ramUsage = script.getRamUsage(server.scripts); - if (!ramUsage) { - helpers.log(ctx, () => `Could not calculate ram usage for ${scriptname} on ${hostname}.`); - return 0; - } - return ramUsage; - }, + getScriptRam: (ctx) => (_scriptname, _hostname) => { + const scriptname = helpers.string(ctx, "scriptname", _scriptname); + const hostname = helpers.string(ctx, "hostname", _hostname ?? ctx.workerScript.hostname); + const server = helpers.getServer(ctx, hostname); + const script = server.scripts.get(scriptname); + if (!script) return 0; + const ramUsage = script.getRamUsage(server.scripts); + if (!ramUsage) { + helpers.log(ctx, () => `Could not calculate ram usage for ${scriptname} on ${hostname}.`); + return 0; + } + return ramUsage; + }, getRunningScript: (ctx) => (fn, hostname, ...args) => { @@ -1781,7 +1773,7 @@ export const ns: InternalAPI = { }; // Wrap the user function to prevent WorkerScript leaking as 'this' }, mv: (ctx) => (_host, _source, _destination) => { - const host = helpers.string(ctx, "host", _host); + const hostname = helpers.string(ctx, "host", _host); const source = helpers.string(ctx, "source", _source); const destination = helpers.string(ctx, "destination", _destination); @@ -1800,44 +1792,40 @@ export const ns: InternalAPI = { return; } - const destServer = helpers.getServer(ctx, host); + const server = helpers.getServer(ctx, hostname); - if (!source_is_txt && destServer.isRunning(source)) + if (!source_is_txt && server.isRunning(source)) throw helpers.makeRuntimeErrorMsg(ctx, `Cannot use 'mv' on a script that is running`); interface File { filename: string; } + let source_file: File | undefined; + let dest_file: File | undefined; - const files = source_is_txt ? destServer.textFiles : destServer.scripts; - let source_file: File | null = null; - let dest_file: File | null = null; - - for (let i = 0; i < files.length; ++i) { - const file = files[i]; - if (file.filename === source) { - source_file = file; - } else if (file.filename === destination) { - dest_file = file; - } + if (source_is_txt) { + // Traverses twice potentially. Inefficient but will soon be replaced with a map. + source_file = server.textFiles.find((textFile) => textFile.filename === source); + dest_file = server.textFiles.find((textFile) => textFile.filename === destination); + } else { + source_file = server.scripts.get(source); + dest_file = server.scripts.get(destination); } + if (!source_file) throw helpers.makeRuntimeErrorMsg(ctx, `Source file ${source} does not exist`); - if (source_file == null) throw helpers.makeRuntimeErrorMsg(ctx, `Source file ${source} does not exist`); - - if (dest_file != null) { + if (dest_file) { if (dest_file instanceof TextFile && source_file instanceof TextFile) { dest_file.text = source_file.text; } else if (dest_file instanceof Script && source_file instanceof Script) { dest_file.code = source_file.code; + // Source needs to be invalidated as well, to invalidate its dependents + source_file.invalidateModule(); dest_file.invalidateModule(); } - - destServer.removeFile(source); + server.removeFile(source); } else { source_file.filename = destination; - if (source_file instanceof Script) { - source_file.invalidateModule(); - } + if (source_file instanceof Script) source_file.invalidateModule(); } }, flags: Flags, diff --git a/src/NetscriptFunctions/Bladeburner.ts b/src/NetscriptFunctions/Bladeburner.ts index 2a4c8925d..3c043e9b1 100644 --- a/src/NetscriptFunctions/Bladeburner.ts +++ b/src/NetscriptFunctions/Bladeburner.ts @@ -15,7 +15,7 @@ export function NetscriptBladeburner(): InternalAPI { return; }; const getBladeburner = function (ctx: NetscriptContext): Bladeburner { - const apiAccess = Player.bitNodeN === 7 || Player.sourceFiles.some((a) => a.n === 7); + const apiAccess = Player.bitNodeN === 7 || Player.sourceFileLvl(7) > 0; if (!apiAccess) { throw helpers.makeRuntimeErrorMsg(ctx, "You have not unlocked the bladeburner API.", "API ACCESS"); } diff --git a/src/NetscriptFunctions/Formulas.ts b/src/NetscriptFunctions/Formulas.ts index dae80bb81..154d44aa5 100644 --- a/src/NetscriptFunctions/Formulas.ts +++ b/src/NetscriptFunctions/Formulas.ts @@ -94,7 +94,7 @@ export function NetscriptFormulas(): InternalAPI { numPeopleKilled: 0, money: 0, city: CityName.Sector12, - location: "", + location: LocationName.TravelAgency, bitNodeN: 0, totalPlaytime: 0, jobs: {}, diff --git a/src/NetscriptFunctions/Singularity.ts b/src/NetscriptFunctions/Singularity.ts index c8fd133ca..e775d1735 100644 --- a/src/NetscriptFunctions/Singularity.ts +++ b/src/NetscriptFunctions/Singularity.ts @@ -50,7 +50,6 @@ import { canGetBonus, onExport } from "../ExportBonus"; import { saveObject } from "../SaveObject"; import { calculateCrimeWorkStats } from "../Work/Formulas"; import { findEnumMember } from "../utils/helpers/enum"; -import { areFilesEqual } from "../Terminal/DirectoryHelpers"; import { Engine } from "../engine"; import { checkEnum } from "../utils/helpers/enum"; @@ -81,7 +80,7 @@ export function NetscriptSingularity(): InternalAPI { //Run a script after reset if (!cbScript) return; const home = Player.getHomeComputer(); - const script = home.scripts.find((serverScript) => areFilesEqual(serverScript.filename, cbScript)); + const script = home.scripts.get(cbScript); if (!script) return; const ramUsage = script.getRamUsage(home.scripts); if (!ramUsage) { @@ -110,9 +109,7 @@ export function NetscriptSingularity(): InternalAPI { return res; }, getOwnedSourceFiles: () => () => { - return Player.sourceFiles.map((sf) => { - return { n: sf.n, lvl: sf.lvl }; - }); + return [...Player.sourceFiles].map(([n, lvl]) => ({ n, lvl })); }, getAugmentationsFromFaction: (ctx) => (_facName) => { helpers.checkSingularityAccess(ctx); diff --git a/src/NetscriptJSEvaluator.ts b/src/NetscriptJSEvaluator.ts index 3660d96b6..bd1e78bd8 100644 --- a/src/NetscriptJSEvaluator.ts +++ b/src/NetscriptJSEvaluator.ts @@ -7,7 +7,8 @@ import { parse } from "acorn"; import { LoadedModule, ScriptURL, ScriptModule } from "./Script/LoadedModule"; import { Script } from "./Script/Script"; -import { areImportsEquals, removeLeadingSlash } from "./Terminal/DirectoryHelpers"; +import { removeLeadingSlash } from "./Terminal/DirectoryHelpers"; +import { ScriptFilename, scriptFilenameFromImport } from "./Types/strings"; // Acorn type def is straight up incomplete so we have to fill with our own. export type Node = any; @@ -46,7 +47,7 @@ const cleanup = new FinalizationRegistry((mapKey: string) => { } }); -export function compile(script: Script, scripts: Script[]): Promise { +export function compile(script: Script, scripts: Map): Promise { // Return the module if it already exists if (script.mod) return script.mod.module; @@ -75,7 +76,7 @@ function addDependencyInfo(script: Script, seenStack: Script[]) { * @param scripts array of other scripts on the server * @param seenStack A stack of scripts that were higher up in the import tree in a recursive call. */ -function generateLoadedModule(script: Script, scripts: Script[], seenStack: Script[]): LoadedModule { +function generateLoadedModule(script: Script, scripts: Map, seenStack: Script[]): LoadedModule { // Early return for recursive calls where the script already has a URL if (script.mod) { addDependencyInfo(script, seenStack); @@ -124,10 +125,10 @@ function generateLoadedModule(script: Script, scripts: Script[], seenStack: Scri let newCode = script.code; // Loop through each node and replace the script name with a blob url. for (const node of importNodes) { - const filename = node.filename.startsWith("./") ? node.filename.substring(2) : node.filename; + const filename = scriptFilenameFromImport(node.filename); // Find the corresponding script. - const importedScript = scripts.find((s) => areImportsEquals(s.filename, filename)); + const importedScript = scripts.get(filename); if (!importedScript) continue; seenStack.push(script); diff --git a/src/NetscriptWorker.ts b/src/NetscriptWorker.ts index 68b8e1d86..dde2c2396 100644 --- a/src/NetscriptWorker.ts +++ b/src/NetscriptWorker.ts @@ -29,10 +29,10 @@ import { roundToTwo } from "./utils/helpers/roundToTwo"; import { parse } from "acorn"; import { simple as walksimple } from "acorn-walk"; -import { areFilesEqual } from "./Terminal/DirectoryHelpers"; import { Terminal } from "./Terminal"; import { ScriptArg } from "@nsdefs"; import { handleUnknownError, CompleteRunOptions } from "./Netscript/NetscriptHelpers"; +import { scriptFilenameFromImport } from "./Types/strings"; export const NetscriptPorts: Map = new Map(); @@ -147,12 +147,7 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c } function getScript(scriptName: string): Script | null { - for (let i = 0; i < server.scripts.length; ++i) { - if (server.scripts[i].filename === scriptName) { - return server.scripts[i]; - } - } - return null; + return server.scripts.get(scriptName) ?? null; } let generatedCode = ""; // Generated Javascript Code @@ -162,10 +157,7 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c walksimple(ast, { ImportDeclaration: (node: Node) => { hasImports = true; - let scriptName = node.source.value; - if (scriptName.startsWith("./")) { - scriptName = scriptName.slice(2); - } + const scriptName = scriptFilenameFromImport(node.source.value, true); const script = getScript(scriptName); if (script == null) { throw new Error("'Import' failed due to invalid script: " + scriptName); @@ -398,7 +390,7 @@ export function runScriptFromScript( * running a large number of scripts. */ // Find the script, fail if it doesn't exist. - const script = host.scripts.find((serverScript) => areFilesEqual(serverScript.filename, scriptname)); + const script = host.scripts.get(scriptname); if (!script) { workerScript.log(caller, () => `Could not find script '${scriptname}' on '${host.hostname}'`); return 0; diff --git a/src/PersonObjects/Player/PlayerObject.ts b/src/PersonObjects/Player/PlayerObject.ts index 6385572b1..18cb5048e 100644 --- a/src/PersonObjects/Player/PlayerObject.ts +++ b/src/PersonObjects/Player/PlayerObject.ts @@ -8,7 +8,6 @@ import * as workMethods from "./PlayerObjectWorkMethods"; import { setPlayer } from "../../Player"; import { Sleeve } from "../Sleeve/Sleeve"; -import { PlayerOwnedSourceFile } from "../../SourceFile/PlayerOwnedSourceFile"; import { Exploit } from "../../Exploits/Exploit"; import { LocationName } from "../../Enums"; @@ -21,6 +20,7 @@ import { HashManager } from "../../Hacknet/HashManager"; import { MoneySourceTracker } from "../../utils/MoneySourceTracker"; import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../../utils/JSONReviver"; +import { JSONMap } from "../../Types/Jsonable"; import { PlayerAchievement } from "../../Achievements/Achievements"; import { cyrb53 } from "../../utils/StringHelperFunctions"; import { getRandomInt } from "../../utils/helpers/getRandomInt"; @@ -59,7 +59,7 @@ export class PlayerObject extends Person implements IPlayer { scriptProdSinceLastAug = 0; sleeves: Sleeve[] = []; sleevesFromCovenant = 0; - sourceFiles: PlayerOwnedSourceFile[] = []; + sourceFiles: JSONMap = new JSONMap(); exploits: Exploit[] = []; achievements: PlayerAchievement[] = []; terminalCommandHistory: string[] = []; @@ -168,9 +168,15 @@ export class PlayerObject extends Person implements IPlayer { /** Initializes a PlayerObject object from a JSON save state. */ static fromJSON(value: IReviverValue): PlayerObject { - if (!value.data.hp?.current || !value.data.hp?.max) value.data.hp = { current: 10, max: 10 }; const player = Generic_fromJSON(PlayerObject, value.data); - if (player.money === null) player.money = 0; + player.hp = { current: player.hp?.current ?? 10, max: player.hp?.max ?? 10 }; + player.money ??= 0; + player.updateSkillLevels(); + if (Array.isArray(player.sourceFiles)) { + // Expect pre-2.3 sourcefile format here. + type OldSourceFiles = { n: number; lvl: number }[]; + player.sourceFiles = new JSONMap((player.sourceFiles as OldSourceFiles).map(({ n, lvl }) => [n, lvl])); + } return player; } } diff --git a/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts b/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts index 06af668c8..59fd504c7 100644 --- a/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts +++ b/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts @@ -586,14 +586,14 @@ export function reapplyAllSourceFiles(this: PlayerObject): void { //Will always be called after reapplyAllAugmentations() so multipliers do not have to be reset //this.resetMultipliers(); - for (let i = 0; i < this.sourceFiles.length; ++i) { - const srcFileKey = "SourceFile" + this.sourceFiles[i].n; + for (const [bn, lvl] of this.sourceFiles) { + const srcFileKey = "SourceFile" + bn; const sourceFileObject = SourceFiles[srcFileKey]; - if (sourceFileObject == null) { - console.error(`Invalid source file number: ${this.sourceFiles[i].n}`); + if (!sourceFileObject) { + console.error(`Invalid source file number: ${bn}`); continue; } - applySourceFile(this.sourceFiles[i]); + applySourceFile(bn, lvl); } applyExploit(); this.updateSkillLevels(); @@ -1222,9 +1222,7 @@ export function canAccessCotMG(this: PlayerObject): boolean { } export function sourceFileLvl(this: PlayerObject, n: number): number { - const sf = this.sourceFiles.find((sf) => sf.n === n); - if (!sf) return 0; - return sf.lvl; + return this.sourceFiles.get(n) ?? 0; } export function focusPenalty(this: PlayerObject): number { diff --git a/src/Player.ts b/src/Player.ts index 5ede04b58..5d465375d 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -10,8 +10,9 @@ export function setPlayer(playerObj: PlayerObject): void { Player = playerObj; } -export function loadPlayer(saveString: string): void { - Player = JSON.parse(saveString, Reviver); - Player.money = parseFloat(Player.money + ""); - Player.exploits = sanitizeExploits(Player.exploits); +export function loadPlayer(saveString: string): PlayerObject { + const player = JSON.parse(saveString, Reviver); + player.money = parseFloat(player.money + ""); + player.exploits = sanitizeExploits(player.exploits); + return player; } diff --git a/src/Prestige.ts b/src/Prestige.ts index b3728e2f0..16f8e75e1 100755 --- a/src/Prestige.ts +++ b/src/Prestige.ts @@ -192,7 +192,7 @@ export function prestigeSourceFile(flume: boolean): void { AddToAllServers(homeComp); prestigeHomeComputer(homeComp); // Ram usage needs to be cleared for bitnode-level resets, due to possible change in singularity cost. - for (const script of homeComp.scripts) script.ramUsage = null; + for (const script of homeComp.scripts.values()) script.ramUsage = null; // Re-create foreign servers initForeignServers(Player.getHomeComputer()); diff --git a/src/Programs/data/ProgramsMetadata.ts b/src/Programs/data/ProgramsMetadata.ts index 594befad0..f71921550 100644 --- a/src/Programs/data/ProgramsMetadata.ts +++ b/src/Programs/data/ProgramsMetadata.ts @@ -20,7 +20,7 @@ function requireHackingLevel(lvl: number) { function bitFlumeRequirements() { return function () { - return Player.sourceFiles.length > 0 && Player.skills.hacking >= 1; + return Player.sourceFiles.size > 0 && Player.skills.hacking >= 1; }; } diff --git a/src/RedPill.tsx b/src/RedPill.tsx index 387c63600..80a7d13be 100644 --- a/src/RedPill.tsx +++ b/src/RedPill.tsx @@ -1,7 +1,6 @@ /** Implementation for what happens when you destroy a BitNode */ import React from "react"; import { Player } from "@player"; -import { PlayerOwnedSourceFile } from "./SourceFile/PlayerOwnedSourceFile"; import { SourceFiles } from "./SourceFile/SourceFiles"; import { dialogBoxCreate } from "./ui/React/DialogBox"; @@ -12,32 +11,26 @@ import { Engine } from "./engine"; function giveSourceFile(bitNodeNumber: number): void { const sourceFileKey = "SourceFile" + bitNodeNumber.toString(); const sourceFile = SourceFiles[sourceFileKey]; - if (sourceFile == null) { + if (!sourceFile) { console.error(`Could not find source file for Bit node: ${bitNodeNumber}`); return; } // Check if player already has this source file - const ownedSourceFile = Player.sourceFiles.find((sourceFile) => sourceFile.n === bitNodeNumber); + let lvl = Player.sourceFileLvl(bitNodeNumber); - if (ownedSourceFile) { - if (ownedSourceFile.lvl >= 3 && ownedSourceFile.n !== 12) { + if (lvl > 0) { + if (lvl >= 3 && bitNodeNumber !== 12) { dialogBoxCreate( `The Source-File for the BitNode you just destroyed, ${sourceFile.name}, is already at max level!`, ); } else { - ++ownedSourceFile.lvl; - dialogBoxCreate( - sourceFile.name + - " was upgraded to level " + - ownedSourceFile.lvl + - " for " + - "destroying its corresponding BitNode!", - ); + lvl++; + Player.sourceFiles.set(bitNodeNumber, lvl); + dialogBoxCreate(`${sourceFile.name} was upgraded to level ${lvl} for destroying its corresponding BitNode!`); } } else { - const newSrcFile = new PlayerOwnedSourceFile(bitNodeNumber, 1); - Player.sourceFiles.push(newSrcFile); + Player.sourceFiles.set(bitNodeNumber, 1); if (bitNodeNumber === 5 && Player.skills.intelligence === 0) { Player.skills.intelligence = 1; } diff --git a/src/RemoteFileAPI/MessageHandlers.ts b/src/RemoteFileAPI/MessageHandlers.ts index 39740ec96..30344d5b2 100644 --- a/src/RemoteFileAPI/MessageHandlers.ts +++ b/src/RemoteFileAPI/MessageHandlers.ts @@ -87,10 +87,7 @@ export const RFARequestHandler: Record void | R const server = GetServer(msg.params.server); if (!server) return error("Server hostname invalid", msg); - const fileNameList: string[] = [ - ...server.textFiles.map((txt): string => txt.filename), - ...server.scripts.map((scr): string => scr.filename), - ]; + const fileNameList: string[] = [...server.textFiles.map((txt): string => txt.filename), ...server.scripts.keys()]; return new RFAMessage({ result: fileNameList, id: msg.id }); }, @@ -105,10 +102,8 @@ export const RFARequestHandler: Record void | R ...server.textFiles.map((txt): FileContent => { return { filename: txt.filename, content: txt.text }; }), - ...server.scripts.map((scr): FileContent => { - return { filename: scr.filename, content: scr.code }; - }), ]; + for (const [filename, script] of server.scripts) fileList.push({ filename, content: script.code }); return new RFAMessage({ result: fileList, id: msg.id }); }, diff --git a/src/SaveObject.tsx b/src/SaveObject.tsx index a599161e4..06b95a44a 100755 --- a/src/SaveObject.tsx +++ b/src/SaveObject.tsx @@ -3,7 +3,7 @@ import { Companies, loadCompanies } from "./Company/Companies"; import { CONSTANTS } from "./Constants"; import { Factions, loadFactions } from "./Faction/Factions"; import { loadAllGangs, AllGangs } from "./Gang/AllGangs"; -import { Player, loadPlayer } from "./Player"; +import { Player, setPlayer, loadPlayer } from "./Player"; import { saveAllServers, loadAllServers, @@ -27,7 +27,6 @@ import { AwardNFG, v1APIBreak } from "./utils/v1APIBreak"; import { AugmentationNames } from "./Augmentation/data/AugmentationNames"; import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation"; import { LocationName } from "./Enums"; -import { PlayerObject } from "./PersonObjects/Player/PlayerObject"; import { pushGameSaved } from "./Electron"; import { defaultMonacoTheme } from "./ScriptEditor/ui/themes"; import { FactionNames } from "./Faction/data/FactionNames"; @@ -35,6 +34,8 @@ import { Faction } from "./Faction/Faction"; import { safelyCreateUniqueServer } from "./Server/ServerHelpers"; import { SpecialServers } from "./Server/data/SpecialServers"; import { v2APIBreak } from "./utils/v2APIBreak"; +import { Script } from "./Script/Script"; +import { JSONMap } from "./Types/Jsonable"; /* SaveObject.js * Defines the object used to save/load games @@ -208,7 +209,7 @@ class BitburnerSaveObject { base64: base64Save, }; - const importedPlayer = PlayerObject.fromJSON(JSON.parse(parsedSave.data.PlayerSave)); + const importedPlayer = loadPlayer(parsedSave.data.PlayerSave); const playerData: ImportPlayerData = { identifier: importedPlayer.identifier, @@ -224,7 +225,7 @@ class BitburnerSaveObject { bitNode: importedPlayer.bitNodeN, bitNodeLevel: importedPlayer.sourceFileLvl(Player.bitNodeN) + 1, - sourceFiles: importedPlayer.sourceFiles?.reduce((total, current) => (total += current.lvl), 0) ?? 0, + sourceFiles: [...importedPlayer.sourceFiles].reduce((total, [__bn, lvl]) => (total += lvl), 0), }; data.playerData = playerData; @@ -345,7 +346,7 @@ function evaluateVersionCompatibility(ver: string | number): void { } return code; } - for (const server of GetAllServers()) { + for (const server of GetAllServers() as unknown as { scripts: Script[] }[]) { for (const script of server.scripts) { script.code = convert(script.code); } @@ -663,6 +664,15 @@ function evaluateVersionCompatibility(ver: string | number): void { } anyPlayer.lastAugReset ??= anyPlayer.lastUpdate - anyPlayer.playtimeSinceLastAug; anyPlayer.lastNodeReset ??= anyPlayer.lastUpdate - anyPlayer.playtimeSinceLastBitnode; + for (const server of GetAllServers()) { + if (Array.isArray(server.scripts)) { + const oldScripts = server.scripts as Script[]; + server.scripts = new JSONMap(); + for (const script of oldScripts) { + server.scripts.set(script.filename, script); + } + } + } } } @@ -673,7 +683,7 @@ function loadGame(saveString: string): boolean { const saveObj = JSON.parse(saveString, Reviver); - loadPlayer(saveObj.PlayerSave); + setPlayer(loadPlayer(saveObj.PlayerSave)); loadAllServers(saveObj.AllServersSave); loadCompanies(saveObj.CompaniesSave); loadFactions(saveObj.FactionsSave); diff --git a/src/Script/RamCalculations.ts b/src/Script/RamCalculations.ts index 214a27334..57f5902c0 100644 --- a/src/Script/RamCalculations.ts +++ b/src/Script/RamCalculations.ts @@ -12,8 +12,8 @@ import { RamCalculationErrorCode } from "./RamCalculationErrorCodes"; import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator"; import { Script } from "./Script"; -import { areImportsEquals } from "../Terminal/DirectoryHelpers"; import { Node } from "../NetscriptJSEvaluator"; +import { ScriptFilename, scriptFilenameFromImport } from "../Types/strings"; export interface RamUsageEntry { type: "ns" | "dom" | "fn" | "misc"; @@ -40,7 +40,7 @@ const memCheckGlobalKey = ".__GLOBAL__"; * RAM usage. Also accounts for imported modules. * @param {Script[]} otherScripts - All other scripts on the server. Used to account for imported scripts * @param {string} code - The code being parsed */ -function parseOnlyRamCalculate(otherScripts: Script[], code: string): RamCalculation { +function parseOnlyRamCalculate(otherScripts: Map, code: string, ns1?: boolean): RamCalculation { try { /** * Maps dependent identifiers to their dependencies. @@ -86,16 +86,9 @@ function parseOnlyRamCalculate(otherScripts: Script[], code: string): RamCalcula if (nextModule === undefined) throw new Error("nextModule should not be undefined"); if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue; - let script = null; - const fn = nextModule.startsWith("./") ? nextModule.slice(2) : nextModule; - for (const s of otherScripts) { - if (areImportsEquals(s.filename, fn)) { - script = s; - break; - } - } - - if (script == null) { + const filename = scriptFilenameFromImport(nextModule, ns1); + const script = otherScripts.get(filename); + if (!script) { return { cost: RamCalculationErrorCode.ImportError }; // No such script on the server } @@ -375,9 +368,13 @@ function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsR * @param {Script[]} otherScripts - All other scripts on the server. * Used to account for imported scripts */ -export function calculateRamUsage(codeCopy: string, otherScripts: Script[]): RamCalculation { +export function calculateRamUsage( + codeCopy: string, + otherScripts: Map, + ns1?: boolean, +): RamCalculation { try { - return parseOnlyRamCalculate(otherScripts, codeCopy); + return parseOnlyRamCalculate(otherScripts, codeCopy, ns1); } catch (e) { console.error(`Failed to parse script for RAM calculations:`); console.error(e); diff --git a/src/Script/Script.ts b/src/Script/Script.ts index aab17a168..b2008eef3 100644 --- a/src/Script/Script.ts +++ b/src/Script/Script.ts @@ -10,6 +10,7 @@ import { LoadedModule, ScriptURL } from "./LoadedModule"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; import { roundToTwo } from "../utils/helpers/roundToTwo"; import { RamCostConstants } from "../Netscript/RamCostGenerator"; +import { ScriptFilename } from "src/Types/strings"; export class Script { code: string; @@ -81,7 +82,7 @@ export class Script { } /** Gets the ram usage, while also attempting to update it if it's currently null */ - getRamUsage(otherScripts: Script[]): number | null { + getRamUsage(otherScripts: Map): number | null { if (this.ramUsage) return this.ramUsage; this.updateRamUsage(otherScripts); return this.ramUsage; @@ -91,8 +92,8 @@ export class Script { * Calculates and updates the script's RAM usage based on its code * @param {Script[]} otherScripts - Other scripts on the server. Used to process imports */ - updateRamUsage(otherScripts: Script[]): void { - const ramCalc = calculateRamUsage(this.code, otherScripts); + updateRamUsage(otherScripts: Map): void { + const ramCalc = calculateRamUsage(this.code, otherScripts, this.filename.endsWith(".script")); if (ramCalc.cost >= RamCostConstants.Base) { this.ramUsage = roundToTwo(ramCalc.cost); this.ramUsageEntries = ramCalc.entries as RamUsageEntry[]; diff --git a/src/ScriptEditor/ui/ScriptEditorRoot.tsx b/src/ScriptEditor/ui/ScriptEditorRoot.tsx index c77893c12..9ac30f80c 100644 --- a/src/ScriptEditor/ui/ScriptEditorRoot.tsx +++ b/src/ScriptEditor/ui/ScriptEditorRoot.tsx @@ -432,19 +432,18 @@ export function Root(props: IProps): React.ReactElement { if (server === null) throw new Error("Server should not be null but it is."); if (isScriptFilename(scriptToSave.fileName)) { //If the current script already exists on the server, overwrite it - for (let i = 0; i < server.scripts.length; i++) { - if (scriptToSave.fileName == server.scripts[i].filename) { - server.scripts[i].saveScript(scriptToSave.fileName, scriptToSave.code, Player.currentServer); - if (Settings.SaveGameOnFileSave) saveObject.saveGame(); - Router.toPage(Page.Terminal); - return; - } + const existingScript = server.scripts.get(scriptToSave.fileName); + if (existingScript) { + existingScript.saveScript(scriptToSave.fileName, scriptToSave.code, Player.currentServer); + if (Settings.SaveGameOnFileSave) saveObject.saveGame(); + Router.toPage(Page.Terminal); + return; } //If the current script does NOT exist, create a new one const script = new Script(); script.saveScript(scriptToSave.fileName, scriptToSave.code, Player.currentServer); - server.scripts.push(script); + server.scripts.set(scriptToSave.fileName, script); } else if (scriptToSave.isTxt) { for (let i = 0; i < server.textFiles.length; ++i) { if (server.textFiles[i].fn === scriptToSave.fileName) { @@ -509,19 +508,18 @@ export function Root(props: IProps): React.ReactElement { if (server === null) throw new Error("Server should not be null but it is."); if (isScriptFilename(currentScript.fileName)) { //If the current script already exists on the server, overwrite it - for (let i = 0; i < server.scripts.length; i++) { - if (currentScript.fileName == server.scripts[i].filename) { - server.scripts[i].saveScript(currentScript.fileName, currentScript.code, Player.currentServer); - if (Settings.SaveGameOnFileSave) saveObject.saveGame(); - rerender(); - return; - } + const existingScript = server.scripts.get(currentScript.fileName); + if (existingScript) { + existingScript.saveScript(currentScript.fileName, currentScript.code, Player.currentServer); + if (Settings.SaveGameOnFileSave) saveObject.saveGame(); + rerender(); + return; } //If the current script does NOT exist, create a new one const script = new Script(); script.saveScript(currentScript.fileName, currentScript.code, Player.currentServer); - server.scripts.push(script); + server.scripts.set(currentScript.fileName, script); } else if (currentScript.isTxt) { for (let i = 0; i < server.textFiles.length; ++i) { if (server.textFiles[i].fn === currentScript.fileName) { @@ -683,7 +681,7 @@ export function Root(props: IProps): React.ReactElement { if (server === null) throw new Error(`Server '${openScript.hostname}' should not be null, but it is.`); const data = openScript.isTxt ? server.textFiles.find((t) => t.filename === openScript.fileName)?.text - : server.scripts.find((s) => s.filename === openScript.fileName)?.code; + : server.scripts.get(openScript.fileName)?.code; return data ?? null; } function handleFilterChange(event: React.ChangeEvent): void { diff --git a/src/Server/AllServers.ts b/src/Server/AllServers.ts index f97db369e..f79634a2d 100644 --- a/src/Server/AllServers.ts +++ b/src/Server/AllServers.ts @@ -8,11 +8,11 @@ import { IMinMaxRange } from "../types"; import { createRandomIp } from "../utils/IPAddress"; import { getRandomInt } from "../utils/helpers/getRandomInt"; import { Reviver } from "../utils/JSONReviver"; -import { isValidIPAddress } from "../utils/helpers/isValidIPAddress"; import { SpecialServers } from "./data/SpecialServers"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; import "../Script/RunningScript"; // For reviver side-effect +import { IPAddress, isIPAddress } from "../Types/strings"; import type { RunningScript } from "../Script/RunningScript"; /** @@ -50,9 +50,7 @@ export function GetServer(s: string): BaseServer | null { if (server) return server; } - if (!isValidIPAddress(s)) { - return GetServerByHostname(s); - } + if (!isIPAddress(s)) return GetServerByHostname(s); const ipserver = GetServerByIP(s); if (ipserver !== undefined) { @@ -88,8 +86,8 @@ export function ipExists(ip: string): boolean { return false; } -export function createUniqueRandomIp(): string { - let ip: string; +export function createUniqueRandomIp(): IPAddress { + let ip: IPAddress; // Repeat generating ip, until unique one is found do { ip = createRandomIp(); @@ -117,7 +115,7 @@ export const renameServer = (hostname: string, newName: string): void => { interface IServerParams { hackDifficulty?: number; hostname: string; - ip: string; + ip: IPAddress; maxRam?: number; moneyAvailable?: number; numOpenPortsRequired: number; diff --git a/src/Server/BaseServer.ts b/src/Server/BaseServer.ts index 7e5bbeba5..a3826a015 100644 --- a/src/Server/BaseServer.ts +++ b/src/Server/BaseServer.ts @@ -11,11 +11,13 @@ import { isScriptFilename } from "../Script/isScriptFilename"; import { createRandomIp } from "../utils/IPAddress"; import { compareArrays } from "../utils/helpers/compareArrays"; import { ScriptArg } from "../Netscript/ScriptArg"; +import { JSONMap } from "../Types/Jsonable"; +import { IPAddress, ScriptFilename, ServerName } from "../Types/strings"; interface IConstructorParams { adminRights?: boolean; hostname: string; - ip?: string; + ip?: IPAddress; isConnectedTo?: boolean; maxRam?: number; organizationName?: string; @@ -42,13 +44,13 @@ export abstract class BaseServer implements IServer { hasAdminRights = false; // Hostname. Must be unique - hostname = ""; + hostname: ServerName = "home"; // Flag indicating whether HTTP Port is open httpPortOpen = false; // IP Address. Must be unique - ip = ""; + ip = "1.1.1.1" as IPAddress; // Flag indicating whether player is currently connected to this server isConnectedTo = false; @@ -73,7 +75,7 @@ export abstract class BaseServer implements IServer { runningScripts: RunningScript[] = []; // Script files on this Server - scripts: Script[] = []; + scripts: JSONMap = new JSONMap(); // Contains the hostnames of all servers that are immediately // reachable from this one @@ -157,13 +159,7 @@ export abstract class BaseServer implements IServer { * Script object on the server (if it exists) */ getScript(scriptName: string): Script | null { - for (let i = 0; i < this.scripts.length; i++) { - if (this.scripts[i].filename === scriptName) { - return this.scripts[i]; - } - } - - return null; + return this.scripts.get(scriptName) ?? null; } /** Returns boolean indicating whether the given script is running on this server */ @@ -184,44 +180,44 @@ export abstract class BaseServer implements IServer { /** * Remove a file from the server - * @param fn {string} Name of file to be deleted + * @param filename {string} Name of file to be deleted * @returns {IReturnStatus} Return status object indicating whether or not file was deleted */ - removeFile(fn: string): IReturnStatus { - if (fn.endsWith(".exe") || fn.match(/^.+\.exe-\d+(?:\.\d*)?%-INC$/) != null) { + removeFile(filename: string): IReturnStatus { + if (filename.endsWith(".exe") || filename.match(/^.+\.exe-\d+(?:\.\d*)?%-INC$/) != null) { for (let i = 0; i < this.programs.length; ++i) { - if (this.programs[i] === fn) { + if (this.programs[i] === filename) { this.programs.splice(i, 1); return { res: true }; } } - } else if (isScriptFilename(fn)) { - const scriptIndex = this.scripts.findIndex((script) => script.filename === fn); - if (scriptIndex === -1) return { res: false, msg: `script ${fn} not found.` }; - if (this.isRunning(fn)) { + } else if (isScriptFilename(filename)) { + const script = this.scripts.get(filename); + if (!script) return { res: false, msg: `script ${filename} not found.` }; + if (this.isRunning(filename)) { return { res: false, msg: "Cannot delete a script that is currently running!" }; } - this.scripts[scriptIndex].invalidateModule(); - this.scripts.splice(scriptIndex, 1); + script.invalidateModule(); + this.scripts.delete(filename); return { res: true }; - } else if (fn.endsWith(".lit")) { + } else if (filename.endsWith(".lit")) { for (let i = 0; i < this.messages.length; ++i) { const f = this.messages[i]; - if (typeof f === "string" && f === fn) { + if (typeof f === "string" && f === filename) { this.messages.splice(i, 1); return { res: true }; } } - } else if (fn.endsWith(".txt")) { + } else if (filename.endsWith(".txt")) { for (let i = 0; i < this.textFiles.length; ++i) { - if (this.textFiles[i].fn === fn) { + if (this.textFiles[i].fn === filename) { this.textFiles.splice(i, 1); return { res: true }; } } - } else if (fn.endsWith(".cct")) { + } else if (filename.endsWith(".cct")) { for (let i = 0; i < this.contracts.length; ++i) { - if (this.contracts[i].fn === fn) { + if (this.contracts[i].fn === filename) { this.contracts.splice(i, 1); return { res: true }; } @@ -266,30 +262,23 @@ export abstract class BaseServer implements IServer { * Write to a script file * Overwrites existing files. Creates new files if the script does not exist. */ - writeToScriptFile(fn: string, code: string): writeResult { - const ret = { success: false, overwritten: false }; - if (!isValidFilePath(fn) || !isScriptFilename(fn)) { - return ret; + writeToScriptFile(filename: string, code: string): writeResult { + if (!isValidFilePath(filename) || !isScriptFilename(filename)) { + return { success: false, overwritten: false }; } // Check if the script already exists, and overwrite it if it does - for (let i = 0; i < this.scripts.length; ++i) { - if (fn === this.scripts[i].filename) { - const script = this.scripts[i]; - script.code = code; - // Set ramUsage to null in order to force recalculation on next run - script.invalidateModule(); - ret.overwritten = true; - ret.success = true; - return ret; - } + const script = this.scripts.get(filename); + if (script) { + script.invalidateModule(); + script.code = code; + return { success: true, overwritten: true }; } // Otherwise, create a new script - const newScript = new Script(fn, code, this.hostname); - this.scripts.push(newScript); - ret.success = true; - return ret; + const newScript = new Script(filename, code, this.hostname); + this.scripts.set(filename, newScript); + return { success: true, overwritten: false }; } // Write to a text file diff --git a/src/Server/Server.ts b/src/Server/Server.ts index 94a974565..1296a32ec 100644 --- a/src/Server/Server.ts +++ b/src/Server/Server.ts @@ -6,12 +6,13 @@ import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; import { createRandomString } from "../utils/helpers/createRandomString"; import { createRandomIp } from "../utils/IPAddress"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; +import { IPAddress, ServerName } from "../Types/strings"; export interface IConstructorParams { adminRights?: boolean; hackDifficulty?: number; hostname: string; - ip?: string; + ip?: IPAddress; isConnectedTo?: boolean; maxRam?: number; moneyAvailable?: number; @@ -60,7 +61,7 @@ export class Server extends BaseServer { // "hacknet-node-X" hostnames are reserved for Hacknet Servers if (this.hostname.startsWith("hacknet-node-") || this.hostname.startsWith("hacknet-server-")) { - this.hostname = createRandomString(10); + this.hostname = createRandomString(10) as ServerName; } this.purchasedByPlayer = params.purchasedByPlayer != null ? params.purchasedByPlayer : false; diff --git a/src/Sidebar/ui/SidebarRoot.tsx b/src/Sidebar/ui/SidebarRoot.tsx index 0a0d1565c..d970c950a 100644 --- a/src/Sidebar/ui/SidebarRoot.tsx +++ b/src/Sidebar/ui/SidebarRoot.tsx @@ -143,10 +143,13 @@ export function SidebarRoot(props: IProps): React.ReactElement { Player.factions.length > 0 || Player.augmentations.length > 0 || Player.queuedAugmentations.length > 0 || - Player.sourceFiles.length > 0; + Player.sourceFiles.size > 0; const canOpenAugmentations = - Player.augmentations.length > 0 || Player.queuedAugmentations.length > 0 || Player.sourceFiles.length > 0; + Player.augmentations.length > 0 || + Player.queuedAugmentations.length > 0 || + Player.sourceFiles.size > 0 || + Player.exploits.length > 0; const canOpenSleeves = Player.sleeves.length > 0; diff --git a/src/SourceFile/PlayerOwnedSourceFile.ts b/src/SourceFile/PlayerOwnedSourceFile.ts deleted file mode 100644 index 55d99667f..000000000 --- a/src/SourceFile/PlayerOwnedSourceFile.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class PlayerOwnedSourceFile { - // Source-File level - lvl = 1; - - // Source-File number - n = 1; - - constructor(n: number, level: number) { - this.n = n; - this.lvl = level; - } -} diff --git a/src/SourceFile/applySourceFile.ts b/src/SourceFile/applySourceFile.ts index b37abac20..ba333f2c3 100644 --- a/src/SourceFile/applySourceFile.ts +++ b/src/SourceFile/applySourceFile.ts @@ -1,21 +1,20 @@ -import { PlayerOwnedSourceFile } from "./PlayerOwnedSourceFile"; import { SourceFiles } from "./SourceFiles"; import { Player } from "@player"; -export function applySourceFile(srcFile: PlayerOwnedSourceFile): void { - const srcFileKey = "SourceFile" + srcFile.n; +export function applySourceFile(bn: number, lvl: number): void { + const srcFileKey = "SourceFile" + bn; const sourceFileObject = SourceFiles[srcFileKey]; if (sourceFileObject == null) { - console.error(`Invalid source file number: ${srcFile.n}`); + console.error(`Invalid source file number: ${bn}`); return; } - switch (srcFile.n) { + switch (bn) { case 1: { // The Source Genesis let mult = 0; - for (let i = 0; i < srcFile.lvl; ++i) { + for (let i = 0; i < lvl; ++i) { mult += 16 / Math.pow(2, i); } const incMult = 1 + mult / 100; @@ -51,7 +50,7 @@ export function applySourceFile(srcFile: PlayerOwnedSourceFile): void { case 2: { // Rise of the Underworld let mult = 0; - for (let i = 0; i < srcFile.lvl; ++i) { + for (let i = 0; i < lvl; ++i) { mult += 24 / Math.pow(2, i); } const incMult = 1 + mult / 100; @@ -63,7 +62,7 @@ export function applySourceFile(srcFile: PlayerOwnedSourceFile): void { case 3: { // Corporatocracy let mult = 0; - for (let i = 0; i < srcFile.lvl; ++i) { + for (let i = 0; i < lvl; ++i) { mult += 8 / Math.pow(2, i); } const incMult = 1 + mult / 100; @@ -79,7 +78,7 @@ export function applySourceFile(srcFile: PlayerOwnedSourceFile): void { case 5: { // Artificial Intelligence let mult = 0; - for (let i = 0; i < srcFile.lvl; ++i) { + for (let i = 0; i < lvl; ++i) { mult += 8 / Math.pow(2, i); } const incMult = 1 + mult / 100; @@ -94,7 +93,7 @@ export function applySourceFile(srcFile: PlayerOwnedSourceFile): void { case 6: { // Bladeburner let mult = 0; - for (let i = 0; i < srcFile.lvl; ++i) { + for (let i = 0; i < lvl; ++i) { mult += 8 / Math.pow(2, i); } const incMult = 1 + mult / 100; @@ -111,7 +110,7 @@ export function applySourceFile(srcFile: PlayerOwnedSourceFile): void { case 7: { // Bladeburner 2079 let mult = 0; - for (let i = 0; i < srcFile.lvl; ++i) { + for (let i = 0; i < lvl; ++i) { mult += 8 / Math.pow(2, i); } const incMult = 1 + mult / 100; @@ -124,7 +123,7 @@ export function applySourceFile(srcFile: PlayerOwnedSourceFile): void { case 8: { // Ghost of Wall Street let mult = 0; - for (let i = 0; i < srcFile.lvl; ++i) { + for (let i = 0; i < lvl; ++i) { mult += 12 / Math.pow(2, i); } const incMult = 1 + mult / 100; @@ -134,7 +133,7 @@ export function applySourceFile(srcFile: PlayerOwnedSourceFile): void { case 9: { // Hacktocracy let mult = 0; - for (let i = 0; i < srcFile.lvl; ++i) { + for (let i = 0; i < lvl; ++i) { mult += 12 / Math.pow(2, i); } const incMult = 1 + mult / 100; @@ -154,7 +153,7 @@ export function applySourceFile(srcFile: PlayerOwnedSourceFile): void { case 11: { // The Big Crash let mult = 0; - for (let i = 0; i < srcFile.lvl; ++i) { + for (let i = 0; i < lvl; ++i) { mult += 32 / Math.pow(2, i); } const incMult = 1 + mult / 100; @@ -169,7 +168,7 @@ export function applySourceFile(srcFile: PlayerOwnedSourceFile): void { // Grants more space on Stanek's Gift. break; default: - console.error(`Invalid source file number: ${srcFile.n}`); + console.error(`Invalid source file number: ${bn}`); break; } diff --git a/src/Terminal/DirectoryServerHelpers.ts b/src/Terminal/DirectoryServerHelpers.ts index 9e4b01db3..b3de7c33c 100644 --- a/src/Terminal/DirectoryServerHelpers.ts +++ b/src/Terminal/DirectoryServerHelpers.ts @@ -41,8 +41,8 @@ export function getSubdirectories(serv: BaseServer, dir: string): string[] { } } - for (const script of serv.scripts) { - processFile(script.filename); + for (const scriptFilename of serv.scripts.keys()) { + processFile(scriptFilename); } for (const txt of serv.textFiles) { @@ -56,7 +56,7 @@ export function getSubdirectories(serv: BaseServer, dir: string): string[] { export function containsFiles(server: BaseServer, dir: string): boolean { const dirWithTrailingSlash = dir + (dir.slice(-1) === "/" ? "" : "/"); - return [...server.scripts.map((s) => s.filename), ...server.textFiles.map((t) => t.fn)].some((filename) => + return [...server.scripts.keys(), ...server.textFiles.map((t) => t.fn)].some((filename) => filename.startsWith(dirWithTrailingSlash), ); } diff --git a/src/Terminal/Terminal.ts b/src/Terminal/Terminal.ts index d133c328f..99af32483 100644 --- a/src/Terminal/Terminal.ts +++ b/src/Terminal/Terminal.ts @@ -400,16 +400,10 @@ export class Terminal { } getScript(filename: string): Script | null { - const s = Player.getCurrentServer(); + const server = Player.getCurrentServer(); const filepath = this.getFilepath(filename); - if (!filepath) return null; - for (const script of s.scripts) { - if (filepath === script.filename) { - return script; - } - } - - return null; + if (filepath === null) return null; + return server.scripts.get(filepath) ?? null; } getTextFile(filename: string): TextFile | null { diff --git a/src/Terminal/commands/common/editor.ts b/src/Terminal/commands/common/editor.ts index e639d6405..ac8346640 100644 --- a/src/Terminal/commands/common/editor.ts +++ b/src/Terminal/commands/common/editor.ts @@ -7,6 +7,7 @@ import { isScriptFilename } from "../../../Script/isScriptFilename"; import { CursorPositions } from "../../../ScriptEditor/CursorPositions"; import { Script } from "../../../Script/Script"; import { isEmpty } from "lodash"; +import { ScriptFilename } from "src/Types/strings"; interface EditorParameters { args: (string | number | boolean)[]; @@ -28,7 +29,7 @@ interface ISimpleScriptGlob { postGlob: string; globError: string; globMatches: string[]; - globAgainst: Script[]; + globAgainst: Map; } function containsSimpleGlob(filename: string): boolean { @@ -45,7 +46,7 @@ function detectSimpleScriptGlob({ args, server }: EditorParameters): ISimpleScri return null; } -function parseSimpleScriptGlob(globString: string, globDatabase: Script[]): ISimpleScriptGlob { +function parseSimpleScriptGlob(globString: string, globDatabase: Map): ISimpleScriptGlob { const parsedGlob: ISimpleScriptGlob = { glob: globString, preGlob: "", diff --git a/src/Terminal/commands/cp.ts b/src/Terminal/commands/cp.ts index 5a5d0b4d7..f14b0d9a8 100644 --- a/src/Terminal/commands/cp.ts +++ b/src/Terminal/commands/cp.ts @@ -56,17 +56,8 @@ export function cp(args: (string | number | boolean)[], server: BaseServer): voi } // Get the current script - let sourceScript = null; - for (let i = 0; i < server.scripts.length; ++i) { - if (areFilesEqual(server.scripts[i].filename, src)) { - sourceScript = server.scripts[i]; - break; - } - } - if (sourceScript == null) { - Terminal.error("cp failed. No such script exists"); - return; - } + const sourceScript = server.scripts.get(src); + if (!sourceScript) return Terminal.error("cp failed. No such script exists"); const sRes = server.writeToScriptFile(dst, sourceScript.code); if (!sRes.success) { diff --git a/src/Terminal/commands/download.ts b/src/Terminal/commands/download.ts index 6a0902639..2370b8624 100644 --- a/src/Terminal/commands/download.ts +++ b/src/Terminal/commands/download.ts @@ -19,8 +19,8 @@ export function exportScripts(pattern: string, server: BaseServer): void { // In the case of script files, we pull from the server.scripts array if (!matchEnding || isScriptFilename(matchEnding)) zipFiles( - server.scripts.map((s) => s.filename), - server.scripts.map((s) => s.code), + [...server.scripts.keys()], + [...server.scripts.values()].map((script) => script.code), ); // In the case of text files, we pull from the server.scripts array if (!matchEnding || matchEnding.endsWith(".txt")) diff --git a/src/Terminal/commands/ls.tsx b/src/Terminal/commands/ls.tsx index bb65f970c..cbdae5a8e 100644 --- a/src/Terminal/commands/ls.tsx +++ b/src/Terminal/commands/ls.tsx @@ -106,7 +106,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi // Get all of the programs and scripts on the machine into one temporary array for (const program of server.programs) handleFn(program, allPrograms); - for (const script of server.scripts) handleFn(script.filename, allScripts); + for (const scriptFilename of server.scripts.keys()) handleFn(scriptFilename, allScripts); for (const txt of server.textFiles) handleFn(txt.fn, allTextFiles); for (const contract of server.contracts) handleFn(contract.fn, allContracts); for (const msgOrLit of server.messages) handleFn(msgOrLit, allMessages); diff --git a/src/Terminal/commands/runScript.ts b/src/Terminal/commands/runScript.ts index 7e2b7ad6d..c2b1b0a84 100644 --- a/src/Terminal/commands/runScript.ts +++ b/src/Terminal/commands/runScript.ts @@ -43,50 +43,43 @@ export function runScript(commandArgs: (string | number | boolean)[], server: Ba } // Check if the script exists and if it does run it - for (let i = 0; i < server.scripts.length; i++) { - if (server.scripts[i].filename !== scriptName) { - continue; - } - // Check for admin rights and that there is enough RAM available to run - const script = server.scripts[i]; - script.server = server.hostname; - const singleRamUsage = script.getRamUsage(server.scripts); - if (!singleRamUsage) return Terminal.error("Error while calculating ram usage for this script."); - const ramUsage = singleRamUsage * numThreads; - const ramAvailable = server.maxRam - server.ramUsed; + const script = server.scripts.get(scriptName); + if (!script) return Terminal.error("No such script"); - if (!server.hasAdminRights) { - Terminal.error("Need root access to run script"); - return; - } + const singleRamUsage = script.getRamUsage(server.scripts); + if (!singleRamUsage) return Terminal.error("Error while calculating ram usage for this script."); + const ramUsage = singleRamUsage * numThreads; + const ramAvailable = server.maxRam - server.ramUsed; - if (ramUsage > ramAvailable + 0.001) { - Terminal.error( - "This machine does not have enough RAM to run this script" + - (numThreads === 1 ? "" : ` with ${numThreads} threads`) + - `. Script requires ${formatRam(ramUsage)} of RAM`, - ); - return; - } - - // Able to run script - const runningScript = new RunningScript(script, singleRamUsage, args); - runningScript.threads = numThreads; - - const success = startWorkerScript(runningScript, server); - if (!success) { - Terminal.error(`Failed to start script`); - return; - } - - Terminal.print( - `Running script with ${numThreads} thread(s), pid ${runningScript.pid} and args: ${JSON.stringify(args)}.`, - ); - if (tailFlag) { - LogBoxEvents.emit(runningScript); - } + if (!server.hasAdminRights) { + Terminal.error("Need root access to run script"); return; } - Terminal.error("No such script"); + if (ramUsage > ramAvailable + 0.001) { + Terminal.error( + "This machine does not have enough RAM to run this script" + + (numThreads === 1 ? "" : ` with ${numThreads} threads`) + + `. Script requires ${formatRam(ramUsage)} of RAM`, + ); + return; + } + + // Able to run script + const runningScript = new RunningScript(script, singleRamUsage, args); + runningScript.threads = numThreads; + + const success = startWorkerScript(runningScript, server); + if (!success) { + Terminal.error(`Failed to start script`); + return; + } + + Terminal.print( + `Running script with ${numThreads} thread(s), pid ${runningScript.pid} and args: ${JSON.stringify(args)}.`, + ); + if (tailFlag) { + LogBoxEvents.emit(runningScript); + } + return; } diff --git a/src/Terminal/commands/scp.ts b/src/Terminal/commands/scp.ts index ab53413e5..d4457ac36 100644 --- a/src/Terminal/commands/scp.ts +++ b/src/Terminal/commands/scp.ts @@ -51,11 +51,8 @@ export function scp(args: (string | number | boolean)[], server: BaseServer): vo } // Get the current script - const sourceScript = server.scripts.find((script) => script.filename === scriptname); - if (!sourceScript) { - Terminal.error("scp failed. No such script exists"); - return; - } + const sourceScript = server.scripts.get(scriptname); + if (!sourceScript) return Terminal.error("scp failed. No such script exists"); const sRes = destServer.writeToScriptFile(scriptname, sourceScript.code); if (!sRes.success) { diff --git a/src/Terminal/determineAllPossibilitiesForTabCompletion.ts b/src/Terminal/determineAllPossibilitiesForTabCompletion.ts index 99b98dc1d..fa1c3c6e0 100644 --- a/src/Terminal/determineAllPossibilitiesForTabCompletion.ts +++ b/src/Terminal/determineAllPossibilitiesForTabCompletion.ts @@ -101,8 +101,8 @@ export async function determineAllPossibilitiesForTabCompletion( } function addAllScripts(): void { - for (const script of currServ.scripts) { - const res = processFilepath(script.filename); + for (const scriptFilename of currServ.scripts.keys()) { + const res = processFilepath(scriptFilename); if (res) { allPos.push(res); } @@ -281,10 +281,7 @@ export async function determineAllPossibilitiesForTabCompletion( // Use regex to remove any leading './', and then check if it matches against // the output of processFilepath or if it matches with a '/' prepended, // this way autocomplete works inside of directories - const script = currServ.scripts.find((script) => { - const fn = filename.replace(/^\.\//g, ""); - return processFilepath(script.filename) === fn || script.filename === "/" + fn; - }); + const script = currServ.scripts.get(filename); if (!script) return; // Doesn't exist. let loadedModule; try { @@ -304,7 +301,7 @@ export async function determineAllPossibilitiesForTabCompletion( const flagFunc = Flags(flags._); const autocompleteData: AutocompleteData = { servers: GetAllServers().map((server) => server.hostname), - scripts: currServ.scripts.map((script) => script.filename), + scripts: [...currServ.scripts.keys()], txts: currServ.textFiles.map((txt) => txt.fn), flags: (schema: unknown) => { if (!Array.isArray(schema)) throw new Error("flags require an array of array"); @@ -334,8 +331,8 @@ export async function determineAllPossibilitiesForTabCompletion( // invocation of `run`. if (input.startsWith("./")) { // All programs and scripts - for (const script of currServ.scripts) { - const res = processFilepath(script.filename); + for (const scriptFilename of currServ.scripts.keys()) { + const res = processFilepath(scriptFilename); if (res) { allPos.push(res); } diff --git a/src/Types/Jsonable.ts b/src/Types/Jsonable.ts new file mode 100644 index 000000000..d083bcc17 --- /dev/null +++ b/src/Types/Jsonable.ts @@ -0,0 +1,20 @@ +import type { IReviverValue } from "../utils/JSONReviver"; +// Jsonable versions of builtin JS class objects +export class JSONSet extends Set { + toJSON(): IReviverValue { + return { ctor: "JSONSet", data: Array.from(this) }; + } + static fromJSON(value: IReviverValue): JSONSet { + return new JSONSet(value.data); + } +} + +export class JSONMap extends Map { + toJSON(): IReviverValue { + return { ctor: "JSONMap", data: Array.from(this) }; + } + + static fromJSON(value: IReviverValue): JSONMap { + return new JSONMap(value.data); + } +} diff --git a/src/Types/strings.ts b/src/Types/strings.ts new file mode 100644 index 000000000..7484d9bc1 --- /dev/null +++ b/src/Types/strings.ts @@ -0,0 +1,29 @@ +// Script Filename +export type ScriptFilename = string /*& { __type: "ScriptFilename" }*/; +/*export function isScriptFilename(value: string): value is ScriptFilename { + // implementation +}*/ +/*export function sanitizeScriptFilename(filename: string): ScriptFilename { + // implementation +}*/ +export function scriptFilenameFromImport(importPath: string, ns1?: boolean): ScriptFilename { + if (importPath.startsWith("./")) importPath = importPath.substring(2); + if (!ns1 && !importPath.endsWith(".js")) importPath += ".js"; + if (ns1 && !importPath.endsWith(".script")) importPath += ".script"; + return importPath as ScriptFilename; +} + +// Server name +export type ServerName = string /*& { __type: "ServerName" }*/; +/*export function isExistingServerName(value: unknown): value is ServerName { + if (AllServers.has(value as ServerName)) return true; // For AllServers as an exported map + return false; +}*/ + +// IP Address +export type IPAddress = string /*& { __type: "IPAddress" }*/; +export function isIPAddress(value: string): value is IPAddress { + const regex = + /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + return regex.test(value); +} diff --git a/src/engine.tsx b/src/engine.tsx index 4a646de1a..c61976c16 100644 --- a/src/engine.tsx +++ b/src/engine.tsx @@ -255,21 +255,19 @@ const Engine: { const contractChancesWhileOffline = Math.floor(timeOffline / (1000 * 60 * 10)); // Generate coding contracts - if (Player.sourceFiles.length > 0) { - let numContracts = 0; - if (contractChancesWhileOffline > 100) { - numContracts += Math.floor(contractChancesWhileOffline * 0.25); - } - if (contractChancesWhileOffline > 0 && contractChancesWhileOffline <= 100) { - for (let i = 0; i < contractChancesWhileOffline; ++i) { - if (Math.random() <= 0.25) { - numContracts++; - } + let numContracts = 0; + if (contractChancesWhileOffline > 100) { + numContracts += Math.floor(contractChancesWhileOffline * 0.25); + } + if (contractChancesWhileOffline > 0 && contractChancesWhileOffline <= 100) { + for (let i = 0; i < contractChancesWhileOffline; ++i) { + if (Math.random() <= 0.25) { + numContracts++; } } - for (let i = 0; i < numContracts; i++) { - generateRandomContract(); - } + } + for (let i = 0; i < numContracts; i++) { + generateRandomContract(); } let offlineReputation = 0; diff --git a/src/ui/CharacterStats.tsx b/src/ui/CharacterStats.tsx index 2d6a1a12b..8b035f8eb 100644 --- a/src/ui/CharacterStats.tsx +++ b/src/ui/CharacterStats.tsx @@ -84,7 +84,7 @@ function MultiplierTable(props: MultTableProps): React.ReactElement { } function CurrentBitNode(): React.ReactElement { - if (Player.sourceFiles.length > 0) { + if (Player.sourceFiles.size > 0) { const index = "BitNode" + Player.bitNodeN; const lvl = Math.min(Player.sourceFileLvl(Player.bitNodeN) + 1, Player.bitNodeN === 12 ? Infinity : 3); return ( @@ -175,7 +175,7 @@ function MoneyModal({ open, onClose }: IMoneyModalProps): React.ReactElement { {convertMoneySourceTrackerToString(Player.moneySourceA)} ); - if (Player.sourceFiles.length !== 0) { + if (Player.sourceFiles.size > 0) { content = ( <> {content} @@ -205,7 +205,7 @@ export function CharacterStats(): React.ReactElement { const timeRows = [ ["Since last Augmentation installation", convertTimeMsToTimeElapsedString(Player.playtimeSinceLastAug)], ]; - if (Player.sourceFiles.length > 0) { + if (Player.sourceFiles.size > 0) { timeRows.push(["Since last Bitnode destroyed", convertTimeMsToTimeElapsedString(Player.playtimeSinceLastBitnode)]); } timeRows.push(["Total", convertTimeMsToTimeElapsedString(Player.totalPlaytime)]); diff --git a/src/ui/React/LogBoxManager.tsx b/src/ui/React/LogBoxManager.tsx index e5f281b81..94f14591e 100644 --- a/src/ui/React/LogBoxManager.tsx +++ b/src/ui/React/LogBoxManager.tsx @@ -20,7 +20,6 @@ import { Settings } from "../../Settings/Settings"; import { ANSIITypography } from "./ANSIITypography"; import { ScriptArg } from "../../Netscript/ScriptArg"; import { useRerender } from "./hooks"; -import { areFilesEqual } from "../../Terminal/DirectoryHelpers"; import { dialogBoxCreate } from "./DialogBox"; let layerCounter = 0; @@ -222,7 +221,7 @@ function LogWindow(props: IProps): React.ReactElement { if (server === null) return; const s = findRunningScript(script.filename, script.args, server); if (s === null) { - const baseScript = server.scripts.find((serverScript) => areFilesEqual(serverScript.filename, script.filename)); + const baseScript = server.scripts.get(script.filename); if (!baseScript) { return dialogBoxCreate( `Could not launch script. The script ${script.filename} no longer exists on the server ${server.hostname}.`, diff --git a/src/utils/IPAddress.ts b/src/utils/IPAddress.ts index 325712bea..9034308f0 100644 --- a/src/utils/IPAddress.ts +++ b/src/utils/IPAddress.ts @@ -1,8 +1,9 @@ +import { IPAddress } from "src/Types/strings"; import { getRandomByte } from "./helpers/getRandomByte"; /** * Generate a random IP address * Does not check to see if the IP already exists in the game */ -export const createRandomIp = (): string => - `${getRandomByte(99)}.${getRandomByte(9)}.${getRandomByte(9)}.${getRandomByte(9)}`; +export const createRandomIp = (): IPAddress => + `${getRandomByte(99)}.${getRandomByte(9)}.${getRandomByte(9)}.${getRandomByte(9)}` as IPAddress; diff --git a/src/utils/JSONReviver.ts b/src/utils/JSONReviver.ts index bf0ab86d2..a81e7db0b 100644 --- a/src/utils/JSONReviver.ts +++ b/src/utils/JSONReviver.ts @@ -1,6 +1,11 @@ /* Generic Reviver, toJSON, and fromJSON functions used for saving and loading objects */ - import { ObjectValidator, validateObject } from "./Validator"; +import { JSONMap, JSONSet } from "../Types/Jsonable"; + +type JsonableClass = (new () => { toJSON: () => IReviverValue }) & { + fromJSON: (value: IReviverValue) => any; + validationData?: ObjectValidator; +}; export interface IReviverValue { ctor: string; @@ -38,15 +43,7 @@ export function Reviver(_key: string, value: unknown): any { return obj; } -export const constructorsForReviver: Partial< - Record< - string, - (new () => object) & { - fromJSON: (value: IReviverValue) => any; - validationData?: ObjectValidator; - } - > -> = {}; +export const constructorsForReviver: Partial> = { JSONSet, JSONMap }; /** * A generic "toJSON" function that creates the data expected by Reviver. diff --git a/src/utils/helpers/isValidIPAddress.ts b/src/utils/helpers/isValidIPAddress.ts deleted file mode 100644 index 6d8395c03..000000000 --- a/src/utils/helpers/isValidIPAddress.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Checks whether a IP Address string is valid. - * @param ipaddress A string representing a potential IP Address - */ -export function isValidIPAddress(ipaddress: string): boolean { - const byteRange = "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"; - const regexStr = `^${byteRange}\.${byteRange}\.${byteRange}\.${byteRange}$`; - const ipAddressRegex = new RegExp(regexStr); - - return ipAddressRegex.test(ipaddress); -} diff --git a/src/utils/v1APIBreak.ts b/src/utils/v1APIBreak.ts index a86b7456f..bd1446621 100644 --- a/src/utils/v1APIBreak.ts +++ b/src/utils/v1APIBreak.ts @@ -95,7 +95,11 @@ export function v1APIBreak(): void { for (const server of GetAllServers()) { for (const change of detect) { const s: IFileLine[] = []; - for (const script of server.scripts) { + const scriptsArray: Script[] = Array.isArray(server.scripts) + ? (server.scripts as Script[]) + : [...server.scripts.values()]; + + for (const script of scriptsArray) { const lines = script.code.split("\n"); for (let i = 0; i < lines.length; i++) { if (lines[i].includes(change[0])) { @@ -121,7 +125,8 @@ export function v1APIBreak(): void { home.writeToTextFile("v1_DETECTED_CHANGES.txt", txt); } - for (const server of GetAllServers()) { + // API break function is called before version31 / 2.3.0 changes - scripts is still an array + for (const server of GetAllServers() as unknown as { scripts: Script[] }[]) { const backups: Script[] = []; for (const script of server.scripts) { if (!hasChanges(script.code)) continue; diff --git a/src/utils/v2APIBreak.ts b/src/utils/v2APIBreak.ts index 8e9219b67..f26b5977f 100644 --- a/src/utils/v2APIBreak.ts +++ b/src/utils/v2APIBreak.ts @@ -227,7 +227,8 @@ export const v2APIBreak = () => { }); } - for (const script of home.scripts) { + // API break function is called before the version31 2.3.0 changes, scripts are still an array. + for (const script of home.scripts as unknown as Script[]) { processScript(rules, script); } diff --git a/test/jest/Netscript/RamCalculation.test.ts b/test/jest/Netscript/RamCalculation.test.ts index 89a27c086..2c107cf0d 100644 --- a/test/jest/Netscript/RamCalculation.test.ts +++ b/test/jest/Netscript/RamCalculation.test.ts @@ -27,9 +27,8 @@ function isRemovedFunction(ctx: NetscriptContext, fn: (ctx: NetscriptContext) => } describe("Netscript RAM Calculation/Generation Tests", function () { - Player.sourceFiles[0] = { n: 4, lvl: 3 }; + Player.sourceFiles.set(4, 3); // For simulating costs of singularity functions. - const sf4 = Player.sourceFiles[0]; const baseCost = RamCostConstants.Base; const maxCost = RamCostConstants.Max; const script = new Script(); @@ -38,7 +37,7 @@ describe("Netscript RAM Calculation/Generation Tests", function () { script.code = code; // Force ram calculation reset script.ramUsage = null; - const ramUsage = script.getRamUsage([]); + const ramUsage = script.getRamUsage(new Map()); if (!ramUsage) throw new Error("Ram usage should be defined."); const runningScript = new RunningScript(script, ramUsage); return runningScript; @@ -63,7 +62,7 @@ describe("Netscript RAM Calculation/Generation Tests", function () { ramUsage: scriptRef.ramUsage, scriptRef, }; - const nsExternal = NetscriptFunctions(workerScript as WorkerScript); + const nsExternal = NetscriptFunctions(workerScript as unknown as WorkerScript); function combinedRamCheck( fn: PotentiallyAsyncFunction, @@ -78,7 +77,7 @@ describe("Netscript RAM Calculation/Generation Tests", function () { expect(getRamCost(...fnPath)).toEqual(expectedRamCost); // Static ram check - const staticCost = calculateRamUsage(code, []).cost; + const staticCost = calculateRamUsage(code, new Map()).cost; expect(staticCost).toBeCloseTo(Math.min(baseCost + expectedRamCost + extraLayerCost, maxCost)); // reset workerScript for dynamic check @@ -144,7 +143,7 @@ describe("Netscript RAM Calculation/Generation Tests", function () { describe("Singularity multiplier checks", () => { // Checks were already done above for SF4.3 having normal ramcost. - sf4.lvl = 3; + Player.sourceFiles.set(4, 3); const lvlToMult = { 0: 16, 1: 16, 2: 4 }; const externalSingularity = nsExternal.singularity; const ramCostSingularity = RamCosts.singularity; @@ -160,7 +159,7 @@ describe("Netscript RAM Calculation/Generation Tests", function () { }); for (const lvl of [0, 1, 2] as const) { it(`SF4.${lvl} check for x${lvlToMult[lvl]} costs`, () => { - sf4.lvl = lvl; + Player.sourceFiles.set(4, lvl); const expectedMult = lvlToMult[lvl]; singObjects.forEach(({ name, baseRam }) => { const fn = getFunction(externalSingularity[name]); diff --git a/test/jest/Netscript/RunScript.test.ts b/test/jest/Netscript/RunScript.test.ts index 17faca6b7..e65a83fc0 100644 --- a/test/jest/Netscript/RunScript.test.ts +++ b/test/jest/Netscript/RunScript.test.ts @@ -5,6 +5,7 @@ import { RunningScript } from "../../../src/Script/RunningScript"; import { AddToAllServers, DeleteServer } from "../../../src/Server/AllServers"; import { WorkerScriptStartStopEventEmitter } from "../../../src/Netscript/WorkerScriptStartStopEventEmitter"; import { AlertEvents } from "../../../src/ui/React/AlertManager"; +import type { Script } from "src/Script/Script"; // Replace Blob/ObjectURL functions, because they don't work natively in Jest global.Blob = class extends Blob { @@ -89,10 +90,11 @@ test.each([ expect(server.writeToScriptFile(s.name, s.code)).toEqual({ success: true, overwritten: false }); } - const script = server.scripts[server.scripts.length - 1]; + const script = server.scripts.get(scripts[scripts.length - 1].name) as Script; expect(script.filename).toEqual(scripts[scripts.length - 1].name); const ramUsage = script.getRamUsage(server.scripts); + if (!ramUsage) throw new Error(`ramUsage calculated to be ${ramUsage}`); const runningScript = new RunningScript(script, ramUsage as number); expect(startWorkerScript(runningScript, server)).toBeGreaterThan(0); // We don't care about start, so subscribe after that. Await script death. diff --git a/test/jest/Netscript/StaticRamParsingCalculation.test.ts b/test/jest/Netscript/StaticRamParsingCalculation.test.ts index 6dfaf11f8..296f21a83 100644 --- a/test/jest/Netscript/StaticRamParsingCalculation.test.ts +++ b/test/jest/Netscript/StaticRamParsingCalculation.test.ts @@ -21,7 +21,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { const code = ` export async function main(ns) { } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, 0); }); @@ -31,7 +31,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { ns.print("Slum snakes r00l!"); } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, 0); }); @@ -41,7 +41,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await ns.hack("joesguns"); } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, HackCost); }); @@ -51,7 +51,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await X.hack("joesguns"); } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, HackCost); }); @@ -62,7 +62,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await ns.hack("joesguns"); } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, HackCost); }); @@ -73,7 +73,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await ns.grow("joesguns"); } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, HackCost + GrowCost); }); @@ -86,7 +86,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await ns.hack("joesguns"); } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, HackCost); }); @@ -101,7 +101,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { async doHacking() { await this.ns.hack("joesguns"); } } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, HackCost); }); @@ -116,7 +116,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { async doHacking() { await this.#ns.hack("joesguns"); } } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, HackCost); }); }); @@ -129,7 +129,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { } function get() { return 0; } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, 0); }); @@ -140,7 +140,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { } function purchaseNode() { return 0; } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; // Works at present, because the parser checks the namespace only, not the function name expectCost(calculated, 0); }); @@ -153,7 +153,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { } function getTask() { return 0; } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, 0); }); }); @@ -165,7 +165,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { ns.hacknet.purchaseNode(0); } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, HacknetCost); }); @@ -175,7 +175,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { ns.corporation.getCorporation(); } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, MaxCost); }); @@ -186,7 +186,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { ns.corporation.getCorporation(); } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, MaxCost); }); @@ -196,7 +196,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { ns.sleeve.getTask(3); } `; - const calculated = calculateRamUsage(code, []).cost; + const calculated = calculateRamUsage(code, new Map()).cost; expectCost(calculated, SleeveGetTaskCost); }); }); @@ -214,7 +214,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { dummy(); } `; - const calculated = calculateRamUsage(code, [lib]).cost; + const calculated = calculateRamUsage(code, new Map([["libTest.js", lib]])).cost; expectCost(calculated, 0); }); @@ -230,7 +230,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await doHack(ns); } `; - const calculated = calculateRamUsage(code, [lib]).cost; + const calculated = calculateRamUsage(code, new Map([["libTest.js", lib]])).cost; expectCost(calculated, HackCost); }); @@ -247,7 +247,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await doHack(ns); } `; - const calculated = calculateRamUsage(code, [lib]).cost; + const calculated = calculateRamUsage(code, new Map([["libTest.js", lib]])).cost; expectCost(calculated, HackCost); }); @@ -264,7 +264,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await test.doHack(ns); } `; - const calculated = calculateRamUsage(code, [lib]).cost; + const calculated = calculateRamUsage(code, new Map([["libTest.js", lib]])).cost; expectCost(calculated, HackCost + GrowCost); }); @@ -286,7 +286,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await test.doHack(ns); } `; - const calculated = calculateRamUsage(code, [lib]).cost; + const calculated = calculateRamUsage(code, new Map([["libTest.js", lib]])).cost; expectCost(calculated, HackCost); }); @@ -312,7 +312,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await growerInstance.doGrow(); } `; - const calculated = calculateRamUsage(code, [lib]).cost; + const calculated = calculateRamUsage(code, new Map([["libTest.js", lib]])).cost; expectCost(calculated, GrowCost); }); }); diff --git a/test/jest/__snapshots__/Save.test.ts.snap b/test/jest/__snapshots__/Save.test.ts.snap index e10bc4680..36e1fa7b0 100644 --- a/test/jest/__snapshots__/Save.test.ts.snap +++ b/test/jest/__snapshots__/Save.test.ts.snap @@ -89,7 +89,10 @@ exports[`load/saveAllServers 1`] = ` "programs": [], "ramUsed": 0, "runningScripts": [], - "scripts": [], + "scripts": { + "ctor": "JSONMap", + "data": [] + }, "serversOnNetwork": [ "home" ], @@ -183,7 +186,10 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = ` "programs": [], "ramUsed": 0, "runningScripts": [], - "scripts": [], + "scripts": { + "ctor": "JSONMap", + "data": [] + }, "serversOnNetwork": [ "home" ],