From 3ce2e83dd8b6614f05c4dbb50c05d07831716327 Mon Sep 17 00:00:00 2001 From: danielyxie Date: Tue, 9 Apr 2019 23:07:12 -0700 Subject: [PATCH] Finished rudimentary filesystem implementation for Terminal --- doc/source/basicgameplay/terminal.rst | 146 ++- src/Augmentation/AugmentationHelpers.js | 7 - src/Constants.ts | 3 + src/Locations/LocationsHelpers.ts | 7 +- src/Message/MessageHelpers.js | 6 +- src/NetscriptFunctions.js | 54 +- src/Script/ScriptHelpers.js | 4 +- src/Server/BaseServer.ts | 69 ++ src/Server/Server.ts | 16 +- src/Terminal.js | 1061 +++++++++-------- src/Terminal/DirectoryHelpers.ts | 129 +- src/{ => Terminal}/HelpText.ts | 38 +- ...termineAllPossibilitiesForTabCompletion.ts | 268 +++-- src/Terminal/tabCompletion.ts | 113 ++ webpack.config.js | 1 + 15 files changed, 1232 insertions(+), 690 deletions(-) rename src/{ => Terminal}/HelpText.ts (91%) create mode 100644 src/Terminal/tabCompletion.ts diff --git a/doc/source/basicgameplay/terminal.rst b/doc/source/basicgameplay/terminal.rst index c2f06d8b0..743c24808 100644 --- a/doc/source/basicgameplay/terminal.rst +++ b/doc/source/basicgameplay/terminal.rst @@ -16,6 +16,84 @@ the terminal and enter:: nano .fconf + +.. _terminal_filesystem: + +Filesystem (Directories) +------------------------ +The Terminal contains a **very** basic filesystem that allows you to store and +organize your files into different directories. Note that this is **not** a true +filesystem implementation. Instead, it is done almost entirely using string manipulation. +For this reason, many of the nice & useful features you'd find in a real +filesystem do not exist. + +Here are the Terminal commands you'll commonly use when dealing with the filesystem. + +* :ref:`ls_terminal_command` +* :ref:`cd_terminal_command` +* :ref:`mv_terminal_command` + +Directories +^^^^^^^^^^^ +In order to create a directory, simply name a file using a full absolute Linux-style path:: + + /scripts/myScript.js + +This will automatically create a "directory" called :code:`scripts`. This will also work +for subdirectories:: + + /scripts/hacking/helpers/myHelperScripts.script + +Files in the root directory do not need to begin with a forward slash:: + + thisIsAFileInTheRootDirectory.txt + +Note that there is no way to manually create or remove directories. The creation and +deletion of directories is automatically handled as you name/rename/delete +files. + +Absolute vs Relative Paths +^^^^^^^^^^^^^^^^^^^^^^^^^^ +Many Terminal commands accept absolute both absolute and relative paths for specifying a +file. + +An absolute path specifies the location of the file from the root directory (/). +Any path that begins with the forward slash is an absolute path:: + + $ nano /scripts/myScript.js + $ cat /serverList.txt + +A relative path specifies the location of the file relative to the current working directory. +Any path that does **not** begin with a forward slash is a relative path. Note that the +Linux-style dot symbols will work for relative paths:: + + . (a single dot) - represents the current directory + .. (two dots) - represents the parent directory + + $ cd .. + $ nano ../scripts/myScript.js + $ nano ../../helper.js + +Netscript +^^^^^^^^^ +Note that in order to reference a file, :ref:`netscript` functions require the +**full** absolute file path. For example + +.. code:: javascript + + run("/scripts/hacking/helpers.myHelperScripts.script"); + rm("/logs/myHackingLogs.txt"); + rm("thisIsAFileInTheRootDirectory.txt"); + +.. note:: A full file path **must** begin with a forward slash (/) if that file + is not in the root directory. + +Missing Features +^^^^^^^^^^^^^^^^ +Terminal/Filesystem features that are not yet implemented: + +* Tab autocompletion does not work with relative paths + Commands -------- @@ -98,6 +176,25 @@ Display a message (.msg), literature (.lit), or text (.txt) file:: $ cat foo.lit $ cat servers.txt +.. _cd_terminal_command: + +cd +^^ + + $ cd [dir] + +Change to the specified directory. + +See :ref:`terminal_filesystem` for details on directories. + +Note that this command works even for directories that don't exist. If you change +to a directory that doesn't exist, it will not be created. A directory is only created +once there is a file in it:: + + $ cd scripts/hacking + $ cd /logs + $ cd .. + check ^^^^^ @@ -234,27 +331,35 @@ killall Kills all scripts on the current server. +.. _ls_terminal_command: + ls ^^ - $ ls [| grep pattern] + $ ls [dir] [| grep pattern] -Prints files on the current server to the Terminal screen. +Prints files and directories on the current server to the Terminal screen. -If this command is run with no arguments, then it prints all files on the current -server to the Terminal screen. The files will be displayed in alphabetical -order. +If this command is run with no arguments, then it prints all files and directories on the current +server to the Terminal screen. Directories will be printed first in alphabetical order, +followed by the files (also in alphabetical order). -The '| grep pattern' is an optional parameter that can be used to only display files -whose filenames match the specified pattern. For example, if you wanted to only display -files with the .script extension, you could use:: +The :code:`dir` optional parameter allows you to specify the directory for which to display +files. +The :code:`| grep pattern` optional parameter allows you to only display files and directories +with a certain pattern in their names. + +Examples:: + + // List files/directories with the '.script' extension in the current directory $ ls | grep .script -Alternatively, if you wanted to display all files with the word *purchase* in the filename, -you could use:: + // List files/directories with the '.js' extension in the root directory + $ ls / | grep .js - $ ls | grep purchase + // List files/directories with the word 'purchase' in the name, in the :code:`scripts` directory + $ ls scripts | grep purchase lscpu @@ -282,6 +387,25 @@ The first example above will print the amount of RAM needed to run 'foo.script' with a single thread. The second example above will print the amount of RAM needed to run 'foo.script' with 50 threads. +.. _mv_terminal_command: + +mv +^^ + + $ mv [source] [destination] + +Move the source file to the specified destination in the filesystem. +See :ref:`terminal_filesystem` for more details about the Terminal's filesystem. +This command only works for scripts and text files (.txt). It cannot, however, be used +to convert from script to text file, or vice versa. + +Note that this function can also be used to rename files. + +Examples:: + + $ mv hacking.script scripts/hacking.script + $ mv myScript.js myOldScript.js + nano ^^^^ diff --git a/src/Augmentation/AugmentationHelpers.js b/src/Augmentation/AugmentationHelpers.js index 9be26fea2..4119fc739 100644 --- a/src/Augmentation/AugmentationHelpers.js +++ b/src/Augmentation/AugmentationHelpers.js @@ -2097,12 +2097,6 @@ function displayAugmentationsContent(contentEl) { innerText:"Purchased Augmentations", })); - //Bladeburner text, once mechanic is unlocked - var bladeburnerText = "\n"; - if (Player.bitNodeN === 6 || hasBladeburnerSF) { - bladeburnerText = "Bladeburner Progress\n\n"; - } - contentEl.appendChild(createElement("pre", { width:"70%", whiteSpace:"pre-wrap", display:"block", innerText:"Below is a list of all Augmentations you have purchased but not yet installed. Click the button below to install them.\n" + @@ -2114,7 +2108,6 @@ function displayAugmentationsContent(contentEl) { "Hacknet Nodes\n" + "Faction/Company reputation\n" + "Stocks\n" + - bladeburnerText + "Installing Augmentations lets you start over with the perks and benefits granted by all " + "of the Augmentations you have ever installed. Also, you will keep any scripts and RAM/Core upgrades " + "on your home computer (but you will lose all programs besides NUKE.exe)." diff --git a/src/Constants.ts b/src/Constants.ts index 649487f6b..9d59f4f13 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -274,6 +274,9 @@ export let CONSTANTS: IMap = { LatestUpdate: ` v0.46.1 + * Added a very rudimentary directory system to the Terminal + ** Details here: https://bitburner.readthedocs.io/en/latest/basicgameplay/terminal.html#filesystem-directories + * Added numHashes(), hashCost(), and spendHashes() functions to the Netscript Hacknet Node API * 'Generate Coding Contract' hash upgrade is now more expensive * 'Generate Coding Contract' hash upgrade now generates the contract randomly on the server, rather than on home computer diff --git a/src/Locations/LocationsHelpers.ts b/src/Locations/LocationsHelpers.ts index 49ca28c8f..1838abf36 100644 --- a/src/Locations/LocationsHelpers.ts +++ b/src/Locations/LocationsHelpers.ts @@ -98,9 +98,10 @@ export function createPurchaseServerPopup(ram: number, p: IPlayer) { yesNoTxtInpBoxClose(); }); - yesNoTxtInpBoxCreate("Would you like to purchase a new server with " + ram + - "GB of RAM for $" + numeralWrapper.formatMoney(cost) + "?

" + - "Please enter the server hostname below:
"); + yesNoTxtInpBoxCreate( + `Would you like to purchase a new server with ${ram} GB of RAM for ` + + `${numeralWrapper.formatMoney(cost)}?

Please enter the server hostname below:
` + ); } /** diff --git a/src/Message/MessageHelpers.js b/src/Message/MessageHelpers.js index 4845e6a30..3aebd9adb 100644 --- a/src/Message/MessageHelpers.js +++ b/src/Message/MessageHelpers.js @@ -70,7 +70,11 @@ function checkForMessagesToSend() { } } else if (jumper0 && !jumper0.recvd && Player.hacking_skill >= 25) { sendMessage(jumper0); - Player.getHomeComputer().programs.push(Programs.Flight.name); + const flightName = Programs.Flight.name; + const homeComp = Player.getHomeComputer(); + if (!homeComp.programs.includes(flightName)) { + homeComp.programs.push(flightName); + } } else if (jumper1 && !jumper1.recvd && Player.hacking_skill >= 40) { sendMessage(jumper1); } else if (cybersecTest && !cybersecTest.recvd && Player.hacking_skill >= 50) { diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index aed916a91..bb5755043 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -2315,56 +2315,14 @@ function NetscriptFunctions(workerScript) { if (ip == null || ip === "") { ip = workerScript.serverIp; } - var s = getServer(ip); - if (s == null) { - throw makeRuntimeRejectMsg(workerScript, `Invalid server specified for rm(): ${ip}`); + const s = safeGetServer(ip, "rm"); + + const status = s.removeFile(fn); + if (!status.res) { + workerScript.log(status.msg); } - if (fn.endsWith(".exe")) { - for (var i = 0; i < s.programs.length; ++i) { - if (s.programs[i] === fn) { - s.programs.splice(i, 1); - return true; - } - } - } else if (isScriptFilename(fn)) { - for (var i = 0; i < s.scripts.length; ++i) { - if (s.scripts[i].filename === fn) { - //Check that the script isnt currently running - for (var j = 0; j < s.runningScripts.length; ++j) { - if (s.runningScripts[j].filename === fn) { - workerScript.scriptRef.log("Cannot delete a script that is currently running!"); - return false; - } - } - s.scripts.splice(i, 1); - return true; - } - } - } else if (fn.endsWith(".lit")) { - for (var i = 0; i < s.messages.length; ++i) { - var f = s.messages[i]; - if (!(f instanceof Message) && isString(f) && f === fn) { - s.messages.splice(i, 1); - return true; - } - } - } else if (fn.endsWith(".txt")) { - for (var i = 0; i < s.textFiles.length; ++i) { - if (s.textFiles[i].fn === fn) { - s.textFiles.splice(i, 1); - return true; - } - } - } else if (fn.endsWith(".cct")) { - for (var i = 0; i < s.contracts.length; ++i) { - if (s.contracts[i].fn === fn) { - s.contracts.splice(i, 1); - return true; - } - } - } - return false; + return status.res; }, scriptRunning : function(scriptname, ip) { if (workerScript.checkingRam) { diff --git a/src/Script/ScriptHelpers.js b/src/Script/ScriptHelpers.js index 9bb62b9e3..354ed67bf 100644 --- a/src/Script/ScriptHelpers.js +++ b/src/Script/ScriptHelpers.js @@ -17,7 +17,7 @@ import { AllServers } from "../Server/AllServers"; import { processSingleServerGrowth } from "../Server/ServerHelpers"; import { Settings } from "../Settings/Settings"; import { EditorSetting } from "../Settings/SettingEnums"; -import { isValidFilename } from "../Terminal/DirectoryHelpers"; +import { isValidFilePath } from "../Terminal/DirectoryHelpers"; import {TextFile} from "../TextFile"; import {Page, routing} from "../ui/navigationTracking"; @@ -248,7 +248,7 @@ function saveAndCloseScriptEditor() { return; } - if (filename !== ".fconf" && !isValidFilename(filename)) { + if (filename !== ".fconf" && !isValidFilePath(filename)) { dialogBoxCreate("Script filename can contain only alphanumerics, hyphens, and underscores"); return; } diff --git a/src/Server/BaseServer.ts b/src/Server/BaseServer.ts index a505c73d9..6fd0282ac 100644 --- a/src/Server/BaseServer.ts +++ b/src/Server/BaseServer.ts @@ -6,6 +6,7 @@ import { Message } from "../Message/Message"; import { RunningScript } from "../Script/RunningScript"; import { Script } from "../Script/Script"; import { TextFile } from "../TextFile"; +import { IReturnStatus } from "../types"; import { isScriptFilename } from "../Script/ScriptHelpersTS"; @@ -123,6 +124,20 @@ export class BaseServer { return null; } + /** + * Returns boolean indicating whether the given script is running on this server + */ + isRunning(fn: string): boolean { + // Check that the script isnt currently running + for (const runningScriptObj of this.runningScripts) { + if (runningScriptObj.filename === fn) { + return true; + } + } + + return false; + } + removeContract(contract: CodingContract) { if (contract instanceof CodingContract) { this.contracts = this.contracts.filter((c) => { @@ -135,6 +150,60 @@ export class BaseServer { } } + /** + * Remove a file from the server + * @param fn {string} Name of file to be deleted + * @returns {IReturnStatus} Return status object indicating whether or not file was deleted + */ + removeFile(fn: string): IReturnStatus { + if (fn.endsWith(".exe")) { + for (let i = 0; i < this.programs.length; ++i) { + if (this.programs[i] === fn) { + this.programs.splice(i, 1); + return { res: true }; + } + } + } else if (isScriptFilename(fn)) { + for (let i = 0; i < this.scripts.length; ++i) { + if (this.scripts[i].filename === fn) { + if (this.isRunning(fn)) { + return { + res: false, + msg: "Cannot delete a script that is currently running!", + }; + } + + this.scripts.splice(i, 1); + return { res: true }; + } + } + } else if (fn.endsWith(".lit")) { + for (let i = 0; i < this.messages.length; ++i) { + let f = this.messages[i]; + if (typeof f === "string" && f === fn) { + this.messages.splice(i, 1); + return { res: true }; + } + } + } else if (fn.endsWith(".txt")) { + for (let i = 0; i < this.textFiles.length; ++i) { + if (this.textFiles[i].fn === fn) { + this.textFiles.splice(i, 1); + return { res: true }; + } + } + } else if (fn.endsWith(".cct")) { + for (let i = 0; i < this.contracts.length; ++i) { + if (this.contracts[i].fn === fn) { + this.contracts.splice(i, 1); + return { res: true }; + } + } + } + + return { res: false, msg: "No such file exists" }; + } + /** * Called when a script is run on this server. * All this function does is add a RunningScript object to the diff --git a/src/Server/Server.ts b/src/Server/Server.ts index 9fff61a69..8b32b06d1 100644 --- a/src/Server/Server.ts +++ b/src/Server/Server.ts @@ -136,14 +136,6 @@ export class Server extends BaseServer { this.minDifficulty = Math.max(1, this.minDifficulty); } - /** - * Strengthens a server's security level (difficulty) by the specified amount - */ - fortify(amt: number): void { - this.hackDifficulty += amt; - this.capDifficulty(); - } - /** * Change this server's maximum money * @param n - Value by which to change the server's maximum money @@ -157,6 +149,14 @@ export class Server extends BaseServer { } } + /** + * Strengthens a server's security level (difficulty) by the specified amount + */ + fortify(amt: number): void { + this.hackDifficulty += amt; + this.capDifficulty(); + } + /** * Lowers the server's security level (difficulty) by the specified amount) */ diff --git a/src/Terminal.js b/src/Terminal.js index 06341e6f7..23e4c44ef 100644 --- a/src/Terminal.js +++ b/src/Terminal.js @@ -1,9 +1,26 @@ import { evaluateDirectoryPath, + evaluateFilePath, + getFirstParentDirectory, + isInRootDirectory, isValidDirectoryPath, - isValidFilename -} from "./Terminal/DirectoryHelpers"; -import { determineAllPossibilitiesForTabCompletion } from "./Terminal/determineAllPossibilitiesForTabCompletion"; + isValidFilename, + removeLeadingSlash, + removeTrailingSlash +} from "./Terminal/DirectoryHelpers"; + +import { + determineAllPossibilitiesForTabCompletion +} from "./Terminal/determineAllPossibilitiesForTabCompletion"; + +import { + TerminalHelpText, + HelpTexts +} from "./Terminal/HelpText"; + +import { + tabCompletion +} from "./Terminal/tabCompletion"; import { Aliases, GlobalAliases, @@ -30,7 +47,6 @@ import {calculateHackingChance, calculateGrowTime, calculateWeakenTime} from "./Hacking"; import { HacknetServer } from "./Hacknet/HacknetServer"; -import {TerminalHelpText, HelpTexts} from "./HelpText"; import {iTutorialNextStep, iTutorialSteps, ITutorial} from "./InteractiveTutorial"; import {showLiterature} from "./Literature"; @@ -52,8 +68,6 @@ import { SpecialServerIps, SpecialServerNames } from "./Server/SpecialServerIps"; import {getTextFile} from "./TextFile"; import { setTimeoutRef } from "./utils/SetTimeoutRef"; -import {containsAllStrings, - longestCommonStart} from "../utils/StringHelperFunctions"; import {Page, routing} from "./ui/navigationTracking"; import {numeralWrapper} from "./ui/numeralFormat"; import {KEY} from "../utils/helpers/keyCodes"; @@ -65,11 +79,13 @@ import {logBoxCreate} from "../utils/LogBox"; import {yesNoBoxCreate, yesNoBoxGetYesButton, yesNoBoxGetNoButton, yesNoBoxClose} from "../utils/YesNoBox"; -import { post, - postContent, - postError, - hackProgressBarPost, - hackProgressPost } from "./ui/postToTerminal"; +import { + post, + postContent, + postError, + hackProgressBarPost, + hackProgressPost +} from "./ui/postToTerminal"; import autosize from 'autosize'; import * as JSZip from 'jszip'; @@ -85,7 +101,7 @@ function isNumber(str) { return !isNaN(str) && !isNaN(parseFloat(str)); } -//Defines key commands in terminal +// Defines key commands in terminal $(document).keydown(function(event) { //Terminal if (routing.isOn(Page.Terminal)) { @@ -95,7 +111,7 @@ $(document).keydown(function(event) { if (event.keyCode === KEY.ENTER) { event.preventDefault(); //Prevent newline from being entered in Script Editor const command = terminalInput.value; - const dir = Terminal.currDir + "/"; + const dir = Terminal.currDir; post( "[" + (FconfSettings.ENABLE_TIMESTAMPS ? getTimestamp() + " " : "") + @@ -270,6 +286,7 @@ $(document).ready(function() { $('.terminal-input').focus(); } }); + $(document).keydown(function(e) { if (routing.isOn(Page.Terminal)) { if (e.which == KEY.CTRL) { @@ -286,7 +303,8 @@ $(document).keydown(function(e) { shiftKeyPressed = false; } } -}) +}); + $(document).keyup(function(e) { if (routing.isOn(Page.Terminal)) { if (e.which == KEY.CTRL) { @@ -296,112 +314,7 @@ $(document).keyup(function(e) { shiftKeyPressed = false; } } -}) - -//Implements a tab completion feature for terminal -// command - Terminal command except for the last incomplete argument -// arg - Incomplete argument string that the function will try to complete, or will display -// a series of possible options for -// allPossibilities - Array of strings containing all possibilities that the -// string can complete to -// index - index of argument that is being "tab completed". By default is 0, the first argument -function tabCompletion(command, arg, allPossibilities, index=0) { - if (!(allPossibilities.constructor === Array)) {return;} - if (!containsAllStrings(allPossibilities)) {return;} - - //if (!command.startsWith("./")) { - //command = command.toLowerCase(); - //} - - //Remove all options in allPossibilities that do not match the current string - //that we are attempting to autocomplete - if (arg == "") { - for (var i = allPossibilities.length-1; i >= 0; --i) { - if (!allPossibilities[i].toLowerCase().startsWith(command.toLowerCase())) { - allPossibilities.splice(i, 1); - } - } - } else { - for (var i = allPossibilities.length-1; i >= 0; --i) { - if (!allPossibilities[i].toLowerCase().startsWith(arg.toLowerCase())) { - allPossibilities.splice(i, 1); - } - } - } - - const textBox = document.getElementById("terminal-input-text-box"); - if (textBox == null) { - console.warn(`Couldn't find terminal input DOM element (id=terminal-input-text-box) when trying to autocomplete`); - return; - } - const oldValue = textBox.value; - const semiColonIndex = oldValue.lastIndexOf(";"); - - var 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 ; replace the whole thing. - textBox.value = val; - } else { - // replace just after the last semicolon - textBox.value = textBox.value.slice(0, semiColonIndex+1)+" "+val; - } - - textBox.focus(); - } else { - var longestStartSubstr = longestCommonStart(allPossibilities); - //If the longest common starting substring of remaining possibilities is the same - //as whatevers already in terminal, just list all possible options. Otherwise, - //change the input in the terminal to the longest common starting substr - var allOptionsStr = ""; - for (var i = 0; i < allPossibilities.length; ++i) { - allOptionsStr += allPossibilities[i]; - allOptionsStr += " "; - } - if (arg == "") { - if (longestStartSubstr == command) { - post("> " + command); - post(allOptionsStr); - } else { - if (semiColonIndex === -1) { - // No ; just replace the whole thing - textBox.value = longestStartSubstr; - } else { - // Multiple commands, so only replace after the last semicolon - textBox.value = textBox.value.slice(0, semiColonIndex + 1) + " " + longestStartSubstr; - } - - textBox.focus(); - } - } else { - if (longestStartSubstr == arg) { - //List all possible options - post("> " + command + " " + arg); - post(allOptionsStr); - } else { - if (semiColonIndex == -1) { - // No ; so just replace the whole thing - textBox.value = command + " " + longestStartSubstr; - } else { - // Multiple commands, so only replace after the last semiclon - textBox.value = textBox.value.slice(0, semiColonIndex + 1) + " " + command + " " + longestStartSubstr; - } - - textBox.focus(); - } - } - - } -} +}); let Terminal = { // Flags to determine whether the player is currently running a hack or an analyze @@ -418,20 +331,21 @@ let Terminal = { // Full Path of current directory // Excludes the trailing forward slash - currDir: "", + currDir: "/", resetTerminalInput: function() { + const dir = Terminal.currDir; if (FconfSettings.WRAP_INPUT) { document.getElementById("terminal-input-td").innerHTML = - "
[" + Player.getCurrentServer().hostname + " ~]" + "$
" + + `
[${Player.getCurrentServer().hostname} ~${dir}]$
` + '