mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-12-13 02:01:51 +00:00
Merge branch 'modloader' into processing-refactor
This commit is contained in:
commit
745dcb3453
3
gulp/mod.js
Normal file
3
gulp/mod.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = function (source, map) {
|
||||
return source + `\nexport let $s=(n,v)=>eval(n+"=v")`;
|
||||
};
|
||||
@ -93,6 +93,9 @@ module.exports = ({ watch = false, standalone = false, chineseVersion = false, w
|
||||
end: "typehints:end",
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: path.resolve(__dirname, "mod.js"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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 {
|
||||
|
||||
78
mod_examples/storing_data_in_savegame.js
Normal file
78
mod_examples/storing_data_in_savegame.js
Normal file
@ -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);
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -96,11 +96,11 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it finished
|
||||
if (currentCharge.remainingTime <= 0.0 && processorComp.queuedEjects.length < 1) {
|
||||
// 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) {
|
||||
processorComp.queuedEjects.push(itemsToEject[j]);
|
||||
}
|
||||
@ -133,6 +133,39 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, extraProgress)) {
|
||||
|
||||
@ -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()),
|
||||
};
|
||||
|
||||
@ -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 = {};
|
||||
@ -66,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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -160,6 +163,11 @@ export class Savegame extends ReadWriteProxy {
|
||||
data.version = 1009;
|
||||
}
|
||||
|
||||
if (data.version === 1009) {
|
||||
SavegameInterface_V1010.migrate1009to1010(data);
|
||||
data.version = 1010;
|
||||
}
|
||||
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
|
||||
@ -269,6 +277,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");
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -17,7 +25,8 @@
|
||||
* pinnedShapes: any,
|
||||
* waypoints: any,
|
||||
* entities: Array<Entity>,
|
||||
* beltPaths: Array<any>
|
||||
* beltPaths: Array<any>,
|
||||
* modExtraData: Object
|
||||
* }} SerializedGame
|
||||
*
|
||||
* @typedef {{
|
||||
@ -25,6 +34,7 @@
|
||||
* dump: SerializedGame,
|
||||
* stats: SavegameStats,
|
||||
* lastUpdate: number,
|
||||
* mods: SavegameStoredMods
|
||||
* }} SavegameData
|
||||
*
|
||||
* @typedef {{
|
||||
|
||||
28
src/js/savegame/schemas/1010.js
Normal file
28
src/js/savegame/schemas/1010.js
Normal file
@ -0,0 +1,28 @@
|
||||
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 = [];
|
||||
|
||||
if (data.dump) {
|
||||
data.dump.modExtraData = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
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