1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-13 02:01:51 +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",
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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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);
}
}

View File

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

View File

@ -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);

View File

@ -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.<number, typeof BaseSavegameInterface>} */
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");

View File

@ -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 {{

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 { 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 `
<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
*/
@ -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,

View File

@ -74,7 +74,7 @@ export class ModsState extends TextualGameState {
<div class="mainInfo">
<span class="name">${mod.metadata.name}</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>
<span class="version"><strong>${T.mods.version}</strong>${mod.metadata.version}</span>
<span class="author"><strong>${T.mods.author}</strong>${mod.metadata.author}</span>

View File

@ -423,6 +423,14 @@ dialogs:
desc: >-
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:
# 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