From 41841651db62a82fd79127dc73818295d349eae2 Mon Sep 17 00:00:00 2001 From: tobspr Date: Tue, 1 Feb 2022 13:59:59 +0100 Subject: [PATCH] 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