From 41841651db62a82fd79127dc73818295d349eae2 Mon Sep 17 00:00:00 2001 From: tobspr Date: Tue, 1 Feb 2022 13:59:59 +0100 Subject: [PATCH 1/6] Store mods in savegame and show warning when it differs --- mod_examples/base.js | 4 ++ mod_examples/custom_css.js | 4 ++ mod_examples/custom_drawing.js | 4 ++ mod_examples/custom_keybinding.js | 4 ++ mod_examples/custom_theme.js | 4 ++ mod_examples/modify_theme.js | 4 ++ mod_examples/modify_ui.js | 4 ++ mod_examples/replace_builtin_sprites.js | 4 ++ mod_examples/translations.js | 4 ++ src/css/ingame_hud/dialogs.scss | 31 +++++++++++ src/js/game/game_system_manager.js | 2 +- src/js/mods/modloader.js | 48 +++++++++++++++- src/js/savegame/savegame.js | 10 +++- .../savegame/savegame_interface_registry.js | 2 + src/js/savegame/savegame_typedefs.js | 9 +++ src/js/savegame/schemas/1010.js | 24 ++++++++ src/js/savegame/schemas/1010.json | 5 ++ src/js/states/main_menu.js | 55 +++++++++++++++++++ src/js/states/mods.js | 2 +- translations/base-en.yaml | 9 +++ 20 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 src/js/savegame/schemas/1010.js create mode 100644 src/js/savegame/schemas/1010.json diff --git a/mod_examples/base.js b/mod_examples/base.js index 09e12f61..f76ded9b 100644 --- a/mod_examples/base.js +++ b/mod_examples/base.js @@ -7,6 +7,10 @@ const METADATA = { id: "base", description: "The most basic mod", minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, }; class Mod extends shapez.Mod { diff --git a/mod_examples/custom_css.js b/mod_examples/custom_css.js index dce316c5..0d28fda7 100644 --- a/mod_examples/custom_css.js +++ b/mod_examples/custom_css.js @@ -7,6 +7,10 @@ const METADATA = { id: "custom-css", description: "Shows how to add custom css", minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, }; class Mod extends shapez.Mod { diff --git a/mod_examples/custom_drawing.js b/mod_examples/custom_drawing.js index e1c25b30..2dccab2d 100644 --- a/mod_examples/custom_drawing.js +++ b/mod_examples/custom_drawing.js @@ -7,6 +7,10 @@ const METADATA = { id: "base", description: "Displays an indicator on every item processing building when its working", minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, }; class ItemProcessorStatusGameSystem extends shapez.GameSystem { diff --git a/mod_examples/custom_keybinding.js b/mod_examples/custom_keybinding.js index 0a6b11fc..0109833c 100644 --- a/mod_examples/custom_keybinding.js +++ b/mod_examples/custom_keybinding.js @@ -7,6 +7,10 @@ const METADATA = { id: "base", description: "Shows how to add a new keybinding", minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, }; class Mod extends shapez.Mod { diff --git a/mod_examples/custom_theme.js b/mod_examples/custom_theme.js index cc4c9de8..a70d949f 100644 --- a/mod_examples/custom_theme.js +++ b/mod_examples/custom_theme.js @@ -7,6 +7,10 @@ const METADATA = { id: "custom-theme", description: "Shows how to add a custom game theme", minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, }; class Mod extends shapez.Mod { diff --git a/mod_examples/modify_theme.js b/mod_examples/modify_theme.js index 4bc9be34..de7f0ad2 100644 --- a/mod_examples/modify_theme.js +++ b/mod_examples/modify_theme.js @@ -7,6 +7,10 @@ const METADATA = { id: "modify-theme", description: "Shows how to modify builtin themes", minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, }; class Mod extends shapez.Mod { diff --git a/mod_examples/modify_ui.js b/mod_examples/modify_ui.js index 0b2d1341..4beb403d 100644 --- a/mod_examples/modify_ui.js +++ b/mod_examples/modify_ui.js @@ -7,6 +7,10 @@ const METADATA = { id: "modify-ui", description: "Shows how to modify a builtin game state, in this case the main menu", minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, }; class Mod extends shapez.Mod { diff --git a/mod_examples/replace_builtin_sprites.js b/mod_examples/replace_builtin_sprites.js index 885846e7..6e88b7fb 100644 --- a/mod_examples/replace_builtin_sprites.js +++ b/mod_examples/replace_builtin_sprites.js @@ -7,6 +7,10 @@ const METADATA = { id: "replace-builtin-sprites", description: "Shows how to replace builtin sprites", minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, }; class Mod extends shapez.Mod { diff --git a/mod_examples/translations.js b/mod_examples/translations.js index 2f3a4015..6b9c708e 100644 --- a/mod_examples/translations.js +++ b/mod_examples/translations.js @@ -7,6 +7,10 @@ const METADATA = { id: "translations", description: "Shows how to add and modify translations", minimumGameVersion: ">=1.5.0", + + // You can specify this parameter if savegames will still work + // after your mod has been uninstalled + doesNotAffectSavegame: true, }; class Mod extends shapez.Mod { diff --git a/src/css/ingame_hud/dialogs.scss b/src/css/ingame_hud/dialogs.scss index cc742d42..6a93ceb7 100644 --- a/src/css/ingame_hud/dialogs.scss +++ b/src/css/ingame_hud/dialogs.scss @@ -169,6 +169,10 @@ margin: 1px 0; } + h3 { + @include S(margin-top, 10px); + } + input { background: #eee; color: #333438; @@ -214,6 +218,33 @@ } } } + + .dialogModsMod { + background: rgba(0, 0, 0, 0.05); + @include S(padding, 5px); + @include S(margin, 10px, 0); + @include S(border-radius, $globalBorderRadius); + display: grid; + grid-template-columns: 1fr D(100px); + + @include DarkThemeOverride { + background: rgba(0, 0, 0, 0.2); + } + + button { + grid-column: 2 / 3; + grid-row: 1 / 3; + align-self: start; + } + + .version { + @include SuperSmallText; + opacity: 0.5; + } + + .name { + } + } } > .buttons { diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index f2d44e52..a799b42a 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -205,7 +205,7 @@ export class GameSystemManager { addBefore("end"); for (const key in MODS_ADDITIONAL_SYSTEMS) { - if (!this.systems[key]) { + if (!this.systems[key] && key !== "end") { logger.error("Mod system not attached due to invalid 'before': ", key); } } diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 48911abd..f91a7b25 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -24,7 +24,8 @@ const LOG = createLogger("mods"); * description: string; * id: string; * minimumGameVersion?: string; - * settings: [] + * settings: []; + * doesNotAffectSavegame?: boolean * }} ModMetadata */ @@ -58,6 +59,51 @@ export class ModLoader { return this.mods.length > 0; } + /** + * + * @returns {import("../savegame/savegame_typedefs").SavegameStoredMods} + */ + getModsListForSavegame() { + return this.mods + .filter(mod => !mod.metadata.doesNotAffectSavegame) + .map(mod => ({ + id: mod.metadata.id, + version: mod.metadata.version, + website: mod.metadata.website, + name: mod.metadata.name, + author: mod.metadata.author, + })); + } + + /** + * + * @param {import("../savegame/savegame_typedefs").SavegameStoredMods} originalMods + */ + computeModDifference(originalMods) { + /** + * @type {import("../savegame/savegame_typedefs").SavegameStoredMods} + */ + let missing = []; + + const current = this.getModsListForSavegame(); + + originalMods.forEach(mod => { + for (let i = 0; i < current.length; ++i) { + const currentMod = current[i]; + if (currentMod.id === mod.id && currentMod.version === mod.version) { + current.splice(i, 1); + return; + } + } + missing.push(mod); + }); + + return { + missing, + extra: current, + }; + } + exposeExports() { if (G_IS_DEV || G_IS_STANDALONE) { let exports = {}; diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index 36ed884f..a2f27d0f 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -14,6 +14,8 @@ import { SavegameInterface_V1006 } from "./schemas/1006"; import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1009 } from "./schemas/1009"; +import { MODS } from "../mods/modloader"; +import { SavegameInterface_V1010 } from "./schemas/1010"; const logger = createLogger("savegame"); @@ -54,7 +56,7 @@ export class Savegame extends ReadWriteProxy { * @returns {number} */ static getCurrentVersion() { - return 1009; + return 1010; } /** @@ -103,6 +105,7 @@ export class Savegame extends ReadWriteProxy { usedInverseRotater: false, }, lastUpdate: Date.now(), + mods: MODS.getModsListForSavegame(), }; } @@ -159,6 +162,10 @@ export class Savegame extends ReadWriteProxy { SavegameInterface_V1009.migrate1008to1009(data); data.version = 1009; } + if (data.version === 1009) { + SavegameInterface_V1010.migrate1009to1010(data); + data.version = 1010; + } return ExplainedResult.good(); } @@ -269,6 +276,7 @@ export class Savegame extends ReadWriteProxy { shadowData.dump = dump; shadowData.lastUpdate = new Date().getTime(); shadowData.version = this.getCurrentVersion(); + shadowData.mods = MODS.getModsListForSavegame(); const reader = this.getDumpReaderForExternalData(shadowData); diff --git a/src/js/savegame/savegame_interface_registry.js b/src/js/savegame/savegame_interface_registry.js index b4dc4233..089b15fc 100644 --- a/src/js/savegame/savegame_interface_registry.js +++ b/src/js/savegame/savegame_interface_registry.js @@ -10,6 +10,7 @@ import { SavegameInterface_V1006 } from "./schemas/1006"; import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1009 } from "./schemas/1009"; +import { SavegameInterface_V1010 } from "./schemas/1010"; /** @type {Object.} */ export const savegameInterfaces = { @@ -23,6 +24,7 @@ export const savegameInterfaces = { 1007: SavegameInterface_V1007, 1008: SavegameInterface_V1008, 1009: SavegameInterface_V1009, + 1010: SavegameInterface_V1010, }; const logger = createLogger("savegame_interface_registry"); diff --git a/src/js/savegame/savegame_typedefs.js b/src/js/savegame/savegame_typedefs.js index c5e0e5c5..c72d83e6 100644 --- a/src/js/savegame/savegame_typedefs.js +++ b/src/js/savegame/savegame_typedefs.js @@ -2,6 +2,14 @@ * @typedef {import("../game/entity").Entity} Entity * * @typedef {{ + * id: string; + * version: string; + * website: string; + * name: string; + * author: string; + * }[]} SavegameStoredMods + * + * @typedef {{ * failedMam: boolean, * trashedCount: number, * usedInverseRotater: boolean @@ -25,6 +33,7 @@ * dump: SerializedGame, * stats: SavegameStats, * lastUpdate: number, + * mods: SavegameStoredMods * }} SavegameData * * @typedef {{ diff --git a/src/js/savegame/schemas/1010.js b/src/js/savegame/schemas/1010.js new file mode 100644 index 00000000..5453f3e2 --- /dev/null +++ b/src/js/savegame/schemas/1010.js @@ -0,0 +1,24 @@ +import { createLogger } from "../../core/logging.js"; +import { SavegameInterface_V1009 } from "./1009.js"; + +const schema = require("./1010.json"); +const logger = createLogger("savegame_interface/1010"); + +export class SavegameInterface_V1010 extends SavegameInterface_V1009 { + getVersion() { + return 1010; + } + + getSchemaUncached() { + return schema; + } + + /** + * @param {import("../savegame_typedefs.js").SavegameData} data + */ + static migrate1009to1010(data) { + logger.log("Migrating 1009 to 1010"); + + data.mods = []; + } +} diff --git a/src/js/savegame/schemas/1010.json b/src/js/savegame/schemas/1010.json new file mode 100644 index 00000000..6682f615 --- /dev/null +++ b/src/js/savegame/schemas/1010.json @@ -0,0 +1,5 @@ +{ + "type": "object", + "required": [], + "additionalProperties": true +} diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 4d62f2ce..10e280b9 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -20,6 +20,7 @@ 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 { Savegame } from "../savegame/savegame"; import { T } from "../translations"; const trim = require("trim"); @@ -615,11 +616,13 @@ export class MainMenuState extends GameState { const savegame = this.app.savegameMgr.getSavegameById(game.internalId); savegame .readAsync() + .then(() => this.checkForModDifferences(savegame)) .then(() => { this.moveToState("InGameState", { savegame, }); }) + .catch(err => { this.dialogs.showWarning( T.dialogs.gameLoadFailure.title, @@ -629,6 +632,57 @@ export class MainMenuState extends GameState { }); } + /** + * @param {Savegame} savegame + */ + checkForModDifferences(savegame) { + const difference = MODS.computeModDifference(savegame.currentData.mods); + + if (difference.missing.length === 0 && difference.extra.length === 0) { + return Promise.resolve(); + } + + let dialogHtml = T.dialogs.modsDifference.desc; + + /** + * + * @param {import("../savegame/savegame_typedefs").SavegameStoredMods[0]} mod + */ + function formatMod(mod) { + return ` +
+
${mod.name}
+
${T.mods.version} ${mod.version}
+ + +
+ `; + } + + if (difference.missing.length > 0) { + dialogHtml += "

" + T.dialogs.modsDifference.missingMods + "

"; + dialogHtml += difference.missing.map(formatMod).join("
"); + } + + if (difference.extra.length > 0) { + dialogHtml += "

" + T.dialogs.modsDifference.newMods + "

"; + dialogHtml += difference.extra.map(formatMod).join("
"); + } + + const signals = this.dialogs.showWarning(T.dialogs.modsDifference.title, dialogHtml, [ + "cancel:good", + "continue:bad", + ]); + + return new Promise(resolve => { + signals.continue.add(resolve); + }); + } + /** * @param {SavegameMetadata} game */ @@ -754,6 +808,7 @@ export class MainMenuState extends GameState { savegame .readAsync() .then(() => this.app.adProvider.showVideoAd()) + .then(() => this.checkForModDifferences(savegame)) .then(() => { this.moveToState("InGameState", { savegame, diff --git a/src/js/states/mods.js b/src/js/states/mods.js index 42add8bd..1e0fe5f1 100644 --- a/src/js/states/mods.js +++ b/src/js/states/mods.js @@ -74,7 +74,7 @@ export class ModsState extends TextualGameState {
${mod.metadata.name} ${mod.metadata.description} - Website + ${T.mods.modWebsite}
${T.mods.version}${mod.metadata.version} ${T.mods.author}${mod.metadata.author} diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 7070c318..0fa240be 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -423,6 +423,14 @@ dialogs: desc: >- Are you sure you want to delete ''? This can not be undone! + modsDifference: + title: Mod Warning + desc: >- + The currently installed mods differ from the mods the savegame was created with. + This might cause the savegame to break or not load at all. Are you sure you want to continue? + missingMods: Missing Mods + newMods: Newly installed Mods + ingame: # This is shown in the top left corner and displays useful keybindings in # every situation @@ -1096,6 +1104,7 @@ mods: author: Author version: Version + modWebsite: Website openFolder: Open Mods Folder folderOnlyStandalone: Opening the mod folder is only possible when running the standalone. browseMods: Browse Mods From 7dbed9fc43453df761842516749d423be941e41a Mon Sep 17 00:00:00 2001 From: tobspr <tobias.springer1@googlemail.com> Date: Tue, 1 Feb 2022 14:02:27 +0100 Subject: [PATCH 2/6] Closes #1357 --- mod_examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod_examples/README.md b/mod_examples/README.md index e38d5834..6ea04c38 100644 --- a/mod_examples/README.md +++ b/mod_examples/README.md @@ -11,7 +11,7 @@ Since the `create-shapezio-mod` package is still in development, the current rec ## Mod Developer Discord -A great place to get help with mod development is the official [shapez.io modloader discord]https://discord.gg/xq5v8uyMue). +A great place to get help with mod development is the official [shapez.io modloader discord](https://discord.gg/xq5v8uyMue). ## Setting up your development environment From 321971daddada3e8e586b40b52c9ed516a60e31b Mon Sep 17 00:00:00 2001 From: Bagel03 <70449196+Bagel03@users.noreply.github.com> Date: Tue, 1 Feb 2022 08:05:58 -0500 Subject: [PATCH 3/6] Fix All Shapez Exports Being Const (#1358) * Allowed setting of variables inside webpack modules * remove console log * Fix stringification of things inside of eval Co-authored-by: Edward Badel <you@example.com> --- gulp/mod.js | 3 +++ gulp/webpack.config.js | 3 +++ src/js/mods/modloader.js | 6 ++++-- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 gulp/mod.js diff --git a/gulp/mod.js b/gulp/mod.js new file mode 100644 index 00000000..87912bb3 --- /dev/null +++ b/gulp/mod.js @@ -0,0 +1,3 @@ +module.exports = function (source, map) { + return source + `\nexport let $s=(n,v)=>eval(n+"=v")`; +}; diff --git a/gulp/webpack.config.js b/gulp/webpack.config.js index 7db0bf0b..3c07d7cc 100644 --- a/gulp/webpack.config.js +++ b/gulp/webpack.config.js @@ -93,6 +93,9 @@ module.exports = ({ watch = false, standalone = false, chineseVersion = false, w end: "typehints:end", }, }, + { + loader: path.resolve(__dirname, "mod.js"), + }, ], }, { diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index f91a7b25..a1658abc 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -112,18 +112,20 @@ export class ModLoader { // @ts-ignore const module = modules(key); for (const member in module) { - if (member === "default") { + if (member === "default" || member === "$s") { + // Setter continue; } if (exports[member]) { throw new Error("Duplicate export of " + member); } + Object.defineProperty(exports, member, { get() { return module[member]; }, set(v) { - module[member] = v; + module["$s"](member, v); }, }); } From 20dca5bcc10cf0e0b388c11653b73b4d42ae0e97 Mon Sep 17 00:00:00 2001 From: tobspr <tobias.springer1@googlemail.com> Date: Tue, 1 Feb 2022 14:21:46 +0100 Subject: [PATCH 4/6] Fix building placer intersection warning --- src/js/game/hud/parts/building_placer.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index ca9c128b..d2904720 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -401,9 +401,10 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { * Checks if there are any entities in the way, returns true if there are * @param {Vector} from * @param {Vector} to + * @param {Vector[]=} ignorePositions * @returns */ - checkForObstales(from, to) { + checkForObstales(from, to, ignorePositions = []) { assert(from.x === to.x || from.y === to.y, "Must be a straight line"); const prop = from.x === to.x ? "y" : "x"; @@ -426,6 +427,9 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { for (let i = start; i <= end; i++) { current[prop] = i; + if (ignorePositions.some(p => p.distanceSquare(current) < 0.1)) { + continue; + } if (!this.root.logic.checkCanPlaceEntity(this.fakeEntity, { allowReplaceBuildings: false })) { return true; } @@ -464,8 +468,11 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { const endLine = mouseTile.toWorldSpaceCenterOfTile(); const midLine = this.currentDirectionLockCorner.toWorldSpaceCenterOfTile(); const anyObstacle = - this.checkForObstales(this.lastDragTile, this.currentDirectionLockCorner) || - this.checkForObstales(this.currentDirectionLockCorner, mouseTile); + this.checkForObstales(this.lastDragTile, this.currentDirectionLockCorner, [ + this.lastDragTile, + mouseTile, + ]) || + this.checkForObstales(this.currentDirectionLockCorner, mouseTile, [this.lastDragTile, mouseTile]); if (anyObstacle) { applyStyles("error"); From bf7e1887a80764413c2e5ad89542ba223c30914c Mon Sep 17 00:00:00 2001 From: tobspr <tobias.springer1@googlemail.com> Date: Tue, 1 Feb 2022 14:37:53 +0100 Subject: [PATCH 5/6] Add example for storing data in the savegame --- mod_examples/README.md | 1 + mod_examples/storing_data_in_savegame.js | 78 ++++++++++++++++++++++++ src/js/mods/mod_signals.js | 3 + src/js/savegame/savegame.js | 1 + src/js/savegame/savegame_serializer.js | 12 +++- src/js/savegame/savegame_typedefs.js | 3 +- src/js/savegame/schemas/1010.js | 4 ++ 7 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 mod_examples/storing_data_in_savegame.js diff --git a/mod_examples/README.md b/mod_examples/README.md index 6ea04c38..579751f9 100644 --- a/mod_examples/README.md +++ b/mod_examples/README.md @@ -38,6 +38,7 @@ To get into shapez.io modding, I highly recommend checking out all of the exampl | [modify_theme.js](modify_theme.js) | Modifies the default game themes | Modifying the builtin themes | | [custom_theme.js](custom_theme.js) | Adds a new UI and map theme | Adding a new game theme | | [mod_settings.js](mod_settings.js) | Shows a dialog counting how often the mod has been launched | Reading and storing mod settings | +| [storing_data_in_savegame.js](storing_data_in_savegame.js) | Shows how to store custom (structured) data in the savegame | Storing custom data in savegame | | [modify_existing_building.js](modify_existing_building.js) | Makes the rotator building always unlocked and adds a new statistic to the building panel | Modifying a builtin building, replacing builtin methods | | [modify_ui.js](modify_ui.js) | Shows how to add custom IU elements to builtin game states (the Main Menu in this case) | Extending builtin UI states, Adding CSS | | [pasting.js](pasting.js) | Shows a dialog when pasting text in the game | Listening to paste events | diff --git a/mod_examples/storing_data_in_savegame.js b/mod_examples/storing_data_in_savegame.js new file mode 100644 index 00000000..92f7733b --- /dev/null +++ b/mod_examples/storing_data_in_savegame.js @@ -0,0 +1,78 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Storing Data in Savegame", + version: "1", + id: "storing-savegame-data", + description: "Shows how to add custom data to a savegame", + minimumGameVersion: ">=1.5.0", +}; + +class Mod extends shapez.Mod { + init() { + //////////////////////////////////////////////////////////////////// + // Option 1: For simple data + this.signals.gameSerialized.add((root, data) => { + data.modExtraData["storing-savegame-data"] = Math.random(); + }); + + this.signals.gameDeserialized.add((root, data) => { + alert("The value stored in the savegame was: " + data.modExtraData["storing-savegame-data"]); + }); + + //////////////////////////////////////////////////////////////////// + // Option 2: If you need a structured way of storing data + + class SomeSerializableObject extends shapez.BasicSerializableObject { + static getId() { + return "SomeSerializableObject"; + } + + static getSchema() { + return { + someInt: shapez.types.int, + someString: shapez.types.string, + someVector: shapez.types.vector, + + // this value is allowed to be null + nullableInt: shapez.types.nullable(shapez.types.int), + + // There is a lot more .. be sure to checkout src/js/savegame/serialization.js + // You can have maps, classes, arrays etc.. + // And if you need something specific you can always ask in the modding discord. + }; + } + + constructor() { + super(); + this.someInt = 42; + this.someString = "Hello World"; + this.someVector = new shapez.Vector(1, 2); + + this.nullableInt = null; + } + } + + // Store our object in the global game root + this.signals.gameInitialized.add(root => { + root.myObject = new SomeSerializableObject(); + }); + + // Save it within the savegame + this.signals.gameSerialized.add((root, data) => { + data.modExtraData["storing-savegame-data-2"] = root.myObject.serialize(); + }); + + // Restore it when the savegame is loaded + this.signals.gameDeserialized.add((root, data) => { + const errorText = root.myObject.deserialize(data.modExtraData["storing-savegame-data-2"]); + if (errorText) { + alert("Mod failed to deserialize from savegame: " + errorText); + } + alert("The other value stored in the savegame (option 2) was " + root.myObject.someInt); + }); + + //////////////////////////////////////////////////////////////////// + } +} diff --git a/src/js/mods/mod_signals.js b/src/js/mods/mod_signals.js index c311af29..a534dd89 100644 --- a/src/js/mods/mod_signals.js +++ b/src/js/mods/mod_signals.js @@ -27,4 +27,7 @@ export const MOD_SIGNALS = { gameStarted: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()), stateEntered: /** @type {TypedSignal<[GameState]>} */ (new Signal()), + + gameSerialized: /** @type {TypedSignal<[GameRoot, import("../savegame/savegame_typedefs").SerializedGame]>} */ (new Signal()), + gameDeserialized: /** @type {TypedSignal<[GameRoot, import("../savegame/savegame_typedefs").SerializedGame]>} */ (new Signal()), }; diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index a2f27d0f..b4472b2b 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -162,6 +162,7 @@ export class Savegame extends ReadWriteProxy { SavegameInterface_V1009.migrate1008to1009(data); data.version = 1009; } + if (data.version === 1009) { SavegameInterface_V1010.migrate1009to1010(data); data.version = 1010; diff --git a/src/js/savegame/savegame_serializer.js b/src/js/savegame/savegame_serializer.js index 3230cdd5..f95c9896 100644 --- a/src/js/savegame/savegame_serializer.js +++ b/src/js/savegame/savegame_serializer.js @@ -1,9 +1,8 @@ import { ExplainedResult } from "../core/explained_result"; -import { createLogger } from "../core/logging"; import { gComponentRegistry } from "../core/global_registries"; +import { createLogger } from "../core/logging"; +import { MOD_SIGNALS } from "../mods/mod_signals"; import { SerializerInternal } from "./serializer_internal"; -import { HUDPinnedShapes } from "../game/hud/parts/pinned_shapes"; -import { HUDWaypoints } from "../game/hud/parts/waypoints"; /** * @typedef {import("../game/component").Component} Component @@ -42,8 +41,12 @@ export class SavegameSerializer { beltPaths: root.systemMgr.systems.belt.serializePaths(), pinnedShapes: root.hud.parts.pinnedShapes ? root.hud.parts.pinnedShapes.serialize() : null, waypoints: root.hud.parts.waypoints ? root.hud.parts.waypoints.serialize() : null, + + modExtraData: {}, }; + MOD_SIGNALS.gameSerialized.dispatch(root, data); + if (G_IS_DEV) { if (sanityChecks) { // Sanity check @@ -151,6 +154,9 @@ export class SavegameSerializer { return ExplainedResult.bad(errorReason); } + // Mods + MOD_SIGNALS.gameDeserialized.dispatch(root, savegame); + return ExplainedResult.good(); } } diff --git a/src/js/savegame/savegame_typedefs.js b/src/js/savegame/savegame_typedefs.js index c72d83e6..b1980115 100644 --- a/src/js/savegame/savegame_typedefs.js +++ b/src/js/savegame/savegame_typedefs.js @@ -25,7 +25,8 @@ * pinnedShapes: any, * waypoints: any, * entities: Array<Entity>, - * beltPaths: Array<any> + * beltPaths: Array<any>, + * modExtraData: Object * }} SerializedGame * * @typedef {{ diff --git a/src/js/savegame/schemas/1010.js b/src/js/savegame/schemas/1010.js index 5453f3e2..8f480800 100644 --- a/src/js/savegame/schemas/1010.js +++ b/src/js/savegame/schemas/1010.js @@ -20,5 +20,9 @@ export class SavegameInterface_V1010 extends SavegameInterface_V1009 { logger.log("Migrating 1009 to 1010"); data.mods = []; + + if (data.dump) { + data.dump.modExtraData = {}; + } } } From d44191092a5d3f97dda7de0f0b71400b9e6edd3a Mon Sep 17 00:00:00 2001 From: Sense101 <67970865+Sense101@users.noreply.github.com> Date: Tue, 1 Feb 2022 13:50:07 +0000 Subject: [PATCH 6/6] Fix double painter bug (#1349) --- src/js/game/components/item_processor.js | 5 ++ src/js/game/systems/item_processor.js | 77 ++++++++++++------------ 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index f7dddec1..be7d1ce4 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -109,6 +109,11 @@ export class ItemProcessorComponent extends Component { * @type {number} */ this.bonusTime = 0; + + /** + * @type {Array<EjectorItemToEject>} + */ + this.queuedEjects = []; } /** diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 36c837fb..6e1032c9 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -94,49 +94,16 @@ export class ItemProcessorSystem extends GameSystemWithFilter { } } - // Check if it finished - if (currentCharge.remainingTime <= 0.0) { + // Check if it finished and we don't already have queued ejects + if (currentCharge.remainingTime <= 0.0 && !processorComp.queuedEjects.length) { const itemsToEject = currentCharge.items; - // Go over all items and try to eject them + // Go over all items and add them to the queue for (let j = 0; j < itemsToEject.length; ++j) { - const { item, requiredSlot, preferredSlot } = itemsToEject[j]; - - assert(ejectorComp, "To eject items, the building needs to have an ejector"); - - let slot = null; - if (requiredSlot !== null && requiredSlot !== undefined) { - // We have a slot override, check if that is free - if (ejectorComp.canEjectOnSlot(requiredSlot)) { - slot = requiredSlot; - } - } else if (preferredSlot !== null && preferredSlot !== undefined) { - // We have a slot preference, try using it but otherwise use a free slot - if (ejectorComp.canEjectOnSlot(preferredSlot)) { - slot = preferredSlot; - } else { - slot = ejectorComp.getFirstFreeSlot(); - } - } else { - // We can eject on any slot - slot = ejectorComp.getFirstFreeSlot(); - } - - if (slot !== null) { - // Alright, we can actually eject - if (!ejectorComp.tryEject(slot, item)) { - assert(false, "Failed to eject"); - } else { - itemsToEject.splice(j, 1); - j -= 1; - } - } + processorComp.queuedEjects.push(itemsToEject[j]); } - // If the charge was entirely emptied to the outputs, start the next charge - if (itemsToEject.length === 0) { - processorComp.ongoingCharges.shift(); - } + processorComp.ongoingCharges.shift(); } } @@ -146,6 +113,40 @@ export class ItemProcessorSystem extends GameSystemWithFilter { this.startNewCharge(entity); } } + + for (let j = 0; j < processorComp.queuedEjects.length; ++j) { + const { item, requiredSlot, preferredSlot } = processorComp.queuedEjects[j]; + + assert(ejectorComp, "To eject items, the building needs to have an ejector"); + + let slot = null; + if (requiredSlot !== null && requiredSlot !== undefined) { + // We have a slot override, check if that is free + if (ejectorComp.canEjectOnSlot(requiredSlot)) { + slot = requiredSlot; + } + } else if (preferredSlot !== null && preferredSlot !== undefined) { + // We have a slot preference, try using it but otherwise use a free slot + if (ejectorComp.canEjectOnSlot(preferredSlot)) { + slot = preferredSlot; + } else { + slot = ejectorComp.getFirstFreeSlot(); + } + } else { + // We can eject on any slot + slot = ejectorComp.getFirstFreeSlot(); + } + + if (slot !== null) { + // Alright, we can actually eject + if (!ejectorComp.tryEject(slot, item)) { + assert(false, "Failed to eject"); + } else { + processorComp.queuedEjects.splice(j, 1); + j -= 1; + } + } + } } }