From 855a4e622d3422c1fff2011cdd62fd496395c129 Mon Sep 17 00:00:00 2001 From: Martin Fournier Date: Tue, 18 Jan 2022 12:21:53 -0500 Subject: [PATCH] Add Electron preload script to allow communication Adds a channel to communicate between the main process & the renderer process, so that the game can easily ship data back to the main process. It uses the Electron contextBridge & ipcRenderer/ipcMain. Connects those events to various save functions. Adds triggered events on game save, game load, and imported game. Adds way for the Electron app to ask for certain actions or data. Hook handlers to disable automatic restore Allows to temporarily disable restore when the game just did an import or deleted a save game. Prevents looping screens. --- electron/gameWindow.js | 1 + electron/preload.js | 38 +++++++++ src/Electron.tsx | 131 +++++++++++++++++++++++++++++- src/SaveObject.tsx | 19 ++++- src/ui/LoadingScreen.tsx | 2 + src/ui/React/DeleteGameButton.tsx | 6 +- src/ui/React/ImportSaveRoot.tsx | 4 +- 7 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 electron/preload.js 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(() => {