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 { HacknetServer } from "../Hacknet/HacknetServer";
import { CityName } from "../Enums"; import { CityName } from "../Enums";
import { Player } from "@player"; import { Player } from "@player";
import { Programs } from "../Programs/Programs"; import { CompletedProgramName } from "../Programs/Programs";
import { GetAllServers, GetServer } from "../Server/AllServers"; import { GetAllServers, GetServer } from "../Server/AllServers";
import { SpecialServers } from "../Server/data/SpecialServers"; import { SpecialServers } from "../Server/data/SpecialServers";
import { Server } from "../Server/Server"; import { Server } from "../Server/Server";
@ -126,33 +126,33 @@ export const achievements: Record<string, Achievement> = {
"BRUTESSH.EXE": { "BRUTESSH.EXE": {
...achievementData["BRUTESSH.EXE"], ...achievementData["BRUTESSH.EXE"],
Icon: "p0", Icon: "p0",
Condition: () => Player.getHomeComputer().programs.includes(Programs.BruteSSHProgram.name), Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.bruteSsh),
}, },
"FTPCRACK.EXE": { "FTPCRACK.EXE": {
...achievementData["FTPCRACK.EXE"], ...achievementData["FTPCRACK.EXE"],
Icon: "p1", Icon: "p1",
Condition: () => Player.getHomeComputer().programs.includes(Programs.FTPCrackProgram.name), Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.ftpCrack),
}, },
//----------------------------------------------------- //-----------------------------------------------------
"RELAYSMTP.EXE": { "RELAYSMTP.EXE": {
...achievementData["RELAYSMTP.EXE"], ...achievementData["RELAYSMTP.EXE"],
Icon: "p2", Icon: "p2",
Condition: () => Player.getHomeComputer().programs.includes(Programs.RelaySMTPProgram.name), Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.relaySmtp),
}, },
"HTTPWORM.EXE": { "HTTPWORM.EXE": {
...achievementData["HTTPWORM.EXE"], ...achievementData["HTTPWORM.EXE"],
Icon: "p3", Icon: "p3",
Condition: () => Player.getHomeComputer().programs.includes(Programs.HTTPWormProgram.name), Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.httpWorm),
}, },
"SQLINJECT.EXE": { "SQLINJECT.EXE": {
...achievementData["SQLINJECT.EXE"], ...achievementData["SQLINJECT.EXE"],
Icon: "p4", Icon: "p4",
Condition: () => Player.getHomeComputer().programs.includes(Programs.SQLInjectProgram.name), Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.sqlInject),
}, },
"FORMULAS.EXE": { "FORMULAS.EXE": {
...achievementData["FORMULAS.EXE"], ...achievementData["FORMULAS.EXE"],
Icon: "formulas", Icon: "formulas",
Condition: () => Player.getHomeComputer().programs.includes(Programs.Formulas.name), Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.formulas),
}, },
"SF1.1": { "SF1.1": {
...achievementData["SF1.1"], ...achievementData["SF1.1"],

@ -1,6 +1,7 @@
// Class definition for a single Augmentation object // Class definition for a single Augmentation object
import * as React from "react"; import * as React from "react";
import type { CompletedProgramName } from "src/Programs/Programs";
import { Faction } from "../Faction/Faction"; import { Faction } from "../Faction/Faction";
import { Factions } from "../Faction/Factions"; import { Factions } from "../Faction/Factions";
import { formatPercent } from "../ui/formatNumber"; import { formatPercent } from "../ui/formatNumber";
@ -64,7 +65,7 @@ export interface IConstructorParams {
bladeburner_success_chance?: number; bladeburner_success_chance?: number;
startingMoney?: number; startingMoney?: number;
programs?: string[]; programs?: CompletedProgramName[];
} }
function generateStatsDescription(mults: Multipliers, programs?: string[], startingMoney?: number): JSX.Element { function generateStatsDescription(mults: Multipliers, programs?: string[], startingMoney?: number): JSX.Element {

@ -1,7 +1,7 @@
import { Augmentation, IConstructorParams } from "../Augmentation"; import { Augmentation, IConstructorParams } from "../Augmentation";
import { AugmentationNames } from "./AugmentationNames"; import { AugmentationNames } from "./AugmentationNames";
import { Player } from "@player"; import { Player } from "@player";
import { Programs } from "../../Programs/Programs"; import { CompletedProgramName } from "../../Programs/Programs";
import { WHRNG } from "../../Casino/RNG"; import { WHRNG } from "../../Casino/RNG";
import React from "react"; import React from "react";
import { FactionNames } from "../../Faction/data/FactionNames"; import { FactionNames } from "../../Faction/data/FactionNames";
@ -1442,7 +1442,7 @@ export const initGeneralAugmentations = (): Augmentation[] => [
hacking_exp: 1.2, hacking_exp: 1.2,
hacking_chance: 1.1, hacking_chance: 1.1,
hacking_speed: 1.05, hacking_speed: 1.05,
programs: [Programs.FTPCrackProgram.name, Programs.RelaySMTPProgram.name], programs: [CompletedProgramName.ftpCrack, CompletedProgramName.relaySmtp],
factions: [FactionNames.BitRunners], factions: [FactionNames.BitRunners],
}), }),
new Augmentation({ new Augmentation({
@ -1495,7 +1495,7 @@ export const initGeneralAugmentations = (): Augmentation[] => [
</> </>
), ),
startingMoney: 1e6, startingMoney: 1e6,
programs: [Programs.BruteSSHProgram.name], programs: [CompletedProgramName.bruteSsh],
factions: [FactionNames.Sector12], factions: [FactionNames.Sector12],
}), }),
new Augmentation({ new Augmentation({
@ -1528,7 +1528,7 @@ export const initGeneralAugmentations = (): Augmentation[] => [
company_rep: 1.0777, company_rep: 1.0777,
crime_success: 1.0777, crime_success: 1.0777,
crime_money: 1.0777, crime_money: 1.0777,
programs: [Programs.DeepscanV1.name, Programs.AutoLink.name], programs: [CompletedProgramName.deepScan1, CompletedProgramName.autoLink],
factions: [FactionNames.Aevum], factions: [FactionNames.Aevum],
}), }),
new Augmentation({ new Augmentation({
@ -2104,16 +2104,16 @@ export const initChurchOfTheMachineGodAugmentations = (): Augmentation[] => [
startingMoney: 1e12, startingMoney: 1e12,
programs: [ programs: [
Programs.BruteSSHProgram.name, CompletedProgramName.bruteSsh,
Programs.FTPCrackProgram.name, CompletedProgramName.ftpCrack,
Programs.RelaySMTPProgram.name, CompletedProgramName.relaySmtp,
Programs.HTTPWormProgram.name, CompletedProgramName.httpWorm,
Programs.SQLInjectProgram.name, CompletedProgramName.sqlInject,
Programs.DeepscanV1.name, CompletedProgramName.deepScan1,
Programs.DeepscanV2.name, CompletedProgramName.deepScan2,
Programs.ServerProfiler.name, CompletedProgramName.serverProfiler,
Programs.AutoLink.name, CompletedProgramName.autoLink,
Programs.Formulas.name, CompletedProgramName.formulas,
], ],
}), }),
]; ];

@ -12,6 +12,7 @@ import { Server } from "./Server/Server";
import { BaseServer } from "./Server/BaseServer"; import { BaseServer } from "./Server/BaseServer";
import { getRandomInt } from "./utils/helpers/getRandomInt"; import { getRandomInt } from "./utils/helpers/getRandomInt";
import { ContractFilePath, resolveContractFilePath } from "./Paths/ContractFilePath";
export function generateRandomContract(): void { export function generateRandomContract(): void {
// First select a random problem type // First select a random problem type
@ -57,7 +58,7 @@ export const generateDummyContract = (problemType: string): void => {
interface IGenerateContractParams { interface IGenerateContractParams {
problemType?: string; problemType?: string;
server?: string; server?: string;
fn?: string; fn?: ContractFilePath;
} }
export function generateContract(params: IGenerateContractParams): void { export function generateContract(params: IGenerateContractParams): void {
@ -84,15 +85,9 @@ export function generateContract(params: IGenerateContractParams): void {
server = getRandomServer(); server = getRandomServer();
} }
// Filename const filename = params.fn ? params.fn : getRandomFilename(server, reward);
let fn;
if (params.fn != null) {
fn = params.fn;
} else {
fn = getRandomFilename(server, reward);
}
const contract = new CodingContract(fn, problemType, reward); const contract = new CodingContract(filename, problemType, reward);
server.addContract(contract); server.addContract(contract);
} }
@ -185,7 +180,10 @@ function getRandomServer(): BaseServer {
return randServer; 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)}`; let contractFn = `contract-${getRandomInt(0, 1e6)}`;
for (let i = 0; i < 1000; ++i) { for (let i = 0; i < 1000; ++i) {
@ -203,6 +201,8 @@ function getRandomFilename(server: BaseServer, reward: ICodingContractReward = {
// Only alphanumeric characters in the reward name. // Only alphanumeric characters in the reward name.
contractFn += `-${reward.name.replace(/[^a-zA-Z0-9]/g, "")}`; contractFn += `-${reward.name.replace(/[^a-zA-Z0-9]/g, "")}`;
} }
contractFn += ".cct";
return contractFn; 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 { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "./utils/JSONReviver";
import { CodingContractEvent } from "./ui/React/CodingContractModal"; 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 */ /* tslint:disable:no-magic-numbers completed-docs max-classes-per-file no-console */
@ -89,7 +90,7 @@ export class CodingContract {
data: unknown; data: unknown;
/* Contract's filename */ /* Contract's filename */
fn: string; fn: ContractFilePath;
/* Describes the reward given if this Contract is solved. The reward is actually /* Describes the reward given if this Contract is solved. The reward is actually
processed outside of this file */ processed outside of this file */
@ -101,17 +102,14 @@ export class CodingContract {
/* String representing the contract's type. Must match type in ContractTypes */ /* String representing the contract's type. Must match type in ContractTypes */
type: string; type: string;
constructor(fn = "", type = "Find Largest Prime Factor", reward: ICodingContractReward | null = null) { constructor(fn = "default.cct", type = "Find Largest Prime Factor", reward: ICodingContractReward | null = null) {
this.fn = fn; const path = resolveContractFilePath(fn);
if (!this.fn.endsWith(".cct")) { if (!path) throw new Error(`Bad file path while creating a coding contract: ${fn}`);
this.fn += ".cct"; if (!CodingContractTypes[type]) {
}
// tslint:disable-next-line
if (CodingContractTypes[type] == null) {
throw new Error(`Error: invalid contract type: ${type} please contact developer`); throw new Error(`Error: invalid contract type: ${type} please contact developer`);
} }
this.fn = path;
this.type = type; this.type = type;
this.data = CodingContractTypes[type].generate(); this.data = CodingContractTypes[type].generate();
this.reward = reward; this.reward = reward;

@ -6,12 +6,11 @@ import { Industry } from "./Industry";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { showLiterature } from "../Literature/LiteratureHelpers"; import { showLiterature } from "../Literature/LiteratureHelpers";
import { LiteratureNames } from "../Literature/data/LiteratureNames"; import { LiteratureName } from "../Literature/data/LiteratureNames";
import { Player } from "@player"; import { Player } from "@player";
import { dialogBoxCreate } from "../ui/React/DialogBox"; import { dialogBoxCreate } from "../ui/React/DialogBox";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver"; import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { isString } from "../utils/helpers/isString";
import { CityName } from "../Enums"; import { CityName } from "../Enums";
import { CorpStateName } from "@nsdefs"; import { CorpStateName } from "@nsdefs";
import { calculateUpgradeCost } from "./helpers"; import { calculateUpgradeCost } from "./helpers";
@ -441,19 +440,9 @@ export class Corporation {
getStarterGuide(): void { getStarterGuide(): void {
// Check if player already has Corporation Handbook // Check if player already has Corporation Handbook
const homeComp = Player.getHomeComputer(); const homeComp = Player.getHomeComputer();
let hasHandbook = false; const handbook = LiteratureName.CorporationManagementHandbook;
const handbookFn = LiteratureNames.CorporationManagementHandbook; if (!homeComp.messages.includes(handbook)) homeComp.messages.push(handbook);
for (let i = 0; i < homeComp.messages.length; ++i) { showLiterature(handbook);
if (isString(homeComp.messages[i]) && homeComp.messages[i] === handbookFn) {
hasHandbook = true;
break;
}
}
if (!hasHandbook) {
homeComp.messages.push(handbookFn);
}
showLiterature(handbookFn);
return; return;
} }

@ -22,7 +22,7 @@ export function checkIfConnectedToDarkweb(): void {
} }
export function listAllDarkwebItems(): 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 item = DarkWebItems[key];
const cost = Player.getHomeComputer().programs.includes(item.program) ? ( 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 // find the program that matches, if any
let item: DarkWebItem | null = null; 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]; const i = DarkWebItems[key];
if (i.program.toLowerCase() == itemName) { if (i.program.toLowerCase() == itemName) {
item = i; item = i;
@ -88,7 +88,7 @@ export function buyAllDarkwebItems(): void {
const itemsToBuy: DarkWebItem[] = []; const itemsToBuy: DarkWebItem[] = [];
let cost = 0; 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]; const item = DarkWebItems[key];
if (!Player.hasProgram(item.program)) { if (!Player.hasProgram(item.program)) {
itemsToBuy.push(item); itemsToBuy.push(item);

@ -1,9 +1,11 @@
import type { CompletedProgramName } from "../Programs/Programs";
export class DarkWebItem { export class DarkWebItem {
program: string; program: CompletedProgramName;
price: number; price: number;
description: string; description: string;
constructor(program: string, price: number, description: string) { constructor(program: CompletedProgramName, price: number, description: string) {
this.program = program; this.program = program;
this.price = price; this.price = price;
this.description = description; this.description = description;

@ -1,23 +1,15 @@
import { DarkWebItem } from "./DarkWebItem"; import { DarkWebItem } from "./DarkWebItem";
import { Programs, initPrograms } from "../Programs/Programs"; import { CompletedProgramName } from "../Programs/Programs";
export const DarkWebItems: Record<string, DarkWebItem> = {}; export const DarkWebItems = {
export function initDarkWebItems() { BruteSSHProgram: new DarkWebItem(CompletedProgramName.bruteSsh, 500e3, "Opens up SSH Ports."),
initPrograms(); FTPCrackProgram: new DarkWebItem(CompletedProgramName.ftpCrack, 1500e3, "Opens up FTP Ports."),
Object.assign(DarkWebItems, { RelaySMTPProgram: new DarkWebItem(CompletedProgramName.relaySmtp, 5e6, "Opens up SMTP Ports."),
BruteSSHProgram: new DarkWebItem(Programs.BruteSSHProgram.name, 500e3, "Opens up SSH Ports."), HTTPWormProgram: new DarkWebItem(CompletedProgramName.httpWorm, 30e6, "Opens up HTTP Ports."),
FTPCrackProgram: new DarkWebItem(Programs.FTPCrackProgram.name, 1500e3, "Opens up FTP Ports."), SQLInjectProgram: new DarkWebItem(CompletedProgramName.sqlInject, 250e6, "Opens up SQL Ports."),
RelaySMTPProgram: new DarkWebItem(Programs.RelaySMTPProgram.name, 5e6, "Opens up SMTP Ports."), ServerProfiler: new DarkWebItem(CompletedProgramName.serverProfiler, 500e3, "Displays detailed server information."),
HTTPWormProgram: new DarkWebItem(Programs.HTTPWormProgram.name, 30e6, "Opens up HTTP Ports."), DeepscanV1: new DarkWebItem(CompletedProgramName.deepScan1, 500000, "Enables 'scan-analyze' with a depth up to 5."),
SQLInjectProgram: new DarkWebItem(Programs.SQLInjectProgram.name, 250e6, "Opens up SQL Ports."), DeepscanV2: new DarkWebItem(CompletedProgramName.deepScan2, 25e6, "Enables 'scan-analyze' with a depth up to 10."),
ServerProfiler: new DarkWebItem( AutolinkProgram: new DarkWebItem(CompletedProgramName.autoLink, 1e6, "Enables direct connect via 'scan-analyze'."),
Programs.ServerProfiler.name, FormulasProgram: new DarkWebItem(CompletedProgramName.formulas, 5e9, "Unlock access to the formulas API."),
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."),
});
}

@ -9,25 +9,21 @@ import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Select, { SelectChangeEvent } from "@mui/material/Select"; import Select, { SelectChangeEvent } from "@mui/material/Select";
import { Player } from "@player"; import { Player } from "@player";
import { Programs as AllPrograms } from "../../Programs/Programs";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import { CompletedProgramName } from "../../Programs/Programs";
export function Programs(): React.ReactElement { export function Programs(): React.ReactElement {
const [program, setProgram] = useState("NUKE.exe"); const [program, setProgram] = useState(CompletedProgramName.bruteSsh);
function setProgramDropdown(event: SelectChangeEvent<string>): void { function setProgramDropdown(event: SelectChangeEvent<string>): void {
setProgram(event.target.value); setProgram(event.target.value as CompletedProgramName);
} }
function addProgram(): void { function addProgram(): void {
if (!Player.hasProgram(program)) { if (!Player.hasProgram(program)) Player.getHomeComputer().programs.push(program);
Player.getHomeComputer().programs.push(program);
}
} }
function addAllPrograms(): void { function addAllPrograms(): void {
for (const i of Object.keys(AllPrograms)) { for (const name of Object.values(CompletedProgramName)) {
if (!Player.hasProgram(AllPrograms[i].name)) { if (!Player.hasProgram(name)) Player.getHomeComputer().programs.push(name);
Player.getHomeComputer().programs.push(AllPrograms[i].name);
}
} }
} }
@ -45,9 +41,9 @@ export function Programs(): React.ReactElement {
</td> </td>
<td> <td>
<Select onChange={setProgramDropdown} value={program}> <Select onChange={setProgramDropdown} value={program}>
{Object.values(AllPrograms).map((program) => ( {Object.values(CompletedProgramName).map((name) => (
<MenuItem key={program.name} value={program.name}> <MenuItem key={name} value={name}>
{program.name} {name}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>

@ -16,41 +16,24 @@ import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails"; import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { ServerName } from "../Types/strings"; import { ServerName } from "../Types/strings";
import { allContentFiles } from "../Paths/ContentFile";
interface IServerProps { interface File {
hostname: ServerName; name: string;
size: number;
} }
function ServerAccordion(props: IServerProps): React.ReactElement { function ServerAccordion(props: { hostname: ServerName }): React.ReactElement {
const server = GetServer(props.hostname); const server = GetServer(props.hostname);
if (server === null) throw new Error(`server '${props.hostname}' should not be null`); if (server === null) throw new Error(`server '${props.hostname}' should not be null`);
let totalSize = 0; 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[] = []; const files: File[] = [];
for (const [path, file] of allContentFiles(server)) {
for (const f of server.scripts.values()) { totalSize += file.content.length;
files.push({ name: f.filename, size: f.code.length }); files.push({ name: path, size: file.content.length });
} }
for (const f of server.textFiles) { if (totalSize === 0) return <></>;
files.push({ name: f.fn, size: f.text.length });
}
files.sort((a: File, b: File): number => b.size - a.size); files.sort((a: File, b: File): number => b.size - a.size);

@ -1,6 +1,5 @@
import { Player } from "@player"; import { Player } from "@player";
import { Router } from "./ui/GameRoot"; import { Router } from "./ui/GameRoot";
import { removeLeadingSlash } from "./Terminal/DirectoryHelpers";
import { Terminal } from "./Terminal"; import { Terminal } from "./Terminal";
import { SnackbarEvents, ToastVariant } from "./ui/React/Snackbar"; import { SnackbarEvents, ToastVariant } from "./ui/React/Snackbar";
import { IReturnStatus } from "./types"; import { IReturnStatus } from "./types";
@ -11,6 +10,8 @@ import { exportScripts } from "./Terminal/commands/download";
import { CONSTANTS } from "./Constants"; import { CONSTANTS } from "./Constants";
import { hash } from "./hash/hash"; import { hash } from "./hash/hash";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import { resolveFilePath } from "./Paths/FilePath";
import { hasScriptExtension } from "./Paths/ScriptFilePath";
interface IReturnWebStatus extends IReturnStatus { interface IReturnWebStatus extends IReturnStatus {
data?: Record<string, unknown>; data?: Record<string, unknown>;
@ -55,23 +56,9 @@ export function initElectron(): void {
} }
function initWebserver(): 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 { document.getFiles = function (): IReturnWebStatus {
const home = GetServer("home"); const home = GetServer("home");
if (home === null) { if (home === null) return { res: false, msg: "Home server does not exist." };
return {
res: false,
msg: "Home server does not exist.",
};
}
return { return {
res: true, res: true,
data: { data: {
@ -85,40 +72,28 @@ function initWebserver(): void {
}; };
document.deleteFile = function (filename: string): IReturnWebStatus { 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"); const home = GetServer("home");
if (home === null) { if (!home) return { res: false, msg: "Home server does not exist." };
return { return home.removeFile(path);
res: false,
msg: "Home server does not exist.",
};
}
return home.removeFile(filename);
}; };
document.saveFile = function (filename: string, code: string): IReturnWebStatus { 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(); code = Buffer.from(code, "base64").toString();
const home = GetServer("home"); const home = GetServer("home");
if (home === null) { if (!home) return { res: false, msg: "Home server does not exist." };
return {
res: false, const { overwritten } = home.writeToScriptFile(path, code);
msg: "Home server does not exist.", 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 { success, overwritten } = home.writeToScriptFile(filename, code); const ramUsage = script.getRamUsage(home.scripts);
let script; return { res: true, data: { overwritten, ramUsage } };
if (success) {
script = home.getScript(filename);
}
return {
res: success,
data: {
overwritten,
ramUsage: script?.ramUsage,
},
};
}; };
} }

@ -1,7 +1,5 @@
import { Player } from "@player"; import { Player } from "@player";
import { LiteratureName } from "./Literature/data/LiteratureNames";
import { LiteratureNames } from "./Literature/data/LiteratureNames";
import { ITutorialEvents } from "./ui/InteractiveTutorial/ITutorialEvents"; import { ITutorialEvents } from "./ui/InteractiveTutorial/ITutorialEvents";
// Ordered array of keys to Interactive Tutorial Steps // Ordered array of keys to Interactive Tutorial Steps
@ -104,7 +102,7 @@ function iTutorialEnd(): void {
ITutorial.isRunning = false; ITutorial.isRunning = false;
ITutorial.currStep = iTutorialSteps.Start; ITutorial.currStep = iTutorialSteps.Start;
const messages = Player.getHomeComputer().messages; const messages = Player.getHomeComputer().messages;
const handbook = LiteratureNames.HackersStartingHandbook; const handbook = LiteratureName.HackersStartingHandbook;
if (!messages.includes(handbook)) messages.push(handbook); if (!messages.includes(handbook)) messages.push(handbook);
ITutorialEvents.emit(); 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. * Lore / world building literature files that can be found on servers.
* These files can be read by the player * These files can be read by the player
*/ */
export class Literature { export class Literature {
title: string; title: string;
fn: string; filename: LiteratureName & FilePath;
txt: string; text: string;
constructor(title: string, filename: string, txt: string) { constructor({ title, filename, text }: LiteratureConstructorParams) {
this.title = title; this.title = title;
this.fn = filename; this.filename = asFilePath(filename);
this.txt = txt; this.text = text;
} }
} }

@ -1,11 +1,12 @@
import { Literatures } from "./Literatures"; import { Literatures } from "./Literatures";
import { dialogBoxCreate } from "../ui/React/DialogBox"; 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]; const litObj = Literatures[fn];
if (litObj == null) { if (litObj == null) {
return; return;
} }
const txt = `<i>${litObj.title}</i><br><br>${litObj.txt}`; const txt = `<i>${litObj.title}</i><br><br>${litObj.text}`;
dialogBoxCreate(txt, true); dialogBoxCreate(txt, true);
} }

@ -1,15 +1,13 @@
import { CityName } from "./../Enums"; import { CityName } from "./../Enums";
import { Literature } from "./Literature"; import { Literature } from "./Literature";
import { LiteratureNames } from "./data/LiteratureNames"; import { LiteratureName } from "./data/LiteratureNames";
import { FactionNames } from "../Faction/data/FactionNames"; import { FactionNames } from "../Faction/data/FactionNames";
export const Literatures: Record<string, Literature> = {}; export const Literatures: Record<LiteratureName, Literature> = {
[LiteratureName.HackersStartingHandbook]: new Literature({
(function () { title: "The Beginner's Guide to Hacking",
let title, fn, txt; filename: LiteratureName.HackersStartingHandbook,
title = "The Beginner's Guide to Hacking"; text:
fn = LiteratureNames.HackersStartingHandbook;
txt =
"Some resources:<br><br>" + "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/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>" + "<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>" + "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>" + "-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 " + "-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."; "Netscript command to copy your scripts onto these servers and then run them.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.CorporationManagementHandbook]: new Literature({
title = "The Complete Handbook for Creating a Successful Corporation"; title: "The Complete Handbook for Creating a Successful Corporation",
fn = LiteratureNames.CorporationManagementHandbook; filename: LiteratureName.CorporationManagementHandbook,
txt = text:
"<u>Getting Started with Corporations</u><br>" + "<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, " + "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 " + "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 " + "-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>" + "possible to see the biggest benefit for your sales<br><br>" +
"-Remember, you need to spend money to make money!<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"; "-Corporations do not reset when installing Augmentations, but they do reset when destroying a BitNode",
}),
Literatures[fn] = new Literature(title, fn, txt); [LiteratureName.HistoryOfSynthoids]: new Literature({
title: "A Brief History of Synthoids",
title = "A Brief History of Synthoids"; filename: LiteratureName.HistoryOfSynthoids,
fn = LiteratureNames.HistoryOfSynthoids; text:
txt =
"Synthetic androids, or Synthoids for short, are genetically engineered robots and, short of Augmentations, " + "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 " + "are composed entirely of organic substances. For this reason, Synthoids are virtually identical to " +
"humans in form, composition, and appearance.<br><br>" + "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 " + "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>` + `${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>" + "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."; "Nobody knows what happened to the terrorist group Ascendis Totalis.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.AGreenTomorrow]: new Literature({
title = "A Green Tomorrow"; title: "A Green Tomorrow",
fn = LiteratureNames.AGreenTomorrow; filename: LiteratureName.AGreenTomorrow,
txt = text:
"Starting a few decades ago, there was a massive global movement towards the generation of renewable energy in an effort to " + "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 " + "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 " + "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 " + "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 " + "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 " + "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."; "the last hope for a green tomorrow.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.AlphaOmega]: new Literature({
title = "Alpha and Omega"; title: "Alpha and Omega",
fn = LiteratureNames.AlphaOmega; filename: LiteratureName.AlphaOmega,
txt = 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. " + "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 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 " + "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 " + "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, " + "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 " + "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.'"; "burns with fire and sulfur, for it is the second true death.'",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.SimulatedReality]: new Literature({
title = "Are We Living in a Computer Simulation?"; title: "Are We Living in a Computer Simulation?",
fn = LiteratureNames.SimulatedReality; filename: LiteratureName.SimulatedReality,
txt = text:
"The idea that we are living in a virtual world is not new. It's a trope that has " + "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 " + "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>" + "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 " + "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>" + "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 " + "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?"; "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); }),
[LiteratureName.BeyondMan]: new Literature({
title = "Beyond Man"; title: "Beyond Man",
fn = LiteratureNames.BeyondMan; filename: LiteratureName.BeyondMan,
txt = text:
"Humanity entered a 'transhuman' era a long time ago. And despite the protests and criticisms of many who cried out against " + "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, " + "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 " + "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>" + "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 " + "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, " + "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."; "corrupted, and evil men that we always were.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.BrighterThanTheSun]: new Literature({
title = "Brighter than the Sun"; title: "Brighter than the Sun",
fn = LiteratureNames.BrighterThanTheSun; filename: LiteratureName.BrighterThanTheSun,
txt = text:
`When people think about the corporations that dominate the East, they typically think of ${FactionNames.KuaiGongInternational}, which ` + `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 " + "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 ` + `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 " + "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 " + "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 " + "the rise of TaiYang Digital so impressive. And if TaiYang continues down this path, then they have " +
"a bright future ahead of them."; "a bright future ahead of them.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.DemocracyIsDead]: new Literature({
title = "Democracy is Dead: The Fall of an Empire"; title: "Democracy is Dead: The Fall of an Empire",
fn = LiteratureNames.DemocracyIsDead; filename: LiteratureName.DemocracyIsDead,
txt = text:
"They rose from the shadows in the street.<br>From the places where the oppressed meet.<br>" + "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>" + "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>" + "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>" + "From the ashes rose a new order, corporatocracy was its name.<br>" +
"Rome, Mongol, Byzantine, all of history is just the same.<br>" + "Rome, Mongol, Byzantine, all of history is just the same.<br>" +
"For man will never change in a fundamental way.<br>" + "For man will never change in a fundamental way.<br>" +
"And now democracy is dead, in the USA."; "And now democracy is dead, in the USA.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.Sector12Crime]: new Literature({
title = `Figures Show Rising Crime Rates in ${CityName.Sector12}`; title: `Figures Show Rising Crime Rates in ${CityName.Sector12}`,
fn = LiteratureNames.Sector12Crime; filename: LiteratureName.Sector12Crime,
txt = text:
"A recent study by analytics company Wilson Inc. shows a significant rise " + "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 ` + `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 " + "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 " + "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 " + "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 " + "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}.`; `be the result of an uprising from criminal organizations such as ${FactionNames.TheSyndicate} or the ${FactionNames.SlumSnakes}.`,
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.ManAndMachine]: new Literature({
title = "Man and the Machine"; title: "Man and the Machine",
fn = LiteratureNames.ManAndMachine; filename: LiteratureName.ManAndMachine,
txt = text:
"In 2005 Ray Kurzweil popularized his theory of the Singularity. He predicted that the rate " + "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 " + "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 " + "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 " + "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 " + "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 " + "where man and machine can merge to become something greater. Most of the world just doesn't " +
"know it yet."; "know it yet.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.SecretSocieties]: new Literature({
title = "Secret Societies"; title: "Secret Societies",
fn = LiteratureNames.SecretSocieties; filename: LiteratureName.SecretSocieties,
txt = text:
"The idea of secret societies has long intrigued the general public by inspiring curiosity, fascination, and " + "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 " + "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 " + "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 ` + `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 ` + `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 " + "that try to push political and social agendas. Perhaps the most intriguing hacker group " +
`is the mysterious ${FactionNames.BitRunners}, whose purpose still remains unknown.`; `is the mysterious ${FactionNames.BitRunners}, whose purpose still remains unknown.`,
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.TheFailedFrontier]: new Literature({
title = "Space: The Failed Frontier"; title: "Space: The Failed Frontier",
fn = LiteratureNames.TheFailedFrontier; filename: LiteratureName.TheFailedFrontier,
txt = text:
"Humans have long dreamed about spaceflight. With enduring interest, we were driven to explore " + "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, " + "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 " + "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 other corporate interests.<br><br>" +
"And as we continue to look at the state of space technology, it becomes more and " + "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 " + "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."; "age where everyone dreamed of going beyond earth for the sake of discovery.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.CodedIntelligence]: new Literature({
title = "Coded Intelligence: Myth or Reality?"; title: "Coded Intelligence: Myth or Reality?",
fn = LiteratureNames.CodedIntelligence; filename: LiteratureName.CodedIntelligence,
txt = text:
"Tremendous progress has been made in the field of Artificial Intelligence over the past few decades. " + "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. " + "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 " + "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>" + "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 " + "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 " + "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.'"; "humans.'",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.SyntheticMuscles]: new Literature({
title = "Synthetic Muscles"; title: "Synthetic Muscles",
fn = LiteratureNames.SyntheticMuscles; filename: LiteratureName.SyntheticMuscles,
txt = text:
"Initial versions of synthetic muscles weren't made of anything organic but were actually " + "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 " + "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 " + "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 " + "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 " + "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 " + "muscle tissue using both synthetic and organic materials. This is typically done using gene therapy " +
"or chemical injections."; "or chemical injections.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.TensionsInTechRace]: new Literature({
title = "Tensions rise in global tech race"; title: "Tensions rise in global tech race",
fn = LiteratureNames.TensionsInTechRace; filename: LiteratureName.TensionsInTechRace,
txt = text:
"Have we entered a new Cold War? Is WWIII just beyond the horizon?<br><br>" + "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, ` + `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. " + "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 " + "DeltaOne and AeroCorp have been working with the CIA and NSA to prepare " +
"for conflict. Meanwhile, the rest of the world sits in earnest " + "for conflict. Meanwhile, the rest of the world sits in earnest " +
"hoping that it never reaches full-scale war. With today's technology " + "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."; "and firepower, a World War would assuredly mean the end of human civilization.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.CostOfImmortality]: new Literature({
title = "The Cost of Immortality"; title: "The Cost of Immortality",
fn = LiteratureNames.CostOfImmortality; filename: LiteratureName.CostOfImmortality,
txt = text:
"Evolution and advances in medical and augmentation technology has lead to drastic improvements " + "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 " + "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 " + "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, " + "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 " + "there are still several key industries such as engineering and education " +
"that have not been automated, and these remain in danger to this cultural " + "that have not been automated, and these remain in danger to this cultural " +
"phenomenon."; "phenomenon.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.TheHiddenWorld]: new Literature({
title = "The Hidden World"; title: "The Hidden World",
fn = LiteratureNames.TheHiddenWorld; filename: LiteratureName.TheHiddenWorld,
txt = text:
"WAKE UP SHEEPLE<br><br>" + "WAKE UP SHEEPLE<br><br>" +
"THE GOVERNMENT DOES NOT EXIST. CORPORATIONS DO NOT RUN SOCIETY<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>` + `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>" + "THEY ARE ALL AROUND YOU<br><br>" +
"After destabilizing the world's governments, they are now entering the final stage of their master plan. " + "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 " + "They will secretly initiate global crises. Terrorism. Pandemics. World War. And out of the chaos " +
"that ensues they will build their New World Order."; "that ensues they will build their New World Order.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.TheNewGod]: new Literature({
title = "The New God"; title: "The New God",
fn = LiteratureNames.TheNewGod; filename: LiteratureName.TheNewGod,
txt = text:
"Everyone has a moment in their life when they wonder about the bigger questions.<br><br>" + "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>" + "What's the point of all this? What is my purpose?<br><br>" +
"Some people dare to think even bigger.<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>" + "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 " + "The Singularity is here. The merging of man and machine. This is where humanity evolves into " +
"something greater. This is our future.<br><br>" + "something greater. This is our future.<br><br>" +
"Embrace it, and you will obey a new god. The God in the Machine."; "Embrace it, and you will obey a new god. The God in the Machine.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.NewTriads]: new Literature({
title = "The New Triads"; title: "The New Triads",
fn = LiteratureNames.NewTriads; filename: LiteratureName.NewTriads,
txt = text:
"The Triads were an ancient transnational crime syndicate based in China, Hong Kong, and other Asian " + "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. " + "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 " + "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>" + "continent.<br><br>" +
`Not much else is known about the ${FactionNames.Tetrads}, or about the efforts the Asian governments and corporations are making ` + `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 ` + `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."; "and powerful corporations in Asia, which has helped facilitate their recent rapid rise.",
Literatures[fn] = new Literature(title, fn, txt); }),
[LiteratureName.TheSecretWar]: new Literature({
title = "The Secret War"; title: "The Secret War",
fn = LiteratureNames.TheSecretWar; filename: LiteratureName.TheSecretWar,
txt = ""; text: "",
Literatures[fn] = new Literature(title, fn, txt); }),
})(); };

@ -1,4 +1,4 @@
export enum LiteratureNames { export enum LiteratureName {
HackersStartingHandbook = "hackers-starting-handbook.lit", HackersStartingHandbook = "hackers-starting-handbook.lit",
CorporationManagementHandbook = "corporation-management-handbook.lit", CorporationManagementHandbook = "corporation-management-handbook.lit",
HistoryOfSynthoids = "history-of-synthoids.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 { export class Message {
// Name of Message file // Name of Message file
filename: MessageFilenames; filename: MessageFilename & FilePath;
// The text contains in the Message // The text contains in the Message
msg: string; msg: string;
constructor(filename: MessageFilenames, msg: string) { constructor(filename: MessageFilename, msg: string) {
this.filename = filename; this.filename = asFilePath(filename);
this.msg = msg; this.msg = msg;
} }
} }

@ -2,7 +2,7 @@ import React from "react";
import { Message } from "./Message"; import { Message } from "./Message";
import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
import { Router } from "../ui/GameRoot"; import { Router } from "../ui/GameRoot";
import { Programs } from "../Programs/Programs"; import { CompletedProgramName } from "../Programs/Programs";
import { Player } from "@player"; import { Player } from "@player";
import { Page } from "../ui/Router"; import { Page } from "../ui/Router";
import { GetServer } from "../Server/AllServers"; import { GetServer } from "../Server/AllServers";
@ -13,16 +13,15 @@ import { FactionNames } from "../Faction/data/FactionNames";
import { Server } from "../Server/Server"; import { Server } from "../Server/Server";
//Sends message to player, including a pop up //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) { 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]; const msg = Messages[name];
if (!(msg instanceof Message)) throw new Error("trying to display nonexistent message");
dialogBoxCreate( dialogBoxCreate(
<> <>
Message received from unknown sender: Message received from unknown sender:
@ -37,40 +36,22 @@ function showMessage(name: MessageFilenames): void {
} }
//Adds a message to a server //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 //Short-circuit if the message has already been saved
if (recvd(msg)) { if (recvd(name)) return;
return; const home = Player.getHomeComputer();
} home.messages.push(name);
const server = GetServer("home");
if (server == null) {
throw new Error("The home server doesn't exist. You done goofed.");
}
server.messages.push(msg.filename);
} }
//Returns whether the given message has already been received //Returns whether the given message has already been received
function recvd(msg: Message): boolean { function recvd(name: MessageFilename): boolean {
const server = GetServer("home"); const home = Player.getHomeComputer();
if (server == null) { return home.messages.includes(name);
throw new Error("The home server doesn't exist. You done goofed.");
}
return server.messages.includes(msg.filename);
} }
//Checks if any of the 'timed' messages should be sent //Checks if any of the 'timed' messages should be sent
function checkForMessagesToSend(): void { function checkForMessagesToSend(): void {
if (Router.page() === Page.BitVerse) return; 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)) { if (Player.hasAugmentation(AugmentationNames.TheRedPill, true)) {
//Get the world daemon required hacking level //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 the daemon can be hacked, send the player icarus.msg
if (Player.skills.hacking >= worldDaemon.requiredHackingSkill) { 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. //If the daemon cannot be hacked, send the player truthgazer.msg a single time.
else if (!recvd(truthGazer)) { else if (!recvd(MessageFilename.TruthGazer)) {
sendMessage(truthGazer); sendMessage(MessageFilename.TruthGazer);
} }
} else if (!recvd(jumper0) && Player.skills.hacking >= 25) { } else if (!recvd(MessageFilename.Jumper0) && Player.skills.hacking >= 25) {
sendMessage(jumper0); sendMessage(MessageFilename.Jumper0);
const flightName = Programs.Flight.name;
const homeComp = Player.getHomeComputer(); const homeComp = Player.getHomeComputer();
if (!homeComp.programs.includes(flightName)) { if (!homeComp.programs.includes(CompletedProgramName.flight)) {
homeComp.programs.push(flightName); homeComp.programs.push(CompletedProgramName.flight);
} }
} else if (!recvd(jumper1) && Player.skills.hacking >= 40) { } else if (!recvd(MessageFilename.Jumper1) && Player.skills.hacking >= 40) {
sendMessage(jumper1); sendMessage(MessageFilename.Jumper1);
} else if (!recvd(cybersecTest) && Player.skills.hacking >= 50) { } else if (!recvd(MessageFilename.CyberSecTest) && Player.skills.hacking >= 50) {
sendMessage(cybersecTest); sendMessage(MessageFilename.CyberSecTest);
} else if (!recvd(jumper2) && Player.skills.hacking >= 175) { } else if (!recvd(MessageFilename.Jumper2) && Player.skills.hacking >= 175) {
sendMessage(jumper2); sendMessage(MessageFilename.Jumper2);
} else if (!recvd(nitesecTest) && Player.skills.hacking >= 200) { } else if (!recvd(MessageFilename.NiteSecTest) && Player.skills.hacking >= 200) {
sendMessage(nitesecTest); sendMessage(MessageFilename.NiteSecTest);
} else if (!recvd(jumper3) && Player.skills.hacking >= 325) { } else if (!recvd(MessageFilename.Jumper3) && Player.skills.hacking >= 325) {
sendMessage(jumper3); sendMessage(MessageFilename.Jumper3);
} else if (!recvd(jumper4) && Player.skills.hacking >= 490) { } else if (!recvd(MessageFilename.Jumper4) && Player.skills.hacking >= 490) {
sendMessage(jumper4); sendMessage(MessageFilename.Jumper4);
} else if (!recvd(bitrunnersTest) && Player.skills.hacking >= 500) { } else if (!recvd(MessageFilename.BitRunnersTest) && Player.skills.hacking >= 500) {
sendMessage(bitrunnersTest); sendMessage(MessageFilename.BitRunnersTest);
} }
} }
export enum MessageFilenames { export enum MessageFilename {
Jumper0 = "j0.msg", Jumper0 = "j0.msg",
Jumper1 = "j1.msg", Jumper1 = "j1.msg",
Jumper2 = "j2.msg", Jumper2 = "j2.msg",
@ -123,11 +103,11 @@ export enum MessageFilenames {
RedPill = "icarus.msg", RedPill = "icarus.msg",
} }
//Reset // This type ensures that all members of the MessageFilename enum are valid keys
const Messages: Record<MessageFilenames, Message> = { const Messages: Record<MessageFilename, Message> = {
//jump3R Messages //jump3R Messages
[MessageFilenames.Jumper0]: new Message( [MessageFilename.Jumper0]: new Message(
MessageFilenames.Jumper0, MessageFilename.Jumper0,
"I know you can sense it. I know you're searching for it. " + "I know you can sense it. I know you're searching for it. " +
"It's why you spend night after " + "It's why you spend night after " +
"night at your computer. \n\nIt's real, I've seen it. And I can " + "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", "-jump3R",
), ),
[MessageFilenames.Jumper1]: new Message( [MessageFilename.Jumper1]: new Message(
MessageFilenames.Jumper1, MessageFilename.Jumper1,
`Soon you will be contacted by a hacking group known as ${FactionNames.CyberSec}. ` + `Soon you will be contacted by a hacking group known as ${FactionNames.CyberSec}. ` +
"They can help you with your search. \n\n" + "They can help you with your search. \n\n" +
"You should join them, garner their favor, and " + "You should join them, garner their favor, and " +
@ -147,31 +127,31 @@ const Messages: Record<MessageFilenames, Message> = {
"-jump3R", "-jump3R",
), ),
[MessageFilenames.Jumper2]: new Message( [MessageFilename.Jumper2]: new Message(
MessageFilenames.Jumper2, MessageFilename.Jumper2,
"Do not try to save the world. There is no world to save. If " + "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 " + "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}.` + `morals will get you killed. \n\nWatch out for a hacking group known as ${FactionNames.NiteSec}.` +
"\n\n-jump3R", "\n\n-jump3R",
), ),
[MessageFilenames.Jumper3]: new Message( [MessageFilename.Jumper3]: new Message(
MessageFilenames.Jumper3, MessageFilename.Jumper3,
"You must learn to walk before you can run. And you must " + "You must learn to walk before you can run. And you must " +
`run before you can fly. Look for ${FactionNames.TheBlackHand}. \n\n` + `run before you can fly. Look for ${FactionNames.TheBlackHand}. \n\n` +
"I.I.I.I \n\n-jump3R", "I.I.I.I \n\n-jump3R",
), ),
[MessageFilenames.Jumper4]: new Message( [MessageFilename.Jumper4]: new Message(
MessageFilenames.Jumper4, MessageFilename.Jumper4,
"To find what you are searching for, you must understand the bits. " + "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" + "The bits are all around us. The runners will help you.\n\n" +
"-jump3R", "-jump3R",
), ),
//Messages from hacking factions //Messages from hacking factions
[MessageFilenames.CyberSecTest]: new Message( [MessageFilename.CyberSecTest]: new Message(
MessageFilenames.CyberSecTest, MessageFilename.CyberSecTest,
"We've been watching you. Your skills are very impressive. But you're wasting " + "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 " + "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" + "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}`, `-${FactionNames.CyberSec}`,
), ),
[MessageFilenames.NiteSecTest]: new Message( [MessageFilename.NiteSecTest]: new Message(
MessageFilenames.NiteSecTest, MessageFilename.NiteSecTest,
"People say that the corrupted governments and corporations rule the world. " + "People say that the corrupted governments and corporations rule the world. " +
"Yes, maybe they do. But do you know who everyone really fears? People " + "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 " + "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}`, `\n\n-${FactionNames.NiteSec}`,
), ),
[MessageFilenames.BitRunnersTest]: new Message( [MessageFilename.BitRunnersTest]: new Message(
MessageFilenames.BitRunnersTest, MessageFilename.BitRunnersTest,
"We know what you are doing. We know what drives you. We know " + "We know what you are doing. We know what drives you. We know " +
"what you are looking for. \n\n " + "what you are looking for. \n\n " +
"We can help you find the answers.\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 //Messages to guide players to the daemon
[MessageFilenames.TruthGazer]: new Message( [MessageFilename.TruthGazer]: new Message(
MessageFilenames.TruthGazer, MessageFilename.TruthGazer,
//"THE TRUTH CAN NO LONGER ESCAPE YOUR GAZE" //"THE TRUTH CAN NO LONGER ESCAPE YOUR GAZE"
"@&*($#@&__TH3__#@A&#@*)__TRU1H__(*)&*)($#@&()E&R)W&\n" + "@&*($#@&__TH3__#@A&#@*)__TRU1H__(*)&*)($#@&()E&R)W&\n" +
"%@*$^$()@&$)$*@__CAN__()(@^#)@&@)#__N0__(#@&#)@&@&(\n" + "%@*$^$()@&$)$*@__CAN__()(@^#)@&@)#__N0__(#@&#)@&@&(\n" +
@ -208,8 +188,8 @@ const Messages: Record<MessageFilenames, Message> = {
"()@)#$*%)$#()$#__Y0UR__(*)$#()%(&(%)*!)($__GAZ3__#(", "()@)#$*%)$#()$#__Y0UR__(*)$#()%(&(%)*!)($__GAZ3__#(",
), ),
[MessageFilenames.RedPill]: new Message( [MessageFilename.RedPill]: new Message(
MessageFilenames.RedPill, MessageFilename.RedPill,
//"FIND THE-CAVE" //"FIND THE-CAVE"
"@)(#V%*N)@(#*)*C)@#%*)*V)@#(*%V@)(#VN%*)@#(*%\n" + "@)(#V%*N)@(#*)*C)@#%*)*V)@#(*%V@)(#VN%*)@#(*%\n" +
")@B(*#%)@)M#B*%V)____FIND___#$@)#%(B*)@#(*%B)\n" + ")@B(*#%)@)M#B*%V)____FIND___#$@)#%(B*)@#(*%B)\n" +

@ -17,6 +17,7 @@ import { BaseServer } from "../Server/BaseServer";
import { ScriptDeath } from "./ScriptDeath"; import { ScriptDeath } from "./ScriptDeath";
import { ScriptArg } from "./ScriptArg"; import { ScriptArg } from "./ScriptArg";
import { NSFull } from "../NetscriptFunctions"; import { NSFull } from "../NetscriptFunctions";
import { ScriptFilePath } from "src/Paths/ScriptFilePath";
export class WorkerScript { export class WorkerScript {
/** Script's arguments */ /** Script's arguments */
@ -60,7 +61,7 @@ export class WorkerScript {
loadedFns: Record<string, boolean> = {}; loadedFns: Record<string, boolean> = {};
/** Filename of script */ /** Filename of script */
name: string; name: ScriptFilePath;
/** Script's output/return value. Currently not used or implemented */ /** Script's output/return value. Currently not used or implemented */
output = ""; output = "";
@ -132,14 +133,6 @@ export class WorkerScript {
return script; 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 { shouldLog(fn: string): boolean {
return this.disableLogs[fn] == null; return this.disableLogs[fn] == null;
} }

@ -14,9 +14,7 @@ import {
import { netscriptCanGrow, netscriptCanWeaken } from "./Hacking/netscriptCanHack"; import { netscriptCanGrow, netscriptCanWeaken } from "./Hacking/netscriptCanHack";
import { Terminal } from "./Terminal"; import { Terminal } from "./Terminal";
import { Player } from "@player"; import { Player } from "@player";
import { Programs } from "./Programs/Programs"; import { CompletedProgramName } from "./Programs/Programs";
import { Script } from "./Script/Script";
import { isScriptFilename } from "./Script/isScriptFilename";
import { PromptEvent } from "./ui/React/PromptManager"; import { PromptEvent } from "./ui/React/PromptManager";
import { GetServer, DeleteServer, AddToAllServers, createUniqueRandomIp } from "./Server/AllServers"; import { GetServer, DeleteServer, AddToAllServers, createUniqueRandomIp } from "./Server/AllServers";
import { import {
@ -36,8 +34,6 @@ import {
} from "./Server/ServerPurchases"; } from "./Server/ServerPurchases";
import { Server } from "./Server/Server"; import { Server } from "./Server/Server";
import { influenceStockThroughServerGrow } from "./StockMarket/PlayerInfluencing"; import { influenceStockThroughServerGrow } from "./StockMarket/PlayerInfluencing";
import { isValidFilePath, removeLeadingSlash } from "./Terminal/DirectoryHelpers";
import { TextFile, getTextFile, createTextFile } from "./TextFile";
import { runScriptFromScript } from "./NetscriptWorker"; import { runScriptFromScript } from "./NetscriptWorker";
import { killWorkerScript } from "./Netscript/killWorkerScript"; import { killWorkerScript } from "./Netscript/killWorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts"; import { workerScripts } from "./Netscript/WorkerScripts";
@ -56,7 +52,6 @@ import {
import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions"; import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions";
import { LogBoxEvents, LogBoxCloserEvents, LogBoxPositionEvents, LogBoxSizeEvents } from "./ui/React/LogBoxManager"; import { LogBoxEvents, LogBoxCloserEvents, LogBoxPositionEvents, LogBoxSizeEvents } from "./ui/React/LogBoxManager";
import { arrayToString } from "./utils/helpers/arrayToString"; import { arrayToString } from "./utils/helpers/arrayToString";
import { isString } from "./utils/helpers/isString";
import { NetscriptGang } from "./NetscriptFunctions/Gang"; import { NetscriptGang } from "./NetscriptFunctions/Gang";
import { NetscriptSleeve } from "./NetscriptFunctions/Sleeve"; import { NetscriptSleeve } from "./NetscriptFunctions/Sleeve";
import { NetscriptExtra } from "./NetscriptFunctions/Extra"; import { NetscriptExtra } from "./NetscriptFunctions/Extra";
@ -91,6 +86,11 @@ import { cloneDeep } from "lodash";
import { FactionWorkType } from "./Enums"; import { FactionWorkType } from "./Enums";
import numeral from "numeral"; import numeral from "numeral";
import { clearPort, peekPort, portHandle, readPort, tryWritePort, writePort } from "./NetscriptPort"; 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 = { export const enums: NSEnums = {
CityName, CityName,
@ -446,22 +446,22 @@ export const ns: InternalAPI<NSFull> = {
} }
const str = helpers.argsToString(args); const str = helpers.argsToString(args);
if (str.startsWith("ERROR") || str.startsWith("FAIL")) { if (str.startsWith("ERROR") || str.startsWith("FAIL")) {
Terminal.error(`${ctx.workerScript.scriptRef.filename}: ${str}`); Terminal.error(`${ctx.workerScript.name}: ${str}`);
return; return;
} }
if (str.startsWith("SUCCESS")) { if (str.startsWith("SUCCESS")) {
Terminal.success(`${ctx.workerScript.scriptRef.filename}: ${str}`); Terminal.success(`${ctx.workerScript.name}: ${str}`);
return; return;
} }
if (str.startsWith("WARN")) { if (str.startsWith("WARN")) {
Terminal.warn(`${ctx.workerScript.scriptRef.filename}: ${str}`); Terminal.warn(`${ctx.workerScript.name}: ${str}`);
return; return;
} }
if (str.startsWith("INFO")) { if (str.startsWith("INFO")) {
Terminal.info(`${ctx.workerScript.scriptRef.filename}: ${str}`); Terminal.info(`${ctx.workerScript.name}: ${str}`);
return; return;
} }
Terminal.print(`${ctx.workerScript.scriptRef.filename}: ${str}`); Terminal.print(`${ctx.workerScript.name}: ${str}`);
}, },
tprintf: tprintf:
(ctx) => (ctx) =>
@ -583,7 +583,7 @@ export const ns: InternalAPI<NSFull> = {
helpers.log(ctx, () => `Already have root access to '${server.hostname}'.`); helpers.log(ctx, () => `Already have root access to '${server.hostname}'.`);
return true; 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!"); throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the NUKE.exe virus!");
} }
if (server.openPortCount < server.numOpenPortsRequired) { if (server.openPortCount < server.numOpenPortsRequired) {
@ -600,7 +600,7 @@ export const ns: InternalAPI<NSFull> = {
helpers.log(ctx, () => "Cannot be executed on this server."); helpers.log(ctx, () => "Cannot be executed on this server.");
return false; 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!"); throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the BruteSSH.exe program!");
} }
if (!server.sshPortOpen) { if (!server.sshPortOpen) {
@ -619,7 +619,7 @@ export const ns: InternalAPI<NSFull> = {
helpers.log(ctx, () => "Cannot be executed on this server."); helpers.log(ctx, () => "Cannot be executed on this server.");
return false; 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!"); throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the FTPCrack.exe program!");
} }
if (!server.ftpPortOpen) { if (!server.ftpPortOpen) {
@ -638,7 +638,7 @@ export const ns: InternalAPI<NSFull> = {
helpers.log(ctx, () => "Cannot be executed on this server."); helpers.log(ctx, () => "Cannot be executed on this server.");
return false; 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!"); throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the relaySMTP.exe program!");
} }
if (!server.smtpPortOpen) { if (!server.smtpPortOpen) {
@ -657,7 +657,7 @@ export const ns: InternalAPI<NSFull> = {
helpers.log(ctx, () => "Cannot be executed on this server."); helpers.log(ctx, () => "Cannot be executed on this server.");
return false; 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!"); throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the HTTPWorm.exe program!");
} }
if (!server.httpPortOpen) { if (!server.httpPortOpen) {
@ -676,7 +676,7 @@ export const ns: InternalAPI<NSFull> = {
helpers.log(ctx, () => "Cannot be executed on this server."); helpers.log(ctx, () => "Cannot be executed on this server.");
return false; 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!"); throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the SQLInject.exe program!");
} }
if (!server.sqlPortOpen) { if (!server.sqlPortOpen) {
@ -691,30 +691,30 @@ export const ns: InternalAPI<NSFull> = {
run: run:
(ctx) => (ctx) =>
(_scriptname, _thread_or_opt = 1, ..._args) => { (_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 runOpts = helpers.runOptions(ctx, _thread_or_opt);
const args = helpers.scriptArgs(ctx, _args); const args = helpers.scriptArgs(ctx, _args);
const scriptServer = GetServer(ctx.workerScript.hostname); const scriptServer = ctx.workerScript.getServer();
if (scriptServer == null) {
throw helpers.makeRuntimeErrorMsg(ctx, "Could not find server. This is a bug. Report to dev.");
}
return runScriptFromScript("run", scriptServer, scriptname, args, ctx.workerScript, runOpts); return runScriptFromScript("run", scriptServer, path, args, ctx.workerScript, runOpts);
}, },
exec: exec:
(ctx) => (ctx) =>
(_scriptname, _hostname, _thread_or_opt = 1, ..._args) => { (_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 hostname = helpers.string(ctx, "hostname", _hostname);
const runOpts = helpers.runOptions(ctx, _thread_or_opt); const runOpts = helpers.runOptions(ctx, _thread_or_opt);
const args = helpers.scriptArgs(ctx, _args); const args = helpers.scriptArgs(ctx, _args);
const server = helpers.getServer(ctx, hostname); 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: spawn:
(ctx) => (ctx) =>
(_scriptname, _thread_or_opt = 1, ..._args) => { (_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 runOpts = helpers.runOptions(ctx, _thread_or_opt);
const args = helpers.scriptArgs(ctx, _args); const args = helpers.scriptArgs(ctx, _args);
const spawnDelay = 10; 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"); 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); }, spawnDelay * 1e3);
helpers.log(ctx, () => `Will execute '${scriptname}' in ${spawnDelay} seconds`); helpers.log(ctx, () => `Will execute '${path}' in ${spawnDelay} seconds`);
if (killWorkerScript(ctx.workerScript)) { if (killWorkerScript(ctx.workerScript)) {
helpers.log(ctx, () => "Exiting..."); helpers.log(ctx, () => "Exiting...");
@ -804,106 +804,70 @@ export const ns: InternalAPI<NSFull> = {
killWorkerScript(ctx.workerScript); killWorkerScript(ctx.workerScript);
throw new ScriptDeath(ctx.workerScript); throw new ScriptDeath(ctx.workerScript);
}, },
scp: scp: (ctx) => (_files, _destination, _source) => {
(ctx) =>
(_files, _destination, _source = ctx.workerScript.hostname) => {
const destination = helpers.string(ctx, "destination", _destination); 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 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 files = Array.isArray(_files) ? _files : [_files];
const lits: (FilePath & LiteratureName)[] = [];
const contentFiles: ContentFilePath[] = [];
//First loop through filenames to find all errors before moving anything. //First loop through filenames to find all errors before moving anything.
for (const file of files) { for (const file of files) {
// Not a string // 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."); throw helpers.makeRuntimeErrorMsg(ctx, "files should be a string or an array of strings.");
}
// Invalid file name if (hasScriptExtension(file) || hasTextExtension(file)) {
if (!isValidFilePath(file)) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: '${file}'`); const path = resolveScriptFilePath(file, ctx.workerScript.name);
if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filepath: ${file}`);
// Invalid file type contentFiles.push(path);
if (!file.endsWith(".lit") && !isScriptFilename(file) && !file.endsWith(".txt")) { continue;
}
if (!file.endsWith(".lit")) {
throw helpers.makeRuntimeErrorMsg(ctx, "Only works for scripts, .lit and .txt files."); 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; let noFailures = true;
//ts detects files as any[] here even though we would have thrown in the above loop if it wasn't string[] // --- Scripts and Text Files---
for (let file of files as string[]) { for (const contentFilePath of contentFiles) {
// 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` const sourceContentFile = sourceServer.getContentFile(contentFilePath);
if (file.startsWith("/") && file.indexOf("/", 1) === -1) file = file.slice(1); if (!sourceContentFile) {
helpers.log(ctx, () => `File '${contentFilePath}' does not exist.`);
// Scp for lit files
if (file.endsWith(".lit")) {
const sourceMessage = sourceServ.messages.find((message) => message === file);
if (!sourceMessage) {
helpers.log(ctx, () => `File '${file}' does not exist.`);
noFailures = false; noFailures = false;
continue; 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 // Overwrite script if it already exists
const destScript = destServer.scripts.get(file); const result = destServer.writeToContentFile(contentFilePath, sourceContentFile.content);
if (destScript) { helpers.log(ctx, () => `Copied file ${contentFilePath} from ${sourceServer} to ${destServer}`);
if (destScript.code === sourceScript.code) { if (result.overwritten) helpers.log(ctx, () => `Warning: ${contentFilePath} was overwritten on ${destServer}`);
helpers.log(ctx, () => `Identical file '${file}' was already on '${destServer?.hostname}'`);
continue;
} }
destScript.code = sourceScript.code;
// Set ramUsage to null in order to force a recalculation prior to next run. // --- Literature Files ---
destScript.invalidateModule(); for (const litFilePath of lits) {
helpers.log(ctx, () => `WARNING: File '${file}' overwritten on '${destServer?.hostname}'`); const sourceMessage = sourceServer.messages.find((message) => message === litFilePath);
if (!sourceMessage) {
helpers.log(ctx, () => `File '${litFilePath}' does not exist.`);
noFailures = false;
continue; continue;
} }
// Create new script if it does not already exist const destMessage = destServer.messages.find((message) => message === litFilePath);
const newScript = new Script(file, sourceScript.code, destServer.hostname); if (destMessage) {
destServer.scripts.set(file, newScript); helpers.log(ctx, () => `File '${litFilePath}' was already on '${destServer?.hostname}'.`);
helpers.log(ctx, () => `File '${file}' copied over to '${destServer?.hostname}'.`); continue;
} }
destServer.messages.push(litFilePath);
helpers.log(ctx, () => `File '${litFilePath}' copied over to '${destServer?.hostname}'.`);
continue;
}
return noFailures; return noFailures;
}, },
ls: (ctx) => (_hostname, _substring) => { ls: (ctx) => (_hostname, _substring) => {
@ -916,7 +880,7 @@ export const ns: InternalAPI<NSFull> = {
...server.messages, ...server.messages,
...server.programs, ...server.programs,
...server.scripts.keys(), ...server.scripts.keys(),
...server.textFiles.map((textFile) => textFile.filename), ...server.textFiles.keys(),
]; ];
if (!substring) return allFilenames.sort(); if (!substring) return allFilenames.sort();
@ -1147,7 +1111,7 @@ export const ns: InternalAPI<NSFull> = {
const filename = helpers.string(ctx, "filename", _filename); const filename = helpers.string(ctx, "filename", _filename);
const hostname = helpers.string(ctx, "hostname", _hostname); const hostname = helpers.string(ctx, "hostname", _hostname);
const server = helpers.getServer(ctx, 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) { for (let i = 0; i < server.programs.length; ++i) {
if (filename.toLowerCase() == server.programs[i].toLowerCase()) { if (filename.toLowerCase() == server.programs[i].toLowerCase()) {
return true; return true;
@ -1160,8 +1124,7 @@ export const ns: InternalAPI<NSFull> = {
} }
const contract = server.contracts.find((c) => c.fn.toLowerCase() === filename.toLowerCase()); const contract = server.contracts.find((c) => c.fn.toLowerCase() === filename.toLowerCase());
if (contract) return true; if (contract) return true;
const txtFile = getTextFile(filename, server); return false;
return txtFile != null;
}, },
isRunning: isRunning:
(ctx) => (ctx) =>
@ -1363,43 +1326,34 @@ export const ns: InternalAPI<NSFull> = {
return writePort(portNumber, data); return writePort(portNumber, data);
}, },
write: (ctx) => (_filename, _data, _mode) => { 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 data = helpers.string(ctx, "data", _data ?? "");
const mode = helpers.string(ctx, "mode", _mode ?? "a"); 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); const server = helpers.getServer(ctx, ctx.workerScript.hostname);
if (isScriptFilename(filename)) { if (hasScriptExtension(filepath)) {
// Write to script if (mode === "w") {
let script = ctx.workerScript.getScriptOnServer(filename, server); server.writeToScriptFile(filepath, data);
if (!script) {
// Create a new script
script = new Script(filename, String(data), server.hostname);
server.scripts.set(filename, script);
return; return;
} }
mode === "w" ? (script.code = data) : (script.code += data); const existingScript = server.scripts.get(filepath);
// Set ram to null so a recalc is performed the next time ram usage is needed const existingCode = existingScript ? existingScript.code : "";
script.invalidateModule(); server.writeToScriptFile(filepath, existingCode + data);
return;
} else {
// Write to text file
if (!filename.endsWith(".txt")) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: ${filename}`);
const txtFile = getTextFile(filename, server);
if (txtFile == null) {
createTextFile(filename, String(data), server);
return; return;
} }
if (!hasTextExtension(filepath)) {
throw helpers.makeRuntimeErrorMsg(ctx, `File path should be a text file or script. ${filepath} is invalid.`);
}
if (mode === "w") { if (mode === "w") {
txtFile.write(String(data)); server.writeToTextFile(filepath, data);
} else {
txtFile.append(String(data));
}
}
return; return;
}
const existingTextFile = server.textFiles.get(filepath);
const existingText = existingTextFile?.text ?? "";
server.writeToTextFile(filepath, mode === "w" ? data : existingText + data);
}, },
tryWritePort: (ctx) => (_portNumber, data) => { tryWritePort: (ctx) => (_portNumber, data) => {
const portNumber = helpers.portNumber(ctx, _portNumber); const portNumber = helpers.portNumber(ctx, _portNumber);
@ -1416,48 +1370,25 @@ export const ns: InternalAPI<NSFull> = {
return readPort(portNumber); return readPort(portNumber);
}, },
read: (ctx) => (_filename) => { read: (ctx) => (_filename) => {
const fn = helpers.string(ctx, "filename", _filename); const path = resolveFilePath(helpers.string(ctx, "filename", _filename), ctx.workerScript.name);
const server = GetServer(ctx.workerScript.hostname); if (!path || (!hasScriptExtension(path) && !hasTextExtension(path))) return "";
if (server == null) { const server = ctx.workerScript.getServer();
throw helpers.makeRuntimeErrorMsg(ctx, "Error getting Server. This is a bug. Report to dev."); return server.getContentFile(path)?.content ?? "";
}
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 "";
}
}
}, },
peek: (ctx) => (_portNumber) => { peek: (ctx) => (_portNumber) => {
const portNumber = helpers.portNumber(ctx, _portNumber); const portNumber = helpers.portNumber(ctx, _portNumber);
return peekPort(portNumber); return peekPort(portNumber);
}, },
clear: (ctx) => (_file) => { clear: (ctx) => (_file) => {
const file = helpers.string(ctx, "file", _file); const path = resolveFilePath(helpers.string(ctx, "file", _file), ctx.workerScript.name);
if (isString(file)) { if (!path || (!hasScriptExtension(path) && !hasTextExtension(path))) {
// Clear text file throw helpers.makeRuntimeErrorMsg(ctx, `Invalid file path or extension: ${_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 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) => { clearPort: (ctx) => (_portNumber) => {
const portNumber = helpers.portNumber(ctx, _portNumber); const portNumber = helpers.portNumber(ctx, _portNumber);
@ -1467,14 +1398,16 @@ export const ns: InternalAPI<NSFull> = {
const portNumber = helpers.portNumber(ctx, _portNumber); const portNumber = helpers.portNumber(ctx, _portNumber);
return portHandle(portNumber); return portHandle(portNumber);
}, },
rm: rm: (ctx) => (_fn, _hostname) => {
(ctx) => const filepath = resolveFilePath(helpers.string(ctx, "fn", _fn), ctx.workerScript.name);
(_fn, _hostname = ctx.workerScript.hostname) => { const hostname = helpers.string(ctx, "hostname", _hostname ?? ctx.workerScript.hostname);
const fn = helpers.string(ctx, "fn", _fn);
const hostname = helpers.string(ctx, "hostname", _hostname);
const s = helpers.getServer(ctx, 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) { if (!status.res) {
helpers.log(ctx, () => status.msg + ""); helpers.log(ctx, () => status.msg + "");
} }
@ -1506,18 +1439,17 @@ export const ns: InternalAPI<NSFull> = {
} }
return suc; return suc;
}, },
getScriptName: (ctx) => () => { getScriptName: (ctx) => () => ctx.workerScript.name,
return ctx.workerScript.name;
},
getScriptRam: (ctx) => (_scriptname, _hostname) => { 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 hostname = helpers.string(ctx, "hostname", _hostname ?? ctx.workerScript.hostname);
const server = helpers.getServer(ctx, hostname); const server = helpers.getServer(ctx, hostname);
const script = server.scripts.get(scriptname); const script = server.scripts.get(path);
if (!script) return 0; if (!script) return 0;
const ramUsage = script.getRamUsage(server.scripts); const ramUsage = script.getRamUsage(server.scripts);
if (!ramUsage) { 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 0;
} }
return ramUsage; return ramUsage;
@ -1704,26 +1636,22 @@ export const ns: InternalAPI<NSFull> = {
}, },
wget: (ctx) => async (_url, _target, _hostname) => { wget: (ctx) => async (_url, _target, _hostname) => {
const url = helpers.string(ctx, "url", _url); 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; 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.`); helpers.log(ctx, () => `Invalid target file: '${target}'. Must be a script or text file.`);
return Promise.resolve(false); return Promise.resolve(false);
} }
const s = helpers.getServer(ctx, hostname);
return new Promise(function (resolve) { return new Promise(function (resolve) {
$.get( $.get(
url, url,
function (data) { function (data) {
let res; let res;
if (isScriptFilename(target)) { if (hasScriptExtension(target)) {
res = s.writeToScriptFile(target, data); res = server.writeToScriptFile(target, data);
} else { } else {
res = s.writeToTextFile(target, data); res = server.writeToTextFile(target, data);
}
if (!res.success) {
helpers.log(ctx, () => "Failed.");
return resolve(false);
} }
if (res.overwritten) { if (res.overwritten) {
helpers.log(ctx, () => `Successfully retrieved content and overwrote '${target}' on '${hostname}'`); 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) => { mv: (ctx) => (_host, _source, _destination) => {
const hostname = helpers.string(ctx, "host", _host); const hostname = helpers.string(ctx, "host", _host);
const source = helpers.string(ctx, "source", _source); const server = helpers.getServer(ctx, hostname);
const destination = helpers.string(ctx, "destination", _destination); 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 (
if (!isValidFilePath(destination)) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: '${destination}'`); (!hasTextExtension(sourcePath) && !hasScriptExtension(sourcePath)) ||
(!hasTextExtension(destinationPath) && !hasScriptExtension(destinationPath))
const source_is_txt = source.endsWith(".txt"); ) {
const dest_is_txt = destination.endsWith(".txt");
if (!isScriptFilename(source) && !source_is_txt)
throw helpers.makeRuntimeErrorMsg(ctx, `'mv' can only be used on scripts and text files (.txt)`); 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 (sourcePath === destinationPath) {
helpers.log(ctx, () => "WARNING: Did nothing, source and destination paths were the same.");
if (source === destination) {
return; return;
} }
const sourceContentFile = server.getContentFile(sourcePath);
const server = helpers.getServer(ctx, hostname); if (!sourceContentFile) {
throw helpers.makeRuntimeErrorMsg(ctx, `Source text file ${sourcePath} does not exist on ${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;
} }
let source_file: File | undefined; const success = sourceContentFile.deleteFromServer(server);
let dest_file: File | undefined; if (success) {
const { overwritten } = server.writeToContentFile(destinationPath, sourceContentFile.content);
if (source_is_txt) { if (overwritten) helpers.log(ctx, () => `WARNING: Overwriting file ${destinationPath} on ${hostname}`);
// Traverses twice potentially. Inefficient but will soon be replaced with a map. helpers.log(ctx, () => `Moved ${sourcePath} to ${destinationPath} on ${hostname}`);
source_file = server.textFiles.find((textFile) => textFile.filename === source); return;
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();
} }
helpers.log(ctx, () => `ERROR: Failed. Was unable to remove file ${sourcePath} from its original location.`);
}, },
flags: Flags, flags: Flags,
...NetscriptExtra(), ...NetscriptExtra(),

@ -26,7 +26,7 @@ import {
calculateGrowTime, calculateGrowTime,
calculateWeakenTime, calculateWeakenTime,
} from "../Hacking"; } 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 { Formulas as IFormulas, Player as IPlayer, Person as IPerson } from "@nsdefs";
import { import {
calculateRespectGain, calculateRespectGain,
@ -55,7 +55,7 @@ import { findCrime } from "../Crime/CrimeHelpers";
export function NetscriptFormulas(): InternalAPI<IFormulas> { export function NetscriptFormulas(): InternalAPI<IFormulas> {
const checkFormulasAccess = function (ctx: NetscriptContext): void { 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.`); 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 { augmentationExists, installAugmentations } from "../Augmentation/AugmentationHelpers";
import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
import { CONSTANTS } from "../Constants"; import { CONSTANTS } from "../Constants";
import { isString } from "../utils/helpers/isString";
import { RunningScript } from "../Script/RunningScript"; import { RunningScript } from "../Script/RunningScript";
import { calculateAchievements } from "../Achievements/Achievements"; import { calculateAchievements } from "../Achievements/Achievements";
@ -52,6 +51,8 @@ import { calculateCrimeWorkStats } from "../Work/Formulas";
import { findEnumMember } from "../utils/helpers/enum"; import { findEnumMember } from "../utils/helpers/enum";
import { Engine } from "../engine"; import { Engine } from "../engine";
import { checkEnum } from "../utils/helpers/enum"; import { checkEnum } from "../utils/helpers/enum";
import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath";
import { root } from "../Paths/Directory";
export function NetscriptSingularity(): InternalAPI<ISingularity> { export function NetscriptSingularity(): InternalAPI<ISingularity> {
const getAugmentation = function (ctx: NetscriptContext, name: string): Augmentation { const getAugmentation = function (ctx: NetscriptContext, name: string): Augmentation {
@ -76,7 +77,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
return company; return company;
}; };
const runAfterReset = function (cbScript: string | null = null) { const runAfterReset = function (cbScript: ScriptFilePath) {
//Run a script after reset //Run a script after reset
if (!cbScript) return; if (!cbScript) return;
const home = Player.getHomeComputer(); 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.`); return Terminal.error(`Attempted to launch ${cbScript} after reset but could not calculate ram usage.`);
} }
const ramAvailable = home.maxRam - home.ramUsed; 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). // Start script with no args and 1 thread (default).
const runningScriptObj = new RunningScript(script, ramUsage, []); const runningScriptObj = new RunningScript(script, ramUsage, []);
startWorkerScript(runningScriptObj, home); startWorkerScript(runningScriptObj, home);
@ -190,7 +193,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
const res = purchaseAugmentation(aug, fac, true); const res = purchaseAugmentation(aug, fac, true);
helpers.log(ctx, () => res); helpers.log(ctx, () => res);
if (isString(res) && res.startsWith("You purchased")) { if (res.startsWith("You purchased")) {
Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain * 10); Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain * 10);
return true; return true;
} else { } else {
@ -199,7 +202,10 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
}, },
softReset: (ctx) => (_cbScript) => { softReset: (ctx) => (_cbScript) => {
helpers.checkSingularityAccess(ctx); 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"); helpers.log(ctx, () => "Soft resetting. This will cause this script to be killed");
installAugmentations(true); installAugmentations(true);
@ -207,7 +213,10 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
}, },
installAugmentations: (ctx) => (_cbScript) => { installAugmentations: (ctx) => (_cbScript) => {
helpers.checkSingularityAccess(ctx); 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) { if (Player.queuedAugmentations.length === 0) {
helpers.log(ctx, () => "You do not have any Augmentations to be installed."); 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.getCurrentServer().isConnectedTo = false;
Player.currentServer = Player.getHomeComputer().hostname; Player.currentServer = Player.getHomeComputer().hostname;
Player.getCurrentServer().isConnectedTo = true; Player.getCurrentServer().isConnectedTo = true;
Terminal.setcwd("/"); Terminal.setcwd(root);
return true; return true;
} }
@ -515,7 +524,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
Player.getCurrentServer().isConnectedTo = false; Player.getCurrentServer().isConnectedTo = false;
Player.currentServer = target.hostname; Player.currentServer = target.hostname;
Player.getCurrentServer().isConnectedTo = true; Player.getCurrentServer().isConnectedTo = true;
Terminal.setcwd("/"); Terminal.setcwd(root);
return true; return true;
} }
} }
@ -526,7 +535,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
Player.getCurrentServer().isConnectedTo = false; Player.getCurrentServer().isConnectedTo = false;
Player.currentServer = target.hostname; Player.currentServer = target.hostname;
Player.getCurrentServer().isConnectedTo = true; Player.getCurrentServer().isConnectedTo = true;
Terminal.setcwd("/"); Terminal.setcwd(root);
return true; return true;
} }
@ -1189,28 +1198,26 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
} }
return item.price; return item.price;
}, },
b1tflum3: b1tflum3: (ctx) => (_nextBN, _cbScript) => {
(ctx) =>
(_nextBN, _callbackScript = "") => {
helpers.checkSingularityAccess(ctx); helpers.checkSingularityAccess(ctx);
const nextBN = helpers.number(ctx, "nextBN", _nextBN); const nextBN = helpers.number(ctx, "nextBN", _nextBN);
const callbackScript = helpers.string(ctx, "callbackScript", _callbackScript); const cbScript = _cbScript
helpers.checkSingularityAccess(ctx); ? 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); enterBitNode(true, Player.bitNodeN, nextBN);
if (callbackScript) if (cbScript) setTimeout(() => runAfterReset(cbScript), 0);
setTimeout(() => {
runAfterReset(callbackScript);
}, 0);
}, },
destroyW0r1dD43m0n: destroyW0r1dD43m0n: (ctx) => (_nextBN, _cbScript) => {
(ctx) =>
(_nextBN, _callbackScript = "") => {
helpers.checkSingularityAccess(ctx); helpers.checkSingularityAccess(ctx);
const nextBN = helpers.number(ctx, "nextBN", _nextBN); const nextBN = helpers.number(ctx, "nextBN", _nextBN);
if (nextBN > 13 || nextBN < 1 || !Number.isInteger(nextBN)) { if (nextBN > 13 || nextBN < 1 || !Number.isInteger(nextBN)) {
throw new Error(`Invalid bitnode specified: ${_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); const wd = GetServer(SpecialServers.WorldDaemon);
if (!(wd instanceof Server)) throw new Error("WorldDaemon was not a normal server. This is a bug contact dev."); 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; wd.backdoorInstalled = true;
calculateAchievements(); calculateAchievements();
enterBitNode(false, Player.bitNodeN, nextBN); enterBitNode(false, Player.bitNodeN, nextBN);
if (callbackScript) if (cbScript) setTimeout(() => runAfterReset(cbScript), 0);
setTimeout(() => {
runAfterReset(callbackScript);
}, 0);
}, },
getCurrentWork: () => () => { getCurrentWork: () => () => {
if (!Player.currentWork) return null; if (!Player.currentWork) return null;

@ -7,8 +7,8 @@ import { parse } from "acorn";
import { LoadedModule, ScriptURL, ScriptModule } from "./Script/LoadedModule"; import { LoadedModule, ScriptURL, ScriptModule } from "./Script/LoadedModule";
import { Script } from "./Script/Script"; import { Script } from "./Script/Script";
import { removeLeadingSlash } from "./Terminal/DirectoryHelpers"; import { ScriptFilePath, resolveScriptFilePath } from "./Paths/ScriptFilePath";
import { ScriptFilename, scriptFilenameFromImport } from "./Types/strings"; import { root } from "./Paths/Directory";
// Acorn type def is straight up incomplete so we have to fill with our own. // Acorn type def is straight up incomplete so we have to fill with our own.
export type Node = any; 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 // Return the module if it already exists
if (script.mod) return script.mod.module; 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 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. * @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 // Early return for recursive calls where the script already has a URL
if (script.mod) { if (script.mod) {
addDependencyInfo(script, seenStack); 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, // 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. // preventing the ranges for other imports from being shifted.
importNodes.sort((a, b) => b.start - a.start); 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. // Loop through each node and replace the script name with a blob url.
for (const node of importNodes) { 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. // Find the corresponding script.
const importedScript = scripts.get(filename); 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. // 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 // We don't include this in the cache key, so that other instances of the
// script dedupe properly. // 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. // 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 url = URL.createObjectURL(makeScriptBlob(adjustedCode)) as ScriptURL;
const module = config.doImport(url).catch((e) => { const module = config.doImport(url).catch((e) => {

@ -32,7 +32,8 @@ import { simple as walksimple } from "acorn-walk";
import { Terminal } from "./Terminal"; import { Terminal } from "./Terminal";
import { ScriptArg } from "@nsdefs"; import { ScriptArg } from "@nsdefs";
import { handleUnknownError, CompleteRunOptions } from "./Netscript/NetscriptHelpers"; 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(); 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"); 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; return server.scripts.get(scriptName) ?? null;
} }
@ -157,11 +158,10 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c
walksimple(ast, { walksimple(ast, {
ImportDeclaration: (node: Node) => { ImportDeclaration: (node: Node) => {
hasImports = true; 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); const script = getScript(scriptName);
if (script == null) { if (!script) throw new Error("'Import' failed due to script not found: " + scriptName);
throw new Error("'Import' failed due to invalid script: " + scriptName);
}
const scriptAst = parse(script.code, { const scriptAst = parse(script.code, {
ecmaVersion: 9, ecmaVersion: 9,
allowReserved: true, allowReserved: true,
@ -380,16 +380,11 @@ export function loadAllRunningScripts(): void {
export function runScriptFromScript( export function runScriptFromScript(
caller: string, caller: string,
host: BaseServer, host: BaseServer,
scriptname: string, scriptname: ScriptFilePath,
args: ScriptArg[], args: ScriptArg[],
workerScript: WorkerScript, workerScript: WorkerScript,
runOpts: CompleteRunOptions, runOpts: CompleteRunOptions,
): number { ): 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); const script = host.scripts.get(scriptname);
if (!script) { if (!script) {
workerScript.log(caller, () => `Could not find script '${scriptname}' on '${host.hostname}'`); 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 { CompanyPosition } from "../../Company/CompanyPosition";
import * as posNames from "../../Company/data/JobTracks"; import * as posNames from "../../Company/data/JobTracks";
import { CONSTANTS } from "../../Constants"; import { CONSTANTS } from "../../Constants";
import { Programs } from "../../Programs/Programs"; import { CompletedProgramName } from "../../Programs/Programs";
import { Exploit } from "../../Exploits/Exploit"; import { Exploit } from "../../Exploits/Exploit";
import { Faction } from "../../Faction/Faction"; import { Faction } from "../../Faction/Faction";
import { Factions } from "../../Faction/Factions"; import { Factions } from "../../Faction/Factions";
@ -45,6 +45,7 @@ import { isCompanyWork } from "../../Work/CompanyWork";
import { serverMetadata } from "../../Server/data/servers"; import { serverMetadata } from "../../Server/data/servers";
import type { PlayerObject } from "./PlayerObject"; import type { PlayerObject } from "./PlayerObject";
import { ProgramFilePath } from "src/Paths/ProgramFilePath";
export function init(this: PlayerObject): void { export function init(this: PlayerObject): void {
/* Initialize Player's home computer */ /* Initialize Player's home computer */
@ -60,7 +61,7 @@ export function init(this: PlayerObject): void {
this.currentServer = SpecialServers.Home; this.currentServer = SpecialServers.Home;
AddToAllServers(t_homeComp); AddToAllServers(t_homeComp);
this.getHomeComputer().programs.push(Programs.NukeProgram.name); this.getHomeComputer().programs.push(CompletedProgramName.nuke);
} }
export function prestigeAugmentation(this: PlayerObject): void { export function prestigeAugmentation(this: PlayerObject): void {
@ -171,18 +172,9 @@ export function calculateSkillProgress(this: PlayerObject, exp: number, mult = 1
return calculateSkillProgressF(exp, mult); 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(); const home = this.getHomeComputer();
if (home == null) { return home.programs.includes(programName);
return false;
}
for (let i = 0; i < home.programs.length; ++i) {
if (programName.toLowerCase() == home.programs[i].toLowerCase()) {
return true;
}
}
return false;
} }
export function setMoney(this: PlayerObject, money: number): void { export function setMoney(this: PlayerObject, money: number): void {

@ -6,7 +6,7 @@ import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
import { initBitNodeMultipliers } from "./BitNode/BitNode"; import { initBitNodeMultipliers } from "./BitNode/BitNode";
import { Companies, initCompanies } from "./Company/Companies"; import { Companies, initCompanies } from "./Company/Companies";
import { resetIndustryResearchTrees } from "./Corporation/IndustryData"; import { resetIndustryResearchTrees } from "./Corporation/IndustryData";
import { Programs } from "./Programs/Programs"; import { CompletedProgramName } from "./Programs/Programs";
import { Factions, initFactions } from "./Faction/Factions"; import { Factions, initFactions } from "./Faction/Factions";
import { joinFaction } from "./Faction/FactionHelpers"; import { joinFaction } from "./Faction/FactionHelpers";
import { updateHashManagerCapacity } from "./Hacknet/HacknetHelpers"; import { updateHashManagerCapacity } from "./Hacknet/HacknetHelpers";
@ -14,7 +14,7 @@ import { prestigeWorkerScripts } from "./NetscriptWorker";
import { Player } from "@player"; import { Player } from "@player";
import { recentScripts } from "./Netscript/RecentScripts"; import { recentScripts } from "./Netscript/RecentScripts";
import { resetPidCounter } from "./Netscript/Pid"; 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 { GetServer, AddToAllServers, initForeignServers, prestigeAllServers } from "./Server/AllServers";
import { prestigeHomeComputer } from "./Server/ServerHelpers"; import { prestigeHomeComputer } from "./Server/ServerHelpers";
@ -53,20 +53,20 @@ export function prestigeAugmentation(): void {
prestigeHomeComputer(homeComp); prestigeHomeComputer(homeComp);
if (augmentationExists(AugmentationNames.Neurolink) && Player.hasAugmentation(AugmentationNames.Neurolink, true)) { if (augmentationExists(AugmentationNames.Neurolink) && Player.hasAugmentation(AugmentationNames.Neurolink, true)) {
homeComp.programs.push(Programs.FTPCrackProgram.name); homeComp.programs.push(CompletedProgramName.ftpCrack);
homeComp.programs.push(Programs.RelaySMTPProgram.name); homeComp.programs.push(CompletedProgramName.relaySmtp);
} }
if (augmentationExists(AugmentationNames.CashRoot) && Player.hasAugmentation(AugmentationNames.CashRoot, true)) { if (augmentationExists(AugmentationNames.CashRoot) && Player.hasAugmentation(AugmentationNames.CashRoot, true)) {
Player.setMoney(1e6); Player.setMoney(1e6);
homeComp.programs.push(Programs.BruteSSHProgram.name); homeComp.programs.push(CompletedProgramName.bruteSsh);
} }
if (augmentationExists(AugmentationNames.PCMatrix) && Player.hasAugmentation(AugmentationNames.PCMatrix, true)) { if (augmentationExists(AugmentationNames.PCMatrix) && Player.hasAugmentation(AugmentationNames.PCMatrix, true)) {
homeComp.programs.push(Programs.DeepscanV1.name); homeComp.programs.push(CompletedProgramName.deepScan1);
homeComp.programs.push(Programs.AutoLink.name); homeComp.programs.push(CompletedProgramName.autoLink);
} }
if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) { if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) {
homeComp.programs.push(Programs.Formulas.name); homeComp.programs.push(CompletedProgramName.formulas);
} }
// Re-create foreign servers // Re-create foreign servers
@ -123,7 +123,8 @@ export function prestigeAugmentation(): void {
// BitNode 3: Corporatocracy // BitNode 3: Corporatocracy
if (Player.bitNodeN === 3) { 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 // Cancel Bladeburner action
@ -247,15 +248,13 @@ export function prestigeSourceFile(flume: boolean): void {
initCompanies(); initCompanies();
if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) { 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 // BitNode 3: Corporatocracy
if (Player.bitNodeN === 3) { if (Player.bitNodeN === 3) {
console.log("why isn't the dialogbox happening?"); // Easiest way to comply with type constraint, instead of revalidating the enum member's file path
homeComp.messages.push(LiteratureNames.CorporationManagementHandbook); homeComp.messages.push(LiteratureName.CorporationManagementHandbook);
dialogBoxCreate( dialogBoxCreate(
"You received a copy of the Corporation Management Handbook on your home computer. " + "You received a copy of the Corporation Management Handbook on your home computer. " +
"Read it if you need help getting started with Corporations!", "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"; import { BaseServer } from "../Server/BaseServer";
export interface IProgramCreate { export interface IProgramCreate {
@ -6,14 +8,19 @@ export interface IProgramCreate {
time: number; time: number;
tooltip: string; tooltip: string;
} }
type ProgramConstructorParams = {
name: CompletedProgramName;
create: IProgramCreate | null;
run: (args: string[], server: BaseServer) => void;
};
export class Program { export class Program {
name = ""; name: ProgramFilePath & CompletedProgramName;
create: IProgramCreate | null; create: IProgramCreate | null;
run: (args: string[], server: BaseServer) => void; run: (args: string[], server: BaseServer) => void;
constructor(name: string, create: IProgramCreate | null, run: (args: string[], server: BaseServer) => void) { constructor({ name, create, run }: ProgramConstructorParams) {
this.name = name; this.name = asProgramFilePath(name);
this.create = create; this.create = create;
this.run = run; this.run = run;
} }

@ -1,4 +1,4 @@
import { Programs } from "./Programs"; import { CompletedProgramName, Programs } from "./Programs";
import { Program } from "./Program"; import { Program } from "./Program";
import { Player } from "@player"; import { Player } from "@player";
@ -6,18 +6,18 @@ import { Player } from "@player";
//Returns the programs this player can create. //Returns the programs this player can create.
export function getAvailableCreatePrograms(): Program[] { export function getAvailableCreatePrograms(): Program[] {
const programs: 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 // Non-creatable program
const create = Programs[key].create;
if (create == null) continue; if (create == null) continue;
// Already has program // Already has program
if (Player.hasProgram(Programs[key].name)) continue; if (Player.hasProgram(program)) continue;
// Does not meet requirements // Does not meet requirements
if (!create.req()) continue; if (!create.req()) continue;
programs.push(Programs[key]); programs.push(Programs[program]);
} }
return programs; return programs;

@ -1,9 +1,319 @@
import { Program } from "./Program"; 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> = {}; function requireHackingLevel(lvl: number) {
export function initPrograms() { return function () {
for (const params of programsMetadata) { return Player.skills.hacking + Player.skills.intelligence / 2 >= lvl;
Programs[params.key] = new Program(params.name, params.create, params.run); };
}
} }
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 { GetServer } from "../Server/AllServers";
import { isValidFilePath } from "../Terminal/DirectoryHelpers";
import { TextFile } from "../TextFile";
import { import {
RFAMessage, RFAMessage,
FileData, FileData,
@ -23,62 +23,48 @@ export const RFARequestHandler: Record<string, (message: RFAMessage) => void | R
if (!isFileData(msg.params)) return error("Misses parameters", msg); if (!isFileData(msg.params)) return error("Misses parameters", msg);
const fileData: FileData = msg.params; 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); const server = GetServer(fileData.server);
if (!server) return error("Server hostname invalid", msg); if (!server) return error("Server hostname invalid", msg);
if (isScriptFilename(fileData.filename)) server.writeToScriptFile(fileData.filename, fileData.content); if (hasTextExtension(filePath) || hasScriptExtension(filePath)) {
// Assume it's a text file server.writeToContentFile(filePath, fileData.content);
else server.writeToTextFile(fileData.filename, fileData.content);
// If and only if the content is actually changed correctly, send back an OK.
const savedCorrectly =
server.getScript(fileData.filename)?.code === fileData.content ||
server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0)?.text === fileData.content;
if (!savedCorrectly) return error("File wasn't saved correctly", msg);
return new RFAMessage({ result: "OK", id: msg.id }); return new RFAMessage({ result: "OK", id: msg.id });
}
return error("Invalid file extension", msg);
}, },
getFile: function (msg: RFAMessage): RFAMessage { getFile: function (msg: RFAMessage): RFAMessage {
if (!isFileLocation(msg.params)) return error("Message misses parameters", msg); if (!isFileLocation(msg.params)) return error("Message misses parameters", msg);
const fileData: FileLocation = msg.params; 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); const server = GetServer(fileData.server);
if (!server) return error("Server hostname invalid", msg); if (!server) return error("Server hostname invalid", msg);
if (isScriptFilename(fileData.filename)) { if (!hasTextExtension(filePath) && !hasScriptExtension(filePath)) return error("Invalid file extension", msg);
const scriptContent = server.getScript(fileData.filename); const file = server.getContentFile(filePath);
if (!scriptContent) return error("File doesn't exist", msg);
return new RFAMessage({ result: scriptContent.code, id: msg.id });
} else {
// Assume it's a text file
const file = server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0);
if (!file) return error("File doesn't exist", msg); 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 { deleteFile: function (msg: RFAMessage): RFAMessage {
if (!isFileLocation(msg.params)) return error("Message misses parameters", msg); if (!isFileLocation(msg.params)) return error("Message misses parameters", msg);
const fileData: FileLocation = msg.params; 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); const server = GetServer(fileData.server);
if (!server) return error("Server hostname invalid", msg); if (!server) return error("Server hostname invalid", msg);
const fileExists = (): boolean => const result = server.removeFile(filePath);
!!server.getScript(fileData.filename) || server.textFiles.some((t: TextFile) => t.filename === fileData.filename); if (result.res) return new RFAMessage({ result: "OK", id: msg.id });
return error(result.msg ?? "Failed", msg);
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 });
}, },
getFileNames: function (msg: RFAMessage): RFAMessage { getFileNames: function (msg: RFAMessage): RFAMessage {
@ -87,7 +73,7 @@ export const RFARequestHandler: Record<string, (message: RFAMessage) => void | R
const server = GetServer(msg.params.server); const server = GetServer(msg.params.server);
if (!server) return error("Server hostname invalid", msg); 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 }); 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); const server = GetServer(msg.params.server);
if (!server) return error("Server hostname invalid", msg); if (!server) return error("Server hostname invalid", msg);
const fileList: FileContent[] = [ const fileList: FileContent[] = [...server.scripts, ...server.textFiles].map(([filename, file]) => ({
...server.textFiles.map((txt): FileContent => { filename,
return { filename: txt.filename, content: txt.text }; content: file.content,
}), }));
];
for (const [filename, script] of server.scripts) fileList.push({ filename, content: script.code });
return new RFAMessage({ result: fileList, id: msg.id }); return new RFAMessage({ result: fileList, id: msg.id });
}, },
calculateRam: function (msg: RFAMessage): RFAMessage { calculateRam: function (msg: RFAMessage): RFAMessage {
if (!isFileLocation(msg.params)) return error("Message misses parameters", msg); if (!isFileLocation(msg.params)) return error("Message misses parameters", msg);
const fileData: FileLocation = msg.params; 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); const server = GetServer(fileData.server);
if (!server) return error("Server hostname invalid", msg); if (!server) return error("Server hostname invalid", msg);
if (!isScriptFilename(fileData.filename)) return error("Filename isn't a script filename", msg); if (!hasScriptExtension(filePath)) return error("Filename isn't a script filename", msg);
const script = server.getScript(fileData.filename); const script = server.scripts.get(filePath);
if (!script) return error("File doesn't exist", msg); if (!script) return error("File doesn't exist", msg);
const ramUsage = script.getRamUsage(server.scripts); const ramUsage = script.getRamUsage(server.scripts);
if (!ramUsage) return error("Ram cost could not be calculated", msg); 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 { v2APIBreak } from "./utils/v2APIBreak";
import { Script } from "./Script/Script"; import { Script } from "./Script/Script";
import { JSONMap } from "./Types/Jsonable"; 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 /* SaveObject.js
* Defines the object used to save/load games * 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 server of GetAllServers() as unknown as { scripts: Script[] }[]) {
for (const script of server.scripts) { 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; anyPlayer.currentWork = null;
} }
if (ver < 24) { 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) { if (ver < 25) {
const removePlayerFields = [ const removePlayerFields = [
@ -659,17 +666,43 @@ function evaluateVersionCompatibility(ver: string | number): void {
for (const sleeve of Player.sleeves) sleeve.shock = 100 - sleeve.shock; for (const sleeve of Player.sleeves) sleeve.shock = 100 - sleeve.shock;
} }
if (ver < 31) { if (ver < 31) {
if (anyPlayer.hashManager !== undefined) { if (anyPlayer.hashManager?.upgrades) {
anyPlayer.hashManager.upgrades["Company Favor"] ??= 0; anyPlayer.hashManager.upgrades["Company Favor"] ??= 0;
} }
anyPlayer.lastAugReset ??= anyPlayer.lastUpdate - anyPlayer.playtimeSinceLastAug; anyPlayer.lastAugReset ??= anyPlayer.lastUpdate - anyPlayer.playtimeSinceLastAug;
anyPlayer.lastNodeReset ??= anyPlayer.lastUpdate - anyPlayer.playtimeSinceLastBitnode; anyPlayer.lastNodeReset ??= anyPlayer.lastUpdate - anyPlayer.playtimeSinceLastBitnode;
const newDirectory = resolveDirectory("v2.3FileChanges/") as Directory;
for (const server of GetAllServers()) { for (const server of GetAllServers()) {
if (Array.isArray(server.scripts)) { let invalidScriptCount = 0;
const oldScripts = server.scripts as Script[]; // 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(); 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) { 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 { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator";
import { Script } from "./Script"; import { Script } from "./Script";
import { Node } from "../NetscriptJSEvaluator"; import { Node } from "../NetscriptJSEvaluator";
import { ScriptFilename, scriptFilenameFromImport } from "../Types/strings"; import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath";
import { root } from "../Paths/Directory";
export interface RamUsageEntry { export interface RamUsageEntry {
type: "ns" | "dom" | "fn" | "misc"; type: "ns" | "dom" | "fn" | "misc";
@ -38,9 +39,9 @@ const memCheckGlobalKey = ".__GLOBAL__";
/** /**
* Parses code into an AST and walks through it recursively to calculate * Parses code into an AST and walks through it recursively to calculate
* RAM usage. Also accounts for imported modules. * RAM usage. Also accounts for imported modules.
* @param {Script[]} otherScripts - All other scripts on the server. Used to account for imported scripts * @param otherScripts - All other scripts on the server. Used to account for imported scripts
* @param {string} code - The code being parsed */ * @param code - The code being parsed */
function parseOnlyRamCalculate(otherScripts: Map<ScriptFilename, Script>, code: string, ns1?: boolean): RamCalculation { function parseOnlyRamCalculate(otherScripts: Map<ScriptFilePath, Script>, code: string, ns1?: boolean): RamCalculation {
try { try {
/** /**
* Maps dependent identifiers to their dependencies. * 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 === undefined) throw new Error("nextModule should not be undefined");
if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue; 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); const script = otherScripts.get(filename);
if (!script) { if (!script) return { cost: RamCalculationErrorCode.ImportError }; // No such file on server
return { cost: RamCalculationErrorCode.ImportError }; // No such script on the server
}
parseCode(script.code, nextModule); parseCode(script.code, nextModule);
} }
@ -370,7 +371,7 @@ function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsR
*/ */
export function calculateRamUsage( export function calculateRamUsage(
codeCopy: string, codeCopy: string,
otherScripts: Map<ScriptFilename, Script>, otherScripts: Map<ScriptFilePath, Script>,
ns1?: boolean, ns1?: boolean,
): RamCalculation { ): RamCalculation {
try { try {

@ -14,6 +14,7 @@ import { ScriptArg } from "@nsdefs";
import { RamCostConstants } from "../Netscript/RamCostGenerator"; import { RamCostConstants } from "../Netscript/RamCostGenerator";
import { PositiveInteger } from "../types"; import { PositiveInteger } from "../types";
import { getKeyList } from "../utils/helpers/getKeyList"; import { getKeyList } from "../utils/helpers/getKeyList";
import { ScriptFilePath } from "../Paths/ScriptFilePath";
export class RunningScript { export class RunningScript {
// Script arguments // Script arguments
@ -24,7 +25,7 @@ export class RunningScript {
dataMap: Record<string, number[]> = {}; dataMap: Record<string, number[]> = {};
// Script filename // Script filename
filename = "default.js"; filename = "default.js" as ScriptFilePath;
// This script's logs. An array of log entries // This script's logs. An array of log entries
logs: React.ReactNode[] = []; logs: React.ReactNode[] = [];

@ -1,20 +1,19 @@
/** import type { BaseServer } from "../Server/BaseServer";
* Class representing a script file.
*
* This does NOT represent a script that is actively running and
* being evaluated. See RunningScript for that
*/
import { calculateRamUsage, RamUsageEntry } from "./RamCalculations"; import { calculateRamUsage, RamUsageEntry } from "./RamCalculations";
import { LoadedModule, ScriptURL } from "./LoadedModule"; import { LoadedModule, ScriptURL } from "./LoadedModule";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { roundToTwo } from "../utils/helpers/roundToTwo"; import { roundToTwo } from "../utils/helpers/roundToTwo";
import { RamCostConstants } from "../Netscript/RamCostGenerator"; 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 { /** A script file as a file on a server.
code: string; * For the execution of a script, see RunningScript and WorkerScript */
filename: string; export class Script implements ContentFile {
code: FormattedCode;
filename: ScriptFilePath;
server: string; server: string;
// Ram calculation, only exists after first poll of ram cost after updating // 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(); 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.filename = fn;
this.code = code; this.code = Script.formatCode(code);
this.server = server; // hostname of server this script is on this.server = server; // hostname of server this script is on
} }
@ -71,18 +80,19 @@ export class Script {
/** /**
* Save a script from the script editor * Save a script from the script editor
* @param {string} code - The new contents of the script * @param filename The new filepath for this Script
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports * @param code The unformatted code to save
* @param hostname The server to save the script to
*/ */
saveScript(filename: string, code: string, hostname: string): void { saveScript(filename: ScriptFilePath, code: string, hostname: string): void {
this.invalidateModule();
this.code = Script.formatCode(code); this.code = Script.formatCode(code);
this.invalidateModule();
this.filename = filename; this.filename = filename;
this.server = hostname; this.server = hostname;
} }
/** Gets the ram usage, while also attempting to update it if it's currently null */ /** 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; if (this.ramUsage) return this.ramUsage;
this.updateRamUsage(otherScripts); this.updateRamUsage(otherScripts);
return this.ramUsage; return this.ramUsage;
@ -92,7 +102,7 @@ export class Script {
* Calculates and updates the script's RAM usage based on its code * Calculates and updates the script's RAM usage based on its code
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports * @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")); const ramCalc = calculateRamUsage(this.code, otherScripts, this.filename.endsWith(".script"));
if (ramCalc.cost >= RamCostConstants.Base) { if (ramCalc.cost >= RamCostConstants.Base) {
this.ramUsage = roundToTwo(ramCalc.cost); 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 */ /** The keys that are relevant in a save file */
static savedKeys = ["code", "filename", "server"] as const; static savedKeys = ["code", "filename", "server"] as const;
@ -120,8 +138,8 @@ export class Script {
* @param {string} code - The code to format * @param {string} code - The code to format
* @returns The formatted code * @returns The formatted code
*/ */
static formatCode(code: string): string { static formatCode(code: string): FormattedCode {
return code.replace(/^\s+|\s+$/g, ""); return code.replace(/^\s+|\s+$/g, "") as FormattedCode;
} }
} }

@ -5,9 +5,8 @@ import { Server } from "../Server/Server";
import { RunningScript } from "./RunningScript"; import { RunningScript } from "./RunningScript";
import { processSingleServerGrowth } from "../Server/ServerHelpers"; import { processSingleServerGrowth } from "../Server/ServerHelpers";
import { GetServer } from "../Server/AllServers"; import { GetServer } from "../Server/AllServers";
import { formatPercent } from "../ui/formatNumber"; import { formatPercent } from "../ui/formatNumber";
import { workerScripts } from "../Netscript/WorkerScripts";
import { compareArrays } from "../utils/helpers/compareArrays"; import { compareArrays } from "../utils/helpers/compareArrays";
export function scriptCalculateOfflineProduction(runningScript: RunningScript): void { export function scriptCalculateOfflineProduction(runningScript: RunningScript): void {
@ -99,10 +98,9 @@ export function findRunningScript(
//Returns a RunningScript object matching the pid on the //Returns a RunningScript object matching the pid on the
//designated server, and false otherwise //designated server, and false otherwise
export function findRunningScriptByPid(pid: number, server: BaseServer): RunningScript | null { export function findRunningScriptByPid(pid: number, server: BaseServer): RunningScript | null {
for (let i = 0; i < server.runningScripts.length; ++i) { const ws = workerScripts.get(pid);
if (server.runningScripts[i].pid === pid) { // Return null if no ws found or if it's on a different server.
return server.runningScripts[i]; if (!ws) return null;
} if (ws.scriptRef.server !== server.hostname) return null;
} return ws.scriptRef;
return null;
} }

@ -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; type ITextModel = monaco.editor.ITextModel;
import { OptionsModal } from "./OptionsModal"; import { OptionsModal } from "./OptionsModal";
import { Options } from "./Options"; import { Options } from "./Options";
import { isValidFilePath } from "../../Terminal/DirectoryHelpers";
import { Player } from "@player"; import { Player } from "@player";
import { Router } from "../../ui/GameRoot"; import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router"; import { Page } from "../../ui/Router";
import { dialogBoxCreate } from "../../ui/React/DialogBox"; import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { isScriptFilename } from "../../Script/isScriptFilename"; import { ScriptFilePath } from "../../Paths/ScriptFilePath";
import { Script } from "../../Script/Script"; import { Script } from "../../Script/Script";
import { TextFile } from "../../TextFile";
import { calculateRamUsage, checkInfiniteLoop } from "../../Script/RamCalculations"; import { calculateRamUsage, checkInfiniteLoop } from "../../Script/RamCalculations";
import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes"; import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes";
import { formatRam } from "../../ui/formatNumber"; import { formatRam } from "../../ui/formatNumber";
@ -48,10 +46,12 @@ import libSource from "!!raw-loader!../NetscriptDefinitions.d.ts";
import { TextField, Tooltip } from "@mui/material"; import { TextField, Tooltip } from "@mui/material";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
import { NetscriptExtra } from "../../NetscriptFunctions/Extra"; import { NetscriptExtra } from "../../NetscriptFunctions/Extra";
import { TextFilePath } from "src/Paths/TextFilePath";
import { ContentFilePath } from "src/Paths/ContentFile";
interface IProps { interface IProps {
// Map of filename -> code // Map of filename -> code
files: Record<string, string>; files: Map<ScriptFilePath | TextFilePath, string>;
hostname: string; hostname: string;
vim: boolean; vim: boolean;
} }
@ -75,20 +75,20 @@ export function SetupTextEditor(): void {
// Holds all the data for a open script // Holds all the data for a open script
class OpenScript { class OpenScript {
fileName: string; path: ContentFilePath;
code: string; code: string;
hostname: string; hostname: string;
lastPosition: monaco.Position; lastPosition: monaco.Position;
model: ITextModel; model: ITextModel;
isTxt: boolean; isTxt: boolean;
constructor(fileName: string, code: string, hostname: string, lastPosition: monaco.Position, model: ITextModel) { constructor(path: ContentFilePath, code: string, hostname: string, lastPosition: monaco.Position, model: ITextModel) {
this.fileName = fileName; this.path = path;
this.code = code; this.code = code;
this.hostname = hostname; this.hostname = hostname;
this.lastPosition = lastPosition; this.lastPosition = lastPosition;
this.model = model; 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); }, 300);
function updateRAM(newCode: string): void { function updateRAM(newCode: string): void {
if (currentScript != null && currentScript.isTxt) { if (!currentScript || currentScript.isTxt) {
setRAM("N/A"); setRAM("N/A");
setRamEntries([["N/A", ""]]); setRamEntries([["N/A", ""]]);
return; return;
@ -338,18 +338,16 @@ export function Root(props: IProps): React.ReactElement {
return; return;
} }
if (props.files) { if (props.files) {
const files = Object.entries(props.files); const files = props.files;
if (!files.length) { if (!files.size) {
editorRef.current.focus(); editorRef.current.focus();
return; return;
} }
for (const [filename, code] of files) { for (const [filename, code] of files) {
// Check if file is already opened // Check if file is already opened
const openScript = openScripts.find( const openScript = openScripts.find((script) => script.path === filename && script.hostname === props.hostname);
(script) => script.fileName === filename && script.hostname === props.hostname,
);
if (openScript) { if (openScript) {
// Script is already opened // Script is already opened
if (openScript.model === undefined || openScript.model === null || openScript.model.isDisposed()) { 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 { function infLoop(newCode: string): void {
if (editorRef.current === null || currentScript === null) return; if (editorRef.current === null || currentScript === null) return;
if (!currentScript.fileName.endsWith(".js")) return; if (!currentScript.path.endsWith(".js")) return;
const awaitWarning = checkInfiniteLoop(newCode); const awaitWarning = checkInfiniteLoop(newCode);
if (awaitWarning !== -1) { if (awaitWarning !== -1) {
const newDecorations = editorRef.current.deltaDecorations(decorations, [ const newDecorations = editorRef.current.deltaDecorations(decorations, [
@ -429,37 +427,9 @@ export function Root(props: IProps): React.ReactElement {
function saveScript(scriptToSave: OpenScript): void { function saveScript(scriptToSave: OpenScript): void {
const server = GetServer(scriptToSave.hostname); const server = GetServer(scriptToSave.hostname);
if (server === null) throw new Error("Server should not be null but it is."); if (!server) throw new Error("Server should not be null but it is.");
if (isScriptFilename(scriptToSave.fileName)) { // This server helper already handles overwriting, etc.
//If the current script already exists on the server, overwrite it server.writeToContentFile(scriptToSave.path, scriptToSave.code);
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 (Settings.SaveGameOnFileSave) saveObject.saveGame(); if (Settings.SaveGameOnFileSave) saveObject.saveGame();
Router.toPage(Page.Terminal); Router.toPage(Page.Terminal);
} }
@ -472,7 +442,7 @@ export function Root(props: IProps): React.ReactElement {
// this is duplicate code with saving later. // this is duplicate code with saving later.
if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) { if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) {
//Make sure filename + code properly follow tutorial //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."); dialogBoxCreate("Don't change the script name for now.");
return; return;
} }
@ -492,50 +462,9 @@ export function Root(props: IProps): React.ReactElement {
return; 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); const server = GetServer(currentScript.hostname);
if (server === null) throw new Error("Server should not be null but it is."); if (server === null) throw new Error("Server should not be null but it is.");
if (isScriptFilename(currentScript.fileName)) { server.writeToContentFile(currentScript.path, currentScript.code);
//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;
}
if (Settings.SaveGameOnFileSave) saveObject.saveGame(); if (Settings.SaveGameOnFileSave) saveObject.saveGame();
rerender(); rerender();
} }
@ -555,9 +484,7 @@ export function Root(props: IProps): React.ReactElement {
if (currentScript !== null) { if (currentScript !== null) {
return openScripts.findIndex( return openScripts.findIndex(
(script) => (script) =>
currentScript !== null && currentScript !== null && script.path === currentScript.path && script.hostname === currentScript.hostname,
script.fileName === currentScript.fileName &&
script.hostname === currentScript.hostname,
); );
} }
@ -596,7 +523,7 @@ export function Root(props: IProps): React.ReactElement {
if (dirty(index)) { if (dirty(index)) {
PromptEvent.emit({ 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) => { resolve: (result: boolean | string) => {
if (result) { if (result) {
// Save changes // Save changes
@ -641,7 +568,7 @@ export function Root(props: IProps): React.ReactElement {
PromptEvent.emit({ PromptEvent.emit({
txt: txt:
"Do you want to overwrite the current editor content with the contents of " + "Do you want to overwrite the current editor content with the contents of " +
openScript.fileName + openScript.path +
" on the server? This cannot be undone.", " on the server? This cannot be undone.",
resolve: (result: boolean | string) => { resolve: (result: boolean | string) => {
if (result) { if (result) {
@ -679,10 +606,8 @@ export function Root(props: IProps): React.ReactElement {
const openScript = openScripts[index]; const openScript = openScripts[index];
const server = GetServer(openScript.hostname); const server = GetServer(openScript.hostname);
if (server === null) throw new Error(`Server '${openScript.hostname}' should not be null, but it is.`); if (server === null) throw new Error(`Server '${openScript.hostname}' should not be null, but it is.`);
const data = openScript.isTxt const data = server.getContentFile(openScript.path)?.content ?? null;
? server.textFiles.find((t) => t.filename === openScript.fileName)?.text return data;
: server.scripts.get(openScript.fileName)?.code;
return data ?? null;
} }
function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>): void { function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>): void {
setFilter(event.target.value); setFilter(event.target.value);
@ -692,7 +617,7 @@ export function Root(props: IProps): React.ReactElement {
setSearchExpanded(!searchExpanded); setSearchExpanded(!searchExpanded);
} }
const filteredOpenScripts = Object.values(openScripts).filter( 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; const tabsMaxWidth = 1640;
@ -747,9 +672,9 @@ export function Root(props: IProps): React.ReactElement {
</Button> </Button>
)} )}
</Tooltip> </Tooltip>
{filteredOpenScripts.map(({ fileName, hostname }, index) => { {filteredOpenScripts.map(({ path: fileName, hostname }, index) => {
const editingCurrentScript = const editingCurrentScript =
currentScript?.fileName === filteredOpenScripts[index].fileName && currentScript?.path === filteredOpenScripts[index].path &&
currentScript?.hostname === filteredOpenScripts[index].hostname; currentScript?.hostname === filteredOpenScripts[index].hostname;
const externalScript = hostname !== "home"; const externalScript = hostname !== "home";
const colorProps = editingCurrentScript const colorProps = editingCurrentScript

@ -2,17 +2,23 @@ import type { Server as IServer } from "@nsdefs";
import { CodingContract } from "../CodingContracts"; import { CodingContract } from "../CodingContracts";
import { RunningScript } from "../Script/RunningScript"; import { RunningScript } from "../Script/RunningScript";
import { Script } from "../Script/Script"; import { Script } from "../Script/Script";
import { isValidFilePath } from "../Terminal/DirectoryHelpers";
import { TextFile } from "../TextFile"; import { TextFile } from "../TextFile";
import { IReturnStatus } from "../types"; 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 { createRandomIp } from "../utils/IPAddress";
import { compareArrays } from "../utils/helpers/compareArrays"; import { compareArrays } from "../utils/helpers/compareArrays";
import { ScriptArg } from "../Netscript/ScriptArg"; import { ScriptArg } from "../Netscript/ScriptArg";
import { JSONMap } from "../Types/Jsonable"; 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 { interface IConstructorParams {
adminRights?: boolean; adminRights?: boolean;
@ -24,7 +30,6 @@ interface IConstructorParams {
} }
interface writeResult { interface writeResult {
success: boolean;
overwritten: boolean; overwritten: boolean;
} }
@ -59,14 +64,15 @@ export abstract class BaseServer implements IServer {
maxRam = 0; maxRam = 0;
// Message files AND Literature files on this Server // Message files AND Literature files on this Server
messages: string[] = []; messages: (MessageFilename | LiteratureName | FilePath)[] = [];
// Name of company/faction/etc. that this server belongs to. // Name of company/faction/etc. that this server belongs to.
// Optional, not applicable to all Servers // Optional, not applicable to all Servers
organizationName = ""; organizationName = "";
// Programs on this servers. Contains only the names of the programs // 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 // RAM (GB) used. i.e. unavailable RAM
ramUsed = 0; ramUsed = 0;
@ -75,7 +81,7 @@ export abstract class BaseServer implements IServer {
runningScripts: RunningScript[] = []; runningScripts: RunningScript[] = [];
// Script files on this Server // 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 // Contains the hostnames of all servers that are immediately
// reachable from this one // reachable from this one
@ -91,7 +97,7 @@ export abstract class BaseServer implements IServer {
sshPortOpen = false; sshPortOpen = false;
// Text files on this server // Text files on this server
textFiles: TextFile[] = []; textFiles: JSONMap<TextFilePath, TextFile> = new JSONMap();
// Flag indicating whether this is a purchased server // Flag indicating whether this is a purchased server
purchasedByPlayer = false; purchasedByPlayer = false;
@ -132,34 +138,17 @@ export abstract class BaseServer implements IServer {
return null; return null;
} }
/** /** Find an actively running script on this server by filepath and args. */
* Find an actively running script on this server getRunningScript(path: ScriptFilePath, scriptArgs: ScriptArg[]): RunningScript | null {
* @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 {
for (const rs of this.runningScripts) { for (const rs of this.runningScripts) {
//compare file names without leading '/' to prevent running multiple script with the same name if (rs.filename === path && compareArrays(rs.args, scriptArgs)) return rs;
if (
(rs.filename.charAt(0) == "/" ? rs.filename.slice(1) : rs.filename) ===
(scriptName.charAt(0) == "/" ? scriptName.slice(1) : scriptName) &&
compareArrays(rs.args, scriptArgs)
) {
return rs;
} }
}
return null; return null;
} }
/** /** Get a TextFile or Script depending on the input path type. */
* Given the name of the script, returns the corresponding getContentFile(path: ContentFilePath): ContentFile | null {
* Script object on the server (if it exists) return (hasTextExtension(path) ? this.textFiles.get(path) : this.scripts.get(path)) ?? null;
*/
getScript(scriptName: string): Script | null {
return this.scripts.get(scriptName) ?? null;
} }
/** Returns boolean indicating whether the given script is running on this server */ /** 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 * 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 * @returns {IReturnStatus} Return status object indicating whether or not file was deleted
*/ */
removeFile(filename: string): IReturnStatus { removeFile(path: FilePath): IReturnStatus {
if (filename.endsWith(".exe") || filename.match(/^.+\.exe-\d+(?:\.\d*)?%-INC$/) != null) { if (hasTextExtension(path)) {
for (let i = 0; i < this.programs.length; ++i) { const textFile = this.textFiles.get(path);
if (this.programs[i] === filename) { if (!textFile) return { res: false, msg: `Text file ${path} not found.` };
this.programs.splice(i, 1); this.textFiles.delete(path);
return { res: true }; return { res: true };
} }
} if (hasScriptExtension(path)) {
} else if (isScriptFilename(filename)) { const script = this.scripts.get(path);
const script = this.scripts.get(filename); if (!script) return { res: false, msg: `Script ${path} not found.` };
if (!script) return { res: false, msg: `script ${filename} not found.` }; if (this.isRunning(path)) return { res: false, msg: "Cannot delete a script that is currently running!" };
if (this.isRunning(filename)) {
return { res: false, msg: "Cannot delete a script that is currently running!" };
}
script.invalidateModule(); script.invalidateModule();
this.scripts.delete(filename); this.scripts.delete(path);
return { res: true };
} else if (filename.endsWith(".lit")) {
for (let i = 0; i < this.messages.length; ++i) {
const f = this.messages[i];
if (typeof f === "string" && f === filename) {
this.messages.splice(i, 1);
return { res: true }; return { res: true };
} }
} if (hasProgramExtension(path)) {
} else if (filename.endsWith(".txt")) { const programIndex = this.programs.findIndex((program) => program === path);
for (let i = 0; i < this.textFiles.length; ++i) { if (programIndex === -1) return { res: false, msg: `Program ${path} does not exist` };
if (this.textFiles[i].fn === filename) { this.programs.splice(programIndex, 1);
this.textFiles.splice(i, 1);
return { res: true }; return { res: true };
} }
} if (path.endsWith(".lit")) {
} else if (filename.endsWith(".cct")) { const litIndex = this.messages.findIndex((lit) => lit === path);
for (let i = 0; i < this.contracts.length; ++i) { if (litIndex === -1) return { res: false, msg: `Literature file ${path} does not exist` };
if (this.contracts[i].fn === filename) { this.messages.splice(litIndex, 1);
this.contracts.splice(i, 1);
return { res: true }; 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; this.ramUsed = ram;
} }
pushProgram(program: string): void { pushProgram(program: ProgramFilePath | CompletedProgramName): void {
if (this.programs.includes(program)) return; if (this.programs.includes(program)) return;
// Remove partially created program if there is one // Remove partially created program if there is one
const existingPartialExeIndex = this.programs.findIndex((p) => p.startsWith(program)); 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 // findIndex returns -1 if there is no match, we only want to splice on a match
if (existingPartialExeIndex > -1) { if (existingPartialExeIndex > -1) this.programs.splice(existingPartialExeIndex, 1);
this.programs.splice(existingPartialExeIndex, 1);
}
this.programs.push(program); this.programs.push(program);
} }
@ -262,47 +242,41 @@ export abstract class BaseServer implements IServer {
* Write to a script file * Write to a script file
* Overwrites existing files. Creates new files if the script does not exist. * Overwrites existing files. Creates new files if the script does not exist.
*/ */
writeToScriptFile(filename: string, code: string): writeResult { writeToScriptFile(filename: ScriptFilePath, code: string): writeResult {
if (!isValidFilePath(filename) || !isScriptFilename(filename)) {
return { success: false, overwritten: false };
}
// Check if the script already exists, and overwrite it if it does // Check if the script already exists, and overwrite it if it does
const script = this.scripts.get(filename); const script = this.scripts.get(filename);
if (script) { if (script) {
script.invalidateModule(); // content setter handles module invalidation and code formatting
script.code = code; script.content = code;
return { success: true, overwritten: true }; return { overwritten: true };
} }
// Otherwise, create a new script // Otherwise, create a new script
const newScript = new Script(filename, code, this.hostname); const newScript = new Script(filename, code, this.hostname);
this.scripts.set(filename, newScript); this.scripts.set(filename, newScript);
return { success: true, overwritten: false }; return { overwritten: false };
} }
// Write to a text file // Write to a text file
// Overwrites existing files. Creates new files if the text file does not exist // Overwrites existing files. Creates new files if the text file does not exist
writeToTextFile(fn: string, txt: string): writeResult { writeToTextFile(textPath: TextFilePath, txt: string): writeResult {
const ret = { success: false, overwritten: false };
if (!isValidFilePath(fn) || !fn.endsWith("txt")) {
return ret;
}
// Check if the text file already exists, and overwrite if it does // Check if the text file already exists, and overwrite if it does
for (let i = 0; i < this.textFiles.length; ++i) { const existingFile = this.textFiles.get(textPath);
if (this.textFiles[i].fn === fn) { // overWrite if already exists
ret.overwritten = true; if (existingFile) {
this.textFiles[i].text = txt; existingFile.text = txt;
ret.success = true; return { overwritten: true };
return ret;
}
} }
// Otherwise create a new text file // Otherwise create a new text file
const newFile = new TextFile(fn, txt); const newFile = new TextFile(textPath, txt);
this.textFiles.push(newFile); this.textFiles.set(textPath, newFile);
ret.success = true; return { overwritten: false };
return ret; }
/** 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 { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { CONSTANTS } from "../Constants"; import { CONSTANTS } from "../Constants";
import { Player } from "@player"; import { Player } from "@player";
import { Programs } from "../Programs/Programs"; import { CompletedProgramName } from "../Programs/Programs";
import { LiteratureNames } from "../Literature/data/LiteratureNames"; import { LiteratureName } from "../Literature/data/LiteratureNames";
import { Person as IPerson } from "@nsdefs"; import { Person as IPerson } from "@nsdefs";
import { isValidNumber } from "../utils/helpers/isValidNumber"; import { isValidNumber } from "../utils/helpers/isValidNumber";
import { Server as IServer } from "@nsdefs"; import { Server as IServer } from "@nsdefs";
@ -249,20 +249,20 @@ export function processSingleServerGrowth(server: Server, threads: number, cores
} }
export function prestigeHomeComputer(homeComp: Server): void { 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.programs.length = 0; //Remove programs
homeComp.runningScripts = []; homeComp.runningScripts = [];
homeComp.serversOnNetwork = []; homeComp.serversOnNetwork = [];
homeComp.isConnectedTo = true; homeComp.isConnectedTo = true;
homeComp.ramUsed = 0; homeComp.ramUsed = 0;
homeComp.programs.push(Programs.NukeProgram.name); homeComp.programs.push(CompletedProgramName.nuke);
if (hasBitflume) { if (hasBitflume) {
homeComp.programs.push(Programs.BitFlume.name); homeComp.programs.push(CompletedProgramName.bitFlume);
} }
homeComp.messages.length = 0; //Remove .lit and .msg files 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 // 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... // This could actually be a JSON file as it should be constant metadata to be imported...
import { IMinMaxRange } from "../../types"; import { IMinMaxRange } from "../../types";
import { LocationName } from "../../Enums"; import { LocationName } from "../../Enums";
import { LiteratureNames } from "../../Literature/data/LiteratureNames"; import { LiteratureName } from "../../Literature/data/LiteratureNames";
import { SpecialServers } from "./SpecialServers"; import { SpecialServers } from "./SpecialServers";
import { ServerName } from "../../Types/strings";
/** /**
* The metadata describing the base state of servers on the network. * The metadata describing the base state of servers on the network.
@ -16,10 +17,10 @@ interface IServerMetadata {
hackDifficulty?: number | IMinMaxRange; hackDifficulty?: number | IMinMaxRange;
/** The DNS name of the server. */ /** The DNS name of the server. */
hostname: string; hostname: ServerName;
/** When populated, the files will be added to the server when created. */ /** 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. * When populated, the exponent of 2^x amount of RAM the server has.
@ -118,7 +119,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 88, min: 88,
}, },
hostname: "blade", hostname: "blade",
literature: [LiteratureNames.BeyondMan], literature: [LiteratureName.BeyondMan],
maxRamExponent: { maxRamExponent: {
max: 9, max: 9,
min: 5, min: 5,
@ -143,7 +144,7 @@ export const serverMetadata: IServerMetadata[] = [
{ {
hackDifficulty: 99, hackDifficulty: 99,
hostname: LocationName.VolhavenNWO.toLowerCase(), hostname: LocationName.VolhavenNWO.toLowerCase(),
literature: [LiteratureNames.TheHiddenWorld], literature: [LiteratureName.TheHiddenWorld],
moneyAvailable: { moneyAvailable: {
max: 40e9, max: 40e9,
min: 20e9, min: 20e9,
@ -167,7 +168,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 45, min: 45,
}, },
hostname: "clarkinc", hostname: "clarkinc",
literature: [LiteratureNames.BeyondMan, LiteratureNames.CostOfImmortality], literature: [LiteratureName.BeyondMan, LiteratureName.CostOfImmortality],
moneyAvailable: { moneyAvailable: {
max: 25e9, max: 25e9,
min: 15e9, min: 15e9,
@ -191,7 +192,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 90, min: 90,
}, },
hostname: "omnitek", hostname: "omnitek",
literature: [LiteratureNames.CodedIntelligence, LiteratureNames.HistoryOfSynthoids], literature: [LiteratureName.CodedIntelligence, LiteratureName.HistoryOfSynthoids],
maxRamExponent: { maxRamExponent: {
max: 9, max: 9,
min: 7, min: 7,
@ -265,7 +266,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 83, min: 83,
}, },
hostname: "fulcrumtech", hostname: "fulcrumtech",
literature: [LiteratureNames.SimulatedReality], literature: [LiteratureName.SimulatedReality],
maxRamExponent: { maxRamExponent: {
max: 11, max: 11,
min: 7, min: 7,
@ -375,7 +376,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 85, min: 85,
}, },
hostname: "helios", hostname: "helios",
literature: [LiteratureNames.BeyondMan], literature: [LiteratureName.BeyondMan],
maxRamExponent: { maxRamExponent: {
max: 8, max: 8,
min: 5, min: 5,
@ -403,7 +404,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 80, min: 80,
}, },
hostname: LocationName.NewTokyoVitaLife.toLowerCase(), hostname: LocationName.NewTokyoVitaLife.toLowerCase(),
literature: [LiteratureNames.AGreenTomorrow], literature: [LiteratureName.AGreenTomorrow],
maxRamExponent: { maxRamExponent: {
max: 7, max: 7,
min: 4, min: 4,
@ -481,7 +482,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 70, min: 70,
}, },
hostname: "titan-labs", hostname: "titan-labs",
literature: [LiteratureNames.CodedIntelligence], literature: [LiteratureName.CodedIntelligence],
maxRamExponent: { maxRamExponent: {
max: 7, max: 7,
min: 4, min: 4,
@ -508,7 +509,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 65, min: 65,
}, },
hostname: "microdyne", hostname: "microdyne",
literature: [LiteratureNames.SyntheticMuscles], literature: [LiteratureName.SyntheticMuscles],
maxRamExponent: { maxRamExponent: {
max: 6, max: 6,
min: 4, min: 4,
@ -535,7 +536,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 70, min: 70,
}, },
hostname: "taiyang-digital", hostname: "taiyang-digital",
literature: [LiteratureNames.AGreenTomorrow, LiteratureNames.BrighterThanTheSun], literature: [LiteratureName.AGreenTomorrow, LiteratureName.BrighterThanTheSun],
moneyAvailable: { moneyAvailable: {
max: 900000000, max: 900000000,
min: 800000000, min: 800000000,
@ -581,7 +582,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 80, min: 80,
}, },
hostname: LocationName.AevumAeroCorp.toLowerCase(), hostname: LocationName.AevumAeroCorp.toLowerCase(),
literature: [LiteratureNames.ManAndMachine], literature: [LiteratureName.ManAndMachine],
moneyAvailable: { moneyAvailable: {
max: 1200000000, max: 1200000000,
min: 1000000000, min: 1000000000,
@ -605,7 +606,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 85, min: 85,
}, },
hostname: "omnia", hostname: "omnia",
literature: [LiteratureNames.HistoryOfSynthoids], literature: [LiteratureName.HistoryOfSynthoids],
maxRamExponent: { maxRamExponent: {
max: 6, max: 6,
min: 4, min: 4,
@ -633,7 +634,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 55, min: 55,
}, },
hostname: "zb-def", hostname: "zb-def",
literature: [LiteratureNames.SyntheticMuscles], literature: [LiteratureName.SyntheticMuscles],
moneyAvailable: { moneyAvailable: {
max: 1100000000, max: 1100000000,
min: 900000000, min: 900000000,
@ -678,7 +679,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 70, min: 70,
}, },
hostname: "solaris", hostname: "solaris",
literature: [LiteratureNames.AGreenTomorrow, LiteratureNames.TheFailedFrontier], literature: [LiteratureName.AGreenTomorrow, LiteratureName.TheFailedFrontier],
maxRamExponent: { maxRamExponent: {
max: 7, max: 7,
min: 4, min: 4,
@ -729,7 +730,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 75, min: 75,
}, },
hostname: "global-pharm", hostname: "global-pharm",
literature: [LiteratureNames.AGreenTomorrow], literature: [LiteratureName.AGreenTomorrow],
maxRamExponent: { maxRamExponent: {
max: 6, max: 6,
min: 3, min: 3,
@ -882,7 +883,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 50, min: 50,
}, },
hostname: "alpha-ent", hostname: "alpha-ent",
literature: [LiteratureNames.Sector12Crime], literature: [LiteratureName.Sector12Crime],
maxRamExponent: { maxRamExponent: {
max: 7, max: 7,
min: 4, min: 4,
@ -937,11 +938,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 45, min: 45,
}, },
hostname: "rothman-uni", hostname: "rothman-uni",
literature: [ literature: [LiteratureName.SecretSocieties, LiteratureName.TheFailedFrontier, LiteratureName.TensionsInTechRace],
LiteratureNames.SecretSocieties,
LiteratureNames.TheFailedFrontier,
LiteratureNames.TensionsInTechRace,
],
maxRamExponent: { maxRamExponent: {
max: 7, max: 7,
min: 4, min: 4,
@ -996,7 +993,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 45, min: 45,
}, },
hostname: "summit-uni", hostname: "summit-uni",
literature: [LiteratureNames.SecretSocieties, LiteratureNames.TheFailedFrontier, LiteratureNames.SyntheticMuscles], literature: [LiteratureName.SecretSocieties, LiteratureName.TheFailedFrontier, LiteratureName.SyntheticMuscles],
maxRamExponent: { maxRamExponent: {
max: 6, max: 6,
min: 4, min: 4,
@ -1047,7 +1044,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 60, min: 60,
}, },
hostname: "catalyst", hostname: "catalyst",
literature: [LiteratureNames.TensionsInTechRace], literature: [LiteratureName.TensionsInTechRace],
maxRamExponent: { maxRamExponent: {
max: 7, max: 7,
min: 4, min: 4,
@ -1100,7 +1097,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 55, min: 55,
}, },
hostname: LocationName.VolhavenCompuTek.toLowerCase(), hostname: LocationName.VolhavenCompuTek.toLowerCase(),
literature: [LiteratureNames.ManAndMachine], literature: [LiteratureName.ManAndMachine],
moneyAvailable: { moneyAvailable: {
max: 250000000, max: 250000000,
min: 220000000, min: 220000000,
@ -1124,7 +1121,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 60, min: 60,
}, },
hostname: "netlink", hostname: "netlink",
literature: [LiteratureNames.SimulatedReality], literature: [LiteratureName.SimulatedReality],
maxRamExponent: { maxRamExponent: {
max: 7, max: 7,
min: 4, min: 4,
@ -1181,7 +1178,7 @@ export const serverMetadata: IServerMetadata[] = [
{ {
hackDifficulty: 10, hackDifficulty: 10,
hostname: LocationName.Sector12FoodNStuff.toLowerCase(), hostname: LocationName.Sector12FoodNStuff.toLowerCase(),
literature: [LiteratureNames.Sector12Crime], literature: [LiteratureName.Sector12Crime],
maxRamExponent: 4, maxRamExponent: 4,
moneyAvailable: 2000000, moneyAvailable: 2000000,
networkLayer: 1, networkLayer: 1,
@ -1239,7 +1236,7 @@ export const serverMetadata: IServerMetadata[] = [
{ {
hackDifficulty: 25, hackDifficulty: 25,
hostname: "neo-net", hostname: "neo-net",
literature: [LiteratureNames.TheHiddenWorld], literature: [LiteratureName.TheHiddenWorld],
maxRamExponent: 5, maxRamExponent: 5,
moneyAvailable: 5000000, moneyAvailable: 5000000,
networkLayer: 3, networkLayer: 3,
@ -1251,7 +1248,7 @@ export const serverMetadata: IServerMetadata[] = [
{ {
hackDifficulty: 30, hackDifficulty: 30,
hostname: "silver-helix", hostname: "silver-helix",
literature: [LiteratureNames.NewTriads], literature: [LiteratureName.NewTriads],
maxRamExponent: 6, maxRamExponent: 6,
moneyAvailable: 45000000, moneyAvailable: 45000000,
networkLayer: 3, networkLayer: 3,
@ -1263,7 +1260,7 @@ export const serverMetadata: IServerMetadata[] = [
{ {
hackDifficulty: 15, hackDifficulty: 15,
hostname: "hong-fang-tea", hostname: "hong-fang-tea",
literature: [LiteratureNames.BrighterThanTheSun], literature: [LiteratureName.BrighterThanTheSun],
maxRamExponent: 4, maxRamExponent: 4,
moneyAvailable: 3000000, moneyAvailable: 3000000,
networkLayer: 1, networkLayer: 1,
@ -1311,7 +1308,7 @@ export const serverMetadata: IServerMetadata[] = [
min: 25, min: 25,
}, },
hostname: "omega-net", hostname: "omega-net",
literature: [LiteratureNames.TheNewGod], literature: [LiteratureName.TheNewGod],
maxRamExponent: 5, maxRamExponent: 5,
moneyAvailable: { moneyAvailable: {
max: 70000000, max: 70000000,
@ -1436,7 +1433,7 @@ export const serverMetadata: IServerMetadata[] = [
{ {
hackDifficulty: 0, hackDifficulty: 0,
hostname: "run4theh111z", hostname: "run4theh111z",
literature: [LiteratureNames.SimulatedReality, LiteratureNames.TheNewGod], literature: [LiteratureName.SimulatedReality, LiteratureName.TheNewGod],
maxRamExponent: { maxRamExponent: {
max: 9, max: 9,
min: 5, min: 5,
@ -1455,7 +1452,7 @@ export const serverMetadata: IServerMetadata[] = [
{ {
hackDifficulty: 0, hackDifficulty: 0,
hostname: "I.I.I.I", hostname: "I.I.I.I",
literature: [LiteratureNames.DemocracyIsDead], literature: [LiteratureName.DemocracyIsDead],
maxRamExponent: { maxRamExponent: {
max: 8, max: 8,
min: 4, min: 4,
@ -1474,7 +1471,7 @@ export const serverMetadata: IServerMetadata[] = [
{ {
hackDifficulty: 0, hackDifficulty: 0,
hostname: "avmnite-02h", hostname: "avmnite-02h",
literature: [LiteratureNames.DemocracyIsDead], literature: [LiteratureName.DemocracyIsDead],
maxRamExponent: { maxRamExponent: {
max: 7, max: 7,
min: 4, min: 4,
@ -1508,7 +1505,7 @@ export const serverMetadata: IServerMetadata[] = [
{ {
hackDifficulty: 0, hackDifficulty: 0,
hostname: "CSEC", hostname: "CSEC",
literature: [LiteratureNames.DemocracyIsDead], literature: [LiteratureName.DemocracyIsDead],
maxRamExponent: 3, maxRamExponent: 3,
moneyAvailable: 0, moneyAvailable: 0,
networkLayer: 2, networkLayer: 2,
@ -1524,7 +1521,7 @@ export const serverMetadata: IServerMetadata[] = [
{ {
hackDifficulty: 0, hackDifficulty: 0,
hostname: "The-Cave", hostname: "The-Cave",
literature: [LiteratureNames.AlphaOmega], literature: [LiteratureName.AlphaOmega],
moneyAvailable: 0, moneyAvailable: 0,
networkLayer: 15, networkLayer: 15,
numOpenPortsRequired: 5, 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"; import { substituteAliases } from "../Alias";
// Helper function to parse individual arguments into number/boolean/string as appropriate // Helper function to parse individual arguments into number/boolean/string as appropriate
function parseArg(arg: string, stringOverride: boolean): string | number | boolean { function parseArg(arg: string): string | number | boolean {
// Handles all numbers including hexadecimal, octal, and binary representations, returning NaN on an unparsable string if (arg === "true") return true;
if (stringOverride) return arg; if (arg === "false") return false;
const asNumber = Number(arg); const argAsNumber = Number(arg);
if (!isNaN(asNumber)) { if (!isNaN(argAsNumber)) return argAsNumber;
return asNumber; // 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; return arg;
} }
export function ParseCommands(commands: string): string[] { /** Split a commands string (what is typed into the terminal) into multiple commands */
// Sanitize input export function splitCommands(commandString: string): string[] {
commands = commands.trim(); const commandArray = commandString.match(/(?:'[^']*'|"[^"]*"|[^;"])*/g);
// Replace all extra whitespace in command with a single space if (!commandArray) return [];
commands = commands.replace(/\s\s+/g, " "); return commandArray.map((command) => command.trim());
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;
} }
export function ParseCommand(command: string): (string | number | boolean)[] { /** Split commands string while also applying aliases */
let idx = 0; export function parseCommands(commands: string): string[] {
const args = []; // Remove any unquoted whitespace longer than length 1
commands = commands.replace(/(?:"[^"]+"|'[^']+'|\s{2,})+?/g, (match) => (match.startsWith(" ") ? " " : match));
let lastQuote = ""; // Split the commands, apply aliases once, then split again and filter out empty strings.
const commandsArr = splitCommands(commands).map(substituteAliases).flatMap(splitCommands).filter(Boolean);
let arg = ""; return commandsArr;
let stringOverride = false; }
while (idx < command.length) { export function parseCommand(command: string): (string | number | boolean)[] {
const c = command.charAt(idx); const commandArgs = command.match(/(?:("[^"]+"|'[^']+'|[^\s]+))+?/g);
// If the current character is a backslash, add the next character verbatim to the argument if (!commandArgs) return [];
if (c === "\\") { const argsToReturn = commandArgs.map(parseArg);
arg += command.charAt(++idx); return argsToReturn;
// 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;
} }

@ -4,21 +4,20 @@ import { Player } from "@player";
import { HacknetServer } from "../Hacknet/HacknetServer"; import { HacknetServer } from "../Hacknet/HacknetServer";
import { BaseServer } from "../Server/BaseServer"; import { BaseServer } from "../Server/BaseServer";
import { Server } from "../Server/Server"; import { Server } from "../Server/Server";
import { Programs } from "../Programs/Programs"; import { CompletedProgramName } from "../Programs/Programs";
import { CodingContractResult } from "../CodingContracts"; import { CodingContractResult } from "../CodingContracts";
import { TerminalEvents, TerminalClearEvents } from "./TerminalEvents"; import { TerminalEvents, TerminalClearEvents } from "./TerminalEvents";
import { TextFile } from "../TextFile"; import { TextFile } from "../TextFile";
import { Script } from "../Script/Script"; import { Script } from "../Script/Script";
import { isScriptFilename } from "../Script/isScriptFilename"; import { hasScriptExtension } from "../Paths/ScriptFilePath";
import { CONSTANTS } from "../Constants"; import { CONSTANTS } from "../Constants";
import { GetServer, GetAllServers } from "../Server/AllServers"; import { GetServer, GetAllServers } from "../Server/AllServers";
import { removeLeadingSlash, isInRootDirectory, evaluateFilePath } from "./DirectoryHelpers";
import { checkIfConnectedToDarkweb } from "../DarkWeb/DarkWeb"; import { checkIfConnectedToDarkweb } from "../DarkWeb/DarkWeb";
import { iTutorialNextStep, iTutorialSteps, ITutorial } from "../InteractiveTutorial"; import { iTutorialNextStep, iTutorialSteps, ITutorial } from "../InteractiveTutorial";
import { getServerOnNetwork, processSingleServerGrowth } from "../Server/ServerHelpers"; import { getServerOnNetwork, processSingleServerGrowth } from "../Server/ServerHelpers";
import { ParseCommand, ParseCommands } from "./Parser"; import { parseCommand, parseCommands } from "./Parser";
import { SpecialServers } from "../Server/data/SpecialServers"; import { SpecialServers } from "../Server/data/SpecialServers";
import { Settings } from "../Settings/Settings"; import { Settings } from "../Settings/Settings";
import { createProgressBarText } from "../utils/helpers/createProgressBarText"; import { createProgressBarText } from "../utils/helpers/createProgressBarText";
@ -77,6 +76,10 @@ import { apr1 } from "./commands/apr1";
import { changelog } from "./commands/changelog"; import { changelog } from "./commands/changelog";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { Engine } from "../engine"; 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 { export class Terminal {
// Flags to determine whether the player is currently running a hack or an analyze // 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 // True if a Coding Contract prompt is opened
contractOpen = false; contractOpen = false;
// Full Path of current directory // Path of current directory
// Excludes the trailing forward slash currDir = "" as Directory;
currDir = "/";
process(cycles: number): void { process(cycles: number): void {
if (this.action === null) return; if (this.action === null) return;
@ -377,46 +379,40 @@ export class Terminal {
} }
getFile(filename: string): Script | TextFile | string | null { getFile(filename: string): Script | TextFile | string | null {
if (isScriptFilename(filename)) { if (hasScriptExtension(filename)) return this.getScript(filename);
return this.getScript(filename); if (hasTextExtension(filename)) return this.getTextFile(filename);
} if (filename.endsWith(".lit")) return this.getLitFile(filename);
if (filename.endsWith(".lit")) {
return this.getLitFile(filename);
}
if (filename.endsWith(".txt")) {
return this.getTextFile(filename);
}
return null; return null;
} }
getFilepath(filename: string): string | null { getFilepath(path: string, useAbsolute?: boolean): FilePath | null {
const path = evaluateFilePath(filename, this.cwd()); // If path starts with a slash, consider it to be an absolute path
if (path === null || !isInRootDirectory(path)) return 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 { getScript(filename: string): Script | null {
const server = Player.getCurrentServer(); const server = Player.getCurrentServer();
const filepath = this.getFilepath(filename); const filepath = this.getFilepath(filename);
if (filepath === null) return null; if (!filepath || !hasScriptExtension(filepath)) return null;
return server.scripts.get(filepath) ?? null; return server.scripts.get(filepath) ?? null;
} }
getTextFile(filename: string): TextFile | null { getTextFile(filename: string): TextFile | null {
const s = Player.getCurrentServer(); const server = Player.getCurrentServer();
const filepath = this.getFilepath(filename); const filepath = this.getFilepath(filename);
if (!filepath) return null; if (!filepath || !hasTextExtension(filepath)) return null;
for (const txt of s.textFiles) { return server.textFiles.get(filepath) ?? null;
if (filepath === txt.fn) {
return txt;
}
}
return null;
} }
getLitFile(filename: string): string | null { getLitFile(filename: string): string | null {
@ -432,32 +428,30 @@ export class Terminal {
return null; return null;
} }
cwd(): string { cwd(): Directory {
return this.currDir; return this.currDir;
} }
setcwd(dir: string): void { setcwd(dir: Directory): void {
this.currDir = dir; this.currDir = dir;
TerminalEvents.emit(); TerminalEvents.emit();
} }
async runContract(contractName: string): Promise<void> { async runContract(contractPath: ContractFilePath): Promise<void> {
// There's already an opened contract // There's already an opened contract
if (this.contractOpen) { if (this.contractOpen) {
return this.error("There's already a Coding Contract in Progress"); return this.error("There's already a Coding Contract in Progress");
} }
const serv = Player.getCurrentServer(); const serv = Player.getCurrentServer();
const contract = serv.getContract(contractName); const contract = serv.getContract(contractPath);
if (contract == null) { if (!contract) return this.error("No such contract");
return this.error("No such contract");
}
this.contractOpen = true; this.contractOpen = true;
const res = await contract.prompt(); const res = await contract.prompt();
//Check if the contract still exists by the time the promise is fulfilled //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; this.contractOpen = false;
return this.error("Contract no longer exists (Was it solved by a script?)"); return this.error("Contract no longer exists (Was it solved by a script?)");
} }
@ -529,7 +523,7 @@ export class Terminal {
continue; continue;
} // Don't print current server } // Don't print current server
const titleDashes = Array((d - 1) * 4 + 1).join("-"); 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)); this.append(new Link(titleDashes, s.hostname));
} else { } else {
this.print(titleDashes + s.hostname); this.print(titleDashes + s.hostname);
@ -564,17 +558,13 @@ export class Terminal {
Player.currentServer = serv.hostname; Player.currentServer = serv.hostname;
Player.getCurrentServer().isConnectedTo = true; Player.getCurrentServer().isConnectedTo = true;
this.print("Connected to " + serv.hostname); this.print("Connected to " + serv.hostname);
this.setcwd("/"); this.setcwd(root);
if (Player.getCurrentServer().hostname == "darkweb") { if (Player.getCurrentServer().hostname == "darkweb") {
checkIfConnectedToDarkweb(); // Posts a 'help' message if connecting to dark web checkIfConnectedToDarkweb(); // Posts a 'help' message if connecting to dark web
} }
} }
executeCommands(commands: string): void { 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 // Handle Terminal History - multiple commands should be saved as one
if (this.commandHistory[this.commandHistory.length - 1] != commands) { if (this.commandHistory[this.commandHistory.length - 1] != commands) {
this.commandHistory.push(commands); this.commandHistory.push(commands);
@ -584,11 +574,8 @@ export class Terminal {
Player.terminalCommandHistory = this.commandHistory; Player.terminalCommandHistory = this.commandHistory;
} }
this.commandHistoryIndex = this.commandHistory.length; this.commandHistoryIndex = this.commandHistory.length;
const allCommands = ParseCommands(commands); const allCommands = parseCommands(commands);
for (const command of allCommands) this.executeCommand(command);
for (let i = 0; i < allCommands.length; i++) {
this.executeCommand(allCommands[i]);
}
} }
clear(): void { clear(): void {
@ -603,20 +590,12 @@ export class Terminal {
} }
executeCommand(command: string): void { executeCommand(command: string): void {
if (this.action !== null) { if (this.action !== null) return this.error(`Cannot execute command (${command}) while an action is in progress`);
this.error(`Cannot execute command (${command}) while an action is in progress`);
return; const commandArray = parseCommand(command);
} if (!commandArray.length) return;
// Allow usage of ./
if (command.startsWith("./")) { const currentServer = Player.getCurrentServer();
command = "run " + command.slice(2);
}
// Only split the first space
const commandArray = ParseCommand(command);
if (commandArray.length == 0) {
return;
}
const s = Player.getCurrentServer();
/****************** Interactive Tutorial Terminal Commands ******************/ /****************** Interactive Tutorial Terminal Commands ******************/
if (ITutorial.isRunning) { if (ITutorial.isRunning) {
const n00dlesServ = GetServer("n00dles"); const n00dlesServ = GetServer("n00dles");
@ -769,11 +748,14 @@ export class Terminal {
} }
/****************** END INTERACTIVE TUTORIAL ******************/ /****************** END INTERACTIVE TUTORIAL ******************/
/* Command parser */ /* Command parser */
const commandName = commandArray[0]; const commandName = commandArray[0];
if (typeof commandName === "number" || typeof commandName === "boolean") { if (typeof commandName !== "string") return this.error(`${commandName} is not a valid command.`);
this.error(`Command ${commandArray[0]} not found`); // run by path command
return; 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: { const commands: {
[key: string]: (args: (string | number | boolean)[], server: BaseServer) => void; [key: string]: (args: (string | number | boolean)[], server: BaseServer) => void;
@ -823,12 +805,9 @@ export class Terminal {
}; };
const f = commands[commandName.toLowerCase()]; const f = commands[commandName.toLowerCase()];
if (!f) { if (!f) return this.error(`Command ${commandName} not found`);
this.error(`Command ${commandArray[0]} not found`);
return;
}
f(commandArray.slice(1), s); f(commandArray, currentServer);
} }
getProgressText(): string { getProgressText(): string {

@ -1,59 +1,35 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { MessageFilenames, showMessage } from "../../Message/MessageHelpers"; import { MessageFilename, showMessage } from "../../Message/MessageHelpers";
import { showLiterature } from "../../Literature/LiteratureHelpers"; import { showLiterature } from "../../Literature/LiteratureHelpers";
import { dialogBoxCreate } from "../../ui/React/DialogBox"; import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { checkEnum } from "../../utils/helpers/enum"; 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 { export function cat(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length !== 1) { if (args.length !== 1) return Terminal.error("Incorrect usage of cat command. Usage: cat [file]");
Terminal.error("Incorrect usage of cat command. Usage: cat [file]");
return;
}
const relative_filename = args[0] + ""; const relative_filename = args[0] + "";
const filename = Terminal.getFilepath(relative_filename); const path = Terminal.getFilepath(relative_filename);
if (!filename) return Terminal.error(`Invalid filename: ${relative_filename}`); if (!path) return Terminal.error(`Invalid filename: ${relative_filename}`);
if (
!filename.endsWith(".msg") && if (hasScriptExtension(path) || hasTextExtension(path)) {
!filename.endsWith(".lit") && const file = server.getContentFile(path);
!filename.endsWith(".txt") && if (!file) return Terminal.error(`No file at path ${path}`);
!filename.endsWith(".script") && return dialogBoxCreate(`${file.filename}\n\n${file.content}`);
!filename.endsWith(".js") }
) { if (!path.endsWith(".msg") && !path.endsWith(".lit")) {
Terminal.error( return Terminal.error("Invalid file extension. Filename must end with .msg, .txt, .lit, .script or .js");
"Only .msg, .txt, .lit, .script and .js files are viewable with cat (filename must end with .msg, .txt, .lit, .script or .js)",
);
return;
} }
if (filename.endsWith(".msg") || filename.endsWith(".lit")) { // Message
for (let i = 0; i < server.messages.length; ++i) { if (checkEnum(MessageFilename, path)) {
if (filename.endsWith(".lit") && server.messages[i] === filename) { if (server.messages.includes(path)) showMessage(path);
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;
} }
if (checkEnum(LiteratureName, path)) {
if (server.messages.includes(path)) showLiterature(path);
} }
} else if (filename.endsWith(".txt")) { Terminal.error(`No file at path ${path}`);
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}`);
} }

@ -1,38 +1,14 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { directoryExistsOnServer, resolveDirectory } from "../../Paths/Directory";
import { evaluateDirectoryPath, removeTrailingSlash } from "../DirectoryHelpers";
import { containsFiles } from "../DirectoryServerHelpers";
export function cd(args: (string | number | boolean)[], server: BaseServer): void { export function cd(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length > 1) { if (args.length > 1) return Terminal.error("Incorrect number of arguments. Usage: cd [dir]");
Terminal.error("Incorrect number of arguments. Usage: cd [dir]"); // If no arg was provided, just use "".
} else { const userInput = String(args[0] ?? "");
let dir = args.length === 1 ? args[0] + "" : "/"; const targetDir = resolveDirectory(userInput, Terminal.currDir);
// Explicitly checking null due to root being ""
let evaledDir: string | null = ""; if (targetDir === null) return Terminal.error(`Could not resolve directory ${userInput}`);
if (dir === "/") { if (!directoryExistsOnServer(targetDir, server)) return Terminal.error(`Directory ${targetDir} does not exist.`);
evaledDir = "/"; Terminal.setcwd(targetDir);
} 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);
}
} }

@ -1,7 +1,7 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { findRunningScript } from "../../Script/ScriptHelpers"; 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 { export function check(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length < 1) { 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]}`); if (!scriptName) return Terminal.error(`Invalid filename: ${args[0]}`);
// Can only tail script files // Can only tail script files
if (!isScriptFilename(scriptName)) { if (!hasScriptExtension(scriptName)) {
Terminal.error( return Terminal.error(`check: File extension must be one of ${validScriptExtensions.join(", ")})`);
`'check' can only be called on scripts files (filename must end with ${validScriptExtensions.join(", ")})`,
);
return;
} }
// Check that the script is running on this machine // Check that the script is running on this machine
const runningScript = findRunningScript(scriptName, args.slice(1), server); const runningScript = findRunningScript(scriptName, args.slice(1), server);
if (runningScript == null) { if (runningScript == null) return Terminal.error(`No script named ${scriptName} is running on the server`);
Terminal.error(`No script named ${scriptName} is running on the server`);
return;
}
runningScript.displayLog(); runningScript.displayLog();
} }
} }

@ -1,13 +1,13 @@
import { Terminal } from "../../../Terminal"; import { Terminal } from "../../../Terminal";
import { removeLeadingSlash, removeTrailingSlash } from "../../DirectoryHelpers";
import { ScriptEditorRouteOptions } from "../../../ui/Router"; import { ScriptEditorRouteOptions } from "../../../ui/Router";
import { Router } from "../../../ui/GameRoot"; import { Router } from "../../../ui/GameRoot";
import { BaseServer } from "../../../Server/BaseServer"; import { BaseServer } from "../../../Server/BaseServer";
import { isScriptFilename } from "../../../Script/isScriptFilename";
import { CursorPositions } from "../../../ScriptEditor/CursorPositions"; import { CursorPositions } from "../../../ScriptEditor/CursorPositions";
import { Script } from "../../../Script/Script"; import { ScriptFilePath, hasScriptExtension } from "../../../Paths/ScriptFilePath";
import { isEmpty } from "lodash"; import { TextFilePath, hasTextExtension } from "../../../Paths/TextFilePath";
import { ScriptFilename } from "src/Types/strings"; 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 { interface EditorParameters {
args: (string | number | boolean)[]; 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( export function commonEditor(
command: string, command: string,
{ args, server }: EditorParameters, { args, server }: EditorParameters,
scriptEditorRouteOptions?: ScriptEditorRouteOptions, scriptEditorRouteOptions?: ScriptEditorRouteOptions,
): void { ): void {
if (args.length < 1) { if (args.length < 1) return Terminal.error(`Incorrect usage of ${command} command. Usage: ${command} [scriptname]`);
Terminal.error(`Incorrect usage of ${command} command. Usage: ${command} [scriptname]`); const filesToOpen: Map<ScriptFilePath | TextFilePath, string> = new Map();
return; 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; // Non-glob, files do not need to already exist
try { const path = Terminal.getFilepath(pattern);
const globSearch = detectSimpleScriptGlob({ args, server }); if (!path) return Terminal.error(`Invalid file path ${arg}`);
if (globSearch) { if (!hasScriptExtension(path) && !hasTextExtension(path)) {
if (isEmpty(globSearch.globError) === false) throw new Error(globSearch.globError); return Terminal.error(`${command}: Only scripts or text files can be edited. Invalid file type: ${arg}`);
filesToLoadOrCreate = globSearch.globMatches;
} }
const file = server.getContentFile(path);
const files = filesToLoadOrCreate.map((arg) => { const content = file ? file.content : isNs2(path) ? newNs2Template : "";
const filename = `${arg}`; filesToOpen.set(path, content);
if (content === newNs2Template) CursorPositions.saveCursor(path, { row: 3, column: 5 });
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}`);
} }
Router.toScriptEditor(filesToOpen, scriptEditorRouteOptions);
} }

@ -1,76 +1,36 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { isScriptFilename } from "../../Script/isScriptFilename"; import { combinePath, getFilenameOnly } from "../../Paths/FilePath";
import { getDestinationFilepath, areFilesEqual } from "../DirectoryHelpers"; import { hasTextExtension } from "../../Paths/TextFilePath";
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
export function cp(args: (string | number | boolean)[], server: BaseServer): void { export function cp(args: (string | number | boolean)[], server: BaseServer): void {
try { if (args.length !== 2) {
if (args.length !== 2) return Terminal.error("Incorrect usage of cp command. Usage: cp [src] [dst]"); return Terminal.error("Incorrect usage of cp command. Usage: cp [source filename] [destination]");
// Convert a relative path source file to the absolute path. }
const src = Terminal.getFilepath(args[0] + ""); // Find the source file
if (src === null) return Terminal.error(`Invalid source filename ${args[0]}`); 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 // Determine the destination file path.
const t_dst = getDestinationFilepath(args[1] + "", src, Terminal.cwd()); const destinationInput = String(args[1]);
if (t_dst === null) return Terminal.error("error parsing dst file"); // 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);
// Convert a relative path destination file to the absolute path. if (!destFilePath) {
const dst = Terminal.getFilepath(t_dst); const destDirectory = Terminal.getDirectory(destinationInput);
if (!dst) return Terminal.error(`Invalid destination filename ${t_dst}`); if (!destDirectory) return Terminal.error(`Could not resolve ${destinationInput} as a FilePath or Directory`);
if (areFilesEqual(src, dst)) return Terminal.error("src and dst cannot be the same"); destFilePath = combinePath(destDirectory, getFilenameOnly(sourceFilePath));
}
const srcExt = src.slice(src.lastIndexOf(".")); if (!hasTextExtension(destFilePath) && !hasScriptExtension(destFilePath)) {
const dstExt = dst.slice(dst.lastIndexOf(".")); return Terminal.error(`cp: Can only copy to script and text files (${destFilePath} is invalid destination)`);
if (srcExt !== dstExt) return Terminal.error("src and dst must have the same extension.");
if (!isScriptFilename(src) && !src.endsWith(".txt")) {
return Terminal.error("cp only works for scripts and .txt files");
} }
// Scp for txt files const result = server.writeToContentFile(destFilePath, source.content);
if (src.endsWith(".txt")) { Terminal.print(`File ${sourceFilePath} copied to ${destFilePath}`);
let txtFile = null; if (result.overwritten) Terminal.warn(`${destFilePath} was overwritten.`);
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 + "");
}
} }

@ -1,79 +1,49 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { isScriptFilename } from "../../Script/isScriptFilename";
import FileSaver from "file-saver"; import FileSaver from "file-saver";
import JSZip from "jszip"; 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 { // Basic globbing implementation only supporting * and ?. Can be broken out somewhere else later.
const matchEnding = pattern.length == 1 || pattern === "*.*" ? null : pattern.slice(1); // Treat *.* the same as * export function exportScripts(pattern: string, server: BaseServer, currDir = root): void {
const zip = new JSZip(); const zip = new JSZip();
// Helper function to zip any file contents whose name matches the pattern
const zipFiles = (fileNames: string[], fileContents: string[]): void => { for (const [name, file] of getGlobbedFileMap(pattern, server, currDir)) {
for (let i = 0; i < fileContents.length; ++i) { zip.file(name, new Blob([file.content], { type: "text/plain" }));
let name = fileNames[i];
if (name.startsWith("/")) name = name.slice(1);
if (!matchEnding || name.endsWith(matchEnding))
zip.file(name, new Blob([fileContents[i]], { type: "text/plain" }));
} }
};
// In the case of script files, we pull from the server.scripts array
if (!matchEnding || isScriptFilename(matchEnding))
zipFiles(
[...server.scripts.keys()],
[...server.scripts.values()].map((script) => script.code),
);
// In the case of text files, we pull from the server.scripts array
if (!matchEnding || matchEnding.endsWith(".txt"))
zipFiles(
server.textFiles.map((s) => s.fn),
server.textFiles.map((s) => s.text),
);
// Return an error if no files matched, rather than an empty zip folder // 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}`); 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)); zip.generateAsync({ type: "blob" }).then((content: Blob) => FileSaver.saveAs(content, zipFn));
} }
export function download(args: (string | number | boolean)[], server: BaseServer): void { export function download(args: (string | number | boolean)[], server: BaseServer): void {
try {
if (args.length !== 1) { if (args.length !== 1) {
Terminal.error("Incorrect usage of download command. Usage: download [script/text file]"); return Terminal.error("Incorrect usage of download command. Usage: download [script/text file]");
return;
} }
const fn = args[0] + ""; const pattern = String(args[0]);
// If the parameter starts with *, download all files that match the wildcard pattern // If the path contains a * or ?, treat as glob
if (fn.startsWith("*")) { if (pattern.includes("*") || pattern.includes("?")) {
try { try {
exportScripts(fn, server); exportScripts(pattern, server, Terminal.currDir);
return; return;
} catch (e: unknown) { } catch (e: any) {
let msg = String(e); const msg = String(e?.message ?? e);
if (e !== null && typeof e == "object" && e.hasOwnProperty("message")) {
msg = String((e as { message: unknown }).message);
}
return Terminal.error(msg); 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")) { const path = Terminal.getFilepath(pattern);
// Download a single text file if (!path) return Terminal.error(`Could not resolve path ${pattern}`);
const txt = Terminal.getTextFile(fn); if (!hasScriptExtension(path) && !hasTextExtension(path)) {
if (txt != null) { return Terminal.error("Can only download script and text files");
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 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 { Terminal } from "../../Terminal";
import { Player } from "@player";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { Server } from "../../Server/Server";
export function grow(args: (string | number | boolean)[], server: BaseServer): void { export function grow(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length !== 0) { if (args.length !== 0) return Terminal.error("Incorrect usage of grow command. Usage: grow");
Terminal.error("Incorrect usage of grow command. Usage: grow");
return;
}
if (!(server instanceof Server)) { if (server.purchasedByPlayer) return Terminal.error("Cannot grow your own machines!");
Terminal.error( if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!");
"Cannot grow your own machines! You are currently connected to your home PC or one of your purchased servers", // 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.");
}
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;
}
Terminal.startGrow(); Terminal.startGrow();
} }

@ -1,37 +1,17 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { Player } from "@player"; import { Player } from "@player";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { Server } from "../../Server/Server";
export function hack(args: (string | number | boolean)[], server: BaseServer): void { export function hack(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length !== 0) { if (args.length !== 0) return Terminal.error("Incorrect usage of hack command. Usage: hack");
Terminal.error("Incorrect usage of hack command. Usage: hack"); if (server.purchasedByPlayer) return Terminal.error("Cannot hack your own machines!");
return; 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 instanceof Server)) { if (server.requiredHackingSkill > Player.skills.hacking) {
Terminal.error( return Terminal.error(
"Cannot hack your own machines! You are currently connected to your home PC or one of your purchased servers", "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(); Terminal.startHack();
} }

@ -1,3 +1,4 @@
import { root } from "../../Paths/Directory";
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { Player } from "@player"; import { Player } from "@player";
@ -10,5 +11,5 @@ export function home(args: (string | number | boolean)[]): void {
Player.currentServer = Player.getHomeComputer().hostname; Player.currentServer = Player.getHomeComputer().hostname;
Player.getCurrentServer().isConnectedTo = true; Player.getCurrentServer().isConnectedTo = true;
Terminal.print("Connected to home"); Terminal.print("Connected to home");
Terminal.setcwd("/"); Terminal.setcwd(root);
} }

@ -1,40 +1,25 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { killWorkerScript } from "../../Netscript/killWorkerScript"; import { killWorkerScript } from "../../Netscript/killWorkerScript";
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
export function kill(args: (string | number | boolean)[], server: BaseServer): void { export function kill(args: (string | number | boolean)[], server: BaseServer): void {
try {
if (args.length < 1) { if (args.length < 1) {
Terminal.error("Incorrect usage of kill command. Usage: kill [scriptname] [arg1] [arg2]..."); return Terminal.error("Incorrect usage of kill command. Usage: kill [pid] or kill [scriptname] [arg1] [arg2]...");
return;
} }
if (typeof args[0] === "boolean") {
return;
}
// Kill by PID
if (typeof args[0] === "number") { if (typeof args[0] === "number") {
const pid = args[0]; const pid = args[0];
const res = killWorkerScript(pid); if (killWorkerScript(pid)) return Terminal.print(`Killing script with PID ${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`);
} }
// 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]); killWorkerScript(runningScript.pid);
if (!scriptName) return Terminal.error(`Invalid filename: ${args[0]}`); Terminal.print(`Killing ${path}`);
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 + "");
}
} }

@ -1,15 +1,28 @@
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import makeStyles from "@mui/styles/makeStyles"; import makeStyles from "@mui/styles/makeStyles";
import { toString } from "lodash";
import React from "react"; import React from "react";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { evaluateDirectoryPath, getFirstParentDirectory, isValidDirectoryPath } from "../DirectoryHelpers";
import { Router } from "../../ui/GameRoot"; import { Router } from "../../ui/GameRoot";
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import libarg from "arg"; import libarg from "arg";
import { showLiterature } from "../../Literature/LiteratureHelpers"; 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 { export function ls(args: (string | number | boolean)[], server: BaseServer): void {
interface LSFlags { interface LSFlags {
@ -31,7 +44,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
incorrectUsage(); incorrectUsage();
return; return;
} }
const filter = flags["--grep"]; const filter = flags["--grep"] ?? "";
const numArgs = args.length; const numArgs = args.length;
function incorrectUsage(): void { function incorrectUsage(): void {
@ -42,74 +55,48 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
return incorrectUsage(); return incorrectUsage();
} }
// Directory path let baseDirectory = Terminal.currDir;
let prefix = Terminal.cwd(); // Parse first argument which should be a directory.
if (!prefix.endsWith("/")) { if (args[0] && typeof args[0] == "string" && !args[0].startsWith("-")) {
prefix += "/"; const directory = resolveDirectory(args[0], args[0].startsWith("/") ? root : Terminal.currDir);
} if (directory !== null && directoryExistsOnServer(directory, server)) {
baseDirectory = directory;
// If first arg doesn't contain a - it must be the file/folder } else return incorrectUsage();
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 = "";
} }
// Display all programs and scripts // Display all programs and scripts
const allPrograms: string[] = []; const allPrograms: ProgramFilePath[] = [];
const allScripts: string[] = []; const allScripts: ScriptFilePath[] = [];
const allTextFiles: string[] = []; const allTextFiles: TextFilePath[] = [];
const allContracts: string[] = []; const allContracts: ContractFilePath[] = [];
const allMessages: string[] = []; const allMessages: FilePath[] = [];
const folders: string[] = []; const folders: Directory[] = [];
function handleFn(fn: string, dest: string[]): void { function handlePath(path: FilePath, dest: FilePath[]): void {
let parsedFn = fn; // This parses out any files not in the starting directory.
if (prefix) { const parsedPath = removeDirectoryFromPath(baseDirectory, path);
if (!fn.startsWith(prefix)) { if (!parsedPath) return;
return;
} else {
parsedFn = fn.slice(prefix.length, fn.length);
}
}
if (filter && !parsedFn.includes(filter)) { if (!parsedPath.includes(filter)) return;
return;
}
// If the fn includes a forward slash, it must be in a subdirectory. // Check if there's a directory in the parsed path, if so we need to add the folder and not the file.
// Therefore, we only list the "first" directory in its path const firstParentDir = getFirstDirectoryInPath(parsedPath);
if (parsedFn.includes("/")) { if (firstParentDir) {
const firstParentDir = getFirstParentDirectory(parsedFn); if (!firstParentDir.includes(filter) || folders.includes(firstParentDir)) return;
if (filter && !firstParentDir.includes(filter)) {
return;
}
if (!folders.includes(firstParentDir)) {
folders.push(firstParentDir); folders.push(firstParentDir);
}
return; return;
} }
dest.push(parsedPath);
dest.push(parsedFn);
} }
// Get all of the programs and scripts on the machine into one temporary array // Get all of the programs and scripts on the machine into one temporary array
for (const program of server.programs) handleFn(program, allPrograms); // Type assertions that programs and msg/lit are filepaths are safe due to checks in
for (const scriptFilename of server.scripts.keys()) handleFn(scriptFilename, allScripts); // Program, Message, and Literature constructors
for (const txt of server.textFiles) handleFn(txt.fn, allTextFiles); for (const program of server.programs) handlePath(program as FilePath, allPrograms);
for (const contract of server.contracts) handleFn(contract.fn, allContracts); for (const scriptFilename of server.scripts.keys()) handlePath(scriptFilename, allScripts);
for (const msgOrLit of server.messages) handleFn(msgOrLit, allMessages); 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 // Sort the files/folders alphabetically then print each
allPrograms.sort(); allPrograms.sort();
@ -119,13 +106,10 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
allMessages.sort(); allMessages.sort();
folders.sort(); folders.sort();
interface ClickableRowProps { interface ScriptRowProps {
row: string; scripts: ScriptFilePath[];
prefix: string;
hostname: string;
} }
function ClickableScriptRow({ scripts }: ScriptRowProps): React.ReactElement {
function ClickableScriptRow({ row, prefix, hostname }: ClickableRowProps): React.ReactElement {
const classes = makeStyles((theme: Theme) => const classes = makeStyles((theme: Theme) =>
createStyles({ createStyles({
scriptLinksWrap: { scriptLinksWrap: {
@ -135,48 +119,38 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
scriptLink: { scriptLink: {
cursor: "pointer", cursor: "pointer",
textDecorationLine: "underline", textDecorationLine: "underline",
paddingRight: "1.15em", marginRight: "1.5em",
"&:last-child": { padding: 0 }, "&:last-child": { marginRight: 0 },
}, },
}), }),
)(); )();
const rowSplit = row.split("~"); function onScriptLinkClick(filename: ScriptFilePath): void {
let rowSplitArray = rowSplit.map((x) => [x.trim(), x.replace(x.trim(), "")]); const filePath = combinePath(baseDirectory, filename);
rowSplitArray = rowSplitArray.filter((x) => !!x[0]); const code = server.scripts.get(filePath)?.content ?? "";
const map = new Map<ContentFilePath, string>();
function onScriptLinkClick(filename: string): void { map.set(filePath, code);
if (!server.isConnectedTo) { Router.toScriptEditor(map);
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 });
} }
return ( return (
<span className={classes.scriptLinksWrap}> <span className={classes.scriptLinksWrap}>
{rowSplitArray.map((rowItem) => ( {scripts.map((script) => (
<span key={"script_" + rowItem[0]}> <span key={script}>
<span className={classes.scriptLink} onClick={() => onScriptLinkClick(rowItem[0])}> <span className={classes.scriptLink} onClick={() => onScriptLinkClick(script)}>
{rowItem[0]} {script}
</span> </span>
<span>{rowItem[1]}</span> <span></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) => const classes = makeStyles((theme: Theme) =>
createStyles({ createStyles({
linksWrap: { linksWrap: {
@ -186,41 +160,33 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
link: { link: {
cursor: "pointer", cursor: "pointer",
textDecorationLine: "underline", textDecorationLine: "underline",
paddingRight: "1.15em", marginRight: "1.5em",
"&:last-child": { padding: 0 }, "&:last-child": { marginRight: 0 },
}, },
}), }),
)(); )();
const rowSplit = row.split("~"); function onMessageLinkClick(filename: FilePath): void {
let rowSplitArray = rowSplit.map((x) => [x.trim(), x.replace(x.trim(), "")]);
rowSplitArray = rowSplitArray.filter((x) => !!x[0]);
function onMessageLinkClick(filename: string): void {
if (!server.isConnectedTo) { if (!server.isConnectedTo) {
return Terminal.error(`File is not on this server, connect to ${hostname} and try again`); return Terminal.error(`File is not on this server, connect to ${server.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.`);
} }
// Message and lit files have no directories
if (filepath.endsWith(".lit")) { if (checkEnum(MessageFilename, filename)) {
showLiterature(filepath); showMessage(filename);
} else if (filepath.endsWith(".msg")) { } else if (checkEnum(LiteratureName, filename)) {
showMessage(filepath as MessageFilenames); showLiterature(filename);
} }
} }
return ( return (
<span className={classes.linksWrap}> <span className={classes.linksWrap}>
{rowSplitArray.map((rowItem) => ( {messages.map((message) => (
<span key={"text_" + rowItem[0]}> <span key={message}>
<span className={classes.link} onClick={() => onMessageLinkClick(rowItem[0])}> <span className={classes.link} onClick={() => onMessageLinkClick(message)}>
{rowItem[0]} {message}
</span> </span>
<span>{rowItem[1]}</span> <span></span>
</span> </span>
))} ))}
</span> </span>
@ -236,39 +202,51 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
Script, Script,
} }
interface FileGroup { type FileGroup =
type: FileType; | {
// Types that are not clickable only need to be string[]
type: FileType.Folder | FileType.Program | FileType.Contract | FileType.TextFile;
segments: string[]; segments: string[];
} }
| { type: FileType.Message; segments: FilePath[] }
| { type: FileType.Script; segments: ScriptFilePath[] };
function postSegments(group: FileGroup, flags: LSFlags): void { function postSegments({ type, segments }: FileGroup, flags: LSFlags): void {
const segments = group.segments;
const linked = group.type === FileType.Script || group.type === FileType.Message;
const maxLength = Math.max(...segments.map((s) => s.length)) + 1; const maxLength = Math.max(...segments.map((s) => s.length)) + 1;
const filesPerRow = flags["-l"] === true ? 1 : Math.ceil(80 / maxLength); const filesPerRow = flags["-l"] === true ? 1 : Math.ceil(80 / maxLength);
for (let i = 0; i < segments.length; i++) { const padLength = Math.max(maxLength + 2, 40);
let row = ""; let i = 0;
for (let col = 0; col < filesPerRow; col++) { if (type === FileType.Script) {
if (!(i < segments.length)) break; while (i < segments.length) {
row += segments[i]; const scripts: ScriptFilePath[] = [];
row += " ".repeat(maxLength * (col + 1) - row.length); for (let col = 0; col < filesPerRow && i < segments.length; col++, i++) {
if (linked) { scripts.push(segments[i]);
row += "~";
} }
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++;
} }
i--; switch (type) {
switch (group.type) {
case FileType.Folder: case FileType.Folder:
Terminal.printRaw(<span style={{ color: "cyan" }}>{row}</span>); Terminal.printRaw(<span style={{ color: "cyan" }}>{row}</span>);
break; 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: default:
Terminal.print(row); Terminal.print(row);
} }
@ -282,8 +260,8 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
{ type: FileType.Program, segments: allPrograms }, { type: FileType.Program, segments: allPrograms },
{ type: FileType.Contract, segments: allContracts }, { type: FileType.Contract, segments: allContracts },
{ type: FileType.Script, segments: allScripts }, { type: FileType.Script, segments: allScripts },
].filter((g) => g.segments.length > 0); ];
for (const group of groups) { 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 { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { isScriptFilename } from "../../Script/isScriptFilename"; import { hasScriptExtension } from "../../Paths/ScriptFilePath";
import { TextFile } from "../../TextFile"; import { hasTextExtension } from "../../Paths/TextFilePath";
import { Script } from "../../Script/Script";
import { getDestinationFilepath, areFilesEqual } from "../DirectoryHelpers";
export function mv(args: (string | number | boolean)[], server: BaseServer): void { export function mv(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length !== 2) { if (args.length !== 2) {
Terminal.error(`Incorrect number of arguments. Usage: mv [src] [dest]`); Terminal.error(`Incorrect number of arguments. Usage: mv [src] [dest]`);
return; return;
} }
const [source, destination] = args.map((arg) => arg + "");
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 sourcePath = Terminal.getFilepath(source); const sourcePath = Terminal.getFilepath(source);
if (!sourcePath) return Terminal.error(`Invalid source filename: ${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 if (
const dest = getDestinationFilepath(t_dest, source, Terminal.cwd()); (!hasScriptExtension(sourcePath) && !hasTextExtension(sourcePath)) ||
if (dest === null) return Terminal.error("error parsing dst file"); (!hasScriptExtension(destinationPath) && !hasTextExtension(destinationPath))
) {
const destFile = Terminal.getFile(dest); return Terminal.error(`'mv' can only be used on scripts and text files (.txt)`);
const destPath = Terminal.getFilepath(dest);
if (!destPath) return Terminal.error(`Invalid destination filename: ${destPath}`);
if (areFilesEqual(sourcePath, destPath)) return Terminal.error(`Source and destination files are the same file`);
// 'mv' command only works on scripts and txt files.
// Also, you can't convert between different file types
if (isScriptFilename(source)) {
const script = srcFile as Script;
if (!isScriptFilename(destPath)) return Terminal.error(`Source and destination files must have the same type`);
// Command doesn't work if script is running
if (server.isRunning(sourcePath)) return Terminal.error(`Cannot use 'mv' on a script that is running`);
if (destFile != null) {
// Already exists, will be overwritten, so we'll delete it
// Command doesn't work if script is running
if (server.isRunning(destPath)) {
Terminal.error(`Cannot use 'mv' on a script that is running`);
return;
} }
const status = server.removeFile(destPath); // Allow content to be moved between scripts and textfiles, no need to limit this.
if (!status.res) { const sourceContentFile = server.getContentFile(sourcePath);
Terminal.error(`Something went wrong...please contact game dev (probably a bug)`); if (!sourceContentFile) return Terminal.error(`Source file ${sourcePath} does not exist`);
return;
} else {
Terminal.print("Warning: The destination file was overwritten");
}
}
script.filename = destPath; if (!sourceContentFile.deleteFromServer(server)) {
} else if (srcFile instanceof TextFile) { return Terminal.error(`Could not remove source file ${sourcePath} from existing location.`);
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 + "");
} }
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 { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { isScriptFilename } from "../../Script/isScriptFilename";
import { runScript } from "./runScript"; import { runScript } from "./runScript";
import { runProgram } from "./runProgram"; 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 { export function run(args: (string | number | boolean)[], server: BaseServer): void {
// Run a program or a script // Run a program or a script
if (args.length < 1) { const arg = args.shift();
Terminal.error("Incorrect number of arguments. Usage: run [program/script] [-t] [num threads] [arg1] [arg2]..."); if (!arg) return Terminal.error("Usage: run [program/script] [-t] [num threads] [arg1] [arg2]...");
} else {
const executableName = args[0] + "";
// Check if its a script or just a program/executable const path = Terminal.getFilepath(String(arg));
if (isScriptFilename(executableName)) { if (!path) return Terminal.error(`${args[0]} is not a valid filepath.`);
runScript(args, server); if (hasScriptExtension(path)) {
} else if (executableName.endsWith(".cct")) { args.shift();
Terminal.runContract(executableName); return runScript(path, args, server);
} else { } else if (hasContractExtension(path)) {
runProgram(args, server); 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 { Terminal } from "../../Terminal";
import { Player } from "@player"; import { Player } from "@player";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { Programs } from "../../Programs/Programs"; import { CompletedProgramName, Programs } from "../../Programs/Programs";
import { ProgramFilePath } from "../../Paths/ProgramFilePath";
export function runProgram(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length < 1) {
return;
}
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 // Check if you have the program on your computer. If you do, execute it, otherwise
// display an error message // 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( 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; return;
} }
Programs[realProgramName].run(args.map(String), server);
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");
} }

@ -3,27 +3,21 @@ import { BaseServer } from "../../Server/BaseServer";
import { LogBoxEvents } from "../../ui/React/LogBoxManager"; import { LogBoxEvents } from "../../ui/React/LogBoxManager";
import { startWorkerScript } from "../../NetscriptWorker"; import { startWorkerScript } from "../../NetscriptWorker";
import { RunningScript } from "../../Script/RunningScript"; import { RunningScript } from "../../Script/RunningScript";
import { findRunningScript } from "../../Script/ScriptHelpers";
import * as libarg from "arg"; import * as libarg from "arg";
import { formatRam } from "../../ui/formatNumber"; import { formatRam } from "../../ui/formatNumber";
import { ScriptArg } from "@nsdefs"; import { ScriptArg } from "@nsdefs";
import { isPositiveInteger } from "../../types"; import { isPositiveInteger } from "../../types";
import { ScriptFilePath } from "../../Paths/ScriptFilePath";
export function runScript(commandArgs: (string | number | boolean)[], server: BaseServer): void { export function runScript(path: ScriptFilePath, commandArgs: (string | number | boolean)[], server: BaseServer): void {
if (commandArgs.length < 1) { // This takes in the absolute filepath, see "run.ts"
Terminal.error( const script = server.scripts.get(path);
`Bug encountered with Terminal.runScript(). Command array has a length of less than 1: ${commandArgs}`, if (!script) return Terminal.error(`Script ${path} does not exist on this server.`);
);
return;
}
const scriptName = Terminal.getFilepath(commandArgs[0] + "");
if (!scriptName) return Terminal.error(`Invalid filename: ${commandArgs[0]}`);
const runArgs = { "--tail": Boolean, "-t": Number }; const runArgs = { "--tail": Boolean, "-t": Number };
const flags = libarg(runArgs, { const flags = libarg(runArgs, {
permissive: true, permissive: true,
argv: commandArgs.slice(1), argv: commandArgs,
}); });
const tailFlag = flags["--tail"] === true; const tailFlag = flags["--tail"] === true;
const numThreads = parseFloat(flags["-t"] ?? 1); const numThreads = parseFloat(flags["-t"] ?? 1);
@ -35,34 +29,24 @@ export function runScript(commandArgs: (string | number | boolean)[], server: Ba
const args = flags["_"] as ScriptArg[]; const args = flags["_"] as ScriptArg[];
// Check if this script is already running // Check if this script is already running
if (findRunningScript(scriptName, args, server) != null) { if (server.getRunningScript(path, args)) {
Terminal.error( return Terminal.error("This script is already running with the same args.");
"This script is already running with the same args. Cannot run multiple instances with the same args",
);
return;
} }
// 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); const singleRamUsage = script.getRamUsage(server.scripts);
if (!singleRamUsage) return Terminal.error("Error while calculating ram usage for this script."); if (!singleRamUsage) return Terminal.error("Error while calculating ram usage for this script.");
const ramUsage = singleRamUsage * numThreads; const ramUsage = singleRamUsage * numThreads;
const ramAvailable = server.maxRam - server.ramUsed; const ramAvailable = server.maxRam - server.ramUsed;
if (!server.hasAdminRights) { if (!server.hasAdminRights) return Terminal.error("Need root access to run script");
Terminal.error("Need root access to run script");
return;
}
if (ramUsage > ramAvailable + 0.001) { if (ramUsage > ramAvailable + 0.001) {
Terminal.error( return Terminal.error(
"This machine does not have enough RAM to run this script" + "This machine does not have enough RAM to run this script" +
(numThreads === 1 ? "" : ` with ${numThreads} threads`) + (numThreads === 1 ? "" : ` with ${numThreads} threads`) +
`. Script requires ${formatRam(ramUsage)} of RAM`, `. Script requires ${formatRam(ramUsage)} of RAM`,
); );
return;
} }
// Able to run script // Able to run script
@ -70,10 +54,7 @@ export function runScript(commandArgs: (string | number | boolean)[], server: Ba
runningScript.threads = numThreads; runningScript.threads = numThreads;
const success = startWorkerScript(runningScript, server); const success = startWorkerScript(runningScript, server);
if (!success) { if (!success) return Terminal.error(`Failed to start script`);
Terminal.error(`Failed to start script`);
return;
}
Terminal.print( Terminal.print(
`Running script with ${numThreads} thread(s), pid ${runningScript.pid} and args: ${JSON.stringify(args)}.`, `Running script with ${numThreads} thread(s), pid ${runningScript.pid} and args: ${JSON.stringify(args)}.`,

@ -1,6 +1,6 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { Player } from "@player"; import { Player } from "@player";
import { Programs } from "../../Programs/Programs"; import { CompletedProgramName } from "../../Programs/Programs";
export function scananalyze(args: (string | number | boolean)[]): void { export function scananalyze(args: (string | number | boolean)[]): void {
if (args.length === 0) { if (args.length === 0) {
@ -19,18 +19,18 @@ export function scananalyze(args: (string | number | boolean)[]): void {
const depth = parseInt(args[0] + ""); const depth = parseInt(args[0] + "");
if (isNaN(depth) || depth < 0) { if (isNaN(depth) || depth < 0) {
Terminal.error("Incorrect usage of scan-analyze command. depth argument must be positive numeric"); return Terminal.error("Incorrect usage of scan-analyze command. depth argument must be positive numeric");
return;
} }
if (depth > 3 && !Player.hasProgram(Programs.DeepscanV1.name) && !Player.hasProgram(Programs.DeepscanV2.name)) { if (
Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 3"); depth > 3 &&
return; !Player.hasProgram(CompletedProgramName.deepScan1) &&
} else if (depth > 5 && !Player.hasProgram(Programs.DeepscanV2.name)) { !Player.hasProgram(CompletedProgramName.deepScan2)
Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 5"); ) {
return; 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) { } else if (depth > 10) {
Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 10"); return Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 10");
return;
} }
Terminal.executeScanAnalyzeCommand(depth, all); Terminal.executeScanAnalyzeCommand(depth, all);
} }

@ -1,71 +1,40 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { GetServer } from "../../Server/AllServers"; 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 { export function scp(args: (string | number | boolean)[], server: BaseServer): void {
try {
if (args.length !== 2) { if (args.length !== 2) {
Terminal.error("Incorrect usage of scp command. Usage: scp [file] [destination hostname]"); return Terminal.error("Incorrect usage of scp command. Usage: scp [source filename] [destination hostname]");
return;
} }
const scriptname = Terminal.getFilepath(args[0] + ""); const [scriptname, destHostname] = args.map((arg) => arg + "");
if (!scriptname) return Terminal.error(`Invalid filename: ${args[0]}`);
if (!scriptname.endsWith(".lit") && !isScriptFilename(scriptname) && !scriptname.endsWith(".txt")) { const path = Terminal.getFilepath(scriptname);
Terminal.error("scp only works for scripts, text files (.txt), and literature files (.lit)"); if (!path) return Terminal.error(`Invalid file path: ${scriptname}`);
return;
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 (!hasScriptExtension(path) && !hasTextExtension(path)) {
if (destServer == null) { return Terminal.error("scp only works for scripts, text files (.txt), and literature files (.lit)");
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 + "");
} }
// 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 { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { findRunningScriptByPid } from "../../Script/ScriptHelpers"; import { findRunningScriptByPid } from "../../Script/ScriptHelpers";
import { isScriptFilename, validScriptExtensions } from "../../Script/isScriptFilename";
import { compareArrays } from "../../utils/helpers/compareArrays"; import { compareArrays } from "../../utils/helpers/compareArrays";
import { LogBoxEvents } from "../../ui/React/LogBoxManager"; import { LogBoxEvents } from "../../ui/React/LogBoxManager";
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
export function tail(commandArray: (string | number | boolean)[], server: BaseServer): void { export function tail(commandArray: (string | number | boolean)[], server: BaseServer): void {
try {
if (commandArray.length < 1) { if (commandArray.length < 1) {
Terminal.error("Incorrect number of arguments. Usage: tail [script] [arg1] [arg2]..."); return Terminal.error("Incorrect number of arguments. Usage: tail [script] [arg1] [arg2]...");
} else if (typeof commandArray[0] === "string") { }
const scriptName = Terminal.getFilepath(commandArray[0]); if (typeof commandArray[0] === "number") {
if (!scriptName) return Terminal.error(`Invalid filename: ${commandArray[0]}`); const runningScript = findRunningScriptByPid(commandArray[0], server);
if (!isScriptFilename(scriptName)) { if (!runningScript) return Terminal.error(`No script with PID ${commandArray[0]} is running on the server`);
Terminal.error(`tail can only be called on ${validScriptExtensions.join(", ")} files, or by PID`); LogBoxEvents.emit(runningScript);
return; 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 // Get script arguments
const args = []; const args = [];
for (let i = 1; i < commandArray.length; ++i) { 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 // go over all the running scripts. If there's a perfect
// match, use it! // match, use it!
for (let i = 0; i < server.runningScripts.length; ++i) { 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]); LogBoxEvents.emit(server.runningScripts[i]);
return; return;
} }
@ -39,7 +42,7 @@ export function tail(commandArray: (string | number | boolean)[], server: BaseSe
if (server.runningScripts[i].args.length < args.length) continue; if (server.runningScripts[i].args.length < args.length) continue;
// make a smaller copy of the args. // make a smaller copy of the args.
const args2 = server.runningScripts[i].args.slice(0, args.length); 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]); 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. // if there's no candidate then we just don't know.
Terminal.error(`No script named ${scriptName} is running on the server`); Terminal.error(`No script named ${path} 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 + "");
}
} }

@ -1,37 +1,12 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { Player } from "@player";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { Server } from "../../Server/Server";
export function weaken(args: (string | number | boolean)[], server: BaseServer): void { export function weaken(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length !== 0) { if (args.length !== 0) return Terminal.error("Incorrect usage of weaken command. Usage: weaken");
Terminal.error("Incorrect usage of weaken command. Usage: weaken");
return;
}
if (!(server instanceof Server)) { if (server.purchasedByPlayer) return Terminal.error("Cannot weaken your own machines!");
Terminal.error( if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!");
"Cannot weaken your own machines! You are currently connected to your home PC or one of your purchased servers", // 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.");
}
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;
}
Terminal.startWeaken(); Terminal.startWeaken();
} }

@ -2,7 +2,8 @@ import $ from "jquery";
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; 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 { export function wget(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length !== 2) { if (args.length !== 2) {
@ -12,20 +13,17 @@ export function wget(args: (string | number | boolean)[], server: BaseServer): v
const url = args[0] + ""; const url = args[0] + "";
const target = Terminal.getFilepath(args[1] + ""); 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`); return Terminal.error(`wget failed: Invalid target file. Target file must be script or text file`);
} }
$.get( $.get(
url, url,
function (data: unknown) { function (data: unknown) {
let res; let res;
if (isScriptFilename(target)) { if (hasTextExtension(target)) {
res = server.writeToScriptFile(target, String(data));
} else {
res = server.writeToTextFile(target, String(data)); res = server.writeToTextFile(target, String(data));
} } else {
if (!res.success) { res = server.writeToScriptFile(target, String(data));
return Terminal.error("wget failed");
} }
if (res.overwritten) { if (res.overwritten) {
return Terminal.print(`wget successfully retrieved content and overwrote ${target}`); 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 { KEY, KEYCODE } from "../../utils/helpers/keyCodes";
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { Player } from "@player"; import { Player } from "@player";
import { determineAllPossibilitiesForTabCompletion } from "../determineAllPossibilitiesForTabCompletion"; import { getTabCompletionPossibilities } from "../getTabCompletionPossibilities";
import { tabCompletion } from "../tabCompletion";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { longestCommonStart } from "../../utils/StringHelperFunctions";
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@ -26,7 +26,6 @@ const useStyles = makeStyles((theme: Theme) =>
padding: theme.spacing(0), padding: theme.spacing(0),
}, },
preformatted: { preformatted: {
whiteSpace: "pre-wrap",
margin: theme.spacing(0), margin: theme.spacing(0),
}, },
list: { list: {
@ -205,54 +204,18 @@ export function TerminalInput(): React.ReactElement {
} }
// Autocomplete // Autocomplete
if (event.key === KEY.TAB && value !== "") { if (event.key === KEY.TAB) {
event.preventDefault(); event.preventDefault();
const possibilities = await getTabCompletionPossibilities(value, Terminal.cwd());
let copy = value; if (possibilities.length === 0) return;
const semiColonIndex = copy.lastIndexOf(";"); if (possibilities.length === 1) {
if (semiColonIndex !== -1) { saveValue(value.replace(/[^ ]*$/, possibilities[0]) + " ");
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) {
return; return;
} }
// More than one possibility, check to see if there is a longer common string than currentText.
let arg = ""; const longestMatch = longestCommonStart(possibilities);
let command = ""; saveValue(value.replace(/[^ ]*$/, longestMatch));
if (commandArray.length == 0) { setPossibilities(possibilities);
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);
}
} }
// Clear screen. // Clear screen.
@ -310,6 +273,7 @@ export function TerminalInput(): React.ReactElement {
} else { } else {
++Terminal.commandHistoryIndex; ++Terminal.commandHistoryIndex;
const prevCommand = Terminal.commandHistory[Terminal.commandHistoryIndex]; const prevCommand = Terminal.commandHistory[Terminal.commandHistoryIndex];
saveValue(prevCommand); saveValue(prevCommand);
} }
} }
@ -405,7 +369,12 @@ export function TerminalInput(): React.ReactElement {
onKeyDown: onKeyDown, onKeyDown: onKeyDown,
}} }}
></TextField> ></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 }}> <Paper sx={{ m: 1, p: 2 }}>
<Typography classes={{ root: classes.preformatted }} color={"primary"} paragraph={false}> <Typography classes={{ root: classes.preformatted }} color={"primary"} paragraph={false}>
Possible autocomplete candidates: Possible autocomplete candidates:

@ -1,29 +1,27 @@
import { dialogBoxCreate } from "./ui/React/DialogBox"; import { dialogBoxCreate } from "./ui/React/DialogBox";
import { BaseServer } from "./Server/BaseServer"; import { BaseServer } from "./Server/BaseServer";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "./utils/JSONReviver"; 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. */ /** Represents a plain text file that is typically stored on a server. */
export class TextFile { export class TextFile implements ContentFile {
/** The full file name. */ /** The full file name. */
fn: string; filename: TextFilePath;
/** The content of the file. */ /** The content of the file. */
text: string; text: string;
//TODO 2.3: Why are we using getter/setter for fn as filename? Rename parameter as more-readable "filename" // Shared interface on Script and TextFile for accessing content
/** The full file name. */ get content() {
get filename(): string { return this.text;
return this.fn; }
set content(text: string) {
this.text = text;
} }
/** The full file name. */ constructor(filename = "default.txt" as TextFilePath, txt = "") {
set filename(value: string) { this.filename = filename;
this.fn = value;
}
constructor(fn = "", txt = "") {
this.fn = (fn.endsWith(".txt") ? fn : `${fn}.txt`).replace(/\s+/g, "");
this.text = txt; this.text = txt;
} }
@ -38,7 +36,7 @@ export class TextFile {
const a: HTMLAnchorElement = document.createElement("a"); const a: HTMLAnchorElement = document.createElement("a");
const url: string = URL.createObjectURL(file); const url: string = URL.createObjectURL(file);
a.href = url; a.href = url;
a.download = this.fn; a.download = this.filename;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
setTimeout(() => { setTimeout(() => {
@ -54,7 +52,7 @@ export class TextFile {
/** Shows the content to the user via the game's dialog box. */ /** Shows the content to the user via the game's dialog box. */
show(): void { 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. */ /** Serialize the current file to a JSON save state. */
@ -67,6 +65,12 @@ export class TextFile {
this.text = txt; 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. */ /** Initializes a TextFile from a JSON save state. */
static fromJSON(value: IReviverValue): TextFile { static fromJSON(value: IReviverValue): TextFile {
return Generic_fromJSON(TextFile, value.data); return Generic_fromJSON(TextFile, value.data);
@ -74,46 +78,3 @@ export class TextFile {
} }
constructorsForReviver.TextFile = 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"; 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> { export class JSONSet<T> extends Set<T> {
toJSON(): IReviverValue { toJSON(): IReviverValue {
return { ctor: "JSONSet", data: Array.from(this) }; 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 { toJSON(): IReviverValue {
return { ctor: "JSONMap", data: Array.from(this) }; 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 // Server name
export type ServerName = string /*& { __type: "ServerName" }*/; export type ServerName = string /*& { __type: "ServerName" }*/;
/*export function isExistingServerName(value: unknown): value is ServerName { /*export function isExistingServerName(value: unknown): value is ServerName {

@ -7,24 +7,26 @@ import { Programs } from "../Programs/Programs";
import { Work, WorkType } from "./Work"; import { Work, WorkType } from "./Work";
import { Program } from "../Programs/Program"; import { Program } from "../Programs/Program";
import { calculateIntelligenceBonus } from "../PersonObjects/formulas/intelligence"; 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 => export const isCreateProgramWork = (w: Work | null): w is CreateProgramWork =>
w !== null && w.type === WorkType.CREATE_PROGRAM; w !== null && w.type === WorkType.CREATE_PROGRAM;
interface CreateProgramWorkParams { interface CreateProgramWorkParams {
programName: string; programName: CompletedProgramName;
singularity: boolean; singularity: boolean;
} }
export class CreateProgramWork extends Work { export class CreateProgramWork extends Work {
programName: string; programName: CompletedProgramName;
// amount of effective work completed on the program (time boosted by skills). // amount of effective work completed on the program (time boosted by skills).
unitCompleted: number; unitCompleted: number;
constructor(params?: CreateProgramWorkParams) { constructor(params?: CreateProgramWorkParams) {
super(WorkType.CREATE_PROGRAM, params?.singularity ?? true); super(WorkType.CREATE_PROGRAM, params?.singularity ?? true);
this.unitCompleted = 0; this.unitCompleted = 0;
this.programName = params?.programName ?? ""; this.programName = params?.programName ?? CompletedProgramName.bruteSsh;
if (params) { if (params) {
for (let i = 0; i < Player.getHomeComputer().programs.length; ++i) { for (let i = 0; i < Player.getHomeComputer().programs.length; ++i) {
@ -50,9 +52,7 @@ export class CreateProgramWork extends Work {
} }
getProgram(): Program { getProgram(): Program {
const p = Object.values(Programs).find((p) => p.name.toLowerCase() === this.programName.toLowerCase()); return Programs[this.programName];
if (!p) throw new Error("Create program work started with invalid program " + this.programName);
return p;
} }
process(cycles: number): boolean { process(cycles: number): boolean {
@ -75,7 +75,7 @@ export class CreateProgramWork extends Work {
return false; return false;
} }
finish(cancelled: boolean): void { finish(cancelled: boolean): void {
const programName = this.programName; const programName = asProgramFilePath(this.programName);
if (!cancelled) { if (!cancelled) {
//Complete case //Complete case
Player.gainIntelligenceExp( Player.gainIntelligenceExp(
@ -95,7 +95,7 @@ export class CreateProgramWork extends Work {
} else if (!Player.getHomeComputer().programs.includes(programName)) { } else if (!Player.getHomeComputer().programs.includes(programName)) {
//Incomplete case //Incomplete case
const perc = ((100 * this.unitCompleted) / this.unitNeeded()).toFixed(2); 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); Player.getHomeComputer().programs.push(incompleteName);
} }
} }

@ -2,7 +2,6 @@ import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions"
import { initAugmentations } from "./Augmentation/AugmentationHelpers"; import { initAugmentations } from "./Augmentation/AugmentationHelpers";
import { AugmentationNames } from "./Augmentation/data/AugmentationNames"; import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
import { initSourceFiles } from "./SourceFile/SourceFiles"; import { initSourceFiles } from "./SourceFile/SourceFiles";
import { initDarkWebItems } from "./DarkWeb/DarkWebItems";
import { generateRandomContract } from "./CodingContractGenerator"; import { generateRandomContract } from "./CodingContractGenerator";
import { initCompanies } from "./Company/Companies"; import { initCompanies } from "./Company/Companies";
import { CONSTANTS } from "./Constants"; import { CONSTANTS } from "./Constants";
@ -235,7 +234,6 @@ const Engine: {
if (loadGame(saveString)) { if (loadGame(saveString)) {
FormatsNeedToChange.emit(); FormatsNeedToChange.emit();
initSourceFiles(); initSourceFiles();
initDarkWebItems();
initAugmentations(); // Also calls Player.reapplyAllAugmentations() initAugmentations(); // Also calls Player.reapplyAllAugmentations()
Player.reapplyAllSourceFiles(); Player.reapplyAllSourceFiles();
if (Player.hasWseAccount) { if (Player.hasWseAccount) {
@ -377,7 +375,6 @@ const Engine: {
// No save found, start new game // No save found, start new game
FormatsNeedToChange.emit(); FormatsNeedToChange.emit();
initSourceFiles(); initSourceFiles();
initDarkWebItems();
Engine.start(); // Run main game loop and Scripts loop Engine.start(); // Run main game loop and Scripts loop
Player.init(); Player.init();
initForeignServers(Player.getHomeComputer()); initForeignServers(Player.getHomeComputer());

@ -23,7 +23,7 @@ import createStyles from "@mui/styles/createStyles";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography"; 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 { Overview } from "./React/Overview";
import { SidebarRoot } from "../Sidebar/ui/SidebarRoot"; import { SidebarRoot } from "../Sidebar/ui/SidebarRoot";
import { AugmentationsRoot } from "../Augmentation/ui/AugmentationsRoot"; import { AugmentationsRoot } from "../Augmentation/ui/AugmentationsRoot";
@ -78,6 +78,8 @@ import { isFactionWork } from "../Work/FactionWork";
import { V2Modal } from "../utils/V2Modal"; import { V2Modal } from "../utils/V2Modal";
import { MathJaxContext } from "better-react-mathjax"; import { MathJaxContext } from "better-react-mathjax";
import { useRerender } from "./React/hooks"; import { useRerender } from "./React/hooks";
import { ScriptFilePath } from "src/Paths/ScriptFilePath";
import { TextFilePath } from "src/Paths/TextFilePath";
const htmlLocation = location; const htmlLocation = location;
@ -126,7 +128,13 @@ function determineStartPage(): Page {
export function GameRoot(): React.ReactElement { export function GameRoot(): React.ReactElement {
const classes = useStyles(); 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 [page, setPage] = useState(determineStartPage());
const rerender = useRerender(); const rerender = useRerender();
const [augPage, setAugPage] = useState<boolean>(false); const [augPage, setAugPage] = useState<boolean>(false);
@ -189,7 +197,7 @@ export function GameRoot(): React.ReactElement {
setPage(Page.Faction); setPage(Page.Faction);
if (faction) setFaction(faction); if (faction) setFaction(faction);
}, },
toScriptEditor: (files: Record<string, string>, options?: ScriptEditorRouteOptions) => { toScriptEditor: (files = new Map(), options) => {
setEditorOptions({ setEditorOptions({
files, files,
vim: !!options?.vim, vim: !!options?.vim,

@ -1,3 +1,5 @@
import { ScriptFilePath } from "../Paths/ScriptFilePath";
import { TextFilePath } from "../Paths/TextFilePath";
import { Faction } from "../Faction/Faction"; import { Faction } from "../Faction/Faction";
import { Location } from "../Locations/Location"; import { Location } from "../Locations/Location";
@ -67,7 +69,7 @@ export interface IRouter {
toFaction(faction: Faction, augPage?: boolean): void; // faction name toFaction(faction: Faction, augPage?: boolean): void; // faction name
toInfiltration(location: Location): void; toInfiltration(location: Location): void;
toJob(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; toLocation(location: Location): void;
toImportSave(base64Save: string, automatic?: boolean): void; toImportSave(base64Save: string, automatic?: boolean): void;
} }

@ -1,8 +1,10 @@
import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
import { PlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation"; import { PlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation";
import { Player } from "@player"; import { Player } from "@player";
import { Script } from "../Script/Script"; import { FormattedCode, Script } from "../Script/Script";
import { GetAllServers } from "../Server/AllServers"; import { GetAllServers } from "../Server/AllServers";
import { resolveTextFilePath } from "../Paths/TextFilePath";
import { resolveScriptFilePath } from "../Paths/ScriptFilePath";
const detect: [string, string][] = [ const detect: [string, string][] = [
["getHackTime", "returns milliseconds"], ["getHackTime", "returns milliseconds"],
@ -52,7 +54,7 @@ function hasChanges(code: string): boolean {
return false; return false;
} }
function convert(code: string): string { function convert(code: string): FormattedCode {
const lines = code.split("\n"); const lines = code.split("\n");
const out: string[] = []; const out: string[] = [];
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
@ -70,7 +72,8 @@ function convert(code: string): string {
} }
out.push(line); out.push(line);
} }
return out.join("\n"); code = out.join("\n");
return Script.formatCode(code);
} }
export function AwardNFG(n = 1): void { export function AwardNFG(n = 1): void {
@ -122,7 +125,9 @@ export function v1APIBreak(): void {
} }
if (txt !== "") { if (txt !== "") {
const home = Player.getHomeComputer(); 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 // 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[] = []; const backups: Script[] = [];
for (const script of server.scripts) { for (const script of server.scripts) {
if (!hasChanges(script.code)) continue; if (!hasChanges(script.code)) continue;
const prefix = script.filename.includes("/") ? "/BACKUP_" : "BACKUP_"; // Sanitize first before combining
backups.push(new Script(prefix + script.filename, script.code, script.server)); 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); script.code = convert(script.code);
} }
server.scripts = server.scripts.concat(backups); server.scripts = server.scripts.concat(backups);

@ -1,3 +1,4 @@
import { TextFilePath } from "../Paths/TextFilePath";
import { saveObject } from "../SaveObject"; import { saveObject } from "../SaveObject";
import { Script } from "../Script/Script"; import { Script } from "../Script/Script";
import { GetAllServers, GetServer } from "../Server/AllServers"; import { GetAllServers, GetServer } from "../Server/AllServers";
@ -232,7 +233,7 @@ export const v2APIBreak = () => {
processScript(rules, script); 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(); openV2Modal();
for (const server of GetAllServers()) { for (const server of GetAllServers()) {

@ -6,6 +6,7 @@ import { AddToAllServers, DeleteServer } from "../../../src/Server/AllServers";
import { WorkerScriptStartStopEventEmitter } from "../../../src/Netscript/WorkerScriptStartStopEventEmitter"; import { WorkerScriptStartStopEventEmitter } from "../../../src/Netscript/WorkerScriptStartStopEventEmitter";
import { AlertEvents } from "../../../src/ui/React/AlertManager"; import { AlertEvents } from "../../../src/ui/React/AlertManager";
import type { Script } from "src/Script/Script"; 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 // Replace Blob/ObjectURL functions, because they don't work natively in Jest
global.Blob = class extends Blob { global.Blob = class extends Blob {
@ -77,7 +78,7 @@ test.each([
return "data:text/javascript," + encodeURIComponent(blob.code); return "data:text/javascript," + encodeURIComponent(blob.code);
}; };
let server; let server = {} as Server;
let eventDelete = () => {}; let eventDelete = () => {};
let alertDelete = () => {}; let alertDelete = () => {};
try { try {
@ -87,10 +88,10 @@ test.each([
server = new Server({ hostname: "home", adminRights: true, maxRam: 8 }); server = new Server({ hostname: "home", adminRights: true, maxRam: 8 });
AddToAllServers(server); AddToAllServers(server);
for (const s of scripts) { 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); expect(script.filename).toEqual(scripts[scripts.length - 1].name);
const ramUsage = script.getRamUsage(server.scripts); 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, "smtpPortOpen": false,
"sqlPortOpen": false, "sqlPortOpen": false,
"sshPortOpen": false, "sshPortOpen": false,
"textFiles": [], "textFiles": {
"ctor": "JSONMap",
"data": []
},
"purchasedByPlayer": true, "purchasedByPlayer": true,
"backdoorInstalled": false, "backdoorInstalled": false,
"baseDifficulty": 1, "baseDifficulty": 1,
@ -99,7 +102,10 @@ exports[`load/saveAllServers 1`] = `
"smtpPortOpen": false, "smtpPortOpen": false,
"sqlPortOpen": false, "sqlPortOpen": false,
"sshPortOpen": false, "sshPortOpen": false,
"textFiles": [], "textFiles": {
"ctor": "JSONMap",
"data": []
},
"purchasedByPlayer": false, "purchasedByPlayer": false,
"backdoorInstalled": false, "backdoorInstalled": false,
"baseDifficulty": 1, "baseDifficulty": 1,
@ -155,7 +161,10 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = `
"smtpPortOpen": false, "smtpPortOpen": false,
"sqlPortOpen": false, "sqlPortOpen": false,
"sshPortOpen": false, "sshPortOpen": false,
"textFiles": [], "textFiles": {
"ctor": "JSONMap",
"data": []
},
"purchasedByPlayer": true, "purchasedByPlayer": true,
"backdoorInstalled": false, "backdoorInstalled": false,
"baseDifficulty": 1, "baseDifficulty": 1,
@ -196,7 +205,10 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = `
"smtpPortOpen": false, "smtpPortOpen": false,
"sqlPortOpen": false, "sqlPortOpen": false,
"sshPortOpen": false, "sshPortOpen": false,
"textFiles": [], "textFiles": {
"ctor": "JSONMap",
"data": []
},
"purchasedByPlayer": false, "purchasedByPlayer": false,
"backdoorInstalled": false, "backdoorInstalled": false,
"baseDifficulty": 1, "baseDifficulty": 1,