Skip to content

Commit

Permalink
REFACTOR: Mitigate cyclic dependency between Jsonable classes (bitbur…
Browse files Browse the repository at this point in the history
  • Loading branch information
catloversg authored and antoinedube committed Dec 2, 2024

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
1 parent 4079c3c commit 01fa3fe
Showing 23 changed files with 121 additions and 114 deletions.
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";
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";
2 changes: 1 addition & 1 deletion src/Electron.tsx
Original file line number Diff line number Diff line change
@@ -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>;
2 changes: 1 addition & 1 deletion src/Faction/Factions.ts
Original file line number Diff line number Diff line change
@@ -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";
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;
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) {
@@ -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
@@ -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 {
2 changes: 1 addition & 1 deletion src/NetscriptWorker.ts
Original file line number Diff line number Diff line change
@@ -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";

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

5 changes: 3 additions & 2 deletions src/SaveObject.ts
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/Server/AllServers.ts
Original file line number Diff line number Diff line change
@@ -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";
2 changes: 1 addition & 1 deletion src/StockMarket/StockMarket.tsx
Original file line number Diff line number Diff line change
@@ -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";
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) => {
2 changes: 1 addition & 1 deletion src/ui/React/AlertManager.tsx
Original file line number Diff line number Diff line change
@@ -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]>();

2 changes: 1 addition & 1 deletion src/ui/React/ImportSave/ImportSave.tsx
Original file line number Diff line number Diff line change
@@ -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: {
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";
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);
}
// 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 }) & {
@@ -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 };

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

0 comments on commit 01fa3fe

Please sign in to comment.