From abf2fd3d9424b8eef84fd3262a3f90159cf55ee7 Mon Sep 17 00:00:00 2001 From: tobspr Date: Thu, 13 Jan 2022 21:20:42 +0100 Subject: [PATCH 001/129] initial modloader draft --- src/js/core/background_resources_loader.js | 2 + src/js/core/globals.js | 3 + src/js/core/loader.js | 4 ++ src/js/game/theme.js | 3 + src/js/main.js | 3 + src/js/mods/demo_mod.js | 31 +++++++++ src/js/mods/mod.js | 50 ++++++++++++++ src/js/mods/mod_interface.js | 77 ++++++++++++++++++++++ src/js/mods/modloader.js | 77 ++++++++++++++++++++++ 9 files changed, 250 insertions(+) create mode 100644 src/js/mods/demo_mod.js create mode 100644 src/js/mods/mod.js create mode 100644 src/js/mods/mod_interface.js create mode 100644 src/js/mods/modloader.js diff --git a/src/js/core/background_resources_loader.js b/src/js/core/background_resources_loader.js index 316619c4..e1a585b6 100644 --- a/src/js/core/background_resources_loader.js +++ b/src/js/core/background_resources_loader.js @@ -9,6 +9,7 @@ import { SOUNDS, MUSIC } from "../platform/sound"; import { AtlasDefinition, atlasFiles } from "./atlas_definitions"; import { initBuildingCodesAfterResourcesLoaded } from "../game/meta_building_registry"; import { cachebust } from "./cachebust"; +import { MODS } from "../mods/modloader"; const logger = createLogger("background_loader"); @@ -232,6 +233,7 @@ export class BackgroundResourcesLoader { this.numAssetsToLoadTotal = 0; this.numAssetsLoaded = 0; }) + .then(MODS.hook_injectSprites.bind(MODS)) ); } } diff --git a/src/js/core/globals.js b/src/js/core/globals.js index 15197880..fcb9a3e8 100644 --- a/src/js/core/globals.js +++ b/src/js/core/globals.js @@ -2,6 +2,8 @@ import { Application } from "../application"; /* typehints:end */ +import { MODS } from "../mods/modloader"; + /** * Used for the bug reporter, and the click detector which both have no handles to this. * It would be nicer to have no globals, but this is the only one. I promise! @@ -14,4 +16,5 @@ export let GLOBAL_APP = null; export function setGlobalApp(app) { assert(!GLOBAL_APP, "Create application twice!"); GLOBAL_APP = app; + MODS.linkApp(app); } diff --git a/src/js/core/loader.js b/src/js/core/loader.js index cadbc048..b955beda 100644 --- a/src/js/core/loader.js +++ b/src/js/core/loader.js @@ -181,6 +181,10 @@ class LoaderImpl { w: sourceSize.w, h: sourceSize.h, }); + if (sprite.linksByResolution[scale]) { + // Seems data is already present, might have been replaced by a mod + continue; + } sprite.linksByResolution[scale] = link; } } diff --git a/src/js/game/theme.js b/src/js/game/theme.js index 251f4433..4f68997e 100644 --- a/src/js/game/theme.js +++ b/src/js/game/theme.js @@ -1,3 +1,5 @@ +import { MODS } from "../mods/modloader"; + export const THEMES = { dark: require("./themes/dark.json"), light: require("./themes/light.json"), @@ -7,4 +9,5 @@ export let THEME = THEMES.light; export function applyGameTheme(id) { THEME = THEMES[id]; + MODS.callHook("preprocessTheme", { id, theme: THEME }); } diff --git a/src/js/main.js b/src/js/main.js index 94f3d37a..3046820c 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -11,6 +11,7 @@ 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"); @@ -19,6 +20,8 @@ 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 new file mode 100644 index 00000000..c5003da0 --- /dev/null +++ b/src/js/mods/demo_mod.js @@ -0,0 +1,31 @@ +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", + }); + } + + hook_init() { + this.interface.registerCss(` + * { + color: red !important; + } + `); + + this.interface.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" + ); + } + + hook_preprocessTheme({ id, theme }) { + theme.map.background = "#eee"; + theme.items.outline = "#000"; + } +} diff --git a/src/js/mods/mod.js b/src/js/mods/mod.js new file mode 100644 index 00000000..7a0b3a7f --- /dev/null +++ b/src/js/mods/mod.js @@ -0,0 +1,50 @@ +/* typehints:start */ +import { ModInterface } from "./mod_interface"; +/* typehints:end */ + +export class Mod { + /** + * + * @param {object} metadata + * @param {string} metadata.name + * @param {string} metadata.version + * @param {string} metadata.authorName + * @param {string} metadata.authorContact + * @param {string} metadata.id + */ + constructor(metadata) { + this.metadata = metadata; + + /** + * @type {ModInterface} + */ + this.interface = undefined; + } + + 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; + } + } +} diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js new file mode 100644 index 00000000..47bff484 --- /dev/null +++ b/src/js/mods/mod_interface.js @@ -0,0 +1,77 @@ +/* typehints:start */ +import { Application } from "../application"; +import { ModLoader } from "./modloader"; +/* typehints:end */ + +import { createLogger } from "../core/logging"; +import { AtlasSprite, SpriteAtlasLink } from "../core/sprites"; +import { Mod } from "./mod"; + +const LOG = createLogger("mod-interface"); + +export class ModInterface { + /** + * + * @param {ModLoader} modLoader + * @param {Mod} mod + */ + constructor(modLoader, mod) { + /** + * @param {Application} app + */ + this.app = undefined; + + this.modLoader = modLoader; + this.mod = mod; + } + + 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); + } + + registerSprite(spriteId, base64string) { + assert(base64string.startsWith("data:image")); + const img = new Image(); + img.src = base64string; + + const sprite = new AtlasSprite(spriteId); + + const link = new SpriteAtlasLink({ + w: img.width, + h: img.height, + atlas: img, + packOffsetX: 0, + packOffsetY: 0, + packedW: img.width, + packedH: img.height, + packedX: 0, + packedY: 0, + }); + + sprite.linksByResolution["0.25"] = link; + sprite.linksByResolution["0.5"] = link; + sprite.linksByResolution["0.75"] = link; + + // @ts-ignore + sprite.modSource = this.mod; + + 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); + } +} diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js new file mode 100644 index 00000000..2b6cbbaf --- /dev/null +++ b/src/js/mods/modloader.js @@ -0,0 +1,77 @@ +import { Loader } from "../core/loader"; +import { createLogger } from "../core/logging"; +import { AtlasSprite } from "../core/sprites"; +import { DemoMod } from "./demo_mod"; +import { Mod } from "./mod"; +import { ModInterface } from "./mod_interface"; + +const LOG = createLogger("mods"); + +export class ModLoader { + constructor() { + LOG.log("modloader created"); + + /** @type {Mod[]} */ + this.mods = []; + + /** @type {Map} */ + this.lazySprites = new Map(); + + this.initialized = false; + } + + linkApp(app) { + this.app = app; + this.mods.forEach(mod => (mod.interface.app = app)); + } + + hook_init() { + 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)); + }); + } + + 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 + */ + 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); + } +} + +export const MODS = new ModLoader(); + +MODS.registerMod(new DemoMod()); From 3e5716504ad86229add6bcba11ec137344a9bf6e Mon Sep 17 00:00:00 2001 From: tobspr Date: Thu, 13 Jan 2022 21:45:09 +0100 Subject: [PATCH 002/129] modloader features --- src/js/game/map_chunk.js | 5 +++++ src/js/game/modes/regular.js | 3 +++ src/js/game/shape_definition.js | 34 ++++++++++++++-------------- src/js/mods/demo_mod.js | 36 +++++++++++++++++++++++++++--- src/js/mods/mod_interface.js | 39 +++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 19 deletions(-) diff --git a/src/js/game/map_chunk.js b/src/js/game/map_chunk.js index 54af1125..0c5390e4 100644 --- a/src/js/game/map_chunk.js +++ b/src/js/game/map_chunk.js @@ -10,6 +10,7 @@ import { COLOR_ITEM_SINGLETONS } from "./items/color_item"; import { GameRoot } from "./root"; import { enumSubShape } from "./shape_definition"; import { Rectangle } from "../core/rectangle"; +import { MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS } from "../mods/mod_interface"; const logger = createLogger("map_chunk"); @@ -192,6 +193,10 @@ export class MapChunk { [enumSubShape.windmill]: Math.round(6 + clamp(distanceToOriginInChunks / 2, 0, 20)), }; + for (const key in MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS) { + weights[key] = MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS[key](distanceToOriginInChunks); + } + if (distanceToOriginInChunks < 7) { // Initial chunks can not spawn the good stuff weights[enumSubShape.star] = 0; diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index 429c1515..c8d6c9e5 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -38,6 +38,7 @@ import { HUDSandboxController } from "../hud/parts/sandbox_controller"; import { queryParamOptions } from "../../core/query_parameters"; import { MetaBlockBuilding } from "../buildings/block"; import { MetaItemProducerBuilding } from "../buildings/item_producer"; +import { MODS } from "../../mods/modloader"; /** @typedef {{ * shape: string, @@ -521,6 +522,8 @@ export function generateLevelDefinitions(limitedVersion = false) { }); } + MODS.callHook("modifyLevelDefinitions", levelDefinitions); + return levelDefinitions; } diff --git a/src/js/game/shape_definition.js b/src/js/game/shape_definition.js index b09d73c5..acc0bd1c 100644 --- a/src/js/game/shape_definition.js +++ b/src/js/game/shape_definition.js @@ -3,6 +3,7 @@ import { globalConfig } from "../core/config"; import { smoothenDpi } from "../core/dpi_manager"; import { DrawParameters } from "../core/draw_parameters"; import { Vector } from "../core/vector"; +import { MODS_ADDITIONAL_SUB_SHAPE_DRAWERS } from "../mods/mod_interface"; import { BasicSerializableObject, types } from "../savegame/serialization"; import { enumColors, enumColorsToHexCode, enumColorToShortcode, enumShortcodeToColor } from "./colors"; import { THEME } from "./theme"; @@ -366,18 +367,11 @@ export class ShapeDefinition extends BasicSerializableObject { context.strokeStyle = THEME.items.outline; context.lineWidth = THEME.items.outlineWidth; - const insetPadding = 0.0; - switch (subShape) { case enumSubShape.rect: { context.beginPath(); const dims = quadrantSize * layerScale; - context.rect( - insetPadding + -quadrantHalfSize, - -insetPadding + quadrantHalfSize - dims, - dims, - dims - ); + context.rect(-quadrantHalfSize, quadrantHalfSize - dims, dims, dims); break; } @@ -385,8 +379,8 @@ export class ShapeDefinition extends BasicSerializableObject { context.beginPath(); const dims = quadrantSize * layerScale; - let originX = insetPadding - quadrantHalfSize; - let originY = -insetPadding + quadrantHalfSize - dims; + let originX = -quadrantHalfSize; + let originY = quadrantHalfSize - dims; const moveInwards = dims * 0.4; context.moveTo(originX, originY + moveInwards); @@ -401,8 +395,8 @@ export class ShapeDefinition extends BasicSerializableObject { context.beginPath(); const dims = quadrantSize * layerScale; - let originX = insetPadding - quadrantHalfSize; - let originY = -insetPadding + quadrantHalfSize - dims; + let originX = -quadrantHalfSize; + let originY = quadrantHalfSize - dims; const moveInwards = dims * 0.4; context.moveTo(originX, originY + moveInwards); context.lineTo(originX + dims, originY); @@ -414,10 +408,10 @@ export class ShapeDefinition extends BasicSerializableObject { case enumSubShape.circle: { context.beginPath(); - context.moveTo(insetPadding + -quadrantHalfSize, -insetPadding + quadrantHalfSize); + context.moveTo(-quadrantHalfSize, quadrantHalfSize); context.arc( - insetPadding + -quadrantHalfSize, - -insetPadding + quadrantHalfSize, + -quadrantHalfSize, + quadrantHalfSize, quadrantSize * layerScale, -Math.PI * 0.5, 0 @@ -427,7 +421,15 @@ export class ShapeDefinition extends BasicSerializableObject { } default: { - assertAlways(false, "Unkown sub shape: " + subShape); + if (MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[subShape]) { + MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[subShape]({ + context, + layerScale, + quadrantSize, + }); + } else { + throw new Error("Unkown sub shape: " + subShape); + } } } diff --git a/src/js/mods/demo_mod.js b/src/js/mods/demo_mod.js index c5003da0..7dca4bad 100644 --- a/src/js/mods/demo_mod.js +++ b/src/js/mods/demo_mod.js @@ -12,20 +12,50 @@ export class DemoMod extends Mod { } hook_init() { + // Add some custom css this.interface.registerCss(` - * { - color: red !important; - } + * { + color: red !important; + } `); + // Replace a builtin sprite this.interface.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({ + id: "line", + shortCode: "L", + weightComputation: distanceToOriginInChunks => + Math.round(20 + Math.max(Math.min(distanceToOriginInChunks, 30), 0)), + + shapeDrawer: ({ context, quadrantSize, layerScale }) => { + const quadrantHalfSize = quadrantSize / 2; + context.beginPath(); + context.moveTo(-quadrantHalfSize, quadrantHalfSize); + context.arc( + -quadrantHalfSize, + quadrantHalfSize, + quadrantSize * layerScale, + -Math.PI * 0.25, + 0 + ); + context.closePath(); + }, + }); } hook_preprocessTheme({ id, theme }) { + // Modify the theme colors theme.map.background = "#eee"; theme.items.outline = "#000"; } + + hook_modifyLevelDefinitions(definitions) { + // Modify the goal of the first level + definitions[0].shape = "LuCuLuCu"; + } } diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index 47bff484..16de4d67 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -6,9 +6,28 @@ import { ModLoader } from "./modloader"; import { createLogger } from "../core/logging"; import { AtlasSprite, SpriteAtlasLink } from "../core/sprites"; import { Mod } from "./mod"; +import { enumShortcodeToSubShape, enumSubShape, enumSubShapeToShortcode } from "../game/shape_definition"; const LOG = createLogger("mod-interface"); +/** + * @type {Object number>} + */ +export const MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS = {}; + +/** + * @typedef {{ + * context: CanvasRenderingContext2D, + * quadrantSize: number, + * layerScale: number, + * }} SubShapeDrawOptions + */ + +/** + * @type {Object void>} + */ +export const MODS_ADDITIONAL_SUB_SHAPE_DRAWERS = {}; + export class ModInterface { /** * @@ -74,4 +93,24 @@ export class ModInterface { } this.modLoader.lazySprites.set(spriteId, sprite); } + + /** + * + * @param {object} param0 + * @param {string} param0.id + * @param {string} param0.shortCode + * @param {(distanceToOriginInChunks: number) => number} param0.weightComputation + * @param {(options: SubShapeDrawOptions) => void} param0.shapeDrawer + */ + registerSubShapeType({ id, shortCode, weightComputation, shapeDrawer }) { + if (shortCode.length !== 1) { + throw new Error("Bad short code: " + shortCode); + } + enumSubShape[id] = id; + enumSubShapeToShortcode[id] = shortCode; + enumShortcodeToSubShape[shortCode] = id; + + MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS[id] = weightComputation; + MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[id] = shapeDrawer; + } } From 6bac7cec57f38ab97a2ace548ad9132b0d6f3bca Mon Sep 17 00:00:00 2001 From: tobspr Date: Thu, 13 Jan 2022 22:14:49 +0100 Subject: [PATCH 003/129] Refactor mods to use signals --- src/js/core/background_resources_loader.js | 2 +- src/js/game/modes/regular.js | 4 +- src/js/game/theme.js | 2 +- src/js/main.js | 5 +- src/js/mods/demo_mod.js | 47 ++++++++-------- src/js/mods/mod.js | 39 +++----------- src/js/mods/mod_interface.js | 34 +++++------- src/js/mods/modloader.js | 63 +++++++++------------- 8 files changed, 74 insertions(+), 122 deletions(-) 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()); From ebb2f3a1c6c6542fdb70137ff36c11fad4d1a776 Mon Sep 17 00:00:00 2001 From: tobspr Date: Thu, 13 Jan 2022 22:30:52 +0100 Subject: [PATCH 004/129] Add support for modifying and registering new transltions --- src/js/core/background_resources_loader.js | 2 +- src/js/core/globals.js | 2 +- src/js/mods/demo_mod.js | 11 ++++++++++ src/js/mods/mod_interface.js | 19 ++++++++++++----- src/js/translations.js | 24 ++++++++++++---------- 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/js/core/background_resources_loader.js b/src/js/core/background_resources_loader.js index 927071a9..10f8d6f0 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.modInterface.injectSprites.bind(MODS)) + .then(MODS.modInterface.injectSprites.bind(MODS.modInterface)) ); } } diff --git a/src/js/core/globals.js b/src/js/core/globals.js index fcb9a3e8..4e72dddf 100644 --- a/src/js/core/globals.js +++ b/src/js/core/globals.js @@ -16,5 +16,5 @@ export let GLOBAL_APP = null; export function setGlobalApp(app) { assert(!GLOBAL_APP, "Create application twice!"); GLOBAL_APP = app; - MODS.linkApp(app); + MODS.app = app; } diff --git a/src/js/mods/demo_mod.js b/src/js/mods/demo_mod.js index f3bcbaf8..5df293ce 100644 --- a/src/js/mods/demo_mod.js +++ b/src/js/mods/demo_mod.js @@ -60,5 +60,16 @@ export class DemoMod extends Mod { this.modLoader.signals.modifyLevelDefinitions.add(definitions => { definitions[0].shape = "LuCuLuCu"; }); + + this.modLoader.modInterface.registerTranslations("en", { + ingame: { + interactiveTutorial: { + title: "Hello", + hints: { + "1_1_extractor": "World!", + }, + }, + }, + }); } } diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index 9ce15d01..6cfc86be 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -8,6 +8,8 @@ import { AtlasSprite, SpriteAtlasLink } from "../core/sprites"; import { Mod } from "./mod"; import { enumShortcodeToSubShape, enumSubShape, enumSubShapeToShortcode } from "../game/shape_definition"; import { Loader } from "../core/loader"; +import { LANGUAGES } from "../languages"; +import { matchDataRecursive, T } from "../translations"; const LOG = createLogger("mod-interface"); @@ -35,11 +37,6 @@ export class ModInterface { * @param {ModLoader} modLoader */ constructor(modLoader) { - /** - * @param {Application} app - */ - this.app = undefined; - this.modLoader = modLoader; /** @type {Map} */ @@ -105,4 +102,16 @@ export class ModInterface { MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS[id] = weightComputation; MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[id] = shapeDrawer; } + + registerTranslations(language, translations) { + const data = LANGUAGES[language]; + if (!data) { + throw new Error("Unknown language: " + language); + } + + matchDataRecursive(data.data, translations, true); + if (language === "en") { + matchDataRecursive(T, translations, true); + } + } } diff --git a/src/js/translations.js b/src/js/translations.js index 6ad926bc..9d976a41 100644 --- a/src/js/translations.js +++ b/src/js/translations.js @@ -24,15 +24,6 @@ if (G_IS_DEV && globalConfig.debug.testTranslations) { mapTranslations(T); } -export function applyLanguage(languageCode) { - logger.log("Applying language:", languageCode); - const data = LANGUAGES[languageCode]; - if (!data) { - logger.error("Language not found:", languageCode); - return false; - } -} - // Language key is something like de-DE or en or en-US function mapLanguageCodeToId(languageKey) { const key = languageKey.toLowerCase(); @@ -97,17 +88,20 @@ export function autoDetectLanguageId() { return "en"; } -function matchDataRecursive(dest, src) { +export function matchDataRecursive(dest, src, addNewKeys = false) { if (typeof dest !== "object" || typeof src !== "object") { return; } + if (dest === null || src === null) { + return; + } for (const key in dest) { if (src[key]) { // console.log("copy", key); const data = dest[key]; if (typeof data === "object") { - matchDataRecursive(dest[key], src[key]); + matchDataRecursive(dest[key], src[key], addNewKeys); } else if (typeof data === "string" || typeof data === "number") { // console.log("match string", key); dest[key] = src[key]; @@ -116,6 +110,14 @@ function matchDataRecursive(dest, src) { } } } + + if (addNewKeys) { + for (const key in src) { + if (!dest[key]) { + dest[key] = JSON.parse(JSON.stringify(src[key])); + } + } + } } export function updateApplicationLanguage(id) { From 7e2501ea6ef74c66340d9a96c69c981ce631d9c1 Mon Sep 17 00:00:00 2001 From: tobspr Date: Thu, 13 Jan 2022 23:16:24 +0100 Subject: [PATCH 005/129] Minor adjustments --- src/js/mods/demo_mod.js | 2 +- src/js/mods/mod_interface.js | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/js/mods/demo_mod.js b/src/js/mods/demo_mod.js index 5df293ce..be3a7c1b 100644 --- a/src/js/mods/demo_mod.js +++ b/src/js/mods/demo_mod.js @@ -35,7 +35,7 @@ export class DemoMod extends Mod { weightComputation: distanceToOriginInChunks => Math.round(20 + Math.max(Math.min(distanceToOriginInChunks, 30), 0)), - shapeDrawer: ({ context, quadrantSize, layerScale }) => { + draw: ({ context, quadrantSize, layerScale }) => { const quadrantHalfSize = quadrantSize / 2; context.beginPath(); context.moveTo(-quadrantHalfSize, quadrantHalfSize); diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index 6cfc86be..eaf1d314 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -5,7 +5,6 @@ import { ModLoader } from "./modloader"; 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"; import { LANGUAGES } from "../languages"; @@ -89,9 +88,9 @@ export class ModInterface { * @param {string} param0.id * @param {string} param0.shortCode * @param {(distanceToOriginInChunks: number) => number} param0.weightComputation - * @param {(options: SubShapeDrawOptions) => void} param0.shapeDrawer + * @param {(options: SubShapeDrawOptions) => void} param0.draw */ - registerSubShapeType({ id, shortCode, weightComputation, shapeDrawer }) { + registerSubShapeType({ id, shortCode, weightComputation, draw }) { if (shortCode.length !== 1) { throw new Error("Bad short code: " + shortCode); } @@ -100,7 +99,7 @@ export class ModInterface { enumShortcodeToSubShape[shortCode] = id; MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS[id] = weightComputation; - MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[id] = shapeDrawer; + MODS_ADDITIONAL_SUB_SHAPE_DRAWERS[id] = draw; } registerTranslations(language, translations) { From 8777e4c6eab77e363218a9ebc57fcbb5ccc3a339 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 06:14:37 +0100 Subject: [PATCH 006/129] Support for string building ids for mods --- src/js/game/building_codes.js | 27 ++++++++---- src/js/game/components/static_map_entity.js | 4 +- src/js/savegame/serialization.js | 2 + src/js/savegame/serialization_data_types.js | 47 +++++++++++++++++++++ 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/src/js/game/building_codes.js b/src/js/game/building_codes.js index 78240c31..5e2b717b 100644 --- a/src/js/game/building_codes.js +++ b/src/js/game/building_codes.js @@ -19,7 +19,7 @@ import { Vector } from "../core/vector"; /** * Stores a lookup table for all building variants (for better performance) - * @type {Object} + * @type {Object} */ export const gBuildingVariants = { // Set later @@ -27,13 +27,13 @@ export const gBuildingVariants = { /** * Mapping from 'metaBuildingId/variant/rotationVariant' to building code - * @type {Map} + * @type {Map} */ const variantsCache = new Map(); /** * Registers a new variant - * @param {number} code + * @param {number|string} code * @param {typeof MetaBuilding} meta * @param {string} variant * @param {number} rotationVariant @@ -54,9 +54,20 @@ export function registerBuildingVariant( }; } +/** + * Hashes the combination of buildng, variant and rotation variant + * @param {string} buildingId + * @param {string} variant + * @param {number} rotationVariant + * @returns + */ +function generateBuildingHash(buildingId, variant, rotationVariant) { + return buildingId + "/" + variant + "/" + rotationVariant; +} + /** * - * @param {number} code + * @param {string|number} code * @returns {BuildingVariantIdentifier} */ export function getBuildingDataFromCode(code) { @@ -70,8 +81,8 @@ export function getBuildingDataFromCode(code) { export function buildBuildingCodeCache() { for (const code in gBuildingVariants) { const data = gBuildingVariants[code]; - const hash = data.metaInstance.getId() + "/" + data.variant + "/" + data.rotationVariant; - variantsCache.set(hash, +code); + const hash = generateBuildingHash(data.metaInstance.getId(), data.variant, data.rotationVariant); + variantsCache.set(hash, isNaN(+code) ? code : +code); } } @@ -80,10 +91,10 @@ export function buildBuildingCodeCache() { * @param {MetaBuilding} metaBuilding * @param {string} variant * @param {number} rotationVariant - * @returns {number} + * @returns {number|string} */ export function getCodeFromBuildingData(metaBuilding, variant, rotationVariant) { - const hash = metaBuilding.getId() + "/" + variant + "/" + rotationVariant; + const hash = generateBuildingHash(metaBuilding.getId(), variant, rotationVariant); const result = variantsCache.get(hash); if (G_IS_DEV) { if (!result) { diff --git a/src/js/game/components/static_map_entity.js b/src/js/game/components/static_map_entity.js index c76a298e..a3d6a8ca 100644 --- a/src/js/game/components/static_map_entity.js +++ b/src/js/game/components/static_map_entity.js @@ -19,7 +19,7 @@ export class StaticMapEntityComponent extends Component { originalRotation: types.float, // See building_codes.js - code: types.uint, + code: types.uintOrString, }; } @@ -99,7 +99,7 @@ export class StaticMapEntityComponent extends Component { * @param {Vector=} param0.tileSize Size of the entity in tiles * @param {number=} param0.rotation Rotation in degrees. Must be multiple of 90 * @param {number=} param0.originalRotation Original Rotation in degrees. Must be multiple of 90 - * @param {number=} param0.code Building code + * @param {number|string=} param0.code Building code */ constructor({ origin = new Vector(), diff --git a/src/js/savegame/serialization.js b/src/js/savegame/serialization.js index 78642ceb..9a0ce3a5 100644 --- a/src/js/savegame/serialization.js +++ b/src/js/savegame/serialization.js @@ -22,6 +22,7 @@ import { TypeString, TypeStructuredObject, TypeVector, + TypePositiveIntegerOrString, } from "./serialization_data_types"; const logger = createLogger("serialization"); @@ -38,6 +39,7 @@ export const types = { vector: new TypeVector(), tileVector: new TypeVector(), bool: new TypeBoolean(), + uintOrString: new TypePositiveIntegerOrString(), /** * @param {BaseDataType} wrapped diff --git a/src/js/savegame/serialization_data_types.js b/src/js/savegame/serialization_data_types.js index 9d3b689f..df352e78 100644 --- a/src/js/savegame/serialization_data_types.js +++ b/src/js/savegame/serialization_data_types.js @@ -213,6 +213,53 @@ export class TypePositiveInteger extends BaseDataType { } } +export class TypePositiveIntegerOrString extends BaseDataType { + serialize(value) { + if (Number.isInteger(value)) { + assert(value >= 0, "type integer got negative value: " + value); + } else if (typeof value === "string") { + // all good + } else { + assertAlways(false, "Type integer|string got non integer or string for serialize: " + value); + } + return value; + } + + /** + * @see BaseDataType.deserialize + * @param {any} value + * @param {GameRoot} root + * @param {object} targetObject + * @param {string|number} targetKey + * @returns {string|void} String error code or null on success + */ + deserialize(value, targetObject, targetKey, root) { + targetObject[targetKey] = value; + } + + getAsJsonSchemaUncached() { + return { + oneOf: [{ type: "integer", minimum: 0 }, { type: "string" }], + }; + } + + verifySerializedValue(value) { + if (Number.isInteger(value)) { + if (value < 0) { + return "Negative value for positive integer"; + } + } else if (typeof value === "string") { + // all good + } else { + return "Not a valid number or string: " + value; + } + } + + getCacheKey() { + return "uint_str"; + } +} + export class TypeBoolean extends BaseDataType { serialize(value) { assert(value === true || value === false, "Type bool got non bool for serialize: " + value); From 01b9bf561c5f782b411e60193e1ca071f1cb3b7c Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 07:05:46 +0100 Subject: [PATCH 007/129] Initial support for adding new buildings --- src/js/game/hud/hud.js | 3 + src/js/game/hud/parts/base_toolbar.js | 9 ++- src/js/game/hud/parts/building_placer.js | 15 +++-- src/js/game/meta_building_registry.js | 11 ++-- src/js/mods/demo_mod.js | 65 ++++++++++++++++++-- src/js/mods/mod_interface.js | 78 +++++++++++++++++++++++- src/js/mods/modloader.js | 4 ++ 7 files changed, 165 insertions(+), 20 deletions(-) diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index 3d22787c..cffc290d 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -1,6 +1,7 @@ import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; import { Signal } from "../../core/signal"; +import { MODS } from "../../mods/modloader"; import { KEYMAPPINGS } from "../key_action_mapper"; import { MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; @@ -91,6 +92,7 @@ export class GameHUD { const frag = document.createDocumentFragment(); for (const key in this.parts) { + MODS.signals.hudElementInitialized.dispatch(this.parts[key]); this.parts[key].createElements(frag); } @@ -98,6 +100,7 @@ export class GameHUD { for (const key in this.parts) { this.parts[key].initialize(); + MODS.signals.hudElementFinalized.dispatch(this.parts[key]); } this.root.keyMapper.getBinding(KEYMAPPINGS.ingame.toggleHud).add(this.toggleUi, this); diff --git a/src/js/game/hud/parts/base_toolbar.js b/src/js/game/hud/parts/base_toolbar.js index 15faad66..bee88574 100644 --- a/src/js/game/hud/parts/base_toolbar.js +++ b/src/js/game/hud/parts/base_toolbar.js @@ -1,4 +1,5 @@ import { gMetaBuildingRegistry } from "../../../core/global_registries"; +import { globalWarn } from "../../../core/logging"; import { STOP_PROPAGATION } from "../../../core/signal"; import { makeDiv, safeModulo } from "../../../core/utils"; import { MetaBlockBuilding } from "../../buildings/block"; @@ -101,7 +102,12 @@ export class HUDBaseToolbar extends BaseHUDPart { rawBinding = KEYMAPPINGS.buildings[metaBuilding.getId()]; } - const binding = actionMapper.getBinding(rawBinding); + if (rawBinding) { + const binding = actionMapper.getBinding(rawBinding); + binding.add(() => this.selectBuildingForPlacement(metaBuilding)); + } else { + globalWarn("Building has no keybinding:", metaBuilding.getId()); + } const itemContainer = makeDiv( this.primaryBuildings.includes(allBuildings[i]) ? rowPrimary : rowSecondary, @@ -110,7 +116,6 @@ export class HUDBaseToolbar extends BaseHUDPart { ); itemContainer.setAttribute("data-icon", "building_icons/" + metaBuilding.getId() + ".png"); itemContainer.setAttribute("data-id", metaBuilding.getId()); - binding.add(() => this.selectBuildingForPlacement(metaBuilding)); const icon = makeDiv(itemContainer, null, ["icon"]); diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index 33e6ebc2..ee7bc804 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -126,12 +126,15 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { rawBinding = KEYMAPPINGS.buildings[metaBuilding.getId()]; } - const binding = this.root.keyMapper.getBinding(rawBinding); - - this.buildingInfoElements.hotkey.innerHTML = T.ingame.buildingPlacement.hotkeyLabel.replace( - "", - "" + binding.getKeyCodeString() + "" - ); + if (rawBinding) { + const binding = this.root.keyMapper.getBinding(rawBinding); + this.buildingInfoElements.hotkey.innerHTML = T.ingame.buildingPlacement.hotkeyLabel.replace( + "", + "" + binding.getKeyCodeString() + "" + ); + } else { + this.buildingInfoElements.hotkey.innerHTML = ""; + } this.buildingInfoElements.tutorialImage.setAttribute( "data-icon", diff --git a/src/js/game/meta_building_registry.js b/src/js/game/meta_building_registry.js index 0c93153d..193d60e5 100644 --- a/src/js/game/meta_building_registry.js +++ b/src/js/game/meta_building_registry.js @@ -205,18 +205,15 @@ export function initMetaBuildingRegistry() { const id = metaBuilding.getId(); if (!["hub"].includes(id)) { if (!KEYMAPPINGS.buildings[id]) { - assertAlways( - false, + console.error( "Building " + id + " has no keybinding assigned! Add it to key_action_mapper.js" ); } if (!T.buildings[id]) { - assertAlways(false, "Translation for building " + id + " missing!"); - } - - if (!T.buildings[id].default) { - assertAlways(false, "Translation for building " + id + " missing (default variant)!"); + console.error("Translation for building " + id + " missing!"); + } else if (!T.buildings[id].default) { + console.error("Translation for building " + id + " missing (default variant)!"); } } }); diff --git a/src/js/mods/demo_mod.js b/src/js/mods/demo_mod.js index be3a7c1b..42f48fa3 100644 --- a/src/js/mods/demo_mod.js +++ b/src/js/mods/demo_mod.js @@ -1,4 +1,25 @@ +/* typehints:start */ +import { Entity } from "../game/entity"; +/* typehints:end */ + import { Mod } from "./mod"; +import { MetaBuilding } from "../game/meta_building"; + +export class MetaDemoModBuilding extends MetaBuilding { + constructor() { + super("demoModBuilding"); + } + + getSilhouetteColor() { + return "red"; + } + + /** + * Creates the entity at the given location + * @param {Entity} entity + */ + setupEntityComponents(entity) {} +} export class DemoMod extends Mod { constructor(modLoader) { @@ -23,10 +44,7 @@ export class DemoMod extends Mod { `); // Replace a builtin sprite - 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" - ); + this.modLoader.modInterface.registerSprite("sprites/colors/red.png", RESOURCES["red.png"]); // Add a new type of sub shape ("Line", short code "L") this.modLoader.modInterface.registerSubShapeType({ @@ -71,5 +89,44 @@ export class DemoMod extends Mod { }, }, }); + + // Register the new building + this.modLoader.modInterface.registerNewBuilding({ + metaClass: MetaDemoModBuilding, + buildingIconBase64: RESOURCES["demoBuilding.png"], + + variantsAndRotations: [ + { + description: "A test building", + name: "A test name", + + regularImageBase64: RESOURCES["demoBuilding.png"], + blueprintImageBase64: RESOURCES["demoBuildingBlueprint.png"], + tutorialImageBase64: RESOURCES["demoBuildingBlueprint.png"], + }, + ], + }); + + // Add it to the regular toolbar + this.modLoader.signals.hudElementInitialized.add(element => { + if (element.constructor.name === "HUDBuildingsToolbar") { + // @ts-ignore + element.primaryBuildings.push(MetaDemoModBuilding); + } + }); } } + +//////////////////////////////////////////////////////////////////////// +// @notice: Later this part will be autogenerated + +const RESOURCES = { + "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", + + "demoBuilding.png": + "data:image/png;base64,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", + + "demoBuildingBlueprint.png": + "data:image/png;base64,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", +}; diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index eaf1d314..fae5f2ba 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -1,14 +1,17 @@ /* typehints:start */ -import { Application } from "../application"; import { ModLoader } from "./modloader"; +import { MetaBuilding } from "../game/meta_building"; /* typehints:end */ +import { defaultBuildingVariant } from "../game/meta_building"; import { createLogger } from "../core/logging"; import { AtlasSprite, SpriteAtlasLink } from "../core/sprites"; import { enumShortcodeToSubShape, enumSubShape, enumSubShapeToShortcode } from "../game/shape_definition"; import { Loader } from "../core/loader"; import { LANGUAGES } from "../languages"; import { matchDataRecursive, T } from "../translations"; +import { registerBuildingVariant } from "../game/building_codes"; +import { gMetaBuildingRegistry } from "../core/global_registries"; const LOG = createLogger("mod-interface"); @@ -113,4 +116,77 @@ export class ModInterface { matchDataRecursive(T, translations, true); } } + + /** + * + * @param {object} param0 + * @param {typeof MetaBuilding} param0.metaClass + * @param {string=} param0.buildingIconBase64 + * @param {({ + * variant?: string; + * rotationVariant?: number; + * name: string; + * description: string; + * blueprintImageBase64?: string; + * regularImageBase64?: string; + * tutorialImageBase64?: string; + * }[])} param0.variantsAndRotations + */ + registerNewBuilding({ metaClass, variantsAndRotations, buildingIconBase64 }) { + const id = new /** @type {new () => MetaBuilding} */ (metaClass)().getId(); + if (gMetaBuildingRegistry.hasId(id)) { + throw new Error("Tried to register building twice: " + id); + } + gMetaBuildingRegistry.register(metaClass); + + T.buildings[id] = {}; + variantsAndRotations.forEach(payload => { + const actualVariant = payload.variant || defaultBuildingVariant; + registerBuildingVariant(id, metaClass, actualVariant, payload.rotationVariant || 0); + T.buildings[id][actualVariant] = { + name: payload.name, + description: payload.description, + }; + + const buildingIdentifier = + id + (actualVariant === defaultBuildingVariant ? "" : "-" + actualVariant); + if (payload.regularImageBase64) { + this.registerSprite( + "sprites/buildings/" + buildingIdentifier + ".png", + payload.regularImageBase64 + ); + } + if (payload.blueprintImageBase64) { + this.registerSprite( + "sprites/blueprints/" + buildingIdentifier + ".png", + payload.blueprintImageBase64 + ); + } + if (payload.tutorialImageBase64) { + this.setBuildingTutorialImage(id, actualVariant, payload.tutorialImageBase64); + } + }); + + if (buildingIconBase64) { + this.setBuildingToolbarIcon(id, buildingIconBase64); + } + } + + setBuildingToolbarIcon(buildingId, iconBase64) { + this.registerCss(` + [data-icon="building_icons/${buildingId}.png"] .icon { + background-image: url('${iconBase64}') !important; + } + `); + } + + setBuildingTutorialImage(buildingId, variant, imageBase64) { + const buildingIdentifier = buildingId + (variant === defaultBuildingVariant ? "" : "-" + variant); + + this.registerCss(` + [data-icon="building_tutorials/${buildingIdentifier}.png"] { + background-image: url('${imageBase64}') !important; + } + `); + } } diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 1a903ae2..75115dd7 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -3,6 +3,7 @@ import { Signal } from "../core/signal"; import { DemoMod } from "./demo_mod"; import { Mod } from "./mod"; import { ModInterface } from "./mod_interface"; +import { BaseHUDPart } from "../game/hud/base_hud_part"; const LOG = createLogger("mods"); @@ -25,6 +26,9 @@ export class ModLoader { injectSprites: new Signal(), preprocessTheme: /** @type {TypedSignal<[Object]>} */ (new Signal()), modifyLevelDefinitions: /** @type {TypedSignal<[Array[Object]]>} */ (new Signal()), + + hudElementInitialized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()), + hudElementFinalized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()), }; this.registerMod(DemoMod); From 16ecbf9c6d45a0b1d7466f4ac8d67e805bbcc449 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 07:18:25 +0100 Subject: [PATCH 008/129] Refactor how mods are loaded to resolve circular dependencies and prepare for future mod loading --- src/js/game/hud/hud.js | 6 +- src/js/game/map_chunk.js | 6 +- src/js/game/modes/regular.js | 4 +- src/js/game/shape_definition.js | 14 ++- src/js/game/theme.js | 4 +- src/js/mods/demo_mod.js | 193 ++++++++++++++++---------------- src/js/mods/mod.js | 5 + src/js/mods/mod_interface.js | 28 ++--- src/js/mods/mod_signals.js | 16 +++ src/js/mods/modloader.js | 22 ++-- 10 files changed, 159 insertions(+), 139 deletions(-) create mode 100644 src/js/mods/mod_signals.js diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index cffc290d..9fe1ba2f 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -1,7 +1,7 @@ import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; import { Signal } from "../../core/signal"; -import { MODS } from "../../mods/modloader"; +import { MOD_SIGNALS } from "../../mods/mod_signals"; import { KEYMAPPINGS } from "../key_action_mapper"; import { MetaBuilding } from "../meta_building"; import { GameRoot } from "../root"; @@ -92,7 +92,7 @@ export class GameHUD { const frag = document.createDocumentFragment(); for (const key in this.parts) { - MODS.signals.hudElementInitialized.dispatch(this.parts[key]); + MOD_SIGNALS.hudElementInitialized.dispatch(this.parts[key]); this.parts[key].createElements(frag); } @@ -100,7 +100,7 @@ export class GameHUD { for (const key in this.parts) { this.parts[key].initialize(); - MODS.signals.hudElementFinalized.dispatch(this.parts[key]); + MOD_SIGNALS.hudElementFinalized.dispatch(this.parts[key]); } this.root.keyMapper.getBinding(KEYMAPPINGS.ingame.toggleHud).add(this.toggleUi, this); diff --git a/src/js/game/map_chunk.js b/src/js/game/map_chunk.js index 0c5390e4..ca2db36f 100644 --- a/src/js/game/map_chunk.js +++ b/src/js/game/map_chunk.js @@ -10,10 +10,14 @@ import { COLOR_ITEM_SINGLETONS } from "./items/color_item"; import { GameRoot } from "./root"; import { enumSubShape } from "./shape_definition"; import { Rectangle } from "../core/rectangle"; -import { MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS } from "../mods/mod_interface"; const logger = createLogger("map_chunk"); +/** + * @type {Object number>} + */ +export const MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS = {}; + export class MapChunk { /** * diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index 0aad3669..699874b5 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -38,7 +38,7 @@ import { HUDSandboxController } from "../hud/parts/sandbox_controller"; import { queryParamOptions } from "../../core/query_parameters"; import { MetaBlockBuilding } from "../buildings/block"; import { MetaItemProducerBuilding } from "../buildings/item_producer"; -import { MODS } from "../../mods/modloader"; +import { MOD_SIGNALS } from "../../mods/mod_signals"; /** @typedef {{ * shape: string, @@ -512,7 +512,7 @@ export function generateLevelDefinitions(limitedVersion = false) { ]), ]; - MODS.signals.modifyLevelDefinitions.dispatch(levelDefinitions); + MOD_SIGNALS.modifyLevelDefinitions.dispatch(levelDefinitions); if (G_IS_DEV) { levelDefinitions.forEach(({ shape }) => { diff --git a/src/js/game/shape_definition.js b/src/js/game/shape_definition.js index acc0bd1c..9cf2d094 100644 --- a/src/js/game/shape_definition.js +++ b/src/js/game/shape_definition.js @@ -3,11 +3,23 @@ import { globalConfig } from "../core/config"; import { smoothenDpi } from "../core/dpi_manager"; import { DrawParameters } from "../core/draw_parameters"; import { Vector } from "../core/vector"; -import { MODS_ADDITIONAL_SUB_SHAPE_DRAWERS } from "../mods/mod_interface"; import { BasicSerializableObject, types } from "../savegame/serialization"; import { enumColors, enumColorsToHexCode, enumColorToShortcode, enumShortcodeToColor } from "./colors"; import { THEME } from "./theme"; +/** + * @typedef {{ + * context: CanvasRenderingContext2D, + * quadrantSize: number, + * layerScale: number, + * }} SubShapeDrawOptions + */ + +/** + * @type {Object void>} + */ +export const MODS_ADDITIONAL_SUB_SHAPE_DRAWERS = {}; + /** * @typedef {{ * subShape: enumSubShape, diff --git a/src/js/game/theme.js b/src/js/game/theme.js index 023710dd..5e371740 100644 --- a/src/js/game/theme.js +++ b/src/js/game/theme.js @@ -1,4 +1,4 @@ -import { MODS } from "../mods/modloader"; +import { MOD_SIGNALS } from "../mods/mod_signals"; export const THEMES = { dark: require("./themes/dark.json"), @@ -9,5 +9,5 @@ export let THEME = THEMES.light; export function applyGameTheme(id) { THEME = THEMES[id]; - MODS.signals.preprocessTheme.dispatch({ id, theme: THEME }); + MOD_SIGNALS.preprocessTheme.dispatch({ id, theme: THEME }); } diff --git a/src/js/mods/demo_mod.js b/src/js/mods/demo_mod.js index 42f48fa3..944cae38 100644 --- a/src/js/mods/demo_mod.js +++ b/src/js/mods/demo_mod.js @@ -2,119 +2,118 @@ import { Entity } from "../game/entity"; /* typehints:end */ -import { Mod } from "./mod"; -import { MetaBuilding } from "../game/meta_building"; +export default function ({ Mod, MetaBuilding }) { + class MetaDemoModBuilding extends MetaBuilding { + constructor() { + super("demoModBuilding"); + } -export class MetaDemoModBuilding extends MetaBuilding { - constructor() { - super("demoModBuilding"); + getSilhouetteColor() { + return "red"; + } + + /** + * Creates the entity at the given location + * @param {Entity} entity + */ + setupEntityComponents(entity) {} } - getSilhouetteColor() { - return "red"; - } + return class ModImpl extends Mod { + constructor(modLoader) { + super( + { + authorContact: "tobias@tobspr.io", + authorName: "tobspr", + name: "Demo Mod", + version: "1", + id: "demo-mod", + }, + modLoader + ); + } - /** - * Creates the entity at the given location - * @param {Entity} entity - */ - setupEntityComponents(entity) {} -} + init() { + // Add some custom css + this.modInterface.registerCss(` + * { + color: red !important; + } + `); -export class DemoMod extends Mod { - constructor(modLoader) { - super( - { - authorContact: "tobias@tobspr.io", - authorName: "tobspr", - name: "Demo Mod", - version: "1", - id: "demo-mod", - }, - modLoader - ); - } + // Replace a builtin sprite + this.modInterface.registerSprite("sprites/colors/red.png", RESOURCES["red.png"]); - init() { - // Add some custom css - this.modLoader.modInterface.registerCss(` - * { - color: red !important; - } - `); + // Add a new type of sub shape ("Line", short code "L") + this.modInterface.registerSubShapeType({ + id: "line", + shortCode: "L", + weightComputation: distanceToOriginInChunks => + Math.round(20 + Math.max(Math.min(distanceToOriginInChunks, 30), 0)), - // Replace a builtin sprite - this.modLoader.modInterface.registerSprite("sprites/colors/red.png", RESOURCES["red.png"]); + draw: ({ context, quadrantSize, layerScale }) => { + const quadrantHalfSize = quadrantSize / 2; + context.beginPath(); + context.moveTo(-quadrantHalfSize, quadrantHalfSize); + context.arc( + -quadrantHalfSize, + quadrantHalfSize, + quadrantSize * layerScale, + -Math.PI * 0.25, + 0 + ); + context.closePath(); + }, + }); - // Add a new type of sub shape ("Line", short code "L") - this.modLoader.modInterface.registerSubShapeType({ - id: "line", - shortCode: "L", - weightComputation: distanceToOriginInChunks => - Math.round(20 + Math.max(Math.min(distanceToOriginInChunks, 30), 0)), + // Modify the theme colors + this.signals.preprocessTheme.add(({ theme }) => { + theme.map.background = "#eee"; + theme.items.outline = "#000"; + }); - draw: ({ context, quadrantSize, layerScale }) => { - const quadrantHalfSize = quadrantSize / 2; - context.beginPath(); - context.moveTo(-quadrantHalfSize, quadrantHalfSize); - context.arc( - -quadrantHalfSize, - quadrantHalfSize, - quadrantSize * layerScale, - -Math.PI * 0.25, - 0 - ); - context.closePath(); - }, - }); + // Modify the goal of the first level + this.signals.modifyLevelDefinitions.add(definitions => { + definitions[0].shape = "LuCuLuCu"; + }); - // Modify the theme colors - this.modLoader.signals.preprocessTheme.add(({ theme }) => { - theme.map.background = "#eee"; - theme.items.outline = "#000"; - }); - - // Modify the goal of the first level - this.modLoader.signals.modifyLevelDefinitions.add(definitions => { - definitions[0].shape = "LuCuLuCu"; - }); - - this.modLoader.modInterface.registerTranslations("en", { - ingame: { - interactiveTutorial: { - title: "Hello", - hints: { - "1_1_extractor": "World!", + this.modInterface.registerTranslations("en", { + ingame: { + interactiveTutorial: { + title: "Hello", + hints: { + "1_1_extractor": "World!", + }, }, }, - }, - }); + }); - // Register the new building - this.modLoader.modInterface.registerNewBuilding({ - metaClass: MetaDemoModBuilding, - buildingIconBase64: RESOURCES["demoBuilding.png"], + // Register the new building + this.modInterface.registerNewBuilding({ + metaClass: MetaDemoModBuilding, + buildingIconBase64: RESOURCES["demoBuilding.png"], - variantsAndRotations: [ - { - description: "A test building", - name: "A test name", + variantsAndRotations: [ + { + description: "A test building", + name: "A test name", - regularImageBase64: RESOURCES["demoBuilding.png"], - blueprintImageBase64: RESOURCES["demoBuildingBlueprint.png"], - tutorialImageBase64: RESOURCES["demoBuildingBlueprint.png"], - }, - ], - }); + regularImageBase64: RESOURCES["demoBuilding.png"], + blueprintImageBase64: RESOURCES["demoBuildingBlueprint.png"], + tutorialImageBase64: RESOURCES["demoBuildingBlueprint.png"], + }, + ], + }); - // Add it to the regular toolbar - this.modLoader.signals.hudElementInitialized.add(element => { - if (element.constructor.name === "HUDBuildingsToolbar") { - // @ts-ignore - element.primaryBuildings.push(MetaDemoModBuilding); - } - }); - } + // Add it to the regular toolbar + this.signals.hudElementInitialized.add(element => { + if (element.constructor.name === "HUDBuildingsToolbar") { + // @ts-ignore + element.primaryBuildings.push(MetaDemoModBuilding); + } + }); + } + }; } //////////////////////////////////////////////////////////////////////// diff --git a/src/js/mods/mod.js b/src/js/mods/mod.js index 65cdd53a..12992999 100644 --- a/src/js/mods/mod.js +++ b/src/js/mods/mod.js @@ -2,6 +2,8 @@ import { ModLoader } from "./modloader"; /* typehints:end */ +import { MOD_SIGNALS } from "./mod_signals"; + export class Mod { /** * @@ -17,6 +19,9 @@ export class Mod { constructor(metadata, modLoader) { this.metadata = metadata; this.modLoader = modLoader; + + this.signals = MOD_SIGNALS; + this.modInterface = modLoader.modInterface; } init() {} diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index fae5f2ba..5d395819 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -6,33 +6,21 @@ import { MetaBuilding } from "../game/meta_building"; import { defaultBuildingVariant } from "../game/meta_building"; import { createLogger } from "../core/logging"; import { AtlasSprite, SpriteAtlasLink } from "../core/sprites"; -import { enumShortcodeToSubShape, enumSubShape, enumSubShapeToShortcode } from "../game/shape_definition"; +import { + enumShortcodeToSubShape, + enumSubShape, + enumSubShapeToShortcode, + MODS_ADDITIONAL_SUB_SHAPE_DRAWERS, +} from "../game/shape_definition"; import { Loader } from "../core/loader"; import { LANGUAGES } from "../languages"; import { matchDataRecursive, T } from "../translations"; import { registerBuildingVariant } from "../game/building_codes"; import { gMetaBuildingRegistry } from "../core/global_registries"; +import { MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS } from "../game/map_chunk"; const LOG = createLogger("mod-interface"); -/** - * @type {Object number>} - */ -export const MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS = {}; - -/** - * @typedef {{ - * context: CanvasRenderingContext2D, - * quadrantSize: number, - * layerScale: number, - * }} SubShapeDrawOptions - */ - -/** - * @type {Object void>} - */ -export const MODS_ADDITIONAL_SUB_SHAPE_DRAWERS = {}; - export class ModInterface { /** * @@ -91,7 +79,7 @@ export class ModInterface { * @param {string} param0.id * @param {string} param0.shortCode * @param {(distanceToOriginInChunks: number) => number} param0.weightComputation - * @param {(options: SubShapeDrawOptions) => void} param0.draw + * @param {(options: import("../game/shape_definition").SubShapeDrawOptions) => void} param0.draw */ registerSubShapeType({ id, shortCode, weightComputation, draw }) { if (shortCode.length !== 1) { diff --git a/src/js/mods/mod_signals.js b/src/js/mods/mod_signals.js new file mode 100644 index 00000000..a2bc0380 --- /dev/null +++ b/src/js/mods/mod_signals.js @@ -0,0 +1,16 @@ +import { Signal } from "../core/signal"; +/* typehints:start */ +import { BaseHUDPart } from "../game/hud/base_hud_part"; +/* typehints:end */ + +// Single file to avoid circular deps + +export const MOD_SIGNALS = { + postInit: new Signal(), + injectSprites: new Signal(), + preprocessTheme: /** @type {TypedSignal<[Object]>} */ (new Signal()), + modifyLevelDefinitions: /** @type {TypedSignal<[Array[Object]]>} */ (new Signal()), + + hudElementInitialized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()), + hudElementFinalized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()), +}; diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 75115dd7..49e30662 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -1,9 +1,8 @@ import { createLogger } from "../core/logging"; -import { Signal } from "../core/signal"; -import { DemoMod } from "./demo_mod"; import { Mod } from "./mod"; import { ModInterface } from "./mod_interface"; -import { BaseHUDPart } from "../game/hud/base_hud_part"; +import { MetaBuilding } from "../game/meta_building"; +import { MOD_SIGNALS } from "./mod_signals"; const LOG = createLogger("mods"); @@ -21,17 +20,14 @@ export class ModLoader { 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.signals = MOD_SIGNALS; - hudElementInitialized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()), - hudElementFinalized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()), - }; - - this.registerMod(DemoMod); + this.registerMod( + /** @type {any} */ (require("./demo_mod").default({ + Mod, + MetaBuilding, + })) + ); this.initMods(); } From bc5eff84f6f4266f9032898dee897699e81fd6be Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 07:37:26 +0100 Subject: [PATCH 009/129] Lazy Load mods to make sure all dependencies are loaded --- src/js/application.js | 6 ++++++ src/js/game/modes/regular.js | 30 ++++++++++++++++++------------ src/js/mods/demo_mod.js | 5 +++-- src/js/mods/mod.js | 5 ++++- src/js/mods/mod_interface.js | 6 +++++- src/js/mods/mod_signals.js | 3 +++ src/js/mods/modloader.js | 17 +++++++---------- 7 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/js/application.js b/src/js/application.js index c49b7027..66e9eb8c 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -35,6 +35,8 @@ import { PuzzleMenuState } from "./states/puzzle_menu"; import { ClientAPI } from "./platform/api"; import { LoginState } from "./states/login"; import { WegameSplashState } from "./states/wegame_splash"; +import { MODS } from "./mods/modloader"; +import { MOD_SIGNALS } from "./mods/mod_signals"; /** * @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface @@ -128,6 +130,8 @@ export class Application { // Store the mouse position, or null if not available /** @type {Vector|null} */ this.mousePosition = null; + + MODS.initMods(); } /** @@ -148,6 +152,8 @@ export class Application { this.analytics = new GoogleAnalyticsImpl(this); this.gameAnalytics = new ShapezGameAnalytics(this); this.achievementProvider = new NoAchievementProvider(this); + + MOD_SIGNALS.platformInstancesInitialized.dispatch(); } /** diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index 699874b5..b52cbc89 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -69,10 +69,16 @@ const tierGrowth = 2.5; const chinaShapes = G_WEGAME_VERSION || G_CHINA_VERSION; +const upgradesCache = {}; + /** * Generates all upgrades * @returns {Object} */ function generateUpgrades(limitedVersion = false) { + if (upgradesCache[limitedVersion]) { + return upgradesCache[limitedVersion]; + } + const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1]; const numEndgameUpgrades = limitedVersion ? 0 : 1000 - fixedImprovements.length - 1; @@ -265,6 +271,8 @@ function generateUpgrades(limitedVersion = false) { } } + MOD_SIGNALS.modifyUpgrades.dispatch(upgrades); + // VALIDATE if (G_IS_DEV) { for (const upgradeId in upgrades) { @@ -280,14 +288,20 @@ function generateUpgrades(limitedVersion = false) { } } + upgradesCache[limitedVersion] = upgrades; return upgrades; } +const levelDefinitionsCache = {}; + /** * Generates the level definitions * @param {boolean} limitedVersion */ export function generateLevelDefinitions(limitedVersion = false) { + if (levelDefinitionsCache[limitedVersion]) { + return levelDefinitionsCache[limitedVersion]; + } const levelDefinitions = [ // 1 // Circle @@ -524,15 +538,11 @@ export function generateLevelDefinitions(limitedVersion = false) { }); } + levelDefinitionsCache[limitedVersion] = levelDefinitions; + return levelDefinitions; } -const fullVersionUpgrades = generateUpgrades(false); -const demoVersionUpgrades = generateUpgrades(true); - -const fullVersionLevels = generateLevelDefinitions(false); -const demoVersionLevels = generateLevelDefinitions(true); - export class RegularGameMode extends GameMode { static getId() { return enumGameModeIds.regular; @@ -603,9 +613,7 @@ export class RegularGameMode extends GameMode { * @returns {Object} */ getUpgrades() { - return this.root.app.restrictionMgr.getHasExtendedUpgrades() - ? fullVersionUpgrades - : demoVersionUpgrades; + return generateUpgrades(!this.root.app.restrictionMgr.getHasExtendedUpgrades()); } /** @@ -613,9 +621,7 @@ export class RegularGameMode extends GameMode { * @returns {Array} */ getLevelDefinitions() { - return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay() - ? fullVersionLevels - : demoVersionLevels; + return generateLevelDefinitions(!this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay()); } /** diff --git a/src/js/mods/demo_mod.js b/src/js/mods/demo_mod.js index 944cae38..e11c3667 100644 --- a/src/js/mods/demo_mod.js +++ b/src/js/mods/demo_mod.js @@ -20,8 +20,9 @@ export default function ({ Mod, MetaBuilding }) { } return class ModImpl extends Mod { - constructor(modLoader) { + constructor(app, modLoader) { super( + app, { authorContact: "tobias@tobspr.io", authorName: "tobspr", @@ -37,7 +38,7 @@ export default function ({ Mod, MetaBuilding }) { // Add some custom css this.modInterface.registerCss(` * { - color: red !important; + font-family: "Comic Sans", "Comic Sans MS", Tahoma !important; } `); diff --git a/src/js/mods/mod.js b/src/js/mods/mod.js index 12992999..b9018b68 100644 --- a/src/js/mods/mod.js +++ b/src/js/mods/mod.js @@ -1,4 +1,5 @@ /* typehints:start */ +import { Application } from "../application"; import { ModLoader } from "./modloader"; /* typehints:end */ @@ -7,6 +8,7 @@ import { MOD_SIGNALS } from "./mod_signals"; export class Mod { /** * + * @param {Application} app * @param {object} metadata * @param {string} metadata.name * @param {string} metadata.version @@ -16,7 +18,8 @@ export class Mod { * * @param {ModLoader} modLoader */ - constructor(metadata, modLoader) { + constructor(app, metadata, modLoader) { + this.app = app; this.metadata = metadata; this.modLoader = modLoader; diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index 5d395819..64c4971e 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -15,7 +15,7 @@ import { import { Loader } from "../core/loader"; import { LANGUAGES } from "../languages"; import { matchDataRecursive, T } from "../translations"; -import { registerBuildingVariant } from "../game/building_codes"; +import { gBuildingVariants, registerBuildingVariant } from "../game/building_codes"; import { gMetaBuildingRegistry } from "../core/global_registries"; import { MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS } from "../game/map_chunk"; @@ -126,11 +126,15 @@ export class ModInterface { throw new Error("Tried to register building twice: " + id); } gMetaBuildingRegistry.register(metaClass); + const metaInstance = gMetaBuildingRegistry.findByClass(metaClass); T.buildings[id] = {}; variantsAndRotations.forEach(payload => { const actualVariant = payload.variant || defaultBuildingVariant; registerBuildingVariant(id, metaClass, actualVariant, payload.rotationVariant || 0); + + gBuildingVariants[id].metaInstance = metaInstance; + T.buildings[id][actualVariant] = { name: payload.name, description: payload.description, diff --git a/src/js/mods/mod_signals.js b/src/js/mods/mod_signals.js index a2bc0380..23670d8b 100644 --- a/src/js/mods/mod_signals.js +++ b/src/js/mods/mod_signals.js @@ -7,9 +7,12 @@ import { BaseHUDPart } from "../game/hud/base_hud_part"; export const MOD_SIGNALS = { postInit: new Signal(), + platformInstancesInitialized: new Signal(), + injectSprites: new Signal(), preprocessTheme: /** @type {TypedSignal<[Object]>} */ (new Signal()), modifyLevelDefinitions: /** @type {TypedSignal<[Array[Object]]>} */ (new Signal()), + modifyUpgrades: /** @type {TypedSignal<[Object]>} */ (new Signal()), hudElementInitialized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()), hudElementFinalized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()), diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 49e30662..8edc2d3d 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -15,20 +15,14 @@ export class ModLoader { this.modInterface = new ModInterface(this); - /** @type {(new (ModLoader) => Mod)[]} */ + /** @type {((Object) => (new (Application, ModLoader) => Mod))[]} */ this.modLoadQueue = []; this.initialized = false; this.signals = MOD_SIGNALS; - this.registerMod( - /** @type {any} */ (require("./demo_mod").default({ - Mod, - MetaBuilding, - })) - ); - this.initMods(); + this.registerMod(/** @type {any} */ (require("./demo_mod").default)); } linkApp(app) { @@ -39,7 +33,10 @@ export class ModLoader { LOG.log("hook:init"); this.initialized = true; this.modLoadQueue.forEach(modClass => { - const mod = new modClass(this); + const mod = new (modClass({ + Mod, + MetaBuilding, + }))(this.app, this); mod.init(); this.mods.push(mod); }); @@ -49,7 +46,7 @@ export class ModLoader { /** * - * @param {new (ModLoader) => Mod} mod + * @param {(Object) => (new (Application, ModLoader) => Mod)} mod */ registerMod(mod) { if (this.initialized) { From a6b024be256c4e6dede72010089e2115ac0c8e8b Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 07:51:48 +0100 Subject: [PATCH 010/129] Expose all exported members automatically to mods --- src/js/mods/demo_mod.js | 6 +++--- src/js/mods/modloader.js | 33 +++++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/js/mods/demo_mod.js b/src/js/mods/demo_mod.js index e11c3667..43ceffd4 100644 --- a/src/js/mods/demo_mod.js +++ b/src/js/mods/demo_mod.js @@ -2,8 +2,8 @@ import { Entity } from "../game/entity"; /* typehints:end */ -export default function ({ Mod, MetaBuilding }) { - class MetaDemoModBuilding extends MetaBuilding { +export default function (shapez) { + class MetaDemoModBuilding extends shapez.MetaBuilding { constructor() { super("demoModBuilding"); } @@ -19,7 +19,7 @@ export default function ({ Mod, MetaBuilding }) { setupEntityComponents(entity) {} } - return class ModImpl extends Mod { + return class ModImpl extends shapez.Mod { constructor(app, modLoader) { super( app, diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 8edc2d3d..5732b083 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -31,12 +31,37 @@ export class ModLoader { initMods() { LOG.log("hook:init"); + + let exports = {}; + + if (G_IS_DEV || G_IS_STANDALONE) { + const modules = require.context("../", true, /\.js$/); + + Array.from(modules.keys()).forEach(key => { + // @ts-ignore + const module = modules(key); + for (const member in module) { + if (member === "default") { + continue; + } + if (exports[member]) { + continue; + } + Object.defineProperty(exports, member, { + get() { + return module[member]; + }, + set(v) { + module[member] = v; + }, + }); + } + }); + } + this.initialized = true; this.modLoadQueue.forEach(modClass => { - const mod = new (modClass({ - Mod, - MetaBuilding, - }))(this.app, this); + const mod = new (modClass(exports))(this.app, this); mod.init(); this.mods.push(mod); }); From 258dbbd98f84bea5ca4815ab80d861f82cd54415 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 07:53:32 +0100 Subject: [PATCH 011/129] Fix duplicate exports --- src/js/game/buildings/logic_gate.js | 2 +- src/js/game/buildings/virtual_processor.js | 2 +- src/js/game/map_chunk_aggregate.js | 3 +-- src/js/mods/modloader.js | 3 +-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/js/game/buildings/logic_gate.js b/src/js/game/buildings/logic_gate.js index b61d4373..a0e63c06 100644 --- a/src/js/game/buildings/logic_gate.js +++ b/src/js/game/buildings/logic_gate.js @@ -15,7 +15,7 @@ export const enumLogicGateVariants = { }; /** @enum {string} */ -export const enumVariantToGate = { +const enumVariantToGate = { [defaultBuildingVariant]: enumLogicGateType.and, [enumLogicGateVariants.not]: enumLogicGateType.not, [enumLogicGateVariants.xor]: enumLogicGateType.xor, diff --git a/src/js/game/buildings/virtual_processor.js b/src/js/game/buildings/virtual_processor.js index b4f91762..ab32a0d3 100644 --- a/src/js/game/buildings/virtual_processor.js +++ b/src/js/game/buildings/virtual_processor.js @@ -19,7 +19,7 @@ export const enumVirtualProcessorVariants = { }; /** @enum {string} */ -export const enumVariantToGate = { +const enumVariantToGate = { [defaultBuildingVariant]: enumLogicGateType.cutter, [enumVirtualProcessorVariants.rotater]: enumLogicGateType.rotater, [enumVirtualProcessorVariants.unstacker]: enumLogicGateType.unstacker, diff --git a/src/js/game/map_chunk_aggregate.js b/src/js/game/map_chunk_aggregate.js index de15362d..f47ed676 100644 --- a/src/js/game/map_chunk_aggregate.js +++ b/src/js/game/map_chunk_aggregate.js @@ -2,10 +2,9 @@ import { globalConfig } from "../core/config"; import { DrawParameters } from "../core/draw_parameters"; import { drawSpriteClipped } from "../core/draw_utils"; import { safeModulo } from "../core/utils"; +import { CHUNK_OVERLAY_RES } from "./map_chunk_view"; import { GameRoot } from "./root"; -export const CHUNK_OVERLAY_RES = 3; - export class MapChunkAggregate { /** * diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 5732b083..beb9e5e1 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -1,7 +1,6 @@ import { createLogger } from "../core/logging"; import { Mod } from "./mod"; import { ModInterface } from "./mod_interface"; -import { MetaBuilding } from "../game/meta_building"; import { MOD_SIGNALS } from "./mod_signals"; const LOG = createLogger("mods"); @@ -45,7 +44,7 @@ export class ModLoader { continue; } if (exports[member]) { - continue; + throw new Error("Duplicate export of " + member); } Object.defineProperty(exports, member, { get() { From 575abd255d39fc78ddcff1e37d53d9a0e48c12b5 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 08:55:18 +0100 Subject: [PATCH 012/129] Allow loading mods from standalone --- electron/.gitignore | 1 + electron/index.js | 27 ++++++++++ electron/mods/README.txt | 6 +++ src/css/states/main_menu.scss | 69 ++++++++++++++++++++++++- src/js/application.js | 7 +-- src/js/core/config.local.template.js | 3 ++ src/js/mods/{demo_mod.js => dev_mod.js} | 18 ++----- src/js/mods/modloader.js | 41 ++++++++++----- src/js/states/main_menu.js | 61 ++++++++++++++++++++-- translations/base-en.yaml | 10 ++++ 10 files changed, 209 insertions(+), 34 deletions(-) create mode 100644 electron/.gitignore create mode 100644 electron/mods/README.txt rename src/js/mods/{demo_mod.js => dev_mod.js} (98%) 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 From ed837703487484fca1b887f8c7e904c5ace9aa96 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 09:05:03 +0100 Subject: [PATCH 013/129] update changelog --- src/js/changelog.js | 7 +++++++ version | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/js/changelog.js b/src/js/changelog.js index ec9a317c..a68c8ce4 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -1,4 +1,11 @@ export const CHANGELOG = [ + { + version: "1.5.0", + date: "unreleased", + entries: [ + "This version adds an official modloader! You can now load mods by placing it in the mods/ folder of the game.", + ], + }, { version: "1.4.4", date: "29.08.2021", diff --git a/version b/version index e1df5de7..3e1ad720 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.4.4 \ No newline at end of file +1.5.0 \ No newline at end of file From e0e5fb8d2a7670fc7ff848787924f28bb3d36ad8 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 09:55:03 +0100 Subject: [PATCH 014/129] Fix mods folder incorrect path --- electron/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/index.js b/electron/index.js index e6f9ce1e..b3f75e72 100644 --- a/electron/index.js +++ b/electron/index.js @@ -17,7 +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"); +let modsPath = path.join(path.dirname(app.getPath("exe")), "mods"); if (!fs.existsSync(storePath)) { // No try-catch by design From 0e86cd3535f6c3612de3f758d9ddaf62f4c1be4a Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 10:14:51 +0100 Subject: [PATCH 015/129] Fix modloading in standalone --- src/js/mods/modloader.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 5c308d8a..3db01878 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -40,11 +40,12 @@ export class ModLoader { const mods = await getIPCRenderer().invoke("get-mods"); mods.forEach(modCode => { - const registerMod = mod => { + window.registerMod = mod => { this.modLoadQueue.push(mod); }; // ugh eval(modCode); + delete window.registerMod; }); } catch (ex) { alert("Failed to load mods: " + ex); From 38b4e93180cbed10c77520fdb425d9b8cf5f9ad4 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 10:39:26 +0100 Subject: [PATCH 016/129] Fix sprites not getting replaced, update demo mod --- src/js/mods/dev_mod.js | 39 +++++++++++++++++++++++++++++------- src/js/mods/mod_interface.js | 1 + 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/js/mods/dev_mod.js b/src/js/mods/dev_mod.js index aa74f59a..dbe4aa0f 100644 --- a/src/js/mods/dev_mod.js +++ b/src/js/mods/dev_mod.js @@ -27,15 +27,23 @@ export default function (shapez) { } init() { + // Increase belt speed + shapez.globalConfig.beltSpeedItemsPerSecond = 25; + // Add some custom css - // this.modInterface.registerCss(` - // button { - // font-family: "Comic Sans", "Comic Sans MS", Tahoma !important; - // } - // `); + this.modInterface.registerCss(` + * { + font-family: "Comic Sans", "Comic Sans MS", Tahoma !important; + } + `); // Replace a builtin sprite - this.modInterface.registerSprite("sprites/colors/red.png", RESOURCES["red.png"]); + ["red", "green", "blue", "yellow", "purple", "cyan", "white"].forEach(color => { + this.modInterface.registerSprite( + "sprites/colors/" + color + ".png", + RESOURCES[color + ".png"] + ); + }); // Add a new type of sub shape ("Line", short code "L") this.modInterface.registerSubShapeType({ @@ -114,8 +122,25 @@ export default function (shapez) { const RESOURCES = { "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,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", + "green.png": + "data:image/png;base64,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", + + "purple.png": + "data:image/png;base64,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", + + "blue.png": + "data:image/png;base64,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", + + "yellow.png": + "data:image/png;base64,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", + + "cyan.png": + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAOxAAADsQH1g+1JAAAE8mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNy4xLWMwMDAgNzkuZGFiYWNiYiwgMjAyMS8wNC8xNC0wMDozOTo0NCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIzLjAgKE1hY2ludG9zaCkiIHhtcDpDcmVhdGVEYXRlPSIyMDIyLTAxLTE0VDEwOjMwOjMyKzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMi0wMS0xNFQxMDozMDo1MCswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMi0wMS0xNFQxMDozMDo1MCswMTowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZTFiZTU2MjEtMTU1ZC00YmFmLWFiNzgtYjM2Y2QzMmIzNTNkIiB4bXBNTTpEb2N1bWVudElE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", + + "white.png": + "data:image/png;base64,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", "demoBuilding.png": "data:image/png;base64,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", diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index 64c4971e..b0a8fe0c 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -63,6 +63,7 @@ export class ModInterface { sprite.linksByResolution["0.75"] = link; this.lazySprites.set(spriteId, sprite); + Loader.sprites.set(spriteId, sprite); } injectSprites() { From f3b8d329fcc90cf9131824c02f3629d378f3f2b5 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 10:56:33 +0100 Subject: [PATCH 017/129] Load dev mod via raw loader --- gulp/package.json | 1 + gulp/yarn.lock | 46 ++++++++++++++++++++++++++++++++++++++++ src/js/mods/dev_mod.js | 7 ++---- src/js/mods/modloader.js | 14 ++++++------ 4 files changed, 57 insertions(+), 11 deletions(-) diff --git a/gulp/package.json b/gulp/package.json index 2a17b4fd..adc4389f 100644 --- a/gulp/package.json +++ b/gulp/package.json @@ -46,6 +46,7 @@ "postcss": ">=5.0.0", "promise-polyfill": "^8.1.0", "query-string": "^6.8.1", + "raw-loader": "^4.0.2", "rusha": "^0.8.13", "serialize-error": "^3.0.0", "stream-browserify": "^3.0.0", diff --git a/gulp/yarn.lock b/gulp/yarn.lock index f4f3ba7f..93ef55b1 100644 --- a/gulp/yarn.lock +++ b/gulp/yarn.lock @@ -989,6 +989,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/json-schema@^7.0.8": + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" + integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz" @@ -1237,6 +1242,11 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz" integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + ajv@^4.7.0: version "4.11.8" resolved "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz" @@ -1255,6 +1265,16 @@ ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.5.5, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + alphanum-sort@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz" @@ -7407,6 +7427,15 @@ loader-utils@^1.0.0, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4 emojis-list "^3.0.0" json5 "^1.0.1" +loader-utils@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129" + integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + localtunnel@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.0.tgz" @@ -10227,6 +10256,14 @@ raw-body@^2.3.2: iconv-lite "0.4.24" unpipe "1.0.0" +raw-loader@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" + integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + rcedit@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/rcedit/-/rcedit-2.0.0.tgz" @@ -10869,6 +10906,15 @@ schema-utils@^2.6.5: ajv "^6.12.0" ajv-keywords "^3.4.1" +schema-utils@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + seek-bzip@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz" diff --git a/src/js/mods/dev_mod.js b/src/js/mods/dev_mod.js index dbe4aa0f..b636dace 100644 --- a/src/js/mods/dev_mod.js +++ b/src/js/mods/dev_mod.js @@ -1,4 +1,4 @@ -export default function (shapez) { +registerMod(shapez => { class MetaDemoModBuilding extends shapez.MetaBuilding { constructor() { super("demoModBuilding"); @@ -27,9 +27,6 @@ export default function (shapez) { } init() { - // Increase belt speed - shapez.globalConfig.beltSpeedItemsPerSecond = 25; - // Add some custom css this.modInterface.registerCss(` * { @@ -115,7 +112,7 @@ export default function (shapez) { }); } }; -} +}); //////////////////////////////////////////////////////////////////////// // @notice: Later this part will be autogenerated diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 3db01878..45c70caa 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -35,9 +35,15 @@ export class ModLoader { async initMods() { LOG.log("hook:init"); - if (G_IS_STANDALONE) { + if (G_IS_STANDALONE || G_IS_DEV) { try { - const mods = await getIPCRenderer().invoke("get-mods"); + let mods = []; + if (G_IS_STANDALONE) { + mods = await getIPCRenderer().invoke("get-mods"); + } else if (G_IS_DEV && globalConfig.debug.loadDevMod) { + // @ts-expect-error + mods = [require("!!raw-loader!./dev_mod")]; + } mods.forEach(modCode => { window.registerMod = mod => { @@ -50,10 +56,6 @@ export class ModLoader { } 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 = {}; From 6fa2515d858b3797fa021c72a55f2dded9492e00 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 11:08:16 +0100 Subject: [PATCH 018/129] Improve mod developing so mods are directly ready to be deployed, load mods from local file server --- gulp/gulpfile.js | 2 +- gulp/webpack.config.js | 1 + gulp/webpack.production.config.js | 1 + src/js/mods/{dev_mod.js => demo_mod.nobuild/index.js} | 0 src/js/mods/mod_interface.js | 11 ++++++++++- src/js/mods/modloader.js | 8 ++++++-- 6 files changed, 19 insertions(+), 4 deletions(-) rename src/js/mods/{dev_mod.js => demo_mod.nobuild/index.js} (100%) diff --git a/gulp/gulpfile.js b/gulp/gulpfile.js index 0f4f4185..87901377 100644 --- a/gulp/gulpfile.js +++ b/gulp/gulpfile.js @@ -146,7 +146,7 @@ gulp.task("main.webserver", () => { */ function serve({ version = "web" }) { browserSync.init({ - server: buildFolder, + server: [buildFolder, path.join(baseDir, "src", "js")], port: 3005, ghostMode: { clicks: false, diff --git a/gulp/webpack.config.js b/gulp/webpack.config.js index 14987cfa..7db0bf0b 100644 --- a/gulp/webpack.config.js +++ b/gulp/webpack.config.js @@ -71,6 +71,7 @@ module.exports = ({ watch = false, standalone = false, chineseVersion = false, w type: "javascript/auto", }, { test: /\.(png|jpe?g|svg)$/, loader: "ignore-loader" }, + { test: /\.nobuild/, loader: "ignore-loader" }, { test: /\.md$/, use: [ diff --git a/gulp/webpack.production.config.js b/gulp/webpack.production.config.js index fd7551e0..72bae9f4 100644 --- a/gulp/webpack.production.config.js +++ b/gulp/webpack.production.config.js @@ -177,6 +177,7 @@ module.exports = ({ type: "javascript/auto", }, { test: /\.(png|jpe?g|svg)$/, loader: "ignore-loader" }, + { test: /\.nobuild/, loader: "ignore-loader" }, { test: /\.js$/, enforce: "pre", diff --git a/src/js/mods/dev_mod.js b/src/js/mods/demo_mod.nobuild/index.js similarity index 100% rename from src/js/mods/dev_mod.js rename to src/js/mods/demo_mod.nobuild/index.js diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index b0a8fe0c..89330d3d 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -1,5 +1,6 @@ /* typehints:start */ import { ModLoader } from "./modloader"; +import { Component } from "../game/component"; import { MetaBuilding } from "../game/meta_building"; /* typehints:end */ @@ -16,7 +17,7 @@ import { Loader } from "../core/loader"; import { LANGUAGES } from "../languages"; import { matchDataRecursive, T } from "../translations"; import { gBuildingVariants, registerBuildingVariant } from "../game/building_codes"; -import { gMetaBuildingRegistry } from "../core/global_registries"; +import { gComponentRegistry, gMetaBuildingRegistry } from "../core/global_registries"; import { MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS } from "../game/map_chunk"; const LOG = createLogger("mod-interface"); @@ -106,6 +107,14 @@ export class ModInterface { } } + /** + * + * @param {typeof Component} component + */ + registerComponent(component) { + gComponentRegistry.register(component); + } + /** * * @param {object} param0 diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 45c70caa..b9a7d4d2 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -41,8 +41,12 @@ export class ModLoader { if (G_IS_STANDALONE) { mods = await getIPCRenderer().invoke("get-mods"); } else if (G_IS_DEV && globalConfig.debug.loadDevMod) { - // @ts-expect-error - mods = [require("!!raw-loader!./dev_mod")]; + const mod = await ( + await fetch("http://localhost:3005/mods/demo_mod.nobuild/index.js", { + method: "GET", + }) + ).text(); + mods.push(mod); } mods.forEach(modCode => { From 4176621b057dbd65d56c074544a556f7ab633157 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 14 Jan 2022 12:34:02 +0100 Subject: [PATCH 019/129] Proper mods ui --- res/ui/icons/mods.png | Bin 0 -> 4450 bytes res/ui/icons/mods_white.png | Bin 0 -> 4455 bytes src/css/main.scss | 1 + src/css/states/main_menu.scss | 57 ++++++++--- src/css/states/mods.scss | 131 +++++++++++++++++++++++++ src/css/states/settings.scss | 48 +++++++-- src/css/variables.scss | 1 + src/js/application.js | 2 + src/js/mods/demo_mod.nobuild/index.js | 15 +-- src/js/mods/mod.js | 5 +- src/js/states/main_menu.js | 50 ++++------ src/js/states/mods.js | 135 ++++++++++++++++++++++++++ src/js/states/settings.js | 23 +++++ translations/base-en.yaml | 26 +++-- 14 files changed, 431 insertions(+), 63 deletions(-) create mode 100644 res/ui/icons/mods.png create mode 100644 res/ui/icons/mods_white.png create mode 100644 src/css/states/mods.scss create mode 100644 src/js/states/mods.js diff --git a/res/ui/icons/mods.png b/res/ui/icons/mods.png new file mode 100644 index 0000000000000000000000000000000000000000..1b3cb477aff032e4770ee2c8010e2d919c9fc5c7 GIT binary patch literal 4450 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 literal 0 HcmV?d00001 diff --git a/res/ui/icons/mods_white.png b/res/ui/icons/mods_white.png new file mode 100644 index 0000000000000000000000000000000000000000..1cb34f27dc90aaec243ce98e1b387f56cd7a2c46 GIT binary patch literal 4455 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 .headerBar { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + + > h1 { + justify-self: start; + } + + .openModsFolder { + background-color: $modsColor; + } + } + + .noModSupport { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + flex-direction: column; + + .steamLink { + @include S(height, 50px); + @include S(width, 220px); + background: #171a23 center center / contain no-repeat; + overflow: hidden; + display: block; + text-indent: -999em; + cursor: pointer; + @include S(margin-top, 30px); + pointer-events: all; + transition: all 0.12s ease-in; + transition-property: opacity, transform; + + @include S(border-radius, $globalBorderRadius); + + &:hover { + opacity: 0.9; + } + } + } + + .modsStats { + @include PlainText; + color: $accentColorDark; + + &.noMods { + @include S(width, 400px); + align-self: center; + justify-self: center; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + @include Text; + @include S(margin-top, 100px); + color: lighten($accentColorDark, 15); + + &::before { + @include S(margin-bottom, 15px); + content: ""; + @include S(width, 50px); + @include S(height, 50px); + background-position: center center; + background-size: contain; + opacity: 0.2; + } + &::before { + /* @load-async */ + background-image: uiResource("res/ui/icons/mods.png") !important; + } + } + } + + .modsList { + @include S(margin-top, 10px); + overflow-y: scroll; + pointer-events: all; + @include S(padding-right, 5px); + flex-grow: 1; + + .mod { + @include S(border-radius, $globalBorderRadius); + background: $accentColorBright; + @include S(margin-bottom, 4px); + @include S(padding, 7px); + @include S(grid-gap, 5px); + display: grid; + grid-template-columns: 1fr D(100px) D(100px) D(100px); + + .checkbox { + align-self: center; + justify-self: center; + } + + .mainInfo { + display: flex; + flex-direction: column; + + .description { + @include SuperSmallText; + @include S(margin-top, 5px); + color: $accentColorDark; + } + .website { + text-transform: uppercase; + align-self: start; + @include SuperSmallText; + @include S(margin-top, 5px); + } + } + + .version, + .author { + display: flex; + flex-direction: column; + strong { + text-transform: uppercase; + color: $accentColorDark; + @include SuperSmallText; + } + } + } + } +} diff --git a/src/css/states/settings.scss b/src/css/states/settings.scss index 5b36c677..75ef6b85 100644 --- a/src/css/states/settings.scss +++ b/src/css/states/settings.scss @@ -15,20 +15,21 @@ } .sidebar { - display: grid; + display: flex; @include S(min-width, 210px); @include S(max-width, 320px); - @include S(grid-gap, 3px); - grid-template-rows: auto auto auto auto auto 1fr; + flex-direction: column; @include StyleBelowWidth($layoutBreak) { - grid-template-rows: 1fr 1fr; - grid-template-columns: auto auto; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + @include S(grid-gap, 5px); max-width: unset !important; } button { text-align: left; + @include S(margin-bottom, 3px); &::after { content: unset; } @@ -37,15 +38,26 @@ @include StyleBelowWidth($layoutBreak) { text-align: center; + height: D(30px) !important; + padding: D(5px) !important; } } .other { - @include S(margin-top, 10px); align-self: end; + margin-top: auto; @include StyleBelowWidth($layoutBreak) { margin-top: 0; + display: grid; + grid-template-columns: 1fr 1fr; + @include S(grid-gap, 5px); + max-width: unset !important; + grid-column: 1 / 3; + + button { + margin: 0 !important; + } } } @@ -69,6 +81,30 @@ } } + button.manageMods { + background-color: lighten($modsColor, 38); + color: $modsColor; + display: flex; + @include S(padding-right, 5px); + .newBadge { + color: #fff; + @include S(border-radius, $globalBorderRadius); + background: $modsColor; + margin-left: auto; + @include S(padding, 0, 3px, 0, 3px); + + @include InlineAnimation(1.3s ease-in-out infinite) { + 50% { + transform: rotate(0deg) scale(1.1); + } + } + } + + &.active { + background-color: $colorGreenBright; + } + } + button.privacy { @include S(margin-top, 4px); } diff --git a/src/css/variables.scss b/src/css/variables.scss index c7b7c17c..fe1fa864 100644 --- a/src/css/variables.scss +++ b/src/css/variables.scss @@ -38,6 +38,7 @@ $colorRedBright: #ef5072; $colorOrangeBright: #ef9d50; $themeColor: #393747; $ingameHudBg: rgba(#333438, 0.9); +$modsColor: rgb(214, 60, 228); $text3dColor: #f4ffff; diff --git a/src/js/application.js b/src/js/application.js index 67e9c353..44207e4d 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -37,6 +37,7 @@ import { LoginState } from "./states/login"; import { WegameSplashState } from "./states/wegame_splash"; import { MODS } from "./mods/modloader"; import { MOD_SIGNALS } from "./mods/mod_signals"; +import { ModsState } from "./states/mods"; /** * @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface @@ -171,6 +172,7 @@ export class Application { ChangelogState, PuzzleMenuState, LoginState, + ModsState, ]; for (let i = 0; i < states.length; ++i) { diff --git a/src/js/mods/demo_mod.nobuild/index.js b/src/js/mods/demo_mod.nobuild/index.js index b636dace..2188defc 100644 --- a/src/js/mods/demo_mod.nobuild/index.js +++ b/src/js/mods/demo_mod.nobuild/index.js @@ -16,11 +16,12 @@ registerMod(shapez => { super( app, { - authorContact: "tobias@tobspr.io", - authorName: "tobspr", + website: "https://tobspr.io", + author: "tobspr", name: "Demo Mod", version: "1", id: "demo-mod", + description: "A simple mod to demonstrate the capatibilities of the mod loader.", }, modLoader ); @@ -28,11 +29,11 @@ registerMod(shapez => { init() { // Add some custom css - this.modInterface.registerCss(` - * { - font-family: "Comic Sans", "Comic Sans MS", Tahoma !important; - } - `); + // this.modInterface.registerCss(` + // * { + // font-family: "Comic Sans", "Comic Sans MS", Tahoma !important; + // } + // `); // Replace a builtin sprite ["red", "green", "blue", "yellow", "purple", "cyan", "white"].forEach(color => { diff --git a/src/js/mods/mod.js b/src/js/mods/mod.js index b9018b68..4e14bfe2 100644 --- a/src/js/mods/mod.js +++ b/src/js/mods/mod.js @@ -12,8 +12,9 @@ export class Mod { * @param {object} metadata * @param {string} metadata.name * @param {string} metadata.version - * @param {string} metadata.authorName - * @param {string} metadata.authorContact + * @param {string} metadata.author + * @param {string} metadata.website + * @param {string} metadata.description * @param {string} metadata.id * * @param {ModLoader} modLoader diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index a6bb940a..91142c3b 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -155,36 +155,22 @@ export class MainMenuState extends GameState { ? `
-

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

- +
+

${T.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 - )} -
+
${mod.metadata.name} @ v${mod.metadata.version}
`; }) .join("")}
- ${T.mainMenu.modsWarningPuzzleDLC} - - + ${T.mainMenu.mods.warningPuzzleDLC}
@@ -381,7 +367,7 @@ export class MainMenuState extends GameState { ".puzzleDlcPlayButton": this.onPuzzleModeButtonClicked, ".puzzleDlcGetButton": this.onPuzzleWishlistButtonClicked, ".wegameDisclaimer > .rating": this.onWegameRatingClicked, - ".modsOpenFolder": this.openModsFolder, + ".editMods": this.onModsClicked, }; for (const key in clickHandling) { @@ -430,6 +416,14 @@ export class MainMenuState extends GameState { this.trackClicks(playBtn, this.onPlayButtonClicked); buttonContainer.appendChild(importButtonElement); } + + // Mods + const modsBtn = makeButton( + this.htmlElement.querySelector(".mainContainer .outer"), + ["modsButton", "styledButton"], + " " + ); + this.trackClicks(modsBtn, this.onModsClicked); } onPuzzleModeButtonClicked(force = false) { @@ -742,6 +736,12 @@ export class MainMenuState extends GameState { ); } + onModsClicked() { + this.moveToState("ModsState", { + backToStateId: "MainMenuState", + }); + } + onContinueButtonClicked() { let latestLastUpdate = 0; let latestInternalId; @@ -763,14 +763,6 @@ 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/src/js/states/mods.js b/src/js/states/mods.js new file mode 100644 index 00000000..d746434d --- /dev/null +++ b/src/js/states/mods.js @@ -0,0 +1,135 @@ +import { THIRDPARTY_URLS } from "../core/config"; +import { TextualGameState } from "../core/textual_game_state"; +import { getIPCRenderer } from "../core/utils"; +import { MODS } from "../mods/modloader"; +import { T } from "../translations"; + +export class ModsState extends TextualGameState { + constructor() { + super("ModsState"); + } + + getStateHeaderTitle() { + return T.mods.title; + } + + internalGetFullHtml() { + let headerHtml = ` +
+

${this.getStateHeaderTitle()}

+ +
+ ${ + G_IS_STANDALONE || G_IS_DEV + ? `` + : "" + } +
+ +
`; + + return ` + ${headerHtml} +
+ ${this.getInnerHTML()} +
+ `; + } + + getMainContentHTML() { + if (!G_IS_STANDALONE && !G_IS_DEV) { + return ` +
+ +

${T.mods.noModSupport}

+ + Get the shapez.io standalone! + + +
+ `; + } + + if (MODS.mods.length === 0) { + return ` + +
+ + ${T.mods.modsInfo} +
+ + `; + } + + let modsHtml = ``; + + MODS.mods.forEach(mod => { + modsHtml += ` +
+
+ ${mod.metadata.name} + ${mod.metadata.description} + Website +
+ ${T.mods.version}${mod.metadata.version} + ${T.mods.author}${mod.metadata.author} +
+ +
+ +
+ `; + }); + return ` + +
+ ${T.mods.modsInfo} +
+ +
+ ${modsHtml} +
+ `; + } + + onEnter() { + const steamLink = this.htmlElement.querySelector(".steamLink"); + if (steamLink) { + this.trackClicks(steamLink, this.onSteamLinkClicked); + } + const openModsFolder = this.htmlElement.querySelector(".openModsFolder"); + if (openModsFolder) { + this.trackClicks(openModsFolder, this.openModsFolder); + } + + const checkboxes = this.htmlElement.querySelectorAll(".checkbox"); + Array.from(checkboxes).forEach(checkbox => { + this.trackClicks(checkbox, this.showModTogglingComingSoon); + }); + } + + showModTogglingComingSoon() { + this.dialogs.showWarning(T.mods.togglingComingSoon.title, T.mods.togglingComingSoon.description); + } + + openModsFolder() { + if (!G_IS_STANDALONE) { + this.dialogs.showWarning(T.global.error, T.mods.folderOnlyStandalone); + return; + } + getIPCRenderer().send("open-mods-folder"); + } + + onSteamLinkClicked() { + this.app.analytics.trackUiClick("mods_steam_link"); + this.app.platformWrapper.openExternalLink( + THIRDPARTY_URLS.stanaloneCampaignLink + "/shapez_modsettings" + ); + + return false; + } + + getDefaultPreviousState() { + return "SettingsState"; + } +} diff --git a/src/js/states/settings.js b/src/js/states/settings.js index 352e0153..b28a4136 100644 --- a/src/js/states/settings.js +++ b/src/js/states/settings.js @@ -19,6 +19,8 @@ export class SettingsState extends TextualGameState {