js/modloader/modmanager.js
import { matchOverwriteRecursive } from "../translations";
import { ShapezAPI } from "./mod";
import { matchOverwriteRecursiveSettings } from "./overwrite";
/**
* @typedef {{
* mods: [
* {
* url: string,
* id: string,
* config: {},
* settings: {},
* },
* ],
* modOrder?: [],
* }} ModPack
*/
const Toposort = require("toposort-class");
const INFOType = {
title: "",
id: "",
description: "",
authors: [],
version: "",
gameVersion: 0,
dependencies: [],
incompatible: [],
translations: {},
settings: {},
updateStaticSettings: () => {},
updateStaticTranslations: id => {},
gameInitializedRootClasses: root => {},
gameInitializedRootManagers: root => {},
gameBeforeFirstUpdate: root => {},
main: () => {},
};
export class ModManager {
/**
*
* @param {ModPack} modPack
*/
constructor(user, modPack = undefined) {
/** @type {Map<String, import("./mod").ModInfo>} */
this.mods = new Map();
this.modPack = modPack;
window["shapezAPI"] = new ShapezAPI(user);
/**
* Registers a mod
* @param {import("./mod").ModInfo} mod
*/
window["registerMod"] = mod => {
this.registerMod(mod);
};
}
registerMod(mod) {
for (const key in INFOType) {
if (!INFOType.hasOwnProperty(key)) continue;
if (mod.hasOwnProperty(key)) continue;
if (mod.id) console.warn("Mod with mod id: " + mod.id + " has no " + key + " specified");
else console.warn("Unknown mod has no " + key + " specified");
return;
}
if (!mod.id.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)) {
console.warn("Mod with mod id: " + mod.id + " has no uuid");
return;
}
if (this.mods.has(mod.id)) {
console.warn("Mod with mod id: " + mod.id + " already registerd");
return;
}
this.mods.set(mod.id, mod);
}
/**
* Adds a mod to the page
* @param {String} url
* @returns {Promise}
*/
addMod(url) {
//TODO: check if is mods website
return Promise.race([
new Promise((resolve, reject) => {
setTimeout(reject, 60 * 1000);
}),
fetch(url, {
method: "GET",
cache: "no-cache",
}),
])
.then(res => res.text())
.catch(err => {
assert(this, "Failed to load mod:", err);
return Promise.reject("Downloading from '" + url + "' timed out");
})
.then(modCode => {
return Promise.race([
new Promise((resolve, reject) => {
setTimeout(reject, 60 * 1000);
}),
new Promise((resolve, reject) => {
this.nextModResolver = resolve;
this.nextModRejector = reject;
const modScript = document.createElement("script");
modScript.textContent = modCode;
modScript.type = "text/javascript";
try {
document.head.appendChild(modScript);
resolve();
} catch (ex) {
console.error("Failed to insert mod, bad js:", ex);
this.nextModResolver = null;
this.nextModRejector = null;
reject("Mod is invalid");
}
}),
]);
})
.catch(err => {
assert(this, "Failed to initializing mod:", err);
return Promise.reject("Initializing mod failed: " + err);
});
}
addModPackMods() {
if (this.modPack && this.modPack.mods) {
let promise = Promise.resolve(null);
for (let i = 0; i < this.modPack.mods.length; i++) {
promise = promise.then(() => {
return this.addMod(this.modPack.mods[i].url);
});
}
return promise;
}
return Promise.reject();
}
/**
* Adds a mod to the page
* @param {Array<String>} urls
*/
addMods(urls) {
let promise = Promise.resolve(null);
for (let i = 0; i < urls.length; ++i) {
const url = urls[i];
promise = promise.then(() => {
return this.addMod(url);
});
}
return promise;
}
/**
* Loads all mods in the mods list
*/
loadMods() {
shapezAPI.mods = this.mods;
if (!this.modPack || !this.modPack.modOrder) {
var sorter = new Toposort();
for (const [id, mod] of this.mods.entries()) {
let isMissingDependecie = false;
let missingDependecie = "";
for (let i = 0; i < mod.dependencies.length; i++) {
const dependencie = mod.dependencies[i];
if (this.mods.has(dependencie)) continue;
isMissingDependecie = true;
missingDependecie = dependencie;
}
if (isMissingDependecie) {
console.warn(
"Mod with mod id: " +
mod.id +
" is disabled because it's missings the dependecie " +
missingDependecie
);
continue;
} else sorter.add(id, mod.dependencies);
}
shapezAPI.modOrder = sorter.sort().reverse();
} else {
/** @typedef {string[]} */
shapezAPI.modOrder = this.modPack.modOrder;
for (const [id, mod] of this.mods.entries()) {
if (shapezAPI.modOrder.includes(id)) continue;
shapezAPI.modOrder.push(id);
}
}
for (let i = 0; i < shapezAPI.modOrder.length; i++) {
this.loadMod(shapezAPI.modOrder[i]);
}
}
/**
* Calls the main mod function
* @param {String} id
*/
loadMod(id) {
var mod = this.mods.get(id);
for (const [id, currentMod] of this.mods.entries()) {
if (mod.incompatible.indexOf(id) >= 0) {
console.warn(
"Mod with mod id: " + mod.id + " is disabled because it's incompatible with " + id
);
return;
}
}
const language = mod.translations["en"];
if (language) {
matchOverwriteRecursive(shapezAPI.translations, language);
}
const settings = this.modPack.mods.find(mod => mod.id === id).settings;
if (settings) {
matchOverwriteRecursiveSettings(mod.settings, settings);
}
if (this.modPack && this.modPack.mods) mod.main(this.modPack.mods.find(mod => mod.id === id).config);
else mod.main();
}
}