From 54a69c97253eacf9f1074c74fc370bc1b212a28d Mon Sep 17 00:00:00 2001 From: tobspr Date: Mon, 17 Jan 2022 12:15:57 +0100 Subject: [PATCH] Support for mod settings --- electron/index.js | 2 +- mod_examples/add_building_basic.js | 1 + mod_examples/add_building_flipper.js | 1 + mod_examples/base.js | 1 + mod_examples/buildings_have_cost.js | 1 + mod_examples/class_extensions.js | 1 + mod_examples/custom_css.js | 1 + mod_examples/custom_drawing.js | 1 + mod_examples/custom_sub_shapes.js | 1 + mod_examples/custom_theme.js | 1 + mod_examples/mod_settings.js | 31 +++++ mod_examples/modify_existing_building.js | 1 + mod_examples/modify_theme.js | 1 + mod_examples/pasting.js | 1 + mod_examples/replace_builtin_sprites.js | 1 + mod_examples/translations.js | 1 + mod_examples/usage_statistics.js | 1 + src/js/application.js | 7 +- src/js/mods/mod.js | 14 ++- src/js/mods/modloader.js | 139 ++++++++++++++--------- 20 files changed, 148 insertions(+), 60 deletions(-) create mode 100644 mod_examples/mod_settings.js diff --git a/electron/index.js b/electron/index.js index bc7cf393..18943589 100644 --- a/electron/index.js +++ b/electron/index.js @@ -316,7 +316,7 @@ async function writeFileSafe(filename, contents) { } ipcMain.handle("fs-job", async (event, job) => { - const filenameSafe = job.filename.replace(/[^a-z\.\-_0-9]/i, ""); + const filenameSafe = job.filename.replace(/[^a-z\.\-_0-9]/i, "_"); const fname = path.join(storePath, filenameSafe); switch (job.type) { case "read": { diff --git a/mod_examples/add_building_basic.js b/mod_examples/add_building_basic.js index 8b497374..97e0a358 100644 --- a/mod_examples/add_building_basic.js +++ b/mod_examples/add_building_basic.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/add_building_flipper.js b/mod_examples/add_building_flipper.js index fdfcb0e2..b3b746f3 100644 --- a/mod_examples/add_building_flipper.js +++ b/mod_examples/add_building_flipper.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/base.js b/mod_examples/base.js index 71be904f..6dc8df8f 100644 --- a/mod_examples/base.js +++ b/mod_examples/base.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/buildings_have_cost.js b/mod_examples/buildings_have_cost.js index 92e0535e..b97197f9 100644 --- a/mod_examples/buildings_have_cost.js +++ b/mod_examples/buildings_have_cost.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/class_extensions.js b/mod_examples/class_extensions.js index 04fa5193..167ca53a 100644 --- a/mod_examples/class_extensions.js +++ b/mod_examples/class_extensions.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/custom_css.js b/mod_examples/custom_css.js index 59a28afe..74d19057 100644 --- a/mod_examples/custom_css.js +++ b/mod_examples/custom_css.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/custom_drawing.js b/mod_examples/custom_drawing.js index 60574c5f..6ba49454 100644 --- a/mod_examples/custom_drawing.js +++ b/mod_examples/custom_drawing.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/custom_sub_shapes.js b/mod_examples/custom_sub_shapes.js index 47e051c1..d44b84d9 100644 --- a/mod_examples/custom_sub_shapes.js +++ b/mod_examples/custom_sub_shapes.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/custom_theme.js b/mod_examples/custom_theme.js index d781dd8d..fd2e063a 100644 --- a/mod_examples/custom_theme.js +++ b/mod_examples/custom_theme.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/mod_settings.js b/mod_examples/mod_settings.js new file mode 100644 index 00000000..4e35f5df --- /dev/null +++ b/mod_examples/mod_settings.js @@ -0,0 +1,31 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Mod Settings", + version: "1", + id: "base", + description: "Shows how to add settings to your mod", + + settings: { + timesLaunched: 0, + }, +}; + +class Mod extends shapez.Mod { + init() { + // Increment the setting every time we launch the mod + this.settings.timesLaunched++; + this.saveSettings(); + + // Show a dialog in the main menu wit the settings + this.signals.stateEntered.add(state => { + if (state instanceof shapez.MainMenuState) { + this.dialogs.showInfo( + "Welcome back", + "You have launched this mod " + this.settings.timesLaunched + " times" + ); + } + }); + } +} diff --git a/mod_examples/modify_existing_building.js b/mod_examples/modify_existing_building.js index be7d122c..1264b2c3 100644 --- a/mod_examples/modify_existing_building.js +++ b/mod_examples/modify_existing_building.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/modify_theme.js b/mod_examples/modify_theme.js index ca012c7f..dfdfb6e9 100644 --- a/mod_examples/modify_theme.js +++ b/mod_examples/modify_theme.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/pasting.js b/mod_examples/pasting.js index ebdda2bb..edd4952c 100644 --- a/mod_examples/pasting.js +++ b/mod_examples/pasting.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/replace_builtin_sprites.js b/mod_examples/replace_builtin_sprites.js index 009aa65a..9865121e 100644 --- a/mod_examples/replace_builtin_sprites.js +++ b/mod_examples/replace_builtin_sprites.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/translations.js b/mod_examples/translations.js index b0b718f2..8a3e07da 100644 --- a/mod_examples/translations.js +++ b/mod_examples/translations.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/mod_examples/usage_statistics.js b/mod_examples/usage_statistics.js index a00ee2c3..36bb81f5 100644 --- a/mod_examples/usage_statistics.js +++ b/mod_examples/usage_statistics.js @@ -1,3 +1,4 @@ +// @ts-nocheck const METADATA = { website: "https://tobspr.io", author: "tobspr", diff --git a/src/js/application.js b/src/js/application.js index cbf6528b..4f3a4c5f 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -77,7 +77,12 @@ export class Application { MODS.app = this; // MODS - await MODS.initMods(); + + try { + await MODS.initMods(); + } catch (ex) { + alert("Failed to load mods (launch with --dev for more info): \n\n" + ex); + } this.unloaded = false; diff --git a/src/js/mods/mod.js b/src/js/mods/mod.js index b130370d..cea152d8 100644 --- a/src/js/mods/mod.js +++ b/src/js/mods/mod.js @@ -7,17 +7,23 @@ import { MOD_SIGNALS } from "./mod_signals"; export class Mod { /** - * @param {Application} app - * @param {ModLoader} modLoader - * @param {import("./modloader").ModMetadata} meta + * @param {object} param0 + * @param {Application} param0.app + * @param {ModLoader} param0.modLoader + * @param {import("./modloader").ModMetadata} param0.meta + * @param {Object} param0.settings + * @param {() => Promise} param0.saveSettings */ - constructor(app, modLoader, meta) { + constructor({ app, modLoader, meta, settings, saveSettings }) { this.app = app; this.modLoader = modLoader; this.metadata = meta; this.signals = MOD_SIGNALS; this.modInterface = modLoader.modInterface; + + this.settings = settings; + this.saveSettings = saveSettings; } init() { diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 1ef26f7a..668411f3 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -3,6 +3,9 @@ import { Application } from "../application"; /* typehints:end */ import { globalConfig } from "../core/config"; import { createLogger } from "../core/logging"; +import { StorageImplBrowserIndexedDB } from "../platform/browser/storage_indexed_db"; +import { StorageImplElectron } from "../platform/electron/storage"; +import { FILE_NOT_FOUND } from "../platform/storage"; import { Mod } from "./mod"; import { ModInterface } from "./mod_interface"; import { MOD_SIGNALS } from "./mod_signals"; @@ -17,6 +20,7 @@ const LOG = createLogger("mods"); * website: string; * description: string; * id: string; + * settings: [] * }} ModMetadata */ @@ -80,51 +84,55 @@ export class ModLoader { } async initMods() { - LOG.log("hook:init"); + if (!G_IS_STANDALONE && !G_IS_DEV) { + this.initialized = true; + return; + } + // Create a storage for reading mod settings + const storage = G_IS_STANDALONE + ? new StorageImplElectron(this.app) + : new StorageImplBrowserIndexedDB(this.app); + await storage.initialize(); + + LOG.log("hook:init", this.app, this.app.storage); this.exposeExports(); - if (G_IS_STANDALONE || G_IS_DEV) { - try { - let mods = []; - if (G_IS_STANDALONE) { - mods = await ipcRenderer.invoke("get-mods"); - } - if (G_IS_DEV && globalConfig.debug.externalModUrl) { - const response = await fetch(globalConfig.debug.externalModUrl, { - method: "GET", - }); - if (response.status !== 200) { - throw new Error( - "Failed to load " + - globalConfig.debug.externalModUrl + - ": " + - response.status + - " " + - response.statusText - ); - } + let mods = []; + if (G_IS_STANDALONE) { + mods = await ipcRenderer.invoke("get-mods"); + } + if (G_IS_DEV && globalConfig.debug.externalModUrl) { + const response = await fetch(globalConfig.debug.externalModUrl, { + method: "GET", + }); + if (response.status !== 200) { + throw new Error( + "Failed to load " + + globalConfig.debug.externalModUrl + + ": " + + response.status + + " " + + response.statusText + ); + } - mods.push(await response.text()); - } + mods.push(await response.text()); + } - window.$shapez_registerMod = (modClass, meta) => { - if (this.modLoadQueue.some(entry => entry.meta.id === meta.id)) { - console.warn( - "Not registering mod", - meta, - "since a mod with the same id is already loaded" - ); - return; - } - this.modLoadQueue.push({ - modClass, - meta, - }); - }; + window.$shapez_registerMod = (modClass, meta) => { + if (this.modLoadQueue.some(entry => entry.meta.id === meta.id)) { + console.warn("Not registering mod", meta, "since a mod with the same id is already loaded"); + return; + } + this.modLoadQueue.push({ + modClass, + meta, + }); + }; - mods.forEach(modCode => { - modCode += ` + mods.forEach(modCode => { + modCode += ` if (typeof Mod !== 'undefined') { if (typeof METADATA !== 'object') { throw new Error("No METADATA variable found"); @@ -132,32 +140,55 @@ export class ModLoader { window.$shapez_registerMod(Mod, METADATA); } `; - try { - const func = new Function(modCode); - func(); - } catch (ex) { - console.error(ex); - alert("Failed to parse mod (launch with --dev for more info): \n\n" + ex); - } - }); - - delete window.$shapez_registerMod; + try { + const func = new Function(modCode); + func(); } catch (ex) { - alert("Failed to load mods (launch with --dev for more info): \n\n" + ex); + console.error(ex); + alert("Failed to parse mod (launch with --dev for more info): \n\n" + ex); } - } + }); + + delete window.$shapez_registerMod; this.initialized = true; - this.modLoadQueue.forEach(({ modClass, meta }) => { + + for (let i = 0; i < this.modLoadQueue.length; i++) { + const { modClass, meta } = this.modLoadQueue[i]; + const modDataFile = "modsettings_" + meta.id + "__" + meta.version + ".json"; + + let settings = meta.settings; + + if (meta.settings) { + try { + const storedSettings = await storage.readFileAsync(modDataFile); + settings = JSON.parse(storedSettings); + } catch (ex) { + if (ex === FILE_NOT_FOUND) { + // Write default data + await storage.writeFileAsync(modDataFile, JSON.stringify(meta.settings)); + } else { + alert("Failed to load settings for " + meta.id + ", will use defaults:\n\n" + ex); + } + } + } + try { - const mod = new modClass(this.app, this, meta); + const mod = new modClass({ + app: this.app, + modLoader: this, + meta, + settings, + saveSettings: () => storage.writeFileAsync(modDataFile, JSON.stringify(mod.settings)), + }); mod.init(); this.mods.push(mod); } catch (ex) { console.error(ex); alert("Failed to initialize mods (launch with --dev for more info): \n\n" + ex); } - }); + } + this.modLoadQueue = []; } }