diff --git a/src/SaveObject.ts b/src/SaveObject.ts index 062dd39f6..95726cdac 100644 --- a/src/SaveObject.ts +++ b/src/SaveObject.ts @@ -160,6 +160,20 @@ function assertParsedSaveData(parsedSaveData: unknown): asserts parsedSaveData i } } +/** + * We sometimes need the raw data in the loaded save object for debugging and showing useful error messages. This object + * contains only what we need. + */ +export const loadedSaveObjectMiniDump = { + /** + * If VersionSave exists, it is always a string. It has 3 formats: + * - x.y: Very early versions (0.1-0.17) used this format. + * - x.y.z: Starting from roughly 0.17, we used this format. Note that in some commits, we mistakenly used the x.y format. + * - x: Starting from v1, we used the version number instead of the version string. + */ + VersionSave: undefined as string | undefined, +}; + class BitburnerSaveObject implements BitburnerSaveObjectType { PlayerSave = ""; AllServersSave = ""; @@ -426,6 +440,18 @@ async function loadGame(saveData: SaveData): Promise { const jsonSaveString = await decodeSaveData(saveData); const saveObj: unknown = JSON.parse(jsonSaveString, Reviver); + + // Extract VersionSave ASAP for debugging and showing useful error messages later. Some checks here are redundant ( + // e.g., the object assertion) because we will do them again later, but that's okay. + if ( + saveObj != null && + typeof saveObj === "object" && + "VersionSave" in saveObj && + typeof saveObj.VersionSave === "string" + ) { + loadedSaveObjectMiniDump.VersionSave = saveObj.VersionSave; + } + assertBitburnerSaveObjectType(saveObj); // "Mandatory" diff --git a/src/ui/React/RecoveryRoot.tsx b/src/ui/React/RecoveryRoot.tsx index a81912c65..623c36933 100644 --- a/src/ui/React/RecoveryRoot.tsx +++ b/src/ui/React/RecoveryRoot.tsx @@ -5,7 +5,7 @@ import { Settings } from "../../Settings/Settings"; import { load } from "../../db"; import { Router } from "../GameRoot"; import { Page } from "../Router"; -import { type CrashReport, newIssueUrl, getCrashReport } from "../../utils/ErrorHelper"; +import { type CrashReport, newIssueUrl, getCrashReport, isSaveDataFromNewerVersions } from "../../utils/ErrorHelper"; import { DeleteGameButton } from "./DeleteGameButton"; import { SoftResetButton } from "./SoftResetButton"; @@ -16,6 +16,9 @@ import { InvalidSaveData, UnsupportedSaveData } from "../../utils/SaveDataUtils" import { downloadContentAsFile } from "../../utils/FileUtils"; import { debounce } from "lodash"; import { Engine } from "../../engine"; +import { JSONReviverError } from "../../utils/GenericReviver"; +import { loadedSaveObjectMiniDump } from "../../SaveObject"; +import { CONSTANTS } from "../../Constants"; export let RecoveryMode = false; let sourceError: unknown; @@ -108,6 +111,19 @@ export function RecoveryRoot({ softReset, crashReport, resetError }: IProps): Re Your save data is invalid. Please import a valid backup save file. ); + } else if ( + sourceError instanceof JSONReviverError && + isSaveDataFromNewerVersions(loadedSaveObjectMiniDump.VersionSave) + ) { + instructions = ( + + Your save data is from a newer version (Version number: {loadedSaveObjectMiniDump.VersionSave}). The current + version number is {CONSTANTS.VersionNumber}. +
+ Please check if you are using the correct build. This may happen when you load the save data of the dev build + (Steam Beta or https://bitburner-official.github.io/bitburner-src) on the stable build. +
+ ); } else { instructions = ( diff --git a/src/utils/ErrorHelper.ts b/src/utils/ErrorHelper.ts index 47dbe0761..b4d8ecb5c 100644 --- a/src/utils/ErrorHelper.ts +++ b/src/utils/ErrorHelper.ts @@ -195,3 +195,18 @@ Copy your save here if possible issueUrl, }; } + +export function isSaveDataFromNewerVersions(versionSave?: string): boolean { + if (versionSave == null) { + return false; + } + // x.y and x.y.z formats are from pre-v1 versions. + if (versionSave.includes(".")) { + return false; + } + const versionNumber = Number(versionSave); + if (!Number.isFinite(versionNumber) || versionNumber <= CONSTANTS.VersionNumber) { + return false; + } + return true; +} diff --git a/src/utils/GenericReviver.ts b/src/utils/GenericReviver.ts index a69e792a6..3712ed127 100644 --- a/src/utils/GenericReviver.ts +++ b/src/utils/GenericReviver.ts @@ -1,6 +1,15 @@ import { constructorsForReviver, isReviverValue } from "./JSONReviver"; import { validateObject } from "./Validator"; +export class JSONReviverError extends Error { + ctor: string; + constructor(message: string, ctor: string) { + super(message); + this.name = this.constructor.name; + this.ctor = ctor; + } +} + /** * A generic "smart reviver" function. * Looks for object values with a `ctor` property and a `data` property. @@ -25,7 +34,7 @@ export function Reviver(_key: string, value: unknown): any { return 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.`); + throw new JSONReviverError(`Could not locate constructor named ${value.ctor}.`, value.ctor); } const obj = ctor.fromJSON(value);