mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-12-15 19:21:49 +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",
|
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
|
## 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
|
## 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 |
|
| [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 |
|
| [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 |
|
| [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_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 |
|
| [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 |
|
| [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",
|
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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
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",
|
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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -401,9 +401,10 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
|
|||||||
* Checks if there are any entities in the way, returns true if there are
|
* Checks if there are any entities in the way, returns true if there are
|
||||||
* @param {Vector} from
|
* @param {Vector} from
|
||||||
* @param {Vector} to
|
* @param {Vector} to
|
||||||
|
* @param {Vector[]=} ignorePositions
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
checkForObstales(from, to) {
|
checkForObstales(from, to, ignorePositions = []) {
|
||||||
assert(from.x === to.x || from.y === to.y, "Must be a straight line");
|
assert(from.x === to.x || from.y === to.y, "Must be a straight line");
|
||||||
|
|
||||||
const prop = from.x === to.x ? "y" : "x";
|
const prop = from.x === to.x ? "y" : "x";
|
||||||
@ -426,6 +427,9 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
|
|||||||
|
|
||||||
for (let i = start; i <= end; i++) {
|
for (let i = start; i <= end; i++) {
|
||||||
current[prop] = i;
|
current[prop] = i;
|
||||||
|
if (ignorePositions.some(p => p.distanceSquare(current) < 0.1)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!this.root.logic.checkCanPlaceEntity(this.fakeEntity, { allowReplaceBuildings: false })) {
|
if (!this.root.logic.checkCanPlaceEntity(this.fakeEntity, { allowReplaceBuildings: false })) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -464,8 +468,11 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
|
|||||||
const endLine = mouseTile.toWorldSpaceCenterOfTile();
|
const endLine = mouseTile.toWorldSpaceCenterOfTile();
|
||||||
const midLine = this.currentDirectionLockCorner.toWorldSpaceCenterOfTile();
|
const midLine = this.currentDirectionLockCorner.toWorldSpaceCenterOfTile();
|
||||||
const anyObstacle =
|
const anyObstacle =
|
||||||
this.checkForObstales(this.lastDragTile, this.currentDirectionLockCorner) ||
|
this.checkForObstales(this.lastDragTile, this.currentDirectionLockCorner, [
|
||||||
this.checkForObstales(this.currentDirectionLockCorner, mouseTile);
|
this.lastDragTile,
|
||||||
|
mouseTile,
|
||||||
|
]) ||
|
||||||
|
this.checkForObstales(this.currentDirectionLockCorner, mouseTile, [this.lastDragTile, mouseTile]);
|
||||||
|
|
||||||
if (anyObstacle) {
|
if (anyObstacle) {
|
||||||
applyStyles("error");
|
applyStyles("error");
|
||||||
|
|||||||
@ -96,11 +96,11 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it finished
|
// Check if it finished and we don't already have queued ejects
|
||||||
if (currentCharge.remainingTime <= 0.0 && processorComp.queuedEjects.length < 1) {
|
if (currentCharge.remainingTime <= 0.0 && !processorComp.queuedEjects.length) {
|
||||||
const itemsToEject = currentCharge.items;
|
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) {
|
for (let j = 0; j < itemsToEject.length; ++j) {
|
||||||
processorComp.queuedEjects.push(itemsToEject[j]);
|
processorComp.queuedEjects.push(itemsToEject[j]);
|
||||||
}
|
}
|
||||||
@ -133,6 +133,39 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
|||||||
slot = ejectorComp.getFirstFreeSlot();
|
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) {
|
if (slot !== null) {
|
||||||
// Alright, we can actually eject
|
// Alright, we can actually eject
|
||||||
if (!ejectorComp.tryEject(slot, item, extraProgress)) {
|
if (!ejectorComp.tryEject(slot, item, extraProgress)) {
|
||||||
|
|||||||
@ -27,4 +27,7 @@ export const MOD_SIGNALS = {
|
|||||||
gameStarted: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()),
|
gameStarted: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()),
|
||||||
|
|
||||||
stateEntered: /** @type {TypedSignal<[GameState]>} */ (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;
|
* 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 = {};
|
||||||
@ -66,18 +112,20 @@ export class ModLoader {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const module = modules(key);
|
const module = modules(key);
|
||||||
for (const member in module) {
|
for (const member in module) {
|
||||||
if (member === "default") {
|
if (member === "default" || member === "$s") {
|
||||||
|
// Setter
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (exports[member]) {
|
if (exports[member]) {
|
||||||
throw new Error("Duplicate export of " + member);
|
throw new Error("Duplicate export of " + member);
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(exports, member, {
|
Object.defineProperty(exports, member, {
|
||||||
get() {
|
get() {
|
||||||
return module[member];
|
return module[member];
|
||||||
},
|
},
|
||||||
set(v) {
|
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_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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,6 +163,11 @@ export class Savegame extends ReadWriteProxy {
|
|||||||
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 +277,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);
|
||||||
|
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { ExplainedResult } from "../core/explained_result";
|
import { ExplainedResult } from "../core/explained_result";
|
||||||
import { createLogger } from "../core/logging";
|
|
||||||
import { gComponentRegistry } from "../core/global_registries";
|
import { gComponentRegistry } from "../core/global_registries";
|
||||||
|
import { createLogger } from "../core/logging";
|
||||||
|
import { MOD_SIGNALS } from "../mods/mod_signals";
|
||||||
import { SerializerInternal } from "./serializer_internal";
|
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
|
* @typedef {import("../game/component").Component} Component
|
||||||
@ -42,8 +41,12 @@ export class SavegameSerializer {
|
|||||||
beltPaths: root.systemMgr.systems.belt.serializePaths(),
|
beltPaths: root.systemMgr.systems.belt.serializePaths(),
|
||||||
pinnedShapes: root.hud.parts.pinnedShapes ? root.hud.parts.pinnedShapes.serialize() : null,
|
pinnedShapes: root.hud.parts.pinnedShapes ? root.hud.parts.pinnedShapes.serialize() : null,
|
||||||
waypoints: root.hud.parts.waypoints ? root.hud.parts.waypoints.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 (G_IS_DEV) {
|
||||||
if (sanityChecks) {
|
if (sanityChecks) {
|
||||||
// Sanity check
|
// Sanity check
|
||||||
@ -151,6 +154,9 @@ export class SavegameSerializer {
|
|||||||
return ExplainedResult.bad(errorReason);
|
return ExplainedResult.bad(errorReason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mods
|
||||||
|
MOD_SIGNALS.gameDeserialized.dispatch(root, savegame);
|
||||||
|
|
||||||
return ExplainedResult.good();
|
return ExplainedResult.good();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
@ -17,7 +25,8 @@
|
|||||||
* pinnedShapes: any,
|
* pinnedShapes: any,
|
||||||
* waypoints: any,
|
* waypoints: any,
|
||||||
* entities: Array<Entity>,
|
* entities: Array<Entity>,
|
||||||
* beltPaths: Array<any>
|
* beltPaths: Array<any>,
|
||||||
|
* modExtraData: Object
|
||||||
* }} SerializedGame
|
* }} SerializedGame
|
||||||
*
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
@ -25,6 +34,7 @@
|
|||||||
* dump: SerializedGame,
|
* dump: SerializedGame,
|
||||||
* stats: SavegameStats,
|
* stats: SavegameStats,
|
||||||
* lastUpdate: number,
|
* lastUpdate: number,
|
||||||
|
* mods: SavegameStoredMods
|
||||||
* }} SavegameData
|
* }} SavegameData
|
||||||
*
|
*
|
||||||
* @typedef {{
|
* @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 { 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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user