Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REFACTOR: Mitigate cyclic dependency between Jsonable classes #1792

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Company/Companies.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/CotMG/Helper.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/Electron.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
Expand Down
2 changes: 1 addition & 1 deletion src/Faction/Factions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/Gang/AllGangs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FactionName } from "@enums";
import { Reviver } from "../utils/JSONReviver";
import { Reviver } from "../utils/GenericReviver";

interface GangTerritory {
power: number;
Expand Down
41 changes: 0 additions & 41 deletions src/Netscript/ErrorMessages.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion src/Netscript/killWorkerScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/NetscriptWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
2 changes: 1 addition & 1 deletion src/PersonObjects/Player/PlayerObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/Player.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
5 changes: 3 additions & 2 deletions src/SaveObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Server/AllServers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/StockMarket/StockMarket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/UncaughtPromiseHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { handleUnknownError } from "./Netscript/ErrorMessages";
import { handleUnknownError } from "./utils/ErrorHandler";

export function setupUncaughtPromiseHandler(): void {
window.addEventListener("unhandledrejection", (e) => {
Expand Down
2 changes: 1 addition & 1 deletion src/ui/React/AlertManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]>();

Expand Down
2 changes: 1 addition & 1 deletion src/ui/React/ImportSave/ImportSave.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
44 changes: 44 additions & 0 deletions src/utils/ErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -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);
}
2 changes: 1 addition & 1 deletion src/utils/ErrorHelper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import type React from "react";

import type { Page } from "../ui/Router";
import { commitHash } from "./helpers/commitHash";
Expand Down
37 changes: 37 additions & 0 deletions src/utils/GenericReviver.ts
Original file line number Diff line number Diff line change
@@ -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);
catloversg marked this conversation as resolved.
Show resolved Hide resolved
}
// 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;
}
19 changes: 19 additions & 0 deletions src/utils/HashUtils.ts
Original file line number Diff line number Diff line change
@@ -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);
}
37 changes: 2 additions & 35 deletions src/utils/JSONReviver.ts
Original file line number Diff line number Diff line change
@@ -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 }) & {
Expand All @@ -14,44 +13,12 @@ export interface IReviverValue<T = unknown> {
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<Record<string, JsonableClass>> = { JSONSet, JSONMap };

/**
Expand Down
20 changes: 0 additions & 20 deletions src/utils/StringHelperFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/utils/helpers/exceptionAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();
Expand Down