diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md index 9905e0217..4e4c7c6ea 100644 --- a/doc/CONTRIBUTING.md +++ b/doc/CONTRIBUTING.md @@ -169,7 +169,7 @@ Saving a file will reload the game automatically. ### How to build the electron app -Tested on Node v16.13.1 (LTS) on Windows. +Tested on Node v20.11.1 (LTS) on Windows. These steps only work in a Bash-like environment, like MinGW for Windows. ```sh @@ -180,10 +180,6 @@ $ npm run build:dev # Use electron-packager to build the app to the .build/ folder. $ npm run electron -# When launching the .exe directly, you'll need the steam_appid.txt file in the root. -# If not using Windows, change this line accordingly. -$ cp .build/bitburner-win32-x64/resources/app/steam_appid.txt .build/bitburner-win32-x64/steam_appid.txt - # And run the game... $ .build/bitburner-win32-x64/bitburner.exe ``` diff --git a/electron/achievements.js b/electron/achievements.js index 635e61d48..440275178 100644 --- a/electron/achievements.js +++ b/electron/achievements.js @@ -1,26 +1,39 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -const greenworks = require("./greenworks"); +const { steamworksClient } = require("./steamworksUtils"); const log = require("electron-log"); -async function enableAchievementsInterval(window) { +function enableAchievementsInterval(window) { // If the Steam API could not be initialized on game start, we'll abort this. - if (global.greenworksError) return; + if (!steamworksClient) { + return; + } // This is backward but the game fills in an array called `document.achievements` and we retrieve it from // here. Hey if it works it works. - const steamAchievements = greenworks.getAchievementNames(); - log.silly(`All Steam achievements ${JSON.stringify(steamAchievements)}`); - const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter((name) => !!name); - log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`); + const allSteamAchievements = steamworksClient.achievement.names(); + log.silly(`All Steam achievements ${JSON.stringify(allSteamAchievements)}`); + const steamAchievements = allSteamAchievements.filter((achievement) => + steamworksClient.achievement.isActivated(achievement), + ); + log.debug(`Player has Steam achievements ${JSON.stringify(steamAchievements)}`); const intervalID = setInterval(async () => { try { const playerAchievements = await window.webContents.executeJavaScript("document.achievements"); - for (const ach of playerAchievements) { - if (!steamAchievements.includes(ach)) continue; // Don't try activating achievements that don't exist Steam-side - if (playerAchieved.includes(ach)) continue; // Don't spam achievements that have already been recorded - log.info(`Granting Steam achievement ${ach}`); - greenworks.activateAchievement(ach, () => undefined); - playerAchieved.push(ach); + for (const achievement of playerAchievements) { + // Don't try activating achievements that don't exist Steam-side + if (!allSteamAchievements.includes(achievement)) { + continue; + } + // Don't spam achievements that have already been recorded + if (steamAchievements.includes(achievement)) { + continue; + } + log.info(`Granting Steam achievement ${achievement}`); + if (steamworksClient.achievement.activate(achievement)) { + steamAchievements.push(achievement); + } else { + log.warn(`Cannot grant Steam achievement ${achievement}`); + } } } catch (error) { log.error(error); @@ -28,25 +41,11 @@ async function enableAchievementsInterval(window) { // The interval probably did not get cleared after a window kill log.warn("Clearing achievements timer"); clearInterval(intervalID); - return; } }, 1000); window.achievementsIntervalID = intervalID; } -function checkSteamAchievement(name) { - return new Promise((resolve) => { - greenworks.getAchievement( - name, - (playerHas) => resolve(playerHas ? name : ""), - (err) => { - log.warn(`Failed to get Steam achievement ${name} status: ${err}`); - resolve(""); - }, - ); - }); -} - function disableAchievementsInterval(window) { if (window.achievementsIntervalID) { clearInterval(window.achievementsIntervalID); diff --git a/electron/gameWindow.js b/electron/gameWindow.js index 72ae1ba71..55167ed58 100644 --- a/electron/gameWindow.js +++ b/electron/gameWindow.js @@ -63,7 +63,7 @@ async function createWindow(killall) { utils.attachUnresponsiveAppHandler(window); menu.refreshMenu(window); - setStopProcessHandler(app, window); + setStopProcessHandler(window); return window; } diff --git a/electron/greenworks.js b/electron/greenworks.js deleted file mode 100644 index 90ef16297..000000000 --- a/electron/greenworks.js +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) 2015 Greenheart Games Pty. Ltd. All rights reserved. -// Use of this source code is governed by the MIT license that can be -// found in the LICENSE file. - -// The source code can be found in https://github.com/greenheartgames/greenworks -var fs = require("fs"); - -var greenworks; - -if (process.arch === "x64") { - if (process.platform === "darwin") greenworks = require("./lib/greenworks-osx64"); - else if (process.platform === "win32") greenworks = require("./lib/greenworks-win64"); - else if (process.platform === "linux") greenworks = require("./lib/greenworks-linux64"); -} - -function error_process(err, error_callback) { - if (err && error_callback) error_callback(err); -} - -if (greenworks) { - greenworks.ugcGetItems = function (options, ugc_matching_type, ugc_query_type, success_callback, error_callback) { - if (typeof options !== "object") { - error_callback = success_callback; - success_callback = ugc_query_type; - ugc_query_type = ugc_matching_type; - ugc_matching_type = options; - options = { - app_id: greenworks.getAppId(), - page_num: 1, - }; - } - greenworks._ugcGetItems(options, ugc_matching_type, ugc_query_type, success_callback, error_callback); - }; - - greenworks.ugcGetUserItems = function ( - options, - ugc_matching_type, - ugc_list_sort_order, - ugc_list, - success_callback, - error_callback, - ) { - if (typeof options !== "object") { - error_callback = success_callback; - success_callback = ugc_list; - ugc_list = ugc_list_sort_order; - ugc_list_sort_order = ugc_matching_type; - ugc_matching_type = options; - options = { - app_id: greenworks.getAppId(), - page_num: 1, - }; - } - greenworks._ugcGetUserItems( - options, - ugc_matching_type, - ugc_list_sort_order, - ugc_list, - success_callback, - error_callback, - ); - }; - - greenworks.ugcSynchronizeItems = function (options, sync_dir, success_callback, error_callback) { - if (typeof options !== "object") { - error_callback = success_callback; - success_callback = sync_dir; - sync_dir = options; - options = { - app_id: greenworks.getAppId(), - page_num: 1, - }; - } - greenworks._ugcSynchronizeItems(options, sync_dir, success_callback, error_callback); - }; - - greenworks.publishWorkshopFile = function ( - options, - file_path, - image_path, - title, - description, - success_callback, - error_callback, - ) { - if (typeof options !== "object") { - error_callback = success_callback; - success_callback = description; - description = title; - title = image_path; - image_path = file_path; - file_path = options; - options = { - app_id: greenworks.getAppId(), - tags: [], - }; - } - greenworks._publishWorkshopFile( - options, - file_path, - image_path, - title, - description, - success_callback, - error_callback, - ); - }; - - greenworks.updatePublishedWorkshopFile = function ( - options, - published_file_handle, - file_path, - image_path, - title, - description, - success_callback, - error_callback, - ) { - if (typeof options !== "object") { - error_callback = success_callback; - success_callback = description; - description = title; - title = image_path; - image_path = file_path; - file_path = published_file_handle; - published_file_handle = options; - options = { - tags: [], // No tags are set - }; - } - greenworks._updatePublishedWorkshopFile( - options, - published_file_handle, - file_path, - image_path, - title, - description, - success_callback, - error_callback, - ); - }; - - // An utility function for publish related APIs. - // It processes remains steps after saving files to Steam Cloud. - function file_share_process(file_name, image_name, next_process_func, error_callback, progress_callback) { - if (progress_callback) progress_callback("Completed on saving files on Steam Cloud."); - greenworks.fileShare( - file_name, - function () { - greenworks.fileShare( - image_name, - function () { - next_process_func(); - }, - function (err) { - error_process(err, error_callback); - }, - ); - }, - function (err) { - error_process(err, error_callback); - }, - ); - } - - // Publishing user generated content(ugc) to Steam contains following steps: - // 1. Save file and image to Steam Cloud. - // 2. Share the file and image. - // 3. publish the file to workshop. - greenworks.ugcPublish = function ( - file_name, - title, - description, - image_name, - success_callback, - error_callback, - progress_callback, - ) { - var publish_file_process = function () { - if (progress_callback) progress_callback("Completed on sharing files."); - greenworks.publishWorkshopFile( - file_name, - image_name, - title, - description, - function (publish_file_id) { - success_callback(publish_file_id); - }, - function (err) { - error_process(err, error_callback); - }, - ); - }; - greenworks.saveFilesToCloud( - [file_name, image_name], - function () { - file_share_process(file_name, image_name, publish_file_process, error_callback, progress_callback); - }, - function (err) { - error_process(err, error_callback); - }, - ); - }; - - // Update publish ugc steps: - // 1. Save new file and image to Steam Cloud. - // 2. Share file and images. - // 3. Update published file. - greenworks.ugcPublishUpdate = function ( - published_file_id, - file_name, - title, - description, - image_name, - success_callback, - error_callback, - progress_callback, - ) { - var update_published_file_process = function () { - if (progress_callback) progress_callback("Completed on sharing files."); - greenworks.updatePublishedWorkshopFile( - published_file_id, - file_name, - image_name, - title, - description, - function () { - success_callback(); - }, - function (err) { - error_process(err, error_callback); - }, - ); - }; - - greenworks.saveFilesToCloud( - [file_name, image_name], - function () { - file_share_process(file_name, image_name, update_published_file_process, error_callback, progress_callback); - }, - function (err) { - error_process(err, error_callback); - }, - ); - }; - - // Greenworks Utils APIs implmentation. - greenworks.Utils.move = function (source_dir, target_dir, success_callback, error_callback) { - fs.rename(source_dir, target_dir, function (err) { - if (err) { - if (error_callback) error_callback(err); - return; - } - if (success_callback) success_callback(); - }); - }; - - greenworks.init = function () { - if (this.initAPI()) return true; - if (!this.isSteamRunning()) throw new Error("Steam initialization failed. Steam is not running."); - var appId; - try { - appId = fs.readFileSync("steam_appid.txt", "utf8"); - } catch (e) { - throw new Error( - "Steam initialization failed. Steam is running," + - "but steam_appid.txt is missing. Expected to find it in: " + - require("path").resolve("steam_appid.txt"), - ); - } - if (!/^\d+ *\r?\n?$/.test(appId)) { - throw new Error( - "Steam initialization failed. " + - "steam_appid.txt appears to be invalid; " + - "it should contain a numeric ID: " + - appId, - ); - } - throw new Error( - "Steam initialization failed, but Steam is running, " + - "and steam_appid.txt is present and valid." + - "Maybe that's not really YOUR app ID? " + - appId.trim(), - ); - }; - - var EventEmitter = require("events").EventEmitter; - greenworks.__proto__ = EventEmitter.prototype; - EventEmitter.call(greenworks); - - greenworks._steam_events.on = function () { - greenworks.emit.apply(greenworks, arguments); - }; - - process.versions["greenworks"] = greenworks._version; -} - -module.exports = greenworks; diff --git a/electron/lib/greenworks-linux64.node b/electron/lib/greenworks-linux64.node deleted file mode 100755 index 0acdfd010..000000000 Binary files a/electron/lib/greenworks-linux64.node and /dev/null differ diff --git a/electron/lib/greenworks-osx64.node b/electron/lib/greenworks-osx64.node deleted file mode 100755 index a93485a9b..000000000 Binary files a/electron/lib/greenworks-osx64.node and /dev/null differ diff --git a/electron/lib/greenworks-win64.node b/electron/lib/greenworks-win64.node deleted file mode 100755 index 56ac49733..000000000 Binary files a/electron/lib/greenworks-win64.node and /dev/null differ diff --git a/electron/lib/libsdkencryptedappticket.dylib b/electron/lib/libsdkencryptedappticket.dylib deleted file mode 100644 index 4c356e794..000000000 Binary files a/electron/lib/libsdkencryptedappticket.dylib and /dev/null differ diff --git a/electron/lib/libsdkencryptedappticket.so b/electron/lib/libsdkencryptedappticket.so deleted file mode 100644 index 5228c960b..000000000 Binary files a/electron/lib/libsdkencryptedappticket.so and /dev/null differ diff --git a/electron/lib/libsteam_api.dylib b/electron/lib/libsteam_api.dylib deleted file mode 100644 index 0bbdeeb9c..000000000 Binary files a/electron/lib/libsteam_api.dylib and /dev/null differ diff --git a/electron/lib/libsteam_api.so b/electron/lib/libsteam_api.so deleted file mode 100644 index 99c916479..000000000 Binary files a/electron/lib/libsteam_api.so and /dev/null differ diff --git a/electron/lib/sdkencryptedappticket64.dll b/electron/lib/sdkencryptedappticket64.dll deleted file mode 100644 index 7e8ecbbb1..000000000 Binary files a/electron/lib/sdkencryptedappticket64.dll and /dev/null differ diff --git a/electron/lib/sdkencryptedappticket64.lib b/electron/lib/sdkencryptedappticket64.lib deleted file mode 100644 index 146f4cfeb..000000000 Binary files a/electron/lib/sdkencryptedappticket64.lib and /dev/null differ diff --git a/electron/lib/steam_api.dll b/electron/lib/steam_api.dll deleted file mode 100644 index 7fde3a36d..000000000 Binary files a/electron/lib/steam_api.dll and /dev/null differ diff --git a/electron/lib/steam_api.lib b/electron/lib/steam_api.lib deleted file mode 100644 index f163d0c6a..000000000 Binary files a/electron/lib/steam_api.lib and /dev/null differ diff --git a/electron/lib/steam_api64.dll b/electron/lib/steam_api64.dll deleted file mode 100644 index 9ad453cf6..000000000 Binary files a/electron/lib/steam_api64.dll and /dev/null differ diff --git a/electron/lib/steam_api64.lib b/electron/lib/steam_api64.lib deleted file mode 100644 index 4a9b01120..000000000 Binary files a/electron/lib/steam_api64.lib and /dev/null differ diff --git a/electron/lib/steam_appid.txt b/electron/lib/steam_appid.txt deleted file mode 100644 index 68267b473..000000000 --- a/electron/lib/steam_appid.txt +++ /dev/null @@ -1 +0,0 @@ -1812820 \ No newline at end of file diff --git a/electron/main.js b/electron/main.js index e18414381..34cf14b2f 100644 --- a/electron/main.js +++ b/electron/main.js @@ -19,7 +19,7 @@ app.on("window-all-closed", () => { process.exit(0); }); -const greenworks = require("./greenworks"); +require("./steamworksUtils"); const gameWindow = require("./gameWindow"); const achievements = require("./achievements"); const utils = require("./utils"); @@ -36,23 +36,9 @@ log.transports.console.level = store.get("console-log-level", "debug"); log.info(`Started app: ${JSON.stringify(process.argv)}`); -// We want to fail gracefully if we cannot connect to Steam -try { - if (greenworks && greenworks.init()) { - log.info("Steam API has been initialized."); - } else { - const error = "Steam API has failed to initialize."; - log.warn(error); - global.greenworksError = error; - } -} catch (ex) { - log.warn(ex.message); - global.greenworksError = ex.message; -} - let isRestoreDisabled = false; -function setStopProcessHandler(app, window) { +function setStopProcessHandler(window) { const closingWindowHandler = async (e) => { // We need to prevent the default closing event to add custom logic e.preventDefault(); @@ -115,7 +101,7 @@ function setStopProcessHandler(app, window) { } }; - const receivedDisableRestoreHandler = async (event, arg) => { + const receivedDisableRestoreHandler = (event, arg) => { if (!window) return log.warn("Window was undefined in disable import handler"); log.debug(`Disabling auto-restore for ${arg.duration}ms.`); @@ -126,7 +112,7 @@ function setStopProcessHandler(app, window) { }, arg.duration); }; - const receivedGameSavedHandler = async (event, arg) => { + const receivedGameSavedHandler = (event, arg) => { if (!window) return log.warn("Window was undefined in game saved handler"); const { save, ...other } = arg; @@ -229,15 +215,15 @@ app.on("ready", async () => { const window = new BrowserWindow({ show: false }); await window.loadFile("export.html"); window.show(); - setStopProcessHandler(app, window); + setStopProcessHandler(window); await utils.exportSave(window); } else { const window = await startWindow(process.argv.includes("--no-scripts")); - if (global.greenworksError) { + if (global.steamworksError) { await dialog.showMessageBox(window, { title: "Bitburner", message: "Could not connect to Steam", - detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`, + detail: `${global.steamworksError.message}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`, type: "warning", buttons: ["OK"], }); diff --git a/electron/menu.js b/electron/menu.js index f1c6502d7..9bf066703 100644 --- a/electron/menu.js +++ b/electron/menu.js @@ -5,6 +5,7 @@ const Store = require("electron-store"); const utils = require("./utils"); const storage = require("./storage"); const store = new Store(); +const { steamworksClient } = require("./steamworksUtils"); function getMenu(window) { const canZoomIn = utils.getZoomFactor() <= 2; @@ -54,7 +55,7 @@ function getMenu(window) { }, { label: "Export Scripts", - click: async () => window.webContents.send("trigger-scripts-export"), + click: () => window.webContents.send("trigger-scripts-export"), }, { type: "separator", @@ -74,7 +75,7 @@ function getMenu(window) { { label: "Load From File", click: async () => { - const defaultPath = await storage.getSaveFolder(window); + const defaultPath = storage.getSaveFolder(window); const result = await dialog.showOpenDialog(window, { title: "Load From File", defaultPath: defaultPath, @@ -103,7 +104,7 @@ function getMenu(window) { click: async () => { try { const saveData = await storage.getSteamCloudSaveData(); - await storage.pushSaveGameForImport(window, saveData, false); + storage.pushSaveGameForImport(window, saveData, false); } catch (error) { log.error(error); utils.writeToast(window, "Could not load from Steam Cloud", "error", 5000); @@ -126,7 +127,7 @@ function getMenu(window) { { label: "Auto-Save to Steam Cloud", type: "checkbox", - enabled: !global.greenworksError, + enabled: steamworksClient !== undefined, checked: storage.isCloudEnabled(), click: (menuItem) => { storage.setCloudEnabledConfig(menuItem.checked); @@ -166,8 +167,8 @@ function getMenu(window) { }, { label: "Open Saves Directory", - click: async () => { - const path = await storage.getSaveFolder(window); + click: () => { + const path = storage.getSaveFolder(window); shell.openPath(path); }, }, @@ -289,10 +290,15 @@ function getMenu(window) { }, { label: "Delete Steam Cloud Data", - enabled: !global.greenworksError, - click: async () => { + enabled: steamworksClient !== undefined, + click: () => { + if (steamworksClient.cloud.listFiles().length === 0) { + return; + } try { - await storage.deleteCloudFile(); + if (!storage.deleteCloudFile()) { + log.warn("Cannot delete Steam Cloud data"); + } } catch (error) { log.error(error); } diff --git a/electron/package-lock.json b/electron/package-lock.json index 224a45f9f..d93dffd3c 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -8,11 +8,33 @@ "name": "bitburner", "version": "3.0.0", "dependencies": { + "@catloversg/steamworks.js": "0.0.1", "electron-log": "^4.4.8", "electron-store": "^8.1.0", "lodash": "^4.17.21" } }, + "node_modules/@catloversg/steamworks.js": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@catloversg/steamworks.js/-/steamworks.js-0.0.1.tgz", + "integrity": "sha512-Kj3JZUVqYuLsF34g/N/Ap/aWsJHJoWKbvn/R19fzlZTJY+XMv5myRcngUuydwzvJzHR+BhVk7FzQVV8D8w5x1Q==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -313,6 +335,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/electron/package.json b/electron/package.json index 85f1ec6cb..87124d7fd 100755 --- a/electron/package.json +++ b/electron/package.json @@ -24,8 +24,9 @@ "buildResources": "public" }, "dependencies": { - "electron-store": "^8.1.0", + "@catloversg/steamworks.js": "0.0.1", "electron-log": "^4.4.8", + "electron-store": "^8.1.0", "lodash": "^4.17.21" } } diff --git a/electron/steam_appid.txt b/electron/steam_appid.txt deleted file mode 100644 index 68267b473..000000000 --- a/electron/steam_appid.txt +++ /dev/null @@ -1 +0,0 @@ -1812820 \ No newline at end of file diff --git a/electron/steamworksUtils.js b/electron/steamworksUtils.js new file mode 100644 index 000000000..dfee6eba4 --- /dev/null +++ b/electron/steamworksUtils.js @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const steamworks = require("@catloversg/steamworks.js"); +const log = require("electron-log"); + +let steamworksClient; +try { + // 1812820 is our Steam App ID. + steamworksClient = steamworks.init(1812820); +} catch (error) { + if (error.message?.includes("Steam is probably not running")) { + log.warn(error.message); + } else { + log.warn(error); + } + global.steamworksError = error; +} + +module.exports = { + steamworksClient, +}; diff --git a/electron/storage.js b/electron/storage.js index 9a68d6a75..d99819848 100644 --- a/electron/storage.js +++ b/electron/storage.js @@ -3,12 +3,12 @@ const { app, ipcMain } = require("electron"); const path = require("path"); const fs = require("fs/promises"); -const greenworks = require("./greenworks"); const log = require("electron-log"); const flatten = require("lodash/flatten"); const Store = require("electron-store"); const { isBinaryFormat } = require("./saveDataBinaryFormat"); const store = new Store(); +const { steamworksClient } = require("./steamworksUtils"); // https://stackoverflow.com/a/69418940 const dirSize = async (directory) => { @@ -33,7 +33,7 @@ const getNewestFile = async (directory) => { }; const getAllSaves = async (window) => { - const rootDirectory = await getSaveFolder(window, true); + const rootDirectory = getSaveFolder(window, true); const data = await fs.readdir(rootDirectory, { withFileTypes: true }); const savesPromises = data .filter((e) => e.isDirectory()) @@ -45,8 +45,8 @@ const getAllSaves = async (window) => { }; async function prepareSaveFolders(window) { - const rootFolder = await getSaveFolder(window, true); - const currentFolder = await getSaveFolder(window); + const rootFolder = getSaveFolder(window, true); + const currentFolder = getSaveFolder(window); const backupsFolder = path.join(rootFolder, "/_backups"); await prepareFolders(rootFolder, currentFolder, backupsFolder); } @@ -87,69 +87,67 @@ function setCloudEnabledConfig(value) { store.set("cloud-enabled", value); } -async function getSaveFolder(window, root = false) { - if (root) return path.join(app.getPath("userData"), "/saves"); +function getSaveFolder(window, root = false) { + if (root) { + return path.join(app.getPath("userData"), "/saves"); + } const identifier = window.gameInfo?.player?.identifier ?? ""; return path.join(app.getPath("userData"), "/saves", `/${identifier}`); } function isCloudEnabled() { // If the Steam API could not be initialized on game start, we'll abort this. - if (global.greenworksError) return false; + if (!steamworksClient) { + return false; + } // If the user disables it in Steam there's nothing we can do - if (!greenworks.isCloudEnabledForUser()) return false; + if (!steamworksClient.cloud.isEnabledForAccount()) { + return false; + } - // Let's check the config file to see if it's been overriden - const enabledInConf = store.get("cloud-enabled", true); - if (!enabledInConf) return false; + // Let's check the config file to see if it's been overridden + if (!store.get("cloud-enabled", true)) { + return false; + } - const isAppEnabled = greenworks.isCloudEnabled(); - if (!isAppEnabled) greenworks.enableCloud(true); + if (!steamworksClient.cloud.isEnabledForApp()) { + steamworksClient.cloud.setEnabledForApp(true); + } return true; } function saveCloudFile(name, content) { - return new Promise((resolve, reject) => { - greenworks.saveTextToFile(name, content, resolve, reject); - }); + steamworksClient.cloud.writeFile(name, content); } -function getFirstCloudFile() { - const nbFiles = greenworks.getFileCount(); - if (nbFiles === 0) throw new Error("No files in cloud"); - const file = greenworks.getFileNameAndSize(0); - log.silly(`Found ${nbFiles} files.`); +function getFilenameOfFirstCloudFile() { + const files = steamworksClient.cloud.listFiles(); + if (files.length === 0) { + throw new Error("No files in cloud"); + } + const file = files[0]; + log.silly(`Found ${files.length} files.`); log.silly(`First File: ${file.name} (${file.size} bytes)`); return file.name; } function getCloudFile() { - const file = getFirstCloudFile(); - return new Promise((resolve, reject) => { - greenworks.readTextFromFile(file, resolve, reject); - }); + return steamworksClient.cloud.readFile(getFilenameOfFirstCloudFile()); } function deleteCloudFile() { - const file = getFirstCloudFile(); - return new Promise((resolve, reject) => { - greenworks.deleteFile(file, resolve, reject); - }); -} - -async function getSteamCloudQuota() { - return new Promise((resolve, reject) => { - greenworks.getCloudQuota(resolve, reject); - }); + return steamworksClient.cloud.deleteFile(getFilenameOfFirstCloudFile()); } async function backupSteamDataToDisk(currentPlayerId) { - const nbFiles = greenworks.getFileCount(); - if (nbFiles === 0) return; + const files = steamworksClient.cloud.listFiles(); + if (files.length === 0) { + return; + } - const file = greenworks.getFileNameAndSize(0); + const file = files[0]; const previousPlayerId = file.name.replace(".json.gz", ""); if (previousPlayerId !== currentPlayerId) { const backupSaveData = await getSteamCloudSaveData(); @@ -170,7 +168,7 @@ async function pushSaveDataToSteamCloud(saveData, currentPlayerId) { } try { - backupSteamDataToDisk(currentPlayerId); + await backupSteamDataToDisk(currentPlayerId); } catch (error) { log.error(error); } @@ -178,8 +176,8 @@ async function pushSaveDataToSteamCloud(saveData, currentPlayerId) { const steamSaveName = `${currentPlayerId}.json.gz`; /** - * When we push save file to Steam Cloud, we use greenworks.saveTextToFile. It seems that this method expects a string - * as the file content. That is why saveData is encoded in base64 and pushed to Steam Cloud as a text file. + * When we push save file to Steam Cloud, we use steamworksClient.cloud.writeFile. This function requires a string as + * the file content. That is why saveData is encoded in base64 and pushed to Steam Cloud as a text file. * * Encoding saveData in UTF-8 (with buffer.toString("utf8")) is not the proper way to convert binary data to string. * Quote from buffer's documentation: "If encoding is 'utf8' and a byte sequence in the input is not valid UTF-8, then @@ -194,7 +192,7 @@ async function pushSaveDataToSteamCloud(saveData, currentPlayerId) { log.debug(`Saving to Steam Cloud as ${steamSaveName}`); try { - await saveCloudFile(steamSaveName, content); + saveCloudFile(steamSaveName, content); } catch (error) { log.error(error); } @@ -208,7 +206,7 @@ async function getSteamCloudSaveData() { return Promise.reject("Steam Cloud is not Enabled"); } log.debug(`Fetching Save in Steam Cloud`); - const cloudString = await getCloudFile(); + const cloudString = getCloudFile(); // Decode cloudString to get save data back. const saveData = Buffer.from(cloudString, "base64"); log.debug(`SaveData: ${saveData.length} bytes`); @@ -216,7 +214,7 @@ async function getSteamCloudSaveData() { } async function saveGameToDisk(window, electronGameData) { - const currentFolder = await getSaveFolder(window); + const currentFolder = getSaveFolder(window); let saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder); const maxFolderSizeBytes = store.get("autosave-quota", 1e8); // 100Mb per playerIndentifier const remainingSpaceBytes = maxFolderSizeBytes - saveFolderSizeBytes; @@ -264,7 +262,7 @@ async function saveGameToDisk(window, electronGameData) { } async function loadLastFromDisk(window) { - const folder = await getSaveFolder(window); + const folder = getSaveFolder(window); const last = await getNewestFile(folder); log.debug(`Last modified file: "${last.file}" (${last.stat.mtime.toLocaleString()})`); return loadFileFromDisk(last.file); @@ -286,7 +284,7 @@ async function loadFileFromDisk(path) { function getSaveInformation(window, save) { return new Promise((resolve) => { - ipcMain.once("get-save-info-response", async (event, data) => { + ipcMain.once("get-save-info-response", (event, data) => { resolve(data); }); window.webContents.send("get-save-info-request", save); @@ -303,7 +301,7 @@ function getCurrentSave(window) { } function pushSaveGameForImport(window, save, automatic) { - ipcMain.once("push-import-result", async (event, arg) => { + ipcMain.once("push-import-result", (event, arg) => { log.debug(`Was save imported? ${arg.wasImported ? "Yes" : "No"}`); }); window.webContents.send("push-save-request", { save, automatic }); @@ -379,7 +377,6 @@ module.exports = { pushSaveGameForImport, pushSaveDataToSteamCloud, getSteamCloudSaveData, - getSteamCloudQuota, deleteCloudFile, saveGameToDisk, loadLastFromDisk, diff --git a/package-lock.json b/package-lock.json index 53acdcf23..d4a21c73e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,7 +79,7 @@ "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.2", "csstype": "3.1.2", - "electron": "^29.4.5", + "electron": "^35.1.5", "electron-packager": "^17.1.2", "eslint": "^8.52.0", "eslint-plugin-react": "^7.33.2", @@ -5129,12 +5129,13 @@ "integrity": "sha512-xPSg0jm4mqgEkNhowKgZFBNtwoEwF6gJ4Dhww+GFpm3IgtNseHQZ5IqdNwnquZEoANxyDAKDRAdVo4Z72VvD/g==" }, "node_modules/@types/node": { - "version": "20.14.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", - "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", + "version": "22.13.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", + "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@types/node-forge": { @@ -8175,14 +8176,15 @@ "dev": true }, "node_modules/electron": { - "version": "29.4.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-29.4.5.tgz", - "integrity": "sha512-DlEuzGbWBYl1Qr0qUYgNZdoixJg4YGHy2HC6fkRjSXSlb01UrQ5ORi8hNLzelzyYx8rNQyyE3zDUuk9EnZwYuA==", + "version": "35.1.5", + "resolved": "https://registry.npmjs.org/electron/-/electron-35.1.5.tgz", + "integrity": "sha512-LolvbKKQUSCGvEwbEQNt1cxD1t+YYClDNwBIjn4d28KM8FSqUn9zJuf6AbqNA7tVs9OFl/EQpmg/m4lZV1hH8g==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^20.9.0", + "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { @@ -18193,10 +18195,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", diff --git a/package.json b/package.json index a91886867..d174271a2 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.2", "csstype": "3.1.2", - "electron": "^29.4.5", + "electron": "^35.1.5", "electron-packager": "^17.1.2", "eslint": "^8.52.0", "eslint-plugin-react": "^7.33.2", diff --git a/tools/package-electron.sh b/tools/package-electron.sh index 920f02af5..b06a6dc7e 100755 --- a/tools/package-electron.sh +++ b/tools/package-electron.sh @@ -15,11 +15,6 @@ cd .. cp -r .app/* .package cp -r electron/* .package -# steam_appid.txt would end up in the resource dir -rm .package/steam_appid.txt - BUILD_PLATFORM="${1:-"all"}" # And finally build the app. npm run electron:packager-$BUILD_PLATFORM - -echo .build/* | xargs -n 1 cp electron/steam_appid.txt