diff --git a/src/js/core/background_resources_loader.js b/src/js/core/background_resources_loader.js index e1a585b6..927071a9 100644 --- a/src/js/core/background_resources_loader.js +++ b/src/js/core/background_resources_loader.js @@ -233,7 +233,7 @@ export class BackgroundResourcesLoader { this.numAssetsToLoadTotal = 0; this.numAssetsLoaded = 0; }) - .then(MODS.hook_injectSprites.bind(MODS)) + .then(MODS.modInterface.injectSprites.bind(MODS)) ); } } diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index c8d6c9e5..0aad3669 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -512,6 +512,8 @@ export function generateLevelDefinitions(limitedVersion = false) { ]), ]; + MODS.signals.modifyLevelDefinitions.dispatch(levelDefinitions); + if (G_IS_DEV) { levelDefinitions.forEach(({ shape }) => { try { @@ -522,8 +524,6 @@ export function generateLevelDefinitions(limitedVersion = false) { }); } - MODS.callHook("modifyLevelDefinitions", levelDefinitions); - return levelDefinitions; } diff --git a/src/js/game/theme.js b/src/js/game/theme.js index 4f68997e..023710dd 100644 --- a/src/js/game/theme.js +++ b/src/js/game/theme.js @@ -9,5 +9,5 @@ export let THEME = THEMES.light; export function applyGameTheme(id) { THEME = THEMES[id]; - MODS.callHook("preprocessTheme", { id, theme: THEME }); + MODS.signals.preprocessTheme.dispatch({ id, theme: THEME }); } diff --git a/src/js/main.js b/src/js/main.js index 3046820c..71ec59a9 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -2,6 +2,8 @@ import "./core/polyfills"; import "./core/assert"; import "./core/error_handler"; +import "./mods/modloader"; + import { createLogger, logSection } from "./core/logging"; import { Application } from "./application"; import { IS_DEBUG } from "./core/config"; @@ -11,7 +13,6 @@ import { initItemRegistry } from "./game/item_registry"; import { initMetaBuildingRegistry } from "./game/meta_building_registry"; import { initGameModeRegistry } from "./game/game_mode_registry"; import { initGameSpeedRegistry } from "./game/game_speed_registry"; -import { MODS } from "./mods/modloader"; const logger = createLogger("main"); @@ -20,8 +21,6 @@ if (window.coreThreadLoadedCb) { window.coreThreadLoadedCb(); } -MODS.hook_init(); - // Logrocket // if (!G_IS_DEV && !G_IS_STANDALONE) { // const monthlyUsers = 300; // thousand diff --git a/src/js/mods/demo_mod.js b/src/js/mods/demo_mod.js index 7dca4bad..f3bcbaf8 100644 --- a/src/js/mods/demo_mod.js +++ b/src/js/mods/demo_mod.js @@ -1,32 +1,35 @@ import { Mod } from "./mod"; export class DemoMod extends Mod { - constructor() { - super({ - authorContact: "tobias@tobspr.io", - authorName: "tobspr", - name: "Demo Mod", - version: "1", - id: "demo-mod", - }); + constructor(modLoader) { + super( + { + authorContact: "tobias@tobspr.io", + authorName: "tobspr", + name: "Demo Mod", + version: "1", + id: "demo-mod", + }, + modLoader + ); } - hook_init() { + init() { // Add some custom css - this.interface.registerCss(` - * { - color: red !important; - } + this.modLoader.modInterface.registerCss(` + * { + color: red !important; + } `); // Replace a builtin sprite - this.interface.registerSprite( + this.modLoader.modInterface.registerSprite( "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" ); // Add a new type of sub shape ("Line", short code "L") - this.interface.registerSubShapeType({ + this.modLoader.modInterface.registerSubShapeType({ id: "line", shortCode: "L", weightComputation: distanceToOriginInChunks => @@ -46,16 +49,16 @@ export class DemoMod extends Mod { context.closePath(); }, }); - } - hook_preprocessTheme({ id, theme }) { // Modify the theme colors - theme.map.background = "#eee"; - theme.items.outline = "#000"; - } + this.modLoader.signals.preprocessTheme.add(({ theme }) => { + theme.map.background = "#eee"; + theme.items.outline = "#000"; + }); - hook_modifyLevelDefinitions(definitions) { // Modify the goal of the first level - definitions[0].shape = "LuCuLuCu"; + this.modLoader.signals.modifyLevelDefinitions.add(definitions => { + definitions[0].shape = "LuCuLuCu"; + }); } } diff --git a/src/js/mods/mod.js b/src/js/mods/mod.js index 7a0b3a7f..65cdd53a 100644 --- a/src/js/mods/mod.js +++ b/src/js/mods/mod.js @@ -1,5 +1,5 @@ /* typehints:start */ -import { ModInterface } from "./mod_interface"; +import { ModLoader } from "./modloader"; /* typehints:end */ export class Mod { @@ -11,40 +11,13 @@ export class Mod { * @param {string} metadata.authorName * @param {string} metadata.authorContact * @param {string} metadata.id + * + * @param {ModLoader} modLoader */ - constructor(metadata) { + constructor(metadata, modLoader) { this.metadata = metadata; - - /** - * @type {ModInterface} - */ - this.interface = undefined; + this.modLoader = modLoader; } - hook_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; - } - } + init() {} } diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index 16de4d67..9ce15d01 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -7,6 +7,7 @@ import { createLogger } from "../core/logging"; import { AtlasSprite, SpriteAtlasLink } from "../core/sprites"; import { Mod } from "./mod"; import { enumShortcodeToSubShape, enumSubShape, enumSubShapeToShortcode } from "../game/shape_definition"; +import { Loader } from "../core/loader"; const LOG = createLogger("mod-interface"); @@ -32,23 +33,22 @@ export class ModInterface { /** * * @param {ModLoader} modLoader - * @param {Mod} mod */ - constructor(modLoader, mod) { + constructor(modLoader) { /** * @param {Application} app */ this.app = undefined; this.modLoader = modLoader; - this.mod = mod; + + /** @type {Map} */ + this.lazySprites = new Map(); } registerCss(cssString) { const element = document.createElement("style"); 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); } @@ -75,23 +75,15 @@ export class ModInterface { sprite.linksByResolution["0.5"] = link; sprite.linksByResolution["0.75"] = link; - // @ts-ignore - sprite.modSource = this.mod; + this.lazySprites.set(spriteId, sprite); + } - const oldSprite = this.modLoader.lazySprites.get(spriteId); - if (oldSprite) { - LOG.error( - "Sprite '" + - spriteId + - "' 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); + injectSprites() { + LOG.log("inject sprites"); + this.lazySprites.forEach((sprite, key) => { + Loader.sprites.set(key, sprite); + console.log("override", key); + }); } /** diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 2b6cbbaf..1a903ae2 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -1,6 +1,5 @@ -import { Loader } from "../core/loader"; import { createLogger } from "../core/logging"; -import { AtlasSprite } from "../core/sprites"; +import { Signal } from "../core/signal"; import { DemoMod } from "./demo_mod"; import { Mod } from "./mod"; import { ModInterface } from "./mod_interface"; @@ -14,64 +13,50 @@ export class ModLoader { /** @type {Mod[]} */ this.mods = []; - /** @type {Map} */ - this.lazySprites = new Map(); + this.modInterface = new ModInterface(this); + + /** @type {(new (ModLoader) => Mod)[]} */ + this.modLoadQueue = []; 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) { this.app = app; - this.mods.forEach(mod => (mod.interface.app = app)); } - hook_init() { + initMods() { LOG.log("hook:init"); this.initialized = true; - this.mods.forEach(mod => { - LOG.log("Loading mod", mod.metadata.name); - mod.interface = new ModInterface(this, mod); - mod.executeGuarded("hook_init", mod.hook_init.bind(mod)); + this.modLoadQueue.forEach(modClass => { + const mod = new modClass(this); + mod.init(); + 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) { - LOG.log("Registering mod", mod.metadata.name); if (this.initialized) { 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(); - -MODS.registerMod(new DemoMod());