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 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();

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 {
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;

View File

@ -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();

View File

@ -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 */
};

View File

@ -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"]);

View File

@ -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();

View File

@ -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 {
<div class="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 class="mainWrapper" data-columns="${showDemoAdvertisement || showPuzzleDLC ? 2 : 1}">
@ -112,7 +115,7 @@ export class MainMenuState extends GameState {
</div>
${
showPuzzleDLC && ownsPuzzleDLC
showPuzzleDLC && ownsPuzzleDLC && !hasMods
? `
<div class="puzzleContainer">
<img class="dlcLogo" src="${cachebust(
@ -126,7 +129,7 @@ export class MainMenuState extends GameState {
}
${
showPuzzleDLC && !ownsPuzzleDLC
showPuzzleDLC && !ownsPuzzleDLC && !hasMods
? `
<div class="puzzleContainer notOwned">
<span class="badge">
@ -147,6 +150,49 @@ export class MainMenuState extends GameState {
</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>
${
@ -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();
}

View File

@ -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 <author>
version: v<version>
openFolder: Open Folder
folderOnlyStandalone: Opening the mod folder is only possible when running the standalone.
puzzleMenu:
play: Play
edit: Edit