Steam demo

This commit is contained in:
tobspr 2022-03-07 09:56:11 +01:00
parent 4e5e5c8ef7
commit 7ade7946dc
12 changed files with 62 additions and 358 deletions

View File

@ -14,8 +14,6 @@ app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling");
const isDev = app.commandLine.hasSwitch("dev");
const isLocal = app.commandLine.hasSwitch("local");
const safeMode = app.commandLine.hasSwitch("safe-mode");
const externalMod = app.commandLine.getSwitchValue("load-mod");
const roamingFolder =
process.env.APPDATA ||
@ -60,7 +58,7 @@ function createWindow() {
useContentSize: false,
minWidth: 800,
minHeight: 600,
title: "shapez.io Standalone",
title: "shapez.io Demo",
transparent: false,
icon: path.join(__dirname, "favicon" + faviconExtension),
// fullscreen: true,
@ -341,49 +339,8 @@ ipcMain.handle("fs-job", async (event, job) => {
}
});
ipcMain.handle("open-mods-folder", async () => {
shell.openPath(modsPath);
});
console.log("Loading mods ...");
function loadMods() {
if (safeMode) {
console.log("Safe Mode enabled for mods, skipping mod search");
}
console.log("Loading mods from", modsPath);
let modFiles = safeMode
? []
: fs
.readdirSync(modsPath)
.filter(filename => filename.endsWith(".js"))
.map(filename => path.join(modsPath, filename));
if (externalMod) {
console.log("Adding external mod source:", externalMod);
const externalModPaths = externalMod.split(",");
modFiles = modFiles.concat(externalModPaths);
}
return modFiles.map(filename => fs.readFileSync(filename, "utf8"));
}
let mods = [];
try {
mods = loadMods();
console.log("Loaded", mods.length, "mods");
} catch (ex) {
console.error("Failed to load mods");
dialog.showErrorBox("Failed to load mods:", ex);
}
ipcMain.handle("get-mods", async () => {
return mods;
return [];
});
steam.init(isDev);
// Only allow achievements and puzzle DLC if no mods are loaded
if (mods.length === 0) {
steam.listen();
}

View File

@ -151,10 +151,10 @@ function gulptasksStandalone($, gulp) {
asar: asar,
executableName: "shapezio",
icon: path.join(electronBaseDir, "favicon"),
name: "shapez.io-standalone" + suffix,
name: "shapez.io-demo" + suffix,
out: tempDestDir,
overwrite: true,
appBundleId: "io.shapez.standalone",
appBundleId: "io.shapez.demo",
appCategoryType: "public.app-category.games",
}).then(
appPaths => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@ -63,7 +63,6 @@ export class RestrictionManager extends ReadWriteProxy {
onHasLegacySavegamesChanged(has119Savegames = false) {
if (has119Savegames && !this.currentData.savegameV1119Imported) {
this.currentData.savegameV1119Imported = true;
console.warn("Current user now has access to all levels due to 1119 savegame");
return this.writeAsync();
}
return Promise.resolve();
@ -74,25 +73,6 @@ export class RestrictionManager extends ReadWriteProxy {
* @returns {boolean}
*/
isLimitedVersion() {
if (IS_MAC) {
// On mac, the full version is always active
return false;
}
if (G_IS_STANDALONE) {
// Standalone is never limited
return false;
}
if (queryParamOptions.embedProvider === "gamedistribution") {
// also full version on gamedistribution
return false;
}
if (G_IS_DEV) {
return typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0;
}
return true;
}
@ -101,7 +81,7 @@ export class RestrictionManager extends ReadWriteProxy {
* @returns {boolean}
*/
getIsStandaloneMarketingActive() {
return this.isLimitedVersion();
return true;
}
/**
@ -109,7 +89,7 @@ export class RestrictionManager extends ReadWriteProxy {
* @returns {boolean}
*/
getIsExportingScreenshotsPossible() {
return !this.isLimitedVersion();
return false;
}
/**
@ -117,7 +97,7 @@ export class RestrictionManager extends ReadWriteProxy {
* @returns {number}
*/
getMaximumWaypoints() {
return this.isLimitedVersion() ? 2 : 1e20;
return 2;
}
/**
@ -125,7 +105,7 @@ export class RestrictionManager extends ReadWriteProxy {
* @returns {boolean}
*/
getHasUnlimitedSavegames() {
return !this.isLimitedVersion();
return false;
}
/**
@ -133,7 +113,7 @@ export class RestrictionManager extends ReadWriteProxy {
* @returns {boolean}
*/
getHasExtendedSettings() {
return !this.isLimitedVersion();
return false;
}
/**
@ -141,7 +121,7 @@ export class RestrictionManager extends ReadWriteProxy {
* @returns {boolean}
*/
getHasExtendedUpgrades() {
return !this.isLimitedVersion() || this.currentData.savegameV1119Imported;
return false;
}
/**
@ -149,6 +129,6 @@ export class RestrictionManager extends ReadWriteProxy {
* @returns {boolean}
*/
getHasExtendedLevelsAndFreeplay() {
return !this.isLimitedVersion() || this.currentData.savegameV1119Imported;
return false;
}
}

View File

@ -1,18 +1,11 @@
/* typehints:start */
import { Application } from "../application";
/* typehints:end */
import { globalConfig } from "../core/config";
import { createLogger } from "../core/logging";
import { StorageImplBrowserIndexedDB } from "../platform/browser/storage_indexed_db";
import { StorageImplElectron } from "../platform/electron/storage";
import { FILE_NOT_FOUND } from "../platform/storage";
import { Mod } from "./mod";
import { ModInterface } from "./mod_interface";
import { MOD_SIGNALS } from "./mod_signals";
import semverValidRange from "semver/ranges/valid";
import semverSatisifies from "semver/functions/satisfies";
const LOG = createLogger("mods");
/**
@ -104,165 +97,9 @@ export class ModLoader {
};
}
exposeExports() {
if (G_IS_DEV || G_IS_STANDALONE) {
let exports = {};
const modules = require.context("../", true, /\.js$/);
Array.from(modules.keys()).forEach(key => {
// @ts-ignore
const module = modules(key);
for (const member in module) {
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.__$S__(member, v);
},
});
}
});
window.shapez = exports;
}
}
exposeExports() {}
async initMods() {
if (!G_IS_STANDALONE && !G_IS_DEV) {
this.initialized = true;
return;
}
// Create a storage for reading mod settings
const storage = G_IS_STANDALONE
? new StorageImplElectron(this.app)
: new StorageImplBrowserIndexedDB(this.app);
await storage.initialize();
LOG.log("hook:init", this.app, this.app.storage);
this.exposeExports();
let mods = [];
if (G_IS_STANDALONE) {
mods = await ipcRenderer.invoke("get-mods");
}
if (G_IS_DEV && globalConfig.debug.externalModUrl) {
const modURLs = Array.isArray(globalConfig.debug.externalModUrl)
? globalConfig.debug.externalModUrl
: [globalConfig.debug.externalModUrl];
for (let i = 0; i < modURLs.length; i++) {
const response = await fetch(modURLs[i], {
method: "GET",
});
if (response.status !== 200) {
throw new Error(
"Failed to load " + modURLs[i] + ": " + response.status + " " + response.statusText
);
}
mods.push(await response.text());
}
}
window.$shapez_registerMod = (modClass, meta) => {
if (this.initialized) {
throw new Error("Can't register mod after modloader is initialized");
}
if (this.modLoadQueue.some(entry => entry.meta.id === meta.id)) {
console.warn("Not registering mod", meta, "since a mod with the same id is already loaded");
return;
}
this.modLoadQueue.push({
modClass,
meta,
});
};
mods.forEach(modCode => {
modCode += `
if (typeof Mod !== 'undefined') {
if (typeof METADATA !== 'object') {
throw new Error("No METADATA variable found");
}
window.$shapez_registerMod(Mod, METADATA);
}
`;
try {
const func = new Function(modCode);
func();
} catch (ex) {
console.error(ex);
alert("Failed to parse mod (launch with --dev for more info): \n\n" + ex);
}
});
delete window.$shapez_registerMod;
for (let i = 0; i < this.modLoadQueue.length; i++) {
const { modClass, meta } = this.modLoadQueue[i];
const modDataFile = "modsettings_" + meta.id + "__" + meta.version + ".json";
if (meta.minimumGameVersion) {
const minimumGameVersion = meta.minimumGameVersion;
if (!semverValidRange(minimumGameVersion)) {
alert("Mod " + meta.id + " has invalid minimumGameVersion: " + minimumGameVersion);
continue;
}
if (!semverSatisifies(G_BUILD_VERSION, minimumGameVersion)) {
alert(
"Mod '" +
meta.id +
"' is incompatible with this version of the game: \n\n" +
"Mod requires version " +
minimumGameVersion +
" but this game has version " +
G_BUILD_VERSION
);
continue;
}
}
let settings = meta.settings;
if (meta.settings) {
try {
const storedSettings = await storage.readFileAsync(modDataFile);
settings = JSON.parse(storedSettings);
} catch (ex) {
if (ex === FILE_NOT_FOUND) {
// Write default data
await storage.writeFileAsync(modDataFile, JSON.stringify(meta.settings));
} else {
alert("Failed to load settings for " + meta.id + ", will use defaults:\n\n" + ex);
}
}
}
try {
const mod = new modClass({
app: this.app,
modLoader: this,
meta,
settings,
saveSettings: () => storage.writeFileAsync(modDataFile, JSON.stringify(mod.settings)),
});
mod.init();
this.mods.push(mod);
} catch (ex) {
console.error(ex);
alert("Failed to initialize mods (launch with --dev for more info): \n\n" + ex);
}
}
this.modLoadQueue = [];
this.initialized = true;
}
}

View File

@ -18,23 +18,7 @@ const analyticsLocalFile = "shapez_token_123.bin";
export class ShapezGameAnalytics extends GameAnalyticsInterface {
get environment() {
if (G_IS_DEV) {
return "dev";
}
if (G_IS_STANDALONE) {
return "steam";
}
if (G_IS_RELEASE) {
return "prod";
}
if (window.location.host.indexOf("alpha") >= 0) {
return "alpha";
} else {
return "beta";
}
return "steam-demo";
}
/**

View File

@ -31,14 +31,10 @@ export const SOUNDS = {
export const MUSIC = {
// The theme always depends on the standalone only, even if running the full
// version in the browser
theme: G_IS_STANDALONE ? "theme-full" : "theme-short",
theme: "theme-short",
menu: "menu",
};
if (G_IS_STANDALONE || G_IS_DEV) {
MUSIC.puzzle = "puzzle-full";
}
export class SoundInstanceInterface {
constructor(key, url) {
this.key = key;

View File

@ -186,10 +186,7 @@ function initializeSettings() {
if (app.platformWrapper.getSupportsFullscreen()) {
app.platformWrapper.setFullscreen(value);
}
},
/**
* @param {Application} app
*/ app => app.restrictionMgr.getHasExtendedSettings()
}
),
new BoolSetting(

View File

@ -38,16 +38,15 @@ export class MainMenuState extends GameState {
getInnerHTML() {
const showLanguageIcon = !G_CHINA_VERSION && !G_WEGAME_VERSION;
const showExitAppButton = G_IS_STANDALONE;
const showUpdateLabel = !G_WEGAME_VERSION;
const showBrowserWarning = !G_IS_STANDALONE && !isSupportedBrowser();
const showPuzzleDLC = !G_WEGAME_VERSION && (G_IS_STANDALONE || G_IS_DEV);
const showPuzzleDLC = false;
const showWegameFooter = G_WEGAME_VERSION;
const hasMods = MODS.anyModsActive();
const hasMods = false;
let showExternalLinks = true;
if (G_IS_STANDALONE) {
if (G_WEGAME_VERSION || G_CHINA_VERSION) {
if (G_WEGAME_VERSION) {
showExternalLinks = false;
}
} else {
@ -58,19 +57,12 @@ export class MainMenuState extends GameState {
}
let showDiscordLink = showExternalLinks;
if (G_CHINA_VERSION) {
showDiscordLink = true;
}
const showCrosspromo = !G_IS_STANDALONE && showExternalLinks;
const showCrosspromo = false;
const showDemoAdvertisement =
showExternalLinks && this.app.restrictionMgr.getIsStandaloneMarketingActive();
const ownsPuzzleDLC =
G_IS_DEV ||
(G_IS_STANDALONE &&
/** @type { PlatformWrapperImplElectron}*/ (this.app.platformWrapper).dlcs.puzzle);
const ownsPuzzleDLC = false;
const bannerHtml = `
<h3>${T.demoBanners.title}</h3>
<p>${T.demoBanners.intro}</p>
@ -96,7 +88,6 @@ export class MainMenuState extends GameState {
<div class="logo">
<img src="${cachebust("res/" + getLogoSprite())}" alt="shapez.io Logo">
${showUpdateLabel ? `<span class="updateLabel">MODS UPDATE!</span>` : ""}
</div>
<div class="mainWrapper" data-columns="${showDemoAdvertisement || showPuzzleDLC ? 2 : 1}">

View File

@ -1,6 +1,5 @@
import { THIRDPARTY_URLS } from "../core/config";
import { TextualGameState } from "../core/textual_game_state";
import { MODS } from "../mods/modloader";
import { T } from "../translations";
export class ModsState extends TextualGameState {
@ -14,35 +13,29 @@ export class ModsState extends TextualGameState {
internalGetFullHtml() {
let headerHtml = `
<div class="headerBar">
<h1><button class="backButton"></button> ${this.getStateHeaderTitle()}</h1>
<div class="headerBar">
<h1><button class="backButton"></button> ${this.getStateHeaderTitle()}</h1>
<div class="actions">
${
(G_IS_STANDALONE || G_IS_DEV) && MODS.mods.length > 0
? `<button class="styledButton browseMods">${T.mods.browseMods}</button>`
: ""
}
${
G_IS_STANDALONE || G_IS_DEV
? `<button class="styledButton openModsFolder">${T.mods.openFolder}</button>`
: ""
}
</div>
<div class="actions">
${
G_IS_STANDALONE
? `<button class="styledButton browseMods">${T.mods.browseMods}</button>`
: ""
}
</div>
</div>`;
</div>`;
return `
${headerHtml}
<div class="container">
${this.getInnerHTML()}
</div>
`;
${headerHtml}
<div class="container">
${this.getInnerHTML()}
</div>
`;
}
getMainContentHTML() {
if (!G_IS_STANDALONE && !G_IS_DEV) {
return `
return `
<div class="noModSupport">
<p>${T.mods.noModSupport}</p>
@ -52,49 +45,6 @@ export class ModsState extends TextualGameState {
</div>
`;
}
if (MODS.mods.length === 0) {
return `
<div class="modsStats noMods">
${T.mods.modsInfo}
<button class="styledButton browseMods">${T.mods.browseMods}</button>
</div>
`;
}
let modsHtml = ``;
MODS.mods.forEach(mod => {
modsHtml += `
<div class="mod">
<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">${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>
<div class="value checkbox checked">
<span class="knob"></span>
</div>
</div>
`;
});
return `
<div class="modsStats">
${T.mods.modsInfo}
</div>
<div class="modsList">
${modsHtml}
</div>
`;
}
onEnter() {

View File

@ -74,7 +74,7 @@ mainMenu:
puzzleDlcWishlist: 添加愿望单!
puzzleDlcViewNow: 查看DLC
mods:
title: 激活游戏模组Mods
title: 激活游戏模组
warningPuzzleDLC: 无法在任何游戏模组Mods下进行“谜题挑战者”DLC请关闭所有游戏模组Mods
dialogs:
buttons:
@ -139,7 +139,8 @@ dialogs:
desc: 您还没有解锁蓝图功能通过第12关的挑战后可解锁蓝图。
keybindingsIntroduction:
title: 实用快捷键
desc: 这个游戏有很多有用的快捷键设定。以下是其中的一些介绍,记得在<strong>按键设置</strong>中查看其他按键设定!<br><br>
desc:
这个游戏有很多有用的快捷键设定。以下是其中的一些介绍,记得在<strong>按键设置</strong>中查看其他按键设定!<br><br>
<code class='keybinding'>CTRL键</code> + 拖动:选择区域以复制或删除。<br> <code
class='keybinding'>SHIFT键</code> 按住以放置多个同一种设施。<br> <code
class='keybinding'>ALT键</code>:反向放置传送带。<br>
@ -318,15 +319,18 @@ ingame:
hints:
1_1_extractor: 在<strong>圆形</strong>上放置一个<strong>开采器</strong>来获取圆形!<br><br>提示:<strong>按下鼠标左键</strong>选中<strong>开采器</strong>
1_2_conveyor: 用<strong>传送带</strong>将您的开采器连接到中心基地上!<br><br>提示:选中<strong>传送带</strong>后<strong>按下鼠标左键可拖动</strong>布置传送带!
1_3_expand: 您可以放置更多的<strong>开采器</strong>和<strong>传送带</strong>来更有效率地完成关卡目标。<br><br>
1_3_expand:
您可以放置更多的<strong>开采器</strong>和<strong>传送带</strong>来更有效率地完成关卡目标。<br><br>
提示:按住 <strong>SHIFT</strong>
键可放置多个<strong>开采器</strong>,注意用<strong>R</strong>
键可旋转<strong>开采器</strong>的出口方向,确保开采的图形可以顺利传送。
2_1_place_cutter: 现在放置一个<strong>切割器</strong>,这个设施可把<strong>圆形</strong>切成两半!<br><br>注意:无论如何放置,切割机总是<strong>从上到下</strong>切割。
2_2_place_trash: 使用切割机后产生的废弃图形会导致<strong>堵塞</strong>。<br><br>注意使用<strong>垃圾桶</strong>清除当前
2_2_place_trash:
使用切割机后产生的废弃图形会导致<strong>堵塞</strong>。<br><br>注意使用<strong>垃圾桶</strong>清除当前
(!) 不需要的废物。
2_3_more_cutters: 干的好!现在放置<strong>2个以上的切割机</strong>来加快当前缓慢的过程!<br><br>提示:用<strong>快捷键0-9</strong>可以快速选择各项设施!
3_1_rectangles: 现在让我们开采一些矩形!找到<strong>矩形地带</strong>并<strong>放置4个开采器</strong>并将它们用<strong>传送带</strong>连接到中心基地。<br><br>
3_1_rectangles:
现在让我们开采一些矩形!找到<strong>矩形地带</strong>并<strong>放置4个开采器</strong>并将它们用<strong>传送带</strong>连接到中心基地。<br><br>
提示:选中<strong>传送带</strong>后按住<strong>SHIFT键</strong>可快速准确地规划<strong>传送带路线!</strong>
21_1_place_quad_painter: 放置<strong>四口上色器</strong>并且获取一些<strong>圆形</strong><strong>白色</strong>和<strong>红色</strong>
21_2_switch_to_wires: 按 <strong>E</strong> 键选择<strong>电线层</strong><br><br>
@ -620,7 +624,8 @@ storyRewards:
desc: 恭喜!您解锁了<strong>旋转机</strong>。它会顺时针将输入的<strong>图形旋转90度</strong>。
reward_painter:
title: 上色
desc: 恭喜!您解锁了<strong>上色器</strong>。开采一些颜色(就像您开采图形一样),将其在上色器中与图形结合来将图形上色!
desc:
恭喜!您解锁了<strong>上色器</strong>。开采一些颜色(就像您开采图形一样),将其在上色器中与图形结合来将图形上色!
<br>注意:如果您不幸患有色盲,可以在设置中启用<strong>色盲模式</strong>
reward_mixer:
title: 混合颜色
@ -637,11 +642,13 @@ storyRewards:
desc: 恭喜!您解锁了<strong>隧道</strong>。它可放置在<strong>传送带</strong>或<strong>设施</strong>下方以运送物品。
reward_rotater_ccw:
title: 逆时针旋转
desc: 恭喜!您解锁了<strong>旋转机</strong>的<strong>逆时针</strong>变体。它可以逆时针旋转<strong>图形</strong>。
desc:
恭喜!您解锁了<strong>旋转机</strong>的<strong>逆时针</strong>变体。它可以逆时针旋转<strong>图形</strong>。
<br>选择<strong>旋转机</strong>然后按"T"键来选取这个变体。
reward_miner_chainable:
title: 链式开采器
desc: 您已经解锁了<strong>链式开采器</strong>!它能<strong>转发资源</strong>给其他的开采器,这样您就能更有效率的开采各类资源了!<br><br>
desc:
您已经解锁了<strong>链式开采器</strong>!它能<strong>转发资源</strong>给其他的开采器,这样您就能更有效率的开采各类资源了!<br><br>
注意:新的开采器已替换了工具栏里旧的开采器!
reward_underground_belt_tier_2:
title: 二级隧道
@ -658,12 +665,14 @@ storyRewards:
<br>它<strong>优先从左边</strong>输出,这样您就可以用它做一个<strong>溢流门</strong>了!
reward_freeplay:
title: 自由模式
desc: 成功了!您解锁了<strong>自由模式</strong>!挑战升级!这意味着现在将<strong>随机</strong>生成图形!
desc:
成功了!您解锁了<strong>自由模式</strong>!挑战升级!这意味着现在将<strong>随机</strong>生成图形!
从现在起,中心基地最为需要的是<strong>产量</strong>,我强烈建议您去制造一台能够自动交付所需图形的机器!<br><br>
基地会在<strong>电线层</strong>输出需要的图形,您需要去分析图形并在此基础上自动配置您的工厂。
reward_blueprints:
title: 蓝图
desc: 您现在可以<strong>复制粘贴</strong>您的工厂的一部分了!按住 CTRL键并拖动鼠标来选择一块区域然后按C键复制。
desc:
您现在可以<strong>复制粘贴</strong>您的工厂的一部分了!按住 CTRL键并拖动鼠标来选择一块区域然后按C键复制。
<br><br>粘贴并<strong>不是免费的</strong>,您需要制造<strong>蓝图图形</strong>来负担。蓝图图形是您刚刚交付的图形。
no_reward:
title: 下一关
@ -691,7 +700,8 @@ storyRewards:
<br>注意:您注意到<strong>传送读取器</strong>和<strong>存储器</strong>输出的他们最后读取的物品了吗?试着在显示屏上展示一下!
reward_constant_signal:
title: 恒定信号
desc: 恭喜!您解锁了生成于电线层之上的<strong>恒定信号</strong>,把它连接到<strong>过滤器</strong>时非常有用。
desc:
恭喜!您解锁了生成于电线层之上的<strong>恒定信号</strong>,把它连接到<strong>过滤器</strong>时非常有用。
<br>比如它能发出图形、颜色、开关值1 / 0的固定信号。
reward_logic_gates:
title: 逻辑门
@ -710,7 +720,8 @@ storyRewards:
<strong>提示</strong>:可在设置中打开电线层教程!
reward_filter:
title: 物品过滤器
desc: 恭喜!您解锁了<strong>物品过滤器</strong>!它会根据在电线层上输入的信号决定是从上面还是右边输出物品。<br><br>
desc:
恭喜!您解锁了<strong>物品过滤器</strong>!它会根据在电线层上输入的信号决定是从上面还是右边输出物品。<br><br>
您也可以输入开关值1 / 0信号来激活或者禁用它。
reward_demo_end:
title: 试玩结束
@ -1093,7 +1104,7 @@ backendErrors:
too-many-likes-already: 您的谜题已经得到了许多玩家的赞赏。如果您仍然希望删除它请联系support@shapez.io!
no-permission: 您没有执行此操作的权限。
mods:
title: 游戏模组Mods
title: 游戏模组
author: 作者
version: 版本
openFolder: 打开游戏模组Mods文件夹
@ -1103,6 +1114,7 @@ mods:
noModSupport: 您需要在Steam平台获得完整版才可以安装游戏模组Mods
togglingComingSoon:
title: 即将开放
description: 当前只能通过将游戏模组Mods文件复制到mods文件夹或从mods文件夹移除来启用或禁用游戏模组Mods
description:
当前只能通过将游戏模组Mods文件复制到mods文件夹或从mods文件夹移除来启用或禁用游戏模组Mods
但是可以切换游戏模组Mods已经计划在之后的更新中实现
modWebsite: Website