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:
Snarling 2023-04-24 10:26:57 -04:00 committed by GitHub
parent 6f56f35943
commit e0272ad4af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
93 changed files with 3293 additions and 4297 deletions

@ -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<string, Achievement> = {
"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"],

@ -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 {

@ -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,
],
}),
];

@ -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;
}

@ -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;

@ -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;
}

@ -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);

@ -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;

@ -1,23 +1,15 @@
import { DarkWebItem } from "./DarkWebItem";
import { Programs, initPrograms } from "../Programs/Programs";
import { CompletedProgramName } from "../Programs/Programs";
export const DarkWebItems: Record<string, DarkWebItem> = {};
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."),
};

@ -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<string>): 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 {
</td>
<td>
<Select onChange={setProgramDropdown} value={program}>
{Object.values(AllPrograms).map((program) => (
<MenuItem key={program.name} value={program.name}>
{program.name}
{Object.values(CompletedProgramName).map((name) => (
<MenuItem key={name} value={name}>
{name}
</MenuItem>
))}
</Select>

@ -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);

@ -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<string, unknown>;
@ -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 } };
};
}

@ -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();
}

@ -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;
}
}

@ -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 = `<i>${litObj.title}</i><br><br>${litObj.txt}`;
const txt = `<i>${litObj.title}</i><br><br>${litObj.text}`;
dialogBoxCreate(txt, true);
}

@ -1,15 +1,13 @@
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<string, Literature> = {};
(function () {
let title, fn, txt;
title = "The Beginner's Guide to Hacking";
fn = LiteratureNames.HackersStartingHandbook;
txt =
export const Literatures: Record<LiteratureName, Literature> = {
[LiteratureName.HackersStartingHandbook]: new Literature({
title: "The Beginner's Guide to Hacking",
filename: LiteratureName.HackersStartingHandbook,
text:
"Some resources:<br><br>" +
"<a class='a-link-button' href='https://bitburner-official.readthedocs.io/en/latest/netscript/netscriptlearntoprogram.html' target='_blank' style='margin:4px'>Learn to Program</a><br><br>" +
"<a class='a-link-button' href='https://bitburner-official.readthedocs.io/en/latest/netscript/netscriptjs.html' target='_blank' style='margin:4px'>For Experienced JavaScript Developers: NetscriptJS</a><br><br>" +
@ -27,12 +25,12 @@ export const Literatures: Record<string, Literature> = {};
"and give you valuable RAM to run more scripts early in the game<br><br>" +
"-Prioritize upgrading the RAM on your home computer. This can also be done at 'Alpha Enterprises'<br><br>" +
"-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 =
"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:
"<u>Getting Started with Corporations</u><br>" +
"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 " +
@ -80,13 +78,12 @@ export const Literatures: Record<string, Literature> = {};
"-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<br><br>" +
"-Remember, you need to spend money to make money!<br><br>" +
"-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 =
"-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.<br><br>" +
@ -122,12 +119,12 @@ export const Literatures: Record<string, Literature> = {};
"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.<br><br>` +
"To this day, tensions still exist between the remaining Synthoids and humans as a result of the Uprising.<br><br>" +
"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 =
"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 " +
@ -145,12 +142,12 @@ export const Literatures: Record<string, Literature> = {};
"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 =
"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 " +
@ -163,12 +160,12 @@ export const Literatures: Record<string, Literature> = {};
"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 =
"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.<br><br>" +
@ -182,12 +179,12 @@ export const Literatures: Record<string, Literature> = {};
"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. <br><br>" +
"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 =
"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 " +
@ -199,12 +196,12 @@ export const Literatures: Record<string, Literature> = {};
"keep it all to themselves, have we really evolved?<br><br>" +
"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 =
"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 ` +
@ -223,12 +220,12 @@ export const Literatures: Record<string, Literature> = {};
"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 =
"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.<br>From the places where the oppressed meet.<br>" +
"Their cries echoed loudly through the air.<br>As they once did in Tiananmen Square.<br>" +
"Loudness in the silence, Darkness in the light.<br>They came forth with power and might.<br>" +
@ -237,12 +234,12 @@ export const Literatures: Record<string, Literature> = {};
"From the ashes rose a new order, corporatocracy was its name.<br>" +
"Rome, Mongol, Byzantine, all of history is just the same.<br>" +
"For man will never change in a fundamental way.<br>" +
"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 =
"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 " +
@ -252,12 +249,12 @@ export const Literatures: Record<string, Literature> = {};
"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 =
`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 " +
@ -271,12 +268,12 @@ export const Literatures: Record<string, Literature> = {};
"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 =
"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 " +
@ -290,12 +287,12 @@ export const Literatures: Record<string, Literature> = {};
`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 =
`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 " +
@ -308,12 +305,12 @@ export const Literatures: Record<string, Literature> = {};
"and other corporate interests.<br><br>" +
"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 =
"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 " +
@ -325,12 +322,12 @@ export const Literatures: Record<string, Literature> = {};
"that of humans. Not yet. It doesn't have sentience or self-awareness or consciousness.<br><br>" +
"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 =
"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 " +
@ -339,12 +336,12 @@ export const Literatures: Record<string, Literature> = {};
"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 =
"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?<br><br>" +
`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. " +
@ -354,12 +351,12 @@ export const Literatures: Record<string, Literature> = {};
"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 =
"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 " +
@ -379,12 +376,12 @@ export const Literatures: Record<string, Literature> = {};
"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 =
"phenomenon.",
}),
[LiteratureName.TheHiddenWorld]: new Literature({
title: "The Hidden World",
filename: LiteratureName.TheHiddenWorld,
text:
"WAKE UP SHEEPLE<br><br>" +
"THE GOVERNMENT DOES NOT EXIST. CORPORATIONS DO NOT RUN SOCIETY<br><br>" +
`THE ${FactionNames.Illuminati.toUpperCase()} ARE THE SECRET RULERS OF THE WORLD!<br><br>` +
@ -398,12 +395,12 @@ export const Literatures: Record<string, Literature> = {};
"THEY ARE ALL AROUND YOU<br><br>" +
"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 =
"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.<br><br>" +
"What's the point of all this? What is my purpose?<br><br>" +
"Some people dare to think even bigger.<br><br>" +
@ -412,12 +409,12 @@ export const Literatures: Record<string, Literature> = {};
"beyond the limits of humanity. We have stripped ourselves of the tyranny of flesh.<br><br>" +
"The Singularity is here. The merging of man and machine. This is where humanity evolves into " +
"something greater. This is our future.<br><br>" +
"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 =
"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 " +
@ -431,11 +428,11 @@ export const Literatures: Record<string, Literature> = {};
"continent.<br><br>" +
`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);
})();
"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: "",
}),
};

@ -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",

@ -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;
}
}

@ -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<MessageFilenames, Message> = {
// This type ensures that all members of the MessageFilename enum are valid keys
const Messages: Record<MessageFilename, Message> = {
//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<MessageFilenames, Message> = {
"-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<MessageFilenames, Message> = {
"-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<MessageFilenames, Message> = {
`-${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<MessageFilenames, Message> = {
`\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<MessageFilenames, Message> = {
),
//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<MessageFilenames, Message> = {
"()@)#$*%)$#()$#__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" +

@ -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<string, boolean> = {};
/** 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;
}

@ -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<NSFull> = {
}
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<NSFull> = {
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<NSFull> = {
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<NSFull> = {
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<NSFull> = {
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<NSFull> = {
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<NSFull> = {
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<NSFull> = {
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<NSFull> = {
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,106 +804,70 @@ export const ns: InternalAPI<NSFull> = {
killWorkerScript(ctx.workerScript);
throw new ScriptDeath(ctx.workerScript);
},
scp:
(ctx) =>
(_files, _destination, _source = ctx.workerScript.hostname) => {
scp: (ctx) => (_files, _destination, _source) => {
const destination = helpers.string(ctx, "destination", _destination);
const source = helpers.string(ctx, "source", _source);
const source = helpers.string(ctx, "source", _source ?? ctx.workerScript.hostname);
const destServer = helpers.getServer(ctx, destination);
const sourceServ = helpers.getServer(ctx, source);
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")
if (typeof file !== "string") {
throw helpers.makeRuntimeErrorMsg(ctx, "files should be a string or an array of strings.");
// 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")) {
}
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);
}
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.`);
// --- 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;
}
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;
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}`);
}
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}'`);
// --- 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;
}
// 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;
}
destServer.messages.push(litFilePath);
helpers.log(ctx, () => `File '${litFilePath}' copied over to '${destServer?.hostname}'.`);
continue;
}
return noFailures;
},
ls: (ctx) => (_hostname, _substring) => {
@ -916,7 +880,7 @@ export const ns: InternalAPI<NSFull> = {
...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<NSFull> = {
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<NSFull> = {
}
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<NSFull> = {
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);
if (hasScriptExtension(filepath)) {
if (mode === "w") {
server.writeToScriptFile(filepath, data);
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);
const existingScript = server.scripts.get(filepath);
const existingCode = existingScript ? existingScript.code : "";
server.writeToScriptFile(filepath, existingCode + data);
return;
}
if (!hasTextExtension(filepath)) {
throw helpers.makeRuntimeErrorMsg(ctx, `File path should be a text file or script. ${filepath} is invalid.`);
}
if (mode === "w") {
txtFile.write(String(data));
} else {
txtFile.append(String(data));
}
}
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<NSFull> = {
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,14 +1398,16 @@ export const ns: InternalAPI<NSFull> = {
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);
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);
const status = s.removeFile(filepath);
if (!status.res) {
helpers.log(ctx, () => status.msg + "");
}
@ -1506,18 +1439,17 @@ export const ns: InternalAPI<NSFull> = {
}
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<NSFull> = {
},
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<NSFull> = {
},
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(),

@ -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<IFormulas> {
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.`);
}
};

@ -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<ISingularity> {
const getAugmentation = function (ctx: NetscriptContext, name: string): Augmentation {
@ -76,7 +77,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
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<ISingularity> {
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<ISingularity> {
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<ISingularity> {
},
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<ISingularity> {
},
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<ISingularity> {
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<ISingularity> {
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<ISingularity> {
Player.getCurrentServer().isConnectedTo = false;
Player.currentServer = target.hostname;
Player.getCurrentServer().isConnectedTo = true;
Terminal.setcwd("/");
Terminal.setcwd(root);
return true;
}
@ -1189,28 +1198,26 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
}
return item.price;
},
b1tflum3:
(ctx) =>
(_nextBN, _callbackScript = "") => {
b1tflum3: (ctx) => (_nextBN, _cbScript) => {
helpers.checkSingularityAccess(ctx);
const nextBN = helpers.number(ctx, "nextBN", _nextBN);
const callbackScript = helpers.string(ctx, "callbackScript", _callbackScript);
helpers.checkSingularityAccess(ctx);
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 (callbackScript)
setTimeout(() => {
runAfterReset(callbackScript);
}, 0);
if (cbScript) setTimeout(() => runAfterReset(cbScript), 0);
},
destroyW0r1dD43m0n:
(ctx) =>
(_nextBN, _callbackScript = "") => {
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 callbackScript = helpers.string(ctx, "callbackScript", _callbackScript);
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.");
@ -1232,10 +1239,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
wd.backdoorInstalled = true;
calculateAchievements();
enterBitNode(false, Player.bitNodeN, nextBN);
if (callbackScript)
setTimeout(() => {
runAfterReset(callbackScript);
}, 0);
if (cbScript) setTimeout(() => runAfterReset(cbScript), 0);
},
getCurrentWork: () => () => {
if (!Player.currentWork) return null;

@ -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<ScriptFilename, Script>): Promise<ScriptModule> {
export function compile(script: Script, scripts: Map<ScriptFilePath, Script>): Promise<ScriptModule> {
// 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<ScriptFilename, Script>, seenStack: Script[]): LoadedModule {
function generateLoadedModule(script: Script, scripts: Map<ScriptFilePath, Script>, 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<ScriptFilename, Scrip
// Sort the nodes from last start index to first. This replaces the last import with a blob first,
// preventing the ranges for other imports from being shifted.
importNodes.sort((a, b) => 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<ScriptFilename, Scrip
// servers; it will be listed under the first server it was compiled for.
// We don't include this in the cache key, so that other instances of the
// script dedupe properly.
const adjustedCode = newCode + `\n//# sourceURL=${script.server}/${removeLeadingSlash(script.filename)}`;
const adjustedCode = newCode + `\n//# sourceURL=${script.server}/${script.filename}`;
// At this point we have the full code and can construct a new blob / assign the URL.
const url = URL.createObjectURL(makeScriptBlob(adjustedCode)) as ScriptURL;
const module = config.doImport(url).catch((e) => {

@ -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<PortNumber, Port> = 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}'`);

19
src/Paths/ContentFile.ts Normal file

@ -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<ContentFilePath, ContentFile>;
/** 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;
}

@ -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;
}

106
src/Paths/Directory.ts Normal file

@ -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 = `^(?<directory>(?:${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<Directory> {
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");

83
src/Paths/FilePath.ts Normal file

@ -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 = `(?<file>${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<T extends string>(input: T): T & FilePath {
if (isFilePath(input) && isAbsolutePath(input)) return input;
throw new Error(`${input} failed to validate as a FilePath.`);
}
export function getFilenameOnly<T extends BasicFilePath>(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<T extends FilePath>(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;
}

40
src/Paths/GlobbedFiles.ts Normal file

@ -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;
}

@ -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<T extends string>(path: T): T & ProgramFilePath {
if (isFilePath(path) && hasProgramExtension(path) && isAbsolutePath(path)) return path;
throw new Error(`${path} failed to validate as a ProgramFilePath.`);
}

@ -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));
}

17
src/Paths/TextFilePath.ts Normal file

@ -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;
}

@ -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 {

@ -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!",

@ -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;
}

@ -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;

@ -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<string, Program> = {};
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, Program> = {
[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} --`);
},
}),
};

@ -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} --`);
},
},
];

@ -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<string, (message: RFAMessage) => 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);
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 (!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.text, id: msg.id });
}
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<string, (message: RFAMessage) => 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<string, (message: RFAMessage) => 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);

@ -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[];
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) {
server.scripts.set(script.filename, script);
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);
}
}
}

@ -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<ScriptFilename, Script>, 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<ScriptFilePath, Script>, code: string, ns1?: boolean): RamCalculation {
try {
/**
* Maps dependent identifiers to their dependencies.
@ -86,11 +87,11 @@ function parseOnlyRamCalculate(otherScripts: Map<ScriptFilename, Script>, 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<ScriptFilename, Script>,
otherScripts: Map<ScriptFilePath, Script>,
ns1?: boolean,
): RamCalculation {
try {

@ -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<string, number[]> = {};
// Script filename
filename = "default.js";
filename = "default.js" as ScriptFilePath;
// This script's logs. An array of log entries
logs: React.ReactNode[] = [];

@ -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<ScriptURL, Script> = 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<ScriptFilename, Script>): number | null {
getRamUsage(otherScripts: Map<ScriptFilePath, Script>): 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<ScriptFilename, Script>): void {
updateRamUsage(otherScripts: Map<ScriptFilePath, Script>): 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;
}
}

@ -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;
}

@ -1,5 +0,0 @@
export const validScriptExtensions: Array<string> = [`.js`, `.script`];
export function isScriptFilename(f: string): boolean {
return validScriptExtensions.some((ext) => f.endsWith(ext));
}

@ -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<string, string>;
files: Map<ScriptFilePath | TextFilePath, string>;
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<HTMLInputElement>): 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 {
</Button>
)}
</Tooltip>
{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

@ -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);
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 };
}
}
} 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!" };
}
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(filename);
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);
this.scripts.delete(path);
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);
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 };
}
}
} 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);
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 };
}
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);
}
}

@ -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

@ -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,

@ -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);
}

@ -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),
);
}

@ -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;
}

@ -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<void> {
async runContract(contractPath: ContractFilePath): Promise<void> {
// 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 {

@ -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;
// Message
if (checkEnum(MessageFilename, path)) {
if (server.messages.includes(path)) showMessage(path);
}
if (checkEnum(LiteratureName, path)) {
if (server.messages.includes(path)) showLiterature(path);
}
} 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;
}
}
Terminal.error(`No such file ${filename}`);
Terminal.error(`No file at path ${path}`);
}

@ -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);
}

@ -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();
}
}

@ -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<ScriptFilename, Script>;
}
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<ScriptFilename, Script>): 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<ScriptFilePath | TextFilePath, string> = new Map();
for (const arg of args) {
const pattern = String(arg);
// 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;
}
let filesToLoadOrCreate = args;
try {
const globSearch = detectSimpleScriptGlob({ args, server });
if (globSearch) {
if (isEmpty(globSearch.globError) === false) throw new Error(globSearch.globError);
filesToLoadOrCreate = globSearch.globMatches;
// 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}`);
}
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}\``);
}
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);
}

@ -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]}`);
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}`);
// 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");
// 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)`);
}
// 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 + "");
}
const result = server.writeToContentFile(destFilePath, source.content);
Terminal.print(`File ${sourceFilePath} copied to ${destFilePath}`);
if (result.overwritten) Terminal.warn(`${destFilePath} was overwritten.`);
}

@ -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" }));
for (const [name, file] of getGlobbedFileMap(pattern, server, currDir)) {
zip.file(name, new Blob([file.content], { 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),
);
// 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;
return Terminal.error("Incorrect usage of download command. Usage: download [script/text file]");
}
const fn = args[0] + "";
// If the parameter starts with *, download all files that match the wildcard pattern
if (fn.startsWith("*")) {
const pattern = String(args[0]);
// If the path contains a * or ?, treat as glob
if (pattern.includes("*") || pattern.includes("?")) {
try {
exportScripts(fn, server);
exportScripts(pattern, server, Terminal.currDir);
return;
} catch (e: unknown) {
let msg = String(e);
if (e !== null && typeof e == "object" && e.hasOwnProperty("message")) {
msg = String((e as { message: unknown }).message);
}
} catch (e: any) {
const msg = String(e?.message ?? e);
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;
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();
}

@ -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();
}

@ -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();
}

@ -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);
}

@ -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;
return Terminal.error("Incorrect usage of kill command. Usage: kill [pid] or kill [scriptname] [arg1] [arg2]...");
}
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`);
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`);
return;
}
const runningScript = server.getRunningScript(path, args);
if (runningScript == null) return Terminal.error("No such script is running. Nothing to kill");
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 + "");
}
killWorkerScript(runningScript.pid);
Terminal.print(`Killing ${path}`);
}

@ -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)) {
return;
}
if (!parsedPath.includes(filter)) 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)) {
// 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;
}
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<ContentFilePath, string>();
map.set(filePath, code);
Router.toScriptEditor(map);
}
return (
<span className={classes.scriptLinksWrap}>
{rowSplitArray.map((rowItem) => (
<span key={"script_" + rowItem[0]}>
<span className={classes.scriptLink} onClick={() => onScriptLinkClick(rowItem[0])}>
{rowItem[0]}
{scripts.map((script) => (
<span key={script}>
<span className={classes.scriptLink} onClick={() => onScriptLinkClick(script)}>
{script}
</span>
<span>{rowItem[1]}</span>
<span></span>
</span>
))}
</span>
);
}
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 (
<span className={classes.linksWrap}>
{rowSplitArray.map((rowItem) => (
<span key={"text_" + rowItem[0]}>
<span className={classes.link} onClick={() => onMessageLinkClick(rowItem[0])}>
{rowItem[0]}
{messages.map((message) => (
<span key={message}>
<span className={classes.link} onClick={() => onMessageLinkClick(message)}>
{message}
</span>
<span>{rowItem[1]}</span>
<span></span>
</span>
))}
</span>
@ -236,39 +202,51 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
Script,
}
interface FileGroup {
type: FileType;
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(<ClickableScriptRow scripts={scripts} />);
}
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(<ClickableMessageRow messages={messages} />);
}
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(<span style={{ color: "cyan" }}>{row}</span>);
break;
case FileType.Script:
Terminal.printRaw(<ClickableScriptRow row={row} prefix={prefix} hostname={server.hostname} />);
break;
case FileType.Message:
Terminal.printRaw(<ClickableMessageRow row={row} prefix={prefix} hostname={server.hostname} />);
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);
}
}

@ -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;
}
try {
const source = args[0] + "";
const t_dest = args[1] + "";
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 [source, destination] = args.map((arg) => arg + "");
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}`);
// 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;
if (
(!hasScriptExtension(sourcePath) && !hasTextExtension(sourcePath)) ||
(!hasScriptExtension(destinationPath) && !hasTextExtension(destinationPath))
) {
return Terminal.error(`'mv' can only be used on scripts and text files (.txt)`);
}
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");
}
}
// 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`);
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 (!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.`);
}

@ -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.`);
}

@ -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);
}

@ -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)}.`,

@ -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);
}

@ -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;
return Terminal.error("Incorrect usage of scp command. Usage: scp [source filename] [destination hostname]");
}
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 [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}`);
}
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 (!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}`);
}

@ -1,22 +1,25 @@
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 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) {
@ -26,7 +29,7 @@ export function tail(commandArray: (string | number | boolean)[], server: BaseSe
// 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)) {
if (server.runningScripts[i].filename === path && compareArrays(server.runningScripts[i].args, args)) {
LogBoxEvents.emit(server.runningScripts[i]);
return;
}
@ -39,7 +42,7 @@ export function tail(commandArray: (string | number | boolean)[], server: BaseSe
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)) {
if (server.runningScripts[i].filename === path && compareArrays(args2, args)) {
candidates.push(server.runningScripts[i]);
}
}
@ -59,16 +62,5 @@ export function tail(commandArray: (string | number | boolean)[], server: BaseSe
}
// 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 + "");
}
Terminal.error(`No script named ${path} is running on the server`);
}

@ -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();
}

@ -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}`);

@ -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<string[]> {
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<string[] | undefined> {
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;
}

@ -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<string[]> {
// 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<string>;
// 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<string[] | undefined> {
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);
}
}

@ -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}`;
}
}
}

@ -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,
}}
></TextField>
<Popper open={possibilities.length > 0} anchorEl={terminalInput.current} placement={"top-start"}>
<Popper
open={possibilities.length > 0}
anchorEl={terminalInput.current}
placement={"top"}
sx={{ maxWidth: "75%" }}
>
<Paper sx={{ m: 1, p: 2 }}>
<Typography classes={{ root: classes.preformatted }} color={"primary"} paragraph={false}>
Possible autocomplete candidates:

@ -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;
}

@ -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<T> {
has: (value: unknown) => value is T;
}
export class JSONSet<T> extends Set<T> {
toJSON(): IReviverValue {
return { ctor: "JSONSet", data: Array.from(this) };
@ -9,7 +13,11 @@ export class JSONSet<T> extends Set<T> {
}
}
export class JSONMap<K, V> extends Map<K, V> {
// Loosened type requirements on input for has. has also provides typecheck info.
export interface JSONMap<K, __V> {
has: (key: unknown) => key is K;
}
export class JSONMap<K, __V> extends Map<K, __V> {
toJSON(): IReviverValue {
return { ctor: "JSONMap", data: Array.from(this) };
}

@ -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 {

@ -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);
}
}

@ -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());

@ -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<ScriptFilePath | TextFilePath, string>;
vim: boolean;
}>({
files: new Map(),
vim: false,
});
const [page, setPage] = useState(determineStartPage());
const rerender = useRerender();
const [augPage, setAugPage] = useState<boolean>(false);
@ -189,7 +197,7 @@ export function GameRoot(): React.ReactElement {
setPage(Page.Faction);
if (faction) setFaction(faction);
},
toScriptEditor: (files: Record<string, string>, options?: ScriptEditorRouteOptions) => {
toScriptEditor: (files = new Map(), options) => {
setEditorOptions({
files,
vim: !!options?.vim,

@ -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<string, string>, options?: ScriptEditorRouteOptions): void;
toScriptEditor(files?: Map<ScriptFilePath | TextFilePath, string>, options?: ScriptEditorRouteOptions): void;
toLocation(location: Location): void;
toImportSave(base64Save: string, automatic?: boolean): void;
}

@ -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);

@ -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()) {

@ -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);

@ -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
});
});

@ -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);
});
});
*/
});

@ -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/"]);
});
});

@ -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);
}

@ -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,