Merge branch 'dev' into feature-catch-errors-and-softreset-recovery

This commit is contained in:
hydroflame 2022-01-15 18:00:33 -05:00 committed by GitHub
commit a454e21977
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 1473 additions and 975 deletions

8
.github/ISSUE_TEMPLATE vendored Normal file

@ -0,0 +1,8 @@
# DELETE THIS AFTER READING
# include (where relevant)
- [ ] Save file
- [ ] Minimal scripts to reproduce the issue
- [ ] Steps to reproduce
- [ ] Version of the game, e.g. Bitburner v1.3.0 (216bf616)

11
.github/PULL_REQUEST_TEMPLATE vendored Normal file

@ -0,0 +1,11 @@
# DELETE THIS AFTER READING
# Documentation
- DO NOT CHANGE any markdown/\*.md, these files are autogenerated from NetscriptDefinitions.d.ts and will be overwritten
- DO NOT re-generate the documentation, makes it harder to review.
# Bug fix
- Include how it was tested
- Include screenshot / gif (if possible)

6
dist/bitburner.d.ts vendored

@ -3848,7 +3848,7 @@ export declare interface NS extends Singularity {
* @param host - Host of target server. * @param host - Host of target server.
* @returns Returns the amount of time in milliseconds it takes to execute the weaken Netscript function. Returns Infinity if called on a Hacknet Server. * @returns Returns the amount of time in milliseconds it takes to execute the weaken Netscript function. Returns Infinity if called on a Hacknet Server.
*/ */
getWeakenTime(host: string): number; getWeakenTime(host?: string): number;
/** /**
* Get the income of a script. * Get the income of a script.
@ -3916,7 +3916,7 @@ export declare interface NS extends Singularity {
* @param args - Formating arguments. * @param args - Formating arguments.
* @returns Formated text. * @returns Formated text.
*/ */
sprintf(format: string, ...args: string[]): string; sprintf(format: string, ...args: any[]): string;
/** /**
* Format a string with an array of arguments. * Format a string with an array of arguments.
@ -3928,7 +3928,7 @@ export declare interface NS extends Singularity {
* @param args - Formating arguments. * @param args - Formating arguments.
* @returns Formated text. * @returns Formated text.
*/ */
vsprintf(format: string, args: string[]): string; vsprintf(format: string, args: any[]): string;
/** /**
* Format a number * Format a number

36
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

@ -39,8 +39,7 @@ There are two methods of obtaining Duplicate Sleeves:
1. Destroy BitNode-10. Each completion give you one additional Duplicate Sleeve 1. Destroy BitNode-10. Each completion give you one additional Duplicate Sleeve
2. Purchase Duplicate Sleeves from :ref:`the faction The Covenant <gameplay_factions>`. 2. Purchase Duplicate Sleeves from :ref:`the faction The Covenant <gameplay_factions>`.
This is only available in BitNodes-10 and above, and is only available after defeating This is only available in BitNodes-10. Sleeves purchased this way are **permanent** (they persist
BitNode-10 at least once. Sleeves purchased this way are **permanent** (they persist
through BitNodes). You can purchase up to 5 Duplicate Sleeves from The Covenant. through BitNodes). You can purchase up to 5 Duplicate Sleeves from The Covenant.
Synchronization Synchronization

@ -113,7 +113,7 @@ The list contains the name of (i.e. the value returned by
| | | to any position from i to i+n. | | | | to any position from i to i+n. |
| | | | | | | |
| | | Assuming you are initially positioned at the start of the array, determine | | | | Assuming you are initially positioned at the start of the array, determine |
| | | whether you are able to reach the last index of the array EXACTLY. | | | | whether you are able to reach the last index of the array. |
+------------------------------------+------------------------------------------------------------------------------------------+ +------------------------------------+------------------------------------------------------------------------------------------+
| Merge Overlapping Intervals | | Given an array of intervals, merge all overlapping intervals. An interval | | Merge Overlapping Intervals | | Given an array of intervals, merge all overlapping intervals. An interval |
| | | is an array with two numbers, where the first number is always less than | | | | is an array with two numbers, where the first number is always less than |

@ -17,9 +17,16 @@ says 'Infiltrate Company'.
When infiltrating a company you will be presented with short active challenges. When infiltrating a company you will be presented with short active challenges.
None of the challenges use the mouse. None of the challenges use the mouse.
The difficulty at the top lowers with better combat stats. It is not recommended The difficulty at the top lowers with better combat stats and charisma. It is not recommended
to attempt infiltrations above mid-normal. to attempt infiltrations above mid-normal.
The "maximum level" is the number of challenges you will need to pass to receive
the infiltration reward.
Every time you fail an infiltration challenge, you will take damage based on the
difficulty of the infiltration. If you are reduced to 0 hp or below, the
infiltration will immediately end.
* Most use spacebar as "action" * Most use spacebar as "action"
* Some use WASD or arrows interchangeably. * Some use WASD or arrows interchangeably.
* A few others use the rest of the keyboard. * A few others use the rest of the keyboard.

@ -16,7 +16,6 @@ Affects:
* Chance to successfully hack a server * Chance to successfully hack a server
* Percent money stolen when hacking a server * Percent money stolen when hacking a server
* Success rate of certain crimes * Success rate of certain crimes
* Success rate of Hacking option during Infiltration
* Time it takes to create a program * Time it takes to create a program
* Faction reputation gain when carrying out Hacking Contracts or Field Work * Faction reputation gain when carrying out Hacking Contracts or Field Work
* Company reputation gain for certain jobs * Company reputation gain for certain jobs
@ -26,7 +25,6 @@ Gain experience by:
* Manually hacking servers through Terminal * Manually hacking servers through Terminal
* Executing hack(), grow(), or weaken() through a script * Executing hack(), grow(), or weaken() through a script
* Committing certain crimes * Committing certain crimes
* Infiltration
* Carrying out Hacking Contracts or doing Field work for Factions * Carrying out Hacking Contracts or doing Field work for Factions
* Working certain jobs at a company * Working certain jobs at a company
* Studying at a university * Studying at a university
@ -38,14 +36,12 @@ Represents the player's physical offensive power
Affects: Affects:
* Success rate of certain crimes * Success rate of certain crimes
* Success rate of Combat options during Infiltration
* Faction reputation gain for Security and Field Work * Faction reputation gain for Security and Field Work
* Company reputation gain for certain jobs * Company reputation gain for certain jobs
Gain experience by: Gain experience by:
* Committing certain crimes * Committing certain crimes
* Infiltration
* Working out at a gym * Working out at a gym
* Doing Security/Field Work for a faction * Doing Security/Field Work for a faction
* Working certain jobs at a company * Working certain jobs at a company
@ -58,15 +54,12 @@ Affects:
* Success rate of certain crimes * Success rate of certain crimes
* The player's HP * The player's HP
* Success rate of Combat options during Infiltration
* How much damage the player takes during Infiltration
* Faction reputation gain for Security and Field Work * Faction reputation gain for Security and Field Work
* Company reputation gain for certain jobs * Company reputation gain for certain jobs
Gain experience by: Gain experience by:
* Committing certain crimes * Committing certain crimes
* Infiltration
* Working out at a gym * Working out at a gym
* Doing Security/Field Work for a faction * Doing Security/Field Work for a faction
* Working certain jobs at a company * Working certain jobs at a company
@ -78,14 +71,12 @@ Represents the player's skill and adeptness in performing certain tasks
Affects: Affects:
* Success rate of certain crimes * Success rate of certain crimes
* Success rate of Combat, Lockpick, and Escape options during Infiltration
* Faction reputation gain for Security and Field Work * Faction reputation gain for Security and Field Work
* Company reputation gain for certain jobs * Company reputation gain for certain jobs
Gain experience by: Gain experience by:
* Committing certain crimes * Committing certain crimes
* Infiltration
* Working out at a gym * Working out at a gym
* Doing Security/Field Work for a faction * Doing Security/Field Work for a faction
* Working certain jobs at a company * Working certain jobs at a company
@ -97,14 +88,12 @@ Represents the player's speed and ability to move
Affects: Affects:
* Success rate of certain crimes * Success rate of certain crimes
* Success rate of Combat, Sneak, and Escape options during Infiltration
* Faction reputation gain for Security and Field Work * Faction reputation gain for Security and Field Work
* Company reputation gain for certain jobs * Company reputation gain for certain jobs
Gain experience by: Gain experience by:
* Committing certain crimes * Committing certain crimes
* Infiltration
* Working out at a gym * Working out at a gym
* Doing Security/Field Work for a faction * Doing Security/Field Work for a faction
* Working certain jobs at a company * Working certain jobs at a company
@ -116,14 +105,12 @@ Represents the player's social abilities
Affects: Affects:
* Success rate of certain crimes * Success rate of certain crimes
* Success rate of Bribe option during Infiltration
* Faction reputation gain for Field Work * Faction reputation gain for Field Work
* Company reputation gain for most jobs * Company reputation gain for most jobs
Gain experience by: Gain experience by:
* Committing certain crimes * Committing certain crimes
* Infiltration
* Studying at a university * Studying at a university
* Working a relevant job at a company * Working a relevant job at a company
* Doing Field work for a Faction * Doing Field work for a Faction

@ -253,7 +253,6 @@ Here's what mine showed at the time I made this::
Take note of the following servers: Take note of the following servers:
* |n00dles|
* |sigma-cosmetics| * |sigma-cosmetics|
* |joesguns| * |joesguns|
* |nectar-net| * |nectar-net|
@ -807,8 +806,7 @@ startup script. Feel free to adjust it to your liking.
// Array of all servers that don't need any ports opened // Array of all servers that don't need any ports opened
// to gain root access. These have 16 GB of RAM // to gain root access. These have 16 GB of RAM
var servers0Port = ["n00dles", var servers0Port = ["sigma-cosmetics",
"sigma-cosmetics",
"joesguns", "joesguns",
"nectar-net", "nectar-net",
"hong-fang-tea", "hong-fang-tea",

@ -66,8 +66,7 @@ And the data in port 1 will look like::
WARNING: Port Handles only work in :ref:`netscriptjs`. They do not work in :ref:`netscript1` WARNING: Port Handles only work in :ref:`netscriptjs`. They do not work in :ref:`netscript1`
The :js:func:`getPortHandle` Netscript function can be used to get a handle to a Netscript Port. The :js:func:`getPortHandle` Netscript function can be used to get a handle to a Netscript Port.
This handle allows you to access several new port-related functions and the This handle allows you to access several new port-related functions. The functions are:
port's underlying data structure, which is just a JavaScript array. The functions are:
.. js:method:: NetscriptPort.writePort(data) .. js:method:: NetscriptPort.writePort(data)
@ -111,22 +110,11 @@ port's underlying data structure, which is just a JavaScript array. The function
Clears all data from the port. Works the same as the Netscript function `clear` Clears all data from the port. Works the same as the Netscript function `clear`
.. js:attribute:: NetscriptPort.data
The Netscript port underlying data structure, which is just a Javascript array. All
valid Javascript Array methods can be called on this.
Port Handle Example:: Port Handle Example::
port = getPortHandle(5); port = getPortHandle(5);
back = port.data.pop(); //Get and remove last element in port back = port.data.pop(); //Get and remove last element in port
//Remove an element from the port
i = port.data.findIndex("foo");
if (i != -1) {
port.data.slice(i, 1);
}
//Wait for port data before reading //Wait for port data before reading
while(port.empty()) { while(port.empty()) {
sleep(10000); sleep(10000);

@ -3,6 +3,9 @@ const greenworks = require("./greenworks");
const log = require("electron-log"); const log = require("electron-log");
function enableAchievementsInterval(window) { function enableAchievementsInterval(window) {
// If the Steam API could not be initialized on game start, we'll abort this.
if (global.greenworksError) return;
// This is backward but the game fills in an array called `document.achievements` and we retrieve it from // This is backward but the game fills in an array called `document.achievements` and we retrieve it from
// here. Hey if it works it works. // here. Hey if it works it works.
const steamAchievements = greenworks.getAchievementNames(); const steamAchievements = greenworks.getAchievementNames();

@ -16,10 +16,18 @@ process.on('uncaughtException', function () {
process.exit(1); process.exit(1);
}); });
// We want to fail gracefully if we cannot connect to Steam
try {
if (greenworks.init()) { if (greenworks.init()) {
log.info("Steam API has been initialized."); log.info("Steam API has been initialized.");
} else { } else {
log.warn("Steam API has failed to initialize."); const error = "Steam API has failed to initialize.";
log.warn(error);
global.greenworksError = error;
}
} catch (ex) {
log.warn(ex.message);
global.greenworksError = ex.message;
} }
function setStopProcessHandler(app, window, enabled) { function setStopProcessHandler(app, window, enabled) {
@ -113,4 +121,13 @@ app.whenReady().then(async () => {
} else { } else {
startWindow(process.argv.includes("--no-scripts")); startWindow(process.argv.includes("--no-scripts"));
} }
if (global.greenworksError) {
dialog.showMessageBox({
title: 'Bitburner',
message: 'Could not connect to Steam',
detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`,
type: 'warning', buttons: ['OK']
});
}
}); });

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -9,15 +9,15 @@ Format a string.
<b>Signature:</b> <b>Signature:</b>
```typescript ```typescript
sprintf(format: string, ...args: string[]): string; sprintf(format: string, ...args: any[]): string;
``` ```
## Parameters ## Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --------- | ------- | -------------------- |
| format | string | String to format. | | format | string | String to format. |
| args | string\[\] | Formating arguments. | | args | any\[\] | Formating arguments. |
<b>Returns:</b> <b>Returns:</b>
@ -30,4 +30,3 @@ Formated text.
RAM cost: 0 GB RAM cost: 0 GB
see: https://github.com/alexei/sprintf.js see: https://github.com/alexei/sprintf.js

@ -9,15 +9,15 @@ Format a string with an array of arguments.
<b>Signature:</b> <b>Signature:</b>
```typescript ```typescript
vsprintf(format: string, args: string[]): string; vsprintf(format: string, args: any[]): string;
``` ```
## Parameters ## Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --------- | ------- | -------------------- |
| format | string | String to format. | | format | string | String to format. |
| args | string\[\] | Formating arguments. | | args | any\[\] | Formating arguments. |
<b>Returns:</b> <b>Returns:</b>
@ -30,4 +30,3 @@ Formated text.
RAM cost: 0 GB RAM cost: 0 GB
see: https://github.com/alexei/sprintf.js see: https://github.com/alexei/sprintf.js

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [UserInterface](./bitburner.userinterface.md) &gt; [getStyles](./bitburner.userinterface.getstyles.md)
## UserInterface.getStyles() method
Get the current styles
<b>Signature:</b>
```typescript
getStyles(): IStyleSettings;
```
<b>Returns:</b>
IStyleSettings
An object containing the player's styles
## Remarks
RAM cost: cost: 0 GB

@ -16,7 +16,10 @@ interface UserInterface
| Method | Description | | Method | Description |
| --- | --- | | --- | --- |
| [getStyles()](./bitburner.userinterface.getstyles.md) | Get the current styles |
| [getTheme()](./bitburner.userinterface.gettheme.md) | Get the current theme | | [getTheme()](./bitburner.userinterface.gettheme.md) | Get the current theme |
| [resetStyles()](./bitburner.userinterface.resetstyles.md) | Resets the player's styles to the default values |
| [resetTheme()](./bitburner.userinterface.resettheme.md) | Resets the player's theme to the default values | | [resetTheme()](./bitburner.userinterface.resettheme.md) | Resets the player's theme to the default values |
| [setStyles(newStyles)](./bitburner.userinterface.setstyles.md) | Sets the current styles |
| [setTheme(newTheme)](./bitburner.userinterface.settheme.md) | Sets the current theme | | [setTheme(newTheme)](./bitburner.userinterface.settheme.md) | Sets the current theme |

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [UserInterface](./bitburner.userinterface.md) &gt; [resetStyles](./bitburner.userinterface.resetstyles.md)
## UserInterface.resetStyles() method
Resets the player's styles to the default values
<b>Signature:</b>
```typescript
resetStyles(): void;
```
<b>Returns:</b>
void
## Remarks
RAM cost: cost: 0 GB

@ -0,0 +1,38 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [UserInterface](./bitburner.userinterface.md) &gt; [setStyles](./bitburner.userinterface.setstyles.md)
## UserInterface.setStyles() method
Sets the current styles
<b>Signature:</b>
```typescript
setStyles(newStyles: IStyleSettings): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| newStyles | IStyleSettings | |
<b>Returns:</b>
void
## Remarks
RAM cost: cost: 0 GB
## Example
Usage example (NS2)
```ts
const styles = ns.ui.getStyles();
styles.fontFamily = 'Comic Sans Ms';
ns.ui.setStyles(styles);
```

@ -209,7 +209,7 @@
}, },
"STOCK_1q": { "STOCK_1q": {
"ID": "STOCK_1q", "ID": "STOCK_1q",
"Name": "Wolf of wall stree.", "Name": "Wolf of wall street.",
"Description": "Make 1q on the stock market." "Description": "Make 1q on the stock market."
}, },
"DISCOUNT": { "DISCOUNT": {
@ -484,3 +484,4 @@
} }
} }
} }

@ -13,7 +13,6 @@ import { SourceFileFlags } from "../SourceFile/SourceFileFlags";
import { dialogBoxCreate } from "../ui/React/DialogBox"; import { dialogBoxCreate } from "../ui/React/DialogBox";
import { clearObject } from "../utils/helpers/clearObject"; import { clearObject } from "../utils/helpers/clearObject";
import { Router } from "../ui/GameRoot";
import { WHRNG } from "../Casino/RNG"; import { WHRNG } from "../Casino/RNG";
@ -2583,7 +2582,6 @@ function installAugmentations(): boolean {
augmentationList += aug.name + level + "<br>"; augmentationList += aug.name + level + "<br>";
} }
Player.queuedAugmentations = []; Player.queuedAugmentations = [];
dialogBoxCreate( dialogBoxCreate(
"You slowly drift to sleep as scientists put you under in order " + "You slowly drift to sleep as scientists put you under in order " +
"to install the following Augmentations:<br>" + "to install the following Augmentations:<br>" +

@ -20,13 +20,15 @@ export const ConsoleHelpText: {
" skill [action] [name] Level or display info about your Bladeburner skills", " skill [action] [name] Level or display info about your Bladeburner skills",
" start [type] [name] Start a Bladeburner action/task", " start [type] [name] Start a Bladeburner action/task",
" stop Stops your current Bladeburner action/task", " stop Stops your current Bladeburner action/task",
" ",
], ],
automate: [ automate: [
"automate [var] [val] [hi/low]", "Usage: automate [var] [val] [hi/low]",
" ", " ",
"A simple way to automate your Bladeburner actions. This console command can be used " + "A simple way to automate your Bladeburner actions. This console command can be used " +
"to automatically start an action when your stamina rises above a certain threshold, and " + "to automatically start an action when your stamina rises above a certain threshold, and " +
"automatically switch to another action when your stamina drops below another threshold.", "automatically switch to another action when your stamina drops below another threshold.",
" ",
" automate status - Check the current status of your automation and get a brief description of what it'll do", " automate status - Check the current status of your automation and get a brief description of what it'll do",
" automate en - Enable the automation feature", " automate en - Enable the automation feature",
" automate dis - Disable the automation feature", " automate dis - Disable the automation feature",
@ -42,11 +44,12 @@ export const ConsoleHelpText: {
"if your stamina is 100 or higher, and then switch to Field Analysis if your stamina drops below " + "if your stamina is 100 or higher, and then switch to Field Analysis if your stamina drops below " +
"50. Note that when setting the action, the name of the action is CASE-SENSITIVE. It must " + "50. Note that when setting the action, the name of the action is CASE-SENSITIVE. It must " +
"exactly match whatever the name is in the UI.", "exactly match whatever the name is in the UI.",
" ",
], ],
clear: ["clear", "", "Clears the console"], clear: ["Usage: clear", " ", "Clears the console", " "],
cls: ["cls", "", "Clears the console"], cls: ["Usage: cls", " ", "Clears the console", " "],
help: [ help: [
"help [command]", "Usage: help [command]",
" ", " ",
"Running 'help' with no arguments displays the general help text, which lists all console commands " + "Running 'help' with no arguments displays the general help text, which lists all console commands " +
"and a brief description of what they do. A command can be specified to get more specific help text " + "and a brief description of what they do. A command can be specified to get more specific help text " +
@ -55,9 +58,10 @@ export const ConsoleHelpText: {
" help automate", " help automate",
" ", " ",
"will display specific information about using the automate console command", "will display specific information about using the automate console command",
" ",
], ],
log: [ log: [
"log [en/dis] [type]", "Usage: log [en/dis] [type]",
" ", " ",
"Enable or disable logging. By default, the results of completing actions such as contracts/operations are logged " + "Enable or disable logging. By default, the results of completing actions such as contracts/operations are logged " +
"in the console. There are also random events that are logged in the console as well. The five categories of " + "in the console. There are also random events that are logged in the console as well. The five categories of " +
@ -75,9 +79,10 @@ export const ConsoleHelpText: {
" ", " ",
" log dis all", " log dis all",
" log en all", " log en all",
" ",
], ],
skill: [ skill: [
"skill [action] [name]", "Usage: skill [action] [name]",
" ", " ",
"Level or display information about your skills.", "Level or display information about your skills.",
" ", " ",
@ -95,9 +100,10 @@ export const ConsoleHelpText: {
"This console command can also be used to level up skills:", "This console command can also be used to level up skills:",
" ", " ",
" skill level [skill name]", " skill level [skill name]",
" ",
], ],
start: [ start: [
"start [type] [name]", "Usage: start [type] [name]",
" ", " ",
"Start an action. An action is specified by its type and its name. The " + "Start an action. An action is specified by its type and its name. The " +
"name is case-sensitive. It must appear exactly as it does in the UI. If " + "name is case-sensitive. It must appear exactly as it does in the UI. If " +
@ -110,6 +116,7 @@ export const ConsoleHelpText: {
" ", " ",
" start contract Tracking", " start contract Tracking",
" start op 'Undercover Operation'", " start op 'Undercover Operation'",
" ",
], ],
stop: ["stop", "", "Stop your current action and go idle."], stop: ["Usage: stop", " ", "Stop your current action and go idle.", " "],
}; };

@ -4,7 +4,6 @@ import { Console } from "./Console";
import { AllPages } from "./AllPages"; import { AllPages } from "./AllPages";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
export function BladeburnerRoot(): React.ReactElement { export function BladeburnerRoot(): React.ReactElement {
@ -24,14 +23,10 @@ export function BladeburnerRoot(): React.ReactElement {
if (bladeburner === null) return <></>; if (bladeburner === null) return <></>;
return ( return (
<Box display="flex" flexDirection="column"> <Box display="flex" flexDirection="column">
<Grid container> <Box sx={{ display: "grid", gridTemplateColumns: "4fr 8fr", p: 1 }}>
<Grid item xs={6}>
<Stats bladeburner={bladeburner} player={player} router={router} /> <Stats bladeburner={bladeburner} player={player} router={router} />
</Grid>
<Grid item xs={6}>
<Console bladeburner={bladeburner} player={player} /> <Console bladeburner={bladeburner} player={player} />
</Grid> </Box>
</Grid>
<AllPages bladeburner={bladeburner} player={player} /> <AllPages bladeburner={bladeburner} player={player} />
</Box> </Box>

@ -23,7 +23,7 @@ const useStyles = makeStyles((theme: Theme) =>
width: "100%", width: "100%",
}, },
input: { input: {
backgroundColor: "#000", backgroundColor: theme.colors.backgroundsecondary,
}, },
nopadding: { nopadding: {
padding: theme.spacing(0), padding: theme.spacing(0),
@ -56,6 +56,7 @@ export function Console(props: IProps): React.ReactElement {
const classes = useStyles(); const classes = useStyles();
const [command, setCommand] = useState(""); const [command, setCommand] = useState("");
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
const consoleInput = useRef<HTMLInputElement>(null);
function handleCommandChange(event: React.ChangeEvent<HTMLInputElement>): void { function handleCommandChange(event: React.ChangeEvent<HTMLInputElement>): void {
setCommand(event.target.value); setCommand(event.target.value);
@ -131,15 +132,21 @@ export function Console(props: IProps): React.ReactElement {
} }
} }
function handleClick(): void {
if (!consoleInput.current) return;
consoleInput.current.focus();
}
return ( return (
<Paper> <Paper sx={{ p: 1 }}>
<Box sx={{ <Box sx={{
height: '60vh', height: '60vh',
paddingBottom: '8px', paddingBottom: '8px',
display: 'flex', display: 'flex',
alignItems: 'stretch', alignItems: 'stretch',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
}}> }}
onClick={handleClick}>
<Box> <Box>
<Logs entries={[...props.bladeburner.consoleLogs]} /> <Logs entries={[...props.bladeburner.consoleLogs]} />
</Box> </Box>
@ -149,6 +156,7 @@ export function Console(props: IProps): React.ReactElement {
autoFocus autoFocus
tabIndex={1} tabIndex={1}
type="text" type="text"
inputRef={consoleInput}
value={command} value={command}
onChange={handleCommandChange} onChange={handleCommandChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@ -182,7 +190,7 @@ function Logs({entries}: ILogProps): React.ReactElement {
useEffect(() => { useEffect(() => {
scrollToBottom(); scrollToBottom();
}, [entries]); }, [entries.length]);
return ( return (
<List sx={{ height: "100%", overflow: "auto", p: 1 }} ref={scrollHook}> <List sx={{ height: "100%", overflow: "auto", p: 1 }} ref={scrollHook}>

@ -3,7 +3,6 @@ import { formatNumber, convertTimeMsToTimeElapsedString } from "../../utils/Stri
import { BladeburnerConstants } from "../data/Constants"; import { BladeburnerConstants } from "../data/Constants";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import { Money } from "../../ui/React/Money"; import { Money } from "../../ui/React/Money";
import { StatsTable } from "../../ui/React/StatsTable";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { Factions } from "../../Faction/Factions"; import { Factions } from "../../Faction/Factions";
import { IRouter } from "../../ui/Router"; import { IRouter } from "../../ui/Router";
@ -44,7 +43,19 @@ export function Stats(props: IProps): React.ReactElement {
} }
return ( return (
<Paper sx={{ p: 1 }}> <Paper sx={{ p: 1, overflowY: 'auto', overflowX: 'hidden', wordBreak: 'break-all' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, maxHeight: '60vh' }}>
<Box sx={{ alignSelf: 'flex-start', width: '100%' }}>
<Button onClick={() => setTravelOpen(true)} sx={{ width: '50%' }}>Travel</Button>
<Tooltip title={!inFaction ? <Typography>Rank 25 required.</Typography> : ""}>
<span>
<Button disabled={!inFaction} onClick={openFaction} sx={{ width: '50%' }}>
Faction
</Button>
</span>
</Tooltip>
<TravelModal open={travelOpen} onClose={() => setTravelOpen(false)} bladeburner={props.bladeburner} />
</Box>
<Box display="flex"> <Box display="flex">
<Tooltip title={<Typography>Your rank within the Bladeburner division.</Typography>}> <Tooltip title={<Typography>Your rank within the Bladeburner division.</Typography>}>
<Typography>Rank: {formatNumber(props.bladeburner.rank, 2)}</Typography> <Typography>Rank: {formatNumber(props.bladeburner.rank, 2)}</Typography>
@ -158,24 +169,16 @@ export function Stats(props: IProps): React.ReactElement {
)} )}
<Typography>Skill Points: {formatNumber(props.bladeburner.skillPoints, 0)}</Typography> <Typography>Skill Points: {formatNumber(props.bladeburner.skillPoints, 0)}</Typography>
<br /> <br />
<StatsTable <Typography>
rows={[ Aug. Success Chance mult: {formatNumber(props.player.bladeburner_success_chance_mult * 100, 1)}%
["Aug. Success Chance mult: ", formatNumber(props.player.bladeburner_success_chance_mult * 100, 1) + "%"],
["Aug. Max Stamina mult: ", formatNumber(props.player.bladeburner_max_stamina_mult * 100, 1) + "%"],
["Aug. Stamina Gain mult: ", formatNumber(props.player.bladeburner_stamina_gain_mult * 100, 1) + "%"],
["Aug. Field Analysis mult: ", formatNumber(props.player.bladeburner_analysis_mult * 100, 1) + "%"],
]}
/>
<br /> <br />
<Button onClick={() => setTravelOpen(true)}>Travel</Button> Aug. Max Stamina mult: {formatNumber(props.player.bladeburner_max_stamina_mult * 100, 1)}%
<Tooltip title={!inFaction ? <Typography>Rank 25 required.</Typography> : ""}> <br />
<span> Aug. Stamina Gain mult: {formatNumber(props.player.bladeburner_stamina_gain_mult * 100, 1)}%
<Button disabled={!inFaction} onClick={openFaction}> <br />
Faction Aug. Field Analysis mult: {formatNumber(props.player.bladeburner_analysis_mult * 100, 1)}%
</Button> </Typography>
</span> </Box>
</Tooltip>
<TravelModal open={travelOpen} onClose={() => setTravelOpen(false)} bladeburner={props.bladeburner} />
</Paper> </Paper>
); );
} }

@ -1,7 +1,7 @@
import { CompanyPosition } from "./CompanyPosition"; import { CompanyPosition } from "./CompanyPosition";
import * as posNames from "./data/companypositionnames"; import * as posNames from "./data/companypositionnames";
import { favorToRep, repToFavor } from "../Faction/formulas/favor";
import { CONSTANTS } from "../Constants";
import { IMap } from "../types"; import { IMap } from "../types";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver"; import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver";
@ -71,7 +71,6 @@ export class Company {
isPlayerEmployed: boolean; isPlayerEmployed: boolean;
playerReputation: number; playerReputation: number;
favor: number; favor: number;
rolloverRep: number;
constructor(p: IConstructorParams = DefaultConstructorParams) { constructor(p: IConstructorParams = DefaultConstructorParams) {
this.name = p.name; this.name = p.name;
@ -84,7 +83,6 @@ export class Company {
this.isPlayerEmployed = false; this.isPlayerEmployed = false;
this.playerReputation = 1; this.playerReputation = 1;
this.favor = 0; this.favor = 0;
this.rolloverRep = 0;
this.isMegacorp = false; this.isMegacorp = false;
if (p.isMegacorp) this.isMegacorp = true; if (p.isMegacorp) this.isMegacorp = true;
} }
@ -137,39 +135,17 @@ export class Company {
if (this.favor == null) { if (this.favor == null) {
this.favor = 0; this.favor = 0;
} }
if (this.rolloverRep == null) { this.favor += this.getFavorGain();
this.rolloverRep = 0;
}
const res = this.getFavorGain();
if (res.length != 2) {
console.error("Invalid result from getFavorGain() function");
return;
} }
this.favor += res[0]; getFavorGain(): number {
this.rolloverRep = res[1];
}
getFavorGain(): number[] {
if (this.favor == null) { if (this.favor == null) {
this.favor = 0; this.favor = 0;
} }
if (this.rolloverRep == null) { const storedRep = Math.max(0, favorToRep(this.favor));
this.rolloverRep = 0; const totalRep = storedRep + this.playerReputation;
} const newFavor = repToFavor(totalRep);
let favorGain = 0, return newFavor - this.favor;
rep = this.playerReputation + this.rolloverRep;
let reqdRep = CONSTANTS.CompanyReputationToFavorBase * Math.pow(CONSTANTS.CompanyReputationToFavorMult, this.favor);
while (rep > 0) {
if (rep >= reqdRep) {
++favorGain;
rep -= reqdRep;
} else {
break;
}
reqdRep *= CONSTANTS.FactionReputationToFavorMult;
}
return [favorGain, rep];
} }
/** /**

@ -15,6 +15,7 @@ import { hasAugmentationPrereqs } from "../FactionHelpers";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import { Reputation } from "../../ui/React/Reputation"; import { Reputation } from "../../ui/React/Reputation";
import { Favor } from "../../ui/React/Favor"; import { Favor } from "../../ui/React/Favor";
import { numeralWrapper } from "../../ui/numeralFormat";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
@ -203,7 +204,7 @@ export function AugmentationsPage(props: IProps): React.ReactElement {
</Typography> </Typography>
} }
> >
<Typography>Price multiplier: x {mult.toFixed(3)}</Typography> <Typography>Price multiplier: x {numeralWrapper.formatMultiplier(mult)}</Typography>
</Tooltip> </Tooltip>
</Box> </Box>
<Button onClick={() => switchSortOrder(PurchaseAugmentationsOrderSetting.Cost)}>Sort by Cost</Button> <Button onClick={() => switchSortOrder(PurchaseAugmentationsOrderSetting.Cost)}>Sort by Cost</Button>

@ -18,8 +18,18 @@ import { HacknetNodeConstants } from "./data/Constants";
import { dialogBoxCreate } from "../ui/React/DialogBox"; import { dialogBoxCreate } from "../ui/React/DialogBox";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver"; import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver";
import { ObjectValidator, minMax } from "../utils/Validator";
export class HacknetNode implements IHacknetNode { export class HacknetNode implements IHacknetNode {
static validationData: ObjectValidator<HacknetNode> = {
cores: minMax(1, 1, HacknetNodeConstants.MaxCores),
level: minMax(1, 1, HacknetNodeConstants.MaxLevel),
ram: minMax(1, 1, HacknetNodeConstants.MaxRam),
onlineTimeSeconds: minMax(0, 0, Infinity),
totalMoneyGenerated: minMax(0, 0, Infinity)
}
// Node's number of cores // Node's number of cores
cores = 1; cores = 1;

@ -27,6 +27,7 @@ import { GetServer } from "../../Server/AllServers";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { Box } from "@mui/material";
interface IProps { interface IProps {
player: IPlayer; player: IPlayer;
@ -136,7 +137,7 @@ export function HacknetRoot(props: IProps): React.ReactElement {
{hasHacknetServers(props.player) && <Button onClick={() => setOpen(true)}>Spend Hashes on Upgrades</Button>} {hasHacknetServers(props.player) && <Button onClick={() => setOpen(true)}>Spend Hashes on Upgrades</Button>}
<Grid container>{nodes}</Grid> <Box sx={{ display: 'grid', width: 'fit-content', gridTemplateColumns: 'repeat(3, 1fr)' }}>{nodes}</Box>
<HashUpgradeModal open={open} onClose={() => setOpen(false)} /> <HashUpgradeModal open={open} onClose={() => setOpen(false)} />
</> </>
); );

@ -114,7 +114,7 @@ export const Literatures: IMap<Literature> = {};
"as working for any military/defense organization or conducting any bioengineering, computing, or robotics related research.<br><br>" + "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, " + "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 " + "and that many of them are blending in as normal humans in society today. In response, many nations have created " +
"Bladeburner divisions, special military branches that are tasked with investigating and dealing with any Synthoid threads.<br><br>" + "Bladeburner divisions, special military branches that are tasked with investigating and dealing with any Synthoid threats.<br><br>" +
"To this day, tensions still exist between the remaining Synthoids and humans as a result of the Uprising.<br><br>" + "To this day, tensions still exist between the remaining Synthoids and humans as a result of the Uprising.<br><br>" +
"Nobody knows what happened to the terrorist group Ascendis Totalis."; "Nobody knows what happened to the terrorist group Ascendis Totalis.";
Literatures[fn] = new Literature(title, fn, txt); Literatures[fn] = new Literature(title, fn, txt);

@ -10,6 +10,7 @@ import { CoinFlip } from "../../Casino/CoinFlip";
import { Roulette } from "../../Casino/Roulette"; import { Roulette } from "../../Casino/Roulette";
import { SlotMachine } from "../../Casino/SlotMachine"; import { SlotMachine } from "../../Casino/SlotMachine";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import { Box } from "@mui/material";
enum GameType { enum GameType {
None = "none", None = "none",
@ -33,15 +34,12 @@ export function CasinoLocation(props: IProps): React.ReactElement {
return ( return (
<> <>
{game === GameType.None && ( {game === GameType.None && (
<> <Box sx={{ display: 'grid', width: 'fit-content' }}>
<Button onClick={() => updateGame(GameType.Coin)}>Play coin flip</Button> <Button onClick={() => updateGame(GameType.Coin)}>Play coin flip</Button>
<br />
<Button onClick={() => updateGame(GameType.Slots)}>Play slots</Button> <Button onClick={() => updateGame(GameType.Slots)}>Play slots</Button>
<br />
<Button onClick={() => updateGame(GameType.Roulette)}>Play roulette</Button> <Button onClick={() => updateGame(GameType.Roulette)}>Play roulette</Button>
<br />
<Button onClick={() => updateGame(GameType.Blackjack)}>Play blackjack</Button> <Button onClick={() => updateGame(GameType.Blackjack)}>Play blackjack</Button>
</> </Box>
)} )}
{game !== GameType.None && ( {game !== GameType.None && (
<> <>

@ -198,7 +198,7 @@ export function CompanyLocation(props: IProps): React.ReactElement {
<Tooltip <Tooltip
title={ title={
<> <>
You will have <Favor favor={company.favor + favorGain[0]} /> company favor upon resetting after You will have <Favor favor={company.favor + favorGain} /> company favor upon resetting after
installing Augmentations installing Augmentations
</> </>
} }
@ -226,8 +226,12 @@ export function CompanyLocation(props: IProps): React.ReactElement {
</Box> </Box>
<Typography>-------------------------</Typography> <Typography>-------------------------</Typography>
<br /> <br />
</>
)}
<Box sx={{ display: 'grid', width: 'fit-content' }}>
{isEmployedHere && (
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<Button onClick={work}>Work</Button> <Button onClick={work}>Work</Button>
&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => setQuitOpen(true)}>Quit</Button> <Button onClick={() => setQuitOpen(true)}>Quit</Button>
<QuitJobModal <QuitJobModal
locName={props.locName} locName={props.locName}
@ -236,9 +240,10 @@ export function CompanyLocation(props: IProps): React.ReactElement {
open={quitOpen} open={quitOpen}
onClose={() => setQuitOpen(false)} onClose={() => setQuitOpen(false)}
/> />
</> </Box>
)} )
<br />
}
{company.hasAgentPositions() && ( {company.hasAgentPositions() && (
<ApplyToJobButton <ApplyToJobButton
company={company} company={company}
@ -328,6 +333,7 @@ export function CompanyLocation(props: IProps): React.ReactElement {
/> />
)} )}
{location.infiltrationData != null && <Button onClick={startInfiltration}>Infiltrate Company</Button>} {location.infiltrationData != null && <Button onClick={startInfiltration}>Infiltrate Company</Button>}
</Box>
</> </>
); );
} }

@ -16,6 +16,7 @@ import { Server } from "../../Server/Server";
import { Money } from "../../ui/React/Money"; import { Money } from "../../ui/React/Money";
import { IRouter } from "../../ui/Router"; import { IRouter } from "../../ui/Router";
import { serverMetadata } from "../../Server/data/servers"; import { serverMetadata } from "../../Server/data/servers";
import { Box } from "@mui/material";
type IProps = { type IProps = {
loc: Location; loc: Location;
@ -56,7 +57,7 @@ export function GymLocation(props: IProps): React.ReactElement {
const cost = CONSTANTS.ClassGymBaseCost * calculateCost(); const cost = CONSTANTS.ClassGymBaseCost * calculateCost();
return ( return (
<> <Box sx={{ display: 'grid', width: 'fit-content' }}>
<Button onClick={trainStrength}> <Button onClick={trainStrength}>
Train Strength (<Money money={cost} player={props.p} /> / sec) Train Strength (<Money money={cost} player={props.p} /> / sec)
</Button> </Button>
@ -72,6 +73,6 @@ export function GymLocation(props: IProps): React.ReactElement {
<Button onClick={trainAgility}> <Button onClick={trainAgility}>
Train Agility (<Money money={cost} player={props.p} /> / sec) Train Agility (<Money money={cost} player={props.p} /> / sec)
</Button> </Button>
</> </Box>
); );
} }

@ -11,6 +11,7 @@ import { Crimes } from "../../Crime/Crimes";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import { Box } from "@mui/material";
export function SlumsLocation(): React.ReactElement { export function SlumsLocation(): React.ReactElement {
const player = use.Player(); const player = use.Player();
@ -113,73 +114,61 @@ export function SlumsLocation(): React.ReactElement {
const heistChance = Crimes.Heist.successRate(player); const heistChance = Crimes.Heist.successRate(player);
return ( return (
<> <Box sx={{ display: 'grid', width: 'fit-content' }}>
<Tooltip title={<>Attempt to shoplift from a low-end retailer</>}> <Tooltip title={<>Attempt to shoplift from a low-end retailer</>}>
<Button onClick={shoplift}> <Button onClick={shoplift}>
Shoplift ({numeralWrapper.formatPercentage(shopliftChance)} chance of success) Shoplift ({numeralWrapper.formatPercentage(shopliftChance)} chance of success)
</Button> </Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={<>Attempt to commit armed robbery on a high-end store</>}> <Tooltip title={<>Attempt to commit armed robbery on a high-end store</>}>
<Button onClick={robStore}> <Button onClick={robStore}>
Rob store ({numeralWrapper.formatPercentage(robStoreChance)} chance of success) Rob store ({numeralWrapper.formatPercentage(robStoreChance)} chance of success)
</Button> </Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={<>Attempt to mug a random person on the street</>}> <Tooltip title={<>Attempt to mug a random person on the street</>}>
<Button onClick={mug}>Mug someone ({numeralWrapper.formatPercentage(mugChance)} chance of success)</Button> <Button onClick={mug}>Mug someone ({numeralWrapper.formatPercentage(mugChance)} chance of success)</Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={<>Attempt to rob property from someone's house</>}> <Tooltip title={<>Attempt to rob property from someone's house</>}>
<Button onClick={larceny}>Larceny ({numeralWrapper.formatPercentage(larcenyChance)} chance of success)</Button> <Button onClick={larceny}>Larceny ({numeralWrapper.formatPercentage(larcenyChance)} chance of success)</Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={<>Attempt to deal drugs</>}> <Tooltip title={<>Attempt to deal drugs</>}>
<Button onClick={dealDrugs}> <Button onClick={dealDrugs}>
Deal Drugs ({numeralWrapper.formatPercentage(drugsChance)} chance of success) Deal Drugs ({numeralWrapper.formatPercentage(drugsChance)} chance of success)
</Button> </Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={<>Attempt to forge corporate bonds</>}> <Tooltip title={<>Attempt to forge corporate bonds</>}>
<Button onClick={bondForgery}> <Button onClick={bondForgery}>
Bond Forgery ({numeralWrapper.formatPercentage(bondChance)} chance of success) Bond Forgery ({numeralWrapper.formatPercentage(bondChance)} chance of success)
</Button> </Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={<>Attempt to smuggle illegal arms into the city</>}> <Tooltip title={<>Attempt to smuggle illegal arms into the city</>}>
<Button onClick={traffickArms}> <Button onClick={traffickArms}>
Traffick illegal Arms ({numeralWrapper.formatPercentage(armsChance)} chance of success) Traffick illegal Arms ({numeralWrapper.formatPercentage(armsChance)} chance of success)
</Button> </Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={<>Attempt to murder a random person on the street</>}> <Tooltip title={<>Attempt to murder a random person on the street</>}>
<Button onClick={homicide}> <Button onClick={homicide}>
Homicide ({numeralWrapper.formatPercentage(homicideChance)} chance of success) Homicide ({numeralWrapper.formatPercentage(homicideChance)} chance of success)
</Button> </Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={<>Attempt to commit grand theft auto</>}> <Tooltip title={<>Attempt to commit grand theft auto</>}>
<Button onClick={grandTheftAuto}> <Button onClick={grandTheftAuto}>
Grand theft Auto ({numeralWrapper.formatPercentage(gtaChance)} chance of success) Grand theft Auto ({numeralWrapper.formatPercentage(gtaChance)} chance of success)
</Button> </Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={<>Attempt to kidnap and ransom a high-profile-target</>}> <Tooltip title={<>Attempt to kidnap and ransom a high-profile-target</>}>
<Button onClick={kidnap}> <Button onClick={kidnap}>
Kidnap and Ransom ({numeralWrapper.formatPercentage(kidnapChance)} chance of success) Kidnap and Ransom ({numeralWrapper.formatPercentage(kidnapChance)} chance of success)
</Button> </Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={<>Attempt to assassinate a high-profile target</>}> <Tooltip title={<>Attempt to assassinate a high-profile target</>}>
<Button onClick={assassinate}> <Button onClick={assassinate}>
Assassinate ({numeralWrapper.formatPercentage(assassinateChance)} chance of success) Assassinate ({numeralWrapper.formatPercentage(assassinateChance)} chance of success)
</Button> </Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={<>Attempt to pull off the ultimate heist</>}> <Tooltip title={<>Attempt to pull off the ultimate heist</>}>
<Button onClick={heist}>Heist ({numeralWrapper.formatPercentage(heistChance)} chance of success)</Button> <Button onClick={heist}>Heist ({numeralWrapper.formatPercentage(heistChance)} chance of success)</Button>
</Tooltip> </Tooltip>
<br /> </Box>
</>
); );
} }

@ -81,7 +81,7 @@ export function SpecialLocation(props: IProps): React.ReactElement {
return <></>; return <></>;
} }
const text = inBladeburner ? "Enter Bladeburner Headquarters" : "Apply to Bladeburner Division"; const text = inBladeburner ? "Enter Bladeburner Headquarters" : "Apply to Bladeburner Division";
return <Button onClick={handleBladeburner}>{text}</Button>; return <><br/><Button onClick={handleBladeburner}>{text}</Button></>;
} }
function renderNoodleBar(): React.ReactElement { function renderNoodleBar(): React.ReactElement {

@ -18,6 +18,7 @@ import { Money } from "../../ui/React/Money";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import { PurchaseServerModal } from "./PurchaseServerModal"; import { PurchaseServerModal } from "./PurchaseServerModal";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { Box } from "@mui/material";
interface IServerProps { interface IServerProps {
ram: number; ram: number;
@ -70,7 +71,9 @@ export function TechVendorLocation(props: IProps): React.ReactElement {
return ( return (
<> <>
<br /> <br />
<Box sx={{ display: 'grid', width: 'fit-content' }}>
{purchaseServerButtons} {purchaseServerButtons}
</Box>
<br /> <br />
<Typography> <Typography>
<i>"You can order bigger servers via scripts. We don't take custom orders in person."</i> <i>"You can order bigger servers via scripts. We don't take custom orders in person."</i>

@ -15,6 +15,7 @@ import { Server } from "../../Server/Server";
import { Money } from "../../ui/React/Money"; import { Money } from "../../ui/React/Money";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import { Box } from "@mui/material";
type IProps = { type IProps = {
loc: Location; loc: Location;
@ -72,45 +73,40 @@ export function UniversityLocation(props: IProps): React.ReactElement {
const earnCharismaExpTooltip = `Gain charisma experience!`; const earnCharismaExpTooltip = `Gain charisma experience!`;
return ( return (
<> <Box sx={{ display: 'grid', width: 'fit-content' }}>
<Tooltip title={earnHackingExpTooltip}> <Tooltip title={earnHackingExpTooltip}>
<Button onClick={study}>Study Computer Science (free)</Button> <Button onClick={study}>Study Computer Science (free)</Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={earnHackingExpTooltip}> <Tooltip title={earnHackingExpTooltip}>
<Button onClick={dataStructures}> <Button onClick={dataStructures}>
Take Data Structures course ( Take Data Structures course (
<Money money={dataStructuresCost} player={player} /> / sec) <Money money={dataStructuresCost} player={player} /> / sec)
</Button> </Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={earnHackingExpTooltip}> <Tooltip title={earnHackingExpTooltip}>
<Button onClick={networks}> <Button onClick={networks}>
Take Networks course ( Take Networks course (
<Money money={networksCost} player={player} /> / sec) <Money money={networksCost} player={player} /> / sec)
</Button> </Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={earnHackingExpTooltip}> <Tooltip title={earnHackingExpTooltip}>
<Button onClick={algorithms}> <Button onClick={algorithms}>
Take Algorithms course ( Take Algorithms course (
<Money money={algorithmsCost} player={player} /> / sec) <Money money={algorithmsCost} player={player} /> / sec)
</Button> </Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={earnCharismaExpTooltip}> <Tooltip title={earnCharismaExpTooltip}>
<Button onClick={management}> <Button onClick={management}>
Take Management course ( Take Management course (
<Money money={managementCost} player={player} /> / sec) <Money money={managementCost} player={player} /> / sec)
</Button> </Button>
</Tooltip> </Tooltip>
<br />
<Tooltip title={earnCharismaExpTooltip}> <Tooltip title={earnCharismaExpTooltip}>
<Button onClick={leadership}> <Button onClick={leadership}>
Take Leadership course ( Take Leadership course (
<Money money={leadershipCost} player={player} /> / sec) <Money money={leadershipCost} player={player} /> / sec)
</Button> </Button>
</Tooltip> </Tooltip>
</> </Box>
); );
} }

@ -364,6 +364,9 @@ export const RamCosts: IMap<any> = {
getTheme: 0, getTheme: 0,
setTheme: 0, setTheme: 0,
resetTheme: 0, resetTheme: 0,
getStyles: 0,
setStyles: 0,
resetStyles: 0,
}, },
heart: { heart: {

@ -33,9 +33,9 @@ export class WorkerScript {
delay: number | null = null; delay: number | null = null;
/** /**
* Holds the Promise resolve() function for when the script is "blocked" by an async op * Holds the Promise reject() function while the script is "blocked" by an async op
*/ */
delayResolve?: () => void; delayReject?: (reason?: any) => void;
/** /**
* Stores names of all functions that have logging disabled * Stores names of all functions that have logging disabled

@ -138,8 +138,8 @@ function killNetscriptDelay(workerScript: WorkerScript): void {
if (workerScript instanceof WorkerScript) { if (workerScript instanceof WorkerScript) {
if (workerScript.delay) { if (workerScript.delay) {
clearTimeout(workerScript.delay); clearTimeout(workerScript.delay);
if (workerScript.delayResolve) { if (workerScript.delayReject) {
workerScript.delayResolve(); workerScript.delayReject(workerScript);
} }
} }
} }

@ -3,12 +3,17 @@ import { GetServer } from "./Server/AllServers";
import { WorkerScript } from "./Netscript/WorkerScript"; import { WorkerScript } from "./Netscript/WorkerScript";
export function netscriptDelay(time: number, workerScript: WorkerScript): Promise<void> { export function netscriptDelay(time: number, workerScript: WorkerScript): Promise<void> {
return new Promise(function (resolve) { return new Promise(function (resolve, reject) {
workerScript.delay = window.setTimeout(() => { workerScript.delay = window.setTimeout(() => {
workerScript.delay = null; workerScript.delay = null;
workerScript.delayReject = undefined;
if (workerScript.env.stopFlag)
reject(workerScript);
else
resolve(); resolve();
}, time); }, time);
workerScript.delayResolve = resolve; workerScript.delayReject = reject;
}); });
} }

@ -335,16 +335,13 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
workerScript.log( workerScript.log(
"hack", "hack",
() => () =>
`Executing ${hostname} in ${convertTimeMsToTimeElapsedString( `Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString(
hackingTime * 1000, hackingTime * 1000,
true, true,
)} (t=${numeralWrapper.formatThreads(threads)})`, )} (t=${numeralWrapper.formatThreads(threads)})`,
); );
return netscriptDelay(hackingTime * 1000, workerScript).then(function () { return netscriptDelay(hackingTime * 1000, workerScript).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
const hackChance = calculateHackingChance(server, Player); const hackChance = calculateHackingChance(server, Player);
const rand = Math.random(); const rand = Math.random();
let expGainedOnSuccess = calculateHackingExpGain(server, Player) * threads; let expGainedOnSuccess = calculateHackingExpGain(server, Player) * threads;
@ -352,7 +349,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
if (rand < hackChance) { if (rand < hackChance) {
// Success! // Success!
const percentHacked = calculatePercentMoneyHacked(server, Player); const percentHacked = calculatePercentMoneyHacked(server, Player);
let maxThreadNeeded = Math.ceil((1 / percentHacked) * (server.moneyAvailable / server.moneyMax)); let maxThreadNeeded = Math.ceil(1 / percentHacked);
if (isNaN(maxThreadNeeded)) { if (isNaN(maxThreadNeeded)) {
// Server has a 'max money' of 0 (probably). We'll set this to an arbitrarily large value // Server has a 'max money' of 0 (probably). We'll set this to an arbitrarily large value
maxThreadNeeded = 1e6; maxThreadNeeded = 1e6;
@ -432,8 +429,10 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
throw makeRuntimeErrorMsg(funcName, `${argName} should be a string`); throw makeRuntimeErrorMsg(funcName, `${argName} should be a string`);
}, },
number: (funcName: string, argName: string, v: any): number => { number: (funcName: string, argName: string, v: any): number => {
if (!isNaN(v)) {
if (typeof v === "number") return v; if (typeof v === "number") return v;
if (!isNaN(v) && !isNaN(parseFloat(v))) return parseFloat(v); if (!isNaN(parseFloat(v))) return parseFloat(v);
}
throw makeRuntimeErrorMsg(funcName, `${argName} should be a number`); throw makeRuntimeErrorMsg(funcName, `${argName} should be a number`);
}, },
boolean: (v: any): boolean => { boolean: (v: any): boolean => {
@ -613,9 +612,6 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
)} (t=${numeralWrapper.formatThreads(threads)}).`, )} (t=${numeralWrapper.formatThreads(threads)}).`,
); );
return netscriptDelay(growTime * 1000, workerScript).then(function () { return netscriptDelay(growTime * 1000, workerScript).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
const moneyBefore = server.moneyAvailable <= 0 ? 1 : server.moneyAvailable; const moneyBefore = server.moneyAvailable <= 0 ? 1 : server.moneyAvailable;
processSingleServerGrowth(server, threads, Player, host.cpuCores); processSingleServerGrowth(server, threads, Player, host.cpuCores);
const moneyAfter = server.moneyAvailable; const moneyAfter = server.moneyAvailable;
@ -684,7 +680,6 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
)} (t=${numeralWrapper.formatThreads(threads)})`, )} (t=${numeralWrapper.formatThreads(threads)})`,
); );
return netscriptDelay(weakenTime * 1000, workerScript).then(function () { return netscriptDelay(weakenTime * 1000, workerScript).then(function () {
if (workerScript.env.stopFlag) return Promise.reject(workerScript);
const host = GetServer(workerScript.hostname); const host = GetServer(workerScript.hostname);
if (host === null) { if (host === null) {
workerScript.log("weaken", () => "Server is null, did it die?"); workerScript.log("weaken", () => "Server is null, did it die?");
@ -697,8 +692,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
workerScript.log( workerScript.log(
"weaken", "weaken",
() => () =>
`'${server.hostname}' security level weakened to ${ `'${server.hostname}' security level weakened to ${server.hackDifficulty
server.hackDifficulty
}. Gained ${numeralWrapper.formatExp(expGain)} hacking exp (t=${numeralWrapper.formatThreads(threads)})`, }. Gained ${numeralWrapper.formatExp(expGain)} hacking exp (t=${numeralWrapper.formatThreads(threads)})`,
); );
workerScript.scriptRef.onlineExpGained += expGain; workerScript.scriptRef.onlineExpGained += expGain;
@ -2011,7 +2005,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return calculateGrowTime(server, Player) * 1000; return calculateGrowTime(server, Player) * 1000;
}, },
getWeakenTime: function (hostname: any): any { getWeakenTime: function (hostname: any = workerScript.hostname): any {
updateDynamicRam("getWeakenTime", getRamCost(Player, "getWeakenTime")); updateDynamicRam("getWeakenTime", getRamCost(Player, "getWeakenTime"));
const server = safeGetServer(hostname, "getWeakenTime"); const server = safeGetServer(hostname, "getWeakenTime");
if (!(server instanceof Server)) { if (!(server instanceof Server)) {

@ -53,23 +53,22 @@ export function NetscriptCodingContract(
const serv = helper.getServer(hostname, "codingcontract.attempt"); const serv = helper.getServer(hostname, "codingcontract.attempt");
if (contract.isSolution(answer)) { if (contract.isSolution(answer)) {
const reward = player.gainCodingContractReward(creward, contract.getDifficulty()); const reward = player.gainCodingContractReward(creward, contract.getDifficulty());
workerScript.log("attempt", () => `Successfully completed Coding Contract '${filename}'. Reward: ${reward}`); workerScript.log("codingcontract.attempt", () => `Successfully completed Coding Contract '${filename}'. Reward: ${reward}`);
serv.removeContract(filename); serv.removeContract(filename);
return returnReward ? reward : true; return returnReward ? reward : true;
} else { } else {
++contract.tries; ++contract.tries;
if (contract.tries >= contract.getMaxNumTries()) { if (contract.tries >= contract.getMaxNumTries()) {
workerScript.log( workerScript.log(
"attempt", "codingcontract.attempt",
() => `Coding Contract attempt '${filename}' failed. Contract is now self-destructing`, () => `Coding Contract attempt '${filename}' failed. Contract is now self-destructing`,
); );
serv.removeContract(filename); serv.removeContract(filename);
} else { } else {
workerScript.log( workerScript.log(
"attempt", "codingcontract.attempt",
() => () =>
`Coding Contract attempt '${filename}' failed. ${ `Coding Contract attempt '${filename}' failed. ${contract.getMaxNumTries() - contract.tries
contract.getMaxNumTries() - contract.tries
} attempts remaining.`, } attempts remaining.`,
); );
} }

@ -311,9 +311,6 @@ export function NetscriptCorporation(
const job = helper.string("assignJob", "job", ajob); const job = helper.string("assignJob", "job", ajob);
const employee = getEmployee(divisionName, cityName, employeeName); const employee = getEmployee(divisionName, cityName, employeeName);
return netscriptDelay(1000, workerScript).then(function () { return netscriptDelay(1000, workerScript).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
return Promise.resolve(AssignJob(employee, job)); return Promise.resolve(AssignJob(employee, job));
}); });
}, },
@ -344,9 +341,6 @@ export function NetscriptCorporation(
(60 * 1000) / (player.hacking_speed_mult * calculateIntelligenceBonus(player.intelligence, 1)), (60 * 1000) / (player.hacking_speed_mult * calculateIntelligenceBonus(player.intelligence, 1)),
workerScript, workerScript,
).then(function () { ).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
return Promise.resolve(ThrowParty(corporation, office, costPerEmployee)); return Promise.resolve(ThrowParty(corporation, office, costPerEmployee));
}); });
}, },
@ -359,9 +353,6 @@ export function NetscriptCorporation(
(60 * 1000) / (player.hacking_speed_mult * calculateIntelligenceBonus(player.intelligence, 1)), (60 * 1000) / (player.hacking_speed_mult * calculateIntelligenceBonus(player.intelligence, 1)),
workerScript, workerScript,
).then(function () { ).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
return Promise.resolve(BuyCoffee(corporation, getDivision(divisionName), getOffice(divisionName, cityName))); return Promise.resolve(BuyCoffee(corporation, getDivision(divisionName), getOffice(divisionName, cityName)));
}); });
}, },

@ -111,7 +111,7 @@ export function NetscriptHacknet(player: IPlayer, workerScript: WorkerScript, he
} }
const node = getHacknetNode(i, "upgradeCache"); const node = getHacknetNode(i, "upgradeCache");
if (!(node instanceof HacknetServer)) { if (!(node instanceof HacknetServer)) {
workerScript.log("upgradeCache", () => "Can only be called on hacknet servers"); workerScript.log("hacknet.upgradeCache", () => "Can only be called on hacknet servers");
return false; return false;
} }
const res = purchaseCacheUpgrade(player, node, n); const res = purchaseCacheUpgrade(player, node, n);
@ -138,7 +138,7 @@ export function NetscriptHacknet(player: IPlayer, workerScript: WorkerScript, he
} }
const node = getHacknetNode(i, "upgradeCache"); const node = getHacknetNode(i, "upgradeCache");
if (!(node instanceof HacknetServer)) { if (!(node instanceof HacknetServer)) {
workerScript.log("getCacheUpgradeCost", () => "Can only be called on hacknet servers"); workerScript.log("hacknet.getCacheUpgradeCost", () => "Can only be called on hacknet servers");
return -1; return -1;
} }
return node.calculateCacheUpgradeCost(n); return node.calculateCacheUpgradeCost(n);

@ -611,9 +611,6 @@ export function NetscriptSingularity(
); );
return netscriptDelay(installTime, workerScript).then(function () { return netscriptDelay(installTime, workerScript).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
workerScript.log("installBackdoor", () => `Successfully installed backdoor on '${server.hostname}'`); workerScript.log("installBackdoor", () => `Successfully installed backdoor on '${server.hostname}'`);
server.backdoorInstalled = true; server.backdoorInstalled = true;

@ -38,9 +38,6 @@ export function NetscriptStanek(player: IPlayer, workerScript: WorkerScript, hel
if (!fragment) throw helper.makeRuntimeErrorMsg("stanek.charge", `No fragment with root (${rootX}, ${rootY}).`); if (!fragment) throw helper.makeRuntimeErrorMsg("stanek.charge", `No fragment with root (${rootX}, ${rootY}).`);
const time = staneksGift.inBonus() ? 200 : 1000; const time = staneksGift.inBonus() ? 200 : 1000;
return netscriptDelay(time, workerScript).then(function () { return netscriptDelay(time, workerScript).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
const charge = staneksGift.charge(player, fragment, workerScript.scriptRef.threads); const charge = staneksGift.charge(player, fragment, workerScript.scriptRef.threads);
workerScript.log("stanek.charge", () => `Charged fragment for ${charge} charge.`); workerScript.log("stanek.charge", () => `Charged fragment for ${charge} charge.`);
return Promise.resolve(); return Promise.resolve();

@ -2,10 +2,11 @@ import { INetscriptHelper } from "./INetscriptHelper";
import { WorkerScript } from "../Netscript/WorkerScript"; import { WorkerScript } from "../Netscript/WorkerScript";
import { IPlayer } from "../PersonObjects/IPlayer"; import { IPlayer } from "../PersonObjects/IPlayer";
import { getRamCost } from "../Netscript/RamCostGenerator"; import { getRamCost } from "../Netscript/RamCostGenerator";
import { UserInterface as IUserInterface, UserInterfaceTheme } from "../ScriptEditor/NetscriptDefinitions"; import { IStyleSettings, UserInterface as IUserInterface, UserInterfaceTheme } from "../ScriptEditor/NetscriptDefinitions";
import { Settings } from "../Settings/Settings"; import { Settings } from "../Settings/Settings";
import { ThemeEvents } from "../ui/React/Theme"; import { ThemeEvents } from "../ui/React/Theme";
import { defaultTheme } from "../Settings/Themes"; import { defaultTheme } from "../Settings/Themes";
import { defaultStyles } from "../Settings/Styles";
export function NetscriptUserInterface( export function NetscriptUserInterface(
player: IPlayer, player: IPlayer,
@ -18,6 +19,11 @@ export function NetscriptUserInterface(
return { ...Settings.theme }; return { ...Settings.theme };
}, },
getStyles: function (): IStyleSettings {
helper.updateDynamicRam("getStyles", getRamCost(player, "ui", "getStyles"));
return { ...Settings.styles };
},
setTheme: function (newTheme: UserInterfaceTheme): void { setTheme: function (newTheme: UserInterfaceTheme): void {
helper.updateDynamicRam("setTheme", getRamCost(player, "ui", "setTheme")); helper.updateDynamicRam("setTheme", getRamCost(player, "ui", "setTheme"));
const hex = /^(#)((?:[A-Fa-f0-9]{3}){1,2})$/; const hex = /^(#)((?:[A-Fa-f0-9]{3}){1,2})$/;
@ -43,11 +49,41 @@ export function NetscriptUserInterface(
} }
}, },
setStyles: function (newStyles: IStyleSettings): void {
helper.updateDynamicRam("setStyles", getRamCost(player, "ui", "setStyles"));
const currentStyles = {...Settings.styles}
const errors: string[] = [];
for (const key of Object.keys(newStyles)) {
if (!((currentStyles as any)[key])) {
// Invalid key
errors.push(`Invalid key "${key}"`);
} else {
(currentStyles as any)[key] = (newStyles as any)[key];
}
}
if (errors.length === 0) {
Object.assign(Settings.styles, currentStyles);
ThemeEvents.emit();
workerScript.log("ui.setStyles", () => `Successfully set styles`);
} else {
workerScript.log("ui.setStyles", () => `Failed to set styles. Errors: ${errors.join(', ')}`);
}
},
resetTheme: function (): void { resetTheme: function (): void {
helper.updateDynamicRam("resetTheme", getRamCost(player, "ui", "resetTheme")); helper.updateDynamicRam("resetTheme", getRamCost(player, "ui", "resetTheme"));
Settings.theme = defaultTheme; Settings.theme = { ...defaultTheme };
ThemeEvents.emit(); ThemeEvents.emit();
workerScript.log("ui.resetTheme", () => `Reinitialized theme to default`); workerScript.log("ui.resetTheme", () => `Reinitialized theme to default`);
}, },
resetStyles: function (): void {
helper.updateDynamicRam("resetStyles", getRamCost(player, "ui", "resetStyles"));
Settings.styles = { ...defaultStyles };
ThemeEvents.emit();
workerScript.log("ui.resetStyles", () => `Reinitialized styles to default`);
}
} }
} }

@ -946,11 +946,6 @@ export function workForFaction(this: IPlayer, numCycles: number): boolean {
default: default:
break; break;
} }
let favorMult = 1 + faction.favor / 100;
if (isNaN(favorMult)) {
favorMult = 1;
}
this.workRepGainRate *= favorMult;
this.workRepGainRate *= BitNodeMultipliers.FactionWorkRepGain; this.workRepGainRate *= BitNodeMultipliers.FactionWorkRepGain;
//Cap the number of cycles being processed to whatever would put you at limit (20 hours) //Cap the number of cycles being processed to whatever would put you at limit (20 hours)

@ -161,8 +161,6 @@ export function prestigeAugmentation(): void {
resetPidCounter(); resetPidCounter();
ProgramsSeen.splice(0, ProgramsSeen.length); ProgramsSeen.splice(0, ProgramsSeen.length);
InvitationsSeen.splice(0, InvitationsSeen.length); InvitationsSeen.splice(0, InvitationsSeen.length);
Router.clearHistory();
} }
// Prestige by destroying Bit Node and gaining a Source File // Prestige by destroying Bit Node and gaining a Source File
@ -308,7 +306,5 @@ export function prestigeSourceFile(flume: boolean): void {
// Gain int exp // Gain int exp
if (SourceFileFlags[5] !== 0 && !flume) Player.gainIntelligenceExp(300); if (SourceFileFlags[5] !== 0 && !flume) Player.gainIntelligenceExp(300);
Router.clearHistory();
resetPidCounter(); resetPidCounter();
} }

@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import { getAvailableCreatePrograms } from "../ProgramHelpers"; import { getAvailableCreatePrograms } from "../ProgramHelpers";
import { Tooltip, Typography } from "@mui/material"; import { Box, Tooltip, Typography } from "@mui/material";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
export const ProgramsSeen: string[] = []; export const ProgramsSeen: string[] = [];
@ -38,6 +38,7 @@ export function ProgramsRoot(): React.ReactElement {
time. Your progress will be saved and you can continue later. time. Your progress will be saved and you can continue later.
</Typography> </Typography>
<Box sx={{ display: 'grid', width: 'fit-content' }}>
{programs.map((program) => { {programs.map((program) => {
const create = program.create; const create = program.create;
if (create === null) return <></>; if (create === null) return <></>;
@ -55,10 +56,10 @@ export function ProgramsRoot(): React.ReactElement {
{program.name} {program.name}
</Button> </Button>
</Tooltip> </Tooltip>
<br />
</React.Fragment> </React.Fragment>
); );
})} })}
</Box>
</> </>
); );
} }

@ -118,7 +118,7 @@ export class Script {
*/ */
saveScript(player: IPlayer, filename: string, code: string, hostname: string, otherScripts: Script[]): void { saveScript(player: IPlayer, filename: string, code: string, hostname: string, otherScripts: Script[]): void {
// Update code and filename // Update code and filename
this.code = code.replace(/^\s+|\s+$/g, ""); this.code = Script.formatCode(code);
this.filename = filename; this.filename = filename;
this.server = hostname; this.server = hostname;
@ -158,6 +158,15 @@ export class Script {
s.rehash(); s.rehash();
return s; return s;
} }
/**
* Formats code: Removes the starting & trailing whitespace
* @param {string} code - The code to format
* @returns The formatted code
*/
static formatCode(code: string): string {
return code.replace(/^\s+|\s+$/g, "");
}
} }
Reviver.constructors.Script = Script; Reviver.constructors.Script = Script;

@ -109,32 +109,6 @@ interface RunningScript {
threads: number; threads: number;
} }
/**
* Interface of a netscript port
* @public
*/
export interface IPort {
/** write data to the port and removes and returns first element if full */
write: (value: any) => any;
/** add data to port if not full.
* @returns true if added and false if full and not added */
tryWrite: (value: any) => boolean;
/** reads and removes first element from port
* if no data in port returns "NULL PORT DATA"
*/
read: () => any;
/** reads first element without removing it from port
* if no data in port returns "NULL PORT DATA"
*/
peek: () => any;
/** check if port is full */
full: () => boolean;
/** check if port is empty */
empty: () => boolean;
/** removes all data from port */
clear: () => void;
}
/** /**
* Data representing the internal values of a crime. * Data representing the internal values of a crime.
* @public * @public
@ -280,6 +254,24 @@ export interface AugmentPair {
cost: number; cost: number;
} }
/**
* @public
*/
export enum PositionTypes {
Long = "L",
Short = "S",
}
/**
* @public
*/
export enum OrderTypes {
LimitBuy = "Limit Buy Order",
LimitSell = "Limit Sell Order",
StopBuy = "Stop Buy Order",
StopSell = "Stop Sell Order",
}
/** /**
* Value in map of {@link StockOrder} * Value in map of {@link StockOrder}
* @public * @public
@ -290,17 +282,18 @@ export interface StockOrderObject {
/** Price per share */ /** Price per share */
price: number; price: number;
/** Order type */ /** Order type */
type: string; type: OrderTypes;
/** Order position */ /** Order position */
position: string; position: PositionTypes;
} }
/** /**
* Return value of {@link TIX.getOrders | getOrders} * Return value of {@link TIX.getOrders | getOrders}
*
* Keys are stock symbols, properties are arrays of {@link StockOrderObject}
* @public * @public
*/ */
export interface StockOrder { export interface StockOrder {
/** Stock Symbol */
[key: string]: StockOrderObject[]; [key: string]: StockOrderObject[];
} }
@ -488,6 +481,8 @@ export interface BitNodeMultipliers {
FourSigmaMarketDataApiCost: number; FourSigmaMarketDataApiCost: number;
/** Influences how much it costs to unlock the stock market's 4S Market Data (NOT API) */ /** Influences how much it costs to unlock the stock market's 4S Market Data (NOT API) */
FourSigmaMarketDataCost: number; FourSigmaMarketDataCost: number;
/** Influences the respect gain and money gain of your gang. */
GangSoftcap: number;
/** Influences the experienced gained when hacking a server. */ /** Influences the experienced gained when hacking a server. */
HackExpGain: number; HackExpGain: number;
/** Influences how quickly the player's hacking level (not experience) scales */ /** Influences how quickly the player's hacking level (not experience) scales */
@ -508,10 +503,14 @@ export interface BitNodeMultipliers {
PurchasedServerLimit: number; PurchasedServerLimit: number;
/** Influences the maximum allowed RAM for a purchased server */ /** Influences the maximum allowed RAM for a purchased server */
PurchasedServerMaxRam: number; PurchasedServerMaxRam: number;
/** Influences cost of any purchased server at or above 128GB */
PurchasedServerSoftCap: number;
/** Influences the minimum favor the player must have with a faction before they can donate to gain rep. */ /** Influences the minimum favor the player must have with a faction before they can donate to gain rep. */
RepToDonateToFaction: number; RepToDonateToFaction: number;
/** Influences how much money can be stolen from a server when a script performs a hack against it. */ /** Influences how much the money on a server can be reduced when a script performs a hack against it. */
ScriptHackMoney: number; ScriptHackMoney: number;
/** Influences how much of the money stolen by a scripted hack will be added to the player's money. */
ScriptHackMoneyGain: number;
/** Influences the growth percentage per cycle against a server. */ /** Influences the growth percentage per cycle against a server. */
ServerGrowthRate: number; ServerGrowthRate: number;
/** Influences the maxmimum money that a server can grow to. */ /** Influences the maxmimum money that a server can grow to. */
@ -524,6 +523,12 @@ export interface BitNodeMultipliers {
ServerWeakenRate: number; ServerWeakenRate: number;
/** Influences how quickly the player's strength level (not exp) scales */ /** Influences how quickly the player's strength level (not exp) scales */
StrengthLevelMultiplier: number; StrengthLevelMultiplier: number;
/** Influences the power of the gift */
StaneksGiftPowerMultiplier: number;
/** Influences the size of the gift */
StaneksGiftExtraSize: number;
/** Influences the hacking skill required to backdoor the world daemon. */
WorldDaemonDifficulty: number;
} }
/** /**
@ -535,9 +540,9 @@ export interface NodeStats {
name: string; name: string;
/** Node's level */ /** Node's level */
level: number; level: number;
/** Node's RAM */ /** Node's RAM (GB) */
ram: number; ram: number;
/** Node's used RAM */ /** Node's used RAM (GB) */
ramUsed: number; ramUsed: number;
/** Node's number of cores */ /** Node's number of cores */
cores: number; cores: number;
@ -956,6 +961,78 @@ export interface SleeveTask {
factionWorkType: string; factionWorkType: string;
} }
/**
* Object representing a port. A port is a serialized queue.
* @public
*/
export interface NetscriptPort {
/**
* Write data to a port.
* @remarks
* RAM cost: 0 GB
*
* @returns The data popped off the queue if it was full.
*/
write(value: string|number): null|string|number;
/**
* Attempt to write data to the port.
* @remarks
* RAM cost: 0 GB
*
* @returns True if the data was added to the port, false if the port was full
*/
tryWrite(value: string|number): boolean;
/**
* Shift an element out of the port.
* @remarks
* RAM cost: 0 GB
*
* This function will remove the first element from the port and return it.
* If the port is empty, then the string NULL PORT DATA will be returned.
* @returns the data read.
*/
read(): string|number;
/**
* Retrieve the first element from the port without removing it.
* @remarks
* RAM cost: 0 GB
*
* This function is used to peek at the data from a port. It returns the
* first element in the specified port without removing that element. If
* the port is empty, the string NULL PORT DATA will be returned.
* @returns the data read
*/
peek(): string|number;
/**
* Check if the port is full.
* @remarks
* RAM cost: 0 GB
*
* @returns true if the port is full, otherwise false
*/
full(): boolean;
/**
* Check if the port is empty.
* @remarks
* RAM cost: 0 GB
*
* @returns true if the port is empty, otherwise false
*/
empty(): boolean;
/**
* Empties all data from the port.
* @remarks
* RAM cost: 0 GB
*/
clear(): void;
}
/** /**
* Stock market API * Stock market API
* @public * @public
@ -1213,6 +1290,8 @@ export interface TIX {
* @remarks * @remarks
* RAM cost: 2.5 GB * RAM cost: 2.5 GB
* This is an object containing information for all the Limit and Stop Orders you have in the stock market. * This is an object containing information for all the Limit and Stop Orders you have in the stock market.
* For each symbol you have a position in, the returned object will have a key with that symbol's name.
* The object's properties are each an array of {@link StockOrderObject}
* The object has the following structure: * The object has the following structure:
* *
* ```ts * ```ts
@ -1320,7 +1399,7 @@ export interface TIX {
/** /**
* Singularity API * Singularity API
* @remarks * @remarks
* This API requires Source-File 4 level 1 to use. The RAM cost of all these functions is multiplied by 16/4/1 based on Source-File 4 levels. * This API requires Source-File 4 to use. The RAM cost of all these functions is multiplied by 16/4/1 based on Source-File 4 levels.
* @public * @public
*/ */
export interface Singularity { export interface Singularity {
@ -3880,6 +3959,36 @@ interface UserInterface {
* RAM cost: cost: 0 GB * RAM cost: cost: 0 GB
*/ */
resetTheme(): void; resetTheme(): void;
/**
* Get the current styles
* @remarks
* RAM cost: cost: 0 GB
*
* @returns An object containing the player's styles
*/
getStyles(): IStyleSettings;
/**
* Sets the current styles
* @remarks
* RAM cost: cost: 0 GB
* @example
* Usage example (NS2)
* ```ts
* const styles = ns.ui.getStyles();
* styles.fontFamily = 'Comic Sans Ms';
* ns.ui.setStyles(styles);
* ```
*/
setStyles(newStyles: IStyleSettings): void;
/**
* Resets the player's styles to the default values
* @remarks
* RAM cost: cost: 0 GB
*/
resetStyles(): void;
} }
/** /**
@ -4410,11 +4519,11 @@ export interface NS extends Singularity {
* //Get logs from foo.script on the foodnstuff server that was run with the arguments [1, "test"] * //Get logs from foo.script on the foodnstuff server that was run with the arguments [1, "test"]
* ns.tail("foo.script", "foodnstuff", 1, "test"); * ns.tail("foo.script", "foodnstuff", 1, "test");
* ``` * ```
* @param fn - Optional. Filename of the script being tailed. If omitted, the current script is tailed. * @param fn - Optional. Filename or PID of the script being tailed. If omitted, the current script is tailed.
* @param host - Optional. Hostname of the script being tailed. Defaults to the server this script is running on. If args are specified, this is not optional. * @param host - Optional. Hostname of the script being tailed. Defaults to the server this script is running on. If args are specified, this is not optional.
* @param args - Arguments for the script being tailed. * @param args - Arguments for the script being tailed.
*/ */
tail(fn?: string, host?: string, ...args: any[]): void; tail(fn?: FilenameOrPID, host?: string, ...args: any[]): void;
/** /**
* Get the list of servers connected to a server. * Get the list of servers connected to a server.
@ -5064,7 +5173,7 @@ export interface NS extends Singularity {
* const [totalRam, ramUsed] = ns.getServerRam("helios"); * const [totalRam, ramUsed] = ns.getServerRam("helios");
* ``` * ```
* @param host - Host of target server. * @param host - Host of target server.
* @returns Array with total and used memory on the specified server. * @returns Array with total and used memory on the specified server, in GB.
*/ */
getServerRam(host: string): [number, number]; getServerRam(host: string): [number, number];
@ -5074,7 +5183,7 @@ export interface NS extends Singularity {
* RAM cost: 0.05 GB * RAM cost: 0.05 GB
* *
* @param host - Hostname of the target server. * @param host - Hostname of the target server.
* @returns max ram * @returns max ram (GB)
*/ */
getServerMaxRam(host: string): number; getServerMaxRam(host: string): number;
/** /**
@ -5083,7 +5192,7 @@ export interface NS extends Singularity {
* RAM cost: 0.05 GB * RAM cost: 0.05 GB
* *
* @param host - Hostname of the target server. * @param host - Hostname of the target server.
* @returns used ram * @returns used ram (GB)
*/ */
getServerUsedRam(host: string): number; getServerUsedRam(host: string): number;
@ -5157,6 +5266,7 @@ export interface NS extends Singularity {
* RAM cost: 0.1 GB * RAM cost: 0.1 GB
* *
* Returns a boolean indicating whether the specified script is running on the target server. * Returns a boolean indicating whether the specified script is running on the target server.
* If you use a PID instead of a filename, the hostname and args parameters are unnecessary.
* Remember that a script is uniquely identified by both its name and its arguments. * Remember that a script is uniquely identified by both its name and its arguments.
* *
* @example * @example
@ -5183,12 +5293,12 @@ export interface NS extends Singularity {
* //The function call will return true if there is a script named foo.script running with the arguments 1, 5, and “test” (in that order) on the joesguns server, and false otherwise: * //The function call will return true if there is a script named foo.script running with the arguments 1, 5, and “test” (in that order) on the joesguns server, and false otherwise:
* ns.isRunning("foo.script", "joesguns", 1, 5, "test"); * ns.isRunning("foo.script", "joesguns", 1, 5, "test");
* ``` * ```
* @param script - Filename of script to check. This is case-sensitive. * @param script - Filename or PID of script to check. This is case-sensitive.
* @param host - Host of target server. * @param host - Host of target server.
* @param args - Arguments to specify/identify which scripts to search for. * @param args - Arguments to specify/identify which scripts to search for.
* @returns True if specified script is running on the target server, and false otherwise. * @returns True if specified script is running on the target server, and false otherwise.
*/ */
isRunning(script: string, host: string, ...args: string[]): boolean; isRunning(script: FilenameOrPID, host: string, ...args: string[]): boolean;
/** /**
* Get general info about a running script. * Get general info about a running script.
@ -5196,10 +5306,14 @@ export interface NS extends Singularity {
* RAM cost: 0.3 GB * RAM cost: 0.3 GB
* *
* Running with no args returns curent script. * Running with no args returns curent script.
* If you use a PID as the first parameter, the hostname and args parameters are unnecessary.
* *
* @param filename - Optional. Filename or PID of the script.
* @param hostname - Optional. Name of host server the script is running on.
* @param args - Arguments to identify the script
* @returns info about a running script * @returns info about a running script
*/ */
getRunningScript(filename?: string | number, hostname?: string, ...args: (string | number)[]): RunningScript; getRunningScript(filename?: FilenameOrPID, hostname?: string, ...args: (string | number)[]): RunningScript;
/** /**
* Get cost of purchasing a server. * Get cost of purchasing a server.
@ -5222,7 +5336,7 @@ export interface NS extends Singularity {
* ns.tprint(i + " -- " + ns.getPurchasedServerCost(Math.pow(2, i))); * ns.tprint(i + " -- " + ns.getPurchasedServerCost(Math.pow(2, i)));
* } * }
* ``` * ```
* @param ram - Amount of RAM of a potential purchased server. Must be a power of 2 (2, 4, 8, 16, etc.). Maximum value of 1048576 (2^20). * @param ram - Amount of RAM of a potential purchased server, in GB. Must be a power of 2 (2, 4, 8, 16, etc.). Maximum value of 1048576 (2^20).
* @returns The cost to purchase a server with the specified amount of ram. * @returns The cost to purchase a server with the specified amount of ram.
*/ */
getPurchasedServerCost(ram: number): number; getPurchasedServerCost(ram: number): number;
@ -5270,7 +5384,7 @@ export interface NS extends Singularity {
* } * }
* ``` * ```
* @param hostname - Host of the purchased server. * @param hostname - Host of the purchased server.
* @param ram - Amount of RAM of the purchased server. Must be a power of 2 (2, 4, 8, 16, etc.). Maximum value of 1048576 (2^20). * @param ram - Amount of RAM of the purchased server, in GB. Must be a power of 2 (2, 4, 8, 16, etc.). Maximum value of 1048576 (2^20).
* @returns The hostname of the newly purchased server. * @returns The hostname of the newly purchased server.
*/ */
purchaseServer(hostname: string, ram: number): string; purchaseServer(hostname: string, ram: number): string;
@ -5311,7 +5425,7 @@ export interface NS extends Singularity {
* Returns the maximum RAM that a purchased server can have. * Returns the maximum RAM that a purchased server can have.
* *
* @remarks RAM cost: 0.05 GB * @remarks RAM cost: 0.05 GB
* @returns Returns the maximum RAM that a purchased server can have. * @returns Returns the maximum RAM (in GB) that a purchased server can have.
*/ */
getPurchasedServerMaxRam(): number; getPurchasedServerMaxRam(): number;
@ -5320,7 +5434,7 @@ export interface NS extends Singularity {
* @remarks * @remarks
* RAM cost: 0 GB * RAM cost: 0 GB
* *
* This function can be used to either write data to a text file (.txt). * This function can be used to write data to a text file (.txt).
* *
* This function will write data to that text file. If the specified text file does not exist, * This function will write data to that text file. If the specified text file does not exist,
* then it will be created. The third argument mode, defines how the data will be written to * then it will be created. The third argument mode, defines how the data will be written to
@ -5329,7 +5443,7 @@ export interface NS extends Singularity {
* then the data will be written in append mode which means that the data will be added at the * then the data will be written in append mode which means that the data will be added at the
* end of the text file. * end of the text file.
* *
* @param handle - Port or text file that will be written to. * @param handle - Filename of the text file that will be written to.
* @param data - Data to write. * @param data - Data to write.
* @param mode - Defines the write mode. Only valid when writing to text files. * @param mode - Defines the write mode. Only valid when writing to text files.
*/ */
@ -5355,13 +5469,13 @@ export interface NS extends Singularity {
* @remarks * @remarks
* RAM cost: 0 GB * RAM cost: 0 GB
* *
* This function is used to read data from a port or from a text file (.txt). * This function is used to read data from a text file (.txt).
* *
* This function will return the data in the specified text * This function will return the data in the specified text
* file. If the text file does not exist, an empty string will be returned. * file. If the text file does not exist, an empty string will be returned.
* *
* @param handle - Port or text file to read from. * @param handle - Filename to read from.
* @returns Data in the specified text file or port. * @returns Data in the specified text file.
*/ */
read(handle: string): any; read(handle: string): any;
@ -5433,9 +5547,8 @@ export interface NS extends Singularity {
* *
* @see https://bitburner.readthedocs.io/en/latest/netscript/netscriptmisc.html#netscript-ports * @see https://bitburner.readthedocs.io/en/latest/netscript/netscriptmisc.html#netscript-ports
* @param port - Port number. Must be an integer between 1 and 20. * @param port - Port number. Must be an integer between 1 and 20.
* @returns Data in the specified port.
*/ */
getPortHandle(port: number): IPort; getPortHandle(port: number): NetscriptPort;
/** /**
* Delete a file. * Delete a file.
@ -5518,7 +5631,7 @@ export interface NS extends Singularity {
* *
* @param script - Filename of script. This is case-sensitive. * @param script - Filename of script. This is case-sensitive.
* @param host - Host of target server the script is located on. This is optional, If it is not specified then the function will se the current server as the target server. * @param host - Host of target server the script is located on. This is optional, If it is not specified then the function will se the current server as the target server.
* @returns Amount of RAM required to run the specified script on the target server, and 0 if the script does not exist. * @returns Amount of RAM (in GB) required to run the specified script on the target server, and 0 if the script does not exist.
*/ */
getScriptRam(script: string, host?: string): number; getScriptRam(script: string, host?: string): number;
@ -5630,7 +5743,7 @@ export interface NS extends Singularity {
* @param args - Formating arguments. * @param args - Formating arguments.
* @returns Formated text. * @returns Formated text.
*/ */
sprintf(format: string, ...args: string[]): string; sprintf(format: string, ...args: any[]): string;
/** /**
* Format a string with an array of arguments. * Format a string with an array of arguments.
@ -5642,7 +5755,7 @@ export interface NS extends Singularity {
* @param args - Formating arguments. * @param args - Formating arguments.
* @returns Formated text. * @returns Formated text.
*/ */
vsprintf(format: string, args: string[]): string; vsprintf(format: string, args: any[]): string;
/** /**
* Format a number * Format a number
@ -6354,3 +6467,12 @@ interface UserInterfaceTheme {
backgroundsecondary: string; backgroundsecondary: string;
button: string; button: string;
} }
/**
* Interface Styles
* @internal
*/
interface IStyleSettings {
fontFamily: string;
lineHeight: number;
}

@ -686,7 +686,9 @@ export function Root(props: IProps): React.ReactElement {
const serverScript = server.scripts.find((s) => s.filename === openScript.fileName); const serverScript = server.scripts.find((s) => s.filename === openScript.fileName);
if (serverScript === undefined) return " *"; if (serverScript === undefined) return " *";
return serverScript.code !== openScript.code ? " *" : ""; // The server code is stored with its starting & trailing whitespace removed
const openScriptFormatted = Script.formatCode(openScript.code);
return serverScript.code !== openScriptFormatted ? " *" : "";
} }
// Toolbars are roughly 112px: // Toolbars are roughly 112px:
@ -846,7 +848,7 @@ export function Root(props: IProps): React.ReactElement {
<span style={{ color: Settings.theme.primary, fontSize: "20px", textAlign: "center" }}> <span style={{ color: Settings.theme.primary, fontSize: "20px", textAlign: "center" }}>
<Typography variant="h4">No open files</Typography> <Typography variant="h4">No open files</Typography>
<Typography variant="h5"> <Typography variant="h5">
Use `nano FILENAME` in Use <code>nano FILENAME</code> in
<br /> <br />
the terminal to open files the terminal to open files
</Typography> </Typography>

@ -1,9 +1,10 @@
import { ISelfInitializer, ISelfLoading } from "../types"; import { ISelfInitializer, ISelfLoading } from "../types";
import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums"; import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums";
import { defaultTheme, ITheme } from "./Themes"; import { defaultTheme, ITheme } from "./Themes";
import { defaultStyles, IStyleSettings } from "./Styles"; import { defaultStyles } from "./Styles";
import { WordWrapOptions } from "../ScriptEditor/ui/Options"; import { WordWrapOptions } from "../ScriptEditor/ui/Options";
import { OverviewSettings } from "../ui/React/Overview"; import { OverviewSettings } from "../ui/React/Overview";
import { IStyleSettings } from "../ScriptEditor/NetscriptDefinitions";
/** /**
* Represents the default settings the player could customize. * Represents the default settings the player could customize.

@ -1,9 +1,4 @@
import React from "react"; import { IStyleSettings } from "../ScriptEditor/NetscriptDefinitions";
export interface IStyleSettings {
fontFamily: React.CSSProperties["fontFamily"];
lineHeight: React.CSSProperties["lineHeight"];
}
export const defaultStyles: IStyleSettings = { export const defaultStyles: IStyleSettings = {
lineHeight: 1.5, lineHeight: 1.5,

@ -29,7 +29,7 @@ export const TerminalHelpText: string[] = [
"lscpu Displays the number of CPU cores on the machine", "lscpu Displays the number of CPU cores on the machine",
"mem [script] [-t n] Displays the amount of RAM required to run the script", "mem [script] [-t n] Displays the amount of RAM required to run the script",
"mv [src] [dest] Move/rename a text or script file", "mv [src] [dest] Move/rename a text or script file",
"nano [file ...] Text editor - Open up and edit one or more scripts or text files", "nano [file ...] | [glob] Text editor - Open up and edit one or more scripts or text files",
"ps Display all scripts that are currently running", "ps Display all scripts that are currently running",
"rm [file] Delete a file from the server", "rm [file] Delete a file from the server",
"run [name] [-t n] [--tail] [args...] Execute a program or script", "run [name] [-t n] [--tail] [args...] Execute a program or script",
@ -40,14 +40,36 @@ export const TerminalHelpText: string[] = [
"tail [script] [args...] Displays dynamic logs for the specified script", "tail [script] [args...] Displays dynamic logs for the specified script",
"top Displays all running scripts and their RAM usage", "top Displays all running scripts and their RAM usage",
"unalias [alias name] Deletes the specified alias", "unalias [alias name] Deletes the specified alias",
"vim [file ...] Text editor - Open up and edit one or more scripts or text files in vim mode", "vim [file ...] | [glob] Text editor - Open up and edit one or more scripts or text files in vim mode",
"weaken Reduce the security of the current machine", "weaken Reduce the security of the current machine",
"wget [url] [target file] Retrieves code/text from a web server", "wget [url] [target file] Retrieves code/text from a web server",
]; ];
const TemplatedHelpTexts: IMap<(command: string) => string[]> = {
scriptEditor: (command) => {
return [
`${command} [file ...] | [glob]`,
` `,
`Opens up the specified file(s) in the Script Editor. Only scripts (.js, .ns, .script) or text files (.txt) `,
`can be edited using the Script Editor. If a file does not exist a new one will be created`,
` `,
`If provided a glob as the only argument, ${command} can spider directories and open all matching `,
`files at once. ${command} cannot create files using globs, so your scripts must already exist.`,
` `,
`Examples:`,
` `,
`${command} test.js`,
`${command} test.js test2.js`,
` `,
`${command} test.*`,
`${command} /my-dir/*.js`,
]
}
}
export const HelpTexts: IMap<string[]> = { export const HelpTexts: IMap<string[]> = {
alias: [ alias: [
'alias [-g] [name="value"] ', 'Usage: alias [-g] [name="value"] ',
" ", " ",
"Create or display aliases. An alias enables a replacement of a word with another string. ", "Create or display aliases. An alias enables a replacement of a word with another string. ",
"It can be used to abbreviate a commonly used command, or commonly used parts of a command. The NAME ", "It can be used to abbreviate a commonly used command, or commonly used parts of a command. The NAME ",
@ -80,15 +102,16 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
], ],
analyze: [ analyze: [
"analyze", "Usage: analyze",
" ", " ",
"Prints details and statistics about the current server. The information that is printed includes basic ", "Prints details and statistics about the current server. The information that is printed includes basic ",
"server details such as the hostname, whether the player has root access, what ports are opened/closed, and also ", "server details such as the hostname, whether the player has root access, what ports are opened/closed, and also ",
"hacking-related information such as an estimated chance to successfully hack, an estimate of how much money is ", "hacking-related information such as an estimated chance to successfully hack, an estimate of how much money is ",
"available on the server, etc.", "available on the server, etc.",
" ",
], ],
backdoor: [ backdoor: [
"backdoor", "Usage: backdoor",
" ", " ",
"Install a backdoor on the current machine, grants a secret bonus depending on the machine.", "Install a backdoor on the current machine, grants a secret bonus depending on the machine.",
" ", " ",
@ -96,7 +119,7 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
], ],
buy: [ buy: [
"buy [-l / -a / program]", "Usage: buy [-l / -a / program]",
" ", " ",
"Purchase a program through the Dark Web. Requires a TOR router to use.", "Purchase a program through the Dark Web. Requires a TOR router to use.",
" ", " ",
@ -106,9 +129,10 @@ export const HelpTexts: IMap<string[]> = {
"If this command is ran with the '-a' flag, it will attempt to purchase all unowned programs.", "If this command is ran with the '-a' flag, it will attempt to purchase all unowned programs.",
" ", " ",
"Otherwise, the name of the program must be passed in as a parameter. This name is NOT case-sensitive.", "Otherwise, the name of the program must be passed in as a parameter. This name is NOT case-sensitive.",
" ",
], ],
cat: [ cat: [
"cat [file]", "Usage: cat [file]",
" ", " ",
"Display message (.msg), literature (.lit), or text (.txt) files. Examples:", "Display message (.msg), literature (.lit), or text (.txt) files. Examples:",
" ", " ",
@ -117,9 +141,10 @@ export const HelpTexts: IMap<string[]> = {
" cat foo.lit", " cat foo.lit",
" ", " ",
" cat servers.txt", " cat servers.txt",
" ",
], ],
cd: [ cd: [
"cd [dir]", "Usage: cd [dir]",
" ", " ",
"Change to the specified directory. Note that this works even for directories that don't exist. If you ", "Change to the specified directory. Note that this works even for directories that don't exist. If you ",
"change to a directory that does not exist, it will not be 'created'. Examples:", "change to a directory that does not exist, it will not be 'created'. Examples:",
@ -129,9 +154,10 @@ export const HelpTexts: IMap<string[]> = {
" cd /logs", " cd /logs",
" ", " ",
" cd ../", " cd ../",
" ",
], ],
check: [ check: [
"check [script name] [args...]", "Usage: check [script name] [args...]",
" ", " ",
"Print the logs of the script specified by the script name and arguments to the Terminal. Each argument must be separated by ", "Print the logs of the script specified by the script name and arguments to the Terminal. Each argument must be separated by ",
"a space. Remember that a running script is uniquely ", "a space. Remember that a running script is uniquely ",
@ -142,29 +168,33 @@ export const HelpTexts: IMap<string[]> = {
"Then to run the 'check' command on this script you would have to pass the same arguments in: ", "Then to run the 'check' command on this script you would have to pass the same arguments in: ",
" ", " ",
" check foo.script 1 2 foodnstuff", " check foo.script 1 2 foodnstuff",
" ",
], ],
clear: [ clear: [
"clear", "Usage: clear",
" ", " ",
"Clear the Terminal screen, deleting all of the text. Note that this does not delete the user's command history, so using the up ", "Clear the Terminal screen, deleting all of the text. Note that this does not delete the user's command history, so using the up ",
"and down arrow keys is still valid. Also note that this is permanent and there is no way to undo this. Synonymous with 'cls' command", "and down arrow keys is still valid. Also note that this is permanent and there is no way to undo this. Synonymous with 'cls' command",
" ",
], ],
cls: [ cls: [
"cls", "Usage: cls",
" ", " ",
"Clear the Terminal screen, deleting all of the text. Note that this does not delete the user's command history, so using the up ", "Clear the Terminal screen, deleting all of the text. Note that this does not delete the user's command history, so using the up ",
"and down arrow keys is still valid. Also note that this is permanent and there is no way to undo this. Synonymous with 'clear' command", "and down arrow keys is still valid. Also note that this is permanent and there is no way to undo this. Synonymous with 'clear' command",
" ",
], ],
connect: [ connect: [
"connect [hostname]", "Usage: connect [hostname]",
" ", " ",
"Connect to a remote server. The hostname or IP address of the remote server must be given as the argument ", "Connect to a remote server. The hostname or IP address of the remote server must be given as the argument ",
"to this command. Note that only servers that are immediately adjacent to the current server in the network can be connected to. To ", "to this command. Note that only servers that are immediately adjacent to the current server in the network can be connected to. To ",
"see which servers can be connected to, use the 'scan' command.", "see which servers can be connected to, use the 'scan' command.",
" ",
], ],
cp: ["cp [src] [dst]", " ", "Copy a file on this server. To copy a file to another server use scp."], cp: ["Usage: cp [src] [dst]", " ", "Copy a file on this server. To copy a file to another server use scp.", " "],
download: [ download: [
"download [script/text file]", "Usage: download [script/text file]",
" ", " ",
"Downloads a script or text file to your computer (like your real life computer).", "Downloads a script or text file to your computer (like your real life computer).",
" ", " ",
@ -178,7 +208,7 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
], ],
expr: [ expr: [
"expr [mathematical expression]", "Usage: expr [mathematical expression]",
" ", " ",
"Evaluate a simple mathematical expression. Supports native JavaScript operators:", "Evaluate a simple mathematical expression. Supports native JavaScript operators:",
" ", " ",
@ -189,27 +219,30 @@ export const HelpTexts: IMap<string[]> = {
" expr 25 * 2 ** 10", " expr 25 * 2 ** 10",
" ", " ",
"Note that letters (non-digits) are not allowed and will be removed from the input.", "Note that letters (non-digits) are not allowed and will be removed from the input.",
" ",
], ],
free: [ free: [
"free", "Usage: free",
" ", " ",
"Displays the memory usage on the current machine. Print the amount of RAM that is available on the current server as well as ", "Displays the memory usage on the current machine. Print the amount of RAM that is available on the current server as well as ",
"how much of it is being used.", "how much of it is being used.",
" ",
], ],
grow: [ grow: [
"grow", "Usage: grow",
" ", " ",
"Spoof transactions in the current server. Increasing the money available by hacking. Requires root access.", "Spoof transactions in the current server. Increasing the money available by hacking. Requires root access.",
"See the wiki page for hacking mechanics.", "See the wiki page for hacking mechanics.",
" ",
], ],
hack: [ hack: [
"hack", "Usage: hack",
" ", " ",
"Attempt to hack the current server. Requires root access in order to be run. See the wiki page for hacking mechanics", "Attempt to hack the current server. Requires root access in order to be run. See the wiki page for hacking mechanics",
" ", " ",
], ],
help: [ help: [
"help [command]", "Usage: help [command]",
" ", " ",
"Display Terminal help information. Without arguments, 'help' prints a list of all valid Terminal commands and a brief ", "Display Terminal help information. Without arguments, 'help' prints a list of all valid Terminal commands and a brief ",
"description of their functionality. You can also pass the name of a Terminal command as an argument to 'help' to print ", "description of their functionality. You can also pass the name of a Terminal command as an argument to 'help' to print ",
@ -218,15 +251,14 @@ export const HelpTexts: IMap<string[]> = {
" help alias", " help alias",
" ", " ",
" help scan-analyze", " help scan-analyze",
" ",
], ],
home: [ home: [
"home" + "Connect to your home computer. This will work no matter what server you are currently connected to.", "Usage: home", " ", "Connect to your home computer. This will work no matter what server you are currently connected to.", " ",
], ],
hostname: ["hostname", " ", "Prints the hostname of the current server"], hostname: ["Usage: hostname", " ", "Prints the hostname of the current server", " "],
kill: [ kill: [
"kill [script name] [args...]", "Usage: kill [script name] [args...] or kill [pid",
" ",
"kill [pid]",
" ", " ",
"Kill the script specified by the script name and arguments OR by its PID.", "Kill the script specified by the script name and arguments OR by its PID.",
" ", " ",
@ -242,17 +274,19 @@ export const HelpTexts: IMap<string[]> = {
" kill foo.script 1 sigma-cosmetics", " kill foo.script 1 sigma-cosmetics",
" ", " ",
"If you are killing the script using its PID, then the PID argument must be numeric", "If you are killing the script using its PID, then the PID argument must be numeric",
" ",
], ],
killall: [ killall: [
"killall", "Usage: killall",
" ", " ",
"Kills all scripts on the current server. ", "Kills all scripts on the current server. ",
"Note that after the 'kill' command is issued for a script, it may take a while for the script to actually stop running. ", "Note that after the 'kill' command is issued for a script, it may take a while for the script to actually stop running. ",
"This will happen if the script is in the middle of a command such as grow() or weaken() that takes time to execute. ", "This will happen if the script is in the middle of a command such as grow() or weaken() that takes time to execute. ",
"The script will not be stopped/killed until after that time has elapsed.", "The script will not be stopped/killed until after that time has elapsed.",
" ",
], ],
ls: [ ls: [
"ls [dir] [| grep pattern]", "Usage: ls [dir] [| grep pattern]",
" ", " ",
"The ls command, with no arguments, prints all files and directories on the current server's directory to the Terminal screen. ", "The ls command, with no arguments, prints all files and directories on the current server's directory to the Terminal screen. ",
"The files will be displayed in alphabetical order. ", "The files will be displayed in alphabetical order. ",
@ -274,11 +308,12 @@ export const HelpTexts: IMap<string[]> = {
"List all files with the word 'purchase' in the filename, in the 'scripts' directory:", "List all files with the word 'purchase' in the filename, in the 'scripts' directory:",
" ", " ",
" ls scripts | grep purchase", " ls scripts | grep purchase",
" ",
], ],
lscpu: ["lscpu", " ", "Prints the number of CPU Cores the current server has"], lscpu: ["Usage: lscpu", " ", "Prints the number of CPU Cores the current server has", " "],
mem: [ mem: [
"mem [script name] [-t num_threads]", "Usage: mem [script name] [-t num_threads]",
" ", " ",
"Displays the amount of RAM needed to run the specified script with a single thread. The command can also be used to print ", "Displays the amount of RAM needed to run the specified script with a single thread. The command can also be used to print ",
"the amount of RAM needed to run a script with multiple threads using the '-t' flag. If the '-t' flag is specified, then ", "the amount of RAM needed to run a script with multiple threads using the '-t' flag. If the '-t' flag is specified, then ",
@ -290,9 +325,10 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
"The first example above will print the amount of RAM needed to run 'foo.script' with a single thread. The second example ", "The first example above will print the amount of RAM needed to run 'foo.script' with a single thread. The second example ",
"above will print the amount of RAM needed to run 'foo.script' with 50 threads.", "above will print the amount of RAM needed to run 'foo.script' with 50 threads.",
" ",
], ],
mv: [ mv: [
"mv [src] [dest]", "Usage: mv [src] [dest]",
" ", " ",
"Move the source file to the specified destination. This can also be used to rename files. ", "Move the source file to the specified destination. This can also be used to rename files. ",
"This command only works for scripts and text files (.txt). This command CANNOT be used to ", "This command only works for scripts and text files (.txt). This command CANNOT be used to ",
@ -305,25 +341,20 @@ export const HelpTexts: IMap<string[]> = {
" mv hacking-controller.script scripts/hacking-controller.script", " mv hacking-controller.script scripts/hacking-controller.script",
" ", " ",
" mv myScript.js myOldScript.js", " mv myScript.js myOldScript.js",
],
nano: [
"nano [file ...]",
" ", " ",
"Opens up the specified file(s) in the Text Editor. Only scripts (.script) or text files (.txt) can be ",
"edited using the Text Editor. If the file does not already exist, then a new, empty one ",
"will be created",
], ],
ps: ["ps", " ", "Prints all scripts that are running on the current server"], nano: TemplatedHelpTexts.scriptEditor('nano'),
ps: ["Usage: ps", " ", "Prints all scripts that are running on the current server", " "],
rm: [ rm: [
"rm [file]", "Usage: rm [file]",
" ", " ",
"Removes the specified file from the current server. A file can be a script, a program, or a message file. ", "Removes the specified file from the current server. A file can be a script, a program, or a message file. ",
" ", " ",
"WARNING: This is permanent and cannot be undone", "WARNING: This is permanent and cannot be undone",
" ",
], ],
run: [ run: [
"run [file name] [-t] [num threads] [args...]", "Usage: run [file name] [-t] [num threads] [args...]",
" ", " ",
"Execute a program, script or coding contract.", "Execute a program, script or coding contract.",
" ", " ",
@ -338,13 +369,14 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
], ],
scan: [ scan: [
"scan", "Usage: scan",
" ", " ",
"Prints all immediately-available network connection. This will print a list of all servers that you can currently connect ", "Prints all immediately-available network connection. This will print a list of all servers that you can currently connect ",
"to using the 'connect' Terminal command.", "to using the 'connect' Terminal command.",
" ",
], ],
"scan-analyze": [ "scan-analyze": [
"scan-analyze [depth] [-a]", "Usage: scan-analyze [depth] [-a]",
" ", " ",
"Prints detailed information about all servers up to [depth] nodes away on the network. Calling ", "Prints detailed information about all servers up to [depth] nodes away on the network. Calling ",
"'scan-analyze 1' will display information for the same servers that are shown by the 'scan' Terminal ", "'scan-analyze 1' will display information for the same servers that are shown by the 'scan' Terminal ",
@ -360,9 +392,10 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
"By default, this command will not display servers that you have purchased. However, you can pass in the ", "By default, this command will not display servers that you have purchased. However, you can pass in the ",
"-a flag at the end of the command if you would like to enable that.", "-a flag at the end of the command if you would like to enable that.",
" ",
], ],
scp: [ scp: [
"scp [filename ...] [target server]", "Usage: scp [filename ...] [target server]",
" ", " ",
"Copies the specified file(s) from the current server to the target server. ", "Copies the specified file(s) from the current server to the target server. ",
"This command only works for script files (.script or .js extension), literature files (.lit extension), ", "This command only works for script files (.script or .js extension), literature files (.lit extension), ",
@ -374,10 +407,10 @@ export const HelpTexts: IMap<string[]> = {
" scp foo.script bar.script n00dles", " scp foo.script bar.script n00dles",
" ", " ",
], ],
sudov: ["sudov", " ", "Prints whether or not you have root access to the current machine"], sudov: ["Usage: sudov", " ", "Prints whether or not you have root access to the current machine", " "],
tail: [ tail: [
"tail [script name] [args...]", "Usage: tail [script name] [args...]",
" ", " ",
"Displays dynamic logs for the script specified by the script name and arguments. Each argument must be separated ", "Displays dynamic logs for the script specified by the script name and arguments. Each argument must be separated ",
"by a space. Remember that a running script is uniquely identified by both its name and the arguments that were used ", "by a space. Remember that a running script is uniquely identified by both its name and the arguments that were used ",
@ -388,15 +421,17 @@ export const HelpTexts: IMap<string[]> = {
"Then in order to check its logs with 'tail' the same arguments must be used: ", "Then in order to check its logs with 'tail' the same arguments must be used: ",
" ", " ",
" tail foo.script 10 50000", " tail foo.script 10 50000",
" ",
], ],
top: [ top: [
"top", "Usage: top",
" ", " ",
"Prints a list of all scripts running on the current server as well as their thread count and how much ", "Prints a list of all scripts running on the current server as well as their thread count and how much ",
"RAM they are using in total.", "RAM they are using in total.",
" ",
], ],
unalias: [ unalias: [
"unalias [alias name]", "Usage: unalias [alias name]",
" ", " ",
"Deletes the specified alias. Note that the double quotation marks are required. ", "Deletes the specified alias. Note that the double quotation marks are required. ",
" ", " ",
@ -409,22 +444,18 @@ export const HelpTexts: IMap<string[]> = {
" unalias r", " unalias r",
" ", " ",
"It is not necessary to differentiate between global and non-global aliases when using 'unalias'", "It is not necessary to differentiate between global and non-global aliases when using 'unalias'",
],
vim: [
"vim [file ...]",
" ", " ",
"Opens up the specified file(s) in the Text Editor in vim mode. Only scripts (.script) or text files (.txt) can be ",
"edited using the Text Editor. If the file does not already exist, then a new, empty one ",
"will be created",
], ],
vim: TemplatedHelpTexts.scriptEditor('vim'),
weaken: [ weaken: [
"weaken", "Usage: weaken",
" ", " ",
"Reduces the security level of the current server. Decreasing the time it takes for all operations on this server.", "Reduces the security level of the current server. Decreasing the time it takes for all operations on this server.",
"Requires root access. See the wiki page for hacking mechanics.", "Requires root access. See the wiki page for hacking mechanics.",
" ",
], ],
wget: [ wget: [
"wget [url] [target file]", "Usage: wget [url] [target file]",
" ", " ",
"Retrieves data from a URL and downloads it to a file on the current server. The data can only ", "Retrieves data from a URL and downloads it to a file on the current server. The data can only ",
"be downloaded to a script (.script, .ns, .js) or a text file (.txt). If the file already exists, ", "be downloaded to a script (.script, .ns, .js) or a text file (.txt). If the file already exists, ",
@ -434,5 +465,6 @@ export const HelpTexts: IMap<string[]> = {
"cross-origin resource sharing (CORS). Example:", "cross-origin resource sharing (CORS). Example:",
" ", " ",
" wget https://raw.githubusercontent.com/danielyxie/bitburner/master/README.md game_readme.txt", " wget https://raw.githubusercontent.com/danielyxie/bitburner/master/README.md game_readme.txt",
" ",
], ],
}; };

@ -1,9 +1,12 @@
import { ITerminal } from "../../ITerminal"; import { ITerminal } from "../../ITerminal";
import { removeLeadingSlash, removeTrailingSlash } from '../../DirectoryHelpers'
import { IRouter, ScriptEditorRouteOptions } from "../../../ui/Router"; import { IRouter, ScriptEditorRouteOptions } from "../../../ui/Router";
import { IPlayer } from "../../../PersonObjects/IPlayer"; import { IPlayer } from "../../../PersonObjects/IPlayer";
import { BaseServer } from "../../../Server/BaseServer"; import { BaseServer } from "../../../Server/BaseServer";
import { isScriptFilename } from "../../../Script/isScriptFilename"; import { isScriptFilename } from "../../../Script/isScriptFilename";
import { CursorPositions } from "../../../ScriptEditor/CursorPositions"; import { CursorPositions } from "../../../ScriptEditor/CursorPositions";
import { Script } from "../../../Script/Script";
import { isEmpty } from "lodash";
interface EditorParameters { interface EditorParameters {
terminal: ITerminal; terminal: ITerminal;
@ -22,6 +25,74 @@ export async function main(ns) {
}`; }`;
interface ISimpleScriptGlob {
glob: string;
preGlob: string;
postGlob: string;
globError: string;
globMatches: string[];
globAgainst: Script[];
}
function containsSimpleGlob(filename: string): boolean {
return filename.includes("*");
}
function detectSimpleScriptGlob(
args: EditorParameters["args"],
player: IPlayer,
terminal: ITerminal,
): ISimpleScriptGlob | null {
if (args.length == 1 && containsSimpleGlob(`${args[0]}`)) {
const filename = `${args[0]}`;
const scripts = player.getCurrentServer().scripts;
const parsedGlob = parseSimpleScriptGlob(filename, scripts, terminal);
return parsedGlob;
}
return null;
}
function parseSimpleScriptGlob(globString: string, globDatabase: Script[], terminal: ITerminal): ISimpleScriptGlob {
const parsedGlob: ISimpleScriptGlob = {
glob: globString,
preGlob: "",
postGlob: "",
globError: "",
globMatches: [],
globAgainst: globDatabase,
};
// Ensure deep globs are minified to simple globs, which act as deep globs in this impl
globString = globString.replace("**", "*");
// Ensure only a single glob is present
if (globString.split("").filter((c) => c == "*").length !== 1) {
parsedGlob.globError = "Only a single glob is supported per command.\nexample: `nano my-dir/*.js`";
return parsedGlob;
}
// Split arg around glob, normalize preGlob path
[parsedGlob.preGlob, parsedGlob.postGlob] = globString.split("*");
parsedGlob.preGlob = removeLeadingSlash(parsedGlob.preGlob);
// Add CWD to preGlob path
const cwd = removeTrailingSlash(terminal.cwd())
parsedGlob.preGlob = `${cwd}/${parsedGlob.preGlob}`
// For every script on the current server, filter matched scripts per glob values & persist
globDatabase.forEach((script) => {
const filename = script.filename.startsWith('/') ? script.filename : `/${script.filename}`
if (filename.startsWith(parsedGlob.preGlob) && filename.endsWith(parsedGlob.postGlob)) {
parsedGlob.globMatches.push(filename);
}
});
// Rebuild glob for potential error reporting
parsedGlob.glob = `${parsedGlob.preGlob}*${parsedGlob.postGlob}`
return parsedGlob;
}
export function commonEditor( export function commonEditor(
command: string, command: string,
{ terminal, router, player, args }: EditorParameters, { terminal, router, player, args }: EditorParameters,
@ -32,8 +103,15 @@ export function commonEditor(
return; return;
} }
let filesToLoadOrCreate = args;
try { try {
const files = args.map((arg) => { const globSearch = detectSimpleScriptGlob(args, player, terminal);
if (globSearch) {
if (isEmpty(globSearch.globError) === false) throw new Error(globSearch.globError);
filesToLoadOrCreate = globSearch.globMatches;
}
const files = filesToLoadOrCreate.map((arg) => {
const filename = `${arg}`; const filename = `${arg}`;
if (isScriptFilename(filename)) { if (isScriptFilename(filename)) {
@ -55,12 +133,18 @@ export function commonEditor(
if (filename.endsWith(".txt")) { if (filename.endsWith(".txt")) {
const filepath = terminal.getFilepath(filename); const filepath = terminal.getFilepath(filename);
const txt = terminal.getTextFile(player, filename); const txt = terminal.getTextFile(player, filename);
return [filepath, txt == null ? "" : txt.text]; return [filepath, txt === null ? "" : txt.text];
} }
throw new Error(`Invalid file. Only scripts (.script, .ns, .js), or text files (.txt) can be edited with ${command}`); throw new Error(
`Invalid file. Only scripts (.script, .ns, .js), or text files (.txt) can be edited with ${command}`,
);
}); });
if (globSearch && files.length === 0) {
throw new Error(`Could not find any valid files to open with ${command} using glob: \`${globSearch.glob}\``)
}
router.toScriptEditor(Object.fromEntries(files), scriptEditorRouteOptions); router.toScriptEditor(Object.fromEntries(files), scriptEditorRouteOptions);
} catch (e) { } catch (e) {
terminal.error(`${e}`); terminal.error(`${e}`);

@ -1,9 +1,13 @@
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 React from "react";
import { ITerminal } from "../ITerminal";
import { IRouter } from "../../ui/Router";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { getFirstParentDirectory, isValidDirectoryPath, evaluateDirectoryPath } from "../../Terminal/DirectoryHelpers"; import { evaluateDirectoryPath, getFirstParentDirectory, isValidDirectoryPath } from "../../Terminal/DirectoryHelpers";
import { IRouter } from "../../ui/Router";
import { ITerminal } from "../ITerminal";
export function ls( export function ls(
terminal: ITerminal, terminal: ITerminal,
@ -113,7 +117,55 @@ export function ls(
allMessages.sort(); allMessages.sort();
folders.sort(); folders.sort();
function postSegments(segments: string[], style?: any): void { interface ClickableScriptRowProps {
row: string;
prefix: string;
hostname: string;
}
function ClickableScriptRow({ row, prefix, hostname }: ClickableScriptRowProps): React.ReactElement {
const classes = makeStyles((theme: Theme) =>
createStyles({
scriptLinksWrap: {
display: "flex",
color: theme.palette.warning.main,
},
scriptLink: {
cursor: "pointer",
textDecorationLine: "underline",
paddingRight: "1.15em",
"&:last-child": { padding: 0 },
},
}),
)();
const rowSplit = row
.split(" ")
.map((x) => x.trim())
.filter((x) => !!x);
function onScriptLinkClick(filename: string): void {
if (player.getCurrentServer().hostname !== hostname) {
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}`);
const code = toString(terminal.getScript(player, filepath)?.code);
router.toScriptEditor({ [filepath]: code });
}
return (
<span className={classes.scriptLinksWrap}>
{rowSplit.map((rowItem) => (
<span key={rowItem} className={classes.scriptLink} onClick={() => onScriptLinkClick(rowItem)}>
{rowItem}
</span>
))}
</span>
);
}
function postSegments(segments: string[], style?: any, linked?: boolean): void {
const maxLength = Math.max(...segments.map((s) => s.length)) + 1; const maxLength = Math.max(...segments.map((s) => s.length)) + 1;
const filesPerRow = Math.floor(80 / maxLength); const filesPerRow = Math.floor(80 / maxLength);
for (let i = 0; i < segments.length; i++) { for (let i = 0; i < segments.length; i++) {
@ -127,11 +179,15 @@ export function ls(
i--; i--;
if (!style) { if (!style) {
terminal.print(row); terminal.print(row);
} else {
if (linked) {
terminal.printRaw(<ClickableScriptRow row={row} prefix={prefix} hostname={server.hostname} />);
} else { } else {
terminal.printRaw(<span style={style}>{row}</span>); terminal.printRaw(<span style={style}>{row}</span>);
} }
} }
} }
}
const groups = [ const groups = [
{ segments: folders, style: { color: "cyan" } }, { segments: folders, style: { color: "cyan" } },
@ -139,9 +195,9 @@ export function ls(
{ segments: allTextFiles }, { segments: allTextFiles },
{ segments: allPrograms }, { segments: allPrograms },
{ segments: allContracts }, { segments: allContracts },
{ segments: allScripts, style: { color: "yellow", fontStyle: "bold" } }, { segments: allScripts, style: { color: "yellow", fontStyle: "bold" }, linked: true },
].filter((g) => g.segments.length > 0); ].filter((g) => g.segments.length > 0);
for (let i = 0; i < groups.length; i++) { for (let i = 0; i < groups.length; i++) {
postSegments(groups[i].segments, groups[i].style); postSegments(groups[i].segments, groups[i].style, groups[i].linked);
} }
} }

@ -6,6 +6,7 @@ import { DarkWebItems } from "../DarkWeb/DarkWebItems";
import { IPlayer } from "../PersonObjects/IPlayer"; import { IPlayer } from "../PersonObjects/IPlayer";
import { GetServer, GetAllServers } from "../Server/AllServers"; import { GetServer, GetAllServers } from "../Server/AllServers";
import { ParseCommand, ParseCommands } from "./Parser"; import { ParseCommand, ParseCommands } from "./Parser";
import { HelpTexts } from "./HelpText";
import { isScriptFilename } from "../Script/isScriptFilename"; import { isScriptFilename } from "../Script/isScriptFilename";
import { compile } from "../NetscriptJSEvaluator"; import { compile } from "../NetscriptJSEvaluator";
import { Flags } from "../NetscriptFunctions/Flags"; import { Flags } from "../NetscriptFunctions/Flags";
@ -178,31 +179,8 @@ export async function determineAllPossibilitiesForTabCompletion(
return input.startsWith(t_cmd); return input.startsWith(t_cmd);
} }
/**
* If the command starts with './' and the index == -1, then the user
* has input ./partialexecutablename so autocomplete the script or program.
* Put './' in front of each script/executable
*/
if (input.startsWith("./") && index == -1) {
//All programs and scripts
for (let i = 0; i < currServ.scripts.length; ++i) {
allPos.push("./" + currServ.scripts[i].filename);
}
//Programs are on home computer
for (let i = 0; i < homeComputer.programs.length; ++i) {
allPos.push("./" + homeComputer.programs[i]);
}
//Contracts on current server
for (let i = 0; i < currServ.contracts.length; ++i) {
allPos.push("./" + currServ.contracts[i].fn)
}
return allPos;
}
// Autocomplete the command // Autocomplete the command
if (index === -1) { if (index === -1 && !input.startsWith('./')) {
return commands.concat(Object.keys(Aliases)).concat(Object.keys(GlobalAliases)); return commands.concat(Object.keys(Aliases)).concat(Object.keys(GlobalAliases));
} }
@ -292,14 +270,21 @@ export async function determineAllPossibilitiesForTabCompletion(
async function scriptAutocomplete(): Promise<string[] | undefined> { async function scriptAutocomplete(): Promise<string[] | undefined> {
if (!isCommand("run") && !isCommand("tail") && !isCommand("kill") && !input.startsWith("./")) return; if (!isCommand("run") && !isCommand("tail") && !isCommand("kill") && !input.startsWith("./")) return;
if (input.startsWith("./")) input = "run " + input.slice(2); let copy = input;
const commands = ParseCommands(input); if (input.startsWith("./")) copy = "run " + input.slice(2);
const commands = ParseCommands(copy);
if (commands.length === 0) return; if (commands.length === 0) return;
const command = ParseCommand(commands[commands.length - 1]); const command = ParseCommand(commands[commands.length - 1]);
const filename = command[1] + ""; const filename = command[1] + "";
if (!isScriptFilename(filename)) return; // Not a script. if (!isScriptFilename(filename)) return; // Not a script.
if (filename.endsWith(".script")) return; // Doesn't work with ns1. if (filename.endsWith(".script")) return; // Doesn't work with ns1.
const script = currServ.scripts.find((script) => script.filename === filename || script.filename === '/' + filename); // 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.find((script) => {
const fn = filename.replace(/^\.\//g, '');
return (processFilepath(script.filename) === fn || script.filename === '/' + fn);
})
if (!script) return; // Doesn't exist. if (!script) return; // Doesn't exist.
if (!script.module) { if (!script.module) {
await compile(p, script, currServ.scripts); await compile(p, script, currServ.scripts);
@ -341,6 +326,35 @@ export async function determineAllPossibilitiesForTabCompletion(
const pos = await scriptAutocomplete(); const pos = await scriptAutocomplete();
if (pos) return pos; 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 script of currServ.scripts) {
const res = processFilepath(script.filename);
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")) { if (isCommand("run")) {
addAllScripts(); addAllScripts();
addAllPrograms(); addAllPrograms();
@ -383,5 +397,11 @@ export async function determineAllPossibilitiesForTabCompletion(
addAllDirectories(); 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; return allPos;
} }

@ -95,20 +95,42 @@ export function TerminalRoot({ terminal, router, player }: IProps): React.ReactE
setKey((key) => key + 1); setKey((key) => key + 1);
} }
useEffect(() => TerminalEvents.subscribe(_.debounce(async () => rerender(), 25, { maxWait: 50 })), []); useEffect(() => {
useEffect(() => TerminalClearEvents.subscribe(_.debounce(async () => clear(), 25, { maxWait: 50 })), []); const debounced = _.debounce(async () => rerender(), 25, { maxWait: 50 });
const unsubscribe = TerminalEvents.subscribe(debounced);
return () => {
debounced.cancel();
unsubscribe();
}
}, []);
function doScroll(): void { useEffect(() => {
const debounced = _.debounce(async () => clear(), 25, { maxWait: 50 });
const unsubscribe = TerminalClearEvents.subscribe(debounced);
return () => {
debounced.cancel();
unsubscribe();
}
}, []);
function doScroll(): number | undefined {
const hook = scrollHook.current; const hook = scrollHook.current;
if (hook !== null) { if (hook !== null) {
setTimeout(() => hook.scrollIntoView(true), 50); return window.setTimeout(() => hook.scrollIntoView(true), 50);
} }
} }
doScroll(); doScroll();
useEffect(() => { useEffect(() => {
setTimeout(doScroll, 50); let scrollId: number;
const id = setTimeout(() => {
scrollId = doScroll() ?? 0;
}, 50);
return () => {
clearTimeout(id);
clearTimeout(scrollId);
}
}, []); }, []);
function lineClass(s: string): string { function lineClass(s: string): string {

@ -279,7 +279,7 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [
"i to i+n.", "i to i+n.",
"\n\nAssuming you are initially positioned", "\n\nAssuming you are initially positioned",
"at the start of the array, determine whether you are", "at the start of the array, determine whether you are",
"able to reach the last index exactly.\n\n", "able to reach the last index.\n\n",
"Your answer should be submitted as 1 or 0, representing true and false respectively", "Your answer should be submitted as 1 or 0, representing true and false respectively",
].join(" "); ].join(" ");
}, },

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { cloneDeep } from "lodash";
import { IPlayer } from "../PersonObjects/IPlayer"; import { IPlayer } from "../PersonObjects/IPlayer";
import { IEngine } from "../IEngine"; import { IEngine } from "../IEngine";
import { ITerminal } from "../Terminal/ITerminal"; import { ITerminal } from "../Terminal/ITerminal";
@ -87,11 +87,6 @@ interface IProps {
engine: IEngine; engine: IEngine;
} }
interface PageHistoryEntry {
page: Page;
args: any[];
}
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
root: { root: {
@ -111,15 +106,6 @@ export let Router: IRouter = {
page: () => { page: () => {
throw new Error("Router called before initialization"); throw new Error("Router called before initialization");
}, },
previousPage: () => {
throw new Error("Router called before initialization");
},
clearHistory: () => {
throw new Error("Router called before initialization");
},
toPreviousPage: (): boolean => {
throw new Error("Router called before initialization");
},
toActiveScripts: () => { toActiveScripts: () => {
throw new Error("Router called before initialization"); throw new Error("Router called before initialization");
}, },
@ -218,9 +204,7 @@ function determineStartPage(player: IPlayer): Page {
export function GameRoot({ player, engine, terminal }: IProps): React.ReactElement { export function GameRoot({ player, engine, terminal }: IProps): React.ReactElement {
const classes = useStyles(); const classes = useStyles();
const [{ files, vim }, setEditorOptions] = useState({ files: {}, vim: false }); const [{ files, vim }, setEditorOptions] = useState({ files: {}, vim: false });
const startPage = determineStartPage(player); const [page, setPage] = useState(determineStartPage(player));
const [page, setPage] = useState(startPage);
const [pageHistory, setPageHistory] = useState<PageHistoryEntry[]>([{ page: startPage, args: [] }]);
const setRerender = useState(0)[1]; const setRerender = useState(0)[1];
const [faction, setFaction] = useState<Faction>( const [faction, setFaction] = useState<Faction>(
player.currentWorkFactionName ? Factions[player.currentWorkFactionName] : (undefined as unknown as Faction), player.currentWorkFactionName ? Factions[player.currentWorkFactionName] : (undefined as unknown as Faction),
@ -256,131 +240,70 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
setTimeout(() => htmlLocation.reload(), 2000); setTimeout(() => htmlLocation.reload(), 2000);
} }
function setCurrentPage(page: Page, ...args: any): void {
const history = [
{ page, args: cloneDeep(args) },
...pageHistory
].slice(0, 20);
setPageHistory(history)
setPage(page)
}
function goBack(fallback: (...args: any[]) => void): void {
const [ , previousPage ] = pageHistory;
if (previousPage) {
const handler = pageToRouterMap[previousPage?.page];
handler(...previousPage.args);
} else {
if (fallback) fallback();
}
const [ , ...history] = pageHistory;
setPageHistory(cloneDeep(history));
}
const pageToRouterMap: { [key: number] : (...args: any[]) => void } = {
[Page.ActiveScripts]: Router.toActiveScripts,
[Page.Augmentations]: Router.toAugmentations,
[Page.Bladeburner]: Router.toBladeburner,
[Page.Stats]: Router.toStats,
[Page.Corporation]: Router.toCorporation,
[Page.CreateProgram]: Router.toCreateProgram,
[Page.DevMenu]: Router.toDevMenu,
[Page.Faction]: Router.toFaction,
[Page.Factions]: Router.toFactions,
[Page.Options]: Router.toGameOptions,
[Page.Gang]: Router.toGang,
[Page.Hacknet]: Router.toHacknetNodes,
[Page.Milestones]: Router.toMilestones,
[Page.Resleeves]: Router.toResleeves,
[Page.ScriptEditor]: Router.toScriptEditor,
[Page.Sleeves]: Router.toSleeves,
[Page.StockMarket]: Router.toStockMarket,
[Page.Terminal]: Router.toTerminal,
[Page.Tutorial]: Router.toTutorial,
[Page.Job]: Router.toJob,
[Page.City]: Router.toCity,
[Page.Travel]: Router.toTravel,
[Page.BitVerse]: Router.toBitVerse,
[Page.Infiltration]: Router.toInfiltration,
[Page.Work]: Router.toWork,
[Page.BladeburnerCinematic]: Router.toBladeburnerCinematic,
[Page.Location]: Router.toLocation,
[Page.StaneksGift]: Router.toStaneksGift,
[Page.Achievements]: Router.toAchievements,
}
Router = { Router = {
page: () => page, page: () => page,
previousPage: () => { toActiveScripts: () => setPage(Page.ActiveScripts),
const [ , previousPage] = pageHistory; toAugmentations: () => setPage(Page.Augmentations),
return previousPage?.page ?? -1; toBladeburner: () => setPage(Page.Bladeburner),
}, toStats: () => setPage(Page.Stats),
clearHistory: () => setPageHistory([]), toCorporation: () => setPage(Page.Corporation),
toPreviousPage: goBack, toCreateProgram: () => setPage(Page.CreateProgram),
toActiveScripts: () => setCurrentPage(Page.ActiveScripts), toDevMenu: () => setPage(Page.DevMenu),
toAugmentations: () => setCurrentPage(Page.Augmentations),
toBladeburner: () => setCurrentPage(Page.Bladeburner),
toStats: () => setCurrentPage(Page.Stats),
toCorporation: () => setCurrentPage(Page.Corporation),
toCreateProgram: () => setCurrentPage(Page.CreateProgram),
toDevMenu: () => setCurrentPage(Page.DevMenu),
toFaction: (faction?: Faction) => { toFaction: (faction?: Faction) => {
setCurrentPage(Page.Faction, faction); setPage(Page.Faction);
if (faction) setFaction(faction); if (faction) setFaction(faction);
}, },
toFactions: () => setCurrentPage(Page.Factions), toFactions: () => setPage(Page.Factions),
toGameOptions: () => setCurrentPage(Page.Options), toGameOptions: () => setPage(Page.Options),
toGang: () => setCurrentPage(Page.Gang), toGang: () => setPage(Page.Gang),
toHacknetNodes: () => setCurrentPage(Page.Hacknet), toHacknetNodes: () => setPage(Page.Hacknet),
toMilestones: () => setCurrentPage(Page.Milestones), toMilestones: () => setPage(Page.Milestones),
toResleeves: () => setCurrentPage(Page.Resleeves), toResleeves: () => setPage(Page.Resleeves),
toScriptEditor: (files: Record<string, string>, options?: ScriptEditorRouteOptions) => { toScriptEditor: (files: Record<string, string>, options?: ScriptEditorRouteOptions) => {
setEditorOptions({ setEditorOptions({
files, files,
vim: !!options?.vim, vim: !!options?.vim,
}); });
setCurrentPage(Page.ScriptEditor, files, options); setPage(Page.ScriptEditor);
}, },
toSleeves: () => setCurrentPage(Page.Sleeves), toSleeves: () => setPage(Page.Sleeves),
toStockMarket: () => setCurrentPage(Page.StockMarket), toStockMarket: () => setPage(Page.StockMarket),
toTerminal: () => setCurrentPage(Page.Terminal), toTerminal: () => setPage(Page.Terminal),
toTutorial: () => setCurrentPage(Page.Tutorial), toTutorial: () => setPage(Page.Tutorial),
toJob: () => { toJob: () => {
setLocation(Locations[player.companyName]); setLocation(Locations[player.companyName]);
setCurrentPage(Page.Job); setPage(Page.Job);
}, },
toCity: () => { toCity: () => {
setCurrentPage(Page.City); setPage(Page.City);
}, },
toTravel: () => { toTravel: () => {
player.gotoLocation(LocationName.TravelAgency); player.gotoLocation(LocationName.TravelAgency);
setCurrentPage(Page.Travel); setPage(Page.Travel);
}, },
toBitVerse: (flume: boolean, quick: boolean) => { toBitVerse: (flume: boolean, quick: boolean) => {
setFlume(flume); setFlume(flume);
setQuick(quick); setQuick(quick);
setCurrentPage(Page.BitVerse, flume, quick); setPage(Page.BitVerse);
}, },
toInfiltration: (location: Location) => { toInfiltration: (location: Location) => {
setLocation(location); setLocation(location);
setCurrentPage(Page.Infiltration, location); setPage(Page.Infiltration);
},
toWork: () => {
setCurrentPage(Page.Work);
}, },
toWork: () => setPage(Page.Work),
toBladeburnerCinematic: () => { toBladeburnerCinematic: () => {
setCurrentPage(Page.BladeburnerCinematic); setPage(Page.BladeburnerCinematic);
setCinematicText(cinematicText); setCinematicText(cinematicText);
}, },
toLocation: (location: Location) => { toLocation: (location: Location) => {
setLocation(location); setLocation(location);
setCurrentPage(Page.Location, location); setPage(Page.Location);
}, },
toStaneksGift: () => { toStaneksGift: () => {
setCurrentPage(Page.StaneksGift); setPage(Page.StaneksGift);
}, },
toAchievements: () => { toAchievements: () => {
setCurrentPage(Page.Achievements); setPage(Page.Achievements);
}, },
}; };
@ -587,20 +510,7 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
<SnackbarProvider> <SnackbarProvider>
<Overview mode={ITutorial.isRunning ? "tutorial" : "overview"}> <Overview mode={ITutorial.isRunning ? "tutorial" : "overview"}>
{!ITutorial.isRunning ? ( {!ITutorial.isRunning ? (
<CharacterOverview <CharacterOverview save={() => saveObject.saveGame()} killScripts={killAllScripts} />
save={() => saveObject.saveGame()}
killScripts={killAllScripts}
router={Router}
allowBackButton={withSidebar} />
) : (
<InteractiveTutorialRoot />
)}
</Overview>
{withSidebar ? (
<Box display="flex" flexDirection="row" width="100%">
<SidebarRoot player={player} router={Router} page={page} />
<Box className={classes.root}>{mainPage}</Box>
</Box>
) : ( ) : (
<Box className={classes.root}>{mainPage}</Box> <Box className={classes.root}>{mainPage}</Box>
)} )}

@ -7,6 +7,7 @@ import createStyles from "@mui/styles/createStyles";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { Reputation } from "./Reputation"; import { Reputation } from "./Reputation";
import { KillScriptsModal } from "./KillScriptsModal"; import { KillScriptsModal } from "./KillScriptsModal";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import Table from "@mui/material/Table"; import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody"; import TableBody from "@mui/material/TableBody";
@ -16,21 +17,19 @@ import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import SaveIcon from "@mui/icons-material/Save"; import SaveIcon from "@mui/icons-material/Save";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ClearAllIcon from "@mui/icons-material/ClearAll"; import ClearAllIcon from "@mui/icons-material/ClearAll";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { use } from "../Context"; import { use } from "../Context";
import { StatsProgressOverviewCell } from "./StatsProgressBar"; import { StatsProgressOverviewCell } from "./StatsProgressBar";
import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers"; import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";
import { IRouter, Page } from "../Router";
import { Box, Tooltip } from "@mui/material"; import { Box, Tooltip } from "@mui/material";
import { CONSTANTS } from "../../Constants";
interface IProps { interface IProps {
save: () => void; save: () => void;
killScripts: () => void; killScripts: () => void;
router: IRouter;
allowBackButton: boolean;
} }
function Intelligence(): React.ReactElement { function Intelligence(): React.ReactElement {
@ -79,105 +78,132 @@ function Bladeburner(): React.ReactElement {
); );
} }
interface WorkInProgressOverviewProps {
tooltip: React.ReactNode;
header: React.ReactNode;
children: React.ReactNode;
onClickFocus: () => void;
}
function WorkInProgressOverview({
tooltip,
children,
onClickFocus,
header,
}: WorkInProgressOverviewProps): React.ReactElement {
const classes = useStyles();
return (
<>
<TableRow>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.workCell }}>
<Tooltip title={<>{tooltip}</>}>
<Typography className={classes.workHeader} sx={{ pt: 1, pb: 0.5 }}>
{header}
</Typography>
</Tooltip>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.workCell }}>
<Typography className={classes.workSubtitles}>{children}</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" align="center" colSpan={2} classes={{ root: classes.cellNone }}>
<Button sx={{ mt: 1 }} onClick={onClickFocus}>
Focus
</Button>
</TableCell>
</TableRow>
</>
);
}
function Work(): React.ReactElement { function Work(): React.ReactElement {
const player = use.Player(); const player = use.Player();
const router = use.Router(); const router = use.Router();
const classes = useStyles(); const onClickFocus = (): void => {
player.startFocusing();
router.toWork();
};
if (!player.isWorking || player.focus) return <></>; if (!player.isWorking || player.focus) return <></>;
if (player.className !== "") { let details = <></>;
return ( let header = <></>;
let innerText = <></>;
if (player.workType === CONSTANTS.WorkTypeCompanyPartTime || player.workType === CONSTANTS.WorkTypeCompany) {
details = (
<> <>
<TableRow> {player.jobs[player.companyName]} at <strong>{player.companyName}</strong>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}>
<Typography>Work&nbsp;in&nbsp;progress:</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}>
<Typography>{player.className}</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" align="center" colSpan={2} classes={{ root: classes.cellNone }}>
<Button
onClick={() => {
player.startFocusing();
router.toWork();
}}
>
Focus
</Button>
</TableCell>
</TableRow>
</> </>
); );
} header = (
<>
if (player.createProgramName !== "") { Working at <strong>{player.companyName}</strong>
return ( </>
);
innerText = (
<>
+<Reputation reputation={player.workRepGained} /> rep
</>
);
} else if (player.workType === CONSTANTS.WorkTypeFaction) {
details = (
<>
{player.factionWorkType} for <strong>{player.currentWorkFactionName}</strong>
</>
);
header = (
<>
Working for <strong>{player.currentWorkFactionName}</strong>
</>
);
innerText = (
<>
+<Reputation reputation={player.workRepGained} /> rep
</>
);
} else if (player.workType === CONSTANTS.WorkTypeStudyClass) {
details = <>{player.workType}</>;
header = <>You are {player.className}</>;
innerText = <>{convertTimeMsToTimeElapsedString(player.timeWorked)}</>;
} else if (player.workType === CONSTANTS.WorkTypeCreateProgram) {
details = <>Coding {player.createProgramName}</>;
header = <>Creating a program</>;
innerText = (
<> <>
<TableRow>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}>
<Typography>Work&nbsp;in&nbsp;progress:</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}>
<Typography>
{player.createProgramName}{" "} {player.createProgramName}{" "}
{((player.timeWorkedCreateProgram / player.timeNeededToCompleteWork) * 100).toFixed(2)}% {((player.timeWorkedCreateProgram / player.timeNeededToCompleteWork) * 100).toFixed(2)}%
</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" align="center" colSpan={2} classes={{ root: classes.cellNone }}>
<Button
onClick={() => {
player.startFocusing();
router.toWork();
}}
>
Focus
</Button>
</TableCell>
</TableRow>
</> </>
); );
} }
return ( return (
<> <WorkInProgressOverview tooltip={details} header={header} onClickFocus={onClickFocus}>
<TableRow> {innerText}
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}> </WorkInProgressOverview>
<Typography>Work&nbsp;in&nbsp;progress:</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}>
<Typography>
+<Reputation reputation={player.workRepGained} /> rep
</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" align="center" colSpan={2} classes={{ root: classes.cellNone }}>
<Button
onClick={() => {
player.startFocusing();
router.toWork();
}}
>
Focus
</Button>
</TableCell>
</TableRow>
</>
); );
} }
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
workCell: {
textAlign: "center",
maxWidth: "200px",
borderBottom: "none",
padding: 0,
margin: 0,
},
workHeader: {
fontSize: "0.9rem",
},
workSubtitles: {
fontSize: "0.8rem",
},
cellNone: { cellNone: {
borderBottom: "none", borderBottom: "none",
padding: 0, padding: 0,
@ -210,7 +236,7 @@ const useStyles = makeStyles((theme: Theme) =>
export { useStyles as characterOverviewStyles }; export { useStyles as characterOverviewStyles };
export function CharacterOverview({ save, killScripts, router, allowBackButton }: IProps): React.ReactElement { export function CharacterOverview({ save, killScripts }: IProps): React.ReactElement {
const [killOpen, setKillOpen] = useState(false); const [killOpen, setKillOpen] = useState(false);
const player = use.Player(); const player = use.Player();
@ -249,9 +275,6 @@ export function CharacterOverview({ save, killScripts, router, allowBackButton }
player.charisma_mult * BitNodeMultipliers.CharismaLevelMultiplier, player.charisma_mult * BitNodeMultipliers.CharismaLevelMultiplier,
); );
const previousPageName = router.previousPage() < 0
? '' : Page[router.previousPage() ?? 0].replace(/([a-z])([A-Z])/g, '$1 $2');
return ( return (
<> <>
<Table sx={{ display: "block", m: 1 }}> <Table sx={{ display: "block", m: 1 }}>
@ -428,24 +451,15 @@ export function CharacterOverview({ save, killScripts, router, allowBackButton }
<Bladeburner /> <Bladeburner />
</TableBody> </TableBody>
</Table> </Table>
<Box sx={{ display: 'flex', borderTop: `1px solid ${Settings.theme.welllight}` }}> <Box sx={{ display: "flex", borderTop: `1px solid ${Settings.theme.welllight}` }}>
<Box sx={{ display: 'flex', flex: 1, justifyContent: 'flex-start', alignItems: 'center' }}> <Box sx={{ display: "flex", flex: 1, justifyContent: "flex-start", alignItems: "center" }}>
<IconButton onClick={save}> <IconButton onClick={save}>
<Tooltip title="Save game"> <Tooltip title="Save game">
<SaveIcon color={Settings.AutosaveInterval !== 0 ? "primary" : "error"} /> <SaveIcon color={Settings.AutosaveInterval !== 0 ? "primary" : "error"} />
</Tooltip> </Tooltip>
</IconButton> </IconButton>
{allowBackButton && (
<IconButton
disabled={!previousPageName}
onClick={() => router.toPreviousPage()}>
<Tooltip title={previousPageName ? `Go back to "${previousPageName}"` : ''}>
<ArrowBackIcon />
</Tooltip>
</IconButton>
)}
</Box> </Box>
<Box sx={{ display: 'flex', flex: 1, justifyContent: 'flex-end', alignItems: 'center' }}> <Box sx={{ display: "flex", flex: 1, justifyContent: "flex-end", alignItems: "center" }}>
<IconButton onClick={() => setKillOpen(true)}> <IconButton onClick={() => setKillOpen(true)}>
<Tooltip title="Kill all running scripts"> <Tooltip title="Kill all running scripts">
<ClearAllIcon color="error" /> <ClearAllIcon color="error" />

@ -25,20 +25,22 @@ export function CorruptableText(props: IProps): JSX.Element {
useEffect(() => { useEffect(() => {
let counter = 5; let counter = 5;
const id = setInterval(() => { const timers: number[] = [];
const intervalId = setInterval(() => {
counter--; counter--;
if (counter > 0) return; if (counter > 0) return;
counter = Math.random() * 5; counter = Math.random() * 5;
const index = Math.random() * content.length; const index = Math.random() * content.length;
const letter = content.charAt(index); const letter = content.charAt(index);
setContent((content) => replace(content, index, randomize(letter))); setContent((content) => replace(content, index, randomize(letter)));
setTimeout(() => { timers.push(window.setTimeout(() => {
setContent((content) => replace(content, index, letter)); setContent((content) => replace(content, index, letter));
}, 500); }, 500));
}, 20); }, 20);
return () => { return () => {
clearInterval(id); clearInterval(intervalId);
timers.forEach((timerId) => clearTimeout(timerId));
}; };
}, []); }, []);

@ -529,12 +529,12 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
</> </>
)} )}
</Grid> </Grid>
<Grid item xs={12} sm={6}> <Box sx={{ display: 'grid', width: 'fit-content', height: 'fit-content' }}>
<Box> <Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<Button onClick={() => props.save()}>Save Game</Button> <Button onClick={() => props.save()}>Save Game</Button>
<Button onClick={() => setDeleteOpen(true)}>Delete Game</Button> <Button onClick={() => setDeleteOpen(true)}>Delete Game</Button>
</Box> </Box>
<Box> <Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<Tooltip title={<Typography>Export your game to a text file.</Typography>}> <Tooltip title={<Typography>Export your game to a text file.</Typography>}>
<Button onClick={() => props.export()}> <Button onClick={() => props.export()}>
<DownloadIcon color="primary" /> <DownloadIcon color="primary" />
@ -571,7 +571,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
} }
/> />
</Box> </Box>
<Box> <Box sx={{ display: 'grid' }}>
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
@ -586,7 +586,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Button onClick={() => props.forceKill()}>Force kill all active scripts</Button> <Button onClick={() => props.forceKill()}>Force kill all active scripts</Button>
</Tooltip> </Tooltip>
</Box> </Box>
<Box> <Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
@ -613,7 +613,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Button onClick={() => setDiagnosticOpen(true)}>Diagnose files</Button> <Button onClick={() => setDiagnosticOpen(true)}>Diagnose files</Button>
</Tooltip> </Tooltip>
</Box> </Box>
<Box> <Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<Button onClick={() => setThemeEditorOpen(true)}>Theme editor</Button> <Button onClick={() => setThemeEditorOpen(true)}>Theme editor</Button>
<Button onClick={() => setStyleEditorOpen(true)}>Style editor</Button> <Button onClick={() => setStyleEditorOpen(true)}>Style editor</Button>
</Box> </Box>
@ -637,7 +637,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Typography>Incremental game plaza</Typography> <Typography>Incremental game plaza</Typography>
</Link> </Link>
</Box> </Box>
</Grid> </Box>
</Grid> </Grid>
<FileDiagnosticModal open={diagnosticOpen} onClose={() => setDiagnosticOpen(false)} /> <FileDiagnosticModal open={diagnosticOpen} onClose={() => setDiagnosticOpen(false)} />
<ConfirmationModal <ConfirmationModal

@ -39,12 +39,14 @@ export function LogBoxManager(): React.ReactElement {
() => () =>
LogBoxEvents.subscribe((script: RunningScript) => { LogBoxEvents.subscribe((script: RunningScript) => {
const id = script.server + "-" + script.filename + script.args.map((x: any): string => `${x}`).join("-"); const id = script.server + "-" + script.filename + script.args.map((x: any): string => `${x}`).join("-");
if (logs.find((l) => l.id === id)) return; if (logs.find((l) => l.id === id)) close(id);
Promise.resolve().then(() => {
logs.push({ logs.push({
id: id, id: id,
script: script, script: script,
}); });
rerender(); rerender();
})
}), }),
[], [],
); );

@ -7,12 +7,13 @@ import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import ReplyIcon from "@mui/icons-material/Reply"; import ReplyIcon from "@mui/icons-material/Reply";
import SaveIcon from '@mui/icons-material/Save'; import SaveIcon from "@mui/icons-material/Save";
import { ThemeEvents } from "./Theme"; import { ThemeEvents } from "./Theme";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { IStyleSettings, defaultStyles } from "../../Settings/Styles"; import { defaultStyles } from "../../Settings/Styles";
import { Tooltip } from "@mui/material"; import { Tooltip } from "@mui/material";
import { IStyleSettings } from "../../ScriptEditor/NetscriptDefinitions";
interface IProps { interface IProps {
open: boolean; open: boolean;
@ -32,9 +33,9 @@ function FontFamilyField({ value, onChange, refreshId } : FontFamilyProps): Reac
function update(newValue: React.CSSProperties["fontFamily"]): void { function update(newValue: React.CSSProperties["fontFamily"]): void {
setFontFamily(newValue); setFontFamily(newValue);
if (!newValue) { if (!newValue) {
setErrorText('Must have a value'); setErrorText("Must have a value");
} else { } else {
setErrorText(''); setErrorText("");
} }
} }
@ -55,7 +56,7 @@ function FontFamilyField({ value, onChange, refreshId } : FontFamilyProps): Reac
onChange={onTextChange} onChange={onTextChange}
fullWidth fullWidth
/> />
) );
} }
interface LineHeightProps { interface LineHeightProps {
@ -71,11 +72,11 @@ function LineHeightField({ value, onChange, refreshId } : LineHeightProps): Reac
function update(newValue: React.CSSProperties["lineHeight"]): void { function update(newValue: React.CSSProperties["lineHeight"]): void {
setLineHeight(newValue); setLineHeight(newValue);
if (!newValue) { if (!newValue) {
setErrorText('Must have a value'); setErrorText("Must have a value");
} else if (isNaN(Number(newValue))) { } else if (isNaN(Number(newValue))) {
setErrorText('Must be a number'); setErrorText("Must be a number");
} else { } else {
setErrorText(''); setErrorText("");
} }
} }
@ -95,7 +96,7 @@ function LineHeightField({ value, onChange, refreshId } : LineHeightProps): Reac
helperText={errorText} helperText={errorText}
onChange={onTextChange} onChange={onTextChange}
/> />
) );
} }
export function StyleEditorModal(props: IProps): React.ReactElement { export function StyleEditorModal(props: IProps): React.ReactElement {
@ -115,7 +116,7 @@ export function StyleEditorModal(props: IProps): React.ReactElement {
} }
function setDefaults(): void { function setDefaults(): void {
const styles = {...defaultStyles} const styles = { ...defaultStyles };
setCustomStyle(styles); setCustomStyle(styles);
persistToSettings(styles); persistToSettings(styles);
setRefreshId(refreshId + 1); setRefreshId(refreshId + 1);
@ -132,14 +133,21 @@ export function StyleEditorModal(props: IProps): React.ReactElement {
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={props.onClose}>
<Typography variant="h6">Styles Editor</Typography> <Typography variant="h6">Styles Editor</Typography>
<Typography> <Typography>
WARNING: Changing styles <strong>may mess up</strong> the interface. Drastic changes are <strong>NOT recommended</strong>. WARNING: Changing styles <strong>may mess up</strong> the interface. Drastic changes are{" "}
<strong>NOT recommended</strong>.
</Typography> </Typography>
<Paper sx={{ p: 2, my: 2 }}> <Paper sx={{ p: 2, my: 2 }}>
<FontFamilyField value={customStyle.fontFamily} refreshId={refreshId} <FontFamilyField
onChange={(value, error) => update({ ...customStyle, fontFamily: value }, error)} /> value={customStyle.fontFamily}
refreshId={refreshId}
onChange={(value, error) => update({ ...customStyle, fontFamily: value as any }, error)}
/>
<br /> <br />
<LineHeightField value={customStyle.lineHeight} refreshId={refreshId} <LineHeightField
onChange={(value, error) => update({ ...customStyle, lineHeight: value }, error)} /> value={customStyle.lineHeight}
refreshId={refreshId}
onChange={(value, error) => update({ ...customStyle, lineHeight: value as any }, error)}
/>
<br /> <br />
<ButtonGroup sx={{ my: 1 }}> <ButtonGroup sx={{ my: 1 }}>
<Button onClick={setDefaults} startIcon={<ReplyIcon />} color="secondary" variant="outlined"> <Button onClick={setDefaults} startIcon={<ReplyIcon />} color="secondary" variant="outlined">

@ -53,9 +53,6 @@ export interface IRouter {
// toRedPill(): void; // toRedPill(): void;
// toworkInProgress(): void; // toworkInProgress(): void;
page(): Page; page(): Page;
previousPage(): Page;
clearHistory(): void;
toPreviousPage(fallback?: (...args: any[]) => void): void;
toActiveScripts(): void; toActiveScripts(): void;
toAugmentations(): void; toAugmentations(): void;
toBitVerse(flume: boolean, quick: boolean): void; toBitVerse(flume: boolean, quick: boolean): void;

@ -36,12 +36,12 @@ export function WorkInProgressRoot(): React.ReactElement {
const faction = Factions[player.currentWorkFactionName]; const faction = Factions[player.currentWorkFactionName];
if (player.workType == CONSTANTS.WorkTypeFaction) { if (player.workType == CONSTANTS.WorkTypeFaction) {
function cancel(): void { function cancel(): void {
router.toFaction(faction);
player.finishFactionWork(true); player.finishFactionWork(true);
router.toPreviousPage(() => router.toFaction(faction));
} }
function unfocus(): void { function unfocus(): void {
router.toFaction(faction);
player.stopFocusing(); player.stopFocusing();
router.toPreviousPage(() => router.toFaction(faction));
} }
return ( return (
<Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}> <Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}>
@ -120,12 +120,13 @@ export function WorkInProgressRoot(): React.ReactElement {
if (player.className !== "") { if (player.className !== "") {
function cancel(): void { function cancel(): void {
player.finishClass(true); player.finishClass(true);
router.toPreviousPage(() => router.toCity()); router.toCity();
} }
function unfocus(): void { function unfocus(): void {
router.toFaction(faction);
router.toCity();
player.stopFocusing(); player.stopFocusing();
router.toPreviousPage(() => router.toCity());
} }
let stopText = ""; let stopText = "";
@ -211,11 +212,11 @@ export function WorkInProgressRoot(): React.ReactElement {
function cancel(): void { function cancel(): void {
player.finishWork(true); player.finishWork(true);
router.toPreviousPage(() => router.toJob()); router.toJob();
} }
function unfocus(): void { function unfocus(): void {
player.stopFocusing(); player.stopFocusing();
router.toPreviousPage(() => router.toJob()); router.toJob();
} }
const position = player.jobs[player.companyName]; const position = player.jobs[player.companyName];
@ -303,11 +304,11 @@ export function WorkInProgressRoot(): React.ReactElement {
if (player.workType == CONSTANTS.WorkTypeCompanyPartTime) { if (player.workType == CONSTANTS.WorkTypeCompanyPartTime) {
function cancel(): void { function cancel(): void {
player.finishWorkPartTime(true); player.finishWorkPartTime(true);
router.toPreviousPage(() => router.toJob()); router.toJob();
} }
function unfocus(): void { function unfocus(): void {
player.stopFocusing(); player.stopFocusing();
router.toPreviousPage(() => router.toJob()); router.toJob();
} }
const comp = Companies[player.companyName]; const comp = Companies[player.companyName];
let companyRep = 0; let companyRep = 0;
@ -439,11 +440,11 @@ export function WorkInProgressRoot(): React.ReactElement {
if (player.createProgramName !== "") { if (player.createProgramName !== "") {
function cancel(): void { function cancel(): void {
player.finishCreateProgramWork(true); player.finishCreateProgramWork(true);
router.toPreviousPage(() => router.toTerminal()); router.toTerminal();
} }
function unfocus(): void { function unfocus(): void {
router.toTerminal();
player.stopFocusing(); player.stopFocusing();
router.toPreviousPage(() => router.toTerminal());
} }
return ( return (
<Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}> <Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}>

@ -1,5 +1,7 @@
/* Generic Reviver, toJSON, and fromJSON functions used for saving and loading objects */ /* Generic Reviver, toJSON, and fromJSON functions used for saving and loading objects */
import { validateObject } from "./Validator";
interface IReviverValue { interface IReviverValue {
ctor: string; ctor: string;
data: any; data: any;
@ -26,7 +28,11 @@ export function Reviver(key: string, value: IReviverValue | null): any {
const ctor = Reviver.constructors[value.ctor]; const ctor = Reviver.constructors[value.ctor];
if (typeof ctor === "function" && typeof ctor.fromJSON === "function") { if (typeof ctor === "function" && typeof ctor.fromJSON === "function") {
return ctor.fromJSON(value); const obj = ctor.fromJSON(value);
if (ctor.validationData !== undefined) {
validateObject(obj, ctor.validationData);
}
return obj;
} }
} }
return value; return value;

78
src/utils/Validator.ts Normal file

@ -0,0 +1,78 @@
export type ObjectValidator<T> = {
[key in keyof T]?: ParameterValidator<T, keyof T>;
}
interface ParameterValidatorObject<Type, Key extends keyof Type> {
default?: any;
min?: number;
max?: number;
func?: (obj: Type, validator: ObjectValidator<Type>, key: Key) => void;
}
type ParameterValidatorFunction<Type, Key extends keyof Type> = (obj: Type, key: Key) => void;
type ParameterValidator<Type, Key extends keyof Type> = ParameterValidatorObject<Type, Key> | ParameterValidatorFunction<Type, Key>
export function validateObject<Type extends Record<string, unknown>, Key extends keyof Type>(obj: Type, validator: ObjectValidator<Type>): void {
for (const key of Object.keys(validator) as Key[]) {
const paramValidator = validator[key];
if (paramValidator !== undefined) {
if (typeof paramValidator === 'function') {
paramValidator(obj, key);
} else {
if (paramValidator.func !== undefined) {
paramValidator.func(obj, validator, key);
} else {
if ((typeof obj[key]) !== (typeof paramValidator.default)) {
obj[key] = paramValidator.default
}
if (typeof obj[key] === 'number' && paramValidator.min !== undefined) {
if (obj[key] < paramValidator.min) obj[key] = paramValidator.min as Type[Key];
}
if (typeof obj[key] === 'number' && paramValidator.max !== undefined) {
if (obj[key] > paramValidator.max) obj[key] = paramValidator.max as Type[Key];
}
}
}
}
}
}
export function minMax<Type, Key extends keyof Type>(def: number, min: number, max: number): (obj: Type, key: Key & keyof Type) => void {
return (obj, key) => {
if (typeof obj[key] !== 'number') {
obj[key] = def as unknown as Type[Key];
return;
}
if ((obj[key] as unknown as number) < min) {
obj[key] = min as unknown as Type[Key];
}
if ((obj[key] as unknown as number) > max) {
obj[key] = max as unknown as Type[Key];
}
};
}
export function oneOf<Type, Key extends keyof Type, Value>(def: Value, options: Value[]): (obj: Type, key: Key & keyof Type) => void {
return (obj, key) => {
if (typeof obj[key] !== typeof def) {
obj[key] = def as unknown as Type[Key];
return;
}
if (!options.includes(obj[key] as unknown as Value)) {
obj[key] = def as unknown as Type[Key];
}
};
}
export function subsetOf<Type, Key extends keyof Type, Value>(options: Value[]): (obj: Type, key: Key & keyof Type) => void {
return (obj, key) => {
if (typeof obj[key] !== 'object' || !Array.isArray(obj[key])) {
obj[key] = [] as unknown as Type[Key];
return;
}
const validValues: Value[] = [];
for (const value of obj[key] as unknown as Value[]) {
if (options.includes(value)) validValues.push(value);
}
obj[key] = validValues as unknown as Type[Key];
};
}

@ -0,0 +1,29 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { jest, describe, expect, test } from "@jest/globals";
import { Script } from "../../src/Script/Script";
import { Player } from "../../src/Player";
jest.mock(`!!raw-loader!../NetscriptDefinitions.d.ts`, () => "", {
virtual: true,
});
const code = `/** @param {NS} ns **/
export async function main(ns) {
ns.print(ns.getWeakenTime('n00dles'));
}`;
describe("Validate Save Script Works", function () {
it("Save", function () {
const server = "home";
const filename = "test.js";
const player = Player;
const script = new Script();
script.saveScript(player, filename, code, server, []);
expect(script.filename).toEqual(filename)
expect(script.code).toEqual(code)
expect(script.server).toEqual(server)
});
});