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/ node_modules/
dist/
input/
.dist .dist
.tmp .tmp
.package .package
.build
assets/
css/
.cypress/ .cypress/
cypress/
dist/
input/
assets/
doc/ doc/
markdown/ markdown/
netscript_tests/
scripts/
test/netscript/
electron/lib electron/lib
electron/greenworks.js electron/greenworks.js
src/ThirdParty/* src/ThirdParty/*
src/JSInterpreter.js src/JSInterpreter.js
tools/engines-check/
test/*.bundle.*
editor.main.js editor.main.js
main.bundle.js main.bundle.js
webpack.config.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' cache: 'npm'
- name: Install npm dependencies - name: Install npm dependencies
run: npm ci run: npm ci
- name: Run linter - name: Run tests
run: npm run test 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 /doc/build
/node_modules /node_modules
/electron/node_modules /electron/node_modules
/dist/*.map
/test/*.map /test/*.map
/test/*.bundle.* /test/*.bundle.*
/test/*.css /test/*.css
/input/bitburner.api.json
.cypress .cypress
# tmp folder for electron # 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. 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/). 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 # 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 Anyone is welcome to contribute to the documentation by editing the [source
files](/doc/source) and then making a pull request with your contributions. files](/doc/source) and then making a pull request with your contributions.
For further guidance, please refer to the "As A Documentor" section of For further guidance, please refer to the "As A Documentor" section of
[CONTRIBUTING](CONTRIBUTING.md). [CONTRIBUTING](./doc/CONTRIBUTING.md).
# Contribution # Contribution
There are many ways to contribute to the game. It can be as simple as fixing 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, 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, 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 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", "baseUrl": "http://localhost:8000",
"fixturesFolder": false,
"trashAssetsBeforeRuns": true, "trashAssetsBeforeRuns": true,
"screenshotsFolder": ".cypress/screenshots",
"videosFolder": ".cypress/videos",
"videoUploadOnPasses": false, "videoUploadOnPasses": false,
"viewportWidth": 1980, "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; limit: number;
} }
/**
* Game Information
* @internal
*/
export declare interface GameInfo {
version: string;
commit: string;
platform: string;
}
/** /**
* Gang API * Gang API
* @remarks * @remarks
@ -3539,9 +3549,9 @@ export declare interface NS extends Singularity {
* @remarks * @remarks
* RAM cost: 0.1 GB * RAM cost: 0.1 GB
* *
* Returns the servers instrinsic growth parameter. This growth * Returns the servers intrinsic growth parameter. This growth
* parameter is a number between 0 and 100 that represents how * parameter is a number typically between 0 and 100 that represents
* quickly the servers money grows. This parameter affects the * how quickly the servers money grows. This parameter affects the
* percentage by which the servers money is increased when using the * percentage by which the servers money is increased when using the
* grow function. A higher growth parameter will result in a * grow function. A higher growth parameter will result in a
* higher percentage increase from grow. * higher percentage increase from grow.
@ -4430,6 +4440,23 @@ export declare interface NS extends Singularity {
* ``` * ```
*/ */
flags(schema: [string, string | number | boolean | string[]][]): any; 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. * List possible fragments.
* @remarks * @remarks
* RAM cost: cost: 0 GB * RAM cost: 0 GB
* *
* @returns List of possible fragments. * @returns List of possible fragments.
*/ */
@ -5983,7 +6010,7 @@ export declare interface Stanek {
/** /**
* List of fragments in Stanek's Gift. * List of fragments in Stanek's Gift.
* @remarks * @remarks
* RAM cost: cost: 5 GB * RAM cost: 5 GB
* *
* @returns List of active fragments placed on Stanek's Gift. * @returns List of active fragments placed on Stanek's Gift.
*/ */
@ -5992,14 +6019,14 @@ export declare interface Stanek {
/** /**
* Clear the board of all fragments. * Clear the board of all fragments.
* @remarks * @remarks
* RAM cost: cost: 0 GB * RAM cost: 0 GB
*/ */
clear(): void; clear(): void;
/** /**
* Check if fragment can be placed at specified location. * Check if fragment can be placed at specified location.
* @remarks * @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 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. * @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. * Place fragment on Stanek's Gift.
* @remarks * @remarks
* RAM cost: cost: 5 GB * RAM cost: 5 GB
* *
* @param rootX - X against which to align the top left of the fragment. * @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. * @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. * Get placed fragment at location.
* @remarks * @remarks
* RAM cost: cost: 5 GB * RAM cost: 5 GB
* *
* @param rootX - X against which to align the top left of the fragment. * @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. * @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. * Remove fragment at location.
* @remarks * @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 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. * @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 * Get the current theme
* @remarks * @remarks
* RAM cost: cost: 0 GB * RAM cost: 0 GB
* *
* @returns An object containing the theme's colors * @returns An object containing the theme's colors
*/ */
@ -6448,7 +6475,7 @@ export declare interface UserInterface {
/** /**
* Sets the current theme * Sets the current theme
* @remarks * @remarks
* RAM cost: cost: 0 GB * RAM cost: 0 GB
* @example * @example
* Usage example (NS2) * Usage example (NS2)
* ```ts * ```ts
@ -6462,14 +6489,14 @@ export declare interface UserInterface {
/** /**
* Resets the player's theme to the default values * Resets the player's theme to the default values
* @remarks * @remarks
* RAM cost: cost: 0 GB * RAM cost: 0 GB
*/ */
resetTheme(): void; resetTheme(): void;
/** /**
* Get the current styles * Get the current styles
* @remarks * @remarks
* RAM cost: cost: 0 GB * RAM cost: 0 GB
* *
* @returns An object containing the player's styles * @returns An object containing the player's styles
*/ */
@ -6478,7 +6505,7 @@ export declare interface UserInterface {
/** /**
* Sets the current styles * Sets the current styles
* @remarks * @remarks
* RAM cost: cost: 0 GB * RAM cost: 0 GB
* @example * @example
* Usage example (NS2) * Usage example (NS2)
* ```ts * ```ts
@ -6492,9 +6519,16 @@ export declare interface UserInterface {
/** /**
* Resets the player's styles to the default values * Resets the player's styles to the default values
* @remarks * @remarks
* RAM cost: cost: 0 GB * RAM cost: 0 GB
*/ */
resetStyles(): void; 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 Docs](http://bitburner.readthedocs.io/), you will
need to have Python installed, along with [Sphinx](http://www.sphinx-doc.org). 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`. 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`.

@ -28,7 +28,7 @@ List of all Source-Files
|| || * Increases the player's charisma and company salary multipliers by 8%/12%/14%. | || || * Increases the player's charisma and company salary multipliers by 8%/12%/14%. |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|| BitNode-4: The Singularity || * Let the player access and use Netscript Singularity Functions in other BitNodes. | || BitNode-4: The Singularity || * Let the player access and use Netscript Singularity Functions in other BitNodes. |
|| || * Each level of this Source-File reduces the RAM cost of singularity functions. | || || * Each level of this Source-File reduces the RAM cost of singularity functions. |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|| BitNode-5: Artificial Intelligence || * Unlocks :ref:`gameplay_intelligence`. | || BitNode-5: Artificial Intelligence || * Unlocks :ref:`gameplay_intelligence`. |
|| || * Unlocks :js:func:`getBitNodeMultipliers` and start with Formulas.exe. | || || * Unlocks :js:func:`getBitNodeMultipliers` and start with Formulas.exe. |

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

@ -30,6 +30,11 @@ from faction to faction.
List of Factions and their Requirements 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 | | 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 RAM of 8 | |
| | | * Total Hacknet Cores of 4 | | | | | * 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 | | | | * $15m | * New Tokyo |
| | | | * Ishima | | | | | * Ishima |
| | | | * Volhaven | | | | | * Volhaven |
@ -74,8 +90,19 @@ List of Factions and their Requirements
| | | | * New Tokyo | | | | | * New Tokyo |
| | | | * Ishima | | | | | * Ishima |
+---------------------+----------------+-----------------------------------------+-------------------------------+ +---------------------+----------------+-----------------------------------------+-------------------------------+
| Hacking | NiteSec | * Install a backdoor on the avmnite-02h | | .. raw:: html
| Groups | | server | |
</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 | | | | The Black Hand | * Install a backdoor on the I.I.I.I | |
@ -86,7 +113,18 @@ List of Factions and their Requirements
| | | server | | | | | 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 | | | | | the Corporation | |
+ +----------------+-----------------------------------------+-------------------------------+ + +----------------+-----------------------------------------+-------------------------------+
| | MegaCorp | * Have 200k reputation with | | | | MegaCorp | * Have 200k reputation with | |
@ -118,8 +156,19 @@ List of Factions and their Requirements
| | | * Install a backdoor on the | | | | | * Install a backdoor on the | |
| | | fulcrumassets server | | | | | fulcrumassets server | |
+---------------------+----------------+-----------------------------------------+-------------------------------+ +---------------------+----------------+-----------------------------------------+-------------------------------+
| Criminal | Slum Snakes | * All Combat Stats of 30 | | .. raw:: html
| Organizations | | * -9 Karma | |
</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 | | | | | * $1m | |
+ +----------------+-----------------------------------------+-------------------------------+ + +----------------+-----------------------------------------+-------------------------------+
| | Tetrads | * Be in Chongqing, New Tokyo, or Ishima | | | | Tetrads | * Be in Chongqing, New Tokyo, or Ishima | |
@ -150,8 +199,19 @@ List of Factions and their Requirements
| | | * -90 Karma | | | | | * -90 Karma | |
| | | * Not working for CIA or NSA | | | | | * Not working for CIA or NSA | |
+---------------------+----------------+-----------------------------------------+-------------------------------+ +---------------------+----------------+-----------------------------------------+-------------------------------+
| Endgame | The Covenant | * 20 Augmentations | | .. raw:: html
| Factions | | * $75b | |
</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 | | | | | * Hacking Level of 850 | |
| | | * All Combat Stats of 850 | | | | | * All Combat Stats of 850 | |
+ +----------------+-----------------------------------------+-------------------------------+ + +----------------+-----------------------------------------+-------------------------------+
@ -165,3 +225,6 @@ List of Factions and their Requirements
| | | * Hacking Level of 1500 | | | | | * Hacking Level of 1500 | |
| | | * All Combat Stats of 1200 | | | | | * 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 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 // This is backward but the game fills in an array called `document.achievements` and we retrieve it from
// here. Hey if it works it works. // here. Hey if it works it works.
const steamAchievements = greenworks.getAchievementNames(); const steamAchievements = greenworks.getAchievementNames();
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); const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter(name => !!name);
log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`); log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`);
const intervalID = setInterval(async () => { const intervalID = setInterval(async () => {

@ -12,11 +12,13 @@ async function initialize(win) {
window = win; window = win;
server = http.createServer(async function (req, res) { server = http.createServer(async function (req, res) {
let body = ""; let body = "";
res.setHeader('Content-Type', 'application/json');
req.on("data", (chunk) => { req.on("data", (chunk) => {
body += chunk.toString(); // convert Buffer to string body += chunk.toString(); // convert Buffer to string
}); });
req.on("end", () => {
req.on("end", async () => {
const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? ''; const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? '';
const isValid = providedToken === getAuthenticationToken(); const isValid = providedToken === getAuthenticationToken();
if (isValid) { if (isValid) {
@ -24,8 +26,11 @@ async function initialize(win) {
} else { } else {
log.log('Invalid authentication token'); log.log('Invalid authentication token');
res.writeHead(401); res.writeHead(401);
res.write('Invalid authentication token');
res.end(); res.end(JSON.stringify({
success: false,
msg: 'Invalid authentication token'
}));
return; return;
} }
@ -35,17 +40,56 @@ async function initialize(win) {
} catch (error) { } catch (error) {
log.warn(`Invalid body data`); log.warn(`Invalid body data`);
res.writeHead(400); res.writeHead(400);
res.write('Invalid body data'); res.end(JSON.stringify({
res.end(); success: false,
msg: 'Invalid body data'
}));
return; return;
} }
if (data) { let result;
window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`).then((result) => { switch(req.method) {
res.write(result); // Request files
res.end(); 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 cp = require("child_process");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const { windowTracker } = require("./windowTracker");
const { fileURLToPath } = require("url"); const { fileURLToPath } = require("url");
const debug = process.argv.includes("--debug"); const debug = process.argv.includes("--debug");
async function createWindow(killall) { async function createWindow(killall) {
const setStopProcessHandler = global.app_handlers.stopProcess; 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({ const window = new BrowserWindow({
icon,
show: false, show: false,
backgroundThrottling: false, backgroundThrottling: false,
backgroundColor: "#000000", 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.removeMenu();
window.maximize();
noScripts = killall ? { query: { noScripts: killall } } : {}; noScripts = killall ? { query: { noScripts: killall } } : {};
window.loadFile("index.html", noScripts); window.loadFile("index.html", noScripts);
window.show(); window.show();

@ -1,12 +1,19 @@
/* eslint-disable no-process-exit */ /* eslint-disable no-process-exit */
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
const { app, dialog, BrowserWindow } = require("electron"); const { app, dialog, BrowserWindow, ipcMain } = require("electron");
const log = require("electron-log"); const log = require("electron-log");
const greenworks = require("./greenworks"); const greenworks = require("./greenworks");
const api = require("./api-server"); const api = require("./api-server");
const gameWindow = require("./gameWindow"); const gameWindow = require("./gameWindow");
const achievements = require("./achievements"); const achievements = require("./achievements");
const utils = require("./utils"); 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.catchErrors();
log.info(`Started app: ${JSON.stringify(process.argv)}`); log.info(`Started app: ${JSON.stringify(process.argv)}`);
@ -30,6 +37,8 @@ try {
global.greenworksError = ex.message; global.greenworksError = ex.message;
} }
let isRestoreDisabled = false;
function setStopProcessHandler(app, window, enabled) { function setStopProcessHandler(app, window, enabled) {
const closingWindowHandler = async (e) => { const closingWindowHandler = async (e) => {
// We need to prevent the default closing event to add custom logic // 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 // Shutdown the http server
api.disable(); 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, // 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. // 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. // So we'll alert the player to close their browser.
@ -87,21 +108,106 @@ function setStopProcessHandler(app, window, enabled) {
process.exit(0); 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) { 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("closed", clearWindowHandler);
window.on("close", closingWindowHandler) window.on("close", closingWindowHandler)
app.on("window-all-closed", stopProcessHandler); app.on("window-all-closed", stopProcessHandler);
} else { } else {
log.debug('Removing closing handlers'); log.debug("Removing closing handlers");
ipcMain.removeAllListeners();
window.removeListener("closed", clearWindowHandler); window.removeListener("closed", clearWindowHandler);
window.removeListener("close", closingWindowHandler); window.removeListener("close", closingWindowHandler);
app.removeListener("window-all-closed", stopProcessHandler); app.removeListener("window-all-closed", stopProcessHandler);
} }
} }
function startWindow(noScript) { async function startWindow(noScript) {
gameWindow.createWindow(noScript); return gameWindow.createWindow(noScript);
} }
global.app_handlers = { global.app_handlers = {
@ -110,7 +216,7 @@ global.app_handlers = {
} }
app.whenReady().then(async () => { app.whenReady().then(async () => {
log.info('Application is ready!'); log.info("Application is ready!");
if (process.argv.includes("--export-save")) { if (process.argv.includes("--export-save")) {
const window = new BrowserWindow({ show: false }); const window = new BrowserWindow({ show: false });
@ -119,15 +225,14 @@ app.whenReady().then(async () => {
setStopProcessHandler(app, window, true); setStopProcessHandler(app, window, true);
await utils.exportSave(window); await utils.exportSave(window);
} else { } else {
startWindow(process.argv.includes("--no-scripts")); const window = await startWindow(process.argv.includes("--no-scripts"));
} if (global.greenworksError) {
await dialog.showMessageBox(window, {
if (global.greenworksError) { title: "Bitburner",
dialog.showMessageBox({ message: "Could not connect to Steam",
title: 'Bitburner', detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`,
message: 'Could not connect to Steam', type: 'warning', buttons: ['OK']
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 */ /* 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 log = require("electron-log");
const Config = require("electron-config");
const api = require("./api-server"); const api = require("./api-server");
const utils = require("./utils"); const utils = require("./utils");
const storage = require("./storage");
const config = new Config();
function getMenu(window) { function getMenu(window) {
return Menu.buildFromTemplate([ 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", label: "Edit",
submenu: [ submenu: [
@ -163,6 +321,17 @@ function getMenu(window) {
label: "Activate", label: "Activate",
click: () => window.webContents.openDevTools(), 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", "version": "1.0.0",
"dependencies": { "dependencies": {
"electron-config": "^2.0.0", "electron-config": "^2.0.0",
"electron-log": "^4.4.4" "electron-log": "^4.4.4",
"lodash": "^4.17.21"
} }
}, },
"node_modules/conf": { "node_modules/conf": {
@ -104,6 +105,11 @@
"node": ">=4" "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": { "node_modules/make-dir": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
@ -259,6 +265,11 @@
"path-exists": "^3.0.0" "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": { "make-dir": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",

@ -16,6 +16,8 @@
"./dist/**/*", "./dist/**/*",
"./node_modules/**/*", "./node_modules/**/*",
"./public/**/*", "./public/**/*",
"./src/**",
"./lib/**,",
"*.js" "*.js"
], ],
"directories": { "directories": {
@ -23,6 +25,7 @@
}, },
"dependencies": { "dependencies": {
"electron-config": "^2.0.0", "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> <link rel="shortcut icon" href="favicon.ico"></head>
<body> <body>
<div id="root"/> <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> </html>

@ -8,4 +8,8 @@ module.exports = {
'.cypress', 'node_modules', 'dist', '.cypress', 'node_modules', 'dist',
], ],
testEnvironment: "jsdom", 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 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. | | [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. | | [getServerSecurityLevel(host)](./bitburner.ns.getserversecuritylevel.md) | Get server security level. |
| [getServerUsedRam(host)](./bitburner.ns.getserverusedram.md) | Get the used RAM on a server. | | [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. | | [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. | | [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. | | [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. | | [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. | | [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. | | [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. | | [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. | | [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. | | [sprintf(format, args)](./bitburner.ns.sprintf.md) | Format a string. |

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

@ -9,5 +9,5 @@ Data refers to the production, sale, and quantity of the products These values a
<b>Signature:</b> <b>Signature:</b>
```typescript ```typescript
cityData: {[key: string]:number[]}; cityData: { [key: string]: number[] };
``` ```

@ -16,7 +16,7 @@ interface Product
| Property | Type | Description | | Property | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| [cityData](./bitburner.product.citydata.md) | {\[key: string\]:number\[\]} | Data refers to the production, sale, and quantity of the products These values are specific to a city For each city, the data is \[qty, prod, sell\] | | [cityData](./bitburner.product.citydata.md) | { \[key: string\]: number\[\] } | Data refers to the production, sale, and quantity of the products These values are specific to a city For each city, the data is \[qty, prod, sell\] |
| [cmp](./bitburner.product.cmp.md) | number | Competition for the product | | [cmp](./bitburner.product.cmp.md) | number | Competition for the product |
| [developmentProgress](./bitburner.product.developmentprogress.md) | number | Creation progress - A number between 0-100 representing percentage | | [developmentProgress](./bitburner.product.developmentprogress.md) | number | Creation progress - A number between 0-100 representing percentage |
| [dmd](./bitburner.product.dmd.md) | number | Demand for the product | | [dmd](./bitburner.product.dmd.md) | number | Demand for the product |

@ -19,5 +19,5 @@ List of active fragments placed on Stanek's Gift.
## Remarks ## 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 ## Remarks
RAM cost: cost: 0.5 GB RAM cost: 0.5 GB

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

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

@ -27,5 +27,5 @@ The fragment at \[rootX, rootY\], if any.
## Remarks ## 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 ## Remarks
RAM cost: cost: 5 GB RAM cost: 5 GB

@ -27,5 +27,5 @@ The fragment at \[rootX, rootY\], if any.
## Remarks ## 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 ## Remarks
RAM cost: cost: 0 GB RAM cost: 0 GB

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

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

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

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

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

@ -24,7 +24,7 @@ void
## Remarks ## Remarks
RAM cost: cost: 0 GB RAM cost: 0 GB
## Example ## 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/react": "^11.4.1",
"@emotion/styled": "^11.3.0", "@emotion/styled": "^11.3.0",
"@material-ui/core": "^4.12.3", "@material-ui/core": "^4.12.3",
"@microsoft/api-documenter": "^7.13.65",
"@microsoft/api-extractor": "^7.18.17",
"@monaco-editor/react": "^4.2.2", "@monaco-editor/react": "^4.2.2",
"@mui/icons-material": "^5.0.3", "@mui/icons-material": "^5.0.3",
"@mui/material": "^5.0.3", "@mui/material": "^5.0.3",
"@mui/styles": "^5.0.1", "@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": "^8.4.1",
"acorn-walk": "^8.1.1", "acorn-walk": "^8.1.1",
"arg": "^5.0.0", "arg": "^5.0.0",
@ -45,7 +36,6 @@
"notistack": "^2.0.2", "notistack": "^2.0.2",
"numeral": "2.0.6", "numeral": "2.0.6",
"prop-types": "^15.8.0", "prop-types": "^15.8.0",
"raw-loader": "^4.0.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
@ -59,10 +49,19 @@
"@babel/preset-env": "^7.15.0", "@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.15.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", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
"@testing-library/cypress": "^8.0.1", "@testing-library/cypress": "^8.0.1",
"@types/acorn": "^4.0.6",
"@types/escodegen": "^0.0.7",
"@types/file-saver": "^2.0.3", "@types/file-saver": "^2.0.3",
"@types/lodash": "^4.14.168", "@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/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0", "@typescript-eslint/parser": "^4.22.0",
"babel-jest": "^27.0.6", "babel-jest": "^27.0.6",
@ -71,6 +70,7 @@
"electron": "^14.0.2", "electron": "^14.0.2",
"electron-packager": "^15.4.0", "electron-packager": "^15.4.0",
"eslint": "^7.24.0", "eslint": "^7.24.0",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^6.3.3", "fork-ts-checker-webpack-plugin": "^6.3.3",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"http-server": "^13.0.1", "http-server": "^13.0.1",
@ -79,6 +79,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mini-css-extract-plugin": "^0.4.1", "mini-css-extract-plugin": "^0.4.1",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"raw-loader": "^4.0.2",
"react-refresh": "^0.10.0", "react-refresh": "^0.10.0",
"regenerator-runtime": "^0.13.9", "regenerator-runtime": "^0.13.9",
"source-map": "^0.7.3", "source-map": "^0.7.3",
@ -102,7 +103,7 @@
"cy:dev": "start-server-and-test start:dev http://localhost:8000 cy:open", "cy:dev": "start-server-and-test start:dev http://localhost:8000 cy:open",
"cy:open": "cypress open", "cy:open": "cypress open",
"cy:run": "cypress run", "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 .", "format": "prettier --write .",
"start": "http-server -p 8000", "start": "http-server -p 8000",
"start:dev": "webpack-dev-server --progress --env.devServer --mode development", "start:dev": "webpack-dev-server --progress --env.devServer --mode development",
@ -112,13 +113,17 @@
"build:dev": "webpack --mode development", "build:dev": "webpack --mode development",
"lint": "eslint --fix . --ext js,jsx,ts,tsx", "lint": "eslint --fix . --ext js,jsx,ts,tsx",
"lint:report": "eslint --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": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"watch": "webpack --watch --mode production", "watch": "webpack --watch --mode production",
"watch:dev": "webpack --watch --mode development", "watch:dev": "webpack --watch --mode development",
"electron": "sh ./package.sh", "electron": "sh ./package.sh",
"electron:packager": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png --no-prune", "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" "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 #!/bin/sh
# Clear out any files remaining from old builds
rm -rf .package
mkdir -p .package/dist/src/ThirdParty || true mkdir -p .package/dist/src/ThirdParty || true
mkdir -p .package/src/ThirdParty || true mkdir -p .package/src/ThirdParty || true
mkdir -p .package/node_modules || true mkdir -p .package/node_modules || true
@ -8,6 +11,7 @@ cp index.html .package
cp -r electron/* .package cp -r electron/* .package
cp -r dist/ext .package/dist cp -r dist/ext .package/dist
cp -r dist/icons .package/dist cp -r dist/icons .package/dist
cp -r dist/images .package/dist
# The css files # The css files
cp dist/vendor.css .package/dist cp dist/vendor.css .package/dist
@ -26,5 +30,6 @@ cd electron
npm install npm install
cd .. cd ..
BUILD_PLATFORM="${1:-"all"}"
# And finally build the app. # 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 // Defined by webpack on startup or compilation
declare let __COMMIT_HASH__: string; 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: { TRAVEL: {
...achievementData["TRAVEL"], ...achievementData["TRAVEL"],
Icon: "travel", Icon: "TRAVEL",
Condition: () => Player.city !== CityName.Sector12, Condition: () => Player.city !== CityName.Sector12,
}, },
WORKOUT: { WORKOUT: {
@ -553,7 +553,9 @@ export const achievements: IMap<Achievement> = {
...achievementData["MAX_CACHE"], ...achievementData["MAX_CACHE"],
Icon: "HASHNETCAP", Icon: "HASHNETCAP",
Visible: () => hasAccessToSF(Player, 9), 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: { SLEEVE_8: {
...achievementData["SLEEVE_8"], ...achievementData["SLEEVE_8"],

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

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

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

@ -26,7 +26,7 @@ export function InstalledAugmentations(): React.ReactElement {
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) { if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
sourceAugs.sort((aug1, aug2) => { 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 = {}; const augP: any = {};
for (const aug of Player.queuedAugmentations) { for (const aug of Player.queuedAugmentations) {
const augObj = Augmentations[aug.name]; 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; const v = augP[mult] ? augP[mult] : 1;
augP[mult] = v * augObj.mults[mult]; augP[mult] = v * augObj.mults[mult];
} }

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

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

@ -117,7 +117,7 @@ export class Action implements IAction {
// Check to make sure weights are summed properly // Check to make sure weights are summed properly
let sum = 0; let sum = 0;
for (const weight in this.weights) { for (const weight of Object.keys(this.weights)) {
if (this.weights.hasOwnProperty(weight)) { if (this.weights.hasOwnProperty(weight)) {
sum += this.weights[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.hasOwnProperty(decay)) {
if (this.decays[decay] > 1) { if (this.decays[decay] > 1) {
throw new Error( throw new Error(
@ -240,7 +240,7 @@ export class Action implements IAction {
} }
let difficulty = this.getDifficulty(); let difficulty = this.getDifficulty();
let competence = 0; let competence = 0;
for (const stat in this.weights) { for (const stat of Object.keys(this.weights)) {
if (this.weights.hasOwnProperty(stat)) { if (this.weights.hasOwnProperty(stat)) {
const playerStatLvl = Player.queryStatFromString(stat); const playerStatLvl = Player.queryStatFromString(stat);
const key = "eff" + stat.charAt(0).toUpperCase() + stat.slice(1); 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 // Can't start a BlackOp if you haven't done the one before it
const blackops = []; const blackops = [];
for (const nm in BlackOperations) { for (const nm of Object.keys(BlackOperations)) {
if (BlackOperations.hasOwnProperty(nm)) { if (BlackOperations.hasOwnProperty(nm)) {
blackops.push(nm); blackops.push(nm);
} }
@ -1074,7 +1074,7 @@ export class Bladeburner implements IBladeburner {
updateSkillMultipliers(): void { updateSkillMultipliers(): void {
this.resetSkillMultipliers(); this.resetSkillMultipliers();
for (const skillName in this.skills) { for (const skillName of Object.keys(this.skills)) {
if (this.skills.hasOwnProperty(skillName)) { if (this.skills.hasOwnProperty(skillName)) {
const skill = Skills[skillName]; const skill = Skills[skillName];
if (skill == null) { if (skill == null) {

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

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

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

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

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

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

@ -111,8 +111,8 @@ export const CONSTANTS: {
TotalNumBitNodes: number; TotalNumBitNodes: number;
LatestUpdate: string; LatestUpdate: string;
} = { } = {
VersionString: "1.3.0", VersionString: "1.4.0",
VersionNumber: 9, VersionNumber: 10,
// Speed (in ms) at which the main loop is updated // Speed (in ms) at which the main loop is updated
_idleSpeed: 200, _idleSpeed: 200,
@ -273,89 +273,22 @@ export const CONSTANTS: {
TotalNumBitNodes: 24, TotalNumBitNodes: 24,
LatestUpdate: ` 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 new mechanic has been added, it's is invoked by calling the new function 'share'.
A VSCode extension is available on the market place. (The documentation for the ext. isn't This mechanic helps you farm reputation faster.
written yet)
** Source-Files ** ** gang **
* SF4 has been reworked.
* New SF -1.
** UI ** * Installing augs means losing a little bit of ascension multipliers.
* Fix some edge case with skill bat tooltips (@MartinFournier) ** There's more but I'm going to write it later. **
* 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)
** Misc. ** ** Misc. **
* Added vim mode to script editor (@billyvg) * Nerf noodle bar.
* 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)
`, `,
}; };

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

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

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