Merge pull request #2328 from danielyxie/dev

update
This commit is contained in:
hydroflame 2022-01-04 12:26:23 -05:00 committed by GitHub
commit 980b58d00a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 2382 additions and 671 deletions

1
.gitignore vendored

@ -4,6 +4,7 @@ Changelog.txt
Netburner.txt Netburner.txt
/doc/build /doc/build
/node_modules /node_modules
/electron/node_modules
/dist/*.map /dist/*.map
/test/*.map /test/*.map
/test/*.bundle.* /test/*.bundle.*

@ -118,10 +118,32 @@ Inside the root of the repo run
`npm install` to install all the dependencies `npm install` to install all the dependencies
`npm run start:dev` to launch the game in dev mode. `npm run start:dev` to launch the game in dev mode.
After that you can open any browser and naviguate to `localhost:8000` and play the game. After that you can open any browser and navigate to `localhost:8000` and play the game.
Saving a file will reload the game automatically. Saving a file will reload the game automatically.
#### Submitting a Pull Request
### How to build the electron app
Tested on Node v16.13.1 (LTS) on Windows
These steps only work in a bash-like environment, like MinGW for Windows.
```sh
# Install the main game dependencies & build the app in debug mode
npm install
npm run build:dev
# Use electron-packager to build the app to the .build/ folder
npm run electron
# When launching the .exe directly, you'll need the steam_appid.txt file in the root
# If not using windows, change this line accordingly
cp .build/bitburner-win32-x64/resources/app/steam_appid.txt .build/bitburner-win32-x64/steam_appid.txt
# And run the game...
.build/bitburner-win32-x64/bitburner.exe
```
### Submitting a Pull Request
When submitting a pull request with your code contributions, please abide by When submitting a pull request with your code contributions, please abide by
the following rules: the following rules:

9
dist/bitburner.d.ts vendored

@ -2720,7 +2720,7 @@ export declare interface NS extends Singularity {
* @param args - Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the second argument numThreads must be filled in with a value. * @param args - Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the second argument numThreads must be filled in with a value.
* @returns Returns the PID of a successfully started script, and 0 otherwise. * @returns Returns the PID of a successfully started script, and 0 otherwise.
*/ */
run(script: string, numThreads?: number, ...args: string[]): number; run(script: string, numThreads?: number, ...args: Array<string | number | boolean>): number;
/** /**
* Start another script on any server. * Start another script on any server.
@ -2760,7 +2760,7 @@ export declare interface NS extends Singularity {
* @param args - Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the third argument numThreads must be filled in with a value. * @param args - Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the third argument numThreads must be filled in with a value.
* @returns Returns the PID of a successfully started script, and 0 otherwise. * @returns Returns the PID of a successfully started script, and 0 otherwise.
*/ */
exec(script: string, host: string, numThreads?: number, ...args: string[]): number; exec(script: string, host: string, numThreads?: number, ...args: Array<string | number | boolean>): number;
/** /**
* Terminate current script and start another in 10s. * Terminate current script and start another in 10s.
@ -3498,7 +3498,7 @@ export declare interface NS extends Singularity {
* Returns 0 if the script does not exist. * Returns 0 if the script does not exist.
* *
* @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 use 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 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;
@ -3678,8 +3678,9 @@ export declare interface NS extends Singularity {
* Queue a toast (bottom-right notification). * Queue a toast (bottom-right notification).
* @param msg - Message in the toast. * @param msg - Message in the toast.
* @param variant - Type of toast, must be one of success, info, warning, error. Defaults to success. * @param variant - Type of toast, must be one of success, info, warning, error. Defaults to success.
* @param duration - Duration of toast in ms, defaults to 2000
*/ */
toast(msg: any, variant?: string): void; toast(msg: any, variant?: string, duration?: number): void;
/** /**
* Download a file from the internet. * Download a file from the internet.

44
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

@ -13,7 +13,8 @@ Server RAM
Perhaps the most important property of a server to make note of is its RAM, Perhaps the most important property of a server to make note of is its RAM,
which refers to how much memory is available on that machine. RAM is which refers to how much memory is available on that machine. RAM is
important because it is required to run Scripts. More RAM allows important because it is required to run Scripts. More RAM allows
the user to run more powerful and complicated scripts. the user to run more powerful and complicated scripts as well as executing
a script with :ref:`more threads <gameplay_scripts_multithreadingscripts>`.
The `free`, `scan-analyze`, and `analyze` Terminal commands The `free`, `scan-analyze`, and `analyze` Terminal commands
can be used to check how much RAM a server has. can be used to check how much RAM a server has.

@ -432,7 +432,10 @@ empty file will be created.
ps ps
^^ ^^
$ ps [-g, --grep pattern]
Prints all scripts that are currently running on the current server. Prints all scripts that are currently running on the current server.
The :code:`-g, --grep pattern` option will only output running scripts where the name matches the provided pattern.
rm rm
^^ ^^

@ -520,10 +520,12 @@ will appear that simply says :code:`Work`. Click this to start working.
Working at :code:`Joe's Guns` earns $110 per second and also grants some experience Working at :code:`Joe's Guns` earns $110 per second and also grants some experience
for every stat except hacking. for every stat except hacking.
Working for a company is completely passive. However, you will not be able to do anything Working for a company is completely passive. You can choose to focus on your work, do
else in the game while you work. You can cancel working at any time. You'll notice that something else simultaneously, or switch between those two. While you focus on work,
cancelling your work early causes you to lose out on some reputation gains, but you will not be able to do anything else in the game. If you do something else meanwhile,
you shouldn't worry about this. Company reputation isn't important right now. you will not gain reputation at the same speed. You can cancel working at any time.
You'll notice that cancelling your work early causes you to lose out on some reputation
gains, but you shouldn't worry about this. Company reputation isn't important right now.
Once your hacking hits level 75, you can visit :code:`Carmichael Security` in the city Once your hacking hits level 75, you can visit :code:`Carmichael Security` in the city
and get a software job there. This job offers higher pay and also earns you and get a software job there. This job offers higher pay and also earns you
@ -720,9 +722,10 @@ navigation menu, and from there select |CyberSec|. In the middle of
the page there should be a button for :code:`Hacking Contracts`. the page there should be a button for :code:`Hacking Contracts`.
Click it to start earning reputation for the |CyberSec| faction (as well Click it to start earning reputation for the |CyberSec| faction (as well
as some hacking experience). The higher your hacking level, the more reputation you as some hacking experience). The higher your hacking level, the more reputation you
will gain. Note that while you are working for a faction, you cannot interact with will gain. Note that while you are working for a faction, you can choose to not interact
the rest of the game in any way. You can cancel your faction work at any time with the rest of the game in any way to gain reputation at full speed. You can also select to
with no penalty. do something else simultaneously, gaining reputation a bit more slowly, until you focus again.
You can cancel your faction work at any time with no penalty to your reputation gained so far.
Purchasing Upgrades and Augmentations Purchasing Upgrades and Augmentations
------------------------------------- -------------------------------------

35
electron/achievements.js Normal file

@ -0,0 +1,35 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const greenworks = require("./greenworks");
function enableAchievementsInterval(window) {
// 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.
const steamAchievements = greenworks.getAchievementNames();
const intervalID = setInterval(async () => {
try {
const playerAchievements = await window.webContents.executeJavaScript("document.achievements");
for (const ach of playerAchievements) {
if (!steamAchievements.includes(ach)) continue;
greenworks.activateAchievement(ach, () => undefined);
}
} catch (error) {
log.error(error);
// The interval probably did not get cleared after a window kill
log.warn('Clearing achievements timer');
clearInterval(intervalID);
return;
}
}, 1000);
window.achievementsIntervalID = intervalID;
}
function disableAchievementsInterval(window) {
if (window.achievementsIntervalID) {
clearInterval(window.achievementsIntervalID);
}
}
module.exports = {
enableAchievementsInterval, disableAchievementsInterval
}

147
electron/api-server.js Normal file

@ -0,0 +1,147 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const http = require("http");
const crypto = require("crypto");
const log = require("electron-log");
const Config = require("electron-config");
const config = new Config();
let server;
let window;
async function initialize(win) {
window = win;
server = http.createServer(async function (req, res) {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString(); // convert Buffer to string
});
req.on("end", () => {
const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? '';
const isValid = providedToken === getAuthenticationToken();
if (isValid) {
log.debug('Valid authentication token');
} else {
log.log('Invalid authentication token');
res.writeHead(401);
res.write('Invalid authentication token');
res.end();
return;
}
let data;
try {
data = JSON.parse(body);
} catch (error) {
log.warn(`Invalid body data`);
res.writeHead(400);
res.write('Invalid body data');
res.end();
return;
}
if (data) {
window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`).then((result) => {
res.write(result);
res.end();
});
}
});
});
const autostart = config.get('autostart', false);
if (autostart) {
try {
await enable()
} catch (error) {
return Promise.reject(error);
}
}
return Promise.resolve();
}
function enable() {
if (isListening()) {
log.warn('API server already listening');
return Promise.resolve();
}
const port = config.get('port', 9990);
log.log(`Starting http server on port ${port}`);
// https://stackoverflow.com/a/62289870
let startFinished = false;
return new Promise((resolve, reject) => {
server.listen(port, "127.0.0.1", () => {
if (!startFinished) {
startFinished = true;
resolve();
}
});
server.once('error', (err) => {
if (!startFinished) {
startFinished = true;
console.log(
'There was an error starting the server in the error listener:',
err
);
reject(err);
}
});
});
}
function disable() {
if (!isListening()) {
log.warn('API server not listening');
return Promise.resolve();
}
log.log('Stopping http server');
return server.close();
}
function toggleServer() {
if (isListening()) {
return disable();
} else {
return enable();
}
}
function isListening() {
return server?.listening ?? false;
}
function toggleAutostart() {
const newValue = !isAutostart();
config.set('autostart', newValue);
log.log(`New autostart value is '${newValue}'`);
}
function isAutostart() {
return config.get('autostart');
}
function getAuthenticationToken() {
const token = config.get('token');
if (token) return token;
const newToken = generateToken();
config.set('token', newToken);
return newToken;
}
function generateToken() {
const buffer = crypto.randomBytes(48);
return buffer.toString('base64')
}
module.exports = {
initialize,
enable, disable, toggleServer,
toggleAutostart, isAutostart,
getAuthenticationToken, isListening,
}

30
electron/exit.html Normal file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Bitburner</title>
<link rel="stylesheet" href="main.css" />
<style>
body {
background-color: black;
color: #0c0;
}
div {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
h1 {
text-align: center;
}
</style>
</head>
<body>
<div>
<h1>Exiting ...</h1>
</div>
</body>
</html>

30
electron/export.html Normal file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Bitburner</title>
<link rel="stylesheet" href="main.css" />
<style>
body {
background-color: black;
color: #0c0;
}
div {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
h1 {
text-align: center;
}
</style>
</head>
<body>
<div>
<h1>Close me when operation is completed.</h1>
</div>
</body>
</html>

56
electron/gameWindow.js Normal file

@ -0,0 +1,56 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { app, BrowserWindow } = require("electron");
const log = require("electron-log");
const utils = require("./utils");
const achievements = require("./achievements");
const menu = require("./menu");
const api = require("./api-server");
const debug = process.argv.includes("--debug");
async function createWindow(killall) {
const setStopProcessHandler = global.app_handlers.stopProcess
const window = new BrowserWindow({
show: false,
backgroundThrottling: false,
backgroundColor: "#000000",
});
window.removeMenu();
window.maximize();
noScripts = killall ? { query: { noScripts: killall } } : {};
window.loadFile("index.html", noScripts);
window.show();
if (debug) window.webContents.openDevTools();
window.webContents.on("new-window", function (e, url) {
// make sure local urls stay in electron perimeter
if (url.substr(0, "file://".length) === "file://") {
return;
}
// and open every other protocols on the browser
e.preventDefault();
utils.openExternal(url);
});
window.webContents.backgroundThrottling = false;
achievements.enableAchievementsInterval(window);
utils.attachUnresponsiveAppHandler(window);
try {
await api.initialize(window);
} catch (error) {
log.error(error);
utils.showErrorBox('Error starting http server', error);
}
menu.refreshMenu(window);
setStopProcessHandler(app, window, true);
return window;
}
module.exports = {
createWindow,
}

@ -1,8 +1,12 @@
/* eslint-disable no-process-exit */ /* eslint-disable no-process-exit */
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
const { app, BrowserWindow, Menu, shell, dialog } = require("electron"); const { app, dialog, BrowserWindow } = require("electron");
const log = require('electron-log'); const log = require("electron-log");
const greenworks = require("./greenworks"); const greenworks = require("./greenworks");
const api = require("./api-server");
const gameWindow = require("./gameWindow");
const achievements = require("./achievements");
const utils = require("./utils");
log.catchErrors(); log.catchErrors();
log.info(`Started app: ${JSON.stringify(process.argv)}`); log.info(`Started app: ${JSON.stringify(process.argv)}`);
@ -18,162 +22,53 @@ if (greenworks.init()) {
log.warn("Steam API has failed to initialize."); log.warn("Steam API has failed to initialize.");
} }
const debug = false;
let win = null;
require("http")
.createServer(async function (req, res) {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString(); // convert Buffer to string
});
req.on("end", () => {
const data = JSON.parse(body);
win.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`).then((result) => {
res.write(result);
res.end();
});
});
})
.listen(9990, "127.0.0.1");
function createWindow(killall) {
win = new BrowserWindow({
show: false,
backgroundThrottling: false,
backgroundColor: "#000000",
});
win.removeMenu();
win.maximize();
noScripts = killall ? { query: { noScripts: killall } } : {};
win.loadFile("index.html", noScripts);
win.show();
if (debug) win.webContents.openDevTools();
win.webContents.on("new-window", function (e, url) {
// make sure local urls stay in electron perimeter
if (url.substr(0, "file://".length) === "file://") {
return;
}
// and open every other protocols on the browser
e.preventDefault();
shell.openExternal(url);
});
win.webContents.backgroundThrottling = false;
// 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.
const achievements = greenworks.getAchievementNames();
const intervalID = setInterval(async () => {
const achs = await win.webContents.executeJavaScript("document.achievements");
for (const ach of achs) {
if (!achievements.includes(ach)) continue;
greenworks.activateAchievement(ach, () => undefined);
}
}, 1000);
win.achievementsIntervalID = intervalID;
const reloadAndKill = (killScripts = true) => {
log.info('Reloading & Killing all scripts...');
setStopProcessHandler(app, win, false);
if (intervalID) clearInterval(intervalID);
win.webContents.forcefullyCrashRenderer();
win.close();
createWindow(killScripts);
};
const promptForReload = () => {
win.off('unresponsive', promptForReload);
dialog.showMessageBox({
type: 'error',
title: 'Bitburner > Application Unresponsive',
message: 'The application is unresponsive, possibly due to an infinite loop in your scripts.',
detail:' Did you forget a ns.sleep(x)?\n\n' +
'The application will be restarted for you, do you want to kill all running scripts?',
buttons: ['Restart', 'Cancel'],
defaultId: 0,
checkboxLabel: 'Kill all running scripts',
checkboxChecked: true,
noLink: true,
}).then(({response, checkboxChecked}) => {
if (response === 0) {
reloadAndKill(checkboxChecked);
} else {
win.on('unresponsive', promptForReload)
}
});
}
win.on('unresponsive', promptForReload);
// Create the Application's main menu
Menu.setApplicationMenu(
Menu.buildFromTemplate([
{
label: "Edit",
submenu: [
{ label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" },
{ label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" },
{ type: "separator" },
{ label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
{ label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
{ label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
{ label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" },
],
},
{
label: "reloads",
submenu: [
{
label: "reload",
accelerator: "f5",
click: () => {
win.loadFile("index.html");
},
},
{
label: "reload & kill all scripts",
click: reloadAndKill
},
],
},
{
label: "fullscreen",
submenu: [
{
label: "toggle",
accelerator: "f9",
click: (() => {
let full = false;
return () => {
full = !full;
win.setFullScreen(full);
};
})(),
},
],
},
{
label: "debug",
submenu: [
{
label: "activate",
click: () => win.webContents.openDevTools(),
},
],
},
]),
);
return win;
}
function setStopProcessHandler(app, window, enabled) { function setStopProcessHandler(app, window, enabled) {
const clearWindowHandler = () => { const closingWindowHandler = async (e) => {
if (window.achievementsIntervalID) { // We need to prevent the default closing event to add custom logic
clearInterval(window.achievementsIntervalID); e.preventDefault();
// First we clear the achievement timer
achievements.disableAchievementsInterval(window);
// Shutdown the http server
api.disable();
// Because of a steam limitation, if the player has launched an external browser,
// steam will keep displaying the game as "Running" in their UI as long as the browser is up.
// So we'll alert the player to close their browser.
if (global.app_playerOpenedExternalLink) {
await dialog.showMessageBox({
title: 'Bitburner',
message: 'You may have to close your browser to properly exit the game.',
detail: 'Steam will keep tracking Bitburner as "Running" if any process started within the game is still running.' +
' This includes launching an external link, which opens up your browser.',
type: 'warning', buttons: ['OK']
});
} }
// We'll try to execute javascript on the page to see if we're stuck
let canRunJS = false;
window.webContents.executeJavaScript('window.stop(); document.close()', true)
.then(() => canRunJS = true);
setTimeout(() => {
// Wait a few milliseconds to prevent a race condition before loading the exit screen
window.webContents.stop();
window.loadFile("exit.html")
}, 20);
// Wait 200ms, if the promise has not yet resolved, let's crash the process since we're possibly in a stuck scenario
setTimeout(() => {
if (!canRunJS) {
// We're stuck, let's crash the process
log.warn('Forcefully crashing the renderer process');
window.webContents.forcefullyCrashRenderer();
}
log.debug('Destroying the window');
window.destroy();
}, 200);
}
const clearWindowHandler = () => {
window = null; window = null;
}; };
@ -181,21 +76,41 @@ function setStopProcessHandler(app, window, enabled) {
log.info('Quitting the app...'); log.info('Quitting the app...');
app.isQuiting = true; app.isQuiting = true;
app.quit(); app.quit();
// eslint-disable-next-line no-process-exit
process.exit(0); process.exit(0);
}; };
if (enabled) { if (enabled) {
log.debug('Adding closing handlers');
window.on("closed", clearWindowHandler); window.on("closed", clearWindowHandler);
window.on("close", closingWindowHandler)
app.on("window-all-closed", stopProcessHandler); app.on("window-all-closed", stopProcessHandler);
} else { } else {
log.debug('Removing closing handlers');
window.removeListener("closed", clearWindowHandler); window.removeListener("closed", clearWindowHandler);
window.removeListener("close", closingWindowHandler);
app.removeListener("window-all-closed", stopProcessHandler); app.removeListener("window-all-closed", stopProcessHandler);
} }
} }
app.whenReady().then(() => { function startWindow(noScript) {
gameWindow.createWindow(noScript);
}
global.app_handlers = {
stopProcess: setStopProcessHandler,
createWindow: startWindow,
}
app.whenReady().then(async () => {
log.info('Application is ready!'); log.info('Application is ready!');
const win = createWindow(process.argv.includes("--no-scripts"));
setStopProcessHandler(app, win, true); if (process.argv.includes("--export-save")) {
const window = new BrowserWindow({ show: false });
await window.loadFile("export.html", false);
window.show();
setStopProcessHandler(app, window, true);
await utils.exportSave(window);
} else {
startWindow(process.argv.includes("--no-scripts"));
}
}); });

140
electron/menu.js Normal file

@ -0,0 +1,140 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { Menu, clipboard, dialog } = require("electron");
const log = require("electron-log");
const api = require("./api-server");
const utils = require("./utils");
function getMenu(window) {
return Menu.buildFromTemplate([
{
label: "Edit",
submenu: [
{ label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" },
{ label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" },
{ type: "separator" },
{ label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
{ label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
{ label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
{ label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" },
],
},
{
label: "Reloads",
submenu: [
{
label: "Reload",
accelerator: "f5",
click: () => window.loadFile("index.html"),
},
{
label: "Reload & Kill All Scripts",
click: () => utils.reloadAndKill(window, true),
},
],
},
{
label: "Fullscreen",
submenu: [
{
label: "Toggle",
accelerator: "f9",
click: (() => {
let full = false;
return () => {
full = !full;
window.setFullScreen(full);
};
})(),
},
],
},
{
label: "API Server",
submenu: [
{
label: api.isListening() ? 'Disable Server' : 'Enable Server',
click: (async () => {
let success = false;
try {
await api.toggleServer();
success = true;
} catch (error) {
log.error(error);
utils.showErrorBox('Error Toggling Server', error);
}
if (success && api.isListening()) {
utils.writeToast(window, "Started API Server", "success");
} else if (success && !api.isListening()) {
utils.writeToast(window, "Stopped API Server", "success");
} else {
utils.writeToast(window, 'Error Toggling Server', "error");
}
refreshMenu(window);
})
},
{
label: api.isAutostart() ? 'Disable Autostart' : 'Enable Autostart',
click: (async () => {
api.toggleAutostart();
if (api.isAutostart()) {
utils.writeToast(window, "Enabled API Server Autostart", "success");
} else {
utils.writeToast(window, "Disabled API Server Autostart", "success");
}
refreshMenu(window);
})
},
{
label: 'Copy Auth Token',
click: (async () => {
const token = api.getAuthenticationToken();
log.log('Wrote authentication token to clipboard');
clipboard.writeText(token);
utils.writeToast(window, "Copied Authentication Token to Clipboard", "info");
})
},
{
type: 'separator',
},
{
label: 'Information',
click: () => {
dialog.showMessageBox({
type: 'info',
title: 'Bitburner > API Server Information',
message: 'The API Server is used to write script files to your in-game home.',
detail: 'There is an official Visual Studio Code extension that makes use of that feature.\n\n' +
'It allows you to write your script file in an external IDE and have them pushed over to the game automatically.\n' +
'If you want more information, head over to: https://github.com/bitburner-official/bitburner-vscode.',
buttons: ['Dismiss', 'Open Extension Link (GitHub)'],
defaultId: 0,
cancelId: 0,
noLink: true,
}).then(({response}) => {
if (response === 1) {
utils.openExternal('https://github.com/bitburner-official/bitburner-vscode');
}
});
}
}
]
},
{
label: "Debug",
submenu: [
{
label: "Activate",
click: () => window.webContents.openDevTools(),
},
],
},
]);
}
function refreshMenu(window) {
Menu.setApplicationMenu(getMenu(window));
}
module.exports = {
getMenu, refreshMenu,
}

325
electron/package-lock.json generated Normal file

@ -0,0 +1,325 @@
{
"name": "bitburner",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "bitburner",
"version": "1.0.0",
"dependencies": {
"electron-config": "^2.0.0",
"electron-log": "^4.4.4"
}
},
"node_modules/conf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz",
"integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==",
"dependencies": {
"dot-prop": "^4.1.0",
"env-paths": "^1.0.0",
"make-dir": "^1.0.0",
"pkg-up": "^2.0.0",
"write-file-atomic": "^2.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/dot-prop": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz",
"integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==",
"dependencies": {
"is-obj": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/electron-config": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/electron-config/-/electron-config-2.0.0.tgz",
"integrity": "sha512-5mGwRK4lsAo6tiy4KNF/zUInYpUGr7JJzLA8FHOoqBWV3kkKJWSrDXo4Uk2Ffm5aeQ1o73XuorfkYhaWFV2O4g==",
"deprecated": "Renamed to `electron-store`.",
"dependencies": {
"conf": "^1.0.0"
}
},
"node_modules/electron-log": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.4.tgz",
"integrity": "sha512-jcNtrVmKXG+CHchLo/jnjjQ9K4/ORguWD23H2nqApTwisQ4Qo3IRQtLiorubajX0Uxg76Xm/Yt+eNfQMoHVr5w=="
},
"node_modules/env-paths": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz",
"integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=",
"engines": {
"node": ">=4"
}
},
"node_modules/find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
"dependencies": {
"locate-path": "^2.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/graceful-fs": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg=="
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
"engines": {
"node": ">=0.8.19"
}
},
"node_modules/is-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
"dependencies": {
"p-locate": "^2.0.0",
"path-exists": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/make-dir": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
"dependencies": {
"pify": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/p-limit": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
"dependencies": {
"p-try": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/p-locate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"dependencies": {
"p-limit": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
"engines": {
"node": ">=4"
}
},
"node_modules/path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
"engines": {
"node": ">=4"
}
},
"node_modules/pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"engines": {
"node": ">=4"
}
},
"node_modules/pkg-up": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
"integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
"dependencies": {
"find-up": "^2.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/signal-exit": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz",
"integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ=="
},
"node_modules/write-file-atomic": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz",
"integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==",
"dependencies": {
"graceful-fs": "^4.1.11",
"imurmurhash": "^0.1.4",
"signal-exit": "^3.0.2"
}
}
},
"dependencies": {
"conf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz",
"integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==",
"requires": {
"dot-prop": "^4.1.0",
"env-paths": "^1.0.0",
"make-dir": "^1.0.0",
"pkg-up": "^2.0.0",
"write-file-atomic": "^2.3.0"
}
},
"dot-prop": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz",
"integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==",
"requires": {
"is-obj": "^1.0.0"
}
},
"electron-config": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/electron-config/-/electron-config-2.0.0.tgz",
"integrity": "sha512-5mGwRK4lsAo6tiy4KNF/zUInYpUGr7JJzLA8FHOoqBWV3kkKJWSrDXo4Uk2Ffm5aeQ1o73XuorfkYhaWFV2O4g==",
"requires": {
"conf": "^1.0.0"
}
},
"electron-log": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.4.tgz",
"integrity": "sha512-jcNtrVmKXG+CHchLo/jnjjQ9K4/ORguWD23H2nqApTwisQ4Qo3IRQtLiorubajX0Uxg76Xm/Yt+eNfQMoHVr5w=="
},
"env-paths": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz",
"integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA="
},
"find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
"requires": {
"locate-path": "^2.0.0"
}
},
"graceful-fs": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg=="
},
"imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
},
"is-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8="
},
"locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
"requires": {
"p-locate": "^2.0.0",
"path-exists": "^3.0.0"
}
},
"make-dir": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
"requires": {
"pify": "^3.0.0"
}
},
"p-limit": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
"requires": {
"p-try": "^1.0.0"
}
},
"p-locate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"requires": {
"p-limit": "^1.1.0"
}
},
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"pkg-up": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
"integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
"requires": {
"find-up": "^2.1.0"
}
},
"signal-exit": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz",
"integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ=="
},
"write-file-atomic": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz",
"integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==",
"requires": {
"graceful-fs": "^4.1.11",
"imurmurhash": "^0.1.4",
"signal-exit": "^3.0.2"
}
}
}
}

@ -20,5 +20,9 @@
], ],
"directories": { "directories": {
"buildResources": "public" "buildResources": "public"
},
"dependencies": {
"electron-config": "^2.0.0",
"electron-log": "^4.4.4"
} }
} }

115
electron/utils.js Normal file

@ -0,0 +1,115 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { app, dialog, shell } = require("electron");
const log = require("electron-log");
const achievements = require("./achievements");
const api = require("./api-server");
function reloadAndKill(window, killScripts) {
const setStopProcessHandler = global.app_handlers.stopProcess
const createWindowHandler = global.app_handlers.createWindow;
log.info('Reloading & Killing all scripts...');
setStopProcessHandler(app, window, false);
achievements.disableAchievementsInterval(window);
api.disable();
window.webContents.forcefullyCrashRenderer();
window.on('closed', () => {
// Wait for window to be closed before opening the new one to prevent race conditions
log.debug('Opening new window');
createWindowHandler(killScripts);
})
window.close();
}
function promptForReload(window) {
detachUnresponsiveAppHandler(window);
dialog.showMessageBox({
type: 'error',
title: 'Bitburner > Application Unresponsive',
message: 'The application is unresponsive, possibly due to an infinite loop in your scripts.',
detail:' Did you forget a ns.sleep(x)?\n\n' +
'The application will be restarted for you, do you want to kill all running scripts?',
buttons: ['Restart', 'Cancel'],
defaultId: 0,
checkboxLabel: 'Kill all running scripts',
checkboxChecked: true,
noLink: true,
}).then(({response, checkboxChecked}) => {
if (response === 0) {
reloadAndKill(window, checkboxChecked);
} else {
attachUnresponsiveAppHandler(window);
}
});
}
function attachUnresponsiveAppHandler(window) {
window.on('unresponsive', () => promptForReload(window));
}
function detachUnresponsiveAppHandler(window) {
window.off('unresponsive', () => promptForReload(window));
}
function showErrorBox(title, error) {
dialog.showErrorBox(
title,
`${error.name}\n\n${error.message}`
);
}
function exportSaveFromIndexedDb() {
return new Promise((resolve) => {
const dbRequest = indexedDB.open("bitburnerSave");
dbRequest.onsuccess = () => {
const db = dbRequest.result;
const transaction = db.transaction(['savestring'], "readonly");
const store = transaction.objectStore('savestring');
const request = store.get('save');
request.onsuccess = () => {
const file = new Blob([request.result], {type: 'text/plain'});
const a = document.createElement("a");
const url = URL.createObjectURL(file);
a.href = url;
a.download = 'save.json';
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
resolve();
}, 0);
}
}
});
}
async function exportSave(window) {
await window.webContents
.executeJavaScript(`${exportSaveFromIndexedDb.toString()}; exportSaveFromIndexedDb();`, true);
}
async function writeTerminal(window, message, type = null) {
await window.webContents
.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true)
}
async function writeToast(window, message, type = "info", duration = 2000) {
await window.webContents
.executeJavaScript(`window.appNotifier.toast("${message}", "${type}", ${duration});`, true)
}
function openExternal(url) {
shell.openExternal(url);
global.app_playerOpenedExternalLink = true;
}
module.exports = {
reloadAndKill, showErrorBox, exportSave,
attachUnresponsiveAppHandler, detachUnresponsiveAppHandler,
openExternal, writeTerminal, writeToast,
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -18,7 +18,7 @@
| [CharacterInfo](./bitburner.characterinfo.md) | | | [CharacterInfo](./bitburner.characterinfo.md) | |
| [CharacterMult](./bitburner.charactermult.md) | | | [CharacterMult](./bitburner.charactermult.md) | |
| [CodingAttemptOptions](./bitburner.codingattemptoptions.md) | Options to affect the behavior of [CodingContract](./bitburner.codingcontract.md) attempt. | | [CodingAttemptOptions](./bitburner.codingattemptoptions.md) | Options to affect the behavior of [CodingContract](./bitburner.codingcontract.md) attempt. |
| [CodingContract](./bitburner.codingcontract.md) | Coding Contact API | | [CodingContract](./bitburner.codingcontract.md) | Coding Contract API |
| [Corporation](./bitburner.corporation.md) | Corporation API | | [Corporation](./bitburner.corporation.md) | Corporation API |
| [CorporationInfo](./bitburner.corporationinfo.md) | General info about a corporation | | [CorporationInfo](./bitburner.corporationinfo.md) | General info about a corporation |
| [CrimeStats](./bitburner.crimestats.md) | Data representing the internal values of a crime. | | [CrimeStats](./bitburner.crimestats.md) | Data representing the internal values of a crime. |

@ -28,5 +28,5 @@ Returns the amount of time in milliseconds it takes to execute the grow Netscrip
RAM cost: 0.05 GB RAM cost: 0.05 GB
Returns the amount of time in milliseconds it takes to execute the grow Netscript function on the target server. The function takes in an optional hackLvl parameter that can be specified to see what the grow time would be at different hacking levels. Returns the amount of time in milliseconds it takes to execute the grow Netscript function on the target server.

@ -28,5 +28,5 @@ Returns the amount of time in milliseconds it takes to execute the hack Netscrip
RAM cost: 0.05 GB RAM cost: 0.05 GB
Returns the amount of time in milliseconds it takes to execute the hack Netscript function on the target server. The function takes in an optional hackLvl parameter that can be specified to see what the hack time would be at different hacking levels. Returns the amount of time in milliseconds it takes to execute the hack Netscript function on the target server.

@ -22,11 +22,11 @@ getWeakenTime(host: string): number;
number number
Returns the amount of time in milliseconds it takes to execute the grow Netscript function. Returns Infinity if called on a Hacknet Server. Returns the amount of time in milliseconds it takes to execute the weaken() Netscript function on the target server. Returns Infinity if called on a Hacknet Server.
## Remarks ## Remarks
RAM cost: 0.05 GB RAM cost: 0.05 GB
Returns the amount of time in milliseconds it takes to execute the weaken() Netscript function on the target server. The function takes in an optional hackLvl parameter that can be specified to see what the weaken time would be at different hacking levels.

@ -4,7 +4,7 @@
## NS.hackAnalyze() method ## NS.hackAnalyze() method
Get the percent of money stolen with a single thread. Get the part of money stolen with a single thread.
<b>Signature:</b> <b>Signature:</b>
@ -22,13 +22,13 @@ hackAnalyze(host: string): number;
number number
The percentage of money you will steal from the target server with a single hack. The part of money you will steal from the target server with a single thread hack.
## Remarks ## Remarks
RAM cost: 1 GB RAM cost: 1 GB
Returns the percentage of the specified servers money you will steal with a single hack. This value is returned in percentage form, not decimal (Netscript functions typically return in decimal form, but not this one). Returns the part of the specified servers money you will steal with a single thread hack.
## Example ## Example
@ -36,6 +36,6 @@ Returns the percentage of the specified servers money you will steal with a s
```ts ```ts
//For example, assume the following returns 0.01: //For example, assume the following returns 0.01:
hackAnalyze("foodnstuff"); hackAnalyze("foodnstuff");
//This means that if hack the foodnstuff server, then you will steal 1% of its total money. If you hack using N threads, then you will steal N*0.01 times its total money. //This means that if hack the foodnstuff server using a single thread, then you will steal 1%, or 0.01 of its total money. If you hack using N threads, then you will steal N*0.01 times its total money.
``` ```

@ -111,7 +111,7 @@ export async function main(ns) {
| [growthAnalyze(host, growthAmount, cores)](./bitburner.ns.growthanalyze.md) | Calculate the number of grow thread needed to grow a server by a certain multiplier. | | [growthAnalyze(host, growthAmount, cores)](./bitburner.ns.growthanalyze.md) | Calculate the number of grow thread needed to grow a server by a certain multiplier. |
| [growthAnalyzeSecurity(threads)](./bitburner.ns.growthanalyzesecurity.md) | Calculate the security increase for a number of thread. | | [growthAnalyzeSecurity(threads)](./bitburner.ns.growthanalyzesecurity.md) | Calculate the security increase for a number of thread. |
| [hack(host, opts)](./bitburner.ns.hack.md) | Steal a servers money. | | [hack(host, opts)](./bitburner.ns.hack.md) | Steal a servers money. |
| [hackAnalyze(host)](./bitburner.ns.hackanalyze.md) | Get the percent of money stolen with a single thread. | | [hackAnalyze(host)](./bitburner.ns.hackanalyze.md) | Get the part of money stolen with a single thread. |
| [hackAnalyzeChance(host)](./bitburner.ns.hackanalyzechance.md) | Get the chance of successfully hacking a server. | | [hackAnalyzeChance(host)](./bitburner.ns.hackanalyzechance.md) | Get the chance of successfully hacking a server. |
| [hackAnalyzeSecurity(threads)](./bitburner.ns.hackanalyzesecurity.md) | Get the security increase for a number of thread. | | [hackAnalyzeSecurity(threads)](./bitburner.ns.hackanalyzesecurity.md) | Get the security increase for a number of thread. |
| [hackAnalyzeThreads(host, hackAmount)](./bitburner.ns.hackanalyzethreads.md) | Predict the effect of hack. | | [hackAnalyzeThreads(host, hackAmount)](./bitburner.ns.hackanalyzethreads.md) | Predict the effect of hack. |

@ -9,7 +9,7 @@ Start another script on the current server.
<b>Signature:</b> <b>Signature:</b>
```typescript ```typescript
run(script: string, numThreads?: number, ...args: string[]): number; run(script: string, numThreads?: number, ...args: Array<string | number | boolean>): number;
``` ```
## Parameters ## Parameters
@ -18,7 +18,7 @@ run(script: string, numThreads?: number, ...args: string[]): number;
| --- | --- | --- | | --- | --- | --- |
| script | string | Filename of script to run. | | script | string | Filename of script to run. |
| numThreads | number | Optional thread count for new script. Set to 1 by default. Will be rounded to nearest integer. | | numThreads | number | Optional thread count for new script. Set to 1 by default. Will be rounded to nearest integer. |
| args | string\[\] | Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the second argument numThreads must be filled in with a value. | | args | Array<string | number | boolean> | Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the second argument numThreads must be filled in with a value. |
<b>Returns:</b> <b>Returns:</b>

@ -30,7 +30,7 @@ RAM cost: 5 GB
This function is used to automatically attempt to commit crimes. If you are already in the middle of some working action (such as working for a company or training at a gym), then running this function will automatically cancel that action and give you your earnings. This function is used to automatically attempt to commit crimes. If you are already in the middle of some working action (such as working for a company or training at a gym), then running this function will automatically cancel that action and give you your earnings.
This function returns the number of seconds it takes to attempt the specified crime (e.g It takes 60 seconds to attempt the Rob Store crime, so running `commitCrime('rob store')` will return 60). This function returns the number of milliseconds it takes to attempt the specified crime (e.g It takes 60 seconds to attempt the Rob Store crime, so running `commitCrime('rob store')` will return 60,000).
Warning: I do not recommend using the time returned from this function to try and schedule your crime attempts. Instead, I would use the isBusy Singularity function to check whether you have finished attempting a crime. This is because although the game sets a certain crime to be X amount of seconds, there is no guarantee that your browser will follow that time limit. Warning: I do not recommend using the time returned from this function to try and schedule your crime attempts. Instead, I would use the isBusy Singularity function to check whether you have finished attempting a crime. This is because although the game sets a certain crime to be X amount of seconds, there is no guarantee that your browser will follow that time limit.

352
package-lock.json generated

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.4.1", "@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0", "@emotion/styled": "^11.3.0",
"@material-ui/core": "^4.12.3",
"@microsoft/api-documenter": "^7.13.65", "@microsoft/api-documenter": "^7.13.65",
"@microsoft/api-extractor": "^7.18.17", "@microsoft/api-extractor": "^7.18.17",
"@monaco-editor/react": "^4.2.2", "@monaco-editor/react": "^4.2.2",
@ -30,15 +31,19 @@
"better-react-mathjax": "^1.0.3", "better-react-mathjax": "^1.0.3",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"date-fns": "^2.25.0", "date-fns": "^2.25.0",
"electron-config": "^2.0.0",
"escodegen": "^1.11.0", "escodegen": "^1.11.0",
"file-saver": "^1.3.8", "file-saver": "^1.3.8",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"jquery": "^3.5.0", "jquery": "^3.5.0",
"js-sha256": "^0.9.0",
"jszip": "^3.7.0", "jszip": "^3.7.0",
"material-ui-color": "^1.2.0", "material-ui-color": "^1.2.0",
"material-ui-popup-state": "^1.5.3",
"monaco-editor": "^0.27.0", "monaco-editor": "^0.27.0",
"notistack": "^2.0.2", "notistack": "^2.0.2",
"numeral": "2.0.6", "numeral": "2.0.6",
"prop-types": "^15.8.0",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
@ -62,7 +67,6 @@
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"cypress": "^8.3.1", "cypress": "^8.3.1",
"electron": "^14.0.2", "electron": "^14.0.2",
"electron-log": "^4.4.3",
"electron-packager": "^15.4.0", "electron-packager": "^15.4.0",
"eslint": "^7.24.0", "eslint": "^7.24.0",
"fork-ts-checker-webpack-plugin": "^6.3.3", "fork-ts-checker-webpack-plugin": "^6.3.3",
@ -2892,7 +2896,6 @@
"version": "4.12.3", "version": "4.12.3",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz", "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz",
"integrity": "sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw==", "integrity": "sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw==",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.4.4", "@babel/runtime": "^7.4.4",
"@material-ui/styles": "^4.11.4", "@material-ui/styles": "^4.11.4",
@ -2929,7 +2932,6 @@
"version": "4.11.4", "version": "4.11.4",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz",
"integrity": "sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew==", "integrity": "sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew==",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.4.4", "@babel/runtime": "^7.4.4",
"@emotion/hash": "^0.8.0", "@emotion/hash": "^0.8.0",
@ -2969,14 +2971,12 @@
"node_modules/@material-ui/styles/node_modules/csstype": { "node_modules/@material-ui/styles/node_modules/csstype": {
"version": "2.6.18", "version": "2.6.18",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz",
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==", "integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ=="
"peer": true
}, },
"node_modules/@material-ui/system": { "node_modules/@material-ui/system": {
"version": "4.12.1", "version": "4.12.1",
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.1.tgz", "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.1.tgz",
"integrity": "sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw==", "integrity": "sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw==",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.4.4", "@babel/runtime": "^7.4.4",
"@material-ui/utils": "^4.11.2", "@material-ui/utils": "^4.11.2",
@ -3004,14 +3004,12 @@
"node_modules/@material-ui/system/node_modules/csstype": { "node_modules/@material-ui/system/node_modules/csstype": {
"version": "2.6.18", "version": "2.6.18",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz",
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==", "integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ=="
"peer": true
}, },
"node_modules/@material-ui/types": { "node_modules/@material-ui/types": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
"integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "*" "@types/react": "*"
}, },
@ -3025,7 +3023,6 @@
"version": "4.11.2", "version": "4.11.2",
"resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz",
"integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.4.4", "@babel/runtime": "^7.4.4",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
@ -6074,8 +6071,7 @@
"node_modules/classnames": { "node_modules/classnames": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
"peer": true
}, },
"node_modules/clean-css": { "node_modules/clean-css": {
"version": "4.2.3", "version": "4.2.3",
@ -6381,6 +6377,58 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"node_modules/conf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz",
"integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==",
"dependencies": {
"dot-prop": "^4.1.0",
"env-paths": "^1.0.0",
"make-dir": "^1.0.0",
"pkg-up": "^2.0.0",
"write-file-atomic": "^2.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/conf/node_modules/env-paths": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz",
"integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=",
"engines": {
"node": ">=4"
}
},
"node_modules/conf/node_modules/make-dir": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
"dependencies": {
"pify": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/conf/node_modules/pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"engines": {
"node": ">=4"
}
},
"node_modules/conf/node_modules/write-file-atomic": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz",
"integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==",
"dependencies": {
"graceful-fs": "^4.1.11",
"imurmurhash": "^0.1.4",
"signal-exit": "^3.0.2"
}
},
"node_modules/config-chain": { "node_modules/config-chain": {
"version": "1.1.13", "version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
@ -7546,6 +7594,17 @@
"webidl-conversions": "^4.0.2" "webidl-conversions": "^4.0.2"
} }
}, },
"node_modules/dot-prop": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz",
"integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==",
"dependencies": {
"is-obj": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/duplexer": { "node_modules/duplexer": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@ -7630,11 +7689,14 @@
"node": ">= 8.6" "node": ">= 8.6"
} }
}, },
"node_modules/electron-log": { "node_modules/electron-config": {
"version": "4.4.3", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.3.tgz", "resolved": "https://registry.npmjs.org/electron-config/-/electron-config-2.0.0.tgz",
"integrity": "sha512-IWxkiVLSpbI4if61kTSLMErYwz+Jq/gnHeTtQ8jcAjtlU8rgTIScWBgZJxk3fVnyvW6M+Ci3Bn9ogHgjgDSvNg==", "integrity": "sha512-5mGwRK4lsAo6tiy4KNF/zUInYpUGr7JJzLA8FHOoqBWV3kkKJWSrDXo4Uk2Ffm5aeQ1o73XuorfkYhaWFV2O4g==",
"dev": true "deprecated": "Renamed to `electron-store`.",
"dependencies": {
"conf": "^1.0.0"
}
}, },
"node_modules/electron-notarize": { "node_modules/electron-notarize": {
"version": "1.1.1", "version": "1.1.1",
@ -11274,6 +11336,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-path-cwd": { "node_modules/is-path-cwd": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
@ -13699,6 +13769,11 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
}, },
"node_modules/js-sha256": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
"integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -14542,7 +14617,6 @@
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/material-ui-popup-state/-/material-ui-popup-state-1.9.3.tgz", "resolved": "https://registry.npmjs.org/material-ui-popup-state/-/material-ui-popup-state-1.9.3.tgz",
"integrity": "sha512-+Ete5Tzw5rXlYfmqptOS8kBUH8vnK5OJsd6IQ7SHtLjU0PsvsmM73M/k8ot0xkX4RmPGuNRsFbK3mlCe/ClQuw==", "integrity": "sha512-+Ete5Tzw5rXlYfmqptOS8kBUH8vnK5OJsd6IQ7SHtLjU0PsvsmM73M/k8ot0xkX4RmPGuNRsFbK3mlCe/ClQuw==",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@material-ui/types": "^6.0.1", "@material-ui/types": "^6.0.1",
@ -14558,7 +14632,6 @@
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-6.0.2.tgz",
"integrity": "sha512-/XUca4wUb9pWimLLdM1PE8KS8rTbDEGohSGkGtk3WST7lm23m+8RYv9uOmrvOg/VSsl4bMiOv4t2/LCb+RLbTg==", "integrity": "sha512-/XUca4wUb9pWimLLdM1PE8KS8rTbDEGohSGkGtk3WST7lm23m+8RYv9uOmrvOg/VSsl4bMiOv4t2/LCb+RLbTg==",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "*" "@types/react": "*"
}, },
@ -16004,6 +16077,78 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/pkg-up": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
"integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
"dependencies": {
"find-up": "^2.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pkg-up/node_modules/find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
"dependencies": {
"locate-path": "^2.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pkg-up/node_modules/locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
"dependencies": {
"p-locate": "^2.0.0",
"path-exists": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pkg-up/node_modules/p-limit": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
"dependencies": {
"p-try": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pkg-up/node_modules/p-locate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"dependencies": {
"p-limit": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pkg-up/node_modules/p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
"engines": {
"node": ">=4"
}
},
"node_modules/pkg-up/node_modules/path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
"engines": {
"node": ">=4"
}
},
"node_modules/plist": { "node_modules/plist": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.0.4.tgz", "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.4.tgz",
@ -16026,8 +16171,7 @@
"node_modules/popper.js": { "node_modules/popper.js": {
"version": "1.16.1-lts", "version": "1.16.1-lts",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz",
"integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==", "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA=="
"peer": true
}, },
"node_modules/portfinder": { "node_modules/portfinder": {
"version": "1.0.28", "version": "1.0.28",
@ -16180,13 +16324,13 @@
} }
}, },
"node_modules/prop-types": { "node_modules/prop-types": {
"version": "15.7.2", "version": "15.8.0",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", "integrity": "sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==",
"dependencies": { "dependencies": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
"react-is": "^16.8.1" "react-is": "^16.13.1"
} }
}, },
"node_modules/prop-types/node_modules/react-is": { "node_modules/prop-types/node_modules/react-is": {
@ -17700,8 +17844,7 @@
"node_modules/signal-exit": { "node_modules/signal-exit": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz",
"integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ=="
"dev": true
}, },
"node_modules/sisteransi": { "node_modules/sisteransi": {
"version": "1.0.5", "version": "1.0.5",
@ -23965,7 +24108,6 @@
"version": "4.12.3", "version": "4.12.3",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz", "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz",
"integrity": "sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw==", "integrity": "sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw==",
"peer": true,
"requires": { "requires": {
"@babel/runtime": "^7.4.4", "@babel/runtime": "^7.4.4",
"@material-ui/styles": "^4.11.4", "@material-ui/styles": "^4.11.4",
@ -23985,7 +24127,6 @@
"version": "4.11.4", "version": "4.11.4",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz",
"integrity": "sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew==", "integrity": "sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew==",
"peer": true,
"requires": { "requires": {
"@babel/runtime": "^7.4.4", "@babel/runtime": "^7.4.4",
"@emotion/hash": "^0.8.0", "@emotion/hash": "^0.8.0",
@ -24008,8 +24149,7 @@
"csstype": { "csstype": {
"version": "2.6.18", "version": "2.6.18",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz",
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==", "integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ=="
"peer": true
} }
} }
}, },
@ -24017,7 +24157,6 @@
"version": "4.12.1", "version": "4.12.1",
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.1.tgz", "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.1.tgz",
"integrity": "sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw==", "integrity": "sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw==",
"peer": true,
"requires": { "requires": {
"@babel/runtime": "^7.4.4", "@babel/runtime": "^7.4.4",
"@material-ui/utils": "^4.11.2", "@material-ui/utils": "^4.11.2",
@ -24028,8 +24167,7 @@
"csstype": { "csstype": {
"version": "2.6.18", "version": "2.6.18",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz",
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==", "integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ=="
"peer": true
} }
} }
}, },
@ -24037,14 +24175,12 @@
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
"integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==",
"peer": true,
"requires": {} "requires": {}
}, },
"@material-ui/utils": { "@material-ui/utils": {
"version": "4.11.2", "version": "4.11.2",
"resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz",
"integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==",
"peer": true,
"requires": { "requires": {
"@babel/runtime": "^7.4.4", "@babel/runtime": "^7.4.4",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
@ -26401,8 +26537,7 @@
"classnames": { "classnames": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
"peer": true
}, },
"clean-css": { "clean-css": {
"version": "4.2.3", "version": "4.2.3",
@ -26657,6 +26792,48 @@
} }
} }
}, },
"conf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz",
"integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==",
"requires": {
"dot-prop": "^4.1.0",
"env-paths": "^1.0.0",
"make-dir": "^1.0.0",
"pkg-up": "^2.0.0",
"write-file-atomic": "^2.3.0"
},
"dependencies": {
"env-paths": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz",
"integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA="
},
"make-dir": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
"requires": {
"pify": "^3.0.0"
}
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"write-file-atomic": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz",
"integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==",
"requires": {
"graceful-fs": "^4.1.11",
"imurmurhash": "^0.1.4",
"signal-exit": "^3.0.2"
}
}
}
},
"config-chain": { "config-chain": {
"version": "1.1.13", "version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
@ -27589,6 +27766,14 @@
"webidl-conversions": "^4.0.2" "webidl-conversions": "^4.0.2"
} }
}, },
"dot-prop": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz",
"integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==",
"requires": {
"is-obj": "^1.0.0"
}
},
"duplexer": { "duplexer": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@ -27703,11 +27888,13 @@
} }
} }
}, },
"electron-log": { "electron-config": {
"version": "4.4.3", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.3.tgz", "resolved": "https://registry.npmjs.org/electron-config/-/electron-config-2.0.0.tgz",
"integrity": "sha512-IWxkiVLSpbI4if61kTSLMErYwz+Jq/gnHeTtQ8jcAjtlU8rgTIScWBgZJxk3fVnyvW6M+Ci3Bn9ogHgjgDSvNg==", "integrity": "sha512-5mGwRK4lsAo6tiy4KNF/zUInYpUGr7JJzLA8FHOoqBWV3kkKJWSrDXo4Uk2Ffm5aeQ1o73XuorfkYhaWFV2O4g==",
"dev": true "requires": {
"conf": "^1.0.0"
}
}, },
"electron-notarize": { "electron-notarize": {
"version": "1.1.1", "version": "1.1.1",
@ -30534,6 +30721,11 @@
"has-tostringtag": "^1.0.0" "has-tostringtag": "^1.0.0"
} }
}, },
"is-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8="
},
"is-path-cwd": { "is-path-cwd": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
@ -32343,6 +32535,11 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
}, },
"js-sha256": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
"integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
},
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -33019,7 +33216,6 @@
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/material-ui-popup-state/-/material-ui-popup-state-1.9.3.tgz", "resolved": "https://registry.npmjs.org/material-ui-popup-state/-/material-ui-popup-state-1.9.3.tgz",
"integrity": "sha512-+Ete5Tzw5rXlYfmqptOS8kBUH8vnK5OJsd6IQ7SHtLjU0PsvsmM73M/k8ot0xkX4RmPGuNRsFbK3mlCe/ClQuw==", "integrity": "sha512-+Ete5Tzw5rXlYfmqptOS8kBUH8vnK5OJsd6IQ7SHtLjU0PsvsmM73M/k8ot0xkX4RmPGuNRsFbK3mlCe/ClQuw==",
"peer": true,
"requires": { "requires": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@material-ui/types": "^6.0.1", "@material-ui/types": "^6.0.1",
@ -33031,7 +33227,6 @@
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-6.0.2.tgz",
"integrity": "sha512-/XUca4wUb9pWimLLdM1PE8KS8rTbDEGohSGkGtk3WST7lm23m+8RYv9uOmrvOg/VSsl4bMiOv4t2/LCb+RLbTg==", "integrity": "sha512-/XUca4wUb9pWimLLdM1PE8KS8rTbDEGohSGkGtk3WST7lm23m+8RYv9uOmrvOg/VSsl4bMiOv4t2/LCb+RLbTg==",
"peer": true,
"requires": {} "requires": {}
} }
} }
@ -34175,6 +34370,59 @@
} }
} }
}, },
"pkg-up": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
"integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
"requires": {
"find-up": "^2.1.0"
},
"dependencies": {
"find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
"requires": {
"locate-path": "^2.0.0"
}
},
"locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
"requires": {
"p-locate": "^2.0.0",
"path-exists": "^3.0.0"
}
},
"p-limit": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
"requires": {
"p-try": "^1.0.0"
}
},
"p-locate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"requires": {
"p-limit": "^1.1.0"
}
},
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
}
}
},
"plist": { "plist": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.0.4.tgz", "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.4.tgz",
@ -34194,8 +34442,7 @@
"popper.js": { "popper.js": {
"version": "1.16.1-lts", "version": "1.16.1-lts",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz",
"integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==", "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA=="
"peer": true
}, },
"portfinder": { "portfinder": {
"version": "1.0.28", "version": "1.0.28",
@ -34310,13 +34557,13 @@
} }
}, },
"prop-types": { "prop-types": {
"version": "15.7.2", "version": "15.8.0",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", "integrity": "sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==",
"requires": { "requires": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
"react-is": "^16.8.1" "react-is": "^16.13.1"
}, },
"dependencies": { "dependencies": {
"react-is": { "react-is": {
@ -35509,8 +35756,7 @@
"signal-exit": { "signal-exit": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz",
"integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ=="
"dev": true
}, },
"sisteransi": { "sisteransi": {
"version": "1.0.5", "version": "1.0.5",

@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.4.1", "@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0", "@emotion/styled": "^11.3.0",
"@material-ui/core": "^4.12.3",
"@microsoft/api-documenter": "^7.13.65", "@microsoft/api-documenter": "^7.13.65",
"@microsoft/api-extractor": "^7.18.17", "@microsoft/api-extractor": "^7.18.17",
"@monaco-editor/react": "^4.2.2", "@monaco-editor/react": "^4.2.2",
@ -31,15 +32,19 @@
"better-react-mathjax": "^1.0.3", "better-react-mathjax": "^1.0.3",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"date-fns": "^2.25.0", "date-fns": "^2.25.0",
"electron-config": "^2.0.0",
"escodegen": "^1.11.0", "escodegen": "^1.11.0",
"file-saver": "^1.3.8", "file-saver": "^1.3.8",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"jquery": "^3.5.0", "jquery": "^3.5.0",
"js-sha256": "^0.9.0",
"jszip": "^3.7.0", "jszip": "^3.7.0",
"material-ui-color": "^1.2.0", "material-ui-color": "^1.2.0",
"material-ui-popup-state": "^1.5.3",
"monaco-editor": "^0.27.0", "monaco-editor": "^0.27.0",
"notistack": "^2.0.2", "notistack": "^2.0.2",
"numeral": "2.0.6", "numeral": "2.0.6",
"prop-types": "^15.8.0",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
@ -64,7 +69,6 @@
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"cypress": "^8.3.1", "cypress": "^8.3.1",
"electron": "^14.0.2", "electron": "^14.0.2",
"electron-log": "^4.4.3",
"electron-packager": "^15.4.0", "electron-packager": "^15.4.0",
"eslint": "^7.24.0", "eslint": "^7.24.0",
"fork-ts-checker-webpack-plugin": "^6.3.3", "fork-ts-checker-webpack-plugin": "^6.3.3",

@ -16,7 +16,10 @@ cp main.css .package/main.css
cp dist/vendor.bundle.js .package/dist/vendor.bundle.js cp dist/vendor.bundle.js .package/dist/vendor.bundle.js
cp main.bundle.js .package/main.bundle.js cp main.bundle.js .package/main.bundle.js
# Adding electron-log dependency # Install electron sub-dependencies
cp -r node_modules/electron-log .package/node_modules/electron-log cd electron
npm install
cd ..
# And finally build the app.
npm run electron:packager npm run electron:packager

@ -36,15 +36,16 @@ export function printAliases(): void {
// Returns true if successful, false otherwise // Returns true if successful, false otherwise
export function parseAliasDeclaration(dec: string, global = false): boolean { export function parseAliasDeclaration(dec: string, global = false): boolean {
const re = /^([\w|!|%|,|@|-]+)="(.+)"$/; const re = /^([\w|!|%|,|@|-]+)=(("(.+)")|('(.+)'))$/;
const matches = dec.match(re); const matches = dec.match(re);
if (matches == null || matches.length != 3) { if (matches == null || matches.length != 7) {
return false; return false;
} }
if (global) { if (global) {
addGlobalAlias(matches[1], matches[2]); addGlobalAlias(matches[1], matches[4] || matches[6]);
} else { } else {
addAlias(matches[1], matches[2]); addAlias(matches[1], matches[4] || matches[6]);
} }
return true; return true;
} }

@ -1925,7 +1925,7 @@ function initAugmentations(): void {
repCost: 7.5e3, repCost: 7.5e3,
moneyCost: 3e7, moneyCost: 3e7,
info: info:
"A tiny chip that sits behind the retinae. This implant lets the" + "user visually detect infrared radiation.", "A tiny chip that sits behind the retinae. This implant lets the user visually detect infrared radiation.",
crime_success_mult: 1.25, crime_success_mult: 1.25,
crime_money_mult: 1.1, crime_money_mult: 1.1,
dexterity_mult: 1.1, dexterity_mult: 1.1,

@ -745,7 +745,7 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.StaneksGiftPowerMultiplier = 0.5; BitNodeMultipliers.StaneksGiftPowerMultiplier = 0.5;
BitNodeMultipliers.StaneksGiftExtraSize = 2; BitNodeMultipliers.StaneksGiftExtraSize = 2;
BitNodeMultipliers.GangSoftcap = 0.8; BitNodeMultipliers.GangSoftcap = 0.8;
BitNodeMultipliers.CorporationSoftCap = 0.9; BitNodeMultipliers.CorporationSoftCap = 0.7;
BitNodeMultipliers.WorldDaemonDifficulty = 2; BitNodeMultipliers.WorldDaemonDifficulty = 2;
break; break;
case 10: // Digital Carbon case 10: // Digital Carbon

@ -112,7 +112,7 @@ export const CONSTANTS: {
LatestUpdate: string; LatestUpdate: string;
} = { } = {
VersionString: "1.2.0", VersionString: "1.2.0",
VersionNumber: 7, VersionNumber: 8,
// Speed (in ms) at which the main loop is updated // Speed (in ms) at which the main loop is updated
_idleSpeed: 200, _idleSpeed: 200,

@ -108,35 +108,32 @@ function WarehouseRoot(props: IProps): React.ReactElement {
} }
} }
let breakdown = <></>; const breakdownItems: JSX.Element[] = [];
for (const matName in props.warehouse.materials) { for (const matName in props.warehouse.materials) {
const mat = props.warehouse.materials[matName]; const mat = props.warehouse.materials[matName];
if (!MaterialSizes.hasOwnProperty(matName)) continue; if (!MaterialSizes.hasOwnProperty(matName)) continue;
if (mat.qty === 0) continue; if (mat.qty === 0) continue;
breakdown = ( breakdownItems.push(<>{matName}: {numeralWrapper.format(mat.qty * MaterialSizes[matName], "0,0.0")}</>);
<>
{breakdown}
{matName}: {numeralWrapper.format(mat.qty * MaterialSizes[matName], "0,0.0")}
<br />
</>
);
} }
for (const prodName in division.products) { for (const prodName in division.products) {
const prod = division.products[prodName]; const prod = division.products[prodName];
if (prod === undefined) continue; if (prod === undefined) continue;
breakdown = ( breakdownItems.push(<>{prodName}: {numeralWrapper.format(prod.data[props.warehouse.loc][0] * prod.siz, "0,0.0")}</>);
<> }
{breakdown}
{prodName}: {numeralWrapper.format(prod.data[props.warehouse.loc][0] * prod.siz, "0,0.0")} let breakdown;
</> if (breakdownItems && breakdownItems.length > 0) {
); breakdown = breakdownItems.reduce(
(previous: JSX.Element, current: JSX.Element): JSX.Element => previous && <>{previous}<br />{current}</> || <>{current}</>);
} else {
breakdown = <>No items in storage.</>
} }
return ( return (
<Paper> <Paper>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Tooltip title={props.warehouse.sizeUsed !== 0 ? <Typography>{breakdown}</Typography> : ""}> <Tooltip title={props.warehouse.sizeUsed !== 0 ? <Typography><>{breakdown}</></Typography> : ""}>
<Typography color={props.warehouse.sizeUsed >= props.warehouse.size ? "error" : "primary"}> <Typography color={props.warehouse.sizeUsed >= props.warehouse.size ? "error" : "primary"}>
Storage: {numeralWrapper.formatBigNumber(props.warehouse.sizeUsed)} /{" "} Storage: {numeralWrapper.formatBigNumber(props.warehouse.sizeUsed)} /{" "}
{numeralWrapper.formatBigNumber(props.warehouse.size)} {numeralWrapper.formatBigNumber(props.warehouse.size)}

@ -4,6 +4,7 @@ import { DarkWebItems } from "./DarkWebItems";
import { Player } from "../Player"; import { Player } from "../Player";
import { Terminal } from "../Terminal"; import { Terminal } from "../Terminal";
import { SpecialServers } from "../Server/data/SpecialServers"; import { SpecialServers } from "../Server/data/SpecialServers";
import { numeralWrapper } from "../ui/numeralFormat";
import { Money } from "../ui/React/Money"; import { Money } from "../ui/React/Money";
import { DarkWebItem } from "./DarkWebItem"; import { DarkWebItem } from "./DarkWebItem";
@ -13,8 +14,8 @@ export function checkIfConnectedToDarkweb(): void {
if (server !== null && SpecialServers.DarkWeb == server.hostname) { if (server !== null && SpecialServers.DarkWeb == server.hostname) {
Terminal.print( Terminal.print(
"You are now connected to the dark web. From the dark web you can purchase illegal items. " + "You are now connected to the dark web. From the dark web you can purchase illegal items. " +
"Use the 'buy -l' command to display a list of all the items you can buy. Use 'buy [item-name] " + "Use the 'buy -l' command to display a list of all the items you can buy. Use 'buy [item-name]' " +
"to purchase an item.", "to purchase an item. Use the 'buy -a' command to purchase unowned all items.",
); );
} }
} }
@ -87,3 +88,30 @@ export function buyDarkwebItem(itemName: string): void {
"You have purchased the " + item.program + " program. The new program can be found on your home computer.", "You have purchased the " + item.program + " program. The new program can be found on your home computer.",
); );
} }
export function buyAllDarkwebItems(): void {
const itemsToBuy: DarkWebItem[] = [];
let cost = 0;
for (const key in DarkWebItems) {
const item = DarkWebItems[key];
if (!Player.hasProgram(item.program)) {
itemsToBuy.push(item);
cost += item.price;
}
}
if (itemsToBuy.length === 0) {
Terminal.print("All available programs have been purchased already.");
return;
}
if (cost > Player.money) {
Terminal.error("Not enough money to purchase remaining programs, " + numeralWrapper.formatMoney(cost) + " required");
return;
}
for (const item of itemsToBuy) {
buyDarkwebItem(item.program);
}
}

@ -4,7 +4,7 @@ import { IEngine } from "./IEngine";
import { IRouter } from "./ui/Router"; import { IRouter } from "./ui/Router";
import { AugmentationNames } from "./Augmentation/data/AugmentationNames"; import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
import React from "react"; import React, { useEffect } from "react";
import { General } from "./DevMenu/ui/General"; import { General } from "./DevMenu/ui/General";
import { Stats } from "./DevMenu/ui/Stats"; import { Stats } from "./DevMenu/ui/Stats";
@ -23,6 +23,7 @@ import { Sleeves } from "./DevMenu/ui/Sleeves";
import { Stanek } from "./DevMenu/ui/Stanek"; import { Stanek } from "./DevMenu/ui/Stanek";
import { TimeSkip } from "./DevMenu/ui/TimeSkip"; import { TimeSkip } from "./DevMenu/ui/TimeSkip";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { Exploit } from "./Exploits/Exploit";
interface IProps { interface IProps {
player: IPlayer; player: IPlayer;
@ -31,6 +32,9 @@ interface IProps {
} }
export function DevMenuRoot(props: IProps): React.ReactElement { export function DevMenuRoot(props: IProps): React.ReactElement {
useEffect(() => {
props.player.giveExploit(Exploit.YoureNotMeantToAccessThis);
}, []);
return ( return (
<> <>
<Typography>Development Menu - Only meant to be used for testing/debugging</Typography> <Typography>Development Menu - Only meant to be used for testing/debugging</Typography>

@ -22,6 +22,9 @@ import { Server } from "./Server/Server";
import { Router } from "./ui/GameRoot"; import { Router } from "./ui/GameRoot";
import { Page } from "./ui/Router"; import { Page } from "./ui/Router";
import { removeLeadingSlash } from "./Terminal/DirectoryHelpers"; import { removeLeadingSlash } from "./Terminal/DirectoryHelpers";
import { Terminal } from './Terminal';
import { SnackbarEvents } from "./ui/React/Snackbar";
import { IMap } from "./types";
interface Achievement { interface Achievement {
ID: string; ID: string;
@ -414,24 +417,27 @@ function calculateAchievements(): void {
} }
export function initElectron(): void { export function initElectron(): void {
setAchievements([]); const userAgent = navigator.userAgent.toLowerCase();
initWebserver(); if (userAgent.indexOf(' electron/') > -1) {
setInterval(calculateAchievements, 5000); // Electron-specific code
setAchievements([]);
initWebserver();
setInterval(calculateAchievements, 5000);
initAppNotifier();
}
} }
function initWebserver(): void { function initWebserver(): void {
(document as any).saveFile = function (filename: string, code: string): string { (document as any).saveFile = function (filename: string, code: string): string {
filename = removeLeadingSlash(filename); if (removeLeadingSlash(filename).includes("/")) {
console.log(code); filename = "/" + removeLeadingSlash(filename);
}
code = Buffer.from(code, "base64").toString(); code = Buffer.from(code, "base64").toString();
console.log(code);
const home = GetServer("home"); const home = GetServer("home");
if (home === null) return "'home' server not found."; if (home === null) return "'home' server not found.";
if (home === null) return "Server should not be null but it is.";
if (isScriptFilename(filename)) { if (isScriptFilename(filename)) {
//If the current script already exists on the server, overwrite it //If the current script already exists on the server, overwrite it
for (let i = 0; i < home.scripts.length; i++) { for (let i = 0; i < home.scripts.length; i++) {
console.log(`${filename} ${home.scripts[i].filename}`);
if (filename == home.scripts[i].filename) { if (filename == home.scripts[i].filename) {
home.scripts[i].saveScript(filename, code, "home", home.scripts); home.scripts[i].saveScript(filename, code, "home", home.scripts);
return "written"; return "written";
@ -448,3 +454,27 @@ function initWebserver(): void {
return "not a script file"; return "not a script file";
}; };
} }
// Expose certain alert functions to allow the wrapper to sends message to the game
function initAppNotifier(): void {
const funcs = {
terminal: (message: string, type?: string) => {
const typesFn: IMap<(s: string) => void> = {
info: Terminal.info,
warn: Terminal.warn,
error: Terminal.error,
success: Terminal.success
};
let fn;
if (type) fn = typesFn[type];
if (!fn) fn = Terminal.print;
fn.bind(Terminal)(message);
},
toast: (message: string, type: "info" | "success" | "warning" | "error" , duration = 2000) =>
SnackbarEvents.emit(message, type, duration),
}
// Will be consumud by the electron wrapper.
// @ts-ignore
window.appNotifier = funcs;
}

@ -18,6 +18,7 @@ export enum Exploit {
TimeCompression = "TimeCompression", TimeCompression = "TimeCompression",
RealityAlteration = "RealityAlteration", RealityAlteration = "RealityAlteration",
N00dles = "N00dles", N00dles = "N00dles",
YoureNotMeantToAccessThis = "YoureNotMeantToAccessThis",
// To the players reading this. Yes you're supposed to add EditSaveFile by // To the players reading this. Yes you're supposed to add EditSaveFile by
// editing your save file, yes you could add them all, no we don't care // editing your save file, yes you could add them all, no we don't care
// that's not the point. // that's not the point.
@ -35,6 +36,7 @@ const names: {
UndocumentedFunctionCall: "by looking beyond the documentation.", UndocumentedFunctionCall: "by looking beyond the documentation.",
RealityAlteration: "by altering reality to suit your whims.", RealityAlteration: "by altering reality to suit your whims.",
N00dles: "by harnessing the power of the n00dles.", N00dles: "by harnessing the power of the n00dles.",
YoureNotMeantToAccessThis: "by accessing the dev menu.",
}; };
export function ExploitName(exploit: string): string { export function ExploitName(exploit: string): string {

@ -29,6 +29,7 @@ import { GetServer } from "../../Server/AllServers";
import { CorruptableText } from "../../ui/React/CorruptableText"; import { CorruptableText } from "../../ui/React/CorruptableText";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import { serverMetadata } from "../../Server/data/servers"; import { serverMetadata } from "../../Server/data/servers";
import { Tooltip } from "@mui/material";
type IProps = { type IProps = {
loc: Location; loc: Location;
@ -92,8 +93,11 @@ export function GenericLocation({ loc }: IProps): React.ReactElement {
return ( return (
<> <>
<Button onClick={() => router.toCity()}>Return to World</Button> <Button onClick={() => router.toCity()}>Return to World</Button>
<Typography variant="h4"> <Typography variant="h4" sx={{ mt: 1 }}>
{backdoorInstalled && !Settings.DisableTextEffects ? <CorruptableText content={loc.name} /> : loc.name} {backdoorInstalled && !Settings.DisableTextEffects ? (
<Tooltip title={`Backdoor installed on ${loc.name}.`}>
<span><CorruptableText content={loc.name} /></span>
</Tooltip>) : loc.name}
</Typography> </Typography>
{locContent} {locContent}
</> </>

@ -1,7 +1,6 @@
import { isString } from "./utils/helpers/isString"; import { isString } from "./utils/helpers/isString";
import { GetServer } from "./Server/AllServers"; import { GetServer } from "./Server/AllServers";
import { WorkerScript } from "./Netscript/WorkerScript"; import { WorkerScript } from "./Netscript/WorkerScript";
import { BlobsMap } from "./NetscriptJSEvaluator";
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) {
@ -22,8 +21,8 @@ export function makeRuntimeRejectMsg(workerScript: WorkerScript, msg: string): s
throw new Error(`WorkerScript constructed with invalid server ip: ${workerScript.hostname}`); throw new Error(`WorkerScript constructed with invalid server ip: ${workerScript.hostname}`);
} }
for (const url in BlobsMap) { for (const scriptUrl of workerScript.scriptRef.dependencies) {
msg = msg.replace(new RegExp(url, "g"), BlobsMap[url]); msg = msg.replace(new RegExp(scriptUrl.url, "g"), scriptUrl.filename);
} }
return "|DELIMITER|" + server.hostname + "|DELIMITER|" + workerScript.name + "|DELIMITER|" + msg; return "|DELIMITER|" + server.hostname + "|DELIMITER|" + workerScript.name + "|DELIMITER|" + msg;
@ -45,7 +44,7 @@ export function resolveNetscriptRequestedThreads(
`Invalid thread count passed to ${functionName}: ${requestedThreads}. Threads must be a positive number.`, `Invalid thread count passed to ${functionName}: ${requestedThreads}. Threads must be a positive number.`,
); );
} }
if (requestedThreads > threads) { if (requestedThreadsAsInt > threads) {
throw makeRuntimeRejectMsg( throw makeRuntimeRejectMsg(
workerScript, workerScript,
`Too many threads requested by ${functionName}. Requested: ${requestedThreads}. Has: ${threads}.`, `Too many threads requested by ${functionName}. Requested: ${requestedThreads}. Has: ${threads}.`,

@ -64,12 +64,13 @@ import { NetscriptSleeve } from "./NetscriptFunctions/Sleeve";
import { NetscriptExtra } from "./NetscriptFunctions/Extra"; import { NetscriptExtra } from "./NetscriptFunctions/Extra";
import { NetscriptHacknet } from "./NetscriptFunctions/Hacknet"; import { NetscriptHacknet } from "./NetscriptFunctions/Hacknet";
import { NetscriptStanek } from "./NetscriptFunctions/Stanek"; import { NetscriptStanek } from "./NetscriptFunctions/Stanek";
import { NetscriptUserInterface } from './NetscriptFunctions/UserInterface'; import { NetscriptUserInterface } from "./NetscriptFunctions/UserInterface";
import { NetscriptBladeburner } from "./NetscriptFunctions/Bladeburner"; import { NetscriptBladeburner } from "./NetscriptFunctions/Bladeburner";
import { NetscriptCodingContract } from "./NetscriptFunctions/CodingContract"; import { NetscriptCodingContract } from "./NetscriptFunctions/CodingContract";
import { NetscriptCorporation } from "./NetscriptFunctions/Corporation"; import { NetscriptCorporation } from "./NetscriptFunctions/Corporation";
import { NetscriptFormulas } from "./NetscriptFunctions/Formulas"; import { NetscriptFormulas } from "./NetscriptFunctions/Formulas";
import { NetscriptStockMarket } from "./NetscriptFunctions/StockMarket"; import { NetscriptStockMarket } from "./NetscriptFunctions/StockMarket";
import { IPort } from "./NetscriptPort";
import { import {
NS as INS, NS as INS,
@ -442,6 +443,26 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
getServer: safeGetServer, getServer: safeGetServer,
checkSingularityAccess: checkSingularityAccess, checkSingularityAccess: checkSingularityAccess,
hack: hack, hack: hack,
getValidPort: (funcName:string, port: any): IPort => {
if (isNaN(port)) {
throw makeRuntimeErrorMsg(
funcName,
`Invalid argument. Must be a port number between 1 and ${CONSTANTS.NumNetscriptPorts}, is ${port}`,
);
}
port = Math.round(port);
if (port < 1 || port > CONSTANTS.NumNetscriptPorts) {
throw makeRuntimeErrorMsg(
funcName,
`Trying to use an invalid port: ${port}. Only ports 1-${CONSTANTS.NumNetscriptPorts} are valid.`,
);
}
const iport = NetscriptPorts[port - 1];
if (iport == null || !(iport instanceof Object)) {
throw makeRuntimeErrorMsg(funcName, `Could not find port: ${port}. This is a bug. Report to dev.`);
}
return iport;
}
}; };
const gang = NetscriptGang(Player, workerScript, helper); const gang = NetscriptGang(Player, workerScript, helper);
@ -952,7 +973,6 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return runScriptFromScript("run", scriptServer, scriptname, args, workerScript, threads); return runScriptFromScript("run", scriptServer, scriptname, args, workerScript, threads);
}, },
exec: function (scriptname: any, hostname: any, threads: any = 1, ...args: any[]): any { exec: function (scriptname: any, hostname: any, threads: any = 1, ...args: any[]): any {
console.log(`${scriptname} ${hostname} ${threads} ${JSON.stringify(args)}`);
updateDynamicRam("exec", getRamCost("exec")); updateDynamicRam("exec", getRamCost("exec"));
if (scriptname === undefined || hostname === undefined) { if (scriptname === undefined || hostname === undefined) {
throw makeRuntimeErrorMsg("exec", "Usage: exec(scriptname, server, [numThreads], [arg1], [arg2]...)"); throw makeRuntimeErrorMsg("exec", "Usage: exec(scriptname, server, [numThreads], [arg1], [arg2]...)");
@ -989,7 +1009,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
workerScript.log("spawn", () => "Exiting..."); workerScript.log("spawn", () => "Exiting...");
} }
}, },
kill: function (filename: any, hostname: any, ...scriptArgs: any): any { kill: function (filename: any, hostname?: any, ...scriptArgs: any): any {
updateDynamicRam("kill", getRamCost("kill")); updateDynamicRam("kill", getRamCost("kill"));
let res; let res;
@ -1737,25 +1757,13 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return res; return res;
}, },
writePort: function (port: any, data: any = ""): any { writePort: function (port: any, data: any = ""): any {
// Write to port
// Port 1-10
if (typeof data !== "string" && typeof data !== "number") { if (typeof data !== "string" && typeof data !== "number") {
throw makeRuntimeErrorMsg( throw makeRuntimeErrorMsg(
"writePort", "writePort",
`Trying to write invalid data to a port: only strings and numbers are valid.`, `Trying to write invalid data to a port: only strings and numbers are valid.`,
); );
} }
port = Math.round(port); const iport = helper.getValidPort("writePort", port);
if (port < 1 || port > CONSTANTS.NumNetscriptPorts) {
throw makeRuntimeErrorMsg(
"writePort",
`Trying to write to invalid port: ${port}. Only ports 1-${CONSTANTS.NumNetscriptPorts} are valid.`,
);
}
const iport = NetscriptPorts[port - 1];
if (iport == null || !(iport instanceof Object)) {
throw makeRuntimeErrorMsg("writePort", `Could not find port: ${port}. This is a bug. Report to dev.`);
}
return Promise.resolve(iport.write(data)); return Promise.resolve(iport.write(data));
}, },
write: function (port: any, data: any = "", mode: any = "a"): any { write: function (port: any, data: any = "", mode: any = "a"): any {
@ -1832,18 +1840,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
}, },
readPort: function (port: any): any { readPort: function (port: any): any {
// Read from port // Read from port
// Port 1-10 const iport = helper.getValidPort("readPort", port);
port = Math.round(port);
if (port < 1 || port > CONSTANTS.NumNetscriptPorts) {
throw makeRuntimeErrorMsg(
"readPort",
`Invalid port: ${port}. Only ports 1-${CONSTANTS.NumNetscriptPorts} are valid.`,
);
}
const iport = NetscriptPorts[port - 1];
if (iport == null || !(iport instanceof Object)) {
throw makeRuntimeErrorMsg("readPort", `Could not find port: ${port}. This is a bug. Report to dev.`);
}
const x = iport.read(); const x = iport.read();
return x; return x;
}, },
@ -1878,23 +1875,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
}, },
peek: function (port: any): any { peek: function (port: any): any {
updateDynamicRam("peek", getRamCost("peek")); updateDynamicRam("peek", getRamCost("peek"));
if (isNaN(port)) { const iport = helper.getValidPort("peek", port);
throw makeRuntimeErrorMsg(
"peek",
`Invalid argument. Must be a port number between 1 and ${CONSTANTS.NumNetscriptPorts}, is ${port}`,
);
}
port = Math.round(port);
if (port < 1 || port > CONSTANTS.NumNetscriptPorts) {
throw makeRuntimeErrorMsg(
"peek",
`Invalid argument. Must be a port number between 1 and ${CONSTANTS.NumNetscriptPorts}, is ${port}`,
);
}
const iport = NetscriptPorts[port - 1];
if (iport == null || !(iport instanceof Object)) {
throw makeRuntimeErrorMsg("peek", `Could not find port: ${port}. This is a bug. Report to dev.`);
}
const x = iport.peek(); const x = iport.peek();
return x; return x;
}, },
@ -1918,38 +1899,12 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
}, },
clearPort: function (port: any): any { clearPort: function (port: any): any {
// Clear port // Clear port
port = Math.round(port); const iport = helper.getValidPort("clearPort", port);
if (port < 1 || port > CONSTANTS.NumNetscriptPorts) {
throw makeRuntimeErrorMsg(
"clear",
`Trying to clear invalid port: ${port}. Only ports 1-${CONSTANTS.NumNetscriptPorts} are valid`,
);
}
const iport = NetscriptPorts[port - 1];
if (iport == null || !(iport instanceof Object)) {
throw makeRuntimeErrorMsg("clear", `Could not find port: ${port}. This is a bug. Report to dev.`);
}
return iport.clear(); return iport.clear();
}, },
getPortHandle: function (port: any): any { getPortHandle: function (port: any): any {
updateDynamicRam("getPortHandle", getRamCost("getPortHandle")); updateDynamicRam("getPortHandle", getRamCost("getPortHandle"));
if (isNaN(port)) { const iport = helper.getValidPort("getPortHandle", port);
throw makeRuntimeErrorMsg(
"getPortHandle",
`Invalid port: ${port} Must be an integer between 1 and ${CONSTANTS.NumNetscriptPorts}.`,
);
}
port = Math.round(port);
if (port < 1 || port > CONSTANTS.NumNetscriptPorts) {
throw makeRuntimeErrorMsg(
"getPortHandle",
`Invalid port: ${port}. Only ports 1-${CONSTANTS.NumNetscriptPorts} are valid.`,
);
}
const iport = NetscriptPorts[port - 1];
if (iport == null || !(iport instanceof Object)) {
throw makeRuntimeErrorMsg("getPortHandle", `Could not find port: ${port}. This is a bug. Report to dev.`);
}
return iport; return iport;
}, },
rm: function (fn: any, hostname: any): any { rm: function (fn: any, hostname: any): any {
@ -2070,7 +2025,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return calculateWeakenTime(server, Player) * 1000; return calculateWeakenTime(server, Player) * 1000;
}, },
getScriptIncome: function (scriptname: any, hostname: any, ...args: any[]): any { getScriptIncome: function (scriptname?: any, hostname?: any, ...args: any[]): any {
updateDynamicRam("getScriptIncome", getRamCost("getScriptIncome")); updateDynamicRam("getScriptIncome", getRamCost("getScriptIncome"));
if (arguments.length === 0) { if (arguments.length === 0) {
const res = []; const res = [];
@ -2099,7 +2054,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return runningScriptObj.onlineMoneyMade / runningScriptObj.onlineRunningTime; return runningScriptObj.onlineMoneyMade / runningScriptObj.onlineRunningTime;
} }
}, },
getScriptExpGain: function (scriptname: any, hostname: any, ...args: any[]): any { getScriptExpGain: function (scriptname?: any, hostname?: any, ...args: any[]): any {
updateDynamicRam("getScriptExpGain", getRamCost("getScriptExpGain")); updateDynamicRam("getScriptExpGain", getRamCost("getScriptExpGain"));
if (arguments.length === 0) { if (arguments.length === 0) {
let total = 0; let total = 0;
@ -2318,10 +2273,12 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
...base, ...base,
...extra, ...extra,
}; };
function getFunctionNames(obj: NS, prefix: string): string[] { function getFunctionNames(obj: any, prefix: string): string[] {
const functionNames: string[] = []; const functionNames: string[] = [];
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
if (typeof value == "function") { if (key === "args") {
continue;
} else if (typeof value == "function") {
functionNames.push(prefix + key); functionNames.push(prefix + key);
} else if (typeof value == "object") { } else if (typeof value == "object") {
functionNames.push(...getFunctionNames(value, key + ".")); functionNames.push(...getFunctionNames(value, key + "."));

@ -496,7 +496,7 @@ export function NetscriptCorporation(
shareSaleCooldown: corporation.shareSaleCooldown, shareSaleCooldown: corporation.shareSaleCooldown,
issuedShares: corporation.issuedShares, issuedShares: corporation.issuedShares,
sharePrice: corporation.sharePrice, sharePrice: corporation.sharePrice,
state: corporation.state + "", state: corporation.state.getState(),
}; };
}, },
}; };

@ -2,8 +2,10 @@ import { makeRuntimeRejectMsg } from "./NetscriptEvaluator";
import { ScriptUrl } from "./Script/ScriptUrl"; import { ScriptUrl } from "./Script/ScriptUrl";
import { WorkerScript } from "./Netscript/WorkerScript"; import { WorkerScript } from "./Netscript/WorkerScript";
import { Script } from "./Script/Script"; import { Script } from "./Script/Script";
import { computeHash } from "./utils/helpers/computeHash";
export const BlobsMap: { [key: string]: string } = {}; import { BlobCache } from "./utils/BlobCache";
import { ImportCache } from "./utils/ImportCache";
import { areImportsEquals } from "./Terminal/DirectoryHelpers";
// Makes a blob that contains the code of a given script. // Makes a blob that contains the code of a given script.
function makeScriptBlob(code: string): Blob { function makeScriptBlob(code: string): Blob {
@ -21,13 +23,22 @@ export async function compile(script: Script, scripts: Script[]): Promise<void>
// by placing it inside an eval call. // by placing it inside an eval call.
await script.updateRamUsage(scripts); await script.updateRamUsage(scripts);
const uurls = _getScriptUrls(script, scripts, []); const uurls = _getScriptUrls(script, scripts, []);
if (script.url) { const url = uurls[uurls.length - 1].url;
URL.revokeObjectURL(script.url); // remove the old reference. if (script.url && script.url !== url) {
delete BlobsMap[script.url]; // Thoughts: Should we be revoking any URLs here?
// If a script is modified repeatedly between two states,
// we could reuse the blob at a later time.
// BlobCache.removeByValue(script.url);
// URL.revokeObjectURL(script.url);
// if (script.dependencies.length > 0) {
// script.dependencies.forEach((dep) => {
// removeBlobFromCache(dep.url);
// URL.revokeObjectURL(dep.url);
// });
// }
} }
if (script.dependencies.length > 0) script.dependencies.forEach((dep) => URL.revokeObjectURL(dep.url)); script.url = url;
script.url = uurls[uurls.length - 1].url; script.module = new Promise((resolve) => resolve(eval("import(url)")));
script.module = new Promise((resolve) => resolve(eval("import(uurls[uurls.length - 1].url)")));
script.dependencies = uurls; script.dependencies = uurls;
} }
@ -117,18 +128,27 @@ function _getScriptUrls(script: Script, scripts: Script[], seen: Script[]): Scri
let transformedCode = script.code.replace( let transformedCode = script.code.replace(
/((?:from|import)\s+(?:'|"))(?:\.\/)?([^'"]+)('|")/g, /((?:from|import)\s+(?:'|"))(?:\.\/)?([^'"]+)('|")/g,
(unmodified, prefix, filename, suffix) => { (unmodified, prefix, filename, suffix) => {
const isAllowedImport = scripts.some((s) => s.filename == filename); const isAllowedImport = scripts.some((s) => areImportsEquals(s.filename, filename));
if (!isAllowedImport) return unmodified; if (!isAllowedImport) return unmodified;
// Find the corresponding script. // Find the corresponding script.
const [importedScript] = scripts.filter((s) => s.filename == filename); const [importedScript] = scripts.filter((s) => areImportsEquals(s.filename, filename));
// Try to get a URL for the requested script and its dependencies. // Check to see if the urls for this script are stored in the cache by the hash value.
const urls = _getScriptUrls(importedScript, scripts, seen); let urls = ImportCache.get(importedScript.hash());
// If we don't have it in the cache, then we need to generate the urls for it.
if (!urls) {
// Try to get a URL for the requested script and its dependencies.
urls = _getScriptUrls(importedScript, scripts, seen);
}
// The top url in the stack is the replacement import file for this script. // The top url in the stack is the replacement import file for this script.
urlStack.push(...urls); urlStack.push(...urls);
return [prefix, urls[urls.length - 1].url, suffix].join(""); const blob = urls[urls.length - 1].url;
ImportCache.store(importedScript.hash(), urls);
// Replace the blob inside the import statement.
return [prefix, blob, suffix].join("");
}, },
); );
@ -136,11 +156,19 @@ function _getScriptUrls(script: Script, scripts: Script[], seen: Script[]): Scri
// accidental calls to window.print() do not bring up the "print screen" dialog // accidental calls to window.print() do not bring up the "print screen" dialog
transformedCode += `\n\nfunction print() {throw new Error("Invalid call to window.print(). Did you mean to use Netscript's print()?");}`; transformedCode += `\n\nfunction print() {throw new Error("Invalid call to window.print(). Did you mean to use Netscript's print()?");}`;
// If we successfully transformed the code, create a blob url for it and // If we successfully transformed the code, create a blob url for it
// push that URL onto the top of the stack. // Compute the hash for the transformed code
const su = new ScriptUrl(script.filename, URL.createObjectURL(makeScriptBlob(transformedCode))); const transformedHash = computeHash(transformedCode);
urlStack.push(su); // Check to see if this transformed hash is in our cache
BlobsMap[su.url] = su.filename; let blob = BlobCache.get(transformedHash);
if (!blob) {
blob = URL.createObjectURL(makeScriptBlob(transformedCode));
}
// Store this blob in the cache. Any script that transforms the same
// (e.g. same scripts on server, same hash value, etc) can use this blob url.
BlobCache.store(transformedHash, blob);
// Push the blob URL onto the top of the stack.
urlStack.push(new ScriptUrl(script.filename, blob));
return urlStack; return urlStack;
} catch (err) { } catch (err) {
// If there is an error, we need to clean up the URLs. // If there is an error, we need to clean up the URLs.

@ -81,7 +81,7 @@ function startNetscript2Script(workerScript: WorkerScript): Promise<WorkerScript
if (propName === "asleep") return f(...args); // OK for multiple simultaneous calls to sleep. if (propName === "asleep") return f(...args); // OK for multiple simultaneous calls to sleep.
const msg = const msg =
"Concurrent calls to Netscript functions not allowed! " + "Concurrent calls to Netscript functions are not allowed! " +
"Did you forget to await hack(), grow(), or some other " + "Did you forget to await hack(), grow(), or some other " +
"promise-returning function? (Currently running: %s tried to run: %s)"; "promise-returning function? (Currently running: %s tried to run: %s)";
if (runningFn) { if (runningFn) {
@ -610,7 +610,7 @@ export function updateOnlineScriptTimes(numCycles = 1): void {
export function loadAllRunningScripts(): void { export function loadAllRunningScripts(): void {
const skipScriptLoad = window.location.href.toLowerCase().indexOf("?noscripts") !== -1; const skipScriptLoad = window.location.href.toLowerCase().indexOf("?noscripts") !== -1;
if (skipScriptLoad) { if (skipScriptLoad) {
Terminal.warn('Skipped loading player scripts during startup'); Terminal.warn("Skipped loading player scripts during startup");
console.info("Skipping the load of any scripts during startup"); console.info("Skipping the load of any scripts during startup");
} }
for (const server of GetAllServers()) { for (const server of GetAllServers()) {
@ -687,7 +687,7 @@ export function runScriptFromScript(
// Check for admin rights and that there is enough RAM availble to run // Check for admin rights and that there is enough RAM availble to run
const script = server.scripts[i]; const script = server.scripts[i];
let ramUsage = script.ramUsage; let ramUsage = script.ramUsage;
threads = Math.round(Number(threads)); threads = Math.floor(Number(threads));
if (threads === 0) { if (threads === 0) {
return 0; return 0;
} }

@ -654,6 +654,8 @@ export function finishWork(this: IPlayer, cancelled: boolean, sing = false): str
this.workRepGained *= this.cancelationPenalty(); this.workRepGained *= this.cancelationPenalty();
} }
const penaltyString = this.cancelationPenalty() === 0.5 ? "half" : "three-quarters";
const company = Companies[this.companyName]; const company = Companies[this.companyName];
company.playerReputation += this.workRepGained; company.playerReputation += this.workRepGained;
@ -665,12 +667,12 @@ export function finishWork(this: IPlayer, cancelled: boolean, sing = false): str
<Money money={this.workMoneyGained} /> <Money money={this.workMoneyGained} />
<br /> <br />
<Reputation reputation={this.workRepGained} /> reputation for the company <br /> <Reputation reputation={this.workRepGained} /> reputation for the company <br />
{numeralWrapper.formatExp(this.workHackExpGained)} hacking exp <br /> {this.workHackExpGained > 0 && <>{numeralWrapper.formatExp(this.workHackExpGained)} hacking exp <br /></>}
{numeralWrapper.formatExp(this.workStrExpGained)} strength exp <br /> {this.workStrExpGained > 0 && <>{numeralWrapper.formatExp(this.workStrExpGained)} strength exp <br /></>}
{numeralWrapper.formatExp(this.workDefExpGained)} defense exp <br /> {this.workDefExpGained > 0 && <>{numeralWrapper.formatExp(this.workDefExpGained)} defense exp <br /></>}
{numeralWrapper.formatExp(this.workDexExpGained)} dexterity exp <br /> {this.workDexExpGained > 0 && <>{numeralWrapper.formatExp(this.workDexExpGained)} dexterity exp <br /></>}
{numeralWrapper.formatExp(this.workAgiExpGained)} agility exp <br /> {this.workAgiExpGained > 0 && <>{numeralWrapper.formatExp(this.workAgiExpGained)} agility exp <br /></>}
{numeralWrapper.formatExp(this.workChaExpGained)} charisma exp {this.workChaExpGained > 0 && <>{numeralWrapper.formatExp(this.workChaExpGained)} charisma exp <br /></>}
<br /> <br />
</> </>
); );
@ -680,7 +682,7 @@ export function finishWork(this: IPlayer, cancelled: boolean, sing = false): str
<> <>
You worked a short shift of {convertTimeMsToTimeElapsedString(this.timeWorked)} <br /> You worked a short shift of {convertTimeMsToTimeElapsedString(this.timeWorked)} <br />
<br /> <br />
Since you cancelled your work early, you only gained half of the reputation you earned. <br /> Since you cancelled your work early, you only gained {penaltyString} of the reputation you earned. <br />
<br /> <br />
{content} {content}
</> </>
@ -1688,6 +1690,11 @@ export function applyForJob(this: IPlayer, entryPosType: CompanyPosition, sing =
return false; return false;
} }
// Check if this company has the position
if (!company.hasPosition(pos)) {
return false;
}
while (true) { while (true) {
const newPos = getNextCompanyPositionHelper(pos); const newPos = getNextCompanyPositionHelper(pos);
if (newPos == null) { if (newPos == null) {
@ -1861,9 +1868,14 @@ export function applyForAgentJob(this: IPlayer, sing = false): boolean {
export function applyForEmployeeJob(this: IPlayer, sing = false): boolean { export function applyForEmployeeJob(this: IPlayer, sing = false): boolean {
const company = Companies[this.location]; //Company being applied to const company = Companies[this.location]; //Company being applied to
if (this.isQualified(company, CompanyPositions[posNames.MiscCompanyPositions[1]])) { const position = posNames.MiscCompanyPositions[1];
// Check if this company has the position
if (!company.hasPosition(position)) {
return false;
}
if (this.isQualified(company, CompanyPositions[position])) {
this.companyName = company.name; this.companyName = company.name;
this.jobs[company.name] = posNames.MiscCompanyPositions[1]; this.jobs[company.name] = position;
if (!sing) { if (!sing) {
dialogBoxCreate("Congratulations, you are now employed at " + this.location); dialogBoxCreate("Congratulations, you are now employed at " + this.location);
} }
@ -1880,8 +1892,13 @@ export function applyForEmployeeJob(this: IPlayer, sing = false): boolean {
export function applyForPartTimeEmployeeJob(this: IPlayer, sing = false): boolean { export function applyForPartTimeEmployeeJob(this: IPlayer, sing = false): boolean {
const company = Companies[this.location]; //Company being applied to const company = Companies[this.location]; //Company being applied to
if (this.isQualified(company, CompanyPositions[posNames.PartTimeCompanyPositions[1]])) { const position = posNames.PartTimeCompanyPositions[1];
this.jobs[company.name] = posNames.PartTimeCompanyPositions[1]; // Check if this company has the position
if (!company.hasPosition(position)) {
return false;
}
if (this.isQualified(company, CompanyPositions[position])) {
this.jobs[company.name] = position;
if (!sing) { if (!sing) {
dialogBoxCreate("Congratulations, you are now employed part-time at " + this.location); dialogBoxCreate("Congratulations, you are now employed part-time at " + this.location);
} }
@ -1898,9 +1915,14 @@ export function applyForPartTimeEmployeeJob(this: IPlayer, sing = false): boolea
export function applyForWaiterJob(this: IPlayer, sing = false): boolean { export function applyForWaiterJob(this: IPlayer, sing = false): boolean {
const company = Companies[this.location]; //Company being applied to const company = Companies[this.location]; //Company being applied to
if (this.isQualified(company, CompanyPositions[posNames.MiscCompanyPositions[0]])) { const position = posNames.MiscCompanyPositions[0];
// Check if this company has the position
if (!company.hasPosition(position)) {
return false;
}
if (this.isQualified(company, CompanyPositions[position])) {
this.companyName = company.name; this.companyName = company.name;
this.jobs[company.name] = posNames.MiscCompanyPositions[0]; this.jobs[company.name] = position;
if (!sing) { if (!sing) {
dialogBoxCreate("Congratulations, you are now employed as a waiter at " + this.location); dialogBoxCreate("Congratulations, you are now employed as a waiter at " + this.location);
} }
@ -1915,9 +1937,14 @@ export function applyForWaiterJob(this: IPlayer, sing = false): boolean {
export function applyForPartTimeWaiterJob(this: IPlayer, sing = false): boolean { export function applyForPartTimeWaiterJob(this: IPlayer, sing = false): boolean {
const company = Companies[this.location]; //Company being applied to const company = Companies[this.location]; //Company being applied to
if (this.isQualified(company, CompanyPositions[posNames.PartTimeCompanyPositions[0]])) { const position = posNames.PartTimeCompanyPositions[0];
// Check if this company has the position
if (!company.hasPosition(position)) {
return false;
}
if (this.isQualified(company, CompanyPositions[position])) {
this.companyName = company.name; this.companyName = company.name;
this.jobs[company.name] = posNames.PartTimeCompanyPositions[0]; this.jobs[company.name] = position;
if (!sing) { if (!sing) {
dialogBoxCreate("Congratulations, you are now employed as a part-time waiter at " + this.location); dialogBoxCreate("Congratulations, you are now employed as a part-time waiter at " + this.location);
} }

@ -17,6 +17,17 @@ export function calculateSkillProgress(exp: number, mult = 1): ISkillProgress {
if (nextExperience < 0) nextExperience = 0; if (nextExperience < 0) nextExperience = 0;
const normalize = (value: number): number => ((value - baseExperience) * 100) / (nextExperience - baseExperience); const normalize = (value: number): number => ((value - baseExperience) * 100) / (nextExperience - baseExperience);
let progress = (nextExperience - baseExperience !== 0) ? normalize(exp) : 99.99;
// Clamp progress: When sleeves are working out, the player gets way too much progress
if (progress < 0) progress = 0
if (progress > 100) progress = 100;
// Clamp floating point imprecisions
let currentExperience = exp - baseExperience;
let remainingExperience = nextExperience - exp;
if (currentExperience < 0) currentExperience = 0;
if (remainingExperience < 0) remainingExperience = 0;
return { return {
currentSkill, currentSkill,
@ -24,7 +35,9 @@ export function calculateSkillProgress(exp: number, mult = 1): ISkillProgress {
baseExperience, baseExperience,
experience: exp, experience: exp,
nextExperience, nextExperience,
progress: (nextExperience - baseExperience !== 0) ? normalize(exp) : 99.99 currentExperience,
remainingExperience,
progress
} }
} }
@ -34,6 +47,8 @@ export interface ISkillProgress {
baseExperience: number; baseExperience: number;
experience: number; experience: number;
nextExperience: number; nextExperience: number;
currentExperience: number;
remainingExperience: number;
progress: number; progress: number;
} }
@ -41,6 +56,7 @@ export function getEmptySkillProgress(): ISkillProgress {
return { return {
currentSkill: 0, nextSkill: 0, currentSkill: 0, nextSkill: 0,
baseExperience: 0, experience: 0, nextExperience: 0, baseExperience: 0, experience: 0, nextExperience: 0,
currentExperience: 0, remainingExperience: 0,
progress: 0, progress: 0,
}; };
} }

@ -21,6 +21,7 @@ import { save } from "./db";
import { v1APIBreak } from "./utils/v1APIBreak"; import { v1APIBreak } from "./utils/v1APIBreak";
import { AugmentationNames } from "./Augmentation/data/AugmentationNames"; import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation"; import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation";
import { LocationName } from "./Locations/data/LocationNames";
/* SaveObject.js /* SaveObject.js
* Defines the object used to save/load games * Defines the object used to save/load games
@ -233,6 +234,13 @@ function evaluateVersionCompatibility(ver: string | number): void {
} }
} }
} }
if (ver < 9) {
if (StockMarket.hasOwnProperty("Joes Guns")) {
const s = StockMarket["Joes Guns"];
delete StockMarket["Joes Guns"];
StockMarket[LocationName.Sector12JoesGuns] = s;
}
}
} }
} }

@ -13,6 +13,7 @@ import { RamCalculationErrorCode } from "./RamCalculationErrorCodes";
import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator"; import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator";
import { Script } from "../Script/Script"; import { Script } from "../Script/Script";
import { WorkerScript } from "../Netscript/WorkerScript"; import { WorkerScript } from "../Netscript/WorkerScript";
import { areImportsEquals } from "../Terminal/DirectoryHelpers";
// These special strings are used to reference the presence of a given logical // These special strings are used to reference the presence of a given logical
// construct within a user script. // construct within a user script.
@ -106,7 +107,7 @@ async function parseOnlyRamCalculate(
let script = null; let script = null;
const fn = nextModule.startsWith("./") ? nextModule.slice(2) : nextModule; const fn = nextModule.startsWith("./") ? nextModule.slice(2) : nextModule;
for (const s of otherScripts) { for (const s of otherScripts) {
if (s.filename === fn) { if (areImportsEquals(s.filename, fn)) {
script = s; script = s;
break; break;
} }

@ -3,6 +3,7 @@
* A Script can have multiple active instances * A Script can have multiple active instances
*/ */
import { Script } from "./Script"; import { Script } from "./Script";
import { ScriptUrl } from "./ScriptUrl";
import { Settings } from "../Settings/Settings"; import { Settings } from "../Settings/Settings";
import { IMap } from "../types"; import { IMap } from "../types";
import { Terminal } from "../Terminal"; import { Terminal } from "../Terminal";
@ -58,6 +59,9 @@ export class RunningScript {
// Number of threads that this script is running with // Number of threads that this script is running with
threads = 1; threads = 1;
// Script urls for the current running script for translating urls back to file names in errors
dependencies: ScriptUrl[] = [];
constructor(script: Script | null = null, args: any[] = []) { constructor(script: Script | null = null, args: any[] = []) {
if (script == null) { if (script == null) {
return; return;
@ -66,6 +70,7 @@ export class RunningScript {
this.args = args; this.args = args;
this.server = script.server; this.server = script.server;
this.ramUsage = script.ramUsage; this.ramUsage = script.ramUsage;
this.dependencies = script.dependencies;
} }
log(txt: string): void { log(txt: string): void {

@ -9,6 +9,7 @@ import { ScriptUrl } from "./ScriptUrl";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver"; import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver";
import { roundToTwo } from "../utils/helpers/roundToTwo"; import { roundToTwo } from "../utils/helpers/roundToTwo";
import { computeHash } from "../utils/helpers/computeHash";
let globalModuleSequenceNumber = 0; let globalModuleSequenceNumber = 0;
@ -40,6 +41,9 @@ export class Script {
// hostname of server that this script is on. // hostname of server that this script is on.
server = ""; server = "";
// sha256 hash of the code in the Script. Do not access directly.
_hash = "";
constructor(fn = "", code = "", server = "", otherScripts: Script[] = []) { constructor(fn = "", code = "", server = "", otherScripts: Script[] = []) {
this.filename = fn; this.filename = fn;
this.code = code; this.code = code;
@ -47,8 +51,10 @@ export class Script {
this.server = server; // hostname of server this script is on this.server = server; // hostname of server this script is on
this.module = ""; this.module = "";
this.moduleSequenceNumber = ++globalModuleSequenceNumber; this.moduleSequenceNumber = ++globalModuleSequenceNumber;
this._hash = "";
if (this.code !== "") { if (this.code !== "") {
this.updateRamUsage(otherScripts); this.updateRamUsage(otherScripts);
this.rehash();
} }
} }
@ -84,6 +90,24 @@ export class Script {
markUpdated(): void { markUpdated(): void {
this.module = ""; this.module = "";
this.moduleSequenceNumber = ++globalModuleSequenceNumber; this.moduleSequenceNumber = ++globalModuleSequenceNumber;
this.rehash();
}
/**
* Force update of the computed hash based on the source code.
*/
rehash(): void {
this._hash = computeHash(this.code);
}
/**
* If the hash is not computed, computes the hash. Otherwise return the computed hash.
* @returns the computed hash of the script
*/
hash(): string {
if (!this._hash)
this.rehash();
return this._hash;
} }
/** /**
@ -125,7 +149,12 @@ export class Script {
// Initializes a Script Object from a JSON save state // Initializes a Script Object from a JSON save state
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
static fromJSON(value: any): Script { static fromJSON(value: any): Script {
return Generic_fromJSON(Script, value.data); const s = Generic_fromJSON(Script, value.data);
// Force the url to blank from the save data. Urls are not valid outside the current browser page load.
s.url = "";
// Rehash the code to ensure that hash is set properly.
s.rehash();
return s;
} }
} }

@ -1045,7 +1045,7 @@ export interface TIX {
getPurchaseCost(sym: string, shares: number, posType: string): number; getPurchaseCost(sym: string, shares: number, posType: string): number;
/** /**
* Calculate profit of setting stocks. * Calculate profit of selling stocks.
* @remarks * @remarks
* RAM cost: 2 GB * RAM cost: 2 GB
* Calculates and returns how much you would gain from selling a given number of shares of a stock. * Calculates and returns how much you would gain from selling a given number of shares of a stock.
@ -1767,7 +1767,7 @@ export interface Singularity {
* *
* This function returns the number of milliseconds it takes to attempt the * This function returns the number of milliseconds it takes to attempt the
* specified crime (e.g It takes 60 seconds to attempt the Rob Store crime, * specified crime (e.g It takes 60 seconds to attempt the Rob Store crime,
* so running `commitCrime('rob store')` will return 60000). * so running `commitCrime('rob store')` will return 60,000).
* *
* Warning: I do not recommend using the time returned from this function to try * Warning: I do not recommend using the time returned from this function to try
* and schedule your crime attempts. Instead, I would use the isBusy Singularity * and schedule your crime attempts. Instead, I would use the isBusy Singularity
@ -2890,7 +2890,7 @@ export interface Bladeburner {
} }
/** /**
* Coding Contact API * Coding Contract API
* @public * @public
*/ */
export interface CodingContract { export interface CodingContract {
@ -4079,31 +4079,28 @@ export interface NS extends Singularity {
hackAnalyzeThreads(host: string, hackAmount: number): number; hackAnalyzeThreads(host: string, hackAmount: number): number;
/** /**
* Get the percent of money stolen with a single thread. * Get the part of money stolen with a single thread.
* @remarks * @remarks
* RAM cost: 1 GB * RAM cost: 1 GB
* *
* Returns the percentage of the specified servers money you will steal with a single hack. * Returns the part of the specified servers money you will steal with a single thread hack.
* This value is returned in percentage form, not decimal
* (Netscript functions typically return in decimal form, but not this one).
* This percentage is influenced by the player's hacking skill.
* *
* @example * @example
* ```ts * ```ts
* // NS1: * // NS1:
* //For example, assume the following returns 0.01: * //For example, assume the following returns 0.01:
* var hackAmount = hackAnalyze("foodnstuff"); * var hackAmount = hackAnalyze("foodnstuff");
* //This means that if hack the foodnstuff server, then you will steal 1% of its total money. If you hack using N threads, then you will steal N*0.01 times its total money. * //This means that if hack the foodnstuff server using a single thread, then you will steal 1%, or 0.01 of its total money. If you hack using N threads, then you will steal N*0.01 times its total money.
* ``` * ```
* @example * @example
* ```ts * ```ts
* // NS2: * // NS2:
* //For example, assume the following returns 0.01: * //For example, assume the following returns 0.01:
* const hackAmount = ns.hackAnalyze("foodnstuff"); * const hackAmount = ns.hackAnalyze("foodnstuff");
* //This means that if hack the foodnstuff server, then you will steal 1% of its total money. If you hack using N threads, then you will steal N*0.01 times its total money. * //This means that if hack the foodnstuff server using a single thread, then you will steal 1%, or 0.01 of its total money. If you hack using N threads, then you will steal N*0.01 times its total money.
* ``` * ```
* @param host - Hostname of the target server. * @param host - Hostname of the target server.
* @returns The percentage of money you will steal from the target server with a single hack. * @returns The part of money you will steal from the target server with a single thread hack.
*/ */
hackAnalyze(host: string): number; hackAnalyze(host: string): number;
@ -4560,7 +4557,7 @@ export interface NS extends Singularity {
* @param args - Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the second argument numThreads must be filled in with a value. * @param args - Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the second argument numThreads must be filled in with a value.
* @returns Returns the PID of a successfully started script, and 0 otherwise. * @returns Returns the PID of a successfully started script, and 0 otherwise.
*/ */
run(script: string, numThreads?: number, ...args: string[]): number; run(script: string, numThreads?: number, ...args: Array<string | number | boolean>): number;
/** /**
* Start another script on any server. * Start another script on any server.
@ -4680,7 +4677,8 @@ export interface NS extends Singularity {
* @param args - Arguments to identify which script to kill. * @param args - Arguments to identify which script to kill.
* @returns True if the script is successfully killed, and false otherwise. * @returns True if the script is successfully killed, and false otherwise.
*/ */
kill(script: string | number, host: string, ...args: string[]): boolean; kill(script: number): boolean;
kill(script: string, host: string, ...args: string[]): boolean;
/** /**
* Terminate all scripts on a server. * Terminate all scripts on a server.
@ -4690,11 +4688,12 @@ export interface NS extends Singularity {
* Kills all running scripts on the specified server. This function returns true * Kills all running scripts on the specified server. This function returns true
* if any scripts were killed, and false otherwise. In other words, it will return * if any scripts were killed, and false otherwise. In other words, it will return
* true if there are any scripts running on the target server. * true if there are any scripts running on the target server.
* If no host is defined, it will kill all scripts, where the script is running.
* *
* @param host - IP or hostname of the server on which to kill all scripts. * @param host - IP or hostname of the server on which to kill all scripts.
* @returns True if any scripts were killed, and false otherwise. * @returns True if any scripts were killed, and false otherwise.
*/ */
killall(host: string): boolean; killall(host?: string): boolean;
/** /**
* Terminates the current script immediately. * Terminates the current script immediately.
@ -5153,7 +5152,7 @@ export interface NS extends Singularity {
* *
* @returns info about a running script * @returns info about a running script
*/ */
getRunningScript(filename: string | number, hostname: string, ...args: (string | number)[]): RunningScript; getRunningScript(filename?: string | number, hostname?: string, ...args: (string | number)[]): RunningScript;
/** /**
* Get cost of purchasing a server. * Get cost of purchasing a server.
@ -5287,7 +5286,7 @@ export interface NS extends Singularity {
* @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.
*/ */
write(handle: string, data?: string[] | number, mode?: "w" | "a"): Promise<void>; write(handle: string, data?: string[] | number | string, mode?: "w" | "a"): Promise<void>;
/** /**
* Attempt to write to a port. * Attempt to write to a port.
@ -5486,8 +5485,6 @@ export interface NS extends Singularity {
* The required time is increased by the security level of the target server and decreased by the player's hacking level. * The required time is increased by the security level of the target server and decreased by the player's hacking level.
* *
* @param host - Host of target server. * @param host - Host of target server.
* @param hackLvl - Optional hacking level for the calculation. Defaults to players current hacking level.
* @param intLvl - Optional intelligence level for the calculation. Defaults to players current intelligence level. (Intelligence is unlocked after obtaining Source-File 5).
* @returns Returns the amount of time in milliseconds it takes to execute the hack Netscript function. Returns Infinity if called on a Hacknet Server. * @returns Returns the amount of time in milliseconds it takes to execute the hack Netscript function. Returns Infinity if called on a Hacknet Server.
*/ */
getHackTime(host: string): number; getHackTime(host: string): number;
@ -5502,8 +5499,6 @@ export interface NS extends Singularity {
* The required time is increased by the security level of the target server and decreased by the player's hacking level. * The required time is increased by the security level of the target server and decreased by the player's hacking level.
* *
* @param host - Host of target server. * @param host - Host of target server.
* @param hackLvl - Optional hacking level for the calculation. Defaults to players current hacking level.
* @param intLvl - Optional intelligence level for the calculation. Defaults to players current intelligence level. (Intelligence is unlocked after obtaining Source-File 5).
* @returns Returns the amount of time in milliseconds it takes to execute the grow Netscript function. Returns Infinity if called on a Hacknet Server. * @returns Returns the amount of time in milliseconds it takes to execute the grow Netscript function. Returns Infinity if called on a Hacknet Server.
*/ */
getGrowTime(host: string): number; getGrowTime(host: string): number;
@ -5518,8 +5513,6 @@ export interface NS extends Singularity {
* The required time is increased by the security level of the target server and decreased by the player's hacking level. * The required time is increased by the security level of the target server and decreased by the player's hacking level.
* *
* @param host - Host of target server. * @param host - Host of target server.
* @param hackLvl - Optional hacking level for the calculation. Defaults to players current hacking level.
* @param intLvl - Optional intelligence level for the calculation. Defaults to players current intelligence level. (Intelligence is unlocked after obtaining Source-File 5).
* @returns Returns the amount of time in milliseconds it takes to execute the grow Netscript function. Returns Infinity if called on a Hacknet Server. * @returns Returns the amount of time in milliseconds it takes to execute the grow Netscript function. Returns Infinity if called on a Hacknet Server.
*/ */
getWeakenTime(host: string): number; getWeakenTime(host: string): number;
@ -5548,7 +5541,8 @@ export interface NS extends Singularity {
* @param args - Arguments that the script is running with. * @param args - Arguments that the script is running with.
* @returns Amount of income the specified script generates while online. * @returns Amount of income the specified script generates while online.
*/ */
getScriptIncome(script: string, host: string, ...args: string[]): number | [number, number]; getScriptIncome(): [number, number];
getScriptIncome(script: string, host: string, ...args: string[]): number;
/** /**
* Get the exp gain of a script. * Get the exp gain of a script.
@ -5567,6 +5561,7 @@ export interface NS extends Singularity {
* @param args - Arguments that the script is running with. * @param args - Arguments that the script is running with.
* @returns Amount of hacking experience the specified script generates while online. * @returns Amount of hacking experience the specified script generates while online.
*/ */
getScriptExpGain(): number;
getScriptExpGain(script: string, host: string, ...args: string[]): number; getScriptExpGain(script: string, host: string, ...args: string[]): number;
/** /**
@ -6127,7 +6122,7 @@ interface CorporationInfo {
issuedShares: number; issuedShares: number;
/** Price of the shares */ /** Price of the shares */
sharePrice: number; sharePrice: number;
/** State of the corporation, like PRODUCTION or EXPORT */ /** State of the corporation. Possible states are START, PURCHASE, PRODUCTION, SALE, EXPORT. */
state: string; state: string;
} }

@ -49,6 +49,7 @@ export function OptionsModal(props: IProps): React.ReactElement {
<MenuItem value="solarized-light">solarized-light</MenuItem> <MenuItem value="solarized-light">solarized-light</MenuItem>
<MenuItem value="vs-dark">dark</MenuItem> <MenuItem value="vs-dark">dark</MenuItem>
<MenuItem value="light">light</MenuItem> <MenuItem value="light">light</MenuItem>
<MenuItem value="dracula">dracula</MenuItem>
</Select> </Select>
</Box> </Box>

@ -93,26 +93,21 @@ class OpenScript {
} }
} }
let openScripts: OpenScript[] = [];
let currentScript: OpenScript | null = null;
// Called every time script editor is opened // Called every time script editor is opened
export function Root(props: IProps): React.ReactElement { export function Root(props: IProps): React.ReactElement {
const setRerender = useState(false)[1];
function rerender(): void {
setRerender((o) => !o);
}
const editorRef = useRef<IStandaloneCodeEditor | null>(null); const editorRef = useRef<IStandaloneCodeEditor | null>(null);
const monacoRef = useRef<Monaco | null>(null); const monacoRef = useRef<Monaco | null>(null);
const vimStatusRef = useRef<HTMLElement>(null); const vimStatusRef = useRef<HTMLElement>(null);
const [vimEditor, setVimEditor] = useState<any>(null); const [vimEditor, setVimEditor] = useState<any>(null);
const [editor, setEditor] = useState<IStandaloneCodeEditor | null>(null); const [editor, setEditor] = useState<IStandaloneCodeEditor | null>(null);
const [openScripts, setOpenScripts] = useState<OpenScript[]>(
window.localStorage.getItem("scriptEditorOpenScripts") !== null
? JSON.parse(window.localStorage.getItem("scriptEditorOpenScripts")!)
: [],
);
const [currentScript, setCurrentScript] = useState<OpenScript | null>(
window.localStorage.getItem("scriptEditorCurrentScript") !== null
? JSON.parse(window.localStorage.getItem("scriptEditorCurrentScript")!)
: null,
);
const [ram, setRAM] = useState("RAM: ???"); const [ram, setRAM] = useState("RAM: ???");
const [updatingRam, setUpdatingRam] = useState(false); const [updatingRam, setUpdatingRam] = useState(false);
const [decorations, setDecorations] = useState<string[]>([]); const [decorations, setDecorations] = useState<string[]>([]);
@ -144,26 +139,6 @@ export function Root(props: IProps): React.ReactElement {
}; };
}, []); }, []);
useEffect(() => {
// Save currentScript
window.localStorage.setItem(
"scriptEditorCurrentScript",
JSON.stringify(currentScript, (key, value) => {
if (key == "model") return undefined;
return value;
}),
);
// Save openScripts
window.localStorage.setItem(
"scriptEditorOpenScripts",
JSON.stringify(openScripts, (key, value) => {
if (key == "model") return undefined;
return value;
}),
);
}, [currentScript, openScripts]);
useEffect(() => { useEffect(() => {
if (currentScript !== null) { if (currentScript !== null) {
updateRAM(currentScript.code); updateRAM(currentScript.code);
@ -171,23 +146,23 @@ export function Root(props: IProps): React.ReactElement {
}, []); }, []);
useEffect(() => { useEffect(() => {
function maybeSave(event: KeyboardEvent): void { function keydown(event: KeyboardEvent): void {
if (Settings.DisableHotkeys) return; if (Settings.DisableHotkeys) return;
//Ctrl + b //Ctrl + b
if (event.keyCode == 66 && (event.ctrlKey || event.metaKey)) { if (event.code == "KeyB" && (event.ctrlKey || event.metaKey)) {
event.preventDefault(); event.preventDefault();
save(); props.router.toTerminal();
} }
// CTRL/CMD + S // CTRL/CMD + S
if (event.code == `KeyS` && (event.ctrlKey || event.metaKey)) { if (event.code == "KeyS" && (event.ctrlKey || event.metaKey)) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
save(); save();
} }
} }
document.addEventListener("keydown", maybeSave); document.addEventListener("keydown", keydown);
return () => document.removeEventListener("keydown", maybeSave); return () => document.removeEventListener("keydown", keydown);
}); });
useEffect(() => { useEffect(() => {
@ -203,7 +178,12 @@ export function Root(props: IProps): React.ReactElement {
save(); save();
}); });
MonacoVim.VimMode.Vim.defineEx("quit", "q", function () { MonacoVim.VimMode.Vim.defineEx("quit", "q", function () {
props.router.toTerminal();
});
// "wqriteandquit" is not a typo, prefix must be found in full string
MonacoVim.VimMode.Vim.defineEx("wqriteandquit", "wq", function () {
save(); save();
props.router.toTerminal();
}); });
editor.focus(); editor.focus();
}); });
@ -362,7 +342,7 @@ export function Root(props: IProps): React.ReactElement {
regenerateModel(openScript); regenerateModel(openScript);
} }
setCurrentScript(openScript); currentScript = openScript;
editorRef.current.setModel(openScript.model); editorRef.current.setModel(openScript.model);
editorRef.current.setPosition(openScript.lastPosition); editorRef.current.setPosition(openScript.lastPosition);
editorRef.current.revealLineInCenter(openScript.lastPosition.lineNumber); editorRef.current.revealLineInCenter(openScript.lastPosition.lineNumber);
@ -376,14 +356,12 @@ export function Root(props: IProps): React.ReactElement {
new monacoRef.current.Position(0, 0), new monacoRef.current.Position(0, 0),
monacoRef.current.editor.createModel(code, filename.endsWith(".txt") ? "plaintext" : "javascript"), monacoRef.current.editor.createModel(code, filename.endsWith(".txt") ? "plaintext" : "javascript"),
); );
setOpenScripts((oldArray) => [...oldArray, newScript]); openScripts.push(newScript);
setCurrentScript({ ...newScript }); currentScript = { ...newScript };
editorRef.current.setModel(newScript.model); editorRef.current.setModel(newScript.model);
updateRAM(newScript.code); updateRAM(newScript.code);
} }
} }
} else {
console.log("here we need to load something if we can");
} }
editorRef.current.focus(); editorRef.current.focus();
@ -422,24 +400,26 @@ export function Root(props: IProps): React.ReactElement {
function updateCode(newCode?: string): void { function updateCode(newCode?: string): void {
if (newCode === undefined) return; if (newCode === undefined) return;
updateRAM(newCode); updateRAM(newCode);
if (editorRef.current !== null) { if (editorRef.current === null) return;
const newPos = editorRef.current.getPosition(); const newPos = editorRef.current.getPosition();
if (newPos === null) return; if (newPos === null) return;
setCurrentScript((oldScript) => ({ ...oldScript!, code: newCode, lastPosition: newPos! })); if (currentScript !== null) {
if (currentScript !== null) { currentScript = { ...currentScript, code: newCode, lastPosition: newPos };
const curIndex = openScripts.findIndex( const curIndex = openScripts.findIndex(
(script) => script.fileName === currentScript.fileName && script.hostname === currentScript.hostname, (script) =>
); currentScript !== null &&
const newArr = [...openScripts]; script.fileName === currentScript.fileName &&
const tempScript = currentScript; script.hostname === currentScript.hostname,
tempScript.code = newCode; );
newArr[curIndex] = tempScript; const newArr = [...openScripts];
setOpenScripts([...newArr]); const tempScript = currentScript;
} tempScript.code = newCode;
try { newArr[curIndex] = tempScript;
infLoop(newCode); openScripts = [...newArr];
} catch (err) {}
} }
try {
infLoop(newCode);
} catch (err) {}
} }
function saveScript(scriptToSave: OpenScript): void { function saveScript(scriptToSave: OpenScript): void {
@ -487,7 +467,7 @@ export function Root(props: IProps): React.ReactElement {
function save(): void { function save(): void {
if (currentScript === null) { if (currentScript === null) {
console.log("currentScript is null when it shouldn't be. Unabel to save script"); console.error("currentScript is null when it shouldn't be. Unable to save script");
return; return;
} }
// this is duplicate code with saving later. // this is duplicate code with saving later.
@ -507,7 +487,6 @@ export function Root(props: IProps): React.ReactElement {
iTutorialNextStep(); iTutorialNextStep();
props.router.toTerminal();
return; return;
} }
@ -536,7 +515,6 @@ export function Root(props: IProps): React.ReactElement {
server.scripts, server.scripts,
); );
if (Settings.SaveGameOnFileSave) saveObject.saveGame(); if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
return; return;
} }
} }
@ -550,7 +528,6 @@ export function Root(props: IProps): React.ReactElement {
if (server.textFiles[i].fn === currentScript.fileName) { if (server.textFiles[i].fn === currentScript.fileName) {
server.textFiles[i].write(currentScript.code); server.textFiles[i].write(currentScript.code);
if (Settings.SaveGameOnFileSave) saveObject.saveGame(); if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
return; return;
} }
} }
@ -562,7 +539,6 @@ export function Root(props: IProps): React.ReactElement {
} }
if (Settings.SaveGameOnFileSave) saveObject.saveGame(); if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
} }
function reorder(list: Array<OpenScript>, startIndex: number, endIndex: number): OpenScript[] { function reorder(list: Array<OpenScript>, startIndex: number, endIndex: number): OpenScript[] {
@ -582,19 +558,22 @@ export function Root(props: IProps): React.ReactElement {
const items = reorder(openScripts, result.source.index, result.destination.index); const items = reorder(openScripts, result.source.index, result.destination.index);
setOpenScripts(items); openScripts = items;
} }
function onTabClick(index: number): void { function onTabClick(index: number): void {
if (currentScript !== null) { if (currentScript !== null) {
// Save currentScript to openScripts // Save currentScript to openScripts
const curIndex = openScripts.findIndex( const curIndex = openScripts.findIndex(
(script) => script.fileName === currentScript.fileName && script.hostname === currentScript.hostname, (script) =>
currentScript !== null &&
script.fileName === currentScript.fileName &&
script.hostname === currentScript.hostname,
); );
openScripts[curIndex] = currentScript; openScripts[curIndex] = currentScript;
} }
setCurrentScript({ ...openScripts[index] }); currentScript = { ...openScripts[index] };
if (editorRef.current !== null && openScripts[index] !== null) { if (editorRef.current !== null && openScripts[index] !== null) {
if (openScripts[index].model === undefined || openScripts[index].model.isDisposed()) { if (openScripts[index].model === undefined || openScripts[index].model.isDisposed()) {
@ -609,25 +588,21 @@ export function Root(props: IProps): React.ReactElement {
} }
} }
async function onTabClose(index: number): Promise<void> { function onTabClose(index: number): void {
// See if the script on the server is up to date // See if the script on the server is up to date
const closingScript = openScripts[index]; const closingScript = openScripts[index];
const savedOpenScripts: Array<OpenScript> = JSON.parse(window.localStorage.getItem("scriptEditorOpenScripts")!); const savedScriptIndex = openScripts.findIndex(
const savedScriptIndex = savedOpenScripts.findIndex(
(script) => script.fileName === closingScript.fileName && script.hostname === closingScript.hostname, (script) => script.fileName === closingScript.fileName && script.hostname === closingScript.hostname,
); );
let savedScriptCode = ""; let savedScriptCode = "";
if (savedScriptIndex !== -1) { if (savedScriptIndex !== -1) {
savedScriptCode = savedOpenScripts[savedScriptIndex].code; savedScriptCode = openScripts[savedScriptIndex].code;
} }
const server = GetServer(closingScript.hostname);
if (server === null) throw new Error(`Server '${closingScript.hostname}' should not be null, but it is.`);
const serverScriptIndex = GetServer(closingScript.hostname)?.scripts.findIndex( const serverScriptIndex = server.scripts.findIndex((script) => script.filename === closingScript.fileName);
(script) => script.filename === closingScript.fileName, if (serverScriptIndex === -1 || savedScriptCode !== server.scripts[serverScriptIndex as number].code) {
);
if (
serverScriptIndex === -1 ||
savedScriptCode !== GetServer(closingScript.hostname)?.scripts[serverScriptIndex as number].code
) {
PromptEvent.emit({ PromptEvent.emit({
txt: "Do you want to save changes to " + closingScript.fileName + "?", txt: "Do you want to save changes to " + closingScript.fileName + "?",
resolve: (result: boolean) => { resolve: (result: boolean) => {
@ -641,15 +616,18 @@ export function Root(props: IProps): React.ReactElement {
} }
if (openScripts.length > 1) { if (openScripts.length > 1) {
setOpenScripts((oldScripts) => oldScripts.filter((value, i) => i !== index)); openScripts = openScripts.filter((value, i) => i !== index);
let indexOffset = -1; let indexOffset = -1;
if (openScripts[index + indexOffset] === undefined) { if (openScripts[index + indexOffset] === undefined) {
indexOffset = 1; indexOffset = 1;
if (openScripts[index + indexOffset] === undefined) {
indexOffset = 0;
}
} }
// Change current script if we closed it // Change current script if we closed it
setCurrentScript(openScripts[index + indexOffset]); currentScript = openScripts[index + indexOffset];
if (editorRef.current !== null) { if (editorRef.current !== null) {
if ( if (
openScripts[index + indexOffset].model === undefined || openScripts[index + indexOffset].model === undefined ||
@ -664,14 +642,26 @@ export function Root(props: IProps): React.ReactElement {
editorRef.current.revealLineInCenter(openScripts[index + indexOffset].lastPosition.lineNumber); editorRef.current.revealLineInCenter(openScripts[index + indexOffset].lastPosition.lineNumber);
editorRef.current.focus(); editorRef.current.focus();
} }
rerender();
} else { } else {
// No more scripts are open // No more scripts are open
setOpenScripts([]); openScripts = [];
setCurrentScript(null); currentScript = null;
props.router.toTerminal(); props.router.toTerminal();
} }
} }
function dirty(index: number): string {
const openScript = openScripts[index];
const server = GetServer(openScript.hostname);
if (server === null) throw new Error(`Server '${openScript.hostname}' should not be null, but it is.`);
const serverScript = server.scripts.find((s) => s.filename === openScript.fileName);
if (serverScript === undefined) return " *";
return serverScript.code !== openScript.code ? " *" : "";
}
// Toolbars are roughly 112px: // Toolbars are roughly 112px:
// 8px body margin top // 8px body margin top
// 38.5px filename tabs // 38.5px filename tabs
@ -718,7 +708,6 @@ export function Root(props: IProps): React.ReactElement {
}} }}
> >
<Button <Button
id={"tabButton" + fileName + hostname}
onClick={() => onTabClick(index)} onClick={() => onTabClick(index)}
style={{ style={{
background: background:
@ -727,10 +716,9 @@ export function Root(props: IProps): React.ReactElement {
: "", : "",
}} }}
> >
{hostname}:~/{fileName} {hostname}:~/{fileName} {dirty(index)}
</Button> </Button>
<Button <Button
id={"tabCloseButton" + fileName + hostname}
onClick={() => onTabClose(index)} onClick={() => onTabClose(index)}
style={{ style={{
maxWidth: "20px", maxWidth: "20px",
@ -779,7 +767,8 @@ export function Root(props: IProps): React.ReactElement {
<Typography color={updatingRam ? "secondary" : "primary"} sx={{ mx: 1 }}> <Typography color={updatingRam ? "secondary" : "primary"} sx={{ mx: 1 }}>
{ram} {ram}
</Typography> </Typography>
<Button onClick={save}>Save & Close (Ctrl/Cmd + s)</Button> <Button onClick={save}>Save (Ctrl/Cmd + s)</Button>
<Button onClick={props.router.toTerminal}>Close (Ctrl/Cmd + b)</Button>
<Typography sx={{ mx: 1 }}> <Typography sx={{ mx: 1 }}>
{" "} {" "}
Documentation:{" "} Documentation:{" "}
@ -825,10 +814,14 @@ export function Root(props: IProps): React.ReactElement {
alignItems: "center", alignItems: "center",
}} }}
> >
<p style={{ color: Settings.theme.primary, fontSize: "20px", textAlign: "center" }}> <span style={{ color: Settings.theme.primary, fontSize: "20px", textAlign: "center" }}>
<h1>No open files</h1> <Typography variant="h4">No open files</Typography>
<h5>Use "nano [File Name]" in the terminal to open files</h5> <Typography variant="h5">
</p> Use `nano FILENAME` in
<br />
the terminal to open files
</Typography>
</span>
</div> </div>
</> </>
); );

@ -218,4 +218,99 @@ export async function loadThemes(monaco: { editor: any }): Promise<void> {
"editor.selectionHighlightBorder": "#073642", "editor.selectionHighlightBorder": "#073642",
}, },
}); });
monaco.editor.defineTheme("dracula", {
base: "vs-dark",
inherit: true,
rules: [
{
background: "282A36",
foreground: "F8F8F2",
token: "",
},
{
foreground: "6272A4",
token: "comment",
},
{
foreground: "F1FA8C",
token: "string",
},
{
token: "number",
foreground: "BD93F9",
},
{
token: "otherkeyvars",
foreground: "BD93F9",
},
{
foreground: "FF79C6",
token: "function",
},
{
foreground: "FF79C6",
token: "keyword",
},
{
token: "storage.type.function.js",
foreground: "FF79C6",
},
{
token: "ns",
foreground: "FFB86C",
fontStyle: "italic",
},
{
token: "netscriptfunction",
foreground: "FF79C6",
},
{
token: "otherkeywords",
foreground: "FF68A7",
},
{
token: "type.identifier.js",
foreground: "7EE9FD",
fontStyle: "italic"
},
{
token: "delimiter.square.js",
foreground: "FFD709",
},
{
token: "delimiter.parenthesis.js",
foreground: "FFD709"
},
{
token: "delimiter.bracket.js",
foreground: "FFD709",
},
{
token: "this",
foreground: "BD93F9",
fontStyle: "italic",
},
],
"colors": {
"editor.foreground": "#F8F8F2",
"editor.background": "#282A36",
"editorLineNumber.foreground": "#6272A4",
"editor.selectionBackground": "#44475A",
"editor.selectionHighlightBackground": "#424450",
"editor.foldBackground": "#21222C",
"editor.wordHighlightBackground": "#8BE9FD50",
"editor.wordHighlightStrongBackground": "#50FA7B50",
"editor.findMatchBackground": "#FFB86C80",
"editor.findMatchHighlightBackground": "#FFFFFF40",
"editor.findRangeHighlightBackground": "#44475A75",
"editor.hoverHighlightBackground": "#8BE9FD50",
"editor.lineHighlightBorder": "#44475A",
"editor.rangeHighlightBackground": "#BD93F915",
"editor.snippetTabstopHighlightBackground": "#282A36",
"editor.snippetTabstopHighlightBorder": "#6272A4",
"editor.snippetFinalTabstopHighlightBackground": "#282A36",
"editor.snippetFinalTabstopHighlightBorder": "#50FA7B",
},
});
} }

@ -113,6 +113,11 @@ interface IDefaultSettings {
* Theme colors * Theme colors
*/ */
theme: ITheme; theme: ITheme;
/*
* Use GiB instead of GB
*/
UseIEC60027_2: boolean;
} }
/** /**
@ -160,6 +165,7 @@ export const defaultSettings: IDefaultSettings = {
SuppressBladeburnerPopup: false, SuppressBladeburnerPopup: false,
SuppressTIXPopup: false, SuppressTIXPopup: false,
SuppressSavedGameToast: false, SuppressSavedGameToast: false,
UseIEC60027_2: false,
theme: defaultTheme, theme: defaultTheme,
}; };
@ -192,6 +198,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
SuppressBladeburnerPopup: defaultSettings.SuppressBladeburnerPopup, SuppressBladeburnerPopup: defaultSettings.SuppressBladeburnerPopup,
SuppressTIXPopup: defaultSettings.SuppressTIXPopup, SuppressTIXPopup: defaultSettings.SuppressTIXPopup,
SuppressSavedGameToast: defaultSettings.SuppressSavedGameToast, SuppressSavedGameToast: defaultSettings.SuppressSavedGameToast,
UseIEC60027_2: defaultSettings.UseIEC60027_2,
MonacoTheme: "monokai", MonacoTheme: "monokai",
MonacoInsertSpaces: false, MonacoInsertSpaces: false,
MonacoFontSize: 20, MonacoFontSize: 20,

@ -408,4 +408,206 @@ export const getPredefinedThemes = (): IMap<IPredefinedTheme> => ({
button: "#000000", button: "#000000",
}, },
}, },
Discord: {
credit: "Thermite",
description: "Discord inspired theme",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924305252017143818",
colors: {
primarylight: "#7389DC",
primary: "#7389DC",
primarydark: "#5964F1",
successlight: "#00CC00",
success: "#20DF20",
successdark: "#0CB80C",
errorlight: "#EA5558",
error: "#EC4145",
errordark: "#E82528",
secondarylight: "#C3C3C3",
secondary: "#9C9C9C",
secondarydark: "#4E4E4E",
warninglight: "#ff0",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#1C4FB3",
welllight: "#999999",
well: "#35383C",
white: "#FFFFFF",
black: "#202225",
hp: "#FF5656",
money: "#43FF43",
hack: "#FFAB3D",
combat: "#8A90FD",
cha: "#FF51D9",
int: "#6495ed",
rep: "#FFFF30",
disabled: "#474B51",
backgroundprimary: "#2F3136",
backgroundsecondary: "#35393E",
button: "#333",
},
},
"One Dark": {
credit: "Dexalt142",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924650660694208512",
colors: {
primarylight: "#98C379",
primary: "#98C379",
primarydark: "#98C379",
successlight: "#98C379",
success: "#98C379",
successdark: "#98C379",
errorlight: "#E06C75",
error: "#BE5046",
errordark: "#BE5046",
secondarylight: "#AAA",
secondary: "#888",
secondarydark: "#666",
warninglight: "#E5C07B",
warning: "#E5C07B",
warningdark: "#D19A66",
infolight: "#61AFEF",
info: "#61AFEF",
infodark: "#61AFEF",
welllight: "#4B5263",
well: "#282C34",
white: "#ABB2BF",
black: "#282C34",
hp: "#E06C75",
money: "#E5C07B",
hack: "#98C379",
combat: "#ABB2BF",
cha: "#C678DD",
int: "#61AFEF",
rep: "#ABB2BF",
disabled: "#56B6C2",
backgroundprimary: "#282C34",
backgroundsecondary: "#21252B",
button: "#4B5263",
},
},
"Muted Gold & Blue": {
credit: "Sloth",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924672660758208563",
colors: {
primarylight: "#E3B54A",
primary: "#CAA243",
primarydark: "#7E6937",
successlight: "#82FF82",
success: "#6FDA6F",
successdark: "#64C364",
errorlight: "#FD5555",
error: "#D84A4A",
errordark: "#AC3939",
secondarylight: "#D8D0B8",
secondary: "#B1AA95",
secondarydark: "#736E5E",
warninglight: "#ff0",
warning: "#cc0",
warningdark: "#990",
infolight: "#69f",
info: "#36c",
infodark: "#039",
welllight: "#444",
well: "#111111",
white: "#fff",
black: "#070300",
hp: "#dd3434",
money: "#ffd700",
hack: "#adff2f",
combat: "#faffdf",
cha: "#a671d1",
int: "#6495ed",
rep: "#faffdf",
disabled: "#66cfbc",
backgroundprimary: "#0A0A0E",
backgroundsecondary: "#0E0E10",
button: "#222222",
},
},
"Default Lite": {
credit: "NmuGmu",
description: "Less eye-straining default theme",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/925263801564151888",
colors: {
primarylight: "#28CF28",
primary: "#21A821",
primarydark: "#177317",
successlight: "#1CFF1C",
success: "#16CA16",
successdark: "#0D910D",
errorlight: "#FF3B3B",
error: "#C32D2D",
errordark: "#8E2121",
secondarylight: "#B3B3B3",
secondary: "#838383",
secondarydark: "#676767",
warninglight: "#FFFF3A",
warning: "#C3C32A",
warningdark: "#8C8C1E",
infolight: "#64CBFF",
info: "#3399CC",
infodark: "#246D91",
welllight: "#404040",
well: "#1C1C1C",
white: "#C3C3C3",
black: "#0A0B0B",
hp: "#C62E2E",
money: "#D6BB27",
hack: "#ADFF2F",
combat: "#E8EDCD",
cha: "#8B5FAF",
int: "#537CC8",
rep: "#E8EDCD",
disabled: "#5AB5A5",
backgroundprimary: "#0C0D0E",
backgroundsecondary: "#121415",
button: "#252829",
},
},
Light: {
credit: "matt",
reference: "https://discord.com/channels/415207508303544321/921991895230611466/926114005456658432",
colors: {
primarylight: "#535353",
primary: "#1A1A1A",
primarydark: "#0d0d0d",
successlight: "#63c439",
success: "#428226",
successdark: "#2E5A1B",
errorlight: "#df7051",
error: "#C94824",
errordark: "#91341B",
secondarylight: "#b3b3b3",
secondary: "#9B9B9B",
secondarydark: "#7A7979",
warninglight: "#e8d464",
warning: "#C6AD20",
warningdark: "#9F8A16",
infolight: "#6299cf",
info: "#3778B7",
infodark: "#30689C",
welllight: "#f9f9f9",
well: "#eaeaea",
white: "#F7F7F7",
black: "#F7F7F7",
hp: "#BF5C41",
money: "#E1B121",
hack: "#47BC38",
combat: "#656262",
cha: "#A568AC",
int: "#889BCF",
rep: "#656262",
disabled: "#70B4BF",
backgroundprimary: "#F7F7F7",
backgroundsecondary: "#f9f9f9",
button: "#eaeaea",
},
},
}); });

@ -273,7 +273,7 @@ export function SidebarRoot(props: IProps): React.ReactElement {
// Alt-o - Options // Alt-o - Options
function handleShortcuts(this: Document, event: KeyboardEvent): any { function handleShortcuts(this: Document, event: KeyboardEvent): any {
if (Settings.DisableHotkeys) return; if (Settings.DisableHotkeys) return;
if (props.player.isWorking || redPillFlag) return; if ((props.player.isWorking && props.player.focus) || redPillFlag) return;
if (event.keyCode == KEY.T && event.altKey) { if (event.keyCode == KEY.T && event.altKey) {
event.preventDefault(); event.preventDefault();
clickTerminal(); clickTerminal();

@ -779,7 +779,7 @@ export const InitStockMetadata: IConstructorParams[] = [
max: 350, max: 350,
min: 200, min: 200,
}, },
name: "Joes Guns", name: LocationName.Sector12JoesGuns,
otlkMag: 1, otlkMag: 1,
spreadPerc: { spreadPerc: {
divisor: 10, divisor: 10,
@ -790,7 +790,7 @@ export const InitStockMetadata: IConstructorParams[] = [
max: 52e3, max: 52e3,
min: 15e3, min: 15e3,
}, },
symbol: StockSymbols["Joes Guns"], symbol: StockSymbols[LocationName.Sector12JoesGuns],
}, },
{ {

@ -32,10 +32,10 @@ StockSymbols[LocationName.VolhavenCompuTek] = "CTK";
StockSymbols[LocationName.AevumNetLinkTechnologies] = "NTLK"; StockSymbols[LocationName.AevumNetLinkTechnologies] = "NTLK";
StockSymbols[LocationName.IshimaOmegaSoftware] = "OMGA"; StockSymbols[LocationName.IshimaOmegaSoftware] = "OMGA";
StockSymbols[LocationName.Sector12FoodNStuff] = "FNS"; StockSymbols[LocationName.Sector12FoodNStuff] = "FNS";
StockSymbols[LocationName.Sector12JoesGuns] = "JGN";
// Stocks for other companies // Stocks for other companies
StockSymbols["Sigma Cosmetics"] = "SGC"; StockSymbols["Sigma Cosmetics"] = "SGC";
StockSymbols["Joes Guns"] = "JGN";
StockSymbols["Catalyst Ventures"] = "CTYS"; StockSymbols["Catalyst Ventures"] = "CTYS";
StockSymbols["Microdyne Technologies"] = "MDYN"; StockSymbols["Microdyne Technologies"] = "MDYN";
StockSymbols["Titan Laboratories"] = "TITN"; StockSymbols["Titan Laboratories"] = "TITN";

@ -176,7 +176,7 @@ export function InfoAndPurchases(props: IProps): React.ReactElement {
<> <>
<Typography>Welcome to the World Stock Exchange (WSE)!</Typography> <Typography>Welcome to the World Stock Exchange (WSE)!</Typography>
<Link href={documentationLink} target={"_blank"}> <Link href={documentationLink} target={"_blank"}>
Investopedia <Typography>Investopedia</Typography>
</Link> </Link>
<br /> <br />
<PurchaseWseAccountButton {...props} /> <PurchaseWseAccountButton {...props} />

@ -48,7 +48,7 @@ export function isValidFilename(filename: string): boolean {
* Checks whether a string is a valid directory name. Only used for the directory itself, * Checks whether a string is a valid directory name. Only used for the directory itself,
* not an entire path * not an entire path
*/ */
export function isValidDirectoryName(name: string): boolean { export function isValidDirectoryName(name: string): boolean {
// Allows alphanumerics, hyphens, underscores, and percentage signs. // Allows alphanumerics, hyphens, underscores, and percentage signs.
// Name can begin with a single period, but otherwise cannot have any // Name can begin with a single period, but otherwise cannot have any
const regex = /^.?[a-zA-Z0-9_-]+$/; const regex = /^.?[a-zA-Z0-9_-]+$/;
@ -310,3 +310,9 @@ export function areFilesEqual(f0: string, f1: string): boolean {
if (!f1.startsWith("/")) f1 = "/" + f1; if (!f1.startsWith("/")) f1 = "/" + f1;
return f0 === f1; return f0 === f1;
} }
export function areImportsEquals(f0: string, f1: string): boolean {
if (!f0.endsWith(".ns") && !f0.endsWith(".js")) f0 = f0 + ".js";
if (!f1.endsWith(".ns") && !f1.endsWith(".js")) f1 = f1 + ".js";
return areFilesEqual(f0, f1);
}

@ -27,7 +27,7 @@ export const TerminalHelpText: string[] = [
"killall Stops all running scripts on the current machine", "killall Stops all running scripts on the current machine",
"ls [dir] [| grep pattern] Displays all files on the machine", "ls [dir] [| grep pattern] Displays all files on the machine",
"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 ...] 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",
@ -35,7 +35,7 @@ export const TerminalHelpText: string[] = [
"run [name] [-t n] [--tail] [args...] Execute a program or script", "run [name] [-t n] [--tail] [args...] Execute a program or script",
"scan Prints all immediately-available network connections", "scan Prints all immediately-available network connections",
"scan-analyze [d] [-a] Prints info for all servers up to <i>d</i> nodes away", "scan-analyze [d] [-a] Prints info for all servers up to <i>d</i> nodes away",
"scp [file] [server] Copies a file to a destination server", "scp [file ...] [server] Copies a file to a destination server",
"sudov Shows whether you have root access on this computer", "sudov Shows whether you have root access on this computer",
"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",
@ -80,7 +80,7 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
], ],
analyze: [ analyze: [
"analze", "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 ",
@ -96,13 +96,15 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
], ],
buy: [ buy: [
"buy [-l / program]", "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.",
" ", " ",
"If this command is ran with the '-l' flag, it will display a list of all programs that can be bought through the ", "If this command is ran with the '-l' flag, it will display a list of all programs that can be bought through the ",
"dark web to the Terminal, as well as their costs.", "dark web to the Terminal, as well as their costs.",
" ", " ",
"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: [
@ -191,7 +193,7 @@ export const HelpTexts: IMap<string[]> = {
free: [ free: [
"free", "free",
" ", " ",
"Display's 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: [
@ -360,12 +362,17 @@ export const HelpTexts: IMap<string[]> = {
"-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]", "scp [filename ...] [target server]",
" ", " ",
"Copies the specified file 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 extension), literature files (.lit extension), ", "This command only works for script files (.script or .js extension), literature files (.lit extension), ",
"and text files (.txt extension). ", "and text files (.txt extension). ",
"The second argument passed in must be the hostname or IP of the target server.", "The second argument passed in must be the hostname or IP of the target server. Examples:",
" ",
"scp foo.script n00dles",
" ",
"scp foo.script bar.script n00dles",
" ",
], ],
sudov: ["sudov", " ", "Prints whether or not you have root access to the current machine"], sudov: ["sudov", " ", "Prints whether or not you have root access to the current machine"],

@ -2,7 +2,7 @@ import { ITerminal } from "../ITerminal";
import { IRouter } from "../../ui/Router"; 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 { listAllDarkwebItems, buyDarkwebItem } from "../../DarkWeb/DarkWeb"; import { listAllDarkwebItems, buyAllDarkwebItems, buyDarkwebItem } from "../../DarkWeb/DarkWeb";
import { SpecialServers } from "../../Server/data/SpecialServers"; import { SpecialServers } from "../../Server/data/SpecialServers";
import { GetServer } from "../../Server/AllServers"; import { GetServer } from "../../Server/AllServers";
@ -22,12 +22,15 @@ export function buy(
if (args.length != 1) { if (args.length != 1) {
terminal.print("Incorrect number of arguments. Usage: "); terminal.print("Incorrect number of arguments. Usage: ");
terminal.print("buy -l"); terminal.print("buy -l");
terminal.print("buy -a");
terminal.print("buy [item name]"); terminal.print("buy [item name]");
return; return;
} }
const arg = args[0] + ""; const arg = args[0] + "";
if (arg == "-l" || arg == "-1" || arg == "--list") { if (arg == "-l" || arg == "-1" || arg == "--list") {
listAllDarkwebItems(); listAllDarkwebItems();
} else if (arg == "-a" || arg == "--all") {
buyAllDarkwebItems();
} else { } else {
buyDarkwebItem(arg); buyDarkwebItem(arg);
} }

@ -53,7 +53,7 @@ export function mv(
// Also, you can't convert between different file types // Also, you can't convert between different file types
if (isScriptFilename(source)) { if (isScriptFilename(source)) {
const script = srcFile as Script; const script = srcFile as Script;
if (!isScriptFilename(dest)) { if (!isScriptFilename(destPath)) {
terminal.error(`Source and destination files must have the same type`); terminal.error(`Source and destination files must have the same type`);
return; return;
} }
@ -66,7 +66,7 @@ export function mv(
if (destFile != null) { if (destFile != null) {
// Already exists, will be overwritten, so we'll delete it // Already exists, will be overwritten, so we'll delete it
const status = server.removeFile(dest); const status = server.removeFile(destPath);
if (!status.res) { if (!status.res) {
terminal.error(`Something went wrong...please contact game dev (probably a bug)`); terminal.error(`Something went wrong...please contact game dev (probably a bug)`);
return; return;

@ -2,6 +2,7 @@ import { ITerminal } from "../ITerminal";
import { IRouter } from "../../ui/Router"; 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 * as libarg from "arg"
export function ps( export function ps(
terminal: ITerminal, terminal: ITerminal,
@ -10,16 +11,40 @@ export function ps(
server: BaseServer, server: BaseServer,
args: (string | number | boolean)[], args: (string | number | boolean)[],
): void { ): void {
if (args.length !== 0) { let flags;
terminal.error("Incorrect usage of ps command. Usage: ps"); try{
flags = libarg({
'--grep': String,
'-g': '--grep'
},
{ argv: args }
)
}catch(e){
// catch passing only -g / --grep with no string to use as the search
terminal.error("Incorrect usage of ps command. Usage: ps [-g, --grep pattern]");
return; return;
} }
for (let i = 0; i < server.runningScripts.length; i++) { const pattern = flags['--grep']
const rsObj = server.runningScripts[i]; if (pattern) {
let res = `(PID - ${rsObj.pid}) ${rsObj.filename}`; const re = new RegExp(pattern.toString())
for (let j = 0; j < rsObj.args.length; ++j) { const matching = server.runningScripts.filter((x) => re.test(x.filename))
res += " " + rsObj.args[j].toString(); for (let i = 0; i < matching.length; i++) {
const rsObj = matching[i];
let res = `(PID - ${rsObj.pid}) ${rsObj.filename}`;
for (let j = 0; j < rsObj.args.length; ++j) {
res += " " + rsObj.args[j].toString();
}
terminal.print(res);
}
}
if(args.length === 0){
for (let i = 0; i < server.runningScripts.length; i++) {
const rsObj = server.runningScripts[i];
let res = `(PID - ${rsObj.pid}) ${rsObj.filename}`;
for (let j = 0; j < rsObj.args.length; ++j) {
res += " " + rsObj.args[j].toString();
}
terminal.print(res);
} }
terminal.print(res);
} }
} }

@ -351,6 +351,11 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
modifyInput("deletewordbefore"); modifyInput("deletewordbefore");
} }
if (event.keyCode === KEY.D && event.altKey) {
event.preventDefault();
modifyInput("deletewordafter");
}
if (event.keyCode === KEY.U && event.ctrlKey) { if (event.keyCode === KEY.U && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
modifyInput("clearbefore"); modifyInput("clearbefore");
@ -360,9 +365,6 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
event.preventDefault(); event.preventDefault();
modifyInput("clearafter"); modifyInput("clearafter");
} }
// TODO AFTER THIS:
// alt + d deletes word after cursor
} }
} }

@ -351,7 +351,7 @@ export function InteractiveTutorialRoot(): React.ReactElement {
continuously hack the n00dles server. continuously hack the n00dles server.
<br /> <br />
<br /> <br />
To save and close the script editor, press the button in the bottom left, or press ctrl + b. To save and close the script editor, press the button in the bottom left, or press ctrl + s then ctrl + b.
</Typography> </Typography>
</> </>
), ),

@ -28,6 +28,7 @@ import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
import { dialogBoxCreate } from "./DialogBox"; import { dialogBoxCreate } from "./DialogBox";
import { ConfirmationModal } from "./ConfirmationModal"; import { ConfirmationModal } from "./ConfirmationModal";
import { ThemeEditorModal } from "./ThemeEditorModal"; import { ThemeEditorModal } from "./ThemeEditorModal";
import { SnackbarEvents } from "./Snackbar";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { save, deleteGame } from "../../db"; import { save, deleteGame } from "../../db";
@ -51,6 +52,12 @@ interface IProps {
softReset: () => void; softReset: () => void;
} }
interface ImportData {
base64: string;
parsed: any;
exportDate?: Date;
}
export function GameOptionsRoot(props: IProps): React.ReactElement { export function GameOptionsRoot(props: IProps): React.ReactElement {
const classes = useStyles(); const classes = useStyles();
const importInput = useRef<HTMLInputElement>(null); const importInput = useRef<HTMLInputElement>(null);
@ -78,12 +85,15 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
const [enableBashHotkeys, setEnableBashHotkeys] = useState(Settings.EnableBashHotkeys); const [enableBashHotkeys, setEnableBashHotkeys] = useState(Settings.EnableBashHotkeys);
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat); const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
const [saveGameOnFileSave, setSaveGameOnFileSave] = useState(Settings.SaveGameOnFileSave); const [saveGameOnFileSave, setSaveGameOnFileSave] = useState(Settings.SaveGameOnFileSave);
const [useIEC60027_2, setUseIEC60027_2] = useState(Settings.UseIEC60027_2);
const [locale, setLocale] = useState(Settings.Locale); const [locale, setLocale] = useState(Settings.Locale);
const [diagnosticOpen, setDiagnosticOpen] = useState(false); const [diagnosticOpen, setDiagnosticOpen] = useState(false);
const [deleteGameOpen, setDeleteOpen] = useState(false); const [deleteGameOpen, setDeleteOpen] = useState(false);
const [themeEditorOpen, setThemeEditorOpen] = useState(false); const [themeEditorOpen, setThemeEditorOpen] = useState(false);
const [softResetOpen, setSoftResetOpen] = useState(false); const [softResetOpen, setSoftResetOpen] = useState(false);
const [importSaveOpen, setImportSaveOpen] = useState(false);
const [importData, setImportData] = useState<ImportData | null>(null);
function handleExecTimeChange(event: any, newValue: number | number[]): void { function handleExecTimeChange(event: any, newValue: number | number[]): void {
setExecTime(newValue as number); setExecTime(newValue as number);
@ -154,6 +164,10 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
setDisableASCIIArt(event.target.checked); setDisableASCIIArt(event.target.checked);
Settings.DisableASCIIArt = event.target.checked; Settings.DisableASCIIArt = event.target.checked;
} }
function handleUseIEC60027_2Change(event: React.ChangeEvent<HTMLInputElement>): void {
setUseIEC60027_2(event.target.checked);
Settings.UseIEC60027_2 = event.target.checked;
}
function handleDisableTextEffectsChange(event: React.ChangeEvent<HTMLInputElement>): void { function handleDisableTextEffectsChange(event: React.ChangeEvent<HTMLInputElement>): void {
setDisableTextEffects(event.target.checked); setDisableTextEffects(event.target.checked);
@ -206,11 +220,60 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
return; return;
} }
const contents = result; const contents = result;
save(contents).then(() => setTimeout(() => location.reload(), 1000));
let newSave;
try {
newSave = window.atob(contents);
newSave = newSave.trim();
} catch (error) {
console.log(error); // We'll handle below
}
if (!newSave || newSave === '') {
SnackbarEvents.emit("Save game had not content or was not base64 encoded", "error", 5000);
return;
}
let parsedSave;
try {
parsedSave = JSON.parse(newSave);
} catch (error) {
console.log(error); // We'll handle below
}
if (!parsedSave || parsedSave.ctor !== 'BitburnerSaveObject' || !parsedSave.data) {
SnackbarEvents.emit("Save game did not seem valid", "error", 5000);
return;
}
const data: ImportData = {
base64: contents,
parsed: parsedSave,
}
// We don't always seem to have this value in the save file. Exporting from the option menu does not set the bonus I think.
const exportTimestamp = parsedSave.data.LastExportBonus;
if (exportTimestamp && exportTimestamp !== '0') {
data.exportDate = new Date(parseInt(exportTimestamp, 10))
}
setImportData(data)
setImportSaveOpen(true);
}; };
reader.readAsText(file); reader.readAsText(file);
} }
function confirmedImportGame(): void {
if (!importData) return;
setImportSaveOpen(false);
save(importData.base64).then(() => {
setImportData(null);
setTimeout(() => location.reload(), 1000)
});
}
function doSoftReset(): void { function doSoftReset(): void {
if (!Settings.SuppressBuyAugmentationConfirmation) { if (!Settings.SuppressBuyAugmentationConfirmation) {
setSoftResetOpen(true); setSoftResetOpen(true);
@ -292,7 +355,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
The maximum number of entries that can be written to a the terminal. Setting this too high can cause The maximum number of entries that can be written to the terminal. Setting this too high can cause
the game to use a lot of memory. the game to use a lot of memory.
</Typography> </Typography>
} }
@ -513,6 +576,16 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
} }
/> />
</ListItem> </ListItem>
<ListItem>
<FormControlLabel
control={<Switch checked={useIEC60027_2} onChange={handleUseIEC60027_2Change} />}
label={
<Tooltip title={<Typography>If this is set all references to memory will use GiB instead of GB, in accordance with IEC 60027-2.</Typography>}>
<Typography>Use GiB instead of GB</Typography>
</Tooltip>
}
/>
</ListItem>
<ListItem> <ListItem>
<Tooltip <Tooltip
title={ title={
@ -618,19 +691,41 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Button onClick={() => setDeleteOpen(true)}>Delete Game</Button> <Button onClick={() => setDeleteOpen(true)}>Delete Game</Button>
</Box> </Box>
<Box> <Box>
<Tooltip title={<Typography>export</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" />
Export Export Game
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip title={<Typography>import</Typography>}> <Tooltip title={<Typography>Import your game from a text file.<br/>This will <strong>overwrite</strong> your current game. Back it up first!</Typography>}>
<Button onClick={startImport}> <Button onClick={startImport}>
<UploadIcon color="primary" /> <UploadIcon color="primary" />
Import Import Game
<input ref={importInput} id="import-game-file-selector" type="file" hidden onChange={onImport} /> <input ref={importInput} id="import-game-file-selector" type="file" hidden onChange={onImport} />
</Button> </Button>
</Tooltip> </Tooltip>
<ConfirmationModal
open={importSaveOpen}
onClose={() => setImportSaveOpen(false)}
onConfirm={() => confirmedImportGame()}
confirmationText={
<>
Importing a new game will <strong>completely wipe</strong> the current data!
<br />
<br />
Make sure to have a backup of your current save file before importing.
<br />
The file you are attempting to import seems valid.
<br />
<br />
{importData?.exportDate && (<>
The export date of the save file is <strong>{importData?.exportDate.toString()}</strong>
<br />
<br />
</>)}
</>
}
/>
</Box> </Box>
<Box> <Box>
<Tooltip <Tooltip

@ -77,6 +77,16 @@ interface IProps {
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
title: {
"&.is-minimized + *": {
border: "none",
margin: 0,
"max-height": 0,
padding: 0,
"pointer-events": "none",
visibility: "hidden"
},
},
logs: { logs: {
overflowY: "scroll", overflowY: "scroll",
overflowX: "hidden", overflowX: "hidden",
@ -107,6 +117,7 @@ function LogWindow(props: IProps): React.ReactElement {
const classes = useStyles(); const classes = useStyles();
const container = useRef<HTMLDivElement>(null); const container = useRef<HTMLDivElement>(null);
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
const [minimized, setMinimized] = useState(false);
function rerender(): void { function rerender(): void {
setRerender((old) => !old); setRerender((old) => !old);
} }
@ -140,15 +151,19 @@ function LogWindow(props: IProps): React.ReactElement {
rerender(); rerender();
} }
function title(): string { function title(full = false): string {
const maxLength = 30; const maxLength = 30;
const t = `${script.filename} ${script.args.map((x: any): string => `${x}`).join(" ")}`; const t = `${script.filename} ${script.args.map((x: any): string => `${x}`).join(" ")}`;
if (t.length <= maxLength) { if (full || t.length <= maxLength) {
return t; return t;
} }
return t.slice(0, maxLength - 3) + "..."; return t.slice(0, maxLength - 3) + "...";
} }
function minimize(): void {
setMinimized(!minimized);
}
function lineClass(s: string): string { function lineClass(s: string): string {
if (s.match(/(^\[[^\]]+\] )?ERROR/) || s.match(/(^\[[^\]]+\] )?FAIL/)) { if (s.match(/(^\[[^\]]+\] )?ERROR/) || s.match(/(^\[[^\]]+\] )?FAIL/)) {
return classes.error; return classes.error;
@ -180,18 +195,20 @@ function LogWindow(props: IProps): React.ReactElement {
> >
<div onMouseDown={updateLayer}> <div onMouseDown={updateLayer}>
<Paper <Paper
className={classes.title + " " + (minimized ? 'is-minimized' : '')}
style={{ style={{
cursor: "grab", cursor: "grab",
}} }}
> >
<Box className="drag" display="flex" alignItems="center"> <Box className="drag" display="flex" alignItems="center">
<Typography color="primary" variant="h6"> <Typography color="primary" variant="h6" title={title(true)}>
{title()} {title()}
</Typography> </Typography>
<Box position="absolute" right={0}> <Box position="absolute" right={0}>
{!workerScripts.has(script.pid) && <Button onClick={run}>Run</Button>} {!workerScripts.has(script.pid) && <Button onClick={run}>Run</Button>}
{workerScripts.has(script.pid) && <Button onClick={kill}>Kill</Button>} {workerScripts.has(script.pid) && <Button onClick={kill}>Kill</Button>}
<Button onClick={minimize}>{minimized ? "\u{1F5D6}" : "\u{1F5D5}"}</Button>
<Button onClick={props.onClose}>Close</Button> <Button onClick={props.onClose}>Close</Button>
</Box> </Box>
</Box> </Box>

@ -9,6 +9,7 @@ interface IProgressProps {
min: number; min: number;
max: number; max: number;
current: number; current: number;
remaining: number;
progress: number; progress: number;
color?: React.CSSProperties["color"]; color?: React.CSSProperties["color"];
} }
@ -18,14 +19,14 @@ interface IStatsOverviewCellProps {
color?: React.CSSProperties["color"]; color?: React.CSSProperties["color"];
} }
export function StatsProgressBar({ min, max, current, progress, color }: IProgressProps): React.ReactElement { export function StatsProgressBar({ min, max, current, remaining, progress, color }: IProgressProps): React.ReactElement {
const tooltip = ( const tooltip = (
<Typography sx={{ textAlign: 'right' }}> <Typography sx={{ textAlign: 'right' }}>
<strong>Progress:</strong>&nbsp; <strong>Progress:</strong>&nbsp;
{numeralWrapper.formatExp(current - min)} / {numeralWrapper.formatExp(max - min)} {numeralWrapper.formatExp(current)} / {numeralWrapper.formatExp(max - min)}
<br /> <br />
<strong>Remaining:</strong>&nbsp; <strong>Remaining:</strong>&nbsp;
{numeralWrapper.formatExp(max - current)} ({progress.toFixed(2)}%) {numeralWrapper.formatExp(remaining)} ({progress.toFixed(2)}%)
</Typography> </Typography>
); );
@ -58,7 +59,8 @@ export function StatsProgressOverviewCell({ progress: skill, color }: IStatsOver
<StatsProgressBar <StatsProgressBar
min={skill.baseExperience} min={skill.baseExperience}
max={skill.nextExperience} max={skill.nextExperience}
current={skill.experience} current={skill.currentExperience}
remaining={skill.remainingExperience}
progress={skill.progress} progress={skill.progress}
color={color} color={color}
/> />

@ -46,6 +46,7 @@ function ColorEditor({ name, onColorChange, color, defaultColor }: IColorEditorP
deferred deferred
value={color} value={color}
onChange={(newColor: Color) => onColorChange(name, "#" + newColor.hex)} onChange={(newColor: Color) => onColorChange(name, "#" + newColor.hex)}
disableAlpha
/> />
</> </>
), ),

@ -62,20 +62,14 @@ export function WorkInProgressRoot(): React.ReactElement {
<Reputation reputation={player.workRepGained} /> ( <Reputation reputation={player.workRepGained} /> (
<ReputationRate reputation={player.workRepGainRate * CYCLES_PER_SEC} />) reputation for this faction <br /> <ReputationRate reputation={player.workRepGainRate * CYCLES_PER_SEC} />) reputation for this faction <br />
<br /> <br />
{numeralWrapper.formatExp(player.workHackExpGained)} ( {player.workHackExpGained > 0 && <>{numeralWrapper.formatExp(player.workHackExpGained)} ({numeralWrapper.formatExp(player.workHackExpGainRate * CYCLES_PER_SEC)} / sec) hacking exp <br /></>}
{numeralWrapper.formatExp(player.workHackExpGainRate * CYCLES_PER_SEC)} / sec) hacking exp <br />
<br /> <br />
{numeralWrapper.formatExp(player.workStrExpGained)} ( {player.workStrExpGained > 0 && <>{numeralWrapper.formatExp(player.workStrExpGained)} ({numeralWrapper.formatExp(player.workStrExpGainRate * CYCLES_PER_SEC)} / sec) strength exp <br /></>}
{numeralWrapper.formatExp(player.workStrExpGainRate * CYCLES_PER_SEC)} / sec) strength exp <br /> {player.workDefExpGained > 0 && <>{numeralWrapper.formatExp(player.workDefExpGained)} ({numeralWrapper.formatExp(player.workDefExpGainRate * CYCLES_PER_SEC)} / sec) defense exp <br /></>}
{numeralWrapper.formatExp(player.workDefExpGained)} ( {player.workDexExpGained > 0 && <>{numeralWrapper.formatExp(player.workDexExpGained)} ({numeralWrapper.formatExp(player.workDexExpGainRate * CYCLES_PER_SEC)} / sec) dexterity exp <br /></>}
{numeralWrapper.formatExp(player.workDefExpGainRate * CYCLES_PER_SEC)} / sec) defense exp <br /> {player.workAgiExpGained > 0 && <>{numeralWrapper.formatExp(player.workAgiExpGained)} ({numeralWrapper.formatExp(player.workAgiExpGainRate * CYCLES_PER_SEC)} / sec) agility exp <br /></>}
{numeralWrapper.formatExp(player.workDexExpGained)} (
{numeralWrapper.formatExp(player.workDexExpGainRate * CYCLES_PER_SEC)} / sec) dexterity exp <br />
{numeralWrapper.formatExp(player.workAgiExpGained)} (
{numeralWrapper.formatExp(player.workAgiExpGainRate * CYCLES_PER_SEC)} / sec) agility exp <br />
<br /> <br />
{numeralWrapper.formatExp(player.workChaExpGained)} ( {player.workChaExpGained > 0 && <>{numeralWrapper.formatExp(player.workChaExpGained)} ({numeralWrapper.formatExp(player.workChaExpGainRate * CYCLES_PER_SEC)} / sec) charisma exp <br /></>}
{numeralWrapper.formatExp(player.workChaExpGainRate * CYCLES_PER_SEC)} / sec) charisma exp <br />
<br /> <br />
You will automatically finish after working for 20 hours. You can cancel earlier if you wish. You will automatically finish after working for 20 hours. You can cancel earlier if you wish.
<br /> <br />
@ -123,18 +117,12 @@ export function WorkInProgressRoot(): React.ReactElement {
<br /> <br />
<br /> <br />
You have gained: <br /> You have gained: <br />
{numeralWrapper.formatExp(player.workHackExpGained)} ( {player.workHackExpGained > 0 && <>{numeralWrapper.formatExp(player.workHackExpGained)} ({numeralWrapper.formatExp(player.workHackExpGainRate * CYCLES_PER_SEC)} / sec) hacking exp <br /></>}
{numeralWrapper.formatExp(player.workHackExpGainRate * CYCLES_PER_SEC)} / sec) hacking exp <br /> {player.workStrExpGained > 0 && <>{numeralWrapper.formatExp(player.workStrExpGained)} ({numeralWrapper.formatExp(player.workStrExpGainRate * CYCLES_PER_SEC)} / sec) strength exp <br /></>}
{numeralWrapper.formatExp(player.workStrExpGained)} ( {player.workDefExpGained > 0 && <>{numeralWrapper.formatExp(player.workDefExpGained)} ({numeralWrapper.formatExp(player.workDefExpGainRate * CYCLES_PER_SEC)} / sec) defense exp <br /></>}
{numeralWrapper.formatExp(player.workStrExpGainRate * CYCLES_PER_SEC)} / sec) strength exp <br /> {player.workDexExpGained > 0 && <>{numeralWrapper.formatExp(player.workDexExpGained)} ({numeralWrapper.formatExp(player.workDexExpGainRate * CYCLES_PER_SEC)} / sec) dexterity exp <br /></>}
{numeralWrapper.formatExp(player.workDefExpGained)} ( {player.workAgiExpGained > 0 && <>{numeralWrapper.formatExp(player.workAgiExpGained)} ({numeralWrapper.formatExp(player.workAgiExpGainRate * CYCLES_PER_SEC)} / sec) agility exp <br /></>}
{numeralWrapper.formatExp(player.workDefExpGainRate * CYCLES_PER_SEC)} / sec) defense exp <br /> {player.workChaExpGained > 0 && <>{numeralWrapper.formatExp(player.workChaExpGained)} ({numeralWrapper.formatExp(player.workChaExpGainRate * CYCLES_PER_SEC)} / sec) charisma exp <br /></>}
{numeralWrapper.formatExp(player.workDexExpGained)} (
{numeralWrapper.formatExp(player.workDexExpGainRate * CYCLES_PER_SEC)} / sec) dexterity exp <br />
{numeralWrapper.formatExp(player.workAgiExpGained)} (
{numeralWrapper.formatExp(player.workAgiExpGainRate * CYCLES_PER_SEC)} / sec) agility exp <br />
{numeralWrapper.formatExp(player.workChaExpGained)} (
{numeralWrapper.formatExp(player.workChaExpGainRate * CYCLES_PER_SEC)} / sec) charisma exp <br />
You may cancel at any time You may cancel at any time
</Typography> </Typography>
</Grid> </Grid>
@ -185,26 +173,26 @@ export function WorkInProgressRoot(): React.ReactElement {
<Reputation reputation={player.workRepGained} /> ( <Reputation reputation={player.workRepGained} /> (
<ReputationRate reputation={player.workRepGainRate * CYCLES_PER_SEC} />) reputation for this company <br /> <ReputationRate reputation={player.workRepGainRate * CYCLES_PER_SEC} />) reputation for this company <br />
<br /> <br />
{numeralWrapper.formatExp(player.workHackExpGained)} ( {player.workHackExpGained > 0 && <>{numeralWrapper.formatExp(player.workHackExpGained)} (
{`${numeralWrapper.formatExp(player.workHackExpGainRate * CYCLES_PER_SEC)} / sec`} {`${numeralWrapper.formatExp(player.workHackExpGainRate * CYCLES_PER_SEC)} / sec`}
) hacking exp <br /> ) hacking exp <br /></>}
<br /> <br />
{numeralWrapper.formatExp(player.workStrExpGained)} ( {player.workStrExpGained > 0 && <>{numeralWrapper.formatExp(player.workStrExpGained)} (
{`${numeralWrapper.formatExp(player.workStrExpGainRate * CYCLES_PER_SEC)} / sec`} {`${numeralWrapper.formatExp(player.workStrExpGainRate * CYCLES_PER_SEC)} / sec`}
) strength exp <br /> ) strength exp <br /></>}
{numeralWrapper.formatExp(player.workDefExpGained)} ( {player.workDefExpGained > 0 && <>{numeralWrapper.formatExp(player.workDefExpGained)} (
{`${numeralWrapper.formatExp(player.workDefExpGainRate * CYCLES_PER_SEC)} / sec`} {`${numeralWrapper.formatExp(player.workDefExpGainRate * CYCLES_PER_SEC)} / sec`}
) defense exp <br /> ) defense exp <br /></>}
{numeralWrapper.formatExp(player.workDexExpGained)} ( {player.workDexExpGained > 0 && <>{numeralWrapper.formatExp(player.workDexExpGained)} (
{`${numeralWrapper.formatExp(player.workDexExpGainRate * CYCLES_PER_SEC)} / sec`} {`${numeralWrapper.formatExp(player.workDexExpGainRate * CYCLES_PER_SEC)} / sec`}
) dexterity exp <br /> ) dexterity exp <br /></>}
{numeralWrapper.formatExp(player.workAgiExpGained)} ( {player.workAgiExpGained > 0 && <>{numeralWrapper.formatExp(player.workAgiExpGained)} (
{`${numeralWrapper.formatExp(player.workAgiExpGainRate * CYCLES_PER_SEC)} / sec`} {`${numeralWrapper.formatExp(player.workAgiExpGainRate * CYCLES_PER_SEC)} / sec`}
) agility exp <br /> ) agility exp <br /></>}
<br /> <br />
{numeralWrapper.formatExp(player.workChaExpGained)} ( {player.workChaExpGained > 0 && <>{numeralWrapper.formatExp(player.workChaExpGained)} (
{`${numeralWrapper.formatExp(player.workChaExpGainRate * CYCLES_PER_SEC)} / sec`} {`${numeralWrapper.formatExp(player.workChaExpGainRate * CYCLES_PER_SEC)} / sec`}
) charisma exp <br /> ) charisma exp <br /></>}
<br /> <br />
You will automatically finish after working for 8 hours. You can cancel earlier if you wish, but you will You will automatically finish after working for 8 hours. You can cancel earlier if you wish, but you will
only gain {penaltyString} of the reputation you've earned so far. only gain {penaltyString} of the reputation you've earned so far.
@ -256,26 +244,26 @@ export function WorkInProgressRoot(): React.ReactElement {
<ReputationRate reputation={player.workRepGainRate * CYCLES_PER_SEC} /> <ReputationRate reputation={player.workRepGainRate * CYCLES_PER_SEC} />
) reputation for this company <br /> ) reputation for this company <br />
<br /> <br />
{numeralWrapper.formatExp(player.workHackExpGained)} ( {player.workHackExpGained > 0 && <>{numeralWrapper.formatExp(player.workHackExpGained)} (
{`${numeralWrapper.formatExp(player.workHackExpGainRate * CYCLES_PER_SEC)} / sec`} {`${numeralWrapper.formatExp(player.workHackExpGainRate * CYCLES_PER_SEC)} / sec`}
) hacking exp <br /> ) hacking exp <br /></>}
<br /> <br />
{numeralWrapper.formatExp(player.workStrExpGained)} ( {player.workStrExpGained > 0 && <>{numeralWrapper.formatExp(player.workStrExpGained)} (
{`${numeralWrapper.formatExp(player.workStrExpGainRate * CYCLES_PER_SEC)} / sec`} {`${numeralWrapper.formatExp(player.workStrExpGainRate * CYCLES_PER_SEC)} / sec`}
) strength exp <br /> ) strength exp <br /></>}
{numeralWrapper.formatExp(player.workDefExpGained)} ( {player.workDefExpGained > 0 && <>{numeralWrapper.formatExp(player.workDefExpGained)} (
{`${numeralWrapper.formatExp(player.workDefExpGainRate * CYCLES_PER_SEC)} / sec`} {`${numeralWrapper.formatExp(player.workDefExpGainRate * CYCLES_PER_SEC)} / sec`}
) defense exp <br /> ) defense exp <br /></>}
{numeralWrapper.formatExp(player.workDexExpGained)} ( {player.workDexExpGained > 0 && <>{numeralWrapper.formatExp(player.workDexExpGained)} (
{`${numeralWrapper.formatExp(player.workDexExpGainRate * CYCLES_PER_SEC)} / sec`} {`${numeralWrapper.formatExp(player.workDexExpGainRate * CYCLES_PER_SEC)} / sec`}
) dexterity exp <br /> ) dexterity exp <br /></>}
{numeralWrapper.formatExp(player.workAgiExpGained)} ( {player.workAgiExpGained > 0 && <>{numeralWrapper.formatExp(player.workAgiExpGained)} (
{`${numeralWrapper.formatExp(player.workAgiExpGainRate * CYCLES_PER_SEC)} / sec`} {`${numeralWrapper.formatExp(player.workAgiExpGainRate * CYCLES_PER_SEC)} / sec`}
) agility exp <br /> ) agility exp <br /></>}
<br /> <br />
{numeralWrapper.formatExp(player.workChaExpGained)} ( {player.workChaExpGained > 0 && <>{numeralWrapper.formatExp(player.workChaExpGained)} (
{`${numeralWrapper.formatExp(player.workChaExpGainRate * CYCLES_PER_SEC)} / sec`} {`${numeralWrapper.formatExp(player.workChaExpGainRate * CYCLES_PER_SEC)} / sec`}
) charisma exp <br /> ) charisma exp <br /></>}
<br /> <br />
You will automatically finish after working for 8 hours. You can cancel earlier if you wish, and there will You will automatically finish after working for 8 hours. You can cancel earlier if you wish, and there will
be no penalty because this is a part-time job. be no penalty because this is a part-time job.

@ -14,10 +14,13 @@ import "numeral/locales/no";
import "numeral/locales/pl"; import "numeral/locales/pl";
import "numeral/locales/ru"; import "numeral/locales/ru";
import { Settings } from "../Settings/Settings";
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
const extraFormats = [1e15, 1e18, 1e21, 1e24, 1e27, 1e30]; const extraFormats = [1e15, 1e18, 1e21, 1e24, 1e27, 1e30];
const extraNotations = ["q", "Q", "s", "S", "o", "n"]; const extraNotations = ["q", "Q", "s", "S", "o", "n"];
const gigaMultiplier = { standard: 1e9, iec60027_2: 2 ** 30 };
class NumeralFormatter { class NumeralFormatter {
// Default Locale // Default Locale
@ -110,11 +113,11 @@ class NumeralFormatter {
} }
formatRAM(n: number): string { formatRAM(n: number): string {
if (n < 1e3) return this.format(n, "0.00") + "GB"; if(Settings.UseIEC60027_2)
if (n < 1e6) return this.format(n / 1e3, "0.00") + "TB"; {
if (n < 1e9) return this.format(n / 1e6, "0.00") + "PB"; return this.format(n * gigaMultiplier.iec60027_2, "0.00ib");
if (n < 1e12) return this.format(n / 1e9, "0.00") + "EB"; }
return this.format(n, "0.00") + "GB"; return this.format(n * gigaMultiplier.standard, "0.00b");
} }
formatPercentage(n: number, decimalPlaces = 2): string { formatPercentage(n: number, decimalPlaces = 2): string {

18
src/utils/BlobCache.ts Normal file

@ -0,0 +1,18 @@
const blobCache: { [hash: string]: string } = {};
export class BlobCache {
static get(hash: string): string {
return blobCache[hash];
}
static store(hash: string, value: string): void {
if (blobCache[hash]) return;
blobCache[hash] = value;
}
static removeByValue(value: string): void {
const keys = Object.keys(blobCache).filter((key) => blobCache[key] === value);
keys.forEach((key) => delete blobCache[key]);
}
}

14
src/utils/ImportCache.ts Normal file

@ -0,0 +1,14 @@
import { ScriptUrl } from "../Script/ScriptUrl";
const importCache: { [hash: string]: ScriptUrl[] } = {};
export class ImportCache {
static get(hash: string): ScriptUrl[] {
return importCache[hash];
}
static store(hash: string, value: ScriptUrl[]): void {
if (importCache[hash]) return;
importCache[hash] = value;
}
}

@ -0,0 +1,12 @@
import { sha256 } from "js-sha256";
/**
* Computes a SHA-256 hash of a string synchronously
* @param message The input string
* @returns The SHA-256 hash in hex
*/
export function computeHash(message: string): string {
const hash = sha256.create();
hash.update(message);
return hash.hex();
}