1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-13 18:21:51 +00:00

Allow loading mods from standalone

This commit is contained in:
tobspr 2022-01-14 08:55:18 +01:00
parent 258dbbd98f
commit 575abd255d
10 changed files with 209 additions and 34 deletions

1
electron/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
mods/*.js

View File

@ -9,6 +9,7 @@ const asyncLock = require("async-lock");
const isDev = process.argv.indexOf("--dev") >= 0; const isDev = process.argv.indexOf("--dev") >= 0;
const isLocal = process.argv.indexOf("--local") >= 0; const isLocal = process.argv.indexOf("--local") >= 0;
const safeMode = process.argv.indexOf("--safe-mode") >= 0;
const roamingFolder = const roamingFolder =
process.env.APPDATA || process.env.APPDATA ||
@ -16,6 +17,7 @@ const roamingFolder =
? process.env.HOME + "/Library/Preferences" ? process.env.HOME + "/Library/Preferences"
: process.env.HOME + "/.local/share"); : process.env.HOME + "/.local/share");
let storePath = path.join(roamingFolder, "shapez.io", "saves"); let storePath = path.join(roamingFolder, "shapez.io", "saves");
let modsPath = path.join(app.getAppPath(), "mods");
if (!fs.existsSync(storePath)) { if (!fs.existsSync(storePath)) {
// No try-catch by design // No try-catch by design
@ -79,6 +81,8 @@ function createWindow() {
if (isDev) { if (isDev) {
menu = new Menu(); menu = new Menu();
win.toggleDevTools();
const mainItem = new MenuItem({ const mainItem = new MenuItem({
label: "Toggle Dev Tools", label: "Toggle Dev Tools",
click: () => win.toggleDevTools(), click: () => win.toggleDevTools(),
@ -279,5 +283,28 @@ ipcMain.on("fs-job", async (event, arg) => {
event.reply("fs-response", { id: arg.id, result }); 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.init(isDev);
steam.listen(); steam.listen();

6
electron/mods/README.txt Normal file
View File

@ -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 ---

View File

@ -185,7 +185,7 @@
.updateLabel { .updateLabel {
position: absolute; position: absolute;
transform: translateX(50%) rotate(-5deg); transform: translateX(50%) rotate(-5deg);
color: #ff590b; color: #300bff;
@include Heading; @include Heading;
font-weight: bold; font-weight: bold;
@include S(right, 40px); @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 { .mainContainer {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -130,8 +130,6 @@ export class Application {
// Store the mouse position, or null if not available // Store the mouse position, or null if not available
/** @type {Vector|null} */ /** @type {Vector|null} */
this.mousePosition = null; this.mousePosition = null;
MODS.initMods();
} }
/** /**
@ -331,8 +329,11 @@ export class Application {
/** /**
* Boots the application * Boots the application
*/ */
boot() { async boot() {
console.log("Booting ..."); console.log("Booting ...");
await MODS.initMods();
this.registerStates(); this.registerStates();
this.registerEventListeners(); this.registerEventListeners();

View File

@ -116,5 +116,8 @@ export default {
// Disables slow asserts, useful for debugging performance // Disables slow asserts, useful for debugging performance
// disableSlowAsserts: true, // disableSlowAsserts: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------
// Loads the dev_mod.js for developing new mods
// loadDevMod: true,
// -----------------------------------------------------------------------------------
/* dev:end */ /* dev:end */
}; };

View File

@ -1,7 +1,3 @@
/* typehints:start */
import { Entity } from "../game/entity";
/* typehints:end */
export default function (shapez) { export default function (shapez) {
class MetaDemoModBuilding extends shapez.MetaBuilding { class MetaDemoModBuilding extends shapez.MetaBuilding {
constructor() { constructor() {
@ -12,10 +8,6 @@ export default function (shapez) {
return "red"; return "red";
} }
/**
* Creates the entity at the given location
* @param {Entity} entity
*/
setupEntityComponents(entity) {} setupEntityComponents(entity) {}
} }
@ -36,11 +28,11 @@ export default function (shapez) {
init() { init() {
// Add some custom css // Add some custom css
this.modInterface.registerCss(` // this.modInterface.registerCss(`
* { // button {
font-family: "Comic Sans", "Comic Sans MS", Tahoma !important; // font-family: "Comic Sans", "Comic Sans MS", Tahoma !important;
} // }
`); // `);
// Replace a builtin sprite // Replace a builtin sprite
this.modInterface.registerSprite("sprites/colors/red.png", RESOURCES["red.png"]); this.modInterface.registerSprite("sprites/colors/red.png", RESOURCES["red.png"]);

View File

@ -1,4 +1,6 @@
import { globalConfig } from "../core/config";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { getIPCRenderer } from "../core/utils";
import { Mod } from "./mod"; import { Mod } from "./mod";
import { ModInterface } from "./mod_interface"; import { ModInterface } from "./mod_interface";
import { MOD_SIGNALS } from "./mod_signals"; import { MOD_SIGNALS } from "./mod_signals";
@ -20,17 +22,39 @@ export class ModLoader {
this.initialized = false; this.initialized = false;
this.signals = MOD_SIGNALS; this.signals = MOD_SIGNALS;
this.registerMod(/** @type {any} */ (require("./demo_mod").default));
} }
linkApp(app) { linkApp(app) {
this.app = app; this.app = app;
} }
initMods() { anyModsActive() {
return this.mods.length > 0;
}
async initMods() {
LOG.log("hook:init"); 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 = {}; let exports = {};
if (G_IS_DEV || G_IS_STANDALONE) { if (G_IS_DEV || G_IS_STANDALONE) {
@ -67,17 +91,6 @@ export class ModLoader {
this.modLoadQueue = []; this.modLoadQueue = [];
this.signals.postInit.dispatch(); 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(); export const MODS = new ModLoader();

View File

@ -8,6 +8,7 @@ import { ReadWriteProxy } from "../core/read_write_proxy";
import { import {
formatSecondsToTimeAgo, formatSecondsToTimeAgo,
generateFileDownload, generateFileDownload,
getIPCRenderer,
isSupportedBrowser, isSupportedBrowser,
makeButton, makeButton,
makeButtonElement, makeButtonElement,
@ -17,6 +18,7 @@ import {
waitNextFrame, waitNextFrame,
} from "../core/utils"; } from "../core/utils";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { MODS } from "../mods/modloader";
import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
import { PlatformWrapperImplElectron } from "../platform/electron/wrapper"; import { PlatformWrapperImplElectron } from "../platform/electron/wrapper";
import { getApplicationSettingById } from "../profile/application_settings"; import { getApplicationSettingById } from "../profile/application_settings";
@ -41,6 +43,7 @@ export class MainMenuState extends GameState {
const showBrowserWarning = !G_IS_STANDALONE && !isSupportedBrowser(); const showBrowserWarning = !G_IS_STANDALONE && !isSupportedBrowser();
const showPuzzleDLC = !G_WEGAME_VERSION && (G_IS_STANDALONE || G_IS_DEV); const showPuzzleDLC = !G_WEGAME_VERSION && (G_IS_STANDALONE || G_IS_DEV);
const showWegameFooter = G_WEGAME_VERSION; const showWegameFooter = G_WEGAME_VERSION;
const hasMods = MODS.anyModsActive();
let showExternalLinks = true; let showExternalLinks = true;
@ -94,7 +97,7 @@ export class MainMenuState extends GameState {
<div class="logo"> <div class="logo">
<img src="${cachebust("res/" + getLogoSprite())}" alt="shapez.io Logo"> <img src="${cachebust("res/" + getLogoSprite())}" alt="shapez.io Logo">
${showUpdateLabel ? `<span class="updateLabel">v${G_BUILD_VERSION}!</span>` : ""} ${showUpdateLabel ? `<span class="updateLabel">MODS UPDATE!</span>` : ""}
</div> </div>
<div class="mainWrapper" data-columns="${showDemoAdvertisement || showPuzzleDLC ? 2 : 1}"> <div class="mainWrapper" data-columns="${showDemoAdvertisement || showPuzzleDLC ? 2 : 1}">
@ -112,7 +115,7 @@ export class MainMenuState extends GameState {
</div> </div>
${ ${
showPuzzleDLC && ownsPuzzleDLC showPuzzleDLC && ownsPuzzleDLC && !hasMods
? ` ? `
<div class="puzzleContainer"> <div class="puzzleContainer">
<img class="dlcLogo" src="${cachebust( <img class="dlcLogo" src="${cachebust(
@ -126,7 +129,7 @@ export class MainMenuState extends GameState {
} }
${ ${
showPuzzleDLC && !ownsPuzzleDLC showPuzzleDLC && !ownsPuzzleDLC && !hasMods
? ` ? `
<div class="puzzleContainer notOwned"> <div class="puzzleContainer notOwned">
<span class="badge"> <span class="badge">
@ -147,6 +150,49 @@ export class MainMenuState extends GameState {
</div>` </div>`
: "" : ""
} }
${
hasMods
? `
<div class="modsList">
<h3>${T.mainMenu.mods.title}
</h3>
<div class="modsList">
${MODS.mods
.map(mod => {
return `
<div class="mod">
<span class="name">${mod.metadata.name}</span>
<span class="version">${T.mainMenu.mods.version.replace(
"<version>",
mod.metadata.version
)}</span>
<span class="author">${T.mainMenu.mods.author.replace(
"<author>",
mod.metadata.authorName
)}</span>
</div>
`;
})
.join("")}
</div>
<div class="dlcHint">
${T.mainMenu.modsWarningPuzzleDLC}
<button class="styledButton modsOpenFolder">${
T.mainMenu.mods.openFolder
}</button>
</div>
</div>
`
: ""
}
</div> </div>
${ ${
@ -335,6 +381,7 @@ export class MainMenuState extends GameState {
".puzzleDlcPlayButton": this.onPuzzleModeButtonClicked, ".puzzleDlcPlayButton": this.onPuzzleModeButtonClicked,
".puzzleDlcGetButton": this.onPuzzleWishlistButtonClicked, ".puzzleDlcGetButton": this.onPuzzleWishlistButtonClicked,
".wegameDisclaimer > .rating": this.onWegameRatingClicked, ".wegameDisclaimer > .rating": this.onWegameRatingClicked,
".modsOpenFolder": this.openModsFolder,
}; };
for (const key in clickHandling) { 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() { onLeave() {
this.dialogs.cleanup(); this.dialogs.cleanup();
} }

View File

@ -126,6 +126,16 @@ mainMenu:
puzzleDlcWishlist: Wishlist now! puzzleDlcWishlist: Wishlist now!
puzzleDlcViewNow: View Dlc 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 <author>
version: v<version>
openFolder: Open Folder
folderOnlyStandalone: Opening the mod folder is only possible when running the standalone.
puzzleMenu: puzzleMenu:
play: Play play: Play
edit: Edit edit: Edit