diff --git a/electron_steam_isbn/.gitignore b/electron_steam_isbn/.gitignore new file mode 100644 index 00000000..0cdb30f4 --- /dev/null +++ b/electron_steam_isbn/.gitignore @@ -0,0 +1 @@ +mods/*.js \ No newline at end of file diff --git a/electron_steam_isbn/electron.code-workspace b/electron_steam_isbn/electron.code-workspace new file mode 100644 index 00000000..4a0bde1d --- /dev/null +++ b/electron_steam_isbn/electron.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "files.exclude": { + "**/node_modules": true, + "**/typedefs_gen": true + } + } +} \ No newline at end of file diff --git a/electron_steam_isbn/favicon.icns b/electron_steam_isbn/favicon.icns new file mode 100644 index 00000000..79e141a5 Binary files /dev/null and b/electron_steam_isbn/favicon.icns differ diff --git a/electron_steam_isbn/favicon.ico b/electron_steam_isbn/favicon.ico new file mode 100644 index 00000000..81a9aa5c Binary files /dev/null and b/electron_steam_isbn/favicon.ico differ diff --git a/electron_steam_isbn/favicon.png b/electron_steam_isbn/favicon.png new file mode 100644 index 00000000..c837c787 Binary files /dev/null and b/electron_steam_isbn/favicon.png differ diff --git a/electron_steam_isbn/index.js b/electron_steam_isbn/index.js new file mode 100644 index 00000000..60def14f --- /dev/null +++ b/electron_steam_isbn/index.js @@ -0,0 +1,389 @@ +/* eslint-disable quotes,no-undef */ + +const { app, BrowserWindow, Menu, MenuItem, ipcMain, shell, dialog, session } = require("electron"); +const path = require("path"); +const url = require("url"); +const fs = require("fs"); +const steam = require("./steam"); +const asyncLock = require("async-lock"); +const windowStateKeeper = require("electron-window-state"); + +// Disable hardware key handling, i.e. being able to pause/resume the game music +// with hardware keys +app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling"); + +const isDev = app.commandLine.hasSwitch("dev"); +const isLocal = app.commandLine.hasSwitch("local"); +const safeMode = app.commandLine.hasSwitch("safe-mode"); +const externalMod = app.commandLine.getSwitchValue("load-mod"); + +const roamingFolder = + process.env.APPDATA || + (process.platform == "darwin" + ? process.env.HOME + "/Library/Preferences" + : process.env.HOME + "/.local/share"); + +let storePath = path.join(roamingFolder, "shapez-china", "saves"); +let modsPath = path.join(roamingFolder, "shapez-china", "mods"); + +if (!fs.existsSync(storePath)) { + // No try-catch by design + fs.mkdirSync(storePath, { recursive: true }); +} + +if (!fs.existsSync(modsPath)) { + fs.mkdirSync(modsPath, { recursive: true }); +} + +/** @type {BrowserWindow} */ +let win = null; +let menu = null; + +function createWindow() { + let faviconExtension = ".png"; + if (process.platform === "win32") { + faviconExtension = ".ico"; + } + + const mainWindowState = windowStateKeeper({ + defaultWidth: 1000, + defaultHeight: 800, + }); + + win = new BrowserWindow({ + x: mainWindowState.x, + y: mainWindowState.y, + width: mainWindowState.width, + height: mainWindowState.height, + show: false, + backgroundColor: "#222428", + useContentSize: false, + minWidth: 800, + minHeight: 600, + title: "图形工厂", + transparent: false, + icon: path.join(__dirname, "favicon" + faviconExtension), + // fullscreen: true, + autoHideMenuBar: !isDev, + webPreferences: { + nodeIntegration: false, + nodeIntegrationInWorker: false, + nodeIntegrationInSubFrames: false, + contextIsolation: true, + enableRemoteModule: false, + disableBlinkFeatures: "Auxclick", + + webSecurity: true, + sandbox: true, + preload: path.join(__dirname, "preload.js"), + experimentalFeatures: false, + }, + allowRunningInsecureContent: false, + }); + + mainWindowState.manage(win); + + if (isLocal) { + win.loadURL("http://localhost:3005"); + } else { + win.loadURL( + url.format({ + pathname: path.join(__dirname, "index.html"), + protocol: "file:", + slashes: true, + }) + ); + } + win.webContents.session.clearCache(); + win.webContents.session.clearStorageData(); + + ////// SECURITY + + // Disable permission requests + win.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => { + callback(false); + }); + session.fromPartition("default").setPermissionRequestHandler((webContents, permission, callback) => { + callback(false); + }); + + app.on("web-contents-created", (event, contents) => { + // Disable vewbiew + contents.on("will-attach-webview", (event, webPreferences, params) => { + event.preventDefault(); + }); + // Disable navigation + contents.on("will-navigate", (event, navigationUrl) => { + event.preventDefault(); + }); + }); + + win.webContents.on("will-redirect", (contentsEvent, navigationUrl) => { + // Log and prevent the app from redirecting to a new page + console.error( + `The application tried to redirect to the following address: '${navigationUrl}'. This attempt was blocked.` + ); + contentsEvent.preventDefault(); + }); + + // Filter loading any module via remote; + // you shouldn't be using remote at all, though + // https://electronjs.org/docs/tutorial/security#16-filter-the-remote-module + app.on("remote-require", (event, webContents, moduleName) => { + event.preventDefault(); + }); + + // built-ins are modules such as "app" + app.on("remote-get-builtin", (event, webContents, moduleName) => { + event.preventDefault(); + }); + + app.on("remote-get-global", (event, webContents, globalName) => { + event.preventDefault(); + }); + + app.on("remote-get-current-window", (event, webContents) => { + event.preventDefault(); + }); + + app.on("remote-get-current-web-contents", (event, webContents) => { + event.preventDefault(); + }); + + //// END SECURITY + + win.webContents.on("new-window", (event, pth) => { + event.preventDefault(); + + if (pth.startsWith("https://") || pth.startsWith("steam://")) { + shell.openExternal(pth); + } + }); + + win.on("closed", () => { + console.log("Window closed"); + win = null; + }); + + if (isDev) { + menu = new Menu(); + + win.webContents.toggleDevTools(); + + const mainItem = new MenuItem({ + label: "Toggle Dev Tools", + click: () => win.webContents.toggleDevTools(), + accelerator: "F12", + }); + menu.append(mainItem); + + const reloadItem = new MenuItem({ + label: "Reload", + click: () => win.reload(), + accelerator: "F5", + }); + menu.append(reloadItem); + + const fullscreenItem = new MenuItem({ + label: "Fullscreen", + click: () => win.setFullScreen(!win.isFullScreen()), + accelerator: "F11", + }); + menu.append(fullscreenItem); + + const mainMenu = new Menu(); + mainMenu.append( + new MenuItem({ + label: "shapez.io", + submenu: menu, + }) + ); + + Menu.setApplicationMenu(mainMenu); + } else { + Menu.setApplicationMenu(null); + } + + win.once("ready-to-show", () => { + win.show(); + win.focus(); + }); +} + +if (!app.requestSingleInstanceLock()) { + app.exit(0); +} else { + app.on("second-instance", () => { + // Someone tried to run a second instance, we should focus + if (win) { + if (win.isMinimized()) { + win.restore(); + } + win.focus(); + } + }); +} + +app.on("ready", createWindow); + +app.on("window-all-closed", () => { + console.log("All windows closed"); + app.quit(); +}); + +ipcMain.on("set-fullscreen", (event, flag) => { + win.setFullScreen(flag); +}); + +ipcMain.on("exit-app", () => { + win.close(); + app.quit(); +}); + +let renameCounter = 1; + +const fileLock = new asyncLock({ + timeout: 30000, + maxPending: 1000, +}); + +function niceFileName(filename) { + return filename.replace(storePath, "@"); +} + +async function writeFileSafe(filename, contents) { + ++renameCounter; + const prefix = "[ " + renameCounter + ":" + niceFileName(filename) + " ] "; + const transactionId = String(new Date().getTime()) + "." + renameCounter; + + if (fileLock.isBusy()) { + console.warn(prefix, "Concurrent write process on", filename); + } + + fileLock.acquire(filename, async () => { + console.log(prefix, "Starting write on", niceFileName(filename), "in transaction", transactionId); + + if (!fs.existsSync(filename)) { + // this one is easy + console.log(prefix, "Writing file instantly because it does not exist:", niceFileName(filename)); + await fs.promises.writeFile(filename, contents, "utf8"); + return; + } + + // first, write a temporary file (.tmp-XXX) + const tempName = filename + ".tmp-" + transactionId; + console.log(prefix, "Writing temporary file", niceFileName(tempName)); + await fs.promises.writeFile(tempName, contents, "utf8"); + + // now, rename the original file to (.backup-XXX) + const oldTemporaryName = filename + ".backup-" + transactionId; + console.log( + prefix, + "Renaming old file", + niceFileName(filename), + "to", + niceFileName(oldTemporaryName) + ); + await fs.promises.rename(filename, oldTemporaryName); + + // now, rename the temporary file (.tmp-XXX) to the target + console.log( + prefix, + "Renaming the temporary file", + niceFileName(tempName), + "to the original", + niceFileName(filename) + ); + await fs.promises.rename(tempName, filename); + + // we are done now, try to create a backup, but don't fail if the backup fails + try { + // check if there is an old backup file + const backupFileName = filename + ".backup"; + if (fs.existsSync(backupFileName)) { + console.log(prefix, "Deleting old backup file", niceFileName(backupFileName)); + // delete the old backup + await fs.promises.unlink(backupFileName); + } + + // rename the old file to the new backup file + console.log(prefix, "Moving", niceFileName(oldTemporaryName), "to the backup file location"); + await fs.promises.rename(oldTemporaryName, backupFileName); + } catch (ex) { + console.error(prefix, "Failed to switch backup files:", ex); + } + }); +} + +ipcMain.handle("fs-job", async (event, job) => { + const filenameSafe = job.filename.replace(/[^a-z\.\-_0-9]/gi, "_"); + const fname = path.join(storePath, filenameSafe); + switch (job.type) { + case "read": { + if (!fs.existsSync(fname)) { + // Special FILE_NOT_FOUND error code + return { error: "file_not_found" }; + } + return await fs.promises.readFile(fname, "utf8"); + } + case "write": { + await writeFileSafe(fname, job.contents); + return job.contents; + } + + case "delete": { + await fs.promises.unlink(fname); + return; + } + + default: + throw new Error("Unknown fs job: " + job.type); + } +}); + +ipcMain.handle("open-mods-folder", async () => { + shell.openPath(modsPath); +}); + +console.log("Loading mods ..."); + +function loadMods() { + if (safeMode) { + console.log("Safe Mode enabled for mods, skipping mod search"); + } + console.log("Loading mods from", modsPath); + let modFiles = safeMode + ? [] + : fs + .readdirSync(modsPath) + .filter(filename => filename.endsWith(".js")) + .map(filename => path.join(modsPath, filename)); + + if (externalMod) { + console.log("Adding external mod source:", externalMod); + const externalModPaths = externalMod.split(","); + modFiles = modFiles.concat(externalModPaths); + } + + return modFiles.map(filename => fs.readFileSync(filename, "utf8")); +} + +let mods = []; +try { + mods = loadMods(); + console.log("Loaded", mods.length, "mods"); +} catch (ex) { + console.error("Failed to load mods"); + dialog.showErrorBox("Failed to load mods:", ex); +} + +ipcMain.handle("get-mods", async () => { + return mods; +}); + +steam.init(isDev); + +// Only allow achievements and puzzle DLC if no mods are loaded +if (mods.length === 0) { + steam.listen(); +} diff --git a/electron_steam_isbn/mods/README.txt b/electron_steam_isbn/mods/README.txt new file mode 100644 index 00000000..666cc18f --- /dev/null +++ b/electron_steam_isbn/mods/README.txt @@ -0,0 +1,6 @@ +Here you can place mods. Every mod should be a single file ending with ".js". + +--- WARNING --- +Mods can potentially access to your filesystem. +Please only install mods from trusted sources and developers. +--- WARNING --- diff --git a/electron_steam_isbn/package.json b/electron_steam_isbn/package.json new file mode 100644 index 00000000..4ae171f8 --- /dev/null +++ b/electron_steam_isbn/package.json @@ -0,0 +1,21 @@ +{ + "name": "electron", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "private": true, + "scripts": { + "startDev": "electron --disable-direct-composition --in-process-gpu . --dev --local", + "startDevGpu": "electron --enable-gpu-rasterization --enable-accelerated-2d-canvas --num-raster-threads=8 --enable-zero-copy . --dev --local", + "start": "electron --disable-direct-composition --in-process-gpu ." + }, + "devDependencies": {}, + "optionalDependencies": { + "shapez.io-private-artifacts": "github:tobspr/shapez.io-private-artifacts#abi-v99" + }, + "dependencies": { + "async-lock": "^1.2.8", + "electron": "16.2.8", + "electron-window-state": "^5.0.3" + } +} diff --git a/electron_steam_isbn/preload.js b/electron_steam_isbn/preload.js new file mode 100644 index 00000000..c6336230 --- /dev/null +++ b/electron_steam_isbn/preload.js @@ -0,0 +1,7 @@ +const { contextBridge, ipcRenderer } = require("electron"); + +contextBridge.exposeInMainWorld("ipcRenderer", { + invoke: ipcRenderer.invoke.bind(ipcRenderer), + on: ipcRenderer.on.bind(ipcRenderer), + send: ipcRenderer.send.bind(ipcRenderer), +}); diff --git a/electron_steam_isbn/steam.js b/electron_steam_isbn/steam.js new file mode 100644 index 00000000..cdda540b --- /dev/null +++ b/electron_steam_isbn/steam.js @@ -0,0 +1,112 @@ +const fs = require("fs"); +const path = require("path"); +const { ipcMain } = require("electron"); + +let greenworks = null; +let appId = null; +let initialized = false; + +try { + greenworks = require("shapez.io-private-artifacts/steam/greenworks"); + appId = parseInt(fs.readFileSync(path.join(__dirname, "steam_appid.txt"), "utf8")); +} catch (err) { + // greenworks is not installed + console.warn("Failed to load steam api:", err); +} + +console.log("App ID:", appId); + +function init(isDev) { + if (!greenworks) { + return; + } + + if (!isDev) { + if (greenworks.restartAppIfNecessary(appId)) { + console.log("Restarting ..."); + process.exit(0); + } + } + + if (!greenworks.init()) { + console.log("Failed to initialize greenworks"); + process.exit(1); + } + + initialized = true; +} + +function listen() { + ipcMain.handle("steam:is-initialized", isInitialized); + + if (!initialized) { + console.warn("Steam not initialized, won't be able to listen"); + return; + } + + if (!greenworks) { + console.warn("Greenworks not loaded, won't be able to listen"); + return; + } + + console.log("Adding listeners"); + + ipcMain.handle("steam:get-achievement-names", getAchievementNames); + ipcMain.handle("steam:activate-achievement", activateAchievement); + + function bufferToHex(buffer) { + return Array.from(new Uint8Array(buffer)) + .map(b => b.toString(16).padStart(2, "0")) + .join(""); + } + + ipcMain.handle("steam:get-ticket", (event, arg) => { + console.log("Requested steam ticket ..."); + return new Promise((resolve, reject) => { + greenworks.getAuthSessionTicket( + success => { + const ticketHex = bufferToHex(success.ticket); + resolve(ticketHex); + }, + error => { + console.error("Failed to get steam ticket:", error); + reject(error); + } + ); + }); + }); + + ipcMain.handle("steam:check-app-ownership", (event, appId) => { + return Promise.resolve(greenworks.isDLCInstalled(appId)); + }); +} + +function isInitialized(event) { + return Promise.resolve(initialized); +} + +function getAchievementNames(event) { + return new Promise((resolve, reject) => { + try { + const achievements = greenworks.getAchievementNames(); + resolve(achievements); + } catch (err) { + reject(err); + } + }); +} + +function activateAchievement(event, id) { + return new Promise((resolve, reject) => { + greenworks.activateAchievement( + id, + () => resolve(), + err => reject(err) + ); + }); +} + +module.exports = { + init, + listen, +}; diff --git a/electron_steam_isbn/steam_appid.txt b/electron_steam_isbn/steam_appid.txt new file mode 100644 index 00000000..a8e9e809 --- /dev/null +++ b/electron_steam_isbn/steam_appid.txt @@ -0,0 +1 @@ +1318690 diff --git a/electron_steam_isbn/yarn.lock b/electron_steam_isbn/yarn.lock new file mode 100644 index 00000000..f33e463b --- /dev/null +++ b/electron_steam_isbn/yarn.lock @@ -0,0 +1,584 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@electron/get@^1.13.0": + version "1.13.1" + resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.13.1.tgz#42a0aa62fd1189638bd966e23effaebb16108368" + integrity sha512-U5vkXDZ9DwXtkPqlB45tfYnnYBN8PePp1z/XDCupnSpdrxT8/ThCv9WCwPLf9oqiSGZTkH6dx2jDUPuoXpjkcA== + dependencies: + debug "^4.1.1" + env-paths "^2.2.0" + fs-extra "^8.1.0" + got "^9.6.0" + progress "^2.0.3" + semver "^6.2.0" + sumchecker "^3.0.1" + optionalDependencies: + global-agent "^3.0.0" + global-tunnel-ng "^2.7.1" + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + +"@types/node@^14.6.2": + version "14.18.20" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.20.tgz#268f028b36eaf51181c3300252f605488c4f0650" + integrity sha512-Q8KKwm9YqEmUBRsqJ2GWJDtXltBDxTdC4m5vTdXBolu2PeQh8LX+f6BTwU+OuXPu37fLxoN6gidqBmnky36FXA== + +async-lock@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.2.8.tgz#7b02bdfa2de603c0713acecd11184cf97bbc7c4c" + integrity sha512-G+26B2jc0Gw0EG/WN2M6IczuGepBsfR1+DtqLnyFSH4p2C668qkOCtEkGNVEaaNAVlYwEMazy1+/jnLxltBkIQ== + +boolean@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.0.2.tgz#df1baa18b6a2b0e70840475e1d93ec8fe75b2570" + integrity sha512-RwywHlpCRc3/Wh81MiCKun4ydaIFyW5Ea6JbL6sRCVx5q5irDw7pMXBUFYF/jArQ6YrG36q0kpovc9P/Kd3I4g== + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + +clone-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= + dependencies: + mimic-response "^1.0.0" + +concat-stream@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +config-chain@^1.1.11: + version "1.1.12" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" + integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.1.0, debug@^4.1.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= + dependencies: + mimic-response "^1.0.0" + +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + +define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +detect-node@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + +electron-window-state@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/electron-window-state/-/electron-window-state-5.0.3.tgz#4f36d09e3f953d87aff103bf010f460056050aa8" + integrity sha512-1mNTwCfkolXl3kMf50yW3vE2lZj0y92P/HYWFBrb+v2S/pCka5mdwN3cagKm458A7NjndSwijynXgcLWRodsVg== + dependencies: + jsonfile "^4.0.0" + mkdirp "^0.5.1" + +electron@16.2.8: + version "16.2.8" + resolved "https://registry.yarnpkg.com/electron/-/electron-16.2.8.tgz#b7f2bd1184701e54a1bc902839d5a3ec95bb8982" + integrity sha512-KSOytY6SPLsh3iCziztqa/WgJyfDOKzCvNqku9gGIqSdT8CqtV66dTU1SOrKZQjRFLxHaF8LbyxUL1vwe4taqw== + dependencies: + "@electron/get" "^1.13.0" + "@types/node" "^14.6.2" + extract-zip "^1.0.3" + +encodeurl@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +es6-error@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" + integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +extract-zip@^1.0.3: + version "1.7.0" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" + integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== + dependencies: + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +get-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +global-agent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-3.0.0.tgz#ae7cd31bd3583b93c5a16437a1afe27cc33a1ab6" + integrity sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q== + dependencies: + boolean "^3.0.1" + es6-error "^4.1.1" + matcher "^3.0.0" + roarr "^2.15.3" + semver "^7.3.2" + serialize-error "^7.0.1" + +global-tunnel-ng@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz#d03b5102dfde3a69914f5ee7d86761ca35d57d8f" + integrity sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg== + dependencies: + encodeurl "^1.0.2" + lodash "^4.17.10" + npm-conf "^1.1.3" + tunnel "^0.0.6" + +globalthis@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.2.tgz#2a235d34f4d8036219f7e34929b5de9e18166b8b" + integrity sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ== + dependencies: + define-properties "^1.1.3" + +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + +inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.4: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= + +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + +lodash@^4.17.10: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +matcher@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" + integrity sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng== + dependencies: + escape-string-regexp "^4.0.0" + +mimic-response@^1.0.0, mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@^0.5.1, mkdirp@^0.5.4: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +normalize-url@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" + integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== + +npm-conf@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9" + integrity sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw== + dependencies: + config-chain "^1.1.11" + pify "^3.0.0" + +object-keys@^1.0.12: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +readable-stream@^2.2.2: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= + dependencies: + lowercase-keys "^1.0.0" + +roarr@^2.15.3: + version "2.15.4" + resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.15.4.tgz#f5fe795b7b838ccfe35dc608e0282b9eba2e7afd" + integrity sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A== + dependencies: + boolean "^3.0.1" + detect-node "^2.0.4" + globalthis "^1.0.1" + json-stringify-safe "^5.0.1" + semver-compare "^1.0.0" + sprintf-js "^1.1.2" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + +semver@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.2: + version "7.3.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" + integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== + dependencies: + lru-cache "^6.0.0" + +serialize-error@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" + integrity sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw== + dependencies: + type-fest "^0.13.1" + +"shapez.io-private-artifacts@github:tobspr/shapez.io-private-artifacts#abi-v99": + version "0.1.0" + resolved "git+ssh://git@github.com/tobspr/shapez.io-private-artifacts.git#3293b20be26060fd36e9f00ded9ab5d0bdf57338" + +sprintf-js@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" + integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +sumchecker@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42" + integrity sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg== + dependencies: + debug "^4.1.0" + +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + +tunnel@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + +type-fest@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" + integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= + dependencies: + prepend-http "^2.0.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" diff --git a/electron_wegame/wegame.js b/electron_wegame/wegame.js index 05a0e186..81c3f9d1 100644 --- a/electron_wegame/wegame.js +++ b/electron_wegame/wegame.js @@ -7,7 +7,7 @@ function init(isDev) { try { console.log("Step 2: Calling need restart app"); const need_restart = railsdk.RailNeedRestartAppForCheckingEnvironment( - 2001639, + 2002030, [`--rail_render_pid=${process.pid}`] //,"--rail_debug_mode", ); console.log("Step 3: Needs restart =", need_restart); @@ -58,6 +58,22 @@ function listen() { return data; }); + + ipcMain.handle("wegame:activate-achievement", async (event, data) => { + console.log("Unlock wegame achievement", data); + + var manager = railsdk.RailAchievementHelper.CreatePlayerAchievement("0"); + var result = manager.MakeAchievement(data); + if (result != railsdk.RailResult.kSuccess) { + console.error("Unlock wegame achievement", data, "failed with code", result); + return false; + } + manager.AsyncStoreAchievement().then( + () => console.log("Achievements stored successfully."), + err => console.error("Failed to unlock achievement async:", err) + ); + return true; + }); } module.exports = { init, listen }; diff --git a/gulp/build_variants.js b/gulp/build_variants.js index 4f67bf9e..3fad5281 100644 --- a/gulp/build_variants.js +++ b/gulp/build_variants.js @@ -9,6 +9,7 @@ * chineseVersion?: boolean, * wegameVersion?: boolean, * steamDemo?: boolean, + * steamIsbnVersion?: boolean, * gogVersion?: boolean * }}>} */ @@ -63,6 +64,13 @@ const BUILD_VARIANTS = { wegameVersion: true, }, }, + "standalone-steam-isbn": { + standalone: true, + electronBaseDir: "electron_steam_isbn", + buildArgs: { + steamIsbnVersion: true, + }, + }, "standalone-gog": { standalone: true, electronBaseDir: "electron_gog", diff --git a/gulp/webpack.config.js b/gulp/webpack.config.js index d0ee4d5f..9f8d0cba 100644 --- a/gulp/webpack.config.js +++ b/gulp/webpack.config.js @@ -10,6 +10,7 @@ module.exports = ({ standalone = false, chineseVersion = false, wegameVersion = false, + steamIsbnVersion = false, steamDemo = false, gogVersion = false, }) => { @@ -39,6 +40,8 @@ module.exports = ({ G_APP_ENVIRONMENT: JSON.stringify("dev"), G_CHINA_VERSION: JSON.stringify(chineseVersion), G_WEGAME_VERSION: JSON.stringify(wegameVersion), + G_ISBN_VERSION: JSON.stringify(wegameVersion || steamIsbnVersion), + G_STEAM_ISBN_VERSION: JSON.stringify(steamIsbnVersion), G_GOG_VERSION: JSON.stringify(gogVersion), G_IS_DEV: "true", G_IS_RELEASE: "false", diff --git a/gulp/webpack.production.config.js b/gulp/webpack.production.config.js index e324d675..ecff5dad 100644 --- a/gulp/webpack.production.config.js +++ b/gulp/webpack.production.config.js @@ -17,6 +17,7 @@ module.exports = ({ chineseVersion = false, wegameVersion = false, + steamIsbnVersion = false, steamDemo = false, gogVersion = false, }) => { @@ -28,6 +29,8 @@ module.exports = ({ G_CHINA_VERSION: JSON.stringify(chineseVersion), G_WEGAME_VERSION: JSON.stringify(wegameVersion), + G_ISBN_VERSION: JSON.stringify(wegameVersion || steamIsbnVersion), + G_STEAM_ISBN_VERSION: JSON.stringify(steamIsbnVersion), G_GOG_VERSION: JSON.stringify(gogVersion), G_IS_RELEASE: environment === "prod" ? "true" : "false", G_IS_STANDALONE: standalone ? "true" : "false", diff --git a/src/js/application.js b/src/js/application.js index d31e6e3f..bfb050ab 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -153,7 +153,7 @@ export class Application { Loader.linkAppAfterBoot(this); - if (G_WEGAME_VERSION) { + if (G_ISBN_VERSION) { this.stateMgr.moveToState("WegameSplashState"); } diff --git a/src/js/core/globals.js b/src/js/core/globals.js index 55238e85..97c98754 100644 --- a/src/js/core/globals.js +++ b/src/js/core/globals.js @@ -21,6 +21,8 @@ export const BUILD_OPTIONS = { APP_ENVIRONMENT: G_APP_ENVIRONMENT, CHINA_VERSION: G_CHINA_VERSION, WEGAME_VERSION: G_WEGAME_VERSION, + ISBN_VERSION: G_ISBN_VERSION, + STEAM_ISBN_VERSION: G_STEAM_ISBN_VERSION, IS_DEV: G_IS_DEV, IS_RELEASE: G_IS_RELEASE, IS_BROWSER: G_IS_BROWSER, diff --git a/src/js/core/utils.js b/src/js/core/utils.js index 941f5c46..a5f67d3e 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -247,7 +247,7 @@ export function formatBigNumber(num, separator = T.global.decimalSeparator) { if (num < 1000) { return sign + "" + num; } else { - if (G_WEGAME_VERSION) { + if (G_ISBN_VERSION) { if (num < 1000000) { if (num < 10000) { return sign + String(num).replace(".0", "").replace(".", separator); @@ -704,7 +704,7 @@ const romanLiteralsCache = ["0"]; * @returns {string} */ export function getRomanNumber(number) { - if (G_WEGAME_VERSION) { + if (G_ISBN_VERSION) { return String(number); } @@ -763,7 +763,7 @@ export function getRomanNumber(number) { * Returns the appropriate logo sprite path */ export function getLogoSprite() { - if (G_WEGAME_VERSION) { + if (G_ISBN_VERSION) { return "logo_wegame.png"; } diff --git a/src/js/game/hud/parts/interactive_tutorial.js b/src/js/game/hud/parts/interactive_tutorial.js index 7548ce43..1601dfdb 100644 --- a/src/js/game/hud/parts/interactive_tutorial.js +++ b/src/js/game/hud/parts/interactive_tutorial.js @@ -188,9 +188,7 @@ export class HUDInteractiveTutorial extends BaseHUDPart { onHintChanged(hintId) { this.elementDescription.innerHTML = T.ingame.interactiveTutorial.hints[hintId]; document.documentElement.setAttribute("data-tutorial-step", hintId); - const folder = G_WEGAME_VERSION - ? "interactive_tutorial.cn.noinline" - : "interactive_tutorial.noinline"; + const folder = G_ISBN_VERSION ? "interactive_tutorial.cn.noinline" : "interactive_tutorial.noinline"; this.elementGif.style.backgroundImage = "url('" + cachebust("res/ui/" + folder + "/" + hintId + ".gif") + "')"; diff --git a/src/js/game/hud/parts/pinned_shapes.js b/src/js/game/hud/parts/pinned_shapes.js index c5bb9a82..99480e7a 100644 --- a/src/js/game/hud/parts/pinned_shapes.js +++ b/src/js/game/hud/parts/pinned_shapes.js @@ -233,7 +233,7 @@ export class HUDPinnedShapes extends BaseHUDPart { // Show small info icon let infoDetector; - if (!G_WEGAME_VERSION) { + if (!G_ISBN_VERSION) { const infoButton = document.createElement("button"); infoButton.classList.add("infoButton"); element.appendChild(infoButton); diff --git a/src/js/game/hud/parts/puzzle_dlc_logo.js b/src/js/game/hud/parts/puzzle_dlc_logo.js index 69519c0d..32c5d3d5 100644 --- a/src/js/game/hud/parts/puzzle_dlc_logo.js +++ b/src/js/game/hud/parts/puzzle_dlc_logo.js @@ -4,7 +4,7 @@ import { BaseHUDPart } from "../base_hud_part"; export class HUDPuzzleDLCLogo extends BaseHUDPart { createElements(parent) { this.element = makeDiv(parent, "ingame_HUD_PuzzleDLCLogo"); - this.element.classList.toggle("china", G_CHINA_VERSION || G_WEGAME_VERSION); + this.element.classList.toggle("china", G_CHINA_VERSION || G_ISBN_VERSION); parent.appendChild(this.element); } diff --git a/src/js/game/hud/parts/shop.js b/src/js/game/hud/parts/shop.js index fa92b743..f0890e73 100644 --- a/src/js/game/hud/parts/shop.js +++ b/src/js/game/hud/parts/shop.js @@ -123,7 +123,7 @@ export class HUDShop extends BaseHUDPart { container.appendChild(pinButton); let infoDetector; - if (!G_WEGAME_VERSION) { + if (!G_ISBN_VERSION) { const viewInfoButton = document.createElement("button"); viewInfoButton.classList.add("showInfo"); container.appendChild(viewInfoButton); diff --git a/src/js/game/hud/parts/waypoints.js b/src/js/game/hud/parts/waypoints.js index 5aac95dc..43e47dc4 100644 --- a/src/js/game/hud/parts/waypoints.js +++ b/src/js/game/hud/parts/waypoints.js @@ -45,7 +45,7 @@ export class HUDWaypoints extends BaseHUDPart { */ createElements(parent) { // Create the helper box on the lower right when zooming out - if (this.root.app.settings.getAllSettings().offerHints && !G_WEGAME_VERSION) { + if (this.root.app.settings.getAllSettings().offerHints && !G_ISBN_VERSION) { this.hintElement = makeDiv( parent, "ingame_HUD_Waypoints_Hint", @@ -121,7 +121,7 @@ export class HUDWaypoints extends BaseHUDPart { } // Catch mouse and key events - if (!G_WEGAME_VERSION) { + if (!G_ISBN_VERSION) { this.root.camera.downPreHandler.add(this.onMouseDown, this); this.root.keyMapper .getBinding(KEYMAPPINGS.navigation.createMarker) diff --git a/src/js/game/modes/levels.js b/src/js/game/modes/levels.js index d6533448..d9f91e0a 100644 --- a/src/js/game/modes/levels.js +++ b/src/js/game/modes/levels.js @@ -5,7 +5,7 @@ import { WEB_STEAM_SSO_AUTHENTICATED } from "../../core/steam_sso"; import { enumHubGoalRewards } from "../tutorial_goals"; export const finalGameShape = "RuCw--Cw:----Ru--"; -const chinaShapes = G_WEGAME_VERSION || G_CHINA_VERSION; +const chinaShapes = G_ISBN_VERSION || G_CHINA_VERSION; //////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////// diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index 8d5d6633..f847bd49 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -65,7 +65,7 @@ const preparementShape = "CpRpCp--:SwSwSwSw"; // Tiers need % of the previous tier as requirement too const tierGrowth = 2.5; -const chinaShapes = G_WEGAME_VERSION || G_CHINA_VERSION; +const chinaShapes = G_ISBN_VERSION || G_CHINA_VERSION; const upgradesCache = {}; @@ -362,7 +362,7 @@ export class RegularGameMode extends GameMode { } if (this.root.app.settings.getAllSettings().offerHints) { - if (!G_WEGAME_VERSION) { + if (!G_ISBN_VERSION) { this.additionalHudParts.tutorialHints = HUDPartTutorialHints; } this.additionalHudParts.interactiveTutorial = HUDInteractiveTutorial; diff --git a/src/js/globals.d.ts b/src/js/globals.d.ts index 37eb4c3f..724b65ee 100644 --- a/src/js/globals.d.ts +++ b/src/js/globals.d.ts @@ -20,6 +20,8 @@ declare const G_IS_RELEASE: boolean; declare const G_CHINA_VERSION: boolean; declare const G_WEGAME_VERSION: boolean; +declare const G_ISBN_VERSION: boolean; +declare const G_STEAM_ISBN_VERSION: boolean; declare const G_GOG_VERSION: boolean; declare const shapez: any; diff --git a/src/js/languages.js b/src/js/languages.js index 6e7bedf6..0a8cbb5b 100644 --- a/src/js/languages.js +++ b/src/js/languages.js @@ -12,7 +12,7 @@ export const LANGUAGES = { "zh-CN": { // simplified chinese name: "简体中文", - data: G_WEGAME_VERSION + data: G_ISBN_VERSION ? require("./built-temp/base-zh-CN-ISBN.json") : require("./built-temp/base-zh-CN.json"), code: "zh", diff --git a/src/js/platform/browser/game_analytics.js b/src/js/platform/browser/game_analytics.js index 894f877a..92f99e04 100644 --- a/src/js/platform/browser/game_analytics.js +++ b/src/js/platform/browser/game_analytics.js @@ -114,7 +114,7 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { initialize() { this.syncKey = null; - if (G_WEGAME_VERSION) { + if (G_ISBN_VERSION) { return; } @@ -221,7 +221,7 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { * @returns {Promise} */ sendToApi(endpoint, data) { - if (G_WEGAME_VERSION) { + if (G_ISBN_VERSION) { return Promise.resolve(); } @@ -263,7 +263,7 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { * @param {string} value */ sendGameEvent(category, value) { - if (G_WEGAME_VERSION) { + if (G_ISBN_VERSION) { return; } diff --git a/src/js/platform/electron/wegame_achievement_provider.js b/src/js/platform/electron/wegame_achievement_provider.js new file mode 100644 index 00000000..15cab3c5 --- /dev/null +++ b/src/js/platform/electron/wegame_achievement_provider.js @@ -0,0 +1,141 @@ +/* typehints:start */ +import { Application } from "../../application"; +import { GameRoot } from "../../game/root"; +/* typehints:end */ + +import { createLogger } from "../../core/logging"; +import { ACHIEVEMENTS, AchievementCollection, AchievementProviderInterface } from "../achievement_provider"; + +const logger = createLogger("achievements/wegame"); + +const ACHIEVEMENT_IDS = { + [ACHIEVEMENTS.belt500Tiles]: "belt_500_tiles", + [ACHIEVEMENTS.blueprint100k]: "blueprint_100k", + [ACHIEVEMENTS.blueprint1m]: "blueprint_1m", + [ACHIEVEMENTS.completeLvl26]: "complete_lvl_26", + [ACHIEVEMENTS.cutShape]: "cut_shape", + [ACHIEVEMENTS.darkMode]: "dark_mode", + [ACHIEVEMENTS.destroy1000]: "destroy_1000", + [ACHIEVEMENTS.irrelevantShape]: "irrelevant_shape", + [ACHIEVEMENTS.level100]: "level_100", + [ACHIEVEMENTS.level50]: "level_50", + [ACHIEVEMENTS.logoBefore18]: "logo_before_18", + [ACHIEVEMENTS.mam]: "mam", + [ACHIEVEMENTS.mapMarkers15]: "map_markers_15", + [ACHIEVEMENTS.openWires]: "open_wires", + [ACHIEVEMENTS.oldLevel17]: "old_level_17", + [ACHIEVEMENTS.noBeltUpgradesUntilBp]: "no_belt_upgrades_until_bp", + [ACHIEVEMENTS.noInverseRotater]: "no_inverse_rotator", // [sic] + [ACHIEVEMENTS.paintShape]: "paint_shape", + [ACHIEVEMENTS.place5000Wires]: "place_5000_wires", + [ACHIEVEMENTS.placeBlueprint]: "place_blueprint", + [ACHIEVEMENTS.placeBp1000]: "place_bp_1000", + [ACHIEVEMENTS.play1h]: "play_1h", + [ACHIEVEMENTS.play10h]: "play_10h", + [ACHIEVEMENTS.play20h]: "play_20h", + [ACHIEVEMENTS.produceLogo]: "produce_logo", + [ACHIEVEMENTS.produceMsLogo]: "produce_ms_logo", + [ACHIEVEMENTS.produceRocket]: "produce_rocket", + [ACHIEVEMENTS.rotateShape]: "rotate_shape", + [ACHIEVEMENTS.speedrunBp30]: "speedrun_bp_30", + [ACHIEVEMENTS.speedrunBp60]: "speedrun_bp_60", + [ACHIEVEMENTS.speedrunBp120]: "speedrun_bp_120", + [ACHIEVEMENTS.stack4Layers]: "stack_4_layers", + [ACHIEVEMENTS.stackShape]: "stack_shape", + [ACHIEVEMENTS.store100Unique]: "store_100_unique", + [ACHIEVEMENTS.storeShape]: "store_shape", + [ACHIEVEMENTS.throughputBp25]: "throughput_bp_25", + [ACHIEVEMENTS.throughputBp50]: "throughput_bp_50", + [ACHIEVEMENTS.throughputLogo25]: "throughput_logo_25", + [ACHIEVEMENTS.throughputLogo50]: "throughput_logo_50", + [ACHIEVEMENTS.throughputRocket10]: "throughput_rocket_10", + [ACHIEVEMENTS.throughputRocket20]: "throughput_rocket_20", + [ACHIEVEMENTS.trash1000]: "trash_1000", + [ACHIEVEMENTS.unlockWires]: "unlock_wires", + [ACHIEVEMENTS.upgradesTier5]: "upgrades_tier_5", + [ACHIEVEMENTS.upgradesTier8]: "upgrades_tier_8", +}; + +export class WegameAchievementProvider extends AchievementProviderInterface { + /** @param {Application} app */ + constructor(app) { + super(app); + + this.initialized = false; + this.collection = new AchievementCollection(this.activate.bind(this)); + + if (G_IS_DEV) { + for (let key in ACHIEVEMENT_IDS) { + assert(this.collection.map.has(key), "Key not found in collection: " + key); + } + } + + logger.log("Collection created with", this.collection.map.size, "achievements"); + } + + /** @returns {boolean} */ + hasAchievements() { + return true; + } + + /** + * @param {GameRoot} root + * @returns {Promise} + */ + onLoad(root) { + this.root = root; + + try { + this.collection = new AchievementCollection(this.activate.bind(this)); + this.collection.initialize(root); + + logger.log("Initialized", this.collection.map.size, "relevant achievements"); + return Promise.resolve(); + } catch (err) { + logger.error("Failed to initialize the collection"); + return Promise.reject(err); + } + } + + /** @returns {Promise} */ + initialize() { + if (!G_IS_STANDALONE) { + logger.warn("Wegame unavailable. Achievements won't sync."); + return Promise.resolve(); + } + + if (!G_WEGAME_VERSION) { + return Promise.resolve(); + } + + this.initialized = true; + return Promise.resolve(); + } + + /** + * @param {string} key + * @returns {Promise} + */ + activate(key) { + let promise; + + if (!G_WEGAME_VERSION) { + return Promise.resolve(); + } + + if (!this.initialized) { + promise = Promise.resolve(); + } else { + promise = ipcRenderer.invoke("wegame:activate-achievement", ACHIEVEMENT_IDS[key]); + } + + return promise + .then(() => { + logger.log("Achievement activated:", key); + }) + .catch(err => { + logger.error("Failed to activate achievement:", key, err); + throw err; + }); + } +} diff --git a/src/js/platform/electron/wrapper.js b/src/js/platform/electron/wrapper.js index 65451395..9e611318 100644 --- a/src/js/platform/electron/wrapper.js +++ b/src/js/platform/electron/wrapper.js @@ -4,6 +4,7 @@ import { createLogger } from "../../core/logging"; import { StorageImplElectron } from "./storage"; import { SteamAchievementProvider } from "./steam_achievement_provider"; import { PlatformWrapperInterface } from "../wrapper"; +import { WegameAchievementProvider } from "./wegame_achievement_provider"; const logger = createLogger("electron-wrapper"); @@ -24,7 +25,10 @@ export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser { this.app.ticker.frameEmitted.add(this.steamOverlayFixRedrawCanvas, this); this.app.storage = new StorageImplElectron(this); - this.app.achievementProvider = new SteamAchievementProvider(this.app); + + this.app.achievementProvider = G_WEGAME_VERSION + ? new WegameAchievementProvider(this.app) + : new SteamAchievementProvider(this.app); return this.initializeAchievementProvider() .then(() => this.initializeDlcStatus()) @@ -70,7 +74,7 @@ export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser { } initializeDlcStatus() { - if (G_WEGAME_VERSION) { + if (G_ISBN_VERSION) { return Promise.resolve(); } diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index 5fb4a156..b615c318 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -140,7 +140,7 @@ function initializeSettings() { options: Object.keys(LANGUAGES), valueGetter: key => key, textGetter: key => LANGUAGES[key].name, - category: G_CHINA_VERSION || G_WEGAME_VERSION ? null : enumCategories.general, + category: G_CHINA_VERSION || G_ISBN_VERSION ? null : enumCategories.general, restartRequired: true, changeCb: (app, id) => null, magicValue: "auto-detect", diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index e42aeed0..ac3a29fb 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -39,21 +39,21 @@ export class MainMenuState extends GameState { } getInnerHTML() { - const showLanguageIcon = !G_CHINA_VERSION && !G_WEGAME_VERSION; + const showLanguageIcon = !G_CHINA_VERSION && !G_ISBN_VERSION; const showExitAppButton = G_IS_STANDALONE; const showPuzzleDLC = - !G_WEGAME_VERSION && + !G_ISBN_VERSION && (G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED) && !G_IS_STEAM_DEMO && !G_GOG_VERSION; - const showWegameFooter = G_WEGAME_VERSION; + const showWegameFooter = G_ISBN_VERSION; const hasMods = MODS.anyModsActive(); - const hasSteamBridge = !G_GOG_VERSION && !G_IS_STEAM_DEMO && !G_WEGAME_VERSION; + const hasSteamBridge = !G_GOG_VERSION && !G_IS_STEAM_DEMO && !G_ISBN_VERSION; let showExternalLinks = true; if (G_IS_STANDALONE) { - if (G_WEGAME_VERSION || G_CHINA_VERSION) { + if (G_ISBN_VERSION || G_CHINA_VERSION) { showExternalLinks = false; } } else { @@ -711,7 +711,7 @@ export class MainMenuState extends GameState { downloadButton.setAttribute("aria-label", "Download"); elem.appendChild(downloadButton); - if (!G_WEGAME_VERSION) { + if (!G_ISBN_VERSION) { const renameButton = document.createElement("button"); renameButton.classList.add("styledButton", "renameGame"); renameButton.setAttribute("aria-label", "Rename Savegame"); diff --git a/src/js/states/preload.js b/src/js/states/preload.js index 746e1769..59944a8c 100644 --- a/src/js/states/preload.js +++ b/src/js/states/preload.js @@ -155,7 +155,7 @@ export class PreloadState extends GameState { .then(() => this.setStatus("Initializing language", 25)) .then(() => { - if (G_CHINA_VERSION || G_WEGAME_VERSION) { + if (G_CHINA_VERSION || G_ISBN_VERSION) { return this.app.settings.updateLanguage("zh-CN"); } @@ -219,7 +219,7 @@ export class PreloadState extends GameState { return; } - if (G_CHINA_VERSION || G_WEGAME_VERSION) { + if (G_CHINA_VERSION || G_ISBN_VERSION) { return; } @@ -286,7 +286,7 @@ export class PreloadState extends GameState { } update() { - if (G_CHINA_VERSION || G_WEGAME_VERSION) { + if (G_CHINA_VERSION || G_ISBN_VERSION) { return; } const now = performance.now(); @@ -320,7 +320,7 @@ export class PreloadState extends GameState { setStatus(text, progress) { logger.log("✅ " + text); - if (G_CHINA_VERSION || G_WEGAME_VERSION) { + if (G_CHINA_VERSION || G_ISBN_VERSION) { return Promise.resolve(); } this.currentStatus = text; diff --git a/src/js/states/settings.js b/src/js/states/settings.js index e1524329..28679b8c 100644 --- a/src/js/states/settings.js +++ b/src/js/states/settings.js @@ -35,10 +35,10 @@ export class SettingsState extends TextualGameState { -
+
${ - G_CHINA_VERSION || G_WEGAME_VERSION + G_CHINA_VERSION || G_ISBN_VERSION ? "" : ` @@ -47,7 +47,7 @@ export class SettingsState extends TextualGameState { ` }
- ${G_WEGAME_VERSION ? "" : `
${T.global.loading} ...
`} + ${G_ISBN_VERSION ? "" : `
${T.global.loading} ...
`}
@@ -117,7 +117,7 @@ export class SettingsState extends TextualGameState { onEnter(payload) { this.renderBuildText(); - if (!G_CHINA_VERSION && !G_WEGAME_VERSION) { + if (!G_CHINA_VERSION && !G_ISBN_VERSION) { this.trackClicks(this.htmlElement.querySelector(".about"), this.onAboutClicked, { preventDefault: false, }); diff --git a/translations/base-zh-CN-ISBN.yaml b/translations/base-zh-CN-ISBN.yaml index d3d2164c..e6b2a852 100644 --- a/translations/base-zh-CN-ISBN.yaml +++ b/translations/base-zh-CN-ISBN.yaml @@ -1,5 +1,6 @@ steamPage: - shortText: “唯一能限制您的,只有您的想象力!” 《图形工厂》 是一款在无限拓展的地图上,通过建造各类工厂设施,来自动化生产与组合出愈加复杂图形的游戏。 + shortText: “唯一能限制您的,只有您的想象力!” 《图形工厂》 + 是一款在无限拓展的地图上,通过建造各类工厂设施,来自动化生产与组合出愈加复杂图形的游戏。 discordLinkShort: 官方讨论区 intro: |- “奇形怪状,放飞想象!” @@ -49,18 +50,9 @@ global: shift: SHIFT键 space: 空格键 loggingIn: 登录 - loadingResources: Downloading additional resources ( %) - discount: -% - discountSummerSale: SPECIAL PROMOTION! Offer ends 7 July demoBanners: title: 试玩版 intro: 购买完整版以解锁所有游戏内容! - playtimeDisclaimer: The full version contains more than 20 hours of content. - playerCount: players like you are currently playing shapez on Steam - untilEndOfDemo: Until end of demo - playtimeDisclaimerDownload: You can continue your savegame in the full version! - Click here to download your savegame. - titleV2: "Play the full version now for:" mainMenu: play: 开始游戏 changelog: 更新日志 @@ -81,15 +73,6 @@ mainMenu: puzzleDlcText: 新增谜题模式将带给您更多的游戏乐趣! puzzleDlcWishlist: 添加心愿单! puzzleDlcViewNow: 查看资料片! - mods: - title: Active Mods - warningPuzzleDLC: Playing the Puzzle DLC is not possible with mods. Please - disable all mods to play the DLC. - playingFullVersion: You are now playing the full version! - logout: Logout - noActiveSavegames: No active savegames found - Click play to start a new game! - playFullVersionV2: Bough shapez on Steam? Play the full version in your Browser! - playFullVersionStandalone: You can now also play the full version in your Browser! dialogs: buttons: ok: 确认 @@ -117,7 +100,7 @@ dialogs: text: 未能读取您的存档! confirmSavegameDelete: title: 确认删除 - text: 您确定要删除这个游戏吗?

"" 等级

该操作无法回退! + text: 您确定要删除这个游戏吗?

'' 等级

该操作无法回退! savegameDeletionError: title: 删除失败 text: 未能删除您的存档! @@ -153,14 +136,16 @@ dialogs: desc: 您还没有解锁蓝图功能!通过第12关的挑战后可解锁蓝图。 keybindingsIntroduction: title: 实用快捷键 - desc: "这个游戏有很多有用的快捷键设定。 以下是其中的一些介绍,记得在按键设置中查看其他按键设定!

+ desc: + "这个游戏有很多有用的快捷键设定。 以下是其中的一些介绍,记得在按键设置中查看其他按键设定!

CTRL键 + 拖动:选择区域以复制或删除。
SHIFT键: 按住以放置多个同一种设施。
ALT键: 反向放置传送带。
" createMarker: title: 创建地图标记 - desc: 填写一个有意义的名称, 还可以同时包含一个形状的 短代码 (您可以 点击这里 - 生成短代码) + desc: + 填写一个有意义的名称, 还可以同时包含一个形状的 短代码 (您可以 点击这里 + 生成短代码!) titleEdit: 编辑地图标记 markerDemoLimit: desc: 在试玩版中您只能创建两个地图标记。请获取完整版以创建更多标记。 @@ -190,27 +175,27 @@ dialogs: title: 设置项目 puzzleLoadFailed: title: 谜题载入失败 - desc: 谜题未能载入! + desc: "谜题未能载入!" submitPuzzle: title: 提交谜题 - descName: 为您的谜题命名! - descIcon: 请输入唯一的短代码,它将作为您的谜题图标显示(您可以在这里生成,或者从以下随机推荐的图形中选择一个): + descName: "为您的谜题命名!" + descIcon: "请输入唯一的短代码,它将作为您的谜题图标显示(您可以在这里生成,或者从以下随机推荐的图形中选择一个):" placeholderName: 谜题标题 puzzleResizeBadBuildings: title: 无法重新定义尺寸 desc: 由于某些设施将会超出区域范围,因此您无法将区域变得更小。 puzzleLoadError: title: 谜题出错! - desc: 谜题未能载入! + desc: "谜题未能载入!" offlineMode: title: 离线模式 desc: 无法访问服务器,所以游戏以离线模式进行。请确认您的互联网访问正常。 puzzleDownloadError: title: 下载出错! - desc: 无法下载谜题! + desc: "无法下载谜题!" puzzleSubmitError: title: 提交出错! - desc: 无法提交谜题! + desc: "无法提交谜题!" puzzleSubmitOk: title: 谜题成功发布! desc: 恭喜!您的谜题已经成功发布,其他玩家已经可以玩到。您可以在“我的谜题”中找到自己已发布的谜题。 @@ -234,44 +219,13 @@ dialogs: desc: 此谜已被标记! puzzleReportError: title: 上报失败 - desc: 无法处理您的上报! + desc: "无法处理您的上报!" puzzleLoadShortKey: title: 输入短代码 desc: 输入谜题的短代码并载入。 puzzleDelete: title: 删除谜题吗? desc: 您是否确认删除 ''?删除谜题后将无法恢复! - modsDifference: - title: Mod Warning - desc: The currently installed mods differ from the mods the savegame was created - with. This might cause the savegame to break or not load at all. Are - you sure you want to continue? - missingMods: Missing Mods - newMods: Newly installed Mods - resourceLoadFailed: - title: Resources failed to load - demoLinkText: shapez demo on Steam - descWeb: "One ore more resources could not be loaded. Make sure you have a - stable internet connection and try again. If this still doesn't - work, make sure to also disable any browser extensions (including - adblockers).<br><br> As an alternative, you can also play the - <demoOnSteamLinkText>. <br><br> Error Message:" - descSteamDemo: "One ore more resources could not be loaded. Try restarting the - game - If that does not help, try reinstalling and verifying the - game files via Steam. <br><br> Error Message:" - steamSsoError: - title: Full Version Logout - desc: You have been logged out from the Full Browser Version since either your - network connection is unstable or you are playing on another - device.<br><br> Please make sure you don't have shapez open in any - other browser tab or another computer with the same Steam - account.<br><br> You can login again in the main menu. - steamSsoNoOwnership: - title: Full Edition not owned - desc: In order to play the Full Edition in your Browser, you need to own both - the base game and the Puzzle DLC on your Steam account.<br><br> - Please make sure you own both, signed in with the correct Steam - account and then try again. ingame: keybindingsOverlay: moveMap: 移动地图 @@ -355,25 +309,28 @@ ingame: interactiveTutorial: title: 新手教程 hints: - 1_1_extractor: 亲爱的玩家,欢迎来到<strong>《图形工厂》<strong>!在这里你可以通过创造各种图形设施与传送带模拟流水线生产,尽情发挥创造力,创办属于自己的工厂!<br><br> + 1_1_extractor: + 亲爱的玩家,欢迎来到<strong>《图形工厂》<strong>!在这里你可以通过创造各种图形设施与传送带模拟流水线生产,尽情发挥创造力,创办属于自己的工厂!<br><br> 在<strong>圆形<strong>上放置一个<strong>开采器</strong>来获取圆形!<br><br>提示:<strong>按下鼠标左键</strong>选中<strong>开采器</strong> 1_2_conveyor: 用<strong>传送带</strong>将您的开采器连接到中心基地上!<br><br>提示:选中<strong>传送带</strong>后<strong>按下鼠标左键可拖动</strong>布置传送带! - 1_3_expand: 您可以放置更多的<strong>开采器</strong>和<strong>传送带</strong>来更有效率地完成关卡目标。<br><br> + 1_3_expand: + 您可以放置更多的<strong>开采器</strong>和<strong>传送带</strong>来更有效率地完成关卡目标。<br><br> 提示:按住 <strong>SHIFT</strong> 键可放置多个<strong>开采器</strong>,注意用<strong>R</strong> 键可旋转<strong>开采器</strong>的出口方向,确保开采的图形可以顺利传送。 2_1_place_cutter: 现在放置一个<strong>切割器</strong>,这个设施可把<strong>圆形</strong>切成两半!<br><br>注意:无论如何放置,切割机总是<strong>从上到下</strong>切割。 - 2_2_place_trash: 使用切割机后产生的废弃图形会导致<strong>堵塞</strong>。<br><br>注意使用<strong>垃圾桶</strong>清除当前 + 2_2_place_trash: + 使用切割机后产生的废弃图形会导致<strong>堵塞</strong>。<br><br>注意使用<strong>垃圾桶</strong>清除当前 (!) 不需要的废物。 2_3_more_cutters: 干的好!现在放置<strong>2个以上的切割机</strong>来加快当前缓慢的过程!<br><br>提示:用<strong>快捷键0-9</strong>可以快速选择各项设施! - 3_1_rectangles: 现在让我们开采一些矩形!找到<strong>矩形地带</strong>并<strong>放置4个开采器</strong>并将它们用<strong>传送带</strong>连接到中心基地。<br><br> + 3_1_rectangles: + 现在让我们开采一些矩形!找到<strong>矩形地带</strong>并<strong>放置4个开采器</strong>并将它们用<strong>传送带</strong>连接到中心基地。<br><br> 提示:选中<strong>传送带</strong>后按住<strong>SHIFT键</strong>可快速准确地规划<strong>传送带路线!</strong> 21_1_place_quad_painter: 放置<strong>四口上色器</strong>并且获取一些<strong>圆形</strong>,<strong>白色</strong>和<strong>红色</strong>! 21_2_switch_to_wires: 按 <strong>E</strong> 键选择<strong>电线层</strong>!<br><br> 然后用导线连接上色器的<strong>四个输入口</strong>! 21_3_place_button: 很好!现在放置一个<strong>开关</strong>并连接导线! 21_4_press_button: 按下<strong>开关</strong>来<strong>产生正信号</strong>以激活<strong>上色器</strong>。<br><br>注:您不用连上所有的输入口!试着只接两个。 - 1_2_hold_and_drag: Hold and drag colors: red: 红色 green: 绿色 @@ -397,14 +354,18 @@ ingame: desc: 点击这里了解完整版内容 get_on_steam: 购买完整版! standaloneAdvantages: + title: 购买完整版! no_thanks: 不需要,谢谢 points: levels: title: 12 个全新关卡! desc: 总共 26 个不同关卡! buildings: - title: 22 个全新设施! + title: 18 个全新设施! desc: 呈现完全体的全自动工厂! + upgrades: + title: 20个等级升级 + desc: 试玩版只有5个等级! markers: title: 无限数量地图标记 desc: 地图再大,不会迷路! @@ -420,12 +381,6 @@ ingame: achievements: title: 成就 desc: 挑战全成就解锁! - mods: - title: Modding support! - desc: Over 80 mods available! - titleV2: "Get the full version now on Steam to unlock:" - titleExpiredV2: Demo completed! - titleEnjoyingDemo: Enjoy the demo? puzzleEditorSettings: zoneTitle: 区域 zoneWidth: 宽度 @@ -663,8 +618,9 @@ storyRewards: desc: 恭喜!您解锁了<strong>旋转机</strong>。它会顺时针将输入的<strong>图形旋转90度</strong>。 reward_painter: title: 上色 - desc: 恭喜!您解锁了<strong>上色器</strong>。开采一些颜色 (就像您开采图形一样),将其在上色器中与图形结合来将图形上色! - <br>注意:如果您不幸患有色盲,可以在设置中启用<strong>色盲模式</strong> + desc: + 恭喜!您解锁了<strong>上色器</strong>。开采一些颜色 (就像您开采图形一样),将其在上色器中与图形结合来将图形上色! + <br>注意:如果您不幸患有色盲,可以在设置中启用<strong>色盲模式</strong>。 reward_mixer: title: 混合颜色 desc: 恭喜!您解锁了<strong>混色器</strong>。它使用<strong>叠加混色法</strong>将两种颜色混合起来。 @@ -680,11 +636,13 @@ storyRewards: desc: 恭喜!您解锁了<strong>隧道</strong>。它可放置在<strong>传送带</strong>或<strong>设施</strong>下方以运送物品。 reward_rotater_ccw: title: 逆时针旋转 - desc: 恭喜!您解锁了<strong>旋转机</strong>的<strong>逆时针</strong>变体。它可以逆时针旋转<strong>图形</strong>。 + desc: + 恭喜!您解锁了<strong>旋转机</strong>的<strong>逆时针</strong>变体。它可以逆时针旋转<strong>图形</strong>。 <br>选择<strong>旋转机</strong>然后按"T"键来选取这个变体。 reward_miner_chainable: title: 链式开采器 - desc: 您已经解锁了<strong>链式开采器</strong>!它能<strong>转发资源</strong>给其他的开采器,这样您就能更有效率的开采各类资源了!<br><br> + desc: + 您已经解锁了<strong>链式开采器</strong>!它能<strong>转发资源</strong>给其他的开采器,这样您就能更有效率的开采各类资源了!<br><br> 注意:新的开采器已替换了工具栏里旧的开采器! reward_underground_belt_tier_2: title: 二级隧道 @@ -701,12 +659,14 @@ storyRewards: <br>它<strong>优先从左边</strong>输出,这样您就可以用它做一个<strong>溢流门</strong>了! reward_freeplay: title: 自由模式 - desc: 成功了!您解锁了<strong>自由模式</strong>!挑战升级!这意味着现在将<strong>随机</strong>生成图形! + desc: + 成功了!您解锁了<strong>自由模式</strong>!挑战升级!这意味着现在将<strong>随机</strong>生成图形! 从现在起,中心基地最为需要的是<strong>产量</strong>,我强烈建议您去制造一台能够自动交付所需图形的机器!<br><br> 基地会在<strong>电线层</strong>输出需要的图形,您需要去分析图形并在此基础上自动配置您的工厂。 reward_blueprints: title: 蓝图 - desc: 您现在可以<strong>复制粘贴</strong>您的工厂的一部分了!按住 CTRL键并拖动鼠标来选择一块区域,然后按C键复制。 + desc: + 您现在可以<strong>复制粘贴</strong>您的工厂的一部分了!按住 CTRL键并拖动鼠标来选择一块区域,然后按C键复制。 <br><br>粘贴并<strong>不是免费的</strong>,您需要制造<strong>蓝图图形</strong>来负担。蓝图图形是您刚刚交付的图形。 no_reward: title: 下一关 @@ -734,12 +694,13 @@ storyRewards: <br>注意:您注意到<strong>传送读取器</strong>和<strong>存储器</strong>输出的他们最后读取的物品了吗?试着在显示屏上展示一下!" reward_constant_signal: title: 恒定信号 - desc: 恭喜!您解锁了生成于电线层之上的<strong>恒定信号</strong>,把它连接到<strong>过滤器</strong>时非常有用。 + desc: + 恭喜!您解锁了生成于电线层之上的<strong>恒定信号</strong>,把它连接到<strong>过滤器</strong>时非常有用。 <br>比如,它能发出图形、颜色、开关值(1 / 0)的固定信号。 reward_logic_gates: title: 逻辑门 desc: 您解锁了<strong>逻辑门</strong>!它们是个好东西!<br> - 您可以用它们来进行"与,或,非,异或"操作。<br><br>作为奖励,我还给您解锁了<strong>晶体管</strong>! + 您可以用它们来进行'与,或,非,异或'操作。<br><br>作为奖励,我还给您解锁了<strong>晶体管</strong>! reward_virtual_processing: title: 模拟处理器 desc: 我刚刚给了一大堆新设施,让您可以<strong>模拟形状的处理过程</strong>!<br> @@ -753,7 +714,8 @@ storyRewards: <strong>提示</strong>:可在设置中打开电线层教程!" reward_filter: title: 物品过滤器 - desc: 恭喜!您解锁了<strong>物品过滤器</strong>!它会根据在电线层上输入的信号决定是从上面还是右边输出物品。<br><br> + desc: + 恭喜!您解锁了<strong>物品过滤器</strong>!它会根据在电线层上输入的信号决定是从上面还是右边输出物品。<br><br> 您也可以输入开关值(1 / 0)信号来激活或者禁用它。 reward_demo_end: title: 试玩结束 @@ -896,7 +858,6 @@ settings: title: 图形工具提示-始终显示 description: 在设施上悬停时是否始终显示图形工具提示,而不是必须按住“Alt”键。 rangeSliderPercentage: <amount> % - newBadge: New! keybindings: title: 按键设定 hint: 提示:使用 CTRL、SHIFT、ALT!这些键在放置设施时有不同的效果。 @@ -909,7 +870,6 @@ keybindings: massSelect: 批量选择 buildings: 设施快捷键 placementModifiers: 放置设施修饰键 - mods: Provided by Mods mappings: confirm: 确认 back: 返回 @@ -981,10 +941,13 @@ keybindings: showShapeTooltip: 显示图形输出提示。 about: title: 关于游戏 - body: |- + body: >- 本游戏由托比亚斯开发,并且已经开源。<br><br> + 这个游戏的开发获得了热情玩家的巨大支持。非常感谢!<br><br> + 本游戏的音乐由佩普森制作——他是个很棒的伙伴。<br><br> + 最后,我想感谢我最好的朋友尼可拉斯——如果没有他的《异星工厂》带给我的体验和启发,《图形工厂》将不会存在。 changelog: title: 版本日志 @@ -1129,22 +1092,3 @@ backendErrors: timeout: 请求超时。 too-many-likes-already: 您的谜题已经得到了许多玩家的赞赏。如果您仍然希望删除它,请联系客服! no-permission: 您没有执行此操作的权限。 -mods: - title: Mods - author: Author - version: Version - modWebsite: Website - openFolder: Open Mods Folder - folderOnlyStandalone: Opening the mod folder is only possible when running the standalone. - browseMods: Browse Mods - modsInfo: To install and manage mods, copy them to the mods folder within the - game directory. You can also use the 'Open Mods Folder' button on the - top right. - noModSupport: You need the standalone version on Steam to install mods. - togglingComingSoon: - title: Coming Soon - description: Enabling or disabling mods is currently only possible by copying - the mod file from or to the mods/ folder. However, being able to - toggle them here is planned for a future update! - browserNoSupport: Due to browser restrictions it is currently only possible to - install mods in the Steam version - Sorry!