mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-12-13 10:11:50 +00:00
Begin mod support
This commit is contained in:
parent
72feaa89e1
commit
ebebb0a188
@ -36,6 +36,8 @@ import { MainMenuState } from "./states/main_menu";
|
||||
import { MobileWarningState } from "./states/mobile_warning";
|
||||
import { PreloadState } from "./states/preload";
|
||||
import { SettingsState } from "./states/settings";
|
||||
import { ModsState } from "./states/mods";
|
||||
import { ModManager } from "./core/mod_manager";
|
||||
|
||||
const logger = createLogger("application");
|
||||
|
||||
@ -70,6 +72,7 @@ export class Application {
|
||||
this.savegameMgr = new SavegameManager(this);
|
||||
this.inputMgr = new InputDistributor(this);
|
||||
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
|
||||
this.modManager = new ModManager(this);
|
||||
|
||||
// Platform dependent stuff
|
||||
|
||||
@ -161,6 +164,7 @@ export class Application {
|
||||
KeybindingsState,
|
||||
AboutState,
|
||||
ChangelogState,
|
||||
ModsState,
|
||||
];
|
||||
|
||||
for (let i = 0; i < states.length; ++i) {
|
||||
|
||||
63
src/js/core/mod_api.js
Normal file
63
src/js/core/mod_api.js
Normal file
@ -0,0 +1,63 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
import { globalLog, globalError } from "./logging";
|
||||
import { GameRoot } from "../game/root";
|
||||
|
||||
export class ModApi {
|
||||
/**
|
||||
*
|
||||
* @param {Application} app
|
||||
*/
|
||||
constructor(app) {
|
||||
this._loadedMods = [];
|
||||
this._activeModInstances = [];
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new mod function
|
||||
* @param {function} mod
|
||||
*/
|
||||
registerModImplementation(mod) {
|
||||
globalLog(this, "🧪 Registering new mod");
|
||||
this._loadedMods.push(mod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all mods
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
_instantiateMods(root) {
|
||||
if (this._loadedMods.length > 0) {
|
||||
globalLog(this, "🧪 Instantiating", this._loadedMods.length, "mods");
|
||||
for (let i = 0; i < this._loadedMods.length; ++i) {
|
||||
const mod = this._loadedMods[i];
|
||||
try {
|
||||
mod(root);
|
||||
} catch (err) {
|
||||
globalError(this, "🧪 Failed to initialize mod:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.signals.aboutToDestruct.add(() => {
|
||||
for (let i = 0; i < this._modClickDetectors.length; ++i) {
|
||||
this._modClickDetectors[i].cleanup();
|
||||
}
|
||||
this._modClickDetectors = [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects css into the page
|
||||
* @param {string} css
|
||||
*/
|
||||
injectCss(css) {
|
||||
const styleElement = document.createElement("style");
|
||||
styleElement.textContent = css;
|
||||
styleElement.type = "text/css";
|
||||
styleElement.media = "all";
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
}
|
||||
334
src/js/core/mod_manager.js
Normal file
334
src/js/core/mod_manager.js
Normal file
@ -0,0 +1,334 @@
|
||||
import { ReadWriteProxy } from "./read_write_proxy";
|
||||
import { ExplainedResult } from "./explained_result";
|
||||
import { globalError, globalLog, globalWarn } from "./logging";
|
||||
import { ModApi } from "./mod_api";
|
||||
import { queryParamOptions } from "./query_parameters";
|
||||
|
||||
// When changing this, make sure to also migrate savegames since they store the installed mods!
|
||||
/**
|
||||
* @typedef {{
|
||||
* id: string,
|
||||
* name: string,
|
||||
* author: string,
|
||||
* website: string,
|
||||
* description: string,
|
||||
* url: string,
|
||||
* version: string,
|
||||
* is_game_changing: boolean
|
||||
* }} ModData
|
||||
*/
|
||||
|
||||
export class ModManager extends ReadWriteProxy {
|
||||
constructor(app) {
|
||||
super(app, "mods.bin");
|
||||
|
||||
this.modApi = new ModApi(app);
|
||||
|
||||
// Whether the mod manager needs a restart to reload all mods
|
||||
this.needsRestart = false;
|
||||
|
||||
// The promise for the next mod
|
||||
this.nextModResolver = null;
|
||||
this.nextModRejector = null;
|
||||
}
|
||||
|
||||
/////// BEGIN RW PROXY //////
|
||||
|
||||
verify(data) {
|
||||
// Todo
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
|
||||
getDefaultData() {
|
||||
return {
|
||||
version: this.getCurrentVersion(),
|
||||
mods: [],
|
||||
};
|
||||
}
|
||||
|
||||
getCurrentVersion() {
|
||||
return 1002;
|
||||
}
|
||||
|
||||
migrate(data) {
|
||||
// Simply reset
|
||||
if (data.version < 1002) {
|
||||
data.mods = [];
|
||||
data.version = 1002;
|
||||
}
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Read and directly write latest data back
|
||||
return this.readAsync()
|
||||
.then(() => this.loadMods())
|
||||
.then(() => this.writeAsync());
|
||||
}
|
||||
|
||||
save() {
|
||||
return this.writeAsync();
|
||||
}
|
||||
|
||||
/////// END RW PROXY //////
|
||||
|
||||
/**
|
||||
* Retursn whether there are any mods enabled
|
||||
*/
|
||||
getHasModsEnabled() {
|
||||
return (
|
||||
this.getNumMods() > 0 ||
|
||||
this.modApi._loadedMods.length > 0 ||
|
||||
this.modApi._activeModInstances.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retursn whether there are any mods enabled which change the game
|
||||
*/
|
||||
getHasGameChangingModsInstalled() {
|
||||
return this.getMods().find(mod => mod.is_game_changing) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a restart is required to apply all mods
|
||||
*/
|
||||
getNeedsRestart() {
|
||||
return this.needsRestart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given mods of the savegame differ from the
|
||||
* installed mods
|
||||
* @param {Array<ModData>} savegameMods
|
||||
*/
|
||||
checkModsNeedSync(savegameMods) {
|
||||
if (this.needsRestart) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const ourString = this.getMods()
|
||||
.filter(m => m.is_game_changing)
|
||||
.map(m => m.url)
|
||||
.join("@@@");
|
||||
const savegameString = savegameMods
|
||||
.filter(m => m.is_game_changing)
|
||||
.map(m => m.url)
|
||||
.join("@@@");
|
||||
|
||||
if (ourString === savegameString) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to register a mod after it was loaded
|
||||
* @param {function} modCallback
|
||||
*/
|
||||
attemptRegisterMod(modCallback) {
|
||||
assert(this.nextModResolver, "Got mod registration while mod promise was not expected");
|
||||
assert(this.nextModRejector, "Got mod registration while mod promise was not expected");
|
||||
|
||||
try {
|
||||
modCallback(this.modApi);
|
||||
} catch (ex) {
|
||||
console.error("Mod failed to load:", ex);
|
||||
const rejector = this.nextModRejector;
|
||||
this.nextModResolver = null;
|
||||
this.nextModRejector = null;
|
||||
rejector(ex);
|
||||
return;
|
||||
}
|
||||
const resolver = this.nextModResolver;
|
||||
this.nextModResolver = null;
|
||||
this.nextModRejector = null;
|
||||
resolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to load all mods
|
||||
*/
|
||||
loadMods() {
|
||||
window.registerMod = mod => this.attemptRegisterMod(mod);
|
||||
|
||||
// Load all mods
|
||||
let promise = Promise.resolve(null);
|
||||
|
||||
const mods = this.getMods();
|
||||
for (let i = 0; i < mods.length; ++i) {
|
||||
const mod = mods[i];
|
||||
|
||||
promise = promise.then(() => {
|
||||
return Promise.race([
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(reject, 60 * 1000);
|
||||
}),
|
||||
fetch(mod.url, {
|
||||
method: "GET",
|
||||
cache: "no-cache",
|
||||
}),
|
||||
])
|
||||
.then(res => res.text())
|
||||
.catch(err => {
|
||||
globalError(this, "Failed to load mod", mod.name, ":", err);
|
||||
return Promise.reject(
|
||||
"Downloading '" + mod.name + "' from '" + mod.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;
|
||||
|
||||
// Make sure we don't get errors from mods
|
||||
window.anyModLoaded = true;
|
||||
|
||||
const modScript = document.createElement("script");
|
||||
modScript.textContent = modCode;
|
||||
modScript.type = "text/javascript";
|
||||
modScript.setAttribute("data-mod-name", mod.name);
|
||||
modScript.setAttribute("data-mod-version", mod.version);
|
||||
try {
|
||||
document.head.appendChild(modScript);
|
||||
} catch (ex) {
|
||||
console.error("Failed to insert mod, bad js:", ex);
|
||||
this.nextModResolver = null;
|
||||
this.nextModRejector = null;
|
||||
reject("Mod is invalid");
|
||||
}
|
||||
}),
|
||||
]);
|
||||
})
|
||||
.catch(err => {
|
||||
globalError(this, "Failed to initializing mod", mod.name, ":", err);
|
||||
return Promise.reject("Initializing '" + mod.name + "' failed: " + err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
promise = promise.catch(err => {
|
||||
this.needsRestart = true;
|
||||
throw err;
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all installed mods
|
||||
* @returns {Array<ModData>}
|
||||
*/
|
||||
getMods() {
|
||||
if (queryParamOptions.modDeveloper) {
|
||||
return [
|
||||
{
|
||||
name: "Local Testing Mod",
|
||||
author: "nobody",
|
||||
website: "http://example.com",
|
||||
description:
|
||||
"This will load the mod from localhost:8000. Make sure to read the modding docs!",
|
||||
url: "http://localhost:8000/mod.js",
|
||||
version: "1.0.0",
|
||||
is_game_changing: false,
|
||||
id: "local_dev",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return this.currentData.mods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of mods
|
||||
* @returns {number}
|
||||
*/
|
||||
getNumMods() {
|
||||
return this.getMods().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a new mod
|
||||
* @param {ModData} mod
|
||||
* @param {string} id
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
installMod(mod, id) {
|
||||
if (queryParamOptions.modDeveloper) {
|
||||
return Promise.reject("Can not install mods in developer mode");
|
||||
}
|
||||
|
||||
// Check if a mod with the same name is already installed
|
||||
const mods = this.getMods();
|
||||
for (let i = 0; i < mods.length; ++i) {
|
||||
if (mods[i].name === mod.name) {
|
||||
return Promise.reject("A mod with the same name is already installed");
|
||||
}
|
||||
}
|
||||
|
||||
this.currentData.mods.push(mod);
|
||||
|
||||
// TODO: Check if this use of trackUiClick is okay.
|
||||
// Track download in the background
|
||||
return this.writeAsync()
|
||||
.then(() => this.app.analytics.trackUiClick("mod/" + id))
|
||||
.then(() => (this.needsRestart = true))
|
||||
.then(() => null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to synchronize the mods from the savegame
|
||||
* @param {Array<ModData>} savegameMods
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
syncModsFromSavegame(savegameMods) {
|
||||
// First, remove all game changing mods from ours
|
||||
let newMods = this.getMods().filter(mod => !mod.is_game_changing);
|
||||
|
||||
// Now, push all game changing mods from the savegame
|
||||
const gamechangingMods = savegameMods.filter(mod => mod.is_game_changing);
|
||||
|
||||
for (let i = 0; i < gamechangingMods.length; ++i) {
|
||||
newMods.push(gamechangingMods[i]);
|
||||
}
|
||||
|
||||
this.currentData.mods = newMods;
|
||||
return this.writeAsync()
|
||||
.then(() => (this.needsRestart = true))
|
||||
.then(() => null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a mod by its name
|
||||
* @param {string} name
|
||||
* @returns {ModData}
|
||||
*/
|
||||
getModByName(name) {
|
||||
return this.getMods().find(m => m.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a mod
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
uninstallMod(name) {
|
||||
if (queryParamOptions.modDeveloper) {
|
||||
return Promise.reject("Can not uninstall mods in developer mode");
|
||||
}
|
||||
|
||||
const mods = this.getMods();
|
||||
for (let i = 0; i < mods.length; ++i) {
|
||||
if (mods[i].name === name) {
|
||||
mods.splice(i, 1);
|
||||
return this.writeAsync()
|
||||
.then(() => (this.needsRestart = true))
|
||||
.then(() => null);
|
||||
}
|
||||
}
|
||||
return Promise.reject("Mod not found in installed mods");
|
||||
}
|
||||
}
|
||||
294
src/js/states/mods.js
Normal file
294
src/js/states/mods.js
Normal file
@ -0,0 +1,294 @@
|
||||
import { TextualGameState } from "../core/textual_game_state";
|
||||
import { T } from "../translations";
|
||||
import { removeAllChildren } from "../core/utils";
|
||||
import { globalWarn } from "../core/logging";
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* id: string,
|
||||
* name: string,
|
||||
* website: string,
|
||||
* description: string,
|
||||
* url: string,
|
||||
* author: string,
|
||||
* version: string,
|
||||
* install_count: string,
|
||||
* is_game_changing: boolean
|
||||
* }} APIModData
|
||||
*/
|
||||
|
||||
export class ModsState extends TextualGameState {
|
||||
constructor() {
|
||||
super("ModsState");
|
||||
}
|
||||
|
||||
getStateHeaderTitle() {
|
||||
return T.mods.title;
|
||||
}
|
||||
|
||||
getMainContentHTML() {
|
||||
return `
|
||||
|
||||
<span class="devHint">
|
||||
${T.mods.dev_hint.replace(
|
||||
"<modding_docs>",
|
||||
`<span class='moddingDocsLink'>${T.mods.modding_docs}</span>`
|
||||
)}
|
||||
</span>
|
||||
|
||||
|
||||
<strong class="category_label">${T.mods.installed_mods}</strong>
|
||||
<div class="installedMods"></div>
|
||||
|
||||
<strong class="category_label">${T.mods.mods_browser}</strong>
|
||||
<div class="modGallery"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
onEnter() {
|
||||
this.installedModsElement = this.htmlElement.querySelector(".installedMods");
|
||||
this.modGalleryElement = this.htmlElement.querySelector(".modGallery");
|
||||
|
||||
this.rerenderInstalledMods();
|
||||
this.refreshModGallery();
|
||||
|
||||
this.trackClicks(this.htmlElement.querySelector(".moddingDocsLink"), this.openModdingDocs);
|
||||
}
|
||||
|
||||
openModdingDocs() {
|
||||
this.app.platformWrapper.openExternalLink("https://github.com/tobspr/yorg.io-3-modding-docs");
|
||||
}
|
||||
|
||||
rerenderInstalledMods() {
|
||||
// TODO: We are leaking click detectors here
|
||||
removeAllChildren(this.installedModsElement);
|
||||
|
||||
const mods = this.app.modManager.getMods();
|
||||
if (mods.length === 0) {
|
||||
this.installedModsElement.innerHTML = T.mods.no_mods_found;
|
||||
return;
|
||||
}
|
||||
|
||||
const frag = document.createDocumentFragment();
|
||||
for (let i = 0; i < mods.length; ++i) {
|
||||
const mod = mods[i];
|
||||
|
||||
const elem = this.makeModElement(mod);
|
||||
frag.appendChild(elem);
|
||||
|
||||
const uninstallButton = document.createElement("button");
|
||||
uninstallButton.classList.add("styledButton", "uninstallMod");
|
||||
uninstallButton.innerText = T.mods.uninstall_mod;
|
||||
elem.appendChild(uninstallButton);
|
||||
this.trackClicks(uninstallButton, () => this.uninstallMod(mod));
|
||||
}
|
||||
|
||||
this.installedModsElement.appendChild(frag);
|
||||
}
|
||||
|
||||
refreshModGallery() {
|
||||
// TODO: We are leaking click detectors here
|
||||
removeAllChildren(this.modGalleryElement);
|
||||
|
||||
this.modGalleryElement.innerHTML = `<span class="prefab_LoadingTextWithAnim">${T.global.loading}</span>`;
|
||||
|
||||
this.app.api
|
||||
.fetchModGallery()
|
||||
.then(mods => this.rerenderModsGallery(mods))
|
||||
.catch(err => {
|
||||
globalWarn(this, "Failed to fetch mod gallery:", err);
|
||||
this.modGalleryElement.innerHTML = T.mods.mod_gallery_fail + " " + err;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {APIModData|import("../core/mod_manager").ModData} mod
|
||||
*/
|
||||
makeModElement(mod) {
|
||||
const elem = document.createElement("div");
|
||||
elem.classList.add("mod", "cardbox");
|
||||
|
||||
const title = document.createElement("span");
|
||||
title.classList.add("title");
|
||||
title.innerText = mod.name;
|
||||
elem.appendChild(title);
|
||||
|
||||
const version = document.createElement("span");
|
||||
version.classList.add("version");
|
||||
version.innerText = mod.version;
|
||||
title.appendChild(version);
|
||||
|
||||
const author = document.createElement("span");
|
||||
author.classList.add("author");
|
||||
author.innerText = mod.author;
|
||||
elem.appendChild(author);
|
||||
|
||||
const website = document.createElement("span");
|
||||
website.classList.add("website");
|
||||
website.innerText = T.mods.website;
|
||||
elem.appendChild(website);
|
||||
this.trackClicks(website, () => this.app.platformWrapper.openExternalLink(mod.website));
|
||||
|
||||
const description = document.createElement("span");
|
||||
description.classList.add("description");
|
||||
description.innerText = mod.description;
|
||||
elem.appendChild(description);
|
||||
|
||||
if (mod.is_game_changing) {
|
||||
const hint = document.createElement("span");
|
||||
hint.classList.add("gameChangingHint");
|
||||
hint.innerText = T.mods.gamechanging_hint;
|
||||
elem.appendChild(hint);
|
||||
}
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Array<APIModData>} mods
|
||||
*/
|
||||
rerenderModsGallery(mods) {
|
||||
// TODO: We are leaking click detectors here
|
||||
removeAllChildren(this.modGalleryElement);
|
||||
|
||||
if (mods.length === 0) {
|
||||
this.modGalleryElement.innerHTML = T.mods.no_mods_found;
|
||||
return;
|
||||
}
|
||||
|
||||
const frag = document.createDocumentFragment();
|
||||
for (let i = 0; i < mods.length; ++i) {
|
||||
const mod = mods[i];
|
||||
|
||||
const elem = this.makeModElement(mod);
|
||||
frag.appendChild(elem);
|
||||
|
||||
const installCount = document.createElement("span");
|
||||
installCount.classList.add("installCount");
|
||||
installCount.innerText = T.mods.install_count.replace("<installs>", "" + mod.install_count);
|
||||
elem.appendChild(installCount);
|
||||
|
||||
if (this.app.modManager.getModByName(mod.name)) {
|
||||
const installedText = document.createElement("span");
|
||||
installedText.innerText = T.mods.mod_installed;
|
||||
installedText.classList.add("installedText");
|
||||
elem.appendChild(installedText);
|
||||
elem.classList.add("installed");
|
||||
} else {
|
||||
const installButton = document.createElement("button");
|
||||
installButton.classList.add("styledButton", "installMod");
|
||||
installButton.innerText = T.mods.install_mod;
|
||||
elem.appendChild(installButton);
|
||||
this.trackClicks(installButton, () => this.tryInstallMod(mod));
|
||||
}
|
||||
}
|
||||
this.modGalleryElement.appendChild(frag);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("../core/mod_manager").ModData} mod
|
||||
*/
|
||||
uninstallMod(mod) {
|
||||
const closeLoading = this.dialogs.showLoadingDialog();
|
||||
this.app.modManager.uninstallMod(mod.name).then(
|
||||
() => {
|
||||
closeLoading();
|
||||
|
||||
const { restart } = this.dialogs.showInfo(
|
||||
T.mods.mod_uninstalled_dialog.title,
|
||||
T.mods.mod_uninstalled_dialog.desc,
|
||||
this.app.platformWrapper.getSupportsRestart() ? ["ok:good", "restart:misc"] : ["ok:good"]
|
||||
);
|
||||
if (restart) {
|
||||
restart.add(() => this.app.platformWrapper.performRestart());
|
||||
}
|
||||
|
||||
this.refreshModGallery();
|
||||
this.rerenderInstalledMods();
|
||||
},
|
||||
err => {
|
||||
closeLoading();
|
||||
this.dialogs.showWarning(T.global.error, err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {APIModData} mod
|
||||
*/
|
||||
tryInstallMod(mod) {
|
||||
const { install } = this.dialogs.showWarning(
|
||||
T.mods.mod_warning.title,
|
||||
`
|
||||
${T.mods.mod_warning.desc}
|
||||
<ul>
|
||||
<li>${T.mods.mod_warning.point_0}</li>
|
||||
<li>${T.mods.mod_warning.point_1}</li>
|
||||
<li>${T.mods.mod_warning.point_2}</li>
|
||||
<li>${T.mods.mod_warning.point_3}</li>
|
||||
${mod.is_game_changing ? `<li>${T.mods.mod_warning.disclaimer_gamechanging}</li>` : ""}
|
||||
</ul>
|
||||
`,
|
||||
// @ts-ignore
|
||||
["cancel:good", window.modsInstallWarningShown ? "install:bad" : "install:bad:timeout"]
|
||||
);
|
||||
|
||||
install.add(() => this.doInstallMod(mod));
|
||||
// @ts-ignore
|
||||
window.modsInstallWarningShown = true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {APIModData} mod
|
||||
*/
|
||||
doInstallMod(mod) {
|
||||
const closeLoading = this.dialogs.showLoadingDialog();
|
||||
|
||||
this.app.modManager
|
||||
.installMod(
|
||||
{
|
||||
name: mod.name,
|
||||
author: mod.author,
|
||||
website: mod.website,
|
||||
url: mod.url,
|
||||
version: mod.version,
|
||||
description: mod.description,
|
||||
is_game_changing: mod.is_game_changing,
|
||||
id: mod.id,
|
||||
},
|
||||
mod.id
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
closeLoading();
|
||||
|
||||
const { restart } = this.dialogs.showInfo(
|
||||
T.mods.mod_installed_dialog.title,
|
||||
T.mods.mod_installed_dialog.desc,
|
||||
this.app.platformWrapper.getSupportsRestart()
|
||||
? ["ok:good", "restart:misc"]
|
||||
: ["ok:good"]
|
||||
);
|
||||
if (restart) {
|
||||
restart.add(() => this.app.platformWrapper.performRestart());
|
||||
}
|
||||
|
||||
this.refreshModGallery();
|
||||
this.rerenderInstalledMods();
|
||||
},
|
||||
err => {
|
||||
closeLoading();
|
||||
this.dialogs.showWarning(T.global.error, err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getDefaultPreviousState() {
|
||||
return "SettingsState";
|
||||
}
|
||||
}
|
||||
@ -55,6 +55,49 @@ global:
|
||||
shift: SHIFT
|
||||
space: SPACE
|
||||
|
||||
mods:
|
||||
title: Mods
|
||||
|
||||
# Links to developer information
|
||||
dev_hint: Interested in creating mods? Check out <modding_docs>!
|
||||
modding_docs: Mod Documentation
|
||||
|
||||
installed_mods: Installed Mods
|
||||
install_count: <installs> mods installed
|
||||
mods_browser: Popular Mods
|
||||
website: Visit Website
|
||||
|
||||
no_mods_found: No mods found
|
||||
mod_gallery_fail: >-
|
||||
Failed to fetch mod gallery:
|
||||
|
||||
install_mod: Install
|
||||
uninstall_mod: Remove
|
||||
mod_installed: Installed
|
||||
|
||||
gamechanging_hint: This mod is gamechanging and thus cannot be removed once added to a game save!
|
||||
|
||||
mod_warning:
|
||||
title: Modded Warning
|
||||
desc: >-
|
||||
Please note the following when installing mods:
|
||||
|
||||
point_0: Mods are not created by the developer.
|
||||
point_1: They may crash and corrupt your games.
|
||||
point_2: Mods are not created by the developer, and the developer (tobspr) is not responsible for any damage caused by mods.
|
||||
point_3: Mods have full access to the game, including other game save files.
|
||||
|
||||
disclaimer_gamechanging: Gamechanging mods cannot be removed once added to a game save.
|
||||
|
||||
mod_installed_dialog:
|
||||
title: Mod Installed
|
||||
desc: The mod has succssfully been installed and will be loaded after a restart.
|
||||
|
||||
mod_uninstalled_dialog:
|
||||
title: Mod Removed
|
||||
desc: The mod has successfully beeen removed and will get unloaded after a restart.
|
||||
|
||||
|
||||
demoBanners:
|
||||
# This is the "advertisement" shown in the main menu and other various places
|
||||
title: Hey!
|
||||
|
||||
Loading…
Reference in New Issue
Block a user