REFACTOR: Mitigate cyclic dependency between Jsonable classes (#1792)

This commit is contained in:
catloversg 2024-11-24 06:53:31 +07:00 committed by GitHub
parent 45a6ca6b8e
commit 05da0efc81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 121 additions and 114 deletions

@ -1,7 +1,7 @@
// Constructs all CompanyPosition objects using the metadata in data/companypositions.ts // Constructs all CompanyPosition objects using the metadata in data/companypositions.ts
import { getCompaniesMetadata } from "./data/CompaniesMetadata"; import { getCompaniesMetadata } from "./data/CompaniesMetadata";
import { Company } from "./Company"; import { Company } from "./Company";
import { Reviver } from "../utils/JSONReviver"; import { Reviver } from "../utils/GenericReviver";
import { assertLoadingType } from "../utils/TypeAssertion"; import { assertLoadingType } from "../utils/TypeAssertion";
import { CompanyName } from "./Enums"; import { CompanyName } from "./Enums";
import { PartialRecord, createEnumKeyedRecord } from "../Types/Record"; import { PartialRecord, createEnumKeyedRecord } from "../Types/Record";

@ -1,5 +1,5 @@
import { dialogBoxCreate } from "../ui/React/DialogBox"; import { dialogBoxCreate } from "../ui/React/DialogBox";
import { Reviver } from "../utils/JSONReviver"; import { Reviver } from "../utils/GenericReviver";
import { BaseGift } from "./BaseGift"; import { BaseGift } from "./BaseGift";
import { StaneksGift } from "./StaneksGift"; import { StaneksGift } from "./StaneksGift";

@ -12,7 +12,7 @@ import { CONSTANTS } from "./Constants";
import { commitHash } from "./utils/helpers/commitHash"; import { commitHash } from "./utils/helpers/commitHash";
import { resolveFilePath } from "./Paths/FilePath"; import { resolveFilePath } from "./Paths/FilePath";
import { hasScriptExtension } from "./Paths/ScriptFilePath"; import { hasScriptExtension } from "./Paths/ScriptFilePath";
import { handleGetSaveDataInfoError } from "./Netscript/ErrorMessages"; import { handleGetSaveDataInfoError } from "./utils/ErrorHandler";
interface IReturnWebStatus extends IReturnStatus { interface IReturnWebStatus extends IReturnStatus {
data?: Record<string, unknown>; data?: Record<string, unknown>;

@ -3,7 +3,7 @@ import type { PlayerObject } from "../PersonObjects/Player/PlayerObject";
import { FactionName, FactionDiscovery } from "@enums"; import { FactionName, FactionDiscovery } from "@enums";
import { Faction } from "./Faction"; import { Faction } from "./Faction";
import { Reviver } from "../utils/JSONReviver"; import { Reviver } from "../utils/GenericReviver";
import { assertLoadingType } from "../utils/TypeAssertion"; import { assertLoadingType } from "../utils/TypeAssertion";
import { PartialRecord, createEnumKeyedRecord, getRecordValues } from "../Types/Record"; import { PartialRecord, createEnumKeyedRecord, getRecordValues } from "../Types/Record";
import { Augmentations } from "../Augmentation/Augmentations"; import { Augmentations } from "../Augmentation/Augmentations";

@ -1,5 +1,5 @@
import { FactionName } from "@enums"; import { FactionName } from "@enums";
import { Reviver } from "../utils/JSONReviver"; import { Reviver } from "../utils/GenericReviver";
interface GangTerritory { interface GangTerritory {
power: number; power: number;

@ -1,7 +1,6 @@
import type { WorkerScript } from "./WorkerScript"; import type { WorkerScript } from "./WorkerScript";
import { ScriptDeath } from "./ScriptDeath"; import { ScriptDeath } from "./ScriptDeath";
import type { NetscriptContext } from "./APIWrapper"; import type { NetscriptContext } from "./APIWrapper";
import { dialogBoxCreate } from "../ui/React/DialogBox";
/** Log a message to a script's logs */ /** Log a message to a script's logs */
export function log(ctx: NetscriptContext, message: () => string) { export function log(ctx: NetscriptContext, message: () => string) {
@ -74,43 +73,3 @@ export function errorMessage(ctx: NetscriptContext, msg: string, type = "RUNTIME
return null; 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);
}

@ -10,7 +10,7 @@ import { GetServer } from "../Server/AllServers";
import { AddRecentScript } from "./RecentScripts"; import { AddRecentScript } from "./RecentScripts";
import { ITutorial } from "../InteractiveTutorial"; import { ITutorial } from "../InteractiveTutorial";
import { AlertEvents } from "../ui/React/AlertManager"; import { AlertEvents } from "../ui/React/AlertManager";
import { handleUnknownError } from "./ErrorMessages"; import { handleUnknownError } from "../utils/ErrorHandler";
import { roundToTwo } from "../utils/helpers/roundToTwo"; import { roundToTwo } from "../utils/helpers/roundToTwo";
export function killWorkerScript(ws: WorkerScript): boolean { export function killWorkerScript(ws: WorkerScript): boolean {

@ -34,7 +34,7 @@ import { parseCommand } from "./Terminal/Parser";
import { Terminal } from "./Terminal"; import { Terminal } from "./Terminal";
import { ScriptArg } from "@nsdefs"; import { ScriptArg } from "@nsdefs";
import { CompleteRunOptions, getRunningScriptsByArgs } from "./Netscript/NetscriptHelpers"; 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 { isLegacyScript, legacyScriptExtension, resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath";
import { root } from "./Paths/Directory"; import { root } from "./Paths/Directory";

@ -22,7 +22,7 @@ import { HashManager } from "../../Hacknet/HashManager";
import { type MoneySource, MoneySourceTracker } from "../../utils/MoneySourceTracker"; import { type MoneySource, MoneySourceTracker } from "../../utils/MoneySourceTracker";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../../utils/JSONReviver"; import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../../utils/JSONReviver";
import { JSONMap, JSONSet } from "../../Types/Jsonable"; import { JSONMap, JSONSet } from "../../Types/Jsonable";
import { cyrb53 } from "../../utils/StringHelperFunctions"; import { cyrb53 } from "../../utils/HashUtils";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive"; import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
import { CONSTANTS } from "../../Constants"; import { CONSTANTS } from "../../Constants";
import { Person } from "../Person"; import { Person } from "../Person";

@ -1,6 +1,6 @@
import { sanitizeExploits } from "./Exploits/Exploit"; import { sanitizeExploits } from "./Exploits/Exploit";
import { Reviver } from "./utils/JSONReviver"; import { Reviver } from "./utils/GenericReviver";
import type { PlayerObject } from "./PersonObjects/Player/PlayerObject"; import type { PlayerObject } from "./PersonObjects/Player/PlayerObject";

@ -24,7 +24,7 @@ import { SnackbarEvents } from "./ui/React/Snackbar";
import * as ExportBonus from "./ExportBonus"; import * as ExportBonus from "./ExportBonus";
import { dialogBoxCreate } from "./ui/React/DialogBox"; 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 { save } from "./db";
import { AwardNFG, v1APIBreak } from "./utils/v1APIBreak"; import { AwardNFG, v1APIBreak } from "./utils/v1APIBreak";
import { AugmentationName, LocationName, ToastVariant } from "@enums"; import { AugmentationName, LocationName, ToastVariant } from "@enums";
@ -45,8 +45,9 @@ import { isBinaryFormat } from "../electron/saveDataBinaryFormat";
import { downloadContentAsFile } from "./utils/FileUtils"; import { downloadContentAsFile } from "./utils/FileUtils";
import { showAPIBreaks } from "./utils/APIBreaks/APIBreak"; import { showAPIBreaks } from "./utils/APIBreaks/APIBreak";
import { breakInfos261 } from "./utils/APIBreaks/2.6.1"; 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 { isObject } from "./utils/helpers/typeAssertion";
import { Reviver } from "./utils/GenericReviver";
/* SaveObject.js /* SaveObject.js
* Defines the object used to save/load games * Defines the object used to save/load games

@ -7,7 +7,7 @@ import { HacknetServer } from "../Hacknet/HacknetServer";
import { IMinMaxRange } from "../types"; import { IMinMaxRange } from "../types";
import { createRandomIp } from "../utils/IPAddress"; import { createRandomIp } from "../utils/IPAddress";
import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive"; import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive";
import { Reviver } from "../utils/JSONReviver"; import { Reviver } from "../utils/GenericReviver";
import { SpecialServers } from "./data/SpecialServers"; import { SpecialServers } from "./data/SpecialServers";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers"; import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { IPAddress, isIPAddress } from "../Types/strings"; import { IPAddress, isIPAddress } from "../Types/strings";

@ -12,7 +12,7 @@ import { CONSTANTS } from "../Constants";
import { formatMoney } from "../ui/formatNumber"; import { formatMoney } from "../ui/formatNumber";
import { dialogBoxCreate } from "../ui/React/DialogBox"; import { dialogBoxCreate } from "../ui/React/DialogBox";
import { Reviver } from "../utils/JSONReviver"; import { Reviver } from "../utils/GenericReviver";
import { NetscriptContext } from "../Netscript/APIWrapper"; import { NetscriptContext } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers"; import { helpers } from "../Netscript/NetscriptHelpers";
import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive"; import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive";

@ -1,4 +1,4 @@
import { handleUnknownError } from "./Netscript/ErrorMessages"; import { handleUnknownError } from "./utils/ErrorHandler";
export function setupUncaughtPromiseHandler(): void { export function setupUncaughtPromiseHandler(): void {
window.addEventListener("unhandledrejection", (e) => { window.addEventListener("unhandledrejection", (e) => {

@ -3,7 +3,7 @@ import { EventEmitter } from "../../utils/EventEmitter";
import { Modal } from "./Modal"; import { Modal } from "./Modal";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { cyrb53 } from "../../utils/StringHelperFunctions"; import { cyrb53 } from "../../utils/HashUtils";
export const AlertEvents = new EventEmitter<[string | JSX.Element]>(); export const AlertEvents = new EventEmitter<[string | JSX.Element]>();

@ -38,7 +38,7 @@ import { useBoolean } from "../hooks";
import { ComparisonIcon } from "./ComparisonIcon"; import { ComparisonIcon } from "./ComparisonIcon";
import { SaveData } from "../../../types"; import { SaveData } from "../../../types";
import { handleGetSaveDataInfoError } from "../../../Netscript/ErrorMessages"; import { handleGetSaveDataInfoError } from "../../../utils/ErrorHandler";
const useStyles = makeStyles()((theme: Theme) => ({ const useStyles = makeStyles()((theme: Theme) => ({
root: { root: {

44
src/utils/ErrorHandler.ts Normal file

@ -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);
}

@ -1,4 +1,4 @@
import React from "react"; import type React from "react";
import type { Page } from "../ui/Router"; import type { Page } from "../ui/Router";
import { commitHash } from "./helpers/commitHash"; import { commitHash } from "./helpers/commitHash";

@ -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
src/utils/HashUtils.ts Normal file

@ -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);
}

@ -1,7 +1,6 @@
/* Generic Reviver, toJSON, and fromJSON functions used for saving and loading objects */ /* 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 { JSONMap, JSONSet } from "../Types/Jsonable";
import { loadActionIdentifier } from "../Bladeburner/utils/loadActionIdentifier";
import { objectAssert } from "./helpers/typeAssertion"; import { objectAssert } from "./helpers/typeAssertion";
type JsonableClass = (new () => { toJSON: () => IReviverValue }) & { type JsonableClass = (new () => { toJSON: () => IReviverValue }) & {
@ -14,44 +13,12 @@ export interface IReviverValue<T = unknown> {
data: T; data: T;
} }
function isReviverValue(value: unknown): value is IReviverValue { export function isReviverValue(value: unknown): value is IReviverValue {
return ( return (
typeof value === "object" && value !== null && "ctor" in value && typeof value.ctor === "string" && "data" in value 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 }; export const constructorsForReviver: Partial<Record<string, JsonableClass>> = { JSONSet, JSONMap };
/** /**

@ -89,26 +89,6 @@ export function generateRandomString(n: number): string {
return str; 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 { export function capitalizeFirstLetter(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1); return s.charAt(0).toUpperCase() + s.slice(1);
} }

@ -2,7 +2,7 @@ import React from "react";
import { dialogBoxCreate } from "../../ui/React/DialogBox"; import { dialogBoxCreate } from "../../ui/React/DialogBox";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { parseUnknownError } from "../ErrorHelper"; import { parseUnknownError } from "../ErrorHelper";
import { cyrb53 } from "../StringHelperFunctions"; import { cyrb53 } from "../HashUtils";
import { commitHash } from "./commitHash"; import { commitHash } from "./commitHash";
const errorSet = new Set<string>(); const errorSet = new Set<string>();