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 {