mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-27 11:27:04 +02:00
FILES: Path rework & typesafety (#479)
* Added new types for various file paths, all in the Paths folder. * TypeSafety and other helper functions related to these types * Added basic globbing support with * and ?. Currently only implemented for Script/Text, on nano and download terminal commands * Enforcing the new types throughout the codebase, plus whatever rewrites happened along the way * Server.textFiles is now a map * TextFile no longer uses a fn property, now it is filename * Added a shared ContentFile interface for shared functionality between TextFile and Script. * related to ContentFile change above, the player is now allowed to move a text file to a script file and vice versa. * File paths no longer conditionally start with slashes, and all directory names other than root have ending slashes. The player is still able to provide paths starting with / but this now indicates that the player is specifying an absolute path instead of one relative to root. * Singularized the MessageFilename and LiteratureName enums * Because they now only accept correct types, server.writeToXFile functions now always succeed (the only reasons they could fail before were invalid filepath). * Fix several issues with tab completion, which included pretty much a complete rewrite * Changed the autocomplete display options so there's less chance it clips outside the display area. * Turned CompletedProgramName into an enum. * Got rid of programsMetadata, and programs and DarkWebItems are now initialized immediately instead of relying on initializers called from the engine. * For any executable (program, cct, or script file) pathing can be used directly to execute without using the run command (previously the command had to start with ./ and it wasn't actually using pathing).
This commit is contained in:
+75
-101
@@ -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<ScriptFilename, Script> = new JSONMap();
|
||||
scripts: JSONMap<ScriptFilePath, Script> = 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<TextFilePath, TextFile> = 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user