Merge branch 'dev' into feature/alert-close-browser

This commit is contained in:
hydroflame 2022-01-02 11:44:47 -05:00 committed by GitHub
commit a2d75ae10c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 502 additions and 59 deletions

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

2
dist/bitburner.d.ts vendored

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

30
electron/export.html Normal file

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

@ -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"));
}
});

@ -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,
}

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

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

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

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

@ -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;
}

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

@ -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")}
<br />
</>
);
breakdownItems.push(`${matName}: ${numeralWrapper.format(mat.qty * MaterialSizes[matName], "0,0.0")}`);
}
for (const prodName in division.products) {
const prod = division.products[prodName];
if (prod === undefined) continue;
breakdown = (
<>
{breakdown}
{prodName}: {numeralWrapper.format(prod.data[props.warehouse.loc][0] * prod.siz, "0,0.0")}
</>
);
breakdownItems.push(`${prodName}: ${numeralWrapper.format(prod.data[props.warehouse.loc][0] * prod.siz, "0,0.0")}`);
}
const breakdown = <>{breakdownItems.join('<br />')}</>
return (
<Paper>
<Box display="flex" alignItems="center">

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

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

@ -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);
}

@ -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;
/**

@ -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();
});

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

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

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

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

@ -28,6 +28,7 @@ import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
import { dialogBoxCreate } from "./DialogBox";
import { ConfirmationModal } from "./ConfirmationModal";
import { ThemeEditorModal } from "./ThemeEditorModal";
import { SnackbarEvents } from "./Snackbar";
import { Settings } from "../../Settings/Settings";
import { save, deleteGame } from "../../db";
@ -51,6 +52,12 @@ interface IProps {
softReset: () => void;
}
interface ImportData {
base64: string;
parsed: any;
exportDate?: Date;
}
export function GameOptionsRoot(props: IProps): React.ReactElement {
const classes = useStyles();
const importInput = useRef<HTMLInputElement>(null);
@ -78,12 +85,15 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
const [enableBashHotkeys, setEnableBashHotkeys] = useState(Settings.EnableBashHotkeys);
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
const [saveGameOnFileSave, setSaveGameOnFileSave] = useState(Settings.SaveGameOnFileSave);
const [useIEC60027_2, setUseIEC60027_2] = useState(Settings.UseIEC60027_2);
const [locale, setLocale] = useState(Settings.Locale);
const [diagnosticOpen, setDiagnosticOpen] = useState(false);
const [deleteGameOpen, setDeleteOpen] = useState(false);
const [themeEditorOpen, setThemeEditorOpen] = useState(false);
const [softResetOpen, setSoftResetOpen] = useState(false);
const [importSaveOpen, setImportSaveOpen] = useState(false);
const [importData, setImportData] = useState<ImportData | null>(null);
function handleExecTimeChange(event: any, newValue: number | number[]): void {
setExecTime(newValue as number);
@ -154,6 +164,10 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
setDisableASCIIArt(event.target.checked);
Settings.DisableASCIIArt = event.target.checked;
}
function handleUseIEC60027_2Change(event: React.ChangeEvent<HTMLInputElement>): void {
setUseIEC60027_2(event.target.checked);
Settings.UseIEC60027_2 = event.target.checked;
}
function handleDisableTextEffectsChange(event: React.ChangeEvent<HTMLInputElement>): void {
setDisableTextEffects(event.target.checked);
@ -206,11 +220,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 {
}
/>
</ListItem>
<ListItem>
<FormControlLabel
control={<Switch checked={useIEC60027_2} onChange={handleUseIEC60027_2Change} />}
label={
<Tooltip title={<Typography>If this is set all references to memory will use GiB instead of GB, in accordance with IEC 60027-2.</Typography>}>
<Typography>Use GiB instead of GB</Typography>
</Tooltip>
}
/>
</ListItem>
<ListItem>
<Tooltip
title={
@ -618,19 +698,41 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Button onClick={() => setDeleteOpen(true)}>Delete Game</Button>
</Box>
<Box>
<Tooltip title={<Typography>export</Typography>}>
<Tooltip title={<Typography>Export your game to a text file.</Typography>}>
<Button onClick={() => props.export()}>
<DownloadIcon color="primary" />
Export
Export Game
</Button>
</Tooltip>
<Tooltip title={<Typography>import</Typography>}>
<Tooltip title={<Typography>Import your game from a text file.<br/>This will <strong>overwrite</strong> your current game. Back it up first!</Typography>}>
<Button onClick={startImport}>
<UploadIcon color="primary" />
Import
Import Game
<input ref={importInput} id="import-game-file-selector" type="file" hidden onChange={onImport} />
</Button>
</Tooltip>
<ConfirmationModal
open={importSaveOpen}
onClose={() => setImportSaveOpen(false)}
onConfirm={() => confirmedImportGame()}
confirmationText={
<>
Importing a new game will <strong>completely wipe</strong> the current data!
<br />
<br />
Make sure to have a backup of your current save file before importing.
<br />
The file you are attempting to import seems valid.
<br />
<br />
{importData?.exportDate && (<>
The export date of the save file is <strong>{importData?.exportDate.toString()}</strong>
<br />
<br />
</>)}
</>
}
/>
</Box>
<Box>
<Tooltip

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

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