Add an import save comparison page

This adds a new page reachable from the import save file
options menu. It shows the difference between the current save
and the data that is being imported, for confirmation.

Includes an "automatic" variant, which has different wording for when
Electron decides it has access to a newer version of the game.

While in this screen, the autosave is disabled.

This also adds a new BypassWrapper component around the game's tree.
It allows for content to be displayed without rendering the nested
pages (import, recovery). This prevents player scripts from messing
with the screen.
This commit is contained in:
Martin Fournier
2022-01-20 14:55:28 -05:00
parent 974344545d
commit 26432082e2
6 changed files with 440 additions and 43 deletions
+335
View File
@@ -0,0 +1,335 @@
import React, { useEffect, useState } from "react";
import {
Paper,
Table,
TableHead,
TableRow,
TableBody,
TableContainer,
TableCell,
Typography,
Tooltip,
Box,
Button,
ButtonGroup,
} from "@mui/material";
import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles";
import { Theme } from "@mui/material/styles";
import ThumbUpAlt from "@mui/icons-material/ThumbUpAlt";
import ThumbDownAlt from "@mui/icons-material/ThumbDownAlt";
import DirectionsRunIcon from "@mui/icons-material/DirectionsRun";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import WarningIcon from "@mui/icons-material/Warning";
import { ImportData, saveObject } from "../../SaveObject";
import { Settings } from "../../Settings/Settings";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { numeralWrapper } from "../numeralFormat";
import { ConfirmationModal } from "./ConfirmationModal";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
padding: theme.spacing(2),
maxWidth: "1000px",
"& .MuiTable-root": {
"& .MuiTableCell-root": {
borderBottom: `1px solid ${Settings.theme.welllight}`,
},
"& .MuiTableHead-root .MuiTableRow-root": {
backgroundColor: Settings.theme.backgroundsecondary,
"& .MuiTableCell-root": {
color: Settings.theme.primary,
fontWeight: "bold",
},
},
"& .MuiTableBody-root": {
"& .MuiTableRow-root:nth-of-type(odd)": {
backgroundColor: Settings.theme.well,
"& .MuiTableCell-root": {
color: Settings.theme.primarylight,
},
},
"& .MuiTableRow-root:nth-of-type(even)": {
backgroundColor: Settings.theme.backgroundsecondary,
"& .MuiTableCell-root": {
color: Settings.theme.primarylight,
},
},
},
},
},
}),
);
function ComparisonIcon({ isBetter }: { isBetter: boolean }): JSX.Element {
if (isBetter) {
return (
<Tooltip
title={
<>
Imported value is <b>larger</b>!
</>
}
>
<ThumbUpAlt color="success" />
</Tooltip>
);
} else {
return (
<Tooltip
title={
<>
Imported value is <b>smaller</b>!
</>
}
>
<ThumbDownAlt color="error" />
</Tooltip>
);
}
}
export interface ImportSaveProps {
importString: string;
automatic: boolean;
onReturning: () => void;
}
let initialAutosave = 0;
export function ImportSaveRoot({ importString, automatic, onReturning }: ImportSaveProps): JSX.Element {
const classes = useStyles();
const [importData, setImportData] = useState<ImportData | undefined>();
const [currentData, setCurrentData] = useState<ImportData | undefined>();
const [importModalOpen, setImportModalOpen] = useState(false);
function handleGoBack(): void {
Settings.AutosaveInterval = initialAutosave;
onReturning();
}
async function handleImport(): Promise<void> {
await saveObject.importGame(importString, true);
}
useEffect(() => {
// We want to disable autosave while we're in this mode
initialAutosave = Settings.AutosaveInterval;
Settings.AutosaveInterval = 0;
}, []);
useEffect(() => {
async function fetchData(): Promise<void> {
const dataBeingImported = await saveObject.getImportDataFromString(importString);
const dataCurrentlyInGame = await saveObject.getImportDataFromString(saveObject.getSaveString(true));
setImportData(dataBeingImported);
setCurrentData(dataCurrentlyInGame);
return Promise.resolve();
}
if (importString) fetchData();
}, [importString]);
if (!importData || !currentData) return <></>;
return (
<Box className={classes.root}>
<Typography variant="h4" sx={{ mb: 2 }}>
Import Save Comparison
</Typography>
{automatic && (
<Typography sx={{ mb: 2 }}>
We've found a <b>NEWER save</b> that you may want to use instead.
</Typography>
)}
<Typography variant="body1" sx={{ mb: 2 }}>
Your current game's data is on the left and the data that will be imported is on the right.
<br />
Please double check everything is fine before proceeding!
</Typography>
<TableContainer color="secondary" component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell></TableCell>
<TableCell>Current Game</TableCell>
<TableCell>Being Imported</TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>Game Identifier</TableCell>
<TableCell>{currentData.playerData?.identifier ?? "n/a"}</TableCell>
<TableCell>{importData.playerData?.identifier ?? "n/a"}</TableCell>
<TableCell>
{importData.playerData?.identifier !== currentData.playerData?.identifier && (
<Tooltip title="These are two different games!">
<WarningIcon color="warning" />
</Tooltip>
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Playtime</TableCell>
<TableCell>{convertTimeMsToTimeElapsedString(currentData.playerData?.totalPlaytime ?? 0)}</TableCell>
<TableCell>{convertTimeMsToTimeElapsedString(importData.playerData?.totalPlaytime ?? 0)}</TableCell>
<TableCell>
{importData.playerData?.totalPlaytime !== currentData.playerData?.totalPlaytime && (
<ComparisonIcon
isBetter={
(importData.playerData?.totalPlaytime ?? 0) > (currentData.playerData?.totalPlaytime ?? 0)
}
/>
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Saved On</TableCell>
<TableCell>{new Date(currentData.playerData?.lastSave ?? 0).toLocaleString()}</TableCell>
<TableCell>{new Date(importData.playerData?.lastSave ?? 0).toLocaleString()}</TableCell>
<TableCell>
{importData.playerData?.lastSave !== currentData.playerData?.lastSave && (
<ComparisonIcon
isBetter={(importData.playerData?.lastSave ?? 0) > (currentData.playerData?.lastSave ?? 0)}
/>
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Money</TableCell>
<TableCell>{numeralWrapper.formatMoney(currentData.playerData?.money ?? 0)}</TableCell>
<TableCell>{numeralWrapper.formatMoney(importData.playerData?.money ?? 0)}</TableCell>
<TableCell>
{importData.playerData?.money !== currentData.playerData?.money && (
<ComparisonIcon
isBetter={(importData.playerData?.money ?? 0) > (currentData.playerData?.money ?? 0)}
/>
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Hacking</TableCell>
<TableCell>{numeralWrapper.formatSkill(currentData.playerData?.hacking ?? 0)}</TableCell>
<TableCell>{numeralWrapper.formatSkill(importData.playerData?.hacking ?? 0)}</TableCell>
<TableCell>
{importData.playerData?.hacking !== currentData.playerData?.hacking && (
<ComparisonIcon
isBetter={(importData.playerData?.hacking ?? 0) > (currentData.playerData?.hacking ?? 0)}
/>
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Augmentations</TableCell>
<TableCell>{currentData.playerData?.augmentations}</TableCell>
<TableCell>{importData.playerData?.augmentations}</TableCell>
<TableCell>
{importData.playerData?.augmentations !== currentData.playerData?.augmentations && (
<ComparisonIcon
isBetter={
(importData.playerData?.augmentations ?? 0) > (currentData.playerData?.augmentations ?? 0)
}
/>
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Factions</TableCell>
<TableCell>{currentData.playerData?.factions}</TableCell>
<TableCell>{importData.playerData?.factions}</TableCell>
<TableCell>
{importData.playerData?.factions !== currentData.playerData?.factions && (
<ComparisonIcon
isBetter={(importData.playerData?.factions ?? 0) > (currentData.playerData?.factions ?? 0)}
/>
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Achievements</TableCell>
<TableCell>{currentData.playerData?.achievements}</TableCell>
<TableCell>{importData.playerData?.achievements}</TableCell>
<TableCell>
{importData.playerData?.achievements !== currentData.playerData?.achievements && (
<ComparisonIcon
isBetter={(importData.playerData?.achievements ?? 0) > (currentData.playerData?.achievements ?? 0)}
/>
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Source Files</TableCell>
<TableCell>{currentData.playerData?.sourceFiles}</TableCell>
<TableCell>{importData.playerData?.sourceFiles}</TableCell>
<TableCell>
{importData.playerData?.sourceFiles !== currentData.playerData?.sourceFiles && (
<ComparisonIcon
isBetter={(importData.playerData?.sourceFiles ?? 0) > (currentData.playerData?.sourceFiles ?? 0)}
/>
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>BitNode</TableCell>
<TableCell>
{currentData.playerData?.bitNode}-{currentData.playerData?.bitNodeLevel}
</TableCell>
<TableCell>
{importData.playerData?.bitNode}-{importData.playerData?.bitNodeLevel}
</TableCell>
<TableCell></TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
<ButtonGroup>
<Button onClick={handleGoBack} sx={{ my: 2 }} startIcon={<ArrowBackIcon />} color="secondary">
Take me back!
</Button>
<Button
onClick={() => setImportModalOpen(true)}
sx={{ my: 2 }}
startIcon={<DirectionsRunIcon />}
color="warning"
>
Proceed with import
</Button>
</ButtonGroup>
<ConfirmationModal
open={importModalOpen}
onClose={() => setImportModalOpen(false)}
onConfirm={handleImport}
confirmationText={
<>
Importing new save game data will <strong>completely wipe</strong> the current game data!
<br />
</>
}
/>
</Box>
</Box>
);
}