diff --git a/src/ui/React/GameOptionsRoot.tsx b/src/ui/React/GameOptionsRoot.tsx index 24443de42..0c436fea6 100644 --- a/src/ui/React/GameOptionsRoot.tsx +++ b/src/ui/React/GameOptionsRoot.tsx @@ -28,6 +28,7 @@ import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal"; import { dialogBoxCreate } from "./DialogBox"; import { ConfirmationModal } from "./ConfirmationModal"; import { ThemeEditorModal } from "./ThemeEditorModal"; +import { SnackbarEvents } from "./Snackbar"; import { Settings } from "../../Settings/Settings"; import { save, deleteGame } from "../../db"; @@ -51,6 +52,12 @@ interface IProps { softReset: () => void; } +interface ImportData { + base64: string; + parsed: any; + exportDate?: Date; +} + export function GameOptionsRoot(props: IProps): React.ReactElement { const classes = useStyles(); const importInput = useRef(null); @@ -84,6 +91,8 @@ export function GameOptionsRoot(props: IProps): React.ReactElement { const [deleteGameOpen, setDeleteOpen] = useState(false); const [themeEditorOpen, setThemeEditorOpen] = useState(false); const [softResetOpen, setSoftResetOpen] = useState(false); + const [importSaveOpen, setImportSaveOpen] = useState(false); + const [importData, setImportData] = useState(null); function handleExecTimeChange(event: any, newValue: number | number[]): void { setExecTime(newValue as number); @@ -206,11 +215,67 @@ export function GameOptionsRoot(props: IProps): React.ReactElement { return; } const contents = result; - save(contents).then(() => setTimeout(() => location.reload(), 1000)); + + // https://stackoverflow.com/a/35002237 + const base64regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; + if (!base64regex.test(contents)) { + SnackbarEvents.emit("Save game was not a base64 string", "error", 5000); + return; + } + + let newSave; + try { + newSave = window.atob(contents); + newSave = newSave.trim(); + } catch (error) { + console.log(error); // We'll handle below + } + + if (!newSave || newSave === '') { + SnackbarEvents.emit("Save game had not content", "error", 5000); + return; + } + + let parsedSave; + try { + parsedSave = JSON.parse(newSave); + } catch (error) { + console.log(error); // We'll handle below + } + + if (!parsedSave || parsedSave.ctor !== 'BitburnerSaveObject' || !parsedSave.data) { + SnackbarEvents.emit("Save game did not seem valid", "error", 5000); + return; + } + + + const data: ImportData = { + base64: contents, + parsed: parsedSave, + } + + // We don't always seem to have this value in the save file. Exporting from the option menu does not set the bonus I think. + const exportTimestamp = parsedSave.data.LastExportBonus; + if (exportTimestamp && exportTimestamp !== '0') { + data.exportDate = new Date(parseInt(exportTimestamp, 10)) + } + + setImportData(data) + setImportSaveOpen(true); }; reader.readAsText(file); } + function confirmedImportGame(): void { + if (!importData) return; + + setImportSaveOpen(false); + save(importData.base64).then(() => { + setImportData(null); + setTimeout(() => location.reload(), 1000) + }); + } + function doSoftReset(): void { if (!Settings.SuppressBuyAugmentationConfirmation) { setSoftResetOpen(true); @@ -618,19 +683,41 @@ export function GameOptionsRoot(props: IProps): React.ReactElement { - export}> + Export your game to a text file.}> - import}> + Import your game from a text file.
This will overwrite your current game. Back it up first!}>
+ setImportSaveOpen(false)} + onConfirm={() => confirmedImportGame()} + confirmationText={ + <> + Importing a new game will completely wipe the current data! +
+
+ Make sure to have a backup of your current save file before importing. +
+ The file you are attempting to import seems valid. +
+
+ {importData?.exportDate && (<> + The export date of the save file is {importData?.exportDate.toString()} +
+
+ )} + + } + />