mirror of
https://github.com/tobspr/shapez.io.git
synced 2026-02-12 02:49:20 +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 { MobileWarningState } from "./states/mobile_warning";
|
||||||
import { PreloadState } from "./states/preload";
|
import { PreloadState } from "./states/preload";
|
||||||
import { SettingsState } from "./states/settings";
|
import { SettingsState } from "./states/settings";
|
||||||
|
import { ModsState } from "./states/mods";
|
||||||
|
import { ModManager } from "./core/mod_manager";
|
||||||
|
|
||||||
const logger = createLogger("application");
|
const logger = createLogger("application");
|
||||||
|
|
||||||
@ -70,6 +72,7 @@ export class Application {
|
|||||||
this.savegameMgr = new SavegameManager(this);
|
this.savegameMgr = new SavegameManager(this);
|
||||||
this.inputMgr = new InputDistributor(this);
|
this.inputMgr = new InputDistributor(this);
|
||||||
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
|
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
|
||||||
|
this.modManager = new ModManager(this);
|
||||||
|
|
||||||
// Platform dependent stuff
|
// Platform dependent stuff
|
||||||
|
|
||||||
@ -161,6 +164,7 @@ export class Application {
|
|||||||
KeybindingsState,
|
KeybindingsState,
|
||||||
AboutState,
|
AboutState,
|
||||||
ChangelogState,
|
ChangelogState,
|
||||||
|
ModsState,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = 0; i < states.length; ++i) {
|
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
|
shift: SHIFT
|
||||||
space: SPACE
|
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:
|
demoBanners:
|
||||||
# This is the "advertisement" shown in the main menu and other various places
|
# This is the "advertisement" shown in the main menu and other various places
|
||||||
title: Hey!
|
title: Hey!
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user