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:
parent
509c01f642
commit
41841651db
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = {};
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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 {{
|
||||
|
||||
24
src/js/savegame/schemas/1010.js
Normal file
24
src/js/savegame/schemas/1010.js
Normal 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 = [];
|
||||
}
|
||||
}
|
||||
5
src/js/savegame/schemas/1010.json
Normal file
5
src/js/savegame/schemas/1010.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"additionalProperties": true
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user