1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-14 02:31:51 +00:00

Refactor mods to use signals

This commit is contained in:
tobspr 2022-01-13 22:14:49 +01:00
parent 3e5716504a
commit 6bac7cec57
8 changed files with 74 additions and 122 deletions

View File

@ -233,7 +233,7 @@ export class BackgroundResourcesLoader {
this.numAssetsToLoadTotal = 0; this.numAssetsToLoadTotal = 0;
this.numAssetsLoaded = 0; this.numAssetsLoaded = 0;
}) })
.then(MODS.hook_injectSprites.bind(MODS)) .then(MODS.modInterface.injectSprites.bind(MODS))
); );
} }
} }

View File

@ -512,6 +512,8 @@ export function generateLevelDefinitions(limitedVersion = false) {
]), ]),
]; ];
MODS.signals.modifyLevelDefinitions.dispatch(levelDefinitions);
if (G_IS_DEV) { if (G_IS_DEV) {
levelDefinitions.forEach(({ shape }) => { levelDefinitions.forEach(({ shape }) => {
try { try {
@ -522,8 +524,6 @@ export function generateLevelDefinitions(limitedVersion = false) {
}); });
} }
MODS.callHook("modifyLevelDefinitions", levelDefinitions);
return levelDefinitions; return levelDefinitions;
} }

View File

@ -9,5 +9,5 @@ export let THEME = THEMES.light;
export function applyGameTheme(id) { export function applyGameTheme(id) {
THEME = THEMES[id]; THEME = THEMES[id];
MODS.callHook("preprocessTheme", { id, theme: THEME }); MODS.signals.preprocessTheme.dispatch({ id, theme: THEME });
} }

View File

@ -2,6 +2,8 @@ import "./core/polyfills";
import "./core/assert"; import "./core/assert";
import "./core/error_handler"; import "./core/error_handler";
import "./mods/modloader";
import { createLogger, logSection } from "./core/logging"; import { createLogger, logSection } from "./core/logging";
import { Application } from "./application"; import { Application } from "./application";
import { IS_DEBUG } from "./core/config"; import { IS_DEBUG } from "./core/config";
@ -11,7 +13,6 @@ import { initItemRegistry } from "./game/item_registry";
import { initMetaBuildingRegistry } from "./game/meta_building_registry"; import { initMetaBuildingRegistry } from "./game/meta_building_registry";
import { initGameModeRegistry } from "./game/game_mode_registry"; import { initGameModeRegistry } from "./game/game_mode_registry";
import { initGameSpeedRegistry } from "./game/game_speed_registry"; import { initGameSpeedRegistry } from "./game/game_speed_registry";
import { MODS } from "./mods/modloader";
const logger = createLogger("main"); const logger = createLogger("main");
@ -20,8 +21,6 @@ if (window.coreThreadLoadedCb) {
window.coreThreadLoadedCb(); window.coreThreadLoadedCb();
} }
MODS.hook_init();
// Logrocket // Logrocket
// if (!G_IS_DEV && !G_IS_STANDALONE) { // if (!G_IS_DEV && !G_IS_STANDALONE) {
// const monthlyUsers = 300; // thousand // const monthlyUsers = 300; // thousand

View File

@ -1,32 +1,35 @@
import { Mod } from "./mod"; import { Mod } from "./mod";
export class DemoMod extends Mod { export class DemoMod extends Mod {
constructor() { constructor(modLoader) {
super({ super(
authorContact: "tobias@tobspr.io", {
authorName: "tobspr", authorContact: "tobias@tobspr.io",
name: "Demo Mod", authorName: "tobspr",
version: "1", name: "Demo Mod",
id: "demo-mod", version: "1",
}); id: "demo-mod",
},
modLoader
);
} }
hook_init() { init() {
// Add some custom css // Add some custom css
this.interface.registerCss(` this.modLoader.modInterface.registerCss(`
* { * {
color: red !important; color: red !important;
} }
`); `);
// Replace a builtin sprite // Replace a builtin sprite
this.interface.registerSprite( this.modLoader.modInterface.registerSprite(
"sprites/colors/red.png", "sprites/colors/red.png",
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAB2AAAAdgB+lymcgAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAATkSURBVHic7ZpfiFRVHMc/v5m99+6qWYhCYKFGVJpPFYgK4UNQb1GwODO5M2OLUmASItGD6OJDYX8IIqPMdWdmdWZkE6GnnozqJWkxENMyg/75tP31z+7MnZ3760GrZf7fmXuvaPfzMvA7v3PO9/e7555z5pwLISEhISEhISH/U6SXyjo4aFaM+atUndUISwEUSmjkW5XouYH86I9eiJxJDC8Tra4EHhBxrGtW+UWQr43K1bMyMWF323ZXCSglko8JJFF5EljYwvUMytFqVcfmTeQuuuljejC5NBqVzQgx4MEWrpdAjjtIbqAwdsJNH+AyAXYi9TDKGwobXPYzI8J7hil7JZP5s5WjptN3VGzdrcpzwICbTgQ9gbLTLOa+6rxOh9iJ1FZV3gEMN6Jqevspok7SKIx/2qi4Ek+udZDDwD1d9wGziO6y8rl9nUlqg4LY8dQosLkHUXOpCmwxC9mxuUY7ln5WRQ8AUS86UWXUKma3CGgrv0i7hirx1Ot4FzxAVGG0lEhu+8dQiqdfUNGDeBQ8gAjDdiL1Wlu/VoXleHITyLhXompwVHkKQITjdPAwukOHrELucLPSpgnQeHxxGfOcwGJ/hAFw5frvAr86UJiysFdJofBro/KmWS+rudfn4OFa4L4FDyCwpCLmSIvyenRw6+12X/kiMN8vYQFzxZy17pKJA3/VFjQcAWWjNMStEzzAgnK0vKlRQcMEiMoT/uoJHhEeb2SvS4Beey3W+q4oeNbryEhdvHWGUiy9DFgUiKRgWVQ6//Pdtca6BESVJcHoCZ4os3WrWl0CnGj1Vnz6ADg47RNANdJy73xzI9VaS/2kEOW3YMQET6Qa/b3OVmsw+qPnAScQRcHiGH32hVpjXQLk0KHLwDeBSAqWs3LkyKVaY+ONEPKx/3oCRmgYU+M/Q1ot+CrmBiCO5hvZGybALI5PIpzyV1KAKF82OydsfgihvOKboKARaRpL0wSY9684fouMgknz/uUfNStseSRmx4YeUYmcxLfjKt9xJCLrzSOZL5o5tAzMLI5PqvK+97qCQZR3WwUPHTxZq192AKc9UxUcZ43q9EvtnNomQDKZEugQMOOJrCAQpnGcjTIx0VZzR++2VcidBoa4ObbICjJsHR0/04lzx5ObVcgeQ9nTva6AEN1l5TPFjt3dtm/H028qusNtvSAQYb+Zz25r7/kfrpc3o5DZqegHbuv5j2SN+1Zsd1vLdQIE1JqdeR4Ya+scHGPm7NVhGRlxPUd1/YWIgtiJ9B5Ub+i8IMLbRj77Yrtb4Kb1exVQiqW2i/AWwe8WFXjZKmTb3gC3oucEANiJVFKVg/Ty8YQ7KqIybBYzPd9ce5IAgMozqfWOw4fAnV612QiFKRXdOJDPfeJFe54lAK5/2NQXOQa6xst2/0U45dD3tFdfn4HH7+28idxF02IDvqwQesj8Y+E6L4MHj0fAXK7PC/vp/f7/sgg7zXz2gBe6avEtAQAzsfTyiGie7i9bJ1Uk0Z/PfOelrrn4unQNFDM/mLPTG1DZB9TdyrSgCrxq3mat8zN48HkEzMWODa1RiYwBK9u4fh9R2WwUM58HoSuwzYtZHD9pWvIQIrsVpmrLFaYQ2W1asjqo4CHAETAXHRwcKBv9j+JE7gUg4lywKqXPOjnACAkJCQkJCQkJ8Yi/AfA6e2lfA0oPAAAAAElFTkSuQmCC" "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAB2AAAAdgB+lymcgAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAATkSURBVHic7ZpfiFRVHMc/v5m99+6qWYhCYKFGVJpPFYgK4UNQb1GwODO5M2OLUmASItGD6OJDYX8IIqPMdWdmdWZkE6GnnozqJWkxENMyg/75tP31z+7MnZ3760GrZf7fmXuvaPfzMvA7v3PO9/e7555z5pwLISEhISEhISH/U6SXyjo4aFaM+atUndUISwEUSmjkW5XouYH86I9eiJxJDC8Tra4EHhBxrGtW+UWQr43K1bMyMWF323ZXCSglko8JJFF5EljYwvUMytFqVcfmTeQuuuljejC5NBqVzQgx4MEWrpdAjjtIbqAwdsJNH+AyAXYi9TDKGwobXPYzI8J7hil7JZP5s5WjptN3VGzdrcpzwICbTgQ9gbLTLOa+6rxOh9iJ1FZV3gEMN6Jqevspok7SKIx/2qi4Ek+udZDDwD1d9wGziO6y8rl9nUlqg4LY8dQosLkHUXOpCmwxC9mxuUY7ln5WRQ8AUS86UWXUKma3CGgrv0i7hirx1Ot4FzxAVGG0lEhu+8dQiqdfUNGDeBQ8gAjDdiL1Wlu/VoXleHITyLhXompwVHkKQITjdPAwukOHrELucLPSpgnQeHxxGfOcwGJ/hAFw5frvAr86UJiysFdJofBro/KmWS+rudfn4OFa4L4FDyCwpCLmSIvyenRw6+12X/kiMN8vYQFzxZy17pKJA3/VFjQcAWWjNMStEzzAgnK0vKlRQcMEiMoT/uoJHhEeb2SvS4Beey3W+q4oeNbryEhdvHWGUiy9DFgUiKRgWVQ6//Pdtca6BESVJcHoCZ4os3WrWl0CnGj1Vnz6ADg47RNANdJy73xzI9VaS/2kEOW3YMQET6Qa/b3OVmsw+qPnAScQRcHiGH32hVpjXQLk0KHLwDeBSAqWs3LkyKVaY+ONEPKx/3oCRmgYU+M/Q1ot+CrmBiCO5hvZGybALI5PIpzyV1KAKF82OydsfgihvOKboKARaRpL0wSY9684fouMgknz/uUfNStseSRmx4YeUYmcxLfjKt9xJCLrzSOZL5o5tAzMLI5PqvK+97qCQZR3WwUPHTxZq192AKc9UxUcZ43q9EvtnNomQDKZEugQMOOJrCAQpnGcjTIx0VZzR++2VcidBoa4ObbICjJsHR0/04lzx5ObVcgeQ9nTva6AEN1l5TPFjt3dtm/H028qusNtvSAQYb+Zz25r7/kfrpc3o5DZqegHbuv5j2SN+1Zsd1vLdQIE1JqdeR4Ya+scHGPm7NVhGRlxPUd1/YWIgtiJ9B5Ub+i8IMLbRj77Yrtb4Kb1exVQiqW2i/AWwe8WFXjZKmTb3gC3oucEANiJVFKVg/Ty8YQ7KqIybBYzPd9ce5IAgMozqfWOw4fAnV612QiFKRXdOJDPfeJFe54lAK5/2NQXOQa6xst2/0U45dD3tFdfn4HH7+28idxF02IDvqwQesj8Y+E6L4MHj0fAXK7PC/vp/f7/sgg7zXz2gBe6avEtAQAzsfTyiGie7i9bJ1Uk0Z/PfOelrrn4unQNFDM/mLPTG1DZB9TdyrSgCrxq3mat8zN48HkEzMWODa1RiYwBK9u4fh9R2WwUM58HoSuwzYtZHD9pWvIQIrsVpmrLFaYQ2W1asjqo4CHAETAXHRwcKBv9j+JE7gUg4lywKqXPOjnACAkJCQkJCQkJ8Yi/AfA6e2lfA0oPAAAAAElFTkSuQmCC"
); );
// Add a new type of sub shape ("Line", short code "L") // Add a new type of sub shape ("Line", short code "L")
this.interface.registerSubShapeType({ this.modLoader.modInterface.registerSubShapeType({
id: "line", id: "line",
shortCode: "L", shortCode: "L",
weightComputation: distanceToOriginInChunks => weightComputation: distanceToOriginInChunks =>
@ -46,16 +49,16 @@ export class DemoMod extends Mod {
context.closePath(); context.closePath();
}, },
}); });
}
hook_preprocessTheme({ id, theme }) {
// Modify the theme colors // Modify the theme colors
theme.map.background = "#eee"; this.modLoader.signals.preprocessTheme.add(({ theme }) => {
theme.items.outline = "#000"; theme.map.background = "#eee";
} theme.items.outline = "#000";
});
hook_modifyLevelDefinitions(definitions) {
// Modify the goal of the first level // Modify the goal of the first level
definitions[0].shape = "LuCuLuCu"; this.modLoader.signals.modifyLevelDefinitions.add(definitions => {
definitions[0].shape = "LuCuLuCu";
});
} }
} }

View File

@ -1,5 +1,5 @@
/* typehints:start */ /* typehints:start */
import { ModInterface } from "./mod_interface"; import { ModLoader } from "./modloader";
/* typehints:end */ /* typehints:end */
export class Mod { export class Mod {
@ -11,40 +11,13 @@ export class Mod {
* @param {string} metadata.authorName * @param {string} metadata.authorName
* @param {string} metadata.authorContact * @param {string} metadata.authorContact
* @param {string} metadata.id * @param {string} metadata.id
*
* @param {ModLoader} modLoader
*/ */
constructor(metadata) { constructor(metadata, modLoader) {
this.metadata = metadata; this.metadata = metadata;
this.modLoader = modLoader;
/**
* @type {ModInterface}
*/
this.interface = undefined;
} }
hook_init() {} init() {}
executeGuarded(taskName, task) {
try {
return task();
} catch (ex) {
console.error(ex);
alert(
"Mod " +
this.metadata.name +
" (version " +
this.metadata.version +
")" +
" failed to execute '" +
taskName +
"':\n\n" +
ex +
"\n\nPlease forward this to the mod author:\n\n" +
this.metadata.authorName +
" (" +
this.metadata.authorContact +
")"
);
throw ex;
}
}
} }

View File

@ -7,6 +7,7 @@ import { createLogger } from "../core/logging";
import { AtlasSprite, SpriteAtlasLink } from "../core/sprites"; import { AtlasSprite, SpriteAtlasLink } from "../core/sprites";
import { Mod } from "./mod"; import { Mod } from "./mod";
import { enumShortcodeToSubShape, enumSubShape, enumSubShapeToShortcode } from "../game/shape_definition"; import { enumShortcodeToSubShape, enumSubShape, enumSubShapeToShortcode } from "../game/shape_definition";
import { Loader } from "../core/loader";
const LOG = createLogger("mod-interface"); const LOG = createLogger("mod-interface");
@ -32,23 +33,22 @@ export class ModInterface {
/** /**
* *
* @param {ModLoader} modLoader * @param {ModLoader} modLoader
* @param {Mod} mod
*/ */
constructor(modLoader, mod) { constructor(modLoader) {
/** /**
* @param {Application} app * @param {Application} app
*/ */
this.app = undefined; this.app = undefined;
this.modLoader = modLoader; this.modLoader = modLoader;
this.mod = mod;
/** @type {Map<string, AtlasSprite>} */
this.lazySprites = new Map();
} }
registerCss(cssString) { registerCss(cssString) {
const element = document.createElement("style"); const element = document.createElement("style");
element.textContent = cssString; element.textContent = cssString;
element.setAttribute("data-mod-id", this.mod.metadata.id);
element.setAttribute("data-mod-name", this.mod.metadata.name);
document.head.appendChild(element); document.head.appendChild(element);
} }
@ -75,23 +75,15 @@ export class ModInterface {
sprite.linksByResolution["0.5"] = link; sprite.linksByResolution["0.5"] = link;
sprite.linksByResolution["0.75"] = link; sprite.linksByResolution["0.75"] = link;
// @ts-ignore this.lazySprites.set(spriteId, sprite);
sprite.modSource = this.mod; }
const oldSprite = this.modLoader.lazySprites.get(spriteId); injectSprites() {
if (oldSprite) { LOG.log("inject sprites");
LOG.error( this.lazySprites.forEach((sprite, key) => {
"Sprite '" + Loader.sprites.set(key, sprite);
spriteId + console.log("override", key);
"' is provided twice, once by mod '" + });
// @ts-ignore
oldSprite.modSource.metadata.name +
"' and once by mod '" +
this.mod.metadata.name +
"'. This could cause artifacts."
);
}
this.modLoader.lazySprites.set(spriteId, sprite);
} }
/** /**

View File

@ -1,6 +1,5 @@
import { Loader } from "../core/loader";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { AtlasSprite } from "../core/sprites"; import { Signal } from "../core/signal";
import { DemoMod } from "./demo_mod"; import { DemoMod } from "./demo_mod";
import { Mod } from "./mod"; import { Mod } from "./mod";
import { ModInterface } from "./mod_interface"; import { ModInterface } from "./mod_interface";
@ -14,64 +13,50 @@ export class ModLoader {
/** @type {Mod[]} */ /** @type {Mod[]} */
this.mods = []; this.mods = [];
/** @type {Map<string, AtlasSprite>} */ this.modInterface = new ModInterface(this);
this.lazySprites = new Map();
/** @type {(new (ModLoader) => Mod)[]} */
this.modLoadQueue = [];
this.initialized = false; this.initialized = false;
this.signals = {
postInit: new Signal(),
injectSprites: new Signal(),
preprocessTheme: /** @type {TypedSignal<[Object]>} */ (new Signal()),
modifyLevelDefinitions: /** @type {TypedSignal<[Array[Object]]>} */ (new Signal()),
};
this.registerMod(DemoMod);
this.initMods();
} }
linkApp(app) { linkApp(app) {
this.app = app; this.app = app;
this.mods.forEach(mod => (mod.interface.app = app));
} }
hook_init() { initMods() {
LOG.log("hook:init"); LOG.log("hook:init");
this.initialized = true; this.initialized = true;
this.mods.forEach(mod => { this.modLoadQueue.forEach(modClass => {
LOG.log("Loading mod", mod.metadata.name); const mod = new modClass(this);
mod.interface = new ModInterface(this, mod); mod.init();
mod.executeGuarded("hook_init", mod.hook_init.bind(mod)); this.mods.push(mod);
}); });
this.modLoadQueue = [];
this.signals.postInit.dispatch();
} }
hook_injectSprites() {
LOG.log("hook:injectSprites");
this.lazySprites.forEach((sprite, key) => {
Loader.sprites.set(key, sprite);
console.log("override", key);
});
}
callHook(id, structuredArgs) {
LOG.log("hook:" + id);
this.mods.forEach(mod => {
const handler = mod["hook_" + id];
if (handler) {
mod.executeGuarded("hook:" + id, handler.bind(mod, structuredArgs));
}
});
}
registerSprite() {}
registerGameState() {}
registerBuilding() {}
/** /**
* *
* @param {Mod} mod * @param {new (ModLoader) => Mod} mod
*/ */
registerMod(mod) { registerMod(mod) {
LOG.log("Registering mod", mod.metadata.name);
if (this.initialized) { if (this.initialized) {
throw new Error("Mods are already initialized, can not add mod afterwards."); throw new Error("Mods are already initialized, can not add mod afterwards.");
} }
this.mods.push(mod); this.modLoadQueue.push(mod);
} }
} }
export const MODS = new ModLoader(); export const MODS = new ModLoader();
MODS.registerMod(new DemoMod());