1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-15 19:21:49 +00:00

Store mods in savegame and show warning when it differs

This commit is contained in:
tobspr 2022-02-01 13:59:59 +01:00
parent 509c01f642
commit 41841651db
20 changed files with 229 additions and 4 deletions

View File

@ -7,6 +7,10 @@ const METADATA = {
id: "base", id: "base",
description: "The most basic mod", description: "The most basic mod",
minimumGameVersion: ">=1.5.0", 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 { class Mod extends shapez.Mod {

View File

@ -7,6 +7,10 @@ const METADATA = {
id: "custom-css", id: "custom-css",
description: "Shows how to add custom css", description: "Shows how to add custom css",
minimumGameVersion: ">=1.5.0", 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 { class Mod extends shapez.Mod {

View File

@ -7,6 +7,10 @@ const METADATA = {
id: "base", id: "base",
description: "Displays an indicator on every item processing building when its working", description: "Displays an indicator on every item processing building when its working",
minimumGameVersion: ">=1.5.0", 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 { class ItemProcessorStatusGameSystem extends shapez.GameSystem {

View File

@ -7,6 +7,10 @@ const METADATA = {
id: "base", id: "base",
description: "Shows how to add a new keybinding", description: "Shows how to add a new keybinding",
minimumGameVersion: ">=1.5.0", 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 { class Mod extends shapez.Mod {

View File

@ -7,6 +7,10 @@ const METADATA = {
id: "custom-theme", id: "custom-theme",
description: "Shows how to add a custom game theme", description: "Shows how to add a custom game theme",
minimumGameVersion: ">=1.5.0", 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 { class Mod extends shapez.Mod {

View File

@ -7,6 +7,10 @@ const METADATA = {
id: "modify-theme", id: "modify-theme",
description: "Shows how to modify builtin themes", description: "Shows how to modify builtin themes",
minimumGameVersion: ">=1.5.0", 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 { class Mod extends shapez.Mod {

View File

@ -7,6 +7,10 @@ const METADATA = {
id: "modify-ui", id: "modify-ui",
description: "Shows how to modify a builtin game state, in this case the main menu", description: "Shows how to modify a builtin game state, in this case the main menu",
minimumGameVersion: ">=1.5.0", 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 { class Mod extends shapez.Mod {

View File

@ -7,6 +7,10 @@ const METADATA = {
id: "replace-builtin-sprites", id: "replace-builtin-sprites",
description: "Shows how to replace builtin sprites", description: "Shows how to replace builtin sprites",
minimumGameVersion: ">=1.5.0", 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 { class Mod extends shapez.Mod {

View File

@ -7,6 +7,10 @@ const METADATA = {
id: "translations", id: "translations",
description: "Shows how to add and modify translations", description: "Shows how to add and modify translations",
minimumGameVersion: ">=1.5.0", 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 { class Mod extends shapez.Mod {

View File

@ -169,6 +169,10 @@
margin: 1px 0; margin: 1px 0;
} }
h3 {
@include S(margin-top, 10px);
}
input { input {
background: #eee; background: #eee;
color: #333438; 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 { > .buttons {

View File

@ -205,7 +205,7 @@ export class GameSystemManager {
addBefore("end"); addBefore("end");
for (const key in MODS_ADDITIONAL_SYSTEMS) { 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); logger.error("Mod system not attached due to invalid 'before': ", key);
} }
} }

View File

@ -24,7 +24,8 @@ const LOG = createLogger("mods");
* description: string; * description: string;
* id: string; * id: string;
* minimumGameVersion?: string; * minimumGameVersion?: string;
* settings: [] * settings: [];
* doesNotAffectSavegame?: boolean
* }} ModMetadata * }} ModMetadata
*/ */
@ -58,6 +59,51 @@ export class ModLoader {
return this.mods.length > 0; 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() { exposeExports() {
if (G_IS_DEV || G_IS_STANDALONE) { if (G_IS_DEV || G_IS_STANDALONE) {
let exports = {}; let exports = {};

View File

@ -14,6 +14,8 @@ import { SavegameInterface_V1006 } from "./schemas/1006";
import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1007 } from "./schemas/1007";
import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1008 } from "./schemas/1008";
import { SavegameInterface_V1009 } from "./schemas/1009"; import { SavegameInterface_V1009 } from "./schemas/1009";
import { MODS } from "../mods/modloader";
import { SavegameInterface_V1010 } from "./schemas/1010";
const logger = createLogger("savegame"); const logger = createLogger("savegame");
@ -54,7 +56,7 @@ export class Savegame extends ReadWriteProxy {
* @returns {number} * @returns {number}
*/ */
static getCurrentVersion() { static getCurrentVersion() {
return 1009; return 1010;
} }
/** /**
@ -103,6 +105,7 @@ export class Savegame extends ReadWriteProxy {
usedInverseRotater: false, usedInverseRotater: false,
}, },
lastUpdate: Date.now(), lastUpdate: Date.now(),
mods: MODS.getModsListForSavegame(),
}; };
} }
@ -159,6 +162,10 @@ export class Savegame extends ReadWriteProxy {
SavegameInterface_V1009.migrate1008to1009(data); SavegameInterface_V1009.migrate1008to1009(data);
data.version = 1009; data.version = 1009;
} }
if (data.version === 1009) {
SavegameInterface_V1010.migrate1009to1010(data);
data.version = 1010;
}
return ExplainedResult.good(); return ExplainedResult.good();
} }
@ -269,6 +276,7 @@ export class Savegame extends ReadWriteProxy {
shadowData.dump = dump; shadowData.dump = dump;
shadowData.lastUpdate = new Date().getTime(); shadowData.lastUpdate = new Date().getTime();
shadowData.version = this.getCurrentVersion(); shadowData.version = this.getCurrentVersion();
shadowData.mods = MODS.getModsListForSavegame();
const reader = this.getDumpReaderForExternalData(shadowData); const reader = this.getDumpReaderForExternalData(shadowData);

View File

@ -10,6 +10,7 @@ import { SavegameInterface_V1006 } from "./schemas/1006";
import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1007 } from "./schemas/1007";
import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1008 } from "./schemas/1008";
import { SavegameInterface_V1009 } from "./schemas/1009"; import { SavegameInterface_V1009 } from "./schemas/1009";
import { SavegameInterface_V1010 } from "./schemas/1010";
/** @type {Object.<number, typeof BaseSavegameInterface>} */ /** @type {Object.<number, typeof BaseSavegameInterface>} */
export const savegameInterfaces = { export const savegameInterfaces = {
@ -23,6 +24,7 @@ export const savegameInterfaces = {
1007: SavegameInterface_V1007, 1007: SavegameInterface_V1007,
1008: SavegameInterface_V1008, 1008: SavegameInterface_V1008,
1009: SavegameInterface_V1009, 1009: SavegameInterface_V1009,
1010: SavegameInterface_V1010,
}; };
const logger = createLogger("savegame_interface_registry"); const logger = createLogger("savegame_interface_registry");

View File

@ -2,6 +2,14 @@
* @typedef {import("../game/entity").Entity} Entity * @typedef {import("../game/entity").Entity} Entity
* *
* @typedef {{ * @typedef {{
* id: string;
* version: string;
* website: string;
* name: string;
* author: string;
* }[]} SavegameStoredMods
*
* @typedef {{
* failedMam: boolean, * failedMam: boolean,
* trashedCount: number, * trashedCount: number,
* usedInverseRotater: boolean * usedInverseRotater: boolean
@ -25,6 +33,7 @@
* dump: SerializedGame, * dump: SerializedGame,
* stats: SavegameStats, * stats: SavegameStats,
* lastUpdate: number, * lastUpdate: number,
* mods: SavegameStoredMods
* }} SavegameData * }} SavegameData
* *
* @typedef {{ * @typedef {{

View File

@ -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 = [];
}
}

View File

@ -0,0 +1,5 @@
{
"type": "object",
"required": [],
"additionalProperties": true
}

View File

@ -20,6 +20,7 @@ import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { MODS } from "../mods/modloader"; import { MODS } from "../mods/modloader";
import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
import { PlatformWrapperImplElectron } from "../platform/electron/wrapper"; import { PlatformWrapperImplElectron } from "../platform/electron/wrapper";
import { Savegame } from "../savegame/savegame";
import { T } from "../translations"; import { T } from "../translations";
const trim = require("trim"); const trim = require("trim");
@ -615,11 +616,13 @@ export class MainMenuState extends GameState {
const savegame = this.app.savegameMgr.getSavegameById(game.internalId); const savegame = this.app.savegameMgr.getSavegameById(game.internalId);
savegame savegame
.readAsync() .readAsync()
.then(() => this.checkForModDifferences(savegame))
.then(() => { .then(() => {
this.moveToState("InGameState", { this.moveToState("InGameState", {
savegame, savegame,
}); });
}) })
.catch(err => { .catch(err => {
this.dialogs.showWarning( this.dialogs.showWarning(
T.dialogs.gameLoadFailure.title, 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 `
<div class="dialogModsMod">
<div class="name">${mod.name}</div>
<div class="version">${T.mods.version} ${mod.version}</div>
<button class="website styledButton" onclick="window.open('${mod.website.replace(
/"'/,
""
)}')">${T.mods.modWebsite}
</button>
</div>
`;
}
if (difference.missing.length > 0) {
dialogHtml += "<h3>" + T.dialogs.modsDifference.missingMods + "</h3>";
dialogHtml += difference.missing.map(formatMod).join("<br>");
}
if (difference.extra.length > 0) {
dialogHtml += "<h3>" + T.dialogs.modsDifference.newMods + "</h3>";
dialogHtml += difference.extra.map(formatMod).join("<br>");
}
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 * @param {SavegameMetadata} game
*/ */
@ -754,6 +808,7 @@ export class MainMenuState extends GameState {
savegame savegame
.readAsync() .readAsync()
.then(() => this.app.adProvider.showVideoAd()) .then(() => this.app.adProvider.showVideoAd())
.then(() => this.checkForModDifferences(savegame))
.then(() => { .then(() => {
this.moveToState("InGameState", { this.moveToState("InGameState", {
savegame, savegame,

View File

@ -74,7 +74,7 @@ export class ModsState extends TextualGameState {
<div class="mainInfo"> <div class="mainInfo">
<span class="name">${mod.metadata.name}</span> <span class="name">${mod.metadata.name}</span>
<span class="description">${mod.metadata.description}</span> <span class="description">${mod.metadata.description}</span>
<a class="website" href="${mod.metadata.website}" target="_blank">Website</a> <a class="website" href="${mod.metadata.website}" target="_blank">${T.mods.modWebsite}</a>
</div> </div>
<span class="version"><strong>${T.mods.version}</strong>${mod.metadata.version}</span> <span class="version"><strong>${T.mods.version}</strong>${mod.metadata.version}</span>
<span class="author"><strong>${T.mods.author}</strong>${mod.metadata.author}</span> <span class="author"><strong>${T.mods.author}</strong>${mod.metadata.author}</span>

View File

@ -423,6 +423,14 @@ dialogs:
desc: >- desc: >-
Are you sure you want to delete '<title>'? This can not be undone! Are you sure you want to delete '<title>'? 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: ingame:
# This is shown in the top left corner and displays useful keybindings in # This is shown in the top left corner and displays useful keybindings in
# every situation # every situation
@ -1096,6 +1104,7 @@ mods:
author: Author author: Author
version: Version version: Version
modWebsite: Website
openFolder: Open Mods Folder openFolder: Open Mods Folder
folderOnlyStandalone: Opening the mod folder is only possible when running the standalone. folderOnlyStandalone: Opening the mod folder is only possible when running the standalone.
browseMods: Browse Mods browseMods: Browse Mods