UI: Show useful error messages when loading unsupported save data from newer versions (#2425)

This commit is contained in:
catloversg
2025-12-19 05:51:48 +07:00
committed by GitHub
parent be7bb0ad7c
commit bd2af9392f
4 changed files with 68 additions and 2 deletions

View File

@@ -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<boolean> {
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"

View File

@@ -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.
</Typography>
);
} else if (
sourceError instanceof JSONReviverError &&
isSaveDataFromNewerVersions(loadedSaveObjectMiniDump.VersionSave)
) {
instructions = (
<Typography variant="h5" color={Settings.theme.warning}>
Your save data is from a newer version (Version number: {loadedSaveObjectMiniDump.VersionSave}). The current
version number is {CONSTANTS.VersionNumber}.
<br />
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.
</Typography>
);
} else {
instructions = (
<Box>

View File

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

View File

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