diff --git a/electron/gameWindow.js b/electron/gameWindow.js index fb14326d4..b73735509 100644 --- a/electron/gameWindow.js +++ b/electron/gameWindow.js @@ -27,6 +27,7 @@ async function createWindow(killall) { backgroundColor: "#000000", webPreferences: { nativeWindowOpen: true, + preload: path.join(__dirname, 'preload.js'), }, }); diff --git a/electron/preload.js b/electron/preload.js new file mode 100644 index 000000000..707a00009 --- /dev/null +++ b/electron/preload.js @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { ipcRenderer, contextBridge } = require('electron') +const log = require("electron-log"); + +contextBridge.exposeInMainWorld( + "electronBridge", { + send: (channel, data) => { + log.log("Send on channel " + channel) + // whitelist channels + let validChannels = [ + "get-save-data-response", + "get-save-info-response", + "push-game-saved", + "push-game-ready", + "push-import-result", + "push-disable-restore", + ]; + if (validChannels.includes(channel)) { + ipcRenderer.send(channel, data); + } + }, + receive: (channel, func) => { + log.log("Receive on channel " + channel) + let validChannels = [ + "get-save-data-request", + "get-save-info-request", + "push-save-request", + "trigger-save", + "trigger-game-export", + "trigger-scripts-export", + ]; + if (validChannels.includes(channel)) { + // Deliberately strip event as it includes `sender` + ipcRenderer.on(channel, (event, ...args) => func(...args)); + } + } + } +); diff --git a/src/Electron.tsx b/src/Electron.tsx index 9fa98afc5..befa41549 100644 --- a/src/Electron.tsx +++ b/src/Electron.tsx @@ -1,10 +1,18 @@ import { Player } from "./Player"; +import { Router } from "./ui/GameRoot"; +import { isScriptFilename } from "./Script/isScriptFilename"; +import { Script } from "./Script/Script"; import { removeLeadingSlash } from "./Terminal/DirectoryHelpers"; import { Terminal } from "./Terminal"; import { SnackbarEvents } from "./ui/React/Snackbar"; import { IMap, IReturnStatus } from "./types"; import { GetServer } from "./Server/AllServers"; import { resolve } from "cypress/types/bluebird"; +import { ImportPlayerData, SaveData, saveObject } from "./SaveObject"; +import { Settings } from "./Settings/Settings"; +import { exportScripts } from "./Terminal/commands/download"; +import { CONSTANTS } from "./Constants"; +import { hash } from "./hash/hash"; export function initElectron(): void { const userAgent = navigator.userAgent.toLowerCase(); @@ -13,6 +21,8 @@ export function initElectron(): void { (document as any).achievements = []; initWebserver(); initAppNotifier(); + initSaveFunctions(); + initElectronBridge(); } } @@ -109,6 +119,123 @@ function initAppNotifier(): void { }; // Will be consumud by the electron wrapper. - // @ts-ignore - window.appNotifier = funcs; + (window as any).appNotifier = funcs; +} + +function initSaveFunctions(): void { + const funcs = { + triggerSave: (): Promise => saveObject.saveGame(true), + triggerGameExport: (): void => { + try { + saveObject.exportGame(); + } catch (error) { + console.log(error); + SnackbarEvents.emit("Could not export game.", "error", 2000); + } + }, + triggerScriptsExport: (): void => exportScripts("*", Player.getHomeComputer()), + getSaveData: (): { save: string; fileName: string } => { + return { + save: saveObject.getSaveString(Settings.ExcludeRunningScriptsFromSave), + fileName: saveObject.getSaveFileName(), + }; + }, + getSaveInfo: async (base64save: string): Promise => { + try { + const data = await saveObject.getImportDataFromString(base64save); + return data.playerData; + } catch (error) { + console.error(error); + return; + } + }, + pushSaveData: (base64save: string, automatic = false): void => Router.toImportSave(base64save, automatic), + }; + + // Will be consumud by the electron wrapper. + (window as any).appSaveFns = funcs; +} + +function initElectronBridge(): void { + const bridge = (window as any).electronBridge as any; + if (!bridge) return; + + bridge.receive("get-save-data-request", () => { + const data = (window as any).appSaveFns.getSaveData(); + bridge.send("get-save-data-response", data); + }); + bridge.receive("get-save-info-request", async (save: string) => { + const data = await (window as any).appSaveFns.getSaveInfo(save); + bridge.send("get-save-info-response", data); + }); + bridge.receive("push-save-request", ({ save, automatic = false }: { save: string; automatic: boolean }) => { + (window as any).appSaveFns.pushSaveData(save, automatic); + }); + bridge.receive("trigger-save", () => { + return (window as any).appSaveFns + .triggerSave() + .then(() => { + bridge.send("save-completed"); + }) + .catch((error: any) => { + console.log(error); + SnackbarEvents.emit("Could not save game.", "error", 2000); + }); + }); + bridge.receive("trigger-game-export", () => { + try { + (window as any).appSaveFns.triggerGameExport(); + } catch (error) { + console.log(error); + SnackbarEvents.emit("Could not export game.", "error", 2000); + } + }); + bridge.receive("trigger-scripts-export", () => { + try { + (window as any).appSaveFns.triggerScriptsExport(); + } catch (error) { + console.log(error); + SnackbarEvents.emit("Could not export scripts.", "error", 2000); + } + }); +} + +export function pushGameSaved(data: SaveData): void { + const bridge = (window as any).electronBridge as any; + if (!bridge) return; + + bridge.send("push-game-saved", data); +} + +export function pushGameReady(): void { + const bridge = (window as any).electronBridge as any; + if (!bridge) return; + + // Send basic information to the electron wrapper + bridge.send("push-game-ready", { + player: { + identifier: Player.identifier, + playtime: Player.totalPlaytime, + lastSave: Player.lastSave, + }, + game: { + version: CONSTANTS.VersionString, + hash: hash(), + }, + }); +} + +export function pushImportResult(wasImported: boolean): void { + const bridge = (window as any).electronBridge as any; + if (!bridge) return; + + bridge.send("push-import-result", { wasImported }); + pushDisableRestore(); +} + +export function pushDisableRestore(): void { + const bridge = (window as any).electronBridge as any; + if (!bridge) return; + + bridge.send("push-disable-restore", { duration: 1000 * 60 }); } diff --git a/src/SaveObject.tsx b/src/SaveObject.tsx index 83e7f0603..2d15b89dd 100755 --- a/src/SaveObject.tsx +++ b/src/SaveObject.tsx @@ -24,11 +24,19 @@ import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation" import { LocationName } from "./Locations/data/LocationNames"; import { SxProps } from "@mui/system"; import { PlayerObject } from "./PersonObjects/Player/PlayerObject"; +import { pushGameSaved } from "./Electron"; /* SaveObject.js * Defines the object used to save/load games */ +export interface SaveData { + playerIdentifier: string; + fileName: string; + save: string; + savedOn: number; +} + export interface ImportData { base64: string; parsed: any; @@ -91,11 +99,20 @@ class BitburnerSaveObject { } saveGame(emitToastEvent = true): Promise { - Player.lastSave = new Date().getTime(); + const savedOn = new Date().getTime(); + Player.lastSave = savedOn; const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave); return new Promise((resolve, reject) => { save(saveString) .then(() => { + const saveData: SaveData = { + playerIdentifier: Player.identifier, + fileName: this.getSaveFileName(), + save: saveString, + savedOn, + }; + pushGameSaved(saveData); + if (emitToastEvent) { SnackbarEvents.emit("Game Saved!", "info", 2000); } diff --git a/src/ui/LoadingScreen.tsx b/src/ui/LoadingScreen.tsx index 2b73fb734..71a2d6bc1 100644 --- a/src/ui/LoadingScreen.tsx +++ b/src/ui/LoadingScreen.tsx @@ -16,6 +16,7 @@ import { GameRoot } from "./GameRoot"; import { CONSTANTS } from "../Constants"; import { ActivateRecoveryMode } from "./React/RecoveryRoot"; import { hash } from "../hash/hash"; +import { pushGameReady } from "../Electron"; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -56,6 +57,7 @@ export function LoadingScreen(): React.ReactElement { throw err; } + pushGameReady(); setLoaded(true); }) .catch((reason) => { diff --git a/src/ui/React/DeleteGameButton.tsx b/src/ui/React/DeleteGameButton.tsx index a2d2c2d15..6f88a2a3b 100644 --- a/src/ui/React/DeleteGameButton.tsx +++ b/src/ui/React/DeleteGameButton.tsx @@ -5,6 +5,7 @@ import Button from "@mui/material/Button"; import { Tooltip } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; +import { pushDisableRestore } from '../../Electron'; interface IProps { color?: "primary" | "warning" | "error"; @@ -21,7 +22,10 @@ export function DeleteGameButton({ color = "primary" }: IProps): React.ReactElem onConfirm={() => { setModalOpened(false); deleteGame() - .then(() => setTimeout(() => location.reload(), 1000)) + .then(() => { + pushDisableRestore(); + setTimeout(() => location.reload(), 1000); + }) .catch((r) => console.error(`Could not delete game: ${r}`)); }} open={modalOpened} diff --git a/src/ui/React/ImportSaveRoot.tsx b/src/ui/React/ImportSaveRoot.tsx index fe7c888c0..edf4cfeb7 100644 --- a/src/ui/React/ImportSaveRoot.tsx +++ b/src/ui/React/ImportSaveRoot.tsx @@ -30,7 +30,7 @@ import { Settings } from "../../Settings/Settings"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { numeralWrapper } from "../numeralFormat"; import { ConfirmationModal } from "./ConfirmationModal"; - +import { pushImportResult } from "../../Electron"; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -117,11 +117,13 @@ export function ImportSaveRoot({ importString, automatic, onReturning }: ImportS function handleGoBack(): void { Settings.AutosaveInterval = initialAutosave; + pushImportResult(false); onReturning(); } async function handleImport(): Promise { await saveObject.importGame(importString, true); + pushImportResult(true); } useEffect(() => {