mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-12-19 12:45:45 +01:00
FILES: Path rework & typesafety (#479)
* Added new types for various file paths, all in the Paths folder. * TypeSafety and other helper functions related to these types * Added basic globbing support with * and ?. Currently only implemented for Script/Text, on nano and download terminal commands * Enforcing the new types throughout the codebase, plus whatever rewrites happened along the way * Server.textFiles is now a map * TextFile no longer uses a fn property, now it is filename * Added a shared ContentFile interface for shared functionality between TextFile and Script. * related to ContentFile change above, the player is now allowed to move a text file to a script file and vice versa. * File paths no longer conditionally start with slashes, and all directory names other than root have ending slashes. The player is still able to provide paths starting with / but this now indicates that the player is specifying an absolute path instead of one relative to root. * Singularized the MessageFilename and LiteratureName enums * Because they now only accept correct types, server.writeToXFile functions now always succeed (the only reasons they could fail before were invalid filepath). * Fix several issues with tab completion, which included pretty much a complete rewrite * Changed the autocomplete display options so there's less chance it clips outside the display area. * Turned CompletedProgramName into an enum. * Got rid of programsMetadata, and programs and DarkWebItems are now initialized immediately instead of relying on initializers called from the engine. * For any executable (program, cct, or script file) pathing can be used directly to execute without using the run command (previously the command had to start with ./ and it wasn't actually using pathing).
This commit is contained in:
parent
6f56f35943
commit
e0272ad4af
@ -13,7 +13,7 @@ import { HacknetNode } from "../Hacknet/HacknetNode";
|
||||
import { HacknetServer } from "../Hacknet/HacknetServer";
|
||||
import { CityName } from "../Enums";
|
||||
import { Player } from "@player";
|
||||
import { Programs } from "../Programs/Programs";
|
||||
import { CompletedProgramName } from "../Programs/Programs";
|
||||
import { GetAllServers, GetServer } from "../Server/AllServers";
|
||||
import { SpecialServers } from "../Server/data/SpecialServers";
|
||||
import { Server } from "../Server/Server";
|
||||
@ -126,33 +126,33 @@ export const achievements: Record<string, Achievement> = {
|
||||
"BRUTESSH.EXE": {
|
||||
...achievementData["BRUTESSH.EXE"],
|
||||
Icon: "p0",
|
||||
Condition: () => Player.getHomeComputer().programs.includes(Programs.BruteSSHProgram.name),
|
||||
Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.bruteSsh),
|
||||
},
|
||||
"FTPCRACK.EXE": {
|
||||
...achievementData["FTPCRACK.EXE"],
|
||||
Icon: "p1",
|
||||
Condition: () => Player.getHomeComputer().programs.includes(Programs.FTPCrackProgram.name),
|
||||
Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.ftpCrack),
|
||||
},
|
||||
//-----------------------------------------------------
|
||||
"RELAYSMTP.EXE": {
|
||||
...achievementData["RELAYSMTP.EXE"],
|
||||
Icon: "p2",
|
||||
Condition: () => Player.getHomeComputer().programs.includes(Programs.RelaySMTPProgram.name),
|
||||
Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.relaySmtp),
|
||||
},
|
||||
"HTTPWORM.EXE": {
|
||||
...achievementData["HTTPWORM.EXE"],
|
||||
Icon: "p3",
|
||||
Condition: () => Player.getHomeComputer().programs.includes(Programs.HTTPWormProgram.name),
|
||||
Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.httpWorm),
|
||||
},
|
||||
"SQLINJECT.EXE": {
|
||||
...achievementData["SQLINJECT.EXE"],
|
||||
Icon: "p4",
|
||||
Condition: () => Player.getHomeComputer().programs.includes(Programs.SQLInjectProgram.name),
|
||||
Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.sqlInject),
|
||||
},
|
||||
"FORMULAS.EXE": {
|
||||
...achievementData["FORMULAS.EXE"],
|
||||
Icon: "formulas",
|
||||
Condition: () => Player.getHomeComputer().programs.includes(Programs.Formulas.name),
|
||||
Condition: () => Player.getHomeComputer().programs.includes(CompletedProgramName.formulas),
|
||||
},
|
||||
"SF1.1": {
|
||||
...achievementData["SF1.1"],
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Class definition for a single Augmentation object
|
||||
import * as React from "react";
|
||||
|
||||
import type { CompletedProgramName } from "src/Programs/Programs";
|
||||
import { Faction } from "../Faction/Faction";
|
||||
import { Factions } from "../Faction/Factions";
|
||||
import { formatPercent } from "../ui/formatNumber";
|
||||
@ -64,7 +65,7 @@ export interface IConstructorParams {
|
||||
bladeburner_success_chance?: number;
|
||||
|
||||
startingMoney?: number;
|
||||
programs?: string[];
|
||||
programs?: CompletedProgramName[];
|
||||
}
|
||||
|
||||
function generateStatsDescription(mults: Multipliers, programs?: string[], startingMoney?: number): JSX.Element {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Augmentation, IConstructorParams } from "../Augmentation";
|
||||
import { AugmentationNames } from "./AugmentationNames";
|
||||
import { Player } from "@player";
|
||||
import { Programs } from "../../Programs/Programs";
|
||||
import { CompletedProgramName } from "../../Programs/Programs";
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
import React from "react";
|
||||
import { FactionNames } from "../../Faction/data/FactionNames";
|
||||
@ -1442,7 +1442,7 @@ export const initGeneralAugmentations = (): Augmentation[] => [
|
||||
hacking_exp: 1.2,
|
||||
hacking_chance: 1.1,
|
||||
hacking_speed: 1.05,
|
||||
programs: [Programs.FTPCrackProgram.name, Programs.RelaySMTPProgram.name],
|
||||
programs: [CompletedProgramName.ftpCrack, CompletedProgramName.relaySmtp],
|
||||
factions: [FactionNames.BitRunners],
|
||||
}),
|
||||
new Augmentation({
|
||||
@ -1495,7 +1495,7 @@ export const initGeneralAugmentations = (): Augmentation[] => [
|
||||
</>
|
||||
),
|
||||
startingMoney: 1e6,
|
||||
programs: [Programs.BruteSSHProgram.name],
|
||||
programs: [CompletedProgramName.bruteSsh],
|
||||
factions: [FactionNames.Sector12],
|
||||
}),
|
||||
new Augmentation({
|
||||
@ -1528,7 +1528,7 @@ export const initGeneralAugmentations = (): Augmentation[] => [
|
||||
company_rep: 1.0777,
|
||||
crime_success: 1.0777,
|
||||
crime_money: 1.0777,
|
||||
programs: [Programs.DeepscanV1.name, Programs.AutoLink.name],
|
||||
programs: [CompletedProgramName.deepScan1, CompletedProgramName.autoLink],
|
||||
factions: [FactionNames.Aevum],
|
||||
}),
|
||||
new Augmentation({
|
||||
@ -2104,16 +2104,16 @@ export const initChurchOfTheMachineGodAugmentations = (): Augmentation[] => [
|
||||
|
||||
startingMoney: 1e12,
|
||||
programs: [
|
||||
Programs.BruteSSHProgram.name,
|
||||
Programs.FTPCrackProgram.name,
|
||||
Programs.RelaySMTPProgram.name,
|
||||
Programs.HTTPWormProgram.name,
|
||||
Programs.SQLInjectProgram.name,
|
||||
Programs.DeepscanV1.name,
|
||||
Programs.DeepscanV2.name,
|
||||
Programs.ServerProfiler.name,
|
||||
Programs.AutoLink.name,
|
||||
Programs.Formulas.name,
|
||||
CompletedProgramName.bruteSsh,
|
||||
CompletedProgramName.ftpCrack,
|
||||
CompletedProgramName.relaySmtp,
|
||||
CompletedProgramName.httpWorm,
|
||||
CompletedProgramName.sqlInject,
|
||||
CompletedProgramName.deepScan1,
|
||||
CompletedProgramName.deepScan2,
|
||||
CompletedProgramName.serverProfiler,
|
||||
CompletedProgramName.autoLink,
|
||||
CompletedProgramName.formulas,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
@ -12,6 +12,7 @@ import { Server } from "./Server/Server";
|
||||
import { BaseServer } from "./Server/BaseServer";
|
||||
|
||||
import { getRandomInt } from "./utils/helpers/getRandomInt";
|
||||
import { ContractFilePath, resolveContractFilePath } from "./Paths/ContractFilePath";
|
||||
|
||||
export function generateRandomContract(): void {
|
||||
// First select a random problem type
|
||||
@ -57,7 +58,7 @@ export const generateDummyContract = (problemType: string): void => {
|
||||
interface IGenerateContractParams {
|
||||
problemType?: string;
|
||||
server?: string;
|
||||
fn?: string;
|
||||
fn?: ContractFilePath;
|
||||
}
|
||||
|
||||
export function generateContract(params: IGenerateContractParams): void {
|
||||
@ -84,15 +85,9 @@ export function generateContract(params: IGenerateContractParams): void {
|
||||
server = getRandomServer();
|
||||
}
|
||||
|
||||
// Filename
|
||||
let fn;
|
||||
if (params.fn != null) {
|
||||
fn = params.fn;
|
||||
} else {
|
||||
fn = getRandomFilename(server, reward);
|
||||
}
|
||||
const filename = params.fn ? params.fn : getRandomFilename(server, reward);
|
||||
|
||||
const contract = new CodingContract(fn, problemType, reward);
|
||||
const contract = new CodingContract(filename, problemType, reward);
|
||||
server.addContract(contract);
|
||||
}
|
||||
|
||||
@ -185,7 +180,10 @@ function getRandomServer(): BaseServer {
|
||||
return randServer;
|
||||
}
|
||||
|
||||
function getRandomFilename(server: BaseServer, reward: ICodingContractReward = { name: "", type: 0 }): string {
|
||||
function getRandomFilename(
|
||||
server: BaseServer,
|
||||
reward: ICodingContractReward = { name: "", type: 0 },
|
||||
): ContractFilePath {
|
||||
let contractFn = `contract-${getRandomInt(0, 1e6)}`;
|
||||
|
||||
for (let i = 0; i < 1000; ++i) {
|
||||
@ -203,6 +201,8 @@ function getRandomFilename(server: BaseServer, reward: ICodingContractReward = {
|
||||
// Only alphanumeric characters in the reward name.
|
||||
contractFn += `-${reward.name.replace(/[^a-zA-Z0-9]/g, "")}`;
|
||||
}
|
||||
|
||||
return contractFn;
|
||||
contractFn += ".cct";
|
||||
const validatedPath = resolveContractFilePath(contractFn);
|
||||
if (!validatedPath) throw new Error(`Generated contract path could not be validated: ${contractFn}`);
|
||||
return validatedPath;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { codingContractTypesMetadata, DescriptionFunc, GeneratorFunc, SolverFunc
|
||||
|
||||
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "./utils/JSONReviver";
|
||||
import { CodingContractEvent } from "./ui/React/CodingContractModal";
|
||||
import { ContractFilePath, resolveContractFilePath } from "./Paths/ContractFilePath";
|
||||
|
||||
/* tslint:disable:no-magic-numbers completed-docs max-classes-per-file no-console */
|
||||
|
||||
@ -89,7 +90,7 @@ export class CodingContract {
|
||||
data: unknown;
|
||||
|
||||
/* Contract's filename */
|
||||
fn: string;
|
||||
fn: ContractFilePath;
|
||||
|
||||
/* Describes the reward given if this Contract is solved. The reward is actually
|
||||
processed outside of this file */
|
||||
@ -101,17 +102,14 @@ export class CodingContract {
|
||||
/* String representing the contract's type. Must match type in ContractTypes */
|
||||
type: string;
|
||||
|
||||
constructor(fn = "", type = "Find Largest Prime Factor", reward: ICodingContractReward | null = null) {
|
||||
this.fn = fn;
|
||||
if (!this.fn.endsWith(".cct")) {
|
||||
this.fn += ".cct";
|
||||
}
|
||||
|
||||
// tslint:disable-next-line
|
||||
if (CodingContractTypes[type] == null) {
|
||||
constructor(fn = "default.cct", type = "Find Largest Prime Factor", reward: ICodingContractReward | null = null) {
|
||||
const path = resolveContractFilePath(fn);
|
||||
if (!path) throw new Error(`Bad file path while creating a coding contract: ${fn}`);
|
||||
if (!CodingContractTypes[type]) {
|
||||
throw new Error(`Error: invalid contract type: ${type} please contact developer`);
|
||||
}
|
||||
|
||||
this.fn = path;
|
||||
this.type = type;
|
||||
this.data = CodingContractTypes[type].generate();
|
||||
this.reward = reward;
|
||||
|
@ -6,12 +6,11 @@ import { Industry } from "./Industry";
|
||||
|
||||
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
|
||||
import { showLiterature } from "../Literature/LiteratureHelpers";
|
||||
import { LiteratureNames } from "../Literature/data/LiteratureNames";
|
||||
import { LiteratureName } from "../Literature/data/LiteratureNames";
|
||||
import { Player } from "@player";
|
||||
|
||||
import { dialogBoxCreate } from "../ui/React/DialogBox";
|
||||
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
|
||||
import { isString } from "../utils/helpers/isString";
|
||||
import { CityName } from "../Enums";
|
||||
import { CorpStateName } from "@nsdefs";
|
||||
import { calculateUpgradeCost } from "./helpers";
|
||||
@ -441,19 +440,9 @@ export class Corporation {
|
||||
getStarterGuide(): void {
|
||||
// Check if player already has Corporation Handbook
|
||||
const homeComp = Player.getHomeComputer();
|
||||
let hasHandbook = false;
|
||||
const handbookFn = LiteratureNames.CorporationManagementHandbook;
|
||||
for (let i = 0; i < homeComp.messages.length; ++i) {
|
||||
if (isString(homeComp.messages[i]) && homeComp.messages[i] === handbookFn) {
|
||||
hasHandbook = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasHandbook) {
|
||||
homeComp.messages.push(handbookFn);
|
||||
}
|
||||
showLiterature(handbookFn);
|
||||
const handbook = LiteratureName.CorporationManagementHandbook;
|
||||
if (!homeComp.messages.includes(handbook)) homeComp.messages.push(handbook);
|
||||
showLiterature(handbook);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ export function checkIfConnectedToDarkweb(): void {
|
||||
}
|
||||
|
||||
export function listAllDarkwebItems(): void {
|
||||
for (const key of Object.keys(DarkWebItems)) {
|
||||
for (const key of Object.keys(DarkWebItems) as (keyof typeof DarkWebItems)[]) {
|
||||
const item = DarkWebItems[key];
|
||||
|
||||
const cost = Player.getHomeComputer().programs.includes(item.program) ? (
|
||||
@ -45,7 +45,7 @@ export function buyDarkwebItem(itemName: string): void {
|
||||
// find the program that matches, if any
|
||||
let item: DarkWebItem | null = null;
|
||||
|
||||
for (const key of Object.keys(DarkWebItems)) {
|
||||
for (const key of Object.keys(DarkWebItems) as (keyof typeof DarkWebItems)[]) {
|
||||
const i = DarkWebItems[key];
|
||||
if (i.program.toLowerCase() == itemName) {
|
||||
item = i;
|
||||
@ -88,7 +88,7 @@ export function buyAllDarkwebItems(): void {
|
||||
const itemsToBuy: DarkWebItem[] = [];
|
||||
let cost = 0;
|
||||
|
||||
for (const key of Object.keys(DarkWebItems)) {
|
||||
for (const key of Object.keys(DarkWebItems) as (keyof typeof DarkWebItems)[]) {
|
||||
const item = DarkWebItems[key];
|
||||
if (!Player.hasProgram(item.program)) {
|
||||
itemsToBuy.push(item);
|
||||
|
@ -1,9 +1,11 @@
|
||||
import type { CompletedProgramName } from "../Programs/Programs";
|
||||
|
||||
export class DarkWebItem {
|
||||
program: string;
|
||||
program: CompletedProgramName;
|
||||
price: number;
|
||||
description: string;
|
||||
|
||||
constructor(program: string, price: number, description: string) {
|
||||
constructor(program: CompletedProgramName, price: number, description: string) {
|
||||
this.program = program;
|
||||
this.price = price;
|
||||
this.description = description;
|
||||
|
@ -1,23 +1,15 @@
|
||||
import { DarkWebItem } from "./DarkWebItem";
|
||||
import { Programs, initPrograms } from "../Programs/Programs";
|
||||
import { CompletedProgramName } from "../Programs/Programs";
|
||||
|
||||
export const DarkWebItems: Record<string, DarkWebItem> = {};
|
||||
export function initDarkWebItems() {
|
||||
initPrograms();
|
||||
Object.assign(DarkWebItems, {
|
||||
BruteSSHProgram: new DarkWebItem(Programs.BruteSSHProgram.name, 500e3, "Opens up SSH Ports."),
|
||||
FTPCrackProgram: new DarkWebItem(Programs.FTPCrackProgram.name, 1500e3, "Opens up FTP Ports."),
|
||||
RelaySMTPProgram: new DarkWebItem(Programs.RelaySMTPProgram.name, 5e6, "Opens up SMTP Ports."),
|
||||
HTTPWormProgram: new DarkWebItem(Programs.HTTPWormProgram.name, 30e6, "Opens up HTTP Ports."),
|
||||
SQLInjectProgram: new DarkWebItem(Programs.SQLInjectProgram.name, 250e6, "Opens up SQL Ports."),
|
||||
ServerProfiler: new DarkWebItem(
|
||||
Programs.ServerProfiler.name,
|
||||
500000,
|
||||
"Displays detailed information about a server.",
|
||||
),
|
||||
DeepscanV1: new DarkWebItem(Programs.DeepscanV1.name, 500000, "Enables 'scan-analyze' with a depth up to 5."),
|
||||
DeepscanV2: new DarkWebItem(Programs.DeepscanV2.name, 25e6, "Enables 'scan-analyze' with a depth up to 10."),
|
||||
AutolinkProgram: new DarkWebItem(Programs.AutoLink.name, 1e6, "Enables direct connect via 'scan-analyze'."),
|
||||
FormulasProgram: new DarkWebItem(Programs.Formulas.name, 5e9, "Unlock access to the formulas API."),
|
||||
});
|
||||
}
|
||||
export const DarkWebItems = {
|
||||
BruteSSHProgram: new DarkWebItem(CompletedProgramName.bruteSsh, 500e3, "Opens up SSH Ports."),
|
||||
FTPCrackProgram: new DarkWebItem(CompletedProgramName.ftpCrack, 1500e3, "Opens up FTP Ports."),
|
||||
RelaySMTPProgram: new DarkWebItem(CompletedProgramName.relaySmtp, 5e6, "Opens up SMTP Ports."),
|
||||
HTTPWormProgram: new DarkWebItem(CompletedProgramName.httpWorm, 30e6, "Opens up HTTP Ports."),
|
||||
SQLInjectProgram: new DarkWebItem(CompletedProgramName.sqlInject, 250e6, "Opens up SQL Ports."),
|
||||
ServerProfiler: new DarkWebItem(CompletedProgramName.serverProfiler, 500e3, "Displays detailed server information."),
|
||||
DeepscanV1: new DarkWebItem(CompletedProgramName.deepScan1, 500000, "Enables 'scan-analyze' with a depth up to 5."),
|
||||
DeepscanV2: new DarkWebItem(CompletedProgramName.deepScan2, 25e6, "Enables 'scan-analyze' with a depth up to 10."),
|
||||
AutolinkProgram: new DarkWebItem(CompletedProgramName.autoLink, 1e6, "Enables direct connect via 'scan-analyze'."),
|
||||
FormulasProgram: new DarkWebItem(CompletedProgramName.formulas, 5e9, "Unlock access to the formulas API."),
|
||||
};
|
||||
|
@ -9,25 +9,21 @@ import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import Select, { SelectChangeEvent } from "@mui/material/Select";
|
||||
import { Player } from "@player";
|
||||
import { Programs as AllPrograms } from "../../Programs/Programs";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { CompletedProgramName } from "../../Programs/Programs";
|
||||
|
||||
export function Programs(): React.ReactElement {
|
||||
const [program, setProgram] = useState("NUKE.exe");
|
||||
const [program, setProgram] = useState(CompletedProgramName.bruteSsh);
|
||||
function setProgramDropdown(event: SelectChangeEvent<string>): void {
|
||||
setProgram(event.target.value);
|
||||
setProgram(event.target.value as CompletedProgramName);
|
||||
}
|
||||
function addProgram(): void {
|
||||
if (!Player.hasProgram(program)) {
|
||||
Player.getHomeComputer().programs.push(program);
|
||||
}
|
||||
if (!Player.hasProgram(program)) Player.getHomeComputer().programs.push(program);
|
||||
}
|
||||
|
||||
function addAllPrograms(): void {
|
||||
for (const i of Object.keys(AllPrograms)) {
|
||||
if (!Player.hasProgram(AllPrograms[i].name)) {
|
||||
Player.getHomeComputer().programs.push(AllPrograms[i].name);
|
||||
}
|
||||
for (const name of Object.values(CompletedProgramName)) {
|
||||
if (!Player.hasProgram(name)) Player.getHomeComputer().programs.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,9 +41,9 @@ export function Programs(): React.ReactElement {
|
||||
</td>
|
||||
<td>
|
||||
<Select onChange={setProgramDropdown} value={program}>
|
||||
{Object.values(AllPrograms).map((program) => (
|
||||
<MenuItem key={program.name} value={program.name}>
|
||||
{program.name}
|
||||
{Object.values(CompletedProgramName).map((name) => (
|
||||
<MenuItem key={name} value={name}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
@ -16,41 +16,24 @@ import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import { ServerName } from "../Types/strings";
|
||||
import { allContentFiles } from "../Paths/ContentFile";
|
||||
|
||||
interface IServerProps {
|
||||
hostname: ServerName;
|
||||
interface File {
|
||||
name: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
function ServerAccordion(props: IServerProps): React.ReactElement {
|
||||
function ServerAccordion(props: { hostname: ServerName }): React.ReactElement {
|
||||
const server = GetServer(props.hostname);
|
||||
if (server === null) throw new Error(`server '${props.hostname}' should not be null`);
|
||||
let totalSize = 0;
|
||||
for (const f of server.scripts.values()) {
|
||||
totalSize += f.code.length;
|
||||
}
|
||||
|
||||
for (const f of server.textFiles) {
|
||||
totalSize += f.text.length;
|
||||
}
|
||||
|
||||
if (totalSize === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
interface File {
|
||||
name: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
const files: File[] = [];
|
||||
|
||||
for (const f of server.scripts.values()) {
|
||||
files.push({ name: f.filename, size: f.code.length });
|
||||
for (const [path, file] of allContentFiles(server)) {
|
||||
totalSize += file.content.length;
|
||||
files.push({ name: path, size: file.content.length });
|
||||
}
|
||||
|
||||
for (const f of server.textFiles) {
|
||||
files.push({ name: f.fn, size: f.text.length });
|
||||
}
|
||||
if (totalSize === 0) return <></>;
|
||||
|
||||
files.sort((a: File, b: File): number => b.size - a.size);
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Player } from "@player";
|
||||
import { Router } from "./ui/GameRoot";
|
||||
import { removeLeadingSlash } from "./Terminal/DirectoryHelpers";
|
||||
import { Terminal } from "./Terminal";
|
||||
import { SnackbarEvents, ToastVariant } from "./ui/React/Snackbar";
|
||||
import { IReturnStatus } from "./types";
|
||||
@ -11,6 +10,8 @@ import { exportScripts } from "./Terminal/commands/download";
|
||||
import { CONSTANTS } from "./Constants";
|
||||
import { hash } from "./hash/hash";
|
||||
import { Buffer } from "buffer";
|
||||
import { resolveFilePath } from "./Paths/FilePath";
|
||||
import { hasScriptExtension } from "./Paths/ScriptFilePath";
|
||||
|
||||
interface IReturnWebStatus extends IReturnStatus {
|
||||
data?: Record<string, unknown>;
|
||||
@ -55,23 +56,9 @@ export function initElectron(): void {
|
||||
}
|
||||
|
||||
function initWebserver(): void {
|
||||
function normalizeFileName(filename: string): string {
|
||||
filename = filename.replace(/\/\/+/g, "/");
|
||||
filename = removeLeadingSlash(filename);
|
||||
if (filename.includes("/")) {
|
||||
filename = "/" + removeLeadingSlash(filename);
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
document.getFiles = function (): IReturnWebStatus {
|
||||
const home = GetServer("home");
|
||||
if (home === null) {
|
||||
return {
|
||||
res: false,
|
||||
msg: "Home server does not exist.",
|
||||
};
|
||||
}
|
||||
if (home === null) return { res: false, msg: "Home server does not exist." };
|
||||
return {
|
||||
res: true,
|
||||
data: {
|
||||
@ -85,40 +72,28 @@ function initWebserver(): void {
|
||||
};
|
||||
|
||||
document.deleteFile = function (filename: string): IReturnWebStatus {
|
||||
filename = normalizeFileName(filename);
|
||||
const path = resolveFilePath(filename);
|
||||
if (!path) return { res: false, msg: "Invalid file path." };
|
||||
const home = GetServer("home");
|
||||
if (home === null) {
|
||||
return {
|
||||
res: false,
|
||||
msg: "Home server does not exist.",
|
||||
};
|
||||
}
|
||||
return home.removeFile(filename);
|
||||
if (!home) return { res: false, msg: "Home server does not exist." };
|
||||
return home.removeFile(path);
|
||||
};
|
||||
|
||||
document.saveFile = function (filename: string, code: string): IReturnWebStatus {
|
||||
filename = normalizeFileName(filename);
|
||||
const path = resolveFilePath(filename);
|
||||
if (!path) return { res: false, msg: "Invalid file path." };
|
||||
if (!hasScriptExtension(path)) return { res: false, msg: "Invalid file extension: must be a script" };
|
||||
|
||||
code = Buffer.from(code, "base64").toString();
|
||||
const home = GetServer("home");
|
||||
if (home === null) {
|
||||
return {
|
||||
res: false,
|
||||
msg: "Home server does not exist.",
|
||||
};
|
||||
}
|
||||
const { success, overwritten } = home.writeToScriptFile(filename, code);
|
||||
let script;
|
||||
if (success) {
|
||||
script = home.getScript(filename);
|
||||
}
|
||||
return {
|
||||
res: success,
|
||||
data: {
|
||||
overwritten,
|
||||
ramUsage: script?.ramUsage,
|
||||
},
|
||||
};
|
||||
if (!home) return { res: false, msg: "Home server does not exist." };
|
||||
|
||||
const { overwritten } = home.writeToScriptFile(path, code);
|
||||
const script = home.scripts.get(path);
|
||||
if (!script) return { res: false, msg: "Somehow failed to get script after writing it. This is a bug." };
|
||||
|
||||
const ramUsage = script.getRamUsage(home.scripts);
|
||||
return { res: true, data: { overwritten, ramUsage } };
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { Player } from "@player";
|
||||
|
||||
import { LiteratureNames } from "./Literature/data/LiteratureNames";
|
||||
|
||||
import { LiteratureName } from "./Literature/data/LiteratureNames";
|
||||
import { ITutorialEvents } from "./ui/InteractiveTutorial/ITutorialEvents";
|
||||
|
||||
// Ordered array of keys to Interactive Tutorial Steps
|
||||
@ -104,7 +102,7 @@ function iTutorialEnd(): void {
|
||||
ITutorial.isRunning = false;
|
||||
ITutorial.currStep = iTutorialSteps.Start;
|
||||
const messages = Player.getHomeComputer().messages;
|
||||
const handbook = LiteratureNames.HackersStartingHandbook;
|
||||
const handbook = LiteratureName.HackersStartingHandbook;
|
||||
if (!messages.includes(handbook)) messages.push(handbook);
|
||||
ITutorialEvents.emit();
|
||||
}
|
||||
|
@ -1,15 +1,19 @@
|
||||
import { FilePath, asFilePath } from "../Paths/FilePath";
|
||||
import type { LiteratureName } from "./data/LiteratureNames";
|
||||
|
||||
type LiteratureConstructorParams = { title: string; filename: LiteratureName; text: string };
|
||||
/**
|
||||
* Lore / world building literature files that can be found on servers.
|
||||
* These files can be read by the player
|
||||
*/
|
||||
export class Literature {
|
||||
title: string;
|
||||
fn: string;
|
||||
txt: string;
|
||||
filename: LiteratureName & FilePath;
|
||||
text: string;
|
||||
|
||||
constructor(title: string, filename: string, txt: string) {
|
||||
constructor({ title, filename, text }: LiteratureConstructorParams) {
|
||||
this.title = title;
|
||||
this.fn = filename;
|
||||
this.txt = txt;
|
||||
this.filename = asFilePath(filename);
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { Literatures } from "./Literatures";
|
||||
import { dialogBoxCreate } from "../ui/React/DialogBox";
|
||||
import { LiteratureName } from "./data/LiteratureNames";
|
||||
|
||||
export function showLiterature(fn: string): void {
|
||||
export function showLiterature(fn: LiteratureName): void {
|
||||
const litObj = Literatures[fn];
|
||||
if (litObj == null) {
|
||||
return;
|
||||
}
|
||||
const txt = `<i>${litObj.title}</i><br><br>${litObj.txt}`;
|
||||
const txt = `<i>${litObj.title}</i><br><br>${litObj.text}`;
|
||||
dialogBoxCreate(txt, true);
|
||||
}
|
||||
|
@ -1,441 +1,438 @@
|
||||
import { CityName } from "./../Enums";
|
||||
import { Literature } from "./Literature";
|
||||
import { LiteratureNames } from "./data/LiteratureNames";
|
||||
import { LiteratureName } from "./data/LiteratureNames";
|
||||
import { FactionNames } from "../Faction/data/FactionNames";
|
||||
|
||||
export const Literatures: Record<string, Literature> = {};
|
||||
|
||||
(function () {
|
||||
let title, fn, txt;
|
||||
title = "The Beginner's Guide to Hacking";
|
||||
fn = LiteratureNames.HackersStartingHandbook;
|
||||
txt =
|
||||
"Some resources:<br><br>" +
|
||||
"<a class='a-link-button' href='https://bitburner-official.readthedocs.io/en/latest/netscript/netscriptlearntoprogram.html' target='_blank' style='margin:4px'>Learn to Program</a><br><br>" +
|
||||
"<a class='a-link-button' href='https://bitburner-official.readthedocs.io/en/latest/netscript/netscriptjs.html' target='_blank' style='margin:4px'>For Experienced JavaScript Developers: NetscriptJS</a><br><br>" +
|
||||
"<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>" +
|
||||
"When starting out, hacking is the most profitable way to earn money and progress. This " +
|
||||
"is a brief collection of tips/pointers on how to make the most out of your hacking scripts.<br><br>" +
|
||||
"-hack() and grow() both work by percentages. hack() steals a certain percentage of the " +
|
||||
"money on a server, and grow() increases the amount of money on a server by some percentage (multiplicatively)<br><br>" +
|
||||
"-Because hack() and grow() work by percentages, they are more effective if the target server has a high amount of money. " +
|
||||
"Therefore, you should try to increase the amount of money on a server (using grow()) to a certain amount before hacking it. Two " +
|
||||
"important Netscript functions for this are getServerMoneyAvailable() and getServerMaxMoney()<br><br>" +
|
||||
"-Keep security level low. Security level affects everything when hacking. Two important Netscript functions " +
|
||||
"for this are getServerSecurityLevel() and getServerMinSecurityLevel()<br><br>" +
|
||||
"-Purchase additional servers by visiting 'Alpha Enterprises' in the city. They are relatively cheap " +
|
||||
"and give you valuable RAM to run more scripts early in the game<br><br>" +
|
||||
"-Prioritize upgrading the RAM on your home computer. This can also be done at 'Alpha Enterprises'<br><br>" +
|
||||
"-Many low level servers have free RAM. You can use this RAM to run your scripts. Use the scp Terminal or " +
|
||||
"Netscript command to copy your scripts onto these servers and then run them.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "The Complete Handbook for Creating a Successful Corporation";
|
||||
fn = LiteratureNames.CorporationManagementHandbook;
|
||||
txt =
|
||||
"<u>Getting Started with Corporations</u><br>" +
|
||||
"To get started, visit the City Hall in Sector-12 in order to create a Corporation. This requires $150b of your own money, " +
|
||||
"but this $150b will get put into your Corporation's funds. If you're in BitNode 3 you also have option to get seed money from " +
|
||||
"the government in exchange for 500m shares. Your Corporation can have many different divisions, each in a different Industry. " +
|
||||
"There are many different types of Industries, each with different properties. To create your first division, click the 'Expand' " +
|
||||
"(into new Industry) button at the top of the management UI. The Agriculture industry is recommended for your first division.<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 " +
|
||||
"different effect on various aspects of your Corporation. It is recommended to have at least one employee at each position.<br><br>" +
|
||||
"Each industry uses some combination of Materials in order to produce other Materials and/or create Products. Specific information " +
|
||||
"about this is displayed in each of your divisions' UI.<br><br>" +
|
||||
"Products are special, industry-specific objects. They are different than Materials because you must manually choose to develop them, " +
|
||||
"and you can choose to develop any number of Products. Developing a Product takes time, but a Product typically generates significantly " +
|
||||
"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 " +
|
||||
"panel of the division UI (e.g. For the Software Industry, the button says 'Develop Software').<br><br>" +
|
||||
"To get your supply chain system started, purchase the Materials that your industry needs to produce other Materials/Products. This can be " +
|
||||
"done by clicking the 'Buy' button next to the corresponding Material(s). After you have the required Materials, you will immediately start " +
|
||||
"production. The amount and quality/effective rating of Materials/Products you produce is based on a variety of factors, such as your employees " +
|
||||
"and their productivity and the quality of materials used for production.<br><br>" +
|
||||
"Once you start producing Materials/Products, you can sell them in order to start earning revenue. This can be done by clicking the 'Sell' " +
|
||||
"button next to the corresponding Material or Product. The amount of Material/Product you sell is dependent on a wide variety of different factors. " +
|
||||
"In order to produce and sell a Product you'll have to fully develop it first.<br><br>" +
|
||||
"These are the basics of getting your Corporation up and running! Now, you can start purchasing upgrades to improve your bottom line. " +
|
||||
"If you need money, consider looking for seed investors, who will give you money in exchange for stock shares. Otherwise, once you feel " +
|
||||
"you are ready, take your Corporation public! Once your Corporation goes public, you can no longer find investors. Instead, your Corporation " +
|
||||
"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 " +
|
||||
"can set dividends for a solid reliable income or you can sell your stock shares in order to make quick money.<br><br>" +
|
||||
"<u>Tips/Pointers</u><br>" +
|
||||
"-Start with one division, such as Agriculture. Get it profitable on it's own, then expand to a division that consumes/produces " +
|
||||
"a material that the division you selected produces/consumes.<br><br>" +
|
||||
"-Materials are profitable, but Products are where the real money is, although if the product had a low development budget or is " +
|
||||
"produced with low quality materials it won't sell well.<br><br>" +
|
||||
"-The 'Smart Supply' upgrade is extremely useful. Consider purchasing it as soon as possible.<br><br>" +
|
||||
"-Purchasing Hardware, Robots, AI Cores, and Real Estate can potentially increase your production. The effects of these depend on " +
|
||||
"what industry you are in.<br><br>" +
|
||||
"-In order to optimize your production, you will need a good balance of all employee positions, about 1/9 should be interning<br><br>" +
|
||||
"-Quality of materials used for production affects the quality/effective rating of materials/products produced, so vertical integration " +
|
||||
"is important for high profits.<br><br>" +
|
||||
"-Materials purchased from the open market are always of quality 1.<br><br>" +
|
||||
"-The price at which you can sell your Materials/Products is highly affected by the quality/effective rating<br><br>" +
|
||||
"-When developing a product, different employee positions affect the development process differently, " +
|
||||
"some improve the development speed, some improve the rating of the finished product.<br><br>" +
|
||||
"-If your employees have low morale or energy, their production will greatly suffer. Having enough interns will make sure those stats get " +
|
||||
"high and stay high.<br><br>" +
|
||||
"-Don't forget to advertise your company. You won't have any business if nobody knows you.<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 " +
|
||||
"possible to see the biggest benefit for your sales<br><br>" +
|
||||
"-Remember, you need to spend money to make money!<br><br>" +
|
||||
"-Corporations do not reset when installing Augmentations, but they do reset when destroying a BitNode";
|
||||
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "A Brief History of Synthoids";
|
||||
fn = LiteratureNames.HistoryOfSynthoids;
|
||||
txt =
|
||||
"Synthetic androids, or Synthoids for short, are genetically engineered robots and, short of Augmentations, " +
|
||||
"are composed entirely of organic substances. For this reason, Synthoids are virtually identical to " +
|
||||
"humans in form, composition, and appearance.<br><br>" +
|
||||
`Synthoids were first designed and manufactured by ${FactionNames.OmniTekIncorporated} sometime around the middle of the century. ` +
|
||||
"Their original purpose was to be used for manual labor and as emergency responders for disasters. As such, they " +
|
||||
"were initially programmed only for their specific tasks. Each iteration that followed improved upon the " +
|
||||
"intelligence and capabilities of the Synthoids. By the 6th iteration, called MK-VI, the Synthoids were " +
|
||||
`so smart and capable enough of making their own decisions that many argued ${FactionNames.OmniTekIncorporated} had created the first ` +
|
||||
"sentient AI. These MK-VI Synthoids were produced in mass quantities (estimates up to 50 billion) with the hopes of increasing society's " +
|
||||
"productivity and bolstering the global economy. Stemming from humanity's desire for technological advancement, optimism " +
|
||||
"and excitement about the future had never been higher.<br><br>" +
|
||||
"All of that excitement and optimism quickly turned to fear, panic, and dread in 2070, when a terrorist group " +
|
||||
`called Ascendis Totalis hacked into ${FactionNames.OmniTekIncorporated} and uploaded a rogue AI into several of their Synthoid manufacturing facilities. ` +
|
||||
`This hack went undetected and for months ${FactionNames.OmniTekIncorporated} unknowingly churned out legions of Synthoids embedded with this ` +
|
||||
"rogue AI. Then, on December 24th, 2070, Omnica activated dormant protocols in the rogue AI, causing all of the " +
|
||||
"infected Synthoids to immediately launch a military campaign to seek and destroy all of humanity.<br><br>" +
|
||||
"What ensued was the deadliest conflict in human history. This crisis, now commonly known as the Synthoid Uprising, " +
|
||||
"resulted in almost ten billion deaths over the course of a year. Despite the nations of the world banding together " +
|
||||
"to combat the threat, the MK-VI Synthoids were simply stronger, faster, more intelligent, and more adaptable than humans, " +
|
||||
"outsmarting them at every turn.<br><br>" +
|
||||
`It wasn't until the sacrifice of an elite international military taskforce, called the ${FactionNames.Bladeburners}, that humanity ` +
|
||||
`was finally able to defeat the Synthoids. The ${FactionNames.Bladeburners}' final act was a suicide bombing mission that ` +
|
||||
"destroyed a large portion of the MK-VI Synthoids, including many of its leaders. In the following " +
|
||||
"weeks militaries from around the world were able to round up and shut down the remaining rogue MK-VI Synthoids, ending " +
|
||||
"the Synthoid Uprising.<br><br>" +
|
||||
`In the aftermath of the bloodshed, the Synthoid Accords were drawn up. These Accords banned ${FactionNames.OmniTekIncorporated} ` +
|
||||
"from manufacturing any Synthoids beyond the MK-III series. They also banned any other corporation " +
|
||||
"from constructing androids with advanced, near-sentient AI. MK-VI Synthoids that did not have the rogue Ascendis Totalis " +
|
||||
"AI were allowed to continue their existence, but they were stripped of all rights and protections as they " +
|
||||
"were not considered humans. They were also banned from doing anything that may pose a global security threat, such " +
|
||||
"as working for any military/defense organization or conducting any bioengineering, computing, or robotics related research.<br><br>" +
|
||||
"Unfortunately, many believe that not all of the rogue MK-VI Synthoids from the Uprising were found and destroyed, " +
|
||||
"and that many of them are blending in as normal humans in society today. In response, many nations have created " +
|
||||
`${FactionNames.Bladeburners} divisions, special military branches that are tasked with investigating and dealing with any Synthoid threats.<br><br>` +
|
||||
"To this day, tensions still exist between the remaining Synthoids and humans as a result of the Uprising.<br><br>" +
|
||||
"Nobody knows what happened to the terrorist group Ascendis Totalis.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "A Green Tomorrow";
|
||||
fn = LiteratureNames.AGreenTomorrow;
|
||||
txt =
|
||||
"Starting a few decades ago, there was a massive global movement towards the generation of renewable energy in an effort to " +
|
||||
"combat global warming and climate change. The shift towards renewable energy was a big success, or so it seemed. In 2045 " +
|
||||
"a staggering 80% of the world's energy came from non-renewable fossil fuels. Now, about three decades later, that " +
|
||||
"number is down to only 15%. Most of the world's energy now comes from nuclear power and renewable sources such as " +
|
||||
"solar and geothermal energy. Unfortunately, these efforts were not the huge success that they seem to be.<br><br>" +
|
||||
"Since 2045 primary energy use has soared almost tenfold. This was mainly due to growing urban populations and " +
|
||||
"the rise of increasingly advanced (and power-hungry) technology that has become ubiquitous in our lives. So, " +
|
||||
"despite the fact that the percentage of our energy that comes from fossil fuels has drastically decreased, " +
|
||||
"the total amount of energy we are producing from fossil fuels has actually increased.<br><br>" +
|
||||
"The grim effects of our species' irresponsible use of energy and neglect of our mother world have become increasingly apparent. " +
|
||||
"Last year a temperature of 190F was recorded in the Death Valley desert, which is over 50% higher than the highest " +
|
||||
"recorded temperature at the beginning of the century. In the last two decades numerous major cities such as Manhattan, Boston, and " +
|
||||
"Los Angeles have been partially or fully submerged by rising sea levels. In the present day, over 75% of the world's agriculture is " +
|
||||
"done in climate-controlled vertical farms, as most traditional farmland has become unusable due to severe climate conditions.<br><br>" +
|
||||
"Despite all of this, the greedy and corrupt corporations that rule the world have done nothing to address these problems that " +
|
||||
"threaten our species. And so it's up to us, the common people. Each and every one of us can make a difference by doing what " +
|
||||
"these corporations won't: taking responsibility. If we don't, pretty soon there won't be an Earth left to save. We are " +
|
||||
"the last hope for a green tomorrow.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "Alpha and Omega";
|
||||
fn = LiteratureNames.AlphaOmega;
|
||||
txt =
|
||||
"Then we saw a new Heaven and a new Earth, for our first Heaven and Earth had gone away, and our sea was no more. " +
|
||||
"And we saw a new holy city, new Aeria, coming down out of this new Heaven, prepared as a bride adorned for her husband. " +
|
||||
"And we heard a loud voice saying, 'Behold, the new dwelling place of the Gods. We will dwell with them, and they " +
|
||||
"will be our people, and we will be with them as their Gods. We will wipe away every tear from their eyes, and death " +
|
||||
"shall be no more, neither shall there be mourning, nor crying, nor pain anymore, for the former things " +
|
||||
"have passed away.'<br><br>" +
|
||||
"And once we were seated on the throne we said 'Behold, I am making all things new.' " +
|
||||
"Also we said, 'Write this down, for these words are trustworthy and true.' And we said to you, " +
|
||||
"'It is done! I am the Alpha and the Omega, the beginning and the end. To the thirsty I will give from the spring " +
|
||||
"of the water of life without payment. The one who conquers will have this heritage, and we will be his God and " +
|
||||
"he will be our son. But as for the cowardly, the faithless, the detestable, as for murderers, " +
|
||||
"the sexually immoral, sorcerers, idolaters, and all liars, their portion will be in the lake that " +
|
||||
"burns with fire and sulfur, for it is the second true death.'";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "Are We Living in a Computer Simulation?";
|
||||
fn = LiteratureNames.SimulatedReality;
|
||||
txt =
|
||||
"The idea that we are living in a virtual world is not new. It's a trope that has " +
|
||||
"been explored constantly in literature and pop culture. However, it is also a legitimate " +
|
||||
"scientific hypothesis that many notable physicists and philosophers have debated for years.<br><br>" +
|
||||
"Proponents for this simulated reality theory often point to how advanced our technology has become, " +
|
||||
"as well as the incredibly fast pace at which it has advanced over the past decades. The amount of computing " +
|
||||
"power available to us has increased over 100-fold since 2060 due to the development of nanoprocessors and " +
|
||||
"quantum computers. Artificial Intelligence has advanced to the point where our entire lives are controlled " +
|
||||
"by robots and machines that handle our day-to-day activities such as autonomous transportation and scheduling. " +
|
||||
"If we consider the pace at which this technology has advanced and assume that these developments continue, it's " +
|
||||
"reasonable to assume that at some point in the future our technology would be advanced enough that " +
|
||||
"we could create simulations that are indistinguishable from reality. However, if continued technological advancement " +
|
||||
"is a reasonable outcome, then it is very likely that such a scenario has already happened. <br><br>" +
|
||||
"Statistically speaking, somewhere out there in the infinite universe there is an advanced, intelligent species " +
|
||||
"that already has such technology. Who's to say that they haven't already created such a virtual reality: our own?";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "Beyond Man";
|
||||
fn = LiteratureNames.BeyondMan;
|
||||
txt =
|
||||
"Humanity entered a 'transhuman' era a long time ago. And despite the protests and criticisms of many who cried out against " +
|
||||
"human augmentation at the time, the transhuman movement continued and prospered. Proponents of the movement ignored the critics, " +
|
||||
"arguing that it was in our inherent nature to better ourselves. To improve. To be more than we were. They claimed that " +
|
||||
"not doing so would be to go against every living organism's biological purpose: evolution and survival of the fittest.<br><br>" +
|
||||
"And here we are today, with technology that is advanced enough to augment humans to a state that " +
|
||||
"can only be described as posthuman. But what do we have to show for it when this augmentation " +
|
||||
"technology is only available to the so-called 'elite'? Are we really better off than before when only 5% of the " +
|
||||
"world's population has access to this technology? When the powerful corporations and organizations of the world " +
|
||||
"keep it all to themselves, have we really evolved?<br><br>" +
|
||||
"Augmentation technology has only further increased the divide between the rich and the poor, between the powerful and " +
|
||||
"the oppressed. We have not become 'more than human'. We have not evolved from nature's original design. We are still the greedy, " +
|
||||
"corrupted, and evil men that we always were.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "Brighter than the Sun";
|
||||
fn = LiteratureNames.BrighterThanTheSun;
|
||||
txt =
|
||||
`When people think about the corporations that dominate the East, they typically think of ${FactionNames.KuaiGongInternational}, which ` +
|
||||
"holds a complete monopoly for manufacturing and commerce in Asia, or Global Pharmaceuticals, the world's largest " +
|
||||
`drug company, or ${FactionNames.OmniTekIncorporated}, the global leader in intelligent and autonomous robots. But there's one company ` +
|
||||
"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>" +
|
||||
"TaiYang Digital is a Chinese internet-technology corporation that provides services such as " +
|
||||
"online advertising, search engines, gaming, media, entertainment, and cloud computing/storage. Its name TaiYang comes from the Chinese word " +
|
||||
"for 'sun'. In Chinese culture, the sun is a 'yang' symbol " +
|
||||
"associated with life, heat, masculinity, and heaven.<br><br>" +
|
||||
"The company was founded " +
|
||||
"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 " +
|
||||
"over 10 trillion yuan. Its services are used daily by over a billion people worldwide.<br><br>" +
|
||||
"TaiYang Digital's meteoric rise is extremely surprising in modern society. This sort of growth is " +
|
||||
"something you'd commonly see in the first half of the century, especially for tech companies. However in " +
|
||||
"the last two decades the number of corporations has significantly declined as the largest entities " +
|
||||
`quickly took over the economy. Corporations such as ${FactionNames.ECorp}, ${FactionNames.MegaCorp}, and ${FactionNames.KuaiGongInternational} have established ` +
|
||||
"such strong monopolies in their market sectors that they have effectively killed off all " +
|
||||
"of the smaller and new corporations that have tried to start up over the years. This is what makes " +
|
||||
"the rise of TaiYang Digital so impressive. And if TaiYang continues down this path, then they have " +
|
||||
"a bright future ahead of them.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "Democracy is Dead: The Fall of an Empire";
|
||||
fn = LiteratureNames.DemocracyIsDead;
|
||||
txt =
|
||||
"They rose from the shadows in the street.<br>From the places where the oppressed meet.<br>" +
|
||||
"Their cries echoed loudly through the air.<br>As they once did in Tiananmen Square.<br>" +
|
||||
"Loudness in the silence, Darkness in the light.<br>They came forth with power and might.<br>" +
|
||||
"Once the beacon of democracy, America was first.<br>Its pillars of society destroyed and dispersed.<br>" +
|
||||
"Soon the cries rose everywhere, with revolt and riot.<br>Until one day, finally, all was quiet.<br>" +
|
||||
"From the ashes rose a new order, corporatocracy was its name.<br>" +
|
||||
"Rome, Mongol, Byzantine, all of history is just the same.<br>" +
|
||||
"For man will never change in a fundamental way.<br>" +
|
||||
"And now democracy is dead, in the USA.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = `Figures Show Rising Crime Rates in ${CityName.Sector12}`;
|
||||
fn = LiteratureNames.Sector12Crime;
|
||||
txt =
|
||||
"A recent study by analytics company Wilson Inc. shows a significant rise " +
|
||||
`in criminal activity in ${CityName.Sector12}. Perhaps the most alarming part of the statistic ` +
|
||||
"is that most of the rise is in violent crime such as homicide and assault. According " +
|
||||
"to the study, the city saw a total of 21,406 reported homicides in 2076, which is over " +
|
||||
"a 20% increase compared to 2075.<br><br>" +
|
||||
"CIA director David Glarow says it's too early to know " +
|
||||
"whether these figures indicate the beginning of a sustained increase in crime rates, or whether " +
|
||||
"the year was just an unfortunate outlier. He states that many intelligence and law enforcement " +
|
||||
"agents have noticed an increase in organized crime activities, and believes that these figures may " +
|
||||
`be the result of an uprising from criminal organizations such as ${FactionNames.TheSyndicate} or the ${FactionNames.SlumSnakes}.`;
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "Man and the Machine";
|
||||
fn = LiteratureNames.ManAndMachine;
|
||||
txt =
|
||||
"In 2005 Ray Kurzweil popularized his theory of the Singularity. He predicted that the rate " +
|
||||
"of technological advancement would continue to accelerate faster and faster until one day " +
|
||||
"machines would be become infinitely more intelligent than humans. This point, called the " +
|
||||
"Singularity, would result in a drastic transformation of the world as we know it. He predicted " +
|
||||
"that the Singularity would arrive by 2045. " +
|
||||
"And yet here we are, more than three decades later, where most would agree that we have not " +
|
||||
"yet reached a point where computers and machines are vastly more intelligent than we are. So what gives?<br><br>" +
|
||||
"The answer is that we have reached the Singularity, just not in the way we expected. The artificial superintelligence " +
|
||||
"that was predicted by Kurzweil and others exists in the world today - in the form of Augmentations. " +
|
||||
"Yes, those Augmentations that the rich and powerful keep to themselves enable humans " +
|
||||
"to become superintelligent beings. The Singularity did not lead to a world where " +
|
||||
"our machines are infinitely more intelligent than us, it led to a world " +
|
||||
"where man and machine can merge to become something greater. Most of the world just doesn't " +
|
||||
"know it yet.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "Secret Societies";
|
||||
fn = LiteratureNames.SecretSocieties;
|
||||
txt =
|
||||
"The idea of secret societies has long intrigued the general public by inspiring curiosity, fascination, and " +
|
||||
"distrust. People have long wondered about who these secret society members are and what they do, with the " +
|
||||
"most radical of conspiracy theorists claiming that they control everything in the entire world. And while the world " +
|
||||
"may never know for sure, it is likely that many secret societies do actually exist, even today.<br><br>" +
|
||||
"However, the secret societies of the modern world are nothing like those that (supposedly) existed " +
|
||||
`decades and centuries ago. The Freemasons, Knights Templar, and ${FactionNames.Illuminati}, while they may have been around ` +
|
||||
"at the turn of the 21st century, almost assuredly do not exist today. The dominance of the Web in " +
|
||||
"our everyday lives and the fact that so much of the world is now digital has given rise to a new breed " +
|
||||
"of secret societies: Internet-based ones.<br><br>" +
|
||||
"Commonly called 'hacker groups', Internet-based secret societies have become well-known in today's " +
|
||||
`world. Some of these, such as ${FactionNames.TheBlackHand}, are black hat groups that claim they are trying to ` +
|
||||
`help the oppressed by attacking the elite and powerful. Others, such as ${FactionNames.NiteSec}, are hacktivist groups ` +
|
||||
"that try to push political and social agendas. Perhaps the most intriguing hacker group " +
|
||||
`is the mysterious ${FactionNames.BitRunners}, whose purpose still remains unknown.`;
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "Space: The Failed Frontier";
|
||||
fn = LiteratureNames.TheFailedFrontier;
|
||||
txt =
|
||||
"Humans have long dreamed about spaceflight. With enduring interest, we were driven to explore " +
|
||||
"the unknown and discover new worlds. We dreamed about conquering the stars. And in our quest, " +
|
||||
"we pushed the boundaries of our scientific limits, and then pushed further. Space exploration " +
|
||||
"lead to the development of many important technologies and new industries.<br><br>" +
|
||||
"But sometime in the middle of the 21st century, all of that changed. Humanity lost its ambitions and " +
|
||||
"aspirations of exploring the cosmos. The once-large funding for agencies like NASA and the European " +
|
||||
"Space Agency gradually whittled away until their eventual disbanding in the 2060's. Not even " +
|
||||
"militaries are fielding flights into space nowadays. The only remnants of the once great mission for cosmic " +
|
||||
"conquest are the countless satellites in near-earth orbit, used for communications, espionage, " +
|
||||
"and other corporate interests.<br><br>" +
|
||||
"And as we continue to look at the state of space technology, it becomes more and " +
|
||||
"more apparent that we will never return to that golden age of space exploration, that " +
|
||||
"age where everyone dreamed of going beyond earth for the sake of discovery.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "Coded Intelligence: Myth or Reality?";
|
||||
fn = LiteratureNames.CodedIntelligence;
|
||||
txt =
|
||||
"Tremendous progress has been made in the field of Artificial Intelligence over the past few decades. " +
|
||||
"Our autonomous vehicles and transportation systems. The electronic personal assistants that control our everyday lives. " +
|
||||
"Medical, service, and manufacturing robots. All of these are examples of how far AI has come and how much it has " +
|
||||
"improved our daily lives. However, the question still remains of whether AI will ever be advanced enough to re-create " +
|
||||
"human intelligence.<br><br>" +
|
||||
`We've certainly come close to artificial intelligence that is similar to humans. For example ${FactionNames.OmniTekIncorporated}'s ` +
|
||||
"CompanionBot, a robot meant to act as a comforting friend for lonely and grieving people, is eerily human-like " +
|
||||
"in its appearance, speech, mannerisms, and even movement. However its artificial intelligence isn't the same as " +
|
||||
"that of humans. Not yet. It doesn't have sentience or self-awareness or consciousness.<br><br>" +
|
||||
"Many neuroscientists believe that we won't ever reach the point of creating artificial human intelligence. 'At the end of " +
|
||||
"the day, AI comes down to 1's and 0's, while the human brain does not. We'll never see AI that is identical to that of " +
|
||||
"humans.'";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "Synthetic Muscles";
|
||||
fn = LiteratureNames.SyntheticMuscles;
|
||||
txt =
|
||||
"Initial versions of synthetic muscles weren't made of anything organic but were actually " +
|
||||
"crude devices made to mimic human muscle function. Some of the early iterations were actually made of " +
|
||||
"common materials such as fishing lines and sewing threads due to their high strength for " +
|
||||
"a cheap cost.<br><br>" +
|
||||
"As technology progressed, however, advances in biomedical engineering paved the way for a new method of " +
|
||||
"creating synthetic muscles. Instead of creating something that closely imitated the functionality " +
|
||||
"of human muscle, scientists discovered a way of forcing the human body itself to augment its own " +
|
||||
"muscle tissue using both synthetic and organic materials. This is typically done using gene therapy " +
|
||||
"or chemical injections.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "Tensions rise in global tech race";
|
||||
fn = LiteratureNames.TensionsInTechRace;
|
||||
txt =
|
||||
"Have we entered a new Cold War? Is WWIII just beyond the horizon?<br><br>" +
|
||||
`After rumors came out that ${FactionNames.OmniTekIncorporated} had begun developing advanced robotic supersoldiers, ` +
|
||||
"geopolitical tensions quickly flared between the USA, Russia, and several Asian superpowers. " +
|
||||
`In a rare show of cooperation between corporations, ${FactionNames.MegaCorp} and ${FactionNames.ECorp} have ` +
|
||||
"reportedly launched hundreds of new surveillance and espionage satellites. " +
|
||||
"Defense contractors such as " +
|
||||
"DeltaOne and AeroCorp have been working with the CIA and NSA to prepare " +
|
||||
"for conflict. Meanwhile, the rest of the world sits in earnest " +
|
||||
"hoping that it never reaches full-scale war. With today's technology " +
|
||||
"and firepower, a World War would assuredly mean the end of human civilization.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "The Cost of Immortality";
|
||||
fn = LiteratureNames.CostOfImmortality;
|
||||
txt =
|
||||
"Evolution and advances in medical and augmentation technology has lead to drastic improvements " +
|
||||
"in human mortality rates. Recent figures show that the life expectancy for humans " +
|
||||
"that live in a first-world country is about 130 years of age, almost double of what it was " +
|
||||
"at the turn of the century. However, this increase in average lifespan has had some " +
|
||||
"significant effects on society and culture.<br><br>" +
|
||||
"Due to longer lifespans and a better quality of life, many adults are holding " +
|
||||
"off on having kids until much later. As a result, the percentage of youth in " +
|
||||
"first-world countries has been decreasing, while the number " +
|
||||
"of senior citizens is significantly increasing.<br><br>" +
|
||||
"Perhaps the most alarming result of all of this is the rapidly shrinking workforce. " +
|
||||
"Despite the increase in life expectancy, the typical retirement age for " +
|
||||
"workers in America has remained about the same, meaning a larger and larger " +
|
||||
"percentage of people in America are retirees. Furthermore, many " +
|
||||
"young adults are holding off on joining the workforce because they feel that " +
|
||||
"they have plenty of time left in their lives for employment, and want to " +
|
||||
"'enjoy life while they're young.' For most industries, this shrinking workforce " +
|
||||
"is not a major issue as most things are handled by robots anyways. However, " +
|
||||
"there are still several key industries such as engineering and education " +
|
||||
"that have not been automated, and these remain in danger to this cultural " +
|
||||
"phenomenon.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "The Hidden World";
|
||||
fn = LiteratureNames.TheHiddenWorld;
|
||||
txt =
|
||||
"WAKE UP SHEEPLE<br><br>" +
|
||||
"THE GOVERNMENT DOES NOT EXIST. CORPORATIONS DO NOT RUN SOCIETY<br><br>" +
|
||||
`THE ${FactionNames.Illuminati.toUpperCase()} ARE THE SECRET RULERS OF THE WORLD!<br><br>` +
|
||||
`Yes, the ${FactionNames.Illuminati} of legends. The ancient secret society that controls the entire ` +
|
||||
"world from the shadows with their invisible hand. The group of the rich and wealthy " +
|
||||
"that have penetrated every major government, financial agency, and corporation in the last " +
|
||||
"three hundred years.<br><br>" +
|
||||
"OPEN YOUR EYES<br><br>" +
|
||||
`It was the ${FactionNames.Illuminati} that brought an end to democracy in the world. They are the driving force ` +
|
||||
"behind everything that happens.<br><br>" +
|
||||
"THEY ARE ALL AROUND YOU<br><br>" +
|
||||
"After destabilizing the world's governments, they are now entering the final stage of their master plan. " +
|
||||
"They will secretly initiate global crises. Terrorism. Pandemics. World War. And out of the chaos " +
|
||||
"that ensues they will build their New World Order.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "The New God";
|
||||
fn = LiteratureNames.TheNewGod;
|
||||
txt =
|
||||
"Everyone has a moment in their life when they wonder about the bigger questions.<br><br>" +
|
||||
"What's the point of all this? What is my purpose?<br><br>" +
|
||||
"Some people dare to think even bigger.<br><br>" +
|
||||
"What will the fate of the human race be?<br><br>" +
|
||||
"We live in an era vastly different from that of 15 or even 20 years ago. We have gone " +
|
||||
"beyond the limits of humanity. We have stripped ourselves of the tyranny of flesh.<br><br>" +
|
||||
"The Singularity is here. The merging of man and machine. This is where humanity evolves into " +
|
||||
"something greater. This is our future.<br><br>" +
|
||||
"Embrace it, and you will obey a new god. The God in the Machine.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "The New Triads";
|
||||
fn = LiteratureNames.NewTriads;
|
||||
txt =
|
||||
"The Triads were an ancient transnational crime syndicate based in China, Hong Kong, and other Asian " +
|
||||
"territories. They were often considered one of the first and biggest criminal secret societies. " +
|
||||
"While most of the branches of the Triads have been destroyed over the past few decades, the " +
|
||||
"crime faction has spawned and inspired a number of other Asian crime organizations over the past few years. " +
|
||||
`The most notable of these is the ${FactionNames.Tetrads}.<br><br>` +
|
||||
`It is widely believed that the ${FactionNames.Tetrads} are a rogue group that splintered off from the Triads sometime in the ` +
|
||||
`mid 21st century. The founders of the ${FactionNames.Tetrads}, all of whom were ex-Triad members, believed that the ` +
|
||||
`Triads were losing their purpose and direction. The ${FactionNames.Tetrads} started off as a small group that mainly engaged ` +
|
||||
"in fraud and extortion. They were largely unknown until just a few years ago when they took over the illegal " +
|
||||
"drug trade in all of the major Asian cities. They quickly became the most powerful crime syndicate in the " +
|
||||
"continent.<br><br>" +
|
||||
`Not much else is known about the ${FactionNames.Tetrads}, or about the efforts the Asian governments and corporations are making ` +
|
||||
`to take down this large new crime organization. Many believe that the ${FactionNames.Tetrads} have infiltrated the governments ` +
|
||||
"and powerful corporations in Asia, which has helped facilitate their recent rapid rise.";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
|
||||
title = "The Secret War";
|
||||
fn = LiteratureNames.TheSecretWar;
|
||||
txt = "";
|
||||
Literatures[fn] = new Literature(title, fn, txt);
|
||||
})();
|
||||
export const Literatures: Record<LiteratureName, Literature> = {
|
||||
[LiteratureName.HackersStartingHandbook]: new Literature({
|
||||
title: "The Beginner's Guide to Hacking",
|
||||
filename: LiteratureName.HackersStartingHandbook,
|
||||
text:
|
||||
"Some resources:<br><br>" +
|
||||
"<a class='a-link-button' href='https://bitburner-official.readthedocs.io/en/latest/netscript/netscriptlearntoprogram.html' target='_blank' style='margin:4px'>Learn to Program</a><br><br>" +
|
||||
"<a class='a-link-button' href='https://bitburner-official.readthedocs.io/en/latest/netscript/netscriptjs.html' target='_blank' style='margin:4px'>For Experienced JavaScript Developers: NetscriptJS</a><br><br>" +
|
||||
"<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>" +
|
||||
"When starting out, hacking is the most profitable way to earn money and progress. This " +
|
||||
"is a brief collection of tips/pointers on how to make the most out of your hacking scripts.<br><br>" +
|
||||
"-hack() and grow() both work by percentages. hack() steals a certain percentage of the " +
|
||||
"money on a server, and grow() increases the amount of money on a server by some percentage (multiplicatively)<br><br>" +
|
||||
"-Because hack() and grow() work by percentages, they are more effective if the target server has a high amount of money. " +
|
||||
"Therefore, you should try to increase the amount of money on a server (using grow()) to a certain amount before hacking it. Two " +
|
||||
"important Netscript functions for this are getServerMoneyAvailable() and getServerMaxMoney()<br><br>" +
|
||||
"-Keep security level low. Security level affects everything when hacking. Two important Netscript functions " +
|
||||
"for this are getServerSecurityLevel() and getServerMinSecurityLevel()<br><br>" +
|
||||
"-Purchase additional servers by visiting 'Alpha Enterprises' in the city. They are relatively cheap " +
|
||||
"and give you valuable RAM to run more scripts early in the game<br><br>" +
|
||||
"-Prioritize upgrading the RAM on your home computer. This can also be done at 'Alpha Enterprises'<br><br>" +
|
||||
"-Many low level servers have free RAM. You can use this RAM to run your scripts. Use the scp Terminal or " +
|
||||
"Netscript command to copy your scripts onto these servers and then run them.",
|
||||
}),
|
||||
[LiteratureName.CorporationManagementHandbook]: new Literature({
|
||||
title: "The Complete Handbook for Creating a Successful Corporation",
|
||||
filename: LiteratureName.CorporationManagementHandbook,
|
||||
text:
|
||||
"<u>Getting Started with Corporations</u><br>" +
|
||||
"To get started, visit the City Hall in Sector-12 in order to create a Corporation. This requires $150b of your own money, " +
|
||||
"but this $150b will get put into your Corporation's funds. If you're in BitNode 3 you also have option to get seed money from " +
|
||||
"the government in exchange for 500m shares. Your Corporation can have many different divisions, each in a different Industry. " +
|
||||
"There are many different types of Industries, each with different properties. To create your first division, click the 'Expand' " +
|
||||
"(into new Industry) button at the top of the management UI. The Agriculture industry is recommended for your first division.<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 " +
|
||||
"different effect on various aspects of your Corporation. It is recommended to have at least one employee at each position.<br><br>" +
|
||||
"Each industry uses some combination of Materials in order to produce other Materials and/or create Products. Specific information " +
|
||||
"about this is displayed in each of your divisions' UI.<br><br>" +
|
||||
"Products are special, industry-specific objects. They are different than Materials because you must manually choose to develop them, " +
|
||||
"and you can choose to develop any number of Products. Developing a Product takes time, but a Product typically generates significantly " +
|
||||
"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 " +
|
||||
"panel of the division UI (e.g. For the Software Industry, the button says 'Develop Software').<br><br>" +
|
||||
"To get your supply chain system started, purchase the Materials that your industry needs to produce other Materials/Products. This can be " +
|
||||
"done by clicking the 'Buy' button next to the corresponding Material(s). After you have the required Materials, you will immediately start " +
|
||||
"production. The amount and quality/effective rating of Materials/Products you produce is based on a variety of factors, such as your employees " +
|
||||
"and their productivity and the quality of materials used for production.<br><br>" +
|
||||
"Once you start producing Materials/Products, you can sell them in order to start earning revenue. This can be done by clicking the 'Sell' " +
|
||||
"button next to the corresponding Material or Product. The amount of Material/Product you sell is dependent on a wide variety of different factors. " +
|
||||
"In order to produce and sell a Product you'll have to fully develop it first.<br><br>" +
|
||||
"These are the basics of getting your Corporation up and running! Now, you can start purchasing upgrades to improve your bottom line. " +
|
||||
"If you need money, consider looking for seed investors, who will give you money in exchange for stock shares. Otherwise, once you feel " +
|
||||
"you are ready, take your Corporation public! Once your Corporation goes public, you can no longer find investors. Instead, your Corporation " +
|
||||
"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 " +
|
||||
"can set dividends for a solid reliable income or you can sell your stock shares in order to make quick money.<br><br>" +
|
||||
"<u>Tips/Pointers</u><br>" +
|
||||
"-Start with one division, such as Agriculture. Get it profitable on it's own, then expand to a division that consumes/produces " +
|
||||
"a material that the division you selected produces/consumes.<br><br>" +
|
||||
"-Materials are profitable, but Products are where the real money is, although if the product had a low development budget or is " +
|
||||
"produced with low quality materials it won't sell well.<br><br>" +
|
||||
"-The 'Smart Supply' upgrade is extremely useful. Consider purchasing it as soon as possible.<br><br>" +
|
||||
"-Purchasing Hardware, Robots, AI Cores, and Real Estate can potentially increase your production. The effects of these depend on " +
|
||||
"what industry you are in.<br><br>" +
|
||||
"-In order to optimize your production, you will need a good balance of all employee positions, about 1/9 should be interning<br><br>" +
|
||||
"-Quality of materials used for production affects the quality/effective rating of materials/products produced, so vertical integration " +
|
||||
"is important for high profits.<br><br>" +
|
||||
"-Materials purchased from the open market are always of quality 1.<br><br>" +
|
||||
"-The price at which you can sell your Materials/Products is highly affected by the quality/effective rating<br><br>" +
|
||||
"-When developing a product, different employee positions affect the development process differently, " +
|
||||
"some improve the development speed, some improve the rating of the finished product.<br><br>" +
|
||||
"-If your employees have low morale or energy, their production will greatly suffer. Having enough interns will make sure those stats get " +
|
||||
"high and stay high.<br><br>" +
|
||||
"-Don't forget to advertise your company. You won't have any business if nobody knows you.<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 " +
|
||||
"possible to see the biggest benefit for your sales<br><br>" +
|
||||
"-Remember, you need to spend money to make money!<br><br>" +
|
||||
"-Corporations do not reset when installing Augmentations, but they do reset when destroying a BitNode",
|
||||
}),
|
||||
[LiteratureName.HistoryOfSynthoids]: new Literature({
|
||||
title: "A Brief History of Synthoids",
|
||||
filename: LiteratureName.HistoryOfSynthoids,
|
||||
text:
|
||||
"Synthetic androids, or Synthoids for short, are genetically engineered robots and, short of Augmentations, " +
|
||||
"are composed entirely of organic substances. For this reason, Synthoids are virtually identical to " +
|
||||
"humans in form, composition, and appearance.<br><br>" +
|
||||
`Synthoids were first designed and manufactured by ${FactionNames.OmniTekIncorporated} sometime around the middle of the century. ` +
|
||||
"Their original purpose was to be used for manual labor and as emergency responders for disasters. As such, they " +
|
||||
"were initially programmed only for their specific tasks. Each iteration that followed improved upon the " +
|
||||
"intelligence and capabilities of the Synthoids. By the 6th iteration, called MK-VI, the Synthoids were " +
|
||||
`so smart and capable enough of making their own decisions that many argued ${FactionNames.OmniTekIncorporated} had created the first ` +
|
||||
"sentient AI. These MK-VI Synthoids were produced in mass quantities (estimates up to 50 billion) with the hopes of increasing society's " +
|
||||
"productivity and bolstering the global economy. Stemming from humanity's desire for technological advancement, optimism " +
|
||||
"and excitement about the future had never been higher.<br><br>" +
|
||||
"All of that excitement and optimism quickly turned to fear, panic, and dread in 2070, when a terrorist group " +
|
||||
`called Ascendis Totalis hacked into ${FactionNames.OmniTekIncorporated} and uploaded a rogue AI into several of their Synthoid manufacturing facilities. ` +
|
||||
`This hack went undetected and for months ${FactionNames.OmniTekIncorporated} unknowingly churned out legions of Synthoids embedded with this ` +
|
||||
"rogue AI. Then, on December 24th, 2070, Omnica activated dormant protocols in the rogue AI, causing all of the " +
|
||||
"infected Synthoids to immediately launch a military campaign to seek and destroy all of humanity.<br><br>" +
|
||||
"What ensued was the deadliest conflict in human history. This crisis, now commonly known as the Synthoid Uprising, " +
|
||||
"resulted in almost ten billion deaths over the course of a year. Despite the nations of the world banding together " +
|
||||
"to combat the threat, the MK-VI Synthoids were simply stronger, faster, more intelligent, and more adaptable than humans, " +
|
||||
"outsmarting them at every turn.<br><br>" +
|
||||
`It wasn't until the sacrifice of an elite international military taskforce, called the ${FactionNames.Bladeburners}, that humanity ` +
|
||||
`was finally able to defeat the Synthoids. The ${FactionNames.Bladeburners}' final act was a suicide bombing mission that ` +
|
||||
"destroyed a large portion of the MK-VI Synthoids, including many of its leaders. In the following " +
|
||||
"weeks militaries from around the world were able to round up and shut down the remaining rogue MK-VI Synthoids, ending " +
|
||||
"the Synthoid Uprising.<br><br>" +
|
||||
`In the aftermath of the bloodshed, the Synthoid Accords were drawn up. These Accords banned ${FactionNames.OmniTekIncorporated} ` +
|
||||
"from manufacturing any Synthoids beyond the MK-III series. They also banned any other corporation " +
|
||||
"from constructing androids with advanced, near-sentient AI. MK-VI Synthoids that did not have the rogue Ascendis Totalis " +
|
||||
"AI were allowed to continue their existence, but they were stripped of all rights and protections as they " +
|
||||
"were not considered humans. They were also banned from doing anything that may pose a global security threat, such " +
|
||||
"as working for any military/defense organization or conducting any bioengineering, computing, or robotics related research.<br><br>" +
|
||||
"Unfortunately, many believe that not all of the rogue MK-VI Synthoids from the Uprising were found and destroyed, " +
|
||||
"and that many of them are blending in as normal humans in society today. In response, many nations have created " +
|
||||
`${FactionNames.Bladeburners} divisions, special military branches that are tasked with investigating and dealing with any Synthoid threats.<br><br>` +
|
||||
"To this day, tensions still exist between the remaining Synthoids and humans as a result of the Uprising.<br><br>" +
|
||||
"Nobody knows what happened to the terrorist group Ascendis Totalis.",
|
||||
}),
|
||||
[LiteratureName.AGreenTomorrow]: new Literature({
|
||||
title: "A Green Tomorrow",
|
||||
filename: LiteratureName.AGreenTomorrow,
|
||||
text:
|
||||
"Starting a few decades ago, there was a massive global movement towards the generation of renewable energy in an effort to " +
|
||||
"combat global warming and climate change. The shift towards renewable energy was a big success, or so it seemed. In 2045 " +
|
||||
"a staggering 80% of the world's energy came from non-renewable fossil fuels. Now, about three decades later, that " +
|
||||
"number is down to only 15%. Most of the world's energy now comes from nuclear power and renewable sources such as " +
|
||||
"solar and geothermal energy. Unfortunately, these efforts were not the huge success that they seem to be.<br><br>" +
|
||||
"Since 2045 primary energy use has soared almost tenfold. This was mainly due to growing urban populations and " +
|
||||
"the rise of increasingly advanced (and power-hungry) technology that has become ubiquitous in our lives. So, " +
|
||||
"despite the fact that the percentage of our energy that comes from fossil fuels has drastically decreased, " +
|
||||
"the total amount of energy we are producing from fossil fuels has actually increased.<br><br>" +
|
||||
"The grim effects of our species' irresponsible use of energy and neglect of our mother world have become increasingly apparent. " +
|
||||
"Last year a temperature of 190F was recorded in the Death Valley desert, which is over 50% higher than the highest " +
|
||||
"recorded temperature at the beginning of the century. In the last two decades numerous major cities such as Manhattan, Boston, and " +
|
||||
"Los Angeles have been partially or fully submerged by rising sea levels. In the present day, over 75% of the world's agriculture is " +
|
||||
"done in climate-controlled vertical farms, as most traditional farmland has become unusable due to severe climate conditions.<br><br>" +
|
||||
"Despite all of this, the greedy and corrupt corporations that rule the world have done nothing to address these problems that " +
|
||||
"threaten our species. And so it's up to us, the common people. Each and every one of us can make a difference by doing what " +
|
||||
"these corporations won't: taking responsibility. If we don't, pretty soon there won't be an Earth left to save. We are " +
|
||||
"the last hope for a green tomorrow.",
|
||||
}),
|
||||
[LiteratureName.AlphaOmega]: new Literature({
|
||||
title: "Alpha and Omega",
|
||||
filename: LiteratureName.AlphaOmega,
|
||||
text:
|
||||
"Then we saw a new Heaven and a new Earth, for our first Heaven and Earth had gone away, and our sea was no more. " +
|
||||
"And we saw a new holy city, new Aeria, coming down out of this new Heaven, prepared as a bride adorned for her husband. " +
|
||||
"And we heard a loud voice saying, 'Behold, the new dwelling place of the Gods. We will dwell with them, and they " +
|
||||
"will be our people, and we will be with them as their Gods. We will wipe away every tear from their eyes, and death " +
|
||||
"shall be no more, neither shall there be mourning, nor crying, nor pain anymore, for the former things " +
|
||||
"have passed away.'<br><br>" +
|
||||
"And once we were seated on the throne we said 'Behold, I am making all things new.' " +
|
||||
"Also we said, 'Write this down, for these words are trustworthy and true.' And we said to you, " +
|
||||
"'It is done! I am the Alpha and the Omega, the beginning and the end. To the thirsty I will give from the spring " +
|
||||
"of the water of life without payment. The one who conquers will have this heritage, and we will be his God and " +
|
||||
"he will be our son. But as for the cowardly, the faithless, the detestable, as for murderers, " +
|
||||
"the sexually immoral, sorcerers, idolaters, and all liars, their portion will be in the lake that " +
|
||||
"burns with fire and sulfur, for it is the second true death.'",
|
||||
}),
|
||||
[LiteratureName.SimulatedReality]: new Literature({
|
||||
title: "Are We Living in a Computer Simulation?",
|
||||
filename: LiteratureName.SimulatedReality,
|
||||
text:
|
||||
"The idea that we are living in a virtual world is not new. It's a trope that has " +
|
||||
"been explored constantly in literature and pop culture. However, it is also a legitimate " +
|
||||
"scientific hypothesis that many notable physicists and philosophers have debated for years.<br><br>" +
|
||||
"Proponents for this simulated reality theory often point to how advanced our technology has become, " +
|
||||
"as well as the incredibly fast pace at which it has advanced over the past decades. The amount of computing " +
|
||||
"power available to us has increased over 100-fold since 2060 due to the development of nanoprocessors and " +
|
||||
"quantum computers. Artificial Intelligence has advanced to the point where our entire lives are controlled " +
|
||||
"by robots and machines that handle our day-to-day activities such as autonomous transportation and scheduling. " +
|
||||
"If we consider the pace at which this technology has advanced and assume that these developments continue, it's " +
|
||||
"reasonable to assume that at some point in the future our technology would be advanced enough that " +
|
||||
"we could create simulations that are indistinguishable from reality. However, if continued technological advancement " +
|
||||
"is a reasonable outcome, then it is very likely that such a scenario has already happened. <br><br>" +
|
||||
"Statistically speaking, somewhere out there in the infinite universe there is an advanced, intelligent species " +
|
||||
"that already has such technology. Who's to say that they haven't already created such a virtual reality: our own?",
|
||||
}),
|
||||
[LiteratureName.BeyondMan]: new Literature({
|
||||
title: "Beyond Man",
|
||||
filename: LiteratureName.BeyondMan,
|
||||
text:
|
||||
"Humanity entered a 'transhuman' era a long time ago. And despite the protests and criticisms of many who cried out against " +
|
||||
"human augmentation at the time, the transhuman movement continued and prospered. Proponents of the movement ignored the critics, " +
|
||||
"arguing that it was in our inherent nature to better ourselves. To improve. To be more than we were. They claimed that " +
|
||||
"not doing so would be to go against every living organism's biological purpose: evolution and survival of the fittest.<br><br>" +
|
||||
"And here we are today, with technology that is advanced enough to augment humans to a state that " +
|
||||
"can only be described as posthuman. But what do we have to show for it when this augmentation " +
|
||||
"technology is only available to the so-called 'elite'? Are we really better off than before when only 5% of the " +
|
||||
"world's population has access to this technology? When the powerful corporations and organizations of the world " +
|
||||
"keep it all to themselves, have we really evolved?<br><br>" +
|
||||
"Augmentation technology has only further increased the divide between the rich and the poor, between the powerful and " +
|
||||
"the oppressed. We have not become 'more than human'. We have not evolved from nature's original design. We are still the greedy, " +
|
||||
"corrupted, and evil men that we always were.",
|
||||
}),
|
||||
[LiteratureName.BrighterThanTheSun]: new Literature({
|
||||
title: "Brighter than the Sun",
|
||||
filename: LiteratureName.BrighterThanTheSun,
|
||||
text:
|
||||
`When people think about the corporations that dominate the East, they typically think of ${FactionNames.KuaiGongInternational}, which ` +
|
||||
"holds a complete monopoly for manufacturing and commerce in Asia, or Global Pharmaceuticals, the world's largest " +
|
||||
`drug company, or ${FactionNames.OmniTekIncorporated}, the global leader in intelligent and autonomous robots. But there's one company ` +
|
||||
"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>" +
|
||||
"TaiYang Digital is a Chinese internet-technology corporation that provides services such as " +
|
||||
"online advertising, search engines, gaming, media, entertainment, and cloud computing/storage. Its name TaiYang comes from the Chinese word " +
|
||||
"for 'sun'. In Chinese culture, the sun is a 'yang' symbol " +
|
||||
"associated with life, heat, masculinity, and heaven.<br><br>" +
|
||||
"The company was founded " +
|
||||
"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 " +
|
||||
"over 10 trillion yuan. Its services are used daily by over a billion people worldwide.<br><br>" +
|
||||
"TaiYang Digital's meteoric rise is extremely surprising in modern society. This sort of growth is " +
|
||||
"something you'd commonly see in the first half of the century, especially for tech companies. However in " +
|
||||
"the last two decades the number of corporations has significantly declined as the largest entities " +
|
||||
`quickly took over the economy. Corporations such as ${FactionNames.ECorp}, ${FactionNames.MegaCorp}, and ${FactionNames.KuaiGongInternational} have established ` +
|
||||
"such strong monopolies in their market sectors that they have effectively killed off all " +
|
||||
"of the smaller and new corporations that have tried to start up over the years. This is what makes " +
|
||||
"the rise of TaiYang Digital so impressive. And if TaiYang continues down this path, then they have " +
|
||||
"a bright future ahead of them.",
|
||||
}),
|
||||
[LiteratureName.DemocracyIsDead]: new Literature({
|
||||
title: "Democracy is Dead: The Fall of an Empire",
|
||||
filename: LiteratureName.DemocracyIsDead,
|
||||
text:
|
||||
"They rose from the shadows in the street.<br>From the places where the oppressed meet.<br>" +
|
||||
"Their cries echoed loudly through the air.<br>As they once did in Tiananmen Square.<br>" +
|
||||
"Loudness in the silence, Darkness in the light.<br>They came forth with power and might.<br>" +
|
||||
"Once the beacon of democracy, America was first.<br>Its pillars of society destroyed and dispersed.<br>" +
|
||||
"Soon the cries rose everywhere, with revolt and riot.<br>Until one day, finally, all was quiet.<br>" +
|
||||
"From the ashes rose a new order, corporatocracy was its name.<br>" +
|
||||
"Rome, Mongol, Byzantine, all of history is just the same.<br>" +
|
||||
"For man will never change in a fundamental way.<br>" +
|
||||
"And now democracy is dead, in the USA.",
|
||||
}),
|
||||
[LiteratureName.Sector12Crime]: new Literature({
|
||||
title: `Figures Show Rising Crime Rates in ${CityName.Sector12}`,
|
||||
filename: LiteratureName.Sector12Crime,
|
||||
text:
|
||||
"A recent study by analytics company Wilson Inc. shows a significant rise " +
|
||||
`in criminal activity in ${CityName.Sector12}. Perhaps the most alarming part of the statistic ` +
|
||||
"is that most of the rise is in violent crime such as homicide and assault. According " +
|
||||
"to the study, the city saw a total of 21,406 reported homicides in 2076, which is over " +
|
||||
"a 20% increase compared to 2075.<br><br>" +
|
||||
"CIA director David Glarow says it's too early to know " +
|
||||
"whether these figures indicate the beginning of a sustained increase in crime rates, or whether " +
|
||||
"the year was just an unfortunate outlier. He states that many intelligence and law enforcement " +
|
||||
"agents have noticed an increase in organized crime activities, and believes that these figures may " +
|
||||
`be the result of an uprising from criminal organizations such as ${FactionNames.TheSyndicate} or the ${FactionNames.SlumSnakes}.`,
|
||||
}),
|
||||
[LiteratureName.ManAndMachine]: new Literature({
|
||||
title: "Man and the Machine",
|
||||
filename: LiteratureName.ManAndMachine,
|
||||
text:
|
||||
"In 2005 Ray Kurzweil popularized his theory of the Singularity. He predicted that the rate " +
|
||||
"of technological advancement would continue to accelerate faster and faster until one day " +
|
||||
"machines would be become infinitely more intelligent than humans. This point, called the " +
|
||||
"Singularity, would result in a drastic transformation of the world as we know it. He predicted " +
|
||||
"that the Singularity would arrive by 2045. " +
|
||||
"And yet here we are, more than three decades later, where most would agree that we have not " +
|
||||
"yet reached a point where computers and machines are vastly more intelligent than we are. So what gives?<br><br>" +
|
||||
"The answer is that we have reached the Singularity, just not in the way we expected. The artificial superintelligence " +
|
||||
"that was predicted by Kurzweil and others exists in the world today - in the form of Augmentations. " +
|
||||
"Yes, those Augmentations that the rich and powerful keep to themselves enable humans " +
|
||||
"to become superintelligent beings. The Singularity did not lead to a world where " +
|
||||
"our machines are infinitely more intelligent than us, it led to a world " +
|
||||
"where man and machine can merge to become something greater. Most of the world just doesn't " +
|
||||
"know it yet.",
|
||||
}),
|
||||
[LiteratureName.SecretSocieties]: new Literature({
|
||||
title: "Secret Societies",
|
||||
filename: LiteratureName.SecretSocieties,
|
||||
text:
|
||||
"The idea of secret societies has long intrigued the general public by inspiring curiosity, fascination, and " +
|
||||
"distrust. People have long wondered about who these secret society members are and what they do, with the " +
|
||||
"most radical of conspiracy theorists claiming that they control everything in the entire world. And while the world " +
|
||||
"may never know for sure, it is likely that many secret societies do actually exist, even today.<br><br>" +
|
||||
"However, the secret societies of the modern world are nothing like those that (supposedly) existed " +
|
||||
`decades and centuries ago. The Freemasons, Knights Templar, and ${FactionNames.Illuminati}, while they may have been around ` +
|
||||
"at the turn of the 21st century, almost assuredly do not exist today. The dominance of the Web in " +
|
||||
"our everyday lives and the fact that so much of the world is now digital has given rise to a new breed " +
|
||||
"of secret societies: Internet-based ones.<br><br>" +
|
||||
"Commonly called 'hacker groups', Internet-based secret societies have become well-known in today's " +
|
||||
`world. Some of these, such as ${FactionNames.TheBlackHand}, are black hat groups that claim they are trying to ` +
|
||||
`help the oppressed by attacking the elite and powerful. Others, such as ${FactionNames.NiteSec}, are hacktivist groups ` +
|
||||
"that try to push political and social agendas. Perhaps the most intriguing hacker group " +
|
||||
`is the mysterious ${FactionNames.BitRunners}, whose purpose still remains unknown.`,
|
||||
}),
|
||||
[LiteratureName.TheFailedFrontier]: new Literature({
|
||||
title: "Space: The Failed Frontier",
|
||||
filename: LiteratureName.TheFailedFrontier,
|
||||
text:
|
||||
"Humans have long dreamed about spaceflight. With enduring interest, we were driven to explore " +
|
||||
"the unknown and discover new worlds. We dreamed about conquering the stars. And in our quest, " +
|
||||
"we pushed the boundaries of our scientific limits, and then pushed further. Space exploration " +
|
||||
"lead to the development of many important technologies and new industries.<br><br>" +
|
||||
"But sometime in the middle of the 21st century, all of that changed. Humanity lost its ambitions and " +
|
||||
"aspirations of exploring the cosmos. The once-large funding for agencies like NASA and the European " +
|
||||
"Space Agency gradually whittled away until their eventual disbanding in the 2060's. Not even " +
|
||||
"militaries are fielding flights into space nowadays. The only remnants of the once great mission for cosmic " +
|
||||
"conquest are the countless satellites in near-earth orbit, used for communications, espionage, " +
|
||||
"and other corporate interests.<br><br>" +
|
||||
"And as we continue to look at the state of space technology, it becomes more and " +
|
||||
"more apparent that we will never return to that golden age of space exploration, that " +
|
||||
"age where everyone dreamed of going beyond earth for the sake of discovery.",
|
||||
}),
|
||||
[LiteratureName.CodedIntelligence]: new Literature({
|
||||
title: "Coded Intelligence: Myth or Reality?",
|
||||
filename: LiteratureName.CodedIntelligence,
|
||||
text:
|
||||
"Tremendous progress has been made in the field of Artificial Intelligence over the past few decades. " +
|
||||
"Our autonomous vehicles and transportation systems. The electronic personal assistants that control our everyday lives. " +
|
||||
"Medical, service, and manufacturing robots. All of these are examples of how far AI has come and how much it has " +
|
||||
"improved our daily lives. However, the question still remains of whether AI will ever be advanced enough to re-create " +
|
||||
"human intelligence.<br><br>" +
|
||||
`We've certainly come close to artificial intelligence that is similar to humans. For example ${FactionNames.OmniTekIncorporated}'s ` +
|
||||
"CompanionBot, a robot meant to act as a comforting friend for lonely and grieving people, is eerily human-like " +
|
||||
"in its appearance, speech, mannerisms, and even movement. However its artificial intelligence isn't the same as " +
|
||||
"that of humans. Not yet. It doesn't have sentience or self-awareness or consciousness.<br><br>" +
|
||||
"Many neuroscientists believe that we won't ever reach the point of creating artificial human intelligence. 'At the end of " +
|
||||
"the day, AI comes down to 1's and 0's, while the human brain does not. We'll never see AI that is identical to that of " +
|
||||
"humans.'",
|
||||
}),
|
||||
[LiteratureName.SyntheticMuscles]: new Literature({
|
||||
title: "Synthetic Muscles",
|
||||
filename: LiteratureName.SyntheticMuscles,
|
||||
text:
|
||||
"Initial versions of synthetic muscles weren't made of anything organic but were actually " +
|
||||
"crude devices made to mimic human muscle function. Some of the early iterations were actually made of " +
|
||||
"common materials such as fishing lines and sewing threads due to their high strength for " +
|
||||
"a cheap cost.<br><br>" +
|
||||
"As technology progressed, however, advances in biomedical engineering paved the way for a new method of " +
|
||||
"creating synthetic muscles. Instead of creating something that closely imitated the functionality " +
|
||||
"of human muscle, scientists discovered a way of forcing the human body itself to augment its own " +
|
||||
"muscle tissue using both synthetic and organic materials. This is typically done using gene therapy " +
|
||||
"or chemical injections.",
|
||||
}),
|
||||
[LiteratureName.TensionsInTechRace]: new Literature({
|
||||
title: "Tensions rise in global tech race",
|
||||
filename: LiteratureName.TensionsInTechRace,
|
||||
text:
|
||||
"Have we entered a new Cold War? Is WWIII just beyond the horizon?<br><br>" +
|
||||
`After rumors came out that ${FactionNames.OmniTekIncorporated} had begun developing advanced robotic supersoldiers, ` +
|
||||
"geopolitical tensions quickly flared between the USA, Russia, and several Asian superpowers. " +
|
||||
`In a rare show of cooperation between corporations, ${FactionNames.MegaCorp} and ${FactionNames.ECorp} have ` +
|
||||
"reportedly launched hundreds of new surveillance and espionage satellites. " +
|
||||
"Defense contractors such as " +
|
||||
"DeltaOne and AeroCorp have been working with the CIA and NSA to prepare " +
|
||||
"for conflict. Meanwhile, the rest of the world sits in earnest " +
|
||||
"hoping that it never reaches full-scale war. With today's technology " +
|
||||
"and firepower, a World War would assuredly mean the end of human civilization.",
|
||||
}),
|
||||
[LiteratureName.CostOfImmortality]: new Literature({
|
||||
title: "The Cost of Immortality",
|
||||
filename: LiteratureName.CostOfImmortality,
|
||||
text:
|
||||
"Evolution and advances in medical and augmentation technology has lead to drastic improvements " +
|
||||
"in human mortality rates. Recent figures show that the life expectancy for humans " +
|
||||
"that live in a first-world country is about 130 years of age, almost double of what it was " +
|
||||
"at the turn of the century. However, this increase in average lifespan has had some " +
|
||||
"significant effects on society and culture.<br><br>" +
|
||||
"Due to longer lifespans and a better quality of life, many adults are holding " +
|
||||
"off on having kids until much later. As a result, the percentage of youth in " +
|
||||
"first-world countries has been decreasing, while the number " +
|
||||
"of senior citizens is significantly increasing.<br><br>" +
|
||||
"Perhaps the most alarming result of all of this is the rapidly shrinking workforce. " +
|
||||
"Despite the increase in life expectancy, the typical retirement age for " +
|
||||
"workers in America has remained about the same, meaning a larger and larger " +
|
||||
"percentage of people in America are retirees. Furthermore, many " +
|
||||
"young adults are holding off on joining the workforce because they feel that " +
|
||||
"they have plenty of time left in their lives for employment, and want to " +
|
||||
"'enjoy life while they're young.' For most industries, this shrinking workforce " +
|
||||
"is not a major issue as most things are handled by robots anyways. However, " +
|
||||
"there are still several key industries such as engineering and education " +
|
||||
"that have not been automated, and these remain in danger to this cultural " +
|
||||
"phenomenon.",
|
||||
}),
|
||||
[LiteratureName.TheHiddenWorld]: new Literature({
|
||||
title: "The Hidden World",
|
||||
filename: LiteratureName.TheHiddenWorld,
|
||||
text:
|
||||
"WAKE UP SHEEPLE<br><br>" +
|
||||
"THE GOVERNMENT DOES NOT EXIST. CORPORATIONS DO NOT RUN SOCIETY<br><br>" +
|
||||
`THE ${FactionNames.Illuminati.toUpperCase()} ARE THE SECRET RULERS OF THE WORLD!<br><br>` +
|
||||
`Yes, the ${FactionNames.Illuminati} of legends. The ancient secret society that controls the entire ` +
|
||||
"world from the shadows with their invisible hand. The group of the rich and wealthy " +
|
||||
"that have penetrated every major government, financial agency, and corporation in the last " +
|
||||
"three hundred years.<br><br>" +
|
||||
"OPEN YOUR EYES<br><br>" +
|
||||
`It was the ${FactionNames.Illuminati} that brought an end to democracy in the world. They are the driving force ` +
|
||||
"behind everything that happens.<br><br>" +
|
||||
"THEY ARE ALL AROUND YOU<br><br>" +
|
||||
"After destabilizing the world's governments, they are now entering the final stage of their master plan. " +
|
||||
"They will secretly initiate global crises. Terrorism. Pandemics. World War. And out of the chaos " +
|
||||
"that ensues they will build their New World Order.",
|
||||
}),
|
||||
[LiteratureName.TheNewGod]: new Literature({
|
||||
title: "The New God",
|
||||
filename: LiteratureName.TheNewGod,
|
||||
text:
|
||||
"Everyone has a moment in their life when they wonder about the bigger questions.<br><br>" +
|
||||
"What's the point of all this? What is my purpose?<br><br>" +
|
||||
"Some people dare to think even bigger.<br><br>" +
|
||||
"What will the fate of the human race be?<br><br>" +
|
||||
"We live in an era vastly different from that of 15 or even 20 years ago. We have gone " +
|
||||
"beyond the limits of humanity. We have stripped ourselves of the tyranny of flesh.<br><br>" +
|
||||
"The Singularity is here. The merging of man and machine. This is where humanity evolves into " +
|
||||
"something greater. This is our future.<br><br>" +
|
||||
"Embrace it, and you will obey a new god. The God in the Machine.",
|
||||
}),
|
||||
[LiteratureName.NewTriads]: new Literature({
|
||||
title: "The New Triads",
|
||||
filename: LiteratureName.NewTriads,
|
||||
text:
|
||||
"The Triads were an ancient transnational crime syndicate based in China, Hong Kong, and other Asian " +
|
||||
"territories. They were often considered one of the first and biggest criminal secret societies. " +
|
||||
"While most of the branches of the Triads have been destroyed over the past few decades, the " +
|
||||
"crime faction has spawned and inspired a number of other Asian crime organizations over the past few years. " +
|
||||
`The most notable of these is the ${FactionNames.Tetrads}.<br><br>` +
|
||||
`It is widely believed that the ${FactionNames.Tetrads} are a rogue group that splintered off from the Triads sometime in the ` +
|
||||
`mid 21st century. The founders of the ${FactionNames.Tetrads}, all of whom were ex-Triad members, believed that the ` +
|
||||
`Triads were losing their purpose and direction. The ${FactionNames.Tetrads} started off as a small group that mainly engaged ` +
|
||||
"in fraud and extortion. They were largely unknown until just a few years ago when they took over the illegal " +
|
||||
"drug trade in all of the major Asian cities. They quickly became the most powerful crime syndicate in the " +
|
||||
"continent.<br><br>" +
|
||||
`Not much else is known about the ${FactionNames.Tetrads}, or about the efforts the Asian governments and corporations are making ` +
|
||||
`to take down this large new crime organization. Many believe that the ${FactionNames.Tetrads} have infiltrated the governments ` +
|
||||
"and powerful corporations in Asia, which has helped facilitate their recent rapid rise.",
|
||||
}),
|
||||
[LiteratureName.TheSecretWar]: new Literature({
|
||||
title: "The Secret War",
|
||||
filename: LiteratureName.TheSecretWar,
|
||||
text: "",
|
||||
}),
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
export enum LiteratureNames {
|
||||
export enum LiteratureName {
|
||||
HackersStartingHandbook = "hackers-starting-handbook.lit",
|
||||
CorporationManagementHandbook = "corporation-management-handbook.lit",
|
||||
HistoryOfSynthoids = "history-of-synthoids.lit",
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { MessageFilenames } from "./MessageHelpers";
|
||||
import { FilePath, asFilePath } from "../Paths/FilePath";
|
||||
import { MessageFilename } from "./MessageHelpers";
|
||||
|
||||
export class Message {
|
||||
// Name of Message file
|
||||
filename: MessageFilenames;
|
||||
filename: MessageFilename & FilePath;
|
||||
|
||||
// The text contains in the Message
|
||||
msg: string;
|
||||
|
||||
constructor(filename: MessageFilenames, msg: string) {
|
||||
this.filename = filename;
|
||||
constructor(filename: MessageFilename, msg: string) {
|
||||
this.filename = asFilePath(filename);
|
||||
this.msg = msg;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
import { Message } from "./Message";
|
||||
import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
|
||||
import { Router } from "../ui/GameRoot";
|
||||
import { Programs } from "../Programs/Programs";
|
||||
import { CompletedProgramName } from "../Programs/Programs";
|
||||
import { Player } from "@player";
|
||||
import { Page } from "../ui/Router";
|
||||
import { GetServer } from "../Server/AllServers";
|
||||
@ -13,16 +13,15 @@ import { FactionNames } from "../Faction/data/FactionNames";
|
||||
import { Server } from "../Server/Server";
|
||||
|
||||
//Sends message to player, including a pop up
|
||||
function sendMessage(msg: Message, forced = false): void {
|
||||
function sendMessage(name: MessageFilename, forced = false): void {
|
||||
if (forced || !Settings.SuppressMessages) {
|
||||
showMessage(msg.filename);
|
||||
showMessage(name);
|
||||
}
|
||||
addMessageToServer(msg);
|
||||
addMessageToServer(name);
|
||||
}
|
||||
|
||||
function showMessage(name: MessageFilenames): void {
|
||||
function showMessage(name: MessageFilename): void {
|
||||
const msg = Messages[name];
|
||||
if (!(msg instanceof Message)) throw new Error("trying to display nonexistent message");
|
||||
dialogBoxCreate(
|
||||
<>
|
||||
Message received from unknown sender:
|
||||
@ -37,40 +36,22 @@ function showMessage(name: MessageFilenames): void {
|
||||
}
|
||||
|
||||
//Adds a message to a server
|
||||
function addMessageToServer(msg: Message): void {
|
||||
function addMessageToServer(name: MessageFilename): void {
|
||||
//Short-circuit if the message has already been saved
|
||||
if (recvd(msg)) {
|
||||
return;
|
||||
}
|
||||
const server = GetServer("home");
|
||||
if (server == null) {
|
||||
throw new Error("The home server doesn't exist. You done goofed.");
|
||||
}
|
||||
server.messages.push(msg.filename);
|
||||
if (recvd(name)) return;
|
||||
const home = Player.getHomeComputer();
|
||||
home.messages.push(name);
|
||||
}
|
||||
|
||||
//Returns whether the given message has already been received
|
||||
function recvd(msg: Message): boolean {
|
||||
const server = GetServer("home");
|
||||
if (server == null) {
|
||||
throw new Error("The home server doesn't exist. You done goofed.");
|
||||
}
|
||||
return server.messages.includes(msg.filename);
|
||||
function recvd(name: MessageFilename): boolean {
|
||||
const home = Player.getHomeComputer();
|
||||
return home.messages.includes(name);
|
||||
}
|
||||
|
||||
//Checks if any of the 'timed' messages should be sent
|
||||
function checkForMessagesToSend(): void {
|
||||
if (Router.page() === Page.BitVerse) return;
|
||||
const jumper0 = Messages[MessageFilenames.Jumper0];
|
||||
const jumper1 = Messages[MessageFilenames.Jumper1];
|
||||
const jumper2 = Messages[MessageFilenames.Jumper2];
|
||||
const jumper3 = Messages[MessageFilenames.Jumper3];
|
||||
const jumper4 = Messages[MessageFilenames.Jumper4];
|
||||
const cybersecTest = Messages[MessageFilenames.CyberSecTest];
|
||||
const nitesecTest = Messages[MessageFilenames.NiteSecTest];
|
||||
const bitrunnersTest = Messages[MessageFilenames.BitRunnersTest];
|
||||
const truthGazer = Messages[MessageFilenames.TruthGazer];
|
||||
const redpill = Messages[MessageFilenames.RedPill];
|
||||
|
||||
if (Player.hasAugmentation(AugmentationNames.TheRedPill, true)) {
|
||||
//Get the world daemon required hacking level
|
||||
@ -80,37 +61,36 @@ function checkForMessagesToSend(): void {
|
||||
}
|
||||
//If the daemon can be hacked, send the player icarus.msg
|
||||
if (Player.skills.hacking >= worldDaemon.requiredHackingSkill) {
|
||||
sendMessage(redpill, Player.sourceFiles.size === 0);
|
||||
sendMessage(MessageFilename.RedPill, Player.sourceFiles.size === 0);
|
||||
}
|
||||
//If the daemon cannot be hacked, send the player truthgazer.msg a single time.
|
||||
else if (!recvd(truthGazer)) {
|
||||
sendMessage(truthGazer);
|
||||
else if (!recvd(MessageFilename.TruthGazer)) {
|
||||
sendMessage(MessageFilename.TruthGazer);
|
||||
}
|
||||
} else if (!recvd(jumper0) && Player.skills.hacking >= 25) {
|
||||
sendMessage(jumper0);
|
||||
const flightName = Programs.Flight.name;
|
||||
} else if (!recvd(MessageFilename.Jumper0) && Player.skills.hacking >= 25) {
|
||||
sendMessage(MessageFilename.Jumper0);
|
||||
const homeComp = Player.getHomeComputer();
|
||||
if (!homeComp.programs.includes(flightName)) {
|
||||
homeComp.programs.push(flightName);
|
||||
if (!homeComp.programs.includes(CompletedProgramName.flight)) {
|
||||
homeComp.programs.push(CompletedProgramName.flight);
|
||||
}
|
||||
} else if (!recvd(jumper1) && Player.skills.hacking >= 40) {
|
||||
sendMessage(jumper1);
|
||||
} else if (!recvd(cybersecTest) && Player.skills.hacking >= 50) {
|
||||
sendMessage(cybersecTest);
|
||||
} else if (!recvd(jumper2) && Player.skills.hacking >= 175) {
|
||||
sendMessage(jumper2);
|
||||
} else if (!recvd(nitesecTest) && Player.skills.hacking >= 200) {
|
||||
sendMessage(nitesecTest);
|
||||
} else if (!recvd(jumper3) && Player.skills.hacking >= 325) {
|
||||
sendMessage(jumper3);
|
||||
} else if (!recvd(jumper4) && Player.skills.hacking >= 490) {
|
||||
sendMessage(jumper4);
|
||||
} else if (!recvd(bitrunnersTest) && Player.skills.hacking >= 500) {
|
||||
sendMessage(bitrunnersTest);
|
||||
} else if (!recvd(MessageFilename.Jumper1) && Player.skills.hacking >= 40) {
|
||||
sendMessage(MessageFilename.Jumper1);
|
||||
} else if (!recvd(MessageFilename.CyberSecTest) && Player.skills.hacking >= 50) {
|
||||
sendMessage(MessageFilename.CyberSecTest);
|
||||
} else if (!recvd(MessageFilename.Jumper2) && Player.skills.hacking >= 175) {
|
||||
sendMessage(MessageFilename.Jumper2);
|
||||
} else if (!recvd(MessageFilename.NiteSecTest) && Player.skills.hacking >= 200) {
|
||||
sendMessage(MessageFilename.NiteSecTest);
|
||||
} else if (!recvd(MessageFilename.Jumper3) && Player.skills.hacking >= 325) {
|
||||
sendMessage(MessageFilename.Jumper3);
|
||||
} else if (!recvd(MessageFilename.Jumper4) && Player.skills.hacking >= 490) {
|
||||
sendMessage(MessageFilename.Jumper4);
|
||||
} else if (!recvd(MessageFilename.BitRunnersTest) && Player.skills.hacking >= 500) {
|
||||
sendMessage(MessageFilename.BitRunnersTest);
|
||||
}
|
||||
}
|
||||
|
||||
export enum MessageFilenames {
|
||||
export enum MessageFilename {
|
||||
Jumper0 = "j0.msg",
|
||||
Jumper1 = "j1.msg",
|
||||
Jumper2 = "j2.msg",
|
||||
@ -123,11 +103,11 @@ export enum MessageFilenames {
|
||||
RedPill = "icarus.msg",
|
||||
}
|
||||
|
||||
//Reset
|
||||
const Messages: Record<MessageFilenames, Message> = {
|
||||
// This type ensures that all members of the MessageFilename enum are valid keys
|
||||
const Messages: Record<MessageFilename, Message> = {
|
||||
//jump3R Messages
|
||||
[MessageFilenames.Jumper0]: new Message(
|
||||
MessageFilenames.Jumper0,
|
||||
[MessageFilename.Jumper0]: new Message(
|
||||
MessageFilename.Jumper0,
|
||||
"I know you can sense it. I know you're searching for it. " +
|
||||
"It's why you spend night after " +
|
||||
"night at your computer. \n\nIt's real, I've seen it. And I can " +
|
||||
@ -137,8 +117,8 @@ const Messages: Record<MessageFilenames, Message> = {
|
||||
"-jump3R",
|
||||
),
|
||||
|
||||
[MessageFilenames.Jumper1]: new Message(
|
||||
MessageFilenames.Jumper1,
|
||||
[MessageFilename.Jumper1]: new Message(
|
||||
MessageFilename.Jumper1,
|
||||
`Soon you will be contacted by a hacking group known as ${FactionNames.CyberSec}. ` +
|
||||
"They can help you with your search. \n\n" +
|
||||
"You should join them, garner their favor, and " +
|
||||
@ -147,31 +127,31 @@ const Messages: Record<MessageFilenames, Message> = {
|
||||
"-jump3R",
|
||||
),
|
||||
|
||||
[MessageFilenames.Jumper2]: new Message(
|
||||
MessageFilenames.Jumper2,
|
||||
[MessageFilename.Jumper2]: new Message(
|
||||
MessageFilename.Jumper2,
|
||||
"Do not try to save the world. There is no world to save. If " +
|
||||
"you want to find the truth, worry only about yourself. Ethics and " +
|
||||
`morals will get you killed. \n\nWatch out for a hacking group known as ${FactionNames.NiteSec}.` +
|
||||
"\n\n-jump3R",
|
||||
),
|
||||
|
||||
[MessageFilenames.Jumper3]: new Message(
|
||||
MessageFilenames.Jumper3,
|
||||
[MessageFilename.Jumper3]: new Message(
|
||||
MessageFilename.Jumper3,
|
||||
"You must learn to walk before you can run. And you must " +
|
||||
`run before you can fly. Look for ${FactionNames.TheBlackHand}. \n\n` +
|
||||
"I.I.I.I \n\n-jump3R",
|
||||
),
|
||||
|
||||
[MessageFilenames.Jumper4]: new Message(
|
||||
MessageFilenames.Jumper4,
|
||||
[MessageFilename.Jumper4]: new Message(
|
||||
MessageFilename.Jumper4,
|
||||
"To find what you are searching for, you must understand the bits. " +
|
||||
"The bits are all around us. The runners will help you.\n\n" +
|
||||
"-jump3R",
|
||||
),
|
||||
|
||||
//Messages from hacking factions
|
||||
[MessageFilenames.CyberSecTest]: new Message(
|
||||
MessageFilenames.CyberSecTest,
|
||||
[MessageFilename.CyberSecTest]: new Message(
|
||||
MessageFilename.CyberSecTest,
|
||||
"We've been watching you. Your skills are very impressive. But you're wasting " +
|
||||
"your talents. If you join us, you can put your skills to good use and change " +
|
||||
"the world for the better. If you join us, we can unlock your full potential. \n\n" +
|
||||
@ -179,8 +159,8 @@ const Messages: Record<MessageFilenames, Message> = {
|
||||
`-${FactionNames.CyberSec}`,
|
||||
),
|
||||
|
||||
[MessageFilenames.NiteSecTest]: new Message(
|
||||
MessageFilenames.NiteSecTest,
|
||||
[MessageFilename.NiteSecTest]: new Message(
|
||||
MessageFilename.NiteSecTest,
|
||||
"People say that the corrupted governments and corporations rule the world. " +
|
||||
"Yes, maybe they do. But do you know who everyone really fears? People " +
|
||||
"like us. Because they can't hide from us. Because they can't fight shadows " +
|
||||
@ -190,8 +170,8 @@ const Messages: Record<MessageFilenames, Message> = {
|
||||
`\n\n-${FactionNames.NiteSec}`,
|
||||
),
|
||||
|
||||
[MessageFilenames.BitRunnersTest]: new Message(
|
||||
MessageFilenames.BitRunnersTest,
|
||||
[MessageFilename.BitRunnersTest]: new Message(
|
||||
MessageFilename.BitRunnersTest,
|
||||
"We know what you are doing. We know what drives you. We know " +
|
||||
"what you are looking for. \n\n " +
|
||||
"We can help you find the answers.\n\n" +
|
||||
@ -199,8 +179,8 @@ const Messages: Record<MessageFilenames, Message> = {
|
||||
),
|
||||
|
||||
//Messages to guide players to the daemon
|
||||
[MessageFilenames.TruthGazer]: new Message(
|
||||
MessageFilenames.TruthGazer,
|
||||
[MessageFilename.TruthGazer]: new Message(
|
||||
MessageFilename.TruthGazer,
|
||||
//"THE TRUTH CAN NO LONGER ESCAPE YOUR GAZE"
|
||||
"@&*($#@&__TH3__#@A&#@*)__TRU1H__(*)&*)($#@&()E&R)W&\n" +
|
||||
"%@*$^$()@&$)$*@__CAN__()(@^#)@&@)#__N0__(#@&#)@&@&(\n" +
|
||||
@ -208,8 +188,8 @@ const Messages: Record<MessageFilenames, Message> = {
|
||||
"()@)#$*%)$#()$#__Y0UR__(*)$#()%(&(%)*!)($__GAZ3__#(",
|
||||
),
|
||||
|
||||
[MessageFilenames.RedPill]: new Message(
|
||||
MessageFilenames.RedPill,
|
||||
[MessageFilename.RedPill]: new Message(
|
||||
MessageFilename.RedPill,
|
||||
//"FIND THE-CAVE"
|
||||
"@)(#V%*N)@(#*)*C)@#%*)*V)@#(*%V@)(#VN%*)@#(*%\n" +
|
||||
")@B(*#%)@)M#B*%V)____FIND___#$@)#%(B*)@#(*%B)\n" +
|
||||
|
@ -17,6 +17,7 @@ import { BaseServer } from "../Server/BaseServer";
|
||||
import { ScriptDeath } from "./ScriptDeath";
|
||||
import { ScriptArg } from "./ScriptArg";
|
||||
import { NSFull } from "../NetscriptFunctions";
|
||||
import { ScriptFilePath } from "src/Paths/ScriptFilePath";
|
||||
|
||||
export class WorkerScript {
|
||||
/** Script's arguments */
|
||||
@ -60,7 +61,7 @@ export class WorkerScript {
|
||||
loadedFns: Record<string, boolean> = {};
|
||||
|
||||
/** Filename of script */
|
||||
name: string;
|
||||
name: ScriptFilePath;
|
||||
|
||||
/** Script's output/return value. Currently not used or implemented */
|
||||
output = "";
|
||||
@ -132,14 +133,6 @@ export class WorkerScript {
|
||||
return script;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the script with the specified filename on the specified server,
|
||||
* or null if it cannot be found
|
||||
*/
|
||||
getScriptOnServer(fn: string, server: BaseServer): Script | null {
|
||||
return server.scripts.get(fn) ?? null;
|
||||
}
|
||||
|
||||
shouldLog(fn: string): boolean {
|
||||
return this.disableLogs[fn] == null;
|
||||
}
|
||||
|
@ -14,9 +14,7 @@ import {
|
||||
import { netscriptCanGrow, netscriptCanWeaken } from "./Hacking/netscriptCanHack";
|
||||
import { Terminal } from "./Terminal";
|
||||
import { Player } from "@player";
|
||||
import { Programs } from "./Programs/Programs";
|
||||
import { Script } from "./Script/Script";
|
||||
import { isScriptFilename } from "./Script/isScriptFilename";
|
||||
import { CompletedProgramName } from "./Programs/Programs";
|
||||
import { PromptEvent } from "./ui/React/PromptManager";
|
||||
import { GetServer, DeleteServer, AddToAllServers, createUniqueRandomIp } from "./Server/AllServers";
|
||||
import {
|
||||
@ -36,8 +34,6 @@ import {
|
||||
} from "./Server/ServerPurchases";
|
||||
import { Server } from "./Server/Server";
|
||||
import { influenceStockThroughServerGrow } from "./StockMarket/PlayerInfluencing";
|
||||
import { isValidFilePath, removeLeadingSlash } from "./Terminal/DirectoryHelpers";
|
||||
import { TextFile, getTextFile, createTextFile } from "./TextFile";
|
||||
import { runScriptFromScript } from "./NetscriptWorker";
|
||||
import { killWorkerScript } from "./Netscript/killWorkerScript";
|
||||
import { workerScripts } from "./Netscript/WorkerScripts";
|
||||
@ -56,7 +52,6 @@ import {
|
||||
import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions";
|
||||
import { LogBoxEvents, LogBoxCloserEvents, LogBoxPositionEvents, LogBoxSizeEvents } from "./ui/React/LogBoxManager";
|
||||
import { arrayToString } from "./utils/helpers/arrayToString";
|
||||
import { isString } from "./utils/helpers/isString";
|
||||
import { NetscriptGang } from "./NetscriptFunctions/Gang";
|
||||
import { NetscriptSleeve } from "./NetscriptFunctions/Sleeve";
|
||||
import { NetscriptExtra } from "./NetscriptFunctions/Extra";
|
||||
@ -91,6 +86,11 @@ import { cloneDeep } from "lodash";
|
||||
import { FactionWorkType } from "./Enums";
|
||||
import numeral from "numeral";
|
||||
import { clearPort, peekPort, portHandle, readPort, tryWritePort, writePort } from "./NetscriptPort";
|
||||
import { FilePath, resolveFilePath } from "./Paths/FilePath";
|
||||
import { hasScriptExtension, resolveScriptFilePath } from "./Paths/ScriptFilePath";
|
||||
import { hasTextExtension } from "./Paths/TextFilePath";
|
||||
import { ContentFilePath } from "./Paths/ContentFile";
|
||||
import { LiteratureName } from "./Literature/data/LiteratureNames";
|
||||
|
||||
export const enums: NSEnums = {
|
||||
CityName,
|
||||
@ -446,22 +446,22 @@ export const ns: InternalAPI<NSFull> = {
|
||||
}
|
||||
const str = helpers.argsToString(args);
|
||||
if (str.startsWith("ERROR") || str.startsWith("FAIL")) {
|
||||
Terminal.error(`${ctx.workerScript.scriptRef.filename}: ${str}`);
|
||||
Terminal.error(`${ctx.workerScript.name}: ${str}`);
|
||||
return;
|
||||
}
|
||||
if (str.startsWith("SUCCESS")) {
|
||||
Terminal.success(`${ctx.workerScript.scriptRef.filename}: ${str}`);
|
||||
Terminal.success(`${ctx.workerScript.name}: ${str}`);
|
||||
return;
|
||||
}
|
||||
if (str.startsWith("WARN")) {
|
||||
Terminal.warn(`${ctx.workerScript.scriptRef.filename}: ${str}`);
|
||||
Terminal.warn(`${ctx.workerScript.name}: ${str}`);
|
||||
return;
|
||||
}
|
||||
if (str.startsWith("INFO")) {
|
||||
Terminal.info(`${ctx.workerScript.scriptRef.filename}: ${str}`);
|
||||
Terminal.info(`${ctx.workerScript.name}: ${str}`);
|
||||
return;
|
||||
}
|
||||
Terminal.print(`${ctx.workerScript.scriptRef.filename}: ${str}`);
|
||||
Terminal.print(`${ctx.workerScript.name}: ${str}`);
|
||||
},
|
||||
tprintf:
|
||||
(ctx) =>
|
||||
@ -583,7 +583,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
helpers.log(ctx, () => `Already have root access to '${server.hostname}'.`);
|
||||
return true;
|
||||
}
|
||||
if (!Player.hasProgram(Programs.NukeProgram.name)) {
|
||||
if (!Player.hasProgram(CompletedProgramName.nuke)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the NUKE.exe virus!");
|
||||
}
|
||||
if (server.openPortCount < server.numOpenPortsRequired) {
|
||||
@ -600,7 +600,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
helpers.log(ctx, () => "Cannot be executed on this server.");
|
||||
return false;
|
||||
}
|
||||
if (!Player.hasProgram(Programs.BruteSSHProgram.name)) {
|
||||
if (!Player.hasProgram(CompletedProgramName.bruteSsh)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the BruteSSH.exe program!");
|
||||
}
|
||||
if (!server.sshPortOpen) {
|
||||
@ -619,7 +619,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
helpers.log(ctx, () => "Cannot be executed on this server.");
|
||||
return false;
|
||||
}
|
||||
if (!Player.hasProgram(Programs.FTPCrackProgram.name)) {
|
||||
if (!Player.hasProgram(CompletedProgramName.ftpCrack)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the FTPCrack.exe program!");
|
||||
}
|
||||
if (!server.ftpPortOpen) {
|
||||
@ -638,7 +638,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
helpers.log(ctx, () => "Cannot be executed on this server.");
|
||||
return false;
|
||||
}
|
||||
if (!Player.hasProgram(Programs.RelaySMTPProgram.name)) {
|
||||
if (!Player.hasProgram(CompletedProgramName.relaySmtp)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the relaySMTP.exe program!");
|
||||
}
|
||||
if (!server.smtpPortOpen) {
|
||||
@ -657,7 +657,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
helpers.log(ctx, () => "Cannot be executed on this server.");
|
||||
return false;
|
||||
}
|
||||
if (!Player.hasProgram(Programs.HTTPWormProgram.name)) {
|
||||
if (!Player.hasProgram(CompletedProgramName.httpWorm)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the HTTPWorm.exe program!");
|
||||
}
|
||||
if (!server.httpPortOpen) {
|
||||
@ -676,7 +676,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
helpers.log(ctx, () => "Cannot be executed on this server.");
|
||||
return false;
|
||||
}
|
||||
if (!Player.hasProgram(Programs.SQLInjectProgram.name)) {
|
||||
if (!Player.hasProgram(CompletedProgramName.sqlInject)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the SQLInject.exe program!");
|
||||
}
|
||||
if (!server.sqlPortOpen) {
|
||||
@ -691,30 +691,30 @@ export const ns: InternalAPI<NSFull> = {
|
||||
run:
|
||||
(ctx) =>
|
||||
(_scriptname, _thread_or_opt = 1, ..._args) => {
|
||||
const scriptname = helpers.string(ctx, "scriptname", _scriptname);
|
||||
const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name);
|
||||
if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `${path} is not a valid script file path.`);
|
||||
const runOpts = helpers.runOptions(ctx, _thread_or_opt);
|
||||
const args = helpers.scriptArgs(ctx, _args);
|
||||
const scriptServer = GetServer(ctx.workerScript.hostname);
|
||||
if (scriptServer == null) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Could not find server. This is a bug. Report to dev.");
|
||||
}
|
||||
const scriptServer = ctx.workerScript.getServer();
|
||||
|
||||
return runScriptFromScript("run", scriptServer, scriptname, args, ctx.workerScript, runOpts);
|
||||
return runScriptFromScript("run", scriptServer, path, args, ctx.workerScript, runOpts);
|
||||
},
|
||||
exec:
|
||||
(ctx) =>
|
||||
(_scriptname, _hostname, _thread_or_opt = 1, ..._args) => {
|
||||
const scriptname = helpers.string(ctx, "scriptname", _scriptname);
|
||||
const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name);
|
||||
if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `${path} is not a valid script file path.`);
|
||||
const hostname = helpers.string(ctx, "hostname", _hostname);
|
||||
const runOpts = helpers.runOptions(ctx, _thread_or_opt);
|
||||
const args = helpers.scriptArgs(ctx, _args);
|
||||
const server = helpers.getServer(ctx, hostname);
|
||||
return runScriptFromScript("exec", server, scriptname, args, ctx.workerScript, runOpts);
|
||||
return runScriptFromScript("exec", server, path, args, ctx.workerScript, runOpts);
|
||||
},
|
||||
spawn:
|
||||
(ctx) =>
|
||||
(_scriptname, _thread_or_opt = 1, ..._args) => {
|
||||
const scriptname = helpers.string(ctx, "scriptname", _scriptname);
|
||||
const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name);
|
||||
if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `${path} is not a valid script file path.`);
|
||||
const runOpts = helpers.runOptions(ctx, _thread_or_opt);
|
||||
const args = helpers.scriptArgs(ctx, _args);
|
||||
const spawnDelay = 10;
|
||||
@ -724,10 +724,10 @@ export const ns: InternalAPI<NSFull> = {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Could not find server. This is a bug. Report to dev");
|
||||
}
|
||||
|
||||
return runScriptFromScript("spawn", scriptServer, scriptname, args, ctx.workerScript, runOpts);
|
||||
return runScriptFromScript("spawn", scriptServer, path, args, ctx.workerScript, runOpts);
|
||||
}, spawnDelay * 1e3);
|
||||
|
||||
helpers.log(ctx, () => `Will execute '${scriptname}' in ${spawnDelay} seconds`);
|
||||
helpers.log(ctx, () => `Will execute '${path}' in ${spawnDelay} seconds`);
|
||||
|
||||
if (killWorkerScript(ctx.workerScript)) {
|
||||
helpers.log(ctx, () => "Exiting...");
|
||||
@ -804,108 +804,72 @@ export const ns: InternalAPI<NSFull> = {
|
||||
killWorkerScript(ctx.workerScript);
|
||||
throw new ScriptDeath(ctx.workerScript);
|
||||
},
|
||||
scp:
|
||||
(ctx) =>
|
||||
(_files, _destination, _source = ctx.workerScript.hostname) => {
|
||||
const destination = helpers.string(ctx, "destination", _destination);
|
||||
const source = helpers.string(ctx, "source", _source);
|
||||
const destServer = helpers.getServer(ctx, destination);
|
||||
const sourceServ = helpers.getServer(ctx, source);
|
||||
const files = Array.isArray(_files) ? _files : [_files];
|
||||
scp: (ctx) => (_files, _destination, _source) => {
|
||||
const destination = helpers.string(ctx, "destination", _destination);
|
||||
const source = helpers.string(ctx, "source", _source ?? ctx.workerScript.hostname);
|
||||
const destServer = helpers.getServer(ctx, destination);
|
||||
const sourceServer = helpers.getServer(ctx, source);
|
||||
const files = Array.isArray(_files) ? _files : [_files];
|
||||
const lits: (FilePath & LiteratureName)[] = [];
|
||||
const contentFiles: ContentFilePath[] = [];
|
||||
//First loop through filenames to find all errors before moving anything.
|
||||
for (const file of files) {
|
||||
// Not a string
|
||||
if (typeof file !== "string") {
|
||||
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.
|
||||
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.");
|
||||
let noFailures = true;
|
||||
// --- Scripts and Text Files---
|
||||
for (const contentFilePath of contentFiles) {
|
||||
const sourceContentFile = sourceServer.getContentFile(contentFilePath);
|
||||
if (!sourceContentFile) {
|
||||
helpers.log(ctx, () => `File '${contentFilePath}' does not exist.`);
|
||||
noFailures = false;
|
||||
continue;
|
||||
}
|
||||
// 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
|
||||
if (!isValidFilePath(file)) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: '${file}'`);
|
||||
|
||||
// Invalid file type
|
||||
if (!file.endsWith(".lit") && !isScriptFilename(file) && !file.endsWith(".txt")) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Only works for scripts, .lit and .txt files.");
|
||||
}
|
||||
// --- Literature Files ---
|
||||
for (const litFilePath of lits) {
|
||||
const sourceMessage = sourceServer.messages.find((message) => message === litFilePath);
|
||||
if (!sourceMessage) {
|
||||
helpers.log(ctx, () => `File '${litFilePath}' does not exist.`);
|
||||
noFailures = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let noFailures = true;
|
||||
//ts detects files as any[] here even though we would have thrown in the above loop if it wasn't string[]
|
||||
for (let file of files as string[]) {
|
||||
// cut off the leading / for files in the root of the server; this assumes that the filename is somewhat normalized and doesn't look like `//file.js`
|
||||
if (file.startsWith("/") && file.indexOf("/", 1) === -1) file = file.slice(1);
|
||||
|
||||
// Scp for lit files
|
||||
if (file.endsWith(".lit")) {
|
||||
const sourceMessage = sourceServ.messages.find((message) => message === file);
|
||||
if (!sourceMessage) {
|
||||
helpers.log(ctx, () => `File '${file}' does not exist.`);
|
||||
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}'.`);
|
||||
const destMessage = destServer.messages.find((message) => message === litFilePath);
|
||||
if (destMessage) {
|
||||
helpers.log(ctx, () => `File '${litFilePath}' was already on '${destServer?.hostname}'.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
return noFailures;
|
||||
},
|
||||
destServer.messages.push(litFilePath);
|
||||
helpers.log(ctx, () => `File '${litFilePath}' copied over to '${destServer?.hostname}'.`);
|
||||
continue;
|
||||
}
|
||||
return noFailures;
|
||||
},
|
||||
ls: (ctx) => (_hostname, _substring) => {
|
||||
const hostname = helpers.string(ctx, "hostname", _hostname);
|
||||
const substring = helpers.string(ctx, "substring", _substring ?? "");
|
||||
@ -916,7 +880,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
...server.messages,
|
||||
...server.programs,
|
||||
...server.scripts.keys(),
|
||||
...server.textFiles.map((textFile) => textFile.filename),
|
||||
...server.textFiles.keys(),
|
||||
];
|
||||
|
||||
if (!substring) return allFilenames.sort();
|
||||
@ -1147,7 +1111,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
const filename = helpers.string(ctx, "filename", _filename);
|
||||
const hostname = helpers.string(ctx, "hostname", _hostname);
|
||||
const server = helpers.getServer(ctx, hostname);
|
||||
if (server.scripts.has(filename)) return true;
|
||||
if (server.scripts.has(filename) || server.textFiles.has(filename)) return true;
|
||||
for (let i = 0; i < server.programs.length; ++i) {
|
||||
if (filename.toLowerCase() == server.programs[i].toLowerCase()) {
|
||||
return true;
|
||||
@ -1160,8 +1124,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
}
|
||||
const contract = server.contracts.find((c) => c.fn.toLowerCase() === filename.toLowerCase());
|
||||
if (contract) return true;
|
||||
const txtFile = getTextFile(filename, server);
|
||||
return txtFile != null;
|
||||
return false;
|
||||
},
|
||||
isRunning:
|
||||
(ctx) =>
|
||||
@ -1363,43 +1326,34 @@ export const ns: InternalAPI<NSFull> = {
|
||||
return writePort(portNumber, data);
|
||||
},
|
||||
write: (ctx) => (_filename, _data, _mode) => {
|
||||
let filename = helpers.string(ctx, "handle", _filename);
|
||||
const filepath = resolveFilePath(helpers.string(ctx, "filename", _filename), ctx.workerScript.name);
|
||||
const data = helpers.string(ctx, "data", _data ?? "");
|
||||
const mode = helpers.string(ctx, "mode", _mode ?? "a");
|
||||
if (!isValidFilePath(filename)) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filepath: ${filename}`);
|
||||
|
||||
if (filename.lastIndexOf("/") === 0) filename = removeLeadingSlash(filename);
|
||||
if (!filepath) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filepath: ${filepath}`);
|
||||
|
||||
const server = helpers.getServer(ctx, ctx.workerScript.hostname);
|
||||
|
||||
if (isScriptFilename(filename)) {
|
||||
// Write to script
|
||||
let script = ctx.workerScript.getScriptOnServer(filename, server);
|
||||
if (!script) {
|
||||
// Create a new script
|
||||
script = new Script(filename, String(data), server.hostname);
|
||||
server.scripts.set(filename, script);
|
||||
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 (hasScriptExtension(filepath)) {
|
||||
if (mode === "w") {
|
||||
txtFile.write(String(data));
|
||||
} else {
|
||||
txtFile.append(String(data));
|
||||
server.writeToScriptFile(filepath, data);
|
||||
return;
|
||||
}
|
||||
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) => {
|
||||
const portNumber = helpers.portNumber(ctx, _portNumber);
|
||||
@ -1416,48 +1370,25 @@ export const ns: InternalAPI<NSFull> = {
|
||||
return readPort(portNumber);
|
||||
},
|
||||
read: (ctx) => (_filename) => {
|
||||
const fn = helpers.string(ctx, "filename", _filename);
|
||||
const server = GetServer(ctx.workerScript.hostname);
|
||||
if (server == null) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Error getting Server. This is a bug. Report to dev.");
|
||||
}
|
||||
if (isScriptFilename(fn)) {
|
||||
// Read from script
|
||||
const script = ctx.workerScript.getScriptOnServer(fn, server);
|
||||
if (script == null) {
|
||||
return "";
|
||||
}
|
||||
return script.code;
|
||||
} else {
|
||||
// Read from text file
|
||||
const txtFile = getTextFile(fn, server);
|
||||
if (txtFile !== null) {
|
||||
return txtFile.text;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
const path = resolveFilePath(helpers.string(ctx, "filename", _filename), ctx.workerScript.name);
|
||||
if (!path || (!hasScriptExtension(path) && !hasTextExtension(path))) return "";
|
||||
const server = ctx.workerScript.getServer();
|
||||
return server.getContentFile(path)?.content ?? "";
|
||||
},
|
||||
peek: (ctx) => (_portNumber) => {
|
||||
const portNumber = helpers.portNumber(ctx, _portNumber);
|
||||
return peekPort(portNumber);
|
||||
},
|
||||
clear: (ctx) => (_file) => {
|
||||
const file = helpers.string(ctx, "file", _file);
|
||||
if (isString(file)) {
|
||||
// Clear text file
|
||||
const fn = file;
|
||||
const server = GetServer(ctx.workerScript.hostname);
|
||||
if (server == null) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Error getting Server. This is a bug. Report to dev.");
|
||||
}
|
||||
const txtFile = getTextFile(fn, server);
|
||||
if (txtFile != null) {
|
||||
txtFile.write("");
|
||||
}
|
||||
} else {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid argument: ${file}`);
|
||||
const path = resolveFilePath(helpers.string(ctx, "file", _file), ctx.workerScript.name);
|
||||
if (!path || (!hasScriptExtension(path) && !hasTextExtension(path))) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid file path or extension: ${_file}`);
|
||||
}
|
||||
const server = ctx.workerScript.getServer();
|
||||
const file = server.getContentFile(path);
|
||||
if (!file) throw helpers.makeRuntimeErrorMsg(ctx, `${path} does not exist on ${server.hostname}`);
|
||||
// The content setter handles invalidating script modules where applicable.
|
||||
file.content = "";
|
||||
},
|
||||
clearPort: (ctx) => (_portNumber) => {
|
||||
const portNumber = helpers.portNumber(ctx, _portNumber);
|
||||
@ -1467,20 +1398,22 @@ export const ns: InternalAPI<NSFull> = {
|
||||
const portNumber = helpers.portNumber(ctx, _portNumber);
|
||||
return portHandle(portNumber);
|
||||
},
|
||||
rm:
|
||||
(ctx) =>
|
||||
(_fn, _hostname = ctx.workerScript.hostname) => {
|
||||
const fn = helpers.string(ctx, "fn", _fn);
|
||||
const hostname = helpers.string(ctx, "hostname", _hostname);
|
||||
const s = helpers.getServer(ctx, hostname);
|
||||
rm: (ctx) => (_fn, _hostname) => {
|
||||
const filepath = resolveFilePath(helpers.string(ctx, "fn", _fn), ctx.workerScript.name);
|
||||
const hostname = helpers.string(ctx, "hostname", _hostname ?? ctx.workerScript.hostname);
|
||||
const s = helpers.getServer(ctx, hostname);
|
||||
if (!filepath) {
|
||||
helpers.log(ctx, () => `Error while parsing filepath ${filepath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const status = s.removeFile(fn);
|
||||
if (!status.res) {
|
||||
helpers.log(ctx, () => status.msg + "");
|
||||
}
|
||||
const status = s.removeFile(filepath);
|
||||
if (!status.res) {
|
||||
helpers.log(ctx, () => status.msg + "");
|
||||
}
|
||||
|
||||
return status.res;
|
||||
},
|
||||
return status.res;
|
||||
},
|
||||
scriptRunning: (ctx) => (_scriptname, _hostname) => {
|
||||
const scriptname = helpers.string(ctx, "scriptname", _scriptname);
|
||||
const hostname = helpers.string(ctx, "hostname", _hostname);
|
||||
@ -1506,18 +1439,17 @@ export const ns: InternalAPI<NSFull> = {
|
||||
}
|
||||
return suc;
|
||||
},
|
||||
getScriptName: (ctx) => () => {
|
||||
return ctx.workerScript.name;
|
||||
},
|
||||
getScriptName: (ctx) => () => ctx.workerScript.name,
|
||||
getScriptRam: (ctx) => (_scriptname, _hostname) => {
|
||||
const scriptname = helpers.string(ctx, "scriptname", _scriptname);
|
||||
const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name);
|
||||
if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `Could not parse file path ${_scriptname}`);
|
||||
const hostname = helpers.string(ctx, "hostname", _hostname ?? ctx.workerScript.hostname);
|
||||
const server = helpers.getServer(ctx, hostname);
|
||||
const script = server.scripts.get(scriptname);
|
||||
const script = server.scripts.get(path);
|
||||
if (!script) return 0;
|
||||
const ramUsage = script.getRamUsage(server.scripts);
|
||||
if (!ramUsage) {
|
||||
helpers.log(ctx, () => `Could not calculate ram usage for ${scriptname} on ${hostname}.`);
|
||||
helpers.log(ctx, () => `Could not calculate ram usage for ${path} on ${hostname}.`);
|
||||
return 0;
|
||||
}
|
||||
return ramUsage;
|
||||
@ -1704,26 +1636,22 @@ export const ns: InternalAPI<NSFull> = {
|
||||
},
|
||||
wget: (ctx) => async (_url, _target, _hostname) => {
|
||||
const url = helpers.string(ctx, "url", _url);
|
||||
const target = helpers.string(ctx, "target", _target);
|
||||
const target = resolveFilePath(helpers.string(ctx, "target", _target), ctx.workerScript.name);
|
||||
const hostname = _hostname ? helpers.string(ctx, "hostname", _hostname) : ctx.workerScript.hostname;
|
||||
if (!isScriptFilename(target) && !target.endsWith(".txt")) {
|
||||
const server = helpers.getServer(ctx, hostname);
|
||||
if (!target || (!hasTextExtension(target) && !hasScriptExtension(target))) {
|
||||
helpers.log(ctx, () => `Invalid target file: '${target}'. Must be a script or text file.`);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const s = helpers.getServer(ctx, hostname);
|
||||
return new Promise(function (resolve) {
|
||||
$.get(
|
||||
url,
|
||||
function (data) {
|
||||
let res;
|
||||
if (isScriptFilename(target)) {
|
||||
res = s.writeToScriptFile(target, data);
|
||||
if (hasScriptExtension(target)) {
|
||||
res = server.writeToScriptFile(target, data);
|
||||
} else {
|
||||
res = s.writeToTextFile(target, data);
|
||||
}
|
||||
if (!res.success) {
|
||||
helpers.log(ctx, () => "Failed.");
|
||||
return resolve(false);
|
||||
res = server.writeToTextFile(target, data);
|
||||
}
|
||||
if (res.overwritten) {
|
||||
helpers.log(ctx, () => `Successfully retrieved content and overwrote '${target}' on '${hostname}'`);
|
||||
@ -1790,59 +1718,34 @@ export const ns: InternalAPI<NSFull> = {
|
||||
},
|
||||
mv: (ctx) => (_host, _source, _destination) => {
|
||||
const hostname = helpers.string(ctx, "host", _host);
|
||||
const source = helpers.string(ctx, "source", _source);
|
||||
const destination = helpers.string(ctx, "destination", _destination);
|
||||
const server = helpers.getServer(ctx, hostname);
|
||||
const sourcePath = resolveFilePath(helpers.string(ctx, "source", _source));
|
||||
if (!sourcePath) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid source filename: '${_source}'`);
|
||||
const destinationPath = resolveFilePath(helpers.string(ctx, "destination", _destination), sourcePath);
|
||||
if (!destinationPath) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid destination filename: '${destinationPath}'`);
|
||||
|
||||
if (!isValidFilePath(source)) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: '${source}'`);
|
||||
if (!isValidFilePath(destination)) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: '${destination}'`);
|
||||
|
||||
const source_is_txt = source.endsWith(".txt");
|
||||
const dest_is_txt = destination.endsWith(".txt");
|
||||
|
||||
if (!isScriptFilename(source) && !source_is_txt)
|
||||
if (
|
||||
(!hasTextExtension(sourcePath) && !hasScriptExtension(sourcePath)) ||
|
||||
(!hasTextExtension(destinationPath) && !hasScriptExtension(destinationPath))
|
||||
) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `'mv' can only be used on scripts and text files (.txt)`);
|
||||
if (source_is_txt != dest_is_txt)
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Source and destination files must have the same type`);
|
||||
|
||||
if (source === destination) {
|
||||
}
|
||||
if (sourcePath === destinationPath) {
|
||||
helpers.log(ctx, () => "WARNING: Did nothing, source and destination paths were the same.");
|
||||
return;
|
||||
}
|
||||
|
||||
const server = helpers.getServer(ctx, hostname);
|
||||
|
||||
if (!source_is_txt && server.isRunning(source))
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Cannot use 'mv' on a script that is running`);
|
||||
|
||||
interface File {
|
||||
filename: string;
|
||||
const sourceContentFile = server.getContentFile(sourcePath);
|
||||
if (!sourceContentFile) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Source text file ${sourcePath} does not exist on ${hostname}`);
|
||||
}
|
||||
let source_file: File | undefined;
|
||||
let dest_file: File | undefined;
|
||||
|
||||
if (source_is_txt) {
|
||||
// Traverses twice potentially. Inefficient but will soon be replaced with a map.
|
||||
source_file = server.textFiles.find((textFile) => textFile.filename === source);
|
||||
dest_file = server.textFiles.find((textFile) => textFile.filename === destination);
|
||||
} else {
|
||||
source_file = server.scripts.get(source);
|
||||
dest_file = server.scripts.get(destination);
|
||||
}
|
||||
if (!source_file) throw helpers.makeRuntimeErrorMsg(ctx, `Source file ${source} does not exist`);
|
||||
|
||||
if (dest_file) {
|
||||
if (dest_file instanceof TextFile && source_file instanceof TextFile) {
|
||||
dest_file.text = source_file.text;
|
||||
} else if (dest_file instanceof Script && source_file instanceof Script) {
|
||||
dest_file.code = source_file.code;
|
||||
// Source needs to be invalidated as well, to invalidate its dependents
|
||||
source_file.invalidateModule();
|
||||
dest_file.invalidateModule();
|
||||
}
|
||||
server.removeFile(source);
|
||||
} else {
|
||||
source_file.filename = destination;
|
||||
if (source_file instanceof Script) source_file.invalidateModule();
|
||||
const success = sourceContentFile.deleteFromServer(server);
|
||||
if (success) {
|
||||
const { overwritten } = server.writeToContentFile(destinationPath, sourceContentFile.content);
|
||||
if (overwritten) helpers.log(ctx, () => `WARNING: Overwriting file ${destinationPath} on ${hostname}`);
|
||||
helpers.log(ctx, () => `Moved ${sourcePath} to ${destinationPath} on ${hostname}`);
|
||||
return;
|
||||
}
|
||||
helpers.log(ctx, () => `ERROR: Failed. Was unable to remove file ${sourcePath} from its original location.`);
|
||||
},
|
||||
flags: Flags,
|
||||
...NetscriptExtra(),
|
||||
|
@ -26,7 +26,7 @@ import {
|
||||
calculateGrowTime,
|
||||
calculateWeakenTime,
|
||||
} from "../Hacking";
|
||||
import { Programs } from "../Programs/Programs";
|
||||
import { CompletedProgramName } from "../Programs/Programs";
|
||||
import { Formulas as IFormulas, Player as IPlayer, Person as IPerson } from "@nsdefs";
|
||||
import {
|
||||
calculateRespectGain,
|
||||
@ -55,7 +55,7 @@ import { findCrime } from "../Crime/CrimeHelpers";
|
||||
|
||||
export function NetscriptFormulas(): InternalAPI<IFormulas> {
|
||||
const checkFormulasAccess = function (ctx: NetscriptContext): void {
|
||||
if (!player.hasProgram(Programs.Formulas.name)) {
|
||||
if (!player.hasProgram(CompletedProgramName.formulas)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Requires Formulas.exe to run.`);
|
||||
}
|
||||
};
|
||||
|
@ -6,7 +6,6 @@ import { StaticAugmentations } from "../Augmentation/StaticAugmentations";
|
||||
import { augmentationExists, installAugmentations } from "../Augmentation/AugmentationHelpers";
|
||||
import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
|
||||
import { CONSTANTS } from "../Constants";
|
||||
import { isString } from "../utils/helpers/isString";
|
||||
import { RunningScript } from "../Script/RunningScript";
|
||||
import { calculateAchievements } from "../Achievements/Achievements";
|
||||
|
||||
@ -52,6 +51,8 @@ import { calculateCrimeWorkStats } from "../Work/Formulas";
|
||||
import { findEnumMember } from "../utils/helpers/enum";
|
||||
import { Engine } from "../engine";
|
||||
import { checkEnum } from "../utils/helpers/enum";
|
||||
import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath";
|
||||
import { root } from "../Paths/Directory";
|
||||
|
||||
export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
const getAugmentation = function (ctx: NetscriptContext, name: string): Augmentation {
|
||||
@ -76,7 +77,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
return company;
|
||||
};
|
||||
|
||||
const runAfterReset = function (cbScript: string | null = null) {
|
||||
const runAfterReset = function (cbScript: ScriptFilePath) {
|
||||
//Run a script after reset
|
||||
if (!cbScript) return;
|
||||
const home = Player.getHomeComputer();
|
||||
@ -87,7 +88,9 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
return Terminal.error(`Attempted to launch ${cbScript} after reset but could not calculate ram usage.`);
|
||||
}
|
||||
const ramAvailable = home.maxRam - home.ramUsed;
|
||||
if (ramUsage > ramAvailable + 0.001) return;
|
||||
if (ramUsage > ramAvailable + 0.001) {
|
||||
return Terminal.error(`Attempted to launch ${cbScript} after reset but there was not enough ram.`);
|
||||
}
|
||||
// Start script with no args and 1 thread (default).
|
||||
const runningScriptObj = new RunningScript(script, ramUsage, []);
|
||||
startWorkerScript(runningScriptObj, home);
|
||||
@ -190,7 +193,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
|
||||
const res = purchaseAugmentation(aug, fac, true);
|
||||
helpers.log(ctx, () => res);
|
||||
if (isString(res) && res.startsWith("You purchased")) {
|
||||
if (res.startsWith("You purchased")) {
|
||||
Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain * 10);
|
||||
return true;
|
||||
} else {
|
||||
@ -199,7 +202,10 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
},
|
||||
softReset: (ctx) => (_cbScript) => {
|
||||
helpers.checkSingularityAccess(ctx);
|
||||
const cbScript = _cbScript ? helpers.string(ctx, "cbScript", _cbScript) : "";
|
||||
const cbScript = _cbScript
|
||||
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
||||
: false;
|
||||
if (cbScript === null) throw helpers.makeRuntimeErrorMsg(ctx, `Could not resolve file path: ${_cbScript}`);
|
||||
|
||||
helpers.log(ctx, () => "Soft resetting. This will cause this script to be killed");
|
||||
installAugmentations(true);
|
||||
@ -207,7 +213,10 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
},
|
||||
installAugmentations: (ctx) => (_cbScript) => {
|
||||
helpers.checkSingularityAccess(ctx);
|
||||
const cbScript = _cbScript ? helpers.string(ctx, "cbScript", _cbScript) : "";
|
||||
const cbScript = _cbScript
|
||||
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
||||
: false;
|
||||
if (cbScript === null) throw helpers.makeRuntimeErrorMsg(ctx, `Could not resolve file path: ${_cbScript}`);
|
||||
|
||||
if (Player.queuedAugmentations.length === 0) {
|
||||
helpers.log(ctx, () => "You do not have any Augmentations to be installed.");
|
||||
@ -502,7 +511,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
Player.getCurrentServer().isConnectedTo = false;
|
||||
Player.currentServer = Player.getHomeComputer().hostname;
|
||||
Player.getCurrentServer().isConnectedTo = true;
|
||||
Terminal.setcwd("/");
|
||||
Terminal.setcwd(root);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -515,7 +524,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
Player.getCurrentServer().isConnectedTo = false;
|
||||
Player.currentServer = target.hostname;
|
||||
Player.getCurrentServer().isConnectedTo = true;
|
||||
Terminal.setcwd("/");
|
||||
Terminal.setcwd(root);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -526,7 +535,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
Player.getCurrentServer().isConnectedTo = false;
|
||||
Player.currentServer = target.hostname;
|
||||
Player.getCurrentServer().isConnectedTo = true;
|
||||
Terminal.setcwd("/");
|
||||
Terminal.setcwd(root);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1189,54 +1198,49 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
}
|
||||
return item.price;
|
||||
},
|
||||
b1tflum3:
|
||||
(ctx) =>
|
||||
(_nextBN, _callbackScript = "") => {
|
||||
helpers.checkSingularityAccess(ctx);
|
||||
const nextBN = helpers.number(ctx, "nextBN", _nextBN);
|
||||
const callbackScript = helpers.string(ctx, "callbackScript", _callbackScript);
|
||||
helpers.checkSingularityAccess(ctx);
|
||||
enterBitNode(true, Player.bitNodeN, nextBN);
|
||||
if (callbackScript)
|
||||
setTimeout(() => {
|
||||
runAfterReset(callbackScript);
|
||||
}, 0);
|
||||
},
|
||||
destroyW0r1dD43m0n:
|
||||
(ctx) =>
|
||||
(_nextBN, _callbackScript = "") => {
|
||||
helpers.checkSingularityAccess(ctx);
|
||||
const nextBN = helpers.number(ctx, "nextBN", _nextBN);
|
||||
if (nextBN > 13 || nextBN < 1 || !Number.isInteger(nextBN)) {
|
||||
throw new Error(`Invalid bitnode specified: ${_nextBN}`);
|
||||
}
|
||||
const callbackScript = helpers.string(ctx, "callbackScript", _callbackScript);
|
||||
b1tflum3: (ctx) => (_nextBN, _cbScript) => {
|
||||
helpers.checkSingularityAccess(ctx);
|
||||
const nextBN = helpers.number(ctx, "nextBN", _nextBN);
|
||||
const cbScript = _cbScript
|
||||
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
||||
: false;
|
||||
if (cbScript === null) throw helpers.makeRuntimeErrorMsg(ctx, `Could not resolve file path: ${_cbScript}`);
|
||||
enterBitNode(true, Player.bitNodeN, nextBN);
|
||||
if (cbScript) setTimeout(() => runAfterReset(cbScript), 0);
|
||||
},
|
||||
destroyW0r1dD43m0n: (ctx) => (_nextBN, _cbScript) => {
|
||||
helpers.checkSingularityAccess(ctx);
|
||||
const nextBN = helpers.number(ctx, "nextBN", _nextBN);
|
||||
if (nextBN > 13 || nextBN < 1 || !Number.isInteger(nextBN)) {
|
||||
throw new Error(`Invalid bitnode specified: ${_nextBN}`);
|
||||
}
|
||||
const cbScript = _cbScript
|
||||
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
||||
: false;
|
||||
if (cbScript === null) throw helpers.makeRuntimeErrorMsg(ctx, `Could not resolve file path: ${_cbScript}`);
|
||||
|
||||
const wd = GetServer(SpecialServers.WorldDaemon);
|
||||
if (!(wd instanceof Server)) throw new Error("WorldDaemon was not a normal server. This is a bug contact dev.");
|
||||
const hackingRequirements = () => {
|
||||
if (Player.skills.hacking < wd.requiredHackingSkill) return false;
|
||||
if (!wd.hasAdminRights) return false;
|
||||
return true;
|
||||
};
|
||||
const bladeburnerRequirements = () => {
|
||||
if (!Player.bladeburner) return false;
|
||||
return Player.bladeburner.blackops[BlackOperationNames.OperationDaedalus];
|
||||
};
|
||||
const wd = GetServer(SpecialServers.WorldDaemon);
|
||||
if (!(wd instanceof Server)) throw new Error("WorldDaemon was not a normal server. This is a bug contact dev.");
|
||||
const hackingRequirements = () => {
|
||||
if (Player.skills.hacking < wd.requiredHackingSkill) return false;
|
||||
if (!wd.hasAdminRights) return false;
|
||||
return true;
|
||||
};
|
||||
const bladeburnerRequirements = () => {
|
||||
if (!Player.bladeburner) return false;
|
||||
return Player.bladeburner.blackops[BlackOperationNames.OperationDaedalus];
|
||||
};
|
||||
|
||||
if (!hackingRequirements() && !bladeburnerRequirements()) {
|
||||
helpers.log(ctx, () => "Requirements not met to destroy the world daemon");
|
||||
return;
|
||||
}
|
||||
if (!hackingRequirements() && !bladeburnerRequirements()) {
|
||||
helpers.log(ctx, () => "Requirements not met to destroy the world daemon");
|
||||
return;
|
||||
}
|
||||
|
||||
wd.backdoorInstalled = true;
|
||||
calculateAchievements();
|
||||
enterBitNode(false, Player.bitNodeN, nextBN);
|
||||
if (callbackScript)
|
||||
setTimeout(() => {
|
||||
runAfterReset(callbackScript);
|
||||
}, 0);
|
||||
},
|
||||
wd.backdoorInstalled = true;
|
||||
calculateAchievements();
|
||||
enterBitNode(false, Player.bitNodeN, nextBN);
|
||||
if (cbScript) setTimeout(() => runAfterReset(cbScript), 0);
|
||||
},
|
||||
getCurrentWork: () => () => {
|
||||
if (!Player.currentWork) return null;
|
||||
return Player.currentWork.APICopy();
|
||||
|
@ -7,8 +7,8 @@ import { parse } from "acorn";
|
||||
|
||||
import { LoadedModule, ScriptURL, ScriptModule } from "./Script/LoadedModule";
|
||||
import { Script } from "./Script/Script";
|
||||
import { removeLeadingSlash } from "./Terminal/DirectoryHelpers";
|
||||
import { ScriptFilename, scriptFilenameFromImport } from "./Types/strings";
|
||||
import { ScriptFilePath, resolveScriptFilePath } from "./Paths/ScriptFilePath";
|
||||
import { root } from "./Paths/Directory";
|
||||
|
||||
// Acorn type def is straight up incomplete so we have to fill with our own.
|
||||
export type Node = any;
|
||||
@ -47,7 +47,7 @@ const cleanup = new FinalizationRegistry((mapKey: string) => {
|
||||
}
|
||||
});
|
||||
|
||||
export function compile(script: Script, scripts: Map<ScriptFilename, Script>): Promise<ScriptModule> {
|
||||
export function compile(script: Script, scripts: Map<ScriptFilePath, Script>): Promise<ScriptModule> {
|
||||
// Return the module if it already exists
|
||||
if (script.mod) return script.mod.module;
|
||||
|
||||
@ -76,7 +76,7 @@ function addDependencyInfo(script: Script, seenStack: Script[]) {
|
||||
* @param scripts array of other scripts on the server
|
||||
* @param seenStack A stack of scripts that were higher up in the import tree in a recursive call.
|
||||
*/
|
||||
function generateLoadedModule(script: Script, scripts: Map<ScriptFilename, Script>, seenStack: Script[]): LoadedModule {
|
||||
function generateLoadedModule(script: Script, scripts: Map<ScriptFilePath, Script>, seenStack: Script[]): LoadedModule {
|
||||
// Early return for recursive calls where the script already has a URL
|
||||
if (script.mod) {
|
||||
addDependencyInfo(script, seenStack);
|
||||
@ -122,10 +122,11 @@ function generateLoadedModule(script: Script, scripts: Map<ScriptFilename, Scrip
|
||||
// Sort the nodes from last start index to first. This replaces the last import with a blob first,
|
||||
// preventing the ranges for other imports from being shifted.
|
||||
importNodes.sort((a, b) => b.start - a.start);
|
||||
let newCode = script.code;
|
||||
let newCode = script.code as string;
|
||||
// Loop through each node and replace the script name with a blob url.
|
||||
for (const node of importNodes) {
|
||||
const filename = scriptFilenameFromImport(node.filename);
|
||||
const filename = resolveScriptFilePath(node.filename, root, ".js");
|
||||
if (!filename) throw new Error(`Failed to parse import: ${node.filename}`);
|
||||
|
||||
// Find the corresponding script.
|
||||
const importedScript = scripts.get(filename);
|
||||
@ -146,7 +147,7 @@ function generateLoadedModule(script: Script, scripts: Map<ScriptFilename, Scrip
|
||||
// servers; it will be listed under the first server it was compiled for.
|
||||
// We don't include this in the cache key, so that other instances of the
|
||||
// script dedupe properly.
|
||||
const adjustedCode = newCode + `\n//# sourceURL=${script.server}/${removeLeadingSlash(script.filename)}`;
|
||||
const adjustedCode = newCode + `\n//# sourceURL=${script.server}/${script.filename}`;
|
||||
// At this point we have the full code and can construct a new blob / assign the URL.
|
||||
const url = URL.createObjectURL(makeScriptBlob(adjustedCode)) as ScriptURL;
|
||||
const module = config.doImport(url).catch((e) => {
|
||||
|
@ -32,7 +32,8 @@ import { simple as walksimple } from "acorn-walk";
|
||||
import { Terminal } from "./Terminal";
|
||||
import { ScriptArg } from "@nsdefs";
|
||||
import { handleUnknownError, CompleteRunOptions } from "./Netscript/NetscriptHelpers";
|
||||
import { scriptFilenameFromImport } from "./Types/strings";
|
||||
import { resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath";
|
||||
import { root } from "./Paths/Directory";
|
||||
|
||||
export const NetscriptPorts: Map<PortNumber, Port> = new Map();
|
||||
|
||||
@ -146,7 +147,7 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c
|
||||
throw new Error("Failed to find underlying Server object for script");
|
||||
}
|
||||
|
||||
function getScript(scriptName: string): Script | null {
|
||||
function getScript(scriptName: ScriptFilePath): Script | null {
|
||||
return server.scripts.get(scriptName) ?? null;
|
||||
}
|
||||
|
||||
@ -157,11 +158,10 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c
|
||||
walksimple(ast, {
|
||||
ImportDeclaration: (node: Node) => {
|
||||
hasImports = true;
|
||||
const scriptName = scriptFilenameFromImport(node.source.value, true);
|
||||
const scriptName = resolveScriptFilePath(node.source.value, root, ".script");
|
||||
if (!scriptName) throw new Error("'Import' failed due to invalid path: " + scriptName);
|
||||
const script = getScript(scriptName);
|
||||
if (script == null) {
|
||||
throw new Error("'Import' failed due to invalid script: " + scriptName);
|
||||
}
|
||||
if (!script) throw new Error("'Import' failed due to script not found: " + scriptName);
|
||||
const scriptAst = parse(script.code, {
|
||||
ecmaVersion: 9,
|
||||
allowReserved: true,
|
||||
@ -380,16 +380,11 @@ export function loadAllRunningScripts(): void {
|
||||
export function runScriptFromScript(
|
||||
caller: string,
|
||||
host: BaseServer,
|
||||
scriptname: string,
|
||||
scriptname: ScriptFilePath,
|
||||
args: ScriptArg[],
|
||||
workerScript: WorkerScript,
|
||||
runOpts: CompleteRunOptions,
|
||||
): number {
|
||||
/* Very inefficient, TODO change data structures so that finding script & checking if it's already running does
|
||||
* not require iterating through all scripts and comparing names/args. This is a big part of slowdown when
|
||||
* running a large number of scripts. */
|
||||
|
||||
// Find the script, fail if it doesn't exist.
|
||||
const script = host.scripts.get(scriptname);
|
||||
if (!script) {
|
||||
workerScript.log(caller, () => `Could not find script '${scriptname}' on '${host.hostname}'`);
|
||||
|
19
src/Paths/ContentFile.ts
Normal file
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;
|
||||
}
|
17
src/Paths/ContractFilePath.ts
Normal file
17
src/Paths/ContractFilePath.ts
Normal file
@ -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
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
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
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;
|
||||
}
|
24
src/Paths/ProgramFilePath.ts
Normal file
24
src/Paths/ProgramFilePath.ts
Normal file
@ -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.`);
|
||||
}
|
30
src/Paths/ScriptFilePath.ts
Normal file
30
src/Paths/ScriptFilePath.ts
Normal file
@ -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
17
src/Paths/TextFilePath.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Directory } from "./Directory";
|
||||
import { FilePath, resolveFilePath } from "./FilePath";
|
||||
|
||||
/** Filepath with the additional constraint of having a .js extension */
|
||||
type WithTextExtension = string & { __fileType: "Text" };
|
||||
export type TextFilePath = FilePath & WithTextExtension;
|
||||
|
||||
/** Check extension only */
|
||||
export function hasTextExtension(path: string): path is WithTextExtension {
|
||||
return path.endsWith(".txt");
|
||||
}
|
||||
|
||||
/** Sanitize a player input, resolve any relative paths, and for imports add the correct extension if missing */
|
||||
export function resolveTextFilePath(path: string, base = "" as FilePath | Directory): TextFilePath | null {
|
||||
const result = resolveFilePath(path, base);
|
||||
return result && hasTextExtension(result) ? result : null;
|
||||
}
|
@ -11,7 +11,7 @@ import { CompanyPositions } from "../../Company/CompanyPositions";
|
||||
import { CompanyPosition } from "../../Company/CompanyPosition";
|
||||
import * as posNames from "../../Company/data/JobTracks";
|
||||
import { CONSTANTS } from "../../Constants";
|
||||
import { Programs } from "../../Programs/Programs";
|
||||
import { CompletedProgramName } from "../../Programs/Programs";
|
||||
import { Exploit } from "../../Exploits/Exploit";
|
||||
import { Faction } from "../../Faction/Faction";
|
||||
import { Factions } from "../../Faction/Factions";
|
||||
@ -45,6 +45,7 @@ import { isCompanyWork } from "../../Work/CompanyWork";
|
||||
import { serverMetadata } from "../../Server/data/servers";
|
||||
|
||||
import type { PlayerObject } from "./PlayerObject";
|
||||
import { ProgramFilePath } from "src/Paths/ProgramFilePath";
|
||||
|
||||
export function init(this: PlayerObject): void {
|
||||
/* Initialize Player's home computer */
|
||||
@ -60,7 +61,7 @@ export function init(this: PlayerObject): void {
|
||||
this.currentServer = SpecialServers.Home;
|
||||
AddToAllServers(t_homeComp);
|
||||
|
||||
this.getHomeComputer().programs.push(Programs.NukeProgram.name);
|
||||
this.getHomeComputer().programs.push(CompletedProgramName.nuke);
|
||||
}
|
||||
|
||||
export function prestigeAugmentation(this: PlayerObject): void {
|
||||
@ -171,18 +172,9 @@ export function calculateSkillProgress(this: PlayerObject, exp: number, mult = 1
|
||||
return calculateSkillProgressF(exp, mult);
|
||||
}
|
||||
|
||||
export function hasProgram(this: PlayerObject, programName: string): boolean {
|
||||
export function hasProgram(this: PlayerObject, programName: CompletedProgramName | ProgramFilePath): boolean {
|
||||
const home = this.getHomeComputer();
|
||||
if (home == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < home.programs.length; ++i) {
|
||||
if (programName.toLowerCase() == home.programs[i].toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return home.programs.includes(programName);
|
||||
}
|
||||
|
||||
export function setMoney(this: PlayerObject, money: number): void {
|
||||
|
@ -6,7 +6,7 @@ import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
|
||||
import { initBitNodeMultipliers } from "./BitNode/BitNode";
|
||||
import { Companies, initCompanies } from "./Company/Companies";
|
||||
import { resetIndustryResearchTrees } from "./Corporation/IndustryData";
|
||||
import { Programs } from "./Programs/Programs";
|
||||
import { CompletedProgramName } from "./Programs/Programs";
|
||||
import { Factions, initFactions } from "./Faction/Factions";
|
||||
import { joinFaction } from "./Faction/FactionHelpers";
|
||||
import { updateHashManagerCapacity } from "./Hacknet/HacknetHelpers";
|
||||
@ -14,7 +14,7 @@ import { prestigeWorkerScripts } from "./NetscriptWorker";
|
||||
import { Player } from "@player";
|
||||
import { recentScripts } from "./Netscript/RecentScripts";
|
||||
import { resetPidCounter } from "./Netscript/Pid";
|
||||
import { LiteratureNames } from "./Literature/data/LiteratureNames";
|
||||
import { LiteratureName } from "./Literature/data/LiteratureNames";
|
||||
|
||||
import { GetServer, AddToAllServers, initForeignServers, prestigeAllServers } from "./Server/AllServers";
|
||||
import { prestigeHomeComputer } from "./Server/ServerHelpers";
|
||||
@ -53,20 +53,20 @@ export function prestigeAugmentation(): void {
|
||||
prestigeHomeComputer(homeComp);
|
||||
|
||||
if (augmentationExists(AugmentationNames.Neurolink) && Player.hasAugmentation(AugmentationNames.Neurolink, true)) {
|
||||
homeComp.programs.push(Programs.FTPCrackProgram.name);
|
||||
homeComp.programs.push(Programs.RelaySMTPProgram.name);
|
||||
homeComp.programs.push(CompletedProgramName.ftpCrack);
|
||||
homeComp.programs.push(CompletedProgramName.relaySmtp);
|
||||
}
|
||||
if (augmentationExists(AugmentationNames.CashRoot) && Player.hasAugmentation(AugmentationNames.CashRoot, true)) {
|
||||
Player.setMoney(1e6);
|
||||
homeComp.programs.push(Programs.BruteSSHProgram.name);
|
||||
homeComp.programs.push(CompletedProgramName.bruteSsh);
|
||||
}
|
||||
if (augmentationExists(AugmentationNames.PCMatrix) && Player.hasAugmentation(AugmentationNames.PCMatrix, true)) {
|
||||
homeComp.programs.push(Programs.DeepscanV1.name);
|
||||
homeComp.programs.push(Programs.AutoLink.name);
|
||||
homeComp.programs.push(CompletedProgramName.deepScan1);
|
||||
homeComp.programs.push(CompletedProgramName.autoLink);
|
||||
}
|
||||
|
||||
if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) {
|
||||
homeComp.programs.push(Programs.Formulas.name);
|
||||
homeComp.programs.push(CompletedProgramName.formulas);
|
||||
}
|
||||
|
||||
// Re-create foreign servers
|
||||
@ -123,7 +123,8 @@ export function prestigeAugmentation(): void {
|
||||
|
||||
// BitNode 3: Corporatocracy
|
||||
if (Player.bitNodeN === 3) {
|
||||
homeComp.messages.push(LiteratureNames.CorporationManagementHandbook);
|
||||
// Easiest way to comply with type constraint, instead of revalidating the enum member's file path
|
||||
homeComp.messages.push(LiteratureName.CorporationManagementHandbook);
|
||||
}
|
||||
|
||||
// Cancel Bladeburner action
|
||||
@ -247,15 +248,13 @@ export function prestigeSourceFile(flume: boolean): void {
|
||||
initCompanies();
|
||||
|
||||
if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) {
|
||||
homeComp.programs.push(Programs.Formulas.name);
|
||||
homeComp.programs.push(CompletedProgramName.formulas);
|
||||
}
|
||||
|
||||
console.log(Player.bitNodeN);
|
||||
dialogBoxCreate("hello");
|
||||
// BitNode 3: Corporatocracy
|
||||
if (Player.bitNodeN === 3) {
|
||||
console.log("why isn't the dialogbox happening?");
|
||||
homeComp.messages.push(LiteratureNames.CorporationManagementHandbook);
|
||||
// Easiest way to comply with type constraint, instead of revalidating the enum member's file path
|
||||
homeComp.messages.push(LiteratureName.CorporationManagementHandbook);
|
||||
dialogBoxCreate(
|
||||
"You received a copy of the Corporation Management Handbook on your home computer. " +
|
||||
"Read it if you need help getting started with Corporations!",
|
||||
|
@ -1,3 +1,5 @@
|
||||
import type { CompletedProgramName } from "./Programs";
|
||||
import { ProgramFilePath, asProgramFilePath } from "../Paths/ProgramFilePath";
|
||||
import { BaseServer } from "../Server/BaseServer";
|
||||
|
||||
export interface IProgramCreate {
|
||||
@ -6,14 +8,19 @@ export interface IProgramCreate {
|
||||
time: number;
|
||||
tooltip: string;
|
||||
}
|
||||
type ProgramConstructorParams = {
|
||||
name: CompletedProgramName;
|
||||
create: IProgramCreate | null;
|
||||
run: (args: string[], server: BaseServer) => void;
|
||||
};
|
||||
|
||||
export class Program {
|
||||
name = "";
|
||||
name: ProgramFilePath & CompletedProgramName;
|
||||
create: IProgramCreate | null;
|
||||
run: (args: string[], server: BaseServer) => void;
|
||||
|
||||
constructor(name: string, create: IProgramCreate | null, run: (args: string[], server: BaseServer) => void) {
|
||||
this.name = name;
|
||||
constructor({ name, create, run }: ProgramConstructorParams) {
|
||||
this.name = asProgramFilePath(name);
|
||||
this.create = create;
|
||||
this.run = run;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Programs } from "./Programs";
|
||||
import { CompletedProgramName, Programs } from "./Programs";
|
||||
import { Program } from "./Program";
|
||||
|
||||
import { Player } from "@player";
|
||||
@ -6,18 +6,18 @@ import { Player } from "@player";
|
||||
//Returns the programs this player can create.
|
||||
export function getAvailableCreatePrograms(): Program[] {
|
||||
const programs: Program[] = [];
|
||||
for (const key of Object.keys(Programs)) {
|
||||
for (const program of Object.values(CompletedProgramName)) {
|
||||
const create = Programs[program].create;
|
||||
// Non-creatable program
|
||||
const create = Programs[key].create;
|
||||
if (create == null) continue;
|
||||
|
||||
// Already has program
|
||||
if (Player.hasProgram(Programs[key].name)) continue;
|
||||
if (Player.hasProgram(program)) continue;
|
||||
|
||||
// Does not meet requirements
|
||||
if (!create.req()) continue;
|
||||
|
||||
programs.push(Programs[key]);
|
||||
programs.push(Programs[program]);
|
||||
}
|
||||
|
||||
return programs;
|
||||
|
@ -1,9 +1,319 @@
|
||||
import { Program } from "./Program";
|
||||
import { programsMetadata } from "./data/ProgramsMetadata";
|
||||
import { CONSTANTS } from "../Constants";
|
||||
import { BaseServer } from "../Server/BaseServer";
|
||||
import { Server } from "../Server/Server";
|
||||
import { Terminal } from "../Terminal";
|
||||
import { Player } from "@player";
|
||||
import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions";
|
||||
import { GetServer } from "../Server/AllServers";
|
||||
import { formatMoney } from "../ui/formatNumber";
|
||||
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
|
||||
import { BitFlumeEvent } from "../BitNode/ui/BitFlumeModal";
|
||||
import { calculateHackingTime, calculateGrowTime, calculateWeakenTime } from "../Hacking";
|
||||
import { FactionNames } from "../Faction/data/FactionNames";
|
||||
|
||||
export const Programs: Record<string, Program> = {};
|
||||
export function initPrograms() {
|
||||
for (const params of programsMetadata) {
|
||||
Programs[params.key] = new Program(params.name, params.create, params.run);
|
||||
}
|
||||
function requireHackingLevel(lvl: number) {
|
||||
return function () {
|
||||
return Player.skills.hacking + Player.skills.intelligence / 2 >= lvl;
|
||||
};
|
||||
}
|
||||
|
||||
function bitFlumeRequirements() {
|
||||
return function () {
|
||||
return Player.sourceFiles.size > 0 && Player.skills.hacking >= 1;
|
||||
};
|
||||
}
|
||||
|
||||
export enum CompletedProgramName {
|
||||
nuke = "NUKE.exe",
|
||||
bruteSsh = "BruteSSH.exe",
|
||||
ftpCrack = "FTPCrack.exe",
|
||||
relaySmtp = "relaySMTP.exe",
|
||||
httpWorm = "HTTPWorm.exe",
|
||||
sqlInject = "SQLInject.exe",
|
||||
deepScan1 = "DeepscanV1.exe",
|
||||
deepScan2 = "DeepscanV2.exe",
|
||||
serverProfiler = "ServerProfiler.exe",
|
||||
autoLink = "AutoLink.exe",
|
||||
formulas = "Formulas.exe",
|
||||
bitFlume = "b1t_flum3.exe",
|
||||
flight = "fl1ght.exe",
|
||||
}
|
||||
|
||||
export const Programs: Record<CompletedProgramName, Program> = {
|
||||
[CompletedProgramName.nuke]: new Program({
|
||||
name: CompletedProgramName.nuke,
|
||||
create: {
|
||||
level: 1,
|
||||
tooltip: "This virus is used to gain root access to a machine if enough ports are opened.",
|
||||
req: requireHackingLevel(1),
|
||||
time: CONSTANTS.MillisecondsPerFiveMinutes,
|
||||
},
|
||||
run: (_args: string[], server: BaseServer): void => {
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error("Cannot nuke this kind of server.");
|
||||
return;
|
||||
}
|
||||
if (server.hasAdminRights) {
|
||||
Terminal.print("You already have root access to this computer. There is no reason to run NUKE.exe");
|
||||
Terminal.print("You can now run scripts on this server.");
|
||||
return;
|
||||
}
|
||||
if (server.openPortCount >= server.numOpenPortsRequired) {
|
||||
server.hasAdminRights = true;
|
||||
Terminal.print("NUKE successful! Gained root access to " + server.hostname);
|
||||
Terminal.print("You can now run scripts on this server.");
|
||||
return;
|
||||
}
|
||||
|
||||
Terminal.print("NUKE unsuccessful. Not enough ports have been opened");
|
||||
},
|
||||
}),
|
||||
[CompletedProgramName.bruteSsh]: new Program({
|
||||
name: CompletedProgramName.bruteSsh,
|
||||
create: {
|
||||
level: 50,
|
||||
tooltip: "This program executes a brute force attack that opens SSH ports",
|
||||
req: requireHackingLevel(50),
|
||||
time: CONSTANTS.MillisecondsPerFiveMinutes * 2,
|
||||
},
|
||||
run: (_args: string[], server: BaseServer): void => {
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error("Cannot run BruteSSH.exe on this kind of server.");
|
||||
return;
|
||||
}
|
||||
if (server.sshPortOpen) {
|
||||
Terminal.print("SSH Port (22) is already open!");
|
||||
return;
|
||||
}
|
||||
|
||||
server.sshPortOpen = true;
|
||||
Terminal.print("Opened SSH Port(22)!");
|
||||
server.openPortCount++;
|
||||
},
|
||||
}),
|
||||
[CompletedProgramName.ftpCrack]: new Program({
|
||||
name: CompletedProgramName.ftpCrack,
|
||||
create: {
|
||||
level: 100,
|
||||
tooltip: "This program cracks open FTP ports",
|
||||
req: requireHackingLevel(100),
|
||||
time: CONSTANTS.MillisecondsPerHalfHour,
|
||||
},
|
||||
run: (_args: string[], server: BaseServer): void => {
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error("Cannot run FTPCrack.exe on this kind of server.");
|
||||
return;
|
||||
}
|
||||
if (server.ftpPortOpen) {
|
||||
Terminal.print("FTP Port (21) is already open!");
|
||||
return;
|
||||
}
|
||||
|
||||
server.ftpPortOpen = true;
|
||||
Terminal.print("Opened FTP Port (21)!");
|
||||
server.openPortCount++;
|
||||
},
|
||||
}),
|
||||
[CompletedProgramName.relaySmtp]: new Program({
|
||||
name: CompletedProgramName.relaySmtp,
|
||||
create: {
|
||||
level: 250,
|
||||
tooltip: "This program opens SMTP ports by redirecting data",
|
||||
req: requireHackingLevel(250),
|
||||
time: CONSTANTS.MillisecondsPer2Hours,
|
||||
},
|
||||
run: (_args: string[], server: BaseServer): void => {
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error("Cannot run relaySMTP.exe on this kind of server.");
|
||||
return;
|
||||
}
|
||||
if (server.smtpPortOpen) {
|
||||
Terminal.print("SMTP Port (25) is already open!");
|
||||
return;
|
||||
}
|
||||
|
||||
server.smtpPortOpen = true;
|
||||
Terminal.print("Opened SMTP Port (25)!");
|
||||
server.openPortCount++;
|
||||
},
|
||||
}),
|
||||
[CompletedProgramName.httpWorm]: new Program({
|
||||
name: CompletedProgramName.httpWorm,
|
||||
create: {
|
||||
level: 500,
|
||||
tooltip: "This virus opens up HTTP ports",
|
||||
req: requireHackingLevel(500),
|
||||
time: CONSTANTS.MillisecondsPer4Hours,
|
||||
},
|
||||
run: (_args: string[], server: BaseServer): void => {
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error("Cannot run HTTPWorm.exe on this kind of server.");
|
||||
return;
|
||||
}
|
||||
if (server.httpPortOpen) {
|
||||
Terminal.print("HTTP Port (80) is already open!");
|
||||
return;
|
||||
}
|
||||
|
||||
server.httpPortOpen = true;
|
||||
Terminal.print("Opened HTTP Port (80)!");
|
||||
server.openPortCount++;
|
||||
},
|
||||
}),
|
||||
[CompletedProgramName.sqlInject]: new Program({
|
||||
name: CompletedProgramName.sqlInject,
|
||||
create: {
|
||||
level: 750,
|
||||
tooltip: "This virus opens SQL ports",
|
||||
req: requireHackingLevel(750),
|
||||
time: CONSTANTS.MillisecondsPer8Hours,
|
||||
},
|
||||
run: (_args: string[], server: BaseServer): void => {
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error("Cannot run SQLInject.exe on this kind of server.");
|
||||
return;
|
||||
}
|
||||
if (server.sqlPortOpen) {
|
||||
Terminal.print("SQL Port (1433) is already open!");
|
||||
return;
|
||||
}
|
||||
|
||||
server.sqlPortOpen = true;
|
||||
Terminal.print("Opened SQL Port (1433)!");
|
||||
server.openPortCount++;
|
||||
},
|
||||
}),
|
||||
[CompletedProgramName.deepScan1]: new Program({
|
||||
name: CompletedProgramName.deepScan1,
|
||||
create: {
|
||||
level: 75,
|
||||
tooltip: "This program allows you to use the scan-analyze command with a depth up to 5",
|
||||
req: requireHackingLevel(75),
|
||||
time: CONSTANTS.MillisecondsPerQuarterHour,
|
||||
},
|
||||
run: (): void => {
|
||||
Terminal.print("This executable cannot be run.");
|
||||
Terminal.print("DeepscanV1.exe lets you run 'scan-analyze' with a depth up to 5.");
|
||||
},
|
||||
}),
|
||||
[CompletedProgramName.deepScan2]: new Program({
|
||||
name: CompletedProgramName.deepScan2,
|
||||
create: {
|
||||
level: 400,
|
||||
tooltip: "This program allows you to use the scan-analyze command with a depth up to 10",
|
||||
req: requireHackingLevel(400),
|
||||
time: CONSTANTS.MillisecondsPer2Hours,
|
||||
},
|
||||
run: (): void => {
|
||||
Terminal.print("This executable cannot be run.");
|
||||
Terminal.print("DeepscanV2.exe lets you run 'scan-analyze' with a depth up to 10.");
|
||||
},
|
||||
}),
|
||||
[CompletedProgramName.serverProfiler]: new Program({
|
||||
name: CompletedProgramName.serverProfiler,
|
||||
create: {
|
||||
level: 75,
|
||||
tooltip: "This program is used to display hacking and Netscript-related information about servers",
|
||||
req: requireHackingLevel(75),
|
||||
time: CONSTANTS.MillisecondsPerHalfHour,
|
||||
},
|
||||
run: (args: string[]): void => {
|
||||
if (args.length !== 1) {
|
||||
Terminal.error("Must pass a server hostname or IP as an argument for ServerProfiler.exe");
|
||||
return;
|
||||
}
|
||||
|
||||
const targetServer = GetServer(args[0]);
|
||||
if (targetServer == null) {
|
||||
Terminal.error("Invalid server IP/hostname");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(targetServer instanceof Server)) {
|
||||
Terminal.error(`ServerProfiler.exe can only be run on normal servers.`);
|
||||
return;
|
||||
}
|
||||
|
||||
Terminal.print(targetServer.hostname + ":");
|
||||
Terminal.print("Server base security level: " + targetServer.baseDifficulty);
|
||||
Terminal.print("Server current security level: " + targetServer.hackDifficulty);
|
||||
Terminal.print("Server growth rate: " + targetServer.serverGrowth);
|
||||
Terminal.print(
|
||||
`Netscript hack() execution time: ${convertTimeMsToTimeElapsedString(
|
||||
calculateHackingTime(targetServer, Player) * 1000,
|
||||
true,
|
||||
)}`,
|
||||
);
|
||||
Terminal.print(
|
||||
`Netscript grow() execution time: ${convertTimeMsToTimeElapsedString(
|
||||
calculateGrowTime(targetServer, Player) * 1000,
|
||||
true,
|
||||
)}`,
|
||||
);
|
||||
Terminal.print(
|
||||
`Netscript weaken() execution time: ${convertTimeMsToTimeElapsedString(
|
||||
calculateWeakenTime(targetServer, Player) * 1000,
|
||||
true,
|
||||
)}`,
|
||||
);
|
||||
},
|
||||
}),
|
||||
[CompletedProgramName.autoLink]: new Program({
|
||||
name: CompletedProgramName.autoLink,
|
||||
create: {
|
||||
level: 25,
|
||||
tooltip: "This program allows you to directly connect to other servers through the 'scan-analyze' command",
|
||||
req: requireHackingLevel(25),
|
||||
time: CONSTANTS.MillisecondsPerQuarterHour,
|
||||
},
|
||||
run: (): void => {
|
||||
Terminal.print("This executable cannot be run.");
|
||||
Terminal.print("AutoLink.exe lets you automatically connect to other servers when using 'scan-analyze'.");
|
||||
Terminal.print("When using scan-analyze, click on a server's hostname to connect to it.");
|
||||
},
|
||||
}),
|
||||
[CompletedProgramName.formulas]: new Program({
|
||||
name: CompletedProgramName.formulas,
|
||||
create: {
|
||||
level: 1000,
|
||||
tooltip: "This program allows you to use the formulas API",
|
||||
req: requireHackingLevel(1000),
|
||||
time: CONSTANTS.MillisecondsPer4Hours,
|
||||
},
|
||||
run: (): void => {
|
||||
Terminal.print("This executable cannot be run.");
|
||||
Terminal.print("Formulas.exe lets you use the formulas API.");
|
||||
},
|
||||
}),
|
||||
[CompletedProgramName.bitFlume]: new Program({
|
||||
name: CompletedProgramName.bitFlume,
|
||||
create: {
|
||||
level: 1,
|
||||
tooltip: "This program creates a portal to the BitNode Nexus (allows you to restart and switch BitNodes)",
|
||||
req: bitFlumeRequirements(),
|
||||
time: CONSTANTS.MillisecondsPerFiveMinutes / 20,
|
||||
},
|
||||
run: (): void => {
|
||||
BitFlumeEvent.emit();
|
||||
},
|
||||
}),
|
||||
[CompletedProgramName.flight]: new Program({
|
||||
name: CompletedProgramName.flight,
|
||||
create: null,
|
||||
run: (): void => {
|
||||
const numAugReq = BitNodeMultipliers.DaedalusAugsRequirement;
|
||||
const fulfilled =
|
||||
Player.augmentations.length >= numAugReq && Player.money > 1e11 && Player.skills.hacking >= 2500;
|
||||
if (!fulfilled) {
|
||||
Terminal.print(`Augmentations: ${Player.augmentations.length} / ${numAugReq}`);
|
||||
Terminal.print(`Money: ${formatMoney(Player.money)} / $100b`);
|
||||
Terminal.print(`Hacking skill: ${Player.skills.hacking} / 2500`);
|
||||
return;
|
||||
}
|
||||
|
||||
Terminal.print("We will contact you.");
|
||||
Terminal.print(`-- ${FactionNames.Daedalus} --`);
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
@ -1,323 +0,0 @@
|
||||
import { IProgramCreate } from "../Program";
|
||||
import { CONSTANTS } from "../../Constants";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { Server } from "../../Server/Server";
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { Player } from "@player";
|
||||
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
|
||||
import { GetServer } from "../../Server/AllServers";
|
||||
import { formatMoney } from "../../ui/formatNumber";
|
||||
import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";
|
||||
import { BitFlumeEvent } from "../../BitNode/ui/BitFlumeModal";
|
||||
import { calculateHackingTime, calculateGrowTime, calculateWeakenTime } from "../../Hacking";
|
||||
import { FactionNames } from "../../Faction/data/FactionNames";
|
||||
|
||||
function requireHackingLevel(lvl: number) {
|
||||
return function () {
|
||||
return Player.skills.hacking + Player.skills.intelligence / 2 >= lvl;
|
||||
};
|
||||
}
|
||||
|
||||
function bitFlumeRequirements() {
|
||||
return function () {
|
||||
return Player.sourceFiles.size > 0 && Player.skills.hacking >= 1;
|
||||
};
|
||||
}
|
||||
|
||||
interface IProgramCreationParams {
|
||||
key: string;
|
||||
name: string;
|
||||
create: IProgramCreate | null;
|
||||
run: (args: string[], server: BaseServer) => void;
|
||||
}
|
||||
|
||||
export const programsMetadata: IProgramCreationParams[] = [
|
||||
{
|
||||
key: "NukeProgram",
|
||||
name: "NUKE.exe",
|
||||
create: {
|
||||
level: 1,
|
||||
tooltip: "This virus is used to gain root access to a machine if enough ports are opened.",
|
||||
req: requireHackingLevel(1),
|
||||
time: CONSTANTS.MillisecondsPerFiveMinutes,
|
||||
},
|
||||
run: (_args: string[], server: BaseServer): void => {
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error("Cannot nuke this kind of server.");
|
||||
return;
|
||||
}
|
||||
if (server.hasAdminRights) {
|
||||
Terminal.print("You already have root access to this computer. There is no reason to run NUKE.exe");
|
||||
Terminal.print("You can now run scripts on this server.");
|
||||
return;
|
||||
}
|
||||
if (server.openPortCount >= server.numOpenPortsRequired) {
|
||||
server.hasAdminRights = true;
|
||||
Terminal.print("NUKE successful! Gained root access to " + server.hostname);
|
||||
Terminal.print("You can now run scripts on this server.");
|
||||
return;
|
||||
}
|
||||
|
||||
Terminal.print("NUKE unsuccessful. Not enough ports have been opened");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "BruteSSHProgram",
|
||||
name: "BruteSSH.exe",
|
||||
create: {
|
||||
level: 50,
|
||||
tooltip: "This program executes a brute force attack that opens SSH ports",
|
||||
req: requireHackingLevel(50),
|
||||
time: CONSTANTS.MillisecondsPerFiveMinutes * 2,
|
||||
},
|
||||
run: (_args: string[], server: BaseServer): void => {
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error("Cannot run BruteSSH.exe on this kind of server.");
|
||||
return;
|
||||
}
|
||||
if (server.sshPortOpen) {
|
||||
Terminal.print("SSH Port (22) is already open!");
|
||||
return;
|
||||
}
|
||||
|
||||
server.sshPortOpen = true;
|
||||
Terminal.print("Opened SSH Port(22)!");
|
||||
server.openPortCount++;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "FTPCrackProgram",
|
||||
name: "FTPCrack.exe",
|
||||
create: {
|
||||
level: 100,
|
||||
tooltip: "This program cracks open FTP ports",
|
||||
req: requireHackingLevel(100),
|
||||
time: CONSTANTS.MillisecondsPerHalfHour,
|
||||
},
|
||||
run: (_args: string[], server: BaseServer): void => {
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error("Cannot run FTPCrack.exe on this kind of server.");
|
||||
return;
|
||||
}
|
||||
if (server.ftpPortOpen) {
|
||||
Terminal.print("FTP Port (21) is already open!");
|
||||
return;
|
||||
}
|
||||
|
||||
server.ftpPortOpen = true;
|
||||
Terminal.print("Opened FTP Port (21)!");
|
||||
server.openPortCount++;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "RelaySMTPProgram",
|
||||
name: "relaySMTP.exe",
|
||||
create: {
|
||||
level: 250,
|
||||
tooltip: "This program opens SMTP ports by redirecting data",
|
||||
req: requireHackingLevel(250),
|
||||
time: CONSTANTS.MillisecondsPer2Hours,
|
||||
},
|
||||
run: (_args: string[], server: BaseServer): void => {
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error("Cannot run relaySMTP.exe on this kind of server.");
|
||||
return;
|
||||
}
|
||||
if (server.smtpPortOpen) {
|
||||
Terminal.print("SMTP Port (25) is already open!");
|
||||
return;
|
||||
}
|
||||
|
||||
server.smtpPortOpen = true;
|
||||
Terminal.print("Opened SMTP Port (25)!");
|
||||
server.openPortCount++;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "HTTPWormProgram",
|
||||
name: "HTTPWorm.exe",
|
||||
create: {
|
||||
level: 500,
|
||||
tooltip: "This virus opens up HTTP ports",
|
||||
req: requireHackingLevel(500),
|
||||
time: CONSTANTS.MillisecondsPer4Hours,
|
||||
},
|
||||
run: (_args: string[], server: BaseServer): void => {
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error("Cannot run HTTPWorm.exe on this kind of server.");
|
||||
return;
|
||||
}
|
||||
if (server.httpPortOpen) {
|
||||
Terminal.print("HTTP Port (80) is already open!");
|
||||
return;
|
||||
}
|
||||
|
||||
server.httpPortOpen = true;
|
||||
Terminal.print("Opened HTTP Port (80)!");
|
||||
server.openPortCount++;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "SQLInjectProgram",
|
||||
name: "SQLInject.exe",
|
||||
create: {
|
||||
level: 750,
|
||||
tooltip: "This virus opens SQL ports",
|
||||
req: requireHackingLevel(750),
|
||||
time: CONSTANTS.MillisecondsPer8Hours,
|
||||
},
|
||||
run: (_args: string[], server: BaseServer): void => {
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error("Cannot run SQLInject.exe on this kind of server.");
|
||||
return;
|
||||
}
|
||||
if (server.sqlPortOpen) {
|
||||
Terminal.print("SQL Port (1433) is already open!");
|
||||
return;
|
||||
}
|
||||
|
||||
server.sqlPortOpen = true;
|
||||
Terminal.print("Opened SQL Port (1433)!");
|
||||
server.openPortCount++;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "DeepscanV1",
|
||||
name: "DeepscanV1.exe",
|
||||
create: {
|
||||
level: 75,
|
||||
tooltip: "This program allows you to use the scan-analyze command with a depth up to 5",
|
||||
req: requireHackingLevel(75),
|
||||
time: CONSTANTS.MillisecondsPerQuarterHour,
|
||||
},
|
||||
run: (): void => {
|
||||
Terminal.print("This executable cannot be run.");
|
||||
Terminal.print("DeepscanV1.exe lets you run 'scan-analyze' with a depth up to 5.");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "DeepscanV2",
|
||||
name: "DeepscanV2.exe",
|
||||
create: {
|
||||
level: 400,
|
||||
tooltip: "This program allows you to use the scan-analyze command with a depth up to 10",
|
||||
req: requireHackingLevel(400),
|
||||
time: CONSTANTS.MillisecondsPer2Hours,
|
||||
},
|
||||
run: (): void => {
|
||||
Terminal.print("This executable cannot be run.");
|
||||
Terminal.print("DeepscanV2.exe lets you run 'scan-analyze' with a depth up to 10.");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "ServerProfiler",
|
||||
name: "ServerProfiler.exe",
|
||||
create: {
|
||||
level: 75,
|
||||
tooltip: "This program is used to display hacking and Netscript-related information about servers",
|
||||
req: requireHackingLevel(75),
|
||||
time: CONSTANTS.MillisecondsPerHalfHour,
|
||||
},
|
||||
run: (args: string[]): void => {
|
||||
if (args.length !== 1) {
|
||||
Terminal.error("Must pass a server hostname or IP as an argument for ServerProfiler.exe");
|
||||
return;
|
||||
}
|
||||
|
||||
const targetServer = GetServer(args[0]);
|
||||
if (targetServer == null) {
|
||||
Terminal.error("Invalid server IP/hostname");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(targetServer instanceof Server)) {
|
||||
Terminal.error(`ServerProfiler.exe can only be run on normal servers.`);
|
||||
return;
|
||||
}
|
||||
|
||||
Terminal.print(targetServer.hostname + ":");
|
||||
Terminal.print("Server base security level: " + targetServer.baseDifficulty);
|
||||
Terminal.print("Server current security level: " + targetServer.hackDifficulty);
|
||||
Terminal.print("Server growth rate: " + targetServer.serverGrowth);
|
||||
Terminal.print(
|
||||
`Netscript hack() execution time: ${convertTimeMsToTimeElapsedString(
|
||||
calculateHackingTime(targetServer, Player) * 1000,
|
||||
true,
|
||||
)}`,
|
||||
);
|
||||
Terminal.print(
|
||||
`Netscript grow() execution time: ${convertTimeMsToTimeElapsedString(
|
||||
calculateGrowTime(targetServer, Player) * 1000,
|
||||
true,
|
||||
)}`,
|
||||
);
|
||||
Terminal.print(
|
||||
`Netscript weaken() execution time: ${convertTimeMsToTimeElapsedString(
|
||||
calculateWeakenTime(targetServer, Player) * 1000,
|
||||
true,
|
||||
)}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "AutoLink",
|
||||
name: "AutoLink.exe",
|
||||
create: {
|
||||
level: 25,
|
||||
tooltip: "This program allows you to directly connect to other servers through the 'scan-analyze' command",
|
||||
req: requireHackingLevel(25),
|
||||
time: CONSTANTS.MillisecondsPerQuarterHour,
|
||||
},
|
||||
run: (): void => {
|
||||
Terminal.print("This executable cannot be run.");
|
||||
Terminal.print("AutoLink.exe lets you automatically connect to other servers when using 'scan-analyze'.");
|
||||
Terminal.print("When using scan-analyze, click on a server's hostname to connect to it.");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "Formulas",
|
||||
name: "Formulas.exe",
|
||||
create: {
|
||||
level: 1000,
|
||||
tooltip: "This program allows you to use the formulas API",
|
||||
req: requireHackingLevel(1000),
|
||||
time: CONSTANTS.MillisecondsPer4Hours,
|
||||
},
|
||||
run: (): void => {
|
||||
Terminal.print("This executable cannot be run.");
|
||||
Terminal.print("Formulas.exe lets you use the formulas API.");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "BitFlume",
|
||||
name: "b1t_flum3.exe",
|
||||
create: {
|
||||
level: 1,
|
||||
tooltip: "This program creates a portal to the BitNode Nexus (allows you to restart and switch BitNodes)",
|
||||
req: bitFlumeRequirements(),
|
||||
time: CONSTANTS.MillisecondsPerFiveMinutes / 20,
|
||||
},
|
||||
run: (): void => {
|
||||
BitFlumeEvent.emit();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "Flight",
|
||||
name: "fl1ght.exe",
|
||||
create: null,
|
||||
run: (): void => {
|
||||
const numAugReq = BitNodeMultipliers.DaedalusAugsRequirement;
|
||||
const fulfilled =
|
||||
Player.augmentations.length >= numAugReq && Player.money > 1e11 && Player.skills.hacking >= 2500;
|
||||
if (!fulfilled) {
|
||||
Terminal.print(`Augmentations: ${Player.augmentations.length} / ${numAugReq}`);
|
||||
Terminal.print(`Money: ${formatMoney(Player.money)} / $100b`);
|
||||
Terminal.print(`Hacking skill: ${Player.skills.hacking} / 2500`);
|
||||
return;
|
||||
}
|
||||
|
||||
Terminal.print("We will contact you.");
|
||||
Terminal.print(`-- ${FactionNames.Daedalus} --`);
|
||||
},
|
||||
},
|
||||
];
|
@ -1,7 +1,7 @@
|
||||
import { isScriptFilename } from "../Script/isScriptFilename";
|
||||
import { resolveFilePath } from "../Paths/FilePath";
|
||||
import { hasTextExtension } from "../Paths/TextFilePath";
|
||||
import { hasScriptExtension } from "../Paths/ScriptFilePath";
|
||||
import { GetServer } from "../Server/AllServers";
|
||||
import { isValidFilePath } from "../Terminal/DirectoryHelpers";
|
||||
import { TextFile } from "../TextFile";
|
||||
import {
|
||||
RFAMessage,
|
||||
FileData,
|
||||
@ -23,62 +23,48 @@ export const RFARequestHandler: Record<string, (message: RFAMessage) => void | R
|
||||
if (!isFileData(msg.params)) return error("Misses parameters", msg);
|
||||
|
||||
const fileData: FileData = msg.params;
|
||||
if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg);
|
||||
const filePath = resolveFilePath(fileData.filename);
|
||||
if (!filePath) return error("Invalid file path", msg);
|
||||
|
||||
const server = GetServer(fileData.server);
|
||||
if (!server) return error("Server hostname invalid", msg);
|
||||
|
||||
if (isScriptFilename(fileData.filename)) server.writeToScriptFile(fileData.filename, fileData.content);
|
||||
// Assume it's a text file
|
||||
else server.writeToTextFile(fileData.filename, fileData.content);
|
||||
|
||||
// If and only if the content is actually changed correctly, send back an OK.
|
||||
const savedCorrectly =
|
||||
server.getScript(fileData.filename)?.code === fileData.content ||
|
||||
server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0)?.text === fileData.content;
|
||||
|
||||
if (!savedCorrectly) return error("File wasn't saved correctly", msg);
|
||||
|
||||
return new RFAMessage({ result: "OK", id: msg.id });
|
||||
if (hasTextExtension(filePath) || hasScriptExtension(filePath)) {
|
||||
server.writeToContentFile(filePath, fileData.content);
|
||||
return new RFAMessage({ result: "OK", id: msg.id });
|
||||
}
|
||||
return error("Invalid file extension", msg);
|
||||
},
|
||||
|
||||
getFile: function (msg: RFAMessage): RFAMessage {
|
||||
if (!isFileLocation(msg.params)) return error("Message misses parameters", msg);
|
||||
|
||||
const fileData: FileLocation = msg.params;
|
||||
if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg);
|
||||
const filePath = resolveFilePath(fileData.filename);
|
||||
if (!filePath) return error("Invalid file path", msg);
|
||||
|
||||
const server = GetServer(fileData.server);
|
||||
if (!server) return error("Server hostname invalid", msg);
|
||||
|
||||
if (isScriptFilename(fileData.filename)) {
|
||||
const scriptContent = server.getScript(fileData.filename);
|
||||
if (!scriptContent) return error("File doesn't exist", msg);
|
||||
return new RFAMessage({ result: scriptContent.code, id: msg.id });
|
||||
} else {
|
||||
// Assume it's a text file
|
||||
const file = server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0);
|
||||
if (!file) return error("File doesn't exist", msg);
|
||||
return new RFAMessage({ result: file.text, id: msg.id });
|
||||
}
|
||||
if (!hasTextExtension(filePath) && !hasScriptExtension(filePath)) return error("Invalid file extension", msg);
|
||||
const file = server.getContentFile(filePath);
|
||||
if (!file) return error("File doesn't exist", msg);
|
||||
return new RFAMessage({ result: file.content, id: msg.id });
|
||||
},
|
||||
|
||||
deleteFile: function (msg: RFAMessage): RFAMessage {
|
||||
if (!isFileLocation(msg.params)) return error("Message misses parameters", msg);
|
||||
|
||||
const fileData: FileLocation = msg.params;
|
||||
if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg);
|
||||
const filePath = resolveFilePath(fileData.filename);
|
||||
if (!filePath) return error("Invalid filename", msg);
|
||||
|
||||
const server = GetServer(fileData.server);
|
||||
if (!server) return error("Server hostname invalid", msg);
|
||||
|
||||
const fileExists = (): boolean =>
|
||||
!!server.getScript(fileData.filename) || server.textFiles.some((t: TextFile) => t.filename === fileData.filename);
|
||||
|
||||
if (!fileExists()) return error("File doesn't exist", msg);
|
||||
server.removeFile(fileData.filename);
|
||||
if (fileExists()) return error("Failed to delete the file", msg);
|
||||
|
||||
return new RFAMessage({ result: "OK", id: msg.id });
|
||||
const result = server.removeFile(filePath);
|
||||
if (result.res) return new RFAMessage({ result: "OK", id: msg.id });
|
||||
return error(result.msg ?? "Failed", msg);
|
||||
},
|
||||
|
||||
getFileNames: function (msg: RFAMessage): RFAMessage {
|
||||
@ -87,7 +73,7 @@ export const RFARequestHandler: Record<string, (message: RFAMessage) => void | R
|
||||
const server = GetServer(msg.params.server);
|
||||
if (!server) return error("Server hostname invalid", msg);
|
||||
|
||||
const fileNameList: string[] = [...server.textFiles.map((txt): string => txt.filename), ...server.scripts.keys()];
|
||||
const fileNameList: string[] = [...server.textFiles.keys(), ...server.scripts.keys()];
|
||||
|
||||
return new RFAMessage({ result: fileNameList, id: msg.id });
|
||||
},
|
||||
@ -98,26 +84,24 @@ export const RFARequestHandler: Record<string, (message: RFAMessage) => void | R
|
||||
const server = GetServer(msg.params.server);
|
||||
if (!server) return error("Server hostname invalid", msg);
|
||||
|
||||
const fileList: FileContent[] = [
|
||||
...server.textFiles.map((txt): FileContent => {
|
||||
return { filename: txt.filename, content: txt.text };
|
||||
}),
|
||||
];
|
||||
for (const [filename, script] of server.scripts) fileList.push({ filename, content: script.code });
|
||||
|
||||
const fileList: FileContent[] = [...server.scripts, ...server.textFiles].map(([filename, file]) => ({
|
||||
filename,
|
||||
content: file.content,
|
||||
}));
|
||||
return new RFAMessage({ result: fileList, id: msg.id });
|
||||
},
|
||||
|
||||
calculateRam: function (msg: RFAMessage): RFAMessage {
|
||||
if (!isFileLocation(msg.params)) return error("Message misses parameters", msg);
|
||||
const fileData: FileLocation = msg.params;
|
||||
if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg);
|
||||
const filePath = resolveFilePath(fileData.filename);
|
||||
if (!filePath) return error("Invalid filename", msg);
|
||||
|
||||
const server = GetServer(fileData.server);
|
||||
if (!server) return error("Server hostname invalid", msg);
|
||||
|
||||
if (!isScriptFilename(fileData.filename)) return error("Filename isn't a script filename", msg);
|
||||
const script = server.getScript(fileData.filename);
|
||||
if (!hasScriptExtension(filePath)) return error("Filename isn't a script filename", msg);
|
||||
const script = server.scripts.get(filePath);
|
||||
if (!script) return error("File doesn't exist", msg);
|
||||
const ramUsage = script.getRamUsage(server.scripts);
|
||||
if (!ramUsage) return error("Ram cost could not be calculated", msg);
|
||||
|
@ -36,6 +36,10 @@ import { SpecialServers } from "./Server/data/SpecialServers";
|
||||
import { v2APIBreak } from "./utils/v2APIBreak";
|
||||
import { Script } from "./Script/Script";
|
||||
import { JSONMap } from "./Types/Jsonable";
|
||||
import { TextFile } from "./TextFile";
|
||||
import { ScriptFilePath, resolveScriptFilePath } from "./Paths/ScriptFilePath";
|
||||
import { Directory, resolveDirectory } from "./Paths/Directory";
|
||||
import { TextFilePath, resolveTextFilePath } from "./Paths/TextFilePath";
|
||||
|
||||
/* SaveObject.js
|
||||
* Defines the object used to save/load games
|
||||
@ -348,7 +352,7 @@ function evaluateVersionCompatibility(ver: string | number): void {
|
||||
}
|
||||
for (const server of GetAllServers() as unknown as { scripts: Script[] }[]) {
|
||||
for (const script of server.scripts) {
|
||||
script.code = convert(script.code);
|
||||
script.content = convert(script.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -490,7 +494,10 @@ function evaluateVersionCompatibility(ver: string | number): void {
|
||||
anyPlayer.currentWork = null;
|
||||
}
|
||||
if (ver < 24) {
|
||||
Player.getHomeComputer().scripts.forEach((s) => s.filename.endsWith(".ns") && (s.filename += ".js"));
|
||||
// Assert the relevant type that was in effect at this version.
|
||||
(Player.getHomeComputer().scripts as unknown as { filename: string }[]).forEach(
|
||||
(s) => s.filename.endsWith(".ns") && (s.filename += ".js"),
|
||||
);
|
||||
}
|
||||
if (ver < 25) {
|
||||
const removePlayerFields = [
|
||||
@ -659,17 +666,43 @@ function evaluateVersionCompatibility(ver: string | number): void {
|
||||
for (const sleeve of Player.sleeves) sleeve.shock = 100 - sleeve.shock;
|
||||
}
|
||||
if (ver < 31) {
|
||||
if (anyPlayer.hashManager !== undefined) {
|
||||
if (anyPlayer.hashManager?.upgrades) {
|
||||
anyPlayer.hashManager.upgrades["Company Favor"] ??= 0;
|
||||
}
|
||||
anyPlayer.lastAugReset ??= anyPlayer.lastUpdate - anyPlayer.playtimeSinceLastAug;
|
||||
anyPlayer.lastNodeReset ??= anyPlayer.lastUpdate - anyPlayer.playtimeSinceLastBitnode;
|
||||
const newDirectory = resolveDirectory("v2.3FileChanges/") as Directory;
|
||||
for (const server of GetAllServers()) {
|
||||
if (Array.isArray(server.scripts)) {
|
||||
const oldScripts = server.scripts as Script[];
|
||||
server.scripts = new JSONMap();
|
||||
for (const script of oldScripts) {
|
||||
server.scripts.set(script.filename, script);
|
||||
let invalidScriptCount = 0;
|
||||
// There was a brief dev window where Server.scripts was already a map but the filepath changes weren't in yet.
|
||||
const oldScripts = Array.isArray(server.scripts) ? (server.scripts as Script[]) : [...server.scripts.values()];
|
||||
server.scripts = new JSONMap();
|
||||
// In case somehow there are previously valid filenames that can't be sanitized, they will go in a new directory with a note.
|
||||
for (const script of oldScripts) {
|
||||
let newFilePath = resolveScriptFilePath(script.filename);
|
||||
if (!newFilePath) {
|
||||
newFilePath = `${newDirectory}script${++invalidScriptCount}.js` as ScriptFilePath;
|
||||
script.content = `// Original path: ${script.filename}. Path was no longer valid\n` + script.content;
|
||||
}
|
||||
script.filename = newFilePath;
|
||||
server.scripts.set(newFilePath, script);
|
||||
}
|
||||
// Handle changing textFiles to a map as well as FilePath changes at the same time.
|
||||
if (Array.isArray(server.textFiles)) {
|
||||
const oldTextFiles = server.textFiles as (TextFile & { fn?: string })[];
|
||||
server.textFiles = new JSONMap();
|
||||
let invalidTextCount = 0;
|
||||
for (const textFile of oldTextFiles) {
|
||||
const oldName = textFile.fn ?? textFile.filename;
|
||||
delete textFile.fn;
|
||||
|
||||
let newFilePath = resolveTextFilePath(oldName);
|
||||
if (!newFilePath) {
|
||||
newFilePath = `${newDirectory}text${++invalidTextCount}.txt` as TextFilePath;
|
||||
textFile.content = `// Original path: ${textFile.filename}. Path was no longer valid\n` + textFile.content;
|
||||
}
|
||||
textFile.filename = newFilePath;
|
||||
server.textFiles.set(newFilePath, textFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,8 @@ import { RamCalculationErrorCode } from "./RamCalculationErrorCodes";
|
||||
import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator";
|
||||
import { Script } from "./Script";
|
||||
import { Node } from "../NetscriptJSEvaluator";
|
||||
import { ScriptFilename, scriptFilenameFromImport } from "../Types/strings";
|
||||
import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath";
|
||||
import { root } from "../Paths/Directory";
|
||||
|
||||
export interface RamUsageEntry {
|
||||
type: "ns" | "dom" | "fn" | "misc";
|
||||
@ -38,9 +39,9 @@ const memCheckGlobalKey = ".__GLOBAL__";
|
||||
/**
|
||||
* Parses code into an AST and walks through it recursively to calculate
|
||||
* RAM usage. Also accounts for imported modules.
|
||||
* @param {Script[]} otherScripts - All other scripts on the server. Used to account for imported scripts
|
||||
* @param {string} code - The code being parsed */
|
||||
function parseOnlyRamCalculate(otherScripts: Map<ScriptFilename, Script>, code: string, ns1?: boolean): RamCalculation {
|
||||
* @param otherScripts - All other scripts on the server. Used to account for imported scripts
|
||||
* @param code - The code being parsed */
|
||||
function parseOnlyRamCalculate(otherScripts: Map<ScriptFilePath, Script>, code: string, ns1?: boolean): RamCalculation {
|
||||
try {
|
||||
/**
|
||||
* Maps dependent identifiers to their dependencies.
|
||||
@ -86,11 +87,11 @@ function parseOnlyRamCalculate(otherScripts: Map<ScriptFilename, Script>, code:
|
||||
if (nextModule === undefined) throw new Error("nextModule should not be undefined");
|
||||
if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue;
|
||||
|
||||
const filename = scriptFilenameFromImport(nextModule, ns1);
|
||||
// Using root as the path base right now. Difficult to implement
|
||||
const filename = resolveScriptFilePath(nextModule, root, ns1 ? ".script" : ".js");
|
||||
if (!filename) return { cost: RamCalculationErrorCode.ImportError }; // Invalid import path
|
||||
const script = otherScripts.get(filename);
|
||||
if (!script) {
|
||||
return { cost: RamCalculationErrorCode.ImportError }; // No such script on the server
|
||||
}
|
||||
if (!script) return { cost: RamCalculationErrorCode.ImportError }; // No such file on server
|
||||
|
||||
parseCode(script.code, nextModule);
|
||||
}
|
||||
@ -370,7 +371,7 @@ function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsR
|
||||
*/
|
||||
export function calculateRamUsage(
|
||||
codeCopy: string,
|
||||
otherScripts: Map<ScriptFilename, Script>,
|
||||
otherScripts: Map<ScriptFilePath, Script>,
|
||||
ns1?: boolean,
|
||||
): RamCalculation {
|
||||
try {
|
||||
|
@ -14,6 +14,7 @@ import { ScriptArg } from "@nsdefs";
|
||||
import { RamCostConstants } from "../Netscript/RamCostGenerator";
|
||||
import { PositiveInteger } from "../types";
|
||||
import { getKeyList } from "../utils/helpers/getKeyList";
|
||||
import { ScriptFilePath } from "../Paths/ScriptFilePath";
|
||||
|
||||
export class RunningScript {
|
||||
// Script arguments
|
||||
@ -24,7 +25,7 @@ export class RunningScript {
|
||||
dataMap: Record<string, number[]> = {};
|
||||
|
||||
// Script filename
|
||||
filename = "default.js";
|
||||
filename = "default.js" as ScriptFilePath;
|
||||
|
||||
// This script's logs. An array of log entries
|
||||
logs: React.ReactNode[] = [];
|
||||
|
@ -1,20 +1,19 @@
|
||||
/**
|
||||
* Class representing a script file.
|
||||
*
|
||||
* This does NOT represent a script that is actively running and
|
||||
* being evaluated. See RunningScript for that
|
||||
*/
|
||||
import type { BaseServer } from "../Server/BaseServer";
|
||||
import { calculateRamUsage, RamUsageEntry } from "./RamCalculations";
|
||||
import { LoadedModule, ScriptURL } from "./LoadedModule";
|
||||
|
||||
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
|
||||
import { roundToTwo } from "../utils/helpers/roundToTwo";
|
||||
import { RamCostConstants } from "../Netscript/RamCostGenerator";
|
||||
import { ScriptFilename } from "src/Types/strings";
|
||||
import { ScriptFilePath } from "../Paths/ScriptFilePath";
|
||||
import { ContentFile } from "../Paths/ContentFile";
|
||||
/** Type for ensuring script code is always trimmed */
|
||||
export type FormattedCode = string & { __type: "FormattedCode" };
|
||||
|
||||
export class Script {
|
||||
code: string;
|
||||
filename: string;
|
||||
/** A script file as a file on a server.
|
||||
* For the execution of a script, see RunningScript and WorkerScript */
|
||||
export class Script implements ContentFile {
|
||||
code: FormattedCode;
|
||||
filename: ScriptFilePath;
|
||||
server: string;
|
||||
|
||||
// Ram calculation, only exists after first poll of ram cost after updating
|
||||
@ -32,9 +31,19 @@ export class Script {
|
||||
*/
|
||||
dependencies: Map<ScriptURL, Script> = new Map();
|
||||
|
||||
constructor(fn = "", code = "", server = "") {
|
||||
get content() {
|
||||
return this.code;
|
||||
}
|
||||
set content(code: string) {
|
||||
const newCode = Script.formatCode(code);
|
||||
if (this.code === newCode) return;
|
||||
this.code = newCode;
|
||||
this.invalidateModule();
|
||||
}
|
||||
|
||||
constructor(fn = "default.js" as ScriptFilePath, code = "", server = "") {
|
||||
this.filename = fn;
|
||||
this.code = code;
|
||||
this.code = Script.formatCode(code);
|
||||
this.server = server; // hostname of server this script is on
|
||||
}
|
||||
|
||||
@ -71,18 +80,19 @@ export class Script {
|
||||
|
||||
/**
|
||||
* Save a script from the script editor
|
||||
* @param {string} code - The new contents of the script
|
||||
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports
|
||||
* @param filename The new filepath for this Script
|
||||
* @param code The unformatted code to save
|
||||
* @param hostname The server to save the script to
|
||||
*/
|
||||
saveScript(filename: string, code: string, hostname: string): void {
|
||||
this.invalidateModule();
|
||||
saveScript(filename: ScriptFilePath, code: string, hostname: string): void {
|
||||
this.code = Script.formatCode(code);
|
||||
this.invalidateModule();
|
||||
this.filename = filename;
|
||||
this.server = hostname;
|
||||
}
|
||||
|
||||
/** Gets the ram usage, while also attempting to update it if it's currently null */
|
||||
getRamUsage(otherScripts: Map<ScriptFilename, Script>): number | null {
|
||||
getRamUsage(otherScripts: Map<ScriptFilePath, Script>): number | null {
|
||||
if (this.ramUsage) return this.ramUsage;
|
||||
this.updateRamUsage(otherScripts);
|
||||
return this.ramUsage;
|
||||
@ -92,7 +102,7 @@ export class Script {
|
||||
* Calculates and updates the script's RAM usage based on its code
|
||||
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports
|
||||
*/
|
||||
updateRamUsage(otherScripts: Map<ScriptFilename, Script>): void {
|
||||
updateRamUsage(otherScripts: Map<ScriptFilePath, Script>): void {
|
||||
const ramCalc = calculateRamUsage(this.code, otherScripts, this.filename.endsWith(".script"));
|
||||
if (ramCalc.cost >= RamCostConstants.Base) {
|
||||
this.ramUsage = roundToTwo(ramCalc.cost);
|
||||
@ -102,6 +112,14 @@ export class Script {
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove script from server. Fails if the provided server isn't the server for this script. */
|
||||
deleteFromServer(server: BaseServer): boolean {
|
||||
if (this.server !== server.hostname || server.isRunning(this.filename)) return false;
|
||||
this.invalidateModule();
|
||||
server.scripts.delete(this.filename);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** The keys that are relevant in a save file */
|
||||
static savedKeys = ["code", "filename", "server"] as const;
|
||||
|
||||
@ -120,8 +138,8 @@ export class Script {
|
||||
* @param {string} code - The code to format
|
||||
* @returns The formatted code
|
||||
*/
|
||||
static formatCode(code: string): string {
|
||||
return code.replace(/^\s+|\s+$/g, "");
|
||||
static formatCode(code: string): FormattedCode {
|
||||
return code.replace(/^\s+|\s+$/g, "") as FormattedCode;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,9 +5,8 @@ import { Server } from "../Server/Server";
|
||||
import { RunningScript } from "./RunningScript";
|
||||
import { processSingleServerGrowth } from "../Server/ServerHelpers";
|
||||
import { GetServer } from "../Server/AllServers";
|
||||
|
||||
import { formatPercent } from "../ui/formatNumber";
|
||||
|
||||
import { workerScripts } from "../Netscript/WorkerScripts";
|
||||
import { compareArrays } from "../utils/helpers/compareArrays";
|
||||
|
||||
export function scriptCalculateOfflineProduction(runningScript: RunningScript): void {
|
||||
@ -99,10 +98,9 @@ export function findRunningScript(
|
||||
//Returns a RunningScript object matching the pid on the
|
||||
//designated server, and false otherwise
|
||||
export function findRunningScriptByPid(pid: number, server: BaseServer): RunningScript | null {
|
||||
for (let i = 0; i < server.runningScripts.length; ++i) {
|
||||
if (server.runningScripts[i].pid === pid) {
|
||||
return server.runningScripts[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
const ws = workerScripts.get(pid);
|
||||
// Return null if no ws found or if it's on a different server.
|
||||
if (!ws) return null;
|
||||
if (ws.scriptRef.server !== server.hostname) return null;
|
||||
return ws.scriptRef;
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
export const validScriptExtensions: Array<string> = [`.js`, `.script`];
|
||||
|
||||
export function isScriptFilename(f: string): boolean {
|
||||
return validScriptExtensions.some((ext) => f.endsWith(ext));
|
||||
}
|
@ -8,14 +8,12 @@ type IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor;
|
||||
type ITextModel = monaco.editor.ITextModel;
|
||||
import { OptionsModal } from "./OptionsModal";
|
||||
import { Options } from "./Options";
|
||||
import { isValidFilePath } from "../../Terminal/DirectoryHelpers";
|
||||
import { Player } from "@player";
|
||||
import { Router } from "../../ui/GameRoot";
|
||||
import { Page } from "../../ui/Router";
|
||||
import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
||||
import { isScriptFilename } from "../../Script/isScriptFilename";
|
||||
import { ScriptFilePath } from "../../Paths/ScriptFilePath";
|
||||
import { Script } from "../../Script/Script";
|
||||
import { TextFile } from "../../TextFile";
|
||||
import { calculateRamUsage, checkInfiniteLoop } from "../../Script/RamCalculations";
|
||||
import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes";
|
||||
import { formatRam } from "../../ui/formatNumber";
|
||||
@ -48,10 +46,12 @@ import libSource from "!!raw-loader!../NetscriptDefinitions.d.ts";
|
||||
import { TextField, Tooltip } from "@mui/material";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { NetscriptExtra } from "../../NetscriptFunctions/Extra";
|
||||
import { TextFilePath } from "src/Paths/TextFilePath";
|
||||
import { ContentFilePath } from "src/Paths/ContentFile";
|
||||
|
||||
interface IProps {
|
||||
// Map of filename -> code
|
||||
files: Record<string, string>;
|
||||
files: Map<ScriptFilePath | TextFilePath, string>;
|
||||
hostname: string;
|
||||
vim: boolean;
|
||||
}
|
||||
@ -75,20 +75,20 @@ export function SetupTextEditor(): void {
|
||||
|
||||
// Holds all the data for a open script
|
||||
class OpenScript {
|
||||
fileName: string;
|
||||
path: ContentFilePath;
|
||||
code: string;
|
||||
hostname: string;
|
||||
lastPosition: monaco.Position;
|
||||
model: ITextModel;
|
||||
isTxt: boolean;
|
||||
|
||||
constructor(fileName: string, code: string, hostname: string, lastPosition: monaco.Position, model: ITextModel) {
|
||||
this.fileName = fileName;
|
||||
constructor(path: ContentFilePath, code: string, hostname: string, lastPosition: monaco.Position, model: ITextModel) {
|
||||
this.path = path;
|
||||
this.code = code;
|
||||
this.hostname = hostname;
|
||||
this.lastPosition = lastPosition;
|
||||
this.model = model;
|
||||
this.isTxt = fileName.endsWith(".txt");
|
||||
this.isTxt = path.endsWith(".txt");
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,7 +232,7 @@ export function Root(props: IProps): React.ReactElement {
|
||||
}, 300);
|
||||
|
||||
function updateRAM(newCode: string): void {
|
||||
if (currentScript != null && currentScript.isTxt) {
|
||||
if (!currentScript || currentScript.isTxt) {
|
||||
setRAM("N/A");
|
||||
setRamEntries([["N/A", ""]]);
|
||||
return;
|
||||
@ -338,18 +338,16 @@ export function Root(props: IProps): React.ReactElement {
|
||||
return;
|
||||
}
|
||||
if (props.files) {
|
||||
const files = Object.entries(props.files);
|
||||
const files = props.files;
|
||||
|
||||
if (!files.length) {
|
||||
if (!files.size) {
|
||||
editorRef.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [filename, code] of files) {
|
||||
// Check if file is already opened
|
||||
const openScript = openScripts.find(
|
||||
(script) => script.fileName === filename && script.hostname === props.hostname,
|
||||
);
|
||||
const openScript = openScripts.find((script) => script.path === filename && script.hostname === props.hostname);
|
||||
if (openScript) {
|
||||
// Script is already opened
|
||||
if (openScript.model === undefined || openScript.model === null || openScript.model.isDisposed()) {
|
||||
@ -383,7 +381,7 @@ export function Root(props: IProps): React.ReactElement {
|
||||
|
||||
function infLoop(newCode: string): void {
|
||||
if (editorRef.current === null || currentScript === null) return;
|
||||
if (!currentScript.fileName.endsWith(".js")) return;
|
||||
if (!currentScript.path.endsWith(".js")) return;
|
||||
const awaitWarning = checkInfiniteLoop(newCode);
|
||||
if (awaitWarning !== -1) {
|
||||
const newDecorations = editorRef.current.deltaDecorations(decorations, [
|
||||
@ -429,37 +427,9 @@ export function Root(props: IProps): React.ReactElement {
|
||||
|
||||
function saveScript(scriptToSave: OpenScript): void {
|
||||
const server = GetServer(scriptToSave.hostname);
|
||||
if (server === null) throw new Error("Server should not be null but it is.");
|
||||
if (isScriptFilename(scriptToSave.fileName)) {
|
||||
//If the current script already exists on the server, overwrite it
|
||||
const existingScript = server.scripts.get(scriptToSave.fileName);
|
||||
if (existingScript) {
|
||||
existingScript.saveScript(scriptToSave.fileName, scriptToSave.code, Player.currentServer);
|
||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||
Router.toPage(Page.Terminal);
|
||||
return;
|
||||
}
|
||||
|
||||
//If the current script does NOT exist, create a new one
|
||||
const script = new Script();
|
||||
script.saveScript(scriptToSave.fileName, scriptToSave.code, Player.currentServer);
|
||||
server.scripts.set(scriptToSave.fileName, script);
|
||||
} else if (scriptToSave.isTxt) {
|
||||
for (let i = 0; i < server.textFiles.length; ++i) {
|
||||
if (server.textFiles[i].fn === scriptToSave.fileName) {
|
||||
server.textFiles[i].write(scriptToSave.code);
|
||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||
Router.toPage(Page.Terminal);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const textFile = new TextFile(scriptToSave.fileName, scriptToSave.code);
|
||||
server.textFiles.push(textFile);
|
||||
} else {
|
||||
dialogBoxCreate("Invalid filename. Must be either a script (.script or .js) or a text file (.txt)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!server) throw new Error("Server should not be null but it is.");
|
||||
// This server helper already handles overwriting, etc.
|
||||
server.writeToContentFile(scriptToSave.path, scriptToSave.code);
|
||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||
Router.toPage(Page.Terminal);
|
||||
}
|
||||
@ -472,7 +442,7 @@ export function Root(props: IProps): React.ReactElement {
|
||||
// this is duplicate code with saving later.
|
||||
if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) {
|
||||
//Make sure filename + code properly follow tutorial
|
||||
if (currentScript.fileName !== "n00dles.script" && currentScript.fileName !== "n00dles.js") {
|
||||
if (currentScript.path !== "n00dles.script" && currentScript.path !== "n00dles.js") {
|
||||
dialogBoxCreate("Don't change the script name for now.");
|
||||
return;
|
||||
}
|
||||
@ -492,50 +462,9 @@ export function Root(props: IProps): React.ReactElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentScript.fileName == "") {
|
||||
dialogBoxCreate("You must specify a filename!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidFilePath(currentScript.fileName)) {
|
||||
dialogBoxCreate(
|
||||
"Script filename can contain only alphanumerics, hyphens, and underscores, and must end with an extension.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const server = GetServer(currentScript.hostname);
|
||||
if (server === null) throw new Error("Server should not be null but it is.");
|
||||
if (isScriptFilename(currentScript.fileName)) {
|
||||
//If the current script already exists on the server, overwrite it
|
||||
const existingScript = server.scripts.get(currentScript.fileName);
|
||||
if (existingScript) {
|
||||
existingScript.saveScript(currentScript.fileName, currentScript.code, Player.currentServer);
|
||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||
rerender();
|
||||
return;
|
||||
}
|
||||
|
||||
//If the current script does NOT exist, create a new one
|
||||
const script = new Script();
|
||||
script.saveScript(currentScript.fileName, currentScript.code, Player.currentServer);
|
||||
server.scripts.set(currentScript.fileName, script);
|
||||
} else if (currentScript.isTxt) {
|
||||
for (let i = 0; i < server.textFiles.length; ++i) {
|
||||
if (server.textFiles[i].fn === currentScript.fileName) {
|
||||
server.textFiles[i].write(currentScript.code);
|
||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||
rerender();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const textFile = new TextFile(currentScript.fileName, currentScript.code);
|
||||
server.textFiles.push(textFile);
|
||||
} else {
|
||||
dialogBoxCreate("Invalid filename. Must be either a script (.script or .js) or a text file (.txt)");
|
||||
return;
|
||||
}
|
||||
|
||||
server.writeToContentFile(currentScript.path, currentScript.code);
|
||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||
rerender();
|
||||
}
|
||||
@ -555,9 +484,7 @@ export function Root(props: IProps): React.ReactElement {
|
||||
if (currentScript !== null) {
|
||||
return openScripts.findIndex(
|
||||
(script) =>
|
||||
currentScript !== null &&
|
||||
script.fileName === currentScript.fileName &&
|
||||
script.hostname === currentScript.hostname,
|
||||
currentScript !== null && script.path === currentScript.path && script.hostname === currentScript.hostname,
|
||||
);
|
||||
}
|
||||
|
||||
@ -596,7 +523,7 @@ export function Root(props: IProps): React.ReactElement {
|
||||
|
||||
if (dirty(index)) {
|
||||
PromptEvent.emit({
|
||||
txt: `Do you want to save changes to ${closingScript.fileName} on ${closingScript.hostname}?`,
|
||||
txt: `Do you want to save changes to ${closingScript.path} on ${closingScript.hostname}?`,
|
||||
resolve: (result: boolean | string) => {
|
||||
if (result) {
|
||||
// Save changes
|
||||
@ -641,7 +568,7 @@ export function Root(props: IProps): React.ReactElement {
|
||||
PromptEvent.emit({
|
||||
txt:
|
||||
"Do you want to overwrite the current editor content with the contents of " +
|
||||
openScript.fileName +
|
||||
openScript.path +
|
||||
" on the server? This cannot be undone.",
|
||||
resolve: (result: boolean | string) => {
|
||||
if (result) {
|
||||
@ -679,10 +606,8 @@ export function Root(props: IProps): React.ReactElement {
|
||||
const openScript = openScripts[index];
|
||||
const server = GetServer(openScript.hostname);
|
||||
if (server === null) throw new Error(`Server '${openScript.hostname}' should not be null, but it is.`);
|
||||
const data = openScript.isTxt
|
||||
? server.textFiles.find((t) => t.filename === openScript.fileName)?.text
|
||||
: server.scripts.get(openScript.fileName)?.code;
|
||||
return data ?? null;
|
||||
const data = server.getContentFile(openScript.path)?.content ?? null;
|
||||
return data;
|
||||
}
|
||||
function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
setFilter(event.target.value);
|
||||
@ -692,7 +617,7 @@ export function Root(props: IProps): React.ReactElement {
|
||||
setSearchExpanded(!searchExpanded);
|
||||
}
|
||||
const filteredOpenScripts = Object.values(openScripts).filter(
|
||||
(script) => script.hostname.includes(filter) || script.fileName.includes(filter),
|
||||
(script) => script.hostname.includes(filter) || script.path.includes(filter),
|
||||
);
|
||||
|
||||
const tabsMaxWidth = 1640;
|
||||
@ -747,9 +672,9 @@ export function Root(props: IProps): React.ReactElement {
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
{filteredOpenScripts.map(({ fileName, hostname }, index) => {
|
||||
{filteredOpenScripts.map(({ path: fileName, hostname }, index) => {
|
||||
const editingCurrentScript =
|
||||
currentScript?.fileName === filteredOpenScripts[index].fileName &&
|
||||
currentScript?.path === filteredOpenScripts[index].path &&
|
||||
currentScript?.hostname === filteredOpenScripts[index].hostname;
|
||||
const externalScript = hostname !== "home";
|
||||
const colorProps = editingCurrentScript
|
||||
|
@ -2,17 +2,23 @@ import type { Server as IServer } from "@nsdefs";
|
||||
import { CodingContract } from "../CodingContracts";
|
||||
import { RunningScript } from "../Script/RunningScript";
|
||||
import { Script } from "../Script/Script";
|
||||
import { isValidFilePath } from "../Terminal/DirectoryHelpers";
|
||||
import { TextFile } from "../TextFile";
|
||||
import { IReturnStatus } from "../types";
|
||||
|
||||
import { isScriptFilename } from "../Script/isScriptFilename";
|
||||
import { ScriptFilePath, hasScriptExtension } from "../Paths/ScriptFilePath";
|
||||
import { TextFilePath, hasTextExtension } from "../Paths/TextFilePath";
|
||||
|
||||
import { createRandomIp } from "../utils/IPAddress";
|
||||
import { compareArrays } from "../utils/helpers/compareArrays";
|
||||
import { ScriptArg } from "../Netscript/ScriptArg";
|
||||
import { JSONMap } from "../Types/Jsonable";
|
||||
import { IPAddress, ScriptFilename, ServerName } from "../Types/strings";
|
||||
import { IPAddress, ServerName } from "../Types/strings";
|
||||
import { FilePath } from "../Paths/FilePath";
|
||||
import { ContentFile, ContentFilePath } from "../Paths/ContentFile";
|
||||
import { ProgramFilePath, hasProgramExtension } from "../Paths/ProgramFilePath";
|
||||
import { MessageFilename } from "src/Message/MessageHelpers";
|
||||
import { LiteratureName } from "src/Literature/data/LiteratureNames";
|
||||
import { CompletedProgramName } from "src/Programs/Programs";
|
||||
|
||||
interface IConstructorParams {
|
||||
adminRights?: boolean;
|
||||
@ -24,7 +30,6 @@ interface IConstructorParams {
|
||||
}
|
||||
|
||||
interface writeResult {
|
||||
success: boolean;
|
||||
overwritten: boolean;
|
||||
}
|
||||
|
||||
@ -59,14 +64,15 @@ export abstract class BaseServer implements IServer {
|
||||
maxRam = 0;
|
||||
|
||||
// Message files AND Literature files on this Server
|
||||
messages: string[] = [];
|
||||
messages: (MessageFilename | LiteratureName | FilePath)[] = [];
|
||||
|
||||
// Name of company/faction/etc. that this server belongs to.
|
||||
// Optional, not applicable to all Servers
|
||||
organizationName = "";
|
||||
|
||||
// Programs on this servers. Contains only the names of the programs
|
||||
programs: string[] = [];
|
||||
// CompletedProgramNames are all typechecked as valid paths in Program constructor
|
||||
programs: (ProgramFilePath | CompletedProgramName)[] = [];
|
||||
|
||||
// RAM (GB) used. i.e. unavailable RAM
|
||||
ramUsed = 0;
|
||||
@ -75,7 +81,7 @@ export abstract class BaseServer implements IServer {
|
||||
runningScripts: RunningScript[] = [];
|
||||
|
||||
// Script files on this Server
|
||||
scripts: JSONMap<ScriptFilename, Script> = new JSONMap();
|
||||
scripts: JSONMap<ScriptFilePath, Script> = new JSONMap();
|
||||
|
||||
// Contains the hostnames of all servers that are immediately
|
||||
// reachable from this one
|
||||
@ -91,7 +97,7 @@ export abstract class BaseServer implements IServer {
|
||||
sshPortOpen = false;
|
||||
|
||||
// Text files on this server
|
||||
textFiles: TextFile[] = [];
|
||||
textFiles: JSONMap<TextFilePath, TextFile> = new JSONMap();
|
||||
|
||||
// Flag indicating whether this is a purchased server
|
||||
purchasedByPlayer = false;
|
||||
@ -132,34 +138,17 @@ export abstract class BaseServer implements IServer {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an actively running script on this server
|
||||
* @param scriptName - Filename of script to search for
|
||||
* @param scriptArgs - Arguments that script is being run with
|
||||
* @returns RunningScript for the specified active script
|
||||
* Returns null if no such script can be found
|
||||
*/
|
||||
getRunningScript(scriptName: string, scriptArgs: ScriptArg[]): RunningScript | null {
|
||||
/** Find an actively running script on this server by filepath and args. */
|
||||
getRunningScript(path: ScriptFilePath, scriptArgs: ScriptArg[]): RunningScript | null {
|
||||
for (const rs of this.runningScripts) {
|
||||
//compare file names without leading '/' to prevent running multiple script with the same name
|
||||
if (
|
||||
(rs.filename.charAt(0) == "/" ? rs.filename.slice(1) : rs.filename) ===
|
||||
(scriptName.charAt(0) == "/" ? scriptName.slice(1) : scriptName) &&
|
||||
compareArrays(rs.args, scriptArgs)
|
||||
) {
|
||||
return rs;
|
||||
}
|
||||
if (rs.filename === path && compareArrays(rs.args, scriptArgs)) return rs;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the name of the script, returns the corresponding
|
||||
* Script object on the server (if it exists)
|
||||
*/
|
||||
getScript(scriptName: string): Script | null {
|
||||
return this.scripts.get(scriptName) ?? null;
|
||||
/** Get a TextFile or Script depending on the input path type. */
|
||||
getContentFile(path: ContentFilePath): ContentFile | null {
|
||||
return (hasTextExtension(path) ? this.textFiles.get(path) : this.scripts.get(path)) ?? null;
|
||||
}
|
||||
|
||||
/** Returns boolean indicating whether the given script is running on this server */
|
||||
@ -180,51 +169,44 @@ export abstract class BaseServer implements IServer {
|
||||
|
||||
/**
|
||||
* Remove a file from the server
|
||||
* @param filename {string} Name of file to be deleted
|
||||
* @param path Name of file to be deleted
|
||||
* @returns {IReturnStatus} Return status object indicating whether or not file was deleted
|
||||
*/
|
||||
removeFile(filename: string): IReturnStatus {
|
||||
if (filename.endsWith(".exe") || filename.match(/^.+\.exe-\d+(?:\.\d*)?%-INC$/) != null) {
|
||||
for (let i = 0; i < this.programs.length; ++i) {
|
||||
if (this.programs[i] === filename) {
|
||||
this.programs.splice(i, 1);
|
||||
return { res: true };
|
||||
}
|
||||
}
|
||||
} else if (isScriptFilename(filename)) {
|
||||
const script = this.scripts.get(filename);
|
||||
if (!script) return { res: false, msg: `script ${filename} not found.` };
|
||||
if (this.isRunning(filename)) {
|
||||
return { res: false, msg: "Cannot delete a script that is currently running!" };
|
||||
}
|
||||
script.invalidateModule();
|
||||
this.scripts.delete(filename);
|
||||
removeFile(path: FilePath): IReturnStatus {
|
||||
if (hasTextExtension(path)) {
|
||||
const textFile = this.textFiles.get(path);
|
||||
if (!textFile) return { res: false, msg: `Text file ${path} not found.` };
|
||||
this.textFiles.delete(path);
|
||||
return { res: true };
|
||||
}
|
||||
if (hasScriptExtension(path)) {
|
||||
const script = this.scripts.get(path);
|
||||
if (!script) return { res: false, msg: `Script ${path} not found.` };
|
||||
if (this.isRunning(path)) return { res: false, msg: "Cannot delete a script that is currently running!" };
|
||||
script.invalidateModule();
|
||||
this.scripts.delete(path);
|
||||
return { res: true };
|
||||
}
|
||||
if (hasProgramExtension(path)) {
|
||||
const programIndex = this.programs.findIndex((program) => program === path);
|
||||
if (programIndex === -1) return { res: false, msg: `Program ${path} does not exist` };
|
||||
this.programs.splice(programIndex, 1);
|
||||
return { res: true };
|
||||
}
|
||||
if (path.endsWith(".lit")) {
|
||||
const litIndex = this.messages.findIndex((lit) => lit === path);
|
||||
if (litIndex === -1) return { res: false, msg: `Literature file ${path} does not exist` };
|
||||
this.messages.splice(litIndex, 1);
|
||||
return { res: true };
|
||||
}
|
||||
if (path.endsWith(".cct")) {
|
||||
const contractIndex = this.contracts.findIndex((program) => program);
|
||||
if (contractIndex === -1) return { res: false, msg: `Contract file ${path} does not exist` };
|
||||
this.contracts.splice(contractIndex, 1);
|
||||
return { res: true };
|
||||
} else if (filename.endsWith(".lit")) {
|
||||
for (let i = 0; i < this.messages.length; ++i) {
|
||||
const f = this.messages[i];
|
||||
if (typeof f === "string" && f === filename) {
|
||||
this.messages.splice(i, 1);
|
||||
return { res: true };
|
||||
}
|
||||
}
|
||||
} else if (filename.endsWith(".txt")) {
|
||||
for (let i = 0; i < this.textFiles.length; ++i) {
|
||||
if (this.textFiles[i].fn === filename) {
|
||||
this.textFiles.splice(i, 1);
|
||||
return { res: true };
|
||||
}
|
||||
}
|
||||
} else if (filename.endsWith(".cct")) {
|
||||
for (let i = 0; i < this.contracts.length; ++i) {
|
||||
if (this.contracts[i].fn === filename) {
|
||||
this.contracts.splice(i, 1);
|
||||
return { res: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { res: false, msg: "No such file exists" };
|
||||
return { res: false, msg: `Unhandled file extension on file path ${path}` };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -245,15 +227,13 @@ export abstract class BaseServer implements IServer {
|
||||
this.ramUsed = ram;
|
||||
}
|
||||
|
||||
pushProgram(program: string): void {
|
||||
pushProgram(program: ProgramFilePath | CompletedProgramName): void {
|
||||
if (this.programs.includes(program)) return;
|
||||
|
||||
// Remove partially created program if there is one
|
||||
const existingPartialExeIndex = this.programs.findIndex((p) => p.startsWith(program));
|
||||
// findIndex returns -1 if there is no match, we only want to splice on a match
|
||||
if (existingPartialExeIndex > -1) {
|
||||
this.programs.splice(existingPartialExeIndex, 1);
|
||||
}
|
||||
if (existingPartialExeIndex > -1) this.programs.splice(existingPartialExeIndex, 1);
|
||||
|
||||
this.programs.push(program);
|
||||
}
|
||||
@ -262,47 +242,41 @@ export abstract class BaseServer implements IServer {
|
||||
* Write to a script file
|
||||
* Overwrites existing files. Creates new files if the script does not exist.
|
||||
*/
|
||||
writeToScriptFile(filename: string, code: string): writeResult {
|
||||
if (!isValidFilePath(filename) || !isScriptFilename(filename)) {
|
||||
return { success: false, overwritten: false };
|
||||
}
|
||||
|
||||
writeToScriptFile(filename: ScriptFilePath, code: string): writeResult {
|
||||
// Check if the script already exists, and overwrite it if it does
|
||||
const script = this.scripts.get(filename);
|
||||
if (script) {
|
||||
script.invalidateModule();
|
||||
script.code = code;
|
||||
return { success: true, overwritten: true };
|
||||
// content setter handles module invalidation and code formatting
|
||||
script.content = code;
|
||||
return { overwritten: true };
|
||||
}
|
||||
|
||||
// Otherwise, create a new script
|
||||
const newScript = new Script(filename, code, this.hostname);
|
||||
this.scripts.set(filename, newScript);
|
||||
return { success: true, overwritten: false };
|
||||
return { overwritten: false };
|
||||
}
|
||||
|
||||
// Write to a text file
|
||||
// Overwrites existing files. Creates new files if the text file does not exist
|
||||
writeToTextFile(fn: string, txt: string): writeResult {
|
||||
const ret = { success: false, overwritten: false };
|
||||
if (!isValidFilePath(fn) || !fn.endsWith("txt")) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
writeToTextFile(textPath: TextFilePath, txt: string): writeResult {
|
||||
// Check if the text file already exists, and overwrite if it does
|
||||
for (let i = 0; i < this.textFiles.length; ++i) {
|
||||
if (this.textFiles[i].fn === fn) {
|
||||
ret.overwritten = true;
|
||||
this.textFiles[i].text = txt;
|
||||
ret.success = true;
|
||||
return ret;
|
||||
}
|
||||
const existingFile = this.textFiles.get(textPath);
|
||||
// overWrite if already exists
|
||||
if (existingFile) {
|
||||
existingFile.text = txt;
|
||||
return { overwritten: true };
|
||||
}
|
||||
|
||||
// Otherwise create a new text file
|
||||
const newFile = new TextFile(fn, txt);
|
||||
this.textFiles.push(newFile);
|
||||
ret.success = true;
|
||||
return ret;
|
||||
const newFile = new TextFile(textPath, txt);
|
||||
this.textFiles.set(textPath, newFile);
|
||||
return { overwritten: false };
|
||||
}
|
||||
|
||||
/** Write to a Script or TextFile */
|
||||
writeToContentFile(path: ContentFilePath, content: string): writeResult {
|
||||
if (hasTextExtension(path)) return this.writeToTextFile(path, content);
|
||||
return this.writeToScriptFile(path, content);
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import { calculateServerGrowth } from "./formulas/grow";
|
||||
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
|
||||
import { CONSTANTS } from "../Constants";
|
||||
import { Player } from "@player";
|
||||
import { Programs } from "../Programs/Programs";
|
||||
import { LiteratureNames } from "../Literature/data/LiteratureNames";
|
||||
import { CompletedProgramName } from "../Programs/Programs";
|
||||
import { LiteratureName } from "../Literature/data/LiteratureNames";
|
||||
import { Person as IPerson } from "@nsdefs";
|
||||
import { isValidNumber } from "../utils/helpers/isValidNumber";
|
||||
import { Server as IServer } from "@nsdefs";
|
||||
@ -249,20 +249,20 @@ export function processSingleServerGrowth(server: Server, threads: number, cores
|
||||
}
|
||||
|
||||
export function prestigeHomeComputer(homeComp: Server): void {
|
||||
const hasBitflume = homeComp.programs.includes(Programs.BitFlume.name);
|
||||
const hasBitflume = homeComp.programs.includes(CompletedProgramName.bitFlume);
|
||||
|
||||
homeComp.programs.length = 0; //Remove programs
|
||||
homeComp.runningScripts = [];
|
||||
homeComp.serversOnNetwork = [];
|
||||
homeComp.isConnectedTo = true;
|
||||
homeComp.ramUsed = 0;
|
||||
homeComp.programs.push(Programs.NukeProgram.name);
|
||||
homeComp.programs.push(CompletedProgramName.nuke);
|
||||
if (hasBitflume) {
|
||||
homeComp.programs.push(Programs.BitFlume.name);
|
||||
homeComp.programs.push(CompletedProgramName.bitFlume);
|
||||
}
|
||||
|
||||
homeComp.messages.length = 0; //Remove .lit and .msg files
|
||||
homeComp.messages.push(LiteratureNames.HackersStartingHandbook);
|
||||
homeComp.messages.push(LiteratureName.HackersStartingHandbook);
|
||||
}
|
||||
|
||||
// Returns the i-th server on the specified server's network
|
||||
|
@ -4,8 +4,9 @@ import { FactionNames } from "../../Faction/data/FactionNames";
|
||||
// This could actually be a JSON file as it should be constant metadata to be imported...
|
||||
import { IMinMaxRange } from "../../types";
|
||||
import { LocationName } from "../../Enums";
|
||||
import { LiteratureNames } from "../../Literature/data/LiteratureNames";
|
||||
import { LiteratureName } from "../../Literature/data/LiteratureNames";
|
||||
import { SpecialServers } from "./SpecialServers";
|
||||
import { ServerName } from "../../Types/strings";
|
||||
|
||||
/**
|
||||
* The metadata describing the base state of servers on the network.
|
||||
@ -16,10 +17,10 @@ interface IServerMetadata {
|
||||
hackDifficulty?: number | IMinMaxRange;
|
||||
|
||||
/** The DNS name of the server. */
|
||||
hostname: string;
|
||||
hostname: ServerName;
|
||||
|
||||
/** When populated, the files will be added to the server when created. */
|
||||
literature?: string[];
|
||||
literature?: LiteratureName[];
|
||||
|
||||
/**
|
||||
* When populated, the exponent of 2^x amount of RAM the server has.
|
||||
@ -118,7 +119,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 88,
|
||||
},
|
||||
hostname: "blade",
|
||||
literature: [LiteratureNames.BeyondMan],
|
||||
literature: [LiteratureName.BeyondMan],
|
||||
maxRamExponent: {
|
||||
max: 9,
|
||||
min: 5,
|
||||
@ -143,7 +144,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
{
|
||||
hackDifficulty: 99,
|
||||
hostname: LocationName.VolhavenNWO.toLowerCase(),
|
||||
literature: [LiteratureNames.TheHiddenWorld],
|
||||
literature: [LiteratureName.TheHiddenWorld],
|
||||
moneyAvailable: {
|
||||
max: 40e9,
|
||||
min: 20e9,
|
||||
@ -167,7 +168,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 45,
|
||||
},
|
||||
hostname: "clarkinc",
|
||||
literature: [LiteratureNames.BeyondMan, LiteratureNames.CostOfImmortality],
|
||||
literature: [LiteratureName.BeyondMan, LiteratureName.CostOfImmortality],
|
||||
moneyAvailable: {
|
||||
max: 25e9,
|
||||
min: 15e9,
|
||||
@ -191,7 +192,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 90,
|
||||
},
|
||||
hostname: "omnitek",
|
||||
literature: [LiteratureNames.CodedIntelligence, LiteratureNames.HistoryOfSynthoids],
|
||||
literature: [LiteratureName.CodedIntelligence, LiteratureName.HistoryOfSynthoids],
|
||||
maxRamExponent: {
|
||||
max: 9,
|
||||
min: 7,
|
||||
@ -265,7 +266,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 83,
|
||||
},
|
||||
hostname: "fulcrumtech",
|
||||
literature: [LiteratureNames.SimulatedReality],
|
||||
literature: [LiteratureName.SimulatedReality],
|
||||
maxRamExponent: {
|
||||
max: 11,
|
||||
min: 7,
|
||||
@ -375,7 +376,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 85,
|
||||
},
|
||||
hostname: "helios",
|
||||
literature: [LiteratureNames.BeyondMan],
|
||||
literature: [LiteratureName.BeyondMan],
|
||||
maxRamExponent: {
|
||||
max: 8,
|
||||
min: 5,
|
||||
@ -403,7 +404,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 80,
|
||||
},
|
||||
hostname: LocationName.NewTokyoVitaLife.toLowerCase(),
|
||||
literature: [LiteratureNames.AGreenTomorrow],
|
||||
literature: [LiteratureName.AGreenTomorrow],
|
||||
maxRamExponent: {
|
||||
max: 7,
|
||||
min: 4,
|
||||
@ -481,7 +482,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 70,
|
||||
},
|
||||
hostname: "titan-labs",
|
||||
literature: [LiteratureNames.CodedIntelligence],
|
||||
literature: [LiteratureName.CodedIntelligence],
|
||||
maxRamExponent: {
|
||||
max: 7,
|
||||
min: 4,
|
||||
@ -508,7 +509,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 65,
|
||||
},
|
||||
hostname: "microdyne",
|
||||
literature: [LiteratureNames.SyntheticMuscles],
|
||||
literature: [LiteratureName.SyntheticMuscles],
|
||||
maxRamExponent: {
|
||||
max: 6,
|
||||
min: 4,
|
||||
@ -535,7 +536,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 70,
|
||||
},
|
||||
hostname: "taiyang-digital",
|
||||
literature: [LiteratureNames.AGreenTomorrow, LiteratureNames.BrighterThanTheSun],
|
||||
literature: [LiteratureName.AGreenTomorrow, LiteratureName.BrighterThanTheSun],
|
||||
moneyAvailable: {
|
||||
max: 900000000,
|
||||
min: 800000000,
|
||||
@ -581,7 +582,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 80,
|
||||
},
|
||||
hostname: LocationName.AevumAeroCorp.toLowerCase(),
|
||||
literature: [LiteratureNames.ManAndMachine],
|
||||
literature: [LiteratureName.ManAndMachine],
|
||||
moneyAvailable: {
|
||||
max: 1200000000,
|
||||
min: 1000000000,
|
||||
@ -605,7 +606,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 85,
|
||||
},
|
||||
hostname: "omnia",
|
||||
literature: [LiteratureNames.HistoryOfSynthoids],
|
||||
literature: [LiteratureName.HistoryOfSynthoids],
|
||||
maxRamExponent: {
|
||||
max: 6,
|
||||
min: 4,
|
||||
@ -633,7 +634,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 55,
|
||||
},
|
||||
hostname: "zb-def",
|
||||
literature: [LiteratureNames.SyntheticMuscles],
|
||||
literature: [LiteratureName.SyntheticMuscles],
|
||||
moneyAvailable: {
|
||||
max: 1100000000,
|
||||
min: 900000000,
|
||||
@ -678,7 +679,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 70,
|
||||
},
|
||||
hostname: "solaris",
|
||||
literature: [LiteratureNames.AGreenTomorrow, LiteratureNames.TheFailedFrontier],
|
||||
literature: [LiteratureName.AGreenTomorrow, LiteratureName.TheFailedFrontier],
|
||||
maxRamExponent: {
|
||||
max: 7,
|
||||
min: 4,
|
||||
@ -729,7 +730,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 75,
|
||||
},
|
||||
hostname: "global-pharm",
|
||||
literature: [LiteratureNames.AGreenTomorrow],
|
||||
literature: [LiteratureName.AGreenTomorrow],
|
||||
maxRamExponent: {
|
||||
max: 6,
|
||||
min: 3,
|
||||
@ -882,7 +883,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 50,
|
||||
},
|
||||
hostname: "alpha-ent",
|
||||
literature: [LiteratureNames.Sector12Crime],
|
||||
literature: [LiteratureName.Sector12Crime],
|
||||
maxRamExponent: {
|
||||
max: 7,
|
||||
min: 4,
|
||||
@ -937,11 +938,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 45,
|
||||
},
|
||||
hostname: "rothman-uni",
|
||||
literature: [
|
||||
LiteratureNames.SecretSocieties,
|
||||
LiteratureNames.TheFailedFrontier,
|
||||
LiteratureNames.TensionsInTechRace,
|
||||
],
|
||||
literature: [LiteratureName.SecretSocieties, LiteratureName.TheFailedFrontier, LiteratureName.TensionsInTechRace],
|
||||
maxRamExponent: {
|
||||
max: 7,
|
||||
min: 4,
|
||||
@ -996,7 +993,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 45,
|
||||
},
|
||||
hostname: "summit-uni",
|
||||
literature: [LiteratureNames.SecretSocieties, LiteratureNames.TheFailedFrontier, LiteratureNames.SyntheticMuscles],
|
||||
literature: [LiteratureName.SecretSocieties, LiteratureName.TheFailedFrontier, LiteratureName.SyntheticMuscles],
|
||||
maxRamExponent: {
|
||||
max: 6,
|
||||
min: 4,
|
||||
@ -1047,7 +1044,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 60,
|
||||
},
|
||||
hostname: "catalyst",
|
||||
literature: [LiteratureNames.TensionsInTechRace],
|
||||
literature: [LiteratureName.TensionsInTechRace],
|
||||
maxRamExponent: {
|
||||
max: 7,
|
||||
min: 4,
|
||||
@ -1100,7 +1097,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 55,
|
||||
},
|
||||
hostname: LocationName.VolhavenCompuTek.toLowerCase(),
|
||||
literature: [LiteratureNames.ManAndMachine],
|
||||
literature: [LiteratureName.ManAndMachine],
|
||||
moneyAvailable: {
|
||||
max: 250000000,
|
||||
min: 220000000,
|
||||
@ -1124,7 +1121,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 60,
|
||||
},
|
||||
hostname: "netlink",
|
||||
literature: [LiteratureNames.SimulatedReality],
|
||||
literature: [LiteratureName.SimulatedReality],
|
||||
maxRamExponent: {
|
||||
max: 7,
|
||||
min: 4,
|
||||
@ -1181,7 +1178,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
{
|
||||
hackDifficulty: 10,
|
||||
hostname: LocationName.Sector12FoodNStuff.toLowerCase(),
|
||||
literature: [LiteratureNames.Sector12Crime],
|
||||
literature: [LiteratureName.Sector12Crime],
|
||||
maxRamExponent: 4,
|
||||
moneyAvailable: 2000000,
|
||||
networkLayer: 1,
|
||||
@ -1239,7 +1236,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
{
|
||||
hackDifficulty: 25,
|
||||
hostname: "neo-net",
|
||||
literature: [LiteratureNames.TheHiddenWorld],
|
||||
literature: [LiteratureName.TheHiddenWorld],
|
||||
maxRamExponent: 5,
|
||||
moneyAvailable: 5000000,
|
||||
networkLayer: 3,
|
||||
@ -1251,7 +1248,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
{
|
||||
hackDifficulty: 30,
|
||||
hostname: "silver-helix",
|
||||
literature: [LiteratureNames.NewTriads],
|
||||
literature: [LiteratureName.NewTriads],
|
||||
maxRamExponent: 6,
|
||||
moneyAvailable: 45000000,
|
||||
networkLayer: 3,
|
||||
@ -1263,7 +1260,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
{
|
||||
hackDifficulty: 15,
|
||||
hostname: "hong-fang-tea",
|
||||
literature: [LiteratureNames.BrighterThanTheSun],
|
||||
literature: [LiteratureName.BrighterThanTheSun],
|
||||
maxRamExponent: 4,
|
||||
moneyAvailable: 3000000,
|
||||
networkLayer: 1,
|
||||
@ -1311,7 +1308,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
min: 25,
|
||||
},
|
||||
hostname: "omega-net",
|
||||
literature: [LiteratureNames.TheNewGod],
|
||||
literature: [LiteratureName.TheNewGod],
|
||||
maxRamExponent: 5,
|
||||
moneyAvailable: {
|
||||
max: 70000000,
|
||||
@ -1436,7 +1433,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
{
|
||||
hackDifficulty: 0,
|
||||
hostname: "run4theh111z",
|
||||
literature: [LiteratureNames.SimulatedReality, LiteratureNames.TheNewGod],
|
||||
literature: [LiteratureName.SimulatedReality, LiteratureName.TheNewGod],
|
||||
maxRamExponent: {
|
||||
max: 9,
|
||||
min: 5,
|
||||
@ -1455,7 +1452,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
{
|
||||
hackDifficulty: 0,
|
||||
hostname: "I.I.I.I",
|
||||
literature: [LiteratureNames.DemocracyIsDead],
|
||||
literature: [LiteratureName.DemocracyIsDead],
|
||||
maxRamExponent: {
|
||||
max: 8,
|
||||
min: 4,
|
||||
@ -1474,7 +1471,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
{
|
||||
hackDifficulty: 0,
|
||||
hostname: "avmnite-02h",
|
||||
literature: [LiteratureNames.DemocracyIsDead],
|
||||
literature: [LiteratureName.DemocracyIsDead],
|
||||
maxRamExponent: {
|
||||
max: 7,
|
||||
min: 4,
|
||||
@ -1508,7 +1505,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
{
|
||||
hackDifficulty: 0,
|
||||
hostname: "CSEC",
|
||||
literature: [LiteratureNames.DemocracyIsDead],
|
||||
literature: [LiteratureName.DemocracyIsDead],
|
||||
maxRamExponent: 3,
|
||||
moneyAvailable: 0,
|
||||
networkLayer: 2,
|
||||
@ -1524,7 +1521,7 @@ export const serverMetadata: IServerMetadata[] = [
|
||||
{
|
||||
hackDifficulty: 0,
|
||||
hostname: "The-Cave",
|
||||
literature: [LiteratureNames.AlphaOmega],
|
||||
literature: [LiteratureName.AlphaOmega],
|
||||
moneyAvailable: 0,
|
||||
networkLayer: 15,
|
||||
numOpenPortsRequired: 5,
|
||||
|
@ -1,316 +0,0 @@
|
||||
/**
|
||||
* Helper functions that implement "directory" functionality in the Terminal.
|
||||
* These aren't "real" directories, it's more of a pseudo-directory implementation
|
||||
* that uses mainly string manipulation.
|
||||
*
|
||||
* This file contains functions that deal only with that string manipulation.
|
||||
* Functions that need to access/process Server-related things can be
|
||||
* found in ./DirectoryServerHelpers.ts
|
||||
*/
|
||||
|
||||
/** Removes leading forward slash ("/") from a string. */
|
||||
export function removeLeadingSlash(s: string): string {
|
||||
if (s.startsWith("/")) {
|
||||
return s.slice(1);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes trailing forward slash ("/") from a string.
|
||||
* Note that this will also remove the slash if it is the leading slash (i.e. if s = "/")
|
||||
*/
|
||||
export function removeTrailingSlash(s: string): string {
|
||||
if (s.endsWith("/")) {
|
||||
return s.slice(0, -1);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a string is a valid filename. Only used for the filename itself,
|
||||
* not the entire filepath
|
||||
*/
|
||||
export function isValidFilename(filename: string): boolean {
|
||||
// Allows alphanumerics, hyphens, underscores, and percentage signs
|
||||
// Must have a file extension
|
||||
const regex = /^[.&'a-zA-Z0-9_-]+\.[a-zA-Z0-9]+(?:-\d+(?:\.\d*)?%-INC)?$/;
|
||||
|
||||
// match() returns null if no match is found
|
||||
return filename.match(regex) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a string is a valid directory name. Only used for the directory itself,
|
||||
* not an entire path
|
||||
*/
|
||||
export function isValidDirectoryName(name: string): boolean {
|
||||
// A valid directory name:
|
||||
// Must be at least 1 character long
|
||||
// Can only include characters in the character set [-.%a-zA-Z0-9_]
|
||||
// Cannot end with a '.'
|
||||
const regex = /^.?[a-zA-Z0-9_-]+$/;
|
||||
|
||||
// match() returns null if no match is found
|
||||
return name.match(regex) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a string is a valid directory path.
|
||||
* This only checks if it has the proper formatting. It does NOT check
|
||||
* if the directories actually exist on Terminal
|
||||
*/
|
||||
export function isValidDirectoryPath(path: string): boolean {
|
||||
let t_path: string = path;
|
||||
|
||||
if (t_path.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (t_path.length === 1) {
|
||||
return t_path === "/";
|
||||
}
|
||||
|
||||
// A full path must have a leading slash, but we'll ignore it for the checks
|
||||
if (t_path.startsWith("/")) {
|
||||
t_path = t_path.slice(1);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Trailing slash does not matter
|
||||
t_path = removeTrailingSlash(t_path);
|
||||
|
||||
// Check that every section of the path is a valid directory name
|
||||
const dirs = t_path.split("/");
|
||||
for (const dir of dirs) {
|
||||
// Special case, "." and ".." are valid for path
|
||||
if (dir === "." || dir === "..") {
|
||||
continue;
|
||||
}
|
||||
if (!isValidDirectoryName(dir)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a string is a valid file path. This only checks if it has the
|
||||
* proper formatting. It dose NOT check if the file actually exists on Terminal
|
||||
*/
|
||||
export function isValidFilePath(path: string): boolean {
|
||||
if (path == null || typeof path !== "string") {
|
||||
return false;
|
||||
}
|
||||
const t_path = path;
|
||||
|
||||
// Impossible for filename to have less than length of 3
|
||||
if (t_path.length < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Full filepath can't end with trailing slash because it must be a file
|
||||
if (t_path.endsWith("/")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Everything after the last forward slash is the filename. Everything before
|
||||
// it is the file path
|
||||
const fnSeparator = t_path.lastIndexOf("/");
|
||||
if (fnSeparator === -1) {
|
||||
return isValidFilename(t_path);
|
||||
}
|
||||
|
||||
const fn = t_path.slice(fnSeparator + 1);
|
||||
const dirPath = t_path.slice(0, fnSeparator + 1);
|
||||
|
||||
return isValidDirectoryPath(dirPath) && isValidFilename(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted string for the first parent directory in a filepath. For example:
|
||||
* /home/var/test/ -> home/
|
||||
* If there is no first parent directory, then it returns "/" for root
|
||||
*/
|
||||
export function getFirstParentDirectory(path: string): string {
|
||||
let t_path = path;
|
||||
t_path = removeLeadingSlash(t_path);
|
||||
t_path = removeTrailingSlash(t_path);
|
||||
|
||||
if (t_path.lastIndexOf("/") === -1) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
const dirs = t_path.split("/");
|
||||
if (dirs.length === 0) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
return dirs[0] + "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a filepath, returns a formatted string for all of the parent directories
|
||||
* in that filepath. For example:
|
||||
* /home/var/tes -> home/var/
|
||||
* /home/var/test/ -> home/var/test/
|
||||
* If there are no parent directories, it returns the empty string
|
||||
*/
|
||||
export function getAllParentDirectories(path: string): string {
|
||||
const t_path = path;
|
||||
const lastSlash = t_path.lastIndexOf("/");
|
||||
if (lastSlash === -1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return t_path.slice(0, lastSlash + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a destination that only contains a directory part, returns the
|
||||
* path to the source filename inside the new destination directory.
|
||||
* Otherwise, returns the path to the destination file.
|
||||
* @param destination The destination path or file name
|
||||
* @param source The source path
|
||||
* @param cwd The current working directory
|
||||
* @returns A file path which may be absolute or relative
|
||||
*/
|
||||
export function getDestinationFilepath(destination: string, source: string, cwd: string): string {
|
||||
const dstDir = evaluateDirectoryPath(destination, cwd);
|
||||
// If evaluating the directory for this destination fails, we have a filename or full path.
|
||||
if (dstDir === null) {
|
||||
return destination;
|
||||
} else {
|
||||
// Append the filename to the directory provided.
|
||||
const t_path = removeTrailingSlash(dstDir);
|
||||
const fileName = getFileName(source);
|
||||
return t_path + "/" + fileName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a filepath, returns the file name (e.g. without directory parts)
|
||||
* For example:
|
||||
* /home/var/test.js -> test.js
|
||||
* ./var/test.js -> test.js
|
||||
* test.js -> test.js
|
||||
*/
|
||||
export function getFileName(path: string): string {
|
||||
const t_path = path;
|
||||
const lastSlash = t_path.lastIndexOf("/");
|
||||
if (lastSlash === -1) {
|
||||
return t_path;
|
||||
}
|
||||
|
||||
return t_path.slice(lastSlash + 1);
|
||||
}
|
||||
|
||||
/** Checks if a file path refers to a file in the root directory. */
|
||||
export function isInRootDirectory(path: string): boolean {
|
||||
if (!isValidFilePath(path)) {
|
||||
return false;
|
||||
}
|
||||
if (path == null || path.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return path.lastIndexOf("/") <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a directory path, including the processing of linux dots.
|
||||
* Returns the full, proper path, or null if an invalid path is passed in
|
||||
*/
|
||||
export function evaluateDirectoryPath(path: string, currPath?: string): string | null {
|
||||
let t_path = path;
|
||||
|
||||
// If the path begins with a slash, then its an absolute path. Otherwise its relative
|
||||
// For relative paths, we need to prepend the current directory
|
||||
if (!t_path.startsWith("/") && currPath) {
|
||||
t_path = currPath + (currPath.endsWith("/") ? "" : "/") + t_path;
|
||||
}
|
||||
|
||||
if (!isValidDirectoryPath(t_path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Trim leading/trailing slashes
|
||||
t_path = removeLeadingSlash(t_path);
|
||||
t_path = removeTrailingSlash(t_path);
|
||||
|
||||
const dirs = t_path.split("/");
|
||||
const reconstructedPath: string[] = [];
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (dir === ".") {
|
||||
// Current directory, do nothing
|
||||
continue;
|
||||
} else if (dir === "..") {
|
||||
// Parent directory
|
||||
const res = reconstructedPath.pop();
|
||||
if (res == null) {
|
||||
return null; // Array was empty, invalid path
|
||||
}
|
||||
} else {
|
||||
reconstructedPath.push(dir);
|
||||
}
|
||||
}
|
||||
|
||||
return "/" + reconstructedPath.join("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a file path, including the processing of linux dots.
|
||||
* Returns the full, proper path, or null if an invalid path is passed in
|
||||
*/
|
||||
export function evaluateFilePath(path: string, currPath?: string): string | null {
|
||||
let t_path = path;
|
||||
|
||||
// If the path begins with a slash, then its an absolute path. Otherwise its relative
|
||||
// For relative paths, we need to prepend the current directory
|
||||
if (!t_path.startsWith("/") && currPath != null) {
|
||||
t_path = currPath + (currPath.endsWith("/") ? "" : "/") + t_path;
|
||||
}
|
||||
|
||||
if (!isValidFilePath(t_path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Trim leading/trailing slashes
|
||||
t_path = removeLeadingSlash(t_path);
|
||||
|
||||
const dirs = t_path.split("/");
|
||||
const reconstructedPath: string[] = [];
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (dir === ".") {
|
||||
// Current directory, do nothing
|
||||
continue;
|
||||
} else if (dir === "..") {
|
||||
// Parent directory
|
||||
const res = reconstructedPath.pop();
|
||||
if (res == null) {
|
||||
return null; // Array was empty, invalid path
|
||||
}
|
||||
} else {
|
||||
reconstructedPath.push(dir);
|
||||
}
|
||||
}
|
||||
|
||||
return "/" + reconstructedPath.join("/");
|
||||
}
|
||||
|
||||
export function areFilesEqual(f0: string, f1: string): boolean {
|
||||
if (!f0.startsWith("/")) f0 = "/" + f0;
|
||||
if (!f1.startsWith("/")) f1 = "/" + f1;
|
||||
return f0 === f1;
|
||||
}
|
||||
|
||||
export function areImportsEquals(f0: string, f1: string): boolean {
|
||||
if (!f0.endsWith(".js")) f0 = f0 + ".js";
|
||||
if (!f1.endsWith(".js")) f1 = f1 + ".js";
|
||||
return areFilesEqual(f0, f1);
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
/**
|
||||
* Helper functions that implement "directory" functionality in the Terminal.
|
||||
* These aren't "real" directories, it's more of a pseudo-directory implementation
|
||||
* that uses mainly string manipulation.
|
||||
*
|
||||
* This file contains function that deal with Server-related directory things.
|
||||
* Functions that deal with the string manipulation can be found in
|
||||
* ./DirectoryHelpers.ts
|
||||
*/
|
||||
import { isValidDirectoryPath, isInRootDirectory, getFirstParentDirectory } from "./DirectoryHelpers";
|
||||
import { BaseServer } from "../Server/BaseServer";
|
||||
|
||||
/**
|
||||
* Given a directory (by the full directory path) and a server, returns all
|
||||
* subdirectories of that directory. This is only for FIRST-LEVEl/immediate subdirectories
|
||||
*/
|
||||
export function getSubdirectories(serv: BaseServer, dir: string): string[] {
|
||||
const res: string[] = [];
|
||||
|
||||
if (!isValidDirectoryPath(dir)) {
|
||||
return res;
|
||||
}
|
||||
|
||||
let t_dir = dir;
|
||||
if (!t_dir.endsWith("/")) {
|
||||
t_dir += "/";
|
||||
}
|
||||
|
||||
function processFile(fn: string): void {
|
||||
if (t_dir === "/" && isInRootDirectory(fn)) {
|
||||
const subdir = getFirstParentDirectory(fn);
|
||||
if (subdir !== "/" && !res.includes(subdir)) {
|
||||
res.push(subdir);
|
||||
}
|
||||
} else if (fn.startsWith(t_dir)) {
|
||||
const remaining = fn.slice(t_dir.length);
|
||||
const subdir = getFirstParentDirectory(remaining);
|
||||
if (subdir !== "/" && !res.includes(subdir)) {
|
||||
res.push(subdir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const scriptFilename of serv.scripts.keys()) {
|
||||
processFile(scriptFilename);
|
||||
}
|
||||
|
||||
for (const txt of serv.textFiles) {
|
||||
processFile(txt.fn);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/** Returns true, if the server's directory itself or one of its subdirectory contains files. */
|
||||
export function containsFiles(server: BaseServer, dir: string): boolean {
|
||||
const dirWithTrailingSlash = dir + (dir.slice(-1) === "/" ? "" : "/");
|
||||
|
||||
return [...server.scripts.keys(), ...server.textFiles.map((t) => t.fn)].some((filename) =>
|
||||
filename.startsWith(dirWithTrailingSlash),
|
||||
);
|
||||
}
|
@ -1,94 +1,36 @@
|
||||
import { KEY } from "../utils/helpers/keyCodes";
|
||||
import { substituteAliases } from "../Alias";
|
||||
// Helper function to parse individual arguments into number/boolean/string as appropriate
|
||||
function parseArg(arg: string, stringOverride: boolean): string | number | boolean {
|
||||
// Handles all numbers including hexadecimal, octal, and binary representations, returning NaN on an unparsable string
|
||||
if (stringOverride) return arg;
|
||||
const asNumber = Number(arg);
|
||||
if (!isNaN(asNumber)) {
|
||||
return asNumber;
|
||||
function parseArg(arg: string): string | number | boolean {
|
||||
if (arg === "true") return true;
|
||||
if (arg === "false") return false;
|
||||
const argAsNumber = Number(arg);
|
||||
if (!isNaN(argAsNumber)) return argAsNumber;
|
||||
// For quoted strings just return the inner string
|
||||
if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith("'") && arg.endsWith("'"))) {
|
||||
return arg.substring(1, arg.length - 1);
|
||||
}
|
||||
|
||||
if (arg === "true" || arg === "false") {
|
||||
return arg === "true";
|
||||
}
|
||||
|
||||
return arg;
|
||||
}
|
||||
|
||||
export function ParseCommands(commands: string): string[] {
|
||||
// Sanitize input
|
||||
commands = commands.trim();
|
||||
// Replace all extra whitespace in command with a single space
|
||||
commands = commands.replace(/\s\s+/g, " ");
|
||||
|
||||
const match = commands.match(/(?:'[^']*'|"[^"]*"|[^;"])*/g);
|
||||
if (!match) return [];
|
||||
// Split commands and execute sequentially
|
||||
const allCommands = match
|
||||
.map(substituteAliases)
|
||||
.map((c) => c.match(/(?:'[^']*'|"[^"]*"|[^;"])*/g))
|
||||
.flat();
|
||||
|
||||
const out: string[] = [];
|
||||
for (const c of allCommands) {
|
||||
if (c === null) continue;
|
||||
if (c.match(/^\s*$/)) {
|
||||
continue;
|
||||
} // Don't run commands that only have whitespace
|
||||
out.push(c.trim());
|
||||
}
|
||||
return out;
|
||||
/** Split a commands string (what is typed into the terminal) into multiple commands */
|
||||
export function splitCommands(commandString: string): string[] {
|
||||
const commandArray = commandString.match(/(?:'[^']*'|"[^"]*"|[^;"])*/g);
|
||||
if (!commandArray) return [];
|
||||
return commandArray.map((command) => command.trim());
|
||||
}
|
||||
|
||||
export function ParseCommand(command: string): (string | number | boolean)[] {
|
||||
let idx = 0;
|
||||
const args = [];
|
||||
|
||||
let lastQuote = "";
|
||||
|
||||
let arg = "";
|
||||
let stringOverride = false;
|
||||
|
||||
while (idx < command.length) {
|
||||
const c = command.charAt(idx);
|
||||
// If the current character is a backslash, add the next character verbatim to the argument
|
||||
if (c === "\\") {
|
||||
arg += command.charAt(++idx);
|
||||
|
||||
// If the current character is a single- or double-quote mark, add it to the current argument.
|
||||
} else if (c === KEY.DOUBLE_QUOTE || c === KEY.QUOTE) {
|
||||
stringOverride = true;
|
||||
// If we're currently in a quoted string argument and this quote mark is the same as the beginning,
|
||||
// the string is done
|
||||
if (lastQuote !== "" && c === lastQuote) {
|
||||
lastQuote = "";
|
||||
// Otherwise if we're not in a string argument, we've begun one
|
||||
} else if (lastQuote === "") {
|
||||
lastQuote = c;
|
||||
// Otherwise if we're in a string argument, add the current character to it
|
||||
} else {
|
||||
arg += c;
|
||||
}
|
||||
// If the current character is a space and we are not inside a string, parse the current argument
|
||||
// and start a new one
|
||||
} else if (c === KEY.SPACE && lastQuote === "") {
|
||||
args.push(parseArg(arg, stringOverride));
|
||||
|
||||
stringOverride = false;
|
||||
arg = "";
|
||||
} else {
|
||||
// Add the current character to the current argument
|
||||
arg += c;
|
||||
}
|
||||
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Add the last arg (if any)
|
||||
if (arg !== "") {
|
||||
args.push(parseArg(arg, stringOverride));
|
||||
}
|
||||
|
||||
return args;
|
||||
/** Split commands string while also applying aliases */
|
||||
export function parseCommands(commands: string): string[] {
|
||||
// Remove any unquoted whitespace longer than length 1
|
||||
commands = commands.replace(/(?:"[^"]+"|'[^']+'|\s{2,})+?/g, (match) => (match.startsWith(" ") ? " " : match));
|
||||
// Split the commands, apply aliases once, then split again and filter out empty strings.
|
||||
const commandsArr = splitCommands(commands).map(substituteAliases).flatMap(splitCommands).filter(Boolean);
|
||||
return commandsArr;
|
||||
}
|
||||
|
||||
export function parseCommand(command: string): (string | number | boolean)[] {
|
||||
const commandArgs = command.match(/(?:("[^"]+"|'[^']+'|[^\s]+))+?/g);
|
||||
if (!commandArgs) return [];
|
||||
const argsToReturn = commandArgs.map(parseArg);
|
||||
return argsToReturn;
|
||||
}
|
||||
|
@ -4,21 +4,20 @@ import { Player } from "@player";
|
||||
import { HacknetServer } from "../Hacknet/HacknetServer";
|
||||
import { BaseServer } from "../Server/BaseServer";
|
||||
import { Server } from "../Server/Server";
|
||||
import { Programs } from "../Programs/Programs";
|
||||
import { CompletedProgramName } from "../Programs/Programs";
|
||||
import { CodingContractResult } from "../CodingContracts";
|
||||
import { TerminalEvents, TerminalClearEvents } from "./TerminalEvents";
|
||||
|
||||
import { TextFile } from "../TextFile";
|
||||
import { Script } from "../Script/Script";
|
||||
import { isScriptFilename } from "../Script/isScriptFilename";
|
||||
import { hasScriptExtension } from "../Paths/ScriptFilePath";
|
||||
import { CONSTANTS } from "../Constants";
|
||||
import { GetServer, GetAllServers } from "../Server/AllServers";
|
||||
|
||||
import { removeLeadingSlash, isInRootDirectory, evaluateFilePath } from "./DirectoryHelpers";
|
||||
import { checkIfConnectedToDarkweb } from "../DarkWeb/DarkWeb";
|
||||
import { iTutorialNextStep, iTutorialSteps, ITutorial } from "../InteractiveTutorial";
|
||||
import { getServerOnNetwork, processSingleServerGrowth } from "../Server/ServerHelpers";
|
||||
import { ParseCommand, ParseCommands } from "./Parser";
|
||||
import { parseCommand, parseCommands } from "./Parser";
|
||||
import { SpecialServers } from "../Server/data/SpecialServers";
|
||||
import { Settings } from "../Settings/Settings";
|
||||
import { createProgressBarText } from "../utils/helpers/createProgressBarText";
|
||||
@ -77,6 +76,10 @@ import { apr1 } from "./commands/apr1";
|
||||
import { changelog } from "./commands/changelog";
|
||||
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
|
||||
import { Engine } from "../engine";
|
||||
import { Directory, resolveDirectory, root } from "../Paths/Directory";
|
||||
import { FilePath, isFilePath, resolveFilePath } from "../Paths/FilePath";
|
||||
import { hasTextExtension } from "../Paths/TextFilePath";
|
||||
import { ContractFilePath } from "src/Paths/ContractFilePath";
|
||||
|
||||
export class Terminal {
|
||||
// Flags to determine whether the player is currently running a hack or an analyze
|
||||
@ -92,9 +95,8 @@ export class Terminal {
|
||||
// True if a Coding Contract prompt is opened
|
||||
contractOpen = false;
|
||||
|
||||
// Full Path of current directory
|
||||
// Excludes the trailing forward slash
|
||||
currDir = "/";
|
||||
// Path of current directory
|
||||
currDir = "" as Directory;
|
||||
|
||||
process(cycles: number): void {
|
||||
if (this.action === null) return;
|
||||
@ -377,46 +379,40 @@ export class Terminal {
|
||||
}
|
||||
|
||||
getFile(filename: string): Script | TextFile | string | null {
|
||||
if (isScriptFilename(filename)) {
|
||||
return this.getScript(filename);
|
||||
}
|
||||
|
||||
if (filename.endsWith(".lit")) {
|
||||
return this.getLitFile(filename);
|
||||
}
|
||||
|
||||
if (filename.endsWith(".txt")) {
|
||||
return this.getTextFile(filename);
|
||||
}
|
||||
|
||||
if (hasScriptExtension(filename)) return this.getScript(filename);
|
||||
if (hasTextExtension(filename)) return this.getTextFile(filename);
|
||||
if (filename.endsWith(".lit")) return this.getLitFile(filename);
|
||||
return null;
|
||||
}
|
||||
|
||||
getFilepath(filename: string): string | null {
|
||||
const path = evaluateFilePath(filename, this.cwd());
|
||||
if (path === null || !isInRootDirectory(path)) return path;
|
||||
getFilepath(path: string, useAbsolute?: boolean): FilePath | null {
|
||||
// If path starts with a slash, consider it to be an absolute path
|
||||
if (useAbsolute || path.startsWith("/")) return resolveFilePath(path);
|
||||
// Otherwise, force path to be seen as relative to the current directory.
|
||||
path = "./" + path;
|
||||
return resolveFilePath(path, this.currDir);
|
||||
}
|
||||
|
||||
return removeLeadingSlash(path);
|
||||
getDirectory(path: string, useAbsolute?: boolean): Directory | null {
|
||||
// If path starts with a slash, consider it to be an absolute path
|
||||
if (useAbsolute || path.startsWith("/")) return resolveDirectory(path);
|
||||
// Otherwise, force path to be seen as relative to the current directory.
|
||||
path = "./" + path;
|
||||
return resolveDirectory(path, this.currDir);
|
||||
}
|
||||
|
||||
getScript(filename: string): Script | null {
|
||||
const server = Player.getCurrentServer();
|
||||
const filepath = this.getFilepath(filename);
|
||||
if (filepath === null) return null;
|
||||
if (!filepath || !hasScriptExtension(filepath)) return null;
|
||||
return server.scripts.get(filepath) ?? null;
|
||||
}
|
||||
|
||||
getTextFile(filename: string): TextFile | null {
|
||||
const s = Player.getCurrentServer();
|
||||
const server = Player.getCurrentServer();
|
||||
const filepath = this.getFilepath(filename);
|
||||
if (!filepath) return null;
|
||||
for (const txt of s.textFiles) {
|
||||
if (filepath === txt.fn) {
|
||||
return txt;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
if (!filepath || !hasTextExtension(filepath)) return null;
|
||||
return server.textFiles.get(filepath) ?? null;
|
||||
}
|
||||
|
||||
getLitFile(filename: string): string | null {
|
||||
@ -432,32 +428,30 @@ export class Terminal {
|
||||
return null;
|
||||
}
|
||||
|
||||
cwd(): string {
|
||||
cwd(): Directory {
|
||||
return this.currDir;
|
||||
}
|
||||
|
||||
setcwd(dir: string): void {
|
||||
setcwd(dir: Directory): void {
|
||||
this.currDir = dir;
|
||||
TerminalEvents.emit();
|
||||
}
|
||||
|
||||
async runContract(contractName: string): Promise<void> {
|
||||
async runContract(contractPath: ContractFilePath): Promise<void> {
|
||||
// There's already an opened contract
|
||||
if (this.contractOpen) {
|
||||
return this.error("There's already a Coding Contract in Progress");
|
||||
}
|
||||
|
||||
const serv = Player.getCurrentServer();
|
||||
const contract = serv.getContract(contractName);
|
||||
if (contract == null) {
|
||||
return this.error("No such contract");
|
||||
}
|
||||
const contract = serv.getContract(contractPath);
|
||||
if (!contract) return this.error("No such contract");
|
||||
|
||||
this.contractOpen = true;
|
||||
const res = await contract.prompt();
|
||||
|
||||
//Check if the contract still exists by the time the promise is fulfilled
|
||||
if (serv.getContract(contractName) == null) {
|
||||
if (serv.getContract(contractPath) == null) {
|
||||
this.contractOpen = false;
|
||||
return this.error("Contract no longer exists (Was it solved by a script?)");
|
||||
}
|
||||
@ -529,7 +523,7 @@ export class Terminal {
|
||||
continue;
|
||||
} // Don't print current server
|
||||
const titleDashes = Array((d - 1) * 4 + 1).join("-");
|
||||
if (Player.hasProgram(Programs.AutoLink.name)) {
|
||||
if (Player.hasProgram(CompletedProgramName.autoLink)) {
|
||||
this.append(new Link(titleDashes, s.hostname));
|
||||
} else {
|
||||
this.print(titleDashes + s.hostname);
|
||||
@ -564,17 +558,13 @@ export class Terminal {
|
||||
Player.currentServer = serv.hostname;
|
||||
Player.getCurrentServer().isConnectedTo = true;
|
||||
this.print("Connected to " + serv.hostname);
|
||||
this.setcwd("/");
|
||||
this.setcwd(root);
|
||||
if (Player.getCurrentServer().hostname == "darkweb") {
|
||||
checkIfConnectedToDarkweb(); // Posts a 'help' message if connecting to dark web
|
||||
}
|
||||
}
|
||||
|
||||
executeCommands(commands: string): void {
|
||||
// Sanitize input
|
||||
commands = commands.trim();
|
||||
commands = commands.replace(/\s\s+/g, " "); // Replace all extra whitespace in command with a single space
|
||||
|
||||
// Handle Terminal History - multiple commands should be saved as one
|
||||
if (this.commandHistory[this.commandHistory.length - 1] != commands) {
|
||||
this.commandHistory.push(commands);
|
||||
@ -584,11 +574,8 @@ export class Terminal {
|
||||
Player.terminalCommandHistory = this.commandHistory;
|
||||
}
|
||||
this.commandHistoryIndex = this.commandHistory.length;
|
||||
const allCommands = ParseCommands(commands);
|
||||
|
||||
for (let i = 0; i < allCommands.length; i++) {
|
||||
this.executeCommand(allCommands[i]);
|
||||
}
|
||||
const allCommands = parseCommands(commands);
|
||||
for (const command of allCommands) this.executeCommand(command);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
@ -603,20 +590,12 @@ export class Terminal {
|
||||
}
|
||||
|
||||
executeCommand(command: string): void {
|
||||
if (this.action !== null) {
|
||||
this.error(`Cannot execute command (${command}) while an action is in progress`);
|
||||
return;
|
||||
}
|
||||
// Allow usage of ./
|
||||
if (command.startsWith("./")) {
|
||||
command = "run " + command.slice(2);
|
||||
}
|
||||
// Only split the first space
|
||||
const commandArray = ParseCommand(command);
|
||||
if (commandArray.length == 0) {
|
||||
return;
|
||||
}
|
||||
const s = Player.getCurrentServer();
|
||||
if (this.action !== null) return this.error(`Cannot execute command (${command}) while an action is in progress`);
|
||||
|
||||
const commandArray = parseCommand(command);
|
||||
if (!commandArray.length) return;
|
||||
|
||||
const currentServer = Player.getCurrentServer();
|
||||
/****************** Interactive Tutorial Terminal Commands ******************/
|
||||
if (ITutorial.isRunning) {
|
||||
const n00dlesServ = GetServer("n00dles");
|
||||
@ -769,11 +748,14 @@ export class Terminal {
|
||||
}
|
||||
/****************** END INTERACTIVE TUTORIAL ******************/
|
||||
/* Command parser */
|
||||
|
||||
const commandName = commandArray[0];
|
||||
if (typeof commandName === "number" || typeof commandName === "boolean") {
|
||||
this.error(`Command ${commandArray[0]} not found`);
|
||||
return;
|
||||
}
|
||||
if (typeof commandName !== "string") return this.error(`${commandName} is not a valid command.`);
|
||||
// run by path command
|
||||
if (isFilePath(commandName)) return run(commandArray, currentServer);
|
||||
|
||||
// Aside from the run-by-path command, we don't need the first entry once we've stored it in commandName.
|
||||
commandArray.shift();
|
||||
|
||||
const commands: {
|
||||
[key: string]: (args: (string | number | boolean)[], server: BaseServer) => void;
|
||||
@ -823,12 +805,9 @@ export class Terminal {
|
||||
};
|
||||
|
||||
const f = commands[commandName.toLowerCase()];
|
||||
if (!f) {
|
||||
this.error(`Command ${commandArray[0]} not found`);
|
||||
return;
|
||||
}
|
||||
if (!f) return this.error(`Command ${commandName} not found`);
|
||||
|
||||
f(commandArray.slice(1), s);
|
||||
f(commandArray, currentServer);
|
||||
}
|
||||
|
||||
getProgressText(): string {
|
||||
|
@ -1,59 +1,35 @@
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { MessageFilenames, showMessage } from "../../Message/MessageHelpers";
|
||||
import { MessageFilename, showMessage } from "../../Message/MessageHelpers";
|
||||
import { showLiterature } from "../../Literature/LiteratureHelpers";
|
||||
import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
||||
import { checkEnum } from "../../utils/helpers/enum";
|
||||
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
|
||||
import { hasTextExtension } from "../../Paths/TextFilePath";
|
||||
import { LiteratureName } from "../../Literature/data/LiteratureNames";
|
||||
|
||||
export function cat(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
if (args.length !== 1) {
|
||||
Terminal.error("Incorrect usage of cat command. Usage: cat [file]");
|
||||
return;
|
||||
}
|
||||
if (args.length !== 1) return Terminal.error("Incorrect usage of cat command. Usage: cat [file]");
|
||||
|
||||
const relative_filename = args[0] + "";
|
||||
const filename = Terminal.getFilepath(relative_filename);
|
||||
if (!filename) return Terminal.error(`Invalid filename: ${relative_filename}`);
|
||||
if (
|
||||
!filename.endsWith(".msg") &&
|
||||
!filename.endsWith(".lit") &&
|
||||
!filename.endsWith(".txt") &&
|
||||
!filename.endsWith(".script") &&
|
||||
!filename.endsWith(".js")
|
||||
) {
|
||||
Terminal.error(
|
||||
"Only .msg, .txt, .lit, .script and .js files are viewable with cat (filename must end with .msg, .txt, .lit, .script or .js)",
|
||||
);
|
||||
return;
|
||||
const path = Terminal.getFilepath(relative_filename);
|
||||
if (!path) return Terminal.error(`Invalid filename: ${relative_filename}`);
|
||||
|
||||
if (hasScriptExtension(path) || hasTextExtension(path)) {
|
||||
const file = server.getContentFile(path);
|
||||
if (!file) return Terminal.error(`No file at path ${path}`);
|
||||
return dialogBoxCreate(`${file.filename}\n\n${file.content}`);
|
||||
}
|
||||
if (!path.endsWith(".msg") && !path.endsWith(".lit")) {
|
||||
return Terminal.error("Invalid file extension. Filename must end with .msg, .txt, .lit, .script or .js");
|
||||
}
|
||||
|
||||
if (filename.endsWith(".msg") || filename.endsWith(".lit")) {
|
||||
for (let i = 0; i < server.messages.length; ++i) {
|
||||
if (filename.endsWith(".lit") && server.messages[i] === filename) {
|
||||
const file = server.messages[i];
|
||||
if (file.endsWith(".msg")) throw new Error(".lit file should not be a .msg");
|
||||
showLiterature(file);
|
||||
return;
|
||||
} else if (filename.endsWith(".msg")) {
|
||||
const file = server.messages[i];
|
||||
if (file !== filename) continue;
|
||||
if (!checkEnum(MessageFilenames, file)) return;
|
||||
showMessage(file);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
// Message
|
||||
if (checkEnum(MessageFilename, path)) {
|
||||
if (server.messages.includes(path)) showMessage(path);
|
||||
}
|
||||
|
||||
Terminal.error(`No such file ${filename}`);
|
||||
if (checkEnum(LiteratureName, path)) {
|
||||
if (server.messages.includes(path)) showLiterature(path);
|
||||
}
|
||||
Terminal.error(`No file at path ${path}`);
|
||||
}
|
||||
|
@ -1,38 +1,14 @@
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
|
||||
import { evaluateDirectoryPath, removeTrailingSlash } from "../DirectoryHelpers";
|
||||
import { containsFiles } from "../DirectoryServerHelpers";
|
||||
import { directoryExistsOnServer, resolveDirectory } from "../../Paths/Directory";
|
||||
|
||||
export function cd(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
if (args.length > 1) {
|
||||
Terminal.error("Incorrect number of arguments. Usage: cd [dir]");
|
||||
} else {
|
||||
let dir = args.length === 1 ? args[0] + "" : "/";
|
||||
|
||||
let evaledDir: string | null = "";
|
||||
if (dir === "/") {
|
||||
evaledDir = "/";
|
||||
} else {
|
||||
// Ignore trailing slashes
|
||||
dir = removeTrailingSlash(dir);
|
||||
|
||||
evaledDir = evaluateDirectoryPath(dir, Terminal.cwd());
|
||||
if (evaledDir === null || evaledDir === "") {
|
||||
Terminal.error("Invalid path. Failed to change directories");
|
||||
return;
|
||||
}
|
||||
if (Terminal.cwd().length > 1 && dir === "..") {
|
||||
Terminal.setcwd(evaledDir);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!containsFiles(server, evaledDir)) {
|
||||
Terminal.error("Invalid path. Failed to change directories");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Terminal.setcwd(evaledDir);
|
||||
}
|
||||
if (args.length > 1) return Terminal.error("Incorrect number of arguments. Usage: cd [dir]");
|
||||
// If no arg was provided, just use "".
|
||||
const userInput = String(args[0] ?? "");
|
||||
const targetDir = resolveDirectory(userInput, Terminal.currDir);
|
||||
// Explicitly checking null due to root being ""
|
||||
if (targetDir === null) return Terminal.error(`Could not resolve directory ${userInput}`);
|
||||
if (!directoryExistsOnServer(targetDir, server)) return Terminal.error(`Directory ${targetDir} does not exist.`);
|
||||
Terminal.setcwd(targetDir);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { findRunningScript } from "../../Script/ScriptHelpers";
|
||||
import { isScriptFilename, validScriptExtensions } from "../../Script/isScriptFilename";
|
||||
import { hasScriptExtension, validScriptExtensions } from "../../Paths/ScriptFilePath";
|
||||
|
||||
export function check(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
if (args.length < 1) {
|
||||
@ -11,19 +11,13 @@ export function check(args: (string | number | boolean)[], server: BaseServer):
|
||||
if (!scriptName) return Terminal.error(`Invalid filename: ${args[0]}`);
|
||||
|
||||
// Can only tail script files
|
||||
if (!isScriptFilename(scriptName)) {
|
||||
Terminal.error(
|
||||
`'check' can only be called on scripts files (filename must end with ${validScriptExtensions.join(", ")})`,
|
||||
);
|
||||
return;
|
||||
if (!hasScriptExtension(scriptName)) {
|
||||
return Terminal.error(`check: File extension must be one of ${validScriptExtensions.join(", ")})`);
|
||||
}
|
||||
|
||||
// Check that the script is running on this machine
|
||||
const runningScript = findRunningScript(scriptName, args.slice(1), server);
|
||||
if (runningScript == null) {
|
||||
Terminal.error(`No script named ${scriptName} is running on the server`);
|
||||
return;
|
||||
}
|
||||
if (runningScript == null) return Terminal.error(`No script named ${scriptName} is running on the server`);
|
||||
runningScript.displayLog();
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { Terminal } from "../../../Terminal";
|
||||
import { removeLeadingSlash, removeTrailingSlash } from "../../DirectoryHelpers";
|
||||
import { ScriptEditorRouteOptions } from "../../../ui/Router";
|
||||
import { Router } from "../../../ui/GameRoot";
|
||||
import { BaseServer } from "../../../Server/BaseServer";
|
||||
import { isScriptFilename } from "../../../Script/isScriptFilename";
|
||||
import { CursorPositions } from "../../../ScriptEditor/CursorPositions";
|
||||
import { Script } from "../../../Script/Script";
|
||||
import { isEmpty } from "lodash";
|
||||
import { ScriptFilename } from "src/Types/strings";
|
||||
import { ScriptFilePath, hasScriptExtension } from "../../../Paths/ScriptFilePath";
|
||||
import { TextFilePath, hasTextExtension } from "../../../Paths/TextFilePath";
|
||||
import { getGlobbedFileMap } from "../../../Paths/GlobbedFiles";
|
||||
|
||||
// 2.3: Globbing implementation was removed from this file. Globbing will be reintroduced as broader functionality and integrated here.
|
||||
|
||||
interface EditorParameters {
|
||||
args: (string | number | boolean)[];
|
||||
@ -23,126 +23,34 @@ export async function main(ns) {
|
||||
|
||||
}`;
|
||||
|
||||
interface ISimpleScriptGlob {
|
||||
glob: string;
|
||||
preGlob: string;
|
||||
postGlob: string;
|
||||
globError: string;
|
||||
globMatches: string[];
|
||||
globAgainst: Map<ScriptFilename, Script>;
|
||||
}
|
||||
|
||||
function containsSimpleGlob(filename: string): boolean {
|
||||
return filename.includes("*");
|
||||
}
|
||||
|
||||
function detectSimpleScriptGlob({ args, server }: EditorParameters): ISimpleScriptGlob | null {
|
||||
if (args.length == 1 && containsSimpleGlob(`${args[0]}`)) {
|
||||
const filename = `${args[0]}`;
|
||||
const scripts = server.scripts;
|
||||
const parsedGlob = parseSimpleScriptGlob(filename, scripts);
|
||||
return parsedGlob;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseSimpleScriptGlob(globString: string, globDatabase: Map<ScriptFilename, Script>): ISimpleScriptGlob {
|
||||
const parsedGlob: ISimpleScriptGlob = {
|
||||
glob: globString,
|
||||
preGlob: "",
|
||||
postGlob: "",
|
||||
globError: "",
|
||||
globMatches: [],
|
||||
globAgainst: globDatabase,
|
||||
};
|
||||
|
||||
// Ensure deep globs are minified to simple globs, which act as deep globs in this impl
|
||||
globString = globString.replace("**", "*");
|
||||
|
||||
// Ensure only a single glob is present
|
||||
if (globString.split("").filter((c) => c == "*").length !== 1) {
|
||||
parsedGlob.globError = "Only a single glob is supported per command.\nexample: `nano my-dir/*.js`";
|
||||
return parsedGlob;
|
||||
}
|
||||
|
||||
// Split arg around glob, normalize preGlob path
|
||||
[parsedGlob.preGlob, parsedGlob.postGlob] = globString.split("*");
|
||||
parsedGlob.preGlob = removeLeadingSlash(parsedGlob.preGlob);
|
||||
|
||||
// Add CWD to preGlob path
|
||||
const cwd = removeTrailingSlash(Terminal.cwd());
|
||||
parsedGlob.preGlob = `${cwd}/${parsedGlob.preGlob}`;
|
||||
|
||||
// For every script on the current server, filter matched scripts per glob values & persist
|
||||
globDatabase.forEach((script) => {
|
||||
const filename = script.filename.startsWith("/") ? script.filename : `/${script.filename}`;
|
||||
if (filename.startsWith(parsedGlob.preGlob) && filename.endsWith(parsedGlob.postGlob)) {
|
||||
parsedGlob.globMatches.push(filename);
|
||||
}
|
||||
});
|
||||
|
||||
// Rebuild glob for potential error reporting
|
||||
parsedGlob.glob = `${parsedGlob.preGlob}*${parsedGlob.postGlob}`;
|
||||
|
||||
return parsedGlob;
|
||||
}
|
||||
|
||||
export function commonEditor(
|
||||
command: string,
|
||||
{ args, server }: EditorParameters,
|
||||
scriptEditorRouteOptions?: ScriptEditorRouteOptions,
|
||||
): void {
|
||||
if (args.length < 1) {
|
||||
Terminal.error(`Incorrect usage of ${command} command. Usage: ${command} [scriptname]`);
|
||||
return;
|
||||
}
|
||||
if (args.length < 1) return Terminal.error(`Incorrect usage of ${command} command. Usage: ${command} [scriptname]`);
|
||||
const filesToOpen: Map<ScriptFilePath | TextFilePath, string> = new Map();
|
||||
for (const arg of args) {
|
||||
const pattern = String(arg);
|
||||
|
||||
let filesToLoadOrCreate = args;
|
||||
try {
|
||||
const globSearch = detectSimpleScriptGlob({ args, server });
|
||||
if (globSearch) {
|
||||
if (isEmpty(globSearch.globError) === false) throw new Error(globSearch.globError);
|
||||
filesToLoadOrCreate = globSearch.globMatches;
|
||||
// Glob of existing files
|
||||
if (pattern.includes("*") || pattern.includes("?")) {
|
||||
for (const [path, file] of getGlobbedFileMap(pattern, server, Terminal.currDir)) {
|
||||
filesToOpen.set(path, file.content);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const files = filesToLoadOrCreate.map((arg) => {
|
||||
const filename = `${arg}`;
|
||||
|
||||
if (isScriptFilename(filename)) {
|
||||
const filepath = Terminal.getFilepath(filename);
|
||||
if (!filepath) throw `Invalid filename: ${filename}`;
|
||||
const script = Terminal.getScript(filename);
|
||||
const fileIsNs2 = isNs2(filename);
|
||||
const code = script !== null ? script.code : fileIsNs2 ? newNs2Template : "";
|
||||
|
||||
if (code === newNs2Template) {
|
||||
CursorPositions.saveCursor(filename, {
|
||||
row: 3,
|
||||
column: 5,
|
||||
});
|
||||
}
|
||||
|
||||
return [filepath, code];
|
||||
}
|
||||
|
||||
if (filename.endsWith(".txt")) {
|
||||
const filepath = Terminal.getFilepath(filename);
|
||||
if (!filepath) throw `Invalid filename: ${filename}`;
|
||||
const txt = Terminal.getTextFile(filename);
|
||||
return [filepath, txt === null ? "" : txt.text];
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Invalid file. Only scripts (.script or .js), or text files (.txt) can be edited with ${command}`,
|
||||
);
|
||||
});
|
||||
|
||||
if (globSearch && files.length === 0) {
|
||||
throw new Error(`Could not find any valid files to open with ${command} using glob: \`${globSearch.glob}\``);
|
||||
// Non-glob, files do not need to already exist
|
||||
const path = Terminal.getFilepath(pattern);
|
||||
if (!path) return Terminal.error(`Invalid file path ${arg}`);
|
||||
if (!hasScriptExtension(path) && !hasTextExtension(path)) {
|
||||
return Terminal.error(`${command}: Only scripts or text files can be edited. Invalid file type: ${arg}`);
|
||||
}
|
||||
|
||||
Router.toScriptEditor(Object.fromEntries(files), scriptEditorRouteOptions);
|
||||
} catch (e) {
|
||||
Terminal.error(`${e}`);
|
||||
const file = server.getContentFile(path);
|
||||
const content = file ? file.content : isNs2(path) ? newNs2Template : "";
|
||||
filesToOpen.set(path, content);
|
||||
if (content === newNs2Template) CursorPositions.saveCursor(path, { row: 3, column: 5 });
|
||||
}
|
||||
Router.toScriptEditor(filesToOpen, scriptEditorRouteOptions);
|
||||
}
|
||||
|
@ -1,76 +1,36 @@
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { isScriptFilename } from "../../Script/isScriptFilename";
|
||||
import { getDestinationFilepath, areFilesEqual } from "../DirectoryHelpers";
|
||||
import { combinePath, getFilenameOnly } from "../../Paths/FilePath";
|
||||
import { hasTextExtension } from "../../Paths/TextFilePath";
|
||||
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
|
||||
|
||||
export function cp(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
try {
|
||||
if (args.length !== 2) return Terminal.error("Incorrect usage of cp command. Usage: cp [src] [dst]");
|
||||
// Convert a relative path source file to the absolute path.
|
||||
const src = Terminal.getFilepath(args[0] + "");
|
||||
if (src === null) return Terminal.error(`Invalid source filename ${args[0]}`);
|
||||
|
||||
// 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 + "");
|
||||
if (args.length !== 2) {
|
||||
return Terminal.error("Incorrect usage of cp command. Usage: cp [source filename] [destination]");
|
||||
}
|
||||
// Find the source file
|
||||
const sourceFilePath = Terminal.getFilepath(String(args[0]));
|
||||
if (!sourceFilePath) return Terminal.error(`Invalid source filename ${args[0]}`);
|
||||
if (!hasTextExtension(sourceFilePath) && !hasScriptExtension(sourceFilePath)) {
|
||||
return Terminal.error("cp: Can only be performed on script and text files");
|
||||
}
|
||||
const source = server.getContentFile(sourceFilePath);
|
||||
if (!source) return Terminal.error(`File not found: ${sourceFilePath}`);
|
||||
|
||||
// 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 { BaseServer } from "../../Server/BaseServer";
|
||||
import { isScriptFilename } from "../../Script/isScriptFilename";
|
||||
import FileSaver from "file-saver";
|
||||
import JSZip from "jszip";
|
||||
import { root } from "../../Paths/Directory";
|
||||
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
|
||||
import { hasTextExtension } from "../../Paths/TextFilePath";
|
||||
import { getGlobbedFileMap } from "../../Paths/GlobbedFiles";
|
||||
|
||||
export function exportScripts(pattern: string, server: BaseServer): void {
|
||||
const matchEnding = pattern.length == 1 || pattern === "*.*" ? null : pattern.slice(1); // Treat *.* the same as *
|
||||
// Basic globbing implementation only supporting * and ?. Can be broken out somewhere else later.
|
||||
export function exportScripts(pattern: string, server: BaseServer, currDir = root): void {
|
||||
const zip = new JSZip();
|
||||
// Helper function to zip any file contents whose name matches the pattern
|
||||
const zipFiles = (fileNames: string[], fileContents: string[]): void => {
|
||||
for (let i = 0; i < fileContents.length; ++i) {
|
||||
let name = fileNames[i];
|
||||
if (name.startsWith("/")) name = name.slice(1);
|
||||
if (!matchEnding || name.endsWith(matchEnding))
|
||||
zip.file(name, new Blob([fileContents[i]], { type: "text/plain" }));
|
||||
}
|
||||
};
|
||||
// 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),
|
||||
);
|
||||
|
||||
for (const [name, file] of getGlobbedFileMap(pattern, server, currDir)) {
|
||||
zip.file(name, new Blob([file.content], { type: "text/plain" }));
|
||||
}
|
||||
|
||||
// Return an error if no files matched, rather than an empty zip folder
|
||||
if (Object.keys(zip.files).length == 0) throw new Error(`No files match the pattern ${pattern}`);
|
||||
const zipFn = `bitburner${isScriptFilename(pattern) ? "Scripts" : pattern === "*.txt" ? "Texts" : "Files"}.zip`;
|
||||
const zipFn = `bitburner${
|
||||
hasScriptExtension(pattern) ? "Scripts" : pattern.endsWith(".txt") ? "Texts" : "Files"
|
||||
}.zip`;
|
||||
zip.generateAsync({ type: "blob" }).then((content: Blob) => FileSaver.saveAs(content, zipFn));
|
||||
}
|
||||
|
||||
export function download(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
try {
|
||||
if (args.length !== 1) {
|
||||
Terminal.error("Incorrect usage of download command. Usage: download [script/text file]");
|
||||
return;
|
||||
}
|
||||
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;
|
||||
if (args.length !== 1) {
|
||||
return Terminal.error("Incorrect usage of download command. Usage: download [script/text file]");
|
||||
}
|
||||
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 { Player } from "@player";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { Server } from "../../Server/Server";
|
||||
|
||||
export function grow(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
if (args.length !== 0) {
|
||||
Terminal.error("Incorrect usage of grow command. Usage: grow");
|
||||
return;
|
||||
}
|
||||
if (args.length !== 0) return Terminal.error("Incorrect usage of grow command. Usage: grow");
|
||||
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error(
|
||||
"Cannot grow your own machines! You are currently connected to your home PC or one of your purchased servers",
|
||||
);
|
||||
}
|
||||
const normalServer = server as Server;
|
||||
// Hack the current PC (usually for money)
|
||||
// You can't grow your home pc or servers you purchased
|
||||
if (normalServer.purchasedByPlayer) {
|
||||
Terminal.error(
|
||||
"Cannot grow your own machines! You are currently connected to your home PC or one of your purchased servers",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!normalServer.hasAdminRights) {
|
||||
Terminal.error("You do not have admin rights for this machine! Cannot grow");
|
||||
return;
|
||||
}
|
||||
if (normalServer.requiredHackingSkill > Player.skills.hacking) {
|
||||
Terminal.error(
|
||||
"Your hacking skill is not high enough to attempt hacking this machine. Try analyzing the machine to determine the required hacking skill",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (server.purchasedByPlayer) return Terminal.error("Cannot grow your own machines!");
|
||||
if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!");
|
||||
// Grow does not require meeting the hacking level, but undefined requiredHackingSkill indicates the wrong type of server.
|
||||
if (server.requiredHackingSkill === undefined) return Terminal.error("Cannot grow this server.");
|
||||
Terminal.startGrow();
|
||||
}
|
||||
|
@ -1,37 +1,17 @@
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { Player } from "@player";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { Server } from "../../Server/Server";
|
||||
|
||||
export function hack(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
if (args.length !== 0) {
|
||||
Terminal.error("Incorrect usage of hack command. Usage: hack");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error(
|
||||
"Cannot hack your own machines! You are currently connected to your home PC or one of your purchased servers",
|
||||
if (args.length !== 0) return Terminal.error("Incorrect usage of hack command. Usage: hack");
|
||||
if (server.purchasedByPlayer) return Terminal.error("Cannot hack your own machines!");
|
||||
if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!");
|
||||
// Acts as a functional check that the server is hackable. Hacknet servers should already be filtered out anyway by purchasedByPlayer
|
||||
if (server.requiredHackingSkill === undefined) return Terminal.error("Cannot hack this server.");
|
||||
if (server.requiredHackingSkill > Player.skills.hacking) {
|
||||
return Terminal.error(
|
||||
"Your hacking skill is not high enough to hack this machine. Try analyzing the machine to determine the required hacking skill",
|
||||
);
|
||||
}
|
||||
const normalServer = server as Server;
|
||||
// Hack the current PC (usually for money)
|
||||
// You can't hack your home pc or servers you purchased
|
||||
if (normalServer.purchasedByPlayer) {
|
||||
Terminal.error(
|
||||
"Cannot hack your own machines! You are currently connected to your home PC or one of your purchased servers",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!normalServer.hasAdminRights) {
|
||||
Terminal.error("You do not have admin rights for this machine! Cannot hack");
|
||||
return;
|
||||
}
|
||||
if (normalServer.requiredHackingSkill > Player.skills.hacking) {
|
||||
Terminal.error(
|
||||
"Your hacking skill is not high enough to attempt hacking this machine. Try analyzing the machine to determine the required hacking skill",
|
||||
);
|
||||
return;
|
||||
}
|
||||
Terminal.startHack();
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { root } from "../../Paths/Directory";
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { Player } from "@player";
|
||||
|
||||
@ -10,5 +11,5 @@ export function home(args: (string | number | boolean)[]): void {
|
||||
Player.currentServer = Player.getHomeComputer().hostname;
|
||||
Player.getCurrentServer().isConnectedTo = true;
|
||||
Terminal.print("Connected to home");
|
||||
Terminal.setcwd("/");
|
||||
Terminal.setcwd(root);
|
||||
}
|
||||
|
@ -1,40 +1,25 @@
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { killWorkerScript } from "../../Netscript/killWorkerScript";
|
||||
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
|
||||
|
||||
export function kill(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
try {
|
||||
if (args.length < 1) {
|
||||
Terminal.error("Incorrect usage of kill command. Usage: kill [scriptname] [arg1] [arg2]...");
|
||||
return;
|
||||
}
|
||||
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 (args.length < 1) {
|
||||
return Terminal.error("Incorrect usage of kill command. Usage: kill [pid] or kill [scriptname] [arg1] [arg2]...");
|
||||
}
|
||||
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 createStyles from "@mui/styles/createStyles";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import { toString } from "lodash";
|
||||
import React from "react";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { evaluateDirectoryPath, getFirstParentDirectory, isValidDirectoryPath } from "../DirectoryHelpers";
|
||||
import { Router } from "../../ui/GameRoot";
|
||||
import { Terminal } from "../../Terminal";
|
||||
import libarg from "arg";
|
||||
import { showLiterature } from "../../Literature/LiteratureHelpers";
|
||||
import { MessageFilenames, showMessage } from "../../Message/MessageHelpers";
|
||||
import { MessageFilename, showMessage } from "../../Message/MessageHelpers";
|
||||
import { ScriptFilePath } from "../../Paths/ScriptFilePath";
|
||||
import { FilePath, combinePath, removeDirectoryFromPath } from "../../Paths/FilePath";
|
||||
import { ContentFilePath } from "../../Paths/ContentFile";
|
||||
import {
|
||||
Directory,
|
||||
directoryExistsOnServer,
|
||||
getFirstDirectoryInPath,
|
||||
resolveDirectory,
|
||||
root,
|
||||
} from "../../Paths/Directory";
|
||||
import { TextFilePath } from "../../Paths/TextFilePath";
|
||||
import { ContractFilePath } from "../../Paths/ContractFilePath";
|
||||
import { ProgramFilePath } from "../../Paths/ProgramFilePath";
|
||||
import { checkEnum } from "../../utils/helpers/enum";
|
||||
import { LiteratureName } from "../../Literature/data/LiteratureNames";
|
||||
|
||||
export function ls(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
interface LSFlags {
|
||||
@ -31,7 +44,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
|
||||
incorrectUsage();
|
||||
return;
|
||||
}
|
||||
const filter = flags["--grep"];
|
||||
const filter = flags["--grep"] ?? "";
|
||||
|
||||
const numArgs = args.length;
|
||||
function incorrectUsage(): void {
|
||||
@ -42,74 +55,48 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
|
||||
return incorrectUsage();
|
||||
}
|
||||
|
||||
// Directory path
|
||||
let prefix = Terminal.cwd();
|
||||
if (!prefix.endsWith("/")) {
|
||||
prefix += "/";
|
||||
}
|
||||
|
||||
// If first arg doesn't contain a - it must be the file/folder
|
||||
const dir = args[0] && typeof args[0] == "string" && !args[0].startsWith("-") ? args[0] : "";
|
||||
const newPath = evaluateDirectoryPath(dir + "", Terminal.cwd());
|
||||
prefix = newPath || "";
|
||||
if (!prefix.endsWith("/")) {
|
||||
prefix += "/";
|
||||
}
|
||||
if (!isValidDirectoryPath(prefix)) {
|
||||
return incorrectUsage();
|
||||
}
|
||||
|
||||
// Root directory, which is the same as no 'prefix' at all
|
||||
if (prefix === "/") {
|
||||
prefix = "";
|
||||
let baseDirectory = Terminal.currDir;
|
||||
// Parse first argument which should be a directory.
|
||||
if (args[0] && typeof args[0] == "string" && !args[0].startsWith("-")) {
|
||||
const directory = resolveDirectory(args[0], args[0].startsWith("/") ? root : Terminal.currDir);
|
||||
if (directory !== null && directoryExistsOnServer(directory, server)) {
|
||||
baseDirectory = directory;
|
||||
} else return incorrectUsage();
|
||||
}
|
||||
|
||||
// Display all programs and scripts
|
||||
const allPrograms: string[] = [];
|
||||
const allScripts: string[] = [];
|
||||
const allTextFiles: string[] = [];
|
||||
const allContracts: string[] = [];
|
||||
const allMessages: string[] = [];
|
||||
const folders: string[] = [];
|
||||
const allPrograms: ProgramFilePath[] = [];
|
||||
const allScripts: ScriptFilePath[] = [];
|
||||
const allTextFiles: TextFilePath[] = [];
|
||||
const allContracts: ContractFilePath[] = [];
|
||||
const allMessages: FilePath[] = [];
|
||||
const folders: Directory[] = [];
|
||||
|
||||
function handleFn(fn: string, dest: string[]): void {
|
||||
let parsedFn = fn;
|
||||
if (prefix) {
|
||||
if (!fn.startsWith(prefix)) {
|
||||
return;
|
||||
} else {
|
||||
parsedFn = fn.slice(prefix.length, fn.length);
|
||||
}
|
||||
}
|
||||
function handlePath(path: FilePath, dest: FilePath[]): void {
|
||||
// This parses out any files not in the starting directory.
|
||||
const parsedPath = removeDirectoryFromPath(baseDirectory, path);
|
||||
if (!parsedPath) return;
|
||||
|
||||
if (filter && !parsedFn.includes(filter)) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
dest.push(parsedPath);
|
||||
}
|
||||
|
||||
// Get all of the programs and scripts on the machine into one temporary array
|
||||
for (const program of server.programs) handleFn(program, allPrograms);
|
||||
for (const scriptFilename of server.scripts.keys()) handleFn(scriptFilename, allScripts);
|
||||
for (const txt of server.textFiles) handleFn(txt.fn, allTextFiles);
|
||||
for (const contract of server.contracts) handleFn(contract.fn, allContracts);
|
||||
for (const msgOrLit of server.messages) handleFn(msgOrLit, allMessages);
|
||||
// Type assertions that programs and msg/lit are filepaths are safe due to checks in
|
||||
// Program, Message, and Literature constructors
|
||||
for (const program of server.programs) handlePath(program as FilePath, allPrograms);
|
||||
for (const scriptFilename of server.scripts.keys()) handlePath(scriptFilename, allScripts);
|
||||
for (const txtFilename of server.textFiles.keys()) handlePath(txtFilename, allTextFiles);
|
||||
for (const contract of server.contracts) handlePath(contract.fn, allContracts);
|
||||
for (const msgOrLit of server.messages) handlePath(msgOrLit as FilePath, allMessages);
|
||||
|
||||
// Sort the files/folders alphabetically then print each
|
||||
allPrograms.sort();
|
||||
@ -119,13 +106,10 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
|
||||
allMessages.sort();
|
||||
folders.sort();
|
||||
|
||||
interface ClickableRowProps {
|
||||
row: string;
|
||||
prefix: string;
|
||||
hostname: string;
|
||||
interface ScriptRowProps {
|
||||
scripts: ScriptFilePath[];
|
||||
}
|
||||
|
||||
function ClickableScriptRow({ row, prefix, hostname }: ClickableRowProps): React.ReactElement {
|
||||
function ClickableScriptRow({ scripts }: ScriptRowProps): React.ReactElement {
|
||||
const classes = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
scriptLinksWrap: {
|
||||
@ -135,48 +119,38 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
|
||||
scriptLink: {
|
||||
cursor: "pointer",
|
||||
textDecorationLine: "underline",
|
||||
paddingRight: "1.15em",
|
||||
"&:last-child": { padding: 0 },
|
||||
marginRight: "1.5em",
|
||||
"&:last-child": { marginRight: 0 },
|
||||
},
|
||||
}),
|
||||
)();
|
||||
|
||||
const rowSplit = row.split("~");
|
||||
let rowSplitArray = rowSplit.map((x) => [x.trim(), x.replace(x.trim(), "")]);
|
||||
rowSplitArray = rowSplitArray.filter((x) => !!x[0]);
|
||||
|
||||
function onScriptLinkClick(filename: string): void {
|
||||
if (!server.isConnectedTo) {
|
||||
return Terminal.error(`File is not on this server, connect to ${hostname} and try again`);
|
||||
}
|
||||
if (filename.startsWith("/")) filename = filename.slice(1);
|
||||
// Terminal.getFilepath needs leading slash to correctly work here
|
||||
if (prefix === "") filename = `/${filename}`;
|
||||
const filepath = Terminal.getFilepath(`${prefix}${filename}`);
|
||||
// Terminal.getScript also calls Terminal.getFilepath and therefore also
|
||||
// needs the given parameter
|
||||
if (!filepath) {
|
||||
return Terminal.error(`Invalid filename (${prefix}${filename}) when clicking script link. This is a bug.`);
|
||||
}
|
||||
const code = toString(Terminal.getScript(`${prefix}${filename}`)?.code);
|
||||
Router.toScriptEditor({ [filepath]: code });
|
||||
function onScriptLinkClick(filename: ScriptFilePath): void {
|
||||
const filePath = combinePath(baseDirectory, filename);
|
||||
const code = server.scripts.get(filePath)?.content ?? "";
|
||||
const map = new Map<ContentFilePath, string>();
|
||||
map.set(filePath, code);
|
||||
Router.toScriptEditor(map);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={classes.scriptLinksWrap}>
|
||||
{rowSplitArray.map((rowItem) => (
|
||||
<span key={"script_" + rowItem[0]}>
|
||||
<span className={classes.scriptLink} onClick={() => onScriptLinkClick(rowItem[0])}>
|
||||
{rowItem[0]}
|
||||
{scripts.map((script) => (
|
||||
<span key={script}>
|
||||
<span className={classes.scriptLink} onClick={() => onScriptLinkClick(script)}>
|
||||
{script}
|
||||
</span>
|
||||
<span>{rowItem[1]}</span>
|
||||
<span></span>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ClickableMessageRow({ row, prefix, hostname }: ClickableRowProps): React.ReactElement {
|
||||
interface MessageRowProps {
|
||||
messages: FilePath[];
|
||||
}
|
||||
function ClickableMessageRow({ messages }: MessageRowProps): React.ReactElement {
|
||||
const classes = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
linksWrap: {
|
||||
@ -186,41 +160,33 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
|
||||
link: {
|
||||
cursor: "pointer",
|
||||
textDecorationLine: "underline",
|
||||
paddingRight: "1.15em",
|
||||
"&:last-child": { padding: 0 },
|
||||
marginRight: "1.5em",
|
||||
"&:last-child": { marginRight: 0 },
|
||||
},
|
||||
}),
|
||||
)();
|
||||
|
||||
const rowSplit = row.split("~");
|
||||
let rowSplitArray = rowSplit.map((x) => [x.trim(), x.replace(x.trim(), "")]);
|
||||
rowSplitArray = rowSplitArray.filter((x) => !!x[0]);
|
||||
|
||||
function onMessageLinkClick(filename: string): void {
|
||||
function onMessageLinkClick(filename: FilePath): void {
|
||||
if (!server.isConnectedTo) {
|
||||
return Terminal.error(`File is not on this server, connect to ${hostname} and try again`);
|
||||
}
|
||||
if (filename.startsWith("/")) filename = filename.slice(1);
|
||||
const filepath = Terminal.getFilepath(`${prefix}${filename}`);
|
||||
if (!filepath) {
|
||||
return Terminal.error(`Invalid filename ${prefix}${filename} when clicking message link. This is a bug.`);
|
||||
return Terminal.error(`File is not on this server, connect to ${server.hostname} and try again`);
|
||||
}
|
||||
// Message and lit files have no directories
|
||||
|
||||
if (filepath.endsWith(".lit")) {
|
||||
showLiterature(filepath);
|
||||
} else if (filepath.endsWith(".msg")) {
|
||||
showMessage(filepath as MessageFilenames);
|
||||
if (checkEnum(MessageFilename, filename)) {
|
||||
showMessage(filename);
|
||||
} else if (checkEnum(LiteratureName, filename)) {
|
||||
showLiterature(filename);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={classes.linksWrap}>
|
||||
{rowSplitArray.map((rowItem) => (
|
||||
<span key={"text_" + rowItem[0]}>
|
||||
<span className={classes.link} onClick={() => onMessageLinkClick(rowItem[0])}>
|
||||
{rowItem[0]}
|
||||
{messages.map((message) => (
|
||||
<span key={message}>
|
||||
<span className={classes.link} onClick={() => onMessageLinkClick(message)}>
|
||||
{message}
|
||||
</span>
|
||||
<span>{rowItem[1]}</span>
|
||||
<span></span>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
@ -236,39 +202,51 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
|
||||
Script,
|
||||
}
|
||||
|
||||
interface FileGroup {
|
||||
type: FileType;
|
||||
segments: string[];
|
||||
}
|
||||
type FileGroup =
|
||||
| {
|
||||
// Types that are not clickable only need to be string[]
|
||||
type: FileType.Folder | FileType.Program | FileType.Contract | FileType.TextFile;
|
||||
segments: string[];
|
||||
}
|
||||
| { type: FileType.Message; segments: FilePath[] }
|
||||
| { type: FileType.Script; segments: ScriptFilePath[] };
|
||||
|
||||
function postSegments(group: FileGroup, flags: LSFlags): void {
|
||||
const segments = group.segments;
|
||||
const linked = group.type === FileType.Script || group.type === FileType.Message;
|
||||
function postSegments({ type, segments }: FileGroup, flags: LSFlags): void {
|
||||
const maxLength = Math.max(...segments.map((s) => s.length)) + 1;
|
||||
const filesPerRow = flags["-l"] === true ? 1 : Math.ceil(80 / maxLength);
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
let row = "";
|
||||
for (let col = 0; col < filesPerRow; col++) {
|
||||
if (!(i < segments.length)) break;
|
||||
row += segments[i];
|
||||
row += " ".repeat(maxLength * (col + 1) - row.length);
|
||||
if (linked) {
|
||||
row += "~";
|
||||
const padLength = Math.max(maxLength + 2, 40);
|
||||
let i = 0;
|
||||
if (type === FileType.Script) {
|
||||
while (i < segments.length) {
|
||||
const scripts: ScriptFilePath[] = [];
|
||||
for (let col = 0; col < filesPerRow && i < segments.length; col++, i++) {
|
||||
scripts.push(segments[i]);
|
||||
}
|
||||
Terminal.printRaw(<ClickableScriptRow scripts={scripts} />);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type === FileType.Message) {
|
||||
while (i < segments.length) {
|
||||
const messages: FilePath[] = [];
|
||||
for (let col = 0; col < filesPerRow && i < segments.length; col++, i++) {
|
||||
messages.push(segments[i]);
|
||||
}
|
||||
Terminal.printRaw(<ClickableMessageRow messages={messages} />);
|
||||
}
|
||||
return;
|
||||
}
|
||||
while (i < segments.length) {
|
||||
let row = "";
|
||||
for (let col = 0; col < filesPerRow; col++, i++) {
|
||||
if (!(i < segments.length)) break;
|
||||
row += segments[i].padEnd(padLength);
|
||||
i++;
|
||||
}
|
||||
i--;
|
||||
|
||||
switch (group.type) {
|
||||
switch (type) {
|
||||
case FileType.Folder:
|
||||
Terminal.printRaw(<span style={{ color: "cyan" }}>{row}</span>);
|
||||
break;
|
||||
case FileType.Script:
|
||||
Terminal.printRaw(<ClickableScriptRow row={row} prefix={prefix} hostname={server.hostname} />);
|
||||
break;
|
||||
case FileType.Message:
|
||||
Terminal.printRaw(<ClickableMessageRow row={row} prefix={prefix} hostname={server.hostname} />);
|
||||
break;
|
||||
default:
|
||||
Terminal.print(row);
|
||||
}
|
||||
@ -282,8 +260,8 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
|
||||
{ type: FileType.Program, segments: allPrograms },
|
||||
{ type: FileType.Contract, segments: allContracts },
|
||||
{ type: FileType.Script, segments: allScripts },
|
||||
].filter((g) => g.segments.length > 0);
|
||||
];
|
||||
for (const group of groups) {
|
||||
postSegments(group, flags);
|
||||
if (group.segments.length > 0) postSegments(group, flags);
|
||||
}
|
||||
}
|
||||
|
@ -1,89 +1,35 @@
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { isScriptFilename } from "../../Script/isScriptFilename";
|
||||
import { TextFile } from "../../TextFile";
|
||||
import { Script } from "../../Script/Script";
|
||||
import { getDestinationFilepath, areFilesEqual } from "../DirectoryHelpers";
|
||||
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
|
||||
import { hasTextExtension } from "../../Paths/TextFilePath";
|
||||
|
||||
export function mv(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
if (args.length !== 2) {
|
||||
Terminal.error(`Incorrect number of arguments. Usage: mv [src] [dest]`);
|
||||
return;
|
||||
}
|
||||
const [source, destination] = args.map((arg) => arg + "");
|
||||
|
||||
try {
|
||||
const source = args[0] + "";
|
||||
const t_dest = args[1] + "";
|
||||
const sourcePath = Terminal.getFilepath(source);
|
||||
if (!sourcePath) return Terminal.error(`Invalid source filename: ${source}`);
|
||||
const destinationPath = Terminal.getFilepath(destination);
|
||||
if (!destinationPath) return Terminal.error(`Invalid destination filename: ${destinationPath}`);
|
||||
|
||||
if (!isScriptFilename(source) && !source.endsWith(".txt")) {
|
||||
Terminal.error(`'mv' can only be used on scripts and text files (.txt)`);
|
||||
return;
|
||||
}
|
||||
|
||||
const srcFile = Terminal.getFile(source);
|
||||
if (srcFile == null) return Terminal.error(`Source file ${source} does not exist`);
|
||||
|
||||
const sourcePath = Terminal.getFilepath(source);
|
||||
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 + "");
|
||||
if (
|
||||
(!hasScriptExtension(sourcePath) && !hasTextExtension(sourcePath)) ||
|
||||
(!hasScriptExtension(destinationPath) && !hasTextExtension(destinationPath))
|
||||
) {
|
||||
return Terminal.error(`'mv' can only be used on scripts and text files (.txt)`);
|
||||
}
|
||||
|
||||
// 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 { BaseServer } from "../../Server/BaseServer";
|
||||
import { isScriptFilename } from "../../Script/isScriptFilename";
|
||||
import { runScript } from "./runScript";
|
||||
import { runProgram } from "./runProgram";
|
||||
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
|
||||
import { hasContractExtension } from "../../Paths/ContractFilePath";
|
||||
import { hasProgramExtension } from "../../Paths/ProgramFilePath";
|
||||
|
||||
export function run(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
// Run a program or a script
|
||||
if (args.length < 1) {
|
||||
Terminal.error("Incorrect number of arguments. Usage: run [program/script] [-t] [num threads] [arg1] [arg2]...");
|
||||
} else {
|
||||
const executableName = args[0] + "";
|
||||
const arg = args.shift();
|
||||
if (!arg) return Terminal.error("Usage: run [program/script] [-t] [num threads] [arg1] [arg2]...");
|
||||
|
||||
// Check if its a script or just a program/executable
|
||||
if (isScriptFilename(executableName)) {
|
||||
runScript(args, server);
|
||||
} else if (executableName.endsWith(".cct")) {
|
||||
Terminal.runContract(executableName);
|
||||
} else {
|
||||
runProgram(args, server);
|
||||
}
|
||||
const path = Terminal.getFilepath(String(arg));
|
||||
if (!path) return Terminal.error(`${args[0]} is not a valid filepath.`);
|
||||
if (hasScriptExtension(path)) {
|
||||
args.shift();
|
||||
return runScript(path, args, server);
|
||||
} else if (hasContractExtension(path)) {
|
||||
Terminal.runContract(path);
|
||||
return;
|
||||
} else if (hasProgramExtension(path)) {
|
||||
return runProgram(path, args, server);
|
||||
}
|
||||
Terminal.error(`Invalid file extension. Only .js, .script, .cct, and .exe files can be ran.`);
|
||||
}
|
||||
|
@ -1,37 +1,21 @@
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { Player } from "@player";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { Programs } from "../../Programs/Programs";
|
||||
|
||||
export function runProgram(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
if (args.length < 1) {
|
||||
return;
|
||||
}
|
||||
import { CompletedProgramName, Programs } from "../../Programs/Programs";
|
||||
import { ProgramFilePath } from "../../Paths/ProgramFilePath";
|
||||
|
||||
export function runProgram(path: ProgramFilePath, args: (string | number | boolean)[], server: BaseServer): void {
|
||||
// Check if you have the program on your computer. If you do, execute it, otherwise
|
||||
// display an error message
|
||||
const programName = args[0] + "";
|
||||
const programLowered = path.toLowerCase();
|
||||
// Support lowercase even though it's an enum
|
||||
|
||||
if (!Player.hasProgram(programName)) {
|
||||
const realProgramName = Object.values(CompletedProgramName).find((name) => name.toLowerCase() === programLowered);
|
||||
if (!realProgramName || !Player.hasProgram(realProgramName)) {
|
||||
Terminal.error(
|
||||
`No such (exe, script, js, ns, or cct) file! (Only programs that exist on your home computer or scripts on ${server.hostname} can be run)`,
|
||||
`No such (exe, script, js, or cct) file! (Only finished programs that exist on your home computer or scripts on ${server.hostname} can be run)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const program of Object.values(Programs)) {
|
||||
if (program.name.toLocaleLowerCase() === programName.toLocaleLowerCase()) {
|
||||
program.run(
|
||||
args.slice(1).map((arg) => arg + ""),
|
||||
server,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Terminal.error("Invalid executable. Cannot be run");
|
||||
Programs[realProgramName].run(args.map(String), server);
|
||||
}
|
||||
|
@ -3,27 +3,21 @@ import { BaseServer } from "../../Server/BaseServer";
|
||||
import { LogBoxEvents } from "../../ui/React/LogBoxManager";
|
||||
import { startWorkerScript } from "../../NetscriptWorker";
|
||||
import { RunningScript } from "../../Script/RunningScript";
|
||||
import { findRunningScript } from "../../Script/ScriptHelpers";
|
||||
import * as libarg from "arg";
|
||||
import { formatRam } from "../../ui/formatNumber";
|
||||
import { ScriptArg } from "@nsdefs";
|
||||
import { isPositiveInteger } from "../../types";
|
||||
import { ScriptFilePath } from "../../Paths/ScriptFilePath";
|
||||
|
||||
export function runScript(commandArgs: (string | number | boolean)[], server: BaseServer): void {
|
||||
if (commandArgs.length < 1) {
|
||||
Terminal.error(
|
||||
`Bug encountered with Terminal.runScript(). Command array has a length of less than 1: ${commandArgs}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptName = Terminal.getFilepath(commandArgs[0] + "");
|
||||
if (!scriptName) return Terminal.error(`Invalid filename: ${commandArgs[0]}`);
|
||||
export function runScript(path: ScriptFilePath, commandArgs: (string | number | boolean)[], server: BaseServer): void {
|
||||
// This takes in the absolute filepath, see "run.ts"
|
||||
const script = server.scripts.get(path);
|
||||
if (!script) return Terminal.error(`Script ${path} does not exist on this server.`);
|
||||
|
||||
const runArgs = { "--tail": Boolean, "-t": Number };
|
||||
const flags = libarg(runArgs, {
|
||||
permissive: true,
|
||||
argv: commandArgs.slice(1),
|
||||
argv: commandArgs,
|
||||
});
|
||||
const tailFlag = flags["--tail"] === true;
|
||||
const numThreads = parseFloat(flags["-t"] ?? 1);
|
||||
@ -35,34 +29,24 @@ export function runScript(commandArgs: (string | number | boolean)[], server: Ba
|
||||
const args = flags["_"] as ScriptArg[];
|
||||
|
||||
// Check if this script is already running
|
||||
if (findRunningScript(scriptName, args, server) != null) {
|
||||
Terminal.error(
|
||||
"This script is already running with the same args. Cannot run multiple instances with the same args",
|
||||
);
|
||||
return;
|
||||
if (server.getRunningScript(path, args)) {
|
||||
return Terminal.error("This script is already running with the same args.");
|
||||
}
|
||||
|
||||
// Check if the script exists and if it does run it
|
||||
const script = server.scripts.get(scriptName);
|
||||
if (!script) return Terminal.error("No such script");
|
||||
|
||||
const singleRamUsage = script.getRamUsage(server.scripts);
|
||||
if (!singleRamUsage) return Terminal.error("Error while calculating ram usage for this script.");
|
||||
|
||||
const ramUsage = singleRamUsage * numThreads;
|
||||
const ramAvailable = server.maxRam - server.ramUsed;
|
||||
|
||||
if (!server.hasAdminRights) {
|
||||
Terminal.error("Need root access to run script");
|
||||
return;
|
||||
}
|
||||
if (!server.hasAdminRights) return Terminal.error("Need root access to run script");
|
||||
|
||||
if (ramUsage > ramAvailable + 0.001) {
|
||||
Terminal.error(
|
||||
return Terminal.error(
|
||||
"This machine does not have enough RAM to run this script" +
|
||||
(numThreads === 1 ? "" : ` with ${numThreads} threads`) +
|
||||
`. Script requires ${formatRam(ramUsage)} of RAM`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Able to run script
|
||||
@ -70,10 +54,7 @@ export function runScript(commandArgs: (string | number | boolean)[], server: Ba
|
||||
runningScript.threads = numThreads;
|
||||
|
||||
const success = startWorkerScript(runningScript, server);
|
||||
if (!success) {
|
||||
Terminal.error(`Failed to start script`);
|
||||
return;
|
||||
}
|
||||
if (!success) return Terminal.error(`Failed to start script`);
|
||||
|
||||
Terminal.print(
|
||||
`Running script with ${numThreads} thread(s), pid ${runningScript.pid} and args: ${JSON.stringify(args)}.`,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { Player } from "@player";
|
||||
import { Programs } from "../../Programs/Programs";
|
||||
import { CompletedProgramName } from "../../Programs/Programs";
|
||||
|
||||
export function scananalyze(args: (string | number | boolean)[]): void {
|
||||
if (args.length === 0) {
|
||||
@ -19,18 +19,18 @@ export function scananalyze(args: (string | number | boolean)[]): void {
|
||||
const depth = parseInt(args[0] + "");
|
||||
|
||||
if (isNaN(depth) || depth < 0) {
|
||||
Terminal.error("Incorrect usage of scan-analyze command. depth argument must be positive numeric");
|
||||
return;
|
||||
return Terminal.error("Incorrect usage of scan-analyze command. depth argument must be positive numeric");
|
||||
}
|
||||
if (depth > 3 && !Player.hasProgram(Programs.DeepscanV1.name) && !Player.hasProgram(Programs.DeepscanV2.name)) {
|
||||
Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 3");
|
||||
return;
|
||||
} else if (depth > 5 && !Player.hasProgram(Programs.DeepscanV2.name)) {
|
||||
Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 5");
|
||||
return;
|
||||
if (
|
||||
depth > 3 &&
|
||||
!Player.hasProgram(CompletedProgramName.deepScan1) &&
|
||||
!Player.hasProgram(CompletedProgramName.deepScan2)
|
||||
) {
|
||||
return Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 3");
|
||||
} else if (depth > 5 && !Player.hasProgram(CompletedProgramName.deepScan2)) {
|
||||
return Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 5");
|
||||
} else if (depth > 10) {
|
||||
Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 10");
|
||||
return;
|
||||
return Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 10");
|
||||
}
|
||||
Terminal.executeScanAnalyzeCommand(depth, all);
|
||||
}
|
||||
|
@ -1,71 +1,40 @@
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { GetServer } from "../../Server/AllServers";
|
||||
import { isScriptFilename } from "../../Script/isScriptFilename";
|
||||
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
|
||||
import { hasTextExtension } from "../../Paths/TextFilePath";
|
||||
import { checkEnum } from "../../utils/helpers/enum";
|
||||
import { LiteratureName } from "../../Literature/data/LiteratureNames";
|
||||
|
||||
export function scp(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
try {
|
||||
if (args.length !== 2) {
|
||||
Terminal.error("Incorrect usage of scp command. Usage: scp [file] [destination hostname]");
|
||||
return;
|
||||
}
|
||||
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 + "");
|
||||
if (args.length !== 2) {
|
||||
return Terminal.error("Incorrect usage of scp command. Usage: scp [source filename] [destination hostname]");
|
||||
}
|
||||
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 { BaseServer } from "../../Server/BaseServer";
|
||||
import { findRunningScriptByPid } from "../../Script/ScriptHelpers";
|
||||
import { isScriptFilename, validScriptExtensions } from "../../Script/isScriptFilename";
|
||||
import { compareArrays } from "../../utils/helpers/compareArrays";
|
||||
import { LogBoxEvents } from "../../ui/React/LogBoxManager";
|
||||
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
|
||||
|
||||
export function tail(commandArray: (string | number | boolean)[], server: BaseServer): void {
|
||||
try {
|
||||
if (commandArray.length < 1) {
|
||||
Terminal.error("Incorrect number of arguments. Usage: tail [script] [arg1] [arg2]...");
|
||||
} else if (typeof commandArray[0] === "string") {
|
||||
const scriptName = Terminal.getFilepath(commandArray[0]);
|
||||
if (!scriptName) return Terminal.error(`Invalid filename: ${commandArray[0]}`);
|
||||
if (!isScriptFilename(scriptName)) {
|
||||
Terminal.error(`tail can only be called on ${validScriptExtensions.join(", ")} files, or by PID`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 (commandArray.length < 1) {
|
||||
return Terminal.error("Incorrect number of arguments. Usage: tail [script] [arg1] [arg2]...");
|
||||
}
|
||||
if (typeof commandArray[0] === "number") {
|
||||
const runningScript = findRunningScriptByPid(commandArray[0], server);
|
||||
if (!runningScript) return Terminal.error(`No script with PID ${commandArray[0]} is running on the server`);
|
||||
LogBoxEvents.emit(runningScript);
|
||||
return;
|
||||
}
|
||||
|
||||
const path = Terminal.getFilepath(String(commandArray[0]));
|
||||
if (!path) return Terminal.error(`Invalid file path: ${commandArray[0]}`);
|
||||
if (!hasScriptExtension(path)) return Terminal.error(`Invalid file extension. Tail can only be used on scripts.`);
|
||||
|
||||
// Get script arguments
|
||||
const args = [];
|
||||
for (let i = 1; i < commandArray.length; ++i) {
|
||||
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 { Player } from "@player";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { Server } from "../../Server/Server";
|
||||
|
||||
export function weaken(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
if (args.length !== 0) {
|
||||
Terminal.error("Incorrect usage of weaken command. Usage: weaken");
|
||||
return;
|
||||
}
|
||||
if (args.length !== 0) return Terminal.error("Incorrect usage of weaken command. Usage: weaken");
|
||||
|
||||
if (!(server instanceof Server)) {
|
||||
Terminal.error(
|
||||
"Cannot weaken your own machines! You are currently connected to your home PC or one of your purchased servers",
|
||||
);
|
||||
}
|
||||
const normalServer = server as Server;
|
||||
// Hack the current PC (usually for money)
|
||||
// You can't weaken your home pc or servers you purchased
|
||||
if (normalServer.purchasedByPlayer) {
|
||||
Terminal.error(
|
||||
"Cannot weaken your own machines! You are currently connected to your home PC or one of your purchased servers",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!normalServer.hasAdminRights) {
|
||||
Terminal.error("You do not have admin rights for this machine! Cannot weaken");
|
||||
return;
|
||||
}
|
||||
if (normalServer.requiredHackingSkill > Player.skills.hacking) {
|
||||
Terminal.error(
|
||||
"Your hacking skill is not high enough to attempt hacking this machine. Try analyzing the machine to determine the required hacking skill",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (server.purchasedByPlayer) return Terminal.error("Cannot weaken your own machines!");
|
||||
if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!");
|
||||
// Weaken does not require meeting the hacking level, but undefined requiredHackingSkill indicates the wrong type of server.
|
||||
if (server.requiredHackingSkill === undefined) return Terminal.error("Cannot weaken this server.");
|
||||
Terminal.startWeaken();
|
||||
}
|
||||
|
@ -2,7 +2,8 @@ import $ from "jquery";
|
||||
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { isScriptFilename } from "../../Script/isScriptFilename";
|
||||
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
|
||||
import { hasTextExtension } from "../../Paths/TextFilePath";
|
||||
|
||||
export function wget(args: (string | number | boolean)[], server: BaseServer): void {
|
||||
if (args.length !== 2) {
|
||||
@ -12,20 +13,17 @@ export function wget(args: (string | number | boolean)[], server: BaseServer): v
|
||||
|
||||
const url = args[0] + "";
|
||||
const target = Terminal.getFilepath(args[1] + "");
|
||||
if (!target || (!isScriptFilename(target) && !target.endsWith(".txt"))) {
|
||||
if (!target || (!hasScriptExtension(target) && !hasTextExtension(target))) {
|
||||
return Terminal.error(`wget failed: Invalid target file. Target file must be script or text file`);
|
||||
}
|
||||
$.get(
|
||||
url,
|
||||
function (data: unknown) {
|
||||
let res;
|
||||
if (isScriptFilename(target)) {
|
||||
res = server.writeToScriptFile(target, String(data));
|
||||
} else {
|
||||
if (hasTextExtension(target)) {
|
||||
res = server.writeToTextFile(target, String(data));
|
||||
}
|
||||
if (!res.success) {
|
||||
return Terminal.error("wget failed");
|
||||
} else {
|
||||
res = server.writeToScriptFile(target, String(data));
|
||||
}
|
||||
if (res.overwritten) {
|
||||
return Terminal.print(`wget successfully retrieved content and overwrote ${target}`);
|
||||
|
@ -1,408 +0,0 @@
|
||||
import { evaluateDirectoryPath, getAllParentDirectories } from "./DirectoryHelpers";
|
||||
import { getSubdirectories } from "./DirectoryServerHelpers";
|
||||
|
||||
import { Aliases, GlobalAliases, substituteAliases } from "../Alias";
|
||||
import { DarkWebItems } from "../DarkWeb/DarkWebItems";
|
||||
import { Player } from "@player";
|
||||
import { GetAllServers } from "../Server/AllServers";
|
||||
import { Server } from "../Server/Server";
|
||||
import { ParseCommand, ParseCommands } from "./Parser";
|
||||
import { HelpTexts } from "./HelpText";
|
||||
import { isScriptFilename } from "../Script/isScriptFilename";
|
||||
import { compile } from "../NetscriptJSEvaluator";
|
||||
import { Flags } from "../NetscriptFunctions/Flags";
|
||||
import { AutocompleteData } from "@nsdefs";
|
||||
import * as libarg from "arg";
|
||||
|
||||
// TODO: this shouldn't be hardcoded in two places with no typechecks to verify equivalence
|
||||
// An array of all Terminal commands
|
||||
const commands = [
|
||||
"alias",
|
||||
"analyze",
|
||||
"backdoor",
|
||||
"cat",
|
||||
"cd",
|
||||
"changelog",
|
||||
"check",
|
||||
"clear",
|
||||
"cls",
|
||||
"connect",
|
||||
"cp",
|
||||
"download",
|
||||
"expr",
|
||||
"free",
|
||||
"grow",
|
||||
"hack",
|
||||
"help",
|
||||
"home",
|
||||
"hostname",
|
||||
"ifconfig",
|
||||
"kill",
|
||||
"killall",
|
||||
"ls",
|
||||
"lscpu",
|
||||
"mem",
|
||||
"mv",
|
||||
"nano",
|
||||
"ps",
|
||||
"rm",
|
||||
"run",
|
||||
"scan-analyze",
|
||||
"scan",
|
||||
"scp",
|
||||
"sudov",
|
||||
"tail",
|
||||
"theme",
|
||||
"top",
|
||||
"vim",
|
||||
"weaken",
|
||||
];
|
||||
|
||||
export async function determineAllPossibilitiesForTabCompletion(
|
||||
input: string,
|
||||
index: number,
|
||||
currPath = "",
|
||||
): Promise<string[]> {
|
||||
input = substituteAliases(input);
|
||||
let allPos: string[] = [];
|
||||
allPos = allPos.concat(Object.keys(GlobalAliases));
|
||||
const currServ = Player.getCurrentServer();
|
||||
const homeComputer = Player.getHomeComputer();
|
||||
|
||||
let parentDirPath = "";
|
||||
let evaledParentDirPath: string | null = null;
|
||||
// Helper functions
|
||||
function addAllCodingContracts(): void {
|
||||
for (const cct of currServ.contracts) {
|
||||
allPos.push(cct.fn);
|
||||
}
|
||||
}
|
||||
|
||||
function addAllLitFiles(): void {
|
||||
for (const file of currServ.messages) {
|
||||
if (!file.endsWith(".msg")) {
|
||||
allPos.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addAllMessages(): void {
|
||||
for (const file of currServ.messages) {
|
||||
if (file.endsWith(".msg")) {
|
||||
allPos.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addAllPrograms(): void {
|
||||
for (const program of homeComputer.programs) {
|
||||
allPos.push(program);
|
||||
}
|
||||
}
|
||||
|
||||
function addAllScripts(): void {
|
||||
for (const scriptFilename of currServ.scripts.keys()) {
|
||||
const res = processFilepath(scriptFilename);
|
||||
if (res) {
|
||||
allPos.push(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addAllTextFiles(): void {
|
||||
for (const txt of currServ.textFiles) {
|
||||
const res = processFilepath(txt.fn);
|
||||
if (res) {
|
||||
allPos.push(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addAllDirectories(): void {
|
||||
// Directories are based on the currently evaluated path
|
||||
const subdirs = getSubdirectories(currServ, evaledParentDirPath == null ? "/" : evaledParentDirPath);
|
||||
|
||||
for (let i = 0; i < subdirs.length; ++i) {
|
||||
const assembledDirPath = evaledParentDirPath == null ? subdirs[i] : evaledParentDirPath + subdirs[i];
|
||||
const res = processFilepath(assembledDirPath);
|
||||
if (res != null) {
|
||||
subdirs[i] = res;
|
||||
}
|
||||
}
|
||||
|
||||
allPos = allPos.concat(subdirs);
|
||||
}
|
||||
|
||||
// Convert from the real absolute path back to the original path used in the input
|
||||
function convertParentPath(filepath: string): string {
|
||||
if (parentDirPath == null || evaledParentDirPath == null) {
|
||||
console.warn(`convertParentPath() called when paths are null`);
|
||||
return filepath;
|
||||
}
|
||||
|
||||
if (!filepath.startsWith(evaledParentDirPath)) {
|
||||
console.warn(
|
||||
`convertParentPath() called for invalid path. (filepath=${filepath}) (evaledParentDirPath=${evaledParentDirPath})`,
|
||||
);
|
||||
return filepath;
|
||||
}
|
||||
|
||||
return parentDirPath + filepath.slice(evaledParentDirPath.length);
|
||||
}
|
||||
|
||||
// Given an a full, absolute filepath, converts it to the proper value
|
||||
// for autocompletion purposes
|
||||
function processFilepath(filepath: string): string | null {
|
||||
if (evaledParentDirPath) {
|
||||
if (filepath.startsWith(evaledParentDirPath)) {
|
||||
return convertParentPath(filepath);
|
||||
}
|
||||
} else if (parentDirPath !== "") {
|
||||
// If the parent directory is the root directory, but we're not searching
|
||||
// it from the root directory, we have to add the original path
|
||||
let t_parentDirPath = parentDirPath;
|
||||
if (!t_parentDirPath.endsWith("/")) {
|
||||
t_parentDirPath += "/";
|
||||
}
|
||||
return parentDirPath + filepath;
|
||||
} else {
|
||||
return filepath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isCommand(cmd: string): boolean {
|
||||
let t_cmd = cmd;
|
||||
if (!t_cmd.endsWith(" ")) {
|
||||
t_cmd += " ";
|
||||
}
|
||||
|
||||
return input.startsWith(t_cmd);
|
||||
}
|
||||
|
||||
// Autocomplete the command
|
||||
if (index === -1 && !input.startsWith("./")) {
|
||||
return commands.concat(Object.keys(Aliases)).concat(Object.keys(GlobalAliases));
|
||||
}
|
||||
|
||||
// Since we're autocompleting an argument and not a command, the argument might
|
||||
// be a file/directory path. We have to account for that when autocompleting
|
||||
const commandArray = input.split(" ");
|
||||
if (commandArray.length === 0) {
|
||||
console.warn(`Tab autocompletion logic reached invalid branch`);
|
||||
return allPos;
|
||||
}
|
||||
const arg = commandArray[commandArray.length - 1];
|
||||
parentDirPath = getAllParentDirectories(arg);
|
||||
evaledParentDirPath = evaluateDirectoryPath(parentDirPath, currPath);
|
||||
if (evaledParentDirPath === "/") {
|
||||
evaledParentDirPath = null;
|
||||
} else if (evaledParentDirPath == null) {
|
||||
// do nothing for some reason tests don't like this?
|
||||
// return allPos; // Invalid path
|
||||
} else {
|
||||
evaledParentDirPath += "/";
|
||||
}
|
||||
|
||||
if (isCommand("buy")) {
|
||||
const options = [];
|
||||
for (const i of Object.keys(DarkWebItems)) {
|
||||
const item = DarkWebItems[i];
|
||||
options.push(item.program);
|
||||
}
|
||||
|
||||
return options.concat(Object.keys(GlobalAliases));
|
||||
}
|
||||
|
||||
if (isCommand("scp") && index === 1) {
|
||||
for (const server of GetAllServers()) {
|
||||
allPos.push(server.hostname);
|
||||
}
|
||||
|
||||
return allPos;
|
||||
}
|
||||
|
||||
if (isCommand("scp") && index === 0) {
|
||||
addAllScripts();
|
||||
addAllLitFiles();
|
||||
addAllTextFiles();
|
||||
addAllDirectories();
|
||||
|
||||
return allPos;
|
||||
}
|
||||
|
||||
if (isCommand("cp") && index === 0) {
|
||||
addAllScripts();
|
||||
addAllTextFiles();
|
||||
addAllDirectories();
|
||||
return allPos;
|
||||
}
|
||||
|
||||
if (isCommand("connect")) {
|
||||
// All directly connected and backdoored servers are reachable
|
||||
return GetAllServers()
|
||||
.filter(
|
||||
(server) =>
|
||||
currServ.serversOnNetwork.includes(server.hostname) || (server instanceof Server && server.backdoorInstalled),
|
||||
)
|
||||
.map((server) => server.hostname);
|
||||
}
|
||||
|
||||
if (isCommand("nano") || isCommand("vim")) {
|
||||
addAllScripts();
|
||||
addAllTextFiles();
|
||||
addAllDirectories();
|
||||
|
||||
return allPos;
|
||||
}
|
||||
|
||||
if (isCommand("rm")) {
|
||||
addAllScripts();
|
||||
addAllPrograms();
|
||||
addAllLitFiles();
|
||||
addAllTextFiles();
|
||||
addAllCodingContracts();
|
||||
addAllDirectories();
|
||||
|
||||
return allPos;
|
||||
}
|
||||
|
||||
async function scriptAutocomplete(): Promise<string[] | undefined> {
|
||||
if (!isCommand("run") && !isCommand("tail") && !isCommand("kill") && !input.startsWith("./")) return;
|
||||
let copy = input;
|
||||
if (input.startsWith("./")) copy = "run " + input.slice(2);
|
||||
const commands = ParseCommands(copy);
|
||||
if (commands.length === 0) return;
|
||||
const command = ParseCommand(commands[commands.length - 1]);
|
||||
const filename = command[1] + "";
|
||||
if (!isScriptFilename(filename)) return; // Not a script.
|
||||
if (filename.endsWith(".script")) return; // Doesn't work with ns1.
|
||||
// Use regex to remove any leading './', and then check if it matches against
|
||||
// the output of processFilepath or if it matches with a '/' prepended,
|
||||
// this way autocomplete works inside of directories
|
||||
const script = currServ.scripts.get(filename);
|
||||
if (!script) return; // Doesn't exist.
|
||||
let loadedModule;
|
||||
try {
|
||||
//Will return the already compiled module if recompilation not needed.
|
||||
loadedModule = await compile(script, currServ.scripts);
|
||||
} catch (e) {
|
||||
//fail silently if the script fails to compile (e.g. syntax error)
|
||||
return;
|
||||
}
|
||||
if (!loadedModule || !loadedModule.autocomplete) return; // Doesn't have an autocomplete function.
|
||||
|
||||
const runArgs = { "--tail": Boolean, "-t": Number };
|
||||
const flags = libarg(runArgs, {
|
||||
permissive: true,
|
||||
argv: command.slice(2),
|
||||
});
|
||||
const flagFunc = Flags(flags._);
|
||||
const autocompleteData: AutocompleteData = {
|
||||
servers: GetAllServers().map((server) => server.hostname),
|
||||
scripts: [...currServ.scripts.keys()],
|
||||
txts: currServ.textFiles.map((txt) => txt.fn),
|
||||
flags: (schema: unknown) => {
|
||||
if (!Array.isArray(schema)) throw new Error("flags require an array of array");
|
||||
pos2 = schema.map((f: unknown) => {
|
||||
if (!Array.isArray(f)) throw new Error("flags require an array of array");
|
||||
if (f[0].length === 1) return "-" + f[0];
|
||||
return "--" + f[0];
|
||||
});
|
||||
try {
|
||||
return flagFunc(schema);
|
||||
} catch (err) {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
};
|
||||
let pos: string[] = [];
|
||||
let pos2: string[] = [];
|
||||
const options = loadedModule.autocomplete(autocompleteData, flags._);
|
||||
if (!Array.isArray(options)) throw new Error("autocomplete did not return list of strings");
|
||||
pos = pos.concat(options.map((x) => String(x)));
|
||||
return pos.concat(pos2);
|
||||
}
|
||||
const pos = await scriptAutocomplete();
|
||||
if (pos) return pos;
|
||||
|
||||
// If input starts with './', essentially treat it as a slimmer
|
||||
// invocation of `run`.
|
||||
if (input.startsWith("./")) {
|
||||
// All programs and scripts
|
||||
for (const scriptFilename of currServ.scripts.keys()) {
|
||||
const res = processFilepath(scriptFilename);
|
||||
if (res) {
|
||||
allPos.push(res);
|
||||
}
|
||||
}
|
||||
|
||||
for (const program of currServ.programs) {
|
||||
const res = processFilepath(program);
|
||||
if (res) {
|
||||
allPos.push(res);
|
||||
}
|
||||
}
|
||||
|
||||
// All coding contracts
|
||||
for (const cct of currServ.contracts) {
|
||||
const res = processFilepath(cct.fn);
|
||||
if (res) {
|
||||
allPos.push(res);
|
||||
}
|
||||
}
|
||||
|
||||
return allPos;
|
||||
}
|
||||
|
||||
if (isCommand("run")) {
|
||||
addAllScripts();
|
||||
addAllPrograms();
|
||||
addAllCodingContracts();
|
||||
addAllDirectories();
|
||||
}
|
||||
|
||||
if (isCommand("kill") || isCommand("tail") || isCommand("mem") || isCommand("check")) {
|
||||
addAllScripts();
|
||||
addAllDirectories();
|
||||
|
||||
return allPos;
|
||||
}
|
||||
|
||||
if (isCommand("cat")) {
|
||||
addAllMessages();
|
||||
addAllLitFiles();
|
||||
addAllTextFiles();
|
||||
addAllDirectories();
|
||||
addAllScripts();
|
||||
|
||||
return allPos;
|
||||
}
|
||||
|
||||
if (isCommand("download") || isCommand("mv")) {
|
||||
addAllScripts();
|
||||
addAllTextFiles();
|
||||
addAllDirectories();
|
||||
|
||||
return allPos;
|
||||
}
|
||||
|
||||
if (isCommand("cd")) {
|
||||
addAllDirectories();
|
||||
|
||||
return allPos;
|
||||
}
|
||||
|
||||
if (isCommand("ls") && index === 0) {
|
||||
addAllDirectories();
|
||||
}
|
||||
|
||||
if (isCommand("help")) {
|
||||
// Get names from here instead of commands array because some
|
||||
// undocumented/nonexistent commands are in the array
|
||||
return Object.keys(HelpTexts);
|
||||
}
|
||||
|
||||
return allPos;
|
||||
}
|
333
src/Terminal/getTabCompletionPossibilities.ts
Normal file
333
src/Terminal/getTabCompletionPossibilities.ts
Normal file
@ -0,0 +1,333 @@
|
||||
import { Aliases, GlobalAliases } from "../Alias";
|
||||
import { DarkWebItems } from "../DarkWeb/DarkWebItems";
|
||||
import { Player } from "@player";
|
||||
import { GetAllServers } from "../Server/AllServers";
|
||||
import { parseCommand, parseCommands } from "./Parser";
|
||||
import { HelpTexts } from "./HelpText";
|
||||
import { compile } from "../NetscriptJSEvaluator";
|
||||
import { Flags } from "../NetscriptFunctions/Flags";
|
||||
import { AutocompleteData } from "@nsdefs";
|
||||
import * as libarg from "arg";
|
||||
import { getAllDirectories, resolveDirectory, root } from "../Paths/Directory";
|
||||
import { resolveScriptFilePath } from "../Paths/ScriptFilePath";
|
||||
|
||||
// TODO: this shouldn't be hardcoded in two places with no typechecks to verify equivalence
|
||||
// An array of all Terminal commands
|
||||
const gameCommands = [
|
||||
"alias",
|
||||
"analyze",
|
||||
"backdoor",
|
||||
"cat",
|
||||
"cd",
|
||||
"changelog",
|
||||
"check",
|
||||
"clear",
|
||||
"cls",
|
||||
"connect",
|
||||
"cp",
|
||||
"download",
|
||||
"expr",
|
||||
"free",
|
||||
"grow",
|
||||
"hack",
|
||||
"help",
|
||||
"home",
|
||||
"hostname",
|
||||
"ifconfig",
|
||||
"kill",
|
||||
"killall",
|
||||
"ls",
|
||||
"lscpu",
|
||||
"mem",
|
||||
"mv",
|
||||
"nano",
|
||||
"ps",
|
||||
"rm",
|
||||
"run",
|
||||
"scan-analyze",
|
||||
"scan",
|
||||
"scp",
|
||||
"sudov",
|
||||
"tail",
|
||||
"theme",
|
||||
"top",
|
||||
"vim",
|
||||
"weaken",
|
||||
];
|
||||
|
||||
/** Suggest all completion possibilities for the last argument in the last command being typed
|
||||
* @param terminalText The current full text entered in the terminal
|
||||
* @param baseDir The current working directory.
|
||||
* @returns Array of possible string replacements for the current text being autocompleted.
|
||||
*/
|
||||
export async function getTabCompletionPossibilities(terminalText: string, baseDir = root): Promise<string[]> {
|
||||
// Get the current command text
|
||||
const currentText = /[^ ]*$/.exec(terminalText)?.[0] ?? "";
|
||||
// Remove the current text from the commands string
|
||||
const valueWithoutCurrent = terminalText.substring(0, terminalText.length - currentText.length);
|
||||
// Parse the commands string, this handles alias replacement as well.
|
||||
const commands = parseCommands(valueWithoutCurrent);
|
||||
if (!commands.length) commands.push("");
|
||||
// parse the last command into a commandArgs array, but convert to string
|
||||
const commandArray = parseCommand(commands[commands.length - 1]).map(String);
|
||||
commandArray.push(currentText);
|
||||
|
||||
/** How many separate strings make up the command, e.g. "run a" would result in 2 strings. */
|
||||
const commandLength = commandArray.length;
|
||||
|
||||
// To prevent needing to convert currentArg to lowercase for every comparison
|
||||
const requiredMatch = currentText.toLowerCase();
|
||||
|
||||
// If a relative directory is included in the path, this will store what the absolute path needs to start with to be valid
|
||||
let pathingRequiredMatch = currentText.toLowerCase();
|
||||
|
||||
/** The directory portion of the current input */
|
||||
let relativeDir = "";
|
||||
const slashIndex = currentText.lastIndexOf("/");
|
||||
|
||||
if (slashIndex !== -1) {
|
||||
relativeDir = currentText.substring(0, slashIndex + 1);
|
||||
const path = resolveDirectory(relativeDir, baseDir);
|
||||
// No valid terminal inputs contain a / that does not indicate a path
|
||||
if (path === null) return [];
|
||||
baseDir = path;
|
||||
pathingRequiredMatch = currentText.replace(/^.*\//, path).toLowerCase();
|
||||
} else if (baseDir !== root) {
|
||||
pathingRequiredMatch = (baseDir + currentText).toLowerCase();
|
||||
}
|
||||
|
||||
const possibilities: string[] = [];
|
||||
const currServ = Player.getCurrentServer();
|
||||
const homeComputer = Player.getHomeComputer();
|
||||
|
||||
// --- Functions for adding different types of data ---
|
||||
|
||||
interface AddAllGenericOptions {
|
||||
// The iterable to iterate through the data
|
||||
iterable: Iterable<string>;
|
||||
// Whether the item can be pathed to. Typically this is true for files (programs are an exception)
|
||||
usePathing?: boolean;
|
||||
// Whether to exclude the current text as one of the autocomplete options
|
||||
ignoreCurrent?: boolean;
|
||||
}
|
||||
function addGeneric({ iterable, usePathing, ignoreCurrent }: AddAllGenericOptions) {
|
||||
const requiredStart = usePathing ? pathingRequiredMatch : requiredMatch;
|
||||
for (const member of iterable) {
|
||||
if (ignoreCurrent && member.length <= requiredStart.length) continue;
|
||||
if (member.toLowerCase().startsWith(requiredStart)) {
|
||||
possibilities.push(usePathing ? relativeDir + member.substring(baseDir.length) : member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addAliases = () => addGeneric({ iterable: Object.keys(Aliases) });
|
||||
const addGlobalAliases = () => addGeneric({ iterable: Object.keys(GlobalAliases) });
|
||||
const addCommands = () => addGeneric({ iterable: gameCommands });
|
||||
const addDarkwebItems = () => addGeneric({ iterable: Object.values(DarkWebItems).map((item) => item.program) });
|
||||
const addServerNames = () => addGeneric({ iterable: GetAllServers().map((server) => server.hostname) });
|
||||
const addScripts = () => addGeneric({ iterable: currServ.scripts.keys(), usePathing: true });
|
||||
const addTextFiles = () => addGeneric({ iterable: currServ.textFiles.keys(), usePathing: true });
|
||||
const addCodingContracts = () => {
|
||||
addGeneric({ iterable: currServ.contracts.map((contract) => contract.fn), usePathing: true });
|
||||
};
|
||||
|
||||
const addLiterature = () => {
|
||||
addGeneric({ iterable: currServ.messages.filter((message) => message.endsWith(".lit")), usePathing: true });
|
||||
};
|
||||
|
||||
const addMessages = () => {
|
||||
addGeneric({ iterable: currServ.messages.filter((message) => message.endsWith(".msg")), usePathing: true });
|
||||
};
|
||||
|
||||
const addReachableServerNames = () => {
|
||||
addGeneric({
|
||||
iterable: GetAllServers()
|
||||
.filter((server) => server.backdoorInstalled || currServ.serversOnNetwork.includes(server.hostname))
|
||||
.map((server) => server.hostname),
|
||||
});
|
||||
};
|
||||
|
||||
const addPrograms = () => {
|
||||
// Only allow completed programs to autocomplete
|
||||
const programs = homeComputer.programs.filter((name) => name.endsWith(".exe"));
|
||||
// At all times, programs can be accessed without pathing
|
||||
addGeneric({ iterable: programs });
|
||||
// If we're on home and a path is being used, also include pathing results
|
||||
if (homeComputer.isConnectedTo && relativeDir) addGeneric({ iterable: programs, usePathing: true });
|
||||
};
|
||||
|
||||
const addDirectories = () => {
|
||||
addGeneric({ iterable: getAllDirectories(currServ), usePathing: true, ignoreCurrent: true });
|
||||
};
|
||||
|
||||
// Just some booleans so the mismatch between command length and arg number are not as confusing.
|
||||
const onCommand = commandLength === 1;
|
||||
const onFirstCommandArg = commandLength === 2;
|
||||
const onSecondCommandArg = commandLength === 3;
|
||||
|
||||
// These are always added.
|
||||
addGlobalAliases();
|
||||
|
||||
// If we're using a relative path, always add directories
|
||||
if (relativeDir) addDirectories();
|
||||
|
||||
// -- Handling different commands -- //
|
||||
// Command is what is being autocompleted
|
||||
if (onCommand) {
|
||||
addAliases();
|
||||
addCommands();
|
||||
// Allow any relative pathing as a command arg to act as previous ./ command
|
||||
if (relativeDir) {
|
||||
addScripts();
|
||||
addPrograms();
|
||||
addCodingContracts();
|
||||
}
|
||||
}
|
||||
|
||||
switch (commandArray[0]) {
|
||||
case "buy":
|
||||
addDarkwebItems();
|
||||
return possibilities;
|
||||
|
||||
case "cat":
|
||||
addScripts();
|
||||
addTextFiles();
|
||||
addMessages();
|
||||
addLiterature();
|
||||
return possibilities;
|
||||
|
||||
case "cd":
|
||||
case "ls":
|
||||
if (onFirstCommandArg && !relativeDir) addDirectories();
|
||||
return possibilities;
|
||||
|
||||
case "check":
|
||||
case "kill":
|
||||
case "mem":
|
||||
case "tail":
|
||||
addScripts();
|
||||
return possibilities;
|
||||
|
||||
case "connect":
|
||||
if (onFirstCommandArg) addReachableServerNames();
|
||||
return possibilities;
|
||||
|
||||
case "cp":
|
||||
if (onFirstCommandArg) {
|
||||
// We're autocompleting a source content file
|
||||
addScripts();
|
||||
addTextFiles();
|
||||
}
|
||||
return possibilities;
|
||||
|
||||
case "download":
|
||||
case "mv":
|
||||
// download only takes one arg, and for mv we only want to autocomplete the first one
|
||||
if (onFirstCommandArg) {
|
||||
addScripts();
|
||||
addTextFiles();
|
||||
}
|
||||
return possibilities;
|
||||
|
||||
case "help":
|
||||
if (onFirstCommandArg) {
|
||||
addGeneric({ iterable: Object.keys(HelpTexts), usePathing: false });
|
||||
}
|
||||
return possibilities;
|
||||
|
||||
case "nano":
|
||||
case "vim":
|
||||
addScripts();
|
||||
addTextFiles();
|
||||
return possibilities;
|
||||
|
||||
case "scp":
|
||||
if (onFirstCommandArg) {
|
||||
addScripts();
|
||||
addTextFiles();
|
||||
addLiterature();
|
||||
} else if (onSecondCommandArg) addServerNames();
|
||||
return possibilities;
|
||||
|
||||
case "rm":
|
||||
addScripts();
|
||||
addPrograms();
|
||||
addLiterature();
|
||||
addTextFiles();
|
||||
addCodingContracts();
|
||||
return possibilities;
|
||||
|
||||
case "run":
|
||||
if (onFirstCommandArg) {
|
||||
addPrograms();
|
||||
addCodingContracts();
|
||||
}
|
||||
// Spill over into next cases
|
||||
case "check":
|
||||
case "tail":
|
||||
case "kill":
|
||||
if (onFirstCommandArg) addScripts();
|
||||
else {
|
||||
const options = await scriptAutocomplete();
|
||||
if (options) addGeneric({ iterable: options, usePathing: false });
|
||||
}
|
||||
return possibilities;
|
||||
|
||||
default:
|
||||
return possibilities;
|
||||
}
|
||||
|
||||
async function scriptAutocomplete(): Promise<string[] | undefined> {
|
||||
let inputCopy = commandArray.join(" ");
|
||||
if (commandLength === 1) inputCopy = "run " + inputCopy;
|
||||
const commands = parseCommands(inputCopy);
|
||||
if (commands.length === 0) return;
|
||||
const command = parseCommand(commands[commands.length - 1]);
|
||||
const filename = resolveScriptFilePath(String(command[1]), baseDir);
|
||||
if (!filename) return; // Not a script path.
|
||||
if (filename.endsWith(".script")) return; // Doesn't work with ns1.
|
||||
const script = currServ.scripts.get(filename);
|
||||
if (!script) return; // Doesn't exist.
|
||||
|
||||
let loadedModule;
|
||||
try {
|
||||
//Will return the already compiled module if recompilation not needed.
|
||||
loadedModule = await compile(script, currServ.scripts);
|
||||
} catch (e) {
|
||||
//fail silently if the script fails to compile (e.g. syntax error)
|
||||
return;
|
||||
}
|
||||
if (!loadedModule || !loadedModule.autocomplete) return; // Doesn't have an autocomplete function.
|
||||
|
||||
const runArgs = { "--tail": Boolean, "-t": Number };
|
||||
const flags = libarg(runArgs, {
|
||||
permissive: true,
|
||||
argv: command.slice(2),
|
||||
});
|
||||
const flagFunc = Flags(flags._);
|
||||
const autocompleteData: AutocompleteData = {
|
||||
servers: GetAllServers().map((server) => server.hostname),
|
||||
scripts: [...currServ.scripts.keys()],
|
||||
txts: [...currServ.textFiles.keys()],
|
||||
flags: (schema: unknown) => {
|
||||
if (!Array.isArray(schema)) throw new Error("flags require an array of array");
|
||||
pos2 = schema.map((f: unknown) => {
|
||||
if (!Array.isArray(f)) throw new Error("flags require an array of array");
|
||||
if (f[0].length === 1) return "-" + f[0];
|
||||
return "--" + f[0];
|
||||
});
|
||||
try {
|
||||
return flagFunc(schema);
|
||||
} catch (err) {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
};
|
||||
let pos: string[] = [];
|
||||
let pos2: string[] = [];
|
||||
const options = loadedModule.autocomplete(autocompleteData, flags._);
|
||||
if (!Array.isArray(options)) throw new Error("autocomplete did not return list of strings");
|
||||
pos = pos.concat(options.map((x) => String(x)));
|
||||
return pos.concat(pos2);
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
import { containsAllStrings, longestCommonStart } from "../utils/StringHelperFunctions";
|
||||
|
||||
/**
|
||||
* Implements tab completion for the Terminal
|
||||
*
|
||||
* @param command {string} Terminal command, excluding the last incomplete argument
|
||||
* @param arg {string} Last argument that is being completed
|
||||
* @param allPossibilities {string[]} All values that `arg` can complete to
|
||||
*/
|
||||
export function tabCompletion(
|
||||
command: string,
|
||||
arg: string,
|
||||
allPossibilities: string[],
|
||||
oldValue: string,
|
||||
): string[] | string | undefined {
|
||||
if (!(allPossibilities.constructor === Array)) {
|
||||
return;
|
||||
}
|
||||
if (!containsAllStrings(allPossibilities)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all options in allPossibilities that do not match the current string
|
||||
// that we are attempting to autocomplete
|
||||
if (arg === "") {
|
||||
for (let i = allPossibilities.length - 1; i >= 0; --i) {
|
||||
if (!allPossibilities[i].toLowerCase().startsWith(command.toLowerCase())) {
|
||||
allPossibilities.splice(i, 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = allPossibilities.length - 1; i >= 0; --i) {
|
||||
if (!allPossibilities[i].toLowerCase().startsWith(arg.toLowerCase())) {
|
||||
allPossibilities.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const semiColonIndex = oldValue.lastIndexOf(";");
|
||||
|
||||
let val = "";
|
||||
if (allPossibilities.length === 0) {
|
||||
return;
|
||||
} else if (allPossibilities.length === 1) {
|
||||
if (arg === "") {
|
||||
//Autocomplete command
|
||||
val = allPossibilities[0];
|
||||
} else {
|
||||
val = command + " " + allPossibilities[0];
|
||||
}
|
||||
|
||||
if (semiColonIndex === -1) {
|
||||
// No semicolon, so replace the whole command
|
||||
return val;
|
||||
} else {
|
||||
// Replace only after the last semicolon
|
||||
return oldValue.slice(0, semiColonIndex + 1) + " " + val;
|
||||
}
|
||||
} else {
|
||||
const longestStartSubstr = longestCommonStart(allPossibilities);
|
||||
/**
|
||||
* If the longest common starting substring of remaining possibilities is the same
|
||||
* as whatever's already in terminal, just list all possible options. Otherwise,
|
||||
* change the input in the terminal to the longest common starting substr
|
||||
*/
|
||||
if (arg === "") {
|
||||
if (longestStartSubstr === command) {
|
||||
return allPossibilities;
|
||||
} else if (semiColonIndex === -1) {
|
||||
// No semicolon, so replace the whole command
|
||||
return longestStartSubstr;
|
||||
} else {
|
||||
// Replace only after the last semicolon
|
||||
return `${oldValue.slice(0, semiColonIndex + 1)} ${longestStartSubstr}`;
|
||||
}
|
||||
} else if (longestStartSubstr === arg) {
|
||||
// List all possible options
|
||||
return allPossibilities;
|
||||
} else if (semiColonIndex == -1) {
|
||||
// No semicolon, so replace the whole command
|
||||
return `${command} ${longestStartSubstr}`;
|
||||
} else {
|
||||
// Replace only after the last semicolon
|
||||
return `${oldValue.slice(0, semiColonIndex + 1)} ${command} ${longestStartSubstr}`;
|
||||
}
|
||||
}
|
||||
}
|
@ -10,9 +10,9 @@ import TextField from "@mui/material/TextField";
|
||||
import { KEY, KEYCODE } from "../../utils/helpers/keyCodes";
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { Player } from "@player";
|
||||
import { determineAllPossibilitiesForTabCompletion } from "../determineAllPossibilitiesForTabCompletion";
|
||||
import { tabCompletion } from "../tabCompletion";
|
||||
import { getTabCompletionPossibilities } from "../getTabCompletionPossibilities";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { longestCommonStart } from "../../utils/StringHelperFunctions";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
@ -26,7 +26,6 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
padding: theme.spacing(0),
|
||||
},
|
||||
preformatted: {
|
||||
whiteSpace: "pre-wrap",
|
||||
margin: theme.spacing(0),
|
||||
},
|
||||
list: {
|
||||
@ -205,54 +204,18 @@ export function TerminalInput(): React.ReactElement {
|
||||
}
|
||||
|
||||
// Autocomplete
|
||||
if (event.key === KEY.TAB && value !== "") {
|
||||
if (event.key === KEY.TAB) {
|
||||
event.preventDefault();
|
||||
|
||||
let copy = value;
|
||||
const semiColonIndex = copy.lastIndexOf(";");
|
||||
if (semiColonIndex !== -1) {
|
||||
copy = copy.slice(semiColonIndex + 1);
|
||||
}
|
||||
|
||||
copy = copy.trim();
|
||||
copy = copy.replace(/\s\s+/g, " ");
|
||||
|
||||
const commandArray = copy.split(" ");
|
||||
let index = commandArray.length - 2;
|
||||
if (index < -1) {
|
||||
index = 0;
|
||||
}
|
||||
const allPos = await determineAllPossibilitiesForTabCompletion(copy, index, Terminal.cwd());
|
||||
if (allPos.length == 0) {
|
||||
const possibilities = await getTabCompletionPossibilities(value, Terminal.cwd());
|
||||
if (possibilities.length === 0) return;
|
||||
if (possibilities.length === 1) {
|
||||
saveValue(value.replace(/[^ ]*$/, possibilities[0]) + " ");
|
||||
return;
|
||||
}
|
||||
|
||||
let arg = "";
|
||||
let command = "";
|
||||
if (commandArray.length == 0) {
|
||||
return;
|
||||
}
|
||||
if (commandArray.length == 1) {
|
||||
command = commandArray[0];
|
||||
} else if (commandArray.length == 2) {
|
||||
command = commandArray[0];
|
||||
arg = commandArray[1];
|
||||
} else if (commandArray.length == 3) {
|
||||
command = commandArray[0] + " " + commandArray[1];
|
||||
arg = commandArray[2];
|
||||
} else {
|
||||
arg = commandArray.pop() + "";
|
||||
command = commandArray.join(" ");
|
||||
}
|
||||
|
||||
let newValue = tabCompletion(command, arg, allPos, value);
|
||||
if (typeof newValue === "string" && newValue !== "") {
|
||||
if (!newValue.endsWith(" ") && !newValue.endsWith("/") && allPos.length === 1) newValue += " ";
|
||||
saveValue(newValue);
|
||||
}
|
||||
if (Array.isArray(newValue)) {
|
||||
setPossibilities(newValue);
|
||||
}
|
||||
// More than one possibility, check to see if there is a longer common string than currentText.
|
||||
const longestMatch = longestCommonStart(possibilities);
|
||||
saveValue(value.replace(/[^ ]*$/, longestMatch));
|
||||
setPossibilities(possibilities);
|
||||
}
|
||||
|
||||
// Clear screen.
|
||||
@ -310,6 +273,7 @@ export function TerminalInput(): React.ReactElement {
|
||||
} else {
|
||||
++Terminal.commandHistoryIndex;
|
||||
const prevCommand = Terminal.commandHistory[Terminal.commandHistoryIndex];
|
||||
|
||||
saveValue(prevCommand);
|
||||
}
|
||||
}
|
||||
@ -405,7 +369,12 @@ export function TerminalInput(): React.ReactElement {
|
||||
onKeyDown: onKeyDown,
|
||||
}}
|
||||
></TextField>
|
||||
<Popper open={possibilities.length > 0} anchorEl={terminalInput.current} placement={"top-start"}>
|
||||
<Popper
|
||||
open={possibilities.length > 0}
|
||||
anchorEl={terminalInput.current}
|
||||
placement={"top"}
|
||||
sx={{ maxWidth: "75%" }}
|
||||
>
|
||||
<Paper sx={{ m: 1, p: 2 }}>
|
||||
<Typography classes={{ root: classes.preformatted }} color={"primary"} paragraph={false}>
|
||||
Possible autocomplete candidates:
|
||||
|
@ -1,29 +1,27 @@
|
||||
import { dialogBoxCreate } from "./ui/React/DialogBox";
|
||||
import { BaseServer } from "./Server/BaseServer";
|
||||
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "./utils/JSONReviver";
|
||||
import { removeLeadingSlash, isInRootDirectory } from "./Terminal/DirectoryHelpers";
|
||||
import { TextFilePath } from "./Paths/TextFilePath";
|
||||
import { ContentFile } from "./Paths/ContentFile";
|
||||
|
||||
/** Represents a plain text file that is typically stored on a server. */
|
||||
export class TextFile {
|
||||
export class TextFile implements ContentFile {
|
||||
/** The full file name. */
|
||||
fn: string;
|
||||
filename: TextFilePath;
|
||||
|
||||
/** The content of the file. */
|
||||
text: string;
|
||||
|
||||
//TODO 2.3: Why are we using getter/setter for fn as filename? Rename parameter as more-readable "filename"
|
||||
/** The full file name. */
|
||||
get filename(): string {
|
||||
return this.fn;
|
||||
// Shared interface on Script and TextFile for accessing content
|
||||
get content() {
|
||||
return this.text;
|
||||
}
|
||||
set content(text: string) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
/** The full file name. */
|
||||
set filename(value: string) {
|
||||
this.fn = value;
|
||||
}
|
||||
|
||||
constructor(fn = "", txt = "") {
|
||||
this.fn = (fn.endsWith(".txt") ? fn : `${fn}.txt`).replace(/\s+/g, "");
|
||||
constructor(filename = "default.txt" as TextFilePath, txt = "") {
|
||||
this.filename = filename;
|
||||
this.text = txt;
|
||||
}
|
||||
|
||||
@ -38,7 +36,7 @@ export class TextFile {
|
||||
const a: HTMLAnchorElement = document.createElement("a");
|
||||
const url: string = URL.createObjectURL(file);
|
||||
a.href = url;
|
||||
a.download = this.fn;
|
||||
a.download = this.filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(() => {
|
||||
@ -54,7 +52,7 @@ export class TextFile {
|
||||
|
||||
/** Shows the content to the user via the game's dialog box. */
|
||||
show(): void {
|
||||
dialogBoxCreate(`${this.fn}\n\n${this.text}`);
|
||||
dialogBoxCreate(`${this.filename}\n\n${this.text}`);
|
||||
}
|
||||
|
||||
/** Serialize the current file to a JSON save state. */
|
||||
@ -67,6 +65,12 @@ export class TextFile {
|
||||
this.text = txt;
|
||||
}
|
||||
|
||||
deleteFromServer(server: BaseServer): boolean {
|
||||
if (!server.textFiles.has(this.filename)) return false;
|
||||
server.textFiles.delete(this.filename);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Initializes a TextFile from a JSON save state. */
|
||||
static fromJSON(value: IReviverValue): TextFile {
|
||||
return Generic_fromJSON(TextFile, value.data);
|
||||
@ -74,46 +78,3 @@ export class TextFile {
|
||||
}
|
||||
|
||||
constructorsForReviver.TextFile = TextFile;
|
||||
|
||||
/**
|
||||
* Retrieve the file object for the filename on the specified server.
|
||||
* @param fn The file name to look for
|
||||
* @param server The server object to look in
|
||||
* @returns The file object, or null if it couldn't find it.
|
||||
*/
|
||||
export function getTextFile(fn: string, server: BaseServer): TextFile | null {
|
||||
let filename: string = !fn.endsWith(".txt") ? `${fn}.txt` : fn;
|
||||
|
||||
if (isInRootDirectory(filename)) {
|
||||
filename = removeLeadingSlash(filename);
|
||||
}
|
||||
|
||||
for (const file of server.textFiles) {
|
||||
if (file.fn === filename) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TextFile on the target server.
|
||||
* @param fn The file name to create.
|
||||
* @param txt The contents of the file.
|
||||
* @param server The server that the file should be created on.
|
||||
* @returns The instance of the file.
|
||||
*/
|
||||
export function createTextFile(fn: string, txt: string, server: BaseServer): TextFile | undefined {
|
||||
if (getTextFile(fn, server) !== null) {
|
||||
// This should probably be a `throw`...
|
||||
/* tslint:disable-next-line:no-console */
|
||||
console.error(`A file named "${fn}" already exists on server ${server.hostname}.`);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
const file: TextFile = new TextFile(fn, txt);
|
||||
server.textFiles.push(file);
|
||||
|
||||
return file;
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
import type { IReviverValue } from "../utils/JSONReviver";
|
||||
// Jsonable versions of builtin JS class objects
|
||||
|
||||
// Loosened type requirements on input for has, also has provides typecheck info.
|
||||
export interface JSONSet<T> {
|
||||
has: (value: unknown) => value is T;
|
||||
}
|
||||
export class JSONSet<T> extends Set<T> {
|
||||
toJSON(): IReviverValue {
|
||||
return { ctor: "JSONSet", data: Array.from(this) };
|
||||
@ -9,7 +13,11 @@ export class JSONSet<T> extends Set<T> {
|
||||
}
|
||||
}
|
||||
|
||||
export class JSONMap<K, V> extends Map<K, V> {
|
||||
// Loosened type requirements on input for has. has also provides typecheck info.
|
||||
export interface JSONMap<K, __V> {
|
||||
has: (key: unknown) => key is K;
|
||||
}
|
||||
export class JSONMap<K, __V> extends Map<K, __V> {
|
||||
toJSON(): IReviverValue {
|
||||
return { ctor: "JSONMap", data: Array.from(this) };
|
||||
}
|
||||
|
@ -1,18 +1,3 @@
|
||||
// Script Filename
|
||||
export type ScriptFilename = string /*& { __type: "ScriptFilename" }*/;
|
||||
/*export function isScriptFilename(value: string): value is ScriptFilename {
|
||||
// implementation
|
||||
}*/
|
||||
/*export function sanitizeScriptFilename(filename: string): ScriptFilename {
|
||||
// implementation
|
||||
}*/
|
||||
export function scriptFilenameFromImport(importPath: string, ns1?: boolean): ScriptFilename {
|
||||
if (importPath.startsWith("./")) importPath = importPath.substring(2);
|
||||
if (!ns1 && !importPath.endsWith(".js")) importPath += ".js";
|
||||
if (ns1 && !importPath.endsWith(".script")) importPath += ".script";
|
||||
return importPath as ScriptFilename;
|
||||
}
|
||||
|
||||
// Server name
|
||||
export type ServerName = string /*& { __type: "ServerName" }*/;
|
||||
/*export function isExistingServerName(value: unknown): value is ServerName {
|
||||
|
@ -7,24 +7,26 @@ import { Programs } from "../Programs/Programs";
|
||||
import { Work, WorkType } from "./Work";
|
||||
import { Program } from "../Programs/Program";
|
||||
import { calculateIntelligenceBonus } from "../PersonObjects/formulas/intelligence";
|
||||
import { asProgramFilePath } from "../Paths/ProgramFilePath";
|
||||
import { CompletedProgramName } from "../Programs/Programs";
|
||||
|
||||
export const isCreateProgramWork = (w: Work | null): w is CreateProgramWork =>
|
||||
w !== null && w.type === WorkType.CREATE_PROGRAM;
|
||||
|
||||
interface CreateProgramWorkParams {
|
||||
programName: string;
|
||||
programName: CompletedProgramName;
|
||||
singularity: boolean;
|
||||
}
|
||||
|
||||
export class CreateProgramWork extends Work {
|
||||
programName: string;
|
||||
programName: CompletedProgramName;
|
||||
// amount of effective work completed on the program (time boosted by skills).
|
||||
unitCompleted: number;
|
||||
|
||||
constructor(params?: CreateProgramWorkParams) {
|
||||
super(WorkType.CREATE_PROGRAM, params?.singularity ?? true);
|
||||
this.unitCompleted = 0;
|
||||
this.programName = params?.programName ?? "";
|
||||
this.programName = params?.programName ?? CompletedProgramName.bruteSsh;
|
||||
|
||||
if (params) {
|
||||
for (let i = 0; i < Player.getHomeComputer().programs.length; ++i) {
|
||||
@ -50,9 +52,7 @@ export class CreateProgramWork extends Work {
|
||||
}
|
||||
|
||||
getProgram(): Program {
|
||||
const p = Object.values(Programs).find((p) => p.name.toLowerCase() === this.programName.toLowerCase());
|
||||
if (!p) throw new Error("Create program work started with invalid program " + this.programName);
|
||||
return p;
|
||||
return Programs[this.programName];
|
||||
}
|
||||
|
||||
process(cycles: number): boolean {
|
||||
@ -75,7 +75,7 @@ export class CreateProgramWork extends Work {
|
||||
return false;
|
||||
}
|
||||
finish(cancelled: boolean): void {
|
||||
const programName = this.programName;
|
||||
const programName = asProgramFilePath(this.programName);
|
||||
if (!cancelled) {
|
||||
//Complete case
|
||||
Player.gainIntelligenceExp(
|
||||
@ -95,7 +95,7 @@ export class CreateProgramWork extends Work {
|
||||
} else if (!Player.getHomeComputer().programs.includes(programName)) {
|
||||
//Incomplete case
|
||||
const perc = ((100 * this.unitCompleted) / this.unitNeeded()).toFixed(2);
|
||||
const incompleteName = programName + "-" + perc + "%-INC";
|
||||
const incompleteName = asProgramFilePath(programName + "-" + perc + "%-INC");
|
||||
Player.getHomeComputer().programs.push(incompleteName);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions"
|
||||
import { initAugmentations } from "./Augmentation/AugmentationHelpers";
|
||||
import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
|
||||
import { initSourceFiles } from "./SourceFile/SourceFiles";
|
||||
import { initDarkWebItems } from "./DarkWeb/DarkWebItems";
|
||||
import { generateRandomContract } from "./CodingContractGenerator";
|
||||
import { initCompanies } from "./Company/Companies";
|
||||
import { CONSTANTS } from "./Constants";
|
||||
@ -235,7 +234,6 @@ const Engine: {
|
||||
if (loadGame(saveString)) {
|
||||
FormatsNeedToChange.emit();
|
||||
initSourceFiles();
|
||||
initDarkWebItems();
|
||||
initAugmentations(); // Also calls Player.reapplyAllAugmentations()
|
||||
Player.reapplyAllSourceFiles();
|
||||
if (Player.hasWseAccount) {
|
||||
@ -377,7 +375,6 @@ const Engine: {
|
||||
// No save found, start new game
|
||||
FormatsNeedToChange.emit();
|
||||
initSourceFiles();
|
||||
initDarkWebItems();
|
||||
Engine.start(); // Run main game loop and Scripts loop
|
||||
Player.init();
|
||||
initForeignServers(Player.getHomeComputer());
|
||||
|
@ -23,7 +23,7 @@ import createStyles from "@mui/styles/createStyles";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { Page, SimplePage, IRouter, ScriptEditorRouteOptions } from "./Router";
|
||||
import { Page, SimplePage, IRouter } from "./Router";
|
||||
import { Overview } from "./React/Overview";
|
||||
import { SidebarRoot } from "../Sidebar/ui/SidebarRoot";
|
||||
import { AugmentationsRoot } from "../Augmentation/ui/AugmentationsRoot";
|
||||
@ -78,6 +78,8 @@ import { isFactionWork } from "../Work/FactionWork";
|
||||
import { V2Modal } from "../utils/V2Modal";
|
||||
import { MathJaxContext } from "better-react-mathjax";
|
||||
import { useRerender } from "./React/hooks";
|
||||
import { ScriptFilePath } from "src/Paths/ScriptFilePath";
|
||||
import { TextFilePath } from "src/Paths/TextFilePath";
|
||||
|
||||
const htmlLocation = location;
|
||||
|
||||
@ -126,7 +128,13 @@ function determineStartPage(): Page {
|
||||
|
||||
export function GameRoot(): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const [{ files, vim }, setEditorOptions] = useState({ files: {}, vim: false });
|
||||
const [{ files, vim }, setEditorOptions] = useState<{
|
||||
files: Map<ScriptFilePath | TextFilePath, string>;
|
||||
vim: boolean;
|
||||
}>({
|
||||
files: new Map(),
|
||||
vim: false,
|
||||
});
|
||||
const [page, setPage] = useState(determineStartPage());
|
||||
const rerender = useRerender();
|
||||
const [augPage, setAugPage] = useState<boolean>(false);
|
||||
@ -189,7 +197,7 @@ export function GameRoot(): React.ReactElement {
|
||||
setPage(Page.Faction);
|
||||
if (faction) setFaction(faction);
|
||||
},
|
||||
toScriptEditor: (files: Record<string, string>, options?: ScriptEditorRouteOptions) => {
|
||||
toScriptEditor: (files = new Map(), options) => {
|
||||
setEditorOptions({
|
||||
files,
|
||||
vim: !!options?.vim,
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { ScriptFilePath } from "../Paths/ScriptFilePath";
|
||||
import { TextFilePath } from "../Paths/TextFilePath";
|
||||
import { Faction } from "../Faction/Faction";
|
||||
import { Location } from "../Locations/Location";
|
||||
|
||||
@ -67,7 +69,7 @@ export interface IRouter {
|
||||
toFaction(faction: Faction, augPage?: boolean): void; // faction name
|
||||
toInfiltration(location: Location): void;
|
||||
toJob(location: Location): void;
|
||||
toScriptEditor(files?: Record<string, string>, options?: ScriptEditorRouteOptions): void;
|
||||
toScriptEditor(files?: Map<ScriptFilePath | TextFilePath, string>, options?: ScriptEditorRouteOptions): void;
|
||||
toLocation(location: Location): void;
|
||||
toImportSave(base64Save: string, automatic?: boolean): void;
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
|
||||
import { PlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation";
|
||||
import { Player } from "@player";
|
||||
import { Script } from "../Script/Script";
|
||||
import { FormattedCode, Script } from "../Script/Script";
|
||||
import { GetAllServers } from "../Server/AllServers";
|
||||
import { resolveTextFilePath } from "../Paths/TextFilePath";
|
||||
import { resolveScriptFilePath } from "../Paths/ScriptFilePath";
|
||||
|
||||
const detect: [string, string][] = [
|
||||
["getHackTime", "returns milliseconds"],
|
||||
@ -52,7 +54,7 @@ function hasChanges(code: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function convert(code: string): string {
|
||||
function convert(code: string): FormattedCode {
|
||||
const lines = code.split("\n");
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@ -70,7 +72,8 @@ function convert(code: string): string {
|
||||
}
|
||||
out.push(line);
|
||||
}
|
||||
return out.join("\n");
|
||||
code = out.join("\n");
|
||||
return Script.formatCode(code);
|
||||
}
|
||||
|
||||
export function AwardNFG(n = 1): void {
|
||||
@ -122,7 +125,9 @@ export function v1APIBreak(): void {
|
||||
}
|
||||
if (txt !== "") {
|
||||
const home = Player.getHomeComputer();
|
||||
home.writeToTextFile("v1_DETECTED_CHANGES.txt", txt);
|
||||
const textPath = resolveTextFilePath("v1_DETECTED_CHANGES.txt");
|
||||
if (!textPath) return console.error("Filepath unexpectedly failed to parse");
|
||||
home.writeToTextFile(textPath, txt);
|
||||
}
|
||||
|
||||
// API break function is called before version31 / 2.3.0 changes - scripts is still an array
|
||||
@ -130,8 +135,14 @@ export function v1APIBreak(): void {
|
||||
const backups: Script[] = [];
|
||||
for (const script of server.scripts) {
|
||||
if (!hasChanges(script.code)) continue;
|
||||
const prefix = script.filename.includes("/") ? "/BACKUP_" : "BACKUP_";
|
||||
backups.push(new Script(prefix + script.filename, script.code, script.server));
|
||||
// Sanitize first before combining
|
||||
const oldFilename = resolveScriptFilePath(script.filename);
|
||||
const filename = resolveScriptFilePath("BACKUP_" + oldFilename);
|
||||
if (!filename) {
|
||||
console.error(`Unexpected error resolving backup path for ${script.filename}`);
|
||||
continue;
|
||||
}
|
||||
backups.push(new Script(filename, script.code, script.server));
|
||||
script.code = convert(script.code);
|
||||
}
|
||||
server.scripts = server.scripts.concat(backups);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { TextFilePath } from "../Paths/TextFilePath";
|
||||
import { saveObject } from "../SaveObject";
|
||||
import { Script } from "../Script/Script";
|
||||
import { GetAllServers, GetServer } from "../Server/AllServers";
|
||||
@ -232,7 +233,7 @@ export const v2APIBreak = () => {
|
||||
processScript(rules, script);
|
||||
}
|
||||
|
||||
home.writeToTextFile("V2_0_0_API_BREAK.txt", formatRules(rules));
|
||||
home.writeToTextFile("V2_0_0_API_BREAK.txt" as TextFilePath, formatRules(rules));
|
||||
openV2Modal();
|
||||
|
||||
for (const server of GetAllServers()) {
|
||||
|
@ -6,6 +6,7 @@ import { AddToAllServers, DeleteServer } from "../../../src/Server/AllServers";
|
||||
import { WorkerScriptStartStopEventEmitter } from "../../../src/Netscript/WorkerScriptStartStopEventEmitter";
|
||||
import { AlertEvents } from "../../../src/ui/React/AlertManager";
|
||||
import type { Script } from "src/Script/Script";
|
||||
import { ScriptFilePath } from "src/Paths/ScriptFilePath";
|
||||
|
||||
// Replace Blob/ObjectURL functions, because they don't work natively in Jest
|
||||
global.Blob = class extends Blob {
|
||||
@ -77,7 +78,7 @@ test.each([
|
||||
return "data:text/javascript," + encodeURIComponent(blob.code);
|
||||
};
|
||||
|
||||
let server;
|
||||
let server = {} as Server;
|
||||
let eventDelete = () => {};
|
||||
let alertDelete = () => {};
|
||||
try {
|
||||
@ -87,10 +88,10 @@ test.each([
|
||||
server = new Server({ hostname: "home", adminRights: true, maxRam: 8 });
|
||||
AddToAllServers(server);
|
||||
for (const s of scripts) {
|
||||
expect(server.writeToScriptFile(s.name, s.code)).toEqual({ success: true, overwritten: false });
|
||||
expect(server.writeToScriptFile(s.name as ScriptFilePath, s.code)).toEqual({ overwritten: false });
|
||||
}
|
||||
|
||||
const script = server.scripts.get(scripts[scripts.length - 1].name) as Script;
|
||||
const script = server.scripts.get(scripts[scripts.length - 1].name as ScriptFilePath) as Script;
|
||||
expect(script.filename).toEqual(scripts[scripts.length - 1].name);
|
||||
|
||||
const ramUsage = script.getRamUsage(server.scripts);
|
||||
|
@ -1,306 +0,0 @@
|
||||
import * as dirHelpers from "../../../src/Terminal/DirectoryHelpers";
|
||||
|
||||
describe("Terminal Directory Tests", function () {
|
||||
describe("removeLeadingSlash()", function () {
|
||||
const removeLeadingSlash = dirHelpers.removeLeadingSlash;
|
||||
|
||||
it("should remove first slash in a string", function () {
|
||||
expect(removeLeadingSlash("/")).toEqual("");
|
||||
expect(removeLeadingSlash("/foo.txt")).toEqual("foo.txt");
|
||||
expect(removeLeadingSlash("/foo/file.txt")).toEqual("foo/file.txt");
|
||||
});
|
||||
|
||||
it("should only remove one slash", function () {
|
||||
expect(removeLeadingSlash("///")).toEqual("//");
|
||||
expect(removeLeadingSlash("//foo")).toEqual("/foo");
|
||||
});
|
||||
|
||||
it("should do nothing for a string that doesn't start with a slash", function () {
|
||||
expect(removeLeadingSlash("foo.txt")).toEqual("foo.txt");
|
||||
expect(removeLeadingSlash("foo/test.txt")).toEqual("foo/test.txt");
|
||||
});
|
||||
|
||||
it("should not fail on an empty string", function () {
|
||||
expect(removeLeadingSlash.bind(null, "")).not.toThrow();
|
||||
expect(removeLeadingSlash("")).toEqual("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeTrailingSlash()", function () {
|
||||
const removeTrailingSlash = dirHelpers.removeTrailingSlash;
|
||||
|
||||
it("should remove last slash in a string", function () {
|
||||
expect(removeTrailingSlash("/")).toEqual("");
|
||||
expect(removeTrailingSlash("foo.txt/")).toEqual("foo.txt");
|
||||
expect(removeTrailingSlash("foo/file.txt/")).toEqual("foo/file.txt");
|
||||
});
|
||||
|
||||
it("should only remove one slash", function () {
|
||||
expect(removeTrailingSlash("///")).toEqual("//");
|
||||
expect(removeTrailingSlash("foo//")).toEqual("foo/");
|
||||
});
|
||||
|
||||
it("should do nothing for a string that doesn't end with a slash", function () {
|
||||
expect(removeTrailingSlash("foo.txt")).toEqual("foo.txt");
|
||||
expect(removeTrailingSlash("foo/test.txt")).toEqual("foo/test.txt");
|
||||
});
|
||||
|
||||
it("should not fail on an empty string", function () {
|
||||
expect(removeTrailingSlash.bind(null, "")).not.toThrow();
|
||||
expect(removeTrailingSlash("")).toEqual("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidFilename()", function () {
|
||||
const isValidFilename = dirHelpers.isValidFilename;
|
||||
|
||||
it("should return true for valid filenames", function () {
|
||||
expect(isValidFilename("test.txt")).toEqual(true);
|
||||
expect(isValidFilename("123.script")).toEqual(true);
|
||||
expect(isValidFilename("foo123.b")).toEqual(true);
|
||||
expect(isValidFilename("my_script.script")).toEqual(true);
|
||||
expect(isValidFilename("my-script.script")).toEqual(true);
|
||||
expect(isValidFilename("_foo.lit")).toEqual(true);
|
||||
expect(isValidFilename("mult.periods.script")).toEqual(true);
|
||||
expect(isValidFilename("mult.per-iods.again.script")).toEqual(true);
|
||||
expect(isValidFilename("BruteSSH.exe-50%-INC")).toEqual(true);
|
||||
expect(isValidFilename("DeepscanV1.exe-1.01%-INC")).toEqual(true);
|
||||
expect(isValidFilename("DeepscanV2.exe-1.00%-INC")).toEqual(true);
|
||||
expect(isValidFilename("AutoLink.exe-1.%-INC")).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false for invalid filenames", function () {
|
||||
expect(isValidFilename("foo")).toEqual(false);
|
||||
expect(isValidFilename("my script.script")).toEqual(false);
|
||||
expect(isValidFilename("a^.txt")).toEqual(false);
|
||||
expect(isValidFilename("b#.lit")).toEqual(false);
|
||||
expect(isValidFilename("lib().js")).toEqual(false);
|
||||
expect(isValidFilename("foo.script_")).toEqual(false);
|
||||
expect(isValidFilename("foo._script")).toEqual(false);
|
||||
expect(isValidFilename("foo.hyphened-ext")).toEqual(false);
|
||||
expect(isValidFilename("")).toEqual(false);
|
||||
expect(isValidFilename("AutoLink-1.%-INC.exe")).toEqual(false);
|
||||
expect(isValidFilename("AutoLink.exe-1.%-INC.exe")).toEqual(false);
|
||||
expect(isValidFilename("foo%.exe")).toEqual(false);
|
||||
expect(isValidFilename("-1.00%-INC")).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidDirectoryName()", function () {
|
||||
const isValidDirectoryName = dirHelpers.isValidDirectoryName;
|
||||
|
||||
it("should return true for valid directory names", function () {
|
||||
expect(isValidDirectoryName("a")).toEqual(true);
|
||||
expect(isValidDirectoryName("foo")).toEqual(true);
|
||||
expect(isValidDirectoryName("foo-dir")).toEqual(true);
|
||||
expect(isValidDirectoryName("foo_dir")).toEqual(true);
|
||||
expect(isValidDirectoryName(".a")).toEqual(true);
|
||||
expect(isValidDirectoryName("1")).toEqual(true);
|
||||
expect(isValidDirectoryName("a1")).toEqual(true);
|
||||
expect(isValidDirectoryName(".a1")).toEqual(true);
|
||||
expect(isValidDirectoryName("._foo")).toEqual(true);
|
||||
expect(isValidDirectoryName("_foo")).toEqual(true);
|
||||
// the changes made to support this broke mv
|
||||
// see https://github.com/danielyxie/bitburner/pull/3653 if you try to re support
|
||||
// expect(isValidDirectoryName("foo.dir")).toEqual(true);
|
||||
// expect(isValidDirectoryName("foov1.0.0.1")).toEqual(true);
|
||||
// expect(isValidDirectoryName("foov1..0..0..1")).toEqual(true);
|
||||
expect(isValidDirectoryName("foov1-0-0-1")).toEqual(true);
|
||||
expect(isValidDirectoryName("foov1-0-0-1-")).toEqual(true);
|
||||
expect(isValidDirectoryName("foov1--0--0--1--")).toEqual(true);
|
||||
expect(isValidDirectoryName("foov1_0_0_1")).toEqual(true);
|
||||
expect(isValidDirectoryName("foov1_0_0_1_")).toEqual(true);
|
||||
expect(isValidDirectoryName("foov1__0__0__1")).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false for invalid directory names", function () {
|
||||
expect(isValidDirectoryName("")).toEqual(false);
|
||||
expect(isValidDirectoryName("👨💻")).toEqual(false);
|
||||
expect(isValidDirectoryName("dir#")).toEqual(false);
|
||||
expect(isValidDirectoryName("dir!")).toEqual(false);
|
||||
expect(isValidDirectoryName("dir*")).toEqual(false);
|
||||
expect(isValidDirectoryName(".")).toEqual(false);
|
||||
expect(isValidDirectoryName("..")).toEqual(false);
|
||||
expect(isValidDirectoryName("1.")).toEqual(false);
|
||||
expect(isValidDirectoryName("foo.")).toEqual(false);
|
||||
expect(isValidDirectoryName("foov1.0.0.1.")).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidDirectoryPath()", function () {
|
||||
const isValidDirectoryPath = dirHelpers.isValidDirectoryPath;
|
||||
|
||||
it("should return false for empty strings", function () {
|
||||
expect(isValidDirectoryPath("")).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true only for the forward slash if the string has length 1", function () {
|
||||
expect(isValidDirectoryPath("/")).toEqual(true);
|
||||
expect(isValidDirectoryPath(" ")).toEqual(false);
|
||||
expect(isValidDirectoryPath(".")).toEqual(false);
|
||||
expect(isValidDirectoryPath("a")).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true for valid directory paths", function () {
|
||||
expect(isValidDirectoryPath("/a")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/dir/a")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/dir/foo")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/.dir/foo-dir")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/.dir/foo_dir")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/.dir/.a")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/dir1/1")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/dir1/a1")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/dir1/.a1")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/dir_/._foo")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/dir-/_foo")).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false if the path does not have a leading slash", function () {
|
||||
expect(isValidDirectoryPath("a")).toEqual(false);
|
||||
expect(isValidDirectoryPath("dir/a")).toEqual(false);
|
||||
expect(isValidDirectoryPath("dir/foo")).toEqual(false);
|
||||
expect(isValidDirectoryPath(".dir/foo-dir")).toEqual(false);
|
||||
expect(isValidDirectoryPath(".dir/foo_dir")).toEqual(false);
|
||||
expect(isValidDirectoryPath(".dir/.a")).toEqual(false);
|
||||
expect(isValidDirectoryPath("dir1/1")).toEqual(false);
|
||||
expect(isValidDirectoryPath("dir1/a1")).toEqual(false);
|
||||
expect(isValidDirectoryPath("dir1/.a1")).toEqual(false);
|
||||
expect(isValidDirectoryPath("dir_/._foo")).toEqual(false);
|
||||
expect(isValidDirectoryPath("dir-/_foo")).toEqual(false);
|
||||
});
|
||||
|
||||
it("should accept dot notation", function () {
|
||||
expect(isValidDirectoryPath("/dir/./a")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/dir/../foo")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/.dir/./foo-dir")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/.dir/../foo_dir")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/.dir/./.a")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/dir1/1/.")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/dir1/a1/..")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/dir1/.a1/..")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/dir_/._foo/.")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/./dir-/_foo")).toEqual(true);
|
||||
expect(isValidDirectoryPath("/../dir-/_foo")).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidFilePath()", function () {
|
||||
const isValidFilePath = dirHelpers.isValidFilePath;
|
||||
|
||||
it("should return false for strings that are too short", function () {
|
||||
expect(isValidFilePath("/a")).toEqual(false);
|
||||
expect(isValidFilePath("a.")).toEqual(false);
|
||||
expect(isValidFilePath(".a")).toEqual(false);
|
||||
expect(isValidFilePath("/.")).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true for arguments that are just filenames", function () {
|
||||
expect(isValidFilePath("test.txt")).toEqual(true);
|
||||
expect(isValidFilePath("123.script")).toEqual(true);
|
||||
expect(isValidFilePath("foo123.b")).toEqual(true);
|
||||
expect(isValidFilePath("my_script.script")).toEqual(true);
|
||||
expect(isValidFilePath("my-script.script")).toEqual(true);
|
||||
expect(isValidFilePath("_foo.lit")).toEqual(true);
|
||||
expect(isValidFilePath("mult.periods.script")).toEqual(true);
|
||||
expect(isValidFilePath("mult.per-iods.again.script")).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return true for valid filepaths", function () {
|
||||
expect(isValidFilePath("/foo/test.txt")).toEqual(true);
|
||||
expect(isValidFilePath("/../123.script")).toEqual(true);
|
||||
expect(isValidFilePath("/./foo123.b")).toEqual(true);
|
||||
expect(isValidFilePath("/dir/my_script.script")).toEqual(true);
|
||||
expect(isValidFilePath("/dir1/dir2/dir3/my-script.script")).toEqual(true);
|
||||
expect(isValidFilePath("/dir1/dir2/././../_foo.lit")).toEqual(true);
|
||||
expect(isValidFilePath("/.dir1/./../.dir2/mult.periods.script")).toEqual(true);
|
||||
expect(isValidFilePath("/_dir/../dir2/mult.per-iods.again.script")).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false for strings that end with a slash", function () {
|
||||
expect(isValidFilePath("/foo/")).toEqual(false);
|
||||
expect(isValidFilePath("foo.txt/")).toEqual(false);
|
||||
expect(isValidFilePath("/")).toEqual(false);
|
||||
expect(isValidFilePath("/_dir/")).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false for invalid arguments", function () {
|
||||
expect(isValidFilePath(null as unknown as string)).toEqual(false);
|
||||
expect(isValidFilePath(undefined as unknown as string)).toEqual(false);
|
||||
expect(isValidFilePath(5 as unknown as string)).toEqual(false);
|
||||
expect(isValidFilePath({} as unknown as string)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFirstParentDirectory()", function () {
|
||||
const getFirstParentDirectory = dirHelpers.getFirstParentDirectory;
|
||||
|
||||
it("should return the first parent directory in a filepath", function () {
|
||||
expect(getFirstParentDirectory("/dir1/foo.txt")).toEqual("dir1/");
|
||||
expect(getFirstParentDirectory("/dir1/dir2/dir3/dir4/foo.txt")).toEqual("dir1/");
|
||||
expect(getFirstParentDirectory("/_dir1/dir2/foo.js")).toEqual("_dir1/");
|
||||
});
|
||||
|
||||
it("should return '/' if there is no first parent directory", function () {
|
||||
expect(getFirstParentDirectory("")).toEqual("/");
|
||||
expect(getFirstParentDirectory(" ")).toEqual("/");
|
||||
expect(getFirstParentDirectory("/")).toEqual("/");
|
||||
expect(getFirstParentDirectory("//")).toEqual("/");
|
||||
expect(getFirstParentDirectory("foo.script")).toEqual("/");
|
||||
expect(getFirstParentDirectory("/foo.txt")).toEqual("/");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAllParentDirectories()", function () {
|
||||
const getAllParentDirectories = dirHelpers.getAllParentDirectories;
|
||||
|
||||
it("should return all parent directories in a filepath", function () {
|
||||
expect(getAllParentDirectories("/")).toEqual("/");
|
||||
expect(getAllParentDirectories("/home/var/foo.txt")).toEqual("/home/var/");
|
||||
expect(getAllParentDirectories("/home/var/")).toEqual("/home/var/");
|
||||
expect(getAllParentDirectories("/home/var/test/")).toEqual("/home/var/test/");
|
||||
});
|
||||
|
||||
it("should return an empty string if there are no parent directories", function () {
|
||||
expect(getAllParentDirectories("foo.txt")).toEqual("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isInRootDirectory()", function () {
|
||||
const isInRootDirectory = dirHelpers.isInRootDirectory;
|
||||
|
||||
it("should return true for filepaths that refer to a file in the root directory", function () {
|
||||
expect(isInRootDirectory("a.b")).toEqual(true);
|
||||
expect(isInRootDirectory("foo.txt")).toEqual(true);
|
||||
expect(isInRootDirectory("/foo.txt")).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false for filepaths that refer to a file that's NOT in the root directory", function () {
|
||||
expect(isInRootDirectory("/dir/foo.txt")).toEqual(false);
|
||||
expect(isInRootDirectory("dir/foo.txt")).toEqual(false);
|
||||
expect(isInRootDirectory("/./foo.js")).toEqual(false);
|
||||
expect(isInRootDirectory("../foo.js")).toEqual(false);
|
||||
expect(isInRootDirectory("/dir1/dir2/dir3/foo.txt")).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false for invalid inputs (inputs that aren't filepaths)", function () {
|
||||
expect(isInRootDirectory(null as unknown as string)).toEqual(false);
|
||||
expect(isInRootDirectory(undefined as unknown as string)).toEqual(false);
|
||||
expect(isInRootDirectory("")).toEqual(false);
|
||||
expect(isInRootDirectory(" ")).toEqual(false);
|
||||
expect(isInRootDirectory("a")).toEqual(false);
|
||||
expect(isInRootDirectory("/dir")).toEqual(false);
|
||||
expect(isInRootDirectory("/dir/")).toEqual(false);
|
||||
expect(isInRootDirectory("/dir/foo")).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("evaluateDirectoryPath()", function () {
|
||||
//const evaluateDirectoryPath = dirHelpers.evaluateDirectoryPath;
|
||||
// TODO
|
||||
});
|
||||
|
||||
describe("evaluateFilePath()", function () {
|
||||
//const evaluateFilePath = dirHelpers.evaluateFilePath;
|
||||
// TODO
|
||||
});
|
||||
});
|
283
test/jest/Terminal/Path.test.ts
Normal file
283
test/jest/Terminal/Path.test.ts
Normal file
@ -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/"]);
|
||||
});
|
||||
});
|
222
test/jest/Terminal/tabCompletion.test.ts
Normal file
222
test/jest/Terminal/tabCompletion.test.ts
Normal file
@ -0,0 +1,222 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
|
||||
import { Player } from "../../../src/Player";
|
||||
import { getTabCompletionPossibilities } from "../../../src/Terminal/getTabCompletionPossibilities";
|
||||
import { Server } from "../../../src/Server/Server";
|
||||
import { AddToAllServers, prestigeAllServers } from "../../../src/Server/AllServers";
|
||||
import { LocationName } from "../../../src/Enums";
|
||||
import { CodingContract } from "../../../src/CodingContracts";
|
||||
import { asFilePath } from "../../../src/Paths/FilePath";
|
||||
import { Directory, isAbsolutePath, isDirectoryPath, root } from "../../../src/Paths/Directory";
|
||||
import { hasTextExtension } from "../../../src/Paths/TextFilePath";
|
||||
import { hasScriptExtension } from "../../../src/Paths/ScriptFilePath";
|
||||
import { LiteratureName } from "../../../src/Literature/data/LiteratureNames";
|
||||
import { MessageFilename } from "../../../src/Message/MessageHelpers";
|
||||
import { Terminal } from "../../../src/Terminal";
|
||||
|
||||
describe("getTabCompletionPossibilities", function () {
|
||||
let closeServer: Server;
|
||||
let farServer: Server;
|
||||
|
||||
beforeEach(() => {
|
||||
prestigeAllServers();
|
||||
Player.init();
|
||||
|
||||
closeServer = new Server({
|
||||
ip: "8.8.8.8",
|
||||
hostname: "near",
|
||||
hackDifficulty: 1,
|
||||
moneyAvailable: 70000,
|
||||
numOpenPortsRequired: 0,
|
||||
organizationName: LocationName.NewTokyoNoodleBar,
|
||||
requiredHackingSkill: 1,
|
||||
serverGrowth: 3000,
|
||||
});
|
||||
farServer = new Server({
|
||||
ip: "4.4.4.4",
|
||||
hostname: "far",
|
||||
hackDifficulty: 1,
|
||||
moneyAvailable: 70000,
|
||||
numOpenPortsRequired: 0,
|
||||
organizationName: LocationName.Sector12JoesGuns,
|
||||
requiredHackingSkill: 1,
|
||||
serverGrowth: 3000,
|
||||
});
|
||||
Player.getHomeComputer().serversOnNetwork.push(closeServer.hostname);
|
||||
closeServer.serversOnNetwork.push(Player.getHomeComputer().hostname);
|
||||
closeServer.serversOnNetwork.push(farServer.hostname);
|
||||
farServer.serversOnNetwork.push(closeServer.hostname);
|
||||
AddToAllServers(closeServer);
|
||||
AddToAllServers(farServer);
|
||||
});
|
||||
|
||||
it("completes the connect command, regardless of folder", async () => {
|
||||
let options = await getTabCompletionPossibilities("connect ", root);
|
||||
expect(options).toEqual(["near"]);
|
||||
options = await getTabCompletionPossibilities("connect ", asDirectory("folder1/"));
|
||||
expect(options).toEqual(["near"]);
|
||||
Terminal.connectToServer("near");
|
||||
options = await getTabCompletionPossibilities("connect ", root);
|
||||
expect(options).toEqual(["home", "far"]);
|
||||
options = await getTabCompletionPossibilities("connect h", asDirectory("folder1/"));
|
||||
// Also test completion of a partially completed text
|
||||
expect(options).toEqual(["home"]);
|
||||
});
|
||||
|
||||
it("completes the buy command", async () => {
|
||||
let options = await getTabCompletionPossibilities("buy ", root);
|
||||
expect(options.sort()).toEqual(
|
||||
[
|
||||
"BruteSSH.exe",
|
||||
"FTPCrack.exe",
|
||||
"relaySMTP.exe",
|
||||
"HTTPWorm.exe",
|
||||
"SQLInject.exe",
|
||||
"DeepscanV1.exe",
|
||||
"DeepscanV2.exe",
|
||||
"AutoLink.exe",
|
||||
"ServerProfiler.exe",
|
||||
"Formulas.exe",
|
||||
].sort(),
|
||||
);
|
||||
// Also test that darkweb items will be completed if they have incorrect capitalization in progress
|
||||
options = await getTabCompletionPossibilities("buy de", root);
|
||||
expect(options.sort()).toEqual(["DeepscanV1.exe", "DeepscanV2.exe"].sort());
|
||||
});
|
||||
|
||||
it("completes the scp command", async () => {
|
||||
writeFiles();
|
||||
let options = await getTabCompletionPossibilities("scp ", root);
|
||||
expect(options.sort()).toEqual(
|
||||
[
|
||||
"note.txt",
|
||||
"folder1/text.txt",
|
||||
"folder1/text2.txt",
|
||||
"hack.js",
|
||||
"weaken.js",
|
||||
"grow.js",
|
||||
"old.script",
|
||||
"folder1/test.js",
|
||||
"anotherFolder/win.js",
|
||||
LiteratureName.AGreenTomorrow,
|
||||
].sort(),
|
||||
);
|
||||
// Test the second command argument (server name)
|
||||
options = await getTabCompletionPossibilities("scp note.txt ", root);
|
||||
expect(options).toEqual(["home", "near", "far"]);
|
||||
});
|
||||
|
||||
it("completes the kill, tail, mem, and check commands", async () => {
|
||||
writeFiles();
|
||||
for (const command of ["kill", "tail", "mem", "check"]) {
|
||||
let options = await getTabCompletionPossibilities(`${command} `, root);
|
||||
expect(options.sort()).toEqual(scriptFilePaths);
|
||||
// From a directory, show only the options in that directory
|
||||
options = await getTabCompletionPossibilities(`${command} `, asDirectory("folder1/"));
|
||||
expect(options.sort()).toEqual(["test.js"]);
|
||||
// From a directory but with relative path .., show stuff in the resolved directory with the relative pathing included
|
||||
options = await getTabCompletionPossibilities(`${command} ../`, asDirectory("folder1/"));
|
||||
expect(options.sort()).toEqual(
|
||||
[...scriptFilePaths.map((path) => "../" + path), "../folder1/", "../anotherFolder/"].sort(),
|
||||
);
|
||||
options = await getTabCompletionPossibilities(`${command} ../folder1/../anotherFolder/`, asDirectory("folder1/"));
|
||||
expect(options.sort()).toEqual(["../folder1/../anotherFolder/win.js"]);
|
||||
}
|
||||
});
|
||||
|
||||
it("completes the nano commands", async () => {
|
||||
writeFiles();
|
||||
const contentFilePaths = [...scriptFilePaths, ...textFilePaths].sort();
|
||||
const options = await getTabCompletionPossibilities("nano ", root);
|
||||
expect(options.sort()).toEqual(contentFilePaths);
|
||||
});
|
||||
|
||||
it("completes the rm command", async () => {
|
||||
writeFiles();
|
||||
const removableFilePaths = [
|
||||
...scriptFilePaths,
|
||||
...textFilePaths,
|
||||
...contractFilePaths,
|
||||
LiteratureName.AGreenTomorrow,
|
||||
"NUKE.exe",
|
||||
].sort();
|
||||
const options = await getTabCompletionPossibilities("rm ", root);
|
||||
expect(options.sort()).toEqual(removableFilePaths);
|
||||
});
|
||||
|
||||
it("completes the run command", async () => {
|
||||
writeFiles();
|
||||
const runnableFilePaths = [...scriptFilePaths, ...contractFilePaths, "NUKE.exe"].sort();
|
||||
let options = await getTabCompletionPossibilities("run ", root);
|
||||
expect(options.sort()).toEqual(runnableFilePaths);
|
||||
// Also check the same files
|
||||
options = await getTabCompletionPossibilities("./", root);
|
||||
expect(options.sort()).toEqual(
|
||||
[...runnableFilePaths.map((path) => "./" + path), "./folder1/", "./anotherFolder/"].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it("completes the cat command", async () => {
|
||||
writeFiles();
|
||||
const cattableFilePaths = [
|
||||
...scriptFilePaths,
|
||||
...textFilePaths,
|
||||
MessageFilename.TruthGazer,
|
||||
LiteratureName.AGreenTomorrow,
|
||||
].sort();
|
||||
const options = await getTabCompletionPossibilities("cat ", root);
|
||||
expect(options.sort()).toEqual(cattableFilePaths);
|
||||
});
|
||||
|
||||
it("completes the download and mv commands", async () => {
|
||||
writeFiles();
|
||||
writeFiles();
|
||||
const contentFilePaths = [...scriptFilePaths, ...textFilePaths].sort();
|
||||
for (const command of ["download", "mv"]) {
|
||||
const options = await getTabCompletionPossibilities(`${command} `, root);
|
||||
expect(options.sort()).toEqual(contentFilePaths);
|
||||
}
|
||||
});
|
||||
|
||||
it("completes the ls and cd commands", async () => {
|
||||
writeFiles();
|
||||
for (const command of ["ls", "cd"]) {
|
||||
const options = await getTabCompletionPossibilities(`${command} `, root);
|
||||
expect(options.sort()).toEqual(["folder1/", "anotherFolder/"].sort());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function asDirectory(dir: string): Directory {
|
||||
if (!isAbsolutePath(dir) || !isDirectoryPath(dir)) throw new Error(`Directory ${dir} failed typechecking`);
|
||||
return dir;
|
||||
}
|
||||
const textFilePaths = ["note.txt", "folder1/text.txt", "folder1/text2.txt"];
|
||||
const scriptFilePaths = [
|
||||
"hack.js",
|
||||
"weaken.js",
|
||||
"grow.js",
|
||||
"old.script",
|
||||
"folder1/test.js",
|
||||
"anotherFolder/win.js",
|
||||
].sort();
|
||||
const contractFilePaths = ["testContract.cct", "anothercontract.cct"];
|
||||
function writeFiles() {
|
||||
const home = Player.getHomeComputer();
|
||||
for (const filename of textFilePaths) {
|
||||
if (!hasTextExtension(filename)) {
|
||||
throw new Error(`Text file ${filename} had the wrong extension.`);
|
||||
}
|
||||
home.writeToTextFile(asFilePath(filename), `File content for ${filename}`);
|
||||
}
|
||||
for (const filename of scriptFilePaths) {
|
||||
if (!hasScriptExtension(filename)) {
|
||||
throw new Error(`Script file ${filename} had the wrong extension.`);
|
||||
}
|
||||
home.writeToScriptFile(asFilePath(filename), `File content for ${filename}`);
|
||||
}
|
||||
for (const filename of contractFilePaths) {
|
||||
home.contracts.push(new CodingContract(filename));
|
||||
}
|
||||
home.messages.push(LiteratureName.AGreenTomorrow, MessageFilename.TruthGazer);
|
||||
}
|
@ -58,7 +58,10 @@ exports[`load/saveAllServers 1`] = `
|
||||
"smtpPortOpen": false,
|
||||
"sqlPortOpen": false,
|
||||
"sshPortOpen": false,
|
||||
"textFiles": [],
|
||||
"textFiles": {
|
||||
"ctor": "JSONMap",
|
||||
"data": []
|
||||
},
|
||||
"purchasedByPlayer": true,
|
||||
"backdoorInstalled": false,
|
||||
"baseDifficulty": 1,
|
||||
@ -99,7 +102,10 @@ exports[`load/saveAllServers 1`] = `
|
||||
"smtpPortOpen": false,
|
||||
"sqlPortOpen": false,
|
||||
"sshPortOpen": false,
|
||||
"textFiles": [],
|
||||
"textFiles": {
|
||||
"ctor": "JSONMap",
|
||||
"data": []
|
||||
},
|
||||
"purchasedByPlayer": false,
|
||||
"backdoorInstalled": false,
|
||||
"baseDifficulty": 1,
|
||||
@ -155,7 +161,10 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = `
|
||||
"smtpPortOpen": false,
|
||||
"sqlPortOpen": false,
|
||||
"sshPortOpen": false,
|
||||
"textFiles": [],
|
||||
"textFiles": {
|
||||
"ctor": "JSONMap",
|
||||
"data": []
|
||||
},
|
||||
"purchasedByPlayer": true,
|
||||
"backdoorInstalled": false,
|
||||
"baseDifficulty": 1,
|
||||
@ -196,7 +205,10 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = `
|
||||
"smtpPortOpen": false,
|
||||
"sqlPortOpen": false,
|
||||
"sshPortOpen": false,
|
||||
"textFiles": [],
|
||||
"textFiles": {
|
||||
"ctor": "JSONMap",
|
||||
"data": []
|
||||
},
|
||||
"purchasedByPlayer": false,
|
||||
"backdoorInstalled": false,
|
||||
"baseDifficulty": 1,
|
||||
|
Loading…
Reference in New Issue
Block a user