diff --git a/electron/.gitignore b/electron/.gitignore new file mode 100644 index 00000000..0cdb30f4 --- /dev/null +++ b/electron/.gitignore @@ -0,0 +1 @@ +mods/*.js \ No newline at end of file diff --git a/electron/index.js b/electron/index.js index e7994050..e6f9ce1e 100644 --- a/electron/index.js +++ b/electron/index.js @@ -9,6 +9,7 @@ const asyncLock = require("async-lock"); const isDev = process.argv.indexOf("--dev") >= 0; const isLocal = process.argv.indexOf("--local") >= 0; +const safeMode = process.argv.indexOf("--safe-mode") >= 0; const roamingFolder = process.env.APPDATA || @@ -16,6 +17,7 @@ const roamingFolder = ? process.env.HOME + "/Library/Preferences" : process.env.HOME + "/.local/share"); let storePath = path.join(roamingFolder, "shapez.io", "saves"); +let modsPath = path.join(app.getAppPath(), "mods"); if (!fs.existsSync(storePath)) { // No try-catch by design @@ -79,6 +81,8 @@ function createWindow() { if (isDev) { menu = new Menu(); + win.toggleDevTools(); + const mainItem = new MenuItem({ label: "Toggle Dev Tools", click: () => win.toggleDevTools(), @@ -279,5 +283,28 @@ ipcMain.on("fs-job", async (event, arg) => { event.reply("fs-response", { id: arg.id, result }); }); +ipcMain.on("open-mods-folder", async () => { + shell.openPath(modsPath); +}); + +ipcMain.handle("get-mods", async (event, arg) => { + if (safeMode) { + console.warn("Not loading mods due to safe mode"); + return []; + } + if (!fs.existsSync(modsPath)) { + console.warn("Mods folder not found:", modsPath); + return []; + } + try { + console.log("Loading mods from", modsPath); + let entries = fs.readdirSync(modsPath); + entries = entries.filter(entry => entry.endsWith(".js")); + return entries.map(filename => fs.readFileSync(path.join(modsPath, filename), { encoding: "utf8" })); + } catch (ex) { + throw new Error(ex); + } +}); + steam.init(isDev); steam.listen(); diff --git a/electron/mods/README.txt b/electron/mods/README.txt new file mode 100644 index 00000000..666cc18f --- /dev/null +++ b/electron/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/src/css/states/main_menu.scss b/src/css/states/main_menu.scss index 15cdbe1c..2c70f5d8 100644 --- a/src/css/states/main_menu.scss +++ b/src/css/states/main_menu.scss @@ -185,7 +185,7 @@ .updateLabel { position: absolute; transform: translateX(50%) rotate(-5deg); - color: #ff590b; + color: #300bff; @include Heading; font-weight: bold; @include S(right, 40px); @@ -290,6 +290,73 @@ } } + .modsList { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + background: #fff; + grid-row: 1 / 2; + grid-column: 2 / 3; + position: relative; + text-align: left; + align-items: flex-start; + @include S(padding, 15px); + @include S(padding-bottom, 10px); + @include S(border-radius, $globalBorderRadius); + + h3 { + @include S(margin-bottom, 10px); + @include Heading; + } + + .dlcHint { + @include SuperSmallText; + @include S(margin-top, 10px); + + display: grid; + grid-template-columns: 1fr auto; + grid-gap: 20px; + align-items: center; + } + + .mod { + background: #eee; + width: 100%; + @include S(border-radius, $globalBorderRadius); + @include S(padding, 5px); + box-sizing: border-box; + @include S(margin-bottom, 5px); + display: grid; + grid-template-columns: 1fr auto auto; + @include S(grid-gap, 5px); + + .author, + .version { + @include SuperSmallText; + } + .name { + overflow: hidden; + } + } + + .modsList { + box-sizing: border-box; + @include S(height, 100px); + @include S(padding, 5px); + border: D(1px) solid #eee; + overflow-y: scroll; + width: 100%; + display: flex; + flex-direction: column; + pointer-events: all; + + :last-child { + margin-bottom: auto; + } + } + } + .mainContainer { display: flex; align-items: center; diff --git a/src/js/application.js b/src/js/application.js index 66e9eb8c..67e9c353 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -130,8 +130,6 @@ export class Application { // Store the mouse position, or null if not available /** @type {Vector|null} */ this.mousePosition = null; - - MODS.initMods(); } /** @@ -331,8 +329,11 @@ export class Application { /** * Boots the application */ - boot() { + async boot() { console.log("Booting ..."); + + await MODS.initMods(); + this.registerStates(); this.registerEventListeners(); diff --git a/src/js/core/config.local.template.js b/src/js/core/config.local.template.js index 41677997..ed230ae4 100644 --- a/src/js/core/config.local.template.js +++ b/src/js/core/config.local.template.js @@ -116,5 +116,8 @@ export default { // Disables slow asserts, useful for debugging performance // disableSlowAsserts: true, // ----------------------------------------------------------------------------------- + // Loads the dev_mod.js for developing new mods + // loadDevMod: true, + // ----------------------------------------------------------------------------------- /* dev:end */ }; diff --git a/src/js/mods/demo_mod.js b/src/js/mods/dev_mod.js similarity index 98% rename from src/js/mods/demo_mod.js rename to src/js/mods/dev_mod.js index 43ceffd4..aa74f59a 100644 --- a/src/js/mods/demo_mod.js +++ b/src/js/mods/dev_mod.js @@ -1,7 +1,3 @@ -/* typehints:start */ -import { Entity } from "../game/entity"; -/* typehints:end */ - export default function (shapez) { class MetaDemoModBuilding extends shapez.MetaBuilding { constructor() { @@ -12,10 +8,6 @@ export default function (shapez) { return "red"; } - /** - * Creates the entity at the given location - * @param {Entity} entity - */ setupEntityComponents(entity) {} } @@ -36,11 +28,11 @@ export default function (shapez) { init() { // Add some custom css - this.modInterface.registerCss(` - * { - font-family: "Comic Sans", "Comic Sans MS", Tahoma !important; - } - `); + // this.modInterface.registerCss(` + // button { + // font-family: "Comic Sans", "Comic Sans MS", Tahoma !important; + // } + // `); // Replace a builtin sprite this.modInterface.registerSprite("sprites/colors/red.png", RESOURCES["red.png"]); diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index beb9e5e1..5c308d8a 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -1,4 +1,6 @@ +import { globalConfig } from "../core/config"; import { createLogger } from "../core/logging"; +import { getIPCRenderer } from "../core/utils"; import { Mod } from "./mod"; import { ModInterface } from "./mod_interface"; import { MOD_SIGNALS } from "./mod_signals"; @@ -20,17 +22,39 @@ export class ModLoader { this.initialized = false; this.signals = MOD_SIGNALS; - - this.registerMod(/** @type {any} */ (require("./demo_mod").default)); } linkApp(app) { this.app = app; } - initMods() { + anyModsActive() { + return this.mods.length > 0; + } + + async initMods() { LOG.log("hook:init"); + if (G_IS_STANDALONE) { + try { + const mods = await getIPCRenderer().invoke("get-mods"); + + mods.forEach(modCode => { + const registerMod = mod => { + this.modLoadQueue.push(mod); + }; + // ugh + eval(modCode); + }); + } catch (ex) { + alert("Failed to load mods: " + ex); + } + } else if (G_IS_DEV) { + if (globalConfig.debug.loadDevMod) { + this.modLoadQueue.push(/** @type {any} */ (require("./dev_mod").default)); + } + } + let exports = {}; if (G_IS_DEV || G_IS_STANDALONE) { @@ -67,17 +91,6 @@ export class ModLoader { this.modLoadQueue = []; this.signals.postInit.dispatch(); } - - /** - * - * @param {(Object) => (new (Application, ModLoader) => Mod)} mod - */ - registerMod(mod) { - if (this.initialized) { - throw new Error("Mods are already initialized, can not add mod afterwards."); - } - this.modLoadQueue.push(mod); - } } export const MODS = new ModLoader(); diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 60495a9c..a6bb940a 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -8,6 +8,7 @@ import { ReadWriteProxy } from "../core/read_write_proxy"; import { formatSecondsToTimeAgo, generateFileDownload, + getIPCRenderer, isSupportedBrowser, makeButton, makeButtonElement, @@ -17,6 +18,7 @@ import { waitNextFrame, } from "../core/utils"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; +import { MODS } from "../mods/modloader"; import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; import { PlatformWrapperImplElectron } from "../platform/electron/wrapper"; import { getApplicationSettingById } from "../profile/application_settings"; @@ -41,6 +43,7 @@ export class MainMenuState extends GameState { const showBrowserWarning = !G_IS_STANDALONE && !isSupportedBrowser(); const showPuzzleDLC = !G_WEGAME_VERSION && (G_IS_STANDALONE || G_IS_DEV); const showWegameFooter = G_WEGAME_VERSION; + const hasMods = MODS.anyModsActive(); let showExternalLinks = true; @@ -94,7 +97,7 @@ export class MainMenuState extends GameState {
@@ -112,7 +115,7 @@ export class MainMenuState extends GameState {
${ - showPuzzleDLC && ownsPuzzleDLC + showPuzzleDLC && ownsPuzzleDLC && !hasMods ? `
@@ -147,6 +150,49 @@ export class MainMenuState extends GameState {
` : "" } + ${ + hasMods + ? ` + +
+

${T.mainMenu.mods.title} + +

+ +
+ ${MODS.mods + .map(mod => { + return ` +
+ ${mod.metadata.name} + ${T.mainMenu.mods.version.replace( + "", + mod.metadata.version + )} + ${T.mainMenu.mods.author.replace( + "", + mod.metadata.authorName + )} +
+ `; + }) + .join("")} +
+ +
+ ${T.mainMenu.modsWarningPuzzleDLC} + + +
+ + +
+ ` + : "" + } + ${ @@ -335,6 +381,7 @@ export class MainMenuState extends GameState { ".puzzleDlcPlayButton": this.onPuzzleModeButtonClicked, ".puzzleDlcGetButton": this.onPuzzleWishlistButtonClicked, ".wegameDisclaimer > .rating": this.onWegameRatingClicked, + ".modsOpenFolder": this.openModsFolder, }; for (const key in clickHandling) { @@ -716,6 +763,14 @@ export class MainMenuState extends GameState { }); } + openModsFolder() { + if (!G_IS_STANDALONE) { + this.dialogs.showWarning(T.global.error, T.mainMenu.mods.folderOnlyStandalone); + return; + } + getIPCRenderer().send("open-mods-folder"); + } + onLeave() { this.dialogs.cleanup(); } diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 3f3b1412..a7a27bfe 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -126,6 +126,16 @@ mainMenu: puzzleDlcWishlist: Wishlist now! puzzleDlcViewNow: View Dlc + modsWarningPuzzleDLC: >- + Playing the Puzzle DLC is not possible with mods. Please disable all mods to play the DLC. + + mods: + title: Active Mods + author: by + version: v + openFolder: Open Folder + folderOnlyStandalone: Opening the mod folder is only possible when running the standalone. + puzzleMenu: play: Play edit: Edit