Merge pull request #3505 from nickofolas/improvement/settings-ui

UI: Overhaul GameOptions UI
This commit is contained in:
hydroflame 2022-04-18 20:51:50 -04:00 committed by GitHub
commit ae3c6194f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 805 additions and 668 deletions

@ -0,0 +1,6 @@
export enum GameOptionsTab {
SYSTEM,
INTERFACE,
GAMEPLAY,
MISC,
}

@ -0,0 +1,356 @@
import { MenuItem, Select, SelectChangeEvent, TextField, Tooltip, Typography } from "@mui/material";
import React, { useState } from "react";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { Settings } from "../../Settings/Settings";
import { OptionSwitch } from "../../ui/React/OptionSwitch";
import { formatTime } from "../../utils/helpers/formatTime";
import { GameOptionsTab } from "../GameOptionsTab";
import { GameOptionsPage } from "./GameOptionsPage";
import { OptionsSlider } from "./OptionsSlider";
interface IProps {
currentTab: GameOptionsTab;
player: IPlayer;
}
export const CurrentOptionsPage = (props: IProps): React.ReactElement => {
const [execTime, setExecTime] = useState(Settings.CodeInstructionRunTime);
const [recentScriptsSize, setRecentScriptsSize] = useState(Settings.MaxRecentScriptsCapacity);
const [logSize, setLogSize] = useState(Settings.MaxLogCapacity);
const [portSize, setPortSize] = useState(Settings.MaxPortCapacity);
const [terminalSize, setTerminalSize] = useState(Settings.MaxTerminalCapacity);
const [autosaveInterval, setAutosaveInterval] = useState(Settings.AutosaveInterval);
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
const [locale, setLocale] = useState(Settings.Locale);
function handleExecTimeChange(event: any, newValue: number | number[]): void {
setExecTime(newValue as number);
Settings.CodeInstructionRunTime = newValue as number;
}
function handleRecentScriptsSizeChange(event: any, newValue: number | number[]): void {
setRecentScriptsSize(newValue as number);
Settings.MaxRecentScriptsCapacity = newValue as number;
}
function handleLogSizeChange(event: any, newValue: number | number[]): void {
setLogSize(newValue as number);
Settings.MaxLogCapacity = newValue as number;
}
function handlePortSizeChange(event: any, newValue: number | number[]): void {
setPortSize(newValue as number);
Settings.MaxPortCapacity = newValue as number;
}
function handleTerminalSizeChange(event: any, newValue: number | number[]): void {
setTerminalSize(newValue as number);
Settings.MaxTerminalCapacity = newValue as number;
}
function handleAutosaveIntervalChange(event: any, newValue: number | number[]): void {
setAutosaveInterval(newValue as number);
Settings.AutosaveInterval = newValue as number;
}
function handleLocaleChange(event: SelectChangeEvent<string>): void {
setLocale(event.target.value as string);
Settings.Locale = event.target.value as string;
}
function handleTimestampFormatChange(event: React.ChangeEvent<HTMLInputElement>): void {
setTimestampFormat(event.target.value);
Settings.TimestampsFormat = event.target.value;
}
const pages = {
[GameOptionsTab.SYSTEM]: (
<GameOptionsPage title="System">
{/* Wrap in a React fragment to prevent the sliders from breaking as list items */}
<>
<OptionsSlider
label=".script exec time (ms)"
value={execTime}
callback={handleExecTimeChange}
step={1}
min={5}
max={100}
tooltip={
<>
The minimum number of milliseconds it takes to execute an operation in Netscript. Setting this too low
can result in poor performance if you have many scripts running.
</>
}
/>
<OptionsSlider
label="Recently killed scripts size"
value={recentScriptsSize}
callback={handleRecentScriptsSizeChange}
step={25}
min={0}
max={500}
tooltip={
<>
The maximum number of lines a script's logs can hold. Setting this too high can cause the game to use a
lot of memory if you have many scripts running.
</>
}
/>
<OptionsSlider
label="Netscript log size"
value={logSize}
callback={handleLogSizeChange}
step={20}
min={20}
max={500}
tooltip={
<>
The maximum number of lines a script's logs can hold. Setting this too high can cause the game to use a
lot of memory if you have many scripts running.
</>
}
/>
<OptionsSlider
label="Netscript port size"
value={portSize}
callback={handlePortSizeChange}
step={1}
min={20}
max={100}
tooltip={
<>
The maximum number of entries that can be written to a port using Netscript's write() function. Setting
this too high can cause the game to use a lot of memory.
</>
}
/>
<OptionsSlider
label="Terminal capacity"
value={terminalSize}
callback={handleTerminalSizeChange}
step={50}
min={50}
max={500}
tooltip={
<>
The maximum number of entries that can be written to the terminal. Setting this too high can cause the
game to use a lot of memory.
</>
}
marks
/>
<OptionsSlider
label="Autosave interval (s)"
value={autosaveInterval}
callback={handleAutosaveIntervalChange}
step={30}
min={0}
max={600}
tooltip={<>The time (in seconds) between each autosave. Set to 0 to disable autosave.</>}
marks
/>
</>
<OptionSwitch
checked={Settings.SuppressSavedGameToast}
onChange={(newValue) => (Settings.SuppressSavedGameToast = newValue)}
text="Suppress Auto-Save Game Toast"
tooltip={<>If this is set, there will be no "Game Saved!" toast appearing after an auto-save.</>}
/>
<OptionSwitch
checked={Settings.SuppressAutosaveDisabledWarnings}
onChange={(newValue) => (Settings.SuppressAutosaveDisabledWarnings = newValue)}
text="Suppress Auto-Save Disabled Warning"
tooltip={<>If this is set, there will be no warning triggered when auto-save is disabled (at 0).</>}
/>
<OptionSwitch
checked={Settings.SaveGameOnFileSave}
onChange={(newValue) => (Settings.SaveGameOnFileSave = newValue)}
text="Save game on file save"
tooltip={<>Save your game any time a file is saved in the script editor.</>}
/>
<OptionSwitch
checked={Settings.ExcludeRunningScriptsFromSave}
onChange={(newValue) => (Settings.ExcludeRunningScriptsFromSave = newValue)}
text="Exclude Running Scripts from Save"
tooltip={
<>
If this is set, the save file will exclude all running scripts. This is only useful if your save is
lagging a lot. You'll have to restart your script every time you launch the game.
</>
}
/>
</GameOptionsPage>
),
[GameOptionsTab.INTERFACE]: (
<GameOptionsPage title="Interface">
<OptionSwitch
checked={Settings.DisableASCIIArt}
onChange={(newValue) => (Settings.DisableASCIIArt = newValue)}
text="Disable ascii art"
tooltip={<>If this is set all ASCII art will be disabled.</>}
/>
<OptionSwitch
checked={Settings.DisableTextEffects}
onChange={(newValue) => (Settings.DisableTextEffects = newValue)}
text="Disable text effects"
tooltip={
<>
If this is set, text effects will not be displayed. This can help if text is difficult to read in certain
areas.
</>
}
/>
<OptionSwitch
checked={Settings.DisableOverviewProgressBars}
onChange={(newValue) => (Settings.DisableOverviewProgressBars = newValue)}
text="Disable Overview Progress Bars"
tooltip={<>If this is set, the progress bars in the character overview will be hidden.</>}
/>
<OptionSwitch
checked={Settings.UseIEC60027_2}
onChange={(newValue) => (Settings.UseIEC60027_2 = newValue)}
text="Use GiB instead of GB"
tooltip={
<>If this is set all references to memory will use GiB instead of GB, in accordance with IEC 60027-2.</>
}
/>
<Tooltip
title={
<Typography>
Terminal commands and log entries will be timestamped. See https://date-fns.org/docs/Getting-Started/
</Typography>
}
>
<TextField
InputProps={{
startAdornment: (
<Typography
color={formatTime(timestampFormat) === "format error" && timestampFormat !== "" ? "error" : "success"}
>
Timestamp format:
</Typography>
),
}}
value={timestampFormat}
onChange={handleTimestampFormatChange}
placeholder="yyyy-MM-dd hh:mm:ss"
/>
</Tooltip>
<>
<Tooltip title={<Typography>Sets the locale for displaying numbers.</Typography>}>
<Typography>Locale</Typography>
</Tooltip>
<Select value={locale} onChange={handleLocaleChange}>
<MenuItem value="en">en</MenuItem>
<MenuItem value="bg">bg</MenuItem>
<MenuItem value="cs">cs</MenuItem>
<MenuItem value="da-dk">da-dk</MenuItem>
<MenuItem value="de">de</MenuItem>
<MenuItem value="en-au">en-au</MenuItem>
<MenuItem value="en-gb">en-gb</MenuItem>
<MenuItem value="es">es</MenuItem>
<MenuItem value="fr">fr</MenuItem>
<MenuItem value="hu">hu</MenuItem>
<MenuItem value="it">it</MenuItem>
<MenuItem value="lv">lv</MenuItem>
<MenuItem value="no">no</MenuItem>
<MenuItem value="pl">pl</MenuItem>
<MenuItem value="ru">ru</MenuItem>
</Select>
</>
</GameOptionsPage>
),
[GameOptionsTab.GAMEPLAY]: (
<GameOptionsPage title="Gameplay">
<OptionSwitch
checked={Settings.SuppressMessages}
onChange={(newValue) => (Settings.SuppressMessages = newValue)}
text="Suppress story messages"
tooltip={
<>
If this is set, then any messages you receive will not appear as popups on the screen. They will still get
sent to your home computer as '.msg' files and can be viewed with the 'cat' Terminal command.
</>
}
/>
<OptionSwitch
checked={Settings.SuppressFactionInvites}
onChange={(newValue) => (Settings.SuppressFactionInvites = newValue)}
text="Suppress faction invites"
tooltip={
<>
If this is set, then any faction invites you receive will not appear as popups on the screen. Your
outstanding faction invites can be viewed in the 'Factions' page.
</>
}
/>
<OptionSwitch
checked={Settings.SuppressTravelConfirmation}
onChange={(newValue) => (Settings.SuppressTravelConfirmation = newValue)}
text="Suppress travel confirmations"
tooltip={
<>
If this is set, the confirmation message before traveling will not show up. You will automatically be
deducted the travel cost as soon as you click.
</>
}
/>
<OptionSwitch
checked={Settings.SuppressBuyAugmentationConfirmation}
onChange={(newValue) => (Settings.SuppressBuyAugmentationConfirmation = newValue)}
text="Suppress augmentations confirmation"
tooltip={<>If this is set, the confirmation message before buying augmentation will not show up.</>}
/>
<OptionSwitch
checked={Settings.SuppressTIXPopup}
onChange={(newValue) => (Settings.SuppressTIXPopup = newValue)}
text="Suppress TIX messages"
tooltip={<>If this is set, the stock market will never create any popup.</>}
/>
{props.player.bladeburner && (
<OptionSwitch
checked={Settings.SuppressBladeburnerPopup}
onChange={(newValue) => (Settings.SuppressBladeburnerPopup = newValue)}
text="Suppress bladeburner popup"
tooltip={
<>
If this is set, then having your Bladeburner actions interrupted by being busy with something else will
not display a popup message.
</>
}
/>
)}
</GameOptionsPage>
),
[GameOptionsTab.MISC]: (
<GameOptionsPage title="Misc">
<OptionSwitch
checked={Settings.DisableHotkeys}
onChange={(newValue) => (Settings.DisableHotkeys = newValue)}
text="Disable hotkeys"
tooltip={
<>
If this is set, then most hotkeys (keyboard shortcuts) in the game are disabled. This includes Terminal
commands, hotkeys to navigate between different parts of the game, and the "Save and Close (Ctrl + b)"
hotkey in the Text Editor.
</>
}
/>
<OptionSwitch
checked={Settings.EnableBashHotkeys}
onChange={(newValue) => (Settings.EnableBashHotkeys = newValue)}
text="Enable bash hotkeys"
tooltip={
<>
Improved Bash emulation mode. Setting this to 1 enables several new Terminal shortcuts and features that
more closely resemble a real Bash-style shell. Note that when this mode is enabled, the default browser
shortcuts are overriden by the new Bash shortcuts.
</>
}
/>
</GameOptionsPage>
),
};
return pages[props.currentTab];
};

@ -0,0 +1,29 @@
import { List, ListItem, Paper, Typography } from "@mui/material";
import { uniqueId } from "lodash";
import React from "react";
interface IProps {
children: React.ReactElement | (React.ReactElement | null)[];
title: string;
}
export const GameOptionsPage = (props: IProps): React.ReactElement => {
return (
<Paper sx={{ height: "fit-content", p: 1 }}>
<Typography variant="h6">{props.title}</Typography>
{(props.children as any)?.length > 1 ? (
<List disablePadding dense>
{(props.children as React.ReactElement[])
.filter((c) => c)
.map((c, i) => (
<ListItem key={uniqueId(String(i))} sx={{ px: 0, display: "block" }}>
{c}
</ListItem>
))}
</List>
) : (
props.children
)}
</Paper>
);
};

@ -0,0 +1,39 @@
import { Box, Container, Typography } from "@mui/material";
import React, { useState } from "react";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { IRouter } from "../../ui/Router";
import { GameOptionsTab } from "../GameOptionsTab";
import { CurrentOptionsPage } from "./CurrentOptionsPage";
import { GameOptionsSidebar } from "./GameOptionsSidebar";
interface IProps {
player: IPlayer;
router: IRouter;
save: () => void;
export: () => void;
forceKill: () => void;
softReset: () => void;
}
export function GameOptionsRoot(props: IProps): React.ReactElement {
const [currentTab, setCurrentTab] = useState(GameOptionsTab.SYSTEM);
return (
<Container disableGutters maxWidth="lg" sx={{ mx: 0 }}>
<Typography variant="h4">Options</Typography>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 3fr", gap: 1 }}>
<GameOptionsSidebar
tab={currentTab}
setTab={(tab: GameOptionsTab) => setCurrentTab(tab)}
player={props.player}
router={props.router}
save={props.save}
export={props.export}
forceKill={props.forceKill}
softReset={props.softReset}
/>
<CurrentOptionsPage currentTab={currentTab} player={props.player} />
</Box>
</Container>
);
}

@ -0,0 +1,336 @@
import {
BugReport,
Chat,
Download,
LibraryBooks,
Palette,
Public,
Reddit,
Save,
SystemUpdateAlt,
Upload,
Bloodtype,
} from "@mui/icons-material";
import { Box, Button, List, ListItemButton, Paper, Tooltip, Typography } from "@mui/material";
import { default as React, useRef, useState } from "react";
import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { ImportData, saveObject } from "../../SaveObject";
import { Settings } from "../../Settings/Settings";
import { StyleEditorButton } from "../../Themes/ui/StyleEditorButton";
import { ThemeEditorButton } from "../../Themes/ui/ThemeEditorButton";
import { ConfirmationModal } from "../../ui/React/ConfirmationModal";
import { DeleteGameButton } from "../../ui/React/DeleteGameButton";
import { SnackbarEvents, ToastVariant } from "../../ui/React/Snackbar";
import { SoftResetButton } from "../../ui/React/SoftResetButton";
import { IRouter } from "../../ui/Router";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { GameOptionsTab } from "../GameOptionsTab";
interface IProps {
tab: GameOptionsTab;
setTab: (tab: GameOptionsTab) => void;
player: IPlayer;
router: IRouter;
save: () => void;
export: () => void;
forceKill: () => void;
softReset: () => void;
}
interface ITabProps {
sideBarProps: IProps;
tab: GameOptionsTab;
tabName: string;
}
const SideBarTab = (props: ITabProps): React.ReactElement => {
return (
<ListItemButton
selected={props.sideBarProps.tab === props.tab}
onClick={() => props.sideBarProps.setTab(props.tab)}
>
<Typography>{props.tabName}</Typography>
</ListItemButton>
);
};
export const GameOptionsSidebar = (props: IProps): React.ReactElement => {
const importInput = useRef<HTMLInputElement>(null);
const [diagnosticOpen, setDiagnosticOpen] = useState(false);
const [importSaveOpen, setImportSaveOpen] = useState(false);
const [importData, setImportData] = useState<ImportData | null>(null);
function startImport(): void {
if (!window.File || !window.FileReader || !window.FileList || !window.Blob) return;
const ii = importInput.current;
if (ii === null) throw new Error("import input should not be null");
ii.click();
}
async function onImport(event: React.ChangeEvent<HTMLInputElement>): Promise<void> {
try {
const base64Save = await saveObject.getImportStringFromFile(event.target.files);
const data = await saveObject.getImportDataFromString(base64Save);
setImportData(data);
setImportSaveOpen(true);
} catch (ex: any) {
SnackbarEvents.emit(ex.toString(), ToastVariant.ERROR, 5000);
}
}
async function confirmedImportGame(): Promise<void> {
if (!importData) return;
try {
await saveObject.importGame(importData.base64);
} catch (ex: any) {
SnackbarEvents.emit(ex.toString(), ToastVariant.ERROR, 5000);
}
setImportSaveOpen(false);
setImportData(null);
}
function compareSaveGame(): void {
if (!importData) return;
props.router.toImportSave(importData.base64);
setImportSaveOpen(false);
setImportData(null);
}
return (
<Box>
<Paper sx={{ height: "fit-content", mb: 1 }}>
<List>
<SideBarTab sideBarProps={props} tab={GameOptionsTab.SYSTEM} tabName="System" />
<SideBarTab sideBarProps={props} tab={GameOptionsTab.GAMEPLAY} tabName="Gameplay" />
<SideBarTab sideBarProps={props} tab={GameOptionsTab.INTERFACE} tabName="Interface" />
<SideBarTab sideBarProps={props} tab={GameOptionsTab.MISC} tabName="Misc" />
</List>
</Paper>
<Box
sx={{
display: "grid",
width: "100%",
height: "fit-content",
gridTemplateAreas: `"save delete"
"export import"
"kill kill"
"reset diagnose"
"browse browse"
"theme style"
"links links"
"devs devs"`,
gridTemplateColumns: "1fr 1fr",
}}
>
<Button onClick={() => props.save()} startIcon={<Save />} sx={{ gridArea: "save" }}>
Save Game
</Button>
<Box sx={{ gridArea: "delete", "& .MuiButton-root": { height: "100%", width: "100%" } }}>
<DeleteGameButton />
</Box>
<Tooltip title={<Typography>Export your game to a text file.</Typography>}>
<Button onClick={() => props.export()} startIcon={<Download />} sx={{ gridArea: "export" }}>
Export Game
</Button>
</Tooltip>
<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} startIcon={<Upload />} sx={{ gridArea: "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()}
additionalButton={<Button onClick={compareSaveGame}>Compare Save</Button>}
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.
{(importData?.playerData?.lastSave ?? 0) > 0 && (
<>
<br />
<br />
The export date of the save file is{" "}
<strong>{new Date(importData?.playerData?.lastSave ?? 0).toLocaleString()}</strong>
</>
)}
{(importData?.playerData?.totalPlaytime ?? 0) > 0 && (
<>
<br />
<br />
Total play time of imported game:{" "}
{convertTimeMsToTimeElapsedString(importData?.playerData?.totalPlaytime ?? 0)}
</>
)}
<br />
<br />
</>
}
/>
<Tooltip
title={
<Typography>
Forcefully kill all active running scripts, in case there is a bug or some unexpected issue with the game.
After using this, save the game and then reload the page. This is different then normal kill in that
normal kill will tell the script to shut down while force kill just removes the references to it (and it
should crash on it's own). This will not remove the files on your computer. Just forcefully kill all
running instances of all scripts.
</Typography>
}
>
<Button onClick={() => props.forceKill()} sx={{ gridArea: "kill" }}>
Force kill all active scripts
</Button>
</Tooltip>
<Box sx={{ gridArea: "reset", "& .MuiButton-root": { height: "100%", width: "100%" } }}>
<SoftResetButton
noConfirmation={Settings.SuppressBuyAugmentationConfirmation}
onTriggered={props.softReset}
/>
</Box>
<Tooltip
title={
<Typography>
If your save file is extremely big you can use this button to view a map of all the files on every server.
Be careful there might be spoilers.
</Typography>
}
>
<Button onClick={() => setDiagnosticOpen(true)} sx={{ gridArea: "diagnose" }}>
Diagnose files
</Button>
</Tooltip>
<Tooltip title="Head to the theme browser to see a collection of prebuilt themes.">
<Button startIcon={<Palette />} onClick={() => props.router.toThemeBrowser()} sx={{ gridArea: "browse" }}>
Theme Browser
</Button>
</Tooltip>
<Box sx={{ gridArea: "theme", "& .MuiButton-root": { height: "100%", width: "100%" } }}>
<ThemeEditorButton router={props.router} />
</Box>
<Box sx={{ gridArea: "style", "& .MuiButton-root": { height: "100%", width: "100%" } }}>
<StyleEditorButton />
</Box>
<Box
sx={{
gridArea: "links",
display: "grid",
gridTemplateAreas: `"bug changelog"
"docs docs"
"discord reddit"
"plaza plaza"`,
gridTemplateColumns: "1fr 1fr",
my: 1,
}}
>
<Button
startIcon={<BugReport />}
href="https://github.com/danielyxie/bitburner/issues/new"
target="_blank"
sx={{ gridArea: "bug" }}
>
Report Bug
</Button>
<Button
startIcon={<SystemUpdateAlt />}
href="https://bitburner.readthedocs.io/en/latest/changelog.html"
target="_blank"
sx={{ gridArea: " changelog" }}
>
Changelog
</Button>
<Button
startIcon={<LibraryBooks />}
href="https://bitburner.readthedocs.io/en/latest/index.html"
target="_blank"
sx={{ gridArea: "docs" }}
>
Documentation
</Button>
<Button startIcon={<Chat />} href="https://discord.gg/TFc3hKD" target="_blank" sx={{ gridArea: "discord" }}>
Discord
</Button>
<Button
startIcon={<Reddit />}
href="https://www.reddit.com/r/bitburner"
target="_blank"
sx={{ gridArea: "reddit" }}
>
Reddit
</Button>
<Button
startIcon={<Public />}
href="https://plaza.dsolver.ca/games/bitburner"
target="_blank"
sx={{ gridArea: "plaza" }}
>
Incremental game plaza
</Button>
</Box>
{!location.href.startsWith("file://") && (
<Box sx={{ gridArea: "devs" }}>
<form
action="https://www.paypal.com/cgi-bin/webscr"
method="post"
target="_blank"
style={{ display: "block" }}
>
<Button sx={{ width: "100%", display: "flex", flexDirection: "column" }} type="submit">
danielyxie / BigD (Original Dev)
<input type="hidden" name="cmd" value="_s-xclick" />
<input
type="hidden"
name="encrypted"
value="-----BEGIN PKCS7-----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-----END PKCS7-----"
/>
<input
type="image"
// src="https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif"
src="https://www.paypalobjects.com/digitalassets/c/website/marketing/apac/C2/logos-buttons/optimize/26_Yellow_PayPal_Pill_Button.png"
name="submit"
alt="PayPal - The safer, easier way to pay online!"
/>
</Button>
</form>
<Button
href="https://www.google.com/search?q=Where+to+donate+blood+near+me%3F"
target="_blank"
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
}}
>
hydroflame (Current Maintainer)
<span style={{ display: "flex", alignItems: "center" }}>
<Bloodtype sx={{ mb: 0.5, mr: 1 }} />
Donate blood!
</span>
</Button>
</Box>
)}
</Box>
<FileDiagnosticModal open={diagnosticOpen} onClose={() => setDiagnosticOpen(false)} />
</Box>
);
};

@ -0,0 +1,38 @@
import { Slider, Tooltip, Typography, Box } from "@mui/material";
import React from "react";
interface IProps {
value: any;
callback: (event: any, newValue: number | number[]) => void;
step: number;
min: number;
max: number;
tooltip: React.ReactElement;
label: string;
marks?: boolean;
}
export const OptionsSlider = (props: IProps): React.ReactElement => {
return (
<Box>
<Tooltip title={<Typography>{props.tooltip}</Typography>}>
<Typography>{props.label}</Typography>
</Tooltip>
<Slider
value={props.value}
onChange={props.callback}
step={props.step}
min={props.min}
max={props.max}
valueLabelDisplay="auto"
sx={{
"& .MuiSlider-thumb": {
height: "12px",
width: "12px",
},
}}
marks={props.marks}
/>
</Box>
);
};

@ -44,7 +44,7 @@ import { CorporationRoot } from "../Corporation/ui/CorporationRoot";
import { InfiltrationRoot } from "../Infiltration/ui/InfiltrationRoot";
import { GraftingRoot } from "../PersonObjects/Grafting/ui/GraftingRoot";
import { WorkInProgressRoot } from "./WorkInProgressRoot";
import { GameOptionsRoot } from "./React/GameOptionsRoot";
import { GameOptionsRoot } from "../GameOptions/ui/GameOptionsRoot";
import { SleeveRoot } from "../PersonObjects/Sleeve/ui/SleeveRoot";
import { HacknetRoot } from "../Hacknet/ui/HacknetRoot";
import { GenericLocation } from "../Locations/ui/GenericLocation";

@ -1,667 +0,0 @@
import React, { useState, useRef } from "react";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { Theme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles";
import Typography from "@mui/material/Typography";
import Slider from "@mui/material/Slider";
import Grid from "@mui/material/Grid";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import Link from "@mui/material/Link";
import Tooltip from "@mui/material/Tooltip";
import TextField from "@mui/material/TextField";
import DownloadIcon from "@mui/icons-material/Download";
import UploadIcon from "@mui/icons-material/Upload";
import SaveIcon from "@mui/icons-material/Save";
import PaletteIcon from "@mui/icons-material/Palette";
import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
import { ConfirmationModal } from "./ConfirmationModal";
import { SnackbarEvents, ToastVariant } from "./Snackbar";
import { Settings } from "../../Settings/Settings";
import { DeleteGameButton } from "./DeleteGameButton";
import { SoftResetButton } from "./SoftResetButton";
import { IRouter } from "../Router";
import { ThemeEditorButton } from "../../Themes/ui/ThemeEditorButton";
import { StyleEditorButton } from "../../Themes/ui/StyleEditorButton";
import { formatTime } from "../../utils/helpers/formatTime";
import { OptionSwitch } from "./OptionSwitch";
import { ImportData, saveObject } from "../../SaveObject";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: 50,
padding: theme.spacing(2),
userSelect: "none",
},
}),
);
interface IProps {
player: IPlayer;
router: IRouter;
save: () => void;
export: () => void;
forceKill: () => void;
softReset: () => void;
}
export function GameOptionsRoot(props: IProps): React.ReactElement {
const classes = useStyles();
const importInput = useRef<HTMLInputElement>(null);
const [execTime, setExecTime] = useState(Settings.CodeInstructionRunTime);
const [recentScriptsSize, setRecentScriptsSize] = useState(Settings.MaxRecentScriptsCapacity);
const [logSize, setLogSize] = useState(Settings.MaxLogCapacity);
const [portSize, setPortSize] = useState(Settings.MaxPortCapacity);
const [terminalSize, setTerminalSize] = useState(Settings.MaxTerminalCapacity);
const [autosaveInterval, setAutosaveInterval] = useState(Settings.AutosaveInterval);
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
const [locale, setLocale] = useState(Settings.Locale);
const [diagnosticOpen, setDiagnosticOpen] = 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);
Settings.CodeInstructionRunTime = newValue as number;
}
function handleRecentScriptsSizeChange(event: any, newValue: number | number[]): void {
setRecentScriptsSize(newValue as number);
Settings.MaxRecentScriptsCapacity = newValue as number;
}
function handleLogSizeChange(event: any, newValue: number | number[]): void {
setLogSize(newValue as number);
Settings.MaxLogCapacity = newValue as number;
}
function handlePortSizeChange(event: any, newValue: number | number[]): void {
setPortSize(newValue as number);
Settings.MaxPortCapacity = newValue as number;
}
function handleTerminalSizeChange(event: any, newValue: number | number[]): void {
setTerminalSize(newValue as number);
Settings.MaxTerminalCapacity = newValue as number;
}
function handleAutosaveIntervalChange(event: any, newValue: number | number[]): void {
setAutosaveInterval(newValue as number);
Settings.AutosaveInterval = newValue as number;
}
function handleLocaleChange(event: SelectChangeEvent<string>): void {
setLocale(event.target.value as string);
Settings.Locale = event.target.value as string;
}
function handleTimestampFormatChange(event: React.ChangeEvent<HTMLInputElement>): void {
setTimestampFormat(event.target.value);
Settings.TimestampsFormat = event.target.value;
}
function startImport(): void {
if (!window.File || !window.FileReader || !window.FileList || !window.Blob) return;
const ii = importInput.current;
if (ii === null) throw new Error("import input should not be null");
ii.click();
}
async function onImport(event: React.ChangeEvent<HTMLInputElement>): Promise<void> {
try {
const base64Save = await saveObject.getImportStringFromFile(event.target.files);
const data = await saveObject.getImportDataFromString(base64Save);
setImportData(data);
setImportSaveOpen(true);
} catch (ex: any) {
SnackbarEvents.emit(ex.toString(), ToastVariant.ERROR, 5000);
}
}
async function confirmedImportGame(): Promise<void> {
if (!importData) return;
try {
await saveObject.importGame(importData.base64);
} catch (ex: any) {
SnackbarEvents.emit(ex.toString(), ToastVariant.ERROR, 5000);
}
setImportSaveOpen(false);
setImportData(null);
}
function compareSaveGame(): void {
if (!importData) return;
props.router.toImportSave(importData.base64);
setImportSaveOpen(false);
setImportData(null);
}
return (
<div className={classes.root} style={{ width: "90%" }}>
<Typography variant="h4" gutterBottom>
Options
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<List>
<ListItem>
<Box display="grid" sx={{ width: "fit-content", gridTemplateColumns: "1fr 3.5fr", gap: 1 }}>
<Tooltip
title={
<Typography>
The minimum number of milliseconds it takes to execute an operation in Netscript. Setting this too
low can result in poor performance if you have many scripts running.
</Typography>
}
>
<Typography>.script exec time (ms)</Typography>
</Tooltip>
<Slider
value={execTime}
onChange={handleExecTimeChange}
step={1}
min={5}
max={100}
valueLabelDisplay="auto"
/>
<Tooltip
title={
<Typography>
The maximum number of recently killed script entries being tracked. Setting this too high can
cause the game to use a lot of memory.
</Typography>
}
>
<Typography>Recently killed scripts size</Typography>
</Tooltip>
<Slider
value={recentScriptsSize}
onChange={handleRecentScriptsSizeChange}
step={25}
min={0}
max={500}
valueLabelDisplay="auto"
/>
<Tooltip
title={
<Typography>
The maximum number of lines a script's logs can hold. Setting this too high can cause the game to
use a lot of memory if you have many scripts running.
</Typography>
}
>
<Typography>Netscript log size</Typography>
</Tooltip>
<Slider
value={logSize}
onChange={handleLogSizeChange}
step={20}
min={20}
max={500}
valueLabelDisplay="auto"
/>
<Tooltip
title={
<Typography>
The maximum number of entries that can be written to a port using Netscript's write() function.
Setting this too high can cause the game to use a lot of memory.
</Typography>
}
>
<Typography>Netscript port size</Typography>
</Tooltip>
<Slider
value={portSize}
onChange={handlePortSizeChange}
step={1}
min={20}
max={100}
valueLabelDisplay="auto"
/>
<Tooltip
title={
<Typography>
The maximum number of entries that can be written to the terminal. Setting this too high can cause
the game to use a lot of memory.
</Typography>
}
>
<Typography>Terminal capacity</Typography>
</Tooltip>
<Slider
value={terminalSize}
onChange={handleTerminalSizeChange}
step={50}
min={50}
max={500}
valueLabelDisplay="auto"
marks
/>
<Tooltip
title={
<Typography>The time (in seconds) between each autosave. Set to 0 to disable autosave.</Typography>
}
>
<Typography>Autosave interval (s)</Typography>
</Tooltip>
<Slider
value={autosaveInterval}
onChange={handleAutosaveIntervalChange}
step={30}
min={0}
max={600}
valueLabelDisplay="auto"
marks
/>
</Box>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.SuppressMessages}
onChange={(newValue) => (Settings.SuppressMessages = newValue)}
text="Suppress story messages"
tooltip={
<>
If this is set, then any messages you receive will not appear as popups on the screen. They will
still get sent to your home computer as '.msg' files and can be viewed with the 'cat' Terminal
command.
</>
}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.SuppressFactionInvites}
onChange={(newValue) => (Settings.SuppressFactionInvites = newValue)}
text="Suppress faction invites"
tooltip={
<>
If this is set, then any faction invites you receive will not appear as popups on the screen. Your
outstanding faction invites can be viewed in the 'Factions' page.
</>
}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.SuppressTravelConfirmation}
onChange={(newValue) => (Settings.SuppressTravelConfirmation = newValue)}
text="Suppress travel confirmations"
tooltip={
<>
If this is set, the confirmation message before traveling will not show up. You will automatically
be deducted the travel cost as soon as you click.
</>
}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.SuppressBuyAugmentationConfirmation}
onChange={(newValue) => (Settings.SuppressBuyAugmentationConfirmation = newValue)}
text="Suppress augmentations confirmation"
tooltip={<>If this is set, the confirmation message before buying augmentation will not show up.</>}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.SuppressTIXPopup}
onChange={(newValue) => (Settings.SuppressTIXPopup = newValue)}
text="Suppress TIX messages"
tooltip={<>If this is set, the stock market will never create any popup.</>}
/>
</ListItem>
{!!props.player.bladeburner && (
<ListItem>
<OptionSwitch
checked={Settings.SuppressBladeburnerPopup}
onChange={(newValue) => (Settings.SuppressBladeburnerPopup = newValue)}
text="Suppress bladeburner popup"
tooltip={
<>
If this is set, then having your Bladeburner actions interrupted by being busy with something else
will not display a popup message.
</>
}
/>
</ListItem>
)}
<ListItem>
<OptionSwitch
checked={Settings.SuppressSavedGameToast}
onChange={(newValue) => (Settings.SuppressSavedGameToast = newValue)}
text="Suppress Auto-Save Game Toast"
tooltip={<>If this is set, there will be no "Game Saved!" toast appearing after an auto-save.</>}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.SuppressAutosaveDisabledWarnings}
onChange={(newValue) => (Settings.SuppressAutosaveDisabledWarnings = newValue)}
text="Suppress Auto-Save Disabled Warning"
tooltip={<>If this is set, there will be no warning triggered when auto-save is disabled (at 0).</>}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.DisableHotkeys}
onChange={(newValue) => (Settings.DisableHotkeys = newValue)}
text="Disable hotkeys"
tooltip={
<>
If this is set, then most hotkeys (keyboard shortcuts) in the game are disabled. This includes
Terminal commands, hotkeys to navigate between different parts of the game, and the "Save and Close
(Ctrl + b)" hotkey in the Text Editor.
</>
}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.DisableASCIIArt}
onChange={(newValue) => (Settings.DisableASCIIArt = newValue)}
text="Disable ascii art"
tooltip={<>If this is set all ASCII art will be disabled.</>}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.DisableTextEffects}
onChange={(newValue) => (Settings.DisableTextEffects = newValue)}
text="Disable text effects"
tooltip={
<>
If this is set, text effects will not be displayed. This can help if text is difficult to read in
certain areas.
</>
}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.DisableOverviewProgressBars}
onChange={(newValue) => (Settings.DisableOverviewProgressBars = newValue)}
text="Disable Overview Progress Bars"
tooltip={<>If this is set, the progress bars in the character overview will be hidden.</>}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.EnableBashHotkeys}
onChange={(newValue) => (Settings.EnableBashHotkeys = newValue)}
text="Enable bash hotkeys"
tooltip={
<>
Improved Bash emulation mode. Setting this to 1 enables several new Terminal shortcuts and features
that more closely resemble a real Bash-style shell. Note that when this mode is enabled, the default
browser shortcuts are overriden by the new Bash shortcuts.
</>
}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.UseIEC60027_2}
onChange={(newValue) => (Settings.UseIEC60027_2 = newValue)}
text="Use GiB instead of GB"
tooltip={
<>
If this is set all references to memory will use GiB instead of GB, in accordance with IEC 60027-2.
</>
}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.ExcludeRunningScriptsFromSave}
onChange={(newValue) => (Settings.ExcludeRunningScriptsFromSave = newValue)}
text="Exclude Running Scripts from Save"
tooltip={
<>
If this is set, the save file will exclude all running scripts. This is only useful if your save is
lagging a lot. You'll have to restart your script every time you launch the game.
</>
}
/>
</ListItem>
<ListItem>
<Tooltip
title={
<Typography>
Terminal commands and log entries will be timestamped. See
https://date-fns.org/docs/Getting-Started/
</Typography>
}
>
<span>
<TextField
InputProps={{
startAdornment: (
<Typography
color={
formatTime(timestampFormat) === "format error" && timestampFormat !== ""
? "error"
: "success"
}
>
Timestamp&nbsp;format:&nbsp;
</Typography>
),
}}
value={timestampFormat}
onChange={handleTimestampFormatChange}
placeholder="yyyy-MM-dd hh:mm:ss"
/>
</span>
</Tooltip>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.SaveGameOnFileSave}
onChange={(newValue) => (Settings.SaveGameOnFileSave = newValue)}
text="Save game on file save"
tooltip={<>Save your game any time a file is saved in the script editor.</>}
/>
</ListItem>
<ListItem>
<Tooltip title={<Typography>Sets the locale for displaying numbers.</Typography>}>
<Typography>Locale&nbsp;</Typography>
</Tooltip>
<Select value={locale} onChange={handleLocaleChange}>
<MenuItem value="en">en</MenuItem>
<MenuItem value="bg">bg</MenuItem>
<MenuItem value="cs">cs</MenuItem>
<MenuItem value="da-dk">da-dk</MenuItem>
<MenuItem value="de">de</MenuItem>
<MenuItem value="en-au">en-au</MenuItem>
<MenuItem value="en-gb">en-gb</MenuItem>
<MenuItem value="es">es</MenuItem>
<MenuItem value="fr">fr</MenuItem>
<MenuItem value="hu">hu</MenuItem>
<MenuItem value="it">it</MenuItem>
<MenuItem value="lv">lv</MenuItem>
<MenuItem value="no">no</MenuItem>
<MenuItem value="pl">pl</MenuItem>
<MenuItem value="ru">ru</MenuItem>
</Select>
</ListItem>
</List>
{!location.href.startsWith("file://") && (
<>
<ListItem>
<Typography>danielyxie / BigD (Original developer): </Typography>
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_blank">
<input type="hidden" name="cmd" value="_s-xclick" />
<input
type="hidden"
name="encrypted"
value="-----BEGIN PKCS7-----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-----END PKCS7-----"
/>
<input
type="image"
src="https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif"
name="submit"
alt="PayPal - The safer, easier way to pay online!"
/>
</form>
</ListItem>
<ListItem>
<Typography>
hydroflame (Current maintainer):{" "}
<Link href="https://www.google.com/search?q=Where+to+donate+blood+near+me%3F" target="_blank">
Donate blood!
</Link>{" "}
</Typography>
</ListItem>
</>
)}
</Grid>
<Box sx={{ display: "grid", width: "fit-content", height: "fit-content" }}>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
<Button onClick={() => props.save()} startIcon={<SaveIcon />}>
Save Game
</Button>
<DeleteGameButton />
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
<Tooltip title={<Typography>Export your game to a text file.</Typography>}>
<Button onClick={() => props.export()} startIcon={<DownloadIcon />}>
Export Game
</Button>
</Tooltip>
<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} startIcon={<UploadIcon />}>
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()}
additionalButton={<Button onClick={compareSaveGame}>Compare Save</Button>}
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.
{(importData?.playerData?.lastSave ?? 0) > 0 && (
<>
<br />
<br />
The export date of the save file is{" "}
<strong>{new Date(importData?.playerData?.lastSave ?? 0).toLocaleString()}</strong>
</>
)}
{(importData?.playerData?.totalPlaytime ?? 0) > 0 && (
<>
<br />
<br />
Total play time of imported game:{" "}
{convertTimeMsToTimeElapsedString(importData?.playerData?.totalPlaytime ?? 0)}
</>
)}
<br />
<br />
</>
}
/>
</Box>
<Box sx={{ display: "grid" }}>
<Tooltip
title={
<Typography>
Forcefully kill all active running scripts, in case there is a bug or some unexpected issue with the
game. After using this, save the game and then reload the page. This is different then normal kill in
that normal kill will tell the script to shut down while force kill just removes the references to it
(and it should crash on it's own). This will not remove the files on your computer. Just forcefully
kill all running instance of all scripts.
</Typography>
}
>
<Button onClick={() => props.forceKill()}>Force kill all active scripts</Button>
</Tooltip>
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
<SoftResetButton
noConfirmation={Settings.SuppressBuyAugmentationConfirmation}
onTriggered={props.softReset}
/>
<Tooltip
title={
<Typography>
If your save file is extremely big you can use this button to view a map of all the files on every
server. Be careful there might be spoilers.
</Typography>
}
>
<Button onClick={() => setDiagnosticOpen(true)}>Diagnose files</Button>
</Tooltip>
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr" }}>
<Tooltip title="Head to the theme browser to see a collection of prebuilt themes.">
<Button startIcon={<PaletteIcon />} onClick={() => props.router.toThemeBrowser()}>
Theme Browser
</Button>
</Tooltip>
<ThemeEditorButton router={props.router} />
<StyleEditorButton />
</Box>
<Box>
<Link href="https://github.com/danielyxie/bitburner/issues/new" target="_blank">
<Typography>Report bug</Typography>
</Link>
<Link href="https://bitburner.readthedocs.io/en/latest/changelog.html" target="_blank">
<Typography>Changelog</Typography>
</Link>
<Link href="https://bitburner.readthedocs.io/en/latest/index.html" target="_blank">
<Typography>Documentation</Typography>
</Link>
<Link href="https://discord.gg/TFc3hKD" target="_blank">
<Typography>Discord</Typography>
</Link>
<Link href="https://www.reddit.com/r/bitburner" target="_blank">
<Typography>Reddit</Typography>
</Link>
<Link href="https://plaza.dsolver.ca/games/bitburner" target="_blank">
<Typography>Incremental game plaza</Typography>
</Link>
</Box>
</Box>
</Grid>
<FileDiagnosticModal open={diagnosticOpen} onClose={() => setDiagnosticOpen(false)} />
</div>
);
}