diff --git a/src/Company/Companies.ts b/src/Company/Companies.ts index f8666c552..c555f4376 100644 --- a/src/Company/Companies.ts +++ b/src/Company/Companies.ts @@ -1,7 +1,7 @@ // Constructs all CompanyPosition objects using the metadata in data/companypositions.ts import { getCompaniesMetadata } from "./data/CompaniesMetadata"; import { Company } from "./Company"; -import { Reviver } from "../utils/JSONReviver"; +import { Reviver } from "../utils/GenericReviver"; import { assertLoadingType } from "../utils/TypeAssertion"; import { CompanyName } from "./Enums"; import { PartialRecord, createEnumKeyedRecord } from "../Types/Record"; diff --git a/src/CotMG/Helper.tsx b/src/CotMG/Helper.tsx index 12227c791..6d37aba99 100644 --- a/src/CotMG/Helper.tsx +++ b/src/CotMG/Helper.tsx @@ -1,5 +1,5 @@ import { dialogBoxCreate } from "../ui/React/DialogBox"; -import { Reviver } from "../utils/JSONReviver"; +import { Reviver } from "../utils/GenericReviver"; import { BaseGift } from "./BaseGift"; import { StaneksGift } from "./StaneksGift"; diff --git a/src/Electron.tsx b/src/Electron.tsx index 88659f06b..58176cebe 100644 --- a/src/Electron.tsx +++ b/src/Electron.tsx @@ -12,7 +12,7 @@ import { CONSTANTS } from "./Constants"; import { commitHash } from "./utils/helpers/commitHash"; import { resolveFilePath } from "./Paths/FilePath"; import { hasScriptExtension } from "./Paths/ScriptFilePath"; -import { handleGetSaveDataInfoError } from "./Netscript/ErrorMessages"; +import { handleGetSaveDataInfoError } from "./utils/ErrorHandler"; interface IReturnWebStatus extends IReturnStatus { data?: Record; diff --git a/src/Faction/Factions.ts b/src/Faction/Factions.ts index 1994e994c..33cb9f26d 100644 --- a/src/Faction/Factions.ts +++ b/src/Faction/Factions.ts @@ -3,7 +3,7 @@ import type { PlayerObject } from "../PersonObjects/Player/PlayerObject"; import { FactionName, FactionDiscovery } from "@enums"; import { Faction } from "./Faction"; -import { Reviver } from "../utils/JSONReviver"; +import { Reviver } from "../utils/GenericReviver"; import { assertLoadingType } from "../utils/TypeAssertion"; import { PartialRecord, createEnumKeyedRecord, getRecordValues } from "../Types/Record"; import { Augmentations } from "../Augmentation/Augmentations"; diff --git a/src/Gang/AllGangs.ts b/src/Gang/AllGangs.ts index 68b03c6aa..7115ae7bf 100644 --- a/src/Gang/AllGangs.ts +++ b/src/Gang/AllGangs.ts @@ -1,5 +1,5 @@ import { FactionName } from "@enums"; -import { Reviver } from "../utils/JSONReviver"; +import { Reviver } from "../utils/GenericReviver"; interface GangTerritory { power: number; diff --git a/src/Netscript/ErrorMessages.ts b/src/Netscript/ErrorMessages.ts index 624e4c80e..8064140f3 100644 --- a/src/Netscript/ErrorMessages.ts +++ b/src/Netscript/ErrorMessages.ts @@ -1,7 +1,6 @@ import type { WorkerScript } from "./WorkerScript"; import { ScriptDeath } from "./ScriptDeath"; import type { NetscriptContext } from "./APIWrapper"; -import { dialogBoxCreate } from "../ui/React/DialogBox"; /** Log a message to a script's logs */ export function log(ctx: NetscriptContext, message: () => string) { @@ -74,43 +73,3 @@ export function errorMessage(ctx: NetscriptContext, msg: string, type = "RUNTIME return null; } } - -/** Generate an error dialog when workerscript is known */ -export function handleUnknownError(e: unknown, ws: WorkerScript | null = null, initialText = "") { - if (e instanceof ScriptDeath) { - // No dialog for ScriptDeath - return; - } - if (ws && typeof e === "string") { - const headerText = basicErrorMessage(ws, "", ""); - if (!e.includes(headerText)) e = basicErrorMessage(ws, e); - } else if (e instanceof SyntaxError) { - const msg = `${e.message} (sorry we can't be more helpful)`; - e = ws ? basicErrorMessage(ws, msg, "SYNTAX") : `SYNTAX ERROR:\n\n${msg}`; - } else if (e instanceof Error) { - // Ignore any cancellation errors from Monaco that get here - if (e.name === "Canceled" && e.message === "Canceled") return; - const msg = `${e.message}${e.stack ? `\nstack:\n${e.stack.toString()}` : ""}`; - e = ws ? basicErrorMessage(ws, msg) : `RUNTIME ERROR:\n\n${msg}`; - } - if (typeof e !== "string") { - console.error("Unexpected error:", e); - const msg = `Unexpected type of error thrown. This error was likely thrown manually within a script. - Error has been logged to the console.\n\nType of error: ${typeof e}\nValue of error: ${e}`; - e = ws ? basicErrorMessage(ws, msg, "UNKNOWN") : msg; - } - dialogBoxCreate(initialText + String(e)); -} - -/** Use this handler to handle the error when we call getSaveData function or getSaveInfo function */ -export function handleGetSaveDataInfoError(error: unknown, fromGetSaveInfo = false) { - console.error(error); - let errorMessage = `Cannot get save ${fromGetSaveInfo ? "info" : "data"}. Error: ${error}.`; - if (error instanceof RangeError) { - errorMessage += " This may be because the save data is too large."; - } - if (error instanceof Error && error.stack) { - errorMessage += `\nStack:\n${error.stack}`; - } - dialogBoxCreate(errorMessage); -} diff --git a/src/Netscript/killWorkerScript.ts b/src/Netscript/killWorkerScript.ts index 5090ee77b..bbb459dbd 100644 --- a/src/Netscript/killWorkerScript.ts +++ b/src/Netscript/killWorkerScript.ts @@ -10,7 +10,7 @@ import { GetServer } from "../Server/AllServers"; import { AddRecentScript } from "./RecentScripts"; import { ITutorial } from "../InteractiveTutorial"; import { AlertEvents } from "../ui/React/AlertManager"; -import { handleUnknownError } from "./ErrorMessages"; +import { handleUnknownError } from "../utils/ErrorHandler"; import { roundToTwo } from "../utils/helpers/roundToTwo"; export function killWorkerScript(ws: WorkerScript): boolean { diff --git a/src/NetscriptWorker.ts b/src/NetscriptWorker.ts index 2d3b4c023..1323cefd5 100644 --- a/src/NetscriptWorker.ts +++ b/src/NetscriptWorker.ts @@ -34,7 +34,7 @@ import { parseCommand } from "./Terminal/Parser"; import { Terminal } from "./Terminal"; import { ScriptArg } from "@nsdefs"; import { CompleteRunOptions, getRunningScriptsByArgs } from "./Netscript/NetscriptHelpers"; -import { handleUnknownError } from "./Netscript/ErrorMessages"; +import { handleUnknownError } from "./utils/ErrorHandler"; import { isLegacyScript, legacyScriptExtension, resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath"; import { root } from "./Paths/Directory"; diff --git a/src/PersonObjects/Player/PlayerObject.ts b/src/PersonObjects/Player/PlayerObject.ts index 65ece9e95..21ec07439 100644 --- a/src/PersonObjects/Player/PlayerObject.ts +++ b/src/PersonObjects/Player/PlayerObject.ts @@ -22,7 +22,7 @@ import { HashManager } from "../../Hacknet/HashManager"; import { type MoneySource, MoneySourceTracker } from "../../utils/MoneySourceTracker"; import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../../utils/JSONReviver"; import { JSONMap, JSONSet } from "../../Types/Jsonable"; -import { cyrb53 } from "../../utils/StringHelperFunctions"; +import { cyrb53 } from "../../utils/HashUtils"; import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive"; import { CONSTANTS } from "../../Constants"; import { Person } from "../Person"; diff --git a/src/Player.ts b/src/Player.ts index 1deb455c5..0cecec30c 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -1,6 +1,6 @@ import { sanitizeExploits } from "./Exploits/Exploit"; -import { Reviver } from "./utils/JSONReviver"; +import { Reviver } from "./utils/GenericReviver"; import type { PlayerObject } from "./PersonObjects/Player/PlayerObject"; diff --git a/src/SaveObject.ts b/src/SaveObject.ts index d72fe94c4..f45b0102d 100644 --- a/src/SaveObject.ts +++ b/src/SaveObject.ts @@ -24,7 +24,7 @@ import { SnackbarEvents } from "./ui/React/Snackbar"; import * as ExportBonus from "./ExportBonus"; import { dialogBoxCreate } from "./ui/React/DialogBox"; -import { Reviver, constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "./utils/JSONReviver"; +import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, type IReviverValue } from "./utils/JSONReviver"; import { save } from "./db"; import { AwardNFG, v1APIBreak } from "./utils/v1APIBreak"; import { AugmentationName, LocationName, ToastVariant } from "@enums"; @@ -45,8 +45,9 @@ import { isBinaryFormat } from "../electron/saveDataBinaryFormat"; import { downloadContentAsFile } from "./utils/FileUtils"; import { showAPIBreaks } from "./utils/APIBreaks/APIBreak"; import { breakInfos261 } from "./utils/APIBreaks/2.6.1"; -import { handleGetSaveDataInfoError } from "./Netscript/ErrorMessages"; +import { handleGetSaveDataInfoError } from "./utils/ErrorHandler"; import { isObject } from "./utils/helpers/typeAssertion"; +import { Reviver } from "./utils/GenericReviver"; /* SaveObject.js * Defines the object used to save/load games diff --git a/src/Server/AllServers.ts b/src/Server/AllServers.ts index 2d3fd26d3..f2c5ba30b 100644 --- a/src/Server/AllServers.ts +++ b/src/Server/AllServers.ts @@ -7,7 +7,7 @@ import { HacknetServer } from "../Hacknet/HacknetServer"; import { IMinMaxRange } from "../types"; import { createRandomIp } from "../utils/IPAddress"; import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive"; -import { Reviver } from "../utils/JSONReviver"; +import { Reviver } from "../utils/GenericReviver"; import { SpecialServers } from "./data/SpecialServers"; import { currentNodeMults } from "../BitNode/BitNodeMultipliers"; import { IPAddress, isIPAddress } from "../Types/strings"; diff --git a/src/StockMarket/StockMarket.tsx b/src/StockMarket/StockMarket.tsx index 97018bb8d..1e2d016fa 100644 --- a/src/StockMarket/StockMarket.tsx +++ b/src/StockMarket/StockMarket.tsx @@ -12,7 +12,7 @@ import { CONSTANTS } from "../Constants"; import { formatMoney } from "../ui/formatNumber"; import { dialogBoxCreate } from "../ui/React/DialogBox"; -import { Reviver } from "../utils/JSONReviver"; +import { Reviver } from "../utils/GenericReviver"; import { NetscriptContext } from "../Netscript/APIWrapper"; import { helpers } from "../Netscript/NetscriptHelpers"; import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive"; diff --git a/src/UncaughtPromiseHandler.ts b/src/UncaughtPromiseHandler.ts index 21f102c68..1ace614f9 100644 --- a/src/UncaughtPromiseHandler.ts +++ b/src/UncaughtPromiseHandler.ts @@ -1,4 +1,4 @@ -import { handleUnknownError } from "./Netscript/ErrorMessages"; +import { handleUnknownError } from "./utils/ErrorHandler"; export function setupUncaughtPromiseHandler(): void { window.addEventListener("unhandledrejection", (e) => { diff --git a/src/ui/React/AlertManager.tsx b/src/ui/React/AlertManager.tsx index 736ce5a01..b0603859b 100644 --- a/src/ui/React/AlertManager.tsx +++ b/src/ui/React/AlertManager.tsx @@ -3,7 +3,7 @@ import { EventEmitter } from "../../utils/EventEmitter"; import { Modal } from "./Modal"; import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; -import { cyrb53 } from "../../utils/StringHelperFunctions"; +import { cyrb53 } from "../../utils/HashUtils"; export const AlertEvents = new EventEmitter<[string | JSX.Element]>(); diff --git a/src/ui/React/ImportSave/ImportSave.tsx b/src/ui/React/ImportSave/ImportSave.tsx index 9bfe818e4..fa576fa09 100644 --- a/src/ui/React/ImportSave/ImportSave.tsx +++ b/src/ui/React/ImportSave/ImportSave.tsx @@ -38,7 +38,7 @@ import { useBoolean } from "../hooks"; import { ComparisonIcon } from "./ComparisonIcon"; import { SaveData } from "../../../types"; -import { handleGetSaveDataInfoError } from "../../../Netscript/ErrorMessages"; +import { handleGetSaveDataInfoError } from "../../../utils/ErrorHandler"; const useStyles = makeStyles()((theme: Theme) => ({ root: { diff --git a/src/utils/ErrorHandler.ts b/src/utils/ErrorHandler.ts new file mode 100644 index 000000000..86bcbe268 --- /dev/null +++ b/src/utils/ErrorHandler.ts @@ -0,0 +1,44 @@ +import { basicErrorMessage } from "../Netscript/ErrorMessages"; +import { ScriptDeath } from "../Netscript/ScriptDeath"; +import type { WorkerScript } from "../Netscript/WorkerScript"; +import { dialogBoxCreate } from "../ui/React/DialogBox"; + +/** Generate an error dialog when workerscript is known */ +export function handleUnknownError(e: unknown, ws: WorkerScript | null = null, initialText = "") { + if (e instanceof ScriptDeath) { + // No dialog for ScriptDeath + return; + } + if (ws && typeof e === "string") { + const headerText = basicErrorMessage(ws, "", ""); + if (!e.includes(headerText)) e = basicErrorMessage(ws, e); + } else if (e instanceof SyntaxError) { + const msg = `${e.message} (sorry we can't be more helpful)`; + e = ws ? basicErrorMessage(ws, msg, "SYNTAX") : `SYNTAX ERROR:\n\n${msg}`; + } else if (e instanceof Error) { + // Ignore any cancellation errors from Monaco that get here + if (e.name === "Canceled" && e.message === "Canceled") return; + const msg = `${e.message}${e.stack ? `\nstack:\n${e.stack.toString()}` : ""}`; + e = ws ? basicErrorMessage(ws, msg) : `RUNTIME ERROR:\n\n${msg}`; + } + if (typeof e !== "string") { + console.error("Unexpected error:", e); + const msg = `Unexpected type of error thrown. This error was likely thrown manually within a script. + Error has been logged to the console.\n\nType of error: ${typeof e}\nValue of error: ${e}`; + e = ws ? basicErrorMessage(ws, msg, "UNKNOWN") : msg; + } + dialogBoxCreate(initialText + String(e)); +} + +/** Use this handler to handle the error when we call getSaveData function or getSaveInfo function */ +export function handleGetSaveDataInfoError(error: unknown, fromGetSaveInfo = false) { + console.error(error); + let errorMessage = `Cannot get save ${fromGetSaveInfo ? "info" : "data"}. Error: ${error}.`; + if (error instanceof RangeError) { + errorMessage += " This may be because the save data is too large."; + } + if (error instanceof Error && error.stack) { + errorMessage += `\nStack:\n${error.stack}`; + } + dialogBoxCreate(errorMessage); +} diff --git a/src/utils/ErrorHelper.ts b/src/utils/ErrorHelper.ts index 91443dc66..1d005c44a 100644 --- a/src/utils/ErrorHelper.ts +++ b/src/utils/ErrorHelper.ts @@ -1,4 +1,4 @@ -import React from "react"; +import type React from "react"; import type { Page } from "../ui/Router"; import { commitHash } from "./helpers/commitHash"; diff --git a/src/utils/GenericReviver.ts b/src/utils/GenericReviver.ts new file mode 100644 index 000000000..f2605d85d --- /dev/null +++ b/src/utils/GenericReviver.ts @@ -0,0 +1,37 @@ +import { loadActionIdentifier } from "../Bladeburner/utils/loadActionIdentifier"; +import { constructorsForReviver, isReviverValue } from "./JSONReviver"; +import { validateObject } from "./Validator"; + +/** + * A generic "smart reviver" function. + * Looks for object values with a `ctor` property and a `data` property. + * If it finds them, and finds a matching constructor, it hands + * off to that `fromJSON` function, passing in the value. */ +export function Reviver(_key: string, value: unknown): any { + if (!isReviverValue(value)) { + return value; + } + const ctor = constructorsForReviver[value.ctor]; + if (!ctor) { + // Known missing constructors with special handling. + switch (value.ctor) { + case "AllServersMap": // Reviver removed in v0.43.1 + case "Industry": // No longer part of save data since v2.3.0 + case "Employee": // Entire object removed from game in v2.2.0 (employees abstracted) + case "Company": // Reviver removed in v2.6.1 + case "Faction": // Reviver removed in v2.6.1 + console.warn(`Legacy load type ${value.ctor} converted to expected format while loading.`); + return value.data; + case "ActionIdentifier": // No longer a class as of v2.6.1 + return loadActionIdentifier(value.data); + } + // Missing constructor with no special handling. Throw error. + throw new Error(`Could not locate constructor named ${value.ctor}. If the save data is valid, this is a bug.`); + } + + const obj = ctor.fromJSON(value); + if (ctor.validationData !== undefined) { + validateObject(obj, ctor.validationData); + } + return obj; +} diff --git a/src/utils/HashUtils.ts b/src/utils/HashUtils.ts new file mode 100644 index 000000000..c786b5047 --- /dev/null +++ b/src/utils/HashUtils.ts @@ -0,0 +1,19 @@ +/** + * Hashes the input string. This is a fast hash, so NOT good for cryptography. + * This has been ripped off here: https://stackoverflow.com/a/52171480 + * @param str The string that is to be hashed + * @param seed A seed to randomize the result + * @returns An hexadecimal string representation of the hashed input + */ +export function cyrb53(str: string, seed = 0): string { + let h1 = 0xdeadbeef ^ seed; + let h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); + return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16); +} diff --git a/src/utils/JSONReviver.ts b/src/utils/JSONReviver.ts index c1736259b..9c015c3b4 100644 --- a/src/utils/JSONReviver.ts +++ b/src/utils/JSONReviver.ts @@ -1,7 +1,6 @@ /* Generic Reviver, toJSON, and fromJSON functions used for saving and loading objects */ -import { ObjectValidator, validateObject } from "./Validator"; +import { ObjectValidator } from "./Validator"; import { JSONMap, JSONSet } from "../Types/Jsonable"; -import { loadActionIdentifier } from "../Bladeburner/utils/loadActionIdentifier"; import { objectAssert } from "./helpers/typeAssertion"; type JsonableClass = (new () => { toJSON: () => IReviverValue }) & { @@ -14,44 +13,12 @@ export interface IReviverValue { data: T; } -function isReviverValue(value: unknown): value is IReviverValue { +export function isReviverValue(value: unknown): value is IReviverValue { return ( typeof value === "object" && value !== null && "ctor" in value && typeof value.ctor === "string" && "data" in value ); } -/** - * A generic "smart reviver" function. - * Looks for object values with a `ctor` property and a `data` property. - * If it finds them, and finds a matching constructor, it hands - * off to that `fromJSON` function, passing in the value. */ -export function Reviver(_key: string, value: unknown): any { - if (!isReviverValue(value)) return value; - const ctor = constructorsForReviver[value.ctor]; - if (!ctor) { - // Known missing constructors with special handling. - switch (value.ctor) { - case "AllServersMap": // Reviver removed in v0.43.1 - case "Industry": // No longer part of save data since v2.3.0 - case "Employee": // Entire object removed from game in v2.2.0 (employees abstracted) - case "Company": // Reviver removed in v2.6.1 - case "Faction": // Reviver removed in v2.6.1 - console.warn(`Legacy load type ${value.ctor} converted to expected format while loading.`); - return value.data; - case "ActionIdentifier": // No longer a class as of v2.6.1 - return loadActionIdentifier(value.data); - } - // Missing constructor with no special handling. Throw error. - throw new Error(`Could not locate constructor named ${value.ctor}. If the save data is valid, this is a bug.`); - } - - const obj = ctor.fromJSON(value); - if (ctor.validationData !== undefined) { - validateObject(obj, ctor.validationData); - } - return obj; -} - export const constructorsForReviver: Partial> = { JSONSet, JSONMap }; /** diff --git a/src/utils/StringHelperFunctions.ts b/src/utils/StringHelperFunctions.ts index 0c00766c4..b77c41426 100644 --- a/src/utils/StringHelperFunctions.ts +++ b/src/utils/StringHelperFunctions.ts @@ -89,26 +89,6 @@ export function generateRandomString(n: number): string { return str; } -/** - * Hashes the input string. This is a fast hash, so NOT good for cryptography. - * This has been ripped off here: https://stackoverflow.com/a/52171480 - * @param str The string that is to be hashed - * @param seed A seed to randomize the result - * @returns An hexadecimal string representation of the hashed input - */ -export function cyrb53(str: string, seed = 0): string { - let h1 = 0xdeadbeef ^ seed; - let h2 = 0x41c6ce57 ^ seed; - for (let i = 0, ch; i < str.length; i++) { - ch = str.charCodeAt(i); - h1 = Math.imul(h1 ^ ch, 2654435761); - h2 = Math.imul(h2 ^ ch, 1597334677); - } - h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); - h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); - return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16); -} - export function capitalizeFirstLetter(s: string): string { return s.charAt(0).toUpperCase() + s.slice(1); } diff --git a/src/utils/helpers/exceptionAlert.tsx b/src/utils/helpers/exceptionAlert.tsx index e70f58d2e..77c7dd2e1 100644 --- a/src/utils/helpers/exceptionAlert.tsx +++ b/src/utils/helpers/exceptionAlert.tsx @@ -2,7 +2,7 @@ import React from "react"; import { dialogBoxCreate } from "../../ui/React/DialogBox"; import Typography from "@mui/material/Typography"; import { parseUnknownError } from "../ErrorHelper"; -import { cyrb53 } from "../StringHelperFunctions"; +import { cyrb53 } from "../HashUtils"; import { commitHash } from "./commitHash"; const errorSet = new Set();