diff --git a/src/Achievements/Achievements.ts b/src/Achievements/Achievements.ts index b32a6e1f2..50b3279ac 100644 --- a/src/Achievements/Achievements.ts +++ b/src/Achievements/Achievements.ts @@ -13,7 +13,7 @@ import { HacknetNode } from "../Hacknet/HacknetNode"; import { HacknetServer } from "../Hacknet/HacknetServer"; import { CityName } from "../Enums"; import { Player } from "@player"; -import { Programs } from "../Programs/Programs"; +import { CompletedProgramName } from "../Programs/Programs"; import { GetAllServers, GetServer } from "../Server/AllServers"; import { SpecialServers } from "../Server/data/SpecialServers"; import { Server } from "../Server/Server"; @@ -126,33 +126,33 @@ export const achievements: Record = { "BRUTESSH.EXE": { ...achievementData["BRUTESSH.EXE"], Icon: "p0", - Condition: () => Player.getHomeComputer().programs.includes(Programs.BruteSSHProgram.name), + Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.bruteSsh), }, "FTPCRACK.EXE": { ...achievementData["FTPCRACK.EXE"], Icon: "p1", - Condition: () => Player.getHomeComputer().programs.includes(Programs.FTPCrackProgram.name), + Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.ftpCrack), }, //----------------------------------------------------- "RELAYSMTP.EXE": { ...achievementData["RELAYSMTP.EXE"], Icon: "p2", - Condition: () => Player.getHomeComputer().programs.includes(Programs.RelaySMTPProgram.name), + Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.relaySmtp), }, "HTTPWORM.EXE": { ...achievementData["HTTPWORM.EXE"], Icon: "p3", - Condition: () => Player.getHomeComputer().programs.includes(Programs.HTTPWormProgram.name), + Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.httpWorm), }, "SQLINJECT.EXE": { ...achievementData["SQLINJECT.EXE"], Icon: "p4", - Condition: () => Player.getHomeComputer().programs.includes(Programs.SQLInjectProgram.name), + Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.sqlInject), }, "FORMULAS.EXE": { ...achievementData["FORMULAS.EXE"], Icon: "formulas", - Condition: () => Player.getHomeComputer().programs.includes(Programs.Formulas.name), + Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.formulas), }, "SF1.1": { ...achievementData["SF1.1"], diff --git a/src/Augmentation/Augmentation.tsx b/src/Augmentation/Augmentation.tsx index 872510252..f1d601861 100644 --- a/src/Augmentation/Augmentation.tsx +++ b/src/Augmentation/Augmentation.tsx @@ -1,6 +1,7 @@ // Class definition for a single Augmentation object import * as React from "react"; +import type { CompletedProgramName } from "src/Programs/Programs"; import { Faction } from "../Faction/Faction"; import { Factions } from "../Faction/Factions"; import { formatPercent } from "../ui/formatNumber"; @@ -64,7 +65,7 @@ export interface IConstructorParams { bladeburner_success_chance?: number; startingMoney?: number; - programs?: string[]; + programs?: CompletedProgramName[]; } function generateStatsDescription(mults: Multipliers, programs?: string[], startingMoney?: number): JSX.Element { diff --git a/src/Augmentation/data/AugmentationCreator.tsx b/src/Augmentation/data/AugmentationCreator.tsx index dfae82585..c854192b6 100644 --- a/src/Augmentation/data/AugmentationCreator.tsx +++ b/src/Augmentation/data/AugmentationCreator.tsx @@ -1,7 +1,7 @@ import { Augmentation, IConstructorParams } from "../Augmentation"; import { AugmentationNames } from "./AugmentationNames"; import { Player } from "@player"; -import { Programs } from "../../Programs/Programs"; +import { CompletedProgramName } from "../../Programs/Programs"; import { WHRNG } from "../../Casino/RNG"; import React from "react"; import { FactionNames } from "../../Faction/data/FactionNames"; @@ -1442,7 +1442,7 @@ export const initGeneralAugmentations = (): Augmentation[] => [ hacking_exp: 1.2, hacking_chance: 1.1, hacking_speed: 1.05, - programs: [Programs.FTPCrackProgram.name, Programs.RelaySMTPProgram.name], + programs: [CompletedProgramName.ftpCrack, CompletedProgramName.relaySmtp], factions: [FactionNames.BitRunners], }), new Augmentation({ @@ -1495,7 +1495,7 @@ export const initGeneralAugmentations = (): Augmentation[] => [ ), startingMoney: 1e6, - programs: [Programs.BruteSSHProgram.name], + programs: [CompletedProgramName.bruteSsh], factions: [FactionNames.Sector12], }), new Augmentation({ @@ -1528,7 +1528,7 @@ export const initGeneralAugmentations = (): Augmentation[] => [ company_rep: 1.0777, crime_success: 1.0777, crime_money: 1.0777, - programs: [Programs.DeepscanV1.name, Programs.AutoLink.name], + programs: [CompletedProgramName.deepScan1, CompletedProgramName.autoLink], factions: [FactionNames.Aevum], }), new Augmentation({ @@ -2104,16 +2104,16 @@ export const initChurchOfTheMachineGodAugmentations = (): Augmentation[] => [ startingMoney: 1e12, programs: [ - Programs.BruteSSHProgram.name, - Programs.FTPCrackProgram.name, - Programs.RelaySMTPProgram.name, - Programs.HTTPWormProgram.name, - Programs.SQLInjectProgram.name, - Programs.DeepscanV1.name, - Programs.DeepscanV2.name, - Programs.ServerProfiler.name, - Programs.AutoLink.name, - Programs.Formulas.name, + CompletedProgramName.bruteSsh, + CompletedProgramName.ftpCrack, + CompletedProgramName.relaySmtp, + CompletedProgramName.httpWorm, + CompletedProgramName.sqlInject, + CompletedProgramName.deepScan1, + CompletedProgramName.deepScan2, + CompletedProgramName.serverProfiler, + CompletedProgramName.autoLink, + CompletedProgramName.formulas, ], }), ]; diff --git a/src/CodingContractGenerator.ts b/src/CodingContractGenerator.ts index d10b0c7b6..c82147b2c 100644 --- a/src/CodingContractGenerator.ts +++ b/src/CodingContractGenerator.ts @@ -12,6 +12,7 @@ import { Server } from "./Server/Server"; import { BaseServer } from "./Server/BaseServer"; import { getRandomInt } from "./utils/helpers/getRandomInt"; +import { ContractFilePath, resolveContractFilePath } from "./Paths/ContractFilePath"; export function generateRandomContract(): void { // First select a random problem type @@ -57,7 +58,7 @@ export const generateDummyContract = (problemType: string): void => { interface IGenerateContractParams { problemType?: string; server?: string; - fn?: string; + fn?: ContractFilePath; } export function generateContract(params: IGenerateContractParams): void { @@ -84,15 +85,9 @@ export function generateContract(params: IGenerateContractParams): void { server = getRandomServer(); } - // Filename - let fn; - if (params.fn != null) { - fn = params.fn; - } else { - fn = getRandomFilename(server, reward); - } + const filename = params.fn ? params.fn : getRandomFilename(server, reward); - const contract = new CodingContract(fn, problemType, reward); + const contract = new CodingContract(filename, problemType, reward); server.addContract(contract); } @@ -185,7 +180,10 @@ function getRandomServer(): BaseServer { return randServer; } -function getRandomFilename(server: BaseServer, reward: ICodingContractReward = { name: "", type: 0 }): string { +function getRandomFilename( + server: BaseServer, + reward: ICodingContractReward = { name: "", type: 0 }, +): ContractFilePath { let contractFn = `contract-${getRandomInt(0, 1e6)}`; for (let i = 0; i < 1000; ++i) { @@ -203,6 +201,8 @@ function getRandomFilename(server: BaseServer, reward: ICodingContractReward = { // Only alphanumeric characters in the reward name. contractFn += `-${reward.name.replace(/[^a-zA-Z0-9]/g, "")}`; } - - return contractFn; + contractFn += ".cct"; + const validatedPath = resolveContractFilePath(contractFn); + if (!validatedPath) throw new Error(`Generated contract path could not be validated: ${contractFn}`); + return validatedPath; } diff --git a/src/CodingContracts.ts b/src/CodingContracts.ts index 7b1efad7c..45bb35462 100644 --- a/src/CodingContracts.ts +++ b/src/CodingContracts.ts @@ -2,6 +2,7 @@ import { codingContractTypesMetadata, DescriptionFunc, GeneratorFunc, SolverFunc import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "./utils/JSONReviver"; import { CodingContractEvent } from "./ui/React/CodingContractModal"; +import { ContractFilePath, resolveContractFilePath } from "./Paths/ContractFilePath"; /* tslint:disable:no-magic-numbers completed-docs max-classes-per-file no-console */ @@ -89,7 +90,7 @@ export class CodingContract { data: unknown; /* Contract's filename */ - fn: string; + fn: ContractFilePath; /* Describes the reward given if this Contract is solved. The reward is actually processed outside of this file */ @@ -101,17 +102,14 @@ export class CodingContract { /* String representing the contract's type. Must match type in ContractTypes */ type: string; - constructor(fn = "", type = "Find Largest Prime Factor", reward: ICodingContractReward | null = null) { - this.fn = fn; - if (!this.fn.endsWith(".cct")) { - this.fn += ".cct"; - } - - // tslint:disable-next-line - if (CodingContractTypes[type] == null) { + constructor(fn = "default.cct", type = "Find Largest Prime Factor", reward: ICodingContractReward | null = null) { + const path = resolveContractFilePath(fn); + if (!path) throw new Error(`Bad file path while creating a coding contract: ${fn}`); + if (!CodingContractTypes[type]) { throw new Error(`Error: invalid contract type: ${type} please contact developer`); } + this.fn = path; this.type = type; this.data = CodingContractTypes[type].generate(); this.reward = reward; diff --git a/src/Corporation/Corporation.tsx b/src/Corporation/Corporation.tsx index b8188f92f..19b407b98 100644 --- a/src/Corporation/Corporation.tsx +++ b/src/Corporation/Corporation.tsx @@ -6,12 +6,11 @@ import { Industry } from "./Industry"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; import { showLiterature } from "../Literature/LiteratureHelpers"; -import { LiteratureNames } from "../Literature/data/LiteratureNames"; +import { LiteratureName } from "../Literature/data/LiteratureNames"; import { Player } from "@player"; import { dialogBoxCreate } from "../ui/React/DialogBox"; import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver"; -import { isString } from "../utils/helpers/isString"; import { CityName } from "../Enums"; import { CorpStateName } from "@nsdefs"; import { calculateUpgradeCost } from "./helpers"; @@ -441,19 +440,9 @@ export class Corporation { getStarterGuide(): void { // Check if player already has Corporation Handbook const homeComp = Player.getHomeComputer(); - let hasHandbook = false; - const handbookFn = LiteratureNames.CorporationManagementHandbook; - for (let i = 0; i < homeComp.messages.length; ++i) { - if (isString(homeComp.messages[i]) && homeComp.messages[i] === handbookFn) { - hasHandbook = true; - break; - } - } - - if (!hasHandbook) { - homeComp.messages.push(handbookFn); - } - showLiterature(handbookFn); + const handbook = LiteratureName.CorporationManagementHandbook; + if (!homeComp.messages.includes(handbook)) homeComp.messages.push(handbook); + showLiterature(handbook); return; } diff --git a/src/DarkWeb/DarkWeb.tsx b/src/DarkWeb/DarkWeb.tsx index 929c6ac19..a3cdb30ee 100644 --- a/src/DarkWeb/DarkWeb.tsx +++ b/src/DarkWeb/DarkWeb.tsx @@ -22,7 +22,7 @@ export function checkIfConnectedToDarkweb(): void { } export function listAllDarkwebItems(): void { - for (const key of Object.keys(DarkWebItems)) { + for (const key of Object.keys(DarkWebItems) as (keyof typeof DarkWebItems)[]) { const item = DarkWebItems[key]; const cost = Player.getHomeComputer().programs.includes(item.program) ? ( @@ -45,7 +45,7 @@ export function buyDarkwebItem(itemName: string): void { // find the program that matches, if any let item: DarkWebItem | null = null; - for (const key of Object.keys(DarkWebItems)) { + for (const key of Object.keys(DarkWebItems) as (keyof typeof DarkWebItems)[]) { const i = DarkWebItems[key]; if (i.program.toLowerCase() == itemName) { item = i; @@ -88,7 +88,7 @@ export function buyAllDarkwebItems(): void { const itemsToBuy: DarkWebItem[] = []; let cost = 0; - for (const key of Object.keys(DarkWebItems)) { + for (const key of Object.keys(DarkWebItems) as (keyof typeof DarkWebItems)[]) { const item = DarkWebItems[key]; if (!Player.hasProgram(item.program)) { itemsToBuy.push(item); diff --git a/src/DarkWeb/DarkWebItem.ts b/src/DarkWeb/DarkWebItem.ts index f5dba0e58..505f423fd 100644 --- a/src/DarkWeb/DarkWebItem.ts +++ b/src/DarkWeb/DarkWebItem.ts @@ -1,9 +1,11 @@ +import type { CompletedProgramName } from "../Programs/Programs"; + export class DarkWebItem { - program: string; + program: CompletedProgramName; price: number; description: string; - constructor(program: string, price: number, description: string) { + constructor(program: CompletedProgramName, price: number, description: string) { this.program = program; this.price = price; this.description = description; diff --git a/src/DarkWeb/DarkWebItems.ts b/src/DarkWeb/DarkWebItems.ts index 1af0c4226..cdf5eb5f8 100644 --- a/src/DarkWeb/DarkWebItems.ts +++ b/src/DarkWeb/DarkWebItems.ts @@ -1,23 +1,15 @@ import { DarkWebItem } from "./DarkWebItem"; -import { Programs, initPrograms } from "../Programs/Programs"; +import { CompletedProgramName } from "../Programs/Programs"; -export const DarkWebItems: Record = {}; -export function initDarkWebItems() { - initPrograms(); - Object.assign(DarkWebItems, { - BruteSSHProgram: new DarkWebItem(Programs.BruteSSHProgram.name, 500e3, "Opens up SSH Ports."), - FTPCrackProgram: new DarkWebItem(Programs.FTPCrackProgram.name, 1500e3, "Opens up FTP Ports."), - RelaySMTPProgram: new DarkWebItem(Programs.RelaySMTPProgram.name, 5e6, "Opens up SMTP Ports."), - HTTPWormProgram: new DarkWebItem(Programs.HTTPWormProgram.name, 30e6, "Opens up HTTP Ports."), - SQLInjectProgram: new DarkWebItem(Programs.SQLInjectProgram.name, 250e6, "Opens up SQL Ports."), - ServerProfiler: new DarkWebItem( - Programs.ServerProfiler.name, - 500000, - "Displays detailed information about a server.", - ), - DeepscanV1: new DarkWebItem(Programs.DeepscanV1.name, 500000, "Enables 'scan-analyze' with a depth up to 5."), - DeepscanV2: new DarkWebItem(Programs.DeepscanV2.name, 25e6, "Enables 'scan-analyze' with a depth up to 10."), - AutolinkProgram: new DarkWebItem(Programs.AutoLink.name, 1e6, "Enables direct connect via 'scan-analyze'."), - FormulasProgram: new DarkWebItem(Programs.Formulas.name, 5e9, "Unlock access to the formulas API."), - }); -} +export const DarkWebItems = { + BruteSSHProgram: new DarkWebItem(CompletedProgramName.bruteSsh, 500e3, "Opens up SSH Ports."), + FTPCrackProgram: new DarkWebItem(CompletedProgramName.ftpCrack, 1500e3, "Opens up FTP Ports."), + RelaySMTPProgram: new DarkWebItem(CompletedProgramName.relaySmtp, 5e6, "Opens up SMTP Ports."), + HTTPWormProgram: new DarkWebItem(CompletedProgramName.httpWorm, 30e6, "Opens up HTTP Ports."), + SQLInjectProgram: new DarkWebItem(CompletedProgramName.sqlInject, 250e6, "Opens up SQL Ports."), + ServerProfiler: new DarkWebItem(CompletedProgramName.serverProfiler, 500e3, "Displays detailed server information."), + DeepscanV1: new DarkWebItem(CompletedProgramName.deepScan1, 500000, "Enables 'scan-analyze' with a depth up to 5."), + DeepscanV2: new DarkWebItem(CompletedProgramName.deepScan2, 25e6, "Enables 'scan-analyze' with a depth up to 10."), + AutolinkProgram: new DarkWebItem(CompletedProgramName.autoLink, 1e6, "Enables direct connect via 'scan-analyze'."), + FormulasProgram: new DarkWebItem(CompletedProgramName.formulas, 5e9, "Unlock access to the formulas API."), +}; diff --git a/src/DevMenu/ui/Programs.tsx b/src/DevMenu/ui/Programs.tsx index 61b777cf7..7eb5120ff 100644 --- a/src/DevMenu/ui/Programs.tsx +++ b/src/DevMenu/ui/Programs.tsx @@ -9,25 +9,21 @@ import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; import Select, { SelectChangeEvent } from "@mui/material/Select"; import { Player } from "@player"; -import { Programs as AllPrograms } from "../../Programs/Programs"; import MenuItem from "@mui/material/MenuItem"; +import { CompletedProgramName } from "../../Programs/Programs"; export function Programs(): React.ReactElement { - const [program, setProgram] = useState("NUKE.exe"); + const [program, setProgram] = useState(CompletedProgramName.bruteSsh); function setProgramDropdown(event: SelectChangeEvent): void { - setProgram(event.target.value); + setProgram(event.target.value as CompletedProgramName); } function addProgram(): void { - if (!Player.hasProgram(program)) { - Player.getHomeComputer().programs.push(program); - } + if (!Player.hasProgram(program)) Player.getHomeComputer().programs.push(program); } function addAllPrograms(): void { - for (const i of Object.keys(AllPrograms)) { - if (!Player.hasProgram(AllPrograms[i].name)) { - Player.getHomeComputer().programs.push(AllPrograms[i].name); - } + for (const name of Object.values(CompletedProgramName)) { + if (!Player.hasProgram(name)) Player.getHomeComputer().programs.push(name); } } @@ -45,9 +41,9 @@ export function Programs(): React.ReactElement { diff --git a/src/Diagnostic/FileDiagnosticModal.tsx b/src/Diagnostic/FileDiagnosticModal.tsx index d21cab4cb..f24792eb8 100644 --- a/src/Diagnostic/FileDiagnosticModal.tsx +++ b/src/Diagnostic/FileDiagnosticModal.tsx @@ -16,41 +16,24 @@ import AccordionSummary from "@mui/material/AccordionSummary"; import AccordionDetails from "@mui/material/AccordionDetails"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { ServerName } from "../Types/strings"; +import { allContentFiles } from "../Paths/ContentFile"; -interface IServerProps { - hostname: ServerName; +interface File { + name: string; + size: number; } -function ServerAccordion(props: IServerProps): React.ReactElement { +function ServerAccordion(props: { hostname: ServerName }): 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.values()) { - totalSize += f.code.length; - } - - for (const f of server.textFiles) { - totalSize += f.text.length; - } - - if (totalSize === 0) { - return <>; - } - - interface File { - name: string; - size: number; - } - const files: File[] = []; - - for (const f of server.scripts.values()) { - files.push({ name: f.filename, size: f.code.length }); + for (const [path, file] of allContentFiles(server)) { + totalSize += file.content.length; + files.push({ name: path, size: file.content.length }); } - for (const f of server.textFiles) { - files.push({ name: f.fn, size: f.text.length }); - } + if (totalSize === 0) return <>; files.sort((a: File, b: File): number => b.size - a.size); diff --git a/src/Electron.tsx b/src/Electron.tsx index 055e395aa..dfb98d50c 100644 --- a/src/Electron.tsx +++ b/src/Electron.tsx @@ -1,6 +1,5 @@ import { Player } from "@player"; import { Router } from "./ui/GameRoot"; -import { removeLeadingSlash } from "./Terminal/DirectoryHelpers"; import { Terminal } from "./Terminal"; import { SnackbarEvents, ToastVariant } from "./ui/React/Snackbar"; import { IReturnStatus } from "./types"; @@ -11,6 +10,8 @@ import { exportScripts } from "./Terminal/commands/download"; import { CONSTANTS } from "./Constants"; import { hash } from "./hash/hash"; import { Buffer } from "buffer"; +import { resolveFilePath } from "./Paths/FilePath"; +import { hasScriptExtension } from "./Paths/ScriptFilePath"; interface IReturnWebStatus extends IReturnStatus { data?: Record; @@ -55,23 +56,9 @@ export function initElectron(): void { } function initWebserver(): void { - function normalizeFileName(filename: string): string { - filename = filename.replace(/\/\/+/g, "/"); - filename = removeLeadingSlash(filename); - if (filename.includes("/")) { - filename = "/" + removeLeadingSlash(filename); - } - return filename; - } - document.getFiles = function (): IReturnWebStatus { const home = GetServer("home"); - if (home === null) { - return { - res: false, - msg: "Home server does not exist.", - }; - } + if (home === null) return { res: false, msg: "Home server does not exist." }; return { res: true, data: { @@ -85,40 +72,28 @@ function initWebserver(): void { }; document.deleteFile = function (filename: string): IReturnWebStatus { - filename = normalizeFileName(filename); + const path = resolveFilePath(filename); + if (!path) return { res: false, msg: "Invalid file path." }; const home = GetServer("home"); - if (home === null) { - return { - res: false, - msg: "Home server does not exist.", - }; - } - return home.removeFile(filename); + if (!home) return { res: false, msg: "Home server does not exist." }; + return home.removeFile(path); }; document.saveFile = function (filename: string, code: string): IReturnWebStatus { - filename = normalizeFileName(filename); + const path = resolveFilePath(filename); + if (!path) return { res: false, msg: "Invalid file path." }; + if (!hasScriptExtension(path)) return { res: false, msg: "Invalid file extension: must be a script" }; code = Buffer.from(code, "base64").toString(); const home = GetServer("home"); - if (home === null) { - return { - res: false, - msg: "Home server does not exist.", - }; - } - const { success, overwritten } = home.writeToScriptFile(filename, code); - let script; - if (success) { - script = home.getScript(filename); - } - return { - res: success, - data: { - overwritten, - ramUsage: script?.ramUsage, - }, - }; + if (!home) return { res: false, msg: "Home server does not exist." }; + + const { overwritten } = home.writeToScriptFile(path, code); + const script = home.scripts.get(path); + if (!script) return { res: false, msg: "Somehow failed to get script after writing it. This is a bug." }; + + const ramUsage = script.getRamUsage(home.scripts); + return { res: true, data: { overwritten, ramUsage } }; }; } diff --git a/src/InteractiveTutorial.ts b/src/InteractiveTutorial.ts index d6afa3f9f..a7aa11cdc 100644 --- a/src/InteractiveTutorial.ts +++ b/src/InteractiveTutorial.ts @@ -1,7 +1,5 @@ import { Player } from "@player"; - -import { LiteratureNames } from "./Literature/data/LiteratureNames"; - +import { LiteratureName } from "./Literature/data/LiteratureNames"; import { ITutorialEvents } from "./ui/InteractiveTutorial/ITutorialEvents"; // Ordered array of keys to Interactive Tutorial Steps @@ -104,7 +102,7 @@ function iTutorialEnd(): void { ITutorial.isRunning = false; ITutorial.currStep = iTutorialSteps.Start; const messages = Player.getHomeComputer().messages; - const handbook = LiteratureNames.HackersStartingHandbook; + const handbook = LiteratureName.HackersStartingHandbook; if (!messages.includes(handbook)) messages.push(handbook); ITutorialEvents.emit(); } diff --git a/src/Literature/Literature.ts b/src/Literature/Literature.ts index 3f71aace2..974160e5e 100644 --- a/src/Literature/Literature.ts +++ b/src/Literature/Literature.ts @@ -1,15 +1,19 @@ +import { FilePath, asFilePath } from "../Paths/FilePath"; +import type { LiteratureName } from "./data/LiteratureNames"; + +type LiteratureConstructorParams = { title: string; filename: LiteratureName; text: string }; /** * Lore / world building literature files that can be found on servers. * These files can be read by the player */ export class Literature { title: string; - fn: string; - txt: string; + filename: LiteratureName & FilePath; + text: string; - constructor(title: string, filename: string, txt: string) { + constructor({ title, filename, text }: LiteratureConstructorParams) { this.title = title; - this.fn = filename; - this.txt = txt; + this.filename = asFilePath(filename); + this.text = text; } } diff --git a/src/Literature/LiteratureHelpers.ts b/src/Literature/LiteratureHelpers.ts index b059d829e..4d3028c4e 100644 --- a/src/Literature/LiteratureHelpers.ts +++ b/src/Literature/LiteratureHelpers.ts @@ -1,11 +1,12 @@ import { Literatures } from "./Literatures"; import { dialogBoxCreate } from "../ui/React/DialogBox"; +import { LiteratureName } from "./data/LiteratureNames"; -export function showLiterature(fn: string): void { +export function showLiterature(fn: LiteratureName): void { const litObj = Literatures[fn]; if (litObj == null) { return; } - const txt = `${litObj.title}

${litObj.txt}`; + const txt = `${litObj.title}

${litObj.text}`; dialogBoxCreate(txt, true); } diff --git a/src/Literature/Literatures.ts b/src/Literature/Literatures.ts index 8ac860218..2ad0821a0 100644 --- a/src/Literature/Literatures.ts +++ b/src/Literature/Literatures.ts @@ -1,441 +1,438 @@ import { CityName } from "./../Enums"; import { Literature } from "./Literature"; -import { LiteratureNames } from "./data/LiteratureNames"; +import { LiteratureName } from "./data/LiteratureNames"; import { FactionNames } from "../Faction/data/FactionNames"; -export const Literatures: Record = {}; - -(function () { - let title, fn, txt; - title = "The Beginner's Guide to Hacking"; - fn = LiteratureNames.HackersStartingHandbook; - txt = - "Some resources:

" + - "Learn to Program

" + - "For Experienced JavaScript Developers: NetscriptJS

" + - "Netscript Documentation

" + - "When starting out, hacking is the most profitable way to earn money and progress. This " + - "is a brief collection of tips/pointers on how to make the most out of your hacking scripts.

" + - "-hack() and grow() both work by percentages. hack() steals a certain percentage of the " + - "money on a server, and grow() increases the amount of money on a server by some percentage (multiplicatively)

" + - "-Because hack() and grow() work by percentages, they are more effective if the target server has a high amount of money. " + - "Therefore, you should try to increase the amount of money on a server (using grow()) to a certain amount before hacking it. Two " + - "important Netscript functions for this are getServerMoneyAvailable() and getServerMaxMoney()

" + - "-Keep security level low. Security level affects everything when hacking. Two important Netscript functions " + - "for this are getServerSecurityLevel() and getServerMinSecurityLevel()

" + - "-Purchase additional servers by visiting 'Alpha Enterprises' in the city. They are relatively cheap " + - "and give you valuable RAM to run more scripts early in the game

" + - "-Prioritize upgrading the RAM on your home computer. This can also be done at 'Alpha Enterprises'

" + - "-Many low level servers have free RAM. You can use this RAM to run your scripts. Use the scp Terminal or " + - "Netscript command to copy your scripts onto these servers and then run them."; - Literatures[fn] = new Literature(title, fn, txt); - - title = "The Complete Handbook for Creating a Successful Corporation"; - fn = LiteratureNames.CorporationManagementHandbook; - txt = - "Getting Started with Corporations
" + - "To get started, visit the City Hall in Sector-12 in order to create a Corporation. This requires $150b of your own money, " + - "but this $150b will get put into your Corporation's funds. If you're in BitNode 3 you also have option to get seed money from " + - "the government in exchange for 500m shares. Your Corporation can have many different divisions, each in a different Industry. " + - "There are many different types of Industries, each with different properties. To create your first division, click the 'Expand' " + - "(into new Industry) button at the top of the management UI. The Agriculture industry is recommended for your first division.

" + - "The first thing you'll need to do is hire some employees. Employees can be assigned to five different positions. Each position has a " + - "different effect on various aspects of your Corporation. It is recommended to have at least one employee at each position.

" + - "Each industry uses some combination of Materials in order to produce other Materials and/or create Products. Specific information " + - "about this is displayed in each of your divisions' UI.

" + - "Products are special, industry-specific objects. They are different than Materials because you must manually choose to develop them, " + - "and you can choose to develop any number of Products. Developing a Product takes time, but a Product typically generates significantly " + - "more revenue than any Material. Not all industries allow you to create Products. To create a Product, look for a button in the top-left " + - "panel of the division UI (e.g. For the Software Industry, the button says 'Develop Software').

" + - "To get your supply chain system started, purchase the Materials that your industry needs to produce other Materials/Products. This can be " + - "done by clicking the 'Buy' button next to the corresponding Material(s). After you have the required Materials, you will immediately start " + - "production. The amount and quality/effective rating of Materials/Products you produce is based on a variety of factors, such as your employees " + - "and their productivity and the quality of materials used for production.

" + - "Once you start producing Materials/Products, you can sell them in order to start earning revenue. This can be done by clicking the 'Sell' " + - "button next to the corresponding Material or Product. The amount of Material/Product you sell is dependent on a wide variety of different factors. " + - "In order to produce and sell a Product you'll have to fully develop it first.

" + - "These are the basics of getting your Corporation up and running! Now, you can start purchasing upgrades to improve your bottom line. " + - "If you need money, consider looking for seed investors, who will give you money in exchange for stock shares. Otherwise, once you feel " + - "you are ready, take your Corporation public! Once your Corporation goes public, you can no longer find investors. Instead, your Corporation " + - "will be publicly traded and its stock price will change based on how well it's performing financially. In order to make money for yourself you " + - "can set dividends for a solid reliable income or you can sell your stock shares in order to make quick money.

" + - "Tips/Pointers
" + - "-Start with one division, such as Agriculture. Get it profitable on it's own, then expand to a division that consumes/produces " + - "a material that the division you selected produces/consumes.

" + - "-Materials are profitable, but Products are where the real money is, although if the product had a low development budget or is " + - "produced with low quality materials it won't sell well.

" + - "-The 'Smart Supply' upgrade is extremely useful. Consider purchasing it as soon as possible.

" + - "-Purchasing Hardware, Robots, AI Cores, and Real Estate can potentially increase your production. The effects of these depend on " + - "what industry you are in.

" + - "-In order to optimize your production, you will need a good balance of all employee positions, about 1/9 should be interning

" + - "-Quality of materials used for production affects the quality/effective rating of materials/products produced, so vertical integration " + - "is important for high profits.

" + - "-Materials purchased from the open market are always of quality 1.

" + - "-The price at which you can sell your Materials/Products is highly affected by the quality/effective rating

" + - "-When developing a product, different employee positions affect the development process differently, " + - "some improve the development speed, some improve the rating of the finished product.

" + - "-If your employees have low morale or energy, their production will greatly suffer. Having enough interns will make sure those stats get " + - "high and stay high.

" + - "-Don't forget to advertise your company. You won't have any business if nobody knows you.

" + - "-Having company awareness is great, but what's really important is your company's popularity. Try to keep your popularity as high as " + - "possible to see the biggest benefit for your sales

" + - "-Remember, you need to spend money to make money!

" + - "-Corporations do not reset when installing Augmentations, but they do reset when destroying a BitNode"; - - Literatures[fn] = new Literature(title, fn, txt); - - title = "A Brief History of Synthoids"; - fn = LiteratureNames.HistoryOfSynthoids; - txt = - "Synthetic androids, or Synthoids for short, are genetically engineered robots and, short of Augmentations, " + - "are composed entirely of organic substances. For this reason, Synthoids are virtually identical to " + - "humans in form, composition, and appearance.

" + - `Synthoids were first designed and manufactured by ${FactionNames.OmniTekIncorporated} sometime around the middle of the century. ` + - "Their original purpose was to be used for manual labor and as emergency responders for disasters. As such, they " + - "were initially programmed only for their specific tasks. Each iteration that followed improved upon the " + - "intelligence and capabilities of the Synthoids. By the 6th iteration, called MK-VI, the Synthoids were " + - `so smart and capable enough of making their own decisions that many argued ${FactionNames.OmniTekIncorporated} had created the first ` + - "sentient AI. These MK-VI Synthoids were produced in mass quantities (estimates up to 50 billion) with the hopes of increasing society's " + - "productivity and bolstering the global economy. Stemming from humanity's desire for technological advancement, optimism " + - "and excitement about the future had never been higher.

" + - "All of that excitement and optimism quickly turned to fear, panic, and dread in 2070, when a terrorist group " + - `called Ascendis Totalis hacked into ${FactionNames.OmniTekIncorporated} and uploaded a rogue AI into several of their Synthoid manufacturing facilities. ` + - `This hack went undetected and for months ${FactionNames.OmniTekIncorporated} unknowingly churned out legions of Synthoids embedded with this ` + - "rogue AI. Then, on December 24th, 2070, Omnica activated dormant protocols in the rogue AI, causing all of the " + - "infected Synthoids to immediately launch a military campaign to seek and destroy all of humanity.

" + - "What ensued was the deadliest conflict in human history. This crisis, now commonly known as the Synthoid Uprising, " + - "resulted in almost ten billion deaths over the course of a year. Despite the nations of the world banding together " + - "to combat the threat, the MK-VI Synthoids were simply stronger, faster, more intelligent, and more adaptable than humans, " + - "outsmarting them at every turn.

" + - `It wasn't until the sacrifice of an elite international military taskforce, called the ${FactionNames.Bladeburners}, that humanity ` + - `was finally able to defeat the Synthoids. The ${FactionNames.Bladeburners}' final act was a suicide bombing mission that ` + - "destroyed a large portion of the MK-VI Synthoids, including many of its leaders. In the following " + - "weeks militaries from around the world were able to round up and shut down the remaining rogue MK-VI Synthoids, ending " + - "the Synthoid Uprising.

" + - `In the aftermath of the bloodshed, the Synthoid Accords were drawn up. These Accords banned ${FactionNames.OmniTekIncorporated} ` + - "from manufacturing any Synthoids beyond the MK-III series. They also banned any other corporation " + - "from constructing androids with advanced, near-sentient AI. MK-VI Synthoids that did not have the rogue Ascendis Totalis " + - "AI were allowed to continue their existence, but they were stripped of all rights and protections as they " + - "were not considered humans. They were also banned from doing anything that may pose a global security threat, such " + - "as working for any military/defense organization or conducting any bioengineering, computing, or robotics related research.

" + - "Unfortunately, many believe that not all of the rogue MK-VI Synthoids from the Uprising were found and destroyed, " + - "and that many of them are blending in as normal humans in society today. In response, many nations have created " + - `${FactionNames.Bladeburners} divisions, special military branches that are tasked with investigating and dealing with any Synthoid threats.

` + - "To this day, tensions still exist between the remaining Synthoids and humans as a result of the Uprising.

" + - "Nobody knows what happened to the terrorist group Ascendis Totalis."; - Literatures[fn] = new Literature(title, fn, txt); - - title = "A Green Tomorrow"; - fn = LiteratureNames.AGreenTomorrow; - txt = - "Starting a few decades ago, there was a massive global movement towards the generation of renewable energy in an effort to " + - "combat global warming and climate change. The shift towards renewable energy was a big success, or so it seemed. In 2045 " + - "a staggering 80% of the world's energy came from non-renewable fossil fuels. Now, about three decades later, that " + - "number is down to only 15%. Most of the world's energy now comes from nuclear power and renewable sources such as " + - "solar and geothermal energy. Unfortunately, these efforts were not the huge success that they seem to be.

" + - "Since 2045 primary energy use has soared almost tenfold. This was mainly due to growing urban populations and " + - "the rise of increasingly advanced (and power-hungry) technology that has become ubiquitous in our lives. So, " + - "despite the fact that the percentage of our energy that comes from fossil fuels has drastically decreased, " + - "the total amount of energy we are producing from fossil fuels has actually increased.

" + - "The grim effects of our species' irresponsible use of energy and neglect of our mother world have become increasingly apparent. " + - "Last year a temperature of 190F was recorded in the Death Valley desert, which is over 50% higher than the highest " + - "recorded temperature at the beginning of the century. In the last two decades numerous major cities such as Manhattan, Boston, and " + - "Los Angeles have been partially or fully submerged by rising sea levels. In the present day, over 75% of the world's agriculture is " + - "done in climate-controlled vertical farms, as most traditional farmland has become unusable due to severe climate conditions.

" + - "Despite all of this, the greedy and corrupt corporations that rule the world have done nothing to address these problems that " + - "threaten our species. And so it's up to us, the common people. Each and every one of us can make a difference by doing what " + - "these corporations won't: taking responsibility. If we don't, pretty soon there won't be an Earth left to save. We are " + - "the last hope for a green tomorrow."; - Literatures[fn] = new Literature(title, fn, txt); - - title = "Alpha and Omega"; - fn = LiteratureNames.AlphaOmega; - txt = - "Then we saw a new Heaven and a new Earth, for our first Heaven and Earth had gone away, and our sea was no more. " + - "And we saw a new holy city, new Aeria, coming down out of this new Heaven, prepared as a bride adorned for her husband. " + - "And we heard a loud voice saying, 'Behold, the new dwelling place of the Gods. We will dwell with them, and they " + - "will be our people, and we will be with them as their Gods. We will wipe away every tear from their eyes, and death " + - "shall be no more, neither shall there be mourning, nor crying, nor pain anymore, for the former things " + - "have passed away.'

" + - "And once we were seated on the throne we said 'Behold, I am making all things new.' " + - "Also we said, 'Write this down, for these words are trustworthy and true.' And we said to you, " + - "'It is done! I am the Alpha and the Omega, the beginning and the end. To the thirsty I will give from the spring " + - "of the water of life without payment. The one who conquers will have this heritage, and we will be his God and " + - "he will be our son. But as for the cowardly, the faithless, the detestable, as for murderers, " + - "the sexually immoral, sorcerers, idolaters, and all liars, their portion will be in the lake that " + - "burns with fire and sulfur, for it is the second true death.'"; - Literatures[fn] = new Literature(title, fn, txt); - - title = "Are We Living in a Computer Simulation?"; - fn = LiteratureNames.SimulatedReality; - txt = - "The idea that we are living in a virtual world is not new. It's a trope that has " + - "been explored constantly in literature and pop culture. However, it is also a legitimate " + - "scientific hypothesis that many notable physicists and philosophers have debated for years.

" + - "Proponents for this simulated reality theory often point to how advanced our technology has become, " + - "as well as the incredibly fast pace at which it has advanced over the past decades. The amount of computing " + - "power available to us has increased over 100-fold since 2060 due to the development of nanoprocessors and " + - "quantum computers. Artificial Intelligence has advanced to the point where our entire lives are controlled " + - "by robots and machines that handle our day-to-day activities such as autonomous transportation and scheduling. " + - "If we consider the pace at which this technology has advanced and assume that these developments continue, it's " + - "reasonable to assume that at some point in the future our technology would be advanced enough that " + - "we could create simulations that are indistinguishable from reality. However, if continued technological advancement " + - "is a reasonable outcome, then it is very likely that such a scenario has already happened.

" + - "Statistically speaking, somewhere out there in the infinite universe there is an advanced, intelligent species " + - "that already has such technology. Who's to say that they haven't already created such a virtual reality: our own?"; - Literatures[fn] = new Literature(title, fn, txt); - - title = "Beyond Man"; - fn = LiteratureNames.BeyondMan; - txt = - "Humanity entered a 'transhuman' era a long time ago. And despite the protests and criticisms of many who cried out against " + - "human augmentation at the time, the transhuman movement continued and prospered. Proponents of the movement ignored the critics, " + - "arguing that it was in our inherent nature to better ourselves. To improve. To be more than we were. They claimed that " + - "not doing so would be to go against every living organism's biological purpose: evolution and survival of the fittest.

" + - "And here we are today, with technology that is advanced enough to augment humans to a state that " + - "can only be described as posthuman. But what do we have to show for it when this augmentation " + - "technology is only available to the so-called 'elite'? Are we really better off than before when only 5% of the " + - "world's population has access to this technology? When the powerful corporations and organizations of the world " + - "keep it all to themselves, have we really evolved?

" + - "Augmentation technology has only further increased the divide between the rich and the poor, between the powerful and " + - "the oppressed. We have not become 'more than human'. We have not evolved from nature's original design. We are still the greedy, " + - "corrupted, and evil men that we always were."; - Literatures[fn] = new Literature(title, fn, txt); - - title = "Brighter than the Sun"; - fn = LiteratureNames.BrighterThanTheSun; - txt = - `When people think about the corporations that dominate the East, they typically think of ${FactionNames.KuaiGongInternational}, which ` + - "holds a complete monopoly for manufacturing and commerce in Asia, or Global Pharmaceuticals, the world's largest " + - `drug company, or ${FactionNames.OmniTekIncorporated}, the global leader in intelligent and autonomous robots. But there's one company ` + - "that has seen a rapid rise in the last year and is poised to dominate not only the East, but the entire world: TaiYang Digital.

" + - "TaiYang Digital is a Chinese internet-technology corporation that provides services such as " + - "online advertising, search engines, gaming, media, entertainment, and cloud computing/storage. Its name TaiYang comes from the Chinese word " + - "for 'sun'. In Chinese culture, the sun is a 'yang' symbol " + - "associated with life, heat, masculinity, and heaven.

" + - "The company was founded " + - "less than 5 years ago and is already the third highest valued company in all of Asia. In 2076 it generated a total revenue of " + - "over 10 trillion yuan. Its services are used daily by over a billion people worldwide.

" + - "TaiYang Digital's meteoric rise is extremely surprising in modern society. This sort of growth is " + - "something you'd commonly see in the first half of the century, especially for tech companies. However in " + - "the last two decades the number of corporations has significantly declined as the largest entities " + - `quickly took over the economy. Corporations such as ${FactionNames.ECorp}, ${FactionNames.MegaCorp}, and ${FactionNames.KuaiGongInternational} have established ` + - "such strong monopolies in their market sectors that they have effectively killed off all " + - "of the smaller and new corporations that have tried to start up over the years. This is what makes " + - "the rise of TaiYang Digital so impressive. And if TaiYang continues down this path, then they have " + - "a bright future ahead of them."; - Literatures[fn] = new Literature(title, fn, txt); - - title = "Democracy is Dead: The Fall of an Empire"; - fn = LiteratureNames.DemocracyIsDead; - txt = - "They rose from the shadows in the street.
From the places where the oppressed meet.
" + - "Their cries echoed loudly through the air.
As they once did in Tiananmen Square.
" + - "Loudness in the silence, Darkness in the light.
They came forth with power and might.
" + - "Once the beacon of democracy, America was first.
Its pillars of society destroyed and dispersed.
" + - "Soon the cries rose everywhere, with revolt and riot.
Until one day, finally, all was quiet.
" + - "From the ashes rose a new order, corporatocracy was its name.
" + - "Rome, Mongol, Byzantine, all of history is just the same.
" + - "For man will never change in a fundamental way.
" + - "And now democracy is dead, in the USA."; - Literatures[fn] = new Literature(title, fn, txt); - - title = `Figures Show Rising Crime Rates in ${CityName.Sector12}`; - fn = LiteratureNames.Sector12Crime; - txt = - "A recent study by analytics company Wilson Inc. shows a significant rise " + - `in criminal activity in ${CityName.Sector12}. Perhaps the most alarming part of the statistic ` + - "is that most of the rise is in violent crime such as homicide and assault. According " + - "to the study, the city saw a total of 21,406 reported homicides in 2076, which is over " + - "a 20% increase compared to 2075.

" + - "CIA director David Glarow says it's too early to know " + - "whether these figures indicate the beginning of a sustained increase in crime rates, or whether " + - "the year was just an unfortunate outlier. He states that many intelligence and law enforcement " + - "agents have noticed an increase in organized crime activities, and believes that these figures may " + - `be the result of an uprising from criminal organizations such as ${FactionNames.TheSyndicate} or the ${FactionNames.SlumSnakes}.`; - Literatures[fn] = new Literature(title, fn, txt); - - title = "Man and the Machine"; - fn = LiteratureNames.ManAndMachine; - txt = - "In 2005 Ray Kurzweil popularized his theory of the Singularity. He predicted that the rate " + - "of technological advancement would continue to accelerate faster and faster until one day " + - "machines would be become infinitely more intelligent than humans. This point, called the " + - "Singularity, would result in a drastic transformation of the world as we know it. He predicted " + - "that the Singularity would arrive by 2045. " + - "And yet here we are, more than three decades later, where most would agree that we have not " + - "yet reached a point where computers and machines are vastly more intelligent than we are. So what gives?

" + - "The answer is that we have reached the Singularity, just not in the way we expected. The artificial superintelligence " + - "that was predicted by Kurzweil and others exists in the world today - in the form of Augmentations. " + - "Yes, those Augmentations that the rich and powerful keep to themselves enable humans " + - "to become superintelligent beings. The Singularity did not lead to a world where " + - "our machines are infinitely more intelligent than us, it led to a world " + - "where man and machine can merge to become something greater. Most of the world just doesn't " + - "know it yet."; - Literatures[fn] = new Literature(title, fn, txt); - - title = "Secret Societies"; - fn = LiteratureNames.SecretSocieties; - txt = - "The idea of secret societies has long intrigued the general public by inspiring curiosity, fascination, and " + - "distrust. People have long wondered about who these secret society members are and what they do, with the " + - "most radical of conspiracy theorists claiming that they control everything in the entire world. And while the world " + - "may never know for sure, it is likely that many secret societies do actually exist, even today.

" + - "However, the secret societies of the modern world are nothing like those that (supposedly) existed " + - `decades and centuries ago. The Freemasons, Knights Templar, and ${FactionNames.Illuminati}, while they may have been around ` + - "at the turn of the 21st century, almost assuredly do not exist today. The dominance of the Web in " + - "our everyday lives and the fact that so much of the world is now digital has given rise to a new breed " + - "of secret societies: Internet-based ones.

" + - "Commonly called 'hacker groups', Internet-based secret societies have become well-known in today's " + - `world. Some of these, such as ${FactionNames.TheBlackHand}, are black hat groups that claim they are trying to ` + - `help the oppressed by attacking the elite and powerful. Others, such as ${FactionNames.NiteSec}, are hacktivist groups ` + - "that try to push political and social agendas. Perhaps the most intriguing hacker group " + - `is the mysterious ${FactionNames.BitRunners}, whose purpose still remains unknown.`; - Literatures[fn] = new Literature(title, fn, txt); - - title = "Space: The Failed Frontier"; - fn = LiteratureNames.TheFailedFrontier; - txt = - "Humans have long dreamed about spaceflight. With enduring interest, we were driven to explore " + - "the unknown and discover new worlds. We dreamed about conquering the stars. And in our quest, " + - "we pushed the boundaries of our scientific limits, and then pushed further. Space exploration " + - "lead to the development of many important technologies and new industries.

" + - "But sometime in the middle of the 21st century, all of that changed. Humanity lost its ambitions and " + - "aspirations of exploring the cosmos. The once-large funding for agencies like NASA and the European " + - "Space Agency gradually whittled away until their eventual disbanding in the 2060's. Not even " + - "militaries are fielding flights into space nowadays. The only remnants of the once great mission for cosmic " + - "conquest are the countless satellites in near-earth orbit, used for communications, espionage, " + - "and other corporate interests.

" + - "And as we continue to look at the state of space technology, it becomes more and " + - "more apparent that we will never return to that golden age of space exploration, that " + - "age where everyone dreamed of going beyond earth for the sake of discovery."; - Literatures[fn] = new Literature(title, fn, txt); - - title = "Coded Intelligence: Myth or Reality?"; - fn = LiteratureNames.CodedIntelligence; - txt = - "Tremendous progress has been made in the field of Artificial Intelligence over the past few decades. " + - "Our autonomous vehicles and transportation systems. The electronic personal assistants that control our everyday lives. " + - "Medical, service, and manufacturing robots. All of these are examples of how far AI has come and how much it has " + - "improved our daily lives. However, the question still remains of whether AI will ever be advanced enough to re-create " + - "human intelligence.

" + - `We've certainly come close to artificial intelligence that is similar to humans. For example ${FactionNames.OmniTekIncorporated}'s ` + - "CompanionBot, a robot meant to act as a comforting friend for lonely and grieving people, is eerily human-like " + - "in its appearance, speech, mannerisms, and even movement. However its artificial intelligence isn't the same as " + - "that of humans. Not yet. It doesn't have sentience or self-awareness or consciousness.

" + - "Many neuroscientists believe that we won't ever reach the point of creating artificial human intelligence. 'At the end of " + - "the day, AI comes down to 1's and 0's, while the human brain does not. We'll never see AI that is identical to that of " + - "humans.'"; - Literatures[fn] = new Literature(title, fn, txt); - - title = "Synthetic Muscles"; - fn = LiteratureNames.SyntheticMuscles; - txt = - "Initial versions of synthetic muscles weren't made of anything organic but were actually " + - "crude devices made to mimic human muscle function. Some of the early iterations were actually made of " + - "common materials such as fishing lines and sewing threads due to their high strength for " + - "a cheap cost.

" + - "As technology progressed, however, advances in biomedical engineering paved the way for a new method of " + - "creating synthetic muscles. Instead of creating something that closely imitated the functionality " + - "of human muscle, scientists discovered a way of forcing the human body itself to augment its own " + - "muscle tissue using both synthetic and organic materials. This is typically done using gene therapy " + - "or chemical injections."; - Literatures[fn] = new Literature(title, fn, txt); - - title = "Tensions rise in global tech race"; - fn = LiteratureNames.TensionsInTechRace; - txt = - "Have we entered a new Cold War? Is WWIII just beyond the horizon?

" + - `After rumors came out that ${FactionNames.OmniTekIncorporated} had begun developing advanced robotic supersoldiers, ` + - "geopolitical tensions quickly flared between the USA, Russia, and several Asian superpowers. " + - `In a rare show of cooperation between corporations, ${FactionNames.MegaCorp} and ${FactionNames.ECorp} have ` + - "reportedly launched hundreds of new surveillance and espionage satellites. " + - "Defense contractors such as " + - "DeltaOne and AeroCorp have been working with the CIA and NSA to prepare " + - "for conflict. Meanwhile, the rest of the world sits in earnest " + - "hoping that it never reaches full-scale war. With today's technology " + - "and firepower, a World War would assuredly mean the end of human civilization."; - Literatures[fn] = new Literature(title, fn, txt); - - title = "The Cost of Immortality"; - fn = LiteratureNames.CostOfImmortality; - txt = - "Evolution and advances in medical and augmentation technology has lead to drastic improvements " + - "in human mortality rates. Recent figures show that the life expectancy for humans " + - "that live in a first-world country is about 130 years of age, almost double of what it was " + - "at the turn of the century. However, this increase in average lifespan has had some " + - "significant effects on society and culture.

" + - "Due to longer lifespans and a better quality of life, many adults are holding " + - "off on having kids until much later. As a result, the percentage of youth in " + - "first-world countries has been decreasing, while the number " + - "of senior citizens is significantly increasing.

" + - "Perhaps the most alarming result of all of this is the rapidly shrinking workforce. " + - "Despite the increase in life expectancy, the typical retirement age for " + - "workers in America has remained about the same, meaning a larger and larger " + - "percentage of people in America are retirees. Furthermore, many " + - "young adults are holding off on joining the workforce because they feel that " + - "they have plenty of time left in their lives for employment, and want to " + - "'enjoy life while they're young.' For most industries, this shrinking workforce " + - "is not a major issue as most things are handled by robots anyways. However, " + - "there are still several key industries such as engineering and education " + - "that have not been automated, and these remain in danger to this cultural " + - "phenomenon."; - Literatures[fn] = new Literature(title, fn, txt); - - title = "The Hidden World"; - fn = LiteratureNames.TheHiddenWorld; - txt = - "WAKE UP SHEEPLE

" + - "THE GOVERNMENT DOES NOT EXIST. CORPORATIONS DO NOT RUN SOCIETY

" + - `THE ${FactionNames.Illuminati.toUpperCase()} ARE THE SECRET RULERS OF THE WORLD!

` + - `Yes, the ${FactionNames.Illuminati} of legends. The ancient secret society that controls the entire ` + - "world from the shadows with their invisible hand. The group of the rich and wealthy " + - "that have penetrated every major government, financial agency, and corporation in the last " + - "three hundred years.

" + - "OPEN YOUR EYES

" + - `It was the ${FactionNames.Illuminati} that brought an end to democracy in the world. They are the driving force ` + - "behind everything that happens.

" + - "THEY ARE ALL AROUND YOU

" + - "After destabilizing the world's governments, they are now entering the final stage of their master plan. " + - "They will secretly initiate global crises. Terrorism. Pandemics. World War. And out of the chaos " + - "that ensues they will build their New World Order."; - Literatures[fn] = new Literature(title, fn, txt); - - title = "The New God"; - fn = LiteratureNames.TheNewGod; - txt = - "Everyone has a moment in their life when they wonder about the bigger questions.

" + - "What's the point of all this? What is my purpose?

" + - "Some people dare to think even bigger.

" + - "What will the fate of the human race be?

" + - "We live in an era vastly different from that of 15 or even 20 years ago. We have gone " + - "beyond the limits of humanity. We have stripped ourselves of the tyranny of flesh.

" + - "The Singularity is here. The merging of man and machine. This is where humanity evolves into " + - "something greater. This is our future.

" + - "Embrace it, and you will obey a new god. The God in the Machine."; - Literatures[fn] = new Literature(title, fn, txt); - - title = "The New Triads"; - fn = LiteratureNames.NewTriads; - txt = - "The Triads were an ancient transnational crime syndicate based in China, Hong Kong, and other Asian " + - "territories. They were often considered one of the first and biggest criminal secret societies. " + - "While most of the branches of the Triads have been destroyed over the past few decades, the " + - "crime faction has spawned and inspired a number of other Asian crime organizations over the past few years. " + - `The most notable of these is the ${FactionNames.Tetrads}.

` + - `It is widely believed that the ${FactionNames.Tetrads} are a rogue group that splintered off from the Triads sometime in the ` + - `mid 21st century. The founders of the ${FactionNames.Tetrads}, all of whom were ex-Triad members, believed that the ` + - `Triads were losing their purpose and direction. The ${FactionNames.Tetrads} started off as a small group that mainly engaged ` + - "in fraud and extortion. They were largely unknown until just a few years ago when they took over the illegal " + - "drug trade in all of the major Asian cities. They quickly became the most powerful crime syndicate in the " + - "continent.

" + - `Not much else is known about the ${FactionNames.Tetrads}, or about the efforts the Asian governments and corporations are making ` + - `to take down this large new crime organization. Many believe that the ${FactionNames.Tetrads} have infiltrated the governments ` + - "and powerful corporations in Asia, which has helped facilitate their recent rapid rise."; - Literatures[fn] = new Literature(title, fn, txt); - - title = "The Secret War"; - fn = LiteratureNames.TheSecretWar; - txt = ""; - Literatures[fn] = new Literature(title, fn, txt); -})(); +export const Literatures: Record = { + [LiteratureName.HackersStartingHandbook]: new Literature({ + title: "The Beginner's Guide to Hacking", + filename: LiteratureName.HackersStartingHandbook, + text: + "Some resources:

" + + "Learn to Program

" + + "For Experienced JavaScript Developers: NetscriptJS

" + + "Netscript Documentation

" + + "When starting out, hacking is the most profitable way to earn money and progress. This " + + "is a brief collection of tips/pointers on how to make the most out of your hacking scripts.

" + + "-hack() and grow() both work by percentages. hack() steals a certain percentage of the " + + "money on a server, and grow() increases the amount of money on a server by some percentage (multiplicatively)

" + + "-Because hack() and grow() work by percentages, they are more effective if the target server has a high amount of money. " + + "Therefore, you should try to increase the amount of money on a server (using grow()) to a certain amount before hacking it. Two " + + "important Netscript functions for this are getServerMoneyAvailable() and getServerMaxMoney()

" + + "-Keep security level low. Security level affects everything when hacking. Two important Netscript functions " + + "for this are getServerSecurityLevel() and getServerMinSecurityLevel()

" + + "-Purchase additional servers by visiting 'Alpha Enterprises' in the city. They are relatively cheap " + + "and give you valuable RAM to run more scripts early in the game

" + + "-Prioritize upgrading the RAM on your home computer. This can also be done at 'Alpha Enterprises'

" + + "-Many low level servers have free RAM. You can use this RAM to run your scripts. Use the scp Terminal or " + + "Netscript command to copy your scripts onto these servers and then run them.", + }), + [LiteratureName.CorporationManagementHandbook]: new Literature({ + title: "The Complete Handbook for Creating a Successful Corporation", + filename: LiteratureName.CorporationManagementHandbook, + text: + "Getting Started with Corporations
" + + "To get started, visit the City Hall in Sector-12 in order to create a Corporation. This requires $150b of your own money, " + + "but this $150b will get put into your Corporation's funds. If you're in BitNode 3 you also have option to get seed money from " + + "the government in exchange for 500m shares. Your Corporation can have many different divisions, each in a different Industry. " + + "There are many different types of Industries, each with different properties. To create your first division, click the 'Expand' " + + "(into new Industry) button at the top of the management UI. The Agriculture industry is recommended for your first division.

" + + "The first thing you'll need to do is hire some employees. Employees can be assigned to five different positions. Each position has a " + + "different effect on various aspects of your Corporation. It is recommended to have at least one employee at each position.

" + + "Each industry uses some combination of Materials in order to produce other Materials and/or create Products. Specific information " + + "about this is displayed in each of your divisions' UI.

" + + "Products are special, industry-specific objects. They are different than Materials because you must manually choose to develop them, " + + "and you can choose to develop any number of Products. Developing a Product takes time, but a Product typically generates significantly " + + "more revenue than any Material. Not all industries allow you to create Products. To create a Product, look for a button in the top-left " + + "panel of the division UI (e.g. For the Software Industry, the button says 'Develop Software').

" + + "To get your supply chain system started, purchase the Materials that your industry needs to produce other Materials/Products. This can be " + + "done by clicking the 'Buy' button next to the corresponding Material(s). After you have the required Materials, you will immediately start " + + "production. The amount and quality/effective rating of Materials/Products you produce is based on a variety of factors, such as your employees " + + "and their productivity and the quality of materials used for production.

" + + "Once you start producing Materials/Products, you can sell them in order to start earning revenue. This can be done by clicking the 'Sell' " + + "button next to the corresponding Material or Product. The amount of Material/Product you sell is dependent on a wide variety of different factors. " + + "In order to produce and sell a Product you'll have to fully develop it first.

" + + "These are the basics of getting your Corporation up and running! Now, you can start purchasing upgrades to improve your bottom line. " + + "If you need money, consider looking for seed investors, who will give you money in exchange for stock shares. Otherwise, once you feel " + + "you are ready, take your Corporation public! Once your Corporation goes public, you can no longer find investors. Instead, your Corporation " + + "will be publicly traded and its stock price will change based on how well it's performing financially. In order to make money for yourself you " + + "can set dividends for a solid reliable income or you can sell your stock shares in order to make quick money.

" + + "Tips/Pointers
" + + "-Start with one division, such as Agriculture. Get it profitable on it's own, then expand to a division that consumes/produces " + + "a material that the division you selected produces/consumes.

" + + "-Materials are profitable, but Products are where the real money is, although if the product had a low development budget or is " + + "produced with low quality materials it won't sell well.

" + + "-The 'Smart Supply' upgrade is extremely useful. Consider purchasing it as soon as possible.

" + + "-Purchasing Hardware, Robots, AI Cores, and Real Estate can potentially increase your production. The effects of these depend on " + + "what industry you are in.

" + + "-In order to optimize your production, you will need a good balance of all employee positions, about 1/9 should be interning

" + + "-Quality of materials used for production affects the quality/effective rating of materials/products produced, so vertical integration " + + "is important for high profits.

" + + "-Materials purchased from the open market are always of quality 1.

" + + "-The price at which you can sell your Materials/Products is highly affected by the quality/effective rating

" + + "-When developing a product, different employee positions affect the development process differently, " + + "some improve the development speed, some improve the rating of the finished product.

" + + "-If your employees have low morale or energy, their production will greatly suffer. Having enough interns will make sure those stats get " + + "high and stay high.

" + + "-Don't forget to advertise your company. You won't have any business if nobody knows you.

" + + "-Having company awareness is great, but what's really important is your company's popularity. Try to keep your popularity as high as " + + "possible to see the biggest benefit for your sales

" + + "-Remember, you need to spend money to make money!

" + + "-Corporations do not reset when installing Augmentations, but they do reset when destroying a BitNode", + }), + [LiteratureName.HistoryOfSynthoids]: new Literature({ + title: "A Brief History of Synthoids", + filename: LiteratureName.HistoryOfSynthoids, + text: + "Synthetic androids, or Synthoids for short, are genetically engineered robots and, short of Augmentations, " + + "are composed entirely of organic substances. For this reason, Synthoids are virtually identical to " + + "humans in form, composition, and appearance.

" + + `Synthoids were first designed and manufactured by ${FactionNames.OmniTekIncorporated} sometime around the middle of the century. ` + + "Their original purpose was to be used for manual labor and as emergency responders for disasters. As such, they " + + "were initially programmed only for their specific tasks. Each iteration that followed improved upon the " + + "intelligence and capabilities of the Synthoids. By the 6th iteration, called MK-VI, the Synthoids were " + + `so smart and capable enough of making their own decisions that many argued ${FactionNames.OmniTekIncorporated} had created the first ` + + "sentient AI. These MK-VI Synthoids were produced in mass quantities (estimates up to 50 billion) with the hopes of increasing society's " + + "productivity and bolstering the global economy. Stemming from humanity's desire for technological advancement, optimism " + + "and excitement about the future had never been higher.

" + + "All of that excitement and optimism quickly turned to fear, panic, and dread in 2070, when a terrorist group " + + `called Ascendis Totalis hacked into ${FactionNames.OmniTekIncorporated} and uploaded a rogue AI into several of their Synthoid manufacturing facilities. ` + + `This hack went undetected and for months ${FactionNames.OmniTekIncorporated} unknowingly churned out legions of Synthoids embedded with this ` + + "rogue AI. Then, on December 24th, 2070, Omnica activated dormant protocols in the rogue AI, causing all of the " + + "infected Synthoids to immediately launch a military campaign to seek and destroy all of humanity.

" + + "What ensued was the deadliest conflict in human history. This crisis, now commonly known as the Synthoid Uprising, " + + "resulted in almost ten billion deaths over the course of a year. Despite the nations of the world banding together " + + "to combat the threat, the MK-VI Synthoids were simply stronger, faster, more intelligent, and more adaptable than humans, " + + "outsmarting them at every turn.

" + + `It wasn't until the sacrifice of an elite international military taskforce, called the ${FactionNames.Bladeburners}, that humanity ` + + `was finally able to defeat the Synthoids. The ${FactionNames.Bladeburners}' final act was a suicide bombing mission that ` + + "destroyed a large portion of the MK-VI Synthoids, including many of its leaders. In the following " + + "weeks militaries from around the world were able to round up and shut down the remaining rogue MK-VI Synthoids, ending " + + "the Synthoid Uprising.

" + + `In the aftermath of the bloodshed, the Synthoid Accords were drawn up. These Accords banned ${FactionNames.OmniTekIncorporated} ` + + "from manufacturing any Synthoids beyond the MK-III series. They also banned any other corporation " + + "from constructing androids with advanced, near-sentient AI. MK-VI Synthoids that did not have the rogue Ascendis Totalis " + + "AI were allowed to continue their existence, but they were stripped of all rights and protections as they " + + "were not considered humans. They were also banned from doing anything that may pose a global security threat, such " + + "as working for any military/defense organization or conducting any bioengineering, computing, or robotics related research.

" + + "Unfortunately, many believe that not all of the rogue MK-VI Synthoids from the Uprising were found and destroyed, " + + "and that many of them are blending in as normal humans in society today. In response, many nations have created " + + `${FactionNames.Bladeburners} divisions, special military branches that are tasked with investigating and dealing with any Synthoid threats.

` + + "To this day, tensions still exist between the remaining Synthoids and humans as a result of the Uprising.

" + + "Nobody knows what happened to the terrorist group Ascendis Totalis.", + }), + [LiteratureName.AGreenTomorrow]: new Literature({ + title: "A Green Tomorrow", + filename: LiteratureName.AGreenTomorrow, + text: + "Starting a few decades ago, there was a massive global movement towards the generation of renewable energy in an effort to " + + "combat global warming and climate change. The shift towards renewable energy was a big success, or so it seemed. In 2045 " + + "a staggering 80% of the world's energy came from non-renewable fossil fuels. Now, about three decades later, that " + + "number is down to only 15%. Most of the world's energy now comes from nuclear power and renewable sources such as " + + "solar and geothermal energy. Unfortunately, these efforts were not the huge success that they seem to be.

" + + "Since 2045 primary energy use has soared almost tenfold. This was mainly due to growing urban populations and " + + "the rise of increasingly advanced (and power-hungry) technology that has become ubiquitous in our lives. So, " + + "despite the fact that the percentage of our energy that comes from fossil fuels has drastically decreased, " + + "the total amount of energy we are producing from fossil fuels has actually increased.

" + + "The grim effects of our species' irresponsible use of energy and neglect of our mother world have become increasingly apparent. " + + "Last year a temperature of 190F was recorded in the Death Valley desert, which is over 50% higher than the highest " + + "recorded temperature at the beginning of the century. In the last two decades numerous major cities such as Manhattan, Boston, and " + + "Los Angeles have been partially or fully submerged by rising sea levels. In the present day, over 75% of the world's agriculture is " + + "done in climate-controlled vertical farms, as most traditional farmland has become unusable due to severe climate conditions.

" + + "Despite all of this, the greedy and corrupt corporations that rule the world have done nothing to address these problems that " + + "threaten our species. And so it's up to us, the common people. Each and every one of us can make a difference by doing what " + + "these corporations won't: taking responsibility. If we don't, pretty soon there won't be an Earth left to save. We are " + + "the last hope for a green tomorrow.", + }), + [LiteratureName.AlphaOmega]: new Literature({ + title: "Alpha and Omega", + filename: LiteratureName.AlphaOmega, + text: + "Then we saw a new Heaven and a new Earth, for our first Heaven and Earth had gone away, and our sea was no more. " + + "And we saw a new holy city, new Aeria, coming down out of this new Heaven, prepared as a bride adorned for her husband. " + + "And we heard a loud voice saying, 'Behold, the new dwelling place of the Gods. We will dwell with them, and they " + + "will be our people, and we will be with them as their Gods. We will wipe away every tear from their eyes, and death " + + "shall be no more, neither shall there be mourning, nor crying, nor pain anymore, for the former things " + + "have passed away.'

" + + "And once we were seated on the throne we said 'Behold, I am making all things new.' " + + "Also we said, 'Write this down, for these words are trustworthy and true.' And we said to you, " + + "'It is done! I am the Alpha and the Omega, the beginning and the end. To the thirsty I will give from the spring " + + "of the water of life without payment. The one who conquers will have this heritage, and we will be his God and " + + "he will be our son. But as for the cowardly, the faithless, the detestable, as for murderers, " + + "the sexually immoral, sorcerers, idolaters, and all liars, their portion will be in the lake that " + + "burns with fire and sulfur, for it is the second true death.'", + }), + [LiteratureName.SimulatedReality]: new Literature({ + title: "Are We Living in a Computer Simulation?", + filename: LiteratureName.SimulatedReality, + text: + "The idea that we are living in a virtual world is not new. It's a trope that has " + + "been explored constantly in literature and pop culture. However, it is also a legitimate " + + "scientific hypothesis that many notable physicists and philosophers have debated for years.

" + + "Proponents for this simulated reality theory often point to how advanced our technology has become, " + + "as well as the incredibly fast pace at which it has advanced over the past decades. The amount of computing " + + "power available to us has increased over 100-fold since 2060 due to the development of nanoprocessors and " + + "quantum computers. Artificial Intelligence has advanced to the point where our entire lives are controlled " + + "by robots and machines that handle our day-to-day activities such as autonomous transportation and scheduling. " + + "If we consider the pace at which this technology has advanced and assume that these developments continue, it's " + + "reasonable to assume that at some point in the future our technology would be advanced enough that " + + "we could create simulations that are indistinguishable from reality. However, if continued technological advancement " + + "is a reasonable outcome, then it is very likely that such a scenario has already happened.

" + + "Statistically speaking, somewhere out there in the infinite universe there is an advanced, intelligent species " + + "that already has such technology. Who's to say that they haven't already created such a virtual reality: our own?", + }), + [LiteratureName.BeyondMan]: new Literature({ + title: "Beyond Man", + filename: LiteratureName.BeyondMan, + text: + "Humanity entered a 'transhuman' era a long time ago. And despite the protests and criticisms of many who cried out against " + + "human augmentation at the time, the transhuman movement continued and prospered. Proponents of the movement ignored the critics, " + + "arguing that it was in our inherent nature to better ourselves. To improve. To be more than we were. They claimed that " + + "not doing so would be to go against every living organism's biological purpose: evolution and survival of the fittest.

" + + "And here we are today, with technology that is advanced enough to augment humans to a state that " + + "can only be described as posthuman. But what do we have to show for it when this augmentation " + + "technology is only available to the so-called 'elite'? Are we really better off than before when only 5% of the " + + "world's population has access to this technology? When the powerful corporations and organizations of the world " + + "keep it all to themselves, have we really evolved?

" + + "Augmentation technology has only further increased the divide between the rich and the poor, between the powerful and " + + "the oppressed. We have not become 'more than human'. We have not evolved from nature's original design. We are still the greedy, " + + "corrupted, and evil men that we always were.", + }), + [LiteratureName.BrighterThanTheSun]: new Literature({ + title: "Brighter than the Sun", + filename: LiteratureName.BrighterThanTheSun, + text: + `When people think about the corporations that dominate the East, they typically think of ${FactionNames.KuaiGongInternational}, which ` + + "holds a complete monopoly for manufacturing and commerce in Asia, or Global Pharmaceuticals, the world's largest " + + `drug company, or ${FactionNames.OmniTekIncorporated}, the global leader in intelligent and autonomous robots. But there's one company ` + + "that has seen a rapid rise in the last year and is poised to dominate not only the East, but the entire world: TaiYang Digital.

" + + "TaiYang Digital is a Chinese internet-technology corporation that provides services such as " + + "online advertising, search engines, gaming, media, entertainment, and cloud computing/storage. Its name TaiYang comes from the Chinese word " + + "for 'sun'. In Chinese culture, the sun is a 'yang' symbol " + + "associated with life, heat, masculinity, and heaven.

" + + "The company was founded " + + "less than 5 years ago and is already the third highest valued company in all of Asia. In 2076 it generated a total revenue of " + + "over 10 trillion yuan. Its services are used daily by over a billion people worldwide.

" + + "TaiYang Digital's meteoric rise is extremely surprising in modern society. This sort of growth is " + + "something you'd commonly see in the first half of the century, especially for tech companies. However in " + + "the last two decades the number of corporations has significantly declined as the largest entities " + + `quickly took over the economy. Corporations such as ${FactionNames.ECorp}, ${FactionNames.MegaCorp}, and ${FactionNames.KuaiGongInternational} have established ` + + "such strong monopolies in their market sectors that they have effectively killed off all " + + "of the smaller and new corporations that have tried to start up over the years. This is what makes " + + "the rise of TaiYang Digital so impressive. And if TaiYang continues down this path, then they have " + + "a bright future ahead of them.", + }), + [LiteratureName.DemocracyIsDead]: new Literature({ + title: "Democracy is Dead: The Fall of an Empire", + filename: LiteratureName.DemocracyIsDead, + text: + "They rose from the shadows in the street.
From the places where the oppressed meet.
" + + "Their cries echoed loudly through the air.
As they once did in Tiananmen Square.
" + + "Loudness in the silence, Darkness in the light.
They came forth with power and might.
" + + "Once the beacon of democracy, America was first.
Its pillars of society destroyed and dispersed.
" + + "Soon the cries rose everywhere, with revolt and riot.
Until one day, finally, all was quiet.
" + + "From the ashes rose a new order, corporatocracy was its name.
" + + "Rome, Mongol, Byzantine, all of history is just the same.
" + + "For man will never change in a fundamental way.
" + + "And now democracy is dead, in the USA.", + }), + [LiteratureName.Sector12Crime]: new Literature({ + title: `Figures Show Rising Crime Rates in ${CityName.Sector12}`, + filename: LiteratureName.Sector12Crime, + text: + "A recent study by analytics company Wilson Inc. shows a significant rise " + + `in criminal activity in ${CityName.Sector12}. Perhaps the most alarming part of the statistic ` + + "is that most of the rise is in violent crime such as homicide and assault. According " + + "to the study, the city saw a total of 21,406 reported homicides in 2076, which is over " + + "a 20% increase compared to 2075.

" + + "CIA director David Glarow says it's too early to know " + + "whether these figures indicate the beginning of a sustained increase in crime rates, or whether " + + "the year was just an unfortunate outlier. He states that many intelligence and law enforcement " + + "agents have noticed an increase in organized crime activities, and believes that these figures may " + + `be the result of an uprising from criminal organizations such as ${FactionNames.TheSyndicate} or the ${FactionNames.SlumSnakes}.`, + }), + [LiteratureName.ManAndMachine]: new Literature({ + title: "Man and the Machine", + filename: LiteratureName.ManAndMachine, + text: + "In 2005 Ray Kurzweil popularized his theory of the Singularity. He predicted that the rate " + + "of technological advancement would continue to accelerate faster and faster until one day " + + "machines would be become infinitely more intelligent than humans. This point, called the " + + "Singularity, would result in a drastic transformation of the world as we know it. He predicted " + + "that the Singularity would arrive by 2045. " + + "And yet here we are, more than three decades later, where most would agree that we have not " + + "yet reached a point where computers and machines are vastly more intelligent than we are. So what gives?

" + + "The answer is that we have reached the Singularity, just not in the way we expected. The artificial superintelligence " + + "that was predicted by Kurzweil and others exists in the world today - in the form of Augmentations. " + + "Yes, those Augmentations that the rich and powerful keep to themselves enable humans " + + "to become superintelligent beings. The Singularity did not lead to a world where " + + "our machines are infinitely more intelligent than us, it led to a world " + + "where man and machine can merge to become something greater. Most of the world just doesn't " + + "know it yet.", + }), + [LiteratureName.SecretSocieties]: new Literature({ + title: "Secret Societies", + filename: LiteratureName.SecretSocieties, + text: + "The idea of secret societies has long intrigued the general public by inspiring curiosity, fascination, and " + + "distrust. People have long wondered about who these secret society members are and what they do, with the " + + "most radical of conspiracy theorists claiming that they control everything in the entire world. And while the world " + + "may never know for sure, it is likely that many secret societies do actually exist, even today.

" + + "However, the secret societies of the modern world are nothing like those that (supposedly) existed " + + `decades and centuries ago. The Freemasons, Knights Templar, and ${FactionNames.Illuminati}, while they may have been around ` + + "at the turn of the 21st century, almost assuredly do not exist today. The dominance of the Web in " + + "our everyday lives and the fact that so much of the world is now digital has given rise to a new breed " + + "of secret societies: Internet-based ones.

" + + "Commonly called 'hacker groups', Internet-based secret societies have become well-known in today's " + + `world. Some of these, such as ${FactionNames.TheBlackHand}, are black hat groups that claim they are trying to ` + + `help the oppressed by attacking the elite and powerful. Others, such as ${FactionNames.NiteSec}, are hacktivist groups ` + + "that try to push political and social agendas. Perhaps the most intriguing hacker group " + + `is the mysterious ${FactionNames.BitRunners}, whose purpose still remains unknown.`, + }), + [LiteratureName.TheFailedFrontier]: new Literature({ + title: "Space: The Failed Frontier", + filename: LiteratureName.TheFailedFrontier, + text: + "Humans have long dreamed about spaceflight. With enduring interest, we were driven to explore " + + "the unknown and discover new worlds. We dreamed about conquering the stars. And in our quest, " + + "we pushed the boundaries of our scientific limits, and then pushed further. Space exploration " + + "lead to the development of many important technologies and new industries.

" + + "But sometime in the middle of the 21st century, all of that changed. Humanity lost its ambitions and " + + "aspirations of exploring the cosmos. The once-large funding for agencies like NASA and the European " + + "Space Agency gradually whittled away until their eventual disbanding in the 2060's. Not even " + + "militaries are fielding flights into space nowadays. The only remnants of the once great mission for cosmic " + + "conquest are the countless satellites in near-earth orbit, used for communications, espionage, " + + "and other corporate interests.

" + + "And as we continue to look at the state of space technology, it becomes more and " + + "more apparent that we will never return to that golden age of space exploration, that " + + "age where everyone dreamed of going beyond earth for the sake of discovery.", + }), + [LiteratureName.CodedIntelligence]: new Literature({ + title: "Coded Intelligence: Myth or Reality?", + filename: LiteratureName.CodedIntelligence, + text: + "Tremendous progress has been made in the field of Artificial Intelligence over the past few decades. " + + "Our autonomous vehicles and transportation systems. The electronic personal assistants that control our everyday lives. " + + "Medical, service, and manufacturing robots. All of these are examples of how far AI has come and how much it has " + + "improved our daily lives. However, the question still remains of whether AI will ever be advanced enough to re-create " + + "human intelligence.

" + + `We've certainly come close to artificial intelligence that is similar to humans. For example ${FactionNames.OmniTekIncorporated}'s ` + + "CompanionBot, a robot meant to act as a comforting friend for lonely and grieving people, is eerily human-like " + + "in its appearance, speech, mannerisms, and even movement. However its artificial intelligence isn't the same as " + + "that of humans. Not yet. It doesn't have sentience or self-awareness or consciousness.

" + + "Many neuroscientists believe that we won't ever reach the point of creating artificial human intelligence. 'At the end of " + + "the day, AI comes down to 1's and 0's, while the human brain does not. We'll never see AI that is identical to that of " + + "humans.'", + }), + [LiteratureName.SyntheticMuscles]: new Literature({ + title: "Synthetic Muscles", + filename: LiteratureName.SyntheticMuscles, + text: + "Initial versions of synthetic muscles weren't made of anything organic but were actually " + + "crude devices made to mimic human muscle function. Some of the early iterations were actually made of " + + "common materials such as fishing lines and sewing threads due to their high strength for " + + "a cheap cost.

" + + "As technology progressed, however, advances in biomedical engineering paved the way for a new method of " + + "creating synthetic muscles. Instead of creating something that closely imitated the functionality " + + "of human muscle, scientists discovered a way of forcing the human body itself to augment its own " + + "muscle tissue using both synthetic and organic materials. This is typically done using gene therapy " + + "or chemical injections.", + }), + [LiteratureName.TensionsInTechRace]: new Literature({ + title: "Tensions rise in global tech race", + filename: LiteratureName.TensionsInTechRace, + text: + "Have we entered a new Cold War? Is WWIII just beyond the horizon?

" + + `After rumors came out that ${FactionNames.OmniTekIncorporated} had begun developing advanced robotic supersoldiers, ` + + "geopolitical tensions quickly flared between the USA, Russia, and several Asian superpowers. " + + `In a rare show of cooperation between corporations, ${FactionNames.MegaCorp} and ${FactionNames.ECorp} have ` + + "reportedly launched hundreds of new surveillance and espionage satellites. " + + "Defense contractors such as " + + "DeltaOne and AeroCorp have been working with the CIA and NSA to prepare " + + "for conflict. Meanwhile, the rest of the world sits in earnest " + + "hoping that it never reaches full-scale war. With today's technology " + + "and firepower, a World War would assuredly mean the end of human civilization.", + }), + [LiteratureName.CostOfImmortality]: new Literature({ + title: "The Cost of Immortality", + filename: LiteratureName.CostOfImmortality, + text: + "Evolution and advances in medical and augmentation technology has lead to drastic improvements " + + "in human mortality rates. Recent figures show that the life expectancy for humans " + + "that live in a first-world country is about 130 years of age, almost double of what it was " + + "at the turn of the century. However, this increase in average lifespan has had some " + + "significant effects on society and culture.

" + + "Due to longer lifespans and a better quality of life, many adults are holding " + + "off on having kids until much later. As a result, the percentage of youth in " + + "first-world countries has been decreasing, while the number " + + "of senior citizens is significantly increasing.

" + + "Perhaps the most alarming result of all of this is the rapidly shrinking workforce. " + + "Despite the increase in life expectancy, the typical retirement age for " + + "workers in America has remained about the same, meaning a larger and larger " + + "percentage of people in America are retirees. Furthermore, many " + + "young adults are holding off on joining the workforce because they feel that " + + "they have plenty of time left in their lives for employment, and want to " + + "'enjoy life while they're young.' For most industries, this shrinking workforce " + + "is not a major issue as most things are handled by robots anyways. However, " + + "there are still several key industries such as engineering and education " + + "that have not been automated, and these remain in danger to this cultural " + + "phenomenon.", + }), + [LiteratureName.TheHiddenWorld]: new Literature({ + title: "The Hidden World", + filename: LiteratureName.TheHiddenWorld, + text: + "WAKE UP SHEEPLE

" + + "THE GOVERNMENT DOES NOT EXIST. CORPORATIONS DO NOT RUN SOCIETY

" + + `THE ${FactionNames.Illuminati.toUpperCase()} ARE THE SECRET RULERS OF THE WORLD!

` + + `Yes, the ${FactionNames.Illuminati} of legends. The ancient secret society that controls the entire ` + + "world from the shadows with their invisible hand. The group of the rich and wealthy " + + "that have penetrated every major government, financial agency, and corporation in the last " + + "three hundred years.

" + + "OPEN YOUR EYES

" + + `It was the ${FactionNames.Illuminati} that brought an end to democracy in the world. They are the driving force ` + + "behind everything that happens.

" + + "THEY ARE ALL AROUND YOU

" + + "After destabilizing the world's governments, they are now entering the final stage of their master plan. " + + "They will secretly initiate global crises. Terrorism. Pandemics. World War. And out of the chaos " + + "that ensues they will build their New World Order.", + }), + [LiteratureName.TheNewGod]: new Literature({ + title: "The New God", + filename: LiteratureName.TheNewGod, + text: + "Everyone has a moment in their life when they wonder about the bigger questions.

" + + "What's the point of all this? What is my purpose?

" + + "Some people dare to think even bigger.

" + + "What will the fate of the human race be?

" + + "We live in an era vastly different from that of 15 or even 20 years ago. We have gone " + + "beyond the limits of humanity. We have stripped ourselves of the tyranny of flesh.

" + + "The Singularity is here. The merging of man and machine. This is where humanity evolves into " + + "something greater. This is our future.

" + + "Embrace it, and you will obey a new god. The God in the Machine.", + }), + [LiteratureName.NewTriads]: new Literature({ + title: "The New Triads", + filename: LiteratureName.NewTriads, + text: + "The Triads were an ancient transnational crime syndicate based in China, Hong Kong, and other Asian " + + "territories. They were often considered one of the first and biggest criminal secret societies. " + + "While most of the branches of the Triads have been destroyed over the past few decades, the " + + "crime faction has spawned and inspired a number of other Asian crime organizations over the past few years. " + + `The most notable of these is the ${FactionNames.Tetrads}.

` + + `It is widely believed that the ${FactionNames.Tetrads} are a rogue group that splintered off from the Triads sometime in the ` + + `mid 21st century. The founders of the ${FactionNames.Tetrads}, all of whom were ex-Triad members, believed that the ` + + `Triads were losing their purpose and direction. The ${FactionNames.Tetrads} started off as a small group that mainly engaged ` + + "in fraud and extortion. They were largely unknown until just a few years ago when they took over the illegal " + + "drug trade in all of the major Asian cities. They quickly became the most powerful crime syndicate in the " + + "continent.

" + + `Not much else is known about the ${FactionNames.Tetrads}, or about the efforts the Asian governments and corporations are making ` + + `to take down this large new crime organization. Many believe that the ${FactionNames.Tetrads} have infiltrated the governments ` + + "and powerful corporations in Asia, which has helped facilitate their recent rapid rise.", + }), + [LiteratureName.TheSecretWar]: new Literature({ + title: "The Secret War", + filename: LiteratureName.TheSecretWar, + text: "", + }), +}; diff --git a/src/Literature/data/LiteratureNames.ts b/src/Literature/data/LiteratureNames.ts index fb005f403..d6fd7e7b2 100644 --- a/src/Literature/data/LiteratureNames.ts +++ b/src/Literature/data/LiteratureNames.ts @@ -1,4 +1,4 @@ -export enum LiteratureNames { +export enum LiteratureName { HackersStartingHandbook = "hackers-starting-handbook.lit", CorporationManagementHandbook = "corporation-management-handbook.lit", HistoryOfSynthoids = "history-of-synthoids.lit", diff --git a/src/Message/Message.ts b/src/Message/Message.ts index 30aa419ab..a84a11cb4 100644 --- a/src/Message/Message.ts +++ b/src/Message/Message.ts @@ -1,14 +1,15 @@ -import { MessageFilenames } from "./MessageHelpers"; +import { FilePath, asFilePath } from "../Paths/FilePath"; +import { MessageFilename } from "./MessageHelpers"; export class Message { // Name of Message file - filename: MessageFilenames; + filename: MessageFilename & FilePath; // The text contains in the Message msg: string; - constructor(filename: MessageFilenames, msg: string) { - this.filename = filename; + constructor(filename: MessageFilename, msg: string) { + this.filename = asFilePath(filename); this.msg = msg; } } diff --git a/src/Message/MessageHelpers.tsx b/src/Message/MessageHelpers.tsx index 4f854575d..d1772b530 100644 --- a/src/Message/MessageHelpers.tsx +++ b/src/Message/MessageHelpers.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Message } from "./Message"; import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; import { Router } from "../ui/GameRoot"; -import { Programs } from "../Programs/Programs"; +import { CompletedProgramName } from "../Programs/Programs"; import { Player } from "@player"; import { Page } from "../ui/Router"; import { GetServer } from "../Server/AllServers"; @@ -13,16 +13,15 @@ import { FactionNames } from "../Faction/data/FactionNames"; import { Server } from "../Server/Server"; //Sends message to player, including a pop up -function sendMessage(msg: Message, forced = false): void { +function sendMessage(name: MessageFilename, forced = false): void { if (forced || !Settings.SuppressMessages) { - showMessage(msg.filename); + showMessage(name); } - addMessageToServer(msg); + addMessageToServer(name); } -function showMessage(name: MessageFilenames): void { +function showMessage(name: MessageFilename): void { const msg = Messages[name]; - if (!(msg instanceof Message)) throw new Error("trying to display nonexistent message"); dialogBoxCreate( <> Message received from unknown sender: @@ -37,40 +36,22 @@ function showMessage(name: MessageFilenames): void { } //Adds a message to a server -function addMessageToServer(msg: Message): void { +function addMessageToServer(name: MessageFilename): void { //Short-circuit if the message has already been saved - if (recvd(msg)) { - return; - } - const server = GetServer("home"); - if (server == null) { - throw new Error("The home server doesn't exist. You done goofed."); - } - server.messages.push(msg.filename); + if (recvd(name)) return; + const home = Player.getHomeComputer(); + home.messages.push(name); } //Returns whether the given message has already been received -function recvd(msg: Message): boolean { - const server = GetServer("home"); - if (server == null) { - throw new Error("The home server doesn't exist. You done goofed."); - } - return server.messages.includes(msg.filename); +function recvd(name: MessageFilename): boolean { + const home = Player.getHomeComputer(); + return home.messages.includes(name); } //Checks if any of the 'timed' messages should be sent function checkForMessagesToSend(): void { if (Router.page() === Page.BitVerse) return; - const jumper0 = Messages[MessageFilenames.Jumper0]; - const jumper1 = Messages[MessageFilenames.Jumper1]; - const jumper2 = Messages[MessageFilenames.Jumper2]; - const jumper3 = Messages[MessageFilenames.Jumper3]; - const jumper4 = Messages[MessageFilenames.Jumper4]; - const cybersecTest = Messages[MessageFilenames.CyberSecTest]; - const nitesecTest = Messages[MessageFilenames.NiteSecTest]; - const bitrunnersTest = Messages[MessageFilenames.BitRunnersTest]; - const truthGazer = Messages[MessageFilenames.TruthGazer]; - const redpill = Messages[MessageFilenames.RedPill]; if (Player.hasAugmentation(AugmentationNames.TheRedPill, true)) { //Get the world daemon required hacking level @@ -80,37 +61,36 @@ function checkForMessagesToSend(): void { } //If the daemon can be hacked, send the player icarus.msg if (Player.skills.hacking >= worldDaemon.requiredHackingSkill) { - sendMessage(redpill, Player.sourceFiles.size === 0); + sendMessage(MessageFilename.RedPill, Player.sourceFiles.size === 0); } //If the daemon cannot be hacked, send the player truthgazer.msg a single time. - else if (!recvd(truthGazer)) { - sendMessage(truthGazer); + else if (!recvd(MessageFilename.TruthGazer)) { + sendMessage(MessageFilename.TruthGazer); } - } else if (!recvd(jumper0) && Player.skills.hacking >= 25) { - sendMessage(jumper0); - const flightName = Programs.Flight.name; + } else if (!recvd(MessageFilename.Jumper0) && Player.skills.hacking >= 25) { + sendMessage(MessageFilename.Jumper0); const homeComp = Player.getHomeComputer(); - if (!homeComp.programs.includes(flightName)) { - homeComp.programs.push(flightName); + if (!homeComp.programs.includes(CompletedProgramName.flight)) { + homeComp.programs.push(CompletedProgramName.flight); } - } else if (!recvd(jumper1) && Player.skills.hacking >= 40) { - sendMessage(jumper1); - } else if (!recvd(cybersecTest) && Player.skills.hacking >= 50) { - sendMessage(cybersecTest); - } else if (!recvd(jumper2) && Player.skills.hacking >= 175) { - sendMessage(jumper2); - } else if (!recvd(nitesecTest) && Player.skills.hacking >= 200) { - sendMessage(nitesecTest); - } else if (!recvd(jumper3) && Player.skills.hacking >= 325) { - sendMessage(jumper3); - } else if (!recvd(jumper4) && Player.skills.hacking >= 490) { - sendMessage(jumper4); - } else if (!recvd(bitrunnersTest) && Player.skills.hacking >= 500) { - sendMessage(bitrunnersTest); + } else if (!recvd(MessageFilename.Jumper1) && Player.skills.hacking >= 40) { + sendMessage(MessageFilename.Jumper1); + } else if (!recvd(MessageFilename.CyberSecTest) && Player.skills.hacking >= 50) { + sendMessage(MessageFilename.CyberSecTest); + } else if (!recvd(MessageFilename.Jumper2) && Player.skills.hacking >= 175) { + sendMessage(MessageFilename.Jumper2); + } else if (!recvd(MessageFilename.NiteSecTest) && Player.skills.hacking >= 200) { + sendMessage(MessageFilename.NiteSecTest); + } else if (!recvd(MessageFilename.Jumper3) && Player.skills.hacking >= 325) { + sendMessage(MessageFilename.Jumper3); + } else if (!recvd(MessageFilename.Jumper4) && Player.skills.hacking >= 490) { + sendMessage(MessageFilename.Jumper4); + } else if (!recvd(MessageFilename.BitRunnersTest) && Player.skills.hacking >= 500) { + sendMessage(MessageFilename.BitRunnersTest); } } -export enum MessageFilenames { +export enum MessageFilename { Jumper0 = "j0.msg", Jumper1 = "j1.msg", Jumper2 = "j2.msg", @@ -123,11 +103,11 @@ export enum MessageFilenames { RedPill = "icarus.msg", } -//Reset -const Messages: Record = { +// This type ensures that all members of the MessageFilename enum are valid keys +const Messages: Record = { //jump3R Messages - [MessageFilenames.Jumper0]: new Message( - MessageFilenames.Jumper0, + [MessageFilename.Jumper0]: new Message( + MessageFilename.Jumper0, "I know you can sense it. I know you're searching for it. " + "It's why you spend night after " + "night at your computer. \n\nIt's real, I've seen it. And I can " + @@ -137,8 +117,8 @@ const Messages: Record = { "-jump3R", ), - [MessageFilenames.Jumper1]: new Message( - MessageFilenames.Jumper1, + [MessageFilename.Jumper1]: new Message( + MessageFilename.Jumper1, `Soon you will be contacted by a hacking group known as ${FactionNames.CyberSec}. ` + "They can help you with your search. \n\n" + "You should join them, garner their favor, and " + @@ -147,31 +127,31 @@ const Messages: Record = { "-jump3R", ), - [MessageFilenames.Jumper2]: new Message( - MessageFilenames.Jumper2, + [MessageFilename.Jumper2]: new Message( + MessageFilename.Jumper2, "Do not try to save the world. There is no world to save. If " + "you want to find the truth, worry only about yourself. Ethics and " + `morals will get you killed. \n\nWatch out for a hacking group known as ${FactionNames.NiteSec}.` + "\n\n-jump3R", ), - [MessageFilenames.Jumper3]: new Message( - MessageFilenames.Jumper3, + [MessageFilename.Jumper3]: new Message( + MessageFilename.Jumper3, "You must learn to walk before you can run. And you must " + `run before you can fly. Look for ${FactionNames.TheBlackHand}. \n\n` + "I.I.I.I \n\n-jump3R", ), - [MessageFilenames.Jumper4]: new Message( - MessageFilenames.Jumper4, + [MessageFilename.Jumper4]: new Message( + MessageFilename.Jumper4, "To find what you are searching for, you must understand the bits. " + "The bits are all around us. The runners will help you.\n\n" + "-jump3R", ), //Messages from hacking factions - [MessageFilenames.CyberSecTest]: new Message( - MessageFilenames.CyberSecTest, + [MessageFilename.CyberSecTest]: new Message( + MessageFilename.CyberSecTest, "We've been watching you. Your skills are very impressive. But you're wasting " + "your talents. If you join us, you can put your skills to good use and change " + "the world for the better. If you join us, we can unlock your full potential. \n\n" + @@ -179,8 +159,8 @@ const Messages: Record = { `-${FactionNames.CyberSec}`, ), - [MessageFilenames.NiteSecTest]: new Message( - MessageFilenames.NiteSecTest, + [MessageFilename.NiteSecTest]: new Message( + MessageFilename.NiteSecTest, "People say that the corrupted governments and corporations rule the world. " + "Yes, maybe they do. But do you know who everyone really fears? People " + "like us. Because they can't hide from us. Because they can't fight shadows " + @@ -190,8 +170,8 @@ const Messages: Record = { `\n\n-${FactionNames.NiteSec}`, ), - [MessageFilenames.BitRunnersTest]: new Message( - MessageFilenames.BitRunnersTest, + [MessageFilename.BitRunnersTest]: new Message( + MessageFilename.BitRunnersTest, "We know what you are doing. We know what drives you. We know " + "what you are looking for. \n\n " + "We can help you find the answers.\n\n" + @@ -199,8 +179,8 @@ const Messages: Record = { ), //Messages to guide players to the daemon - [MessageFilenames.TruthGazer]: new Message( - MessageFilenames.TruthGazer, + [MessageFilename.TruthGazer]: new Message( + MessageFilename.TruthGazer, //"THE TRUTH CAN NO LONGER ESCAPE YOUR GAZE" "@&*($#@&__TH3__#@A&#@*)__TRU1H__(*)&*)($#@&()E&R)W&\n" + "%@*$^$()@&$)$*@__CAN__()(@^#)@&@)#__N0__(#@&#)@&@&(\n" + @@ -208,8 +188,8 @@ const Messages: Record = { "()@)#$*%)$#()$#__Y0UR__(*)$#()%(&(%)*!)($__GAZ3__#(", ), - [MessageFilenames.RedPill]: new Message( - MessageFilenames.RedPill, + [MessageFilename.RedPill]: new Message( + MessageFilename.RedPill, //"FIND THE-CAVE" "@)(#V%*N)@(#*)*C)@#%*)*V)@#(*%V@)(#VN%*)@#(*%\n" + ")@B(*#%)@)M#B*%V)____FIND___#$@)#%(B*)@#(*%B)\n" + diff --git a/src/Netscript/WorkerScript.ts b/src/Netscript/WorkerScript.ts index e9613a58d..1bafc81ea 100644 --- a/src/Netscript/WorkerScript.ts +++ b/src/Netscript/WorkerScript.ts @@ -17,6 +17,7 @@ import { BaseServer } from "../Server/BaseServer"; import { ScriptDeath } from "./ScriptDeath"; import { ScriptArg } from "./ScriptArg"; import { NSFull } from "../NetscriptFunctions"; +import { ScriptFilePath } from "src/Paths/ScriptFilePath"; export class WorkerScript { /** Script's arguments */ @@ -60,7 +61,7 @@ export class WorkerScript { loadedFns: Record = {}; /** Filename of script */ - name: string; + name: ScriptFilePath; /** Script's output/return value. Currently not used or implemented */ output = ""; @@ -132,14 +133,6 @@ export class WorkerScript { return script; } - /** - * Returns the script with the specified filename on the specified server, - * or null if it cannot be found - */ - getScriptOnServer(fn: string, server: BaseServer): Script | null { - return server.scripts.get(fn) ?? null; - } - shouldLog(fn: string): boolean { return this.disableLogs[fn] == null; } diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts index f09433594..9fc8b7bf8 100644 --- a/src/NetscriptFunctions.ts +++ b/src/NetscriptFunctions.ts @@ -14,9 +14,7 @@ import { import { netscriptCanGrow, netscriptCanWeaken } from "./Hacking/netscriptCanHack"; import { Terminal } from "./Terminal"; import { Player } from "@player"; -import { Programs } from "./Programs/Programs"; -import { Script } from "./Script/Script"; -import { isScriptFilename } from "./Script/isScriptFilename"; +import { CompletedProgramName } from "./Programs/Programs"; import { PromptEvent } from "./ui/React/PromptManager"; import { GetServer, DeleteServer, AddToAllServers, createUniqueRandomIp } from "./Server/AllServers"; import { @@ -36,8 +34,6 @@ import { } from "./Server/ServerPurchases"; import { Server } from "./Server/Server"; import { influenceStockThroughServerGrow } from "./StockMarket/PlayerInfluencing"; -import { isValidFilePath, removeLeadingSlash } from "./Terminal/DirectoryHelpers"; -import { TextFile, getTextFile, createTextFile } from "./TextFile"; import { runScriptFromScript } from "./NetscriptWorker"; import { killWorkerScript } from "./Netscript/killWorkerScript"; import { workerScripts } from "./Netscript/WorkerScripts"; @@ -56,7 +52,6 @@ import { import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions"; import { LogBoxEvents, LogBoxCloserEvents, LogBoxPositionEvents, LogBoxSizeEvents } from "./ui/React/LogBoxManager"; import { arrayToString } from "./utils/helpers/arrayToString"; -import { isString } from "./utils/helpers/isString"; import { NetscriptGang } from "./NetscriptFunctions/Gang"; import { NetscriptSleeve } from "./NetscriptFunctions/Sleeve"; import { NetscriptExtra } from "./NetscriptFunctions/Extra"; @@ -91,6 +86,11 @@ import { cloneDeep } from "lodash"; import { FactionWorkType } from "./Enums"; import numeral from "numeral"; import { clearPort, peekPort, portHandle, readPort, tryWritePort, writePort } from "./NetscriptPort"; +import { FilePath, resolveFilePath } from "./Paths/FilePath"; +import { hasScriptExtension, resolveScriptFilePath } from "./Paths/ScriptFilePath"; +import { hasTextExtension } from "./Paths/TextFilePath"; +import { ContentFilePath } from "./Paths/ContentFile"; +import { LiteratureName } from "./Literature/data/LiteratureNames"; export const enums: NSEnums = { CityName, @@ -446,22 +446,22 @@ export const ns: InternalAPI = { } const str = helpers.argsToString(args); if (str.startsWith("ERROR") || str.startsWith("FAIL")) { - Terminal.error(`${ctx.workerScript.scriptRef.filename}: ${str}`); + Terminal.error(`${ctx.workerScript.name}: ${str}`); return; } if (str.startsWith("SUCCESS")) { - Terminal.success(`${ctx.workerScript.scriptRef.filename}: ${str}`); + Terminal.success(`${ctx.workerScript.name}: ${str}`); return; } if (str.startsWith("WARN")) { - Terminal.warn(`${ctx.workerScript.scriptRef.filename}: ${str}`); + Terminal.warn(`${ctx.workerScript.name}: ${str}`); return; } if (str.startsWith("INFO")) { - Terminal.info(`${ctx.workerScript.scriptRef.filename}: ${str}`); + Terminal.info(`${ctx.workerScript.name}: ${str}`); return; } - Terminal.print(`${ctx.workerScript.scriptRef.filename}: ${str}`); + Terminal.print(`${ctx.workerScript.name}: ${str}`); }, tprintf: (ctx) => @@ -583,7 +583,7 @@ export const ns: InternalAPI = { helpers.log(ctx, () => `Already have root access to '${server.hostname}'.`); return true; } - if (!Player.hasProgram(Programs.NukeProgram.name)) { + if (!Player.hasProgram(CompletedProgramName.nuke)) { throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the NUKE.exe virus!"); } if (server.openPortCount < server.numOpenPortsRequired) { @@ -600,7 +600,7 @@ export const ns: InternalAPI = { helpers.log(ctx, () => "Cannot be executed on this server."); return false; } - if (!Player.hasProgram(Programs.BruteSSHProgram.name)) { + if (!Player.hasProgram(CompletedProgramName.bruteSsh)) { throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the BruteSSH.exe program!"); } if (!server.sshPortOpen) { @@ -619,7 +619,7 @@ export const ns: InternalAPI = { helpers.log(ctx, () => "Cannot be executed on this server."); return false; } - if (!Player.hasProgram(Programs.FTPCrackProgram.name)) { + if (!Player.hasProgram(CompletedProgramName.ftpCrack)) { throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the FTPCrack.exe program!"); } if (!server.ftpPortOpen) { @@ -638,7 +638,7 @@ export const ns: InternalAPI = { helpers.log(ctx, () => "Cannot be executed on this server."); return false; } - if (!Player.hasProgram(Programs.RelaySMTPProgram.name)) { + if (!Player.hasProgram(CompletedProgramName.relaySmtp)) { throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the relaySMTP.exe program!"); } if (!server.smtpPortOpen) { @@ -657,7 +657,7 @@ export const ns: InternalAPI = { helpers.log(ctx, () => "Cannot be executed on this server."); return false; } - if (!Player.hasProgram(Programs.HTTPWormProgram.name)) { + if (!Player.hasProgram(CompletedProgramName.httpWorm)) { throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the HTTPWorm.exe program!"); } if (!server.httpPortOpen) { @@ -676,7 +676,7 @@ export const ns: InternalAPI = { helpers.log(ctx, () => "Cannot be executed on this server."); return false; } - if (!Player.hasProgram(Programs.SQLInjectProgram.name)) { + if (!Player.hasProgram(CompletedProgramName.sqlInject)) { throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the SQLInject.exe program!"); } if (!server.sqlPortOpen) { @@ -691,30 +691,30 @@ export const ns: InternalAPI = { run: (ctx) => (_scriptname, _thread_or_opt = 1, ..._args) => { - const scriptname = helpers.string(ctx, "scriptname", _scriptname); + const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name); + if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `${path} is not a valid script file path.`); const runOpts = helpers.runOptions(ctx, _thread_or_opt); const args = helpers.scriptArgs(ctx, _args); - const scriptServer = GetServer(ctx.workerScript.hostname); - if (scriptServer == null) { - throw helpers.makeRuntimeErrorMsg(ctx, "Could not find server. This is a bug. Report to dev."); - } + const scriptServer = ctx.workerScript.getServer(); - return runScriptFromScript("run", scriptServer, scriptname, args, ctx.workerScript, runOpts); + return runScriptFromScript("run", scriptServer, path, args, ctx.workerScript, runOpts); }, exec: (ctx) => (_scriptname, _hostname, _thread_or_opt = 1, ..._args) => { - const scriptname = helpers.string(ctx, "scriptname", _scriptname); + const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name); + if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `${path} is not a valid script file path.`); const hostname = helpers.string(ctx, "hostname", _hostname); const runOpts = helpers.runOptions(ctx, _thread_or_opt); const args = helpers.scriptArgs(ctx, _args); const server = helpers.getServer(ctx, hostname); - return runScriptFromScript("exec", server, scriptname, args, ctx.workerScript, runOpts); + return runScriptFromScript("exec", server, path, args, ctx.workerScript, runOpts); }, spawn: (ctx) => (_scriptname, _thread_or_opt = 1, ..._args) => { - const scriptname = helpers.string(ctx, "scriptname", _scriptname); + const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name); + if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `${path} is not a valid script file path.`); const runOpts = helpers.runOptions(ctx, _thread_or_opt); const args = helpers.scriptArgs(ctx, _args); const spawnDelay = 10; @@ -724,10 +724,10 @@ export const ns: InternalAPI = { throw helpers.makeRuntimeErrorMsg(ctx, "Could not find server. This is a bug. Report to dev"); } - return runScriptFromScript("spawn", scriptServer, scriptname, args, ctx.workerScript, runOpts); + return runScriptFromScript("spawn", scriptServer, path, args, ctx.workerScript, runOpts); }, spawnDelay * 1e3); - helpers.log(ctx, () => `Will execute '${scriptname}' in ${spawnDelay} seconds`); + helpers.log(ctx, () => `Will execute '${path}' in ${spawnDelay} seconds`); if (killWorkerScript(ctx.workerScript)) { helpers.log(ctx, () => "Exiting..."); @@ -804,108 +804,72 @@ export const ns: InternalAPI = { killWorkerScript(ctx.workerScript); throw new ScriptDeath(ctx.workerScript); }, - scp: - (ctx) => - (_files, _destination, _source = ctx.workerScript.hostname) => { - const destination = helpers.string(ctx, "destination", _destination); - const source = helpers.string(ctx, "source", _source); - const destServer = helpers.getServer(ctx, destination); - const sourceServ = helpers.getServer(ctx, source); - const files = Array.isArray(_files) ? _files : [_files]; + scp: (ctx) => (_files, _destination, _source) => { + const destination = helpers.string(ctx, "destination", _destination); + const source = helpers.string(ctx, "source", _source ?? ctx.workerScript.hostname); + const destServer = helpers.getServer(ctx, destination); + const sourceServer = helpers.getServer(ctx, source); + const files = Array.isArray(_files) ? _files : [_files]; + const lits: (FilePath & LiteratureName)[] = []; + const contentFiles: ContentFilePath[] = []; + //First loop through filenames to find all errors before moving anything. + for (const file of files) { + // Not a string + if (typeof file !== "string") { + throw helpers.makeRuntimeErrorMsg(ctx, "files should be a string or an array of strings."); + } + if (hasScriptExtension(file) || hasTextExtension(file)) { + const path = resolveScriptFilePath(file, ctx.workerScript.name); + if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filepath: ${file}`); + contentFiles.push(path); + continue; + } + if (!file.endsWith(".lit")) { + throw helpers.makeRuntimeErrorMsg(ctx, "Only works for scripts, .lit and .txt files."); + } + const sanitizedPath = resolveFilePath(file, ctx.workerScript.name); + if (!sanitizedPath || !checkEnum(LiteratureName, sanitizedPath)) { + throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: '${file}'`); + } + lits.push(sanitizedPath); + } - //First loop through filenames to find all errors before moving anything. - for (const file of files) { - // Not a string - if (typeof file !== "string") - throw helpers.makeRuntimeErrorMsg(ctx, "files should be a string or an array of strings."); + let noFailures = true; + // --- Scripts and Text Files--- + for (const contentFilePath of contentFiles) { + const sourceContentFile = sourceServer.getContentFile(contentFilePath); + if (!sourceContentFile) { + helpers.log(ctx, () => `File '${contentFilePath}' does not exist.`); + noFailures = false; + continue; + } + // Overwrite script if it already exists + const result = destServer.writeToContentFile(contentFilePath, sourceContentFile.content); + helpers.log(ctx, () => `Copied file ${contentFilePath} from ${sourceServer} to ${destServer}`); + if (result.overwritten) helpers.log(ctx, () => `Warning: ${contentFilePath} was overwritten on ${destServer}`); + } - // Invalid file name - if (!isValidFilePath(file)) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: '${file}'`); - - // Invalid file type - if (!file.endsWith(".lit") && !isScriptFilename(file) && !file.endsWith(".txt")) { - throw helpers.makeRuntimeErrorMsg(ctx, "Only works for scripts, .lit and .txt files."); - } + // --- Literature Files --- + for (const litFilePath of lits) { + const sourceMessage = sourceServer.messages.find((message) => message === litFilePath); + if (!sourceMessage) { + helpers.log(ctx, () => `File '${litFilePath}' does not exist.`); + noFailures = false; + continue; } - let noFailures = true; - //ts detects files as any[] here even though we would have thrown in the above loop if it wasn't string[] - for (let file of files as string[]) { - // cut off the leading / for files in the root of the server; this assumes that the filename is somewhat normalized and doesn't look like `//file.js` - if (file.startsWith("/") && file.indexOf("/", 1) === -1) file = file.slice(1); - - // Scp for lit files - if (file.endsWith(".lit")) { - const sourceMessage = sourceServ.messages.find((message) => message === file); - if (!sourceMessage) { - helpers.log(ctx, () => `File '${file}' does not exist.`); - noFailures = false; - continue; - } - - const destMessage = destServer.messages.find((message) => message === file); - if (destMessage) { - helpers.log(ctx, () => `File '${file}' was already on '${destServer?.hostname}'.`); - continue; - } - - destServer.messages.push(file); - helpers.log(ctx, () => `File '${file}' copied over to '${destServer?.hostname}'.`); - continue; - } - - // Scp for text files - if (file.endsWith(".txt")) { - const sourceTextFile = sourceServ.textFiles.find((textFile) => textFile.fn === file); - if (!sourceTextFile) { - helpers.log(ctx, () => `File '${file}' does not exist.`); - noFailures = false; - continue; - } - - const destTextFile = destServer.textFiles.find((textFile) => textFile.fn === file); - if (destTextFile) { - destTextFile.text = sourceTextFile.text; - helpers.log(ctx, () => `File '${file}' overwritten on '${destServer?.hostname}'.`); - continue; - } - - const newFile = new TextFile(sourceTextFile.fn, sourceTextFile.text); - destServer.textFiles.push(newFile); - helpers.log(ctx, () => `File '${file}' copied over to '${destServer?.hostname}'.`); - continue; - } - - // Scp for script files - const sourceScript = sourceServ.scripts.get(file); - if (!sourceScript) { - helpers.log(ctx, () => `File '${file}' does not exist.`); - noFailures = false; - continue; - } - - // Overwrite script if it already exists - const destScript = destServer.scripts.get(file); - if (destScript) { - if (destScript.code === sourceScript.code) { - helpers.log(ctx, () => `Identical file '${file}' was already on '${destServer?.hostname}'`); - continue; - } - destScript.code = sourceScript.code; - // Set ramUsage to null in order to force a recalculation prior to next run. - destScript.invalidateModule(); - helpers.log(ctx, () => `WARNING: File '${file}' overwritten on '${destServer?.hostname}'`); - continue; - } - - // Create new script if it does not already exist - const newScript = new Script(file, sourceScript.code, destServer.hostname); - destServer.scripts.set(file, newScript); - helpers.log(ctx, () => `File '${file}' copied over to '${destServer?.hostname}'.`); + const destMessage = destServer.messages.find((message) => message === litFilePath); + if (destMessage) { + helpers.log(ctx, () => `File '${litFilePath}' was already on '${destServer?.hostname}'.`); + continue; } - return noFailures; - }, + destServer.messages.push(litFilePath); + helpers.log(ctx, () => `File '${litFilePath}' copied over to '${destServer?.hostname}'.`); + continue; + } + return noFailures; + }, ls: (ctx) => (_hostname, _substring) => { const hostname = helpers.string(ctx, "hostname", _hostname); const substring = helpers.string(ctx, "substring", _substring ?? ""); @@ -916,7 +880,7 @@ export const ns: InternalAPI = { ...server.messages, ...server.programs, ...server.scripts.keys(), - ...server.textFiles.map((textFile) => textFile.filename), + ...server.textFiles.keys(), ]; if (!substring) return allFilenames.sort(); @@ -1147,7 +1111,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); - if (server.scripts.has(filename)) return true; + if (server.scripts.has(filename) || server.textFiles.has(filename)) return true; for (let i = 0; i < server.programs.length; ++i) { if (filename.toLowerCase() == server.programs[i].toLowerCase()) { return true; @@ -1160,8 +1124,7 @@ export const ns: InternalAPI = { } const contract = server.contracts.find((c) => c.fn.toLowerCase() === filename.toLowerCase()); if (contract) return true; - const txtFile = getTextFile(filename, server); - return txtFile != null; + return false; }, isRunning: (ctx) => @@ -1363,43 +1326,34 @@ export const ns: InternalAPI = { return writePort(portNumber, data); }, write: (ctx) => (_filename, _data, _mode) => { - let filename = helpers.string(ctx, "handle", _filename); + const filepath = resolveFilePath(helpers.string(ctx, "filename", _filename), ctx.workerScript.name); 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 (filename.lastIndexOf("/") === 0) filename = removeLeadingSlash(filename); + if (!filepath) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filepath: ${filepath}`); const server = helpers.getServer(ctx, ctx.workerScript.hostname); - 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; - } - 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 (hasScriptExtension(filepath)) { if (mode === "w") { - txtFile.write(String(data)); - } else { - txtFile.append(String(data)); + server.writeToScriptFile(filepath, data); + return; } + const existingScript = server.scripts.get(filepath); + const existingCode = existingScript ? existingScript.code : ""; + server.writeToScriptFile(filepath, existingCode + data); + return; } - return; + if (!hasTextExtension(filepath)) { + throw helpers.makeRuntimeErrorMsg(ctx, `File path should be a text file or script. ${filepath} is invalid.`); + } + if (mode === "w") { + server.writeToTextFile(filepath, data); + return; + } + const existingTextFile = server.textFiles.get(filepath); + const existingText = existingTextFile?.text ?? ""; + server.writeToTextFile(filepath, mode === "w" ? data : existingText + data); }, tryWritePort: (ctx) => (_portNumber, data) => { const portNumber = helpers.portNumber(ctx, _portNumber); @@ -1416,48 +1370,25 @@ export const ns: InternalAPI = { return readPort(portNumber); }, read: (ctx) => (_filename) => { - const fn = helpers.string(ctx, "filename", _filename); - const server = GetServer(ctx.workerScript.hostname); - if (server == null) { - throw helpers.makeRuntimeErrorMsg(ctx, "Error getting Server. This is a bug. Report to dev."); - } - if (isScriptFilename(fn)) { - // Read from script - const script = ctx.workerScript.getScriptOnServer(fn, server); - if (script == null) { - return ""; - } - return script.code; - } else { - // Read from text file - const txtFile = getTextFile(fn, server); - if (txtFile !== null) { - return txtFile.text; - } else { - return ""; - } - } + const path = resolveFilePath(helpers.string(ctx, "filename", _filename), ctx.workerScript.name); + if (!path || (!hasScriptExtension(path) && !hasTextExtension(path))) return ""; + const server = ctx.workerScript.getServer(); + return server.getContentFile(path)?.content ?? ""; }, peek: (ctx) => (_portNumber) => { const portNumber = helpers.portNumber(ctx, _portNumber); return peekPort(portNumber); }, clear: (ctx) => (_file) => { - const file = helpers.string(ctx, "file", _file); - if (isString(file)) { - // Clear text file - const fn = file; - const server = GetServer(ctx.workerScript.hostname); - if (server == null) { - throw helpers.makeRuntimeErrorMsg(ctx, "Error getting Server. This is a bug. Report to dev."); - } - const txtFile = getTextFile(fn, server); - if (txtFile != null) { - txtFile.write(""); - } - } else { - throw helpers.makeRuntimeErrorMsg(ctx, `Invalid argument: ${file}`); + const path = resolveFilePath(helpers.string(ctx, "file", _file), ctx.workerScript.name); + if (!path || (!hasScriptExtension(path) && !hasTextExtension(path))) { + throw helpers.makeRuntimeErrorMsg(ctx, `Invalid file path or extension: ${_file}`); } + const server = ctx.workerScript.getServer(); + const file = server.getContentFile(path); + if (!file) throw helpers.makeRuntimeErrorMsg(ctx, `${path} does not exist on ${server.hostname}`); + // The content setter handles invalidating script modules where applicable. + file.content = ""; }, clearPort: (ctx) => (_portNumber) => { const portNumber = helpers.portNumber(ctx, _portNumber); @@ -1467,20 +1398,22 @@ export const ns: InternalAPI = { const portNumber = helpers.portNumber(ctx, _portNumber); return portHandle(portNumber); }, - rm: - (ctx) => - (_fn, _hostname = ctx.workerScript.hostname) => { - const fn = helpers.string(ctx, "fn", _fn); - const hostname = helpers.string(ctx, "hostname", _hostname); - const s = helpers.getServer(ctx, hostname); + rm: (ctx) => (_fn, _hostname) => { + const filepath = resolveFilePath(helpers.string(ctx, "fn", _fn), ctx.workerScript.name); + const hostname = helpers.string(ctx, "hostname", _hostname ?? ctx.workerScript.hostname); + const s = helpers.getServer(ctx, hostname); + if (!filepath) { + helpers.log(ctx, () => `Error while parsing filepath ${filepath}`); + return false; + } - const status = s.removeFile(fn); - if (!status.res) { - helpers.log(ctx, () => status.msg + ""); - } + const status = s.removeFile(filepath); + if (!status.res) { + helpers.log(ctx, () => status.msg + ""); + } - return status.res; - }, + return status.res; + }, scriptRunning: (ctx) => (_scriptname, _hostname) => { const scriptname = helpers.string(ctx, "scriptname", _scriptname); const hostname = helpers.string(ctx, "hostname", _hostname); @@ -1506,18 +1439,17 @@ export const ns: InternalAPI = { } return suc; }, - getScriptName: (ctx) => () => { - return ctx.workerScript.name; - }, + getScriptName: (ctx) => () => ctx.workerScript.name, getScriptRam: (ctx) => (_scriptname, _hostname) => { - const scriptname = helpers.string(ctx, "scriptname", _scriptname); + const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name); + if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `Could not parse file path ${_scriptname}`); const hostname = helpers.string(ctx, "hostname", _hostname ?? ctx.workerScript.hostname); const server = helpers.getServer(ctx, hostname); - const script = server.scripts.get(scriptname); + const script = server.scripts.get(path); if (!script) return 0; const ramUsage = script.getRamUsage(server.scripts); if (!ramUsage) { - helpers.log(ctx, () => `Could not calculate ram usage for ${scriptname} on ${hostname}.`); + helpers.log(ctx, () => `Could not calculate ram usage for ${path} on ${hostname}.`); return 0; } return ramUsage; @@ -1704,26 +1636,22 @@ export const ns: InternalAPI = { }, wget: (ctx) => async (_url, _target, _hostname) => { const url = helpers.string(ctx, "url", _url); - const target = helpers.string(ctx, "target", _target); + const target = resolveFilePath(helpers.string(ctx, "target", _target), ctx.workerScript.name); const hostname = _hostname ? helpers.string(ctx, "hostname", _hostname) : ctx.workerScript.hostname; - if (!isScriptFilename(target) && !target.endsWith(".txt")) { + const server = helpers.getServer(ctx, hostname); + if (!target || (!hasTextExtension(target) && !hasScriptExtension(target))) { helpers.log(ctx, () => `Invalid target file: '${target}'. Must be a script or text file.`); return Promise.resolve(false); } - const s = helpers.getServer(ctx, hostname); return new Promise(function (resolve) { $.get( url, function (data) { let res; - if (isScriptFilename(target)) { - res = s.writeToScriptFile(target, data); + if (hasScriptExtension(target)) { + res = server.writeToScriptFile(target, data); } else { - res = s.writeToTextFile(target, data); - } - if (!res.success) { - helpers.log(ctx, () => "Failed."); - return resolve(false); + res = server.writeToTextFile(target, data); } if (res.overwritten) { helpers.log(ctx, () => `Successfully retrieved content and overwrote '${target}' on '${hostname}'`); @@ -1790,59 +1718,34 @@ export const ns: InternalAPI = { }, mv: (ctx) => (_host, _source, _destination) => { const hostname = helpers.string(ctx, "host", _host); - const source = helpers.string(ctx, "source", _source); - const destination = helpers.string(ctx, "destination", _destination); + const server = helpers.getServer(ctx, hostname); + const sourcePath = resolveFilePath(helpers.string(ctx, "source", _source)); + if (!sourcePath) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid source filename: '${_source}'`); + const destinationPath = resolveFilePath(helpers.string(ctx, "destination", _destination), sourcePath); + if (!destinationPath) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid destination filename: '${destinationPath}'`); - if (!isValidFilePath(source)) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: '${source}'`); - if (!isValidFilePath(destination)) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: '${destination}'`); - - const source_is_txt = source.endsWith(".txt"); - const dest_is_txt = destination.endsWith(".txt"); - - if (!isScriptFilename(source) && !source_is_txt) + if ( + (!hasTextExtension(sourcePath) && !hasScriptExtension(sourcePath)) || + (!hasTextExtension(destinationPath) && !hasScriptExtension(destinationPath)) + ) { throw helpers.makeRuntimeErrorMsg(ctx, `'mv' can only be used on scripts and text files (.txt)`); - if (source_is_txt != dest_is_txt) - throw helpers.makeRuntimeErrorMsg(ctx, `Source and destination files must have the same type`); - - if (source === destination) { + } + if (sourcePath === destinationPath) { + helpers.log(ctx, () => "WARNING: Did nothing, source and destination paths were the same."); return; } - - const server = helpers.getServer(ctx, hostname); - - if (!source_is_txt && server.isRunning(source)) - throw helpers.makeRuntimeErrorMsg(ctx, `Cannot use 'mv' on a script that is running`); - - interface File { - filename: string; + const sourceContentFile = server.getContentFile(sourcePath); + if (!sourceContentFile) { + throw helpers.makeRuntimeErrorMsg(ctx, `Source text file ${sourcePath} does not exist on ${hostname}`); } - let source_file: File | undefined; - let dest_file: File | undefined; - - 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 (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(); - } - server.removeFile(source); - } else { - source_file.filename = destination; - if (source_file instanceof Script) source_file.invalidateModule(); + const success = sourceContentFile.deleteFromServer(server); + if (success) { + const { overwritten } = server.writeToContentFile(destinationPath, sourceContentFile.content); + if (overwritten) helpers.log(ctx, () => `WARNING: Overwriting file ${destinationPath} on ${hostname}`); + helpers.log(ctx, () => `Moved ${sourcePath} to ${destinationPath} on ${hostname}`); + return; } + helpers.log(ctx, () => `ERROR: Failed. Was unable to remove file ${sourcePath} from its original location.`); }, flags: Flags, ...NetscriptExtra(), diff --git a/src/NetscriptFunctions/Formulas.ts b/src/NetscriptFunctions/Formulas.ts index 154d44aa5..aa0830b8e 100644 --- a/src/NetscriptFunctions/Formulas.ts +++ b/src/NetscriptFunctions/Formulas.ts @@ -26,7 +26,7 @@ import { calculateGrowTime, calculateWeakenTime, } from "../Hacking"; -import { Programs } from "../Programs/Programs"; +import { CompletedProgramName } from "../Programs/Programs"; import { Formulas as IFormulas, Player as IPlayer, Person as IPerson } from "@nsdefs"; import { calculateRespectGain, @@ -55,7 +55,7 @@ import { findCrime } from "../Crime/CrimeHelpers"; export function NetscriptFormulas(): InternalAPI { const checkFormulasAccess = function (ctx: NetscriptContext): void { - if (!player.hasProgram(Programs.Formulas.name)) { + if (!player.hasProgram(CompletedProgramName.formulas)) { throw helpers.makeRuntimeErrorMsg(ctx, `Requires Formulas.exe to run.`); } }; diff --git a/src/NetscriptFunctions/Singularity.ts b/src/NetscriptFunctions/Singularity.ts index e775d1735..22f316bee 100644 --- a/src/NetscriptFunctions/Singularity.ts +++ b/src/NetscriptFunctions/Singularity.ts @@ -6,7 +6,6 @@ import { StaticAugmentations } from "../Augmentation/StaticAugmentations"; import { augmentationExists, installAugmentations } from "../Augmentation/AugmentationHelpers"; import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; import { CONSTANTS } from "../Constants"; -import { isString } from "../utils/helpers/isString"; import { RunningScript } from "../Script/RunningScript"; import { calculateAchievements } from "../Achievements/Achievements"; @@ -52,6 +51,8 @@ import { calculateCrimeWorkStats } from "../Work/Formulas"; import { findEnumMember } from "../utils/helpers/enum"; import { Engine } from "../engine"; import { checkEnum } from "../utils/helpers/enum"; +import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath"; +import { root } from "../Paths/Directory"; export function NetscriptSingularity(): InternalAPI { const getAugmentation = function (ctx: NetscriptContext, name: string): Augmentation { @@ -76,7 +77,7 @@ export function NetscriptSingularity(): InternalAPI { return company; }; - const runAfterReset = function (cbScript: string | null = null) { + const runAfterReset = function (cbScript: ScriptFilePath) { //Run a script after reset if (!cbScript) return; const home = Player.getHomeComputer(); @@ -87,7 +88,9 @@ export function NetscriptSingularity(): InternalAPI { return Terminal.error(`Attempted to launch ${cbScript} after reset but could not calculate ram usage.`); } const ramAvailable = home.maxRam - home.ramUsed; - if (ramUsage > ramAvailable + 0.001) return; + if (ramUsage > ramAvailable + 0.001) { + return Terminal.error(`Attempted to launch ${cbScript} after reset but there was not enough ram.`); + } // Start script with no args and 1 thread (default). const runningScriptObj = new RunningScript(script, ramUsage, []); startWorkerScript(runningScriptObj, home); @@ -190,7 +193,7 @@ export function NetscriptSingularity(): InternalAPI { const res = purchaseAugmentation(aug, fac, true); helpers.log(ctx, () => res); - if (isString(res) && res.startsWith("You purchased")) { + if (res.startsWith("You purchased")) { Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain * 10); return true; } else { @@ -199,7 +202,10 @@ export function NetscriptSingularity(): InternalAPI { }, softReset: (ctx) => (_cbScript) => { helpers.checkSingularityAccess(ctx); - const cbScript = _cbScript ? helpers.string(ctx, "cbScript", _cbScript) : ""; + const cbScript = _cbScript + ? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name) + : false; + if (cbScript === null) throw helpers.makeRuntimeErrorMsg(ctx, `Could not resolve file path: ${_cbScript}`); helpers.log(ctx, () => "Soft resetting. This will cause this script to be killed"); installAugmentations(true); @@ -207,7 +213,10 @@ export function NetscriptSingularity(): InternalAPI { }, installAugmentations: (ctx) => (_cbScript) => { helpers.checkSingularityAccess(ctx); - const cbScript = _cbScript ? helpers.string(ctx, "cbScript", _cbScript) : ""; + const cbScript = _cbScript + ? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name) + : false; + if (cbScript === null) throw helpers.makeRuntimeErrorMsg(ctx, `Could not resolve file path: ${_cbScript}`); if (Player.queuedAugmentations.length === 0) { helpers.log(ctx, () => "You do not have any Augmentations to be installed."); @@ -502,7 +511,7 @@ export function NetscriptSingularity(): InternalAPI { Player.getCurrentServer().isConnectedTo = false; Player.currentServer = Player.getHomeComputer().hostname; Player.getCurrentServer().isConnectedTo = true; - Terminal.setcwd("/"); + Terminal.setcwd(root); return true; } @@ -515,7 +524,7 @@ export function NetscriptSingularity(): InternalAPI { Player.getCurrentServer().isConnectedTo = false; Player.currentServer = target.hostname; Player.getCurrentServer().isConnectedTo = true; - Terminal.setcwd("/"); + Terminal.setcwd(root); return true; } } @@ -526,7 +535,7 @@ export function NetscriptSingularity(): InternalAPI { Player.getCurrentServer().isConnectedTo = false; Player.currentServer = target.hostname; Player.getCurrentServer().isConnectedTo = true; - Terminal.setcwd("/"); + Terminal.setcwd(root); return true; } @@ -1189,54 +1198,49 @@ export function NetscriptSingularity(): InternalAPI { } return item.price; }, - b1tflum3: - (ctx) => - (_nextBN, _callbackScript = "") => { - helpers.checkSingularityAccess(ctx); - const nextBN = helpers.number(ctx, "nextBN", _nextBN); - const callbackScript = helpers.string(ctx, "callbackScript", _callbackScript); - helpers.checkSingularityAccess(ctx); - enterBitNode(true, Player.bitNodeN, nextBN); - if (callbackScript) - setTimeout(() => { - runAfterReset(callbackScript); - }, 0); - }, - destroyW0r1dD43m0n: - (ctx) => - (_nextBN, _callbackScript = "") => { - helpers.checkSingularityAccess(ctx); - const nextBN = helpers.number(ctx, "nextBN", _nextBN); - if (nextBN > 13 || nextBN < 1 || !Number.isInteger(nextBN)) { - throw new Error(`Invalid bitnode specified: ${_nextBN}`); - } - const callbackScript = helpers.string(ctx, "callbackScript", _callbackScript); + b1tflum3: (ctx) => (_nextBN, _cbScript) => { + helpers.checkSingularityAccess(ctx); + const nextBN = helpers.number(ctx, "nextBN", _nextBN); + const cbScript = _cbScript + ? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name) + : false; + if (cbScript === null) throw helpers.makeRuntimeErrorMsg(ctx, `Could not resolve file path: ${_cbScript}`); + enterBitNode(true, Player.bitNodeN, nextBN); + if (cbScript) setTimeout(() => runAfterReset(cbScript), 0); + }, + destroyW0r1dD43m0n: (ctx) => (_nextBN, _cbScript) => { + helpers.checkSingularityAccess(ctx); + const nextBN = helpers.number(ctx, "nextBN", _nextBN); + if (nextBN > 13 || nextBN < 1 || !Number.isInteger(nextBN)) { + throw new Error(`Invalid bitnode specified: ${_nextBN}`); + } + const cbScript = _cbScript + ? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name) + : false; + if (cbScript === null) throw helpers.makeRuntimeErrorMsg(ctx, `Could not resolve file path: ${_cbScript}`); - const wd = GetServer(SpecialServers.WorldDaemon); - if (!(wd instanceof Server)) throw new Error("WorldDaemon was not a normal server. This is a bug contact dev."); - const hackingRequirements = () => { - if (Player.skills.hacking < wd.requiredHackingSkill) return false; - if (!wd.hasAdminRights) return false; - return true; - }; - const bladeburnerRequirements = () => { - if (!Player.bladeburner) return false; - return Player.bladeburner.blackops[BlackOperationNames.OperationDaedalus]; - }; + const wd = GetServer(SpecialServers.WorldDaemon); + if (!(wd instanceof Server)) throw new Error("WorldDaemon was not a normal server. This is a bug contact dev."); + const hackingRequirements = () => { + if (Player.skills.hacking < wd.requiredHackingSkill) return false; + if (!wd.hasAdminRights) return false; + return true; + }; + const bladeburnerRequirements = () => { + if (!Player.bladeburner) return false; + return Player.bladeburner.blackops[BlackOperationNames.OperationDaedalus]; + }; - if (!hackingRequirements() && !bladeburnerRequirements()) { - helpers.log(ctx, () => "Requirements not met to destroy the world daemon"); - return; - } + if (!hackingRequirements() && !bladeburnerRequirements()) { + helpers.log(ctx, () => "Requirements not met to destroy the world daemon"); + return; + } - wd.backdoorInstalled = true; - calculateAchievements(); - enterBitNode(false, Player.bitNodeN, nextBN); - if (callbackScript) - setTimeout(() => { - runAfterReset(callbackScript); - }, 0); - }, + wd.backdoorInstalled = true; + calculateAchievements(); + enterBitNode(false, Player.bitNodeN, nextBN); + if (cbScript) setTimeout(() => runAfterReset(cbScript), 0); + }, getCurrentWork: () => () => { if (!Player.currentWork) return null; return Player.currentWork.APICopy(); diff --git a/src/NetscriptJSEvaluator.ts b/src/NetscriptJSEvaluator.ts index bd1e78bd8..d96f14981 100644 --- a/src/NetscriptJSEvaluator.ts +++ b/src/NetscriptJSEvaluator.ts @@ -7,8 +7,8 @@ import { parse } from "acorn"; import { LoadedModule, ScriptURL, ScriptModule } from "./Script/LoadedModule"; import { Script } from "./Script/Script"; -import { removeLeadingSlash } from "./Terminal/DirectoryHelpers"; -import { ScriptFilename, scriptFilenameFromImport } from "./Types/strings"; +import { ScriptFilePath, resolveScriptFilePath } from "./Paths/ScriptFilePath"; +import { root } from "./Paths/Directory"; // Acorn type def is straight up incomplete so we have to fill with our own. export type Node = any; @@ -47,7 +47,7 @@ const cleanup = new FinalizationRegistry((mapKey: string) => { } }); -export function compile(script: Script, scripts: Map): Promise { +export function compile(script: Script, scripts: Map): Promise { // Return the module if it already exists if (script.mod) return script.mod.module; @@ -76,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: Map, 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); @@ -122,10 +122,11 @@ function generateLoadedModule(script: Script, scripts: Map b.start - a.start); - let newCode = script.code; + let newCode = script.code as string; // Loop through each node and replace the script name with a blob url. for (const node of importNodes) { - const filename = scriptFilenameFromImport(node.filename); + const filename = resolveScriptFilePath(node.filename, root, ".js"); + if (!filename) throw new Error(`Failed to parse import: ${node.filename}`); // Find the corresponding script. const importedScript = scripts.get(filename); @@ -146,7 +147,7 @@ function generateLoadedModule(script: Script, scripts: Map { diff --git a/src/NetscriptWorker.ts b/src/NetscriptWorker.ts index dde2c2396..640169c63 100644 --- a/src/NetscriptWorker.ts +++ b/src/NetscriptWorker.ts @@ -32,7 +32,8 @@ import { simple as walksimple } from "acorn-walk"; import { Terminal } from "./Terminal"; import { ScriptArg } from "@nsdefs"; import { handleUnknownError, CompleteRunOptions } from "./Netscript/NetscriptHelpers"; -import { scriptFilenameFromImport } from "./Types/strings"; +import { resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath"; +import { root } from "./Paths/Directory"; export const NetscriptPorts: Map = new Map(); @@ -146,7 +147,7 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c throw new Error("Failed to find underlying Server object for script"); } - function getScript(scriptName: string): Script | null { + function getScript(scriptName: ScriptFilePath): Script | null { return server.scripts.get(scriptName) ?? null; } @@ -157,11 +158,10 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c walksimple(ast, { ImportDeclaration: (node: Node) => { hasImports = true; - const scriptName = scriptFilenameFromImport(node.source.value, true); + const scriptName = resolveScriptFilePath(node.source.value, root, ".script"); + if (!scriptName) throw new Error("'Import' failed due to invalid path: " + scriptName); const script = getScript(scriptName); - if (script == null) { - throw new Error("'Import' failed due to invalid script: " + scriptName); - } + if (!script) throw new Error("'Import' failed due to script not found: " + scriptName); const scriptAst = parse(script.code, { ecmaVersion: 9, allowReserved: true, @@ -380,16 +380,11 @@ export function loadAllRunningScripts(): void { export function runScriptFromScript( caller: string, host: BaseServer, - scriptname: string, + scriptname: ScriptFilePath, args: ScriptArg[], workerScript: WorkerScript, runOpts: CompleteRunOptions, ): number { - /* Very inefficient, TODO change data structures so that finding script & checking if it's already running does - * not require iterating through all scripts and comparing names/args. This is a big part of slowdown when - * running a large number of scripts. */ - - // Find the script, fail if it doesn't exist. const script = host.scripts.get(scriptname); if (!script) { workerScript.log(caller, () => `Could not find script '${scriptname}' on '${host.hostname}'`); diff --git a/src/Paths/ContentFile.ts b/src/Paths/ContentFile.ts new file mode 100644 index 000000000..862e1a580 --- /dev/null +++ b/src/Paths/ContentFile.ts @@ -0,0 +1,19 @@ +import type { BaseServer } from "../Server/BaseServer"; +import { ScriptFilePath } from "./ScriptFilePath"; +import { TextFilePath } from "./TextFilePath"; + +/** Provide a common interface for accessing script and text files */ +export type ContentFilePath = ScriptFilePath | TextFilePath; +export interface ContentFile { + filename: ContentFilePath; + content: string; + download: () => void; + deleteFromServer: (server: BaseServer) => boolean; +} +export type ContentFileMap = Map; + +/** Generator function to allow iterating through all content files on a server */ +export function* allContentFiles(server: BaseServer): Generator<[ContentFilePath, ContentFile], void, undefined> { + yield* server.scripts; + yield* server.textFiles; +} diff --git a/src/Paths/ContractFilePath.ts b/src/Paths/ContractFilePath.ts new file mode 100644 index 000000000..2be83b3e3 --- /dev/null +++ b/src/Paths/ContractFilePath.ts @@ -0,0 +1,17 @@ +import { Directory } from "./Directory"; +import { FilePath, resolveFilePath } from "./FilePath"; + +/** Filepath with the additional constraint of having a .cct extension */ +type WithContractExtension = string & { __fileType: "Contract" }; +export type ContractFilePath = FilePath & WithContractExtension; + +/** Check extension only */ +export function hasContractExtension(path: string): path is WithContractExtension { + return path.endsWith(".cct"); +} + +/** Sanitize a player input, resolve any relative paths, and for imports add the correct extension if missing */ +export function resolveContractFilePath(path: string, base = "" as FilePath | Directory): ContractFilePath | null { + const result = resolveFilePath(path, base); + return result && hasContractExtension(result) ? result : null; +} diff --git a/src/Paths/Directory.ts b/src/Paths/Directory.ts new file mode 100644 index 000000000..0b579edce --- /dev/null +++ b/src/Paths/Directory.ts @@ -0,0 +1,106 @@ +import { allContentFiles } from "./ContentFile"; +import type { BaseServer } from "../Server/BaseServer"; +import { FilePath } from "./FilePath"; + +/** The directory part of a BasicFilePath. Everything up to and including the last / + * e.g. "file.js" => "", or "dir/file.js" => "dir/", or "../test.js" => "../" */ +export type BasicDirectory = string & { __type: "Directory" }; + +/** Type for use in Directory and FilePath to indicate path is absolute */ +export type AbsolutePath = string & { __absolutePath: true }; + +/** A directory path that is also absolute. Absolute Rules (FilePath and DirectoryPath): + * 1. Specific directory names "." and ".." are disallowed */ +export type Directory = BasicDirectory & AbsolutePath; +export const root = "" as Directory; + +/** Regex string for a single valid path character. Invalid characters: + * /: Invalid because it is the directory separator. It's allowed in the directory part, but only as the separator. + * *, ?, [, and ]: Invalid in actual paths because they are used for globbing. + * \: Invalid to avoid confusion with an escape character + * (space): Invalid to avoid confusion with terminal command separator */ +export const oneValidCharacter = "[^\\\\\\/* ?\\[\\]]"; + +/** Regex string for matching the directory part of a valid filepath */ +export const directoryRegexString = `^(?(?:${oneValidCharacter}+\\\/)*)`; + +/** Actual RegExp for validating that an entire string is a BasicDirectory */ +const basicDirectoryRegex = new RegExp(directoryRegexString + "$"); + +export function isDirectoryPath(path: string): path is BasicDirectory { + return basicDirectoryRegex.test(path); +} + +/** Regex to check if relative parts are included (directory names ".." and ".") */ +const relativeRegex = /(?:^|\/)\.{1,2}\//; +export function isAbsolutePath(path: string): path is AbsolutePath { + return !relativeRegex.test(path); +} + +/** Sanitize and resolve a player-provided potentially-relative path to an absolute path. + * @param path The player-provided directory path, e.g. 2nd argument for terminal cp command + * @param base The starting directory. */ +export function resolveDirectory(path: string, base = root): Directory | null { + // Always use absolute path if player-provided path starts with / + if (path.startsWith("/")) { + base = root; + path = path.substring(1); + } + // Add a trailing / if it is not present + if (path && !path.endsWith("/")) path = path + "/"; + if (!isDirectoryPath(path)) return null; + return resolveValidatedDirectory(path, base); +} + +/** Resolve an already-typechecked directory path with respect to an absolute path */ +export function resolveValidatedDirectory(relative: BasicDirectory, absolute: Directory): Directory | null { + if (!relative) return absolute; + const relativeArray = relative.split(/(?<=\/)/); + const absoluteArray = absolute.split(/(?<=\/)/).filter(Boolean); + while (relativeArray.length) { + // We just checked length so we know this is a string + const nextDir = relativeArray.shift() as string; + switch (nextDir) { + case "./": + break; + case "../": + if (!absoluteArray.length) return null; + absoluteArray.pop(); + break; + default: + absoluteArray.push(nextDir); + } + } + return absoluteArray.join("") as Directory; +} + +/** Check if a given directory exists on a server, e.g. for checking if the player can CD into that directory */ +export function directoryExistsOnServer(directory: Directory, server: BaseServer): boolean { + for (const scriptFilePath of server.scripts.keys()) if (scriptFilePath.startsWith(directory)) return true; + for (const textFilePath of server.textFiles.keys()) if (textFilePath.startsWith(directory)) return true; + return false; +} + +/** Returns the first directory, other than root, in a file path. If in root, returns null. */ +export function getFirstDirectoryInPath(path: FilePath | Directory): Directory | null { + const firstSlashIndex = path.indexOf("/"); + if (firstSlashIndex === -1) return null; + return path.substring(0, firstSlashIndex + 1) as Directory; +} + +export function getAllDirectories(server: BaseServer): Set { + const dirSet = new Set([root]); + function peel(path: FilePath | Directory) { + const lastSlashIndex = path.lastIndexOf("/", path.length - 2); + if (lastSlashIndex === -1) return; + const newDir = path.substring(0, lastSlashIndex + 1) as Directory; + if (dirSet.has(newDir)) return; + dirSet.add(newDir); + peel(newDir); + } + for (const [filename] of allContentFiles(server)) peel(filename); + return dirSet; +} + +// This is to validate the assertion earlier that root is in fact a Directory +if (!isDirectoryPath(root) || !isAbsolutePath(root)) throw new Error("Root failed to validate"); diff --git a/src/Paths/FilePath.ts b/src/Paths/FilePath.ts new file mode 100644 index 000000000..1c1e7abbb --- /dev/null +++ b/src/Paths/FilePath.ts @@ -0,0 +1,83 @@ +import { + Directory, + isAbsolutePath, + BasicDirectory, + resolveValidatedDirectory, + oneValidCharacter, + directoryRegexString, + AbsolutePath, +} from "./Directory"; +/** Filepath Rules: + * 1. File extension cannot contain a "/" + * 2. Last character before the extension cannot be a "/" as this would be a blank filename + * 3. Must not contain a leading "/" + * 4. Directory names cannot be 0-length (no "//") + * 5. The characters *, ?, [, and ] cannot exist in the filepath*/ +type BasicFilePath = string & { __type: "FilePath" }; + +/** A file path that is also an absolute path. Additional absolute rules: + * 1. Specific directory names "." and ".." are disallowed + * Absoluteness is typechecked with isAbsolutePath in DirectoryPath.ts */ +export type FilePath = BasicFilePath & AbsolutePath; + +// Capturing group named file which captures the entire filename part of a file path. +const filenameRegexString = `(?${oneValidCharacter}+\\.${oneValidCharacter}+)$`; + +/** Regex made of the two above regex parts to test for a whole valid filepath. */ +const basicFilePathRegex = new RegExp(directoryRegexString + filenameRegexString) as RegExp & { + exec: (path: string) => null | { groups: { directory: BasicDirectory; file: FilePath } }; +}; + +/** Simple validation function with no modification. Can be combined with isAbsolutePath to get a real FilePath */ +export function isFilePath(path: string): path is BasicFilePath { + return basicFilePathRegex.test(path); +} + +export function asFilePath(input: T): T & FilePath { + if (isFilePath(input) && isAbsolutePath(input)) return input; + throw new Error(`${input} failed to validate as a FilePath.`); +} + +export function getFilenameOnly(path: T): T & FilePath { + const start = path.lastIndexOf("/") + 1; + return path.substring(start) as T & FilePath; +} + +/** Validate while also capturing and returning directory and file parts */ +function getFileParts(path: string): { directory: BasicDirectory; file: FilePath } | null { + const result = basicFilePathRegex.exec(path) as null | { groups: { directory: BasicDirectory; file: FilePath } }; + return result ? result.groups : null; +} + +/** Sanitizes a player input and resolves a relative file path to an absolute one. + * @param path The player-provided path string. Can include relative directories. + * @param base The absolute base for resolving a relative path. */ +export function resolveFilePath(path: string, base = "" as FilePath | Directory): FilePath | null { + if (isAbsolutePath(path)) { + if (path.startsWith("/")) path = path.substring(1); + // Because we modified the string since checking absoluteness, we have to assert that it's still absolute here. + return isFilePath(path) ? (path as FilePath) : null; + } + // Turn base into a DirectoryName in case it was not + base = getBaseDirectory(base); + const pathParts = getFileParts(path); + if (!pathParts) return null; + const directory = resolveValidatedDirectory(pathParts.directory, base); + // Have to specifically check null here instead of truthiness, because empty string is a valid DirectoryPath + return directory === null ? null : combinePath(directory, pathParts.file); +} + +/** Remove the file part from an absolute path (FilePath or DirectoryPath - no modification is done for a DirectoryPath) */ +function getBaseDirectory(path: FilePath | Directory): Directory { + return path.replace(/[^\/\*]+\.[^\/\*]+$/, "") as Directory; +} +/** Combine an absolute DirectoryPath and FilePath to create a new FilePath */ +export function combinePath(directory: Directory, file: T): T { + // Preserves the specific file type because the filepart is preserved. + return (directory + file) as T; +} + +export function removeDirectoryFromPath(directory: Directory, path: FilePath): FilePath | null { + if (!path.startsWith(directory)) return null; + return path.substring(directory.length) as FilePath; +} diff --git a/src/Paths/GlobbedFiles.ts b/src/Paths/GlobbedFiles.ts new file mode 100644 index 000000000..bad633156 --- /dev/null +++ b/src/Paths/GlobbedFiles.ts @@ -0,0 +1,40 @@ +import { BaseServer } from "../Server/BaseServer"; +import { root } from "./Directory"; +import { ContentFileMap, allContentFiles } from "./ContentFile"; + +/** Search for files (Script and TextFile only) that match a given glob pattern + * @param pattern The glob pattern. Supported glob characters are * and ? + * @param server The server to search using the pattern + * @param currentDir The base directory. Optional, defaults to root. Also forced to root if the pattern starts with / + * @returns A map keyed by paths (ScriptFilePath or TextFilePath) with files as values (Script or TextFile). */ +export function getGlobbedFileMap(pattern: string, server: BaseServer, currentDir = root): ContentFileMap { + const map: ContentFileMap = new Map(); + // A pattern starting with / indicates wanting to match things from root directory instead of current. + if (pattern.startsWith("/")) { + currentDir = root; + pattern = pattern.substring(1); + } + // Only search within the current directory + pattern = currentDir + pattern; + + // This globbing supports * and ?. + // * will be turned into regex .* + // ? will be turned into regex . + // All other special regex characters in the pattern will need to be escaped out. + const charsToEscape = new Set(["/", "\\", "^", "$", ".", "|", "+", "(", ")", "[", "{"]); + pattern = pattern + .split("") + .map((char) => { + if (char === "*") return ".*"; + if (char === "?") return "."; + if (charsToEscape.has(char)) return "\\" + char; + return char; + }) + .join(""); + const regex = new RegExp(`^${pattern}$`); + + for (const [path, file] of allContentFiles(server)) { + if (regex.test(path)) map.set(path, file); + } + return map; +} diff --git a/src/Paths/ProgramFilePath.ts b/src/Paths/ProgramFilePath.ts new file mode 100644 index 000000000..54a45a549 --- /dev/null +++ b/src/Paths/ProgramFilePath.ts @@ -0,0 +1,24 @@ +import { Directory, isAbsolutePath } from "./Directory"; +import { FilePath, isFilePath, resolveFilePath } from "./FilePath"; + +/** Filepath with the additional constraint of having a .cct extension */ +type WithProgramExtension = string & { __fileType: "Program" }; +export type ProgramFilePath = FilePath & WithProgramExtension; + +/** Check extension only. Programs are a bit different than others because of incomplete programs. */ +export function hasProgramExtension(path: string): path is WithProgramExtension { + if (path.endsWith(".exe")) return true; + const extension = path.substring(path.indexOf(".")); + return /^\.exe-[0-9]{1-2}\.[0-9]{2}%-INC$/.test(extension); +} + +/** Sanitize a player input, resolve any relative paths, and for imports add the correct extension if missing */ +export function resolveProgramFilePath(path: string, base = "" as FilePath | Directory): ProgramFilePath | null { + const result = resolveFilePath(path, base); + return result && hasProgramExtension(result) ? result : null; +} + +export function asProgramFilePath(path: T): T & ProgramFilePath { + if (isFilePath(path) && hasProgramExtension(path) && isAbsolutePath(path)) return path; + throw new Error(`${path} failed to validate as a ProgramFilePath.`); +} diff --git a/src/Paths/ScriptFilePath.ts b/src/Paths/ScriptFilePath.ts new file mode 100644 index 000000000..b365ef2e0 --- /dev/null +++ b/src/Paths/ScriptFilePath.ts @@ -0,0 +1,30 @@ +import { Directory } from "./Directory"; +import { FilePath, resolveFilePath } from "./FilePath"; + +/** Type for just checking a .js extension with no other verification*/ +type WithScriptExtension = string & { __fileType: "Script" }; +/** Type for a valid absolute FilePath with a script extension */ +export type ScriptFilePath = FilePath & WithScriptExtension; + +/** Valid extensions. Used for some error messaging. */ +export type ScriptExtension = ".js" | ".script"; +export const validScriptExtensions: ScriptExtension[] = [".js", ".script"]; + +/** Sanitize a player input, resolve any relative paths, and for imports add the correct extension if missing + * @param path The player-provided path to a file. Can contain relative parts. + * @param base The base + */ +export function resolveScriptFilePath( + path: string, + base = "" as FilePath | Directory, + extensionToAdd?: ScriptExtension, +): ScriptFilePath | null { + if (extensionToAdd && !path.endsWith(extensionToAdd)) path = path + extensionToAdd; + const result = resolveFilePath(path, base); + return result && hasScriptExtension(result) ? result : null; +} + +/** Just check extension */ +export function hasScriptExtension(path: string): path is WithScriptExtension { + return validScriptExtensions.some((extension) => path.endsWith(extension)); +} diff --git a/src/Paths/TextFilePath.ts b/src/Paths/TextFilePath.ts new file mode 100644 index 000000000..85c22065f --- /dev/null +++ b/src/Paths/TextFilePath.ts @@ -0,0 +1,17 @@ +import { Directory } from "./Directory"; +import { FilePath, resolveFilePath } from "./FilePath"; + +/** Filepath with the additional constraint of having a .js extension */ +type WithTextExtension = string & { __fileType: "Text" }; +export type TextFilePath = FilePath & WithTextExtension; + +/** Check extension only */ +export function hasTextExtension(path: string): path is WithTextExtension { + return path.endsWith(".txt"); +} + +/** Sanitize a player input, resolve any relative paths, and for imports add the correct extension if missing */ +export function resolveTextFilePath(path: string, base = "" as FilePath | Directory): TextFilePath | null { + const result = resolveFilePath(path, base); + return result && hasTextExtension(result) ? result : null; +} diff --git a/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts b/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts index 59fd504c7..f14e7d085 100644 --- a/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts +++ b/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts @@ -11,7 +11,7 @@ import { CompanyPositions } from "../../Company/CompanyPositions"; import { CompanyPosition } from "../../Company/CompanyPosition"; import * as posNames from "../../Company/data/JobTracks"; import { CONSTANTS } from "../../Constants"; -import { Programs } from "../../Programs/Programs"; +import { CompletedProgramName } from "../../Programs/Programs"; import { Exploit } from "../../Exploits/Exploit"; import { Faction } from "../../Faction/Faction"; import { Factions } from "../../Faction/Factions"; @@ -45,6 +45,7 @@ import { isCompanyWork } from "../../Work/CompanyWork"; import { serverMetadata } from "../../Server/data/servers"; import type { PlayerObject } from "./PlayerObject"; +import { ProgramFilePath } from "src/Paths/ProgramFilePath"; export function init(this: PlayerObject): void { /* Initialize Player's home computer */ @@ -60,7 +61,7 @@ export function init(this: PlayerObject): void { this.currentServer = SpecialServers.Home; AddToAllServers(t_homeComp); - this.getHomeComputer().programs.push(Programs.NukeProgram.name); + this.getHomeComputer().programs.push(CompletedProgramName.nuke); } export function prestigeAugmentation(this: PlayerObject): void { @@ -171,18 +172,9 @@ export function calculateSkillProgress(this: PlayerObject, exp: number, mult = 1 return calculateSkillProgressF(exp, mult); } -export function hasProgram(this: PlayerObject, programName: string): boolean { +export function hasProgram(this: PlayerObject, programName: CompletedProgramName | ProgramFilePath): boolean { const home = this.getHomeComputer(); - if (home == null) { - return false; - } - - for (let i = 0; i < home.programs.length; ++i) { - if (programName.toLowerCase() == home.programs[i].toLowerCase()) { - return true; - } - } - return false; + return home.programs.includes(programName); } export function setMoney(this: PlayerObject, money: number): void { diff --git a/src/Prestige.ts b/src/Prestige.ts index 16f8e75e1..e1e584186 100755 --- a/src/Prestige.ts +++ b/src/Prestige.ts @@ -6,7 +6,7 @@ import { AugmentationNames } from "./Augmentation/data/AugmentationNames"; import { initBitNodeMultipliers } from "./BitNode/BitNode"; import { Companies, initCompanies } from "./Company/Companies"; import { resetIndustryResearchTrees } from "./Corporation/IndustryData"; -import { Programs } from "./Programs/Programs"; +import { CompletedProgramName } from "./Programs/Programs"; import { Factions, initFactions } from "./Faction/Factions"; import { joinFaction } from "./Faction/FactionHelpers"; import { updateHashManagerCapacity } from "./Hacknet/HacknetHelpers"; @@ -14,7 +14,7 @@ import { prestigeWorkerScripts } from "./NetscriptWorker"; import { Player } from "@player"; import { recentScripts } from "./Netscript/RecentScripts"; import { resetPidCounter } from "./Netscript/Pid"; -import { LiteratureNames } from "./Literature/data/LiteratureNames"; +import { LiteratureName } from "./Literature/data/LiteratureNames"; import { GetServer, AddToAllServers, initForeignServers, prestigeAllServers } from "./Server/AllServers"; import { prestigeHomeComputer } from "./Server/ServerHelpers"; @@ -53,20 +53,20 @@ export function prestigeAugmentation(): void { prestigeHomeComputer(homeComp); if (augmentationExists(AugmentationNames.Neurolink) && Player.hasAugmentation(AugmentationNames.Neurolink, true)) { - homeComp.programs.push(Programs.FTPCrackProgram.name); - homeComp.programs.push(Programs.RelaySMTPProgram.name); + homeComp.programs.push(CompletedProgramName.ftpCrack); + homeComp.programs.push(CompletedProgramName.relaySmtp); } if (augmentationExists(AugmentationNames.CashRoot) && Player.hasAugmentation(AugmentationNames.CashRoot, true)) { Player.setMoney(1e6); - homeComp.programs.push(Programs.BruteSSHProgram.name); + homeComp.programs.push(CompletedProgramName.bruteSsh); } if (augmentationExists(AugmentationNames.PCMatrix) && Player.hasAugmentation(AugmentationNames.PCMatrix, true)) { - homeComp.programs.push(Programs.DeepscanV1.name); - homeComp.programs.push(Programs.AutoLink.name); + homeComp.programs.push(CompletedProgramName.deepScan1); + homeComp.programs.push(CompletedProgramName.autoLink); } if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) { - homeComp.programs.push(Programs.Formulas.name); + homeComp.programs.push(CompletedProgramName.formulas); } // Re-create foreign servers @@ -123,7 +123,8 @@ export function prestigeAugmentation(): void { // BitNode 3: Corporatocracy if (Player.bitNodeN === 3) { - homeComp.messages.push(LiteratureNames.CorporationManagementHandbook); + // Easiest way to comply with type constraint, instead of revalidating the enum member's file path + homeComp.messages.push(LiteratureName.CorporationManagementHandbook); } // Cancel Bladeburner action @@ -247,15 +248,13 @@ export function prestigeSourceFile(flume: boolean): void { initCompanies(); if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) { - homeComp.programs.push(Programs.Formulas.name); + homeComp.programs.push(CompletedProgramName.formulas); } - console.log(Player.bitNodeN); - dialogBoxCreate("hello"); // BitNode 3: Corporatocracy if (Player.bitNodeN === 3) { - console.log("why isn't the dialogbox happening?"); - homeComp.messages.push(LiteratureNames.CorporationManagementHandbook); + // Easiest way to comply with type constraint, instead of revalidating the enum member's file path + homeComp.messages.push(LiteratureName.CorporationManagementHandbook); dialogBoxCreate( "You received a copy of the Corporation Management Handbook on your home computer. " + "Read it if you need help getting started with Corporations!", diff --git a/src/Programs/Program.ts b/src/Programs/Program.ts index 6480eff48..8c8e36b95 100644 --- a/src/Programs/Program.ts +++ b/src/Programs/Program.ts @@ -1,3 +1,5 @@ +import type { CompletedProgramName } from "./Programs"; +import { ProgramFilePath, asProgramFilePath } from "../Paths/ProgramFilePath"; import { BaseServer } from "../Server/BaseServer"; export interface IProgramCreate { @@ -6,14 +8,19 @@ export interface IProgramCreate { time: number; tooltip: string; } +type ProgramConstructorParams = { + name: CompletedProgramName; + create: IProgramCreate | null; + run: (args: string[], server: BaseServer) => void; +}; export class Program { - name = ""; + name: ProgramFilePath & CompletedProgramName; create: IProgramCreate | null; run: (args: string[], server: BaseServer) => void; - constructor(name: string, create: IProgramCreate | null, run: (args: string[], server: BaseServer) => void) { - this.name = name; + constructor({ name, create, run }: ProgramConstructorParams) { + this.name = asProgramFilePath(name); this.create = create; this.run = run; } diff --git a/src/Programs/ProgramHelpers.ts b/src/Programs/ProgramHelpers.ts index bb604e7a3..513b9a41d 100644 --- a/src/Programs/ProgramHelpers.ts +++ b/src/Programs/ProgramHelpers.ts @@ -1,4 +1,4 @@ -import { Programs } from "./Programs"; +import { CompletedProgramName, Programs } from "./Programs"; import { Program } from "./Program"; import { Player } from "@player"; @@ -6,18 +6,18 @@ import { Player } from "@player"; //Returns the programs this player can create. export function getAvailableCreatePrograms(): Program[] { const programs: Program[] = []; - for (const key of Object.keys(Programs)) { + for (const program of Object.values(CompletedProgramName)) { + const create = Programs[program].create; // Non-creatable program - const create = Programs[key].create; if (create == null) continue; // Already has program - if (Player.hasProgram(Programs[key].name)) continue; + if (Player.hasProgram(program)) continue; // Does not meet requirements if (!create.req()) continue; - programs.push(Programs[key]); + programs.push(Programs[program]); } return programs; diff --git a/src/Programs/Programs.ts b/src/Programs/Programs.ts index 5c457de21..b5319575b 100644 --- a/src/Programs/Programs.ts +++ b/src/Programs/Programs.ts @@ -1,9 +1,319 @@ import { Program } from "./Program"; -import { programsMetadata } from "./data/ProgramsMetadata"; +import { CONSTANTS } from "../Constants"; +import { BaseServer } from "../Server/BaseServer"; +import { Server } from "../Server/Server"; +import { Terminal } from "../Terminal"; +import { Player } from "@player"; +import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions"; +import { GetServer } from "../Server/AllServers"; +import { formatMoney } from "../ui/formatNumber"; +import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; +import { BitFlumeEvent } from "../BitNode/ui/BitFlumeModal"; +import { calculateHackingTime, calculateGrowTime, calculateWeakenTime } from "../Hacking"; +import { FactionNames } from "../Faction/data/FactionNames"; -export const Programs: Record = {}; -export function initPrograms() { - for (const params of programsMetadata) { - Programs[params.key] = new Program(params.name, params.create, params.run); - } +function requireHackingLevel(lvl: number) { + return function () { + return Player.skills.hacking + Player.skills.intelligence / 2 >= lvl; + }; } + +function bitFlumeRequirements() { + return function () { + return Player.sourceFiles.size > 0 && Player.skills.hacking >= 1; + }; +} + +export enum CompletedProgramName { + nuke = "NUKE.exe", + bruteSsh = "BruteSSH.exe", + ftpCrack = "FTPCrack.exe", + relaySmtp = "relaySMTP.exe", + httpWorm = "HTTPWorm.exe", + sqlInject = "SQLInject.exe", + deepScan1 = "DeepscanV1.exe", + deepScan2 = "DeepscanV2.exe", + serverProfiler = "ServerProfiler.exe", + autoLink = "AutoLink.exe", + formulas = "Formulas.exe", + bitFlume = "b1t_flum3.exe", + flight = "fl1ght.exe", +} + +export const Programs: Record = { + [CompletedProgramName.nuke]: new Program({ + name: CompletedProgramName.nuke, + create: { + level: 1, + tooltip: "This virus is used to gain root access to a machine if enough ports are opened.", + req: requireHackingLevel(1), + time: CONSTANTS.MillisecondsPerFiveMinutes, + }, + run: (_args: string[], server: BaseServer): void => { + if (!(server instanceof Server)) { + Terminal.error("Cannot nuke this kind of server."); + return; + } + if (server.hasAdminRights) { + Terminal.print("You already have root access to this computer. There is no reason to run NUKE.exe"); + Terminal.print("You can now run scripts on this server."); + return; + } + if (server.openPortCount >= server.numOpenPortsRequired) { + server.hasAdminRights = true; + Terminal.print("NUKE successful! Gained root access to " + server.hostname); + Terminal.print("You can now run scripts on this server."); + return; + } + + Terminal.print("NUKE unsuccessful. Not enough ports have been opened"); + }, + }), + [CompletedProgramName.bruteSsh]: new Program({ + name: CompletedProgramName.bruteSsh, + create: { + level: 50, + tooltip: "This program executes a brute force attack that opens SSH ports", + req: requireHackingLevel(50), + time: CONSTANTS.MillisecondsPerFiveMinutes * 2, + }, + run: (_args: string[], server: BaseServer): void => { + if (!(server instanceof Server)) { + Terminal.error("Cannot run BruteSSH.exe on this kind of server."); + return; + } + if (server.sshPortOpen) { + Terminal.print("SSH Port (22) is already open!"); + return; + } + + server.sshPortOpen = true; + Terminal.print("Opened SSH Port(22)!"); + server.openPortCount++; + }, + }), + [CompletedProgramName.ftpCrack]: new Program({ + name: CompletedProgramName.ftpCrack, + create: { + level: 100, + tooltip: "This program cracks open FTP ports", + req: requireHackingLevel(100), + time: CONSTANTS.MillisecondsPerHalfHour, + }, + run: (_args: string[], server: BaseServer): void => { + if (!(server instanceof Server)) { + Terminal.error("Cannot run FTPCrack.exe on this kind of server."); + return; + } + if (server.ftpPortOpen) { + Terminal.print("FTP Port (21) is already open!"); + return; + } + + server.ftpPortOpen = true; + Terminal.print("Opened FTP Port (21)!"); + server.openPortCount++; + }, + }), + [CompletedProgramName.relaySmtp]: new Program({ + name: CompletedProgramName.relaySmtp, + create: { + level: 250, + tooltip: "This program opens SMTP ports by redirecting data", + req: requireHackingLevel(250), + time: CONSTANTS.MillisecondsPer2Hours, + }, + run: (_args: string[], server: BaseServer): void => { + if (!(server instanceof Server)) { + Terminal.error("Cannot run relaySMTP.exe on this kind of server."); + return; + } + if (server.smtpPortOpen) { + Terminal.print("SMTP Port (25) is already open!"); + return; + } + + server.smtpPortOpen = true; + Terminal.print("Opened SMTP Port (25)!"); + server.openPortCount++; + }, + }), + [CompletedProgramName.httpWorm]: new Program({ + name: CompletedProgramName.httpWorm, + create: { + level: 500, + tooltip: "This virus opens up HTTP ports", + req: requireHackingLevel(500), + time: CONSTANTS.MillisecondsPer4Hours, + }, + run: (_args: string[], server: BaseServer): void => { + if (!(server instanceof Server)) { + Terminal.error("Cannot run HTTPWorm.exe on this kind of server."); + return; + } + if (server.httpPortOpen) { + Terminal.print("HTTP Port (80) is already open!"); + return; + } + + server.httpPortOpen = true; + Terminal.print("Opened HTTP Port (80)!"); + server.openPortCount++; + }, + }), + [CompletedProgramName.sqlInject]: new Program({ + name: CompletedProgramName.sqlInject, + create: { + level: 750, + tooltip: "This virus opens SQL ports", + req: requireHackingLevel(750), + time: CONSTANTS.MillisecondsPer8Hours, + }, + run: (_args: string[], server: BaseServer): void => { + if (!(server instanceof Server)) { + Terminal.error("Cannot run SQLInject.exe on this kind of server."); + return; + } + if (server.sqlPortOpen) { + Terminal.print("SQL Port (1433) is already open!"); + return; + } + + server.sqlPortOpen = true; + Terminal.print("Opened SQL Port (1433)!"); + server.openPortCount++; + }, + }), + [CompletedProgramName.deepScan1]: new Program({ + name: CompletedProgramName.deepScan1, + create: { + level: 75, + tooltip: "This program allows you to use the scan-analyze command with a depth up to 5", + req: requireHackingLevel(75), + time: CONSTANTS.MillisecondsPerQuarterHour, + }, + run: (): void => { + Terminal.print("This executable cannot be run."); + Terminal.print("DeepscanV1.exe lets you run 'scan-analyze' with a depth up to 5."); + }, + }), + [CompletedProgramName.deepScan2]: new Program({ + name: CompletedProgramName.deepScan2, + create: { + level: 400, + tooltip: "This program allows you to use the scan-analyze command with a depth up to 10", + req: requireHackingLevel(400), + time: CONSTANTS.MillisecondsPer2Hours, + }, + run: (): void => { + Terminal.print("This executable cannot be run."); + Terminal.print("DeepscanV2.exe lets you run 'scan-analyze' with a depth up to 10."); + }, + }), + [CompletedProgramName.serverProfiler]: new Program({ + name: CompletedProgramName.serverProfiler, + create: { + level: 75, + tooltip: "This program is used to display hacking and Netscript-related information about servers", + req: requireHackingLevel(75), + time: CONSTANTS.MillisecondsPerHalfHour, + }, + run: (args: string[]): void => { + if (args.length !== 1) { + Terminal.error("Must pass a server hostname or IP as an argument for ServerProfiler.exe"); + return; + } + + const targetServer = GetServer(args[0]); + if (targetServer == null) { + Terminal.error("Invalid server IP/hostname"); + return; + } + + if (!(targetServer instanceof Server)) { + Terminal.error(`ServerProfiler.exe can only be run on normal servers.`); + return; + } + + Terminal.print(targetServer.hostname + ":"); + Terminal.print("Server base security level: " + targetServer.baseDifficulty); + Terminal.print("Server current security level: " + targetServer.hackDifficulty); + Terminal.print("Server growth rate: " + targetServer.serverGrowth); + Terminal.print( + `Netscript hack() execution time: ${convertTimeMsToTimeElapsedString( + calculateHackingTime(targetServer, Player) * 1000, + true, + )}`, + ); + Terminal.print( + `Netscript grow() execution time: ${convertTimeMsToTimeElapsedString( + calculateGrowTime(targetServer, Player) * 1000, + true, + )}`, + ); + Terminal.print( + `Netscript weaken() execution time: ${convertTimeMsToTimeElapsedString( + calculateWeakenTime(targetServer, Player) * 1000, + true, + )}`, + ); + }, + }), + [CompletedProgramName.autoLink]: new Program({ + name: CompletedProgramName.autoLink, + create: { + level: 25, + tooltip: "This program allows you to directly connect to other servers through the 'scan-analyze' command", + req: requireHackingLevel(25), + time: CONSTANTS.MillisecondsPerQuarterHour, + }, + run: (): void => { + Terminal.print("This executable cannot be run."); + Terminal.print("AutoLink.exe lets you automatically connect to other servers when using 'scan-analyze'."); + Terminal.print("When using scan-analyze, click on a server's hostname to connect to it."); + }, + }), + [CompletedProgramName.formulas]: new Program({ + name: CompletedProgramName.formulas, + create: { + level: 1000, + tooltip: "This program allows you to use the formulas API", + req: requireHackingLevel(1000), + time: CONSTANTS.MillisecondsPer4Hours, + }, + run: (): void => { + Terminal.print("This executable cannot be run."); + Terminal.print("Formulas.exe lets you use the formulas API."); + }, + }), + [CompletedProgramName.bitFlume]: new Program({ + name: CompletedProgramName.bitFlume, + create: { + level: 1, + tooltip: "This program creates a portal to the BitNode Nexus (allows you to restart and switch BitNodes)", + req: bitFlumeRequirements(), + time: CONSTANTS.MillisecondsPerFiveMinutes / 20, + }, + run: (): void => { + BitFlumeEvent.emit(); + }, + }), + [CompletedProgramName.flight]: new Program({ + name: CompletedProgramName.flight, + create: null, + run: (): void => { + const numAugReq = BitNodeMultipliers.DaedalusAugsRequirement; + const fulfilled = + Player.augmentations.length >= numAugReq && Player.money > 1e11 && Player.skills.hacking >= 2500; + if (!fulfilled) { + Terminal.print(`Augmentations: ${Player.augmentations.length} / ${numAugReq}`); + Terminal.print(`Money: ${formatMoney(Player.money)} / $100b`); + Terminal.print(`Hacking skill: ${Player.skills.hacking} / 2500`); + return; + } + + Terminal.print("We will contact you."); + Terminal.print(`-- ${FactionNames.Daedalus} --`); + }, + }), +}; diff --git a/src/Programs/data/ProgramsMetadata.ts b/src/Programs/data/ProgramsMetadata.ts deleted file mode 100644 index f71921550..000000000 --- a/src/Programs/data/ProgramsMetadata.ts +++ /dev/null @@ -1,323 +0,0 @@ -import { IProgramCreate } from "../Program"; -import { CONSTANTS } from "../../Constants"; -import { BaseServer } from "../../Server/BaseServer"; -import { Server } from "../../Server/Server"; -import { Terminal } from "../../Terminal"; -import { Player } from "@player"; -import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; -import { GetServer } from "../../Server/AllServers"; -import { formatMoney } from "../../ui/formatNumber"; -import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers"; -import { BitFlumeEvent } from "../../BitNode/ui/BitFlumeModal"; -import { calculateHackingTime, calculateGrowTime, calculateWeakenTime } from "../../Hacking"; -import { FactionNames } from "../../Faction/data/FactionNames"; - -function requireHackingLevel(lvl: number) { - return function () { - return Player.skills.hacking + Player.skills.intelligence / 2 >= lvl; - }; -} - -function bitFlumeRequirements() { - return function () { - return Player.sourceFiles.size > 0 && Player.skills.hacking >= 1; - }; -} - -interface IProgramCreationParams { - key: string; - name: string; - create: IProgramCreate | null; - run: (args: string[], server: BaseServer) => void; -} - -export const programsMetadata: IProgramCreationParams[] = [ - { - key: "NukeProgram", - name: "NUKE.exe", - create: { - level: 1, - tooltip: "This virus is used to gain root access to a machine if enough ports are opened.", - req: requireHackingLevel(1), - time: CONSTANTS.MillisecondsPerFiveMinutes, - }, - run: (_args: string[], server: BaseServer): void => { - if (!(server instanceof Server)) { - Terminal.error("Cannot nuke this kind of server."); - return; - } - if (server.hasAdminRights) { - Terminal.print("You already have root access to this computer. There is no reason to run NUKE.exe"); - Terminal.print("You can now run scripts on this server."); - return; - } - if (server.openPortCount >= server.numOpenPortsRequired) { - server.hasAdminRights = true; - Terminal.print("NUKE successful! Gained root access to " + server.hostname); - Terminal.print("You can now run scripts on this server."); - return; - } - - Terminal.print("NUKE unsuccessful. Not enough ports have been opened"); - }, - }, - { - key: "BruteSSHProgram", - name: "BruteSSH.exe", - create: { - level: 50, - tooltip: "This program executes a brute force attack that opens SSH ports", - req: requireHackingLevel(50), - time: CONSTANTS.MillisecondsPerFiveMinutes * 2, - }, - run: (_args: string[], server: BaseServer): void => { - if (!(server instanceof Server)) { - Terminal.error("Cannot run BruteSSH.exe on this kind of server."); - return; - } - if (server.sshPortOpen) { - Terminal.print("SSH Port (22) is already open!"); - return; - } - - server.sshPortOpen = true; - Terminal.print("Opened SSH Port(22)!"); - server.openPortCount++; - }, - }, - { - key: "FTPCrackProgram", - name: "FTPCrack.exe", - create: { - level: 100, - tooltip: "This program cracks open FTP ports", - req: requireHackingLevel(100), - time: CONSTANTS.MillisecondsPerHalfHour, - }, - run: (_args: string[], server: BaseServer): void => { - if (!(server instanceof Server)) { - Terminal.error("Cannot run FTPCrack.exe on this kind of server."); - return; - } - if (server.ftpPortOpen) { - Terminal.print("FTP Port (21) is already open!"); - return; - } - - server.ftpPortOpen = true; - Terminal.print("Opened FTP Port (21)!"); - server.openPortCount++; - }, - }, - { - key: "RelaySMTPProgram", - name: "relaySMTP.exe", - create: { - level: 250, - tooltip: "This program opens SMTP ports by redirecting data", - req: requireHackingLevel(250), - time: CONSTANTS.MillisecondsPer2Hours, - }, - run: (_args: string[], server: BaseServer): void => { - if (!(server instanceof Server)) { - Terminal.error("Cannot run relaySMTP.exe on this kind of server."); - return; - } - if (server.smtpPortOpen) { - Terminal.print("SMTP Port (25) is already open!"); - return; - } - - server.smtpPortOpen = true; - Terminal.print("Opened SMTP Port (25)!"); - server.openPortCount++; - }, - }, - { - key: "HTTPWormProgram", - name: "HTTPWorm.exe", - create: { - level: 500, - tooltip: "This virus opens up HTTP ports", - req: requireHackingLevel(500), - time: CONSTANTS.MillisecondsPer4Hours, - }, - run: (_args: string[], server: BaseServer): void => { - if (!(server instanceof Server)) { - Terminal.error("Cannot run HTTPWorm.exe on this kind of server."); - return; - } - if (server.httpPortOpen) { - Terminal.print("HTTP Port (80) is already open!"); - return; - } - - server.httpPortOpen = true; - Terminal.print("Opened HTTP Port (80)!"); - server.openPortCount++; - }, - }, - { - key: "SQLInjectProgram", - name: "SQLInject.exe", - create: { - level: 750, - tooltip: "This virus opens SQL ports", - req: requireHackingLevel(750), - time: CONSTANTS.MillisecondsPer8Hours, - }, - run: (_args: string[], server: BaseServer): void => { - if (!(server instanceof Server)) { - Terminal.error("Cannot run SQLInject.exe on this kind of server."); - return; - } - if (server.sqlPortOpen) { - Terminal.print("SQL Port (1433) is already open!"); - return; - } - - server.sqlPortOpen = true; - Terminal.print("Opened SQL Port (1433)!"); - server.openPortCount++; - }, - }, - { - key: "DeepscanV1", - name: "DeepscanV1.exe", - create: { - level: 75, - tooltip: "This program allows you to use the scan-analyze command with a depth up to 5", - req: requireHackingLevel(75), - time: CONSTANTS.MillisecondsPerQuarterHour, - }, - run: (): void => { - Terminal.print("This executable cannot be run."); - Terminal.print("DeepscanV1.exe lets you run 'scan-analyze' with a depth up to 5."); - }, - }, - { - key: "DeepscanV2", - name: "DeepscanV2.exe", - create: { - level: 400, - tooltip: "This program allows you to use the scan-analyze command with a depth up to 10", - req: requireHackingLevel(400), - time: CONSTANTS.MillisecondsPer2Hours, - }, - run: (): void => { - Terminal.print("This executable cannot be run."); - Terminal.print("DeepscanV2.exe lets you run 'scan-analyze' with a depth up to 10."); - }, - }, - { - key: "ServerProfiler", - name: "ServerProfiler.exe", - create: { - level: 75, - tooltip: "This program is used to display hacking and Netscript-related information about servers", - req: requireHackingLevel(75), - time: CONSTANTS.MillisecondsPerHalfHour, - }, - run: (args: string[]): void => { - if (args.length !== 1) { - Terminal.error("Must pass a server hostname or IP as an argument for ServerProfiler.exe"); - return; - } - - const targetServer = GetServer(args[0]); - if (targetServer == null) { - Terminal.error("Invalid server IP/hostname"); - return; - } - - if (!(targetServer instanceof Server)) { - Terminal.error(`ServerProfiler.exe can only be run on normal servers.`); - return; - } - - Terminal.print(targetServer.hostname + ":"); - Terminal.print("Server base security level: " + targetServer.baseDifficulty); - Terminal.print("Server current security level: " + targetServer.hackDifficulty); - Terminal.print("Server growth rate: " + targetServer.serverGrowth); - Terminal.print( - `Netscript hack() execution time: ${convertTimeMsToTimeElapsedString( - calculateHackingTime(targetServer, Player) * 1000, - true, - )}`, - ); - Terminal.print( - `Netscript grow() execution time: ${convertTimeMsToTimeElapsedString( - calculateGrowTime(targetServer, Player) * 1000, - true, - )}`, - ); - Terminal.print( - `Netscript weaken() execution time: ${convertTimeMsToTimeElapsedString( - calculateWeakenTime(targetServer, Player) * 1000, - true, - )}`, - ); - }, - }, - { - key: "AutoLink", - name: "AutoLink.exe", - create: { - level: 25, - tooltip: "This program allows you to directly connect to other servers through the 'scan-analyze' command", - req: requireHackingLevel(25), - time: CONSTANTS.MillisecondsPerQuarterHour, - }, - run: (): void => { - Terminal.print("This executable cannot be run."); - Terminal.print("AutoLink.exe lets you automatically connect to other servers when using 'scan-analyze'."); - Terminal.print("When using scan-analyze, click on a server's hostname to connect to it."); - }, - }, - { - key: "Formulas", - name: "Formulas.exe", - create: { - level: 1000, - tooltip: "This program allows you to use the formulas API", - req: requireHackingLevel(1000), - time: CONSTANTS.MillisecondsPer4Hours, - }, - run: (): void => { - Terminal.print("This executable cannot be run."); - Terminal.print("Formulas.exe lets you use the formulas API."); - }, - }, - { - key: "BitFlume", - name: "b1t_flum3.exe", - create: { - level: 1, - tooltip: "This program creates a portal to the BitNode Nexus (allows you to restart and switch BitNodes)", - req: bitFlumeRequirements(), - time: CONSTANTS.MillisecondsPerFiveMinutes / 20, - }, - run: (): void => { - BitFlumeEvent.emit(); - }, - }, - { - key: "Flight", - name: "fl1ght.exe", - create: null, - run: (): void => { - const numAugReq = BitNodeMultipliers.DaedalusAugsRequirement; - const fulfilled = - Player.augmentations.length >= numAugReq && Player.money > 1e11 && Player.skills.hacking >= 2500; - if (!fulfilled) { - Terminal.print(`Augmentations: ${Player.augmentations.length} / ${numAugReq}`); - Terminal.print(`Money: ${formatMoney(Player.money)} / $100b`); - Terminal.print(`Hacking skill: ${Player.skills.hacking} / 2500`); - return; - } - - Terminal.print("We will contact you."); - Terminal.print(`-- ${FactionNames.Daedalus} --`); - }, - }, -]; diff --git a/src/RemoteFileAPI/MessageHandlers.ts b/src/RemoteFileAPI/MessageHandlers.ts index 30344d5b2..fbf59647c 100644 --- a/src/RemoteFileAPI/MessageHandlers.ts +++ b/src/RemoteFileAPI/MessageHandlers.ts @@ -1,7 +1,7 @@ -import { isScriptFilename } from "../Script/isScriptFilename"; +import { resolveFilePath } from "../Paths/FilePath"; +import { hasTextExtension } from "../Paths/TextFilePath"; +import { hasScriptExtension } from "../Paths/ScriptFilePath"; import { GetServer } from "../Server/AllServers"; -import { isValidFilePath } from "../Terminal/DirectoryHelpers"; -import { TextFile } from "../TextFile"; import { RFAMessage, FileData, @@ -23,62 +23,48 @@ export const RFARequestHandler: Record void | R if (!isFileData(msg.params)) return error("Misses parameters", msg); const fileData: FileData = msg.params; - if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg); + const filePath = resolveFilePath(fileData.filename); + if (!filePath) return error("Invalid file path", msg); const server = GetServer(fileData.server); if (!server) return error("Server hostname invalid", msg); - if (isScriptFilename(fileData.filename)) server.writeToScriptFile(fileData.filename, fileData.content); - // Assume it's a text file - else server.writeToTextFile(fileData.filename, fileData.content); - - // If and only if the content is actually changed correctly, send back an OK. - const savedCorrectly = - server.getScript(fileData.filename)?.code === fileData.content || - server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0)?.text === fileData.content; - - if (!savedCorrectly) return error("File wasn't saved correctly", msg); - - return new RFAMessage({ result: "OK", id: msg.id }); + if (hasTextExtension(filePath) || hasScriptExtension(filePath)) { + server.writeToContentFile(filePath, fileData.content); + return new RFAMessage({ result: "OK", id: msg.id }); + } + return error("Invalid file extension", msg); }, getFile: function (msg: RFAMessage): RFAMessage { if (!isFileLocation(msg.params)) return error("Message misses parameters", msg); const fileData: FileLocation = msg.params; - if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg); + const filePath = resolveFilePath(fileData.filename); + if (!filePath) return error("Invalid file path", msg); const server = GetServer(fileData.server); if (!server) return error("Server hostname invalid", msg); - if (isScriptFilename(fileData.filename)) { - const scriptContent = server.getScript(fileData.filename); - if (!scriptContent) return error("File doesn't exist", msg); - return new RFAMessage({ result: scriptContent.code, id: msg.id }); - } else { - // Assume it's a text file - const file = server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0); - if (!file) return error("File doesn't exist", msg); - return new RFAMessage({ result: file.text, id: msg.id }); - } + if (!hasTextExtension(filePath) && !hasScriptExtension(filePath)) return error("Invalid file extension", msg); + const file = server.getContentFile(filePath); + if (!file) return error("File doesn't exist", msg); + return new RFAMessage({ result: file.content, id: msg.id }); }, deleteFile: function (msg: RFAMessage): RFAMessage { if (!isFileLocation(msg.params)) return error("Message misses parameters", msg); + const fileData: FileLocation = msg.params; - if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg); + const filePath = resolveFilePath(fileData.filename); + if (!filePath) return error("Invalid filename", msg); const server = GetServer(fileData.server); if (!server) return error("Server hostname invalid", msg); - const fileExists = (): boolean => - !!server.getScript(fileData.filename) || server.textFiles.some((t: TextFile) => t.filename === fileData.filename); - - if (!fileExists()) return error("File doesn't exist", msg); - server.removeFile(fileData.filename); - if (fileExists()) return error("Failed to delete the file", msg); - - return new RFAMessage({ result: "OK", id: msg.id }); + const result = server.removeFile(filePath); + if (result.res) return new RFAMessage({ result: "OK", id: msg.id }); + return error(result.msg ?? "Failed", msg); }, getFileNames: function (msg: RFAMessage): RFAMessage { @@ -87,7 +73,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.keys()]; + const fileNameList: string[] = [...server.textFiles.keys(), ...server.scripts.keys()]; return new RFAMessage({ result: fileNameList, id: msg.id }); }, @@ -98,26 +84,24 @@ export const RFARequestHandler: Record void | R const server = GetServer(msg.params.server); if (!server) return error("Server hostname invalid", msg); - const fileList: FileContent[] = [ - ...server.textFiles.map((txt): FileContent => { - return { filename: txt.filename, content: txt.text }; - }), - ]; - for (const [filename, script] of server.scripts) fileList.push({ filename, content: script.code }); - + const fileList: FileContent[] = [...server.scripts, ...server.textFiles].map(([filename, file]) => ({ + filename, + content: file.content, + })); return new RFAMessage({ result: fileList, id: msg.id }); }, calculateRam: function (msg: RFAMessage): RFAMessage { if (!isFileLocation(msg.params)) return error("Message misses parameters", msg); const fileData: FileLocation = msg.params; - if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg); + const filePath = resolveFilePath(fileData.filename); + if (!filePath) return error("Invalid filename", msg); const server = GetServer(fileData.server); if (!server) return error("Server hostname invalid", msg); - if (!isScriptFilename(fileData.filename)) return error("Filename isn't a script filename", msg); - const script = server.getScript(fileData.filename); + if (!hasScriptExtension(filePath)) return error("Filename isn't a script filename", msg); + const script = server.scripts.get(filePath); if (!script) return error("File doesn't exist", msg); const ramUsage = script.getRamUsage(server.scripts); if (!ramUsage) return error("Ram cost could not be calculated", msg); diff --git a/src/SaveObject.tsx b/src/SaveObject.tsx index 06b95a44a..b12fec40f 100755 --- a/src/SaveObject.tsx +++ b/src/SaveObject.tsx @@ -36,6 +36,10 @@ import { SpecialServers } from "./Server/data/SpecialServers"; import { v2APIBreak } from "./utils/v2APIBreak"; import { Script } from "./Script/Script"; import { JSONMap } from "./Types/Jsonable"; +import { TextFile } from "./TextFile"; +import { ScriptFilePath, resolveScriptFilePath } from "./Paths/ScriptFilePath"; +import { Directory, resolveDirectory } from "./Paths/Directory"; +import { TextFilePath, resolveTextFilePath } from "./Paths/TextFilePath"; /* SaveObject.js * Defines the object used to save/load games @@ -348,7 +352,7 @@ function evaluateVersionCompatibility(ver: string | number): void { } for (const server of GetAllServers() as unknown as { scripts: Script[] }[]) { for (const script of server.scripts) { - script.code = convert(script.code); + script.content = convert(script.code); } } } @@ -490,7 +494,10 @@ function evaluateVersionCompatibility(ver: string | number): void { anyPlayer.currentWork = null; } if (ver < 24) { - Player.getHomeComputer().scripts.forEach((s) => s.filename.endsWith(".ns") && (s.filename += ".js")); + // Assert the relevant type that was in effect at this version. + (Player.getHomeComputer().scripts as unknown as { filename: string }[]).forEach( + (s) => s.filename.endsWith(".ns") && (s.filename += ".js"), + ); } if (ver < 25) { const removePlayerFields = [ @@ -659,17 +666,43 @@ function evaluateVersionCompatibility(ver: string | number): void { for (const sleeve of Player.sleeves) sleeve.shock = 100 - sleeve.shock; } if (ver < 31) { - if (anyPlayer.hashManager !== undefined) { + if (anyPlayer.hashManager?.upgrades) { anyPlayer.hashManager.upgrades["Company Favor"] ??= 0; } anyPlayer.lastAugReset ??= anyPlayer.lastUpdate - anyPlayer.playtimeSinceLastAug; anyPlayer.lastNodeReset ??= anyPlayer.lastUpdate - anyPlayer.playtimeSinceLastBitnode; + const newDirectory = resolveDirectory("v2.3FileChanges/") as Directory; 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); + let invalidScriptCount = 0; + // There was a brief dev window where Server.scripts was already a map but the filepath changes weren't in yet. + const oldScripts = Array.isArray(server.scripts) ? (server.scripts as Script[]) : [...server.scripts.values()]; + server.scripts = new JSONMap(); + // In case somehow there are previously valid filenames that can't be sanitized, they will go in a new directory with a note. + for (const script of oldScripts) { + let newFilePath = resolveScriptFilePath(script.filename); + if (!newFilePath) { + newFilePath = `${newDirectory}script${++invalidScriptCount}.js` as ScriptFilePath; + script.content = `// Original path: ${script.filename}. Path was no longer valid\n` + script.content; + } + script.filename = newFilePath; + server.scripts.set(newFilePath, script); + } + // Handle changing textFiles to a map as well as FilePath changes at the same time. + if (Array.isArray(server.textFiles)) { + const oldTextFiles = server.textFiles as (TextFile & { fn?: string })[]; + server.textFiles = new JSONMap(); + let invalidTextCount = 0; + for (const textFile of oldTextFiles) { + const oldName = textFile.fn ?? textFile.filename; + delete textFile.fn; + + let newFilePath = resolveTextFilePath(oldName); + if (!newFilePath) { + newFilePath = `${newDirectory}text${++invalidTextCount}.txt` as TextFilePath; + textFile.content = `// Original path: ${textFile.filename}. Path was no longer valid\n` + textFile.content; + } + textFile.filename = newFilePath; + server.textFiles.set(newFilePath, textFile); } } } diff --git a/src/Script/RamCalculations.ts b/src/Script/RamCalculations.ts index 57f5902c0..1125f5319 100644 --- a/src/Script/RamCalculations.ts +++ b/src/Script/RamCalculations.ts @@ -13,7 +13,8 @@ import { RamCalculationErrorCode } from "./RamCalculationErrorCodes"; import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator"; import { Script } from "./Script"; import { Node } from "../NetscriptJSEvaluator"; -import { ScriptFilename, scriptFilenameFromImport } from "../Types/strings"; +import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath"; +import { root } from "../Paths/Directory"; export interface RamUsageEntry { type: "ns" | "dom" | "fn" | "misc"; @@ -38,9 +39,9 @@ const memCheckGlobalKey = ".__GLOBAL__"; /** * Parses code into an AST and walks through it recursively to calculate * 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: Map, code: string, ns1?: boolean): RamCalculation { + * @param otherScripts - All other scripts on the server. Used to account for imported scripts + * @param code - The code being parsed */ +function parseOnlyRamCalculate(otherScripts: Map, code: string, ns1?: boolean): RamCalculation { try { /** * Maps dependent identifiers to their dependencies. @@ -86,11 +87,11 @@ function parseOnlyRamCalculate(otherScripts: Map, code: if (nextModule === undefined) throw new Error("nextModule should not be undefined"); if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue; - const filename = scriptFilenameFromImport(nextModule, ns1); + // Using root as the path base right now. Difficult to implement + const filename = resolveScriptFilePath(nextModule, root, ns1 ? ".script" : ".js"); + if (!filename) return { cost: RamCalculationErrorCode.ImportError }; // Invalid import path const script = otherScripts.get(filename); - if (!script) { - return { cost: RamCalculationErrorCode.ImportError }; // No such script on the server - } + if (!script) return { cost: RamCalculationErrorCode.ImportError }; // No such file on server parseCode(script.code, nextModule); } @@ -370,7 +371,7 @@ function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsR */ export function calculateRamUsage( codeCopy: string, - otherScripts: Map, + otherScripts: Map, ns1?: boolean, ): RamCalculation { try { diff --git a/src/Script/RunningScript.ts b/src/Script/RunningScript.ts index f8c50da43..1ddf167c5 100644 --- a/src/Script/RunningScript.ts +++ b/src/Script/RunningScript.ts @@ -14,6 +14,7 @@ import { ScriptArg } from "@nsdefs"; import { RamCostConstants } from "../Netscript/RamCostGenerator"; import { PositiveInteger } from "../types"; import { getKeyList } from "../utils/helpers/getKeyList"; +import { ScriptFilePath } from "../Paths/ScriptFilePath"; export class RunningScript { // Script arguments @@ -24,7 +25,7 @@ export class RunningScript { dataMap: Record = {}; // Script filename - filename = "default.js"; + filename = "default.js" as ScriptFilePath; // This script's logs. An array of log entries logs: React.ReactNode[] = []; diff --git a/src/Script/Script.ts b/src/Script/Script.ts index b2008eef3..108279d7f 100644 --- a/src/Script/Script.ts +++ b/src/Script/Script.ts @@ -1,20 +1,19 @@ -/** - * Class representing a script file. - * - * This does NOT represent a script that is actively running and - * being evaluated. See RunningScript for that - */ +import type { BaseServer } from "../Server/BaseServer"; import { calculateRamUsage, RamUsageEntry } from "./RamCalculations"; 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"; +import { ScriptFilePath } from "../Paths/ScriptFilePath"; +import { ContentFile } from "../Paths/ContentFile"; +/** Type for ensuring script code is always trimmed */ +export type FormattedCode = string & { __type: "FormattedCode" }; -export class Script { - code: string; - filename: string; +/** A script file as a file on a server. + * For the execution of a script, see RunningScript and WorkerScript */ +export class Script implements ContentFile { + code: FormattedCode; + filename: ScriptFilePath; server: string; // Ram calculation, only exists after first poll of ram cost after updating @@ -32,9 +31,19 @@ export class Script { */ dependencies: Map = new Map(); - constructor(fn = "", code = "", server = "") { + get content() { + return this.code; + } + set content(code: string) { + const newCode = Script.formatCode(code); + if (this.code === newCode) return; + this.code = newCode; + this.invalidateModule(); + } + + constructor(fn = "default.js" as ScriptFilePath, code = "", server = "") { this.filename = fn; - this.code = code; + this.code = Script.formatCode(code); this.server = server; // hostname of server this script is on } @@ -71,18 +80,19 @@ export class Script { /** * Save a script from the script editor - * @param {string} code - The new contents of the script - * @param {Script[]} otherScripts - Other scripts on the server. Used to process imports + * @param filename The new filepath for this Script + * @param code The unformatted code to save + * @param hostname The server to save the script to */ - saveScript(filename: string, code: string, hostname: string): void { - this.invalidateModule(); + saveScript(filename: ScriptFilePath, code: string, hostname: string): void { this.code = Script.formatCode(code); + this.invalidateModule(); this.filename = filename; this.server = hostname; } /** Gets the ram usage, while also attempting to update it if it's currently null */ - getRamUsage(otherScripts: Map): number | null { + getRamUsage(otherScripts: Map): number | null { if (this.ramUsage) return this.ramUsage; this.updateRamUsage(otherScripts); return this.ramUsage; @@ -92,7 +102,7 @@ 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: Map): void { + updateRamUsage(otherScripts: Map): void { const ramCalc = calculateRamUsage(this.code, otherScripts, this.filename.endsWith(".script")); if (ramCalc.cost >= RamCostConstants.Base) { this.ramUsage = roundToTwo(ramCalc.cost); @@ -102,6 +112,14 @@ export class Script { } } + /** Remove script from server. Fails if the provided server isn't the server for this script. */ + deleteFromServer(server: BaseServer): boolean { + if (this.server !== server.hostname || server.isRunning(this.filename)) return false; + this.invalidateModule(); + server.scripts.delete(this.filename); + return true; + } + /** The keys that are relevant in a save file */ static savedKeys = ["code", "filename", "server"] as const; @@ -120,8 +138,8 @@ export class Script { * @param {string} code - The code to format * @returns The formatted code */ - static formatCode(code: string): string { - return code.replace(/^\s+|\s+$/g, ""); + static formatCode(code: string): FormattedCode { + return code.replace(/^\s+|\s+$/g, "") as FormattedCode; } } diff --git a/src/Script/ScriptHelpers.ts b/src/Script/ScriptHelpers.ts index 1097584f4..eaee3cd99 100644 --- a/src/Script/ScriptHelpers.ts +++ b/src/Script/ScriptHelpers.ts @@ -5,9 +5,8 @@ import { Server } from "../Server/Server"; import { RunningScript } from "./RunningScript"; import { processSingleServerGrowth } from "../Server/ServerHelpers"; import { GetServer } from "../Server/AllServers"; - import { formatPercent } from "../ui/formatNumber"; - +import { workerScripts } from "../Netscript/WorkerScripts"; import { compareArrays } from "../utils/helpers/compareArrays"; export function scriptCalculateOfflineProduction(runningScript: RunningScript): void { @@ -99,10 +98,9 @@ export function findRunningScript( //Returns a RunningScript object matching the pid on the //designated server, and false otherwise export function findRunningScriptByPid(pid: number, server: BaseServer): RunningScript | null { - for (let i = 0; i < server.runningScripts.length; ++i) { - if (server.runningScripts[i].pid === pid) { - return server.runningScripts[i]; - } - } - return null; + const ws = workerScripts.get(pid); + // Return null if no ws found or if it's on a different server. + if (!ws) return null; + if (ws.scriptRef.server !== server.hostname) return null; + return ws.scriptRef; } diff --git a/src/Script/isScriptFilename.ts b/src/Script/isScriptFilename.ts deleted file mode 100644 index a9853e6e9..000000000 --- a/src/Script/isScriptFilename.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const validScriptExtensions: Array = [`.js`, `.script`]; - -export function isScriptFilename(f: string): boolean { - return validScriptExtensions.some((ext) => f.endsWith(ext)); -} diff --git a/src/ScriptEditor/ui/ScriptEditorRoot.tsx b/src/ScriptEditor/ui/ScriptEditorRoot.tsx index 9ac30f80c..946091fc0 100644 --- a/src/ScriptEditor/ui/ScriptEditorRoot.tsx +++ b/src/ScriptEditor/ui/ScriptEditorRoot.tsx @@ -8,14 +8,12 @@ type IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor; type ITextModel = monaco.editor.ITextModel; import { OptionsModal } from "./OptionsModal"; import { Options } from "./Options"; -import { isValidFilePath } from "../../Terminal/DirectoryHelpers"; import { Player } from "@player"; import { Router } from "../../ui/GameRoot"; import { Page } from "../../ui/Router"; import { dialogBoxCreate } from "../../ui/React/DialogBox"; -import { isScriptFilename } from "../../Script/isScriptFilename"; +import { ScriptFilePath } from "../../Paths/ScriptFilePath"; import { Script } from "../../Script/Script"; -import { TextFile } from "../../TextFile"; import { calculateRamUsage, checkInfiniteLoop } from "../../Script/RamCalculations"; import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes"; import { formatRam } from "../../ui/formatNumber"; @@ -48,10 +46,12 @@ import libSource from "!!raw-loader!../NetscriptDefinitions.d.ts"; import { TextField, Tooltip } from "@mui/material"; import { useRerender } from "../../ui/React/hooks"; import { NetscriptExtra } from "../../NetscriptFunctions/Extra"; +import { TextFilePath } from "src/Paths/TextFilePath"; +import { ContentFilePath } from "src/Paths/ContentFile"; interface IProps { // Map of filename -> code - files: Record; + files: Map; hostname: string; vim: boolean; } @@ -75,20 +75,20 @@ export function SetupTextEditor(): void { // Holds all the data for a open script class OpenScript { - fileName: string; + path: ContentFilePath; code: string; hostname: string; lastPosition: monaco.Position; model: ITextModel; isTxt: boolean; - constructor(fileName: string, code: string, hostname: string, lastPosition: monaco.Position, model: ITextModel) { - this.fileName = fileName; + constructor(path: ContentFilePath, code: string, hostname: string, lastPosition: monaco.Position, model: ITextModel) { + this.path = path; this.code = code; this.hostname = hostname; this.lastPosition = lastPosition; this.model = model; - this.isTxt = fileName.endsWith(".txt"); + this.isTxt = path.endsWith(".txt"); } } @@ -232,7 +232,7 @@ export function Root(props: IProps): React.ReactElement { }, 300); function updateRAM(newCode: string): void { - if (currentScript != null && currentScript.isTxt) { + if (!currentScript || currentScript.isTxt) { setRAM("N/A"); setRamEntries([["N/A", ""]]); return; @@ -338,18 +338,16 @@ export function Root(props: IProps): React.ReactElement { return; } if (props.files) { - const files = Object.entries(props.files); + const files = props.files; - if (!files.length) { + if (!files.size) { editorRef.current.focus(); return; } for (const [filename, code] of files) { // Check if file is already opened - const openScript = openScripts.find( - (script) => script.fileName === filename && script.hostname === props.hostname, - ); + const openScript = openScripts.find((script) => script.path === filename && script.hostname === props.hostname); if (openScript) { // Script is already opened if (openScript.model === undefined || openScript.model === null || openScript.model.isDisposed()) { @@ -383,7 +381,7 @@ export function Root(props: IProps): React.ReactElement { function infLoop(newCode: string): void { if (editorRef.current === null || currentScript === null) return; - if (!currentScript.fileName.endsWith(".js")) return; + if (!currentScript.path.endsWith(".js")) return; const awaitWarning = checkInfiniteLoop(newCode); if (awaitWarning !== -1) { const newDecorations = editorRef.current.deltaDecorations(decorations, [ @@ -429,37 +427,9 @@ export function Root(props: IProps): React.ReactElement { function saveScript(scriptToSave: OpenScript): void { const server = GetServer(scriptToSave.hostname); - 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 - 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.set(scriptToSave.fileName, script); - } else if (scriptToSave.isTxt) { - for (let i = 0; i < server.textFiles.length; ++i) { - if (server.textFiles[i].fn === scriptToSave.fileName) { - server.textFiles[i].write(scriptToSave.code); - if (Settings.SaveGameOnFileSave) saveObject.saveGame(); - Router.toPage(Page.Terminal); - return; - } - } - const textFile = new TextFile(scriptToSave.fileName, scriptToSave.code); - server.textFiles.push(textFile); - } else { - dialogBoxCreate("Invalid filename. Must be either a script (.script or .js) or a text file (.txt)"); - return; - } - + if (!server) throw new Error("Server should not be null but it is."); + // This server helper already handles overwriting, etc. + server.writeToContentFile(scriptToSave.path, scriptToSave.code); if (Settings.SaveGameOnFileSave) saveObject.saveGame(); Router.toPage(Page.Terminal); } @@ -472,7 +442,7 @@ export function Root(props: IProps): React.ReactElement { // this is duplicate code with saving later. if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) { //Make sure filename + code properly follow tutorial - if (currentScript.fileName !== "n00dles.script" && currentScript.fileName !== "n00dles.js") { + if (currentScript.path !== "n00dles.script" && currentScript.path !== "n00dles.js") { dialogBoxCreate("Don't change the script name for now."); return; } @@ -492,50 +462,9 @@ export function Root(props: IProps): React.ReactElement { return; } - if (currentScript.fileName == "") { - dialogBoxCreate("You must specify a filename!"); - return; - } - - if (!isValidFilePath(currentScript.fileName)) { - dialogBoxCreate( - "Script filename can contain only alphanumerics, hyphens, and underscores, and must end with an extension.", - ); - return; - } - const server = GetServer(currentScript.hostname); 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 - 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.set(currentScript.fileName, script); - } else if (currentScript.isTxt) { - for (let i = 0; i < server.textFiles.length; ++i) { - if (server.textFiles[i].fn === currentScript.fileName) { - server.textFiles[i].write(currentScript.code); - if (Settings.SaveGameOnFileSave) saveObject.saveGame(); - rerender(); - return; - } - } - const textFile = new TextFile(currentScript.fileName, currentScript.code); - server.textFiles.push(textFile); - } else { - dialogBoxCreate("Invalid filename. Must be either a script (.script or .js) or a text file (.txt)"); - return; - } - + server.writeToContentFile(currentScript.path, currentScript.code); if (Settings.SaveGameOnFileSave) saveObject.saveGame(); rerender(); } @@ -555,9 +484,7 @@ export function Root(props: IProps): React.ReactElement { if (currentScript !== null) { return openScripts.findIndex( (script) => - currentScript !== null && - script.fileName === currentScript.fileName && - script.hostname === currentScript.hostname, + currentScript !== null && script.path === currentScript.path && script.hostname === currentScript.hostname, ); } @@ -596,7 +523,7 @@ export function Root(props: IProps): React.ReactElement { if (dirty(index)) { PromptEvent.emit({ - txt: `Do you want to save changes to ${closingScript.fileName} on ${closingScript.hostname}?`, + txt: `Do you want to save changes to ${closingScript.path} on ${closingScript.hostname}?`, resolve: (result: boolean | string) => { if (result) { // Save changes @@ -641,7 +568,7 @@ export function Root(props: IProps): React.ReactElement { PromptEvent.emit({ txt: "Do you want to overwrite the current editor content with the contents of " + - openScript.fileName + + openScript.path + " on the server? This cannot be undone.", resolve: (result: boolean | string) => { if (result) { @@ -679,10 +606,8 @@ export function Root(props: IProps): React.ReactElement { const openScript = openScripts[index]; const server = GetServer(openScript.hostname); 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.get(openScript.fileName)?.code; - return data ?? null; + const data = server.getContentFile(openScript.path)?.content ?? null; + return data; } function handleFilterChange(event: React.ChangeEvent): void { setFilter(event.target.value); @@ -692,7 +617,7 @@ export function Root(props: IProps): React.ReactElement { setSearchExpanded(!searchExpanded); } const filteredOpenScripts = Object.values(openScripts).filter( - (script) => script.hostname.includes(filter) || script.fileName.includes(filter), + (script) => script.hostname.includes(filter) || script.path.includes(filter), ); const tabsMaxWidth = 1640; @@ -747,9 +672,9 @@ export function Root(props: IProps): React.ReactElement { )} - {filteredOpenScripts.map(({ fileName, hostname }, index) => { + {filteredOpenScripts.map(({ path: fileName, hostname }, index) => { const editingCurrentScript = - currentScript?.fileName === filteredOpenScripts[index].fileName && + currentScript?.path === filteredOpenScripts[index].path && currentScript?.hostname === filteredOpenScripts[index].hostname; const externalScript = hostname !== "home"; const colorProps = editingCurrentScript diff --git a/src/Server/BaseServer.ts b/src/Server/BaseServer.ts index a3826a015..0bb4c49b2 100644 --- a/src/Server/BaseServer.ts +++ b/src/Server/BaseServer.ts @@ -2,17 +2,23 @@ import type { Server as IServer } from "@nsdefs"; import { CodingContract } from "../CodingContracts"; import { RunningScript } from "../Script/RunningScript"; import { Script } from "../Script/Script"; -import { isValidFilePath } from "../Terminal/DirectoryHelpers"; import { TextFile } from "../TextFile"; import { IReturnStatus } from "../types"; -import { isScriptFilename } from "../Script/isScriptFilename"; +import { ScriptFilePath, hasScriptExtension } from "../Paths/ScriptFilePath"; +import { TextFilePath, hasTextExtension } from "../Paths/TextFilePath"; 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"; +import { IPAddress, ServerName } from "../Types/strings"; +import { FilePath } from "../Paths/FilePath"; +import { ContentFile, ContentFilePath } from "../Paths/ContentFile"; +import { ProgramFilePath, hasProgramExtension } from "../Paths/ProgramFilePath"; +import { MessageFilename } from "src/Message/MessageHelpers"; +import { LiteratureName } from "src/Literature/data/LiteratureNames"; +import { CompletedProgramName } from "src/Programs/Programs"; interface IConstructorParams { adminRights?: boolean; @@ -24,7 +30,6 @@ interface IConstructorParams { } interface writeResult { - success: boolean; overwritten: boolean; } @@ -59,14 +64,15 @@ export abstract class BaseServer implements IServer { maxRam = 0; // Message files AND Literature files on this Server - messages: string[] = []; + messages: (MessageFilename | LiteratureName | FilePath)[] = []; // Name of company/faction/etc. that this server belongs to. // Optional, not applicable to all Servers organizationName = ""; // Programs on this servers. Contains only the names of the programs - programs: string[] = []; + // CompletedProgramNames are all typechecked as valid paths in Program constructor + programs: (ProgramFilePath | CompletedProgramName)[] = []; // RAM (GB) used. i.e. unavailable RAM ramUsed = 0; @@ -75,7 +81,7 @@ export abstract class BaseServer implements IServer { runningScripts: RunningScript[] = []; // Script files on this Server - scripts: JSONMap = new JSONMap(); + scripts: JSONMap = new JSONMap(); // Contains the hostnames of all servers that are immediately // reachable from this one @@ -91,7 +97,7 @@ export abstract class BaseServer implements IServer { sshPortOpen = false; // Text files on this server - textFiles: TextFile[] = []; + textFiles: JSONMap = new JSONMap(); // Flag indicating whether this is a purchased server purchasedByPlayer = false; @@ -132,34 +138,17 @@ export abstract class BaseServer implements IServer { return null; } - /** - * Find an actively running script on this server - * @param scriptName - Filename of script to search for - * @param scriptArgs - Arguments that script is being run with - * @returns RunningScript for the specified active script - * Returns null if no such script can be found - */ - getRunningScript(scriptName: string, scriptArgs: ScriptArg[]): RunningScript | null { + /** Find an actively running script on this server by filepath and args. */ + getRunningScript(path: ScriptFilePath, scriptArgs: ScriptArg[]): RunningScript | null { for (const rs of this.runningScripts) { - //compare file names without leading '/' to prevent running multiple script with the same name - if ( - (rs.filename.charAt(0) == "/" ? rs.filename.slice(1) : rs.filename) === - (scriptName.charAt(0) == "/" ? scriptName.slice(1) : scriptName) && - compareArrays(rs.args, scriptArgs) - ) { - return rs; - } + if (rs.filename === path && compareArrays(rs.args, scriptArgs)) return rs; } - return null; } - /** - * Given the name of the script, returns the corresponding - * Script object on the server (if it exists) - */ - getScript(scriptName: string): Script | null { - return this.scripts.get(scriptName) ?? null; + /** Get a TextFile or Script depending on the input path type. */ + getContentFile(path: ContentFilePath): ContentFile | null { + return (hasTextExtension(path) ? this.textFiles.get(path) : this.scripts.get(path)) ?? null; } /** Returns boolean indicating whether the given script is running on this server */ @@ -180,51 +169,44 @@ export abstract class BaseServer implements IServer { /** * Remove a file from the server - * @param filename {string} Name of file to be deleted + * @param path Name of file to be deleted * @returns {IReturnStatus} Return status object indicating whether or not file was deleted */ - 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] === filename) { - this.programs.splice(i, 1); - return { res: true }; - } - } - } 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!" }; - } - script.invalidateModule(); - this.scripts.delete(filename); + removeFile(path: FilePath): IReturnStatus { + if (hasTextExtension(path)) { + const textFile = this.textFiles.get(path); + if (!textFile) return { res: false, msg: `Text file ${path} not found.` }; + this.textFiles.delete(path); + return { res: true }; + } + if (hasScriptExtension(path)) { + const script = this.scripts.get(path); + if (!script) return { res: false, msg: `Script ${path} not found.` }; + if (this.isRunning(path)) return { res: false, msg: "Cannot delete a script that is currently running!" }; + script.invalidateModule(); + this.scripts.delete(path); + return { res: true }; + } + if (hasProgramExtension(path)) { + const programIndex = this.programs.findIndex((program) => program === path); + if (programIndex === -1) return { res: false, msg: `Program ${path} does not exist` }; + this.programs.splice(programIndex, 1); + return { res: true }; + } + if (path.endsWith(".lit")) { + const litIndex = this.messages.findIndex((lit) => lit === path); + if (litIndex === -1) return { res: false, msg: `Literature file ${path} does not exist` }; + this.messages.splice(litIndex, 1); + return { res: true }; + } + if (path.endsWith(".cct")) { + const contractIndex = this.contracts.findIndex((program) => program); + if (contractIndex === -1) return { res: false, msg: `Contract file ${path} does not exist` }; + this.contracts.splice(contractIndex, 1); return { res: true }; - } else if (filename.endsWith(".lit")) { - for (let i = 0; i < this.messages.length; ++i) { - const f = this.messages[i]; - if (typeof f === "string" && f === filename) { - this.messages.splice(i, 1); - return { res: true }; - } - } - } else if (filename.endsWith(".txt")) { - for (let i = 0; i < this.textFiles.length; ++i) { - if (this.textFiles[i].fn === filename) { - this.textFiles.splice(i, 1); - return { res: true }; - } - } - } else if (filename.endsWith(".cct")) { - for (let i = 0; i < this.contracts.length; ++i) { - if (this.contracts[i].fn === filename) { - this.contracts.splice(i, 1); - return { res: true }; - } - } } - return { res: false, msg: "No such file exists" }; + return { res: false, msg: `Unhandled file extension on file path ${path}` }; } /** @@ -245,15 +227,13 @@ export abstract class BaseServer implements IServer { this.ramUsed = ram; } - pushProgram(program: string): void { + pushProgram(program: ProgramFilePath | CompletedProgramName): void { if (this.programs.includes(program)) return; // Remove partially created program if there is one const existingPartialExeIndex = this.programs.findIndex((p) => p.startsWith(program)); // findIndex returns -1 if there is no match, we only want to splice on a match - if (existingPartialExeIndex > -1) { - this.programs.splice(existingPartialExeIndex, 1); - } + if (existingPartialExeIndex > -1) this.programs.splice(existingPartialExeIndex, 1); this.programs.push(program); } @@ -262,47 +242,41 @@ export abstract class BaseServer implements IServer { * Write to a script file * Overwrites existing files. Creates new files if the script does not exist. */ - writeToScriptFile(filename: string, code: string): writeResult { - if (!isValidFilePath(filename) || !isScriptFilename(filename)) { - return { success: false, overwritten: false }; - } - + writeToScriptFile(filename: ScriptFilePath, code: string): writeResult { // Check if the script already exists, and overwrite it if it does const script = this.scripts.get(filename); if (script) { - script.invalidateModule(); - script.code = code; - return { success: true, overwritten: true }; + // content setter handles module invalidation and code formatting + script.content = code; + return { overwritten: true }; } // Otherwise, create a new script const newScript = new Script(filename, code, this.hostname); this.scripts.set(filename, newScript); - return { success: true, overwritten: false }; + return { overwritten: false }; } // Write to a text file // Overwrites existing files. Creates new files if the text file does not exist - writeToTextFile(fn: string, txt: string): writeResult { - const ret = { success: false, overwritten: false }; - if (!isValidFilePath(fn) || !fn.endsWith("txt")) { - return ret; - } - + writeToTextFile(textPath: TextFilePath, txt: string): writeResult { // Check if the text file already exists, and overwrite if it does - for (let i = 0; i < this.textFiles.length; ++i) { - if (this.textFiles[i].fn === fn) { - ret.overwritten = true; - this.textFiles[i].text = txt; - ret.success = true; - return ret; - } + const existingFile = this.textFiles.get(textPath); + // overWrite if already exists + if (existingFile) { + existingFile.text = txt; + return { overwritten: true }; } // Otherwise create a new text file - const newFile = new TextFile(fn, txt); - this.textFiles.push(newFile); - ret.success = true; - return ret; + const newFile = new TextFile(textPath, txt); + this.textFiles.set(textPath, newFile); + return { overwritten: false }; + } + + /** Write to a Script or TextFile */ + writeToContentFile(path: ContentFilePath, content: string): writeResult { + if (hasTextExtension(path)) return this.writeToTextFile(path, content); + return this.writeToScriptFile(path, content); } } diff --git a/src/Server/ServerHelpers.ts b/src/Server/ServerHelpers.ts index c90b468e3..0fac502d1 100644 --- a/src/Server/ServerHelpers.ts +++ b/src/Server/ServerHelpers.ts @@ -6,8 +6,8 @@ import { calculateServerGrowth } from "./formulas/grow"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; import { CONSTANTS } from "../Constants"; import { Player } from "@player"; -import { Programs } from "../Programs/Programs"; -import { LiteratureNames } from "../Literature/data/LiteratureNames"; +import { CompletedProgramName } from "../Programs/Programs"; +import { LiteratureName } from "../Literature/data/LiteratureNames"; import { Person as IPerson } from "@nsdefs"; import { isValidNumber } from "../utils/helpers/isValidNumber"; import { Server as IServer } from "@nsdefs"; @@ -249,20 +249,20 @@ export function processSingleServerGrowth(server: Server, threads: number, cores } export function prestigeHomeComputer(homeComp: Server): void { - const hasBitflume = homeComp.programs.includes(Programs.BitFlume.name); + const hasBitflume = homeComp.programs.includes(CompletedProgramName.bitFlume); homeComp.programs.length = 0; //Remove programs homeComp.runningScripts = []; homeComp.serversOnNetwork = []; homeComp.isConnectedTo = true; homeComp.ramUsed = 0; - homeComp.programs.push(Programs.NukeProgram.name); + homeComp.programs.push(CompletedProgramName.nuke); if (hasBitflume) { - homeComp.programs.push(Programs.BitFlume.name); + homeComp.programs.push(CompletedProgramName.bitFlume); } homeComp.messages.length = 0; //Remove .lit and .msg files - homeComp.messages.push(LiteratureNames.HackersStartingHandbook); + homeComp.messages.push(LiteratureName.HackersStartingHandbook); } // Returns the i-th server on the specified server's network diff --git a/src/Server/data/servers.ts b/src/Server/data/servers.ts index e957a0f0c..e468e106e 100644 --- a/src/Server/data/servers.ts +++ b/src/Server/data/servers.ts @@ -4,8 +4,9 @@ import { FactionNames } from "../../Faction/data/FactionNames"; // This could actually be a JSON file as it should be constant metadata to be imported... import { IMinMaxRange } from "../../types"; import { LocationName } from "../../Enums"; -import { LiteratureNames } from "../../Literature/data/LiteratureNames"; +import { LiteratureName } from "../../Literature/data/LiteratureNames"; import { SpecialServers } from "./SpecialServers"; +import { ServerName } from "../../Types/strings"; /** * The metadata describing the base state of servers on the network. @@ -16,10 +17,10 @@ interface IServerMetadata { hackDifficulty?: number | IMinMaxRange; /** The DNS name of the server. */ - hostname: string; + hostname: ServerName; /** When populated, the files will be added to the server when created. */ - literature?: string[]; + literature?: LiteratureName[]; /** * When populated, the exponent of 2^x amount of RAM the server has. @@ -118,7 +119,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 88, }, hostname: "blade", - literature: [LiteratureNames.BeyondMan], + literature: [LiteratureName.BeyondMan], maxRamExponent: { max: 9, min: 5, @@ -143,7 +144,7 @@ export const serverMetadata: IServerMetadata[] = [ { hackDifficulty: 99, hostname: LocationName.VolhavenNWO.toLowerCase(), - literature: [LiteratureNames.TheHiddenWorld], + literature: [LiteratureName.TheHiddenWorld], moneyAvailable: { max: 40e9, min: 20e9, @@ -167,7 +168,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 45, }, hostname: "clarkinc", - literature: [LiteratureNames.BeyondMan, LiteratureNames.CostOfImmortality], + literature: [LiteratureName.BeyondMan, LiteratureName.CostOfImmortality], moneyAvailable: { max: 25e9, min: 15e9, @@ -191,7 +192,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 90, }, hostname: "omnitek", - literature: [LiteratureNames.CodedIntelligence, LiteratureNames.HistoryOfSynthoids], + literature: [LiteratureName.CodedIntelligence, LiteratureName.HistoryOfSynthoids], maxRamExponent: { max: 9, min: 7, @@ -265,7 +266,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 83, }, hostname: "fulcrumtech", - literature: [LiteratureNames.SimulatedReality], + literature: [LiteratureName.SimulatedReality], maxRamExponent: { max: 11, min: 7, @@ -375,7 +376,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 85, }, hostname: "helios", - literature: [LiteratureNames.BeyondMan], + literature: [LiteratureName.BeyondMan], maxRamExponent: { max: 8, min: 5, @@ -403,7 +404,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 80, }, hostname: LocationName.NewTokyoVitaLife.toLowerCase(), - literature: [LiteratureNames.AGreenTomorrow], + literature: [LiteratureName.AGreenTomorrow], maxRamExponent: { max: 7, min: 4, @@ -481,7 +482,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 70, }, hostname: "titan-labs", - literature: [LiteratureNames.CodedIntelligence], + literature: [LiteratureName.CodedIntelligence], maxRamExponent: { max: 7, min: 4, @@ -508,7 +509,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 65, }, hostname: "microdyne", - literature: [LiteratureNames.SyntheticMuscles], + literature: [LiteratureName.SyntheticMuscles], maxRamExponent: { max: 6, min: 4, @@ -535,7 +536,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 70, }, hostname: "taiyang-digital", - literature: [LiteratureNames.AGreenTomorrow, LiteratureNames.BrighterThanTheSun], + literature: [LiteratureName.AGreenTomorrow, LiteratureName.BrighterThanTheSun], moneyAvailable: { max: 900000000, min: 800000000, @@ -581,7 +582,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 80, }, hostname: LocationName.AevumAeroCorp.toLowerCase(), - literature: [LiteratureNames.ManAndMachine], + literature: [LiteratureName.ManAndMachine], moneyAvailable: { max: 1200000000, min: 1000000000, @@ -605,7 +606,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 85, }, hostname: "omnia", - literature: [LiteratureNames.HistoryOfSynthoids], + literature: [LiteratureName.HistoryOfSynthoids], maxRamExponent: { max: 6, min: 4, @@ -633,7 +634,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 55, }, hostname: "zb-def", - literature: [LiteratureNames.SyntheticMuscles], + literature: [LiteratureName.SyntheticMuscles], moneyAvailable: { max: 1100000000, min: 900000000, @@ -678,7 +679,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 70, }, hostname: "solaris", - literature: [LiteratureNames.AGreenTomorrow, LiteratureNames.TheFailedFrontier], + literature: [LiteratureName.AGreenTomorrow, LiteratureName.TheFailedFrontier], maxRamExponent: { max: 7, min: 4, @@ -729,7 +730,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 75, }, hostname: "global-pharm", - literature: [LiteratureNames.AGreenTomorrow], + literature: [LiteratureName.AGreenTomorrow], maxRamExponent: { max: 6, min: 3, @@ -882,7 +883,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 50, }, hostname: "alpha-ent", - literature: [LiteratureNames.Sector12Crime], + literature: [LiteratureName.Sector12Crime], maxRamExponent: { max: 7, min: 4, @@ -937,11 +938,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 45, }, hostname: "rothman-uni", - literature: [ - LiteratureNames.SecretSocieties, - LiteratureNames.TheFailedFrontier, - LiteratureNames.TensionsInTechRace, - ], + literature: [LiteratureName.SecretSocieties, LiteratureName.TheFailedFrontier, LiteratureName.TensionsInTechRace], maxRamExponent: { max: 7, min: 4, @@ -996,7 +993,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 45, }, hostname: "summit-uni", - literature: [LiteratureNames.SecretSocieties, LiteratureNames.TheFailedFrontier, LiteratureNames.SyntheticMuscles], + literature: [LiteratureName.SecretSocieties, LiteratureName.TheFailedFrontier, LiteratureName.SyntheticMuscles], maxRamExponent: { max: 6, min: 4, @@ -1047,7 +1044,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 60, }, hostname: "catalyst", - literature: [LiteratureNames.TensionsInTechRace], + literature: [LiteratureName.TensionsInTechRace], maxRamExponent: { max: 7, min: 4, @@ -1100,7 +1097,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 55, }, hostname: LocationName.VolhavenCompuTek.toLowerCase(), - literature: [LiteratureNames.ManAndMachine], + literature: [LiteratureName.ManAndMachine], moneyAvailable: { max: 250000000, min: 220000000, @@ -1124,7 +1121,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 60, }, hostname: "netlink", - literature: [LiteratureNames.SimulatedReality], + literature: [LiteratureName.SimulatedReality], maxRamExponent: { max: 7, min: 4, @@ -1181,7 +1178,7 @@ export const serverMetadata: IServerMetadata[] = [ { hackDifficulty: 10, hostname: LocationName.Sector12FoodNStuff.toLowerCase(), - literature: [LiteratureNames.Sector12Crime], + literature: [LiteratureName.Sector12Crime], maxRamExponent: 4, moneyAvailable: 2000000, networkLayer: 1, @@ -1239,7 +1236,7 @@ export const serverMetadata: IServerMetadata[] = [ { hackDifficulty: 25, hostname: "neo-net", - literature: [LiteratureNames.TheHiddenWorld], + literature: [LiteratureName.TheHiddenWorld], maxRamExponent: 5, moneyAvailable: 5000000, networkLayer: 3, @@ -1251,7 +1248,7 @@ export const serverMetadata: IServerMetadata[] = [ { hackDifficulty: 30, hostname: "silver-helix", - literature: [LiteratureNames.NewTriads], + literature: [LiteratureName.NewTriads], maxRamExponent: 6, moneyAvailable: 45000000, networkLayer: 3, @@ -1263,7 +1260,7 @@ export const serverMetadata: IServerMetadata[] = [ { hackDifficulty: 15, hostname: "hong-fang-tea", - literature: [LiteratureNames.BrighterThanTheSun], + literature: [LiteratureName.BrighterThanTheSun], maxRamExponent: 4, moneyAvailable: 3000000, networkLayer: 1, @@ -1311,7 +1308,7 @@ export const serverMetadata: IServerMetadata[] = [ min: 25, }, hostname: "omega-net", - literature: [LiteratureNames.TheNewGod], + literature: [LiteratureName.TheNewGod], maxRamExponent: 5, moneyAvailable: { max: 70000000, @@ -1436,7 +1433,7 @@ export const serverMetadata: IServerMetadata[] = [ { hackDifficulty: 0, hostname: "run4theh111z", - literature: [LiteratureNames.SimulatedReality, LiteratureNames.TheNewGod], + literature: [LiteratureName.SimulatedReality, LiteratureName.TheNewGod], maxRamExponent: { max: 9, min: 5, @@ -1455,7 +1452,7 @@ export const serverMetadata: IServerMetadata[] = [ { hackDifficulty: 0, hostname: "I.I.I.I", - literature: [LiteratureNames.DemocracyIsDead], + literature: [LiteratureName.DemocracyIsDead], maxRamExponent: { max: 8, min: 4, @@ -1474,7 +1471,7 @@ export const serverMetadata: IServerMetadata[] = [ { hackDifficulty: 0, hostname: "avmnite-02h", - literature: [LiteratureNames.DemocracyIsDead], + literature: [LiteratureName.DemocracyIsDead], maxRamExponent: { max: 7, min: 4, @@ -1508,7 +1505,7 @@ export const serverMetadata: IServerMetadata[] = [ { hackDifficulty: 0, hostname: "CSEC", - literature: [LiteratureNames.DemocracyIsDead], + literature: [LiteratureName.DemocracyIsDead], maxRamExponent: 3, moneyAvailable: 0, networkLayer: 2, @@ -1524,7 +1521,7 @@ export const serverMetadata: IServerMetadata[] = [ { hackDifficulty: 0, hostname: "The-Cave", - literature: [LiteratureNames.AlphaOmega], + literature: [LiteratureName.AlphaOmega], moneyAvailable: 0, networkLayer: 15, numOpenPortsRequired: 5, diff --git a/src/Terminal/DirectoryHelpers.ts b/src/Terminal/DirectoryHelpers.ts deleted file mode 100644 index 85659aa62..000000000 --- a/src/Terminal/DirectoryHelpers.ts +++ /dev/null @@ -1,316 +0,0 @@ -/** - * Helper functions that implement "directory" functionality in the Terminal. - * These aren't "real" directories, it's more of a pseudo-directory implementation - * that uses mainly string manipulation. - * - * This file contains functions that deal only with that string manipulation. - * Functions that need to access/process Server-related things can be - * found in ./DirectoryServerHelpers.ts - */ - -/** Removes leading forward slash ("/") from a string. */ -export function removeLeadingSlash(s: string): string { - if (s.startsWith("/")) { - return s.slice(1); - } - - return s; -} - -/** - * Removes trailing forward slash ("/") from a string. - * Note that this will also remove the slash if it is the leading slash (i.e. if s = "/") - */ -export function removeTrailingSlash(s: string): string { - if (s.endsWith("/")) { - return s.slice(0, -1); - } - - return s; -} - -/** - * Checks whether a string is a valid filename. Only used for the filename itself, - * not the entire filepath - */ -export function isValidFilename(filename: string): boolean { - // Allows alphanumerics, hyphens, underscores, and percentage signs - // Must have a file extension - const regex = /^[.&'a-zA-Z0-9_-]+\.[a-zA-Z0-9]+(?:-\d+(?:\.\d*)?%-INC)?$/; - - // match() returns null if no match is found - return filename.match(regex) != null; -} - -/** - * Checks whether a string is a valid directory name. Only used for the directory itself, - * not an entire path - */ -export function isValidDirectoryName(name: string): boolean { - // A valid directory name: - // Must be at least 1 character long - // Can only include characters in the character set [-.%a-zA-Z0-9_] - // Cannot end with a '.' - const regex = /^.?[a-zA-Z0-9_-]+$/; - - // match() returns null if no match is found - return name.match(regex) != null; -} - -/** - * Checks whether a string is a valid directory path. - * This only checks if it has the proper formatting. It does NOT check - * if the directories actually exist on Terminal - */ -export function isValidDirectoryPath(path: string): boolean { - let t_path: string = path; - - if (t_path.length === 0) { - return false; - } - if (t_path.length === 1) { - return t_path === "/"; - } - - // A full path must have a leading slash, but we'll ignore it for the checks - if (t_path.startsWith("/")) { - t_path = t_path.slice(1); - } else { - return false; - } - - // Trailing slash does not matter - t_path = removeTrailingSlash(t_path); - - // Check that every section of the path is a valid directory name - const dirs = t_path.split("/"); - for (const dir of dirs) { - // Special case, "." and ".." are valid for path - if (dir === "." || dir === "..") { - continue; - } - if (!isValidDirectoryName(dir)) { - return false; - } - } - - return true; -} - -/** - * Checks whether a string is a valid file path. This only checks if it has the - * proper formatting. It dose NOT check if the file actually exists on Terminal - */ -export function isValidFilePath(path: string): boolean { - if (path == null || typeof path !== "string") { - return false; - } - const t_path = path; - - // Impossible for filename to have less than length of 3 - if (t_path.length < 3) { - return false; - } - - // Full filepath can't end with trailing slash because it must be a file - if (t_path.endsWith("/")) { - return false; - } - - // Everything after the last forward slash is the filename. Everything before - // it is the file path - const fnSeparator = t_path.lastIndexOf("/"); - if (fnSeparator === -1) { - return isValidFilename(t_path); - } - - const fn = t_path.slice(fnSeparator + 1); - const dirPath = t_path.slice(0, fnSeparator + 1); - - return isValidDirectoryPath(dirPath) && isValidFilename(fn); -} - -/** - * Returns a formatted string for the first parent directory in a filepath. For example: - * /home/var/test/ -> home/ - * If there is no first parent directory, then it returns "/" for root - */ -export function getFirstParentDirectory(path: string): string { - let t_path = path; - t_path = removeLeadingSlash(t_path); - t_path = removeTrailingSlash(t_path); - - if (t_path.lastIndexOf("/") === -1) { - return "/"; - } - - const dirs = t_path.split("/"); - if (dirs.length === 0) { - return "/"; - } - - return dirs[0] + "/"; -} - -/** - * Given a filepath, returns a formatted string for all of the parent directories - * in that filepath. For example: - * /home/var/tes -> home/var/ - * /home/var/test/ -> home/var/test/ - * If there are no parent directories, it returns the empty string - */ -export function getAllParentDirectories(path: string): string { - const t_path = path; - const lastSlash = t_path.lastIndexOf("/"); - if (lastSlash === -1) { - return ""; - } - - return t_path.slice(0, lastSlash + 1); -} - -/** - * Given a destination that only contains a directory part, returns the - * path to the source filename inside the new destination directory. - * Otherwise, returns the path to the destination file. - * @param destination The destination path or file name - * @param source The source path - * @param cwd The current working directory - * @returns A file path which may be absolute or relative - */ -export function getDestinationFilepath(destination: string, source: string, cwd: string): string { - const dstDir = evaluateDirectoryPath(destination, cwd); - // If evaluating the directory for this destination fails, we have a filename or full path. - if (dstDir === null) { - return destination; - } else { - // Append the filename to the directory provided. - const t_path = removeTrailingSlash(dstDir); - const fileName = getFileName(source); - return t_path + "/" + fileName; - } -} - -/** - * Given a filepath, returns the file name (e.g. without directory parts) - * For example: - * /home/var/test.js -> test.js - * ./var/test.js -> test.js - * test.js -> test.js - */ -export function getFileName(path: string): string { - const t_path = path; - const lastSlash = t_path.lastIndexOf("/"); - if (lastSlash === -1) { - return t_path; - } - - return t_path.slice(lastSlash + 1); -} - -/** Checks if a file path refers to a file in the root directory. */ -export function isInRootDirectory(path: string): boolean { - if (!isValidFilePath(path)) { - return false; - } - if (path == null || path.length === 0) { - return false; - } - - return path.lastIndexOf("/") <= 0; -} - -/** - * Evaluates a directory path, including the processing of linux dots. - * Returns the full, proper path, or null if an invalid path is passed in - */ -export function evaluateDirectoryPath(path: string, currPath?: string): string | null { - let t_path = path; - - // If the path begins with a slash, then its an absolute path. Otherwise its relative - // For relative paths, we need to prepend the current directory - if (!t_path.startsWith("/") && currPath) { - t_path = currPath + (currPath.endsWith("/") ? "" : "/") + t_path; - } - - if (!isValidDirectoryPath(t_path)) { - return null; - } - - // Trim leading/trailing slashes - t_path = removeLeadingSlash(t_path); - t_path = removeTrailingSlash(t_path); - - const dirs = t_path.split("/"); - const reconstructedPath: string[] = []; - - for (const dir of dirs) { - if (dir === ".") { - // Current directory, do nothing - continue; - } else if (dir === "..") { - // Parent directory - const res = reconstructedPath.pop(); - if (res == null) { - return null; // Array was empty, invalid path - } - } else { - reconstructedPath.push(dir); - } - } - - return "/" + reconstructedPath.join("/"); -} - -/** - * Evaluates a file path, including the processing of linux dots. - * Returns the full, proper path, or null if an invalid path is passed in - */ -export function evaluateFilePath(path: string, currPath?: string): string | null { - let t_path = path; - - // If the path begins with a slash, then its an absolute path. Otherwise its relative - // For relative paths, we need to prepend the current directory - if (!t_path.startsWith("/") && currPath != null) { - t_path = currPath + (currPath.endsWith("/") ? "" : "/") + t_path; - } - - if (!isValidFilePath(t_path)) { - return null; - } - - // Trim leading/trailing slashes - t_path = removeLeadingSlash(t_path); - - const dirs = t_path.split("/"); - const reconstructedPath: string[] = []; - - for (const dir of dirs) { - if (dir === ".") { - // Current directory, do nothing - continue; - } else if (dir === "..") { - // Parent directory - const res = reconstructedPath.pop(); - if (res == null) { - return null; // Array was empty, invalid path - } - } else { - reconstructedPath.push(dir); - } - } - - return "/" + reconstructedPath.join("/"); -} - -export function areFilesEqual(f0: string, f1: string): boolean { - if (!f0.startsWith("/")) f0 = "/" + f0; - if (!f1.startsWith("/")) f1 = "/" + f1; - return f0 === f1; -} - -export function areImportsEquals(f0: string, f1: string): boolean { - if (!f0.endsWith(".js")) f0 = f0 + ".js"; - if (!f1.endsWith(".js")) f1 = f1 + ".js"; - return areFilesEqual(f0, f1); -} diff --git a/src/Terminal/DirectoryServerHelpers.ts b/src/Terminal/DirectoryServerHelpers.ts deleted file mode 100644 index b3de7c33c..000000000 --- a/src/Terminal/DirectoryServerHelpers.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Helper functions that implement "directory" functionality in the Terminal. - * These aren't "real" directories, it's more of a pseudo-directory implementation - * that uses mainly string manipulation. - * - * This file contains function that deal with Server-related directory things. - * Functions that deal with the string manipulation can be found in - * ./DirectoryHelpers.ts - */ -import { isValidDirectoryPath, isInRootDirectory, getFirstParentDirectory } from "./DirectoryHelpers"; -import { BaseServer } from "../Server/BaseServer"; - -/** - * Given a directory (by the full directory path) and a server, returns all - * subdirectories of that directory. This is only for FIRST-LEVEl/immediate subdirectories - */ -export function getSubdirectories(serv: BaseServer, dir: string): string[] { - const res: string[] = []; - - if (!isValidDirectoryPath(dir)) { - return res; - } - - let t_dir = dir; - if (!t_dir.endsWith("/")) { - t_dir += "/"; - } - - function processFile(fn: string): void { - if (t_dir === "/" && isInRootDirectory(fn)) { - const subdir = getFirstParentDirectory(fn); - if (subdir !== "/" && !res.includes(subdir)) { - res.push(subdir); - } - } else if (fn.startsWith(t_dir)) { - const remaining = fn.slice(t_dir.length); - const subdir = getFirstParentDirectory(remaining); - if (subdir !== "/" && !res.includes(subdir)) { - res.push(subdir); - } - } - } - - for (const scriptFilename of serv.scripts.keys()) { - processFile(scriptFilename); - } - - for (const txt of serv.textFiles) { - processFile(txt.fn); - } - - return res; -} - -/** Returns true, if the server's directory itself or one of its subdirectory contains files. */ -export function containsFiles(server: BaseServer, dir: string): boolean { - const dirWithTrailingSlash = dir + (dir.slice(-1) === "/" ? "" : "/"); - - return [...server.scripts.keys(), ...server.textFiles.map((t) => t.fn)].some((filename) => - filename.startsWith(dirWithTrailingSlash), - ); -} diff --git a/src/Terminal/Parser.ts b/src/Terminal/Parser.ts index 9f7d23e6a..d900674f2 100644 --- a/src/Terminal/Parser.ts +++ b/src/Terminal/Parser.ts @@ -1,94 +1,36 @@ -import { KEY } from "../utils/helpers/keyCodes"; import { substituteAliases } from "../Alias"; // Helper function to parse individual arguments into number/boolean/string as appropriate -function parseArg(arg: string, stringOverride: boolean): string | number | boolean { - // Handles all numbers including hexadecimal, octal, and binary representations, returning NaN on an unparsable string - if (stringOverride) return arg; - const asNumber = Number(arg); - if (!isNaN(asNumber)) { - return asNumber; +function parseArg(arg: string): string | number | boolean { + if (arg === "true") return true; + if (arg === "false") return false; + const argAsNumber = Number(arg); + if (!isNaN(argAsNumber)) return argAsNumber; + // For quoted strings just return the inner string + if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith("'") && arg.endsWith("'"))) { + return arg.substring(1, arg.length - 1); } - - if (arg === "true" || arg === "false") { - return arg === "true"; - } - return arg; } -export function ParseCommands(commands: string): string[] { - // Sanitize input - commands = commands.trim(); - // Replace all extra whitespace in command with a single space - commands = commands.replace(/\s\s+/g, " "); - - const match = commands.match(/(?:'[^']*'|"[^"]*"|[^;"])*/g); - if (!match) return []; - // Split commands and execute sequentially - const allCommands = match - .map(substituteAliases) - .map((c) => c.match(/(?:'[^']*'|"[^"]*"|[^;"])*/g)) - .flat(); - - const out: string[] = []; - for (const c of allCommands) { - if (c === null) continue; - if (c.match(/^\s*$/)) { - continue; - } // Don't run commands that only have whitespace - out.push(c.trim()); - } - return out; +/** Split a commands string (what is typed into the terminal) into multiple commands */ +export function splitCommands(commandString: string): string[] { + const commandArray = commandString.match(/(?:'[^']*'|"[^"]*"|[^;"])*/g); + if (!commandArray) return []; + return commandArray.map((command) => command.trim()); } -export function ParseCommand(command: string): (string | number | boolean)[] { - let idx = 0; - const args = []; - - let lastQuote = ""; - - let arg = ""; - let stringOverride = false; - - while (idx < command.length) { - const c = command.charAt(idx); - // If the current character is a backslash, add the next character verbatim to the argument - if (c === "\\") { - arg += command.charAt(++idx); - - // If the current character is a single- or double-quote mark, add it to the current argument. - } else if (c === KEY.DOUBLE_QUOTE || c === KEY.QUOTE) { - stringOverride = true; - // If we're currently in a quoted string argument and this quote mark is the same as the beginning, - // the string is done - if (lastQuote !== "" && c === lastQuote) { - lastQuote = ""; - // Otherwise if we're not in a string argument, we've begun one - } else if (lastQuote === "") { - lastQuote = c; - // Otherwise if we're in a string argument, add the current character to it - } else { - arg += c; - } - // If the current character is a space and we are not inside a string, parse the current argument - // and start a new one - } else if (c === KEY.SPACE && lastQuote === "") { - args.push(parseArg(arg, stringOverride)); - - stringOverride = false; - arg = ""; - } else { - // Add the current character to the current argument - arg += c; - } - - idx++; - } - - // Add the last arg (if any) - if (arg !== "") { - args.push(parseArg(arg, stringOverride)); - } - - return args; +/** Split commands string while also applying aliases */ +export function parseCommands(commands: string): string[] { + // Remove any unquoted whitespace longer than length 1 + commands = commands.replace(/(?:"[^"]+"|'[^']+'|\s{2,})+?/g, (match) => (match.startsWith(" ") ? " " : match)); + // Split the commands, apply aliases once, then split again and filter out empty strings. + const commandsArr = splitCommands(commands).map(substituteAliases).flatMap(splitCommands).filter(Boolean); + return commandsArr; +} + +export function parseCommand(command: string): (string | number | boolean)[] { + const commandArgs = command.match(/(?:("[^"]+"|'[^']+'|[^\s]+))+?/g); + if (!commandArgs) return []; + const argsToReturn = commandArgs.map(parseArg); + return argsToReturn; } diff --git a/src/Terminal/Terminal.ts b/src/Terminal/Terminal.ts index 99af32483..635065e8c 100644 --- a/src/Terminal/Terminal.ts +++ b/src/Terminal/Terminal.ts @@ -4,21 +4,20 @@ import { Player } from "@player"; import { HacknetServer } from "../Hacknet/HacknetServer"; import { BaseServer } from "../Server/BaseServer"; import { Server } from "../Server/Server"; -import { Programs } from "../Programs/Programs"; +import { CompletedProgramName } from "../Programs/Programs"; import { CodingContractResult } from "../CodingContracts"; import { TerminalEvents, TerminalClearEvents } from "./TerminalEvents"; import { TextFile } from "../TextFile"; import { Script } from "../Script/Script"; -import { isScriptFilename } from "../Script/isScriptFilename"; +import { hasScriptExtension } from "../Paths/ScriptFilePath"; import { CONSTANTS } from "../Constants"; import { GetServer, GetAllServers } from "../Server/AllServers"; -import { removeLeadingSlash, isInRootDirectory, evaluateFilePath } from "./DirectoryHelpers"; import { checkIfConnectedToDarkweb } from "../DarkWeb/DarkWeb"; import { iTutorialNextStep, iTutorialSteps, ITutorial } from "../InteractiveTutorial"; import { getServerOnNetwork, processSingleServerGrowth } from "../Server/ServerHelpers"; -import { ParseCommand, ParseCommands } from "./Parser"; +import { parseCommand, parseCommands } from "./Parser"; import { SpecialServers } from "../Server/data/SpecialServers"; import { Settings } from "../Settings/Settings"; import { createProgressBarText } from "../utils/helpers/createProgressBarText"; @@ -77,6 +76,10 @@ import { apr1 } from "./commands/apr1"; import { changelog } from "./commands/changelog"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; import { Engine } from "../engine"; +import { Directory, resolveDirectory, root } from "../Paths/Directory"; +import { FilePath, isFilePath, resolveFilePath } from "../Paths/FilePath"; +import { hasTextExtension } from "../Paths/TextFilePath"; +import { ContractFilePath } from "src/Paths/ContractFilePath"; export class Terminal { // Flags to determine whether the player is currently running a hack or an analyze @@ -92,9 +95,8 @@ export class Terminal { // True if a Coding Contract prompt is opened contractOpen = false; - // Full Path of current directory - // Excludes the trailing forward slash - currDir = "/"; + // Path of current directory + currDir = "" as Directory; process(cycles: number): void { if (this.action === null) return; @@ -377,46 +379,40 @@ export class Terminal { } getFile(filename: string): Script | TextFile | string | null { - if (isScriptFilename(filename)) { - return this.getScript(filename); - } - - if (filename.endsWith(".lit")) { - return this.getLitFile(filename); - } - - if (filename.endsWith(".txt")) { - return this.getTextFile(filename); - } - + if (hasScriptExtension(filename)) return this.getScript(filename); + if (hasTextExtension(filename)) return this.getTextFile(filename); + if (filename.endsWith(".lit")) return this.getLitFile(filename); return null; } - getFilepath(filename: string): string | null { - const path = evaluateFilePath(filename, this.cwd()); - if (path === null || !isInRootDirectory(path)) return path; + getFilepath(path: string, useAbsolute?: boolean): FilePath | null { + // If path starts with a slash, consider it to be an absolute path + if (useAbsolute || path.startsWith("/")) return resolveFilePath(path); + // Otherwise, force path to be seen as relative to the current directory. + path = "./" + path; + return resolveFilePath(path, this.currDir); + } - return removeLeadingSlash(path); + getDirectory(path: string, useAbsolute?: boolean): Directory | null { + // If path starts with a slash, consider it to be an absolute path + if (useAbsolute || path.startsWith("/")) return resolveDirectory(path); + // Otherwise, force path to be seen as relative to the current directory. + path = "./" + path; + return resolveDirectory(path, this.currDir); } getScript(filename: string): Script | null { const server = Player.getCurrentServer(); const filepath = this.getFilepath(filename); - if (filepath === null) return null; + if (!filepath || !hasScriptExtension(filepath)) return null; return server.scripts.get(filepath) ?? null; } getTextFile(filename: string): TextFile | null { - const s = Player.getCurrentServer(); + const server = Player.getCurrentServer(); const filepath = this.getFilepath(filename); - if (!filepath) return null; - for (const txt of s.textFiles) { - if (filepath === txt.fn) { - return txt; - } - } - - return null; + if (!filepath || !hasTextExtension(filepath)) return null; + return server.textFiles.get(filepath) ?? null; } getLitFile(filename: string): string | null { @@ -432,32 +428,30 @@ export class Terminal { return null; } - cwd(): string { + cwd(): Directory { return this.currDir; } - setcwd(dir: string): void { + setcwd(dir: Directory): void { this.currDir = dir; TerminalEvents.emit(); } - async runContract(contractName: string): Promise { + async runContract(contractPath: ContractFilePath): Promise { // There's already an opened contract if (this.contractOpen) { return this.error("There's already a Coding Contract in Progress"); } const serv = Player.getCurrentServer(); - const contract = serv.getContract(contractName); - if (contract == null) { - return this.error("No such contract"); - } + const contract = serv.getContract(contractPath); + if (!contract) return this.error("No such contract"); this.contractOpen = true; const res = await contract.prompt(); //Check if the contract still exists by the time the promise is fulfilled - if (serv.getContract(contractName) == null) { + if (serv.getContract(contractPath) == null) { this.contractOpen = false; return this.error("Contract no longer exists (Was it solved by a script?)"); } @@ -529,7 +523,7 @@ export class Terminal { continue; } // Don't print current server const titleDashes = Array((d - 1) * 4 + 1).join("-"); - if (Player.hasProgram(Programs.AutoLink.name)) { + if (Player.hasProgram(CompletedProgramName.autoLink)) { this.append(new Link(titleDashes, s.hostname)); } else { this.print(titleDashes + s.hostname); @@ -564,17 +558,13 @@ export class Terminal { Player.currentServer = serv.hostname; Player.getCurrentServer().isConnectedTo = true; this.print("Connected to " + serv.hostname); - this.setcwd("/"); + this.setcwd(root); if (Player.getCurrentServer().hostname == "darkweb") { checkIfConnectedToDarkweb(); // Posts a 'help' message if connecting to dark web } } executeCommands(commands: string): void { - // Sanitize input - commands = commands.trim(); - commands = commands.replace(/\s\s+/g, " "); // Replace all extra whitespace in command with a single space - // Handle Terminal History - multiple commands should be saved as one if (this.commandHistory[this.commandHistory.length - 1] != commands) { this.commandHistory.push(commands); @@ -584,11 +574,8 @@ export class Terminal { Player.terminalCommandHistory = this.commandHistory; } this.commandHistoryIndex = this.commandHistory.length; - const allCommands = ParseCommands(commands); - - for (let i = 0; i < allCommands.length; i++) { - this.executeCommand(allCommands[i]); - } + const allCommands = parseCommands(commands); + for (const command of allCommands) this.executeCommand(command); } clear(): void { @@ -603,20 +590,12 @@ export class Terminal { } executeCommand(command: string): void { - if (this.action !== null) { - this.error(`Cannot execute command (${command}) while an action is in progress`); - return; - } - // Allow usage of ./ - if (command.startsWith("./")) { - command = "run " + command.slice(2); - } - // Only split the first space - const commandArray = ParseCommand(command); - if (commandArray.length == 0) { - return; - } - const s = Player.getCurrentServer(); + if (this.action !== null) return this.error(`Cannot execute command (${command}) while an action is in progress`); + + const commandArray = parseCommand(command); + if (!commandArray.length) return; + + const currentServer = Player.getCurrentServer(); /****************** Interactive Tutorial Terminal Commands ******************/ if (ITutorial.isRunning) { const n00dlesServ = GetServer("n00dles"); @@ -769,11 +748,14 @@ export class Terminal { } /****************** END INTERACTIVE TUTORIAL ******************/ /* Command parser */ + const commandName = commandArray[0]; - if (typeof commandName === "number" || typeof commandName === "boolean") { - this.error(`Command ${commandArray[0]} not found`); - return; - } + if (typeof commandName !== "string") return this.error(`${commandName} is not a valid command.`); + // run by path command + if (isFilePath(commandName)) return run(commandArray, currentServer); + + // Aside from the run-by-path command, we don't need the first entry once we've stored it in commandName. + commandArray.shift(); const commands: { [key: string]: (args: (string | number | boolean)[], server: BaseServer) => void; @@ -823,12 +805,9 @@ export class Terminal { }; const f = commands[commandName.toLowerCase()]; - if (!f) { - this.error(`Command ${commandArray[0]} not found`); - return; - } + if (!f) return this.error(`Command ${commandName} not found`); - f(commandArray.slice(1), s); + f(commandArray, currentServer); } getProgressText(): string { diff --git a/src/Terminal/commands/cat.ts b/src/Terminal/commands/cat.ts index 8b637a748..130d37a6f 100644 --- a/src/Terminal/commands/cat.ts +++ b/src/Terminal/commands/cat.ts @@ -1,59 +1,35 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; -import { MessageFilenames, showMessage } from "../../Message/MessageHelpers"; +import { MessageFilename, showMessage } from "../../Message/MessageHelpers"; import { showLiterature } from "../../Literature/LiteratureHelpers"; import { dialogBoxCreate } from "../../ui/React/DialogBox"; import { checkEnum } from "../../utils/helpers/enum"; +import { hasScriptExtension } from "../../Paths/ScriptFilePath"; +import { hasTextExtension } from "../../Paths/TextFilePath"; +import { LiteratureName } from "../../Literature/data/LiteratureNames"; export function cat(args: (string | number | boolean)[], server: BaseServer): void { - if (args.length !== 1) { - Terminal.error("Incorrect usage of cat command. Usage: cat [file]"); - return; - } + if (args.length !== 1) return Terminal.error("Incorrect usage of cat command. Usage: cat [file]"); + const relative_filename = args[0] + ""; - const filename = Terminal.getFilepath(relative_filename); - if (!filename) return Terminal.error(`Invalid filename: ${relative_filename}`); - if ( - !filename.endsWith(".msg") && - !filename.endsWith(".lit") && - !filename.endsWith(".txt") && - !filename.endsWith(".script") && - !filename.endsWith(".js") - ) { - Terminal.error( - "Only .msg, .txt, .lit, .script and .js files are viewable with cat (filename must end with .msg, .txt, .lit, .script or .js)", - ); - return; + const path = Terminal.getFilepath(relative_filename); + if (!path) return Terminal.error(`Invalid filename: ${relative_filename}`); + + if (hasScriptExtension(path) || hasTextExtension(path)) { + const file = server.getContentFile(path); + if (!file) return Terminal.error(`No file at path ${path}`); + return dialogBoxCreate(`${file.filename}\n\n${file.content}`); + } + if (!path.endsWith(".msg") && !path.endsWith(".lit")) { + return Terminal.error("Invalid file extension. Filename must end with .msg, .txt, .lit, .script or .js"); } - if (filename.endsWith(".msg") || filename.endsWith(".lit")) { - for (let i = 0; i < server.messages.length; ++i) { - if (filename.endsWith(".lit") && server.messages[i] === filename) { - const file = server.messages[i]; - if (file.endsWith(".msg")) throw new Error(".lit file should not be a .msg"); - showLiterature(file); - return; - } else if (filename.endsWith(".msg")) { - const file = server.messages[i]; - if (file !== filename) continue; - if (!checkEnum(MessageFilenames, file)) return; - showMessage(file); - return; - } - } - } else if (filename.endsWith(".txt")) { - const txt = Terminal.getTextFile(relative_filename); - if (txt != null) { - txt.show(); - return; - } - } else if (filename.endsWith(".script") || filename.endsWith(".js")) { - const script = Terminal.getScript(relative_filename); - if (script != null) { - dialogBoxCreate(`${script.filename}\n\n${script.code}`); - return; - } + // Message + if (checkEnum(MessageFilename, path)) { + if (server.messages.includes(path)) showMessage(path); } - - Terminal.error(`No such file ${filename}`); + if (checkEnum(LiteratureName, path)) { + if (server.messages.includes(path)) showLiterature(path); + } + Terminal.error(`No file at path ${path}`); } diff --git a/src/Terminal/commands/cd.ts b/src/Terminal/commands/cd.ts index e67ef32ed..c36886f1f 100644 --- a/src/Terminal/commands/cd.ts +++ b/src/Terminal/commands/cd.ts @@ -1,38 +1,14 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; - -import { evaluateDirectoryPath, removeTrailingSlash } from "../DirectoryHelpers"; -import { containsFiles } from "../DirectoryServerHelpers"; +import { directoryExistsOnServer, resolveDirectory } from "../../Paths/Directory"; export function cd(args: (string | number | boolean)[], server: BaseServer): void { - if (args.length > 1) { - Terminal.error("Incorrect number of arguments. Usage: cd [dir]"); - } else { - let dir = args.length === 1 ? args[0] + "" : "/"; - - let evaledDir: string | null = ""; - if (dir === "/") { - evaledDir = "/"; - } else { - // Ignore trailing slashes - dir = removeTrailingSlash(dir); - - evaledDir = evaluateDirectoryPath(dir, Terminal.cwd()); - if (evaledDir === null || evaledDir === "") { - Terminal.error("Invalid path. Failed to change directories"); - return; - } - if (Terminal.cwd().length > 1 && dir === "..") { - Terminal.setcwd(evaledDir); - return; - } - - if (!containsFiles(server, evaledDir)) { - Terminal.error("Invalid path. Failed to change directories"); - return; - } - } - - Terminal.setcwd(evaledDir); - } + if (args.length > 1) return Terminal.error("Incorrect number of arguments. Usage: cd [dir]"); + // If no arg was provided, just use "". + const userInput = String(args[0] ?? ""); + const targetDir = resolveDirectory(userInput, Terminal.currDir); + // Explicitly checking null due to root being "" + if (targetDir === null) return Terminal.error(`Could not resolve directory ${userInput}`); + if (!directoryExistsOnServer(targetDir, server)) return Terminal.error(`Directory ${targetDir} does not exist.`); + Terminal.setcwd(targetDir); } diff --git a/src/Terminal/commands/check.ts b/src/Terminal/commands/check.ts index fc5193f42..8019a93f0 100644 --- a/src/Terminal/commands/check.ts +++ b/src/Terminal/commands/check.ts @@ -1,7 +1,7 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; import { findRunningScript } from "../../Script/ScriptHelpers"; -import { isScriptFilename, validScriptExtensions } from "../../Script/isScriptFilename"; +import { hasScriptExtension, validScriptExtensions } from "../../Paths/ScriptFilePath"; export function check(args: (string | number | boolean)[], server: BaseServer): void { if (args.length < 1) { @@ -11,19 +11,13 @@ export function check(args: (string | number | boolean)[], server: BaseServer): if (!scriptName) return Terminal.error(`Invalid filename: ${args[0]}`); // Can only tail script files - if (!isScriptFilename(scriptName)) { - Terminal.error( - `'check' can only be called on scripts files (filename must end with ${validScriptExtensions.join(", ")})`, - ); - return; + if (!hasScriptExtension(scriptName)) { + return Terminal.error(`check: File extension must be one of ${validScriptExtensions.join(", ")})`); } // Check that the script is running on this machine const runningScript = findRunningScript(scriptName, args.slice(1), server); - if (runningScript == null) { - Terminal.error(`No script named ${scriptName} is running on the server`); - return; - } + if (runningScript == null) return Terminal.error(`No script named ${scriptName} is running on the server`); runningScript.displayLog(); } } diff --git a/src/Terminal/commands/common/editor.ts b/src/Terminal/commands/common/editor.ts index ac8346640..91184edc6 100644 --- a/src/Terminal/commands/common/editor.ts +++ b/src/Terminal/commands/common/editor.ts @@ -1,13 +1,13 @@ import { Terminal } from "../../../Terminal"; -import { removeLeadingSlash, removeTrailingSlash } from "../../DirectoryHelpers"; import { ScriptEditorRouteOptions } from "../../../ui/Router"; import { Router } from "../../../ui/GameRoot"; import { BaseServer } from "../../../Server/BaseServer"; -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"; +import { ScriptFilePath, hasScriptExtension } from "../../../Paths/ScriptFilePath"; +import { TextFilePath, hasTextExtension } from "../../../Paths/TextFilePath"; +import { getGlobbedFileMap } from "../../../Paths/GlobbedFiles"; + +// 2.3: Globbing implementation was removed from this file. Globbing will be reintroduced as broader functionality and integrated here. interface EditorParameters { args: (string | number | boolean)[]; @@ -23,126 +23,34 @@ export async function main(ns) { }`; -interface ISimpleScriptGlob { - glob: string; - preGlob: string; - postGlob: string; - globError: string; - globMatches: string[]; - globAgainst: Map; -} - -function containsSimpleGlob(filename: string): boolean { - return filename.includes("*"); -} - -function detectSimpleScriptGlob({ args, server }: EditorParameters): ISimpleScriptGlob | null { - if (args.length == 1 && containsSimpleGlob(`${args[0]}`)) { - const filename = `${args[0]}`; - const scripts = server.scripts; - const parsedGlob = parseSimpleScriptGlob(filename, scripts); - return parsedGlob; - } - return null; -} - -function parseSimpleScriptGlob(globString: string, globDatabase: Map): ISimpleScriptGlob { - const parsedGlob: ISimpleScriptGlob = { - glob: globString, - preGlob: "", - postGlob: "", - globError: "", - globMatches: [], - globAgainst: globDatabase, - }; - - // Ensure deep globs are minified to simple globs, which act as deep globs in this impl - globString = globString.replace("**", "*"); - - // Ensure only a single glob is present - if (globString.split("").filter((c) => c == "*").length !== 1) { - parsedGlob.globError = "Only a single glob is supported per command.\nexample: `nano my-dir/*.js`"; - return parsedGlob; - } - - // Split arg around glob, normalize preGlob path - [parsedGlob.preGlob, parsedGlob.postGlob] = globString.split("*"); - parsedGlob.preGlob = removeLeadingSlash(parsedGlob.preGlob); - - // Add CWD to preGlob path - const cwd = removeTrailingSlash(Terminal.cwd()); - parsedGlob.preGlob = `${cwd}/${parsedGlob.preGlob}`; - - // For every script on the current server, filter matched scripts per glob values & persist - globDatabase.forEach((script) => { - const filename = script.filename.startsWith("/") ? script.filename : `/${script.filename}`; - if (filename.startsWith(parsedGlob.preGlob) && filename.endsWith(parsedGlob.postGlob)) { - parsedGlob.globMatches.push(filename); - } - }); - - // Rebuild glob for potential error reporting - parsedGlob.glob = `${parsedGlob.preGlob}*${parsedGlob.postGlob}`; - - return parsedGlob; -} - export function commonEditor( command: string, { args, server }: EditorParameters, scriptEditorRouteOptions?: ScriptEditorRouteOptions, ): void { - if (args.length < 1) { - Terminal.error(`Incorrect usage of ${command} command. Usage: ${command} [scriptname]`); - return; - } + if (args.length < 1) return Terminal.error(`Incorrect usage of ${command} command. Usage: ${command} [scriptname]`); + const filesToOpen: Map = new Map(); + for (const arg of args) { + const pattern = String(arg); - let filesToLoadOrCreate = args; - try { - const globSearch = detectSimpleScriptGlob({ args, server }); - if (globSearch) { - if (isEmpty(globSearch.globError) === false) throw new Error(globSearch.globError); - filesToLoadOrCreate = globSearch.globMatches; + // Glob of existing files + if (pattern.includes("*") || pattern.includes("?")) { + for (const [path, file] of getGlobbedFileMap(pattern, server, Terminal.currDir)) { + filesToOpen.set(path, file.content); + } + continue; } - const files = filesToLoadOrCreate.map((arg) => { - const filename = `${arg}`; - - if (isScriptFilename(filename)) { - const filepath = Terminal.getFilepath(filename); - if (!filepath) throw `Invalid filename: ${filename}`; - const script = Terminal.getScript(filename); - const fileIsNs2 = isNs2(filename); - const code = script !== null ? script.code : fileIsNs2 ? newNs2Template : ""; - - if (code === newNs2Template) { - CursorPositions.saveCursor(filename, { - row: 3, - column: 5, - }); - } - - return [filepath, code]; - } - - if (filename.endsWith(".txt")) { - const filepath = Terminal.getFilepath(filename); - if (!filepath) throw `Invalid filename: ${filename}`; - const txt = Terminal.getTextFile(filename); - return [filepath, txt === null ? "" : txt.text]; - } - - throw new Error( - `Invalid file. Only scripts (.script or .js), or text files (.txt) can be edited with ${command}`, - ); - }); - - if (globSearch && files.length === 0) { - throw new Error(`Could not find any valid files to open with ${command} using glob: \`${globSearch.glob}\``); + // Non-glob, files do not need to already exist + const path = Terminal.getFilepath(pattern); + if (!path) return Terminal.error(`Invalid file path ${arg}`); + if (!hasScriptExtension(path) && !hasTextExtension(path)) { + return Terminal.error(`${command}: Only scripts or text files can be edited. Invalid file type: ${arg}`); } - - Router.toScriptEditor(Object.fromEntries(files), scriptEditorRouteOptions); - } catch (e) { - Terminal.error(`${e}`); + const file = server.getContentFile(path); + const content = file ? file.content : isNs2(path) ? newNs2Template : ""; + filesToOpen.set(path, content); + if (content === newNs2Template) CursorPositions.saveCursor(path, { row: 3, column: 5 }); } + Router.toScriptEditor(filesToOpen, scriptEditorRouteOptions); } diff --git a/src/Terminal/commands/cp.ts b/src/Terminal/commands/cp.ts index f14b0d9a8..ade51e545 100644 --- a/src/Terminal/commands/cp.ts +++ b/src/Terminal/commands/cp.ts @@ -1,76 +1,36 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; -import { isScriptFilename } from "../../Script/isScriptFilename"; -import { getDestinationFilepath, areFilesEqual } from "../DirectoryHelpers"; +import { combinePath, getFilenameOnly } from "../../Paths/FilePath"; +import { hasTextExtension } from "../../Paths/TextFilePath"; +import { hasScriptExtension } from "../../Paths/ScriptFilePath"; export function cp(args: (string | number | boolean)[], server: BaseServer): void { - try { - if (args.length !== 2) return Terminal.error("Incorrect usage of cp command. Usage: cp [src] [dst]"); - // Convert a relative path source file to the absolute path. - const src = Terminal.getFilepath(args[0] + ""); - if (src === null) return Terminal.error(`Invalid source filename ${args[0]}`); - - // Get the destination based on the source file and the current directory - const t_dst = getDestinationFilepath(args[1] + "", src, Terminal.cwd()); - if (t_dst === null) return Terminal.error("error parsing dst file"); - - // Convert a relative path destination file to the absolute path. - const dst = Terminal.getFilepath(t_dst); - if (!dst) return Terminal.error(`Invalid destination filename ${t_dst}`); - if (areFilesEqual(src, dst)) return Terminal.error("src and dst cannot be the same"); - - const srcExt = src.slice(src.lastIndexOf(".")); - const dstExt = dst.slice(dst.lastIndexOf(".")); - if (srcExt !== dstExt) return Terminal.error("src and dst must have the same extension."); - - if (!isScriptFilename(src) && !src.endsWith(".txt")) { - return Terminal.error("cp only works for scripts and .txt files"); - } - - // Scp for txt files - if (src.endsWith(".txt")) { - let txtFile = null; - for (let i = 0; i < server.textFiles.length; ++i) { - if (areFilesEqual(server.textFiles[i].fn, src)) { - txtFile = server.textFiles[i]; - break; - } - } - - if (txtFile === null) { - return Terminal.error("No such file exists!"); - } - - const tRes = server.writeToTextFile(dst, txtFile.text); - if (!tRes.success) { - Terminal.error("cp failed"); - return; - } - if (tRes.overwritten) { - Terminal.print(`WARNING: ${dst} already exists and will be overwritten`); - Terminal.print(`${dst} overwritten`); - return; - } - Terminal.print(`${dst} copied`); - return; - } - - // Get the current script - 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) { - Terminal.error(`cp failed`); - return; - } - if (sRes.overwritten) { - Terminal.print(`WARNING: ${dst} already exists and will be overwritten`); - Terminal.print(`${dst} overwritten`); - return; - } - Terminal.print(`${dst} copied`); - } catch (e) { - Terminal.error(e + ""); + if (args.length !== 2) { + return Terminal.error("Incorrect usage of cp command. Usage: cp [source filename] [destination]"); } + // Find the source file + const sourceFilePath = Terminal.getFilepath(String(args[0])); + if (!sourceFilePath) return Terminal.error(`Invalid source filename ${args[0]}`); + if (!hasTextExtension(sourceFilePath) && !hasScriptExtension(sourceFilePath)) { + return Terminal.error("cp: Can only be performed on script and text files"); + } + const source = server.getContentFile(sourceFilePath); + if (!source) return Terminal.error(`File not found: ${sourceFilePath}`); + + // Determine the destination file path. + const destinationInput = String(args[1]); + // First treat the input as a file path. If that fails, try treating it as a directory and reusing source filename. + let destFilePath = Terminal.getFilepath(destinationInput); + if (!destFilePath) { + const destDirectory = Terminal.getDirectory(destinationInput); + if (!destDirectory) return Terminal.error(`Could not resolve ${destinationInput} as a FilePath or Directory`); + destFilePath = combinePath(destDirectory, getFilenameOnly(sourceFilePath)); + } + if (!hasTextExtension(destFilePath) && !hasScriptExtension(destFilePath)) { + return Terminal.error(`cp: Can only copy to script and text files (${destFilePath} is invalid destination)`); + } + + const result = server.writeToContentFile(destFilePath, source.content); + Terminal.print(`File ${sourceFilePath} copied to ${destFilePath}`); + if (result.overwritten) Terminal.warn(`${destFilePath} was overwritten.`); } diff --git a/src/Terminal/commands/download.ts b/src/Terminal/commands/download.ts index 2370b8624..6f7f9fa38 100644 --- a/src/Terminal/commands/download.ts +++ b/src/Terminal/commands/download.ts @@ -1,79 +1,49 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; -import { isScriptFilename } from "../../Script/isScriptFilename"; import FileSaver from "file-saver"; import JSZip from "jszip"; +import { root } from "../../Paths/Directory"; +import { hasScriptExtension } from "../../Paths/ScriptFilePath"; +import { hasTextExtension } from "../../Paths/TextFilePath"; +import { getGlobbedFileMap } from "../../Paths/GlobbedFiles"; -export function exportScripts(pattern: string, server: BaseServer): void { - const matchEnding = pattern.length == 1 || pattern === "*.*" ? null : pattern.slice(1); // Treat *.* the same as * +// Basic globbing implementation only supporting * and ?. Can be broken out somewhere else later. +export function exportScripts(pattern: string, server: BaseServer, currDir = root): void { const zip = new JSZip(); - // Helper function to zip any file contents whose name matches the pattern - const zipFiles = (fileNames: string[], fileContents: string[]): void => { - for (let i = 0; i < fileContents.length; ++i) { - let name = fileNames[i]; - if (name.startsWith("/")) name = name.slice(1); - if (!matchEnding || name.endsWith(matchEnding)) - zip.file(name, new Blob([fileContents[i]], { type: "text/plain" })); - } - }; - // In the case of script files, we pull from the server.scripts array - if (!matchEnding || isScriptFilename(matchEnding)) - zipFiles( - [...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")) - zipFiles( - server.textFiles.map((s) => s.fn), - server.textFiles.map((s) => s.text), - ); + + for (const [name, file] of getGlobbedFileMap(pattern, server, currDir)) { + zip.file(name, new Blob([file.content], { type: "text/plain" })); + } // Return an error if no files matched, rather than an empty zip folder if (Object.keys(zip.files).length == 0) throw new Error(`No files match the pattern ${pattern}`); - const zipFn = `bitburner${isScriptFilename(pattern) ? "Scripts" : pattern === "*.txt" ? "Texts" : "Files"}.zip`; + const zipFn = `bitburner${ + hasScriptExtension(pattern) ? "Scripts" : pattern.endsWith(".txt") ? "Texts" : "Files" + }.zip`; zip.generateAsync({ type: "blob" }).then((content: Blob) => FileSaver.saveAs(content, zipFn)); } export function download(args: (string | number | boolean)[], server: BaseServer): void { - try { - if (args.length !== 1) { - Terminal.error("Incorrect usage of download command. Usage: download [script/text file]"); - return; - } - const fn = args[0] + ""; - // If the parameter starts with *, download all files that match the wildcard pattern - if (fn.startsWith("*")) { - try { - exportScripts(fn, server); - return; - } catch (e: unknown) { - let msg = String(e); - if (e !== null && typeof e == "object" && e.hasOwnProperty("message")) { - msg = String((e as { message: unknown }).message); - } - return Terminal.error(msg); - } - } else if (isScriptFilename(fn)) { - // Download a single script - const script = Terminal.getScript(fn); - if (script != null) { - return script.download(); - } - } else if (fn.endsWith(".txt")) { - // Download a single text file - const txt = Terminal.getTextFile(fn); - if (txt != null) { - return txt.download(); - } - } else { - Terminal.error(`Cannot download this filetype`); - return; - } - Terminal.error(`${fn} does not exist`); - return; - } catch (e) { - Terminal.error(e + ""); - return; + if (args.length !== 1) { + return Terminal.error("Incorrect usage of download command. Usage: download [script/text file]"); } + const pattern = String(args[0]); + // If the path contains a * or ?, treat as glob + if (pattern.includes("*") || pattern.includes("?")) { + try { + exportScripts(pattern, server, Terminal.currDir); + return; + } catch (e: any) { + const msg = String(e?.message ?? e); + return Terminal.error(msg); + } + } + const path = Terminal.getFilepath(pattern); + if (!path) return Terminal.error(`Could not resolve path ${pattern}`); + if (!hasScriptExtension(path) && !hasTextExtension(path)) { + return Terminal.error("Can only download script and text files"); + } + const file = server.getContentFile(path); + if (!file) return Terminal.error(`File not found: ${path}`); + return file.download(); } diff --git a/src/Terminal/commands/grow.ts b/src/Terminal/commands/grow.ts index dee13a95f..280457315 100644 --- a/src/Terminal/commands/grow.ts +++ b/src/Terminal/commands/grow.ts @@ -1,37 +1,12 @@ import { Terminal } from "../../Terminal"; -import { Player } from "@player"; import { BaseServer } from "../../Server/BaseServer"; -import { Server } from "../../Server/Server"; export function grow(args: (string | number | boolean)[], server: BaseServer): void { - if (args.length !== 0) { - Terminal.error("Incorrect usage of grow command. Usage: grow"); - return; - } + if (args.length !== 0) return Terminal.error("Incorrect usage of grow command. Usage: grow"); - if (!(server instanceof Server)) { - Terminal.error( - "Cannot grow your own machines! You are currently connected to your home PC or one of your purchased servers", - ); - } - const normalServer = server as Server; - // Hack the current PC (usually for money) - // You can't grow your home pc or servers you purchased - if (normalServer.purchasedByPlayer) { - Terminal.error( - "Cannot grow your own machines! You are currently connected to your home PC or one of your purchased servers", - ); - return; - } - if (!normalServer.hasAdminRights) { - Terminal.error("You do not have admin rights for this machine! Cannot grow"); - return; - } - if (normalServer.requiredHackingSkill > Player.skills.hacking) { - Terminal.error( - "Your hacking skill is not high enough to attempt hacking this machine. Try analyzing the machine to determine the required hacking skill", - ); - return; - } + if (server.purchasedByPlayer) return Terminal.error("Cannot grow your own machines!"); + if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!"); + // Grow does not require meeting the hacking level, but undefined requiredHackingSkill indicates the wrong type of server. + if (server.requiredHackingSkill === undefined) return Terminal.error("Cannot grow this server."); Terminal.startGrow(); } diff --git a/src/Terminal/commands/hack.ts b/src/Terminal/commands/hack.ts index 368807855..46df900a5 100644 --- a/src/Terminal/commands/hack.ts +++ b/src/Terminal/commands/hack.ts @@ -1,37 +1,17 @@ import { Terminal } from "../../Terminal"; import { Player } from "@player"; import { BaseServer } from "../../Server/BaseServer"; -import { Server } from "../../Server/Server"; export function hack(args: (string | number | boolean)[], server: BaseServer): void { - if (args.length !== 0) { - Terminal.error("Incorrect usage of hack command. Usage: hack"); - return; - } - - if (!(server instanceof Server)) { - Terminal.error( - "Cannot hack your own machines! You are currently connected to your home PC or one of your purchased servers", + if (args.length !== 0) return Terminal.error("Incorrect usage of hack command. Usage: hack"); + if (server.purchasedByPlayer) return Terminal.error("Cannot hack your own machines!"); + if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!"); + // Acts as a functional check that the server is hackable. Hacknet servers should already be filtered out anyway by purchasedByPlayer + if (server.requiredHackingSkill === undefined) return Terminal.error("Cannot hack this server."); + if (server.requiredHackingSkill > Player.skills.hacking) { + return Terminal.error( + "Your hacking skill is not high enough to hack this machine. Try analyzing the machine to determine the required hacking skill", ); } - const normalServer = server as Server; - // Hack the current PC (usually for money) - // You can't hack your home pc or servers you purchased - if (normalServer.purchasedByPlayer) { - Terminal.error( - "Cannot hack your own machines! You are currently connected to your home PC or one of your purchased servers", - ); - return; - } - if (!normalServer.hasAdminRights) { - Terminal.error("You do not have admin rights for this machine! Cannot hack"); - return; - } - if (normalServer.requiredHackingSkill > Player.skills.hacking) { - Terminal.error( - "Your hacking skill is not high enough to attempt hacking this machine. Try analyzing the machine to determine the required hacking skill", - ); - return; - } Terminal.startHack(); } diff --git a/src/Terminal/commands/home.ts b/src/Terminal/commands/home.ts index 471d9a684..beab626e1 100644 --- a/src/Terminal/commands/home.ts +++ b/src/Terminal/commands/home.ts @@ -1,3 +1,4 @@ +import { root } from "../../Paths/Directory"; import { Terminal } from "../../Terminal"; import { Player } from "@player"; @@ -10,5 +11,5 @@ export function home(args: (string | number | boolean)[]): void { Player.currentServer = Player.getHomeComputer().hostname; Player.getCurrentServer().isConnectedTo = true; Terminal.print("Connected to home"); - Terminal.setcwd("/"); + Terminal.setcwd(root); } diff --git a/src/Terminal/commands/kill.ts b/src/Terminal/commands/kill.ts index ea663a6ec..e2f38500c 100644 --- a/src/Terminal/commands/kill.ts +++ b/src/Terminal/commands/kill.ts @@ -1,40 +1,25 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; import { killWorkerScript } from "../../Netscript/killWorkerScript"; +import { hasScriptExtension } from "../../Paths/ScriptFilePath"; export function kill(args: (string | number | boolean)[], server: BaseServer): void { - try { - if (args.length < 1) { - Terminal.error("Incorrect usage of kill command. Usage: kill [scriptname] [arg1] [arg2]..."); - return; - } - if (typeof args[0] === "boolean") { - return; - } - - // Kill by PID - if (typeof args[0] === "number") { - const pid = args[0]; - const res = killWorkerScript(pid); - if (res) { - Terminal.print(`Killing script with PID ${pid}`); - } else { - Terminal.error(`Failed to kill script with PID ${pid}. No such script is running`); - } - - return; - } - - const scriptName = Terminal.getFilepath(args[0]); - if (!scriptName) return Terminal.error(`Invalid filename: ${args[0]}`); - const runningScript = server.getRunningScript(scriptName, args.slice(1)); - if (runningScript == null) { - Terminal.error("No such script is running. Nothing to kill"); - return; - } - killWorkerScript({ runningScript: runningScript, hostname: server.hostname }); - Terminal.print(`Killing ${scriptName}`); - } catch (e) { - Terminal.error(e + ""); + if (args.length < 1) { + return Terminal.error("Incorrect usage of kill command. Usage: kill [pid] or kill [scriptname] [arg1] [arg2]..."); } + if (typeof args[0] === "number") { + const pid = args[0]; + if (killWorkerScript(pid)) return Terminal.print(`Killing script with PID ${pid}`); + } + // Shift args doesn't need to be sliced to check runningScript args + const fileName = String(args.shift()); + const path = Terminal.getFilepath(fileName); + if (!path) return Terminal.error(`Could not parse filename: ${fileName}`); + if (!hasScriptExtension(path)) return Terminal.error(`${path} does not have a script file extension`); + + const runningScript = server.getRunningScript(path, args); + if (runningScript == null) return Terminal.error("No such script is running. Nothing to kill"); + + killWorkerScript(runningScript.pid); + Terminal.print(`Killing ${path}`); } diff --git a/src/Terminal/commands/ls.tsx b/src/Terminal/commands/ls.tsx index cbdae5a8e..40e1660e7 100644 --- a/src/Terminal/commands/ls.tsx +++ b/src/Terminal/commands/ls.tsx @@ -1,15 +1,28 @@ import { Theme } from "@mui/material/styles"; import createStyles from "@mui/styles/createStyles"; import makeStyles from "@mui/styles/makeStyles"; -import { toString } from "lodash"; import React from "react"; import { BaseServer } from "../../Server/BaseServer"; -import { evaluateDirectoryPath, getFirstParentDirectory, isValidDirectoryPath } from "../DirectoryHelpers"; import { Router } from "../../ui/GameRoot"; import { Terminal } from "../../Terminal"; import libarg from "arg"; import { showLiterature } from "../../Literature/LiteratureHelpers"; -import { MessageFilenames, showMessage } from "../../Message/MessageHelpers"; +import { MessageFilename, showMessage } from "../../Message/MessageHelpers"; +import { ScriptFilePath } from "../../Paths/ScriptFilePath"; +import { FilePath, combinePath, removeDirectoryFromPath } from "../../Paths/FilePath"; +import { ContentFilePath } from "../../Paths/ContentFile"; +import { + Directory, + directoryExistsOnServer, + getFirstDirectoryInPath, + resolveDirectory, + root, +} from "../../Paths/Directory"; +import { TextFilePath } from "../../Paths/TextFilePath"; +import { ContractFilePath } from "../../Paths/ContractFilePath"; +import { ProgramFilePath } from "../../Paths/ProgramFilePath"; +import { checkEnum } from "../../utils/helpers/enum"; +import { LiteratureName } from "../../Literature/data/LiteratureNames"; export function ls(args: (string | number | boolean)[], server: BaseServer): void { interface LSFlags { @@ -31,7 +44,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi incorrectUsage(); return; } - const filter = flags["--grep"]; + const filter = flags["--grep"] ?? ""; const numArgs = args.length; function incorrectUsage(): void { @@ -42,74 +55,48 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi return incorrectUsage(); } - // Directory path - let prefix = Terminal.cwd(); - if (!prefix.endsWith("/")) { - prefix += "/"; - } - - // If first arg doesn't contain a - it must be the file/folder - const dir = args[0] && typeof args[0] == "string" && !args[0].startsWith("-") ? args[0] : ""; - const newPath = evaluateDirectoryPath(dir + "", Terminal.cwd()); - prefix = newPath || ""; - if (!prefix.endsWith("/")) { - prefix += "/"; - } - if (!isValidDirectoryPath(prefix)) { - return incorrectUsage(); - } - - // Root directory, which is the same as no 'prefix' at all - if (prefix === "/") { - prefix = ""; + let baseDirectory = Terminal.currDir; + // Parse first argument which should be a directory. + if (args[0] && typeof args[0] == "string" && !args[0].startsWith("-")) { + const directory = resolveDirectory(args[0], args[0].startsWith("/") ? root : Terminal.currDir); + if (directory !== null && directoryExistsOnServer(directory, server)) { + baseDirectory = directory; + } else return incorrectUsage(); } // Display all programs and scripts - const allPrograms: string[] = []; - const allScripts: string[] = []; - const allTextFiles: string[] = []; - const allContracts: string[] = []; - const allMessages: string[] = []; - const folders: string[] = []; + const allPrograms: ProgramFilePath[] = []; + const allScripts: ScriptFilePath[] = []; + const allTextFiles: TextFilePath[] = []; + const allContracts: ContractFilePath[] = []; + const allMessages: FilePath[] = []; + const folders: Directory[] = []; - function handleFn(fn: string, dest: string[]): void { - let parsedFn = fn; - if (prefix) { - if (!fn.startsWith(prefix)) { - return; - } else { - parsedFn = fn.slice(prefix.length, fn.length); - } - } + function handlePath(path: FilePath, dest: FilePath[]): void { + // This parses out any files not in the starting directory. + const parsedPath = removeDirectoryFromPath(baseDirectory, path); + if (!parsedPath) return; - if (filter && !parsedFn.includes(filter)) { + if (!parsedPath.includes(filter)) return; + + // Check if there's a directory in the parsed path, if so we need to add the folder and not the file. + const firstParentDir = getFirstDirectoryInPath(parsedPath); + if (firstParentDir) { + if (!firstParentDir.includes(filter) || folders.includes(firstParentDir)) return; + folders.push(firstParentDir); return; } - - // If the fn includes a forward slash, it must be in a subdirectory. - // Therefore, we only list the "first" directory in its path - if (parsedFn.includes("/")) { - const firstParentDir = getFirstParentDirectory(parsedFn); - if (filter && !firstParentDir.includes(filter)) { - return; - } - - if (!folders.includes(firstParentDir)) { - folders.push(firstParentDir); - } - - return; - } - - dest.push(parsedFn); + dest.push(parsedPath); } // 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 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); + // Type assertions that programs and msg/lit are filepaths are safe due to checks in + // Program, Message, and Literature constructors + for (const program of server.programs) handlePath(program as FilePath, allPrograms); + for (const scriptFilename of server.scripts.keys()) handlePath(scriptFilename, allScripts); + for (const txtFilename of server.textFiles.keys()) handlePath(txtFilename, allTextFiles); + for (const contract of server.contracts) handlePath(contract.fn, allContracts); + for (const msgOrLit of server.messages) handlePath(msgOrLit as FilePath, allMessages); // Sort the files/folders alphabetically then print each allPrograms.sort(); @@ -119,13 +106,10 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi allMessages.sort(); folders.sort(); - interface ClickableRowProps { - row: string; - prefix: string; - hostname: string; + interface ScriptRowProps { + scripts: ScriptFilePath[]; } - - function ClickableScriptRow({ row, prefix, hostname }: ClickableRowProps): React.ReactElement { + function ClickableScriptRow({ scripts }: ScriptRowProps): React.ReactElement { const classes = makeStyles((theme: Theme) => createStyles({ scriptLinksWrap: { @@ -135,48 +119,38 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi scriptLink: { cursor: "pointer", textDecorationLine: "underline", - paddingRight: "1.15em", - "&:last-child": { padding: 0 }, + marginRight: "1.5em", + "&:last-child": { marginRight: 0 }, }, }), )(); - const rowSplit = row.split("~"); - let rowSplitArray = rowSplit.map((x) => [x.trim(), x.replace(x.trim(), "")]); - rowSplitArray = rowSplitArray.filter((x) => !!x[0]); - - function onScriptLinkClick(filename: string): void { - if (!server.isConnectedTo) { - return Terminal.error(`File is not on this server, connect to ${hostname} and try again`); - } - if (filename.startsWith("/")) filename = filename.slice(1); - // Terminal.getFilepath needs leading slash to correctly work here - if (prefix === "") filename = `/${filename}`; - const filepath = Terminal.getFilepath(`${prefix}${filename}`); - // Terminal.getScript also calls Terminal.getFilepath and therefore also - // needs the given parameter - if (!filepath) { - return Terminal.error(`Invalid filename (${prefix}${filename}) when clicking script link. This is a bug.`); - } - const code = toString(Terminal.getScript(`${prefix}${filename}`)?.code); - Router.toScriptEditor({ [filepath]: code }); + function onScriptLinkClick(filename: ScriptFilePath): void { + const filePath = combinePath(baseDirectory, filename); + const code = server.scripts.get(filePath)?.content ?? ""; + const map = new Map(); + map.set(filePath, code); + Router.toScriptEditor(map); } return ( - {rowSplitArray.map((rowItem) => ( - - onScriptLinkClick(rowItem[0])}> - {rowItem[0]} + {scripts.map((script) => ( + + onScriptLinkClick(script)}> + {script} - {rowItem[1]} + ))} ); } - function ClickableMessageRow({ row, prefix, hostname }: ClickableRowProps): React.ReactElement { + interface MessageRowProps { + messages: FilePath[]; + } + function ClickableMessageRow({ messages }: MessageRowProps): React.ReactElement { const classes = makeStyles((theme: Theme) => createStyles({ linksWrap: { @@ -186,41 +160,33 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi link: { cursor: "pointer", textDecorationLine: "underline", - paddingRight: "1.15em", - "&:last-child": { padding: 0 }, + marginRight: "1.5em", + "&:last-child": { marginRight: 0 }, }, }), )(); - const rowSplit = row.split("~"); - let rowSplitArray = rowSplit.map((x) => [x.trim(), x.replace(x.trim(), "")]); - rowSplitArray = rowSplitArray.filter((x) => !!x[0]); - - function onMessageLinkClick(filename: string): void { + function onMessageLinkClick(filename: FilePath): void { if (!server.isConnectedTo) { - return Terminal.error(`File is not on this server, connect to ${hostname} and try again`); - } - if (filename.startsWith("/")) filename = filename.slice(1); - const filepath = Terminal.getFilepath(`${prefix}${filename}`); - if (!filepath) { - return Terminal.error(`Invalid filename ${prefix}${filename} when clicking message link. This is a bug.`); + return Terminal.error(`File is not on this server, connect to ${server.hostname} and try again`); } + // Message and lit files have no directories - if (filepath.endsWith(".lit")) { - showLiterature(filepath); - } else if (filepath.endsWith(".msg")) { - showMessage(filepath as MessageFilenames); + if (checkEnum(MessageFilename, filename)) { + showMessage(filename); + } else if (checkEnum(LiteratureName, filename)) { + showLiterature(filename); } } return ( - {rowSplitArray.map((rowItem) => ( - - onMessageLinkClick(rowItem[0])}> - {rowItem[0]} + {messages.map((message) => ( + + onMessageLinkClick(message)}> + {message} - {rowItem[1]} + ))} @@ -236,39 +202,51 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi Script, } - interface FileGroup { - type: FileType; - segments: string[]; - } + type FileGroup = + | { + // Types that are not clickable only need to be string[] + type: FileType.Folder | FileType.Program | FileType.Contract | FileType.TextFile; + segments: string[]; + } + | { type: FileType.Message; segments: FilePath[] } + | { type: FileType.Script; segments: ScriptFilePath[] }; - function postSegments(group: FileGroup, flags: LSFlags): void { - const segments = group.segments; - const linked = group.type === FileType.Script || group.type === FileType.Message; + function postSegments({ type, segments }: FileGroup, flags: LSFlags): void { const maxLength = Math.max(...segments.map((s) => s.length)) + 1; const filesPerRow = flags["-l"] === true ? 1 : Math.ceil(80 / maxLength); - for (let i = 0; i < segments.length; i++) { - let row = ""; - for (let col = 0; col < filesPerRow; col++) { - if (!(i < segments.length)) break; - row += segments[i]; - row += " ".repeat(maxLength * (col + 1) - row.length); - if (linked) { - row += "~"; + const padLength = Math.max(maxLength + 2, 40); + let i = 0; + if (type === FileType.Script) { + while (i < segments.length) { + const scripts: ScriptFilePath[] = []; + for (let col = 0; col < filesPerRow && i < segments.length; col++, i++) { + scripts.push(segments[i]); } + Terminal.printRaw(); + } + return; + } + if (type === FileType.Message) { + while (i < segments.length) { + const messages: FilePath[] = []; + for (let col = 0; col < filesPerRow && i < segments.length; col++, i++) { + messages.push(segments[i]); + } + Terminal.printRaw(); + } + return; + } + while (i < segments.length) { + let row = ""; + for (let col = 0; col < filesPerRow; col++, i++) { + if (!(i < segments.length)) break; + row += segments[i].padEnd(padLength); i++; } - i--; - - switch (group.type) { + switch (type) { case FileType.Folder: Terminal.printRaw({row}); break; - case FileType.Script: - Terminal.printRaw(); - break; - case FileType.Message: - Terminal.printRaw(); - break; default: Terminal.print(row); } @@ -282,8 +260,8 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi { type: FileType.Program, segments: allPrograms }, { type: FileType.Contract, segments: allContracts }, { type: FileType.Script, segments: allScripts }, - ].filter((g) => g.segments.length > 0); + ]; for (const group of groups) { - postSegments(group, flags); + if (group.segments.length > 0) postSegments(group, flags); } } diff --git a/src/Terminal/commands/mv.ts b/src/Terminal/commands/mv.ts index 965de7ca2..c6e26d720 100644 --- a/src/Terminal/commands/mv.ts +++ b/src/Terminal/commands/mv.ts @@ -1,89 +1,35 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; -import { isScriptFilename } from "../../Script/isScriptFilename"; -import { TextFile } from "../../TextFile"; -import { Script } from "../../Script/Script"; -import { getDestinationFilepath, areFilesEqual } from "../DirectoryHelpers"; +import { hasScriptExtension } from "../../Paths/ScriptFilePath"; +import { hasTextExtension } from "../../Paths/TextFilePath"; export function mv(args: (string | number | boolean)[], server: BaseServer): void { if (args.length !== 2) { Terminal.error(`Incorrect number of arguments. Usage: mv [src] [dest]`); return; } + const [source, destination] = args.map((arg) => arg + ""); - try { - const source = args[0] + ""; - const t_dest = args[1] + ""; + const sourcePath = Terminal.getFilepath(source); + if (!sourcePath) return Terminal.error(`Invalid source filename: ${source}`); + const destinationPath = Terminal.getFilepath(destination); + if (!destinationPath) return Terminal.error(`Invalid destination filename: ${destinationPath}`); - if (!isScriptFilename(source) && !source.endsWith(".txt")) { - Terminal.error(`'mv' can only be used on scripts and text files (.txt)`); - return; - } - - const srcFile = Terminal.getFile(source); - if (srcFile == null) return Terminal.error(`Source file ${source} does not exist`); - - const sourcePath = Terminal.getFilepath(source); - if (!sourcePath) return Terminal.error(`Invalid source filename: ${source}`); - - // Get the destination based on the source file and the current directory - const dest = getDestinationFilepath(t_dest, source, Terminal.cwd()); - if (dest === null) return Terminal.error("error parsing dst file"); - - const destFile = Terminal.getFile(dest); - const destPath = Terminal.getFilepath(dest); - if (!destPath) return Terminal.error(`Invalid destination filename: ${destPath}`); - if (areFilesEqual(sourcePath, destPath)) return Terminal.error(`Source and destination files are the same file`); - - // 'mv' command only works on scripts and txt files. - // Also, you can't convert between different file types - if (isScriptFilename(source)) { - const script = srcFile as Script; - if (!isScriptFilename(destPath)) return Terminal.error(`Source and destination files must have the same type`); - - // Command doesn't work if script is running - if (server.isRunning(sourcePath)) return Terminal.error(`Cannot use 'mv' on a script that is running`); - - if (destFile != null) { - // Already exists, will be overwritten, so we'll delete it - - // Command doesn't work if script is running - if (server.isRunning(destPath)) { - Terminal.error(`Cannot use 'mv' on a script that is running`); - return; - } - - const status = server.removeFile(destPath); - if (!status.res) { - Terminal.error(`Something went wrong...please contact game dev (probably a bug)`); - return; - } else { - Terminal.print("Warning: The destination file was overwritten"); - } - } - - script.filename = destPath; - } else if (srcFile instanceof TextFile) { - const textFile = srcFile; - if (!dest.endsWith(".txt")) { - Terminal.error(`Source and destination files must have the same type`); - return; - } - - if (destFile != null) { - // Already exists, will be overwritten, so we'll delete it - const status = server.removeFile(destPath); - if (!status.res) { - Terminal.error(`Something went wrong...please contact game dev (probably a bug)`); - return; - } else { - Terminal.print("Warning: The destination file was overwritten"); - } - } - - textFile.fn = destPath; - } - } catch (e) { - Terminal.error(e + ""); + if ( + (!hasScriptExtension(sourcePath) && !hasTextExtension(sourcePath)) || + (!hasScriptExtension(destinationPath) && !hasTextExtension(destinationPath)) + ) { + return Terminal.error(`'mv' can only be used on scripts and text files (.txt)`); } + + // Allow content to be moved between scripts and textfiles, no need to limit this. + const sourceContentFile = server.getContentFile(sourcePath); + if (!sourceContentFile) return Terminal.error(`Source file ${sourcePath} does not exist`); + + if (!sourceContentFile.deleteFromServer(server)) { + return Terminal.error(`Could not remove source file ${sourcePath} from existing location.`); + } + Terminal.print(`Moved ${sourcePath} to ${destinationPath}`); + const { overwritten } = server.writeToContentFile(destinationPath, sourceContentFile.content); + if (overwritten) Terminal.warn(`${destinationPath} was overwritten.`); } diff --git a/src/Terminal/commands/run.ts b/src/Terminal/commands/run.ts index dc35f649c..25dd83532 100644 --- a/src/Terminal/commands/run.ts +++ b/src/Terminal/commands/run.ts @@ -1,23 +1,26 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; -import { isScriptFilename } from "../../Script/isScriptFilename"; import { runScript } from "./runScript"; import { runProgram } from "./runProgram"; +import { hasScriptExtension } from "../../Paths/ScriptFilePath"; +import { hasContractExtension } from "../../Paths/ContractFilePath"; +import { hasProgramExtension } from "../../Paths/ProgramFilePath"; export function run(args: (string | number | boolean)[], server: BaseServer): void { // Run a program or a script - if (args.length < 1) { - Terminal.error("Incorrect number of arguments. Usage: run [program/script] [-t] [num threads] [arg1] [arg2]..."); - } else { - const executableName = args[0] + ""; + const arg = args.shift(); + if (!arg) return Terminal.error("Usage: run [program/script] [-t] [num threads] [arg1] [arg2]..."); - // Check if its a script or just a program/executable - if (isScriptFilename(executableName)) { - runScript(args, server); - } else if (executableName.endsWith(".cct")) { - Terminal.runContract(executableName); - } else { - runProgram(args, server); - } + const path = Terminal.getFilepath(String(arg)); + if (!path) return Terminal.error(`${args[0]} is not a valid filepath.`); + if (hasScriptExtension(path)) { + args.shift(); + return runScript(path, args, server); + } else if (hasContractExtension(path)) { + Terminal.runContract(path); + return; + } else if (hasProgramExtension(path)) { + return runProgram(path, args, server); } + Terminal.error(`Invalid file extension. Only .js, .script, .cct, and .exe files can be ran.`); } diff --git a/src/Terminal/commands/runProgram.ts b/src/Terminal/commands/runProgram.ts index fc9c6d97d..1c20c067e 100644 --- a/src/Terminal/commands/runProgram.ts +++ b/src/Terminal/commands/runProgram.ts @@ -1,37 +1,21 @@ import { Terminal } from "../../Terminal"; import { Player } from "@player"; import { BaseServer } from "../../Server/BaseServer"; -import { Programs } from "../../Programs/Programs"; - -export function runProgram(args: (string | number | boolean)[], server: BaseServer): void { - if (args.length < 1) { - return; - } +import { CompletedProgramName, Programs } from "../../Programs/Programs"; +import { ProgramFilePath } from "../../Paths/ProgramFilePath"; +export function runProgram(path: ProgramFilePath, args: (string | number | boolean)[], server: BaseServer): void { // Check if you have the program on your computer. If you do, execute it, otherwise // display an error message - const programName = args[0] + ""; + const programLowered = path.toLowerCase(); + // Support lowercase even though it's an enum - if (!Player.hasProgram(programName)) { + const realProgramName = Object.values(CompletedProgramName).find((name) => name.toLowerCase() === programLowered); + if (!realProgramName || !Player.hasProgram(realProgramName)) { Terminal.error( - `No such (exe, script, js, ns, or cct) file! (Only programs that exist on your home computer or scripts on ${server.hostname} can be run)`, + `No such (exe, script, js, or cct) file! (Only finished programs that exist on your home computer or scripts on ${server.hostname} can be run)`, ); return; } - - if (args.length < 1) { - return; - } - - for (const program of Object.values(Programs)) { - if (program.name.toLocaleLowerCase() === programName.toLocaleLowerCase()) { - program.run( - args.slice(1).map((arg) => arg + ""), - server, - ); - return; - } - } - - Terminal.error("Invalid executable. Cannot be run"); + Programs[realProgramName].run(args.map(String), server); } diff --git a/src/Terminal/commands/runScript.ts b/src/Terminal/commands/runScript.ts index c2b1b0a84..eec62fcfc 100644 --- a/src/Terminal/commands/runScript.ts +++ b/src/Terminal/commands/runScript.ts @@ -3,27 +3,21 @@ import { BaseServer } from "../../Server/BaseServer"; import { LogBoxEvents } from "../../ui/React/LogBoxManager"; import { startWorkerScript } from "../../NetscriptWorker"; import { RunningScript } from "../../Script/RunningScript"; -import { findRunningScript } from "../../Script/ScriptHelpers"; import * as libarg from "arg"; import { formatRam } from "../../ui/formatNumber"; import { ScriptArg } from "@nsdefs"; import { isPositiveInteger } from "../../types"; +import { ScriptFilePath } from "../../Paths/ScriptFilePath"; -export function runScript(commandArgs: (string | number | boolean)[], server: BaseServer): void { - if (commandArgs.length < 1) { - Terminal.error( - `Bug encountered with Terminal.runScript(). Command array has a length of less than 1: ${commandArgs}`, - ); - return; - } - - const scriptName = Terminal.getFilepath(commandArgs[0] + ""); - if (!scriptName) return Terminal.error(`Invalid filename: ${commandArgs[0]}`); +export function runScript(path: ScriptFilePath, commandArgs: (string | number | boolean)[], server: BaseServer): void { + // This takes in the absolute filepath, see "run.ts" + const script = server.scripts.get(path); + if (!script) return Terminal.error(`Script ${path} does not exist on this server.`); const runArgs = { "--tail": Boolean, "-t": Number }; const flags = libarg(runArgs, { permissive: true, - argv: commandArgs.slice(1), + argv: commandArgs, }); const tailFlag = flags["--tail"] === true; const numThreads = parseFloat(flags["-t"] ?? 1); @@ -35,34 +29,24 @@ export function runScript(commandArgs: (string | number | boolean)[], server: Ba const args = flags["_"] as ScriptArg[]; // Check if this script is already running - if (findRunningScript(scriptName, args, server) != null) { - Terminal.error( - "This script is already running with the same args. Cannot run multiple instances with the same args", - ); - return; + if (server.getRunningScript(path, args)) { + return Terminal.error("This script is already running with the same args."); } - // Check if the script exists and if it does run it - const script = server.scripts.get(scriptName); - if (!script) return Terminal.error("No such script"); - 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 (!server.hasAdminRights) { - Terminal.error("Need root access to run script"); - return; - } + if (!server.hasAdminRights) return Terminal.error("Need root access to run script"); if (ramUsage > ramAvailable + 0.001) { - Terminal.error( + return 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 @@ -70,10 +54,7 @@ export function runScript(commandArgs: (string | number | boolean)[], server: Ba runningScript.threads = numThreads; const success = startWorkerScript(runningScript, server); - if (!success) { - Terminal.error(`Failed to start script`); - return; - } + if (!success) return Terminal.error(`Failed to start script`); Terminal.print( `Running script with ${numThreads} thread(s), pid ${runningScript.pid} and args: ${JSON.stringify(args)}.`, diff --git a/src/Terminal/commands/scananalyze.ts b/src/Terminal/commands/scananalyze.ts index 1c8b410ee..871f37fd3 100644 --- a/src/Terminal/commands/scananalyze.ts +++ b/src/Terminal/commands/scananalyze.ts @@ -1,6 +1,6 @@ import { Terminal } from "../../Terminal"; import { Player } from "@player"; -import { Programs } from "../../Programs/Programs"; +import { CompletedProgramName } from "../../Programs/Programs"; export function scananalyze(args: (string | number | boolean)[]): void { if (args.length === 0) { @@ -19,18 +19,18 @@ export function scananalyze(args: (string | number | boolean)[]): void { const depth = parseInt(args[0] + ""); if (isNaN(depth) || depth < 0) { - Terminal.error("Incorrect usage of scan-analyze command. depth argument must be positive numeric"); - return; + return Terminal.error("Incorrect usage of scan-analyze command. depth argument must be positive numeric"); } - if (depth > 3 && !Player.hasProgram(Programs.DeepscanV1.name) && !Player.hasProgram(Programs.DeepscanV2.name)) { - Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 3"); - return; - } else if (depth > 5 && !Player.hasProgram(Programs.DeepscanV2.name)) { - Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 5"); - return; + if ( + depth > 3 && + !Player.hasProgram(CompletedProgramName.deepScan1) && + !Player.hasProgram(CompletedProgramName.deepScan2) + ) { + return Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 3"); + } else if (depth > 5 && !Player.hasProgram(CompletedProgramName.deepScan2)) { + return Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 5"); } else if (depth > 10) { - Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 10"); - return; + return Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 10"); } Terminal.executeScanAnalyzeCommand(depth, all); } diff --git a/src/Terminal/commands/scp.ts b/src/Terminal/commands/scp.ts index d4457ac36..25c3a04b6 100644 --- a/src/Terminal/commands/scp.ts +++ b/src/Terminal/commands/scp.ts @@ -1,71 +1,40 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; import { GetServer } from "../../Server/AllServers"; -import { isScriptFilename } from "../../Script/isScriptFilename"; +import { hasScriptExtension } from "../../Paths/ScriptFilePath"; +import { hasTextExtension } from "../../Paths/TextFilePath"; +import { checkEnum } from "../../utils/helpers/enum"; +import { LiteratureName } from "../../Literature/data/LiteratureNames"; export function scp(args: (string | number | boolean)[], server: BaseServer): void { - try { - if (args.length !== 2) { - Terminal.error("Incorrect usage of scp command. Usage: scp [file] [destination hostname]"); - return; - } - const scriptname = Terminal.getFilepath(args[0] + ""); - if (!scriptname) return Terminal.error(`Invalid filename: ${args[0]}`); - if (!scriptname.endsWith(".lit") && !isScriptFilename(scriptname) && !scriptname.endsWith(".txt")) { - Terminal.error("scp only works for scripts, text files (.txt), and literature files (.lit)"); - return; - } - - const destServer = GetServer(args[1] + ""); - if (destServer == null) { - Terminal.error(`Invalid destination. ${args[1]} not found`); - return; - } - - // Scp for lit files - if (scriptname.endsWith(".lit")) { - if (!server.messages.includes(scriptname)) return Terminal.error("No such file exists!"); - - const onDestServer = destServer.messages.includes(scriptname); - if (!onDestServer) destServer.messages.push(scriptname); - return Terminal.print(`${scriptname} ${onDestServer ? "was already on" : "copied to"} ${destServer.hostname}`); - } - - // Scp for txt files - if (scriptname.endsWith(".txt")) { - const txtFile = server.textFiles.find((txtFile) => txtFile.fn === scriptname); - if (!txtFile) return Terminal.error("No such file exists!"); - - const tRes = destServer.writeToTextFile(txtFile.fn, txtFile.text); - if (!tRes.success) { - Terminal.error("scp failed"); - return; - } - if (tRes.overwritten) { - Terminal.print(`WARNING: ${scriptname} already exists on ${destServer.hostname} and will be overwritten`); - Terminal.print(`${scriptname} overwritten on ${destServer.hostname}`); - return; - } - Terminal.print(`${scriptname} copied over to ${destServer.hostname}`); - return; - } - - // Get the current script - 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) { - Terminal.error(`scp failed`); - return; - } - if (sRes.overwritten) { - Terminal.print(`WARNING: ${scriptname} already exists on ${destServer.hostname} and will be overwritten`); - Terminal.print(`${scriptname} overwritten on ${destServer.hostname}`); - return; - } - Terminal.print(`${scriptname} copied over to ${destServer.hostname}`); - } catch (e) { - Terminal.error(e + ""); + if (args.length !== 2) { + return Terminal.error("Incorrect usage of scp command. Usage: scp [source filename] [destination hostname]"); } + const [scriptname, destHostname] = args.map((arg) => arg + ""); + + const path = Terminal.getFilepath(scriptname); + if (!path) return Terminal.error(`Invalid file path: ${scriptname}`); + + const destServer = GetServer(destHostname); + if (!destServer) return Terminal.error(`Invalid destination server: ${args[1]}`); + + // Lit files + if (path.endsWith(".lit")) { + if (!checkEnum(LiteratureName, path) || !server.messages.includes(path)) { + return Terminal.error(`No file at path ${path}`); + } + if (destServer.messages.includes(path)) return Terminal.print(`${path} was already on ${destHostname}`); + destServer.messages.push(path); + return Terminal.print(`Copied ${path} to ${destHostname}`); + } + + if (!hasScriptExtension(path) && !hasTextExtension(path)) { + return Terminal.error("scp only works for scripts, text files (.txt), and literature files (.lit)"); + } + // Text or script + const source = server.getContentFile(path); + if (!source) return Terminal.error(`No file at path ${path}`); + const { overwritten } = destServer.writeToContentFile(path, source.content); + if (overwritten) Terminal.warn(`${path} already exists on ${destHostname} and will be overwritten`); + Terminal.print(`${path} copied to ${destHostname}`); } diff --git a/src/Terminal/commands/tail.ts b/src/Terminal/commands/tail.ts index 1f42f91e0..ebddb5531 100644 --- a/src/Terminal/commands/tail.ts +++ b/src/Terminal/commands/tail.ts @@ -1,74 +1,66 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; import { findRunningScriptByPid } from "../../Script/ScriptHelpers"; -import { isScriptFilename, validScriptExtensions } from "../../Script/isScriptFilename"; import { compareArrays } from "../../utils/helpers/compareArrays"; import { LogBoxEvents } from "../../ui/React/LogBoxManager"; +import { hasScriptExtension } from "../../Paths/ScriptFilePath"; export function tail(commandArray: (string | number | boolean)[], server: BaseServer): void { - try { - if (commandArray.length < 1) { - Terminal.error("Incorrect number of arguments. Usage: tail [script] [arg1] [arg2]..."); - } else if (typeof commandArray[0] === "string") { - const scriptName = Terminal.getFilepath(commandArray[0]); - if (!scriptName) return Terminal.error(`Invalid filename: ${commandArray[0]}`); - if (!isScriptFilename(scriptName)) { - Terminal.error(`tail can only be called on ${validScriptExtensions.join(", ")} files, or by PID`); - return; - } - - // Get script arguments - const args = []; - for (let i = 1; i < commandArray.length; ++i) { - args.push(commandArray[i]); - } - - // go over all the running scripts. If there's a perfect - // match, use it! - for (let i = 0; i < server.runningScripts.length; ++i) { - if (server.runningScripts[i].filename === scriptName && compareArrays(server.runningScripts[i].args, args)) { - LogBoxEvents.emit(server.runningScripts[i]); - return; - } - } - - // Find all scripts that are potential candidates. - const candidates = []; - for (let i = 0; i < server.runningScripts.length; ++i) { - // only scripts that have more arguments (equal arguments is already caught) - if (server.runningScripts[i].args.length < args.length) continue; - // make a smaller copy of the args. - const args2 = server.runningScripts[i].args.slice(0, args.length); - if (server.runningScripts[i].filename === scriptName && compareArrays(args2, args)) { - candidates.push(server.runningScripts[i]); - } - } - - // If there's only 1 possible choice, use that. - if (candidates.length === 1) { - LogBoxEvents.emit(candidates[0]); - return; - } - - // otherwise lists all possible conflicting choices. - if (candidates.length > 1) { - Terminal.error("Found several potential candidates:"); - for (const candidate of candidates) Terminal.error(`${candidate.filename} ${candidate.args.join(" ")}`); - Terminal.error("Script arguments need to be specified."); - return; - } - - // if there's no candidate then we just don't know. - Terminal.error(`No script named ${scriptName} is running on the server`); - } else if (typeof commandArray[0] === "number") { - const runningScript = findRunningScriptByPid(commandArray[0], server); - if (runningScript == null) { - Terminal.error(`No script with PID ${commandArray[0]} is running on the server`); - return; - } - LogBoxEvents.emit(runningScript); - } - } catch (e) { - Terminal.error(e + ""); + if (commandArray.length < 1) { + return Terminal.error("Incorrect number of arguments. Usage: tail [script] [arg1] [arg2]..."); } + if (typeof commandArray[0] === "number") { + const runningScript = findRunningScriptByPid(commandArray[0], server); + if (!runningScript) return Terminal.error(`No script with PID ${commandArray[0]} is running on the server`); + LogBoxEvents.emit(runningScript); + return; + } + + const path = Terminal.getFilepath(String(commandArray[0])); + if (!path) return Terminal.error(`Invalid file path: ${commandArray[0]}`); + if (!hasScriptExtension(path)) return Terminal.error(`Invalid file extension. Tail can only be used on scripts.`); + + // Get script arguments + const args = []; + for (let i = 1; i < commandArray.length; ++i) { + args.push(commandArray[i]); + } + + // go over all the running scripts. If there's a perfect + // match, use it! + for (let i = 0; i < server.runningScripts.length; ++i) { + if (server.runningScripts[i].filename === path && compareArrays(server.runningScripts[i].args, args)) { + LogBoxEvents.emit(server.runningScripts[i]); + return; + } + } + + // Find all scripts that are potential candidates. + const candidates = []; + for (let i = 0; i < server.runningScripts.length; ++i) { + // only scripts that have more arguments (equal arguments is already caught) + if (server.runningScripts[i].args.length < args.length) continue; + // make a smaller copy of the args. + const args2 = server.runningScripts[i].args.slice(0, args.length); + if (server.runningScripts[i].filename === path && compareArrays(args2, args)) { + candidates.push(server.runningScripts[i]); + } + } + + // If there's only 1 possible choice, use that. + if (candidates.length === 1) { + LogBoxEvents.emit(candidates[0]); + return; + } + + // otherwise lists all possible conflicting choices. + if (candidates.length > 1) { + Terminal.error("Found several potential candidates:"); + for (const candidate of candidates) Terminal.error(`${candidate.filename} ${candidate.args.join(" ")}`); + Terminal.error("Script arguments need to be specified."); + return; + } + + // if there's no candidate then we just don't know. + Terminal.error(`No script named ${path} is running on the server`); } diff --git a/src/Terminal/commands/weaken.ts b/src/Terminal/commands/weaken.ts index 4b2a657b3..4de82a491 100644 --- a/src/Terminal/commands/weaken.ts +++ b/src/Terminal/commands/weaken.ts @@ -1,37 +1,12 @@ import { Terminal } from "../../Terminal"; -import { Player } from "@player"; import { BaseServer } from "../../Server/BaseServer"; -import { Server } from "../../Server/Server"; export function weaken(args: (string | number | boolean)[], server: BaseServer): void { - if (args.length !== 0) { - Terminal.error("Incorrect usage of weaken command. Usage: weaken"); - return; - } + if (args.length !== 0) return Terminal.error("Incorrect usage of weaken command. Usage: weaken"); - if (!(server instanceof Server)) { - Terminal.error( - "Cannot weaken your own machines! You are currently connected to your home PC or one of your purchased servers", - ); - } - const normalServer = server as Server; - // Hack the current PC (usually for money) - // You can't weaken your home pc or servers you purchased - if (normalServer.purchasedByPlayer) { - Terminal.error( - "Cannot weaken your own machines! You are currently connected to your home PC or one of your purchased servers", - ); - return; - } - if (!normalServer.hasAdminRights) { - Terminal.error("You do not have admin rights for this machine! Cannot weaken"); - return; - } - if (normalServer.requiredHackingSkill > Player.skills.hacking) { - Terminal.error( - "Your hacking skill is not high enough to attempt hacking this machine. Try analyzing the machine to determine the required hacking skill", - ); - return; - } + if (server.purchasedByPlayer) return Terminal.error("Cannot weaken your own machines!"); + if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!"); + // Weaken does not require meeting the hacking level, but undefined requiredHackingSkill indicates the wrong type of server. + if (server.requiredHackingSkill === undefined) return Terminal.error("Cannot weaken this server."); Terminal.startWeaken(); } diff --git a/src/Terminal/commands/wget.ts b/src/Terminal/commands/wget.ts index 3378eab4d..2249d31e8 100644 --- a/src/Terminal/commands/wget.ts +++ b/src/Terminal/commands/wget.ts @@ -2,7 +2,8 @@ import $ from "jquery"; import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; -import { isScriptFilename } from "../../Script/isScriptFilename"; +import { hasScriptExtension } from "../../Paths/ScriptFilePath"; +import { hasTextExtension } from "../../Paths/TextFilePath"; export function wget(args: (string | number | boolean)[], server: BaseServer): void { if (args.length !== 2) { @@ -12,20 +13,17 @@ export function wget(args: (string | number | boolean)[], server: BaseServer): v const url = args[0] + ""; const target = Terminal.getFilepath(args[1] + ""); - if (!target || (!isScriptFilename(target) && !target.endsWith(".txt"))) { + if (!target || (!hasScriptExtension(target) && !hasTextExtension(target))) { return Terminal.error(`wget failed: Invalid target file. Target file must be script or text file`); } $.get( url, function (data: unknown) { let res; - if (isScriptFilename(target)) { - res = server.writeToScriptFile(target, String(data)); - } else { + if (hasTextExtension(target)) { res = server.writeToTextFile(target, String(data)); - } - if (!res.success) { - return Terminal.error("wget failed"); + } else { + res = server.writeToScriptFile(target, String(data)); } if (res.overwritten) { return Terminal.print(`wget successfully retrieved content and overwrote ${target}`); diff --git a/src/Terminal/determineAllPossibilitiesForTabCompletion.ts b/src/Terminal/determineAllPossibilitiesForTabCompletion.ts deleted file mode 100644 index fa1c3c6e0..000000000 --- a/src/Terminal/determineAllPossibilitiesForTabCompletion.ts +++ /dev/null @@ -1,408 +0,0 @@ -import { evaluateDirectoryPath, getAllParentDirectories } from "./DirectoryHelpers"; -import { getSubdirectories } from "./DirectoryServerHelpers"; - -import { Aliases, GlobalAliases, substituteAliases } from "../Alias"; -import { DarkWebItems } from "../DarkWeb/DarkWebItems"; -import { Player } from "@player"; -import { GetAllServers } from "../Server/AllServers"; -import { Server } from "../Server/Server"; -import { ParseCommand, ParseCommands } from "./Parser"; -import { HelpTexts } from "./HelpText"; -import { isScriptFilename } from "../Script/isScriptFilename"; -import { compile } from "../NetscriptJSEvaluator"; -import { Flags } from "../NetscriptFunctions/Flags"; -import { AutocompleteData } from "@nsdefs"; -import * as libarg from "arg"; - -// TODO: this shouldn't be hardcoded in two places with no typechecks to verify equivalence -// An array of all Terminal commands -const commands = [ - "alias", - "analyze", - "backdoor", - "cat", - "cd", - "changelog", - "check", - "clear", - "cls", - "connect", - "cp", - "download", - "expr", - "free", - "grow", - "hack", - "help", - "home", - "hostname", - "ifconfig", - "kill", - "killall", - "ls", - "lscpu", - "mem", - "mv", - "nano", - "ps", - "rm", - "run", - "scan-analyze", - "scan", - "scp", - "sudov", - "tail", - "theme", - "top", - "vim", - "weaken", -]; - -export async function determineAllPossibilitiesForTabCompletion( - input: string, - index: number, - currPath = "", -): Promise { - input = substituteAliases(input); - let allPos: string[] = []; - allPos = allPos.concat(Object.keys(GlobalAliases)); - const currServ = Player.getCurrentServer(); - const homeComputer = Player.getHomeComputer(); - - let parentDirPath = ""; - let evaledParentDirPath: string | null = null; - // Helper functions - function addAllCodingContracts(): void { - for (const cct of currServ.contracts) { - allPos.push(cct.fn); - } - } - - function addAllLitFiles(): void { - for (const file of currServ.messages) { - if (!file.endsWith(".msg")) { - allPos.push(file); - } - } - } - - function addAllMessages(): void { - for (const file of currServ.messages) { - if (file.endsWith(".msg")) { - allPos.push(file); - } - } - } - - function addAllPrograms(): void { - for (const program of homeComputer.programs) { - allPos.push(program); - } - } - - function addAllScripts(): void { - for (const scriptFilename of currServ.scripts.keys()) { - const res = processFilepath(scriptFilename); - if (res) { - allPos.push(res); - } - } - } - - function addAllTextFiles(): void { - for (const txt of currServ.textFiles) { - const res = processFilepath(txt.fn); - if (res) { - allPos.push(res); - } - } - } - - function addAllDirectories(): void { - // Directories are based on the currently evaluated path - const subdirs = getSubdirectories(currServ, evaledParentDirPath == null ? "/" : evaledParentDirPath); - - for (let i = 0; i < subdirs.length; ++i) { - const assembledDirPath = evaledParentDirPath == null ? subdirs[i] : evaledParentDirPath + subdirs[i]; - const res = processFilepath(assembledDirPath); - if (res != null) { - subdirs[i] = res; - } - } - - allPos = allPos.concat(subdirs); - } - - // Convert from the real absolute path back to the original path used in the input - function convertParentPath(filepath: string): string { - if (parentDirPath == null || evaledParentDirPath == null) { - console.warn(`convertParentPath() called when paths are null`); - return filepath; - } - - if (!filepath.startsWith(evaledParentDirPath)) { - console.warn( - `convertParentPath() called for invalid path. (filepath=${filepath}) (evaledParentDirPath=${evaledParentDirPath})`, - ); - return filepath; - } - - return parentDirPath + filepath.slice(evaledParentDirPath.length); - } - - // Given an a full, absolute filepath, converts it to the proper value - // for autocompletion purposes - function processFilepath(filepath: string): string | null { - if (evaledParentDirPath) { - if (filepath.startsWith(evaledParentDirPath)) { - return convertParentPath(filepath); - } - } else if (parentDirPath !== "") { - // If the parent directory is the root directory, but we're not searching - // it from the root directory, we have to add the original path - let t_parentDirPath = parentDirPath; - if (!t_parentDirPath.endsWith("/")) { - t_parentDirPath += "/"; - } - return parentDirPath + filepath; - } else { - return filepath; - } - - return null; - } - - function isCommand(cmd: string): boolean { - let t_cmd = cmd; - if (!t_cmd.endsWith(" ")) { - t_cmd += " "; - } - - return input.startsWith(t_cmd); - } - - // Autocomplete the command - if (index === -1 && !input.startsWith("./")) { - return commands.concat(Object.keys(Aliases)).concat(Object.keys(GlobalAliases)); - } - - // Since we're autocompleting an argument and not a command, the argument might - // be a file/directory path. We have to account for that when autocompleting - const commandArray = input.split(" "); - if (commandArray.length === 0) { - console.warn(`Tab autocompletion logic reached invalid branch`); - return allPos; - } - const arg = commandArray[commandArray.length - 1]; - parentDirPath = getAllParentDirectories(arg); - evaledParentDirPath = evaluateDirectoryPath(parentDirPath, currPath); - if (evaledParentDirPath === "/") { - evaledParentDirPath = null; - } else if (evaledParentDirPath == null) { - // do nothing for some reason tests don't like this? - // return allPos; // Invalid path - } else { - evaledParentDirPath += "/"; - } - - if (isCommand("buy")) { - const options = []; - for (const i of Object.keys(DarkWebItems)) { - const item = DarkWebItems[i]; - options.push(item.program); - } - - return options.concat(Object.keys(GlobalAliases)); - } - - if (isCommand("scp") && index === 1) { - for (const server of GetAllServers()) { - allPos.push(server.hostname); - } - - return allPos; - } - - if (isCommand("scp") && index === 0) { - addAllScripts(); - addAllLitFiles(); - addAllTextFiles(); - addAllDirectories(); - - return allPos; - } - - if (isCommand("cp") && index === 0) { - addAllScripts(); - addAllTextFiles(); - addAllDirectories(); - return allPos; - } - - if (isCommand("connect")) { - // All directly connected and backdoored servers are reachable - return GetAllServers() - .filter( - (server) => - currServ.serversOnNetwork.includes(server.hostname) || (server instanceof Server && server.backdoorInstalled), - ) - .map((server) => server.hostname); - } - - if (isCommand("nano") || isCommand("vim")) { - addAllScripts(); - addAllTextFiles(); - addAllDirectories(); - - return allPos; - } - - if (isCommand("rm")) { - addAllScripts(); - addAllPrograms(); - addAllLitFiles(); - addAllTextFiles(); - addAllCodingContracts(); - addAllDirectories(); - - return allPos; - } - - async function scriptAutocomplete(): Promise { - if (!isCommand("run") && !isCommand("tail") && !isCommand("kill") && !input.startsWith("./")) return; - let copy = input; - if (input.startsWith("./")) copy = "run " + input.slice(2); - const commands = ParseCommands(copy); - if (commands.length === 0) return; - const command = ParseCommand(commands[commands.length - 1]); - const filename = command[1] + ""; - if (!isScriptFilename(filename)) return; // Not a script. - if (filename.endsWith(".script")) return; // Doesn't work with ns1. - // 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.get(filename); - if (!script) return; // Doesn't exist. - let loadedModule; - try { - //Will return the already compiled module if recompilation not needed. - loadedModule = await compile(script, currServ.scripts); - } catch (e) { - //fail silently if the script fails to compile (e.g. syntax error) - return; - } - if (!loadedModule || !loadedModule.autocomplete) return; // Doesn't have an autocomplete function. - - const runArgs = { "--tail": Boolean, "-t": Number }; - const flags = libarg(runArgs, { - permissive: true, - argv: command.slice(2), - }); - const flagFunc = Flags(flags._); - const autocompleteData: AutocompleteData = { - servers: GetAllServers().map((server) => server.hostname), - 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"); - pos2 = schema.map((f: unknown) => { - if (!Array.isArray(f)) throw new Error("flags require an array of array"); - if (f[0].length === 1) return "-" + f[0]; - return "--" + f[0]; - }); - try { - return flagFunc(schema); - } catch (err) { - return {}; - } - }, - }; - let pos: string[] = []; - let pos2: string[] = []; - const options = loadedModule.autocomplete(autocompleteData, flags._); - if (!Array.isArray(options)) throw new Error("autocomplete did not return list of strings"); - pos = pos.concat(options.map((x) => String(x))); - return pos.concat(pos2); - } - const pos = await scriptAutocomplete(); - if (pos) return pos; - - // If input starts with './', essentially treat it as a slimmer - // invocation of `run`. - if (input.startsWith("./")) { - // All programs and scripts - for (const scriptFilename of currServ.scripts.keys()) { - const res = processFilepath(scriptFilename); - if (res) { - allPos.push(res); - } - } - - for (const program of currServ.programs) { - const res = processFilepath(program); - if (res) { - allPos.push(res); - } - } - - // All coding contracts - for (const cct of currServ.contracts) { - const res = processFilepath(cct.fn); - if (res) { - allPos.push(res); - } - } - - return allPos; - } - - if (isCommand("run")) { - addAllScripts(); - addAllPrograms(); - addAllCodingContracts(); - addAllDirectories(); - } - - if (isCommand("kill") || isCommand("tail") || isCommand("mem") || isCommand("check")) { - addAllScripts(); - addAllDirectories(); - - return allPos; - } - - if (isCommand("cat")) { - addAllMessages(); - addAllLitFiles(); - addAllTextFiles(); - addAllDirectories(); - addAllScripts(); - - return allPos; - } - - if (isCommand("download") || isCommand("mv")) { - addAllScripts(); - addAllTextFiles(); - addAllDirectories(); - - return allPos; - } - - if (isCommand("cd")) { - addAllDirectories(); - - return allPos; - } - - if (isCommand("ls") && index === 0) { - addAllDirectories(); - } - - if (isCommand("help")) { - // Get names from here instead of commands array because some - // undocumented/nonexistent commands are in the array - return Object.keys(HelpTexts); - } - - return allPos; -} diff --git a/src/Terminal/getTabCompletionPossibilities.ts b/src/Terminal/getTabCompletionPossibilities.ts new file mode 100644 index 000000000..5f22c2350 --- /dev/null +++ b/src/Terminal/getTabCompletionPossibilities.ts @@ -0,0 +1,333 @@ +import { Aliases, GlobalAliases } from "../Alias"; +import { DarkWebItems } from "../DarkWeb/DarkWebItems"; +import { Player } from "@player"; +import { GetAllServers } from "../Server/AllServers"; +import { parseCommand, parseCommands } from "./Parser"; +import { HelpTexts } from "./HelpText"; +import { compile } from "../NetscriptJSEvaluator"; +import { Flags } from "../NetscriptFunctions/Flags"; +import { AutocompleteData } from "@nsdefs"; +import * as libarg from "arg"; +import { getAllDirectories, resolveDirectory, root } from "../Paths/Directory"; +import { resolveScriptFilePath } from "../Paths/ScriptFilePath"; + +// TODO: this shouldn't be hardcoded in two places with no typechecks to verify equivalence +// An array of all Terminal commands +const gameCommands = [ + "alias", + "analyze", + "backdoor", + "cat", + "cd", + "changelog", + "check", + "clear", + "cls", + "connect", + "cp", + "download", + "expr", + "free", + "grow", + "hack", + "help", + "home", + "hostname", + "ifconfig", + "kill", + "killall", + "ls", + "lscpu", + "mem", + "mv", + "nano", + "ps", + "rm", + "run", + "scan-analyze", + "scan", + "scp", + "sudov", + "tail", + "theme", + "top", + "vim", + "weaken", +]; + +/** Suggest all completion possibilities for the last argument in the last command being typed + * @param terminalText The current full text entered in the terminal + * @param baseDir The current working directory. + * @returns Array of possible string replacements for the current text being autocompleted. + */ +export async function getTabCompletionPossibilities(terminalText: string, baseDir = root): Promise { + // Get the current command text + const currentText = /[^ ]*$/.exec(terminalText)?.[0] ?? ""; + // Remove the current text from the commands string + const valueWithoutCurrent = terminalText.substring(0, terminalText.length - currentText.length); + // Parse the commands string, this handles alias replacement as well. + const commands = parseCommands(valueWithoutCurrent); + if (!commands.length) commands.push(""); + // parse the last command into a commandArgs array, but convert to string + const commandArray = parseCommand(commands[commands.length - 1]).map(String); + commandArray.push(currentText); + + /** How many separate strings make up the command, e.g. "run a" would result in 2 strings. */ + const commandLength = commandArray.length; + + // To prevent needing to convert currentArg to lowercase for every comparison + const requiredMatch = currentText.toLowerCase(); + + // If a relative directory is included in the path, this will store what the absolute path needs to start with to be valid + let pathingRequiredMatch = currentText.toLowerCase(); + + /** The directory portion of the current input */ + let relativeDir = ""; + const slashIndex = currentText.lastIndexOf("/"); + + if (slashIndex !== -1) { + relativeDir = currentText.substring(0, slashIndex + 1); + const path = resolveDirectory(relativeDir, baseDir); + // No valid terminal inputs contain a / that does not indicate a path + if (path === null) return []; + baseDir = path; + pathingRequiredMatch = currentText.replace(/^.*\//, path).toLowerCase(); + } else if (baseDir !== root) { + pathingRequiredMatch = (baseDir + currentText).toLowerCase(); + } + + const possibilities: string[] = []; + const currServ = Player.getCurrentServer(); + const homeComputer = Player.getHomeComputer(); + + // --- Functions for adding different types of data --- + + interface AddAllGenericOptions { + // The iterable to iterate through the data + iterable: Iterable; + // Whether the item can be pathed to. Typically this is true for files (programs are an exception) + usePathing?: boolean; + // Whether to exclude the current text as one of the autocomplete options + ignoreCurrent?: boolean; + } + function addGeneric({ iterable, usePathing, ignoreCurrent }: AddAllGenericOptions) { + const requiredStart = usePathing ? pathingRequiredMatch : requiredMatch; + for (const member of iterable) { + if (ignoreCurrent && member.length <= requiredStart.length) continue; + if (member.toLowerCase().startsWith(requiredStart)) { + possibilities.push(usePathing ? relativeDir + member.substring(baseDir.length) : member); + } + } + } + + const addAliases = () => addGeneric({ iterable: Object.keys(Aliases) }); + const addGlobalAliases = () => addGeneric({ iterable: Object.keys(GlobalAliases) }); + const addCommands = () => addGeneric({ iterable: gameCommands }); + const addDarkwebItems = () => addGeneric({ iterable: Object.values(DarkWebItems).map((item) => item.program) }); + const addServerNames = () => addGeneric({ iterable: GetAllServers().map((server) => server.hostname) }); + const addScripts = () => addGeneric({ iterable: currServ.scripts.keys(), usePathing: true }); + const addTextFiles = () => addGeneric({ iterable: currServ.textFiles.keys(), usePathing: true }); + const addCodingContracts = () => { + addGeneric({ iterable: currServ.contracts.map((contract) => contract.fn), usePathing: true }); + }; + + const addLiterature = () => { + addGeneric({ iterable: currServ.messages.filter((message) => message.endsWith(".lit")), usePathing: true }); + }; + + const addMessages = () => { + addGeneric({ iterable: currServ.messages.filter((message) => message.endsWith(".msg")), usePathing: true }); + }; + + const addReachableServerNames = () => { + addGeneric({ + iterable: GetAllServers() + .filter((server) => server.backdoorInstalled || currServ.serversOnNetwork.includes(server.hostname)) + .map((server) => server.hostname), + }); + }; + + const addPrograms = () => { + // Only allow completed programs to autocomplete + const programs = homeComputer.programs.filter((name) => name.endsWith(".exe")); + // At all times, programs can be accessed without pathing + addGeneric({ iterable: programs }); + // If we're on home and a path is being used, also include pathing results + if (homeComputer.isConnectedTo && relativeDir) addGeneric({ iterable: programs, usePathing: true }); + }; + + const addDirectories = () => { + addGeneric({ iterable: getAllDirectories(currServ), usePathing: true, ignoreCurrent: true }); + }; + + // Just some booleans so the mismatch between command length and arg number are not as confusing. + const onCommand = commandLength === 1; + const onFirstCommandArg = commandLength === 2; + const onSecondCommandArg = commandLength === 3; + + // These are always added. + addGlobalAliases(); + + // If we're using a relative path, always add directories + if (relativeDir) addDirectories(); + + // -- Handling different commands -- // + // Command is what is being autocompleted + if (onCommand) { + addAliases(); + addCommands(); + // Allow any relative pathing as a command arg to act as previous ./ command + if (relativeDir) { + addScripts(); + addPrograms(); + addCodingContracts(); + } + } + + switch (commandArray[0]) { + case "buy": + addDarkwebItems(); + return possibilities; + + case "cat": + addScripts(); + addTextFiles(); + addMessages(); + addLiterature(); + return possibilities; + + case "cd": + case "ls": + if (onFirstCommandArg && !relativeDir) addDirectories(); + return possibilities; + + case "check": + case "kill": + case "mem": + case "tail": + addScripts(); + return possibilities; + + case "connect": + if (onFirstCommandArg) addReachableServerNames(); + return possibilities; + + case "cp": + if (onFirstCommandArg) { + // We're autocompleting a source content file + addScripts(); + addTextFiles(); + } + return possibilities; + + case "download": + case "mv": + // download only takes one arg, and for mv we only want to autocomplete the first one + if (onFirstCommandArg) { + addScripts(); + addTextFiles(); + } + return possibilities; + + case "help": + if (onFirstCommandArg) { + addGeneric({ iterable: Object.keys(HelpTexts), usePathing: false }); + } + return possibilities; + + case "nano": + case "vim": + addScripts(); + addTextFiles(); + return possibilities; + + case "scp": + if (onFirstCommandArg) { + addScripts(); + addTextFiles(); + addLiterature(); + } else if (onSecondCommandArg) addServerNames(); + return possibilities; + + case "rm": + addScripts(); + addPrograms(); + addLiterature(); + addTextFiles(); + addCodingContracts(); + return possibilities; + + case "run": + if (onFirstCommandArg) { + addPrograms(); + addCodingContracts(); + } + // Spill over into next cases + case "check": + case "tail": + case "kill": + if (onFirstCommandArg) addScripts(); + else { + const options = await scriptAutocomplete(); + if (options) addGeneric({ iterable: options, usePathing: false }); + } + return possibilities; + + default: + return possibilities; + } + + async function scriptAutocomplete(): Promise { + let inputCopy = commandArray.join(" "); + if (commandLength === 1) inputCopy = "run " + inputCopy; + const commands = parseCommands(inputCopy); + if (commands.length === 0) return; + const command = parseCommand(commands[commands.length - 1]); + const filename = resolveScriptFilePath(String(command[1]), baseDir); + if (!filename) return; // Not a script path. + if (filename.endsWith(".script")) return; // Doesn't work with ns1. + const script = currServ.scripts.get(filename); + if (!script) return; // Doesn't exist. + + let loadedModule; + try { + //Will return the already compiled module if recompilation not needed. + loadedModule = await compile(script, currServ.scripts); + } catch (e) { + //fail silently if the script fails to compile (e.g. syntax error) + return; + } + if (!loadedModule || !loadedModule.autocomplete) return; // Doesn't have an autocomplete function. + + const runArgs = { "--tail": Boolean, "-t": Number }; + const flags = libarg(runArgs, { + permissive: true, + argv: command.slice(2), + }); + const flagFunc = Flags(flags._); + const autocompleteData: AutocompleteData = { + servers: GetAllServers().map((server) => server.hostname), + scripts: [...currServ.scripts.keys()], + txts: [...currServ.textFiles.keys()], + flags: (schema: unknown) => { + if (!Array.isArray(schema)) throw new Error("flags require an array of array"); + pos2 = schema.map((f: unknown) => { + if (!Array.isArray(f)) throw new Error("flags require an array of array"); + if (f[0].length === 1) return "-" + f[0]; + return "--" + f[0]; + }); + try { + return flagFunc(schema); + } catch (err) { + return {}; + } + }, + }; + let pos: string[] = []; + let pos2: string[] = []; + const options = loadedModule.autocomplete(autocompleteData, flags._); + if (!Array.isArray(options)) throw new Error("autocomplete did not return list of strings"); + pos = pos.concat(options.map((x) => String(x))); + return pos.concat(pos2); + } +} diff --git a/src/Terminal/tabCompletion.ts b/src/Terminal/tabCompletion.ts deleted file mode 100644 index 7e2f8de2b..000000000 --- a/src/Terminal/tabCompletion.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { containsAllStrings, longestCommonStart } from "../utils/StringHelperFunctions"; - -/** - * Implements tab completion for the Terminal - * - * @param command {string} Terminal command, excluding the last incomplete argument - * @param arg {string} Last argument that is being completed - * @param allPossibilities {string[]} All values that `arg` can complete to - */ -export function tabCompletion( - command: string, - arg: string, - allPossibilities: string[], - oldValue: string, -): string[] | string | undefined { - if (!(allPossibilities.constructor === Array)) { - return; - } - if (!containsAllStrings(allPossibilities)) { - return; - } - - // Remove all options in allPossibilities that do not match the current string - // that we are attempting to autocomplete - if (arg === "") { - for (let i = allPossibilities.length - 1; i >= 0; --i) { - if (!allPossibilities[i].toLowerCase().startsWith(command.toLowerCase())) { - allPossibilities.splice(i, 1); - } - } - } else { - for (let i = allPossibilities.length - 1; i >= 0; --i) { - if (!allPossibilities[i].toLowerCase().startsWith(arg.toLowerCase())) { - allPossibilities.splice(i, 1); - } - } - } - - const semiColonIndex = oldValue.lastIndexOf(";"); - - let val = ""; - if (allPossibilities.length === 0) { - return; - } else if (allPossibilities.length === 1) { - if (arg === "") { - //Autocomplete command - val = allPossibilities[0]; - } else { - val = command + " " + allPossibilities[0]; - } - - if (semiColonIndex === -1) { - // No semicolon, so replace the whole command - return val; - } else { - // Replace only after the last semicolon - return oldValue.slice(0, semiColonIndex + 1) + " " + val; - } - } else { - const longestStartSubstr = longestCommonStart(allPossibilities); - /** - * If the longest common starting substring of remaining possibilities is the same - * as whatever's already in terminal, just list all possible options. Otherwise, - * change the input in the terminal to the longest common starting substr - */ - if (arg === "") { - if (longestStartSubstr === command) { - return allPossibilities; - } else if (semiColonIndex === -1) { - // No semicolon, so replace the whole command - return longestStartSubstr; - } else { - // Replace only after the last semicolon - return `${oldValue.slice(0, semiColonIndex + 1)} ${longestStartSubstr}`; - } - } else if (longestStartSubstr === arg) { - // List all possible options - return allPossibilities; - } else if (semiColonIndex == -1) { - // No semicolon, so replace the whole command - return `${command} ${longestStartSubstr}`; - } else { - // Replace only after the last semicolon - return `${oldValue.slice(0, semiColonIndex + 1)} ${command} ${longestStartSubstr}`; - } - } -} diff --git a/src/Terminal/ui/TerminalInput.tsx b/src/Terminal/ui/TerminalInput.tsx index fc1abe889..ba3521e91 100644 --- a/src/Terminal/ui/TerminalInput.tsx +++ b/src/Terminal/ui/TerminalInput.tsx @@ -10,9 +10,9 @@ import TextField from "@mui/material/TextField"; import { KEY, KEYCODE } from "../../utils/helpers/keyCodes"; import { Terminal } from "../../Terminal"; import { Player } from "@player"; -import { determineAllPossibilitiesForTabCompletion } from "../determineAllPossibilitiesForTabCompletion"; -import { tabCompletion } from "../tabCompletion"; +import { getTabCompletionPossibilities } from "../getTabCompletionPossibilities"; import { Settings } from "../../Settings/Settings"; +import { longestCommonStart } from "../../utils/StringHelperFunctions"; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -26,7 +26,6 @@ const useStyles = makeStyles((theme: Theme) => padding: theme.spacing(0), }, preformatted: { - whiteSpace: "pre-wrap", margin: theme.spacing(0), }, list: { @@ -205,54 +204,18 @@ export function TerminalInput(): React.ReactElement { } // Autocomplete - if (event.key === KEY.TAB && value !== "") { + if (event.key === KEY.TAB) { event.preventDefault(); - - let copy = value; - const semiColonIndex = copy.lastIndexOf(";"); - if (semiColonIndex !== -1) { - copy = copy.slice(semiColonIndex + 1); - } - - copy = copy.trim(); - copy = copy.replace(/\s\s+/g, " "); - - const commandArray = copy.split(" "); - let index = commandArray.length - 2; - if (index < -1) { - index = 0; - } - const allPos = await determineAllPossibilitiesForTabCompletion(copy, index, Terminal.cwd()); - if (allPos.length == 0) { + const possibilities = await getTabCompletionPossibilities(value, Terminal.cwd()); + if (possibilities.length === 0) return; + if (possibilities.length === 1) { + saveValue(value.replace(/[^ ]*$/, possibilities[0]) + " "); return; } - - let arg = ""; - let command = ""; - if (commandArray.length == 0) { - return; - } - if (commandArray.length == 1) { - command = commandArray[0]; - } else if (commandArray.length == 2) { - command = commandArray[0]; - arg = commandArray[1]; - } else if (commandArray.length == 3) { - command = commandArray[0] + " " + commandArray[1]; - arg = commandArray[2]; - } else { - arg = commandArray.pop() + ""; - command = commandArray.join(" "); - } - - let newValue = tabCompletion(command, arg, allPos, value); - if (typeof newValue === "string" && newValue !== "") { - if (!newValue.endsWith(" ") && !newValue.endsWith("/") && allPos.length === 1) newValue += " "; - saveValue(newValue); - } - if (Array.isArray(newValue)) { - setPossibilities(newValue); - } + // More than one possibility, check to see if there is a longer common string than currentText. + const longestMatch = longestCommonStart(possibilities); + saveValue(value.replace(/[^ ]*$/, longestMatch)); + setPossibilities(possibilities); } // Clear screen. @@ -310,6 +273,7 @@ export function TerminalInput(): React.ReactElement { } else { ++Terminal.commandHistoryIndex; const prevCommand = Terminal.commandHistory[Terminal.commandHistoryIndex]; + saveValue(prevCommand); } } @@ -405,7 +369,12 @@ export function TerminalInput(): React.ReactElement { onKeyDown: onKeyDown, }} > - 0} anchorEl={terminalInput.current} placement={"top-start"}> + 0} + anchorEl={terminalInput.current} + placement={"top"} + sx={{ maxWidth: "75%" }} + > Possible autocomplete candidates: diff --git a/src/TextFile.ts b/src/TextFile.ts index 19ab4f737..5964bcac7 100644 --- a/src/TextFile.ts +++ b/src/TextFile.ts @@ -1,29 +1,27 @@ import { dialogBoxCreate } from "./ui/React/DialogBox"; import { BaseServer } from "./Server/BaseServer"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "./utils/JSONReviver"; -import { removeLeadingSlash, isInRootDirectory } from "./Terminal/DirectoryHelpers"; +import { TextFilePath } from "./Paths/TextFilePath"; +import { ContentFile } from "./Paths/ContentFile"; /** Represents a plain text file that is typically stored on a server. */ -export class TextFile { +export class TextFile implements ContentFile { /** The full file name. */ - fn: string; + filename: TextFilePath; /** The content of the file. */ text: string; - //TODO 2.3: Why are we using getter/setter for fn as filename? Rename parameter as more-readable "filename" - /** The full file name. */ - get filename(): string { - return this.fn; + // Shared interface on Script and TextFile for accessing content + get content() { + return this.text; + } + set content(text: string) { + this.text = text; } - /** The full file name. */ - set filename(value: string) { - this.fn = value; - } - - constructor(fn = "", txt = "") { - this.fn = (fn.endsWith(".txt") ? fn : `${fn}.txt`).replace(/\s+/g, ""); + constructor(filename = "default.txt" as TextFilePath, txt = "") { + this.filename = filename; this.text = txt; } @@ -38,7 +36,7 @@ export class TextFile { const a: HTMLAnchorElement = document.createElement("a"); const url: string = URL.createObjectURL(file); a.href = url; - a.download = this.fn; + a.download = this.filename; document.body.appendChild(a); a.click(); setTimeout(() => { @@ -54,7 +52,7 @@ export class TextFile { /** Shows the content to the user via the game's dialog box. */ show(): void { - dialogBoxCreate(`${this.fn}\n\n${this.text}`); + dialogBoxCreate(`${this.filename}\n\n${this.text}`); } /** Serialize the current file to a JSON save state. */ @@ -67,6 +65,12 @@ export class TextFile { this.text = txt; } + deleteFromServer(server: BaseServer): boolean { + if (!server.textFiles.has(this.filename)) return false; + server.textFiles.delete(this.filename); + return true; + } + /** Initializes a TextFile from a JSON save state. */ static fromJSON(value: IReviverValue): TextFile { return Generic_fromJSON(TextFile, value.data); @@ -74,46 +78,3 @@ export class TextFile { } constructorsForReviver.TextFile = TextFile; - -/** - * Retrieve the file object for the filename on the specified server. - * @param fn The file name to look for - * @param server The server object to look in - * @returns The file object, or null if it couldn't find it. - */ -export function getTextFile(fn: string, server: BaseServer): TextFile | null { - let filename: string = !fn.endsWith(".txt") ? `${fn}.txt` : fn; - - if (isInRootDirectory(filename)) { - filename = removeLeadingSlash(filename); - } - - for (const file of server.textFiles) { - if (file.fn === filename) { - return file; - } - } - - return null; -} - -/** - * Creates a TextFile on the target server. - * @param fn The file name to create. - * @param txt The contents of the file. - * @param server The server that the file should be created on. - * @returns The instance of the file. - */ -export function createTextFile(fn: string, txt: string, server: BaseServer): TextFile | undefined { - if (getTextFile(fn, server) !== null) { - // This should probably be a `throw`... - /* tslint:disable-next-line:no-console */ - console.error(`A file named "${fn}" already exists on server ${server.hostname}.`); - - return undefined; - } - const file: TextFile = new TextFile(fn, txt); - server.textFiles.push(file); - - return file; -} diff --git a/src/Types/Jsonable.ts b/src/Types/Jsonable.ts index d083bcc17..5d5075e93 100644 --- a/src/Types/Jsonable.ts +++ b/src/Types/Jsonable.ts @@ -1,5 +1,9 @@ import type { IReviverValue } from "../utils/JSONReviver"; -// Jsonable versions of builtin JS class objects + +// Loosened type requirements on input for has, also has provides typecheck info. +export interface JSONSet { + has: (value: unknown) => value is T; +} export class JSONSet extends Set { toJSON(): IReviverValue { return { ctor: "JSONSet", data: Array.from(this) }; @@ -9,7 +13,11 @@ export class JSONSet extends Set { } } -export class JSONMap extends Map { +// Loosened type requirements on input for has. has also provides typecheck info. +export interface JSONMap { + has: (key: unknown) => key is K; +} +export class JSONMap extends Map { toJSON(): IReviverValue { return { ctor: "JSONMap", data: Array.from(this) }; } diff --git a/src/Types/strings.ts b/src/Types/strings.ts index 7484d9bc1..1206effcd 100644 --- a/src/Types/strings.ts +++ b/src/Types/strings.ts @@ -1,18 +1,3 @@ -// 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 { diff --git a/src/Work/CreateProgramWork.ts b/src/Work/CreateProgramWork.ts index 0b835c7d6..3d7261756 100644 --- a/src/Work/CreateProgramWork.ts +++ b/src/Work/CreateProgramWork.ts @@ -7,24 +7,26 @@ import { Programs } from "../Programs/Programs"; import { Work, WorkType } from "./Work"; import { Program } from "../Programs/Program"; import { calculateIntelligenceBonus } from "../PersonObjects/formulas/intelligence"; +import { asProgramFilePath } from "../Paths/ProgramFilePath"; +import { CompletedProgramName } from "../Programs/Programs"; export const isCreateProgramWork = (w: Work | null): w is CreateProgramWork => w !== null && w.type === WorkType.CREATE_PROGRAM; interface CreateProgramWorkParams { - programName: string; + programName: CompletedProgramName; singularity: boolean; } export class CreateProgramWork extends Work { - programName: string; + programName: CompletedProgramName; // amount of effective work completed on the program (time boosted by skills). unitCompleted: number; constructor(params?: CreateProgramWorkParams) { super(WorkType.CREATE_PROGRAM, params?.singularity ?? true); this.unitCompleted = 0; - this.programName = params?.programName ?? ""; + this.programName = params?.programName ?? CompletedProgramName.bruteSsh; if (params) { for (let i = 0; i < Player.getHomeComputer().programs.length; ++i) { @@ -50,9 +52,7 @@ export class CreateProgramWork extends Work { } getProgram(): Program { - const p = Object.values(Programs).find((p) => p.name.toLowerCase() === this.programName.toLowerCase()); - if (!p) throw new Error("Create program work started with invalid program " + this.programName); - return p; + return Programs[this.programName]; } process(cycles: number): boolean { @@ -75,7 +75,7 @@ export class CreateProgramWork extends Work { return false; } finish(cancelled: boolean): void { - const programName = this.programName; + const programName = asProgramFilePath(this.programName); if (!cancelled) { //Complete case Player.gainIntelligenceExp( @@ -95,7 +95,7 @@ export class CreateProgramWork extends Work { } else if (!Player.getHomeComputer().programs.includes(programName)) { //Incomplete case const perc = ((100 * this.unitCompleted) / this.unitNeeded()).toFixed(2); - const incompleteName = programName + "-" + perc + "%-INC"; + const incompleteName = asProgramFilePath(programName + "-" + perc + "%-INC"); Player.getHomeComputer().programs.push(incompleteName); } } diff --git a/src/engine.tsx b/src/engine.tsx index c61976c16..66ae6612d 100644 --- a/src/engine.tsx +++ b/src/engine.tsx @@ -2,7 +2,6 @@ import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions" import { initAugmentations } from "./Augmentation/AugmentationHelpers"; import { AugmentationNames } from "./Augmentation/data/AugmentationNames"; import { initSourceFiles } from "./SourceFile/SourceFiles"; -import { initDarkWebItems } from "./DarkWeb/DarkWebItems"; import { generateRandomContract } from "./CodingContractGenerator"; import { initCompanies } from "./Company/Companies"; import { CONSTANTS } from "./Constants"; @@ -235,7 +234,6 @@ const Engine: { if (loadGame(saveString)) { FormatsNeedToChange.emit(); initSourceFiles(); - initDarkWebItems(); initAugmentations(); // Also calls Player.reapplyAllAugmentations() Player.reapplyAllSourceFiles(); if (Player.hasWseAccount) { @@ -377,7 +375,6 @@ const Engine: { // No save found, start new game FormatsNeedToChange.emit(); initSourceFiles(); - initDarkWebItems(); Engine.start(); // Run main game loop and Scripts loop Player.init(); initForeignServers(Player.getHomeComputer()); diff --git a/src/ui/GameRoot.tsx b/src/ui/GameRoot.tsx index 9b53a9bd7..3568b074b 100644 --- a/src/ui/GameRoot.tsx +++ b/src/ui/GameRoot.tsx @@ -23,7 +23,7 @@ import createStyles from "@mui/styles/createStyles"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; -import { Page, SimplePage, IRouter, ScriptEditorRouteOptions } from "./Router"; +import { Page, SimplePage, IRouter } from "./Router"; import { Overview } from "./React/Overview"; import { SidebarRoot } from "../Sidebar/ui/SidebarRoot"; import { AugmentationsRoot } from "../Augmentation/ui/AugmentationsRoot"; @@ -78,6 +78,8 @@ import { isFactionWork } from "../Work/FactionWork"; import { V2Modal } from "../utils/V2Modal"; import { MathJaxContext } from "better-react-mathjax"; import { useRerender } from "./React/hooks"; +import { ScriptFilePath } from "src/Paths/ScriptFilePath"; +import { TextFilePath } from "src/Paths/TextFilePath"; const htmlLocation = location; @@ -126,7 +128,13 @@ function determineStartPage(): Page { export function GameRoot(): React.ReactElement { const classes = useStyles(); - const [{ files, vim }, setEditorOptions] = useState({ files: {}, vim: false }); + const [{ files, vim }, setEditorOptions] = useState<{ + files: Map; + vim: boolean; + }>({ + files: new Map(), + vim: false, + }); const [page, setPage] = useState(determineStartPage()); const rerender = useRerender(); const [augPage, setAugPage] = useState(false); @@ -189,7 +197,7 @@ export function GameRoot(): React.ReactElement { setPage(Page.Faction); if (faction) setFaction(faction); }, - toScriptEditor: (files: Record, options?: ScriptEditorRouteOptions) => { + toScriptEditor: (files = new Map(), options) => { setEditorOptions({ files, vim: !!options?.vim, diff --git a/src/ui/Router.ts b/src/ui/Router.ts index b5fd14c5d..fb99e0b03 100644 --- a/src/ui/Router.ts +++ b/src/ui/Router.ts @@ -1,3 +1,5 @@ +import { ScriptFilePath } from "../Paths/ScriptFilePath"; +import { TextFilePath } from "../Paths/TextFilePath"; import { Faction } from "../Faction/Faction"; import { Location } from "../Locations/Location"; @@ -67,7 +69,7 @@ export interface IRouter { toFaction(faction: Faction, augPage?: boolean): void; // faction name toInfiltration(location: Location): void; toJob(location: Location): void; - toScriptEditor(files?: Record, options?: ScriptEditorRouteOptions): void; + toScriptEditor(files?: Map, options?: ScriptEditorRouteOptions): void; toLocation(location: Location): void; toImportSave(base64Save: string, automatic?: boolean): void; } diff --git a/src/utils/v1APIBreak.ts b/src/utils/v1APIBreak.ts index bd1446621..4f041021d 100644 --- a/src/utils/v1APIBreak.ts +++ b/src/utils/v1APIBreak.ts @@ -1,8 +1,10 @@ import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; import { PlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation"; import { Player } from "@player"; -import { Script } from "../Script/Script"; +import { FormattedCode, Script } from "../Script/Script"; import { GetAllServers } from "../Server/AllServers"; +import { resolveTextFilePath } from "../Paths/TextFilePath"; +import { resolveScriptFilePath } from "../Paths/ScriptFilePath"; const detect: [string, string][] = [ ["getHackTime", "returns milliseconds"], @@ -52,7 +54,7 @@ function hasChanges(code: string): boolean { return false; } -function convert(code: string): string { +function convert(code: string): FormattedCode { const lines = code.split("\n"); const out: string[] = []; for (let i = 0; i < lines.length; i++) { @@ -70,7 +72,8 @@ function convert(code: string): string { } out.push(line); } - return out.join("\n"); + code = out.join("\n"); + return Script.formatCode(code); } export function AwardNFG(n = 1): void { @@ -122,7 +125,9 @@ export function v1APIBreak(): void { } if (txt !== "") { const home = Player.getHomeComputer(); - home.writeToTextFile("v1_DETECTED_CHANGES.txt", txt); + const textPath = resolveTextFilePath("v1_DETECTED_CHANGES.txt"); + if (!textPath) return console.error("Filepath unexpectedly failed to parse"); + home.writeToTextFile(textPath, txt); } // API break function is called before version31 / 2.3.0 changes - scripts is still an array @@ -130,8 +135,14 @@ export function v1APIBreak(): void { const backups: Script[] = []; for (const script of server.scripts) { if (!hasChanges(script.code)) continue; - const prefix = script.filename.includes("/") ? "/BACKUP_" : "BACKUP_"; - backups.push(new Script(prefix + script.filename, script.code, script.server)); + // Sanitize first before combining + const oldFilename = resolveScriptFilePath(script.filename); + const filename = resolveScriptFilePath("BACKUP_" + oldFilename); + if (!filename) { + console.error(`Unexpected error resolving backup path for ${script.filename}`); + continue; + } + backups.push(new Script(filename, script.code, script.server)); script.code = convert(script.code); } server.scripts = server.scripts.concat(backups); diff --git a/src/utils/v2APIBreak.ts b/src/utils/v2APIBreak.ts index f26b5977f..73dd75468 100644 --- a/src/utils/v2APIBreak.ts +++ b/src/utils/v2APIBreak.ts @@ -1,3 +1,4 @@ +import { TextFilePath } from "../Paths/TextFilePath"; import { saveObject } from "../SaveObject"; import { Script } from "../Script/Script"; import { GetAllServers, GetServer } from "../Server/AllServers"; @@ -232,7 +233,7 @@ export const v2APIBreak = () => { processScript(rules, script); } - home.writeToTextFile("V2_0_0_API_BREAK.txt", formatRules(rules)); + home.writeToTextFile("V2_0_0_API_BREAK.txt" as TextFilePath, formatRules(rules)); openV2Modal(); for (const server of GetAllServers()) { diff --git a/test/jest/Netscript/RunScript.test.ts b/test/jest/Netscript/RunScript.test.ts index e65a83fc0..c2436a259 100644 --- a/test/jest/Netscript/RunScript.test.ts +++ b/test/jest/Netscript/RunScript.test.ts @@ -6,6 +6,7 @@ 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"; +import { ScriptFilePath } from "src/Paths/ScriptFilePath"; // Replace Blob/ObjectURL functions, because they don't work natively in Jest global.Blob = class extends Blob { @@ -77,7 +78,7 @@ test.each([ return "data:text/javascript," + encodeURIComponent(blob.code); }; - let server; + let server = {} as Server; let eventDelete = () => {}; let alertDelete = () => {}; try { @@ -87,10 +88,10 @@ test.each([ server = new Server({ hostname: "home", adminRights: true, maxRam: 8 }); AddToAllServers(server); for (const s of scripts) { - expect(server.writeToScriptFile(s.name, s.code)).toEqual({ success: true, overwritten: false }); + expect(server.writeToScriptFile(s.name as ScriptFilePath, s.code)).toEqual({ overwritten: false }); } - const script = server.scripts.get(scripts[scripts.length - 1].name) as Script; + const script = server.scripts.get(scripts[scripts.length - 1].name as ScriptFilePath) as Script; expect(script.filename).toEqual(scripts[scripts.length - 1].name); const ramUsage = script.getRamUsage(server.scripts); diff --git a/test/jest/Terminal/Directory.test.ts b/test/jest/Terminal/Directory.test.ts deleted file mode 100644 index c421ac66a..000000000 --- a/test/jest/Terminal/Directory.test.ts +++ /dev/null @@ -1,306 +0,0 @@ -import * as dirHelpers from "../../../src/Terminal/DirectoryHelpers"; - -describe("Terminal Directory Tests", function () { - describe("removeLeadingSlash()", function () { - const removeLeadingSlash = dirHelpers.removeLeadingSlash; - - it("should remove first slash in a string", function () { - expect(removeLeadingSlash("/")).toEqual(""); - expect(removeLeadingSlash("/foo.txt")).toEqual("foo.txt"); - expect(removeLeadingSlash("/foo/file.txt")).toEqual("foo/file.txt"); - }); - - it("should only remove one slash", function () { - expect(removeLeadingSlash("///")).toEqual("//"); - expect(removeLeadingSlash("//foo")).toEqual("/foo"); - }); - - it("should do nothing for a string that doesn't start with a slash", function () { - expect(removeLeadingSlash("foo.txt")).toEqual("foo.txt"); - expect(removeLeadingSlash("foo/test.txt")).toEqual("foo/test.txt"); - }); - - it("should not fail on an empty string", function () { - expect(removeLeadingSlash.bind(null, "")).not.toThrow(); - expect(removeLeadingSlash("")).toEqual(""); - }); - }); - - describe("removeTrailingSlash()", function () { - const removeTrailingSlash = dirHelpers.removeTrailingSlash; - - it("should remove last slash in a string", function () { - expect(removeTrailingSlash("/")).toEqual(""); - expect(removeTrailingSlash("foo.txt/")).toEqual("foo.txt"); - expect(removeTrailingSlash("foo/file.txt/")).toEqual("foo/file.txt"); - }); - - it("should only remove one slash", function () { - expect(removeTrailingSlash("///")).toEqual("//"); - expect(removeTrailingSlash("foo//")).toEqual("foo/"); - }); - - it("should do nothing for a string that doesn't end with a slash", function () { - expect(removeTrailingSlash("foo.txt")).toEqual("foo.txt"); - expect(removeTrailingSlash("foo/test.txt")).toEqual("foo/test.txt"); - }); - - it("should not fail on an empty string", function () { - expect(removeTrailingSlash.bind(null, "")).not.toThrow(); - expect(removeTrailingSlash("")).toEqual(""); - }); - }); - - describe("isValidFilename()", function () { - const isValidFilename = dirHelpers.isValidFilename; - - it("should return true for valid filenames", function () { - expect(isValidFilename("test.txt")).toEqual(true); - expect(isValidFilename("123.script")).toEqual(true); - expect(isValidFilename("foo123.b")).toEqual(true); - expect(isValidFilename("my_script.script")).toEqual(true); - expect(isValidFilename("my-script.script")).toEqual(true); - expect(isValidFilename("_foo.lit")).toEqual(true); - expect(isValidFilename("mult.periods.script")).toEqual(true); - expect(isValidFilename("mult.per-iods.again.script")).toEqual(true); - expect(isValidFilename("BruteSSH.exe-50%-INC")).toEqual(true); - expect(isValidFilename("DeepscanV1.exe-1.01%-INC")).toEqual(true); - expect(isValidFilename("DeepscanV2.exe-1.00%-INC")).toEqual(true); - expect(isValidFilename("AutoLink.exe-1.%-INC")).toEqual(true); - }); - - it("should return false for invalid filenames", function () { - expect(isValidFilename("foo")).toEqual(false); - expect(isValidFilename("my script.script")).toEqual(false); - expect(isValidFilename("a^.txt")).toEqual(false); - expect(isValidFilename("b#.lit")).toEqual(false); - expect(isValidFilename("lib().js")).toEqual(false); - expect(isValidFilename("foo.script_")).toEqual(false); - expect(isValidFilename("foo._script")).toEqual(false); - expect(isValidFilename("foo.hyphened-ext")).toEqual(false); - expect(isValidFilename("")).toEqual(false); - expect(isValidFilename("AutoLink-1.%-INC.exe")).toEqual(false); - expect(isValidFilename("AutoLink.exe-1.%-INC.exe")).toEqual(false); - expect(isValidFilename("foo%.exe")).toEqual(false); - expect(isValidFilename("-1.00%-INC")).toEqual(false); - }); - }); - - describe("isValidDirectoryName()", function () { - const isValidDirectoryName = dirHelpers.isValidDirectoryName; - - it("should return true for valid directory names", function () { - expect(isValidDirectoryName("a")).toEqual(true); - expect(isValidDirectoryName("foo")).toEqual(true); - expect(isValidDirectoryName("foo-dir")).toEqual(true); - expect(isValidDirectoryName("foo_dir")).toEqual(true); - expect(isValidDirectoryName(".a")).toEqual(true); - expect(isValidDirectoryName("1")).toEqual(true); - expect(isValidDirectoryName("a1")).toEqual(true); - expect(isValidDirectoryName(".a1")).toEqual(true); - expect(isValidDirectoryName("._foo")).toEqual(true); - expect(isValidDirectoryName("_foo")).toEqual(true); - // the changes made to support this broke mv - // see https://github.com/danielyxie/bitburner/pull/3653 if you try to re support - // expect(isValidDirectoryName("foo.dir")).toEqual(true); - // expect(isValidDirectoryName("foov1.0.0.1")).toEqual(true); - // expect(isValidDirectoryName("foov1..0..0..1")).toEqual(true); - expect(isValidDirectoryName("foov1-0-0-1")).toEqual(true); - expect(isValidDirectoryName("foov1-0-0-1-")).toEqual(true); - expect(isValidDirectoryName("foov1--0--0--1--")).toEqual(true); - expect(isValidDirectoryName("foov1_0_0_1")).toEqual(true); - expect(isValidDirectoryName("foov1_0_0_1_")).toEqual(true); - expect(isValidDirectoryName("foov1__0__0__1")).toEqual(true); - }); - - it("should return false for invalid directory names", function () { - expect(isValidDirectoryName("")).toEqual(false); - expect(isValidDirectoryName("👨‍💻")).toEqual(false); - expect(isValidDirectoryName("dir#")).toEqual(false); - expect(isValidDirectoryName("dir!")).toEqual(false); - expect(isValidDirectoryName("dir*")).toEqual(false); - expect(isValidDirectoryName(".")).toEqual(false); - expect(isValidDirectoryName("..")).toEqual(false); - expect(isValidDirectoryName("1.")).toEqual(false); - expect(isValidDirectoryName("foo.")).toEqual(false); - expect(isValidDirectoryName("foov1.0.0.1.")).toEqual(false); - }); - }); - - describe("isValidDirectoryPath()", function () { - const isValidDirectoryPath = dirHelpers.isValidDirectoryPath; - - it("should return false for empty strings", function () { - expect(isValidDirectoryPath("")).toEqual(false); - }); - - it("should return true only for the forward slash if the string has length 1", function () { - expect(isValidDirectoryPath("/")).toEqual(true); - expect(isValidDirectoryPath(" ")).toEqual(false); - expect(isValidDirectoryPath(".")).toEqual(false); - expect(isValidDirectoryPath("a")).toEqual(false); - }); - - it("should return true for valid directory paths", function () { - expect(isValidDirectoryPath("/a")).toEqual(true); - expect(isValidDirectoryPath("/dir/a")).toEqual(true); - expect(isValidDirectoryPath("/dir/foo")).toEqual(true); - expect(isValidDirectoryPath("/.dir/foo-dir")).toEqual(true); - expect(isValidDirectoryPath("/.dir/foo_dir")).toEqual(true); - expect(isValidDirectoryPath("/.dir/.a")).toEqual(true); - expect(isValidDirectoryPath("/dir1/1")).toEqual(true); - expect(isValidDirectoryPath("/dir1/a1")).toEqual(true); - expect(isValidDirectoryPath("/dir1/.a1")).toEqual(true); - expect(isValidDirectoryPath("/dir_/._foo")).toEqual(true); - expect(isValidDirectoryPath("/dir-/_foo")).toEqual(true); - }); - - it("should return false if the path does not have a leading slash", function () { - expect(isValidDirectoryPath("a")).toEqual(false); - expect(isValidDirectoryPath("dir/a")).toEqual(false); - expect(isValidDirectoryPath("dir/foo")).toEqual(false); - expect(isValidDirectoryPath(".dir/foo-dir")).toEqual(false); - expect(isValidDirectoryPath(".dir/foo_dir")).toEqual(false); - expect(isValidDirectoryPath(".dir/.a")).toEqual(false); - expect(isValidDirectoryPath("dir1/1")).toEqual(false); - expect(isValidDirectoryPath("dir1/a1")).toEqual(false); - expect(isValidDirectoryPath("dir1/.a1")).toEqual(false); - expect(isValidDirectoryPath("dir_/._foo")).toEqual(false); - expect(isValidDirectoryPath("dir-/_foo")).toEqual(false); - }); - - it("should accept dot notation", function () { - expect(isValidDirectoryPath("/dir/./a")).toEqual(true); - expect(isValidDirectoryPath("/dir/../foo")).toEqual(true); - expect(isValidDirectoryPath("/.dir/./foo-dir")).toEqual(true); - expect(isValidDirectoryPath("/.dir/../foo_dir")).toEqual(true); - expect(isValidDirectoryPath("/.dir/./.a")).toEqual(true); - expect(isValidDirectoryPath("/dir1/1/.")).toEqual(true); - expect(isValidDirectoryPath("/dir1/a1/..")).toEqual(true); - expect(isValidDirectoryPath("/dir1/.a1/..")).toEqual(true); - expect(isValidDirectoryPath("/dir_/._foo/.")).toEqual(true); - expect(isValidDirectoryPath("/./dir-/_foo")).toEqual(true); - expect(isValidDirectoryPath("/../dir-/_foo")).toEqual(true); - }); - }); - - describe("isValidFilePath()", function () { - const isValidFilePath = dirHelpers.isValidFilePath; - - it("should return false for strings that are too short", function () { - expect(isValidFilePath("/a")).toEqual(false); - expect(isValidFilePath("a.")).toEqual(false); - expect(isValidFilePath(".a")).toEqual(false); - expect(isValidFilePath("/.")).toEqual(false); - }); - - it("should return true for arguments that are just filenames", function () { - expect(isValidFilePath("test.txt")).toEqual(true); - expect(isValidFilePath("123.script")).toEqual(true); - expect(isValidFilePath("foo123.b")).toEqual(true); - expect(isValidFilePath("my_script.script")).toEqual(true); - expect(isValidFilePath("my-script.script")).toEqual(true); - expect(isValidFilePath("_foo.lit")).toEqual(true); - expect(isValidFilePath("mult.periods.script")).toEqual(true); - expect(isValidFilePath("mult.per-iods.again.script")).toEqual(true); - }); - - it("should return true for valid filepaths", function () { - expect(isValidFilePath("/foo/test.txt")).toEqual(true); - expect(isValidFilePath("/../123.script")).toEqual(true); - expect(isValidFilePath("/./foo123.b")).toEqual(true); - expect(isValidFilePath("/dir/my_script.script")).toEqual(true); - expect(isValidFilePath("/dir1/dir2/dir3/my-script.script")).toEqual(true); - expect(isValidFilePath("/dir1/dir2/././../_foo.lit")).toEqual(true); - expect(isValidFilePath("/.dir1/./../.dir2/mult.periods.script")).toEqual(true); - expect(isValidFilePath("/_dir/../dir2/mult.per-iods.again.script")).toEqual(true); - }); - - it("should return false for strings that end with a slash", function () { - expect(isValidFilePath("/foo/")).toEqual(false); - expect(isValidFilePath("foo.txt/")).toEqual(false); - expect(isValidFilePath("/")).toEqual(false); - expect(isValidFilePath("/_dir/")).toEqual(false); - }); - - it("should return false for invalid arguments", function () { - expect(isValidFilePath(null as unknown as string)).toEqual(false); - expect(isValidFilePath(undefined as unknown as string)).toEqual(false); - expect(isValidFilePath(5 as unknown as string)).toEqual(false); - expect(isValidFilePath({} as unknown as string)).toEqual(false); - }); - }); - - describe("getFirstParentDirectory()", function () { - const getFirstParentDirectory = dirHelpers.getFirstParentDirectory; - - it("should return the first parent directory in a filepath", function () { - expect(getFirstParentDirectory("/dir1/foo.txt")).toEqual("dir1/"); - expect(getFirstParentDirectory("/dir1/dir2/dir3/dir4/foo.txt")).toEqual("dir1/"); - expect(getFirstParentDirectory("/_dir1/dir2/foo.js")).toEqual("_dir1/"); - }); - - it("should return '/' if there is no first parent directory", function () { - expect(getFirstParentDirectory("")).toEqual("/"); - expect(getFirstParentDirectory(" ")).toEqual("/"); - expect(getFirstParentDirectory("/")).toEqual("/"); - expect(getFirstParentDirectory("//")).toEqual("/"); - expect(getFirstParentDirectory("foo.script")).toEqual("/"); - expect(getFirstParentDirectory("/foo.txt")).toEqual("/"); - }); - }); - - describe("getAllParentDirectories()", function () { - const getAllParentDirectories = dirHelpers.getAllParentDirectories; - - it("should return all parent directories in a filepath", function () { - expect(getAllParentDirectories("/")).toEqual("/"); - expect(getAllParentDirectories("/home/var/foo.txt")).toEqual("/home/var/"); - expect(getAllParentDirectories("/home/var/")).toEqual("/home/var/"); - expect(getAllParentDirectories("/home/var/test/")).toEqual("/home/var/test/"); - }); - - it("should return an empty string if there are no parent directories", function () { - expect(getAllParentDirectories("foo.txt")).toEqual(""); - }); - }); - - describe("isInRootDirectory()", function () { - const isInRootDirectory = dirHelpers.isInRootDirectory; - - it("should return true for filepaths that refer to a file in the root directory", function () { - expect(isInRootDirectory("a.b")).toEqual(true); - expect(isInRootDirectory("foo.txt")).toEqual(true); - expect(isInRootDirectory("/foo.txt")).toEqual(true); - }); - - it("should return false for filepaths that refer to a file that's NOT in the root directory", function () { - expect(isInRootDirectory("/dir/foo.txt")).toEqual(false); - expect(isInRootDirectory("dir/foo.txt")).toEqual(false); - expect(isInRootDirectory("/./foo.js")).toEqual(false); - expect(isInRootDirectory("../foo.js")).toEqual(false); - expect(isInRootDirectory("/dir1/dir2/dir3/foo.txt")).toEqual(false); - }); - - it("should return false for invalid inputs (inputs that aren't filepaths)", function () { - expect(isInRootDirectory(null as unknown as string)).toEqual(false); - expect(isInRootDirectory(undefined as unknown as string)).toEqual(false); - expect(isInRootDirectory("")).toEqual(false); - expect(isInRootDirectory(" ")).toEqual(false); - expect(isInRootDirectory("a")).toEqual(false); - expect(isInRootDirectory("/dir")).toEqual(false); - expect(isInRootDirectory("/dir/")).toEqual(false); - expect(isInRootDirectory("/dir/foo")).toEqual(false); - }); - }); - - describe("evaluateDirectoryPath()", function () { - //const evaluateDirectoryPath = dirHelpers.evaluateDirectoryPath; - // TODO - }); - - describe("evaluateFilePath()", function () { - //const evaluateFilePath = dirHelpers.evaluateFilePath; - // TODO - }); -}); diff --git a/test/jest/Terminal/Path.test.ts b/test/jest/Terminal/Path.test.ts new file mode 100644 index 000000000..3e1ea91df --- /dev/null +++ b/test/jest/Terminal/Path.test.ts @@ -0,0 +1,283 @@ +import { FilePath, isFilePath } from "../../../src/Paths/FilePath"; +import { + Directory, + getFirstDirectoryInPath, + isAbsolutePath, + isDirectoryPath, + resolveDirectory, +} from "../../../src/Paths/Directory"; + +const validBaseDirectory = "foo/bar/"; +if (!isDirectoryPath(validBaseDirectory) || !isAbsolutePath(validBaseDirectory)) { + throw new Error("The valid base directory was actually not valid."); +} + +// Actual validation is in two separate functions as above, combining them here for simplicity in tests. +function isValidDirectory(name: string) { + return isAbsolutePath(name) && isDirectoryPath(name); +} +function isValidFilePath(name: string) { + return isAbsolutePath(name) && isFilePath(name); +} + +describe("Terminal Directory Tests", function () { + describe("resolveDirectory()", function () { + it("Should fail when provided multiple leading slashes", function () { + expect(resolveDirectory("///")).toBe(null); + expect(resolveDirectory("//foo")).toBe(null); + }); + it("should do nothing for valid directory path", function () { + expect(resolveDirectory("")).toBe(""); + expect(resolveDirectory("foo/bar/")).toBe("foo/bar/"); + }); + it("should provide relative pathing", function () { + // The leading slash indicates an absolute path instead of relative. + expect(resolveDirectory("/", validBaseDirectory)).toBe(""); + expect(resolveDirectory("./", validBaseDirectory)).toBe("foo/bar/"); + expect(resolveDirectory("../", validBaseDirectory)).toBe("foo/"); + expect(resolveDirectory("../../", validBaseDirectory)).toBe(""); + expect(resolveDirectory("../../../", validBaseDirectory)).toBe(null); + expect(resolveDirectory("baz", validBaseDirectory)).toBe("foo/bar/baz/"); + expect(resolveDirectory("baz/", validBaseDirectory)).toBe("foo/bar/baz/"); + }); + }); + + describe("isFilePath()", function () { + // Actual validation occurs in two steps, validating the filepath structure and then validating that it's not a relative path + it("should return true for valid filenames", function () { + expect(isFilePath("test.txt")).toBe(true); + expect(isFilePath("123.script")).toBe(true); + expect(isFilePath("foo123.b")).toBe(true); + expect(isFilePath("my_script.script")).toBe(true); + expect(isFilePath("my-script.script")).toBe(true); + expect(isFilePath("_foo.lit")).toBe(true); + expect(isFilePath("mult.periods.script")).toBe(true); + expect(isFilePath("mult.per-iods.again.script")).toBe(true); + expect(isFilePath("BruteSSH.exe-50%-INC")).toBe(true); + expect(isFilePath("DeepscanV1.exe-1.01%-INC")).toBe(true); + expect(isFilePath("DeepscanV2.exe-1.00%-INC")).toBe(true); + expect(isFilePath("AutoLink.exe-1.%-INC")).toBe(true); + }); + + it("should return false for invalid filenames", function () { + expect(isFilePath("foo")).toBe(false); + expect(isFilePath("my script.script")).toBe(false); + //expect(isFilePath("a^.txt")).toBe(false); + //expect(isFilePath("b#.lit")).toBe(false); + //expect(isFilePath("lib().js")).toBe(false); + //expect(isFilePath("foo.script_")).toBe(false); + //expect(isFilePath("foo._script")).toBe(false); + //expect(isFilePath("foo.hyphened-ext")).toBe(false); + expect(isFilePath("")).toBe(false); + //expect(isFilePath("AutoLink-1.%-INC.exe")).toBe(false); + //expect(isFilePath("AutoLink.exe-1.%-INC.exe")).toBe(false); + //expect(isFilePath("foo%.exe")).toBe(false); + //expect(isFilePath("-1.00%-INC")).toBe(false); + }); + }); + + describe("isDirectoryPath()", function () { + it("should return true for valid directory names", function () { + expect(isValidDirectory("a/")).toBe(true); + expect(isValidDirectory("foo/")).toBe(true); + expect(isValidDirectory("foo-dir/")).toBe(true); + expect(isValidDirectory("foo_dir/")).toBe(true); + expect(isValidDirectory(".a/")).toBe(true); + expect(isValidDirectory("1/")).toBe(true); + expect(isValidDirectory("a1/")).toBe(true); + expect(isValidDirectory(".a1/")).toBe(true); + expect(isValidDirectory("._foo/")).toBe(true); + expect(isValidDirectory("_foo/")).toBe(true); + // the changes made to support this broke mv + // see https://github.com/danielyxie/bitburner/pull/3653 if you try to re support + expect(isValidDirectory("foo.dir/")).toBe(true); + expect(isValidDirectory("foov1.0.0.1/")).toBe(true); + expect(isValidDirectory("foov1..0..0..1/")).toBe(true); + expect(isValidDirectory("foov1-0-0-1/")).toBe(true); + expect(isValidDirectory("foov1-0-0-1-/")).toBe(true); + expect(isValidDirectory("foov1--0--0--1--/")).toBe(true); + expect(isValidDirectory("foov1_0_0_1/")).toBe(true); + expect(isValidDirectory("foov1_0_0_1_/")).toBe(true); + expect(isValidDirectory("foov1__0__0__1/")).toBe(true); + }); + + it("should return false for invalid directory names", function () { + // expect(isValidDirectory("👨‍💻/")).toBe(false); + // expect(isValidDirectory("dir#/")).toBe(false); + // expect(isValidDirectory("dir!/")).toBe(false); + expect(isValidDirectory("dir*/")).toBe(false); + expect(isValidDirectory("./")).toBe(false); + expect(isValidDirectory("../")).toBe(false); + // expect(isValidDirectory("1./")).toBe(false); + //expect(isValidDirectory("foo./")).toBe(false); + //expect(isValidDirectory("foov1.0.0.1./")).toBe(false); + }); + }); + + describe("isValidDirectoryPath()", function () { + it("should return true only for the forward slash if the string has length 1", function () { + //expect(isValidDirectory("/")).toBe(true); + expect(isValidDirectory(" ")).toBe(false); + expect(isValidDirectory(".")).toBe(false); + expect(isValidDirectory("a")).toBe(false); + }); + + it("should return true for valid directory paths", function () { + expect(isValidDirectory("a/")).toBe(true); + expect(isValidDirectory("dir/a/")).toBe(true); + expect(isValidDirectory("dir/foo/")).toBe(true); + expect(isValidDirectory(".dir/foo-dir/")).toBe(true); + expect(isValidDirectory(".dir/foo_dir/")).toBe(true); + expect(isValidDirectory(".dir/.a/")).toBe(true); + expect(isValidDirectory("dir1/1/")).toBe(true); + expect(isValidDirectory("dir1/a1/")).toBe(true); + expect(isValidDirectory("dir1/.a1/")).toBe(true); + expect(isValidDirectory("dir_/._foo/")).toBe(true); + expect(isValidDirectory("dir-/_foo/")).toBe(true); + }); + + it("should return false if the path has a leading slash", function () { + expect(isValidDirectory("/a")).toBe(false); + expect(isValidDirectory("/dir/a")).toBe(false); + expect(isValidDirectory("/dir/foo")).toBe(false); + expect(isValidDirectory("/.dir/foo-dir")).toBe(false); + expect(isValidDirectory("/.dir/foo_dir")).toBe(false); + expect(isValidDirectory("/.dir/.a")).toBe(false); + expect(isValidDirectory("/dir1/1")).toBe(false); + expect(isValidDirectory("/dir1/a1")).toBe(false); + expect(isValidDirectory("/dir1/.a1")).toBe(false); + expect(isValidDirectory("/dir_/._foo")).toBe(false); + expect(isValidDirectory("/dir-/_foo")).toBe(false); + }); + + it("should accept dot notation", function () { + // These are relative paths so we will forego the absolute check + expect(isDirectoryPath("dir/./a/")).toBe(true); + expect(isDirectoryPath("dir/../foo/")).toBe(true); + expect(isDirectoryPath(".dir/./foo-dir/")).toBe(true); + expect(isDirectoryPath(".dir/../foo_dir/")).toBe(true); + expect(isDirectoryPath(".dir/./.a/")).toBe(true); + expect(isDirectoryPath("dir1/1/./")).toBe(true); + expect(isDirectoryPath("dir1/a1/../")).toBe(true); + expect(isDirectoryPath("dir1/.a1/../")).toBe(true); + expect(isDirectoryPath("dir_/._foo/./")).toBe(true); + expect(isDirectoryPath("./dir-/_foo/")).toBe(true); + expect(isDirectoryPath("../dir-/_foo/")).toBe(true); + }); + }); + + describe("isValidFilePath()", function () { + it("should return false for strings that are too short", function () { + expect(isValidFilePath("/a")).toBe(false); + expect(isValidFilePath("a.")).toBe(false); + expect(isValidFilePath(".a")).toBe(false); + expect(isValidFilePath("/.")).toBe(false); + }); + + it("should return true for arguments that are just filenames", function () { + expect(isValidFilePath("test.txt")).toBe(true); + expect(isValidFilePath("123.script")).toBe(true); + expect(isValidFilePath("foo123.b")).toBe(true); + expect(isValidFilePath("my_script.script")).toBe(true); + expect(isValidFilePath("my-script.script")).toBe(true); + expect(isValidFilePath("_foo.lit")).toBe(true); + expect(isValidFilePath("mult.periods.script")).toBe(true); + expect(isValidFilePath("mult.per-iods.again.script")).toBe(true); + }); + + it("should return true for valid filepaths", function () { + // Some of these include relative paths, will not check absoluteness + expect(isFilePath("foo/test.txt")).toBe(true); + expect(isFilePath("../123.script")).toBe(true); + expect(isFilePath("./foo123.b")).toBe(true); + expect(isFilePath("dir/my_script.script")).toBe(true); + expect(isFilePath("dir1/dir2/dir3/my-script.script")).toBe(true); + expect(isFilePath("dir1/dir2/././../_foo.lit")).toBe(true); + expect(isFilePath(".dir1/./../.dir2/mult.periods.script")).toBe(true); + expect(isFilePath("_dir/../dir2/mult.per-iods.again.script")).toBe(true); + }); + + it("should return false for strings that begin with a slash", function () { + expect(isValidFilePath("/foo/foo.txt")).toBe(false); + expect(isValidFilePath("/foo.txt/bar.script")).toBe(false); + expect(isValidFilePath("/filename.ext")).toBe(false); + expect(isValidFilePath("/_dir/test.js")).toBe(false); + }); + + it("should return false for invalid arguments", function () { + expect(isValidFilePath(null as unknown as string)).toBe(false); + expect(isValidFilePath(undefined as unknown as string)).toBe(false); + expect(isValidFilePath(5 as unknown as string)).toBe(false); + expect(isValidFilePath({} as unknown as string)).toBe(false); + }); + }); + + describe("getFirstDirectoryInPath()", function () { + // Strings cannot be passed in directly, so we'll wrap some typechecking + function firstDirectory(path: string): string | null | undefined { + if (!isAbsolutePath(path)) return undefined; + if (!isFilePath(path) && !isDirectoryPath(path)) return undefined; + return getFirstDirectoryInPath(path); + } + + it("should return the first parent directory in a filepath", function () { + expect(firstDirectory("dir1/foo.txt")).toBe("dir1/"); + expect(firstDirectory("dir1/dir2/dir3/dir4/foo.txt")).toBe("dir1/"); + expect(firstDirectory("_dir1/dir2/foo.js")).toBe("_dir1/"); + }); + + it("should return null if there is no first parent directory", function () { + expect(firstDirectory("")).toBe(null); + expect(firstDirectory(" ")).toBe(undefined); //Invalid path + expect(firstDirectory("/")).toBe(undefined); //Invalid path + expect(firstDirectory("//")).toBe(undefined); //Invalid path + expect(firstDirectory("foo.script")).toBe(null); + expect(firstDirectory("/foo.txt")).toBe(undefined); //Invalid path; + }); + }); + /* + describe("getAllParentDirectories()", function () { + const getAllParentDirectories = dirHelpers.getAllParentDirectories; + + it("should return all parent directories in a filepath", function () { + expect(getAllParentDirectories("/")).toBe("/"); + expect(getAllParentDirectories("/home/var/foo.txt")).toBe("/home/var/"); + expect(getAllParentDirectories("/home/var/")).toBe("/home/var/"); + expect(getAllParentDirectories("/home/var/test/")).toBe("/home/var/test/"); + }); + + it("should return an empty string if there are no parent directories", function () { + expect(getAllParentDirectories("foo.txt")).toBe(""); + }); + }); + + describe("isInRootDirectory()", function () { + const isInRootDirectory = dirHelpers.isInRootDirectory; + + it("should return true for filepaths that refer to a file in the root directory", function () { + expect(isInRootDirectory("a.b")).toBe(true); + expect(isInRootDirectory("foo.txt")).toBe(true); + expect(isInRootDirectory("/foo.txt")).toBe(true); + }); + + it("should return false for filepaths that refer to a file that's NOT in the root directory", function () { + expect(isInRootDirectory("/dir/foo.txt")).toBe(false); + expect(isInRootDirectory("dir/foo.txt")).toBe(false); + expect(isInRootDirectory("/./foo.js")).toBe(false); + expect(isInRootDirectory("../foo.js")).toBe(false); + expect(isInRootDirectory("/dir1/dir2/dir3/foo.txt")).toBe(false); + }); + + it("should return false for invalid inputs (inputs that aren't filepaths)", function () { + expect(isInRootDirectory(null as unknown as string)).toBe(false); + expect(isInRootDirectory(undefined as unknown as string)).toBe(false); + expect(isInRootDirectory("")).toBe(false); + expect(isInRootDirectory(" ")).toBe(false); + expect(isInRootDirectory("a")).toBe(false); + expect(isInRootDirectory("/dir")).toBe(false); + expect(isInRootDirectory("/dir/")).toBe(false); + expect(isInRootDirectory("/dir/foo")).toBe(false); + }); + }); + */ +}); diff --git a/test/jest/Terminal/determineAllPossibilitiesForTabCompletion.test.ts b/test/jest/Terminal/determineAllPossibilitiesForTabCompletion.test.ts deleted file mode 100644 index 4f9f247a4..000000000 --- a/test/jest/Terminal/determineAllPossibilitiesForTabCompletion.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* eslint-disable no-await-in-loop */ - -import { Player } from "../../../src/Player"; -import { determineAllPossibilitiesForTabCompletion } from "../../../src/Terminal/determineAllPossibilitiesForTabCompletion"; -import { Server } from "../../../src/Server/Server"; -import { AddToAllServers, prestigeAllServers } from "../../../src/Server/AllServers"; -import { LocationName } from "../../../src/Enums"; -import { CodingContract } from "../../../src/CodingContracts"; -import { initDarkWebItems } from "../../../src/DarkWeb/DarkWebItems"; - -describe("determineAllPossibilitiesForTabCompletion", function () { - let closeServer: Server; - let farServer: Server; - - beforeEach(() => { - prestigeAllServers(); - initDarkWebItems(); - Player.init(); - - closeServer = new Server({ - ip: "8.8.8.8", - hostname: "near", - hackDifficulty: 1, - moneyAvailable: 70000, - numOpenPortsRequired: 0, - organizationName: LocationName.NewTokyoNoodleBar, - requiredHackingSkill: 1, - serverGrowth: 3000, - }); - farServer = new Server({ - ip: "4.4.4.4", - hostname: "far", - hackDifficulty: 1, - moneyAvailable: 70000, - numOpenPortsRequired: 0, - organizationName: LocationName.Sector12JoesGuns, - requiredHackingSkill: 1, - serverGrowth: 3000, - }); - Player.getHomeComputer().serversOnNetwork.push(closeServer.hostname); - closeServer.serversOnNetwork.push(Player.getHomeComputer().hostname); - closeServer.serversOnNetwork.push(farServer.hostname); - farServer.serversOnNetwork.push(closeServer.hostname); - AddToAllServers(closeServer); - AddToAllServers(farServer); - }); - - it("completes the connect command", async () => { - const options = await determineAllPossibilitiesForTabCompletion("connect ", 0); - expect(options).toEqual(["near"]); - }); - - it("completes the buy command", async () => { - const options = await determineAllPossibilitiesForTabCompletion("buy ", 0); - expect(options.sort()).toEqual( - [ - "BruteSSH.exe", - "FTPCrack.exe", - "relaySMTP.exe", - "HTTPWorm.exe", - "SQLInject.exe", - "DeepscanV1.exe", - "DeepscanV2.exe", - "AutoLink.exe", - "ServerProfiler.exe", - "Formulas.exe", - ].sort(), - ); - }); - - it("completes the scp command", async () => { - Player.getHomeComputer().writeToTextFile("note.txt", "oh hai mark"); - Player.getHomeComputer().messages.push("af.lit"); - Player.getHomeComputer().writeToScriptFile("/www/script.js", "oh hai mark"); - const options1 = await determineAllPossibilitiesForTabCompletion("scp ", 0); - expect(options1).toEqual(["/www/script.js", "af.lit", "note.txt", "www/"]); - - const options2 = await determineAllPossibilitiesForTabCompletion("scp note.txt ", 1); - expect(options2).toEqual(["home", "near", "far"]); - }); - - it("completes the kill, tail, mem, and check commands", async () => { - Player.getHomeComputer().writeToScriptFile("/www/script.js", "oh hai mark"); - for (const command of ["kill", "tail", "mem", "check"]) { - const options = await determineAllPossibilitiesForTabCompletion(`${command} `, 0); - expect(options).toEqual(["/www/script.js", "www/"]); - } - }); - - it("completes the nano commands", async () => { - Player.getHomeComputer().writeToScriptFile("/www/script.js", "oh hai mark"); - Player.getHomeComputer().writeToTextFile("note.txt", "oh hai mark"); - const options = await determineAllPossibilitiesForTabCompletion("nano ", 0); - expect(options).toEqual(["/www/script.js", "note.txt", "www/"]); - }); - - it("completes the rm command", async () => { - Player.getHomeComputer().writeToTextFile("note.txt", "oh hai mark"); - Player.getHomeComputer().writeToScriptFile("/www/script.js", "oh hai mark"); - Player.getHomeComputer().contracts.push(new CodingContract("linklist.cct")); - Player.getHomeComputer().messages.push("asl.msg"); - Player.getHomeComputer().messages.push("af.lit"); - const options = await determineAllPossibilitiesForTabCompletion("rm ", 0); - expect(options).toEqual(["/www/script.js", "NUKE.exe", "af.lit", "note.txt", "linklist.cct", "www/"]); - }); - - it("completes the run command", async () => { - Player.getHomeComputer().writeToScriptFile("/www/script.js", "oh hai mark"); - Player.getHomeComputer().contracts.push(new CodingContract("linklist.cct")); - const options = await determineAllPossibilitiesForTabCompletion("run ", 0); - expect(options).toEqual(["/www/script.js", "NUKE.exe", "linklist.cct", "www/"]); - }); - - it("completes the cat command", async () => { - Player.getHomeComputer().writeToTextFile("/www/note.txt", "oh hai mark"); - Player.getHomeComputer().messages.push("asl.msg"); - Player.getHomeComputer().messages.push("af.lit"); - const options = await determineAllPossibilitiesForTabCompletion("cat ", 0); - expect(options).toEqual(["asl.msg", "af.lit", "/www/note.txt", "www/"]); - }); - - it("completes the download and mv commands", async () => { - Player.getHomeComputer().writeToScriptFile("/www/script.js", "oh hai mark"); - Player.getHomeComputer().writeToTextFile("note.txt", "oh hai mark"); - for (const command of ["download", "mv"]) { - const options = await determineAllPossibilitiesForTabCompletion(`${command} `, 0); - expect(options).toEqual(["/www/script.js", "note.txt", "www/"]); - } - }); - - it("completes the cd command", async () => { - Player.getHomeComputer().writeToScriptFile("/www/script.js", "oh hai mark"); - const options = await determineAllPossibilitiesForTabCompletion("cd ", 0); - expect(options).toEqual(["www/"]); - }); - - it("completes the ls and cd commands", async () => { - Player.getHomeComputer().writeToScriptFile("/www/script.js", "oh hai mark"); - for (const command of ["ls", "cd"]) { - const options = await determineAllPossibilitiesForTabCompletion(`${command} `, 0); - expect(options).toEqual(["www/"]); - } - }); - - it("completes commands starting with ./", async () => { - Player.getHomeComputer().writeToScriptFile("/www/script.js", "oh hai mark"); - const options = await determineAllPossibilitiesForTabCompletion("run ./", 0); - expect(options).toEqual([".//www/script.js", "NUKE.exe", "./www/"]); - }); -}); diff --git a/test/jest/Terminal/tabCompletion.test.ts b/test/jest/Terminal/tabCompletion.test.ts new file mode 100644 index 000000000..83bd812e7 --- /dev/null +++ b/test/jest/Terminal/tabCompletion.test.ts @@ -0,0 +1,222 @@ +/* eslint-disable no-await-in-loop */ + +import { Player } from "../../../src/Player"; +import { getTabCompletionPossibilities } from "../../../src/Terminal/getTabCompletionPossibilities"; +import { Server } from "../../../src/Server/Server"; +import { AddToAllServers, prestigeAllServers } from "../../../src/Server/AllServers"; +import { LocationName } from "../../../src/Enums"; +import { CodingContract } from "../../../src/CodingContracts"; +import { asFilePath } from "../../../src/Paths/FilePath"; +import { Directory, isAbsolutePath, isDirectoryPath, root } from "../../../src/Paths/Directory"; +import { hasTextExtension } from "../../../src/Paths/TextFilePath"; +import { hasScriptExtension } from "../../../src/Paths/ScriptFilePath"; +import { LiteratureName } from "../../../src/Literature/data/LiteratureNames"; +import { MessageFilename } from "../../../src/Message/MessageHelpers"; +import { Terminal } from "../../../src/Terminal"; + +describe("getTabCompletionPossibilities", function () { + let closeServer: Server; + let farServer: Server; + + beforeEach(() => { + prestigeAllServers(); + Player.init(); + + closeServer = new Server({ + ip: "8.8.8.8", + hostname: "near", + hackDifficulty: 1, + moneyAvailable: 70000, + numOpenPortsRequired: 0, + organizationName: LocationName.NewTokyoNoodleBar, + requiredHackingSkill: 1, + serverGrowth: 3000, + }); + farServer = new Server({ + ip: "4.4.4.4", + hostname: "far", + hackDifficulty: 1, + moneyAvailable: 70000, + numOpenPortsRequired: 0, + organizationName: LocationName.Sector12JoesGuns, + requiredHackingSkill: 1, + serverGrowth: 3000, + }); + Player.getHomeComputer().serversOnNetwork.push(closeServer.hostname); + closeServer.serversOnNetwork.push(Player.getHomeComputer().hostname); + closeServer.serversOnNetwork.push(farServer.hostname); + farServer.serversOnNetwork.push(closeServer.hostname); + AddToAllServers(closeServer); + AddToAllServers(farServer); + }); + + it("completes the connect command, regardless of folder", async () => { + let options = await getTabCompletionPossibilities("connect ", root); + expect(options).toEqual(["near"]); + options = await getTabCompletionPossibilities("connect ", asDirectory("folder1/")); + expect(options).toEqual(["near"]); + Terminal.connectToServer("near"); + options = await getTabCompletionPossibilities("connect ", root); + expect(options).toEqual(["home", "far"]); + options = await getTabCompletionPossibilities("connect h", asDirectory("folder1/")); + // Also test completion of a partially completed text + expect(options).toEqual(["home"]); + }); + + it("completes the buy command", async () => { + let options = await getTabCompletionPossibilities("buy ", root); + expect(options.sort()).toEqual( + [ + "BruteSSH.exe", + "FTPCrack.exe", + "relaySMTP.exe", + "HTTPWorm.exe", + "SQLInject.exe", + "DeepscanV1.exe", + "DeepscanV2.exe", + "AutoLink.exe", + "ServerProfiler.exe", + "Formulas.exe", + ].sort(), + ); + // Also test that darkweb items will be completed if they have incorrect capitalization in progress + options = await getTabCompletionPossibilities("buy de", root); + expect(options.sort()).toEqual(["DeepscanV1.exe", "DeepscanV2.exe"].sort()); + }); + + it("completes the scp command", async () => { + writeFiles(); + let options = await getTabCompletionPossibilities("scp ", root); + expect(options.sort()).toEqual( + [ + "note.txt", + "folder1/text.txt", + "folder1/text2.txt", + "hack.js", + "weaken.js", + "grow.js", + "old.script", + "folder1/test.js", + "anotherFolder/win.js", + LiteratureName.AGreenTomorrow, + ].sort(), + ); + // Test the second command argument (server name) + options = await getTabCompletionPossibilities("scp note.txt ", root); + expect(options).toEqual(["home", "near", "far"]); + }); + + it("completes the kill, tail, mem, and check commands", async () => { + writeFiles(); + for (const command of ["kill", "tail", "mem", "check"]) { + let options = await getTabCompletionPossibilities(`${command} `, root); + expect(options.sort()).toEqual(scriptFilePaths); + // From a directory, show only the options in that directory + options = await getTabCompletionPossibilities(`${command} `, asDirectory("folder1/")); + expect(options.sort()).toEqual(["test.js"]); + // From a directory but with relative path .., show stuff in the resolved directory with the relative pathing included + options = await getTabCompletionPossibilities(`${command} ../`, asDirectory("folder1/")); + expect(options.sort()).toEqual( + [...scriptFilePaths.map((path) => "../" + path), "../folder1/", "../anotherFolder/"].sort(), + ); + options = await getTabCompletionPossibilities(`${command} ../folder1/../anotherFolder/`, asDirectory("folder1/")); + expect(options.sort()).toEqual(["../folder1/../anotherFolder/win.js"]); + } + }); + + it("completes the nano commands", async () => { + writeFiles(); + const contentFilePaths = [...scriptFilePaths, ...textFilePaths].sort(); + const options = await getTabCompletionPossibilities("nano ", root); + expect(options.sort()).toEqual(contentFilePaths); + }); + + it("completes the rm command", async () => { + writeFiles(); + const removableFilePaths = [ + ...scriptFilePaths, + ...textFilePaths, + ...contractFilePaths, + LiteratureName.AGreenTomorrow, + "NUKE.exe", + ].sort(); + const options = await getTabCompletionPossibilities("rm ", root); + expect(options.sort()).toEqual(removableFilePaths); + }); + + it("completes the run command", async () => { + writeFiles(); + const runnableFilePaths = [...scriptFilePaths, ...contractFilePaths, "NUKE.exe"].sort(); + let options = await getTabCompletionPossibilities("run ", root); + expect(options.sort()).toEqual(runnableFilePaths); + // Also check the same files + options = await getTabCompletionPossibilities("./", root); + expect(options.sort()).toEqual( + [...runnableFilePaths.map((path) => "./" + path), "./folder1/", "./anotherFolder/"].sort(), + ); + }); + + it("completes the cat command", async () => { + writeFiles(); + const cattableFilePaths = [ + ...scriptFilePaths, + ...textFilePaths, + MessageFilename.TruthGazer, + LiteratureName.AGreenTomorrow, + ].sort(); + const options = await getTabCompletionPossibilities("cat ", root); + expect(options.sort()).toEqual(cattableFilePaths); + }); + + it("completes the download and mv commands", async () => { + writeFiles(); + writeFiles(); + const contentFilePaths = [...scriptFilePaths, ...textFilePaths].sort(); + for (const command of ["download", "mv"]) { + const options = await getTabCompletionPossibilities(`${command} `, root); + expect(options.sort()).toEqual(contentFilePaths); + } + }); + + it("completes the ls and cd commands", async () => { + writeFiles(); + for (const command of ["ls", "cd"]) { + const options = await getTabCompletionPossibilities(`${command} `, root); + expect(options.sort()).toEqual(["folder1/", "anotherFolder/"].sort()); + } + }); +}); + +function asDirectory(dir: string): Directory { + if (!isAbsolutePath(dir) || !isDirectoryPath(dir)) throw new Error(`Directory ${dir} failed typechecking`); + return dir; +} +const textFilePaths = ["note.txt", "folder1/text.txt", "folder1/text2.txt"]; +const scriptFilePaths = [ + "hack.js", + "weaken.js", + "grow.js", + "old.script", + "folder1/test.js", + "anotherFolder/win.js", +].sort(); +const contractFilePaths = ["testContract.cct", "anothercontract.cct"]; +function writeFiles() { + const home = Player.getHomeComputer(); + for (const filename of textFilePaths) { + if (!hasTextExtension(filename)) { + throw new Error(`Text file ${filename} had the wrong extension.`); + } + home.writeToTextFile(asFilePath(filename), `File content for ${filename}`); + } + for (const filename of scriptFilePaths) { + if (!hasScriptExtension(filename)) { + throw new Error(`Script file ${filename} had the wrong extension.`); + } + home.writeToScriptFile(asFilePath(filename), `File content for ${filename}`); + } + for (const filename of contractFilePaths) { + home.contracts.push(new CodingContract(filename)); + } + home.messages.push(LiteratureName.AGreenTomorrow, MessageFilename.TruthGazer); +} diff --git a/test/jest/__snapshots__/Save.test.ts.snap b/test/jest/__snapshots__/Save.test.ts.snap index 36e1fa7b0..2a0db2f89 100644 --- a/test/jest/__snapshots__/Save.test.ts.snap +++ b/test/jest/__snapshots__/Save.test.ts.snap @@ -58,7 +58,10 @@ exports[`load/saveAllServers 1`] = ` "smtpPortOpen": false, "sqlPortOpen": false, "sshPortOpen": false, - "textFiles": [], + "textFiles": { + "ctor": "JSONMap", + "data": [] + }, "purchasedByPlayer": true, "backdoorInstalled": false, "baseDifficulty": 1, @@ -99,7 +102,10 @@ exports[`load/saveAllServers 1`] = ` "smtpPortOpen": false, "sqlPortOpen": false, "sshPortOpen": false, - "textFiles": [], + "textFiles": { + "ctor": "JSONMap", + "data": [] + }, "purchasedByPlayer": false, "backdoorInstalled": false, "baseDifficulty": 1, @@ -155,7 +161,10 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = ` "smtpPortOpen": false, "sqlPortOpen": false, "sshPortOpen": false, - "textFiles": [], + "textFiles": { + "ctor": "JSONMap", + "data": [] + }, "purchasedByPlayer": true, "backdoorInstalled": false, "baseDifficulty": 1, @@ -196,7 +205,10 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = ` "smtpPortOpen": false, "sqlPortOpen": false, "sshPortOpen": false, - "textFiles": [], + "textFiles": { + "ctor": "JSONMap", + "data": [] + }, "purchasedByPlayer": false, "backdoorInstalled": false, "baseDifficulty": 1,