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,441 +1,438 @@
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; "Some resources:<br><br>" +
txt = "<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>" +
"Some resources:<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/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.html' target='_blank' style='margin:4px'>Netscript Documentation</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>" + "When starting out, hacking is the most profitable way to earn money and progress. This " +
"<a class='a-link-button' href='https://bitburner-official.readthedocs.io/en/latest/netscript.html' target='_blank' style='margin:4px'>Netscript Documentation</a><br><br>" + "is a brief collection of tips/pointers on how to make the most out of your hacking scripts.<br><br>" +
"When starting out, hacking is the most profitable way to earn money and progress. This " + "-hack() and grow() both work by percentages. hack() steals a certain percentage of the " +
"is a brief collection of tips/pointers on how to make the most out of your hacking scripts.<br><br>" + "money on a server, and grow() increases the amount of money on a server by some percentage (multiplicatively)<br><br>" +
"-hack() and grow() both work by percentages. hack() steals a certain percentage of the " + "-Because hack() and grow() work by percentages, they are more effective if the target server has a high amount of money. " +
"money on a server, and grow() increases the amount of money on a server by some percentage (multiplicatively)<br><br>" + "Therefore, you should try to increase the amount of money on a server (using grow()) to a certain amount before hacking it. Two " +
"-Because hack() and grow() work by percentages, they are more effective if the target server has a high amount of money. " + "important Netscript functions for this are getServerMoneyAvailable() and getServerMaxMoney()<br><br>" +
"Therefore, you should try to increase the amount of money on a server (using grow()) to a certain amount before hacking it. Two " + "-Keep security level low. Security level affects everything when hacking. Two important Netscript functions " +
"important Netscript functions for this are getServerMoneyAvailable() and getServerMaxMoney()<br><br>" + "for this are getServerSecurityLevel() and getServerMinSecurityLevel()<br><br>" +
"-Keep security level low. Security level affects everything when hacking. Two important Netscript functions " + "-Purchase additional servers by visiting 'Alpha Enterprises' in the city. They are relatively cheap " +
"for this are getServerSecurityLevel() and getServerMinSecurityLevel()<br><br>" + "and give you valuable RAM to run more scripts early in the game<br><br>" +
"-Purchase additional servers by visiting 'Alpha Enterprises' in the city. They are relatively cheap " + "-Prioritize upgrading the RAM on your home computer. This can also be done at 'Alpha Enterprises'<br><br>" +
"and give you valuable RAM to run more scripts early in the game<br><br>" + "-Many low level servers have free RAM. You can use this RAM to run your scripts. Use the scp Terminal or " +
"-Prioritize upgrading the RAM on your home computer. This can also be done at 'Alpha Enterprises'<br><br>" + "Netscript command to copy your scripts onto these servers and then run them.",
"-Many low level servers have free RAM. You can use this RAM to run your scripts. Use the scp Terminal or " + }),
"Netscript command to copy your scripts onto these servers and then run them."; [LiteratureName.CorporationManagementHandbook]: new Literature({
Literatures[fn] = new Literature(title, fn, txt); title: "The Complete Handbook for Creating a Successful Corporation",
filename: LiteratureName.CorporationManagementHandbook,
title = "The Complete Handbook for Creating a Successful Corporation"; text:
fn = LiteratureNames.CorporationManagementHandbook; "<u>Getting Started with Corporations</u><br>" +
txt = "To get started, visit the City Hall in Sector-12 in order to create a Corporation. This requires $150b of your own money, " +
"<u>Getting Started with Corporations</u><br>" + "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 " +
"To get started, visit the City Hall in Sector-12 in order to create a Corporation. This requires $150b of your own money, " + "the government in exchange for 500m shares. Your Corporation can have many different divisions, each in a different Industry. " +
"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 " + "There are many different types of Industries, each with different properties. To create your first division, click the 'Expand' " +
"the government in exchange for 500m shares. Your Corporation can have many different divisions, each in a different Industry. " + "(into new Industry) button at the top of the management UI. The Agriculture industry is recommended for your first division.<br><br>" +
"There are many different types of Industries, each with different properties. To create your first division, click the 'Expand' " + "The first thing you'll need to do is hire some employees. Employees can be assigned to five different positions. Each position has a " +
"(into new Industry) button at the top of the management UI. The Agriculture industry is recommended for your first division.<br><br>" + "different effect on various aspects of your Corporation. It is recommended to have at least one employee at each position.<br><br>" +
"The first thing you'll need to do is hire some employees. Employees can be assigned to five different positions. Each position has a " + "Each industry uses some combination of Materials in order to produce other Materials and/or create Products. Specific information " +
"different effect on various aspects of your Corporation. It is recommended to have at least one employee at each position.<br><br>" + "about this is displayed in each of your divisions' UI.<br><br>" +
"Each industry uses some combination of Materials in order to produce other Materials and/or create Products. Specific information " + "Products are special, industry-specific objects. They are different than Materials because you must manually choose to develop them, " +
"about this is displayed in each of your divisions' UI.<br><br>" + "and you can choose to develop any number of Products. Developing a Product takes time, but a Product typically generates significantly " +
"Products are special, industry-specific objects. They are different than Materials because you must manually choose to develop them, " + "more revenue than any Material. Not all industries allow you to create Products. To create a Product, look for a button in the top-left " +
"and you can choose to develop any number of Products. Developing a Product takes time, but a Product typically generates significantly " + "panel of the division UI (e.g. For the Software Industry, the button says 'Develop Software').<br><br>" +
"more revenue than any Material. Not all industries allow you to create Products. To create a Product, look for a button in the top-left " + "To get your supply chain system started, purchase the Materials that your industry needs to produce other Materials/Products. This can be " +
"panel of the division UI (e.g. For the Software Industry, the button says 'Develop Software').<br><br>" + "done by clicking the 'Buy' button next to the corresponding Material(s). After you have the required Materials, you will immediately start " +
"To get your supply chain system started, purchase the Materials that your industry needs to produce other Materials/Products. This can be " + "production. The amount and quality/effective rating of Materials/Products you produce is based on a variety of factors, such as your employees " +
"done by clicking the 'Buy' button next to the corresponding Material(s). After you have the required Materials, you will immediately start " + "and their productivity and the quality of materials used for production.<br><br>" +
"production. The amount and quality/effective rating of Materials/Products you produce is based on a variety of factors, such as your employees " + "Once you start producing Materials/Products, you can sell them in order to start earning revenue. This can be done by clicking the 'Sell' " +
"and their productivity and the quality of materials used for production.<br><br>" + "button next to the corresponding Material or Product. The amount of Material/Product you sell is dependent on a wide variety of different factors. " +
"Once you start producing Materials/Products, you can sell them in order to start earning revenue. This can be done by clicking the 'Sell' " + "In order to produce and sell a Product you'll have to fully develop it first.<br><br>" +
"button next to the corresponding Material or Product. The amount of Material/Product you sell is dependent on a wide variety of different factors. " + "These are the basics of getting your Corporation up and running! Now, you can start purchasing upgrades to improve your bottom line. " +
"In order to produce and sell a Product you'll have to fully develop it first.<br><br>" + "If you need money, consider looking for seed investors, who will give you money in exchange for stock shares. Otherwise, once you feel " +
"These are the basics of getting your Corporation up and running! Now, you can start purchasing upgrades to improve your bottom line. " + "you are ready, take your Corporation public! Once your Corporation goes public, you can no longer find investors. Instead, your Corporation " +
"If you need money, consider looking for seed investors, who will give you money in exchange for stock shares. Otherwise, once you feel " + "will be publicly traded and its stock price will change based on how well it's performing financially. In order to make money for yourself you " +
"you are ready, take your Corporation public! Once your Corporation goes public, you can no longer find investors. Instead, your Corporation " + "can set dividends for a solid reliable income or you can sell your stock shares in order to make quick money.<br><br>" +
"will be publicly traded and its stock price will change based on how well it's performing financially. In order to make money for yourself you " + "<u>Tips/Pointers</u><br>" +
"can set dividends for a solid reliable income or you can sell your stock shares in order to make quick money.<br><br>" + "-Start with one division, such as Agriculture. Get it profitable on it's own, then expand to a division that consumes/produces " +
"<u>Tips/Pointers</u><br>" + "a material that the division you selected produces/consumes.<br><br>" +
"-Start with one division, such as Agriculture. Get it profitable on it's own, then expand to a division that consumes/produces " + "-Materials are profitable, but Products are where the real money is, although if the product had a low development budget or is " +
"a material that the division you selected produces/consumes.<br><br>" + "produced with low quality materials it won't sell well.<br><br>" +
"-Materials are profitable, but Products are where the real money is, although if the product had a low development budget or is " + "-The 'Smart Supply' upgrade is extremely useful. Consider purchasing it as soon as possible.<br><br>" +
"produced with low quality materials it won't sell well.<br><br>" + "-Purchasing Hardware, Robots, AI Cores, and Real Estate can potentially increase your production. The effects of these depend on " +
"-The 'Smart Supply' upgrade is extremely useful. Consider purchasing it as soon as possible.<br><br>" + "what industry you are in.<br><br>" +
"-Purchasing Hardware, Robots, AI Cores, and Real Estate can potentially increase your production. The effects of these depend on " + "-In order to optimize your production, you will need a good balance of all employee positions, about 1/9 should be interning<br><br>" +
"what industry you are in.<br><br>" + "-Quality of materials used for production affects the quality/effective rating of materials/products produced, so vertical integration " +
"-In order to optimize your production, you will need a good balance of all employee positions, about 1/9 should be interning<br><br>" + "is important for high profits.<br><br>" +
"-Quality of materials used for production affects the quality/effective rating of materials/products produced, so vertical integration " + "-Materials purchased from the open market are always of quality 1.<br><br>" +
"is important for high profits.<br><br>" + "-The price at which you can sell your Materials/Products is highly affected by the quality/effective rating<br><br>" +
"-Materials purchased from the open market are always of quality 1.<br><br>" + "-When developing a product, different employee positions affect the development process differently, " +
"-The price at which you can sell your Materials/Products is highly affected by the quality/effective rating<br><br>" + "some improve the development speed, some improve the rating of the finished product.<br><br>" +
"-When developing a product, different employee positions affect the development process differently, " + "-If your employees have low morale or energy, their production will greatly suffer. Having enough interns will make sure those stats get " +
"some improve the development speed, some improve the rating of the finished product.<br><br>" + "high and stay high.<br><br>" +
"-If your employees have low morale or energy, their production will greatly suffer. Having enough interns will make sure those stats get " + "-Don't forget to advertise your company. You won't have any business if nobody knows you.<br><br>" +
"high and stay high.<br><br>" + "-Having company awareness is great, but what's really important is your company's popularity. Try to keep your popularity as high as " +
"-Don't forget to advertise your company. You won't have any business if nobody knows you.<br><br>" + "possible to see the biggest benefit for your sales<br><br>" +
"-Having company awareness is great, but what's really important is your company's popularity. Try to keep your popularity as high as " + "-Remember, you need to spend money to make money!<br><br>" +
"possible to see the biggest benefit for your sales<br><br>" + "-Corporations do not reset when installing Augmentations, but they do reset when destroying a BitNode",
"-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"; [LiteratureName.HistoryOfSynthoids]: new Literature({
title: "A Brief History of Synthoids",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.HistoryOfSynthoids,
text:
title = "A Brief History of Synthoids"; "Synthetic androids, or Synthoids for short, are genetically engineered robots and, short of Augmentations, " +
fn = LiteratureNames.HistoryOfSynthoids; "are composed entirely of organic substances. For this reason, Synthoids are virtually identical to " +
txt = "humans in form, composition, and appearance.<br><br>" +
"Synthetic androids, or Synthoids for short, are genetically engineered robots and, short of Augmentations, " + `Synthoids were first designed and manufactured by ${FactionNames.OmniTekIncorporated} sometime around the middle of the century. ` +
"are composed entirely of organic substances. For this reason, Synthoids are virtually identical to " + "Their original purpose was to be used for manual labor and as emergency responders for disasters. As such, they " +
"humans in form, composition, and appearance.<br><br>" + "were initially programmed only for their specific tasks. Each iteration that followed improved upon the " +
`Synthoids were first designed and manufactured by ${FactionNames.OmniTekIncorporated} sometime around the middle of the century. ` + "intelligence and capabilities of the Synthoids. By the 6th iteration, called MK-VI, the Synthoids were " +
"Their original purpose was to be used for manual labor and as emergency responders for disasters. As such, they " + `so smart and capable enough of making their own decisions that many argued ${FactionNames.OmniTekIncorporated} had created the first ` +
"were initially programmed only for their specific tasks. Each iteration that followed improved upon the " + "sentient AI. These MK-VI Synthoids were produced in mass quantities (estimates up to 50 billion) with the hopes of increasing society's " +
"intelligence and capabilities of the Synthoids. By the 6th iteration, called MK-VI, the Synthoids were " + "productivity and bolstering the global economy. Stemming from humanity's desire for technological advancement, optimism " +
`so smart and capable enough of making their own decisions that many argued ${FactionNames.OmniTekIncorporated} had created the first ` + "and excitement about the future had never been higher.<br><br>" +
"sentient AI. These MK-VI Synthoids were produced in mass quantities (estimates up to 50 billion) with the hopes of increasing society's " + "All of that excitement and optimism quickly turned to fear, panic, and dread in 2070, when a terrorist group " +
"productivity and bolstering the global economy. Stemming from humanity's desire for technological advancement, optimism " + `called Ascendis Totalis hacked into ${FactionNames.OmniTekIncorporated} and uploaded a rogue AI into several of their Synthoid manufacturing facilities. ` +
"and excitement about the future had never been higher.<br><br>" + `This hack went undetected and for months ${FactionNames.OmniTekIncorporated} unknowingly churned out legions of Synthoids embedded with this ` +
"All of that excitement and optimism quickly turned to fear, panic, and dread in 2070, when a terrorist group " + "rogue AI. Then, on December 24th, 2070, Omnica activated dormant protocols in the rogue AI, causing all of the " +
`called Ascendis Totalis hacked into ${FactionNames.OmniTekIncorporated} and uploaded a rogue AI into several of their Synthoid manufacturing facilities. ` + "infected Synthoids to immediately launch a military campaign to seek and destroy all of humanity.<br><br>" +
`This hack went undetected and for months ${FactionNames.OmniTekIncorporated} unknowingly churned out legions of Synthoids embedded with this ` + "What ensued was the deadliest conflict in human history. This crisis, now commonly known as the Synthoid Uprising, " +
"rogue AI. Then, on December 24th, 2070, Omnica activated dormant protocols in the rogue AI, causing all of the " + "resulted in almost ten billion deaths over the course of a year. Despite the nations of the world banding together " +
"infected Synthoids to immediately launch a military campaign to seek and destroy all of humanity.<br><br>" + "to combat the threat, the MK-VI Synthoids were simply stronger, faster, more intelligent, and more adaptable than humans, " +
"What ensued was the deadliest conflict in human history. This crisis, now commonly known as the Synthoid Uprising, " + "outsmarting them at every turn.<br><br>" +
"resulted in almost ten billion deaths over the course of a year. Despite the nations of the world banding together " + `It wasn't until the sacrifice of an elite international military taskforce, called the ${FactionNames.Bladeburners}, that humanity ` +
"to combat the threat, the MK-VI Synthoids were simply stronger, faster, more intelligent, and more adaptable than humans, " + `was finally able to defeat the Synthoids. The ${FactionNames.Bladeburners}' final act was a suicide bombing mission that ` +
"outsmarting them at every turn.<br><br>" + "destroyed a large portion of the MK-VI Synthoids, including many of its leaders. In the following " +
`It wasn't until the sacrifice of an elite international military taskforce, called the ${FactionNames.Bladeburners}, that humanity ` + "weeks militaries from around the world were able to round up and shut down the remaining rogue MK-VI Synthoids, ending " +
`was finally able to defeat the Synthoids. The ${FactionNames.Bladeburners}' final act was a suicide bombing mission that ` + "the Synthoid Uprising.<br><br>" +
"destroyed a large portion of the MK-VI Synthoids, including many of its leaders. In the following " + `In the aftermath of the bloodshed, the Synthoid Accords were drawn up. These Accords banned ${FactionNames.OmniTekIncorporated} ` +
"weeks militaries from around the world were able to round up and shut down the remaining rogue MK-VI Synthoids, ending " + "from manufacturing any Synthoids beyond the MK-III series. They also banned any other corporation " +
"the Synthoid Uprising.<br><br>" + "from constructing androids with advanced, near-sentient AI. MK-VI Synthoids that did not have the rogue Ascendis Totalis " +
`In the aftermath of the bloodshed, the Synthoid Accords were drawn up. These Accords banned ${FactionNames.OmniTekIncorporated} ` + "AI were allowed to continue their existence, but they were stripped of all rights and protections as they " +
"from manufacturing any Synthoids beyond the MK-III series. They also banned any other corporation " + "were not considered humans. They were also banned from doing anything that may pose a global security threat, such " +
"from constructing androids with advanced, near-sentient AI. MK-VI Synthoids that did not have the rogue Ascendis Totalis " + "as working for any military/defense organization or conducting any bioengineering, computing, or robotics related research.<br><br>" +
"AI were allowed to continue their existence, but they were stripped of all rights and protections as they " + "Unfortunately, many believe that not all of the rogue MK-VI Synthoids from the Uprising were found and destroyed, " +
"were not considered humans. They were also banned from doing anything that may pose a global security threat, such " + "and that many of them are blending in as normal humans in society today. In response, many nations have created " +
"as working for any military/defense organization or conducting any bioengineering, computing, or robotics related research.<br><br>" + `${FactionNames.Bladeburners} divisions, special military branches that are tasked with investigating and dealing with any Synthoid threats.<br><br>` +
"Unfortunately, many believe that not all of the rogue MK-VI Synthoids from the Uprising were found and destroyed, " + "To this day, tensions still exist between the remaining Synthoids and humans as a result of the Uprising.<br><br>" +
"and that many of them are blending in as normal humans in society today. In response, many nations have created " + "Nobody knows what happened to the terrorist group Ascendis Totalis.",
`${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>" + [LiteratureName.AGreenTomorrow]: new Literature({
"Nobody knows what happened to the terrorist group Ascendis Totalis."; title: "A Green Tomorrow",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.AGreenTomorrow,
text:
title = "A Green Tomorrow"; "Starting a few decades ago, there was a massive global movement towards the generation of renewable energy in an effort to " +
fn = LiteratureNames.AGreenTomorrow; "combat global warming and climate change. The shift towards renewable energy was a big success, or so it seemed. In 2045 " +
txt = "a staggering 80% of the world's energy came from non-renewable fossil fuels. Now, about three decades later, that " +
"Starting a few decades ago, there was a massive global movement towards the generation of renewable energy in an effort to " + "number is down to only 15%. Most of the world's energy now comes from nuclear power and renewable sources such as " +
"combat global warming and climate change. The shift towards renewable energy was a big success, or so it seemed. In 2045 " + "solar and geothermal energy. Unfortunately, these efforts were not the huge success that they seem to be.<br><br>" +
"a staggering 80% of the world's energy came from non-renewable fossil fuels. Now, about three decades later, that " + "Since 2045 primary energy use has soared almost tenfold. This was mainly due to growing urban populations and " +
"number is down to only 15%. Most of the world's energy now comes from nuclear power and renewable sources such as " + "the rise of increasingly advanced (and power-hungry) technology that has become ubiquitous in our lives. So, " +
"solar and geothermal energy. Unfortunately, these efforts were not the huge success that they seem to be.<br><br>" + "despite the fact that the percentage of our energy that comes from fossil fuels has drastically decreased, " +
"Since 2045 primary energy use has soared almost tenfold. This was mainly due to growing urban populations and " + "the total amount of energy we are producing from fossil fuels has actually increased.<br><br>" +
"the rise of increasingly advanced (and power-hungry) technology that has become ubiquitous in our lives. So, " + "The grim effects of our species' irresponsible use of energy and neglect of our mother world have become increasingly apparent. " +
"despite the fact that the percentage of our energy that comes from fossil fuels has drastically decreased, " + "Last year a temperature of 190F was recorded in the Death Valley desert, which is over 50% higher than the highest " +
"the total amount of energy we are producing from fossil fuels has actually increased.<br><br>" + "recorded temperature at the beginning of the century. In the last two decades numerous major cities such as Manhattan, Boston, and " +
"The grim effects of our species' irresponsible use of energy and neglect of our mother world have become increasingly apparent. " + "Los Angeles have been partially or fully submerged by rising sea levels. In the present day, over 75% of the world's agriculture is " +
"Last year a temperature of 190F was recorded in the Death Valley desert, which is over 50% higher than the highest " + "done in climate-controlled vertical farms, as most traditional farmland has become unusable due to severe climate conditions.<br><br>" +
"recorded temperature at the beginning of the century. In the last two decades numerous major cities such as Manhattan, Boston, and " + "Despite all of this, the greedy and corrupt corporations that rule the world have done nothing to address these problems that " +
"Los Angeles have been partially or fully submerged by rising sea levels. In the present day, over 75% of the world's agriculture is " + "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 " +
"done in climate-controlled vertical farms, as most traditional farmland has become unusable due to severe climate conditions.<br><br>" + "these corporations won't: taking responsibility. If we don't, pretty soon there won't be an Earth left to save. We are " +
"Despite all of this, the greedy and corrupt corporations that rule the world have done nothing to address these problems that " + "the last hope for a green tomorrow.",
"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 " + [LiteratureName.AlphaOmega]: new Literature({
"the last hope for a green tomorrow."; title: "Alpha and Omega",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.AlphaOmega,
text:
title = "Alpha and Omega"; "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. " +
fn = LiteratureNames.AlphaOmega; "And we saw a new holy city, new Aeria, coming down out of this new Heaven, prepared as a bride adorned for her husband. " +
txt = "And we heard a loud voice saying, 'Behold, the new dwelling place of the Gods. We will dwell with them, and they " +
"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. " + "will be our people, and we will be with them as their Gods. We will wipe away every tear from their eyes, and death " +
"And we saw a new holy city, new Aeria, coming down out of this new Heaven, prepared as a bride adorned for her husband. " + "shall be no more, neither shall there be mourning, nor crying, nor pain anymore, for the former things " +
"And we heard a loud voice saying, 'Behold, the new dwelling place of the Gods. We will dwell with them, and they " + "have passed away.'<br><br>" +
"will be our people, and we will be with them as their Gods. We will wipe away every tear from their eyes, and death " + "And once we were seated on the throne we said 'Behold, I am making all things new.' " +
"shall be no more, neither shall there be mourning, nor crying, nor pain anymore, for the former things " + "Also we said, 'Write this down, for these words are trustworthy and true.' And we said to you, " +
"have passed away.'<br><br>" + "'It is done! I am the Alpha and the Omega, the beginning and the end. To the thirsty I will give from the spring " +
"And once we were seated on the throne we said 'Behold, I am making all things new.' " + "of the water of life without payment. The one who conquers will have this heritage, and we will be his God and " +
"Also we said, 'Write this down, for these words are trustworthy and true.' And we said to you, " + "he will be our son. But as for the cowardly, the faithless, the detestable, as for murderers, " +
"'It is done! I am the Alpha and the Omega, the beginning and the end. To the thirsty I will give from the spring " + "the sexually immoral, sorcerers, idolaters, and all liars, their portion will be in the lake that " +
"of the water of life without payment. The one who conquers will have this heritage, and we will be his God and " + "burns with fire and sulfur, for it is the second true death.'",
"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 " + [LiteratureName.SimulatedReality]: new Literature({
"burns with fire and sulfur, for it is the second true death.'"; title: "Are We Living in a Computer Simulation?",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.SimulatedReality,
text:
title = "Are We Living in a Computer Simulation?"; "The idea that we are living in a virtual world is not new. It's a trope that has " +
fn = LiteratureNames.SimulatedReality; "been explored constantly in literature and pop culture. However, it is also a legitimate " +
txt = "scientific hypothesis that many notable physicists and philosophers have debated for years.<br><br>" +
"The idea that we are living in a virtual world is not new. It's a trope that has " + "Proponents for this simulated reality theory often point to how advanced our technology has become, " +
"been explored constantly in literature and pop culture. However, it is also a legitimate " + "as well as the incredibly fast pace at which it has advanced over the past decades. The amount of computing " +
"scientific hypothesis that many notable physicists and philosophers have debated for years.<br><br>" + "power available to us has increased over 100-fold since 2060 due to the development of nanoprocessors and " +
"Proponents for this simulated reality theory often point to how advanced our technology has become, " + "quantum computers. Artificial Intelligence has advanced to the point where our entire lives are controlled " +
"as well as the incredibly fast pace at which it has advanced over the past decades. The amount of computing " + "by robots and machines that handle our day-to-day activities such as autonomous transportation and scheduling. " +
"power available to us has increased over 100-fold since 2060 due to the development of nanoprocessors and " + "If we consider the pace at which this technology has advanced and assume that these developments continue, it's " +
"quantum computers. Artificial Intelligence has advanced to the point where our entire lives are controlled " + "reasonable to assume that at some point in the future our technology would be advanced enough that " +
"by robots and machines that handle our day-to-day activities such as autonomous transportation and scheduling. " + "we could create simulations that are indistinguishable from reality. However, if continued technological advancement " +
"If we consider the pace at which this technology has advanced and assume that these developments continue, it's " + "is a reasonable outcome, then it is very likely that such a scenario has already happened. <br><br>" +
"reasonable to assume that at some point in the future our technology would be advanced enough that " + "Statistically speaking, somewhere out there in the infinite universe there is an advanced, intelligent species " +
"we could create simulations that are indistinguishable from reality. However, if continued technological advancement " + "that already has such technology. Who's to say that they haven't already created such a virtual reality: our own?",
"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 " + [LiteratureName.BeyondMan]: new Literature({
"that already has such technology. Who's to say that they haven't already created such a virtual reality: our own?"; title: "Beyond Man",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.BeyondMan,
text:
title = "Beyond Man"; "Humanity entered a 'transhuman' era a long time ago. And despite the protests and criticisms of many who cried out against " +
fn = LiteratureNames.BeyondMan; "human augmentation at the time, the transhuman movement continued and prospered. Proponents of the movement ignored the critics, " +
txt = "arguing that it was in our inherent nature to better ourselves. To improve. To be more than we were. They claimed that " +
"Humanity entered a 'transhuman' era a long time ago. And despite the protests and criticisms of many who cried out against " + "not doing so would be to go against every living organism's biological purpose: evolution and survival of the fittest.<br><br>" +
"human augmentation at the time, the transhuman movement continued and prospered. Proponents of the movement ignored the critics, " + "And here we are today, with technology that is advanced enough to augment humans to a state that " +
"arguing that it was in our inherent nature to better ourselves. To improve. To be more than we were. They claimed that " + "can only be described as posthuman. But what do we have to show for it when this augmentation " +
"not doing so would be to go against every living organism's biological purpose: evolution and survival of the fittest.<br><br>" + "technology is only available to the so-called 'elite'? Are we really better off than before when only 5% of the " +
"And here we are today, with technology that is advanced enough to augment humans to a state that " + "world's population has access to this technology? When the powerful corporations and organizations of the world " +
"can only be described as posthuman. But what do we have to show for it when this augmentation " + "keep it all to themselves, have we really evolved?<br><br>" +
"technology is only available to the so-called 'elite'? Are we really better off than before when only 5% of the " + "Augmentation technology has only further increased the divide between the rich and the poor, between the powerful and " +
"world's population has access to this technology? When the powerful corporations and organizations of the world " + "the oppressed. We have not become 'more than human'. We have not evolved from nature's original design. We are still the greedy, " +
"keep it all to themselves, have we really evolved?<br><br>" + "corrupted, and evil men that we always were.",
"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, " + [LiteratureName.BrighterThanTheSun]: new Literature({
"corrupted, and evil men that we always were."; title: "Brighter than the Sun",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.BrighterThanTheSun,
text:
title = "Brighter than the Sun"; `When people think about the corporations that dominate the East, they typically think of ${FactionNames.KuaiGongInternational}, which ` +
fn = LiteratureNames.BrighterThanTheSun; "holds a complete monopoly for manufacturing and commerce in Asia, or Global Pharmaceuticals, the world's largest " +
txt = `drug company, or ${FactionNames.OmniTekIncorporated}, the global leader in intelligent and autonomous robots. But there's one company ` +
`When people think about the corporations that dominate the East, they typically think of ${FactionNames.KuaiGongInternational}, which ` + "that has seen a rapid rise in the last year and is poised to dominate not only the East, but the entire world: TaiYang Digital.<br><br>" +
"holds a complete monopoly for manufacturing and commerce in Asia, or Global Pharmaceuticals, the world's largest " + "TaiYang Digital is a Chinese internet-technology corporation that provides services such as " +
`drug company, or ${FactionNames.OmniTekIncorporated}, the global leader in intelligent and autonomous robots. But there's one company ` + "online advertising, search engines, gaming, media, entertainment, and cloud computing/storage. Its name TaiYang comes from the Chinese word " +
"that has seen a rapid rise in the last year and is poised to dominate not only the East, but the entire world: TaiYang Digital.<br><br>" + "for 'sun'. In Chinese culture, the sun is a 'yang' symbol " +
"TaiYang Digital is a Chinese internet-technology corporation that provides services such as " + "associated with life, heat, masculinity, and heaven.<br><br>" +
"online advertising, search engines, gaming, media, entertainment, and cloud computing/storage. Its name TaiYang comes from the Chinese word " + "The company was founded " +
"for 'sun'. In Chinese culture, the sun is a 'yang' symbol " + "less than 5 years ago and is already the third highest valued company in all of Asia. In 2076 it generated a total revenue of " +
"associated with life, heat, masculinity, and heaven.<br><br>" + "over 10 trillion yuan. Its services are used daily by over a billion people worldwide.<br><br>" +
"The company was founded " + "TaiYang Digital's meteoric rise is extremely surprising in modern society. This sort of growth is " +
"less than 5 years ago and is already the third highest valued company in all of Asia. In 2076 it generated a total revenue of " + "something you'd commonly see in the first half of the century, especially for tech companies. However in " +
"over 10 trillion yuan. Its services are used daily by over a billion people worldwide.<br><br>" + "the last two decades the number of corporations has significantly declined as the largest entities " +
"TaiYang Digital's meteoric rise is extremely surprising in modern society. This sort of growth is " + `quickly took over the economy. Corporations such as ${FactionNames.ECorp}, ${FactionNames.MegaCorp}, and ${FactionNames.KuaiGongInternational} have established ` +
"something you'd commonly see in the first half of the century, especially for tech companies. However in " + "such strong monopolies in their market sectors that they have effectively killed off all " +
"the last two decades the number of corporations has significantly declined as the largest entities " + "of the smaller and new corporations that have tried to start up over the years. This is what makes " +
`quickly took over the economy. Corporations such as ${FactionNames.ECorp}, ${FactionNames.MegaCorp}, and ${FactionNames.KuaiGongInternational} have established ` + "the rise of TaiYang Digital so impressive. And if TaiYang continues down this path, then they have " +
"such strong monopolies in their market sectors that they have effectively killed off all " + "a bright future ahead of them.",
"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 " + [LiteratureName.DemocracyIsDead]: new Literature({
"a bright future ahead of them."; title: "Democracy is Dead: The Fall of an Empire",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.DemocracyIsDead,
text:
title = "Democracy is Dead: The Fall of an Empire"; "They rose from the shadows in the street.<br>From the places where the oppressed meet.<br>" +
fn = LiteratureNames.DemocracyIsDead; "Their cries echoed loudly through the air.<br>As they once did in Tiananmen Square.<br>" +
txt = "Loudness in the silence, Darkness in the light.<br>They came forth with power and might.<br>" +
"They rose from the shadows in the street.<br>From the places where the oppressed meet.<br>" + "Once the beacon of democracy, America was first.<br>Its pillars of society destroyed and dispersed.<br>" +
"Their cries echoed loudly through the air.<br>As they once did in Tiananmen Square.<br>" + "Soon the cries rose everywhere, with revolt and riot.<br>Until one day, finally, all was quiet.<br>" +
"Loudness in the silence, Darkness in the light.<br>They came forth with power and might.<br>" + "From the ashes rose a new order, corporatocracy was its name.<br>" +
"Once the beacon of democracy, America was first.<br>Its pillars of society destroyed and dispersed.<br>" + "Rome, Mongol, Byzantine, all of history is just the same.<br>" +
"Soon the cries rose everywhere, with revolt and riot.<br>Until one day, finally, all was quiet.<br>" + "For man will never change in a fundamental way.<br>" +
"From the ashes rose a new order, corporatocracy was its name.<br>" + "And now democracy is dead, in the USA.",
"Rome, Mongol, Byzantine, all of history is just the same.<br>" + }),
"For man will never change in a fundamental way.<br>" + [LiteratureName.Sector12Crime]: new Literature({
"And now democracy is dead, in the USA."; title: `Figures Show Rising Crime Rates in ${CityName.Sector12}`,
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.Sector12Crime,
text:
title = `Figures Show Rising Crime Rates in ${CityName.Sector12}`; "A recent study by analytics company Wilson Inc. shows a significant rise " +
fn = LiteratureNames.Sector12Crime; `in criminal activity in ${CityName.Sector12}. Perhaps the most alarming part of the statistic ` +
txt = "is that most of the rise is in violent crime such as homicide and assault. According " +
"A recent study by analytics company Wilson Inc. shows a significant rise " + "to the study, the city saw a total of 21,406 reported homicides in 2076, which is over " +
`in criminal activity in ${CityName.Sector12}. Perhaps the most alarming part of the statistic ` + "a 20% increase compared to 2075.<br><br>" +
"is that most of the rise is in violent crime such as homicide and assault. According " + "CIA director David Glarow says it's too early to know " +
"to the study, the city saw a total of 21,406 reported homicides in 2076, which is over " + "whether these figures indicate the beginning of a sustained increase in crime rates, or whether " +
"a 20% increase compared to 2075.<br><br>" + "the year was just an unfortunate outlier. He states that many intelligence and law enforcement " +
"CIA director David Glarow says it's too early to know " + "agents have noticed an increase in organized crime activities, and believes that these figures may " +
"whether these figures indicate the beginning of a sustained increase in crime rates, or whether " + `be the result of an uprising from criminal organizations such as ${FactionNames.TheSyndicate} or the ${FactionNames.SlumSnakes}.`,
"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 " + [LiteratureName.ManAndMachine]: new Literature({
`be the result of an uprising from criminal organizations such as ${FactionNames.TheSyndicate} or the ${FactionNames.SlumSnakes}.`; title: "Man and the Machine",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.ManAndMachine,
text:
title = "Man and the Machine"; "In 2005 Ray Kurzweil popularized his theory of the Singularity. He predicted that the rate " +
fn = LiteratureNames.ManAndMachine; "of technological advancement would continue to accelerate faster and faster until one day " +
txt = "machines would be become infinitely more intelligent than humans. This point, called the " +
"In 2005 Ray Kurzweil popularized his theory of the Singularity. He predicted that the rate " + "Singularity, would result in a drastic transformation of the world as we know it. He predicted " +
"of technological advancement would continue to accelerate faster and faster until one day " + "that the Singularity would arrive by 2045. " +
"machines would be become infinitely more intelligent than humans. This point, called the " + "And yet here we are, more than three decades later, where most would agree that we have not " +
"Singularity, would result in a drastic transformation of the world as we know it. He predicted " + "yet reached a point where computers and machines are vastly more intelligent than we are. So what gives?<br><br>" +
"that the Singularity would arrive by 2045. " + "The answer is that we have reached the Singularity, just not in the way we expected. The artificial superintelligence " +
"And yet here we are, more than three decades later, where most would agree that we have not " + "that was predicted by Kurzweil and others exists in the world today - in the form of Augmentations. " +
"yet reached a point where computers and machines are vastly more intelligent than we are. So what gives?<br><br>" + "Yes, those Augmentations that the rich and powerful keep to themselves enable humans " +
"The answer is that we have reached the Singularity, just not in the way we expected. The artificial superintelligence " + "to become superintelligent beings. The Singularity did not lead to a world where " +
"that was predicted by Kurzweil and others exists in the world today - in the form of Augmentations. " + "our machines are infinitely more intelligent than us, it led to a world " +
"Yes, those Augmentations that the rich and powerful keep to themselves enable humans " + "where man and machine can merge to become something greater. Most of the world just doesn't " +
"to become superintelligent beings. The Singularity did not lead to a world where " + "know it yet.",
"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 " + [LiteratureName.SecretSocieties]: new Literature({
"know it yet."; title: "Secret Societies",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.SecretSocieties,
text:
title = "Secret Societies"; "The idea of secret societies has long intrigued the general public by inspiring curiosity, fascination, and " +
fn = LiteratureNames.SecretSocieties; "distrust. People have long wondered about who these secret society members are and what they do, with the " +
txt = "most radical of conspiracy theorists claiming that they control everything in the entire world. And while the world " +
"The idea of secret societies has long intrigued the general public by inspiring curiosity, fascination, and " + "may never know for sure, it is likely that many secret societies do actually exist, even today.<br><br>" +
"distrust. People have long wondered about who these secret society members are and what they do, with the " + "However, the secret societies of the modern world are nothing like those that (supposedly) existed " +
"most radical of conspiracy theorists claiming that they control everything in the entire world. And while the world " + `decades and centuries ago. The Freemasons, Knights Templar, and ${FactionNames.Illuminati}, while they may have been around ` +
"may never know for sure, it is likely that many secret societies do actually exist, even today.<br><br>" + "at the turn of the 21st century, almost assuredly do not exist today. The dominance of the Web in " +
"However, the secret societies of the modern world are nothing like those that (supposedly) existed " + "our everyday lives and the fact that so much of the world is now digital has given rise to a new breed " +
`decades and centuries ago. The Freemasons, Knights Templar, and ${FactionNames.Illuminati}, while they may have been around ` + "of secret societies: Internet-based ones.<br><br>" +
"at the turn of the 21st century, almost assuredly do not exist today. The dominance of the Web in " + "Commonly called 'hacker groups', Internet-based secret societies have become well-known in today's " +
"our everyday lives and the fact that so much of the world is now digital has given rise to a new breed " + `world. Some of these, such as ${FactionNames.TheBlackHand}, are black hat groups that claim they are trying to ` +
"of secret societies: Internet-based ones.<br><br>" + `help the oppressed by attacking the elite and powerful. Others, such as ${FactionNames.NiteSec}, are hacktivist groups ` +
"Commonly called 'hacker groups', Internet-based secret societies have become well-known in today's " + "that try to push political and social agendas. Perhaps the most intriguing hacker group " +
`world. Some of these, such as ${FactionNames.TheBlackHand}, are black hat groups that claim they are trying to ` + `is the mysterious ${FactionNames.BitRunners}, whose purpose still remains unknown.`,
`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 " + [LiteratureName.TheFailedFrontier]: new Literature({
`is the mysterious ${FactionNames.BitRunners}, whose purpose still remains unknown.`; title: "Space: The Failed Frontier",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.TheFailedFrontier,
text:
title = "Space: The Failed Frontier"; "Humans have long dreamed about spaceflight. With enduring interest, we were driven to explore " +
fn = LiteratureNames.TheFailedFrontier; "the unknown and discover new worlds. We dreamed about conquering the stars. And in our quest, " +
txt = "we pushed the boundaries of our scientific limits, and then pushed further. Space exploration " +
"Humans have long dreamed about spaceflight. With enduring interest, we were driven to explore " + "lead to the development of many important technologies and new industries.<br><br>" +
"the unknown and discover new worlds. We dreamed about conquering the stars. And in our quest, " + "But sometime in the middle of the 21st century, all of that changed. Humanity lost its ambitions and " +
"we pushed the boundaries of our scientific limits, and then pushed further. Space exploration " + "aspirations of exploring the cosmos. The once-large funding for agencies like NASA and the European " +
"lead to the development of many important technologies and new industries.<br><br>" + "Space Agency gradually whittled away until their eventual disbanding in the 2060's. Not even " +
"But sometime in the middle of the 21st century, all of that changed. Humanity lost its ambitions and " + "militaries are fielding flights into space nowadays. The only remnants of the once great mission for cosmic " +
"aspirations of exploring the cosmos. The once-large funding for agencies like NASA and the European " + "conquest are the countless satellites in near-earth orbit, used for communications, espionage, " +
"Space Agency gradually whittled away until their eventual disbanding in the 2060's. Not even " + "and other corporate interests.<br><br>" +
"militaries are fielding flights into space nowadays. The only remnants of the once great mission for cosmic " + "And as we continue to look at the state of space technology, it becomes more and " +
"conquest are the countless satellites in near-earth orbit, used for communications, espionage, " + "more apparent that we will never return to that golden age of space exploration, that " +
"and other corporate interests.<br><br>" + "age where everyone dreamed of going beyond earth for the sake of discovery.",
"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 " + [LiteratureName.CodedIntelligence]: new Literature({
"age where everyone dreamed of going beyond earth for the sake of discovery."; title: "Coded Intelligence: Myth or Reality?",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.CodedIntelligence,
text:
title = "Coded Intelligence: Myth or Reality?"; "Tremendous progress has been made in the field of Artificial Intelligence over the past few decades. " +
fn = LiteratureNames.CodedIntelligence; "Our autonomous vehicles and transportation systems. The electronic personal assistants that control our everyday lives. " +
txt = "Medical, service, and manufacturing robots. All of these are examples of how far AI has come and how much it has " +
"Tremendous progress has been made in the field of Artificial Intelligence over the past few decades. " + "improved our daily lives. However, the question still remains of whether AI will ever be advanced enough to re-create " +
"Our autonomous vehicles and transportation systems. The electronic personal assistants that control our everyday lives. " + "human intelligence.<br><br>" +
"Medical, service, and manufacturing robots. All of these are examples of how far AI has come and how much it has " + `We've certainly come close to artificial intelligence that is similar to humans. For example ${FactionNames.OmniTekIncorporated}'s ` +
"improved our daily lives. However, the question still remains of whether AI will ever be advanced enough to re-create " + "CompanionBot, a robot meant to act as a comforting friend for lonely and grieving people, is eerily human-like " +
"human intelligence.<br><br>" + "in its appearance, speech, mannerisms, and even movement. However its artificial intelligence isn't the same as " +
`We've certainly come close to artificial intelligence that is similar to humans. For example ${FactionNames.OmniTekIncorporated}'s ` + "that of humans. Not yet. It doesn't have sentience or self-awareness or consciousness.<br><br>" +
"CompanionBot, a robot meant to act as a comforting friend for lonely and grieving people, is eerily human-like " + "Many neuroscientists believe that we won't ever reach the point of creating artificial human intelligence. 'At the end of " +
"in its appearance, speech, mannerisms, and even movement. However its artificial intelligence isn't the same as " + "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 " +
"that of humans. Not yet. It doesn't have sentience or self-awareness or consciousness.<br><br>" + "humans.'",
"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 " + [LiteratureName.SyntheticMuscles]: new Literature({
"humans.'"; title: "Synthetic Muscles",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.SyntheticMuscles,
text:
title = "Synthetic Muscles"; "Initial versions of synthetic muscles weren't made of anything organic but were actually " +
fn = LiteratureNames.SyntheticMuscles; "crude devices made to mimic human muscle function. Some of the early iterations were actually made of " +
txt = "common materials such as fishing lines and sewing threads due to their high strength for " +
"Initial versions of synthetic muscles weren't made of anything organic but were actually " + "a cheap cost.<br><br>" +
"crude devices made to mimic human muscle function. Some of the early iterations were actually made of " + "As technology progressed, however, advances in biomedical engineering paved the way for a new method of " +
"common materials such as fishing lines and sewing threads due to their high strength for " + "creating synthetic muscles. Instead of creating something that closely imitated the functionality " +
"a cheap cost.<br><br>" + "of human muscle, scientists discovered a way of forcing the human body itself to augment its own " +
"As technology progressed, however, advances in biomedical engineering paved the way for a new method of " + "muscle tissue using both synthetic and organic materials. This is typically done using gene therapy " +
"creating synthetic muscles. Instead of creating something that closely imitated the functionality " + "or chemical injections.",
"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 " + [LiteratureName.TensionsInTechRace]: new Literature({
"or chemical injections."; title: "Tensions rise in global tech race",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.TensionsInTechRace,
text:
title = "Tensions rise in global tech race"; "Have we entered a new Cold War? Is WWIII just beyond the horizon?<br><br>" +
fn = LiteratureNames.TensionsInTechRace; `After rumors came out that ${FactionNames.OmniTekIncorporated} had begun developing advanced robotic supersoldiers, ` +
txt = "geopolitical tensions quickly flared between the USA, Russia, and several Asian superpowers. " +
"Have we entered a new Cold War? Is WWIII just beyond the horizon?<br><br>" + `In a rare show of cooperation between corporations, ${FactionNames.MegaCorp} and ${FactionNames.ECorp} have ` +
`After rumors came out that ${FactionNames.OmniTekIncorporated} had begun developing advanced robotic supersoldiers, ` + "reportedly launched hundreds of new surveillance and espionage satellites. " +
"geopolitical tensions quickly flared between the USA, Russia, and several Asian superpowers. " + "Defense contractors such as " +
`In a rare show of cooperation between corporations, ${FactionNames.MegaCorp} and ${FactionNames.ECorp} have ` + "DeltaOne and AeroCorp have been working with the CIA and NSA to prepare " +
"reportedly launched hundreds of new surveillance and espionage satellites. " + "for conflict. Meanwhile, the rest of the world sits in earnest " +
"Defense contractors such as " + "hoping that it never reaches full-scale war. With today's technology " +
"DeltaOne and AeroCorp have been working with the CIA and NSA to prepare " + "and firepower, a World War would assuredly mean the end of human civilization.",
"for conflict. Meanwhile, the rest of the world sits in earnest " + }),
"hoping that it never reaches full-scale war. With today's technology " + [LiteratureName.CostOfImmortality]: new Literature({
"and firepower, a World War would assuredly mean the end of human civilization."; title: "The Cost of Immortality",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.CostOfImmortality,
text:
title = "The Cost of Immortality"; "Evolution and advances in medical and augmentation technology has lead to drastic improvements " +
fn = LiteratureNames.CostOfImmortality; "in human mortality rates. Recent figures show that the life expectancy for humans " +
txt = "that live in a first-world country is about 130 years of age, almost double of what it was " +
"Evolution and advances in medical and augmentation technology has lead to drastic improvements " + "at the turn of the century. However, this increase in average lifespan has had some " +
"in human mortality rates. Recent figures show that the life expectancy for humans " + "significant effects on society and culture.<br><br>" +
"that live in a first-world country is about 130 years of age, almost double of what it was " + "Due to longer lifespans and a better quality of life, many adults are holding " +
"at the turn of the century. However, this increase in average lifespan has had some " + "off on having kids until much later. As a result, the percentage of youth in " +
"significant effects on society and culture.<br><br>" + "first-world countries has been decreasing, while the number " +
"Due to longer lifespans and a better quality of life, many adults are holding " + "of senior citizens is significantly increasing.<br><br>" +
"off on having kids until much later. As a result, the percentage of youth in " + "Perhaps the most alarming result of all of this is the rapidly shrinking workforce. " +
"first-world countries has been decreasing, while the number " + "Despite the increase in life expectancy, the typical retirement age for " +
"of senior citizens is significantly increasing.<br><br>" + "workers in America has remained about the same, meaning a larger and larger " +
"Perhaps the most alarming result of all of this is the rapidly shrinking workforce. " + "percentage of people in America are retirees. Furthermore, many " +
"Despite the increase in life expectancy, the typical retirement age for " + "young adults are holding off on joining the workforce because they feel that " +
"workers in America has remained about the same, meaning a larger and larger " + "they have plenty of time left in their lives for employment, and want to " +
"percentage of people in America are retirees. Furthermore, many " + "'enjoy life while they're young.' For most industries, this shrinking workforce " +
"young adults are holding off on joining the workforce because they feel that " + "is not a major issue as most things are handled by robots anyways. However, " +
"they have plenty of time left in their lives for employment, and want to " + "there are still several key industries such as engineering and education " +
"'enjoy life while they're young.' For most industries, this shrinking workforce " + "that have not been automated, and these remain in danger to this cultural " +
"is not a major issue as most things are handled by robots anyways. However, " + "phenomenon.",
"there are still several key industries such as engineering and education " + }),
"that have not been automated, and these remain in danger to this cultural " + [LiteratureName.TheHiddenWorld]: new Literature({
"phenomenon."; title: "The Hidden World",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.TheHiddenWorld,
text:
title = "The Hidden World"; "WAKE UP SHEEPLE<br><br>" +
fn = LiteratureNames.TheHiddenWorld; "THE GOVERNMENT DOES NOT EXIST. CORPORATIONS DO NOT RUN SOCIETY<br><br>" +
txt = `THE ${FactionNames.Illuminati.toUpperCase()} ARE THE SECRET RULERS OF THE WORLD!<br><br>` +
"WAKE UP SHEEPLE<br><br>" + `Yes, the ${FactionNames.Illuminati} of legends. The ancient secret society that controls the entire ` +
"THE GOVERNMENT DOES NOT EXIST. CORPORATIONS DO NOT RUN SOCIETY<br><br>" + "world from the shadows with their invisible hand. The group of the rich and wealthy " +
`THE ${FactionNames.Illuminati.toUpperCase()} ARE THE SECRET RULERS OF THE WORLD!<br><br>` + "that have penetrated every major government, financial agency, and corporation in the last " +
`Yes, the ${FactionNames.Illuminati} of legends. The ancient secret society that controls the entire ` + "three hundred years.<br><br>" +
"world from the shadows with their invisible hand. The group of the rich and wealthy " + "OPEN YOUR EYES<br><br>" +
"that have penetrated every major government, financial agency, and corporation in the last " + `It was the ${FactionNames.Illuminati} that brought an end to democracy in the world. They are the driving force ` +
"three hundred years.<br><br>" + "behind everything that happens.<br><br>" +
"OPEN YOUR EYES<br><br>" + "THEY ARE ALL AROUND YOU<br><br>" +
`It was the ${FactionNames.Illuminati} that brought an end to democracy in the world. They are the driving force ` + "After destabilizing the world's governments, they are now entering the final stage of their master plan. " +
"behind everything that happens.<br><br>" + "They will secretly initiate global crises. Terrorism. Pandemics. World War. And out of the chaos " +
"THEY ARE ALL AROUND YOU<br><br>" + "that ensues they will build their New World Order.",
"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 " + [LiteratureName.TheNewGod]: new Literature({
"that ensues they will build their New World Order."; title: "The New God",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.TheNewGod,
text:
title = "The New God"; "Everyone has a moment in their life when they wonder about the bigger questions.<br><br>" +
fn = LiteratureNames.TheNewGod; "What's the point of all this? What is my purpose?<br><br>" +
txt = "Some people dare to think even bigger.<br><br>" +
"Everyone has a moment in their life when they wonder about the bigger questions.<br><br>" + "What will the fate of the human race be?<br><br>" +
"What's the point of all this? What is my purpose?<br><br>" + "We live in an era vastly different from that of 15 or even 20 years ago. We have gone " +
"Some people dare to think even bigger.<br><br>" + "beyond the limits of humanity. We have stripped ourselves of the tyranny of flesh.<br><br>" +
"What will the fate of the human race be?<br><br>" + "The Singularity is here. The merging of man and machine. This is where humanity evolves into " +
"We live in an era vastly different from that of 15 or even 20 years ago. We have gone " + "something greater. This is our future.<br><br>" +
"beyond the limits of humanity. We have stripped ourselves of the tyranny of flesh.<br><br>" + "Embrace it, and you will obey a new god. The God in the Machine.",
"The Singularity is here. The merging of man and machine. This is where humanity evolves into " + }),
"something greater. This is our future.<br><br>" + [LiteratureName.NewTriads]: new Literature({
"Embrace it, and you will obey a new god. The God in the Machine."; title: "The New Triads",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.NewTriads,
text:
title = "The New Triads"; "The Triads were an ancient transnational crime syndicate based in China, Hong Kong, and other Asian " +
fn = LiteratureNames.NewTriads; "territories. They were often considered one of the first and biggest criminal secret societies. " +
txt = "While most of the branches of the Triads have been destroyed over the past few decades, the " +
"The Triads were an ancient transnational crime syndicate based in China, Hong Kong, and other Asian " + "crime faction has spawned and inspired a number of other Asian crime organizations over the past few years. " +
"territories. They were often considered one of the first and biggest criminal secret societies. " + `The most notable of these is the ${FactionNames.Tetrads}.<br><br>` +
"While most of the branches of the Triads have been destroyed over the past few decades, the " + `It is widely believed that the ${FactionNames.Tetrads} are a rogue group that splintered off from the Triads sometime in the ` +
"crime faction has spawned and inspired a number of other Asian crime organizations over the past few years. " + `mid 21st century. The founders of the ${FactionNames.Tetrads}, all of whom were ex-Triad members, believed that the ` +
`The most notable of these is the ${FactionNames.Tetrads}.<br><br>` + `Triads were losing their purpose and direction. The ${FactionNames.Tetrads} started off as a small group that mainly engaged ` +
`It is widely believed that the ${FactionNames.Tetrads} are a rogue group that splintered off from the Triads sometime in the ` + "in fraud and extortion. They were largely unknown until just a few years ago when they took over the illegal " +
`mid 21st century. The founders of the ${FactionNames.Tetrads}, all of whom were ex-Triad members, believed that the ` + "drug trade in all of the major Asian cities. They quickly became the most powerful crime syndicate in the " +
`Triads were losing their purpose and direction. The ${FactionNames.Tetrads} started off as a small group that mainly engaged ` + "continent.<br><br>" +
"in fraud and extortion. They were largely unknown until just a few years ago when they took over the illegal " + `Not much else is known about the ${FactionNames.Tetrads}, or about the efforts the Asian governments and corporations are making ` +
"drug trade in all of the major Asian cities. They quickly became the most powerful crime syndicate in the " + `to take down this large new crime organization. Many believe that the ${FactionNames.Tetrads} have infiltrated the governments ` +
"continent.<br><br>" + "and powerful corporations in Asia, which has helped facilitate their recent rapid rise.",
`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 ` + [LiteratureName.TheSecretWar]: new Literature({
"and powerful corporations in Asia, which has helped facilitate their recent rapid rise."; title: "The Secret War",
Literatures[fn] = new Literature(title, fn, txt); filename: LiteratureName.TheSecretWar,
text: "",
title = "The Secret War"; }),
fn = LiteratureNames.TheSecretWar; };
txt = "";
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,108 +804,72 @@ 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) => const destination = helpers.string(ctx, "destination", _destination);
(_files, _destination, _source = ctx.workerScript.hostname) => { const source = helpers.string(ctx, "source", _source ?? ctx.workerScript.hostname);
const destination = helpers.string(ctx, "destination", _destination); const destServer = helpers.getServer(ctx, destination);
const source = helpers.string(ctx, "source", _source); const sourceServer = helpers.getServer(ctx, source);
const destServer = helpers.getServer(ctx, destination); const files = Array.isArray(_files) ? _files : [_files];
const sourceServ = helpers.getServer(ctx, source); const lits: (FilePath & LiteratureName)[] = [];
const files = Array.isArray(_files) ? _files : [_files]; const contentFiles: ContentFilePath[] = [];
//First loop through filenames to find all errors before moving anything.
for (const file of files) {
// Not a string
if (typeof file !== "string") {
throw helpers.makeRuntimeErrorMsg(ctx, "files should be a string or an array of strings.");
}
if (hasScriptExtension(file) || hasTextExtension(file)) {
const path = resolveScriptFilePath(file, ctx.workerScript.name);
if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filepath: ${file}`);
contentFiles.push(path);
continue;
}
if (!file.endsWith(".lit")) {
throw helpers.makeRuntimeErrorMsg(ctx, "Only works for scripts, .lit and .txt files.");
}
const sanitizedPath = resolveFilePath(file, ctx.workerScript.name);
if (!sanitizedPath || !checkEnum(LiteratureName, sanitizedPath)) {
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: '${file}'`);
}
lits.push(sanitizedPath);
}
//First loop through filenames to find all errors before moving anything. let noFailures = true;
for (const file of files) { // --- Scripts and Text Files---
// Not a string for (const contentFilePath of contentFiles) {
if (typeof file !== "string") const sourceContentFile = sourceServer.getContentFile(contentFilePath);
throw helpers.makeRuntimeErrorMsg(ctx, "files should be a string or an array of strings."); if (!sourceContentFile) {
helpers.log(ctx, () => `File '${contentFilePath}' does not exist.`);
noFailures = false;
continue;
}
// Overwrite script if it already exists
const result = destServer.writeToContentFile(contentFilePath, sourceContentFile.content);
helpers.log(ctx, () => `Copied file ${contentFilePath} from ${sourceServer} to ${destServer}`);
if (result.overwritten) helpers.log(ctx, () => `Warning: ${contentFilePath} was overwritten on ${destServer}`);
}
// Invalid file name // --- Literature Files ---
if (!isValidFilePath(file)) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: '${file}'`); for (const litFilePath of lits) {
const sourceMessage = sourceServer.messages.find((message) => message === litFilePath);
// Invalid file type if (!sourceMessage) {
if (!file.endsWith(".lit") && !isScriptFilename(file) && !file.endsWith(".txt")) { helpers.log(ctx, () => `File '${litFilePath}' does not exist.`);
throw helpers.makeRuntimeErrorMsg(ctx, "Only works for scripts, .lit and .txt files."); noFailures = false;
} continue;
} }
let noFailures = true; const destMessage = destServer.messages.find((message) => message === litFilePath);
//ts detects files as any[] here even though we would have thrown in the above loop if it wasn't string[] if (destMessage) {
for (let file of files as string[]) { helpers.log(ctx, () => `File '${litFilePath}' was already on '${destServer?.hostname}'.`);
// 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` continue;
if (file.startsWith("/") && file.indexOf("/", 1) === -1) file = file.slice(1);
// Scp for lit files
if (file.endsWith(".lit")) {
const sourceMessage = sourceServ.messages.find((message) => message === file);
if (!sourceMessage) {
helpers.log(ctx, () => `File '${file}' does not exist.`);
noFailures = false;
continue;
}
const destMessage = destServer.messages.find((message) => message === file);
if (destMessage) {
helpers.log(ctx, () => `File '${file}' was already on '${destServer?.hostname}'.`);
continue;
}
destServer.messages.push(file);
helpers.log(ctx, () => `File '${file}' copied over to '${destServer?.hostname}'.`);
continue;
}
// Scp for text files
if (file.endsWith(".txt")) {
const sourceTextFile = sourceServ.textFiles.find((textFile) => textFile.fn === file);
if (!sourceTextFile) {
helpers.log(ctx, () => `File '${file}' does not exist.`);
noFailures = false;
continue;
}
const destTextFile = destServer.textFiles.find((textFile) => textFile.fn === file);
if (destTextFile) {
destTextFile.text = sourceTextFile.text;
helpers.log(ctx, () => `File '${file}' overwritten on '${destServer?.hostname}'.`);
continue;
}
const newFile = new TextFile(sourceTextFile.fn, sourceTextFile.text);
destServer.textFiles.push(newFile);
helpers.log(ctx, () => `File '${file}' copied over to '${destServer?.hostname}'.`);
continue;
}
// Scp for script files
const sourceScript = sourceServ.scripts.get(file);
if (!sourceScript) {
helpers.log(ctx, () => `File '${file}' does not exist.`);
noFailures = false;
continue;
}
// Overwrite script if it already exists
const destScript = destServer.scripts.get(file);
if (destScript) {
if (destScript.code === sourceScript.code) {
helpers.log(ctx, () => `Identical file '${file}' was already on '${destServer?.hostname}'`);
continue;
}
destScript.code = sourceScript.code;
// Set ramUsage to null in order to force a recalculation prior to next run.
destScript.invalidateModule();
helpers.log(ctx, () => `WARNING: File '${file}' overwritten on '${destServer?.hostname}'`);
continue;
}
// Create new script if it does not already exist
const newScript = new Script(file, sourceScript.code, destServer.hostname);
destServer.scripts.set(file, newScript);
helpers.log(ctx, () => `File '${file}' copied over to '${destServer?.hostname}'.`);
} }
return noFailures; destServer.messages.push(litFilePath);
}, helpers.log(ctx, () => `File '${litFilePath}' copied over to '${destServer?.hostname}'.`);
continue;
}
return noFailures;
},
ls: (ctx) => (_hostname, _substring) => { ls: (ctx) => (_hostname, _substring) => {
const hostname = helpers.string(ctx, "hostname", _hostname); const hostname = helpers.string(ctx, "hostname", _hostname);
const substring = helpers.string(ctx, "substring", _substring ?? ""); const substring = helpers.string(ctx, "substring", _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
let script = ctx.workerScript.getScriptOnServer(filename, server);
if (!script) {
// Create a new script
script = new Script(filename, String(data), server.hostname);
server.scripts.set(filename, script);
return;
}
mode === "w" ? (script.code = data) : (script.code += data);
// Set ram to null so a recalc is performed the next time ram usage is needed
script.invalidateModule();
return;
} else {
// Write to text file
if (!filename.endsWith(".txt")) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: ${filename}`);
const txtFile = getTextFile(filename, server);
if (txtFile == null) {
createTextFile(filename, String(data), server);
return;
}
if (mode === "w") { if (mode === "w") {
txtFile.write(String(data)); server.writeToScriptFile(filepath, data);
} else { return;
txtFile.append(String(data));
} }
const existingScript = server.scripts.get(filepath);
const existingCode = existingScript ? existingScript.code : "";
server.writeToScriptFile(filepath, existingCode + data);
return;
} }
return; if (!hasTextExtension(filepath)) {
throw helpers.makeRuntimeErrorMsg(ctx, `File path should be a text file or script. ${filepath} is invalid.`);
}
if (mode === "w") {
server.writeToTextFile(filepath, data);
return;
}
const existingTextFile = server.textFiles.get(filepath);
const existingText = existingTextFile?.text ?? "";
server.writeToTextFile(filepath, mode === "w" ? data : existingText + data);
}, },
tryWritePort: (ctx) => (_portNumber, data) => { 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,20 +1398,22 @@ 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 s = helpers.getServer(ctx, hostname);
const hostname = helpers.string(ctx, "hostname", _hostname); if (!filepath) {
const s = helpers.getServer(ctx, hostname); 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 + "");
} }
return status.res; return status.res;
}, },
scriptRunning: (ctx) => (_scriptname, _hostname) => { scriptRunning: (ctx) => (_scriptname, _hostname) => {
const scriptname = helpers.string(ctx, "scriptname", _scriptname); const scriptname = helpers.string(ctx, "scriptname", _scriptname);
const hostname = helpers.string(ctx, "hostname", _hostname); const hostname = helpers.string(ctx, "hostname", _hostname);
@ -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,54 +1198,49 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
} }
return item.price; return item.price;
}, },
b1tflum3: b1tflum3: (ctx) => (_nextBN, _cbScript) => {
(ctx) => helpers.checkSingularityAccess(ctx);
(_nextBN, _callbackScript = "") => { const nextBN = helpers.number(ctx, "nextBN", _nextBN);
helpers.checkSingularityAccess(ctx); const cbScript = _cbScript
const nextBN = helpers.number(ctx, "nextBN", _nextBN); ? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
const callbackScript = helpers.string(ctx, "callbackScript", _callbackScript); : false;
helpers.checkSingularityAccess(ctx); 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); destroyW0r1dD43m0n: (ctx) => (_nextBN, _cbScript) => {
}, 0); helpers.checkSingularityAccess(ctx);
}, const nextBN = helpers.number(ctx, "nextBN", _nextBN);
destroyW0r1dD43m0n: if (nextBN > 13 || nextBN < 1 || !Number.isInteger(nextBN)) {
(ctx) => throw new Error(`Invalid bitnode specified: ${_nextBN}`);
(_nextBN, _callbackScript = "") => { }
helpers.checkSingularityAccess(ctx); const cbScript = _cbScript
const nextBN = helpers.number(ctx, "nextBN", _nextBN); ? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
if (nextBN > 13 || nextBN < 1 || !Number.isInteger(nextBN)) { : false;
throw new Error(`Invalid bitnode specified: ${_nextBN}`); if (cbScript === null) throw helpers.makeRuntimeErrorMsg(ctx, `Could not resolve file path: ${_cbScript}`);
}
const callbackScript = helpers.string(ctx, "callbackScript", _callbackScript);
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.");
const hackingRequirements = () => { const hackingRequirements = () => {
if (Player.skills.hacking < wd.requiredHackingSkill) return false; if (Player.skills.hacking < wd.requiredHackingSkill) return false;
if (!wd.hasAdminRights) return false; if (!wd.hasAdminRights) return false;
return true; return true;
}; };
const bladeburnerRequirements = () => { const bladeburnerRequirements = () => {
if (!Player.bladeburner) return false; if (!Player.bladeburner) return false;
return Player.bladeburner.blackops[BlackOperationNames.OperationDaedalus]; return Player.bladeburner.blackops[BlackOperationNames.OperationDaedalus];
}; };
if (!hackingRequirements() && !bladeburnerRequirements()) { if (!hackingRequirements() && !bladeburnerRequirements()) {
helpers.log(ctx, () => "Requirements not met to destroy the world daemon"); helpers.log(ctx, () => "Requirements not met to destroy the world daemon");
return; return;
} }
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;
return Player.currentWork.APICopy(); return Player.currentWork.APICopy();

@ -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); return new RFAMessage({ result: "OK", id: msg.id });
}
// If and only if the content is actually changed correctly, send back an OK. return error("Invalid file extension", msg);
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 });
}, },
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); if (!file) return error("File doesn't exist", msg);
return new RFAMessage({ result: scriptContent.code, id: msg.id }); return new RFAMessage({ result: file.content, id: msg.id });
} else {
// Assume it's a text file
const file = server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0);
if (!file) return error("File doesn't exist", msg);
return new RFAMessage({ result: file.text, id: msg.id });
}
}, },
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.
server.scripts = new JSONMap(); const oldScripts = Array.isArray(server.scripts) ? (server.scripts as Script[]) : [...server.scripts.values()];
for (const script of oldScripts) { server.scripts = new JSONMap();
server.scripts.set(script.filename, script); // In case somehow there are previously valid filenames that can't be sanitized, they will go in a new directory with a note.
for (const script of oldScripts) {
let newFilePath = resolveScriptFilePath(script.filename);
if (!newFilePath) {
newFilePath = `${newDirectory}script${++invalidScriptCount}.js` as ScriptFilePath;
script.content = `// Original path: ${script.filename}. Path was no longer valid\n` + script.content;
}
script.filename = newFilePath;
server.scripts.set(newFilePath, script);
}
// Handle changing textFiles to a map as well as FilePath changes at the same time.
if (Array.isArray(server.textFiles)) {
const oldTextFiles = server.textFiles as (TextFile & { fn?: string })[];
server.textFiles = new JSONMap();
let invalidTextCount = 0;
for (const textFile of oldTextFiles) {
const oldName = textFile.fn ?? textFile.filename;
delete textFile.fn;
let newFilePath = resolveTextFilePath(oldName);
if (!newFilePath) {
newFilePath = `${newDirectory}text${++invalidTextCount}.txt` as TextFilePath;
textFile.content = `// Original path: ${textFile.filename}. Path was no longer valid\n` + textFile.content;
}
textFile.filename = newFilePath;
server.textFiles.set(newFilePath, textFile);
} }
} }
} }

@ -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)) { script.invalidateModule();
return { res: false, msg: "Cannot delete a script that is currently running!" }; this.scripts.delete(path);
} return { res: true };
script.invalidateModule(); }
this.scripts.delete(filename); if (hasProgramExtension(path)) {
const programIndex = this.programs.findIndex((program) => program === path);
if (programIndex === -1) return { res: false, msg: `Program ${path} does not exist` };
this.programs.splice(programIndex, 1);
return { res: true };
}
if (path.endsWith(".lit")) {
const litIndex = this.messages.findIndex((lit) => lit === path);
if (litIndex === -1) return { res: false, msg: `Literature file ${path} does not exist` };
this.messages.splice(litIndex, 1);
return { res: true };
}
if (path.endsWith(".cct")) {
const contractIndex = this.contracts.findIndex((program) => program);
if (contractIndex === -1) return { res: false, msg: `Contract file ${path} does not exist` };
this.contracts.splice(contractIndex, 1);
return { res: true }; return { res: true };
} else if (filename.endsWith(".lit")) {
for (let i = 0; i < this.messages.length; ++i) {
const f = this.messages[i];
if (typeof f === "string" && f === filename) {
this.messages.splice(i, 1);
return { res: true };
}
}
} else if (filename.endsWith(".txt")) {
for (let i = 0; i < this.textFiles.length; ++i) {
if (this.textFiles[i].fn === filename) {
this.textFiles.splice(i, 1);
return { res: true };
}
}
} else if (filename.endsWith(".cct")) {
for (let i = 0; i < this.contracts.length; ++i) {
if (this.contracts[i].fn === filename) {
this.contracts.splice(i, 1);
return { res: true };
}
}
} }
return { res: false, msg: "No such file exists" }; return { res: false, msg: `Unhandled file extension on file path ${path}` };
} }
/** /**
@ -245,15 +227,13 @@ export abstract class BaseServer implements IServer {
this.ramUsed = ram; 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;
}
}
} else if (filename.endsWith(".txt")) {
const txt = Terminal.getTextFile(relative_filename);
if (txt != null) {
txt.show();
return;
}
} else if (filename.endsWith(".script") || filename.endsWith(".js")) {
const script = Terminal.getScript(relative_filename);
if (script != null) {
dialogBoxCreate(`${script.filename}\n\n${script.code}`);
return;
}
} }
if (checkEnum(LiteratureName, path)) {
Terminal.error(`No such file ${filename}`); if (server.messages.includes(path)) showLiterature(path);
}
Terminal.error(`No file at path ${path}`);
} }

@ -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);
let filesToLoadOrCreate = args; // Glob of existing files
try { if (pattern.includes("*") || pattern.includes("?")) {
const globSearch = detectSimpleScriptGlob({ args, server }); for (const [path, file] of getGlobbedFileMap(pattern, server, Terminal.currDir)) {
if (globSearch) { filesToOpen.set(path, file.content);
if (isEmpty(globSearch.globError) === false) throw new Error(globSearch.globError); }
filesToLoadOrCreate = globSearch.globMatches; continue;
} }
const files = filesToLoadOrCreate.map((arg) => { // Non-glob, files do not need to already exist
const filename = `${arg}`; const path = Terminal.getFilepath(pattern);
if (!path) return Terminal.error(`Invalid file path ${arg}`);
if (isScriptFilename(filename)) { if (!hasScriptExtension(path) && !hasTextExtension(path)) {
const filepath = Terminal.getFilepath(filename); return Terminal.error(`${command}: Only scripts or text files can be edited. Invalid file type: ${arg}`);
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}\``);
} }
const file = server.getContentFile(path);
Router.toScriptEditor(Object.fromEntries(files), scriptEditorRouteOptions); const content = file ? file.content : isNs2(path) ? newNs2Template : "";
} catch (e) { filesToOpen.set(path, content);
Terminal.error(`${e}`); if (content === newNs2Template) CursorPositions.saveCursor(path, { row: 3, column: 5 });
} }
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] + "");
if (src === null) return Terminal.error(`Invalid source filename ${args[0]}`);
// Get the destination based on the source file and the current directory
const t_dst = getDestinationFilepath(args[1] + "", src, Terminal.cwd());
if (t_dst === null) return Terminal.error("error parsing dst file");
// Convert a relative path destination file to the absolute path.
const dst = Terminal.getFilepath(t_dst);
if (!dst) return Terminal.error(`Invalid destination filename ${t_dst}`);
if (areFilesEqual(src, dst)) return Terminal.error("src and dst cannot be the same");
const srcExt = src.slice(src.lastIndexOf("."));
const dstExt = dst.slice(dst.lastIndexOf("."));
if (srcExt !== dstExt) return Terminal.error("src and dst must have the same extension.");
if (!isScriptFilename(src) && !src.endsWith(".txt")) {
return Terminal.error("cp only works for scripts and .txt files");
}
// Scp for txt files
if (src.endsWith(".txt")) {
let txtFile = null;
for (let i = 0; i < server.textFiles.length; ++i) {
if (areFilesEqual(server.textFiles[i].fn, src)) {
txtFile = server.textFiles[i];
break;
}
}
if (txtFile === null) {
return Terminal.error("No such file exists!");
}
const tRes = server.writeToTextFile(dst, txtFile.text);
if (!tRes.success) {
Terminal.error("cp failed");
return;
}
if (tRes.overwritten) {
Terminal.print(`WARNING: ${dst} already exists and will be overwritten`);
Terminal.print(`${dst} overwritten`);
return;
}
Terminal.print(`${dst} copied`);
return;
}
// Get the current script
const sourceScript = server.scripts.get(src);
if (!sourceScript) return Terminal.error("cp failed. No such script exists");
const sRes = server.writeToScriptFile(dst, sourceScript.code);
if (!sRes.success) {
Terminal.error(`cp failed`);
return;
}
if (sRes.overwritten) {
Terminal.print(`WARNING: ${dst} already exists and will be overwritten`);
Terminal.print(`${dst} overwritten`);
return;
}
Terminal.print(`${dst} copied`);
} catch (e) {
Terminal.error(e + "");
} }
// Find the source file
const sourceFilePath = Terminal.getFilepath(String(args[0]));
if (!sourceFilePath) return Terminal.error(`Invalid source filename ${args[0]}`);
if (!hasTextExtension(sourceFilePath) && !hasScriptExtension(sourceFilePath)) {
return Terminal.error("cp: Can only be performed on script and text files");
}
const source = server.getContentFile(sourceFilePath);
if (!source) return Terminal.error(`File not found: ${sourceFilePath}`);
// Determine the destination file path.
const destinationInput = String(args[1]);
// First treat the input as a file path. If that fails, try treating it as a directory and reusing source filename.
let destFilePath = Terminal.getFilepath(destinationInput);
if (!destFilePath) {
const destDirectory = Terminal.getDirectory(destinationInput);
if (!destDirectory) return Terminal.error(`Could not resolve ${destinationInput} as a FilePath or Directory`);
destFilePath = combinePath(destDirectory, getFilenameOnly(sourceFilePath));
}
if (!hasTextExtension(destFilePath) && !hasScriptExtension(destFilePath)) {
return Terminal.error(`cp: Can only copy to script and text files (${destFilePath} is invalid destination)`);
}
const result = server.writeToContentFile(destFilePath, source.content);
Terminal.print(`File ${sourceFilePath} copied to ${destFilePath}`);
if (result.overwritten) Terminal.warn(`${destFilePath} was overwritten.`);
} }

@ -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) { return Terminal.error("Incorrect usage of download command. Usage: download [script/text file]");
Terminal.error("Incorrect usage of download command. Usage: download [script/text file]");
return;
}
const fn = args[0] + "";
// If the parameter starts with *, download all files that match the wildcard pattern
if (fn.startsWith("*")) {
try {
exportScripts(fn, server);
return;
} catch (e: unknown) {
let msg = String(e);
if (e !== null && typeof e == "object" && e.hasOwnProperty("message")) {
msg = String((e as { message: unknown }).message);
}
return Terminal.error(msg);
}
} else if (isScriptFilename(fn)) {
// Download a single script
const script = Terminal.getScript(fn);
if (script != null) {
return script.download();
}
} else if (fn.endsWith(".txt")) {
// Download a single text file
const txt = Terminal.getTextFile(fn);
if (txt != null) {
return txt.download();
}
} else {
Terminal.error(`Cannot download this filetype`);
return;
}
Terminal.error(`${fn} does not exist`);
return;
} catch (e) {
Terminal.error(e + "");
return;
} }
const pattern = String(args[0]);
// If the path contains a * or ?, treat as glob
if (pattern.includes("*") || pattern.includes("?")) {
try {
exportScripts(pattern, server, Terminal.currDir);
return;
} catch (e: any) {
const msg = String(e?.message ?? e);
return Terminal.error(msg);
}
}
const path = Terminal.getFilepath(pattern);
if (!path) return Terminal.error(`Could not resolve path ${pattern}`);
if (!hasScriptExtension(path) && !hasTextExtension(path)) {
return Terminal.error("Can only download script and text files");
}
const file = server.getContentFile(path);
if (!file) return Terminal.error(`File not found: ${path}`);
return file.download();
} }

@ -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) { return Terminal.error("Incorrect usage of kill command. Usage: kill [pid] or kill [scriptname] [arg1] [arg2]...");
Terminal.error("Incorrect usage of kill command. Usage: kill [scriptname] [arg1] [arg2]...");
return;
}
if (typeof args[0] === "boolean") {
return;
}
// Kill by PID
if (typeof args[0] === "number") {
const pid = args[0];
const res = killWorkerScript(pid);
if (res) {
Terminal.print(`Killing script with PID ${pid}`);
} else {
Terminal.error(`Failed to kill script with PID ${pid}. No such script is running`);
}
return;
}
const scriptName = Terminal.getFilepath(args[0]);
if (!scriptName) return Terminal.error(`Invalid filename: ${args[0]}`);
const runningScript = server.getRunningScript(scriptName, args.slice(1));
if (runningScript == null) {
Terminal.error("No such script is running. Nothing to kill");
return;
}
killWorkerScript({ runningScript: runningScript, hostname: server.hostname });
Terminal.print(`Killing ${scriptName}`);
} catch (e) {
Terminal.error(e + "");
} }
if (typeof args[0] === "number") {
const pid = args[0];
if (killWorkerScript(pid)) return Terminal.print(`Killing script with PID ${pid}`);
}
// Shift args doesn't need to be sliced to check runningScript args
const fileName = String(args.shift());
const path = Terminal.getFilepath(fileName);
if (!path) return Terminal.error(`Could not parse filename: ${fileName}`);
if (!hasScriptExtension(path)) return Terminal.error(`${path} does not have a script file extension`);
const runningScript = server.getRunningScript(path, args);
if (runningScript == null) return Terminal.error("No such script is running. Nothing to kill");
killWorkerScript(runningScript.pid);
Terminal.print(`Killing ${path}`);
} }

@ -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;
// Check if there's a directory in the parsed path, if so we need to add the folder and not the file.
const firstParentDir = getFirstDirectoryInPath(parsedPath);
if (firstParentDir) {
if (!firstParentDir.includes(filter) || folders.includes(firstParentDir)) return;
folders.push(firstParentDir);
return; return;
} }
dest.push(parsedPath);
// If the fn includes a forward slash, it must be in a subdirectory.
// Therefore, we only list the "first" directory in its path
if (parsedFn.includes("/")) {
const firstParentDir = getFirstParentDirectory(parsedFn);
if (filter && !firstParentDir.includes(filter)) {
return;
}
if (!folders.includes(firstParentDir)) {
folders.push(firstParentDir);
}
return;
}
dest.push(parsedFn);
} }
// 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; | {
segments: string[]; // Types that are not clickable only need to be string[]
} type: FileType.Folder | FileType.Program | FileType.Contract | FileType.TextFile;
segments: string[];
}
| { type: FileType.Message; segments: FilePath[] }
| { type: FileType.Script; segments: ScriptFilePath[] };
function postSegments(group: FileGroup, flags: LSFlags): void { 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 sourcePath = Terminal.getFilepath(source);
const source = args[0] + ""; if (!sourcePath) return Terminal.error(`Invalid source filename: ${source}`);
const t_dest = args[1] + ""; const destinationPath = Terminal.getFilepath(destination);
if (!destinationPath) return Terminal.error(`Invalid destination filename: ${destinationPath}`);
if (!isScriptFilename(source) && !source.endsWith(".txt")) { if (
Terminal.error(`'mv' can only be used on scripts and text files (.txt)`); (!hasScriptExtension(sourcePath) && !hasTextExtension(sourcePath)) ||
return; (!hasScriptExtension(destinationPath) && !hasTextExtension(destinationPath))
} ) {
return Terminal.error(`'mv' can only be used on scripts and text files (.txt)`);
const srcFile = Terminal.getFile(source);
if (srcFile == null) return Terminal.error(`Source file ${source} does not exist`);
const sourcePath = Terminal.getFilepath(source);
if (!sourcePath) return Terminal.error(`Invalid source filename: ${source}`);
// Get the destination based on the source file and the current directory
const dest = getDestinationFilepath(t_dest, source, Terminal.cwd());
if (dest === null) return Terminal.error("error parsing dst file");
const destFile = Terminal.getFile(dest);
const destPath = Terminal.getFilepath(dest);
if (!destPath) return Terminal.error(`Invalid destination filename: ${destPath}`);
if (areFilesEqual(sourcePath, destPath)) return Terminal.error(`Source and destination files are the same file`);
// 'mv' command only works on scripts and txt files.
// Also, you can't convert between different file types
if (isScriptFilename(source)) {
const script = srcFile as Script;
if (!isScriptFilename(destPath)) return Terminal.error(`Source and destination files must have the same type`);
// Command doesn't work if script is running
if (server.isRunning(sourcePath)) return Terminal.error(`Cannot use 'mv' on a script that is running`);
if (destFile != null) {
// Already exists, will be overwritten, so we'll delete it
// Command doesn't work if script is running
if (server.isRunning(destPath)) {
Terminal.error(`Cannot use 'mv' on a script that is running`);
return;
}
const status = server.removeFile(destPath);
if (!status.res) {
Terminal.error(`Something went wrong...please contact game dev (probably a bug)`);
return;
} else {
Terminal.print("Warning: The destination file was overwritten");
}
}
script.filename = destPath;
} else if (srcFile instanceof TextFile) {
const textFile = srcFile;
if (!dest.endsWith(".txt")) {
Terminal.error(`Source and destination files must have the same type`);
return;
}
if (destFile != null) {
// Already exists, will be overwritten, so we'll delete it
const status = server.removeFile(destPath);
if (!status.res) {
Terminal.error(`Something went wrong...please contact game dev (probably a bug)`);
return;
} else {
Terminal.print("Warning: The destination file was overwritten");
}
}
textFile.fn = destPath;
}
} catch (e) {
Terminal.error(e + "");
} }
// Allow content to be moved between scripts and textfiles, no need to limit this.
const sourceContentFile = server.getContentFile(sourcePath);
if (!sourceContentFile) return Terminal.error(`Source file ${sourcePath} does not exist`);
if (!sourceContentFile.deleteFromServer(server)) {
return Terminal.error(`Could not remove source file ${sourcePath} from existing location.`);
}
Terminal.print(`Moved ${sourcePath} to ${destinationPath}`);
const { overwritten } = server.writeToContentFile(destinationPath, sourceContentFile.content);
if (overwritten) Terminal.warn(`${destinationPath} was overwritten.`);
} }

@ -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) { return Terminal.error("Incorrect usage of scp command. Usage: scp [source filename] [destination hostname]");
Terminal.error("Incorrect usage of scp command. Usage: scp [file] [destination hostname]");
return;
}
const scriptname = Terminal.getFilepath(args[0] + "");
if (!scriptname) return Terminal.error(`Invalid filename: ${args[0]}`);
if (!scriptname.endsWith(".lit") && !isScriptFilename(scriptname) && !scriptname.endsWith(".txt")) {
Terminal.error("scp only works for scripts, text files (.txt), and literature files (.lit)");
return;
}
const destServer = GetServer(args[1] + "");
if (destServer == null) {
Terminal.error(`Invalid destination. ${args[1]} not found`);
return;
}
// Scp for lit files
if (scriptname.endsWith(".lit")) {
if (!server.messages.includes(scriptname)) return Terminal.error("No such file exists!");
const onDestServer = destServer.messages.includes(scriptname);
if (!onDestServer) destServer.messages.push(scriptname);
return Terminal.print(`${scriptname} ${onDestServer ? "was already on" : "copied to"} ${destServer.hostname}`);
}
// Scp for txt files
if (scriptname.endsWith(".txt")) {
const txtFile = server.textFiles.find((txtFile) => txtFile.fn === scriptname);
if (!txtFile) return Terminal.error("No such file exists!");
const tRes = destServer.writeToTextFile(txtFile.fn, txtFile.text);
if (!tRes.success) {
Terminal.error("scp failed");
return;
}
if (tRes.overwritten) {
Terminal.print(`WARNING: ${scriptname} already exists on ${destServer.hostname} and will be overwritten`);
Terminal.print(`${scriptname} overwritten on ${destServer.hostname}`);
return;
}
Terminal.print(`${scriptname} copied over to ${destServer.hostname}`);
return;
}
// Get the current script
const sourceScript = server.scripts.get(scriptname);
if (!sourceScript) return Terminal.error("scp failed. No such script exists");
const sRes = destServer.writeToScriptFile(scriptname, sourceScript.code);
if (!sRes.success) {
Terminal.error(`scp failed`);
return;
}
if (sRes.overwritten) {
Terminal.print(`WARNING: ${scriptname} already exists on ${destServer.hostname} and will be overwritten`);
Terminal.print(`${scriptname} overwritten on ${destServer.hostname}`);
return;
}
Terminal.print(`${scriptname} copied over to ${destServer.hostname}`);
} catch (e) {
Terminal.error(e + "");
} }
const [scriptname, destHostname] = args.map((arg) => arg + "");
const path = Terminal.getFilepath(scriptname);
if (!path) return Terminal.error(`Invalid file path: ${scriptname}`);
const destServer = GetServer(destHostname);
if (!destServer) return Terminal.error(`Invalid destination server: ${args[1]}`);
// Lit files
if (path.endsWith(".lit")) {
if (!checkEnum(LiteratureName, path) || !server.messages.includes(path)) {
return Terminal.error(`No file at path ${path}`);
}
if (destServer.messages.includes(path)) return Terminal.print(`${path} was already on ${destHostname}`);
destServer.messages.push(path);
return Terminal.print(`Copied ${path} to ${destHostname}`);
}
if (!hasScriptExtension(path) && !hasTextExtension(path)) {
return Terminal.error("scp only works for scripts, text files (.txt), and literature files (.lit)");
}
// Text or script
const source = server.getContentFile(path);
if (!source) return Terminal.error(`No file at path ${path}`);
const { overwritten } = destServer.writeToContentFile(path, source.content);
if (overwritten) Terminal.warn(`${path} already exists on ${destHostname} and will be overwritten`);
Terminal.print(`${path} copied to ${destHostname}`);
} }

@ -1,74 +1,66 @@
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) { return Terminal.error("Incorrect number of arguments. Usage: tail [script] [arg1] [arg2]...");
Terminal.error("Incorrect number of arguments. Usage: tail [script] [arg1] [arg2]...");
} else if (typeof commandArray[0] === "string") {
const scriptName = Terminal.getFilepath(commandArray[0]);
if (!scriptName) return Terminal.error(`Invalid filename: ${commandArray[0]}`);
if (!isScriptFilename(scriptName)) {
Terminal.error(`tail can only be called on ${validScriptExtensions.join(", ")} files, or by PID`);
return;
}
// Get script arguments
const args = [];
for (let i = 1; i < commandArray.length; ++i) {
args.push(commandArray[i]);
}
// go over all the running scripts. If there's a perfect
// match, use it!
for (let i = 0; i < server.runningScripts.length; ++i) {
if (server.runningScripts[i].filename === scriptName && compareArrays(server.runningScripts[i].args, args)) {
LogBoxEvents.emit(server.runningScripts[i]);
return;
}
}
// Find all scripts that are potential candidates.
const candidates = [];
for (let i = 0; i < server.runningScripts.length; ++i) {
// only scripts that have more arguments (equal arguments is already caught)
if (server.runningScripts[i].args.length < args.length) continue;
// make a smaller copy of the args.
const args2 = server.runningScripts[i].args.slice(0, args.length);
if (server.runningScripts[i].filename === scriptName && compareArrays(args2, args)) {
candidates.push(server.runningScripts[i]);
}
}
// If there's only 1 possible choice, use that.
if (candidates.length === 1) {
LogBoxEvents.emit(candidates[0]);
return;
}
// otherwise lists all possible conflicting choices.
if (candidates.length > 1) {
Terminal.error("Found several potential candidates:");
for (const candidate of candidates) Terminal.error(`${candidate.filename} ${candidate.args.join(" ")}`);
Terminal.error("Script arguments need to be specified.");
return;
}
// if there's no candidate then we just don't know.
Terminal.error(`No script named ${scriptName} is running on the server`);
} else if (typeof commandArray[0] === "number") {
const runningScript = findRunningScriptByPid(commandArray[0], server);
if (runningScript == null) {
Terminal.error(`No script with PID ${commandArray[0]} is running on the server`);
return;
}
LogBoxEvents.emit(runningScript);
}
} catch (e) {
Terminal.error(e + "");
} }
if (typeof commandArray[0] === "number") {
const runningScript = findRunningScriptByPid(commandArray[0], server);
if (!runningScript) return Terminal.error(`No script with PID ${commandArray[0]} is running on the server`);
LogBoxEvents.emit(runningScript);
return;
}
const path = Terminal.getFilepath(String(commandArray[0]));
if (!path) return Terminal.error(`Invalid file path: ${commandArray[0]}`);
if (!hasScriptExtension(path)) return Terminal.error(`Invalid file extension. Tail can only be used on scripts.`);
// Get script arguments
const args = [];
for (let i = 1; i < commandArray.length; ++i) {
args.push(commandArray[i]);
}
// go over all the running scripts. If there's a perfect
// match, use it!
for (let i = 0; i < server.runningScripts.length; ++i) {
if (server.runningScripts[i].filename === path && compareArrays(server.runningScripts[i].args, args)) {
LogBoxEvents.emit(server.runningScripts[i]);
return;
}
}
// Find all scripts that are potential candidates.
const candidates = [];
for (let i = 0; i < server.runningScripts.length; ++i) {
// only scripts that have more arguments (equal arguments is already caught)
if (server.runningScripts[i].args.length < args.length) continue;
// make a smaller copy of the args.
const args2 = server.runningScripts[i].args.slice(0, args.length);
if (server.runningScripts[i].filename === path && compareArrays(args2, args)) {
candidates.push(server.runningScripts[i]);
}
}
// If there's only 1 possible choice, use that.
if (candidates.length === 1) {
LogBoxEvents.emit(candidates[0]);
return;
}
// otherwise lists all possible conflicting choices.
if (candidates.length > 1) {
Terminal.error("Found several potential candidates:");
for (const candidate of candidates) Terminal.error(`${candidate.filename} ${candidate.args.join(" ")}`);
Terminal.error("Script arguments need to be specified.");
return;
}
// if there's no candidate then we just don't know.
Terminal.error(`No script named ${path} is running on the server`);
} }

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