merge latest dev

This commit is contained in:
phyzical 2022-02-15 20:24:24 +08:00
commit a2b4a63c2e
298 changed files with 7864 additions and 2831 deletions

@ -1,27 +1,26 @@
node_modules/
dist/
input/
.dist
.tmp
.package
assets/
css/
.build
.cypress/
cypress/
dist/
input/
assets/
doc/
markdown/
netscript_tests/
scripts/
test/netscript/
electron/lib
electron/greenworks.js
src/ThirdParty/*
src/JSInterpreter.js
tools/engines-check/
test/*.bundle.*
editor.main.js
main.bundle.js
webpack.config.js
webpack.config-test.js

100
.github/workflows/bump-version.yml vendored Normal file

@ -0,0 +1,100 @@
name: Bump BitBurner Version
on:
workflow_dispatch:
inputs:
version:
description: 'Version (format: x.y.z)'
required: true
versionNumber:
description: 'Version Number (for saves migration)'
required: true
changelog:
description: 'Changelog (url that points to RAW markdown)'
default: ''
buildApp:
description: 'Include Application Build'
type: boolean
default: 'true'
required: true
buildDoc:
description: 'Include Documentation Build'
type: boolean
default: 'true'
required: true
prepareRelease:
description: 'Prepare Draft Release'
type: boolean
default: 'true'
required: true
jobs:
bumpVersion:
name: Bump Version
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Install pandoc dependency
run: sudo apt-get install -y pandoc
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Use Node.js 16.13.1
uses: actions/setup-node@v2
with:
node-version: 16.13.1
cache: 'npm'
- name: Install NPM dependencies for version updater
working-directory: ./tools/bump-version
run: npm ci
- name: Bump version & update changelogs
working-directory: ./tools/bump-version
run: |
curl ${{ github.event.inputs.changelog }} > changes.md
node index.js --version=${{ github.event.inputs.version }} --versionNumber=${{ github.event.inputs.versionNumber }} < changes.md
- name: Install NPM dependencies for app
if: ${{ github.event.inputs.buildApp == 'true' || github.event.inputs.buildDoc == 'true' }}
run: npm ci
- name: Build Production App
if: ${{ github.event.inputs.buildApp == 'true' }}
run: npm run build
- name: Build Documentation
if: ${{ github.event.inputs.buildDoc == 'true' }}
run: npm run doc
- name: Commit Files
run: |
git config --global user.name "GitHub"
git config --global user.email "noreply@github.com"
git checkout -b bump/v${{ github.event.inputs.version }}
git add -A
echo "Bump version to v${{ github.event.inputs.version }}" > commitmessage.txt
echo "" >> commitmessage.txt
cat ./tools/bump-version/changes.md >> commitmessage.txt
git commit -F commitmessage.txt
git push -u origin bump/v${{ github.event.inputs.version }}
- name: Create Pull Request
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr create \
--base "${{ github.ref_name }}" \
--head "bump/v${{ github.event.inputs.version }}" \
--title "Bump version to v${{ github.event.inputs.version }}" \
--body-file ./tools/bump-version/changes.md
- name: Prepare release
if: ${{ github.event.inputs.prepareRelease == 'true' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RELEASE_TITLE="$(head -n 1 ./tools/bump-version/changes.md | sed 's/## //')"
RELEASE_TITLE="${RELEASE_TITLE:-v${{ github.event.inputs.version }}}"
gh release create \
v${{ github.event.inputs.version }} \
--target dev \
--title "$RELEASE_TITLE" \
--notes-file ./tools/bump-version/changes.md \
--generate-notes \
--draft

@ -51,5 +51,5 @@ jobs:
cache: 'npm'
- name: Install npm dependencies
run: npm ci
- name: Run linter
- name: Run tests
run: npm run test

44
.github/workflows/fetch-changes.yml vendored Normal file

@ -0,0 +1,44 @@
name: Fetch Merged Changes
on:
workflow_dispatch:
inputs:
fromCommit:
description: 'From Commit SHA (full-length)'
required: true
toCommit:
description: 'To Commit SHA (full-length, if omitted will use latest)'
jobs:
fetchChangelog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node v16.13.1
uses: actions/setup-node@v2
with:
node-version: 16.13.1
cache: 'npm'
- name: Install NPM dependencies
working-directory: ./tools/fetch-changelog
run: npm ci
- name: Fetch Changes from GitHub API
working-directory: ./tools/fetch-changelog
env:
GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
node index.js \
--from=${{ github.event.inputs.fromCommit }} \
--to=${{ github.event.inputs.toCommit }} > changes.md
echo
echo "============================================================"
echo
cat changes.md
echo
echo "============================================================"
echo
echo "You may want to go to https://gist.github.com/ to upload the final changelog"
echo "The next step will require an url because we can't easily pass multiline strings to actions"
- uses: actions/upload-artifact@v2
with:
name: bitburner_changelog___DRAFT.md
path: ./tools/fetch-changelog/changes.md

2
.gitignore vendored

@ -5,10 +5,10 @@ Netburner.txt
/doc/build
/node_modules
/electron/node_modules
/dist/*.map
/test/*.map
/test/*.bundle.*
/test/*.css
/input/bitburner.api.json
.cypress
# tmp folder for electron

@ -6,7 +6,7 @@ Bitburner is a programming-based [incremental game](https://en.wikipedia.org/wik
that revolves around hacking and cyberpunk themes.
The game can be played at https://danielyxie.github.io/bitburner or installed through [Steam](https://store.steampowered.com/app/1812820/Bitburner/).
See the [frequently asked questions](./FAQ.md) for more information . To discuss the game or get help, join the [official discord server](https://discord.gg/TFc3hKD)
See the [frequently asked questions](./doc/FAQ.md) for more information . To discuss the game or get help, join the [official discord server](https://discord.gg/TFc3hKD)
# Documentation
@ -18,13 +18,13 @@ The [in-game documentation](./markdown/bitburner.md) is generated from the [Type
Anyone is welcome to contribute to the documentation by editing the [source
files](/doc/source) and then making a pull request with your contributions.
For further guidance, please refer to the "As A Documentor" section of
[CONTRIBUTING](CONTRIBUTING.md).
[CONTRIBUTING](./doc/CONTRIBUTING.md).
# Contribution
There are many ways to contribute to the game. It can be as simple as fixing
a typo, correcting a bug, or improving the UI. For guidance on doing so,
please refer to the [CONTRIBUTING](CONTRIBUTING.md) document.
please refer to the [CONTRIBUTING](./doc/CONTRIBUTING.md) document.
You will retain all ownership of the Copyright of any contributions you make,
and will have the same rights to use or license your contributions. By

BIN
assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -1,10 +1,14 @@
{
"baseUrl": "http://localhost:8000",
"fixturesFolder": false,
"trashAssetsBeforeRuns": true,
"screenshotsFolder": ".cypress/screenshots",
"videosFolder": ".cypress/videos",
"videoUploadOnPasses": false,
"viewportWidth": 1980,
"viewportHeight": 1080
"viewportHeight": 1080,
"fixturesFolder": "test/cypress/fixtures",
"integrationFolder": "test/cypress/integration",
"pluginsFile": "test/cypress/plugins/index.js",
"supportFile": "test/cypress/support/index.js",
"screenshotsFolder": ".cypress/screenshots",
"videosFolder": ".cypress/videos",
"downloadsFolder": ".cypress/downloads"
}

@ -1,9 +0,0 @@
export {};
beforeEach(() => {
cy.visit("/");
cy.clearLocalStorage();
cy.window().then((win) => {
win.indexedDB.deleteDatabase("bitburnerSave");
});
});

66
dist/bitburner.d.ts vendored

@ -1173,6 +1173,16 @@ export declare interface Fragment {
limit: number;
}
/**
* Game Information
* @internal
*/
export declare interface GameInfo {
version: string;
commit: string;
platform: string;
}
/**
* Gang API
* @remarks
@ -3539,9 +3549,9 @@ export declare interface NS extends Singularity {
* @remarks
* RAM cost: 0.1 GB
*
* Returns the servers instrinsic growth parameter. This growth
* parameter is a number between 0 and 100 that represents how
* quickly the servers money grows. This parameter affects the
* Returns the servers intrinsic growth parameter. This growth
* parameter is a number typically between 0 and 100 that represents
* how quickly the servers money grows. This parameter affects the
* percentage by which the servers money is increased when using the
* grow function. A higher growth parameter will result in a
* higher percentage increase from grow.
@ -4430,6 +4440,23 @@ export declare interface NS extends Singularity {
* ```
*/
flags(schema: [string, string | number | boolean | string[]][]): any;
/**
* Share your computer with your factions.
* @remarks
* RAM cost: 2.4 GB
*
* Increases your rep gain of hacking contracts while share is called.
* Scales with thread count.
*/
share(): Promise<void>;
/**
* Calculate your share power. Based on all the active share calls.
* @remarks
* RAM cost: 0.2 GB
*/
getSharePower(): number;
}
/**
@ -5974,7 +6001,7 @@ export declare interface Stanek {
/**
* List possible fragments.
* @remarks
* RAM cost: cost: 0 GB
* RAM cost: 0 GB
*
* @returns List of possible fragments.
*/
@ -5983,7 +6010,7 @@ export declare interface Stanek {
/**
* List of fragments in Stanek's Gift.
* @remarks
* RAM cost: cost: 5 GB
* RAM cost: 5 GB
*
* @returns List of active fragments placed on Stanek's Gift.
*/
@ -5992,14 +6019,14 @@ export declare interface Stanek {
/**
* Clear the board of all fragments.
* @remarks
* RAM cost: cost: 0 GB
* RAM cost: 0 GB
*/
clear(): void;
/**
* Check if fragment can be placed at specified location.
* @remarks
* RAM cost: cost: 0.5 GB
* RAM cost: 0.5 GB
*
* @param rootX - rootX Root X against which to align the top left of the fragment.
* @param rootY - rootY Root Y against which to align the top left of the fragment.
@ -6011,7 +6038,7 @@ export declare interface Stanek {
/**
* Place fragment on Stanek's Gift.
* @remarks
* RAM cost: cost: 5 GB
* RAM cost: 5 GB
*
* @param rootX - X against which to align the top left of the fragment.
* @param rootY - Y against which to align the top left of the fragment.
@ -6023,7 +6050,7 @@ export declare interface Stanek {
/**
* Get placed fragment at location.
* @remarks
* RAM cost: cost: 5 GB
* RAM cost: 5 GB
*
* @param rootX - X against which to align the top left of the fragment.
* @param rootY - Y against which to align the top left of the fragment.
@ -6034,7 +6061,7 @@ export declare interface Stanek {
/**
* Remove fragment at location.
* @remarks
* RAM cost: cost: 0.15 GB
* RAM cost: 0.15 GB
*
* @param rootX - X against which to align the top left of the fragment.
* @param rootY - Y against which to align the top left of the fragment.
@ -6439,7 +6466,7 @@ export declare interface UserInterface {
/**
* Get the current theme
* @remarks
* RAM cost: cost: 0 GB
* RAM cost: 0 GB
*
* @returns An object containing the theme's colors
*/
@ -6448,7 +6475,7 @@ export declare interface UserInterface {
/**
* Sets the current theme
* @remarks
* RAM cost: cost: 0 GB
* RAM cost: 0 GB
* @example
* Usage example (NS2)
* ```ts
@ -6462,14 +6489,14 @@ export declare interface UserInterface {
/**
* Resets the player's theme to the default values
* @remarks
* RAM cost: cost: 0 GB
* RAM cost: 0 GB
*/
resetTheme(): void;
/**
* Get the current styles
* @remarks
* RAM cost: cost: 0 GB
* RAM cost: 0 GB
*
* @returns An object containing the player's styles
*/
@ -6478,7 +6505,7 @@ export declare interface UserInterface {
/**
* Sets the current styles
* @remarks
* RAM cost: cost: 0 GB
* RAM cost: 0 GB
* @example
* Usage example (NS2)
* ```ts
@ -6492,9 +6519,16 @@ export declare interface UserInterface {
/**
* Resets the player's styles to the default values
* @remarks
* RAM cost: cost: 0 GB
* RAM cost: 0 GB
*/
resetStyles(): void;
/**
* Gets the current game information (version, commit, ...)
* @remarks
* RAM cost: 0 GB
*/
getGameInfo(): GameInfo;
}
/**

20
dist/engine.bundle.js vendored

File diff suppressed because one or more lines are too long

BIN
dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

20
dist/main.bundle.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/main.bundle.js.map vendored Normal file

File diff suppressed because one or more lines are too long

44
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

1
dist/vendor.bundle.js.map vendored Normal file

File diff suppressed because one or more lines are too long

@ -182,7 +182,7 @@ To contribute to and view your changes to the BitBurner documentation on [Read T
Docs](http://bitburner.readthedocs.io/), you will
need to have Python installed, along with [Sphinx](http://www.sphinx-doc.org).
To make change to the [in-game documentation](./markdown/bitburner.md), you will need to modify the [TypeScript definitions](./src/ScriptEditor/NetscriptDefinitions.d.ts), not the markdown files.
To make change to the [in-game documentation](../markdown/bitburner.md), you will need to modify the [TypeScript definitions](../src/ScriptEditor/NetscriptDefinitions.d.ts), not the markdown files.
We are using [API Extractor](https://api-extractor.com/pages/tsdoc/doc_comment_syntax/) (tsdoc hints) to generate the markdown doc. Make your changes to the TypeScript definitions and then run `npm run doc`.

@ -30,6 +30,11 @@ from faction to faction.
List of Factions and their Requirements
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. raw:: html
<details><summary><a>Early Game Factions</a></summary>
.. _gameplay_factions::
+---------------------+----------------+-----------------------------------------+-------------------------------+
| Early Game | Faction Name | Requirements | Joining this Faction prevents |
@ -46,7 +51,18 @@ List of Factions and their Requirements
| | | * Total Hacknet RAM of 8 | |
| | | * Total Hacknet Cores of 4 | |
+---------------------+----------------+-----------------------------------------+-------------------------------+
| City Factions | Sector-12 | * Be in Sector-12 | * Chongqing |
.. raw:: html
</details>
<details><summary><a>City Factions</a></summary>
.. _gameplay_factions::
+---------------------+----------------+-----------------------------------------+-------------------------------+
| City Factions | Faction Name | Requirements | Joining this Faction prevents |
| | | | you from joining: |
+ +----------------+-----------------------------------------+-------------------------------+
| | Sector-12 | * Be in Sector-12 | * Chongqing |
| | | * $15m | * New Tokyo |
| | | | * Ishima |
| | | | * Volhaven |
@ -74,8 +90,19 @@ List of Factions and their Requirements
| | | | * New Tokyo |
| | | | * Ishima |
+---------------------+----------------+-----------------------------------------+-------------------------------+
| Hacking | NiteSec | * Install a backdoor on the avmnite-02h | |
| Groups | | server | |
.. raw:: html
</details>
<details><summary><a>Hacking Groups</a></summary>
.. _gameplay_factions::
+---------------------+----------------+-----------------------------------------+-------------------------------+
| Hacking | Faction Name | Requirements | Joining this Faction prevents |
| Groups | | | you from joining: |
+ +----------------+-----------------------------------------+-------------------------------+
| | NiteSec | * Install a backdoor on the avmnite-02h | |
| | | server | |
| | | | |
+ +----------------+-----------------------------------------+-------------------------------+
| | The Black Hand | * Install a backdoor on the I.I.I.I | |
@ -86,7 +113,18 @@ List of Factions and their Requirements
| | | server | |
| | | | |
+---------------------+----------------+-----------------------------------------+-------------------------------+
| Megacorporations | ECorp | * Have 200k reputation with | |
.. raw:: html
</details>
<details><summary><a>Megacorporations</a></summary>
.. _gameplay_factions::
+---------------------+----------------+-----------------------------------------+-------------------------------+
| Megacorporations | Faction Name | Requirements | Joining this Faction prevents |
| | | | you from joining: |
+ +----------------+-----------------------------------------+-------------------------------+
| | ECorp | * Have 200k reputation with | |
| | | the Corporation | |
+ +----------------+-----------------------------------------+-------------------------------+
| | MegaCorp | * Have 200k reputation with | |
@ -118,8 +156,19 @@ List of Factions and their Requirements
| | | * Install a backdoor on the | |
| | | fulcrumassets server | |
+---------------------+----------------+-----------------------------------------+-------------------------------+
| Criminal | Slum Snakes | * All Combat Stats of 30 | |
| Organizations | | * -9 Karma | |
.. raw:: html
</details>
<details><summary><a>Criminal Organizations</a></summary>
.. _gameplay_factions::
+---------------------+----------------+-----------------------------------------+-------------------------------+
| Criminal | Faction Name | Requirements | Joining this Faction prevents |
| Organizations | | | you from joining: |
+ +----------------+-----------------------------------------+-------------------------------+
| | Slum Snakes | * All Combat Stats of 30 | |
| | | * -9 Karma | |
| | | * $1m | |
+ +----------------+-----------------------------------------+-------------------------------+
| | Tetrads | * Be in Chongqing, New Tokyo, or Ishima | |
@ -150,8 +199,19 @@ List of Factions and their Requirements
| | | * -90 Karma | |
| | | * Not working for CIA or NSA | |
+---------------------+----------------+-----------------------------------------+-------------------------------+
| Endgame | The Covenant | * 20 Augmentations | |
| Factions | | * $75b | |
.. raw:: html
</details>
<details><summary><a>Endgame Factions</a></summary>
.. _gameplay_factions::
+---------------------+----------------+-----------------------------------------+-------------------------------+
| Endgame | Faction Name | Requirements | Joining this Faction prevents |
| Factions | | | you from joining: |
+ +----------------+-----------------------------------------+-------------------------------+
| | The Covenant | * 20 Augmentations | |
| | | * $75b | |
| | | * Hacking Level of 850 | |
| | | * All Combat Stats of 850 | |
+ +----------------+-----------------------------------------+-------------------------------+
@ -165,3 +225,6 @@ List of Factions and their Requirements
| | | * Hacking Level of 1500 | |
| | | * All Combat Stats of 1200 | |
+---------------------+----------------+-----------------------------------------+-------------------------------+
.. raw:: html
</details><br>

@ -23,4 +23,4 @@ Bug
---
Otherwise, the game is probably frozen/stuck due to a bug. To report a bug, follow
the guidelines `here <https://github.com/danielyxie/bitburner/blob/master/CONTRIBUTING.md#reporting-bugs>`_.
the guidelines `here <https://github.com/danielyxie/bitburner/blob/master/doc/CONTRIBUTING.md#reporting-bugs>`_.

@ -9,7 +9,7 @@ async 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();
log.debug(`All Steam achievements ${JSON.stringify(steamAchievements)}`);
log.silly(`All Steam achievements ${JSON.stringify(steamAchievements)}`);
const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter(name => !!name);
log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`);
const intervalID = setInterval(async () => {

@ -12,11 +12,13 @@ async function initialize(win) {
window = win;
server = http.createServer(async function (req, res) {
let body = "";
res.setHeader('Content-Type', 'application/json');
req.on("data", (chunk) => {
body += chunk.toString(); // convert Buffer to string
});
req.on("end", () => {
req.on("end", async () => {
const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? '';
const isValid = providedToken === getAuthenticationToken();
if (isValid) {
@ -24,8 +26,11 @@ async function initialize(win) {
} else {
log.log('Invalid authentication token');
res.writeHead(401);
res.write('Invalid authentication token');
res.end();
res.end(JSON.stringify({
success: false,
msg: 'Invalid authentication token'
}));
return;
}
@ -35,17 +40,56 @@ async function initialize(win) {
} catch (error) {
log.warn(`Invalid body data`);
res.writeHead(400);
res.write('Invalid body data');
res.end();
res.end(JSON.stringify({
success: false,
msg: 'Invalid body data'
}));
return;
}
if (data) {
window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`).then((result) => {
res.write(result);
res.end();
});
let result;
switch(req.method) {
// Request files
case "GET":
result = await window.webContents.executeJavaScript(`document.getFiles()`);
break;
// Create or update files
// Support POST for VScode implementation
case "POST":
case "PUT":
if (!data) {
log.warn(`Invalid script update request - No data`);
res.writeHead(400);
res.end(JSON.stringify({
success: false,
msg: 'Invalid script update request - No data'
}));
return;
}
result = await window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`);
break;
// Delete files
case "DELETE":
result = await window.webContents.executeJavaScript(`document.deleteFile("${data.filename}")`);
break;
}
if (!result.res) {
//We've encountered an error
res.writeHead(400);
log.warn(`Api Server Error`, result.msg);
}
res.end(JSON.stringify({
success: result.res,
msg: result.msg,
data: result.data
}));
});
});

@ -8,20 +8,42 @@ const api = require("./api-server");
const cp = require("child_process");
const path = require("path");
const fs = require("fs");
const { windowTracker } = require("./windowTracker");
const { fileURLToPath } = require("url");
const debug = process.argv.includes("--debug");
async function createWindow(killall) {
const setStopProcessHandler = global.app_handlers.stopProcess;
let icon;
if (process.platform == 'linux') {
icon = path.join(__dirname, 'icon.png');
}
const tracker = windowTracker('main');
const window = new BrowserWindow({
icon,
show: false,
backgroundThrottling: false,
backgroundColor: "#000000",
title: 'Bitburner',
x: tracker.state.x,
y: tracker.state.y,
width: tracker.state.width,
height: tracker.state.height,
minWidth: 600,
minHeight: 400,
webPreferences: {
nativeWindowOpen: true,
preload: path.join(__dirname, 'preload.js'),
},
});
setTimeout(() => tracker.track(window), 1000);
if (tracker.state.isMaximized) window.maximize();
window.removeMenu();
window.maximize();
noScripts = killall ? { query: { noScripts: killall } } : {};
window.loadFile("index.html", noScripts);
window.show();

@ -1,12 +1,19 @@
/* eslint-disable no-process-exit */
/* eslint-disable @typescript-eslint/no-var-requires */
const { app, dialog, BrowserWindow } = require("electron");
const { app, dialog, BrowserWindow, ipcMain } = 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");
const storage = require("./storage");
const debounce = require("lodash/debounce");
const Config = require("electron-config");
const config = new Config();
log.transports.file.level = config.get("file-log-level", "info");
log.transports.console.level = config.get("console-log-level", "debug");
log.catchErrors();
log.info(`Started app: ${JSON.stringify(process.argv)}`);
@ -30,6 +37,8 @@ try {
global.greenworksError = ex.message;
}
let isRestoreDisabled = false;
function setStopProcessHandler(app, window, enabled) {
const closingWindowHandler = async (e) => {
// We need to prevent the default closing event to add custom logic
@ -41,6 +50,18 @@ function setStopProcessHandler(app, window, enabled) {
// Shutdown the http server
api.disable();
// Trigger debounced saves right now before closing
try {
await saveToDisk.flush();
} catch (error) {
log.error(error);
}
try {
await saveToCloud.flush();
} catch (error) {
log.error(error);
}
// 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.
@ -87,21 +108,106 @@ function setStopProcessHandler(app, window, enabled) {
process.exit(0);
};
const receivedGameReadyHandler = async (event, arg) => {
if (!window) {
log.warn("Window was undefined in game info handler");
return;
}
log.debug("Received game information", arg);
window.gameInfo = { ...arg };
await storage.prepareSaveFolders(window);
const restoreNewest = config.get("onload-restore-newest", true);
if (restoreNewest && !isRestoreDisabled) {
try {
await storage.restoreIfNewerExists(window)
} catch (error) {
log.error("Could not restore newer file", error);
}
}
}
const receivedDisableRestoreHandler = async (event, arg) => {
if (!window) {
log.warn("Window was undefined in disable import handler");
return;
}
log.debug(`Disabling auto-restore for ${arg.duration}ms.`);
isRestoreDisabled = true;
setTimeout(() => {
isRestoreDisabled = false;
log.debug("Re-enabling auto-restore");
}, arg.duration);
}
const receivedGameSavedHandler = async (event, arg) => {
if (!window) {
log.warn("Window was undefined in game saved handler");
return;
}
const { save, ...other } = arg;
log.silly("Received game saved info", {...other, save: `${save.length} bytes`});
if (storage.isAutosaveEnabled()) {
saveToDisk(save, arg.fileName);
}
if (storage.isCloudEnabled()) {
const minimumPlaytime = 1000 * 60 * 15;
const playtime = window.gameInfo.player.playtime;
log.silly(window.gameInfo);
if (playtime > minimumPlaytime) {
saveToCloud(save);
} else {
log.debug(`Auto-save to cloud disabled for save game under ${minimumPlaytime}ms (${playtime}ms)`);
}
}
}
const saveToCloud = debounce(async (save) => {
log.debug("Saving to Steam Cloud ...")
try {
const playerId = window.gameInfo.player.identifier;
await storage.pushGameSaveToSteamCloud(save, playerId);
log.silly("Saved Game to Steam Cloud");
} catch (error) {
log.error(error);
utils.writeToast(window, "Could not save to Steam Cloud.", "error", 5000);
}
}, config.get("cloud-save-min-time", 1000 * 60 * 15), { leading: true });
const saveToDisk = debounce(async (save, fileName) => {
log.debug("Saving to Disk ...")
try {
const file = await storage.saveGameToDisk(window, { save, fileName });
log.silly(`Saved Game to '${file.replaceAll('\\', '\\\\')}'`);
} catch (error) {
log.error(error);
utils.writeToast(window, "Could not save to disk", "error", 5000);
}
}, config.get("disk-save-min-time", 1000 * 60 * 5), { leading: true });
if (enabled) {
log.debug('Adding closing handlers');
log.debug("Adding closing handlers");
ipcMain.on("push-game-ready", receivedGameReadyHandler);
ipcMain.on("push-game-saved", receivedGameSavedHandler);
ipcMain.on("push-disable-restore", receivedDisableRestoreHandler)
window.on("closed", clearWindowHandler);
window.on("close", closingWindowHandler)
app.on("window-all-closed", stopProcessHandler);
} else {
log.debug('Removing closing handlers');
log.debug("Removing closing handlers");
ipcMain.removeAllListeners();
window.removeListener("closed", clearWindowHandler);
window.removeListener("close", closingWindowHandler);
app.removeListener("window-all-closed", stopProcessHandler);
}
}
function startWindow(noScript) {
gameWindow.createWindow(noScript);
async function startWindow(noScript) {
return gameWindow.createWindow(noScript);
}
global.app_handlers = {
@ -110,7 +216,7 @@ global.app_handlers = {
}
app.whenReady().then(async () => {
log.info('Application is ready!');
log.info("Application is ready!");
if (process.argv.includes("--export-save")) {
const window = new BrowserWindow({ show: false });
@ -119,15 +225,14 @@ app.whenReady().then(async () => {
setStopProcessHandler(app, window, true);
await utils.exportSave(window);
} else {
startWindow(process.argv.includes("--no-scripts"));
}
const window = await startWindow(process.argv.includes("--no-scripts"));
if (global.greenworksError) {
dialog.showMessageBox({
title: 'Bitburner',
message: 'Could not connect to Steam',
await dialog.showMessageBox(window, {
title: "Bitburner",
message: "Could not connect to Steam",
detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`,
type: 'warning', buttons: ['OK']
});
}
}
});

@ -1,11 +1,169 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { Menu, clipboard, dialog } = require("electron");
const { app, Menu, clipboard, dialog, shell } = require("electron");
const log = require("electron-log");
const Config = require("electron-config");
const api = require("./api-server");
const utils = require("./utils");
const storage = require("./storage");
const config = new Config();
function getMenu(window) {
return Menu.buildFromTemplate([
{
label: "File",
submenu: [
{
label: "Save Game",
click: () => window.webContents.send("trigger-save"),
},
{
label: "Export Save",
click: () => window.webContents.send("trigger-game-export"),
},
{
label: "Export Scripts",
click: async () => window.webContents.send("trigger-scripts-export"),
},
{
type: "separator",
},
{
label: "Load Last Save",
click: async () => {
try {
const saveGame = await storage.loadLastFromDisk(window);
window.webContents.send("push-save-request", { save: saveGame });
} catch (error) {
log.error(error);
utils.writeToast(window, "Could not load last save from disk", "error", 5000);
}
}
},
{
label: "Load From File",
click: async () => {
const defaultPath = await storage.getSaveFolder(window);
const result = await dialog.showOpenDialog(window, {
title: "Load From File",
defaultPath: defaultPath,
buttonLabel: "Load",
filters: [
{ name: "Game Saves", extensions: ["json", "json.gz", "txt"] },
{ name: "All", extensions: ["*"] },
],
properties: [
"openFile", "dontAddToRecent",
]
});
if (result.canceled) return;
const file = result.filePaths[0];
try {
const saveGame = await storage.loadFileFromDisk(file);
window.webContents.send("push-save-request", { save: saveGame });
} catch (error) {
log.error(error);
utils.writeToast(window, "Could not load save from disk", "error", 5000);
}
}
},
{
label: "Load From Steam Cloud",
enabled: storage.isCloudEnabled(),
click: async () => {
try {
const saveGame = await storage.getSteamCloudSaveString();
await storage.pushSaveGameForImport(window, saveGame, false);
} catch (error) {
log.error(error);
utils.writeToast(window, "Could not load from Steam Cloud", "error", 5000);
}
}
},
{
type: "separator",
},
{
label: "Compress Disk Saves (.gz)",
type: "checkbox",
checked: storage.isSaveCompressionEnabled(),
click: (menuItem) => {
storage.setSaveCompressionConfig(menuItem.checked);
utils.writeToast(window,
`${menuItem.checked ? "Enabled" : "Disabled"} Save Compression`, "info", 5000);
refreshMenu(window);
},
},
{
label: "Auto-Save to Disk",
type: "checkbox",
checked: storage.isAutosaveEnabled(),
click: (menuItem) => {
storage.setAutosaveConfig(menuItem.checked);
utils.writeToast(window,
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Disk`, "info", 5000);
refreshMenu(window);
},
},
{
label: "Auto-Save to Steam Cloud",
type: "checkbox",
enabled: !global.greenworksError,
checked: storage.isCloudEnabled(),
click: (menuItem) => {
storage.setCloudEnabledConfig(menuItem.checked);
utils.writeToast(window,
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Steam Cloud`, "info", 5000);
refreshMenu(window);
},
},
{
label: "Restore Newest on Load",
type: "checkbox",
checked: config.get("onload-restore-newest", true),
click: (menuItem) => {
config.set("onload-restore-newest", menuItem.checked);
utils.writeToast(window,
`${menuItem.checked ? "Enabled" : "Disabled"} Restore Newest on Load`, "info", 5000);
refreshMenu(window);
},
},
{
type: "separator",
},
{
label: "Open Directory",
submenu: [
{
label: "Open Game Directory",
click: () => shell.openPath(app.getAppPath()),
},
{
label: "Open Saves Directory",
click: async () => {
const path = await storage.getSaveFolder(window);
shell.openPath(path);
},
},
{
label: "Open Logs Directory",
click: () => shell.openPath(app.getPath("logs")),
},
{
label: "Open Data Directory",
click: () => shell.openPath(app.getPath("userData")),
},
]
},
{
type: "separator",
},
{
label: "Quit",
click: () => app.quit(),
},
]
},
{
label: "Edit",
submenu: [
@ -163,6 +321,17 @@ function getMenu(window) {
label: "Activate",
click: () => window.webContents.openDevTools(),
},
{
label: "Delete Steam Cloud Data",
enabled: !global.greenworksError,
click: async () => {
try {
await storage.deleteCloudFile();
} catch (error) {
log.error(error);
}
}
}
],
},
]);

@ -9,7 +9,8 @@
"version": "1.0.0",
"dependencies": {
"electron-config": "^2.0.0",
"electron-log": "^4.4.4"
"electron-log": "^4.4.4",
"lodash": "^4.17.21"
}
},
"node_modules/conf": {
@ -104,6 +105,11 @@
"node": ">=4"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/make-dir": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
@ -259,6 +265,11 @@
"path-exists": "^3.0.0"
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"make-dir": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",

@ -16,6 +16,8 @@
"./dist/**/*",
"./node_modules/**/*",
"./public/**/*",
"./src/**",
"./lib/**,",
"*.js"
],
"directories": {
@ -23,6 +25,7 @@
},
"dependencies": {
"electron-config": "^2.0.0",
"electron-log": "^4.4.4"
"electron-log": "^4.4.4",
"lodash": "^4.17.21"
}
}

38
electron/preload.js Normal file

@ -0,0 +1,38 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { ipcRenderer, contextBridge } = require('electron')
const log = require("electron-log");
contextBridge.exposeInMainWorld(
"electronBridge", {
send: (channel, data) => {
log.log("Send on channel " + channel)
// whitelist channels
let validChannels = [
"get-save-data-response",
"get-save-info-response",
"push-game-saved",
"push-game-ready",
"push-import-result",
"push-disable-restore",
];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
log.log("Receive on channel " + channel)
let validChannels = [
"get-save-data-request",
"get-save-info-request",
"push-save-request",
"trigger-save",
"trigger-game-export",
"trigger-scripts-export",
];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
}
}
);

386
electron/storage.js Normal file

@ -0,0 +1,386 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { app, ipcMain } = require("electron");
const zlib = require("zlib");
const path = require("path");
const fs = require("fs/promises");
const { promisify } = require("util");
const gzip = promisify(zlib.gzip);
const gunzip = promisify(zlib.gunzip);
const greenworks = require("./greenworks");
const log = require("electron-log");
const flatten = require("lodash/flatten");
const Config = require("electron-config");
const config = new Config();
// https://stackoverflow.com/a/69418940
const dirSize = async (directory) => {
const files = await fs.readdir(directory);
const stats = files.map(file => fs.stat(path.join(directory, file)));
return (await Promise.all(stats)).reduce((accumulator, { size }) => accumulator + size, 0);
}
const getDirFileStats = async (directory) => {
const files = await fs.readdir(directory);
const stats = files.map((f) => {
const file = path.join(directory, f);
return fs.stat(file).then((stat) => ({ file, stat }));
});
const data = (await Promise.all(stats));
return data;
};
const getNewestFile = async (directory) => {
const data = await getDirFileStats(directory)
return data.sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime())[0];
};
const getAllSaves = async (window) => {
const rootDirectory = await getSaveFolder(window, true);
const data = await fs.readdir(rootDirectory, { withFileTypes: true});
const savesPromises = data.filter((e) => e.isDirectory()).
map((dir) => path.join(rootDirectory, dir.name)).
map((dir) => getDirFileStats(dir));
const saves = await Promise.all(savesPromises);
const flat = flatten(saves);
return flat;
}
async function prepareSaveFolders(window) {
const rootFolder = await getSaveFolder(window, true);
const currentFolder = await getSaveFolder(window);
const backupsFolder = path.join(rootFolder, "/_backups")
await prepareFolders(rootFolder, currentFolder, backupsFolder);
}
async function prepareFolders(...folders) {
for (const folder of folders) {
try {
// Making sure the folder exists
// eslint-disable-next-line no-await-in-loop
await fs.stat(folder);
} catch (error) {
if (error.code === 'ENOENT') {
log.warn(`'${folder}' not found, creating it...`);
// eslint-disable-next-line no-await-in-loop
await fs.mkdir(folder);
} else {
log.error(error);
}
}
}
}
async function getFolderSizeInBytes(saveFolder) {
try {
return await dirSize(saveFolder);
} catch (error) {
log.error(error);
}
}
function setAutosaveConfig(value) {
config.set("autosave-enabled", value);
}
function isAutosaveEnabled() {
return config.get("autosave-enabled", true);
}
function setSaveCompressionConfig(value) {
config.set("save-compression-enabled", value);
}
function isSaveCompressionEnabled() {
return config.get("save-compression-enabled", true);
}
function setCloudEnabledConfig(value) {
config.set("cloud-enabled", value);
}
async function getSaveFolder(window, root = false) {
if (root) return path.join(app.getPath("userData"), "/saves");
const identifier = window.gameInfo?.player?.identifier ?? "";
return path.join(app.getPath("userData"), "/saves", `/${identifier}`);
}
function isCloudEnabled() {
// If the Steam API could not be initialized on game start, we'll abort this.
if (global.greenworksError) return false;
// If the user disables it in Steam there's nothing we can do
if (!greenworks.isCloudEnabledForUser()) return false;
// Let's check the config file to see if it's been overriden
const enabledInConf = config.get("cloud-enabled", true);
if (!enabledInConf) return false;
const isAppEnabled = greenworks.isCloudEnabled();
if (!isAppEnabled) greenworks.enableCloud(true);
return true;
}
function saveCloudFile(name, content) {
return new Promise((resolve, reject) => {
greenworks.saveTextToFile(name, content, resolve, reject);
})
}
function getFirstCloudFile() {
const nbFiles = greenworks.getFileCount();
if (nbFiles === 0) throw new Error('No files in cloud');
const file = greenworks.getFileNameAndSize(0);
log.silly(`Found ${nbFiles} files.`)
log.silly(`First File: ${file.name} (${file.size} bytes)`);
return file.name;
}
function getCloudFile() {
const file = getFirstCloudFile();
return new Promise((resolve, reject) => {
greenworks.readTextFromFile(file, resolve, reject);
});
}
function deleteCloudFile() {
const file = getFirstCloudFile();
return new Promise((resolve, reject) => {
greenworks.deleteFile(file, resolve, reject);
});
}
async function getSteamCloudQuota() {
return new Promise((resolve, reject) => {
greenworks.getCloudQuota(resolve, reject)
});
}
async function backupSteamDataToDisk(currentPlayerId) {
const nbFiles = greenworks.getFileCount();
if (nbFiles === 0) return;
const file = greenworks.getFileNameAndSize(0);
const previousPlayerId = file.name.replace(".json.gz", "");
if (previousPlayerId !== currentPlayerId) {
const backupSave = await getSteamCloudSaveString();
const backupFile = path.join(app.getPath("userData"), "/saves/_backups", `${previousPlayerId}.json.gz`);
const buffer = Buffer.from(backupSave, 'base64').toString('utf8');
saveContent = await gzip(buffer);
await fs.writeFile(backupFile, saveContent, 'utf8');
log.debug(`Saved backup game to '${backupFile}`);
}
}
async function pushGameSaveToSteamCloud(base64save, currentPlayerId) {
if (!isCloudEnabled) return Promise.reject("Steam Cloud is not Enabled");
try {
backupSteamDataToDisk(currentPlayerId);
} catch (error) {
log.error(error);
}
const steamSaveName = `${currentPlayerId}.json.gz`;
// Let's decode the base64 string so GZIP is more efficient.
const buffer = Buffer.from(base64save, "base64");
const compressedBuffer = await gzip(buffer);
// We can't use utf8 for some reason, steamworks is unhappy.
const content = compressedBuffer.toString("base64");
log.debug(`Uncompressed: ${base64save.length} bytes`);
log.debug(`Compressed: ${content.length} bytes`);
log.debug(`Saving to Steam Cloud as ${steamSaveName}`);
try {
await saveCloudFile(steamSaveName, content);
} catch (error) {
log.error(error);
}
}
async function getSteamCloudSaveString() {
if (!isCloudEnabled()) return Promise.reject("Steam Cloud is not Enabled");
log.debug(`Fetching Save in Steam Cloud`);
const cloudString = await getCloudFile();
const gzippedBase64Buffer = Buffer.from(cloudString, "base64");
const uncompressedBuffer = await gunzip(gzippedBase64Buffer);
const content = uncompressedBuffer.toString("base64");
log.debug(`Compressed: ${cloudString.length} bytes`);
log.debug(`Uncompressed: ${content.length} bytes`);
return content;
}
async function saveGameToDisk(window, saveData) {
const currentFolder = await getSaveFolder(window);
let saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder);
const maxFolderSizeBytes = config.get("autosave-quota", 1e8); // 100Mb per playerIndentifier
const remainingSpaceBytes = maxFolderSizeBytes - saveFolderSizeBytes;
log.debug(`Folder Usage: ${saveFolderSizeBytes} bytes`);
log.debug(`Folder Capacity: ${maxFolderSizeBytes} bytes`);
log.debug(`Remaining: ${remainingSpaceBytes} bytes (${(saveFolderSizeBytes / maxFolderSizeBytes * 100).toFixed(2)}% used)`)
const shouldCompress = isSaveCompressionEnabled();
const fileName = saveData.fileName;
const file = path.join(currentFolder, fileName + (shouldCompress ? ".gz" : ""));
try {
let saveContent = saveData.save;
if (shouldCompress) {
// Let's decode the base64 string so GZIP is more efficient.
const buffer = Buffer.from(saveContent, 'base64').toString('utf8');
saveContent = await gzip(buffer);
}
await fs.writeFile(file, saveContent, 'utf8');
log.debug(`Saved Game to '${file}'`);
log.debug(`Save Size: ${saveContent.length} bytes`);
} catch (error) {
log.error(error);
}
const fileStats = await getDirFileStats(currentFolder);
const oldestFiles = fileStats
.sort((a, b) => a.stat.mtime.getTime() - b.stat.mtime.getTime())
.map(f => f.file).filter(f => f !== file);
while (saveFolderSizeBytes > maxFolderSizeBytes && oldestFiles.length > 0) {
const fileToRemove = oldestFiles.shift();
log.debug(`Over Quota -> Removing "${fileToRemove}"`);
try {
// eslint-disable-next-line no-await-in-loop
await fs.unlink(fileToRemove);
} catch (error) {
log.error(error);
}
// eslint-disable-next-line no-await-in-loop
saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder);
log.debug(`Save Folder: ${saveFolderSizeBytes} bytes`);
log.debug(`Remaining: ${maxFolderSizeBytes - saveFolderSizeBytes} bytes (${(saveFolderSizeBytes / maxFolderSizeBytes * 100).toFixed(2)}% used)`)
}
return file;
}
async function loadLastFromDisk(window) {
const folder = await getSaveFolder(window);
const last = await getNewestFile(folder);
log.debug(`Last modified file: "${last.file}" (${last.stat.mtime.toLocaleString()})`);
return loadFileFromDisk(last.file);
}
async function loadFileFromDisk(path) {
const buffer = await fs.readFile(path);
let content;
if (path.endsWith('.gz')) {
const uncompressedBuffer = await gunzip(buffer);
content = uncompressedBuffer.toString('base64');
log.debug(`Uncompressed file content (new size: ${content.length} bytes)`);
} else {
content = buffer.toString('utf8');
log.debug(`Loaded file with ${content.length} bytes`)
}
return content;
}
function getSaveInformation(window, save) {
return new Promise((resolve) => {
ipcMain.once("get-save-info-response", async (event, data) => {
resolve(data);
});
window.webContents.send("get-save-info-request", save);
});
}
function getCurrentSave(window) {
return new Promise((resolve) => {
ipcMain.once('get-save-data-response', (event, data) => {
resolve(data);
});
window.webContents.send('get-save-data-request');
});
}
function pushSaveGameForImport(window, save, automatic) {
ipcMain.once("push-import-result", async (event, arg) => {
log.debug(`Was save imported? ${arg.wasImported ? "Yes" : "No"}`);
});
window.webContents.send("push-save-request", { save, automatic });
}
async function restoreIfNewerExists(window) {
const currentSave = await getCurrentSave(window);
const currentData = await getSaveInformation(window, currentSave.save);
const steam = {};
const disk = {};
try {
steam.save = await getSteamCloudSaveString();
steam.data = await getSaveInformation(window, steam.save);
} catch (error) {
log.error("Could not retrieve steam file");
log.debug(error);
}
try {
const saves = (await getAllSaves()).
sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime());
if (saves.length > 0) {
disk.save = await loadFileFromDisk(saves[0].file);
disk.data = await getSaveInformation(window, disk.save);
}
} catch(error) {
log.error("Could not retrieve disk file");
log.debug(error);
}
const lowPlaytime = 1000 * 60 * 15;
let bestMatch;
if (!steam.data && !disk.data) {
log.info("No data to import");
} else {
// We'll just compare using the lastSave field for now.
if (!steam.data) {
log.debug('Best potential save match: Disk');
bestMatch = disk;
} else if (!disk.data) {
log.debug('Best potential save match: Steam Cloud');
bestMatch = steam;
} else if ((steam.data.lastSave >= disk.data.lastSave)
|| (steam.data.playtime + lowPlaytime > disk.data.playtime)) {
// We want to prioritze steam data if the playtime is very close
log.debug('Best potential save match: Steam Cloud');
bestMatch = steam;
} else {
log.debug('Best potential save match: disk');
bestMatch = disk;
}
}
if (bestMatch) {
if (bestMatch.data.lastSave > currentData.lastSave + 5000) {
// We add a few seconds to the currentSave's lastSave to prioritize it
log.info("Found newer data than the current's save file");
log.silly(bestMatch.data);
await pushSaveGameForImport(window, bestMatch.save, true);
return true;
} else if(bestMatch.data.playtime > currentData.playtime && currentData.playtime < lowPlaytime) {
log.info("Found older save, but with more playtime, and current less than 15 mins played");
log.silly(bestMatch.data);
await pushSaveGameForImport(window, bestMatch.save, true);
return true;
} else {
log.debug("Current save data is the freshest");
return false;
}
}
}
module.exports = {
getCurrentSave, getSaveInformation,
restoreIfNewerExists, pushSaveGameForImport,
pushGameSaveToSteamCloud, getSteamCloudSaveString, getSteamCloudQuota, deleteCloudFile,
saveGameToDisk, loadLastFromDisk, loadFileFromDisk,
getSaveFolder, prepareSaveFolders, getAllSaves,
isCloudEnabled, setCloudEnabledConfig,
isAutosaveEnabled, setAutosaveConfig,
isSaveCompressionEnabled, setSaveCompressionConfig,
};

63
electron/windowTracker.js Normal file

@ -0,0 +1,63 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { screen } = require("electron");
const log = require("electron-log");
const debounce = require("lodash/debounce");
const Config = require("electron-config");
const config = new Config();
// https://stackoverflow.com/a/68627253
const windowTracker = (windowName) => {
let window, windowState;
const setBounds = () => {
// Restore from appConfig
if (config.has(`window.${windowName}`)) {
windowState = config.get(`window.${windowName}`);
return;
}
const size = screen.getPrimaryDisplay().workAreaSize;
// Default
windowState = {
x: undefined,
y: undefined,
width: size.width,
height: size.height,
isMaximized: true,
};
};
const saveState = debounce(() => {
if (!windowState.isMaximized) {
windowState = window.getBounds();
}
windowState.isMaximized = window.isMaximized();
log.silly(`Saving window.${windowName} to configs`);
config.set(`window.${windowName}`, windowState);
log.silly(windowState);
}, 1000);
const track = (win) => {
window = win;
['resize', 'move', 'close'].forEach((event) => {
win.on(event, saveState);
});
};
setBounds();
return {
state: {
x: windowState.x,
y: windowState.y,
width: windowState.width,
height: windowState.height,
isMaximized: windowState.isMaximized,
},
track,
};
};
module.exports = { windowTracker };

@ -73,5 +73,5 @@
<link rel="shortcut icon" href="favicon.ico"></head>
<body>
<div id="root"/>
<script type="text/javascript" src="dist/vendor.bundle.js"></script><script type="text/javascript" src="main.bundle.js"></script></body>
<script type="text/javascript" src="dist/vendor.bundle.js"></script><script type="text/javascript" src="dist/main.bundle.js"></script></body>
</html>

@ -8,4 +8,8 @@ module.exports = {
'.cypress', 'node_modules', 'dist',
],
testEnvironment: "jsdom",
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
}
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -28,5 +28,5 @@ Parameter that affects the percentage by which the servers money is increased
RAM cost: 0.1 GB
Returns the servers instrinsic “growth parameter”. This growth parameter is a number between 0 and 100 that represents how quickly the servers money grows. This parameter affects the percentage by which the servers money is increased when using the grow function. A higher growth parameter will result in a higher percentage increase from grow.
Returns the servers intrinsic “growth parameter”. This growth parameter is a number typically between 0 and 100 that represents how quickly the servers money grows. This parameter affects the percentage by which the servers money is increased when using the grow function. A higher growth parameter will result in a higher percentage increase from grow.

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [NS](./bitburner.ns.md) &gt; [getSharePower](./bitburner.ns.getsharepower.md)
## NS.getSharePower() method
Calculate your share power. Based on all the active share calls.
<b>Signature:</b>
```typescript
getSharePower(): number;
```
<b>Returns:</b>
number
## Remarks
RAM cost: 0.2 GB

@ -107,6 +107,7 @@ export async function main(ns) {
| [getServerRequiredHackingLevel(host)](./bitburner.ns.getserverrequiredhackinglevel.md) | Returns the required hacking level of the target server. |
| [getServerSecurityLevel(host)](./bitburner.ns.getserversecuritylevel.md) | Get server security level. |
| [getServerUsedRam(host)](./bitburner.ns.getserverusedram.md) | Get the used RAM on a server. |
| [getSharePower()](./bitburner.ns.getsharepower.md) | Calculate your share power. Based on all the active share calls. |
| [getTimeSinceLastAug()](./bitburner.ns.gettimesincelastaug.md) | Returns the amount of time in milliseconds that have passed since you last installed Augmentations. |
| [getWeakenTime(host)](./bitburner.ns.getweakentime.md) | Get the execution time of a weaken() call. |
| [grow(host, opts)](./bitburner.ns.grow.md) | Spoof money in a servers bank account, increasing the amount available. |
@ -144,6 +145,7 @@ export async function main(ns) {
| [scriptKill(script, host)](./bitburner.ns.scriptkill.md) | Kill all scripts with a filename. |
| [scriptRunning(script, host)](./bitburner.ns.scriptrunning.md) | Check if any script with a filename is running. |
| [serverExists(host)](./bitburner.ns.serverexists.md) | Returns a boolean denoting whether or not the specified server exists. |
| [share()](./bitburner.ns.share.md) | Share your computer with your factions. |
| [sleep(millis)](./bitburner.ns.sleep.md) | Suspends the script for n milliseconds. |
| [spawn(script, numThreads, args)](./bitburner.ns.spawn.md) | Terminate current script and start another in 10s. |
| [sprintf(format, args)](./bitburner.ns.sprintf.md) | Format a string. |

@ -48,7 +48,7 @@ for (let i = 0; i < scripts.length; ++i) {
```ts
// NS2:
const ps = ns.ps("home");
for (script of ps) {
for (let script of ps) {
ns.tprint(`${script.filename} ${ps[i].threads}`);
ns.tprint(script.args);
}

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [NS](./bitburner.ns.md) &gt; [share](./bitburner.ns.share.md)
## NS.share() method
Share your computer with your factions.
<b>Signature:</b>
```typescript
share(): Promise<void>;
```
<b>Returns:</b>
Promise&lt;void&gt;
## Remarks
RAM cost: 2.4 GB
Increases your rep gain of hacking contracts while share is called. Scales with thread count.

@ -19,5 +19,5 @@ List of active fragments placed on Stanek's Gift.
## Remarks
RAM cost: cost: 5 GB
RAM cost: 5 GB

@ -29,5 +29,5 @@ true if the fragment can be placed at that position. false otherwise.
## Remarks
RAM cost: cost: 0.5 GB
RAM cost: 0.5 GB

@ -17,5 +17,5 @@ void
## Remarks
RAM cost: cost: 0 GB
RAM cost: 0 GB

@ -19,5 +19,5 @@ List of possible fragments.
## Remarks
RAM cost: cost: 0 GB
RAM cost: 0 GB

@ -27,5 +27,5 @@ The fragment at \[rootX, rootY\], if any.
## Remarks
RAM cost: cost: 5 GB
RAM cost: 5 GB

@ -29,5 +29,5 @@ true if the fragment can be placed at that position. false otherwise.
## Remarks
RAM cost: cost: 5 GB
RAM cost: 5 GB

@ -27,5 +27,5 @@ The fragment at \[rootX, rootY\], if any.
## Remarks
RAM cost: cost: 0.15 GB
RAM cost: 0.15 GB

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [UserInterface](./bitburner.userinterface.md) &gt; [getGameInfo](./bitburner.userinterface.getgameinfo.md)
## UserInterface.getGameInfo() method
Gets the current game information (version, commit, ...)
<b>Signature:</b>
```typescript
getGameInfo(): GameInfo;
```
<b>Returns:</b>
GameInfo
## Remarks
RAM cost: 0 GB

@ -19,5 +19,5 @@ An object containing the player's styles
## Remarks
RAM cost: cost: 0 GB
RAM cost: 0 GB

@ -19,5 +19,5 @@ An object containing the theme's colors
## Remarks
RAM cost: cost: 0 GB
RAM cost: 0 GB

@ -16,6 +16,7 @@ interface UserInterface
| Method | Description |
| --- | --- |
| [getGameInfo()](./bitburner.userinterface.getgameinfo.md) | Gets the current game information (version, commit, ...) |
| [getStyles()](./bitburner.userinterface.getstyles.md) | Get the current styles |
| [getTheme()](./bitburner.userinterface.gettheme.md) | Get the current theme |
| [resetStyles()](./bitburner.userinterface.resetstyles.md) | Resets the player's styles to the default values |

@ -17,5 +17,5 @@ void
## Remarks
RAM cost: cost: 0 GB
RAM cost: 0 GB

@ -17,5 +17,5 @@ void
## Remarks
RAM cost: cost: 0 GB
RAM cost: 0 GB

@ -24,7 +24,7 @@ void
## Remarks
RAM cost: cost: 0 GB
RAM cost: 0 GB
## Example

@ -24,7 +24,7 @@ void
## Remarks
RAM cost: cost: 0 GB
RAM cost: 0 GB
## Example

1911
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -13,19 +13,10 @@
"@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",
"@mui/icons-material": "^5.0.3",
"@mui/material": "^5.0.3",
"@mui/styles": "^5.0.1",
"@types/acorn": "^4.0.6",
"@types/escodegen": "^0.0.7",
"@types/numeral": "0.0.25",
"@types/react": "^17.0.21",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.9",
"@types/react-resizable": "^1.7.3",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^5.0.0",
@ -45,7 +36,6 @@
"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",
"react-dom": "^17.0.2",
@ -59,10 +49,19 @@
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.15.0",
"@microsoft/api-documenter": "^7.13.65",
"@microsoft/api-extractor": "^7.18.17",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
"@testing-library/cypress": "^8.0.1",
"@types/acorn": "^4.0.6",
"@types/escodegen": "^0.0.7",
"@types/file-saver": "^2.0.3",
"@types/lodash": "^4.14.168",
"@types/numeral": "0.0.25",
"@types/react": "^17.0.21",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.9",
"@types/react-resizable": "^1.7.3",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"babel-jest": "^27.0.6",
@ -71,6 +70,7 @@
"electron": "^14.0.2",
"electron-packager": "^15.4.0",
"eslint": "^7.24.0",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^6.3.3",
"html-webpack-plugin": "^3.2.0",
"http-server": "^13.0.1",
@ -79,6 +79,7 @@
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^0.4.1",
"prettier": "^2.3.2",
"raw-loader": "^4.0.2",
"react-refresh": "^0.10.0",
"regenerator-runtime": "^0.13.9",
"source-map": "^0.7.3",
@ -102,7 +103,7 @@
"cy:dev": "start-server-and-test start:dev http://localhost:8000 cy:open",
"cy:open": "cypress open",
"cy:run": "cypress run",
"doc": "npx api-extractor run && npx api-documenter markdown",
"doc": "npx api-extractor run && npx api-documenter markdown && rm input/bitburner.api.json && rm -r input",
"format": "prettier --write .",
"start": "http-server -p 8000",
"start:dev": "webpack-dev-server --progress --env.devServer --mode development",
@ -112,13 +113,17 @@
"build:dev": "webpack --mode development",
"lint": "eslint --fix . --ext js,jsx,ts,tsx",
"lint:report": "eslint --ext js,jsx,ts,tsx .",
"preinstall": "node ./scripts/engines-check.js",
"preinstall": "node ./tools/engines-check/engines-check.js",
"test": "jest",
"test:watch": "jest --watch",
"watch": "webpack --watch --mode production",
"watch:dev": "webpack --watch --mode development",
"electron": "sh ./package.sh",
"electron:packager": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png --no-prune",
"electron:packager-all": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png",
"electron:packager-win": "electron-packager .package bitburner --platform win32 --arch x64 --out .build --overwrite --icon .package/icon.png",
"electron:packager-mac": "electron-packager .package bitburner --platform darwin --arch x64 --out .build --overwrite --icon .package/icon.png",
"electron:packager-linux": "electron-packager .package bitburner --platform linux --arch x64 --out .build --overwrite --icon .package/icon.png",
"allbuild": "npm run build && npm run electron && git add --all && git commit --amend --no-edit && git push -f -u origin dev"
}
}

@ -1,5 +1,8 @@
#!/bin/sh
# Clear out any files remaining from old builds
rm -rf .package
mkdir -p .package/dist/src/ThirdParty || true
mkdir -p .package/src/ThirdParty || true
mkdir -p .package/node_modules || true
@ -8,6 +11,7 @@ cp index.html .package
cp -r electron/* .package
cp -r dist/ext .package/dist
cp -r dist/icons .package/dist
cp -r dist/images .package/dist
# The css files
cp dist/vendor.css .package/dist
@ -26,5 +30,6 @@ cd electron
npm install
cd ..
BUILD_PLATFORM="${1:-"all"}"
# And finally build the app.
npm run electron:packager
npm run electron:packager-$BUILD_PLATFORM

@ -1,2 +1,8 @@
// Defined by webpack on startup or compilation
declare let __COMMIT_HASH__: string;
// When using file-loader, we'll get a path to the resource
declare module "*.png" {
const value: string;
export default value;
}

@ -380,7 +380,7 @@ export const achievements: IMap<Achievement> = {
},
TRAVEL: {
...achievementData["TRAVEL"],
Icon: "travel",
Icon: "TRAVEL",
Condition: () => Player.city !== CityName.Sector12,
},
WORKOUT: {
@ -553,7 +553,9 @@ export const achievements: IMap<Achievement> = {
...achievementData["MAX_CACHE"],
Icon: "HASHNETCAP",
Visible: () => hasAccessToSF(Player, 9),
Condition: () => hasHacknetServers(Player) && Player.hashManager.hashes === Player.hashManager.capacity,
Condition: () => hasHacknetServers(Player) &&
Player.hashManager.hashes === Player.hashManager.capacity &&
Player.hashManager.capacity > 0,
},
SLEEVE_8: {
...achievementData["SLEEVE_8"],

@ -22,12 +22,12 @@ export function loadGlobalAliases(saveString: string): void {
// Prints all aliases to terminal
export function printAliases(): void {
for (const name in Aliases) {
for (const name of Object.keys(Aliases)) {
if (Aliases.hasOwnProperty(name)) {
Terminal.print("alias " + name + "=" + Aliases[name]);
}
}
for (const name in GlobalAliases) {
for (const name of Object.keys(GlobalAliases)) {
if (GlobalAliases.hasOwnProperty(name)) {
Terminal.print("global alias " + name + "=" + GlobalAliases[name]);
}

@ -526,7 +526,7 @@ export class Augmentation {
// Adds this Augmentation to all Factions
addToAllFactions(): void {
for (const fac in Factions) {
for (const fac of Object.keys(Factions)) {
if (Factions.hasOwnProperty(fac)) {
const facObj: Faction | null = Factions[fac];
if (facObj == null) {

@ -112,7 +112,7 @@ function getRandomBonus(): any {
}
function initAugmentations(): void {
for (const name in Factions) {
for (const name of Object.keys(Factions)) {
if (Factions.hasOwnProperty(name)) {
Factions[name].augmentations = [];
}
@ -2050,7 +2050,7 @@ function initAugmentations(): void {
info:
"A brain implant carefully assembled around the synapses, which " +
"micromanages the activity and levels of various neuroreceptor " +
"chemicals and modulates electrical acvitiy to optimize concentration, " +
"chemicals and modulates electrical activity to optimize concentration, " +
"allowing the user to multitask much more effectively.",
stats: (
<>
@ -2498,7 +2498,7 @@ function initAugmentations(): void {
CONSTANTS.MultipleAugMultiplier * [1, 0.96, 0.94, 0.93][SourceFileFlags[11]],
Player.queuedAugmentations.length,
);
for (const name in Augmentations) {
for (const name of Object.keys(Augmentations)) {
if (Augmentations.hasOwnProperty(name)) {
Augmentations[name].baseCost *= mult;
}
@ -2525,7 +2525,7 @@ function applyAugmentation(aug: IPlayerOwnedAugmentation, reapply = false): void
const augObj = Augmentations[aug.name];
// Apply multipliers
for (const mult in augObj.mults) {
for (const mult of Object.keys(augObj.mults)) {
const v = Player.getMult(mult) * augObj.mults[mult];
Player.setMult(mult, v);
}

@ -26,7 +26,7 @@ export function InstalledAugmentations(): React.ReactElement {
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
sourceAugs.sort((aug1, aug2) => {
return aug1.name <= aug2.name ? -1 : 1;
return aug1.name.localeCompare(aug2.name);
});
}

@ -17,7 +17,7 @@ function calculateAugmentedStats(): any {
const augP: any = {};
for (const aug of Player.queuedAugmentations) {
const augObj = Augmentations[aug.name];
for (const mult in augObj.mults) {
for (const mult of Object.keys(augObj.mults)) {
const v = augP[mult] ? augP[mult] : 1;
augP[mult] = v * augObj.mults[mult];
}

@ -578,7 +578,7 @@ export function initBitNodeMultipliers(p: IPlayer): void {
if (p.bitNodeN == null) {
p.bitNodeN = 1;
}
for (const mult in BitNodeMultipliers) {
for (const mult of Object.keys(BitNodeMultipliers)) {
if (BitNodeMultipliers.hasOwnProperty(mult)) {
BitNodeMultipliers[mult] = 1;
}

@ -85,7 +85,7 @@ function BitNodePortal(props: IPortalProps): React.ReactElement {
</Typography>
}
>
<span onClick={() => setPortalOpen(true)} className={cssClass}>
<span onClick={() => setPortalOpen(true)} className={cssClass} aria-label={`enter-bitnode-${bitNode.number.toString()}`}>
<b>O</b>
</span>
</Tooltip>

@ -117,7 +117,7 @@ export class Action implements IAction {
// Check to make sure weights are summed properly
let sum = 0;
for (const weight in this.weights) {
for (const weight of Object.keys(this.weights)) {
if (this.weights.hasOwnProperty(weight)) {
sum += this.weights[weight];
}
@ -131,7 +131,7 @@ export class Action implements IAction {
);
}
for (const decay in this.decays) {
for (const decay of Object.keys(this.decays)) {
if (this.decays.hasOwnProperty(decay)) {
if (this.decays[decay] > 1) {
throw new Error(
@ -240,7 +240,7 @@ export class Action implements IAction {
}
let difficulty = this.getDifficulty();
let competence = 0;
for (const stat in this.weights) {
for (const stat of Object.keys(this.weights)) {
if (this.weights.hasOwnProperty(stat)) {
const playerStatLvl = Player.queryStatFromString(stat);
const key = "eff" + stat.charAt(0).toUpperCase() + stat.slice(1);

@ -135,7 +135,7 @@ export class Bladeburner implements IBladeburner {
// Can't start a BlackOp if you haven't done the one before it
const blackops = [];
for (const nm in BlackOperations) {
for (const nm of Object.keys(BlackOperations)) {
if (BlackOperations.hasOwnProperty(nm)) {
blackops.push(nm);
}
@ -1074,7 +1074,7 @@ export class Bladeburner implements IBladeburner {
updateSkillMultipliers(): void {
this.resetSkillMultipliers();
for (const skillName in this.skills) {
for (const skillName of Object.keys(this.skills)) {
if (this.skills.hasOwnProperty(skillName)) {
const skill = Skills[skillName];
if (skill == null) {

@ -12,7 +12,7 @@ interface IProps {
export function BlackOpList(props: IProps): React.ReactElement {
let blackops: BlackOperation[] = [];
for (const blackopName in BlackOperations) {
for (const blackopName of Object.keys(BlackOperations)) {
if (BlackOperations.hasOwnProperty(blackopName)) {
blackops.push(BlackOperations[blackopName]);
}

@ -12,7 +12,7 @@ interface IProps {
export function GeneralActionList(props: IProps): React.ReactElement {
const actions: Action[] = [];
for (const name in GeneralActions) {
for (const name of Object.keys(GeneralActions)) {
if (GeneralActions.hasOwnProperty(name)) {
actions.push(GeneralActions[name]);
}

@ -14,6 +14,7 @@ import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField";
const MAX_BET = 100e6;
export const DECK_COUNT = 5; // 5-deck multideck
enum Result {
Pending = "",
@ -45,7 +46,7 @@ export class Blackjack extends Game<Props, State> {
constructor(props: Props) {
super(props);
this.deck = new Deck(5); // 5-deck multideck
this.deck = new Deck(DECK_COUNT);
const initialBet = 1e6;

@ -40,13 +40,13 @@ const strategies: {
} = {
Red: {
match: (n: number): boolean => {
if (n === 0) return false;
return redNumbers.includes(n);
},
payout: 1,
},
Black: {
match: (n: number): boolean => {
if (n === 0) return false;
return !redNumbers.includes(n);
},
payout: 1,
@ -118,12 +118,6 @@ export function Roulette(props: IProps): React.ReactElement {
const [status, setStatus] = useState<string | JSX.Element>("waiting");
const [n, setN] = useState(0);
const [lock, setLock] = useState(true);
const [strategy, setStrategy] = useState<Strategy>({
payout: 0,
match: (): boolean => {
return false;
},
});
useEffect(() => {
const i = window.setInterval(step, 50);
@ -156,13 +150,12 @@ export function Roulette(props: IProps): React.ReactElement {
return `${n}${color}`;
}
function play(s: Strategy): void {
function play(strategy: Strategy): void {
if (reachedLimit(props.p)) return;
setCanPlay(false);
setLock(false);
setStatus("playing");
setStrategy(s);
setTimeout(() => {
let n = Math.floor(rng.random() * 37);

@ -157,20 +157,20 @@ export function SlotMachine(props: IProps): React.ReactElement {
function step(): void {
let stoppedOne = false;
const copy = index.slice();
for (const i in copy) {
for (let i = 0; i < copy.length; i++) {
if (copy[i] === locks[i] && !stoppedOne) continue;
copy[i] = (copy[i] + 1) % symbols.length;
copy[i] = (copy[i] - 1 >= 0) ? copy[i] - 1 : symbols.length - 1;
stoppedOne = true;
}
setIndex(copy);
if (stoppedOne && copy.every((e, i) => e === locks[i])) {
checkWinnings();
checkWinnings(getTable(copy, symbols));
}
}
function getTable(): string[][] {
function getTable(index:number[], symbols:string[]): string[][] {
return [
[
symbols[(index[0] + symbols.length - 1) % symbols.length],
@ -209,8 +209,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
]);
}
function checkWinnings(): void {
const t = getTable();
function checkWinnings(t:string[][]): void {
const getPaylineData = function (payline: number[][]): string[] {
const data = [];
for (const point of payline) {
@ -267,7 +266,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
setInvestment(investment);
}
const t = getTable();
const t = getTable(index, symbols);
// prettier-ignore
return (
<>

@ -26,7 +26,7 @@ export function initCompanies(): void {
});
// Reset data
for (const companyName in Companies) {
for (const companyName of Object.keys(Companies)) {
const company = Companies[companyName];
const oldCompany = oldCompanies[companyName];
if (!(oldCompany instanceof Company)) {

@ -111,8 +111,8 @@ export const CONSTANTS: {
TotalNumBitNodes: number;
LatestUpdate: string;
} = {
VersionString: "1.3.0",
VersionNumber: 9,
VersionString: "1.4.0",
VersionNumber: 10,
// Speed (in ms) at which the main loop is updated
_idleSpeed: 200,
@ -273,89 +273,22 @@ export const CONSTANTS: {
TotalNumBitNodes: 24,
LatestUpdate: `
v1.3.0 - 2022-01-04 Cleaning up
-------------------------------
v1.4.0 - 2022-01-18 Sharing is caring
-------------------------------------
** External IDE integration **
** Computer sharing **
* The Steam version has a webserver that allows integration with external IDEs.
A VSCode extension is available on the market place. (The documentation for the ext. isn't
written yet)
* A new mechanic has been added, it's is invoked by calling the new function 'share'.
This mechanic helps you farm reputation faster.
** Source-Files **
** gang **
* SF4 has been reworked.
* New SF -1.
* Installing augs means losing a little bit of ascension multipliers.
** UI **
* Fix some edge case with skill bat tooltips (@MartinFournier)
* Made some background match theme color (@Kejikus)
* Fix problem with script editor height not adjusting correctly (@billyvg)
* Fix some formatting issues with Bladeburner (@MartinFournier, @nickofolas)
* Fix some functions like 'alert' format messages better (@MageKing17)
* Many community themes added.
* New script editor theme (@Hedrauta, @Dexalt142)
* Improvements to tail windows (@theit8514)
* Training is more consise (@mikomyazaki)
* Fix Investopedia not displaying properly (@JotaroS)
* Remove alpha from theme editor (@MartinFournier)
* Fix corporation tooltip not displaying properly (@MartinFournier)
* Add tooltip on backdoored location names (@MartinFournier)
* Allow toasts to be dismissed by clicking them (@nickofolas)
* Darkweb item listing now shows what you own. (@hexnaught)
** Bug fix **
* Fix unit tests (@MartinFournier)
* Fixed issue with 'cat' and 'read' not finding foldered files (@Nick-Colclasure)
* Buying on the dark web will remove incomplete exe (@hexnaught)
* Fix bug that would cause the game to crash trying to go to a job without a job (@hexnaught)
* purchaseServer validation (@nickofolas)
* Script Editor focuses code when changing tab (@MartinFournier)
* Fix script editor for .txt files (@65-7a)
* Fix 'buy' command not displaying correctly. (@hexnaught)
* Fix hackAnalyzeThread returning NaN (@mikomyazaki)
* Electron handles exceptions better (@MageKing17)
* Electron will handle 'unresponsive' event and present the opportunity to reload the game with no scripts (@MartinFournier)
* Fix 'cp' between folders (@theit8514)
* Fix throwing null/undefined errors (@nickofolas)
* Allow shortcuts to work when unfocused (@MageKing17)
* Fix some dependency issue (@locriacyber)
* Fix corporation state returning an object instead of a string (@antonvmironov)
* Fix 'mv' overwriting files (@theit8514)
* Fix joesguns not being influenced by hack/grow (@dou867, @MartinFournier)
* Added warning when opening external links. (@MartinFournier)
* Prevent applying for positions that aren't offered (@TheMas3212)
* Import has validation (@MartinFournier)
** There's more but I'm going to write it later. **
** Misc. **
* Added vim mode to script editor (@billyvg)
* Clean up script editor code (@Rez855)
* 'cat' works on scripts (@65-7a)
* Add wordWrap for Monaco (@MartinFournier)
* Include map bundles in electron for easier debugging (@MartinFournier)
* Fix importing very large files (@MartinFournier)
* Cache program blob, reducing ram usage of the game (@theit8514)
* Dev menu can set server to $0 (@mikomyazaki)
* 'backdoor' allows direct connect (@mikomyazaki)
* Github workflow work (@MartinFournier)
* workForFaction / workForCompany have a new parameter (@theit8514)
* Alias accept single quotes (@sporkwitch, @FaintSpeaker)
* Add grep options to 'ps' (@maxtimum)
* Added buy all option to 'buy' (@anthonydroberts)
* Added more shortcuts to terminal input (@Frank-py)
* Refactor some port code (@ErzengelLichtes)
* Settings to control GiB vs GB (@ErzengelLichtes)
* Add electron option to export save game (@MartinFournier)
* Electron improvements (@MartinFournier)
* Expose some notifications functions to electron (@MartinFournier)
* Documentation (@MartinFournier, @cyn, @millennIumAMbiguity, @2PacIsAlive,
@TheCoderJT, @hexnaught, @sschmidTU, @FOLLGAD, @Hedrauta, @Xynrati,
@mikomyazaki, @Icehawk78, @aaronransley, @TheMas3212, @Hedrauta, @alkemann,
@ReeseJones, @amclark42, @thadguidry, @jasonhaxstuff, @pan-kuleczka, @jhollowe,
@ApatheticsAnonymous, @erplsf, @daanflore, @nickofolas, @Kebap, @smolgumball,
@woody-lam-cwl)
* Nerf noodle bar.
`,
};

@ -307,7 +307,7 @@ export class Corporation {
if (upgN === 1) {
for (let i = 0; i < this.divisions.length; ++i) {
const industry = this.divisions[i];
for (const city in industry.warehouses) {
for (const city of Object.keys(industry.warehouses)) {
const warehouse = industry.warehouses[city];
if (warehouse === 0) continue;
if (industry.warehouses.hasOwnProperty(city) && warehouse instanceof Warehouse) {

@ -378,7 +378,7 @@ export class Industry implements IIndustry {
updateWarehouseSizeUsed(warehouse: Warehouse): void {
warehouse.updateMaterialSizeUsed();
for (const prodName in this.products) {
for (const prodName of Object.keys(this.products)) {
if (this.products.hasOwnProperty(prodName)) {
const prod = this.products[prodName];
if (prod === undefined) continue;
@ -414,7 +414,7 @@ export class Industry implements IIndustry {
// Process offices (and the employees in them)
let employeeSalary = 0;
for (const officeLoc in this.offices) {
for (const officeLoc of Object.keys(this.offices)) {
const office = this.offices[officeLoc];
if (office === 0) continue;
if (office instanceof OfficeSpace) {
@ -473,7 +473,7 @@ export class Industry implements IIndustry {
if (this.warehouses[CorporationConstants.Cities[i]] instanceof Warehouse) {
const wh = this.warehouses[CorporationConstants.Cities[i]];
if (wh === 0) continue;
for (const name in reqMats) {
for (const name of Object.keys(reqMats)) {
if (reqMats.hasOwnProperty(name)) {
wh.materials[name].processMarket();
}
@ -496,7 +496,7 @@ export class Industry implements IIndustry {
// Process change in demand and competition for this industry's products
processProductMarket(marketCycles = 1): void {
// Demand gradually decreases, and competition gradually increases
for (const name in this.products) {
for (const name of Object.keys(this.products)) {
if (this.products.hasOwnProperty(name)) {
const product = this.products[name];
if (product === undefined) continue;
@ -534,7 +534,7 @@ export class Industry implements IIndustry {
}
const warehouse = this.warehouses[city];
if (warehouse === 0) continue;
for (const matName in warehouse.materials) {
for (const matName of Object.keys(warehouse.materials)) {
if (warehouse.materials.hasOwnProperty(matName)) {
const mat = warehouse.materials[matName];
mat.imp = 0;
@ -555,7 +555,7 @@ export class Industry implements IIndustry {
switch (this.state) {
case "PURCHASE": {
/* Process purchase of materials */
for (const matName in warehouse.materials) {
for (const matName of Object.keys(warehouse.materials)) {
if (!warehouse.materials.hasOwnProperty(matName)) continue;
const mat = warehouse.materials[matName];
let buyAmt = 0;
@ -577,7 +577,7 @@ export class Industry implements IIndustry {
// smart supply
const smartBuy: { [key: string]: number | undefined } = {};
for (const matName in warehouse.materials) {
for (const matName of Object.keys(warehouse.materials)) {
if (!warehouse.materials.hasOwnProperty(matName)) continue;
if (!warehouse.smartSupplyEnabled || !Object.keys(this.reqMats).includes(matName)) continue;
const mat = warehouse.materials[matName];
@ -594,7 +594,7 @@ export class Industry implements IIndustry {
// Find which material were trying to create the least amount of product with.
let worseAmt = 1e99;
for (const matName in smartBuy) {
for (const matName of Object.keys(smartBuy)) {
const buyAmt = smartBuy[matName];
if (buyAmt === undefined) throw new Error(`Somehow smartbuy matname is undefined`);
const reqMat = this.reqMats[matName];
@ -604,7 +604,7 @@ export class Industry implements IIndustry {
}
// Align all the materials to the smallest amount.
for (const matName in smartBuy) {
for (const matName of Object.keys(smartBuy)) {
const reqMat = this.reqMats[matName];
if (reqMat === undefined) throw new Error(`reqMat "${matName}" is undefined`);
smartBuy[matName] = worseAmt * reqMat;
@ -612,7 +612,7 @@ export class Industry implements IIndustry {
// Calculate the total size of all things were trying to buy
let totalSize = 0;
for (const matName in smartBuy) {
for (const matName of Object.keys(smartBuy)) {
const buyAmt = smartBuy[matName];
if (buyAmt === undefined) throw new Error(`Somehow smartbuy matname is undefined`);
totalSize += buyAmt * MaterialSizes[matName];
@ -621,7 +621,7 @@ export class Industry implements IIndustry {
// Shrink to the size of available space.
const freeSpace = warehouse.size - warehouse.sizeUsed;
if (totalSize > freeSpace) {
for (const matName in smartBuy) {
for (const matName of Object.keys(smartBuy)) {
const buyAmt = smartBuy[matName];
if (buyAmt === undefined) throw new Error(`Somehow smartbuy matname is undefined`);
smartBuy[matName] = Math.floor((buyAmt * freeSpace) / totalSize);
@ -629,7 +629,7 @@ export class Industry implements IIndustry {
}
// Use the materials already in the warehouse if the option is on.
for (const matName in smartBuy) {
for (const matName of Object.keys(smartBuy)) {
if (!warehouse.smartSupplyUseLeftovers[matName]) continue;
const mat = warehouse.materials[matName];
const buyAmt = smartBuy[matName];
@ -638,7 +638,7 @@ export class Industry implements IIndustry {
}
// buy them
for (const matName in smartBuy) {
for (const matName of Object.keys(smartBuy)) {
const mat = warehouse.materials[matName];
const buyAmt = smartBuy[matName];
if (buyAmt === undefined) throw new Error(`Somehow smartbuy matname is undefined`);
@ -675,7 +675,7 @@ export class Industry implements IIndustry {
for (let tmp = 0; tmp < this.prodMats.length; ++tmp) {
totalMatSize += MaterialSizes[this.prodMats[tmp]];
}
for (const reqMatName in this.reqMats) {
for (const reqMatName of Object.keys(this.reqMats)) {
const normQty = this.reqMats[reqMatName];
if (normQty === undefined) continue;
totalMatSize -= MaterialSizes[reqMatName] * normQty;
@ -695,7 +695,7 @@ export class Industry implements IIndustry {
// Make sure we have enough resource to make our materials
let producableFrac = 1;
for (const reqMatName in this.reqMats) {
for (const reqMatName of Object.keys(this.reqMats)) {
if (this.reqMats.hasOwnProperty(reqMatName)) {
const reqMat = this.reqMats[reqMatName];
if (reqMat === undefined) continue;
@ -712,7 +712,7 @@ export class Industry implements IIndustry {
// Make our materials if they are producable
if (producableFrac > 0 && prod > 0) {
for (const reqMatName in this.reqMats) {
for (const reqMatName of Object.keys(this.reqMats)) {
const reqMat = this.reqMats[reqMatName];
if (reqMat === undefined) continue;
const reqMatQtyNeeded = reqMat * prod * producableFrac;
@ -729,7 +729,7 @@ export class Industry implements IIndustry {
Math.pow(warehouse.materials["AICores"].qty, this.aiFac) / 10e3;
}
} else {
for (const reqMatName in this.reqMats) {
for (const reqMatName of Object.keys(this.reqMats)) {
if (this.reqMats.hasOwnProperty(reqMatName)) {
warehouse.materials[reqMatName].prd = 0;
}
@ -745,7 +745,7 @@ export class Industry implements IIndustry {
//If this doesn't produce any materials, then it only creates
//Products. Creating products will consume materials. The
//Production of all consumed materials must be set to 0
for (const reqMatName in this.reqMats) {
for (const reqMatName of Object.keys(this.reqMats)) {
warehouse.materials[reqMatName].prd = 0;
}
}
@ -753,7 +753,7 @@ export class Industry implements IIndustry {
case "SALE":
/* Process sale of materials */
for (const matName in warehouse.materials) {
for (const matName of Object.keys(warehouse.materials)) {
if (warehouse.materials.hasOwnProperty(matName)) {
const mat = warehouse.materials[matName];
if (mat.sCost < 0 || mat.sllman[0] === false) {
@ -884,7 +884,7 @@ export class Industry implements IIndustry {
break;
case "EXPORT":
for (const matName in warehouse.materials) {
for (const matName of Object.keys(warehouse.materials)) {
if (warehouse.materials.hasOwnProperty(matName)) {
const mat = warehouse.materials[matName];
mat.totalExp = 0; //Reset export
@ -996,7 +996,7 @@ export class Industry implements IIndustry {
//Create products
if (this.state === "PRODUCTION") {
for (const prodName in this.products) {
for (const prodName of Object.keys(this.products)) {
const prod = this.products[prodName];
if (prod === undefined) continue;
if (!prod.fin) {
@ -1028,7 +1028,7 @@ export class Industry implements IIndustry {
}
//Produce Products
for (const prodName in this.products) {
for (const prodName of Object.keys(this.products)) {
if (this.products.hasOwnProperty(prodName)) {
const prod = this.products[prodName];
if (prod instanceof Product && prod.fin) {
@ -1070,7 +1070,7 @@ export class Industry implements IIndustry {
//Calculate net change in warehouse storage making the Products will cost
let netStorageSize = product.siz;
for (const reqMatName in product.reqMats) {
for (const reqMatName of Object.keys(product.reqMats)) {
if (product.reqMats.hasOwnProperty(reqMatName)) {
const normQty = product.reqMats[reqMatName];
netStorageSize -= MaterialSizes[reqMatName] * normQty;
@ -1087,7 +1087,7 @@ export class Industry implements IIndustry {
//Make sure we have enough resources to make our Products
let producableFrac = 1;
for (const reqMatName in product.reqMats) {
for (const reqMatName of Object.keys(product.reqMats)) {
if (product.reqMats.hasOwnProperty(reqMatName)) {
const req = product.reqMats[reqMatName] * prod;
if (warehouse.materials[reqMatName].qty < req) {
@ -1098,7 +1098,7 @@ export class Industry implements IIndustry {
//Make our Products if they are producable
if (producableFrac > 0 && prod > 0) {
for (const reqMatName in product.reqMats) {
for (const reqMatName of Object.keys(product.reqMats)) {
if (product.reqMats.hasOwnProperty(reqMatName)) {
const reqMatQtyNeeded = product.reqMats[reqMatName] * prod * producableFrac;
warehouse.materials[reqMatName].qty -= reqMatQtyNeeded;
@ -1117,7 +1117,7 @@ export class Industry implements IIndustry {
case "SALE": {
//Process sale of Products
product.pCost = 0; //Estimated production cost
for (const reqMatName in product.reqMats) {
for (const reqMatName of Object.keys(product.reqMats)) {
if (product.reqMats.hasOwnProperty(reqMatName)) {
product.pCost += product.reqMats[reqMatName] * warehouse.materials[reqMatName].bCost;
}
@ -1253,7 +1253,7 @@ export class Industry implements IIndustry {
}
discontinueProduct(product: Product): void {
for (const productName in this.products) {
for (const productName of Object.keys(this.products)) {
if (this.products.hasOwnProperty(productName)) {
if (product === this.products[productName]) {
delete this.products[productName];
@ -1358,7 +1358,7 @@ export class Industry implements IIndustry {
// Since ResearchTree data isnt saved, we'll update the Research Tree data
// based on the stored 'researched' property in the Industry object
if (Object.keys(researchTree.researched).length !== Object.keys(this.researched).length) {
for (const research in this.researched) {
for (const research of Object.keys(this.researched)) {
researchTree.research(research);
}
}

@ -103,7 +103,7 @@ export class OfficeSpace {
calculateEmployeeProductivity(corporation: ICorporation, industry: IIndustry): void {
//Reset
for (const name in this.employeeProd) {
for (const name of Object.keys(this.employeeProd)) {
this.employeeProd[name] = 0;
}

@ -198,7 +198,7 @@ export class Product {
//Calculate the product's required materials
//For now, just set it to be the same as the requirements to make materials
for (const matName in industry.reqMats) {
for (const matName of Object.keys(industry.reqMats)) {
if (industry.reqMats.hasOwnProperty(matName)) {
const reqMat = industry.reqMats[matName];
if (reqMat === undefined) continue;
@ -209,7 +209,7 @@ export class Product {
//Calculate the product's size
//For now, just set it to be the same size as the requirements to make materials
this.siz = 0;
for (const matName in industry.reqMats) {
for (const matName of Object.keys(industry.reqMats)) {
const reqMat = industry.reqMats[matName];
if (reqMat === undefined) continue;
this.siz += MaterialSizes[matName] * reqMat;

@ -85,7 +85,7 @@ export class Warehouse {
// Re-calculate how much space is being used by this Warehouse
updateMaterialSizeUsed(): void {
this.sizeUsed = 0;
for (const matName in this.materials) {
for (const matName of Object.keys(this.materials)) {
const mat = this.materials[matName];
if (MaterialSizes.hasOwnProperty(matName)) {
this.sizeUsed += mat.qty * MaterialSizes[matName];

@ -26,6 +26,7 @@ import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableRow from "@mui/material/TableRow";
import { TableCell } from "../../ui/React/Table";
import { Box } from "@mui/material";
interface IProps {
office: OfficeSpace;
@ -430,20 +431,17 @@ export function IndustryOffice(props: IProps): React.ReactElement {
<Typography>
Size: {props.office.employees.length} / {props.office.size} employees
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr', width: 'fit-content' }}>
<Box sx={{ gridTemplateColumns: 'repeat(3, 1fr)' }}>
<Tooltip title={<Typography>Automatically hires an employee and gives him/her a random name</Typography>}>
<span>
<Button disabled={props.office.atCapacity()} onClick={autohireEmployeeButtonOnClick}>
Hire Employee
</Button>
</span>
</Tooltip>
<br />
<Tooltip title={<Typography>Upgrade the office's size so that it can hold more employees!</Typography>}>
<span>
<Button disabled={corp.funds < 0} onClick={() => setUpgradeOfficeSizeOpen(true)}>
Upgrade size
</Button>
</span>
</Tooltip>
<UpgradeOfficeSizeModal
rerender={props.rerender}
@ -457,11 +455,9 @@ export function IndustryOffice(props: IProps): React.ReactElement {
<Tooltip
title={<Typography>Throw an office party to increase your employee's morale and happiness</Typography>}
>
<span>
<Button disabled={corp.funds < 0} onClick={() => setThrowPartyOpen(true)}>
Throw Party
</Button>
</span>
</Tooltip>
<ThrowPartyModal
rerender={props.rerender}
@ -472,9 +468,9 @@ export function IndustryOffice(props: IProps): React.ReactElement {
</>
)}
<br />
</Box>
<SwitchButton manualMode={employeeManualAssignMode} switchMode={setEmployeeManualAssignMode} />
</Box>
{employeeManualAssignMode ? (
<ManualManagement rerender={props.rerender} office={props.office} />
) : (

Some files were not shown because too many files have changed in this diff Show More