diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ecf90a90c..f0b2e0f8e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -121,7 +121,29 @@ Inside the root of the repo run
After that you can open any browser and navigate to `localhost:8000` and play the game.
Saving a file will reload the game automatically.
-#### Submitting a Pull Request
+
+### How to build the electron app
+
+Tested on Node v16.13.1 (LTS) on Windows
+These steps only work in a bash-like environment, like MinGW for Windows.
+
+```sh
+# Install the main game dependencies & build the app in debug mode
+npm install
+npm run build:dev
+
+# Use electron-packager to build the app to the .build/ folder
+npm run electron
+
+# When launching the .exe directly, you'll need the steam_appid.txt file in the root
+# If not using windows, change this line accordingly
+cp .build/bitburner-win32-x64/resources/app/steam_appid.txt .build/bitburner-win32-x64/steam_appid.txt
+
+# And run the game...
+.build/bitburner-win32-x64/bitburner.exe
+```
+
+### Submitting a Pull Request
When submitting a pull request with your code contributions, please abide by
the following rules:
diff --git a/dist/bitburner.d.ts b/dist/bitburner.d.ts
index b1f213753..44ab57d24 100644
--- a/dist/bitburner.d.ts
+++ b/dist/bitburner.d.ts
@@ -3498,7 +3498,7 @@ export declare interface NS extends Singularity {
* Returns 0 if the script does not exist.
*
* @param script - Filename of script. This is case-sensitive.
- * @param host - Host of target server the script is located on. This is optional, If it is not specified then the function will se the current server as the target server.
+ * @param host - Host of target server the script is located on. This is optional, If it is not specified then the function will use the current server as the target server.
* @returns Amount of RAM required to run the specified script on the target server, and 0 if the script does not exist.
*/
getScriptRam(script: string, host?: string): number;
diff --git a/electron/export.html b/electron/export.html
new file mode 100644
index 000000000..dd76a842b
--- /dev/null
+++ b/electron/export.html
@@ -0,0 +1,30 @@
+
+
+
+
+ Bitburner
+
+
+
+
+
+
Close me when operation is completed.
+
+
+
diff --git a/electron/main.js b/electron/main.js
index 489f5b078..8b959d947 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -1,11 +1,12 @@
/* eslint-disable no-process-exit */
/* eslint-disable @typescript-eslint/no-var-requires */
-const { app, dialog } = require("electron");
+const { app, dialog, BrowserWindow } = require("electron");
const log = require("electron-log");
const greenworks = require("./greenworks");
const api = require("./api-server");
const gameWindow = require("./gameWindow");
const achievements = require("./achievements");
+const utils = require("./utils");
log.catchErrors();
log.info(`Started app: ${JSON.stringify(process.argv)}`);
@@ -100,7 +101,16 @@ global.app_handlers = {
createWindow: startWindow,
}
-app.whenReady().then(() => {
+app.whenReady().then(async () => {
log.info('Application is ready!');
- startWindow(process.argv.includes("--no-scripts"));
+
+ if (process.argv.includes("--export-save")) {
+ const window = new BrowserWindow({ show: false });
+ await window.loadFile("export.html", false);
+ window.show();
+ setStopProcessHandler(app, window, true);
+ await utils.exportSave(window);
+ } else {
+ startWindow(process.argv.includes("--no-scripts"));
+ }
});
diff --git a/electron/utils.js b/electron/utils.js
index 0c22632c3..8e45eba3e 100644
--- a/electron/utils.js
+++ b/electron/utils.js
@@ -62,7 +62,38 @@ function showErrorBox(title, error) {
);
}
+function exportSaveFromIndexedDb() {
+ return new Promise((resolve) => {
+ const dbRequest = indexedDB.open("bitburnerSave");
+ dbRequest.onsuccess = () => {
+ const db = dbRequest.result;
+ const transaction = db.transaction(['savestring'], "readonly");
+ const store = transaction.objectStore('savestring');
+ const request = store.get('save');
+ request.onsuccess = () => {
+ const file = new Blob([request.result], {type: 'text/plain'});
+ const a = document.createElement("a");
+ const url = URL.createObjectURL(file);
+ a.href = url;
+ a.download = 'save.json';
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(function () {
+ document.body.removeChild(a);
+ window.URL.revokeObjectURL(url);
+ resolve();
+ }, 0);
+ }
+ }
+ });
+}
+
+async function exportSave(window) {
+ await window.webContents
+ .executeJavaScript(`${exportSaveFromIndexedDb.toString()}; exportSaveFromIndexedDb();`, true);
+}
+
module.exports = {
- reloadAndKill, showErrorBox,
+ reloadAndKill, showErrorBox, exportSave,
attachUnresponsiveAppHandler, detachUnresponsiveAppHandler,
}
diff --git a/markdown/bitburner.ns.hackanalyze.md b/markdown/bitburner.ns.hackanalyze.md
index 5aa470dcb..c1adbc8f2 100644
--- a/markdown/bitburner.ns.hackanalyze.md
+++ b/markdown/bitburner.ns.hackanalyze.md
@@ -4,7 +4,7 @@
## NS.hackAnalyze() method
-Get the percent of money stolen with a single thread.
+Get the part of money stolen with a single thread.
Signature:
@@ -22,13 +22,13 @@ hackAnalyze(host: string): number;
number
-The percentage of money you will steal from the target server with a single hack.
+The part of money you will steal from the target server with a single thread hack.
## Remarks
RAM cost: 1 GB
-Returns the percentage of the specified server’s money you will steal with a single hack. This value is returned in percentage form, not decimal (Netscript functions typically return in decimal form, but not this one).
+Returns the part of the specified server’s money you will steal with a single thread hack.
## Example
@@ -36,6 +36,6 @@ Returns the percentage of the specified server’s money you will steal with a s
```ts
//For example, assume the following returns 0.01:
hackAnalyze("foodnstuff");
-//This means that if hack the foodnstuff server, then you will steal 1% of its total money. If you hack using N threads, then you will steal N*0.01 times its total money.
+//This means that if hack the foodnstuff server using a single thread, then you will steal 1%, or 0.01 of its total money. If you hack using N threads, then you will steal N*0.01 times its total money.
```
diff --git a/markdown/bitburner.ns.md b/markdown/bitburner.ns.md
index 41ab69aa4..88fb50336 100644
--- a/markdown/bitburner.ns.md
+++ b/markdown/bitburner.ns.md
@@ -111,7 +111,7 @@ export async function main(ns) {
| [growthAnalyze(host, growthAmount, cores)](./bitburner.ns.growthanalyze.md) | Calculate the number of grow thread needed to grow a server by a certain multiplier. |
| [growthAnalyzeSecurity(threads)](./bitburner.ns.growthanalyzesecurity.md) | Calculate the security increase for a number of thread. |
| [hack(host, opts)](./bitburner.ns.hack.md) | Steal a servers money. |
-| [hackAnalyze(host)](./bitburner.ns.hackanalyze.md) | Get the percent of money stolen with a single thread. |
+| [hackAnalyze(host)](./bitburner.ns.hackanalyze.md) | Get the part of money stolen with a single thread. |
| [hackAnalyzeChance(host)](./bitburner.ns.hackanalyzechance.md) | Get the chance of successfully hacking a server. |
| [hackAnalyzeSecurity(threads)](./bitburner.ns.hackanalyzesecurity.md) | Get the security increase for a number of thread. |
| [hackAnalyzeThreads(host, hackAmount)](./bitburner.ns.hackanalyzethreads.md) | Predict the effect of hack. |
diff --git a/markdown/bitburner.singularity.commitcrime.md b/markdown/bitburner.singularity.commitcrime.md
index 3f5d969e8..7d62e41f0 100644
--- a/markdown/bitburner.singularity.commitcrime.md
+++ b/markdown/bitburner.singularity.commitcrime.md
@@ -30,7 +30,7 @@ RAM cost: 5 GB
This function is used to automatically attempt to commit crimes. If you are already in the middle of some ‘working’ action (such as working for a company or training at a gym), then running this function will automatically cancel that action and give you your earnings.
-This function returns the number of seconds it takes to attempt the specified crime (e.g It takes 60 seconds to attempt the ‘Rob Store’ crime, so running `commitCrime('rob store')` will return 60).
+This function returns the number of milliseconds it takes to attempt the specified crime (e.g It takes 60 seconds to attempt the ‘Rob Store’ crime, so running `commitCrime('rob store')` will return 60,000).
Warning: I do not recommend using the time returned from this function to try and schedule your crime attempts. Instead, I would use the isBusy Singularity function to check whether you have finished attempting a crime. This is because although the game sets a certain crime to be X amount of seconds, there is no guarantee that your browser will follow that time limit.
diff --git a/package.sh b/package.sh
index 1783f3475..cf8b13847 100755
--- a/package.sh
+++ b/package.sh
@@ -16,4 +16,10 @@ cp main.css .package/main.css
cp dist/vendor.bundle.js .package/dist/vendor.bundle.js
cp main.bundle.js .package/main.bundle.js
+# Install electron sub-dependencies
+cd electron
+npm install
+cd ..
+
+# And finally build the app.
npm run electron:packager
diff --git a/src/Alias.ts b/src/Alias.ts
index 238c531e0..0ccc7c27c 100644
--- a/src/Alias.ts
+++ b/src/Alias.ts
@@ -38,13 +38,14 @@ export function printAliases(): void {
export function parseAliasDeclaration(dec: string, global = false): boolean {
const re = /^([\w|!|%|,|@|-]+)=(("(.+)")|('(.+)'))$/;
const matches = dec.match(re);
- if (matches == null || matches.length != 3) {
+ if (matches == null || matches.length != 7) {
return false;
}
+
if (global) {
- addGlobalAlias(matches[1], matches[2]);
+ addGlobalAlias(matches[1], matches[4] || matches[6]);
} else {
- addAlias(matches[1], matches[2]);
+ addAlias(matches[1], matches[4] || matches[6]);
}
return true;
}
diff --git a/src/Augmentation/AugmentationHelpers.tsx b/src/Augmentation/AugmentationHelpers.tsx
index 8522bb22a..2d053a220 100644
--- a/src/Augmentation/AugmentationHelpers.tsx
+++ b/src/Augmentation/AugmentationHelpers.tsx
@@ -1925,7 +1925,7 @@ function initAugmentations(): void {
repCost: 7.5e3,
moneyCost: 3e7,
info:
- "A tiny chip that sits behind the retinae. This implant lets the" + "user visually detect infrared radiation.",
+ "A tiny chip that sits behind the retinae. This implant lets the user visually detect infrared radiation.",
crime_success_mult: 1.25,
crime_money_mult: 1.1,
dexterity_mult: 1.1,
diff --git a/src/Corporation/ui/IndustryWarehouse.tsx b/src/Corporation/ui/IndustryWarehouse.tsx
index 2bb035d03..5a786fada 100644
--- a/src/Corporation/ui/IndustryWarehouse.tsx
+++ b/src/Corporation/ui/IndustryWarehouse.tsx
@@ -108,31 +108,22 @@ function WarehouseRoot(props: IProps): React.ReactElement {
}
}
- let breakdown = <>>;
+ const breakdownItems: string[] = [];
for (const matName in props.warehouse.materials) {
const mat = props.warehouse.materials[matName];
if (!MaterialSizes.hasOwnProperty(matName)) continue;
if (mat.qty === 0) continue;
- breakdown = (
- <>
- {breakdown}
- {matName}: {numeralWrapper.format(mat.qty * MaterialSizes[matName], "0,0.0")}
-
- >
- );
+ breakdownItems.push(`${matName}: ${numeralWrapper.format(mat.qty * MaterialSizes[matName], "0,0.0")}`);
}
for (const prodName in division.products) {
const prod = division.products[prodName];
if (prod === undefined) continue;
- breakdown = (
- <>
- {breakdown}
- {prodName}: {numeralWrapper.format(prod.data[props.warehouse.loc][0] * prod.siz, "0,0.0")}
- >
- );
+ breakdownItems.push(`${prodName}: ${numeralWrapper.format(prod.data[props.warehouse.loc][0] * prod.siz, "0,0.0")}`);
}
+ const breakdown = <>{breakdownItems.join(' ')}>
+
return (
diff --git a/src/Locations/ui/GenericLocation.tsx b/src/Locations/ui/GenericLocation.tsx
index d91d03c68..1c927d0ed 100644
--- a/src/Locations/ui/GenericLocation.tsx
+++ b/src/Locations/ui/GenericLocation.tsx
@@ -29,6 +29,7 @@ import { GetServer } from "../../Server/AllServers";
import { CorruptableText } from "../../ui/React/CorruptableText";
import { use } from "../../ui/Context";
import { serverMetadata } from "../../Server/data/servers";
+import { Tooltip } from "@mui/material";
type IProps = {
loc: Location;
@@ -92,8 +93,11 @@ export function GenericLocation({ loc }: IProps): React.ReactElement {
return (
<>
-
- {backdoorInstalled && !Settings.DisableTextEffects ? : loc.name}
+
+ {backdoorInstalled && !Settings.DisableTextEffects ? (
+
+
+ ) : loc.name}
{locContent}
>
diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts
index 5ffbce6c4..b52f1b83c 100644
--- a/src/NetscriptFunctions.ts
+++ b/src/NetscriptFunctions.ts
@@ -1009,7 +1009,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
workerScript.log("spawn", () => "Exiting...");
}
},
- kill: function (filename: any, hostname: any, ...scriptArgs: any): any {
+ kill: function (filename: any, hostname?: any, ...scriptArgs: any): any {
updateDynamicRam("kill", getRamCost("kill"));
let res;
@@ -2025,7 +2025,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return calculateWeakenTime(server, Player) * 1000;
},
- getScriptIncome: function (scriptname: any, hostname: any, ...args: any[]): any {
+ getScriptIncome: function (scriptname?: any, hostname?: any, ...args: any[]): any {
updateDynamicRam("getScriptIncome", getRamCost("getScriptIncome"));
if (arguments.length === 0) {
const res = [];
@@ -2054,7 +2054,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return runningScriptObj.onlineMoneyMade / runningScriptObj.onlineRunningTime;
}
},
- getScriptExpGain: function (scriptname: any, hostname: any, ...args: any[]): any {
+ getScriptExpGain: function (scriptname?: any, hostname?: any, ...args: any[]): any {
updateDynamicRam("getScriptExpGain", getRamCost("getScriptExpGain"));
if (arguments.length === 0) {
let total = 0;
diff --git a/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx b/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx
index e3723daea..3b77c2f7b 100644
--- a/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx
+++ b/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx
@@ -1690,6 +1690,11 @@ export function applyForJob(this: IPlayer, entryPosType: CompanyPosition, sing =
return false;
}
+ // Check if this company has the position
+ if (!company.hasPosition(pos)) {
+ return false;
+ }
+
while (true) {
const newPos = getNextCompanyPositionHelper(pos);
if (newPos == null) {
@@ -1863,9 +1868,14 @@ export function applyForAgentJob(this: IPlayer, sing = false): boolean {
export function applyForEmployeeJob(this: IPlayer, sing = false): boolean {
const company = Companies[this.location]; //Company being applied to
- if (this.isQualified(company, CompanyPositions[posNames.MiscCompanyPositions[1]])) {
+ const position = posNames.MiscCompanyPositions[1];
+ // Check if this company has the position
+ if (!company.hasPosition(position)) {
+ return false;
+ }
+ if (this.isQualified(company, CompanyPositions[position])) {
this.companyName = company.name;
- this.jobs[company.name] = posNames.MiscCompanyPositions[1];
+ this.jobs[company.name] = position;
if (!sing) {
dialogBoxCreate("Congratulations, you are now employed at " + this.location);
}
@@ -1882,8 +1892,13 @@ export function applyForEmployeeJob(this: IPlayer, sing = false): boolean {
export function applyForPartTimeEmployeeJob(this: IPlayer, sing = false): boolean {
const company = Companies[this.location]; //Company being applied to
- if (this.isQualified(company, CompanyPositions[posNames.PartTimeCompanyPositions[1]])) {
- this.jobs[company.name] = posNames.PartTimeCompanyPositions[1];
+ const position = posNames.PartTimeCompanyPositions[1];
+ // Check if this company has the position
+ if (!company.hasPosition(position)) {
+ return false;
+ }
+ if (this.isQualified(company, CompanyPositions[position])) {
+ this.jobs[company.name] = position;
if (!sing) {
dialogBoxCreate("Congratulations, you are now employed part-time at " + this.location);
}
@@ -1900,9 +1915,14 @@ export function applyForPartTimeEmployeeJob(this: IPlayer, sing = false): boolea
export function applyForWaiterJob(this: IPlayer, sing = false): boolean {
const company = Companies[this.location]; //Company being applied to
- if (this.isQualified(company, CompanyPositions[posNames.MiscCompanyPositions[0]])) {
+ const position = posNames.MiscCompanyPositions[0];
+ // Check if this company has the position
+ if (!company.hasPosition(position)) {
+ return false;
+ }
+ if (this.isQualified(company, CompanyPositions[position])) {
this.companyName = company.name;
- this.jobs[company.name] = posNames.MiscCompanyPositions[0];
+ this.jobs[company.name] = position;
if (!sing) {
dialogBoxCreate("Congratulations, you are now employed as a waiter at " + this.location);
}
@@ -1917,9 +1937,14 @@ export function applyForWaiterJob(this: IPlayer, sing = false): boolean {
export function applyForPartTimeWaiterJob(this: IPlayer, sing = false): boolean {
const company = Companies[this.location]; //Company being applied to
- if (this.isQualified(company, CompanyPositions[posNames.PartTimeCompanyPositions[0]])) {
+ const position = posNames.PartTimeCompanyPositions[0];
+ // Check if this company has the position
+ if (!company.hasPosition(position)) {
+ return false;
+ }
+ if (this.isQualified(company, CompanyPositions[position])) {
this.companyName = company.name;
- this.jobs[company.name] = posNames.PartTimeCompanyPositions[0];
+ this.jobs[company.name] = position;
if (!sing) {
dialogBoxCreate("Congratulations, you are now employed as a part-time waiter at " + this.location);
}
diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts
index 01a405a47..cfc02ffe9 100644
--- a/src/ScriptEditor/NetscriptDefinitions.d.ts
+++ b/src/ScriptEditor/NetscriptDefinitions.d.ts
@@ -1767,7 +1767,7 @@ export interface Singularity {
*
* This function returns the number of milliseconds it takes to attempt the
* specified crime (e.g It takes 60 seconds to attempt the ‘Rob Store’ crime,
- * so running `commitCrime('rob store')` will return 60000).
+ * so running `commitCrime('rob store')` will return 60,000).
*
* Warning: I do not recommend using the time returned from this function to try
* and schedule your crime attempts. Instead, I would use the isBusy Singularity
@@ -4680,8 +4680,9 @@ export interface NS extends Singularity {
* @param args - Arguments to identify which script to kill.
* @returns True if the script is successfully killed, and false otherwise.
*/
- kill(script: string | number, host: string, ...args: string[]): boolean;
-
+ kill(script: number): boolean;
+ kill(script: string, host: string, ...args: string[]): boolean;
+
/**
* Terminate all scripts on a server.
* @remarks
@@ -5543,7 +5544,8 @@ export interface NS extends Singularity {
* @param args - Arguments that the script is running with.
* @returns Amount of income the specified script generates while online.
*/
- getScriptIncome(script: string, host: string, ...args: string[]): number | [number, number];
+ getScriptIncome(): [number, number];
+ getScriptIncome(script: string, host: string, ...args: string[]): number;
/**
* Get the exp gain of a script.
@@ -5562,6 +5564,7 @@ export interface NS extends Singularity {
* @param args - Arguments that the script is running with.
* @returns Amount of hacking experience the specified script generates while online.
*/
+ getScriptExpGain(): number;
getScriptExpGain(script: string, host: string, ...args: string[]): number;
/**
diff --git a/src/ScriptEditor/ui/ScriptEditorRoot.tsx b/src/ScriptEditor/ui/ScriptEditorRoot.tsx
index b2cc86304..944d65943 100644
--- a/src/ScriptEditor/ui/ScriptEditorRoot.tsx
+++ b/src/ScriptEditor/ui/ScriptEditorRoot.tsx
@@ -178,7 +178,12 @@ export function Root(props: IProps): React.ReactElement {
save();
});
MonacoVim.VimMode.Vim.defineEx("quit", "q", function () {
+ props.router.toTerminal();
+ });
+ // "wqriteandquit" is not a typo, prefix must be found in full string
+ MonacoVim.VimMode.Vim.defineEx("wqriteandquit", "wq", function () {
save();
+ props.router.toTerminal();
});
editor.focus();
});
diff --git a/src/Settings/Settings.ts b/src/Settings/Settings.ts
index 3654a87a5..09ff446c9 100644
--- a/src/Settings/Settings.ts
+++ b/src/Settings/Settings.ts
@@ -113,6 +113,11 @@ interface IDefaultSettings {
* Theme colors
*/
theme: ITheme;
+
+ /*
+ * Use GiB instead of GB
+ */
+ UseIEC60027_2: boolean;
}
/**
@@ -160,6 +165,7 @@ export const defaultSettings: IDefaultSettings = {
SuppressBladeburnerPopup: false,
SuppressTIXPopup: false,
SuppressSavedGameToast: false,
+ UseIEC60027_2: false,
theme: defaultTheme,
};
@@ -192,6 +198,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
SuppressBladeburnerPopup: defaultSettings.SuppressBladeburnerPopup,
SuppressTIXPopup: defaultSettings.SuppressTIXPopup,
SuppressSavedGameToast: defaultSettings.SuppressSavedGameToast,
+ UseIEC60027_2: defaultSettings.UseIEC60027_2,
MonacoTheme: "monokai",
MonacoInsertSpaces: false,
MonacoFontSize: 20,
diff --git a/src/Settings/Themes.ts b/src/Settings/Themes.ts
index 873b55565..eed026a57 100644
--- a/src/Settings/Themes.ts
+++ b/src/Settings/Themes.ts
@@ -408,4 +408,206 @@ export const getPredefinedThemes = (): IMap => ({
button: "#000000",
},
},
+
+ Discord: {
+ credit: "Thermite",
+ description: "Discord inspired theme",
+ reference: "https://discord.com/channels/415207508303544321/921991895230611466/924305252017143818",
+ colors: {
+ primarylight: "#7389DC",
+ primary: "#7389DC",
+ primarydark: "#5964F1",
+ successlight: "#00CC00",
+ success: "#20DF20",
+ successdark: "#0CB80C",
+ errorlight: "#EA5558",
+ error: "#EC4145",
+ errordark: "#E82528",
+ secondarylight: "#C3C3C3",
+ secondary: "#9C9C9C",
+ secondarydark: "#4E4E4E",
+ warninglight: "#ff0",
+ warning: "#cc0",
+ warningdark: "#990",
+ infolight: "#69f",
+ info: "#36c",
+ infodark: "#1C4FB3",
+ welllight: "#999999",
+ well: "#35383C",
+ white: "#FFFFFF",
+ black: "#202225",
+ hp: "#FF5656",
+ money: "#43FF43",
+ hack: "#FFAB3D",
+ combat: "#8A90FD",
+ cha: "#FF51D9",
+ int: "#6495ed",
+ rep: "#FFFF30",
+ disabled: "#474B51",
+ backgroundprimary: "#2F3136",
+ backgroundsecondary: "#35393E",
+ button: "#333",
+ },
+ },
+
+ "One Dark": {
+ credit: "Dexalt142",
+ reference: "https://discord.com/channels/415207508303544321/921991895230611466/924650660694208512",
+ colors: {
+ primarylight: "#98C379",
+ primary: "#98C379",
+ primarydark: "#98C379",
+ successlight: "#98C379",
+ success: "#98C379",
+ successdark: "#98C379",
+ errorlight: "#E06C75",
+ error: "#BE5046",
+ errordark: "#BE5046",
+ secondarylight: "#AAA",
+ secondary: "#888",
+ secondarydark: "#666",
+ warninglight: "#E5C07B",
+ warning: "#E5C07B",
+ warningdark: "#D19A66",
+ infolight: "#61AFEF",
+ info: "#61AFEF",
+ infodark: "#61AFEF",
+ welllight: "#4B5263",
+ well: "#282C34",
+ white: "#ABB2BF",
+ black: "#282C34",
+ hp: "#E06C75",
+ money: "#E5C07B",
+ hack: "#98C379",
+ combat: "#ABB2BF",
+ cha: "#C678DD",
+ int: "#61AFEF",
+ rep: "#ABB2BF",
+ disabled: "#56B6C2",
+ backgroundprimary: "#282C34",
+ backgroundsecondary: "#21252B",
+ button: "#4B5263",
+ },
+ },
+
+ "Muted Gold & Blue": {
+ credit: "Sloth",
+ reference: "https://discord.com/channels/415207508303544321/921991895230611466/924672660758208563",
+ colors: {
+ primarylight: "#E3B54A",
+ primary: "#CAA243",
+ primarydark: "#7E6937",
+ successlight: "#82FF82",
+ success: "#6FDA6F",
+ successdark: "#64C364",
+ errorlight: "#FD5555",
+ error: "#D84A4A",
+ errordark: "#AC3939",
+ secondarylight: "#D8D0B8",
+ secondary: "#B1AA95",
+ secondarydark: "#736E5E",
+ warninglight: "#ff0",
+ warning: "#cc0",
+ warningdark: "#990",
+ infolight: "#69f",
+ info: "#36c",
+ infodark: "#039",
+ welllight: "#444",
+ well: "#111111",
+ white: "#fff",
+ black: "#070300",
+ hp: "#dd3434",
+ money: "#ffd700",
+ hack: "#adff2f",
+ combat: "#faffdf",
+ cha: "#a671d1",
+ int: "#6495ed",
+ rep: "#faffdf",
+ disabled: "#66cfbc",
+ backgroundprimary: "#0A0A0E",
+ backgroundsecondary: "#0E0E10",
+ button: "#222222",
+ },
+ },
+
+ "Default Lite": {
+ credit: "NmuGmu",
+ description: "Less eye-straining default theme",
+ reference: "https://discord.com/channels/415207508303544321/921991895230611466/925263801564151888",
+ colors: {
+ primarylight: "#28CF28",
+ primary: "#21A821",
+ primarydark: "#177317",
+ successlight: "#1CFF1C",
+ success: "#16CA16",
+ successdark: "#0D910D",
+ errorlight: "#FF3B3B",
+ error: "#C32D2D",
+ errordark: "#8E2121",
+ secondarylight: "#B3B3B3",
+ secondary: "#838383",
+ secondarydark: "#676767",
+ warninglight: "#FFFF3A",
+ warning: "#C3C32A",
+ warningdark: "#8C8C1E",
+ infolight: "#64CBFF",
+ info: "#3399CC",
+ infodark: "#246D91",
+ welllight: "#404040",
+ well: "#1C1C1C",
+ white: "#C3C3C3",
+ black: "#0A0B0B",
+ hp: "#C62E2E",
+ money: "#D6BB27",
+ hack: "#ADFF2F",
+ combat: "#E8EDCD",
+ cha: "#8B5FAF",
+ int: "#537CC8",
+ rep: "#E8EDCD",
+ disabled: "#5AB5A5",
+ backgroundprimary: "#0C0D0E",
+ backgroundsecondary: "#121415",
+ button: "#252829",
+ },
+ },
+
+ Light: {
+ credit: "matt",
+ reference: "https://discord.com/channels/415207508303544321/921991895230611466/926114005456658432",
+ colors: {
+ primarylight: "#535353",
+ primary: "#1A1A1A",
+ primarydark: "#0d0d0d",
+ successlight: "#63c439",
+ success: "#428226",
+ successdark: "#2E5A1B",
+ errorlight: "#df7051",
+ error: "#C94824",
+ errordark: "#91341B",
+ secondarylight: "#b3b3b3",
+ secondary: "#9B9B9B",
+ secondarydark: "#7A7979",
+ warninglight: "#e8d464",
+ warning: "#C6AD20",
+ warningdark: "#9F8A16",
+ infolight: "#6299cf",
+ info: "#3778B7",
+ infodark: "#30689C",
+ welllight: "#f9f9f9",
+ well: "#eaeaea",
+ white: "#F7F7F7",
+ black: "#F7F7F7",
+ hp: "#BF5C41",
+ money: "#E1B121",
+ hack: "#47BC38",
+ combat: "#656262",
+ cha: "#A568AC",
+ int: "#889BCF",
+ rep: "#656262",
+ disabled: "#70B4BF",
+ backgroundprimary: "#F7F7F7",
+ backgroundsecondary: "#f9f9f9",
+ button: "#eaeaea",
+ },
+ },
});
diff --git a/src/StockMarket/data/StockSymbols.ts b/src/StockMarket/data/StockSymbols.ts
index 773174282..707f47ca6 100644
--- a/src/StockMarket/data/StockSymbols.ts
+++ b/src/StockMarket/data/StockSymbols.ts
@@ -32,10 +32,10 @@ StockSymbols[LocationName.VolhavenCompuTek] = "CTK";
StockSymbols[LocationName.AevumNetLinkTechnologies] = "NTLK";
StockSymbols[LocationName.IshimaOmegaSoftware] = "OMGA";
StockSymbols[LocationName.Sector12FoodNStuff] = "FNS";
+StockSymbols[LocationName.Sector12JoesGuns] = "JGN";
// Stocks for other companies
StockSymbols["Sigma Cosmetics"] = "SGC";
-StockSymbols["Joes Guns"] = "JGN";
StockSymbols["Catalyst Ventures"] = "CTYS";
StockSymbols["Microdyne Technologies"] = "MDYN";
StockSymbols["Titan Laboratories"] = "TITN";
diff --git a/src/StockMarket/ui/InfoAndPurchases.tsx b/src/StockMarket/ui/InfoAndPurchases.tsx
index 03516e115..12a57aa56 100644
--- a/src/StockMarket/ui/InfoAndPurchases.tsx
+++ b/src/StockMarket/ui/InfoAndPurchases.tsx
@@ -176,7 +176,7 @@ export function InfoAndPurchases(props: IProps): React.ReactElement {
<>
Welcome to the World Stock Exchange (WSE)!
- Investopedia
+ Investopedia
diff --git a/src/ui/React/GameOptionsRoot.tsx b/src/ui/React/GameOptionsRoot.tsx
index 24443de42..73b8a364e 100644
--- a/src/ui/React/GameOptionsRoot.tsx
+++ b/src/ui/React/GameOptionsRoot.tsx
@@ -28,6 +28,7 @@ import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
import { dialogBoxCreate } from "./DialogBox";
import { ConfirmationModal } from "./ConfirmationModal";
import { ThemeEditorModal } from "./ThemeEditorModal";
+import { SnackbarEvents } from "./Snackbar";
import { Settings } from "../../Settings/Settings";
import { save, deleteGame } from "../../db";
@@ -51,6 +52,12 @@ interface IProps {
softReset: () => void;
}
+interface ImportData {
+ base64: string;
+ parsed: any;
+ exportDate?: Date;
+}
+
export function GameOptionsRoot(props: IProps): React.ReactElement {
const classes = useStyles();
const importInput = useRef(null);
@@ -78,12 +85,15 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
const [enableBashHotkeys, setEnableBashHotkeys] = useState(Settings.EnableBashHotkeys);
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
const [saveGameOnFileSave, setSaveGameOnFileSave] = useState(Settings.SaveGameOnFileSave);
+ const [useIEC60027_2, setUseIEC60027_2] = useState(Settings.UseIEC60027_2);
const [locale, setLocale] = useState(Settings.Locale);
const [diagnosticOpen, setDiagnosticOpen] = useState(false);
const [deleteGameOpen, setDeleteOpen] = useState(false);
const [themeEditorOpen, setThemeEditorOpen] = useState(false);
const [softResetOpen, setSoftResetOpen] = useState(false);
+ const [importSaveOpen, setImportSaveOpen] = useState(false);
+ const [importData, setImportData] = useState(null);
function handleExecTimeChange(event: any, newValue: number | number[]): void {
setExecTime(newValue as number);
@@ -154,6 +164,10 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
setDisableASCIIArt(event.target.checked);
Settings.DisableASCIIArt = event.target.checked;
}
+ function handleUseIEC60027_2Change(event: React.ChangeEvent): void {
+ setUseIEC60027_2(event.target.checked);
+ Settings.UseIEC60027_2 = event.target.checked;
+ }
function handleDisableTextEffectsChange(event: React.ChangeEvent): void {
setDisableTextEffects(event.target.checked);
@@ -206,11 +220,67 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
return;
}
const contents = result;
- save(contents).then(() => setTimeout(() => location.reload(), 1000));
+
+ // https://stackoverflow.com/a/35002237
+ const base64regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
+ if (!base64regex.test(contents)) {
+ SnackbarEvents.emit("Save game was not a base64 string", "error", 5000);
+ return;
+ }
+
+ let newSave;
+ try {
+ newSave = window.atob(contents);
+ newSave = newSave.trim();
+ } catch (error) {
+ console.log(error); // We'll handle below
+ }
+
+ if (!newSave || newSave === '') {
+ SnackbarEvents.emit("Save game had not content", "error", 5000);
+ return;
+ }
+
+ let parsedSave;
+ try {
+ parsedSave = JSON.parse(newSave);
+ } catch (error) {
+ console.log(error); // We'll handle below
+ }
+
+ if (!parsedSave || parsedSave.ctor !== 'BitburnerSaveObject' || !parsedSave.data) {
+ SnackbarEvents.emit("Save game did not seem valid", "error", 5000);
+ return;
+ }
+
+
+ const data: ImportData = {
+ base64: contents,
+ parsed: parsedSave,
+ }
+
+ // We don't always seem to have this value in the save file. Exporting from the option menu does not set the bonus I think.
+ const exportTimestamp = parsedSave.data.LastExportBonus;
+ if (exportTimestamp && exportTimestamp !== '0') {
+ data.exportDate = new Date(parseInt(exportTimestamp, 10))
+ }
+
+ setImportData(data)
+ setImportSaveOpen(true);
};
reader.readAsText(file);
}
+ function confirmedImportGame(): void {
+ if (!importData) return;
+
+ setImportSaveOpen(false);
+ save(importData.base64).then(() => {
+ setImportData(null);
+ setTimeout(() => location.reload(), 1000)
+ });
+ }
+
function doSoftReset(): void {
if (!Settings.SuppressBuyAugmentationConfirmation) {
setSoftResetOpen(true);
@@ -513,6 +583,16 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
}
/>
+
+ }
+ label={
+ If this is set all references to memory will use GiB instead of GB, in accordance with IEC 60027-2.}>
+ Use GiB instead of GB
+
+ }
+ />
+
setDeleteOpen(true)}>Delete Game
- export}>
+ Export your game to a text file.}>
- import}>
+ Import your game from a text file. This will overwrite your current game. Back it up first!}>
+ setImportSaveOpen(false)}
+ onConfirm={() => confirmedImportGame()}
+ confirmationText={
+ <>
+ Importing a new game will completely wipe the current data!
+
+
+ Make sure to have a backup of your current save file before importing.
+
+ The file you are attempting to import seems valid.
+
+
+ {importData?.exportDate && (<>
+ The export date of the save file is {importData?.exportDate.toString()}
+
+
+ >)}
+ >
+ }
+ />
onColorChange(name, "#" + newColor.hex)}
+ disableAlpha
/>
>
),
diff --git a/src/ui/numeralFormat.ts b/src/ui/numeralFormat.ts
index 2a8da34cb..fe4241aa5 100644
--- a/src/ui/numeralFormat.ts
+++ b/src/ui/numeralFormat.ts
@@ -14,10 +14,13 @@ import "numeral/locales/no";
import "numeral/locales/pl";
import "numeral/locales/ru";
+import { Settings } from "../Settings/Settings";
+
/* eslint-disable class-methods-use-this */
const extraFormats = [1e15, 1e18, 1e21, 1e24, 1e27, 1e30];
const extraNotations = ["q", "Q", "s", "S", "o", "n"];
+const gigaMultiplier = { standard: 1e9, iec60027_2: 2 ** 30 };
class NumeralFormatter {
// Default Locale
@@ -110,11 +113,11 @@ class NumeralFormatter {
}
formatRAM(n: number): string {
- if (n < 1e3) return this.format(n, "0.00") + "GB";
- if (n < 1e6) return this.format(n / 1e3, "0.00") + "TB";
- if (n < 1e9) return this.format(n / 1e6, "0.00") + "PB";
- if (n < 1e12) return this.format(n / 1e9, "0.00") + "EB";
- return this.format(n, "0.00") + "GB";
+ if(Settings.UseIEC60027_2)
+ {
+ return this.format(n * gigaMultiplier.iec60027_2, "0.00ib");
+ }
+ return this.format(n * gigaMultiplier.standard, "0.00b");
}
formatPercentage(n: number, decimalPlaces = 2): string {