mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-16 06:18:42 +02:00
REFACTOR: Split Settings.ts to reduce number of imports (#2600)
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, TextField, Tooltip, Typography } from "@mui/material";
|
||||
import { GameOptionsPage } from "./GameOptionsPage";
|
||||
import { isValidConnectionHostname, isValidConnectionPort, Settings } from "../../Settings/Settings";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { isValidConnectionHostname, isValidConnectionPort } from "../../Settings/SettingsUtils";
|
||||
import { RemoteFileApiConnectionStatus } from "./RemoteFileApiConnectionStatus";
|
||||
import { newRemoteFileApiConnection } from "../../RemoteFileAPI/RemoteFileAPI";
|
||||
import { OptionSwitch } from "../../ui/React/OptionSwitch";
|
||||
|
||||
@@ -35,6 +35,7 @@ import { giveExportBonus } from "./ExportBonus";
|
||||
import { loadInfiltrations } from "./Infiltration/SaveLoadInfiltration";
|
||||
import { InfiltrationState } from "./Infiltration/formulas/game";
|
||||
import { hasDarknetAccess } from "./DarkNet/utils/darknetAuthUtils";
|
||||
import { loadSettings } from "./Settings/SettingsUtils";
|
||||
|
||||
/* SaveObject.js
|
||||
* Defines the object used to save/load games
|
||||
@@ -504,7 +505,7 @@ async function loadGame(saveData: SaveData): Promise<boolean> {
|
||||
if (saveObj.SettingsSave) {
|
||||
try {
|
||||
// Try to set saved settings.
|
||||
Settings.load(saveObj.SettingsSave);
|
||||
loadSettings(saveObj.SettingsSave);
|
||||
} catch (e) {
|
||||
console.error("SettingsSave was present but an error occurred while loading:");
|
||||
console.error(e);
|
||||
|
||||
@@ -1,78 +1,9 @@
|
||||
import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums";
|
||||
import { defaultTheme } from "../Themes/Themes";
|
||||
import { defaultStyles } from "../Themes/Styles";
|
||||
import type { CursorStyle, CursorBlinking, WordWrapOptions, StickyScroll, Minimap } from "../ScriptEditor/ui/Options";
|
||||
import type { CursorBlinking, CursorStyle, Minimap, StickyScroll, WordWrapOptions } from "../ScriptEditor/ui/Options";
|
||||
import { defaultMonacoTheme } from "../ScriptEditor/ui/themes";
|
||||
import { assertObject } from "../utils/TypeAssertion";
|
||||
import type { Result } from "@nsdefs";
|
||||
import {
|
||||
assertAndSanitizeEditorTheme,
|
||||
assertAndSanitizeKeyBindings,
|
||||
assertAndSanitizeMainTheme,
|
||||
assertAndSanitizeStyles,
|
||||
} from "../JsonSchema/JSONSchemaAssertion";
|
||||
import { mergePlayerDefinedKeyBindings, type PlayerDefinedKeyBindingsType } from "../utils/KeyBindingUtils";
|
||||
import { toggleSuppressErrorModals } from "../ErrorHandling/ErrorState";
|
||||
|
||||
/**
|
||||
* This function won't be able to catch **all** invalid hostnames. In order to validate a hostname properly, we need to
|
||||
* import a good validation library or write one by ourselves. Considering that we only need to catch common mistakes,
|
||||
* it's not worth the effort.
|
||||
*
|
||||
* Some invalid hostnames that we don't catch:
|
||||
* - Invalid/missing TLD: "abc".
|
||||
* - Use space character: "a a.com"
|
||||
* - Use non-http schemes in the hostname: "ftp://a.com"
|
||||
* - etc.
|
||||
*/
|
||||
export function isValidConnectionHostname(hostname: string): Result {
|
||||
// Return a user-friendly error message.
|
||||
if (hostname === "") {
|
||||
return {
|
||||
success: false,
|
||||
message: "Hostname cannot be empty",
|
||||
};
|
||||
}
|
||||
/**
|
||||
* We expect a hostname, but the player may mistakenly put other unexpected things. We will try to catch common mistakes:
|
||||
* - Specify a scheme: http or https.
|
||||
* - Specify a port.
|
||||
* - Specify a pathname or search params.
|
||||
*/
|
||||
try {
|
||||
// Check scheme.
|
||||
if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Do not specify scheme (e.g., http, https)",
|
||||
};
|
||||
}
|
||||
// Parse to a URL with a default scheme.
|
||||
const url = new URL(`http://${hostname}`);
|
||||
// Check port, pathname, and search params.
|
||||
if (url.port !== "" || url.pathname !== "/" || url.search !== "") {
|
||||
return {
|
||||
success: false,
|
||||
message: "Do not specify port, pathname, or search parameters",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Invalid hostname: ${hostname}`,
|
||||
};
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export function isValidConnectionPort(port: number): Result {
|
||||
// 0 is a special value for port. It's an invalid port, but the player can use it to disable RFA.
|
||||
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
||||
return { success: false, message: "Invalid port" };
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
import { defaultStyles } from "../Themes/Styles";
|
||||
import { defaultTheme } from "../Themes/Themes";
|
||||
import type { PlayerDefinedKeyBindingsType } from "../utils/KeyBindingUtils";
|
||||
import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums";
|
||||
|
||||
/** The current options the player has customized to their play style. */
|
||||
export const Settings = {
|
||||
@@ -211,74 +142,4 @@ export const Settings = {
|
||||
KeyBindings: {} as PlayerDefinedKeyBindingsType,
|
||||
/** Whether to sync Steam achievements */
|
||||
SyncSteamAchievements: true,
|
||||
|
||||
load(saveString: string) {
|
||||
const save: unknown = JSON.parse(saveString);
|
||||
assertObject(save);
|
||||
save.overview && Object.assign(Settings.overview, save.overview);
|
||||
try {
|
||||
// Sanitize theme data. Invalid theme data may crash the game or make it stuck in the loading page.
|
||||
assertAndSanitizeMainTheme(save.theme);
|
||||
Object.assign(Settings.theme, save.theme);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
// Sanitize editor theme data. Invalid editor theme data may crash the game when the player opens the script editor.
|
||||
assertAndSanitizeEditorTheme(save.EditorTheme);
|
||||
Object.assign(Settings.EditorTheme, save.EditorTheme);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
// Sanitize styles.
|
||||
assertAndSanitizeStyles(save.styles);
|
||||
Object.assign(Settings.styles, save.styles);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
/**
|
||||
* KeyBindings data does not exist in old save files. Technically, this check is unnecessary. If KeyBindings is
|
||||
* undefined, assertAndSanitizeKeyBindings will throw an error, and that error will be caught here. However, it
|
||||
* means that there will be an error logged in the console every time the player loads an old save file, and this
|
||||
* logged error is kind of a "false positive" one.
|
||||
*/
|
||||
if (save.KeyBindings !== undefined) {
|
||||
try {
|
||||
// Sanitize key bindings.
|
||||
assertAndSanitizeKeyBindings(save.KeyBindings);
|
||||
Object.assign(Settings.KeyBindings, save.KeyBindings);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
Object.assign(Settings, save, {
|
||||
overview: Settings.overview,
|
||||
theme: Settings.theme,
|
||||
EditorTheme: Settings.EditorTheme,
|
||||
styles: Settings.styles,
|
||||
KeyBindings: Settings.KeyBindings,
|
||||
});
|
||||
/**
|
||||
* The hostname and port of RFA have not been validated properly, so the save data may contain invalid data. In that
|
||||
* case, we set them to the default value.
|
||||
*/
|
||||
if (!isValidConnectionHostname(Settings.RemoteFileApiAddress).success) {
|
||||
Settings.RemoteFileApiAddress = "localhost";
|
||||
}
|
||||
if (!isValidConnectionPort(Settings.RemoteFileApiPort).success) {
|
||||
Settings.RemoteFileApiPort = 0;
|
||||
}
|
||||
|
||||
// Merge Settings.KeyBindings with DefaultKeyBindings.
|
||||
mergePlayerDefinedKeyBindings(Settings.KeyBindings);
|
||||
|
||||
// Set up initial state for error modal suppression
|
||||
toggleSuppressErrorModals(Settings.SuppressErrorModals, true);
|
||||
|
||||
// Disable this feature for existing save files.
|
||||
if (save.MonacoAutoSaveOnFocusChange === undefined) {
|
||||
Settings.MonacoAutoSaveOnFocusChange = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
141
src/Settings/SettingsUtils.ts
Normal file
141
src/Settings/SettingsUtils.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { Result } from "@nsdefs";
|
||||
import { toggleSuppressErrorModals } from "../ErrorHandling/ErrorState";
|
||||
import {
|
||||
assertAndSanitizeEditorTheme,
|
||||
assertAndSanitizeKeyBindings,
|
||||
assertAndSanitizeMainTheme,
|
||||
assertAndSanitizeStyles,
|
||||
} from "../JsonSchema/JSONSchemaAssertion";
|
||||
import { mergePlayerDefinedKeyBindings } from "../utils/KeyBindingUtils";
|
||||
import { assertObject } from "../utils/TypeAssertion";
|
||||
import { Settings } from "./Settings";
|
||||
|
||||
/**
|
||||
* This function won't be able to catch **all** invalid hostnames. In order to validate a hostname properly, we need to
|
||||
* import a good validation library or write one by ourselves. Considering that we only need to catch common mistakes,
|
||||
* it's not worth the effort.
|
||||
*
|
||||
* Some invalid hostnames that we don't catch:
|
||||
* - Invalid/missing TLD: "abc".
|
||||
* - Use space character: "a a.com"
|
||||
* - Use non-http schemes in the hostname: "ftp://a.com"
|
||||
* - etc.
|
||||
*/
|
||||
export function isValidConnectionHostname(hostname: string): Result {
|
||||
// Return a user-friendly error message.
|
||||
if (hostname === "") {
|
||||
return {
|
||||
success: false,
|
||||
message: "Hostname cannot be empty",
|
||||
};
|
||||
}
|
||||
/**
|
||||
* We expect a hostname, but the player may mistakenly put other unexpected things. We will try to catch common mistakes:
|
||||
* - Specify a scheme: http or https.
|
||||
* - Specify a port.
|
||||
* - Specify a pathname or search params.
|
||||
*/
|
||||
try {
|
||||
// Check scheme.
|
||||
if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Do not specify scheme (e.g., http, https)",
|
||||
};
|
||||
}
|
||||
// Parse to a URL with a default scheme.
|
||||
const url = new URL(`http://${hostname}`);
|
||||
// Check port, pathname, and search params.
|
||||
if (url.port !== "" || url.pathname !== "/" || url.search !== "") {
|
||||
return {
|
||||
success: false,
|
||||
message: "Do not specify port, pathname, or search parameters",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Invalid hostname: ${hostname}`,
|
||||
};
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export function isValidConnectionPort(port: number): Result {
|
||||
// 0 is a special value for port. It's an invalid port, but the player can use it to disable RFA.
|
||||
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
||||
return { success: false, message: "Invalid port" };
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export function loadSettings(saveString: string) {
|
||||
const save: unknown = JSON.parse(saveString);
|
||||
assertObject(save);
|
||||
save.overview && Object.assign(Settings.overview, save.overview);
|
||||
try {
|
||||
// Sanitize theme data. Invalid theme data may crash the game or make it stuck in the loading page.
|
||||
assertAndSanitizeMainTheme(save.theme);
|
||||
Object.assign(Settings.theme, save.theme);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
// Sanitize editor theme data. Invalid editor theme data may crash the game when the player opens the script editor.
|
||||
assertAndSanitizeEditorTheme(save.EditorTheme);
|
||||
Object.assign(Settings.EditorTheme, save.EditorTheme);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
// Sanitize styles.
|
||||
assertAndSanitizeStyles(save.styles);
|
||||
Object.assign(Settings.styles, save.styles);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
/**
|
||||
* KeyBindings data does not exist in old save files. Technically, this check is unnecessary. If KeyBindings is
|
||||
* undefined, assertAndSanitizeKeyBindings will throw an error, and that error will be caught here. However, it
|
||||
* means that there will be an error logged in the console every time the player loads an old save file, and this
|
||||
* logged error is kind of a "false positive" one.
|
||||
*/
|
||||
if (save.KeyBindings !== undefined) {
|
||||
try {
|
||||
// Sanitize key bindings.
|
||||
assertAndSanitizeKeyBindings(save.KeyBindings);
|
||||
Object.assign(Settings.KeyBindings, save.KeyBindings);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
Object.assign(Settings, save, {
|
||||
overview: Settings.overview,
|
||||
theme: Settings.theme,
|
||||
EditorTheme: Settings.EditorTheme,
|
||||
styles: Settings.styles,
|
||||
KeyBindings: Settings.KeyBindings,
|
||||
});
|
||||
/**
|
||||
* The hostname and port of RFA have not been validated properly, so the save data may contain invalid data. In that
|
||||
* case, we set them to the default value.
|
||||
*/
|
||||
if (!isValidConnectionHostname(Settings.RemoteFileApiAddress).success) {
|
||||
Settings.RemoteFileApiAddress = "localhost";
|
||||
}
|
||||
if (!isValidConnectionPort(Settings.RemoteFileApiPort).success) {
|
||||
Settings.RemoteFileApiPort = 0;
|
||||
}
|
||||
|
||||
// Merge Settings.KeyBindings with DefaultKeyBindings.
|
||||
mergePlayerDefinedKeyBindings(Settings.KeyBindings);
|
||||
|
||||
// Set up initial state for error modal suppression
|
||||
toggleSuppressErrorModals(Settings.SuppressErrorModals, true);
|
||||
|
||||
// Disable this feature for existing save files.
|
||||
if (save.MonacoAutoSaveOnFocusChange === undefined) {
|
||||
Settings.MonacoAutoSaveOnFocusChange = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user