diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
| commit | 31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch) | |
| tree | 8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/launcher/minecraft/mod | |
| parent | 934382c8a1ce738589dee9ee0f14e1cec812770e (diff) | |
| parent | fad6a1066616b69d7f5fef01178efdf014c59537 (diff) | |
| download | Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip | |
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc
git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e
git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/launcher/minecraft/mod')
| -rw-r--r-- | meshmc/launcher/minecraft/mod/LocalModParseTask.cpp | 423 | ||||
| -rw-r--r-- | meshmc/launcher/minecraft/mod/LocalModParseTask.h | 59 | ||||
| -rw-r--r-- | meshmc/launcher/minecraft/mod/Mod.cpp | 158 | ||||
| -rw-r--r-- | meshmc/launcher/minecraft/mod/Mod.h | 140 | ||||
| -rw-r--r-- | meshmc/launcher/minecraft/mod/ModDetails.h | 37 | ||||
| -rw-r--r-- | meshmc/launcher/minecraft/mod/ModFolderLoadTask.cpp | 38 | ||||
| -rw-r--r-- | meshmc/launcher/minecraft/mod/ModFolderLoadTask.h | 52 | ||||
| -rw-r--r-- | meshmc/launcher/minecraft/mod/ModFolderModel.cpp | 573 | ||||
| -rw-r--r-- | meshmc/launcher/minecraft/mod/ModFolderModel.h | 169 | ||||
| -rw-r--r-- | meshmc/launcher/minecraft/mod/ModFolderModel_test.cpp | 71 | ||||
| -rw-r--r-- | meshmc/launcher/minecraft/mod/ResourcePackFolderModel.cpp | 50 | ||||
| -rw-r--r-- | meshmc/launcher/minecraft/mod/ResourcePackFolderModel.h | 35 | ||||
| -rw-r--r-- | meshmc/launcher/minecraft/mod/TexturePackFolderModel.cpp | 50 | ||||
| -rw-r--r-- | meshmc/launcher/minecraft/mod/TexturePackFolderModel.h | 35 |
14 files changed, 1890 insertions, 0 deletions
diff --git a/meshmc/launcher/minecraft/mod/LocalModParseTask.cpp b/meshmc/launcher/minecraft/mod/LocalModParseTask.cpp new file mode 100644 index 0000000000..09ff5f20ae --- /dev/null +++ b/meshmc/launcher/minecraft/mod/LocalModParseTask.cpp @@ -0,0 +1,423 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "LocalModParseTask.h" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonValue> +#include <toml.h> + +#include "MMCZip.h" + +#include "settings/INIFile.h" +#include "FileSystem.h" + +namespace +{ + + // NEW format + // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 + + // OLD format: + // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc + std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents) + { + auto getInfoFromArray = + [&](QJsonArray arr) -> std::shared_ptr<ModDetails> { + if (!arr.at(0).isObject()) { + return nullptr; + } + std::shared_ptr<ModDetails> details = + std::make_shared<ModDetails>(); + auto firstObj = arr.at(0).toObject(); + details->mod_id = firstObj.value("modid").toString(); + auto name = firstObj.value("name").toString(); + // NOTE: ignore stupid example mods copies where the author didn't + // even bother to change the name + if (name != "Example Mod") { + details->name = name; + } + details->version = firstObj.value("version").toString(); + details->updateurl = firstObj.value("updateUrl").toString(); + auto homeurl = firstObj.value("url").toString().trimmed(); + if (!homeurl.isEmpty()) { + // fix up url. + if (!homeurl.startsWith("http://") && + !homeurl.startsWith("https://") && + !homeurl.startsWith("ftp://")) { + homeurl.prepend("http://"); + } + } + details->homeurl = homeurl; + details->description = firstObj.value("description").toString(); + QJsonArray authors = firstObj.value("authorList").toArray(); + if (authors.size() == 0) { + // FIXME: what is the format of this? is there any? + authors = firstObj.value("authors").toArray(); + } + + for (auto author : authors) { + details->authors.append(author.toString()); + } + details->credits = firstObj.value("credits").toString(); + return details; + }; + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + // this is the very old format that had just the array + if (jsonDoc.isArray()) { + return getInfoFromArray(jsonDoc.array()); + } else if (jsonDoc.isObject()) { + auto val = jsonDoc.object().value("modinfoversion"); + if (val.isUndefined()) { + val = jsonDoc.object().value("modListVersion"); + } + int version = val.toDouble(); + if (version != 2) { + qCritical() << "BAD stuff happened to mod json:"; + qCritical() << contents; + return nullptr; + } + auto arrVal = jsonDoc.object().value("modlist"); + if (arrVal.isUndefined()) { + arrVal = jsonDoc.object().value("modList"); + } + if (arrVal.isArray()) { + return getInfoFromArray(arrVal.toArray()); + } + } + return nullptr; + } + + // https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md + std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents) + { + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + + char errbuf[200]; + // top-level table + toml_table_t* tomlData = + toml_parse(contents.data(), errbuf, sizeof(errbuf)); + + if (!tomlData) { + return nullptr; + } + + // array defined by [[mods]] + toml_array_t* tomlModsArr = toml_array_in(tomlData, "mods"); + if (!tomlModsArr) { + qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!"; + return nullptr; + } + + // we only really care about the first element, since multiple mods in + // one file is not supported by us at the moment + toml_table_t* tomlModsTable0 = toml_table_at(tomlModsArr, 0); + if (!tomlModsTable0) { + qWarning() << "Corrupted mods.toml? [[mods]] didn't have an " + "element at index 0!"; + return nullptr; + } + + // mandatory properties - always in [[mods]] + toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId"); + if (modIdDatum.ok) { + details->mod_id = modIdDatum.u.s; + // library says this is required for strings + free(modIdDatum.u.s); + } + toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version"); + if (versionDatum.ok) { + details->version = versionDatum.u.s; + free(versionDatum.u.s); + } + toml_datum_t displayNameDatum = + toml_string_in(tomlModsTable0, "displayName"); + if (displayNameDatum.ok) { + details->name = displayNameDatum.u.s; + free(displayNameDatum.u.s); + } + toml_datum_t descriptionDatum = + toml_string_in(tomlModsTable0, "description"); + if (descriptionDatum.ok) { + details->description = descriptionDatum.u.s; + free(descriptionDatum.u.s); + } + + // optional properties - can be in the root table or [[mods]] + toml_datum_t authorsDatum = toml_string_in(tomlData, "authors"); + QString authors = ""; + if (authorsDatum.ok) { + authors = authorsDatum.u.s; + free(authorsDatum.u.s); + } else { + authorsDatum = toml_string_in(tomlModsTable0, "authors"); + if (authorsDatum.ok) { + authors = authorsDatum.u.s; + free(authorsDatum.u.s); + } + } + if (!authors.isEmpty()) { + // author information is stored as a string now, not a list + details->authors.append(authors); + } + // is credits even used anywhere? including this for completion/parity + // with old data version + toml_datum_t creditsDatum = toml_string_in(tomlData, "credits"); + QString credits = ""; + if (creditsDatum.ok) { + authors = creditsDatum.u.s; + free(creditsDatum.u.s); + } else { + creditsDatum = toml_string_in(tomlModsTable0, "credits"); + if (creditsDatum.ok) { + credits = creditsDatum.u.s; + free(creditsDatum.u.s); + } + } + details->credits = credits; + toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL"); + QString homeurl = ""; + if (homeurlDatum.ok) { + homeurl = homeurlDatum.u.s; + free(homeurlDatum.u.s); + } else { + homeurlDatum = toml_string_in(tomlModsTable0, "displayURL"); + if (homeurlDatum.ok) { + homeurl = homeurlDatum.u.s; + free(homeurlDatum.u.s); + } + } + if (!homeurl.isEmpty()) { + // fix up url. + if (!homeurl.startsWith("http://") && + !homeurl.startsWith("https://") && + !homeurl.startsWith("ftp://")) { + homeurl.prepend("http://"); + } + } + details->homeurl = homeurl; + + // this seems to be recursive, so it should free everything + toml_free(tomlData); + + return details; + } + + // https://fabricmc.net/wiki/documentation:fabric_mod_json + std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents) + { + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + auto schemaVersion = object.contains("schemaVersion") + ? object.value("schemaVersion").toInt(0) + : 0; + + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + + details->mod_id = object.value("id").toString(); + details->version = object.value("version").toString(); + + details->name = object.contains("name") + ? object.value("name").toString() + : details->mod_id; + details->description = object.value("description").toString(); + + if (schemaVersion >= 1) { + QJsonArray authors = object.value("authors").toArray(); + for (auto author : authors) { + if (author.isObject()) { + details->authors.append( + author.toObject().value("name").toString()); + } else { + details->authors.append(author.toString()); + } + } + + if (object.contains("contact")) { + QJsonObject contact = object.value("contact").toObject(); + + if (contact.contains("homepage")) { + details->homeurl = contact.value("homepage").toString(); + } + } + } + return details; + } + + std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents) + { + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + // Read the data + details->name = "Minecraft Forge"; + details->mod_id = "Forge"; + details->homeurl = "http://www.minecraftforge.net/forum/"; + INIFile ini; + if (!ini.loadFile(contents)) + return details; + + QString major = ini.get("forge.major.number", "0").toString(); + QString minor = ini.get("forge.minor.number", "0").toString(); + QString revision = ini.get("forge.revision.number", "0").toString(); + QString build = ini.get("forge.build.number", "0").toString(); + + details->version = major + "." + minor + "." + revision + "." + build; + return details; + } + + std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents) + { + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + if (object.contains("name")) { + details->mod_id = details->name = object.value("name").toString(); + } + if (object.contains("version")) { + details->version = object.value("version").toString(""); + } else { + details->version = object.value("revision").toString(""); + } + details->mcversion = object.value("mcversion").toString(); + auto author = object.value("author").toString(); + if (!author.isEmpty()) { + details->authors.append(author); + } + details->description = object.value("description").toString(); + details->homeurl = object.value("url").toString(); + return details; + } + +} // namespace + +LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, + const QFileInfo& modFile) + : m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) +{ +} + +void LocalModParseTask::processAsZip() +{ + QString zipPath = m_modFile.filePath(); + + QByteArray modsToml = + MMCZip::readFileFromZip(zipPath, "META-INF/mods.toml"); + if (!modsToml.isEmpty()) { + m_result->details = ReadMCModTOML(modsToml); + + // to replace ${file.jarVersion} with the actual version, as needed + if (m_result->details && + m_result->details->version == "${file.jarVersion}") { + QByteArray manifestData = + MMCZip::readFileFromZip(zipPath, "META-INF/MANIFEST.MF"); + if (!manifestData.isEmpty()) { + // quick and dirty line-by-line parser + auto manifestLines = manifestData.split('\n'); + QString manifestVersion = ""; + for (auto& line : manifestLines) { + if (QString(line).startsWith("Implementation-Version: ")) { + manifestVersion = + QString(line).remove("Implementation-Version: "); + break; + } + } + + // some mods use ${projectversion} in their build.gradle, + // causing this mess to show up in MANIFEST.MF also keep with + // forge's behavior of setting the version to "NONE" if none is + // found + if (manifestVersion.contains( + "task ':jar' property 'archiveVersion'") || + manifestVersion == "") { + manifestVersion = "NONE"; + } + + m_result->details->version = manifestVersion; + } + } + return; + } + + QByteArray mcmodInfo = MMCZip::readFileFromZip(zipPath, "mcmod.info"); + if (!mcmodInfo.isEmpty()) { + m_result->details = ReadMCModInfo(mcmodInfo); + return; + } + + QByteArray fabricModJson = + MMCZip::readFileFromZip(zipPath, "fabric.mod.json"); + if (!fabricModJson.isEmpty()) { + m_result->details = ReadFabricModInfo(fabricModJson); + return; + } + + QByteArray forgeVersionProps = + MMCZip::readFileFromZip(zipPath, "forgeversion.properties"); + if (!forgeVersionProps.isEmpty()) { + m_result->details = ReadForgeInfo(forgeVersionProps); + return; + } +} + +void LocalModParseTask::processAsFolder() +{ + QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info")); + if (mcmod_info.isFile()) { + QFile mcmod(mcmod_info.filePath()); + if (!mcmod.open(QIODevice::ReadOnly)) + return; + auto data = mcmod.readAll(); + if (data.isEmpty() || data.isNull()) + return; + m_result->details = ReadMCModInfo(data); + } +} + +void LocalModParseTask::processAsLitemod() +{ + QByteArray litemodJson = + MMCZip::readFileFromZip(m_modFile.filePath(), "litemod.json"); + if (!litemodJson.isEmpty()) { + m_result->details = ReadLiteModInfo(litemodJson); + } +} + +void LocalModParseTask::run() +{ + switch (m_type) { + case Mod::MOD_ZIPFILE: + processAsZip(); + break; + case Mod::MOD_FOLDER: + processAsFolder(); + break; + case Mod::MOD_LITEMOD: + processAsLitemod(); + break; + default: + break; + } + emit finished(m_token); +} diff --git a/meshmc/launcher/minecraft/mod/LocalModParseTask.h b/meshmc/launcher/minecraft/mod/LocalModParseTask.h new file mode 100644 index 0000000000..bd85bbc27b --- /dev/null +++ b/meshmc/launcher/minecraft/mod/LocalModParseTask.h @@ -0,0 +1,59 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once +#include <QRunnable> +#include <QDebug> +#include <QObject> +#include "Mod.h" +#include "ModDetails.h" + +class LocalModParseTask : public QObject, public QRunnable +{ + Q_OBJECT + public: + struct Result { + QString id; + std::shared_ptr<ModDetails> details; + }; + using ResultPtr = std::shared_ptr<Result>; + ResultPtr result() const + { + return m_result; + } + + LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile); + void run(); + + signals: + void finished(int token); + + private: + void processAsZip(); + void processAsFolder(); + void processAsLitemod(); + + private: + int m_token; + Mod::ModType m_type; + QFileInfo m_modFile; + ResultPtr m_result; +}; diff --git a/meshmc/launcher/minecraft/mod/Mod.cpp b/meshmc/launcher/minecraft/mod/Mod.cpp new file mode 100644 index 0000000000..dae36f968e --- /dev/null +++ b/meshmc/launcher/minecraft/mod/Mod.cpp @@ -0,0 +1,158 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <QDir> +#include <QString> + +#include "Mod.h" +#include <QDebug> +#include <FileSystem.h> + +namespace +{ + + ModDetails invalidDetails; + +} + +Mod::Mod(const QFileInfo& file) +{ + repath(file); + m_changedDateTime = file.lastModified(); +} + +void Mod::repath(const QFileInfo& file) +{ + m_file = file; + QString name_base = file.fileName(); + + m_type = Mod::MOD_UNKNOWN; + + m_mmc_id = name_base; + + if (m_file.isDir()) { + m_type = MOD_FOLDER; + m_name = name_base; + } else if (m_file.isFile()) { + if (name_base.endsWith(".disabled")) { + m_enabled = false; + name_base.chop(9); + } else { + m_enabled = true; + } + if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) { + m_type = MOD_ZIPFILE; + name_base.chop(4); + } else if (name_base.endsWith(".litemod")) { + m_type = MOD_LITEMOD; + name_base.chop(8); + } else { + m_type = MOD_SINGLEFILE; + } + m_name = name_base; + } +} + +bool Mod::enable(bool value) +{ + if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) + return false; + + if (m_enabled == value) + return false; + + QString path = m_file.absoluteFilePath(); + if (value) { + QFile foo(path); + if (!path.endsWith(".disabled")) + return false; + path.chop(9); + if (!foo.rename(path)) + return false; + } else { + QFile foo(path); + path += ".disabled"; + if (!foo.rename(path)) + return false; + } + repath(QFileInfo(path)); + m_enabled = value; + return true; +} + +bool Mod::destroy() +{ + m_type = MOD_UNKNOWN; + return FS::deletePath(m_file.filePath()); +} + +const ModDetails& Mod::details() const +{ + if (!m_localDetails) + return invalidDetails; + return *m_localDetails; +} + +QString Mod::version() const +{ + return details().version; +} + +QString Mod::name() const +{ + auto& d = details(); + if (!d.name.isEmpty()) { + return d.name; + } + return m_name; +} + +QString Mod::homeurl() const +{ + return details().homeurl; +} + +QString Mod::description() const +{ + return details().description; +} + +QStringList Mod::authors() const +{ + return details().authors; +} diff --git a/meshmc/launcher/minecraft/mod/Mod.h b/meshmc/launcher/minecraft/mod/Mod.h new file mode 100644 index 0000000000..430ae9519d --- /dev/null +++ b/meshmc/launcher/minecraft/mod/Mod.h @@ -0,0 +1,140 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include <QFileInfo> +#include <QDateTime> +#include <QList> +#include <memory> + +#include "ModDetails.h" + +class Mod +{ + public: + enum ModType { + MOD_UNKNOWN, //!< Indicates an unspecified mod type. + MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class + //!< files. + MOD_SINGLEFILE, //!< The mod is a single file (not a zip file). + MOD_FOLDER, //!< The mod is in a folder on the filesystem. + MOD_LITEMOD, //!< The mod is a litemod + }; + + Mod() = default; + Mod(const QFileInfo& file); + + QFileInfo filename() const + { + return m_file; + } + QString mmc_id() const + { + return m_mmc_id; + } + ModType type() const + { + return m_type; + } + bool valid() + { + return m_type != MOD_UNKNOWN; + } + + QDateTime dateTimeChanged() const + { + return m_changedDateTime; + } + + bool enabled() const + { + return m_enabled; + } + + const ModDetails& details() const; + + QString name() const; + QString version() const; + QString homeurl() const; + QString description() const; + QStringList authors() const; + + bool enable(bool value); + + // delete all the files of this mod + bool destroy(); + + // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) + void repath(const QFileInfo& file); + + bool shouldResolve() + { + return !m_resolving && !m_resolved; + } + bool isResolving() + { + return m_resolving; + } + int resolutionTicket() + { + return m_resolutionTicket; + } + void setResolving(bool resolving, int resolutionTicket) + { + m_resolving = resolving; + m_resolutionTicket = resolutionTicket; + } + void finishResolvingWithDetails(std::shared_ptr<ModDetails> details) + { + m_resolving = false; + m_resolved = true; + m_localDetails = details; + } + + protected: + QFileInfo m_file; + QDateTime m_changedDateTime; + QString m_mmc_id; + QString m_name; + bool m_enabled = true; + bool m_resolving = false; + bool m_resolved = false; + int m_resolutionTicket = 0; + ModType m_type = MOD_UNKNOWN; + std::shared_ptr<ModDetails> m_localDetails; +}; diff --git a/meshmc/launcher/minecraft/mod/ModDetails.h b/meshmc/launcher/minecraft/mod/ModDetails.h new file mode 100644 index 0000000000..37aa78b7cf --- /dev/null +++ b/meshmc/launcher/minecraft/mod/ModDetails.h @@ -0,0 +1,37 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <QString> +#include <QStringList> + +struct ModDetails { + QString mod_id; + QString name; + QString version; + QString mcversion; + QString homeurl; + QString updateurl; + QString description; + QStringList authors; + QString credits; +}; diff --git a/meshmc/launcher/minecraft/mod/ModFolderLoadTask.cpp b/meshmc/launcher/minecraft/mod/ModFolderLoadTask.cpp new file mode 100644 index 0000000000..9272009539 --- /dev/null +++ b/meshmc/launcher/minecraft/mod/ModFolderLoadTask.cpp @@ -0,0 +1,38 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "ModFolderLoadTask.h" +#include <QDebug> + +ModFolderLoadTask::ModFolderLoadTask(QDir dir) + : m_dir(dir), m_result(new Result()) +{ +} + +void ModFolderLoadTask::run() +{ + m_dir.refresh(); + for (auto entry : m_dir.entryInfoList()) { + Mod m(entry); + m_result->mods[m.mmc_id()] = m; + } + emit succeeded(); +} diff --git a/meshmc/launcher/minecraft/mod/ModFolderLoadTask.h b/meshmc/launcher/minecraft/mod/ModFolderLoadTask.h new file mode 100644 index 0000000000..1aaafa6bbb --- /dev/null +++ b/meshmc/launcher/minecraft/mod/ModFolderLoadTask.h @@ -0,0 +1,52 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once +#include <QRunnable> +#include <QObject> +#include <QDir> +#include <QMap> +#include "Mod.h" +#include <memory> + +class ModFolderLoadTask : public QObject, public QRunnable +{ + Q_OBJECT + public: + struct Result { + QMap<QString, Mod> mods; + }; + using ResultPtr = std::shared_ptr<Result>; + ResultPtr result() const + { + return m_result; + } + + public: + ModFolderLoadTask(QDir dir); + void run(); + signals: + void succeeded(); + + private: + QDir m_dir; + ResultPtr m_result; +}; diff --git a/meshmc/launcher/minecraft/mod/ModFolderModel.cpp b/meshmc/launcher/minecraft/mod/ModFolderModel.cpp new file mode 100644 index 0000000000..faaa814590 --- /dev/null +++ b/meshmc/launcher/minecraft/mod/ModFolderModel.cpp @@ -0,0 +1,573 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModFolderModel.h" +#include <FileSystem.h> +#include <QMimeData> +#include <QUrl> +#include <QUuid> +#include <QString> +#include <QFileSystemWatcher> +#include <QDebug> +#include "ModFolderLoadTask.h" +#include <QThreadPool> +#include <algorithm> +#include "LocalModParseTask.h" + +ModFolderModel::ModFolderModel(const QString& dir) + : QAbstractListModel(), m_dir(dir) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | + QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher = new QFileSystemWatcher(this); + connect(m_watcher, SIGNAL(directoryChanged(QString)), this, + SLOT(directoryChanged(QString))); +} + +void ModFolderModel::startWatching() +{ + if (is_watching) + return; + + update(); + + is_watching = m_watcher->addPath(m_dir.absolutePath()); + if (is_watching) { + qDebug() << "Started watching " << m_dir.absolutePath(); + } else { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void ModFolderModel::stopWatching() +{ + if (!is_watching) + return; + + is_watching = !m_watcher->removePath(m_dir.absolutePath()); + if (!is_watching) { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } else { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +bool ModFolderModel::update() +{ + if (!isValid()) { + return false; + } + if (m_update) { + scheduled_update = true; + return true; + } + + auto task = new ModFolderLoadTask(m_dir); + m_update = task->result(); + QThreadPool* threadPool = QThreadPool::globalInstance(); + connect(task, &ModFolderLoadTask::succeeded, this, + &ModFolderModel::finishUpdate); + threadPool->start(task); + return true; +} + +void ModFolderModel::finishUpdate() +{ + auto keys1 = modsIndex.keys(); + QSet<QString> currentSet(keys1.begin(), keys1.end()); + auto& newMods = m_update->mods; + auto keys2 = newMods.keys(); + QSet<QString> newSet(keys2.begin(), keys2.end()); + + // see if the kept mods changed in some way + { + QSet<QString> kept = currentSet; + kept.intersect(newSet); + for (auto& keptMod : kept) { + auto& newMod = newMods[keptMod]; + auto row = modsIndex[keptMod]; + auto& currentMod = mods[row]; + if (newMod.dateTimeChanged() == currentMod.dateTimeChanged()) { + // no significant change, ignore... + continue; + } + auto& oldMod = mods[row]; + if (oldMod.isResolving()) { + activeTickets.remove(oldMod.resolutionTicket()); + } + oldMod = newMod; + resolveMod(mods[row]); + emit dataChanged(index(row, 0), + index(row, columnCount(QModelIndex()) - 1)); + } + } + + // remove mods no longer present + { + QSet<QString> removed = currentSet; + QList<int> removedRows; + removed.subtract(newSet); + for (auto& removedMod : removed) { + removedRows.append(modsIndex[removedMod]); + } + std::sort(removedRows.begin(), removedRows.end(), std::greater<int>()); + for (auto iter = removedRows.begin(); iter != removedRows.end(); + iter++) { + int removedIndex = *iter; + beginRemoveRows(QModelIndex(), removedIndex, removedIndex); + auto removedIter = mods.begin() + removedIndex; + if (removedIter->isResolving()) { + activeTickets.remove(removedIter->resolutionTicket()); + } + mods.erase(removedIter); + endRemoveRows(); + } + } + + // add new mods to the end + { + QSet<QString> added = newSet; + added.subtract(currentSet); + beginInsertRows(QModelIndex(), mods.size(), + mods.size() + added.size() - 1); + for (auto& addedMod : added) { + mods.append(newMods[addedMod]); + resolveMod(mods.last()); + } + endInsertRows(); + } + + // update index + { + modsIndex.clear(); + int idx = 0; + for (auto& mod : mods) { + modsIndex[mod.mmc_id()] = idx; + idx++; + } + } + + m_update.reset(); + + emit updateFinished(); + + if (scheduled_update) { + scheduled_update = false; + update(); + } +} + +void ModFolderModel::resolveMod(Mod& m) +{ + if (!m.shouldResolve()) { + return; + } + + auto task = + new LocalModParseTask(nextResolutionTicket, m.type(), m.filename()); + auto result = task->result(); + result->id = m.mmc_id(); + activeTickets.insert(nextResolutionTicket, result); + m.setResolving(true, nextResolutionTicket); + nextResolutionTicket++; + QThreadPool* threadPool = QThreadPool::globalInstance(); + connect(task, &LocalModParseTask::finished, this, + &ModFolderModel::finishModParse); + threadPool->start(task); +} + +void ModFolderModel::finishModParse(int token) +{ + auto iter = activeTickets.find(token); + if (iter == activeTickets.end()) { + return; + } + auto result = *iter; + activeTickets.remove(token); + int row = modsIndex[result->id]; + auto& mod = mods[row]; + mod.finishResolvingWithDetails(result->details); + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); +} + +void ModFolderModel::disableInteraction(bool disabled) +{ + if (interaction_disabled == disabled) { + return; + } + interaction_disabled = disabled; + if (size()) { + emit dataChanged(index(0), index(size() - 1)); + } +} + +void ModFolderModel::directoryChanged(QString path) +{ + update(); +} + +bool ModFolderModel::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +// FIXME: this does not take disabled mod (with extra .disable extension) into +// account... +bool ModFolderModel::installMod(const QString& filename) +{ + if (interaction_disabled) { + return false; + } + + // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using + // the empty result of QFileInfo::fileName + auto originalPath = FS::NormalizePath(filename); + QFileInfo fileinfo(originalPath); + + if (!fileinfo.exists() || !fileinfo.isReadable()) { + qWarning() << "Caught attempt to install non-existing file or " + "file-like object:" + << originalPath; + return false; + } + qDebug() << "installing: " << fileinfo.absoluteFilePath(); + + Mod installedMod(fileinfo); + if (!installedMod.valid()) { + qDebug() << originalPath << "is not a valid mod. Ignoring it."; + return false; + } + + auto type = installedMod.type(); + if (type == Mod::MOD_UNKNOWN) { + qDebug() << "Cannot recognize mod type of" << originalPath + << ", ignoring it."; + return false; + } + + auto newpath = + FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName())); + if (originalPath == newpath) { + qDebug() << "Overwriting the mod (" << originalPath + << ") with itself makes no sense..."; + return false; + } + + if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || + type == Mod::MOD_LITEMOD) { + if (QFile::exists(newpath) || + QFile::exists(newpath + QString(".disabled"))) { + if (!QFile::remove(newpath)) { + // FIXME: report error in a user-visible way + qWarning() << "Copy from" << originalPath << "to" << newpath + << "has failed."; + return false; + } + qDebug() << newpath << "has been deleted."; + } + if (!QFile::copy(fileinfo.filePath(), newpath)) { + qWarning() << "Copy from" << originalPath << "to" << newpath + << "has failed."; + // FIXME: report error in a user-visible way + return false; + } + FS::updateTimestamp(newpath); + installedMod.repath(QFileInfo(newpath)); + update(); + return true; + } else if (type == Mod::MOD_FOLDER) { + QString from = fileinfo.filePath(); + if (QFile::exists(newpath)) { + qDebug() << "Ignoring folder " << from << ", it would merge with " + << newpath; + return false; + } + + if (!FS::copy(from, newpath)()) { + qWarning() << "Copy of folder from" << originalPath << "to" + << newpath << "has (potentially partially) failed."; + return false; + } + installedMod.repath(QFileInfo(newpath)); + update(); + return true; + } + return false; +} + +bool ModFolderModel::setModStatus(const QModelIndexList& indexes, + ModStatusAction enable) +{ + if (interaction_disabled) { + return false; + } + + if (indexes.isEmpty()) + return true; + + for (auto index : indexes) { + if (index.column() != 0) { + continue; + } + setModStatus(index.row(), enable); + } + return true; +} + +bool ModFolderModel::deleteMods(const QModelIndexList& indexes) +{ + if (interaction_disabled) { + return false; + } + + if (indexes.isEmpty()) + return true; + + for (auto i : indexes) { + Mod& m = mods[i.row()]; + m.destroy(); + } + return true; +} + +int ModFolderModel::columnCount(const QModelIndex& parent) const +{ + return NUM_COLUMNS; +} + +QVariant ModFolderModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= mods.size()) + return QVariant(); + + switch (role) { + case Qt::DisplayRole: + switch (column) { + case NameColumn: + return mods[row].name(); + case VersionColumn: { + switch (mods[row].type()) { + case Mod::MOD_FOLDER: + return tr("Folder"); + case Mod::MOD_SINGLEFILE: + return tr("File"); + default: + break; + } + return mods[row].version(); + } + case DateColumn: + return mods[row].dateTimeChanged(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return mods[row].mmc_id(); + + case Qt::CheckStateRole: + switch (column) { + case ActiveColumn: + return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +bool ModFolderModel::setData(const QModelIndex& index, const QVariant& value, + int role) +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) { + return false; + } + + if (role == Qt::CheckStateRole) { + return setModStatus(index.row(), Toggle); + } + return false; +} + +bool ModFolderModel::setModStatus(int row, + ModFolderModel::ModStatusAction action) +{ + if (row < 0 || row >= mods.size()) { + return false; + } + + auto& mod = mods[row]; + bool desiredStatus; + switch (action) { + case Enable: + desiredStatus = true; + break; + case Disable: + desiredStatus = false; + break; + case Toggle: + default: + desiredStatus = !mod.enabled(); + break; + } + + if (desiredStatus == mod.enabled()) { + return true; + } + + // preserve the row, but change its ID + auto oldId = mod.mmc_id(); + if (!mod.enable(!mod.enabled())) { + return false; + } + auto newId = mod.mmc_id(); + if (modsIndex.contains(newId)) { + // NOTE: this could handle a corner case, where we are overwriting a + // file, because the same 'mod' exists both enabled and disabled But is + // it necessary? + } + modsIndex.remove(oldId); + modsIndex[newId] = row; + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + return true; +} + +QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, + int role) const +{ + switch (role) { + case Qt::DisplayRole: + switch (section) { + case ActiveColumn: + return QString(); + case NameColumn: + return tr("Name"); + case VersionColumn: + return tr("Version"); + case DateColumn: + return tr("Last changed"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) { + case ActiveColumn: + return tr("Is the mod enabled?"); + case NameColumn: + return tr("The name of the mod."); + case VersionColumn: + return tr("The version of the mod."); + case DateColumn: + return tr("The date and time this mod was last changed (or " + "added)."); + default: + return QVariant(); + } + default: + return QVariant(); + } + return QVariant(); +} + +Qt::ItemFlags ModFolderModel::flags(const QModelIndex& index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + auto flags = defaultFlags; + if (interaction_disabled) { + flags &= ~Qt::ItemIsDropEnabled; + } else { + flags |= Qt::ItemIsDropEnabled; + if (index.isValid()) { + flags |= Qt::ItemIsUserCheckable; + } + } + return flags; +} + +Qt::DropActions ModFolderModel::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +QStringList ModFolderModel::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, + int, int, const QModelIndex&) +{ + if (action == Qt::IgnoreAction) { + return true; + } + + // check if the action is supported + if (!data || !(action & supportedDropActions())) { + return false; + } + + // files dropped from outside? + if (data->hasUrls()) { + auto urls = data->urls(); + for (auto url : urls) { + // only local files may be dropped... + if (!url.isLocalFile()) { + continue; + } + // TODO: implement not only copy, but also move + // FIXME: handle errors here + installMod(url.toLocalFile()); + } + return true; + } + return false; +} diff --git a/meshmc/launcher/minecraft/mod/ModFolderModel.h b/meshmc/launcher/minecraft/mod/ModFolderModel.h new file mode 100644 index 0000000000..845c6d6e4a --- /dev/null +++ b/meshmc/launcher/minecraft/mod/ModFolderModel.h @@ -0,0 +1,169 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <QList> +#include <QMap> +#include <QSet> +#include <QString> +#include <QDir> +#include <QAbstractListModel> + +#include "Mod.h" + +#include "ModFolderLoadTask.h" +#include "LocalModParseTask.h" + +class LegacyInstance; +class BaseInstance; +class QFileSystemWatcher; + +/** + * A legacy mod list. + * Backed by a folder. + */ +class ModFolderModel : public QAbstractListModel +{ + Q_OBJECT + public: + enum Columns { + ActiveColumn = 0, + NameColumn, + VersionColumn, + DateColumn, + NUM_COLUMNS + }; + enum ModStatusAction { Disable, Enable, Toggle }; + ModFolderModel(const QString& dir); + + virtual QVariant data(const QModelIndex& index, + int role = Qt::DisplayRole) const override; + virtual bool setData(const QModelIndex& index, const QVariant& value, + int role = Qt::EditRole) override; + Qt::DropActions supportedDropActions() const override; + + /// flags, mostly to support drag&drop + virtual Qt::ItemFlags flags(const QModelIndex& index) const override; + QStringList mimeTypes() const override; + bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, + int column, const QModelIndex& parent) override; + + virtual int rowCount(const QModelIndex&) const override + { + return size(); + } + + virtual QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + virtual int columnCount(const QModelIndex& parent) const override; + + size_t size() const + { + return mods.size(); + }; + bool empty() const + { + return size() == 0; + } + Mod& operator[](size_t index) + { + return mods[index]; + } + const Mod& at(size_t index) const + { + return mods.at(index); + } + + /// Reloads the mod list and returns true if the list changed. + bool update(); + + /** + * Adds the given mod to the list at the given index - if the list supports + * custom ordering + */ + bool installMod(const QString& filename); + + /// Deletes all the selected mods + bool deleteMods(const QModelIndexList& indexes); + + /// Enable or disable listed mods + bool setModStatus(const QModelIndexList& indexes, ModStatusAction action); + + void startWatching(); + void stopWatching(); + + bool isValid(); + + QDir dir() + { + return m_dir; + } + + const QList<Mod>& allMods() + { + return mods; + } + + public slots: + void disableInteraction(bool disabled); + + private slots: + void directoryChanged(QString path); + void finishUpdate(); + void finishModParse(int token); + + signals: + void updateFinished(); + + private: + void resolveMod(Mod& m); + bool setModStatus(int index, ModStatusAction action); + + protected: + QFileSystemWatcher* m_watcher; + bool is_watching = false; + ModFolderLoadTask::ResultPtr m_update; + bool scheduled_update = false; + bool interaction_disabled = false; + QDir m_dir; + QMap<QString, int> modsIndex; + QMap<int, LocalModParseTask::ResultPtr> activeTickets; + int nextResolutionTicket = 0; + QList<Mod> mods; +}; diff --git a/meshmc/launcher/minecraft/mod/ModFolderModel_test.cpp b/meshmc/launcher/minecraft/mod/ModFolderModel_test.cpp new file mode 100644 index 0000000000..12b0d44478 --- /dev/null +++ b/meshmc/launcher/minecraft/mod/ModFolderModel_test.cpp @@ -0,0 +1,71 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <QTest> +#include <QTemporaryDir> +#include "TestUtil.h" + +#include "FileSystem.h" +#include "minecraft/mod/ModFolderModel.h" + +class ModFolderModelTest : public QObject +{ + Q_OBJECT + + private slots: + // test for GH-1178 - install a folder with files to a mod list + void test_1178() + { + // source + QString source = QFINDTESTDATA("data/test_folder"); + + // sanity check + QVERIFY(!source.endsWith('/')); + + auto verify = [](QString path) { + QDir target_dir(FS::PathCombine(path, "test_folder")); + QVERIFY(target_dir.entryList().contains("pack.mcmeta")); + QVERIFY(target_dir.entryList().contains("assets")); + }; + + // 1. test with no trailing / + { + QString folder = source; + QTemporaryDir tempDir; + ModFolderModel m(tempDir.path()); + m.installMod(folder); + verify(tempDir.path()); + } + + // 2. test with trailing / + { + QString folder = source + '/'; + QTemporaryDir tempDir; + ModFolderModel m(tempDir.path()); + m.installMod(folder); + verify(tempDir.path()); + } + } +}; + +QTEST_GUILESS_MAIN(ModFolderModelTest) + +#include "ModFolderModel_test.moc" diff --git a/meshmc/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/meshmc/launcher/minecraft/mod/ResourcePackFolderModel.cpp new file mode 100644 index 0000000000..2f43cb2ff1 --- /dev/null +++ b/meshmc/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -0,0 +1,50 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "ResourcePackFolderModel.h" + +ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir) + : ModFolderModel(dir) +{ +} + +QVariant ResourcePackFolderModel::headerData(int section, + Qt::Orientation orientation, + int role) const +{ + if (role == Qt::ToolTipRole) { + switch (section) { + case ActiveColumn: + return tr("Is the resource pack enabled?"); + case NameColumn: + return tr("The name of the resource pack."); + case VersionColumn: + return tr("The version of the resource pack."); + case DateColumn: + return tr("The date and time this resource pack was last " + "changed (or added)."); + default: + return QVariant(); + } + } + + return ModFolderModel::headerData(section, orientation, role); +} diff --git a/meshmc/launcher/minecraft/mod/ResourcePackFolderModel.h b/meshmc/launcher/minecraft/mod/ResourcePackFolderModel.h new file mode 100644 index 0000000000..7c3008a432 --- /dev/null +++ b/meshmc/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -0,0 +1,35 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "ModFolderModel.h" + +class ResourcePackFolderModel : public ModFolderModel +{ + Q_OBJECT + + public: + explicit ResourcePackFolderModel(const QString& dir); + + QVariant headerData(int section, Qt::Orientation orientation, + int role) const override; +}; diff --git a/meshmc/launcher/minecraft/mod/TexturePackFolderModel.cpp b/meshmc/launcher/minecraft/mod/TexturePackFolderModel.cpp new file mode 100644 index 0000000000..af8510d643 --- /dev/null +++ b/meshmc/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -0,0 +1,50 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "TexturePackFolderModel.h" + +TexturePackFolderModel::TexturePackFolderModel(const QString& dir) + : ModFolderModel(dir) +{ +} + +QVariant TexturePackFolderModel::headerData(int section, + Qt::Orientation orientation, + int role) const +{ + if (role == Qt::ToolTipRole) { + switch (section) { + case ActiveColumn: + return tr("Is the texture pack enabled?"); + case NameColumn: + return tr("The name of the texture pack."); + case VersionColumn: + return tr("The version of the texture pack."); + case DateColumn: + return tr("The date and time this texture pack was last " + "changed (or added)."); + default: + return QVariant(); + } + } + + return ModFolderModel::headerData(section, orientation, role); +} diff --git a/meshmc/launcher/minecraft/mod/TexturePackFolderModel.h b/meshmc/launcher/minecraft/mod/TexturePackFolderModel.h new file mode 100644 index 0000000000..d7a49d0ffe --- /dev/null +++ b/meshmc/launcher/minecraft/mod/TexturePackFolderModel.h @@ -0,0 +1,35 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "ModFolderModel.h" + +class TexturePackFolderModel : public ModFolderModel +{ + Q_OBJECT + + public: + explicit TexturePackFolderModel(const QString& dir); + + QVariant headerData(int section, Qt::Orientation orientation, + int role) const override; +}; |
