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
/doc/build
/node_modules
/electron/node_modules
/dist/*.map
/test/*.map
/test/*.bundle.*

@ -118,10 +118,32 @@ Inside the root of the repo run
`npm install` to install all the dependencies
`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.
#### 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
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.
* @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.
@ -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.
* @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.
@ -3498,7 +3498,7 @@ export declare interface NS extends Singularity {
* Returns 0 if the script does not exist.
*
* @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.
*/
getScriptRam(script: string, host?: string): number;
@ -3678,8 +3678,9 @@ export declare interface NS extends Singularity {
* Queue a toast (bottom-right notification).
* @param msg - Message in the toast.
* @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.

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,
which refers to how much memory is available on that machine. RAM is
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
can be used to check how much RAM a server has.

@ -432,7 +432,10 @@ empty file will be created.
ps
^^
$ ps [-g, --grep pattern]
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
^^

@ -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
for every stat except hacking.
Working for a company is completely passive. However, you will not be able to do anything
else in the game while you work. 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.
Working for a company is completely passive. You can choose to focus on your work, do
something else simultaneously, or switch between those two. While you focus on work,
you will not be able to do anything else in the game. If you do something else meanwhile,
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
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`.
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
will gain. Note that while you are working for a faction, you cannot interact with
the rest of the game in any way. You can cancel your faction work at any time
with no penalty.
will gain. Note that while you are working for a faction, you can choose to not interact
with the rest of the game in any way to gain reputation at full speed. You can also select to
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
-------------------------------------

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 @typescript-eslint/no-var-requires */
const { app, BrowserWindow, Menu, shell, dialog } = require("electron");
const log = require('electron-log');
const { app, dialog, BrowserWindow } = require("electron");
const log = require("electron-log");
const greenworks = require("./greenworks");
const api = require("./api-server");
const gameWindow = require("./gameWindow");
const achievements = require("./achievements");
const utils = require("./utils");
log.catchErrors();
log.info(`Started app: ${JSON.stringify(process.argv)}`);
@ -18,162 +22,53 @@ if (greenworks.init()) {
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) {
const clearWindowHandler = () => {
if (window.achievementsIntervalID) {
clearInterval(window.achievementsIntervalID);
const closingWindowHandler = async (e) => {
// We need to prevent the default closing event to add custom logic
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;
};
@ -181,21 +76,41 @@ function setStopProcessHandler(app, window, enabled) {
log.info('Quitting the app...');
app.isQuiting = true;
app.quit();
// eslint-disable-next-line no-process-exit
process.exit(0);
};
if (enabled) {
log.debug('Adding closing handlers');
window.on("closed", clearWindowHandler);
window.on("close", closingWindowHandler)
app.on("window-all-closed", stopProcessHandler);
} else {
log.debug('Removing closing handlers');
window.removeListener("closed", clearWindowHandler);
window.removeListener("close", closingWindowHandler);
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!');
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"
}
}
}
}

@ -5,20 +5,24 @@
"main": "main.js",
"author": "Daniel Xie & Olivier Gagnon",
"mac": {
"icon": "./public/icons/mac/icon.icns",
"icon": "./public/icons/mac/icon.icns",
"category": "public.app-category.games"
},
"win": {
"icon": "./public/icons/png/256x256.png"
"icon": "./public/icons/png/256x256.png"
},
"files": [
"./build/**/*",
"./dist/**/*",
"./node_modules/**/*",
"./public/**/*",
"./public/**/*",
"*.js"
],
"directories": {
"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) | |
| [CharacterMult](./bitburner.charactermult.md) | |
| [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 |
| [CorporationInfo](./bitburner.corporationinfo.md) | General info about a corporation |
| [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
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
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
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
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
Get the percent of money stolen with a single thread.
Get the part of money stolen with a single thread.
<b>Signature:</b>
@ -22,13 +22,13 @@ hackAnalyze(host: string): 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
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
@ -36,6 +36,6 @@ Returns the percentage of the specified servers money you will steal with a s
```ts
//For example, assume the following returns 0.01:
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. |
| [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. |
| [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. |
| [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. |

@ -9,7 +9,7 @@ Start another script on the current server.
<b>Signature:</b>
```typescript
run(script: string, numThreads?: number, ...args: string[]): number;
run(script: string, numThreads?: number, ...args: Array<string | number | boolean>): number;
```
## Parameters
@ -18,7 +18,7 @@ run(script: string, numThreads?: number, ...args: string[]): number;
| --- | --- | --- |
| 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. |
| 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>

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

352
package-lock.json generated

@ -11,6 +11,7 @@
"dependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@material-ui/core": "^4.12.3",
"@microsoft/api-documenter": "^7.13.65",
"@microsoft/api-extractor": "^7.18.17",
"@monaco-editor/react": "^4.2.2",
@ -30,15 +31,19 @@
"better-react-mathjax": "^1.0.3",
"clsx": "^1.1.1",
"date-fns": "^2.25.0",
"electron-config": "^2.0.0",
"escodegen": "^1.11.0",
"file-saver": "^1.3.8",
"fs": "^0.0.1-security",
"jquery": "^3.5.0",
"js-sha256": "^0.9.0",
"jszip": "^3.7.0",
"material-ui-color": "^1.2.0",
"material-ui-popup-state": "^1.5.3",
"monaco-editor": "^0.27.0",
"notistack": "^2.0.2",
"numeral": "2.0.6",
"prop-types": "^15.8.0",
"raw-loader": "^4.0.2",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0",
@ -62,7 +67,6 @@
"babel-loader": "^8.0.5",
"cypress": "^8.3.1",
"electron": "^14.0.2",
"electron-log": "^4.4.3",
"electron-packager": "^15.4.0",
"eslint": "^7.24.0",
"fork-ts-checker-webpack-plugin": "^6.3.3",
@ -2892,7 +2896,6 @@
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz",
"integrity": "sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.4.4",
"@material-ui/styles": "^4.11.4",
@ -2929,7 +2932,6 @@
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz",
"integrity": "sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.4.4",
"@emotion/hash": "^0.8.0",
@ -2969,14 +2971,12 @@
"node_modules/@material-ui/styles/node_modules/csstype": {
"version": "2.6.18",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz",
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==",
"peer": true
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ=="
},
"node_modules/@material-ui/system": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.1.tgz",
"integrity": "sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.4.4",
"@material-ui/utils": "^4.11.2",
@ -3004,14 +3004,12 @@
"node_modules/@material-ui/system/node_modules/csstype": {
"version": "2.6.18",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz",
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==",
"peer": true
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ=="
},
"node_modules/@material-ui/types": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
"integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==",
"peer": true,
"peerDependencies": {
"@types/react": "*"
},
@ -3025,7 +3023,6 @@
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz",
"integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.4.4",
"prop-types": "^15.7.2",
@ -6074,8 +6071,7 @@
"node_modules/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==",
"peer": true
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"node_modules/clean-css": {
"version": "4.2.3",
@ -6381,6 +6377,58 @@
"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": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
@ -7546,6 +7594,17 @@
"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": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@ -7630,11 +7689,14 @@
"node": ">= 8.6"
}
},
"node_modules/electron-log": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.3.tgz",
"integrity": "sha512-IWxkiVLSpbI4if61kTSLMErYwz+Jq/gnHeTtQ8jcAjtlU8rgTIScWBgZJxk3fVnyvW6M+Ci3Bn9ogHgjgDSvNg==",
"dev": true
"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-notarize": {
"version": "1.1.1",
@ -11274,6 +11336,14 @@
"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": {
"version": "2.2.0",
"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",
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -14542,7 +14617,6 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/material-ui-popup-state/-/material-ui-popup-state-1.9.3.tgz",
"integrity": "sha512-+Ete5Tzw5rXlYfmqptOS8kBUH8vnK5OJsd6IQ7SHtLjU0PsvsmM73M/k8ot0xkX4RmPGuNRsFbK3mlCe/ClQuw==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@material-ui/types": "^6.0.1",
@ -14558,7 +14632,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-6.0.2.tgz",
"integrity": "sha512-/XUca4wUb9pWimLLdM1PE8KS8rTbDEGohSGkGtk3WST7lm23m+8RYv9uOmrvOg/VSsl4bMiOv4t2/LCb+RLbTg==",
"peer": true,
"peerDependencies": {
"@types/react": "*"
},
@ -16004,6 +16077,78 @@
"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": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.0.4.tgz",
@ -16026,8 +16171,7 @@
"node_modules/popper.js": {
"version": "1.16.1-lts",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz",
"integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==",
"peer": true
"integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA=="
},
"node_modules/portfinder": {
"version": "1.0.28",
@ -16180,13 +16324,13 @@
}
},
"node_modules/prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"version": "15.8.0",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz",
"integrity": "sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
"react-is": "^16.13.1"
}
},
"node_modules/prop-types/node_modules/react-is": {
@ -17700,8 +17844,7 @@
"node_modules/signal-exit": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz",
"integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==",
"dev": true
"integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ=="
},
"node_modules/sisteransi": {
"version": "1.0.5",
@ -23965,7 +24108,6 @@
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz",
"integrity": "sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw==",
"peer": true,
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/styles": "^4.11.4",
@ -23985,7 +24127,6 @@
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz",
"integrity": "sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew==",
"peer": true,
"requires": {
"@babel/runtime": "^7.4.4",
"@emotion/hash": "^0.8.0",
@ -24008,8 +24149,7 @@
"csstype": {
"version": "2.6.18",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz",
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==",
"peer": true
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ=="
}
}
},
@ -24017,7 +24157,6 @@
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.1.tgz",
"integrity": "sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw==",
"peer": true,
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/utils": "^4.11.2",
@ -24028,8 +24167,7 @@
"csstype": {
"version": "2.6.18",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz",
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==",
"peer": true
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ=="
}
}
},
@ -24037,14 +24175,12 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
"integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==",
"peer": true,
"requires": {}
},
"@material-ui/utils": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz",
"integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==",
"peer": true,
"requires": {
"@babel/runtime": "^7.4.4",
"prop-types": "^15.7.2",
@ -26401,8 +26537,7 @@
"classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==",
"peer": true
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"clean-css": {
"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": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
@ -27589,6 +27766,14 @@
"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": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@ -27703,11 +27888,13 @@
}
}
},
"electron-log": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.3.tgz",
"integrity": "sha512-IWxkiVLSpbI4if61kTSLMErYwz+Jq/gnHeTtQ8jcAjtlU8rgTIScWBgZJxk3fVnyvW6M+Ci3Bn9ogHgjgDSvNg==",
"dev": true
"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-notarize": {
"version": "1.1.1",
@ -30534,6 +30721,11 @@
"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": {
"version": "2.2.0",
"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",
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -33019,7 +33216,6 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/material-ui-popup-state/-/material-ui-popup-state-1.9.3.tgz",
"integrity": "sha512-+Ete5Tzw5rXlYfmqptOS8kBUH8vnK5OJsd6IQ7SHtLjU0PsvsmM73M/k8ot0xkX4RmPGuNRsFbK3mlCe/ClQuw==",
"peer": true,
"requires": {
"@babel/runtime": "^7.12.5",
"@material-ui/types": "^6.0.1",
@ -33031,7 +33227,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-6.0.2.tgz",
"integrity": "sha512-/XUca4wUb9pWimLLdM1PE8KS8rTbDEGohSGkGtk3WST7lm23m+8RYv9uOmrvOg/VSsl4bMiOv4t2/LCb+RLbTg==",
"peer": true,
"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": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.0.4.tgz",
@ -34194,8 +34442,7 @@
"popper.js": {
"version": "1.16.1-lts",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz",
"integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==",
"peer": true
"integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA=="
},
"portfinder": {
"version": "1.0.28",
@ -34310,13 +34557,13 @@
}
},
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"version": "15.8.0",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz",
"integrity": "sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
"react-is": "^16.13.1"
},
"dependencies": {
"react-is": {
@ -35509,8 +35756,7 @@
"signal-exit": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz",
"integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==",
"dev": true
"integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ=="
},
"sisteransi": {
"version": "1.0.5",

@ -12,6 +12,7 @@
"dependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@material-ui/core": "^4.12.3",
"@microsoft/api-documenter": "^7.13.65",
"@microsoft/api-extractor": "^7.18.17",
"@monaco-editor/react": "^4.2.2",
@ -31,15 +32,19 @@
"better-react-mathjax": "^1.0.3",
"clsx": "^1.1.1",
"date-fns": "^2.25.0",
"electron-config": "^2.0.0",
"escodegen": "^1.11.0",
"file-saver": "^1.3.8",
"fs": "^0.0.1-security",
"jquery": "^3.5.0",
"js-sha256": "^0.9.0",
"jszip": "^3.7.0",
"material-ui-color": "^1.2.0",
"material-ui-popup-state": "^1.5.3",
"monaco-editor": "^0.27.0",
"notistack": "^2.0.2",
"numeral": "2.0.6",
"prop-types": "^15.8.0",
"raw-loader": "^4.0.2",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0",
@ -64,7 +69,6 @@
"babel-loader": "^8.0.5",
"cypress": "^8.3.1",
"electron": "^14.0.2",
"electron-log": "^4.4.3",
"electron-packager": "^15.4.0",
"eslint": "^7.24.0",
"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 main.bundle.js .package/main.bundle.js
# Adding electron-log dependency
cp -r node_modules/electron-log .package/node_modules/electron-log
# Install electron sub-dependencies
cd electron
npm install
cd ..
# And finally build the app.
npm run electron:packager

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

@ -1925,7 +1925,7 @@ function initAugmentations(): void {
repCost: 7.5e3,
moneyCost: 3e7,
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_money_mult: 1.1,
dexterity_mult: 1.1,

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

@ -112,7 +112,7 @@ export const CONSTANTS: {
LatestUpdate: string;
} = {
VersionString: "1.2.0",
VersionNumber: 7,
VersionNumber: 8,
// Speed (in ms) at which the main loop is updated
_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) {
const mat = props.warehouse.materials[matName];
if (!MaterialSizes.hasOwnProperty(matName)) continue;
if (mat.qty === 0) continue;
breakdown = (
<>
{breakdown}
{matName}: {numeralWrapper.format(mat.qty * MaterialSizes[matName], "0,0.0")}
<br />
</>
);
breakdownItems.push(<>{matName}: {numeralWrapper.format(mat.qty * MaterialSizes[matName], "0,0.0")}</>);
}
for (const prodName in division.products) {
const prod = division.products[prodName];
if (prod === undefined) continue;
breakdown = (
<>
{breakdown}
{prodName}: {numeralWrapper.format(prod.data[props.warehouse.loc][0] * prod.siz, "0,0.0")}
</>
);
breakdownItems.push(<>{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 (
<Paper>
<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"}>
Storage: {numeralWrapper.formatBigNumber(props.warehouse.sizeUsed)} /{" "}
{numeralWrapper.formatBigNumber(props.warehouse.size)}

@ -4,6 +4,7 @@ import { DarkWebItems } from "./DarkWebItems";
import { Player } from "../Player";
import { Terminal } from "../Terminal";
import { SpecialServers } from "../Server/data/SpecialServers";
import { numeralWrapper } from "../ui/numeralFormat";
import { Money } from "../ui/React/Money";
import { DarkWebItem } from "./DarkWebItem";
@ -13,8 +14,8 @@ export function checkIfConnectedToDarkweb(): void {
if (server !== null && SpecialServers.DarkWeb == server.hostname) {
Terminal.print(
"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] " +
"to purchase an item.",
"Use the 'buy -l' command to display a list of all the items you can buy. Use 'buy [item-name]' " +
"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.",
);
}
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 { AugmentationNames } from "./Augmentation/data/AugmentationNames";
import React from "react";
import React, { useEffect } from "react";
import { General } from "./DevMenu/ui/General";
import { Stats } from "./DevMenu/ui/Stats";
@ -23,6 +23,7 @@ import { Sleeves } from "./DevMenu/ui/Sleeves";
import { Stanek } from "./DevMenu/ui/Stanek";
import { TimeSkip } from "./DevMenu/ui/TimeSkip";
import Typography from "@mui/material/Typography";
import { Exploit } from "./Exploits/Exploit";
interface IProps {
player: IPlayer;
@ -31,6 +32,9 @@ interface IProps {
}
export function DevMenuRoot(props: IProps): React.ReactElement {
useEffect(() => {
props.player.giveExploit(Exploit.YoureNotMeantToAccessThis);
}, []);
return (
<>
<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 { Page } from "./ui/Router";
import { removeLeadingSlash } from "./Terminal/DirectoryHelpers";
import { Terminal } from './Terminal';
import { SnackbarEvents } from "./ui/React/Snackbar";
import { IMap } from "./types";
interface Achievement {
ID: string;
@ -414,24 +417,27 @@ function calculateAchievements(): void {
}
export function initElectron(): void {
setAchievements([]);
initWebserver();
setInterval(calculateAchievements, 5000);
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.indexOf(' electron/') > -1) {
// Electron-specific code
setAchievements([]);
initWebserver();
setInterval(calculateAchievements, 5000);
initAppNotifier();
}
}
function initWebserver(): void {
(document as any).saveFile = function (filename: string, code: string): string {
filename = removeLeadingSlash(filename);
console.log(code);
if (removeLeadingSlash(filename).includes("/")) {
filename = "/" + removeLeadingSlash(filename);
}
code = Buffer.from(code, "base64").toString();
console.log(code);
const home = GetServer("home");
if (home === null) return "'home' server not found.";
if (home === null) return "Server should not be null but it is.";
if (isScriptFilename(filename)) {
//If the current script already exists on the server, overwrite it
for (let i = 0; i < home.scripts.length; i++) {
console.log(`${filename} ${home.scripts[i].filename}`);
if (filename == home.scripts[i].filename) {
home.scripts[i].saveScript(filename, code, "home", home.scripts);
return "written";
@ -448,3 +454,27 @@ function initWebserver(): void {
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",
RealityAlteration = "RealityAlteration",
N00dles = "N00dles",
YoureNotMeantToAccessThis = "YoureNotMeantToAccessThis",
// 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
// that's not the point.
@ -35,6 +36,7 @@ const names: {
UndocumentedFunctionCall: "by looking beyond the documentation.",
RealityAlteration: "by altering reality to suit your whims.",
N00dles: "by harnessing the power of the n00dles.",
YoureNotMeantToAccessThis: "by accessing the dev menu.",
};
export function ExploitName(exploit: string): string {

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

@ -1,7 +1,6 @@
import { isString } from "./utils/helpers/isString";
import { GetServer } from "./Server/AllServers";
import { WorkerScript } from "./Netscript/WorkerScript";
import { BlobsMap } from "./NetscriptJSEvaluator";
export function netscriptDelay(time: number, workerScript: WorkerScript): Promise<void> {
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}`);
}
for (const url in BlobsMap) {
msg = msg.replace(new RegExp(url, "g"), BlobsMap[url]);
for (const scriptUrl of workerScript.scriptRef.dependencies) {
msg = msg.replace(new RegExp(scriptUrl.url, "g"), scriptUrl.filename);
}
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.`,
);
}
if (requestedThreads > threads) {
if (requestedThreadsAsInt > threads) {
throw makeRuntimeRejectMsg(
workerScript,
`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 { NetscriptHacknet } from "./NetscriptFunctions/Hacknet";
import { NetscriptStanek } from "./NetscriptFunctions/Stanek";
import { NetscriptUserInterface } from './NetscriptFunctions/UserInterface';
import { NetscriptUserInterface } from "./NetscriptFunctions/UserInterface";
import { NetscriptBladeburner } from "./NetscriptFunctions/Bladeburner";
import { NetscriptCodingContract } from "./NetscriptFunctions/CodingContract";
import { NetscriptCorporation } from "./NetscriptFunctions/Corporation";
import { NetscriptFormulas } from "./NetscriptFunctions/Formulas";
import { NetscriptStockMarket } from "./NetscriptFunctions/StockMarket";
import { IPort } from "./NetscriptPort";
import {
NS as INS,
@ -442,6 +443,26 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
getServer: safeGetServer,
checkSingularityAccess: checkSingularityAccess,
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);
@ -952,7 +973,6 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return runScriptFromScript("run", scriptServer, scriptname, args, workerScript, threads);
},
exec: function (scriptname: any, hostname: any, threads: any = 1, ...args: any[]): any {
console.log(`${scriptname} ${hostname} ${threads} ${JSON.stringify(args)}`);
updateDynamicRam("exec", getRamCost("exec"));
if (scriptname === undefined || hostname === undefined) {
throw makeRuntimeErrorMsg("exec", "Usage: exec(scriptname, server, [numThreads], [arg1], [arg2]...)");
@ -989,7 +1009,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
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"));
let res;
@ -1737,25 +1757,13 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return res;
},
writePort: function (port: any, data: any = ""): any {
// Write to port
// Port 1-10
if (typeof data !== "string" && typeof data !== "number") {
throw makeRuntimeErrorMsg(
"writePort",
`Trying to write invalid data to a port: only strings and numbers are valid.`,
);
}
port = Math.round(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.`);
}
const iport = helper.getValidPort("writePort", port);
return Promise.resolve(iport.write(data));
},
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 {
// Read from port
// Port 1-10
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 iport = helper.getValidPort("readPort", port);
const x = iport.read();
return x;
},
@ -1878,23 +1875,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
},
peek: function (port: any): any {
updateDynamicRam("peek", getRamCost("peek"));
if (isNaN(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 iport = helper.getValidPort("peek", port);
const x = iport.peek();
return x;
},
@ -1918,38 +1899,12 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
},
clearPort: function (port: any): any {
// Clear port
port = Math.round(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.`);
}
const iport = helper.getValidPort("clearPort", port);
return iport.clear();
},
getPortHandle: function (port: any): any {
updateDynamicRam("getPortHandle", getRamCost("getPortHandle"));
if (isNaN(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.`);
}
const iport = helper.getValidPort("getPortHandle", port);
return iport;
},
rm: function (fn: any, hostname: any): any {
@ -2070,7 +2025,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
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"));
if (arguments.length === 0) {
const res = [];
@ -2099,7 +2054,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
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"));
if (arguments.length === 0) {
let total = 0;
@ -2318,10 +2273,12 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
...base,
...extra,
};
function getFunctionNames(obj: NS, prefix: string): string[] {
function getFunctionNames(obj: any, prefix: string): string[] {
const functionNames: string[] = [];
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);
} else if (typeof value == "object") {
functionNames.push(...getFunctionNames(value, key + "."));

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

@ -2,8 +2,10 @@ import { makeRuntimeRejectMsg } from "./NetscriptEvaluator";
import { ScriptUrl } from "./Script/ScriptUrl";
import { WorkerScript } from "./Netscript/WorkerScript";
import { Script } from "./Script/Script";
export const BlobsMap: { [key: string]: string } = {};
import { computeHash } from "./utils/helpers/computeHash";
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.
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.
await script.updateRamUsage(scripts);
const uurls = _getScriptUrls(script, scripts, []);
if (script.url) {
URL.revokeObjectURL(script.url); // remove the old reference.
delete BlobsMap[script.url];
const url = uurls[uurls.length - 1].url;
if (script.url && script.url !== 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 = uurls[uurls.length - 1].url;
script.module = new Promise((resolve) => resolve(eval("import(uurls[uurls.length - 1].url)")));
script.url = url;
script.module = new Promise((resolve) => resolve(eval("import(url)")));
script.dependencies = uurls;
}
@ -117,18 +128,27 @@ function _getScriptUrls(script: Script, scripts: Script[], seen: Script[]): Scri
let transformedCode = script.code.replace(
/((?:from|import)\s+(?:'|"))(?:\.\/)?([^'"]+)('|")/g,
(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;
// 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.
const urls = _getScriptUrls(importedScript, scripts, seen);
// Check to see if the urls for this script are stored in the cache by the hash value.
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.
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
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
// push that URL onto the top of the stack.
const su = new ScriptUrl(script.filename, URL.createObjectURL(makeScriptBlob(transformedCode)));
urlStack.push(su);
BlobsMap[su.url] = su.filename;
// If we successfully transformed the code, create a blob url for it
// Compute the hash for the transformed code
const transformedHash = computeHash(transformedCode);
// Check to see if this transformed hash is in our cache
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;
} catch (err) {
// 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.
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 " +
"promise-returning function? (Currently running: %s tried to run: %s)";
if (runningFn) {
@ -610,7 +610,7 @@ export function updateOnlineScriptTimes(numCycles = 1): void {
export function loadAllRunningScripts(): void {
const skipScriptLoad = window.location.href.toLowerCase().indexOf("?noscripts") !== -1;
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");
}
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
const script = server.scripts[i];
let ramUsage = script.ramUsage;
threads = Math.round(Number(threads));
threads = Math.floor(Number(threads));
if (threads === 0) {
return 0;
}

@ -654,6 +654,8 @@ export function finishWork(this: IPlayer, cancelled: boolean, sing = false): str
this.workRepGained *= this.cancelationPenalty();
}
const penaltyString = this.cancelationPenalty() === 0.5 ? "half" : "three-quarters";
const company = Companies[this.companyName];
company.playerReputation += this.workRepGained;
@ -665,12 +667,12 @@ export function finishWork(this: IPlayer, cancelled: boolean, sing = false): str
<Money money={this.workMoneyGained} />
<br />
<Reputation reputation={this.workRepGained} /> reputation for the company <br />
{numeralWrapper.formatExp(this.workHackExpGained)} hacking exp <br />
{numeralWrapper.formatExp(this.workStrExpGained)} strength exp <br />
{numeralWrapper.formatExp(this.workDefExpGained)} defense exp <br />
{numeralWrapper.formatExp(this.workDexExpGained)} dexterity exp <br />
{numeralWrapper.formatExp(this.workAgiExpGained)} agility exp <br />
{numeralWrapper.formatExp(this.workChaExpGained)} charisma exp
{this.workHackExpGained > 0 && <>{numeralWrapper.formatExp(this.workHackExpGained)} hacking exp <br /></>}
{this.workStrExpGained > 0 && <>{numeralWrapper.formatExp(this.workStrExpGained)} strength exp <br /></>}
{this.workDefExpGained > 0 && <>{numeralWrapper.formatExp(this.workDefExpGained)} defense exp <br /></>}
{this.workDexExpGained > 0 && <>{numeralWrapper.formatExp(this.workDexExpGained)} dexterity exp <br /></>}
{this.workAgiExpGained > 0 && <>{numeralWrapper.formatExp(this.workAgiExpGained)} agility exp <br /></>}
{this.workChaExpGained > 0 && <>{numeralWrapper.formatExp(this.workChaExpGained)} charisma exp <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 />
<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 />
{content}
</>
@ -1688,6 +1690,11 @@ export function applyForJob(this: IPlayer, entryPosType: CompanyPosition, sing =
return false;
}
// Check if this company has the position
if (!company.hasPosition(pos)) {
return false;
}
while (true) {
const newPos = getNextCompanyPositionHelper(pos);
if (newPos == null) {
@ -1861,9 +1868,14 @@ export function applyForAgentJob(this: IPlayer, sing = false): boolean {
export function applyForEmployeeJob(this: IPlayer, sing = false): boolean {
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.jobs[company.name] = posNames.MiscCompanyPositions[1];
this.jobs[company.name] = position;
if (!sing) {
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 {
const company = Companies[this.location]; //Company being applied to
if (this.isQualified(company, CompanyPositions[posNames.PartTimeCompanyPositions[1]])) {
this.jobs[company.name] = posNames.PartTimeCompanyPositions[1];
const position = 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) {
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 {
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.jobs[company.name] = posNames.MiscCompanyPositions[0];
this.jobs[company.name] = position;
if (!sing) {
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 {
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.jobs[company.name] = posNames.PartTimeCompanyPositions[0];
this.jobs[company.name] = position;
if (!sing) {
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;
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 {
currentSkill,
@ -24,7 +35,9 @@ export function calculateSkillProgress(exp: number, mult = 1): ISkillProgress {
baseExperience,
experience: exp,
nextExperience,
progress: (nextExperience - baseExperience !== 0) ? normalize(exp) : 99.99
currentExperience,
remainingExperience,
progress
}
}
@ -34,6 +47,8 @@ export interface ISkillProgress {
baseExperience: number;
experience: number;
nextExperience: number;
currentExperience: number;
remainingExperience: number;
progress: number;
}
@ -41,6 +56,7 @@ export function getEmptySkillProgress(): ISkillProgress {
return {
currentSkill: 0, nextSkill: 0,
baseExperience: 0, experience: 0, nextExperience: 0,
currentExperience: 0, remainingExperience: 0,
progress: 0,
};
}

@ -21,6 +21,7 @@ import { save } from "./db";
import { v1APIBreak } from "./utils/v1APIBreak";
import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation";
import { LocationName } from "./Locations/data/LocationNames";
/* SaveObject.js
* 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 { Script } from "../Script/Script";
import { WorkerScript } from "../Netscript/WorkerScript";
import { areImportsEquals } from "../Terminal/DirectoryHelpers";
// These special strings are used to reference the presence of a given logical
// construct within a user script.
@ -106,7 +107,7 @@ async function parseOnlyRamCalculate(
let script = null;
const fn = nextModule.startsWith("./") ? nextModule.slice(2) : nextModule;
for (const s of otherScripts) {
if (s.filename === fn) {
if (areImportsEquals(s.filename, fn)) {
script = s;
break;
}

@ -3,6 +3,7 @@
* A Script can have multiple active instances
*/
import { Script } from "./Script";
import { ScriptUrl } from "./ScriptUrl";
import { Settings } from "../Settings/Settings";
import { IMap } from "../types";
import { Terminal } from "../Terminal";
@ -58,6 +59,9 @@ export class RunningScript {
// Number of threads that this script is running with
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[] = []) {
if (script == null) {
return;
@ -66,6 +70,7 @@ export class RunningScript {
this.args = args;
this.server = script.server;
this.ramUsage = script.ramUsage;
this.dependencies = script.dependencies;
}
log(txt: string): void {

@ -9,6 +9,7 @@ import { ScriptUrl } from "./ScriptUrl";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver";
import { roundToTwo } from "../utils/helpers/roundToTwo";
import { computeHash } from "../utils/helpers/computeHash";
let globalModuleSequenceNumber = 0;
@ -40,6 +41,9 @@ export class Script {
// hostname of server that this script is on.
server = "";
// sha256 hash of the code in the Script. Do not access directly.
_hash = "";
constructor(fn = "", code = "", server = "", otherScripts: Script[] = []) {
this.filename = fn;
this.code = code;
@ -47,8 +51,10 @@ export class Script {
this.server = server; // hostname of server this script is on
this.module = "";
this.moduleSequenceNumber = ++globalModuleSequenceNumber;
this._hash = "";
if (this.code !== "") {
this.updateRamUsage(otherScripts);
this.rehash();
}
}
@ -84,6 +90,24 @@ export class Script {
markUpdated(): void {
this.module = "";
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
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
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;
/**
* Calculate profit of setting stocks.
* Calculate profit of selling stocks.
* @remarks
* RAM cost: 2 GB
* Calculates and returns how much you would gain from selling a given number of shares of a stock.
@ -1765,9 +1765,9 @@ export interface Singularity {
* function will automatically cancel that action and give you your
* earnings.
*
* 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 60000).
* 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
@ -2305,7 +2305,7 @@ export interface Hacknet {
*
* @example
* ```ts
* // NS1:
* // NS1:
* var upgradeName = "Sell for Corporation Funds";
* if (hacknet.numHashes() > hacknet.hashCost(upgradeName)) {
* hacknet.spendHashes(upgName);
@ -2313,7 +2313,7 @@ export interface Hacknet {
* ```
* @example
* ```ts
* // NS2:
* // NS2:
* const upgradeName = "Sell for Corporation Funds";
* if (ns.hacknet.numHashes() > ns.hacknet.hashCost(upgradeName)) {
* ns.hacknet.spendHashes(upgName);
@ -2371,7 +2371,7 @@ export interface Hacknet {
* ```
* @example
* ```ts
* // NS2:
* // NS2:
* const upgrades = ns.hacknet.getHashUpgrades(); // ["Sell for Money","Sell for Corporation Funds",...]
* ```
* @returns An array containing the available upgrades
@ -2890,7 +2890,7 @@ export interface Bladeburner {
}
/**
* Coding Contact API
* Coding Contract API
* @public
*/
export interface CodingContract {
@ -4079,31 +4079,28 @@ export interface NS extends Singularity {
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
* 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).
* This percentage is influenced by the player's hacking skill.
* Returns the part of the specified servers money you will steal with a single thread hack.
*
* @example
* ```ts
* // NS1:
* //For example, assume the following returns 0.01:
* 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
* ```ts
* // NS2:
* //For example, assume the following returns 0.01:
* 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.
* @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;
@ -4304,10 +4301,10 @@ export interface NS extends Singularity {
* // NS1:
* //Get logs from foo.script on the current server that was run with no args
* getScriptLogs("foo.script");
*
*
* //Open logs from foo.script on the foodnstuff server that was run with no args
* getScriptLogs("foo.script", "foodnstuff");
*
*
* //Open logs from foo.script on the foodnstuff server that was run with the arguments [1, "test"]
* getScriptLogs("foo.script", "foodnstuff", 1, "test");
* ```
@ -4316,10 +4313,10 @@ export interface NS extends Singularity {
* // NS2:
* //Get logs from foo.script on the current server that was run with no args
* ns.getScriptLogs("foo.script");
*
*
* //Open logs from foo.script on the foodnstuff server that was run with no args
* ns.getScriptLogs("foo.script", "foodnstuff");
*
*
* //Open logs from foo.script on the foodnstuff server that was run with the arguments [1, "test"]
* ns.getScriptLogs("foo.script", "foodnstuff", 1, "test");
* ```
@ -4347,10 +4344,10 @@ export interface NS extends Singularity {
* // NS1:
* //Open logs from foo.script on the current server that was run with no args
* tail("foo.script");
*
*
* //Get logs from foo.script on the foodnstuff server that was run with no args
* tail("foo.script", "foodnstuff");
*
*
* //Get logs from foo.script on the foodnstuff server that was run with the arguments [1, "test"]
* tail("foo.script", "foodnstuff", 1, "test");
* ```
@ -4359,10 +4356,10 @@ export interface NS extends Singularity {
* // NS2:
* //Open logs from foo.script on the current server that was run with no args
* ns.tail("foo.script");
*
*
* //Get logs from foo.script on the foodnstuff server that was run with no args
* ns.tail("foo.script", "foodnstuff");
*
*
* //Get logs from foo.script on the foodnstuff server that was run with the arguments [1, "test"]
* ns.tail("foo.script", "foodnstuff", 1, "test");
* ```
@ -4536,10 +4533,10 @@ export interface NS extends Singularity {
* // NS1:
* //The simplest way to use the run command is to call it with just the script name. The following example will run foo.script single-threaded with no arguments:
* run("foo.script");
*
*
* //The following example will run foo.script but with 5 threads instead of single-threaded:
* run("foo.script", 5);
*
*
* //This next example will run foo.script single-threaded, and will pass the string foodnstuff into the script as an argument:
* run("foo.script", 1, 'foodnstuff');
* ```
@ -4548,10 +4545,10 @@ export interface NS extends Singularity {
* // NS2:
* //The simplest way to use the run command is to call it with just the script name. The following example will run foo.script single-threaded with no arguments:
* ns.run("foo.script");
*
*
* //The following example will run foo.script but with 5 threads instead of single-threaded:
* ns.run("foo.script", 5);
*
*
* //This next example will run foo.script single-threaded, and will pass the string foodnstuff into the script as an argument:
* ns.run("foo.script", 1, 'foodnstuff');
* ```
@ -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.
* @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.
@ -4584,10 +4581,10 @@ export interface NS extends Singularity {
* // NS1:
* //The simplest way to use the exec command is to call it with just the script name and the target server. The following example will try to run generic-hack.script on the foodnstuff server:
* exec("generic-hack.script", "foodnstuff");
*
*
* //The following example will try to run the script generic-hack.script on the joesguns server with 10 threads:
* exec("generic-hack.script", "joesguns", 10);
*
*
* //This last example will try to run the script foo.script on the foodnstuff server with 5 threads. It will also pass the number 1 and the string “test” in as arguments to the script:
* exec("foo.script", "foodnstuff", 5, 1, "test");
* ```
@ -4596,10 +4593,10 @@ export interface NS extends Singularity {
* // NS2:
* //The simplest way to use the exec command is to call it with just the script name and the target server. The following example will try to run generic-hack.script on the foodnstuff server:
* ns.exec("generic-hack.script", "foodnstuff");
*
*
* //The following example will try to run the script generic-hack.script on the joesguns server with 10 threads:
* ns.exec("generic-hack.script", "joesguns", 10);
*
*
* //This last example will try to run the script foo.script on the foodnstuff server with 5 threads. It will also pass the number 1 and the string “test” in as arguments to the script:
* ns.exec("foo.script", "foodnstuff", 5, 1, "test");
* ```
@ -4656,10 +4653,10 @@ export interface NS extends Singularity {
* // NS1:
* //The following example will try to kill a script named foo.script on the foodnstuff server that was ran with no arguments:
* kill("foo.script", "foodnstuff");
*
*
* //The following will try to kill a script named foo.script on the current server that was ran with no arguments:
* kill("foo.script", getHostname());
*
*
* //The following will try to kill a script named foo.script on the current server that was ran with the arguments 1 and “foodnstuff”:
* kill("foo.script", getHostname(), 1, "foodnstuff");
* ```
@ -4668,10 +4665,10 @@ export interface NS extends Singularity {
* // NS2:
* //The following example will try to kill a script named foo.script on the foodnstuff server that was ran with no arguments:
* ns.kill("foo.script", "foodnstuff");
*
*
* //The following will try to kill a script named foo.script on the current server that was ran with no arguments:
* ns.kill("foo.script", getHostname());
*
*
* //The following will try to kill a script named foo.script on the current server that was ran with the arguments 1 and “foodnstuff”:
* ns.kill("foo.script", getHostname(), 1, "foodnstuff");
* ```
@ -4680,7 +4677,8 @@ export interface NS extends Singularity {
* @param args - Arguments to identify which script to kill.
* @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.
@ -4690,11 +4688,12 @@ export interface NS extends Singularity {
* 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
* 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.
* @returns True if any scripts were killed, and false otherwise.
*/
killall(host: string): boolean;
killall(host?: string): boolean;
/**
* Terminates the current script immediately.
@ -4716,7 +4715,7 @@ export interface NS extends Singularity {
* // NS1:
* //Copies foo.lit from the helios server to the home computer:
* scp("foo.lit", "helios", "home");
*
*
* //Tries to copy three files from rothman-uni to home computer:
* files = ["foo1.lit", "foo2.script", "foo3.script"];
* scp(files, "rothman-uni", "home");
@ -4726,7 +4725,7 @@ export interface NS extends Singularity {
* // NS2:
* //Copies foo.lit from the helios server to the home computer:
* await ns.scp("foo.lit", "helios", "home");
*
*
* //Tries to copy three files from rothman-uni to home computer:
* files = ["foo1.lit", "foo2.script", "foo3.script"];
* await ns.scp(files, "rothman-uni", "home");
@ -5086,7 +5085,7 @@ export interface NS extends Singularity {
* // NS1:
* //The function call will return true if the script named foo.script exists on the foodnstuff server, and false otherwise.
* fileExists("foo.script", "foodnstuff");
*
*
* //The function call will return true if the current server contains the FTPCrack.exe program, and false otherwise.
* fileExists("ftpcrack.exe");
* ```
@ -5095,7 +5094,7 @@ export interface NS extends Singularity {
* // NS2:
* // The function call will return true if the script named foo.script exists on the foodnstuff server, and false otherwise.
* ns.fileExists("foo.script", "foodnstuff");
*
*
* // The function call will return true if the current server contains the FTPCrack.exe program, and false otherwise.
* ns.fileExists("ftpcrack.exe");
* ```
@ -5118,10 +5117,10 @@ export interface NS extends Singularity {
* // NS1:
* //The function call will return true if there is a script named foo.script with no arguments running on the foodnstuff server, and false otherwise:
* isRunning("foo.script", "foodnstuff");
*
*
* //The function call will return true if there is a script named foo.script with no arguments running on the current server, and false otherwise:
* isRunning("foo.script", getHostname());
*
*
* //The function call will return true if there is a script named foo.script running with the arguments 1, 5, and “test” (in that order) on the joesguns server, and false otherwise:
* isRunning("foo.script", "joesguns", 1, 5, "test");
* ```
@ -5130,10 +5129,10 @@ export interface NS extends Singularity {
* // NS2:
* //The function call will return true if there is a script named foo.script with no arguments running on the foodnstuff server, and false otherwise:
* ns.isRunning("foo.script", "foodnstuff");
*
*
* //The function call will return true if there is a script named foo.script with no arguments running on the current server, and false otherwise:
* ns.isRunning("foo.script", ns.getHostname());
*
*
* //The function call will return true if there is a script named foo.script running with the arguments 1, 5, and “test” (in that order) on the joesguns server, and false otherwise:
* ns.isRunning("foo.script", "joesguns", 1, 5, "test");
* ```
@ -5153,7 +5152,7 @@ export interface NS extends Singularity {
*
* @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.
@ -5287,7 +5286,7 @@ export interface NS extends Singularity {
* @param data - Data to write.
* @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.
@ -5421,7 +5420,7 @@ export interface NS extends Singularity {
* // NS1:
* //The function call will return true if there is any script named foo.script running on the foodnstuff server, and false otherwise:
* scriptRunning("foo.script", "foodnstuff");
*
*
* //The function call will return true if there is any script named “foo.script” running on the current server, and false otherwise:
* scriptRunning("foo.script", getHostname());
* ```
@ -5430,7 +5429,7 @@ export interface NS extends Singularity {
* // NS2:
* //The function call will return true if there is any script named foo.script running on the foodnstuff server, and false otherwise:
* ns.scriptRunning("foo.script", "foodnstuff");
*
*
* //The function call will return true if there is any script named “foo.script” running on the current server, and false otherwise:
* ns.scriptRunning("foo.script", ns.getHostname());
* ```
@ -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.
*
* @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.
*/
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.
*
* @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.
*/
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.
*
* @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.
*/
getWeakenTime(host: string): number;
@ -5548,7 +5541,8 @@ export interface NS extends Singularity {
* @param args - Arguments that the script is running with.
* @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.
@ -5567,6 +5561,7 @@ export interface NS extends Singularity {
* @param args - Arguments that the script is running with.
* @returns Amount of hacking experience the specified script generates while online.
*/
getScriptExpGain(): number;
getScriptExpGain(script: string, host: string, ...args: string[]): number;
/**
@ -5787,7 +5782,7 @@ export interface NS extends Singularity {
* ['help', false], // a default boolean means this flag is a boolean
* ]);
* tprint(data);
*
*
* // example.ns
* export async function main(ns) {
* const data = ns.flags([
@ -6127,7 +6122,7 @@ interface CorporationInfo {
issuedShares: number;
/** Price of the shares */
sharePrice: number;
/** State of the corporation, like PRODUCTION or EXPORT */
/** State of the corporation. Possible states are START, PURCHASE, PRODUCTION, SALE, EXPORT. */
state: string;
}

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

@ -93,26 +93,21 @@ class OpenScript {
}
}
let openScripts: OpenScript[] = [];
let currentScript: OpenScript | null = null;
// Called every time script editor is opened
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 monacoRef = useRef<Monaco | null>(null);
const vimStatusRef = useRef<HTMLElement>(null);
const [vimEditor, setVimEditor] = useState<any>(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 [updatingRam, setUpdatingRam] = useState(false);
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(() => {
if (currentScript !== null) {
updateRAM(currentScript.code);
@ -171,23 +146,23 @@ export function Root(props: IProps): React.ReactElement {
}, []);
useEffect(() => {
function maybeSave(event: KeyboardEvent): void {
function keydown(event: KeyboardEvent): void {
if (Settings.DisableHotkeys) return;
//Ctrl + b
if (event.keyCode == 66 && (event.ctrlKey || event.metaKey)) {
if (event.code == "KeyB" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
save();
props.router.toTerminal();
}
// CTRL/CMD + S
if (event.code == `KeyS` && (event.ctrlKey || event.metaKey)) {
if (event.code == "KeyS" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
event.stopPropagation();
save();
}
}
document.addEventListener("keydown", maybeSave);
return () => document.removeEventListener("keydown", maybeSave);
document.addEventListener("keydown", keydown);
return () => document.removeEventListener("keydown", keydown);
});
useEffect(() => {
@ -203,7 +178,12 @@ export function Root(props: IProps): React.ReactElement {
save();
});
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();
props.router.toTerminal();
});
editor.focus();
});
@ -362,7 +342,7 @@ export function Root(props: IProps): React.ReactElement {
regenerateModel(openScript);
}
setCurrentScript(openScript);
currentScript = openScript;
editorRef.current.setModel(openScript.model);
editorRef.current.setPosition(openScript.lastPosition);
editorRef.current.revealLineInCenter(openScript.lastPosition.lineNumber);
@ -376,14 +356,12 @@ export function Root(props: IProps): React.ReactElement {
new monacoRef.current.Position(0, 0),
monacoRef.current.editor.createModel(code, filename.endsWith(".txt") ? "plaintext" : "javascript"),
);
setOpenScripts((oldArray) => [...oldArray, newScript]);
setCurrentScript({ ...newScript });
openScripts.push(newScript);
currentScript = { ...newScript };
editorRef.current.setModel(newScript.model);
updateRAM(newScript.code);
}
}
} else {
console.log("here we need to load something if we can");
}
editorRef.current.focus();
@ -422,24 +400,26 @@ export function Root(props: IProps): React.ReactElement {
function updateCode(newCode?: string): void {
if (newCode === undefined) return;
updateRAM(newCode);
if (editorRef.current !== null) {
const newPos = editorRef.current.getPosition();
if (newPos === null) return;
setCurrentScript((oldScript) => ({ ...oldScript!, code: newCode, lastPosition: newPos! }));
if (currentScript !== null) {
const curIndex = openScripts.findIndex(
(script) => script.fileName === currentScript.fileName && script.hostname === currentScript.hostname,
);
const newArr = [...openScripts];
const tempScript = currentScript;
tempScript.code = newCode;
newArr[curIndex] = tempScript;
setOpenScripts([...newArr]);
}
try {
infLoop(newCode);
} catch (err) {}
if (editorRef.current === null) return;
const newPos = editorRef.current.getPosition();
if (newPos === null) return;
if (currentScript !== null) {
currentScript = { ...currentScript, code: newCode, lastPosition: newPos };
const curIndex = openScripts.findIndex(
(script) =>
currentScript !== null &&
script.fileName === currentScript.fileName &&
script.hostname === currentScript.hostname,
);
const newArr = [...openScripts];
const tempScript = currentScript;
tempScript.code = newCode;
newArr[curIndex] = tempScript;
openScripts = [...newArr];
}
try {
infLoop(newCode);
} catch (err) {}
}
function saveScript(scriptToSave: OpenScript): void {
@ -487,7 +467,7 @@ export function Root(props: IProps): React.ReactElement {
function save(): void {
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;
}
// this is duplicate code with saving later.
@ -507,7 +487,6 @@ export function Root(props: IProps): React.ReactElement {
iTutorialNextStep();
props.router.toTerminal();
return;
}
@ -536,7 +515,6 @@ export function Root(props: IProps): React.ReactElement {
server.scripts,
);
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
return;
}
}
@ -550,7 +528,6 @@ export function Root(props: IProps): React.ReactElement {
if (server.textFiles[i].fn === currentScript.fileName) {
server.textFiles[i].write(currentScript.code);
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
return;
}
}
@ -562,7 +539,6 @@ export function Root(props: IProps): React.ReactElement {
}
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
}
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);
setOpenScripts(items);
openScripts = items;
}
function onTabClick(index: number): void {
if (currentScript !== null) {
// Save currentScript to openScripts
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;
}
setCurrentScript({ ...openScripts[index] });
currentScript = { ...openScripts[index] };
if (editorRef.current !== null && openScripts[index] !== null) {
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
const closingScript = openScripts[index];
const savedOpenScripts: Array<OpenScript> = JSON.parse(window.localStorage.getItem("scriptEditorOpenScripts")!);
const savedScriptIndex = savedOpenScripts.findIndex(
const savedScriptIndex = openScripts.findIndex(
(script) => script.fileName === closingScript.fileName && script.hostname === closingScript.hostname,
);
let savedScriptCode = "";
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(
(script) => script.filename === closingScript.fileName,
);
if (
serverScriptIndex === -1 ||
savedScriptCode !== GetServer(closingScript.hostname)?.scripts[serverScriptIndex as number].code
) {
const serverScriptIndex = server.scripts.findIndex((script) => script.filename === closingScript.fileName);
if (serverScriptIndex === -1 || savedScriptCode !== server.scripts[serverScriptIndex as number].code) {
PromptEvent.emit({
txt: "Do you want to save changes to " + closingScript.fileName + "?",
resolve: (result: boolean) => {
@ -641,15 +616,18 @@ export function Root(props: IProps): React.ReactElement {
}
if (openScripts.length > 1) {
setOpenScripts((oldScripts) => oldScripts.filter((value, i) => i !== index));
openScripts = openScripts.filter((value, i) => i !== index);
let indexOffset = -1;
if (openScripts[index + indexOffset] === undefined) {
indexOffset = 1;
if (openScripts[index + indexOffset] === undefined) {
indexOffset = 0;
}
}
// Change current script if we closed it
setCurrentScript(openScripts[index + indexOffset]);
currentScript = openScripts[index + indexOffset];
if (editorRef.current !== null) {
if (
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.focus();
}
rerender();
} else {
// No more scripts are open
setOpenScripts([]);
setCurrentScript(null);
openScripts = [];
currentScript = null;
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:
// 8px body margin top
// 38.5px filename tabs
@ -718,7 +708,6 @@ export function Root(props: IProps): React.ReactElement {
}}
>
<Button
id={"tabButton" + fileName + hostname}
onClick={() => onTabClick(index)}
style={{
background:
@ -727,10 +716,9 @@ export function Root(props: IProps): React.ReactElement {
: "",
}}
>
{hostname}:~/{fileName}
{hostname}:~/{fileName} {dirty(index)}
</Button>
<Button
id={"tabCloseButton" + fileName + hostname}
onClick={() => onTabClose(index)}
style={{
maxWidth: "20px",
@ -779,7 +767,8 @@ export function Root(props: IProps): React.ReactElement {
<Typography color={updatingRam ? "secondary" : "primary"} sx={{ mx: 1 }}>
{ram}
</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 }}>
{" "}
Documentation:{" "}
@ -825,10 +814,14 @@ export function Root(props: IProps): React.ReactElement {
alignItems: "center",
}}
>
<p style={{ color: Settings.theme.primary, fontSize: "20px", textAlign: "center" }}>
<h1>No open files</h1>
<h5>Use "nano [File Name]" in the terminal to open files</h5>
</p>
<span style={{ color: Settings.theme.primary, fontSize: "20px", textAlign: "center" }}>
<Typography variant="h4">No open files</Typography>
<Typography variant="h5">
Use `nano FILENAME` in
<br />
the terminal to open files
</Typography>
</span>
</div>
</>
);

@ -218,4 +218,99 @@ export async function loadThemes(monaco: { editor: any }): Promise<void> {
"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: ITheme;
/*
* Use GiB instead of GB
*/
UseIEC60027_2: boolean;
}
/**
@ -160,6 +165,7 @@ export const defaultSettings: IDefaultSettings = {
SuppressBladeburnerPopup: false,
SuppressTIXPopup: false,
SuppressSavedGameToast: false,
UseIEC60027_2: false,
theme: defaultTheme,
};
@ -192,6 +198,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
SuppressBladeburnerPopup: defaultSettings.SuppressBladeburnerPopup,
SuppressTIXPopup: defaultSettings.SuppressTIXPopup,
SuppressSavedGameToast: defaultSettings.SuppressSavedGameToast,
UseIEC60027_2: defaultSettings.UseIEC60027_2,
MonacoTheme: "monokai",
MonacoInsertSpaces: false,
MonacoFontSize: 20,

@ -408,4 +408,206 @@ export const getPredefinedThemes = (): IMap<IPredefinedTheme> => ({
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
function handleShortcuts(this: Document, event: KeyboardEvent): any {
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) {
event.preventDefault();
clickTerminal();

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

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

@ -176,7 +176,7 @@ export function InfoAndPurchases(props: IProps): React.ReactElement {
<>
<Typography>Welcome to the World Stock Exchange (WSE)!</Typography>
<Link href={documentationLink} target={"_blank"}>
Investopedia
<Typography>Investopedia</Typography>
</Link>
<br />
<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,
* not an entire path
*/
export function isValidDirectoryName(name: string): boolean {
export function isValidDirectoryName(name: string): boolean {
// Allows alphanumerics, hyphens, underscores, and percentage signs.
// Name can begin with a single period, but otherwise cannot have any
const regex = /^.?[a-zA-Z0-9_-]+$/;
@ -310,3 +310,9 @@ export function areFilesEqual(f0: string, f1: string): boolean {
if (!f1.startsWith("/")) f1 = "/" + 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",
"ls [dir] [| grep pattern] Displays all files 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",
"nano [file ...] Text editor - Open up and edit one or more scripts or text files",
"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",
"scan Prints all immediately-available network connections",
"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",
"tail [script] [args...] Displays dynamic logs for the specified script",
"top Displays all running scripts and their RAM usage",
@ -80,7 +80,7 @@ export const HelpTexts: IMap<string[]> = {
" ",
],
analyze: [
"analze",
"analyze",
" ",
"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 ",
@ -96,13 +96,15 @@ export const HelpTexts: IMap<string[]> = {
" ",
],
buy: [
"buy [-l / program]",
"buy [-l / -a / program]",
" ",
"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 ",
"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.",
],
cat: [
@ -191,7 +193,7 @@ export const HelpTexts: IMap<string[]> = {
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.",
],
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.",
],
scp: [
"scp [filename] [target server]",
"scp [filename ...] [target server]",
" ",
"Copies the specified file from the current server to the target server. ",
"This command only works for script files (.script extension), literature files (.lit extension), ",
"Copies the specified file(s) from the current server to the target server. ",
"This command only works for script files (.script or .js extension), literature files (.lit extension), ",
"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"],

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

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

@ -2,6 +2,7 @@ import { ITerminal } from "../ITerminal";
import { IRouter } from "../../ui/Router";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { BaseServer } from "../../Server/BaseServer";
import * as libarg from "arg"
export function ps(
terminal: ITerminal,
@ -10,16 +11,40 @@ export function ps(
server: BaseServer,
args: (string | number | boolean)[],
): void {
if (args.length !== 0) {
terminal.error("Incorrect usage of ps command. Usage: ps");
let flags;
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;
}
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();
const pattern = flags['--grep']
if (pattern) {
const re = new RegExp(pattern.toString())
const matching = server.runningScripts.filter((x) => re.test(x.filename))
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);
}
}

@ -350,7 +350,12 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
event.preventDefault();
modifyInput("deletewordbefore");
}
if (event.keyCode === KEY.D && event.altKey) {
event.preventDefault();
modifyInput("deletewordafter");
}
if (event.keyCode === KEY.U && event.ctrlKey) {
event.preventDefault();
modifyInput("clearbefore");
@ -360,9 +365,6 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
event.preventDefault();
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.
<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>
</>
),

@ -28,6 +28,7 @@ import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
import { dialogBoxCreate } from "./DialogBox";
import { ConfirmationModal } from "./ConfirmationModal";
import { ThemeEditorModal } from "./ThemeEditorModal";
import { SnackbarEvents } from "./Snackbar";
import { Settings } from "../../Settings/Settings";
import { save, deleteGame } from "../../db";
@ -51,6 +52,12 @@ interface IProps {
softReset: () => void;
}
interface ImportData {
base64: string;
parsed: any;
exportDate?: Date;
}
export function GameOptionsRoot(props: IProps): React.ReactElement {
const classes = useStyles();
const importInput = useRef<HTMLInputElement>(null);
@ -78,12 +85,15 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
const [enableBashHotkeys, setEnableBashHotkeys] = useState(Settings.EnableBashHotkeys);
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
const [saveGameOnFileSave, setSaveGameOnFileSave] = useState(Settings.SaveGameOnFileSave);
const [useIEC60027_2, setUseIEC60027_2] = useState(Settings.UseIEC60027_2);
const [locale, setLocale] = useState(Settings.Locale);
const [diagnosticOpen, setDiagnosticOpen] = useState(false);
const [deleteGameOpen, setDeleteOpen] = useState(false);
const [themeEditorOpen, setThemeEditorOpen] = 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 {
setExecTime(newValue as number);
@ -154,6 +164,10 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
setDisableASCIIArt(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 {
setDisableTextEffects(event.target.checked);
@ -206,11 +220,60 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
return;
}
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);
}
function confirmedImportGame(): void {
if (!importData) return;
setImportSaveOpen(false);
save(importData.base64).then(() => {
setImportData(null);
setTimeout(() => location.reload(), 1000)
});
}
function doSoftReset(): void {
if (!Settings.SuppressBuyAugmentationConfirmation) {
setSoftResetOpen(true);
@ -292,7 +355,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Tooltip
title={
<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.
</Typography>
}
@ -513,6 +576,16 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
}
/>
</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>
<Tooltip
title={
@ -618,19 +691,41 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Button onClick={() => setDeleteOpen(true)}>Delete Game</Button>
</Box>
<Box>
<Tooltip title={<Typography>export</Typography>}>
<Tooltip title={<Typography>Export your game to a text file.</Typography>}>
<Button onClick={() => props.export()}>
<DownloadIcon color="primary" />
Export
Export Game
</Button>
</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}>
<UploadIcon color="primary" />
Import
Import Game
<input ref={importInput} id="import-game-file-selector" type="file" hidden onChange={onImport} />
</Button>
</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>
<Tooltip

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

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

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

@ -62,20 +62,14 @@ export function WorkInProgressRoot(): React.ReactElement {
<Reputation reputation={player.workRepGained} /> (
<ReputationRate reputation={player.workRepGainRate * CYCLES_PER_SEC} />) reputation for this faction <br />
<br />
{numeralWrapper.formatExp(player.workHackExpGained)} (
{numeralWrapper.formatExp(player.workHackExpGainRate * CYCLES_PER_SEC)} / sec) hacking exp <br />
{player.workHackExpGained > 0 && <>{numeralWrapper.formatExp(player.workHackExpGained)} ({numeralWrapper.formatExp(player.workHackExpGainRate * CYCLES_PER_SEC)} / sec) hacking exp <br /></>}
<br />
{numeralWrapper.formatExp(player.workStrExpGained)} (
{numeralWrapper.formatExp(player.workStrExpGainRate * CYCLES_PER_SEC)} / sec) strength exp <br />
{numeralWrapper.formatExp(player.workDefExpGained)} (
{numeralWrapper.formatExp(player.workDefExpGainRate * CYCLES_PER_SEC)} / sec) defense 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 />
{player.workStrExpGained > 0 && <>{numeralWrapper.formatExp(player.workStrExpGained)} ({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 /></>}
{player.workDexExpGained > 0 && <>{numeralWrapper.formatExp(player.workDexExpGained)} ({numeralWrapper.formatExp(player.workDexExpGainRate * CYCLES_PER_SEC)} / sec) dexterity exp <br /></>}
{player.workAgiExpGained > 0 && <>{numeralWrapper.formatExp(player.workAgiExpGained)} ({numeralWrapper.formatExp(player.workAgiExpGainRate * CYCLES_PER_SEC)} / sec) agility exp <br /></>}
<br />
{numeralWrapper.formatExp(player.workChaExpGained)} (
{numeralWrapper.formatExp(player.workChaExpGainRate * CYCLES_PER_SEC)} / sec) charisma exp <br />
{player.workChaExpGained > 0 && <>{numeralWrapper.formatExp(player.workChaExpGained)} ({numeralWrapper.formatExp(player.workChaExpGainRate * CYCLES_PER_SEC)} / sec) charisma exp <br /></>}
<br />
You will automatically finish after working for 20 hours. You can cancel earlier if you wish.
<br />
@ -123,18 +117,12 @@ export function WorkInProgressRoot(): React.ReactElement {
<br />
<br />
You have gained: <br />
{numeralWrapper.formatExp(player.workHackExpGained)} (
{numeralWrapper.formatExp(player.workHackExpGainRate * CYCLES_PER_SEC)} / sec) hacking exp <br />
{numeralWrapper.formatExp(player.workStrExpGained)} (
{numeralWrapper.formatExp(player.workStrExpGainRate * CYCLES_PER_SEC)} / sec) strength exp <br />
{numeralWrapper.formatExp(player.workDefExpGained)} (
{numeralWrapper.formatExp(player.workDefExpGainRate * CYCLES_PER_SEC)} / sec) defense 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 />
{player.workHackExpGained > 0 && <>{numeralWrapper.formatExp(player.workHackExpGained)} ({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 /></>}
{player.workDefExpGained > 0 && <>{numeralWrapper.formatExp(player.workDefExpGained)} ({numeralWrapper.formatExp(player.workDefExpGainRate * CYCLES_PER_SEC)} / sec) defense exp <br /></>}
{player.workDexExpGained > 0 && <>{numeralWrapper.formatExp(player.workDexExpGained)} ({numeralWrapper.formatExp(player.workDexExpGainRate * CYCLES_PER_SEC)} / sec) dexterity exp <br /></>}
{player.workAgiExpGained > 0 && <>{numeralWrapper.formatExp(player.workAgiExpGained)} ({numeralWrapper.formatExp(player.workAgiExpGainRate * CYCLES_PER_SEC)} / sec) agility exp <br /></>}
{player.workChaExpGained > 0 && <>{numeralWrapper.formatExp(player.workChaExpGained)} ({numeralWrapper.formatExp(player.workChaExpGainRate * CYCLES_PER_SEC)} / sec) charisma exp <br /></>}
You may cancel at any time
</Typography>
</Grid>
@ -185,26 +173,26 @@ export function WorkInProgressRoot(): React.ReactElement {
<Reputation reputation={player.workRepGained} /> (
<ReputationRate reputation={player.workRepGainRate * CYCLES_PER_SEC} />) reputation for this company <br />
<br />
{numeralWrapper.formatExp(player.workHackExpGained)} (
{player.workHackExpGained > 0 && <>{numeralWrapper.formatExp(player.workHackExpGained)} (
{`${numeralWrapper.formatExp(player.workHackExpGainRate * CYCLES_PER_SEC)} / sec`}
) hacking exp <br />
) hacking exp <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.workDefExpGained)} (
) strength exp <br /></>}
{player.workDefExpGained > 0 && <>{numeralWrapper.formatExp(player.workDefExpGained)} (
{`${numeralWrapper.formatExp(player.workDefExpGainRate * CYCLES_PER_SEC)} / sec`}
) defense exp <br />
{numeralWrapper.formatExp(player.workDexExpGained)} (
) defense exp <br /></>}
{player.workDexExpGained > 0 && <>{numeralWrapper.formatExp(player.workDexExpGained)} (
{`${numeralWrapper.formatExp(player.workDexExpGainRate * CYCLES_PER_SEC)} / sec`}
) dexterity exp <br />
{numeralWrapper.formatExp(player.workAgiExpGained)} (
) dexterity exp <br /></>}
{player.workAgiExpGained > 0 && <>{numeralWrapper.formatExp(player.workAgiExpGained)} (
{`${numeralWrapper.formatExp(player.workAgiExpGainRate * CYCLES_PER_SEC)} / sec`}
) agility exp <br />
) agility exp <br /></>}
<br />
{numeralWrapper.formatExp(player.workChaExpGained)} (
{player.workChaExpGained > 0 && <>{numeralWrapper.formatExp(player.workChaExpGained)} (
{`${numeralWrapper.formatExp(player.workChaExpGainRate * CYCLES_PER_SEC)} / sec`}
) charisma exp <br />
) charisma exp <br /></>}
<br />
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.
@ -256,26 +244,26 @@ export function WorkInProgressRoot(): React.ReactElement {
<ReputationRate reputation={player.workRepGainRate * CYCLES_PER_SEC} />
) reputation for this company <br />
<br />
{numeralWrapper.formatExp(player.workHackExpGained)} (
{player.workHackExpGained > 0 && <>{numeralWrapper.formatExp(player.workHackExpGained)} (
{`${numeralWrapper.formatExp(player.workHackExpGainRate * CYCLES_PER_SEC)} / sec`}
) hacking exp <br />
) hacking exp <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.workDefExpGained)} (
) strength exp <br /></>}
{player.workDefExpGained > 0 && <>{numeralWrapper.formatExp(player.workDefExpGained)} (
{`${numeralWrapper.formatExp(player.workDefExpGainRate * CYCLES_PER_SEC)} / sec`}
) defense exp <br />
{numeralWrapper.formatExp(player.workDexExpGained)} (
) defense exp <br /></>}
{player.workDexExpGained > 0 && <>{numeralWrapper.formatExp(player.workDexExpGained)} (
{`${numeralWrapper.formatExp(player.workDexExpGainRate * CYCLES_PER_SEC)} / sec`}
) dexterity exp <br />
{numeralWrapper.formatExp(player.workAgiExpGained)} (
) dexterity exp <br /></>}
{player.workAgiExpGained > 0 && <>{numeralWrapper.formatExp(player.workAgiExpGained)} (
{`${numeralWrapper.formatExp(player.workAgiExpGainRate * CYCLES_PER_SEC)} / sec`}
) agility exp <br />
) agility exp <br /></>}
<br />
{numeralWrapper.formatExp(player.workChaExpGained)} (
{player.workChaExpGained > 0 && <>{numeralWrapper.formatExp(player.workChaExpGained)} (
{`${numeralWrapper.formatExp(player.workChaExpGainRate * CYCLES_PER_SEC)} / sec`}
) charisma exp <br />
) charisma exp <br /></>}
<br />
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.

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