summaryrefslogtreecommitdiff
path: root/meshmc/launcher/modplatform
diff options
context:
space:
mode:
Diffstat (limited to 'meshmc/launcher/modplatform')
-rw-r--r--meshmc/launcher/modplatform/atlauncher/ATLPackIndex.cpp72
-rw-r--r--meshmc/launcher/modplatform/atlauncher/ATLPackIndex.h70
-rw-r--r--meshmc/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp876
-rw-r--r--meshmc/launcher/modplatform/atlauncher/ATLPackInstallTask.h147
-rw-r--r--meshmc/launcher/modplatform/atlauncher/ATLPackManifest.cpp245
-rw-r--r--meshmc/launcher/modplatform/atlauncher/ATLPackManifest.h147
-rw-r--r--meshmc/launcher/modplatform/flame/FileResolvingTask.cpp82
-rw-r--r--meshmc/launcher/modplatform/flame/FileResolvingTask.h56
-rw-r--r--meshmc/launcher/modplatform/flame/FlamePackIndex.cpp151
-rw-r--r--meshmc/launcher/modplatform/flame/FlamePackIndex.h63
-rw-r--r--meshmc/launcher/modplatform/flame/PackManifest.cpp155
-rw-r--r--meshmc/launcher/modplatform/flame/PackManifest.h79
-rw-r--r--meshmc/launcher/modplatform/legacy_ftb/PackFetchTask.cpp202
-rw-r--r--meshmc/launcher/modplatform/legacy_ftb/PackFetchTask.h70
-rw-r--r--meshmc/launcher/modplatform/legacy_ftb/PackHelpers.h61
-rw-r--r--meshmc/launcher/modplatform/legacy_ftb/PackInstallTask.cpp247
-rw-r--r--meshmc/launcher/modplatform/legacy_ftb/PackInstallTask.h82
-rw-r--r--meshmc/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp60
-rw-r--r--meshmc/launcher/modplatform/legacy_ftb/PrivatePackManager.h65
-rw-r--r--meshmc/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp280
-rw-r--r--meshmc/launcher/modplatform/modpacksch/FTBPackInstallTask.h88
-rw-r--r--meshmc/launcher/modplatform/modpacksch/FTBPackManifest.cpp205
-rw-r--r--meshmc/launcher/modplatform/modpacksch/FTBPackManifest.h154
-rw-r--r--meshmc/launcher/modplatform/modrinth/ModrinthPackIndex.cpp84
-rw-r--r--meshmc/launcher/modplatform/modrinth/ModrinthPackIndex.h62
-rw-r--r--meshmc/launcher/modplatform/modrinth/ModrinthPackManifest.cpp85
-rw-r--r--meshmc/launcher/modplatform/modrinth/ModrinthPackManifest.h62
-rw-r--r--meshmc/launcher/modplatform/technic/SingleZipPackInstallTask.cpp168
-rw-r--r--meshmc/launcher/modplatform/technic/SingleZipPackInstallTask.h88
-rw-r--r--meshmc/launcher/modplatform/technic/SolderPackInstallTask.cpp236
-rw-r--r--meshmc/launcher/modplatform/technic/SolderPackInstallTask.h90
-rw-r--r--meshmc/launcher/modplatform/technic/TechnicPackProcessor.cpp218
-rw-r--r--meshmc/launcher/modplatform/technic/TechnicPackProcessor.h62
33 files changed, 4812 insertions, 0 deletions
diff --git a/meshmc/launcher/modplatform/atlauncher/ATLPackIndex.cpp b/meshmc/launcher/modplatform/atlauncher/ATLPackIndex.cpp
new file mode 100644
index 0000000000..95757676f2
--- /dev/null
+++ b/meshmc/launcher/modplatform/atlauncher/ATLPackIndex.cpp
@@ -0,0 +1,72 @@
+/* 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2021 Petr Mrazek <peterix@gmail.com>
+ *
+ * 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 "ATLPackIndex.h"
+
+#include <QRegularExpression>
+
+#include "Json.h"
+
+static void loadIndexedVersion(ATLauncher::IndexedVersion& v, QJsonObject& obj)
+{
+ v.version = Json::requireString(obj, "version");
+ v.minecraft = Json::requireString(obj, "minecraft");
+}
+
+void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack& m, QJsonObject& obj)
+{
+ m.id = Json::requireInteger(obj, "id");
+ m.position = Json::requireInteger(obj, "position");
+ m.name = Json::requireString(obj, "name");
+ m.type = Json::requireString(obj, "type") == "private"
+ ? ATLauncher::PackType::Private
+ : ATLauncher::PackType::Public;
+ auto versionsArr = Json::requireArray(obj, "versions");
+ for (const auto versionRaw : versionsArr) {
+ auto versionObj = Json::requireObject(versionRaw);
+ ATLauncher::IndexedVersion version;
+ loadIndexedVersion(version, versionObj);
+ m.versions.append(version);
+ }
+ m.system = Json::ensureBoolean(obj, QString("system"), false);
+ m.description = Json::ensureString(obj, "description", "");
+
+ m.safeName = Json::requireString(obj, "name")
+ .replace(QRegularExpression("[^A-Za-z0-9]"), "");
+}
diff --git a/meshmc/launcher/modplatform/atlauncher/ATLPackIndex.h b/meshmc/launcher/modplatform/atlauncher/ATLPackIndex.h
new file mode 100644
index 0000000000..3bdd3098d3
--- /dev/null
+++ b/meshmc/launcher/modplatform/atlauncher/ATLPackIndex.h
@@ -0,0 +1,70 @@
+/* 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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 "ATLPackManifest.h"
+
+#include <QString>
+#include <QVector>
+#include <QMetaType>
+
+namespace ATLauncher
+{
+
+ struct IndexedVersion {
+ QString version;
+ QString minecraft;
+ };
+
+ struct IndexedPack {
+ int id;
+ int position;
+ QString name;
+ PackType type;
+ QVector<IndexedVersion> versions;
+ bool system;
+ QString description;
+
+ QString safeName;
+ };
+
+ void loadIndexedPack(IndexedPack& m, QJsonObject& obj);
+} // namespace ATLauncher
+
+Q_DECLARE_METATYPE(ATLauncher::IndexedPack)
diff --git a/meshmc/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/meshmc/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
new file mode 100644
index 0000000000..8c0e5507f0
--- /dev/null
+++ b/meshmc/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -0,0 +1,876 @@
+/* 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2021 Petr Mrazek <peterix@gmail.com>
+ *
+ * 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 "ATLPackInstallTask.h"
+
+#include <QtConcurrent/QtConcurrent>
+#include <QRegularExpression>
+
+#include "MMCZip.h"
+#include "minecraft/OneSixVersionFormat.h"
+#include "Version.h"
+#include "net/ChecksumValidator.h"
+#include "FileSystem.h"
+#include "Json.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "settings/INISettingsObject.h"
+#include "meta/Index.h"
+#include "meta/Version.h"
+#include "meta/VersionList.h"
+
+#include "BuildConfig.h"
+#include "Application.h"
+
+namespace ATLauncher
+{
+
+ PackInstallTask::PackInstallTask(UserInteractionSupport* support,
+ QString pack, QString version)
+ {
+ m_support = support;
+ m_pack = pack;
+ m_version_name = version;
+ }
+
+ bool PackInstallTask::abort()
+ {
+ if (abortable) {
+ return jobPtr->abort();
+ }
+ return false;
+ }
+
+ void PackInstallTask::executeTask()
+ {
+ qDebug() << "PackInstallTask::executeTask: "
+ << QThread::currentThreadId();
+ auto* netJob =
+ new NetJob("ATLauncher::VersionFetch", APPLICATION->network());
+ auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL +
+ "packs/%1/versions/%2/Configs.json")
+ .arg(m_pack)
+ .arg(m_version_name);
+ netJob->addNetAction(
+ Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+
+ QObject::connect(netJob, &NetJob::succeeded, this,
+ &PackInstallTask::onDownloadSucceeded);
+ QObject::connect(netJob, &NetJob::failed, this,
+ &PackInstallTask::onDownloadFailed);
+ }
+
+ void PackInstallTask::onDownloadSucceeded()
+ {
+ qDebug() << "PackInstallTask::onDownloadSucceeded: "
+ << QThread::currentThreadId();
+ jobPtr.reset();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at "
+ << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto obj = doc.object();
+
+ ATLauncher::PackVersion version;
+ try {
+ ATLauncher::loadVersion(version, obj);
+ } catch (const JSONValidationError& e) {
+ emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
+ return;
+ }
+ m_version = version;
+
+ auto vlist = APPLICATION->metadataIndex()->get("net.minecraft");
+ if (!vlist) {
+ emitFailed(tr("Failed to get local metadata index for %1")
+ .arg("net.minecraft"));
+ return;
+ }
+
+ auto ver = vlist->getVersion(m_version.minecraft);
+ if (!ver) {
+ emitFailed(tr("Failed to get local metadata index for '%1' v%2")
+ .arg("net.minecraft")
+ .arg(m_version.minecraft));
+ return;
+ }
+ ver->load(Net::Mode::Online);
+ minecraftVersion = ver;
+
+ if (m_version.noConfigs) {
+ downloadMods();
+ } else {
+ installConfigs();
+ }
+ }
+
+ void PackInstallTask::onDownloadFailed(QString reason)
+ {
+ qDebug() << "PackInstallTask::onDownloadFailed: "
+ << QThread::currentThreadId();
+ emitFailed(reason);
+ jobPtr.reset();
+ }
+
+ QString PackInstallTask::getDirForModType(ModType type, QString raw)
+ {
+ switch (type) {
+ // Mod types that can either be ignored at this stage, or ignored
+ // completely.
+ case ModType::Root:
+ case ModType::Extract:
+ case ModType::Decomp:
+ case ModType::TexturePackExtract:
+ case ModType::ResourcePackExtract:
+ case ModType::MCPC:
+ return Q_NULLPTR;
+ case ModType::Forge:
+ // Forge detection happens later on, if it cannot be detected it
+ // will install a jarmod component.
+ case ModType::Jar:
+ return "jarmods";
+ case ModType::Mods:
+ return "mods";
+ case ModType::Flan:
+ return "Flan";
+ case ModType::Dependency:
+ return FS::PathCombine("mods", m_version.minecraft);
+ case ModType::Ic2Lib:
+ return FS::PathCombine("mods", "ic2");
+ case ModType::DenLib:
+ return FS::PathCombine("mods", "denlib");
+ case ModType::Coremods:
+ return "coremods";
+ case ModType::Plugins:
+ return "plugins";
+ case ModType::TexturePack:
+ return "texturepacks";
+ case ModType::ResourcePack:
+ return "resourcepacks";
+ case ModType::ShaderPack:
+ return "shaderpacks";
+ case ModType::Millenaire:
+ qWarning() << "Unsupported mod type: " + raw;
+ return Q_NULLPTR;
+ case ModType::Unknown:
+ emitFailed(tr("Unknown mod type: %1").arg(raw));
+ return Q_NULLPTR;
+ }
+
+ return Q_NULLPTR;
+ }
+
+ QString PackInstallTask::getVersionForLoader(QString uid)
+ {
+ if (m_version.loader.recommended || m_version.loader.latest ||
+ m_version.loader.choose) {
+ auto vlist = APPLICATION->metadataIndex()->get(uid);
+ if (!vlist) {
+ emitFailed(
+ tr("Failed to get local metadata index for %1").arg(uid));
+ return Q_NULLPTR;
+ }
+
+ if (!vlist->isLoaded()) {
+ vlist->load(Net::Mode::Online);
+ }
+
+ if (m_version.loader.recommended || m_version.loader.latest) {
+ for (int i = 0; i < vlist->versions().size(); i++) {
+ auto version = vlist->versions().at(i);
+ auto reqs = version->requirements();
+
+ // filter by minecraft version, if the loader depends on a
+ // certain version. not all mod loaders depend on a given
+ // Minecraft version, so we won't do this filtering for
+ // those loaders.
+ if (m_version.loader.type != "fabric") {
+ auto iter =
+ std::find_if(reqs.begin(), reqs.end(),
+ [](const Meta::Require& req) {
+ return req.uid == "net.minecraft";
+ });
+ if (iter == reqs.end())
+ continue;
+ if (iter->equalsVersion != m_version.minecraft)
+ continue;
+ }
+
+ if (m_version.loader.recommended) {
+ // first recommended build we find, we use.
+ if (!version->isRecommended())
+ continue;
+ }
+
+ return version->descriptor();
+ }
+
+ emitFailed(tr("Failed to find version for %1 loader")
+ .arg(m_version.loader.type));
+ return Q_NULLPTR;
+ } else if (m_version.loader.choose) {
+ // Fabric Loader doesn't depend on a given Minecraft version.
+ if (m_version.loader.type == "fabric") {
+ return m_support->chooseVersion(vlist, Q_NULLPTR);
+ }
+
+ return m_support->chooseVersion(vlist, m_version.minecraft);
+ }
+ }
+
+ if (m_version.loader.version == Q_NULLPTR ||
+ m_version.loader.version.isEmpty()) {
+ emitFailed(tr("No loader version set for modpack!"));
+ return Q_NULLPTR;
+ }
+
+ return m_version.loader.version;
+ }
+
+ QString PackInstallTask::detectLibrary(VersionLibrary library)
+ {
+ // Try to detect what the library is
+ if (!library.server.isEmpty() &&
+ library.server.split("/").length() >= 3) {
+ auto lastSlash = library.server.lastIndexOf("/");
+ auto locationAndVersion = library.server.mid(0, lastSlash);
+ auto fileName = library.server.mid(lastSlash + 1);
+
+ lastSlash = locationAndVersion.lastIndexOf("/");
+ auto location = locationAndVersion.mid(0, lastSlash);
+ auto version = locationAndVersion.mid(lastSlash + 1);
+
+ lastSlash = location.lastIndexOf("/");
+ auto group = location.mid(0, lastSlash).replace("/", ".");
+ auto artefact = location.mid(lastSlash + 1);
+
+ return group + ":" + artefact + ":" + version;
+ }
+
+ if (library.file.contains("-")) {
+ auto lastSlash = library.file.lastIndexOf("-");
+ auto name = library.file.mid(0, lastSlash);
+ auto version = library.file.mid(lastSlash + 1).remove(".jar");
+
+ if (name == QString("guava")) {
+ return "com.google.guava:guava:" + version;
+ } else if (name == QString("commons-lang3")) {
+ return "org.apache.commons:commons-lang3:" + version;
+ }
+ }
+
+ return "org.projecttick.atlauncher:" + library.md5 + ":1";
+ }
+
+ bool PackInstallTask::createLibrariesComponent(
+ QString instanceRoot, std::shared_ptr<PackProfile> profile)
+ {
+ if (m_version.libraries.isEmpty()) {
+ return true;
+ }
+
+ QList<GradleSpecifier> exempt;
+ for (const auto& componentUid : componentsToInstall.keys()) {
+ auto componentVersion = componentsToInstall.value(componentUid);
+
+ for (const auto& library : componentVersion->data()->libraries) {
+ GradleSpecifier lib(library->rawName());
+ exempt.append(lib);
+ }
+ }
+
+ {
+ for (const auto& library : minecraftVersion->data()->libraries) {
+ GradleSpecifier lib(library->rawName());
+ exempt.append(lib);
+ }
+ }
+
+ auto uuid = QUuid::createUuid();
+ auto id = uuid.toString().remove('{').remove('}');
+ auto target_id = "org.projecttick.atlauncher." + id;
+
+ auto patchDir = FS::PathCombine(instanceRoot, "patches");
+ if (!FS::ensureFolderPathExists(patchDir)) {
+ return false;
+ }
+ auto patchFileName = FS::PathCombine(patchDir, target_id + ".json");
+
+ auto f = std::make_shared<VersionFile>();
+ f->name = m_pack + " " + m_version_name + " (libraries)";
+
+ for (const auto& lib : m_version.libraries) {
+ auto libName = detectLibrary(lib);
+ GradleSpecifier libSpecifier(libName);
+
+ bool libExempt = false;
+ for (const auto& existingLib : exempt) {
+ if (libSpecifier.matchName(existingLib)) {
+ // If the pack specifies a newer version of the lib, use
+ // that!
+ libExempt = Version(libSpecifier.version()) >=
+ Version(existingLib.version());
+ }
+ }
+ if (libExempt)
+ continue;
+
+ auto library = std::make_shared<Library>();
+ library->setRawName(libName);
+
+ switch (lib.download) {
+ case DownloadType::Server:
+ library->setAbsoluteUrl(
+ BuildConfig.ATL_DOWNLOAD_SERVER_URL + lib.url);
+ break;
+ case DownloadType::Direct:
+ library->setAbsoluteUrl(lib.url);
+ break;
+ case DownloadType::Browser:
+ case DownloadType::Unknown:
+ emitFailed(tr("Unknown or unsupported download type: %1")
+ .arg(lib.download_raw));
+ return false;
+ }
+
+ f->libraries.append(library);
+ }
+
+ if (f->libraries.isEmpty()) {
+ return true;
+ }
+
+ QFile file(patchFileName);
+ if (!file.open(QFile::WriteOnly)) {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
+ file.close();
+
+ profile->appendComponent(new Component(profile.get(), target_id, f));
+ return true;
+ }
+
+ bool
+ PackInstallTask::createPackComponent(QString instanceRoot,
+ std::shared_ptr<PackProfile> profile)
+ {
+ if (m_version.mainClass == QString() &&
+ m_version.extraArguments == QString()) {
+ return true;
+ }
+
+ auto uuid = QUuid::createUuid();
+ auto id = uuid.toString().remove('{').remove('}');
+ auto target_id = "org.projecttick.atlauncher." + id;
+
+ auto patchDir = FS::PathCombine(instanceRoot, "patches");
+ if (!FS::ensureFolderPathExists(patchDir)) {
+ return false;
+ }
+ auto patchFileName = FS::PathCombine(patchDir, target_id + ".json");
+
+ QStringList mainClasses;
+ QStringList tweakers;
+ for (const auto& componentUid : componentsToInstall.keys()) {
+ auto componentVersion = componentsToInstall.value(componentUid);
+
+ if (componentVersion->data()->mainClass != QString("")) {
+ mainClasses.append(componentVersion->data()->mainClass);
+ }
+ tweakers.append(componentVersion->data()->addTweakers);
+ }
+
+ auto f = std::make_shared<VersionFile>();
+ f->name = m_pack + " " + m_version_name;
+ if (m_version.mainClass != QString() &&
+ !mainClasses.contains(m_version.mainClass)) {
+ f->mainClass = m_version.mainClass;
+ }
+
+ // Parse out tweakers
+ auto args = m_version.extraArguments.split(" ");
+ QString previous;
+ for (auto arg : args) {
+ if (arg.startsWith("--tweakClass=") || previous == "--tweakClass") {
+ auto tweakClass = arg.remove("--tweakClass=");
+ if (tweakers.contains(tweakClass))
+ continue;
+
+ f->addTweakers.append(tweakClass);
+ }
+ previous = arg;
+ }
+
+ if (f->mainClass == QString() && f->addTweakers.isEmpty()) {
+ return true;
+ }
+
+ QFile file(patchFileName);
+ if (!file.open(QFile::WriteOnly)) {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
+ file.close();
+
+ profile->appendComponent(new Component(profile.get(), target_id, f));
+ return true;
+ }
+
+ void PackInstallTask::installConfigs()
+ {
+ qDebug() << "PackInstallTask::installConfigs: "
+ << QThread::currentThreadId();
+ setStatus(tr("Downloading configs..."));
+ jobPtr = new NetJob(tr("Config download"), APPLICATION->network());
+
+ auto path =
+ QString("Configs/%1/%2.zip").arg(m_pack).arg(m_version_name);
+ auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL +
+ "packs/%1/versions/%2/Configs.zip")
+ .arg(m_pack)
+ .arg(m_version_name);
+ auto entry =
+ APPLICATION->metacache()->resolveEntry("ATLauncherPacks", path);
+ entry->setStale(true);
+
+ auto dl = Net::Download::makeCached(url, entry);
+ if (!m_version.configs.sha1.isEmpty()) {
+ auto rawSha1 =
+ QByteArray::fromHex(m_version.configs.sha1.toLatin1());
+ dl->addValidator(
+ new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
+ }
+ jobPtr->addNetAction(dl);
+ archivePath = entry->getFullPath();
+
+ connect(jobPtr.get(), &NetJob::succeeded, this, [&]() {
+ abortable = false;
+ extractConfigs();
+ jobPtr.reset();
+ });
+ connect(jobPtr.get(), &NetJob::failed, [&](QString reason) {
+ abortable = false;
+ emitFailed(reason);
+ jobPtr.reset();
+ });
+ connect(jobPtr.get(), &NetJob::progress,
+ [&](qint64 current, qint64 total) {
+ abortable = true;
+ setProgress(current, total);
+ });
+
+ jobPtr->start();
+ }
+
+ void PackInstallTask::extractConfigs()
+ {
+ qDebug() << "PackInstallTask::extractConfigs: "
+ << QThread::currentThreadId();
+ setStatus(tr("Extracting configs..."));
+
+ QDir extractDir(m_stagingPath);
+
+ QString extractPath = extractDir.absolutePath() + "/minecraft";
+ QString archivePathCopy = archivePath;
+ m_extractFuture = QtConcurrent::run(
+ QThreadPool::globalInstance(), [archivePathCopy, extractPath]() {
+ return MMCZip::extractDir(archivePathCopy, extractPath);
+ });
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished,
+ this, [&]() { downloadMods(); });
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled,
+ this, [&]() { emitAborted(); });
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+ }
+
+ void PackInstallTask::downloadMods()
+ {
+ qDebug() << "PackInstallTask::installMods: "
+ << QThread::currentThreadId();
+
+ QVector<ATLauncher::VersionMod> optionalMods;
+ for (const auto& mod : m_version.mods) {
+ if (mod.optional) {
+ optionalMods.push_back(mod);
+ }
+ }
+
+ // Select optional mods, if pack contains any
+ QVector<QString> selectedMods;
+ if (!optionalMods.isEmpty()) {
+ setStatus(tr("Selecting optional mods..."));
+ selectedMods = m_support->chooseOptionalMods(optionalMods);
+ }
+
+ setStatus(tr("Downloading mods..."));
+
+ jarmods.clear();
+ jobPtr = new NetJob(tr("Mod download"), APPLICATION->network());
+ for (const auto& mod : m_version.mods) {
+ // skip non-client mods
+ if (!mod.client)
+ continue;
+
+ // skip optional mods that were not selected
+ if (mod.optional && !selectedMods.contains(mod.name))
+ continue;
+
+ QString url;
+ switch (mod.download) {
+ case DownloadType::Server:
+ url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url;
+ break;
+ case DownloadType::Browser:
+ emitFailed(tr("Unsupported download type: %1")
+ .arg(mod.download_raw));
+ return;
+ case DownloadType::Direct:
+ url = mod.url;
+ break;
+ case DownloadType::Unknown:
+ emitFailed(
+ tr("Unknown download type: %1").arg(mod.download_raw));
+ return;
+ }
+
+ QFileInfo fileName(mod.file);
+ auto cacheName = fileName.completeBaseName() + "-" + mod.md5 + "." +
+ fileName.suffix();
+
+ if (mod.type == ModType::Extract ||
+ mod.type == ModType::TexturePackExtract ||
+ mod.type == ModType::ResourcePackExtract) {
+ auto entry = APPLICATION->metacache()->resolveEntry(
+ "ATLauncherPacks", cacheName);
+ entry->setStale(true);
+ modsToExtract.insert(entry->getFullPath(), mod);
+
+ auto dl = Net::Download::makeCached(url, entry);
+ if (!mod.md5.isEmpty()) {
+ auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(
+ QCryptographicHash::Md5, rawMd5));
+ }
+ jobPtr->addNetAction(dl);
+ } else if (mod.type == ModType::Decomp) {
+ auto entry = APPLICATION->metacache()->resolveEntry(
+ "ATLauncherPacks", cacheName);
+ entry->setStale(true);
+ modsToDecomp.insert(entry->getFullPath(), mod);
+
+ auto dl = Net::Download::makeCached(url, entry);
+ if (!mod.md5.isEmpty()) {
+ auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(
+ QCryptographicHash::Md5, rawMd5));
+ }
+ jobPtr->addNetAction(dl);
+ } else {
+ auto relpath = getDirForModType(mod.type, mod.type_raw);
+ if (relpath == Q_NULLPTR)
+ continue;
+
+ auto entry = APPLICATION->metacache()->resolveEntry(
+ "ATLauncherPacks", cacheName);
+ entry->setStale(true);
+
+ auto dl = Net::Download::makeCached(url, entry);
+ if (!mod.md5.isEmpty()) {
+ auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(
+ QCryptographicHash::Md5, rawMd5));
+ }
+ jobPtr->addNetAction(dl);
+
+ auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath,
+ mod.file);
+ qDebug() << "Will download" << url << "to" << path;
+ modsToCopy[entry->getFullPath()] = path;
+
+ if (mod.type == ModType::Forge) {
+ auto vlist =
+ APPLICATION->metadataIndex()->get("net.minecraftforge");
+ if (vlist) {
+ auto ver = vlist->getVersion(mod.version);
+ if (ver) {
+ ver->load(Net::Mode::Online);
+ componentsToInstall.insert("net.minecraftforge",
+ ver);
+ continue;
+ }
+ }
+
+ qDebug() << "Jarmod: " + path;
+ jarmods.push_back(path);
+ }
+
+ if (mod.type == ModType::Jar) {
+ qDebug() << "Jarmod: " + path;
+ jarmods.push_back(path);
+ }
+ }
+ }
+
+ connect(jobPtr.get(), &NetJob::succeeded, this,
+ &PackInstallTask::onModsDownloaded);
+ connect(jobPtr.get(), &NetJob::failed, [&](QString reason) {
+ abortable = false;
+ emitFailed(reason);
+ jobPtr.reset();
+ });
+ connect(jobPtr.get(), &NetJob::progress,
+ [&](qint64 current, qint64 total) {
+ abortable = true;
+ setProgress(current, total);
+ });
+
+ jobPtr->start();
+ }
+
+ void PackInstallTask::onModsDownloaded()
+ {
+ abortable = false;
+
+ qDebug() << "PackInstallTask::onModsDownloaded: "
+ << QThread::currentThreadId();
+
+ if (!modsToExtract.empty() || !modsToDecomp.empty() ||
+ !modsToCopy.empty()) {
+ auto modsToExtractCopy = modsToExtract;
+ auto modsToDecompCopy = modsToDecomp;
+ auto modsToCopyCopy = modsToCopy;
+ m_modExtractFuture = QtConcurrent::run(
+ QThreadPool::globalInstance(),
+ [this, modsToExtractCopy, modsToDecompCopy, modsToCopyCopy]() {
+ return this->extractMods(modsToExtractCopy,
+ modsToDecompCopy, modsToCopyCopy);
+ });
+ connect(&m_modExtractFutureWatcher,
+ &QFutureWatcher<QStringList>::finished, this,
+ &PackInstallTask::onModsExtracted);
+ connect(&m_modExtractFutureWatcher,
+ &QFutureWatcher<QStringList>::canceled, this,
+ [&]() { emitAborted(); });
+ m_modExtractFutureWatcher.setFuture(m_modExtractFuture);
+ } else {
+ install();
+ }
+ }
+
+ void PackInstallTask::onModsExtracted()
+ {
+ qDebug() << "PackInstallTask::onModsExtracted: "
+ << QThread::currentThreadId();
+ if (m_modExtractFuture.result()) {
+ install();
+ } else {
+ emitFailed(tr("Failed to extract mods..."));
+ }
+ }
+
+ bool
+ PackInstallTask::extractMods(const QMap<QString, VersionMod>& toExtract,
+ const QMap<QString, VersionMod>& toDecomp,
+ const QMap<QString, QString>& toCopy)
+ {
+ qDebug() << "PackInstallTask::extractMods: "
+ << QThread::currentThreadId();
+
+ setStatus(tr("Extracting mods..."));
+ for (auto iter = toExtract.begin(); iter != toExtract.end(); iter++) {
+ auto& modPath = iter.key();
+ auto& mod = iter.value();
+
+ QString extractToDir;
+ if (mod.type == ModType::Extract) {
+ extractToDir =
+ getDirForModType(mod.extractTo, mod.extractTo_raw);
+ } else if (mod.type == ModType::TexturePackExtract) {
+ extractToDir = FS::PathCombine("texturepacks", "extracted");
+ } else if (mod.type == ModType::ResourcePackExtract) {
+ extractToDir = FS::PathCombine("resourcepacks", "extracted");
+ }
+
+ QDir extractDir(m_stagingPath);
+ auto extractToPath = FS::PathCombine(extractDir.absolutePath(),
+ "minecraft", extractToDir);
+
+ QString folderToExtract = "";
+ if (mod.type == ModType::Extract) {
+ folderToExtract = mod.extractFolder;
+ folderToExtract.remove(QRegularExpression("^/"));
+ }
+
+ qDebug() << "Extracting " + mod.file + " to " + extractToDir;
+ if (!MMCZip::extractDir(modPath, folderToExtract, extractToPath)) {
+ // assume error
+ return false;
+ }
+ }
+
+ for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) {
+ auto& modPath = iter.key();
+ auto& mod = iter.value();
+ auto extractToDir =
+ getDirForModType(mod.decompType, mod.decompType_raw);
+
+ QDir extractDir(m_stagingPath);
+ auto extractToPath =
+ FS::PathCombine(extractDir.absolutePath(), "minecraft",
+ extractToDir, mod.decompFile);
+
+ qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir;
+ if (!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) {
+ qWarning() << "Failed to extract" << mod.decompFile;
+ return false;
+ }
+ }
+
+ for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) {
+ auto& from = iter.key();
+ auto& to = iter.value();
+ FS::copy fileCopyOperation(from, to);
+ if (!fileCopyOperation()) {
+ qWarning() << "Failed to copy" << from << "to" << to;
+ return false;
+ }
+ }
+ return true;
+ }
+
+ void PackInstallTask::install()
+ {
+ qDebug() << "PackInstallTask::install: " << QThread::currentThreadId();
+ setStatus(tr("Installing modpack"));
+
+ auto instanceConfigPath =
+ FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings =
+ std::make_shared<INISettingsObject>(instanceConfigPath);
+ instanceSettings->suspendSave();
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+
+ MinecraftInstance instance(m_globalSettings, instanceSettings,
+ m_stagingPath);
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+
+ // Use a component to add libraries BEFORE Minecraft
+ if (!createLibrariesComponent(instance.instanceRoot(), components)) {
+ emitFailed(tr("Failed to create libraries component"));
+ return;
+ }
+
+ // Minecraft
+ components->setComponentVersion("net.minecraft", m_version.minecraft,
+ true);
+
+ // Loader
+ if (m_version.loader.type == QString("forge")) {
+ auto version = getVersionForLoader("net.minecraftforge");
+ if (version == Q_NULLPTR)
+ return;
+
+ components->setComponentVersion("net.minecraftforge", version,
+ true);
+ } else if (m_version.loader.type == QString("fabric")) {
+ auto version = getVersionForLoader("net.fabricmc.fabric-loader");
+ if (version == Q_NULLPTR)
+ return;
+
+ components->setComponentVersion("net.fabricmc.fabric-loader",
+ version, true);
+ } else if (m_version.loader.type == QString("neoforge")) {
+ auto version = getVersionForLoader("net.neoforged");
+ if (version == Q_NULLPTR)
+ return;
+
+ components->setComponentVersion("net.neoforged", version, true);
+ } else if (m_version.loader.type == QString("quilt")) {
+ auto version = getVersionForLoader("org.quiltmc.quilt-loader");
+ if (version == Q_NULLPTR)
+ return;
+
+ components->setComponentVersion("org.quiltmc.quilt-loader", version,
+ true);
+ } else if (m_version.loader.type != QString()) {
+ emitFailed(tr("Unknown loader type: ") + m_version.loader.type);
+ return;
+ }
+
+ for (const auto& componentUid : componentsToInstall.keys()) {
+ auto version = componentsToInstall.value(componentUid);
+ components->setComponentVersion(componentUid, version->version());
+ }
+
+ components->installJarMods(jarmods);
+
+ // Use a component to fill in the rest of the data
+ // todo: use more detection
+ if (!createPackComponent(instance.instanceRoot(), components)) {
+ emitFailed(tr("Failed to create pack component"));
+ return;
+ }
+
+ components->saveNow();
+
+ instance.setName(m_instName);
+ instance.setIconKey(m_instIcon);
+ instanceSettings->resumeSave();
+
+ jarmods.clear();
+ emitSucceeded();
+ }
+
+} // namespace ATLauncher
diff --git a/meshmc/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/meshmc/launcher/modplatform/atlauncher/ATLPackInstallTask.h
new file mode 100644
index 0000000000..248002ea5f
--- /dev/null
+++ b/meshmc/launcher/modplatform/atlauncher/ATLPackInstallTask.h
@@ -0,0 +1,147 @@
+/* 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2021 Petr Mrazek <peterix@gmail.com>
+ *
+ * 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 <meta/VersionList.h>
+#include "ATLPackManifest.h"
+
+#include "InstanceTask.h"
+#include "net/NetJob.h"
+#include "settings/INISettingsObject.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "meta/Version.h"
+
+#include <nonstd/optional>
+
+namespace ATLauncher
+{
+
+ class UserInteractionSupport
+ {
+
+ public:
+ /**
+ * Requests a user interaction to select which optional mods should be
+ * installed.
+ */
+ virtual QVector<QString>
+ chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) = 0;
+
+ /**
+ * Requests a user interaction to select a component version from a
+ * given version list and constrained to a given Minecraft version.
+ */
+ virtual QString chooseVersion(Meta::VersionListPtr vlist,
+ QString minecraftVersion) = 0;
+ };
+
+ class PackInstallTask : public InstanceTask
+ {
+ Q_OBJECT
+
+ public:
+ explicit PackInstallTask(UserInteractionSupport* support, QString pack,
+ QString version);
+ virtual ~PackInstallTask() {}
+
+ bool canAbort() const override
+ {
+ return true;
+ }
+ bool abort() override;
+
+ protected:
+ virtual void executeTask() override;
+
+ private slots:
+ void onDownloadSucceeded();
+ void onDownloadFailed(QString reason);
+
+ void onModsDownloaded();
+ void onModsExtracted();
+
+ private:
+ QString getDirForModType(ModType type, QString raw);
+ QString getVersionForLoader(QString uid);
+ QString detectLibrary(VersionLibrary library);
+
+ bool createLibrariesComponent(QString instanceRoot,
+ std::shared_ptr<PackProfile> profile);
+ bool createPackComponent(QString instanceRoot,
+ std::shared_ptr<PackProfile> profile);
+
+ void installConfigs();
+ void extractConfigs();
+ void downloadMods();
+ bool extractMods(const QMap<QString, VersionMod>& toExtract,
+ const QMap<QString, VersionMod>& toDecomp,
+ const QMap<QString, QString>& toCopy);
+ void install();
+
+ private:
+ UserInteractionSupport* m_support;
+
+ bool abortable = false;
+
+ NetJob::Ptr jobPtr;
+ QByteArray response;
+
+ QString m_pack;
+ QString m_version_name;
+ PackVersion m_version;
+
+ QMap<QString, VersionMod> modsToExtract;
+ QMap<QString, VersionMod> modsToDecomp;
+ QMap<QString, QString> modsToCopy;
+
+ QString archivePath;
+ QStringList jarmods;
+ Meta::VersionPtr minecraftVersion;
+ QMap<QString, Meta::VersionPtr> componentsToInstall;
+
+ QFuture<nonstd::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+
+ QFuture<bool> m_modExtractFuture;
+ QFutureWatcher<bool> m_modExtractFutureWatcher;
+ };
+
+} // namespace ATLauncher
diff --git a/meshmc/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/meshmc/launcher/modplatform/atlauncher/ATLPackManifest.cpp
new file mode 100644
index 0000000000..08a2818699
--- /dev/null
+++ b/meshmc/launcher/modplatform/atlauncher/ATLPackManifest.cpp
@@ -0,0 +1,245 @@
+/* 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2021 Petr Mrazek <peterix@gmail.com>
+ *
+ * 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 "ATLPackManifest.h"
+
+#include "Json.h"
+
+static ATLauncher::DownloadType parseDownloadType(QString rawType)
+{
+ if (rawType == QString("server")) {
+ return ATLauncher::DownloadType::Server;
+ } else if (rawType == QString("browser")) {
+ return ATLauncher::DownloadType::Browser;
+ } else if (rawType == QString("direct")) {
+ return ATLauncher::DownloadType::Direct;
+ }
+
+ return ATLauncher::DownloadType::Unknown;
+}
+
+static ATLauncher::ModType parseModType(QString rawType)
+{
+ // See https://wiki.atlauncher.com/mod_types
+ if (rawType == QString("root")) {
+ return ATLauncher::ModType::Root;
+ } else if (rawType == QString("forge")) {
+ return ATLauncher::ModType::Forge;
+ } else if (rawType == QString("jar")) {
+ return ATLauncher::ModType::Jar;
+ } else if (rawType == QString("mods")) {
+ return ATLauncher::ModType::Mods;
+ } else if (rawType == QString("flan")) {
+ return ATLauncher::ModType::Flan;
+ } else if (rawType == QString("dependency") ||
+ rawType == QString("depandency")) {
+ return ATLauncher::ModType::Dependency;
+ } else if (rawType == QString("ic2lib")) {
+ return ATLauncher::ModType::Ic2Lib;
+ } else if (rawType == QString("denlib")) {
+ return ATLauncher::ModType::DenLib;
+ } else if (rawType == QString("coremods")) {
+ return ATLauncher::ModType::Coremods;
+ } else if (rawType == QString("mcpc")) {
+ return ATLauncher::ModType::MCPC;
+ } else if (rawType == QString("plugins")) {
+ return ATLauncher::ModType::Plugins;
+ } else if (rawType == QString("extract")) {
+ return ATLauncher::ModType::Extract;
+ } else if (rawType == QString("decomp")) {
+ return ATLauncher::ModType::Decomp;
+ } else if (rawType == QString("texturepack")) {
+ return ATLauncher::ModType::TexturePack;
+ } else if (rawType == QString("resourcepack")) {
+ return ATLauncher::ModType::ResourcePack;
+ } else if (rawType == QString("shaderpack")) {
+ return ATLauncher::ModType::ShaderPack;
+ } else if (rawType == QString("texturepackextract")) {
+ return ATLauncher::ModType::TexturePackExtract;
+ } else if (rawType == QString("resourcepackextract")) {
+ return ATLauncher::ModType::ResourcePackExtract;
+ } else if (rawType == QString("millenaire")) {
+ return ATLauncher::ModType::Millenaire;
+ }
+
+ return ATLauncher::ModType::Unknown;
+}
+
+static void loadVersionLoader(ATLauncher::VersionLoader& p, QJsonObject& obj)
+{
+ p.type = Json::requireString(obj, "type");
+ p.choose = Json::ensureBoolean(obj, QString("choose"), false);
+
+ auto metadata = Json::requireObject(obj, "metadata");
+ p.latest = Json::ensureBoolean(metadata, QString("latest"), false);
+ p.recommended =
+ Json::ensureBoolean(metadata, QString("recommended"), false);
+
+ // Minecraft Forge
+ if (p.type == "forge") {
+ p.version = Json::ensureString(metadata, "version", "");
+ }
+
+ // Fabric Loader
+ if (p.type == "fabric") {
+ p.version = Json::ensureString(metadata, "loader", "");
+ }
+}
+
+static void loadVersionLibrary(ATLauncher::VersionLibrary& p, QJsonObject& obj)
+{
+ p.url = Json::requireString(obj, "url");
+ p.file = Json::requireString(obj, "file");
+ p.md5 = Json::requireString(obj, "md5");
+
+ p.download_raw = Json::requireString(obj, "download");
+ p.download = parseDownloadType(p.download_raw);
+
+ p.server = Json::ensureString(obj, "server", "");
+}
+
+static void loadVersionConfigs(ATLauncher::VersionConfigs& p, QJsonObject& obj)
+{
+ p.filesize = Json::requireInteger(obj, "filesize");
+ p.sha1 = Json::requireString(obj, "sha1");
+}
+
+static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj)
+{
+ p.name = Json::requireString(obj, "name");
+ p.version = Json::requireString(obj, "version");
+ p.url = Json::requireString(obj, "url");
+ p.file = Json::requireString(obj, "file");
+ p.md5 = Json::ensureString(obj, "md5", "");
+
+ p.download_raw = Json::requireString(obj, "download");
+ p.download = parseDownloadType(p.download_raw);
+
+ p.type_raw = Json::requireString(obj, "type");
+ p.type = parseModType(p.type_raw);
+
+ // This contributes to the Minecraft Forge detection, where we rely on
+ // mod.type being "Forge" when the mod represents Forge. As there is little
+ // difference between "Jar" and "Forge, some packs regretfully use "Jar".
+ // This will correct the type to "Forge" in these cases (as best it can).
+ if (p.name == QString("Minecraft Forge") &&
+ p.type == ATLauncher::ModType::Jar) {
+ p.type_raw = "forge";
+ p.type = ATLauncher::ModType::Forge;
+ }
+
+ if (obj.contains("extractTo")) {
+ p.extractTo_raw = Json::requireString(obj, "extractTo");
+ p.extractTo = parseModType(p.extractTo_raw);
+ p.extractFolder =
+ Json::ensureString(obj, "extractFolder", "").replace("%s%", "/");
+ }
+
+ if (obj.contains("decompType")) {
+ p.decompType_raw = Json::requireString(obj, "decompType");
+ p.decompType = parseModType(p.decompType_raw);
+ p.decompFile = Json::requireString(obj, "decompFile");
+ }
+
+ p.description = Json::ensureString(obj, QString("description"), "");
+ p.optional = Json::ensureBoolean(obj, QString("optional"), false);
+ p.recommended = Json::ensureBoolean(obj, QString("recommended"), false);
+ p.selected = Json::ensureBoolean(obj, QString("selected"), false);
+ p.hidden = Json::ensureBoolean(obj, QString("hidden"), false);
+ p.library = Json::ensureBoolean(obj, QString("library"), false);
+ p.group = Json::ensureString(obj, QString("group"), "");
+ if (obj.contains("depends")) {
+ auto dependsArr = Json::requireArray(obj, "depends");
+ for (const auto depends : dependsArr) {
+ p.depends.append(Json::requireString(depends));
+ }
+ }
+
+ p.client = Json::ensureBoolean(obj, QString("client"), false);
+
+ // computed
+ p.effectively_hidden = p.hidden || p.library;
+}
+
+void ATLauncher::loadVersion(PackVersion& v, QJsonObject& obj)
+{
+ v.version = Json::requireString(obj, "version");
+ v.minecraft = Json::requireString(obj, "minecraft");
+ v.noConfigs = Json::ensureBoolean(obj, QString("noConfigs"), false);
+
+ if (obj.contains("mainClass")) {
+ auto main = Json::requireObject(obj, "mainClass");
+ v.mainClass = Json::ensureString(main, "mainClass", "");
+ }
+
+ if (obj.contains("extraArguments")) {
+ auto arguments = Json::requireObject(obj, "extraArguments");
+ v.extraArguments = Json::ensureString(arguments, "arguments", "");
+ }
+
+ if (obj.contains("loader")) {
+ auto loader = Json::requireObject(obj, "loader");
+ loadVersionLoader(v.loader, loader);
+ }
+
+ if (obj.contains("libraries")) {
+ auto libraries = Json::requireArray(obj, "libraries");
+ for (const auto libraryRaw : libraries) {
+ auto libraryObj = Json::requireObject(libraryRaw);
+ ATLauncher::VersionLibrary target;
+ loadVersionLibrary(target, libraryObj);
+ v.libraries.append(target);
+ }
+ }
+
+ if (obj.contains("mods")) {
+ auto mods = Json::requireArray(obj, "mods");
+ for (const auto modRaw : mods) {
+ auto modObj = Json::requireObject(modRaw);
+ ATLauncher::VersionMod mod;
+ loadVersionMod(mod, modObj);
+ v.mods.append(mod);
+ }
+ }
+
+ if (obj.contains("configs")) {
+ auto configsObj = Json::requireObject(obj, "configs");
+ loadVersionConfigs(v.configs, configsObj);
+ }
+}
diff --git a/meshmc/launcher/modplatform/atlauncher/ATLPackManifest.h b/meshmc/launcher/modplatform/atlauncher/ATLPackManifest.h
new file mode 100644
index 0000000000..18f65d16d5
--- /dev/null
+++ b/meshmc/launcher/modplatform/atlauncher/ATLPackManifest.h
@@ -0,0 +1,147 @@
+/* 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 2020 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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 <QString>
+#include <QVector>
+#include <QJsonObject>
+
+namespace ATLauncher
+{
+
+ enum class PackType { Public, Private };
+
+ enum class ModType {
+ Root,
+ Forge,
+ Jar,
+ Mods,
+ Flan,
+ Dependency,
+ Ic2Lib,
+ DenLib,
+ Coremods,
+ MCPC,
+ Plugins,
+ Extract,
+ Decomp,
+ TexturePack,
+ ResourcePack,
+ ShaderPack,
+ TexturePackExtract,
+ ResourcePackExtract,
+ Millenaire,
+ Unknown
+ };
+
+ enum class DownloadType { Server, Browser, Direct, Unknown };
+
+ struct VersionLoader {
+ QString type;
+ bool latest;
+ bool recommended;
+ bool choose;
+
+ QString version;
+ };
+
+ struct VersionLibrary {
+ QString url;
+ QString file;
+ QString server;
+ QString md5;
+ DownloadType download;
+ QString download_raw;
+ };
+
+ struct VersionMod {
+ QString name;
+ QString version;
+ QString url;
+ QString file;
+ QString md5;
+ DownloadType download;
+ QString download_raw;
+ ModType type;
+ QString type_raw;
+
+ ModType extractTo;
+ QString extractTo_raw;
+ QString extractFolder;
+
+ ModType decompType;
+ QString decompType_raw;
+ QString decompFile;
+
+ QString description;
+ bool optional;
+ bool recommended;
+ bool selected;
+ bool hidden;
+ bool library;
+ QString group;
+ QVector<QString> depends;
+
+ bool client;
+
+ // computed
+ bool effectively_hidden;
+ };
+
+ struct VersionConfigs {
+ int filesize;
+ QString sha1;
+ };
+
+ struct PackVersion {
+ QString version;
+ QString minecraft;
+ bool noConfigs;
+ QString mainClass;
+ QString extraArguments;
+
+ VersionLoader loader;
+ QVector<VersionLibrary> libraries;
+ QVector<VersionMod> mods;
+ VersionConfigs configs;
+ };
+
+ void loadVersion(PackVersion& v, QJsonObject& obj);
+
+} // namespace ATLauncher
diff --git a/meshmc/launcher/modplatform/flame/FileResolvingTask.cpp b/meshmc/launcher/modplatform/flame/FileResolvingTask.cpp
new file mode 100644
index 0000000000..884d3831f0
--- /dev/null
+++ b/meshmc/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -0,0 +1,82 @@
+/* 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 "FileResolvingTask.h"
+#include "Json.h"
+
+Flame::FileResolvingTask::FileResolvingTask(
+ shared_qobject_ptr<QNetworkAccessManager> network,
+ Flame::Manifest& toProcess)
+ : m_network(network), m_toProcess(toProcess)
+{
+}
+
+void Flame::FileResolvingTask::executeTask()
+{
+ setStatus(tr("Resolving mod IDs..."));
+ setProgress(0, m_toProcess.files.size());
+ m_dljob = new NetJob("Mod id resolver", m_network);
+ results.resize(m_toProcess.files.size());
+ int index = 0;
+ for (auto& file : m_toProcess.files) {
+ auto projectIdStr = QString::number(file.projectId);
+ auto fileIdStr = QString::number(file.fileId);
+ QString metaurl =
+ QString("https://api.curseforge.com/v1/mods/%1/files/%2")
+ .arg(projectIdStr, fileIdStr);
+ auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]);
+ m_dljob->addNetAction(dl);
+ index++;
+ }
+ connect(m_dljob.get(), &NetJob::finished, this,
+ &Flame::FileResolvingTask::netJobFinished);
+ m_dljob->start();
+}
+
+void Flame::FileResolvingTask::netJobFinished()
+{
+ int index = 0;
+ int unresolved = 0;
+ for (auto& bytes : results) {
+ auto& out = m_toProcess.files[index];
+ try {
+ if (!out.parseFromBytes(bytes)) {
+ unresolved++;
+ qWarning() << "Resolving of" << out.projectId << out.fileId
+ << "failed: mod may have restricted downloads";
+ }
+ } catch (const JSONValidationError& e) {
+ unresolved++;
+ qCritical() << "Resolving of" << out.projectId << out.fileId
+ << "failed because of a parsing error:";
+ qCritical() << e.cause();
+ qCritical() << "JSON:";
+ qCritical() << bytes;
+ }
+ index++;
+ }
+ if (unresolved > 0) {
+ qWarning() << unresolved
+ << "mod(s) could not be resolved (restricted downloads). "
+ "They will be skipped.";
+ }
+ emitSucceeded();
+}
diff --git a/meshmc/launcher/modplatform/flame/FileResolvingTask.h b/meshmc/launcher/modplatform/flame/FileResolvingTask.h
new file mode 100644
index 0000000000..a59df3d3a4
--- /dev/null
+++ b/meshmc/launcher/modplatform/flame/FileResolvingTask.h
@@ -0,0 +1,56 @@
+/* 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 "tasks/Task.h"
+#include "net/NetJob.h"
+#include "PackManifest.h"
+
+namespace Flame
+{
+ class FileResolvingTask : public Task
+ {
+ Q_OBJECT
+ public:
+ explicit FileResolvingTask(
+ shared_qobject_ptr<QNetworkAccessManager> network,
+ Flame::Manifest& toProcess);
+ virtual ~FileResolvingTask() {};
+
+ const Flame::Manifest& getResults() const
+ {
+ return m_toProcess;
+ }
+
+ protected:
+ virtual void executeTask() override;
+
+ protected slots:
+ void netJobFinished();
+
+ private: /* data */
+ shared_qobject_ptr<QNetworkAccessManager> m_network;
+ Flame::Manifest m_toProcess;
+ QVector<QByteArray> results;
+ NetJob::Ptr m_dljob;
+ };
+} // namespace Flame
diff --git a/meshmc/launcher/modplatform/flame/FlamePackIndex.cpp b/meshmc/launcher/modplatform/flame/FlamePackIndex.cpp
new file mode 100644
index 0000000000..6808d84f91
--- /dev/null
+++ b/meshmc/launcher/modplatform/flame/FlamePackIndex.cpp
@@ -0,0 +1,151 @@
+/* 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 "FlamePackIndex.h"
+
+#include "Json.h"
+
+void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj)
+{
+ pack.addonId = Json::requireInteger(obj, "id");
+ pack.name = Json::requireString(obj, "name");
+ pack.description = Json::ensureString(obj, "summary", "");
+
+ // API v1: links.websiteUrl, API v2: websiteUrl at root
+ if (obj.contains("links") && obj.value("links").isObject()) {
+ pack.websiteUrl =
+ Json::ensureString(obj.value("links").toObject(), "websiteUrl", "");
+ } else {
+ pack.websiteUrl = Json::ensureString(obj, "websiteUrl", "");
+ }
+
+ // API v1: logo is a single object, API v2: attachments is an array
+ bool thumbnailFound = false;
+ if (obj.contains("logo") && obj.value("logo").isObject()) {
+ auto logoObj = obj.value("logo").toObject();
+ pack.logoName = Json::ensureString(logoObj, "title", pack.name);
+ pack.logoUrl = Json::ensureString(logoObj, "thumbnailUrl", "");
+ thumbnailFound = !pack.logoUrl.isEmpty();
+ }
+ if (!thumbnailFound && obj.contains("attachments")) {
+ auto attachments = Json::requireArray(obj, "attachments");
+ for (auto attachmentRaw : attachments) {
+ auto attachmentObj = Json::requireObject(attachmentRaw);
+ bool isDefault = attachmentObj.value("isDefault").toBool(false);
+ if (isDefault) {
+ thumbnailFound = true;
+ pack.logoName = Json::requireString(attachmentObj, "title");
+ pack.logoUrl =
+ Json::requireString(attachmentObj, "thumbnailUrl");
+ break;
+ }
+ }
+ }
+ if (!thumbnailFound) {
+ pack.logoName = pack.name;
+ pack.logoUrl = "";
+ }
+
+ auto authors = Json::requireArray(obj, "authors");
+ for (auto authorIter : authors) {
+ auto author = Json::requireObject(authorIter);
+ Flame::ModpackAuthor packAuthor;
+ packAuthor.name = Json::requireString(author, "name");
+ packAuthor.url = Json::ensureString(author, "url", "");
+ pack.authors.append(packAuthor);
+ }
+
+ // API v1: mainFileId, API v2: defaultFileId
+ int defaultFileId = 0;
+ if (obj.contains("mainFileId")) {
+ defaultFileId = Json::requireInteger(obj, "mainFileId");
+ } else {
+ defaultFileId = Json::requireInteger(obj, "defaultFileId");
+ }
+
+ bool found = false;
+ // check if there are some files before adding the pack
+ auto files = Json::ensureArray(obj, "latestFiles");
+ for (auto fileIter : files) {
+ auto file = Json::requireObject(fileIter);
+ int id = Json::requireInteger(file, "id");
+
+ // NOTE: for now, ignore everything that's not the default...
+ if (id != defaultFileId) {
+ continue;
+ }
+
+ // API v1: gameVersions, API v2: gameVersion
+ QJsonArray versionArray;
+ if (file.contains("gameVersions")) {
+ versionArray = Json::requireArray(file, "gameVersions");
+ } else {
+ versionArray = Json::requireArray(file, "gameVersion");
+ }
+ if (versionArray.size() < 1) {
+ continue;
+ }
+
+ found = true;
+ break;
+ }
+ if (!found) {
+ throw JSONValidationError(
+ QString("Pack with no good file, skipping: %1").arg(pack.name));
+ }
+}
+
+void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr)
+{
+ QVector<Flame::IndexedVersion> unsortedVersions;
+ for (auto versionIter : arr) {
+ auto version = Json::requireObject(versionIter);
+ Flame::IndexedVersion file;
+
+ file.addonId = pack.addonId;
+ file.fileId = Json::requireInteger(version, "id");
+ QJsonArray versionArray;
+ if (version.contains("gameVersions")) {
+ versionArray = Json::requireArray(version, "gameVersions");
+ } else {
+ versionArray = Json::requireArray(version, "gameVersion");
+ }
+ if (versionArray.size() < 1) {
+ continue;
+ }
+
+ // pick the latest version supported
+ file.mcVersion = versionArray[0].toString();
+ file.version = Json::requireString(version, "displayName");
+ file.downloadUrl = Json::ensureString(version, "downloadUrl", "");
+ file.fileName = Json::ensureString(version, "fileName", "");
+ unsortedVersions.append(file);
+ }
+
+ auto orderSortPredicate = [](const IndexedVersion& a,
+ const IndexedVersion& b) -> bool {
+ return a.fileId > b.fileId;
+ };
+ std::sort(unsortedVersions.begin(), unsortedVersions.end(),
+ orderSortPredicate);
+ pack.versions = unsortedVersions;
+ pack.versionsLoaded = true;
+}
diff --git a/meshmc/launcher/modplatform/flame/FlamePackIndex.h b/meshmc/launcher/modplatform/flame/FlamePackIndex.h
new file mode 100644
index 0000000000..ab3ec77ec1
--- /dev/null
+++ b/meshmc/launcher/modplatform/flame/FlamePackIndex.h
@@ -0,0 +1,63 @@
+/* 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 <QList>
+#include <QMetaType>
+#include <QString>
+#include <QVector>
+
+namespace Flame
+{
+
+ struct ModpackAuthor {
+ QString name;
+ QString url;
+ };
+
+ struct IndexedVersion {
+ int addonId;
+ int fileId;
+ QString version;
+ QString mcVersion;
+ QString downloadUrl;
+ QString fileName;
+ };
+
+ struct IndexedPack {
+ int addonId;
+ QString name;
+ QString description;
+ QList<ModpackAuthor> authors;
+ QString logoName;
+ QString logoUrl;
+ QString websiteUrl;
+
+ bool versionsLoaded = false;
+ QVector<IndexedVersion> versions;
+ };
+
+ void loadIndexedPack(IndexedPack& m, QJsonObject& obj);
+ void loadIndexedPackVersions(IndexedPack& m, QJsonArray& arr);
+} // namespace Flame
+
+Q_DECLARE_METATYPE(Flame::IndexedPack)
diff --git a/meshmc/launcher/modplatform/flame/PackManifest.cpp b/meshmc/launcher/modplatform/flame/PackManifest.cpp
new file mode 100644
index 0000000000..020370b29f
--- /dev/null
+++ b/meshmc/launcher/modplatform/flame/PackManifest.cpp
@@ -0,0 +1,155 @@
+/* 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 "PackManifest.h"
+#include "Json.h"
+
+static void loadFileV1(Flame::File& f, QJsonObject& file)
+{
+ f.projectId = Json::requireInteger(file, "projectID");
+ f.fileId = Json::requireInteger(file, "fileID");
+ f.required = Json::ensureBoolean(file, QString("required"), true);
+}
+
+static void loadModloaderV1(Flame::Modloader& m, QJsonObject& modLoader)
+{
+ m.id = Json::requireString(modLoader, "id");
+ m.primary = Json::ensureBoolean(modLoader, QString("primary"), false);
+}
+
+static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft)
+{
+ m.version = Json::requireString(minecraft, "version");
+ // extra libraries... apparently only used for a custom Minecraft launcher
+ // in the 1.2.5 FTB retro pack intended use is likely hardcoded in the
+ // 'Flame' client, the manifest says nothing
+ m.libraries =
+ Json::ensureString(minecraft, QString("libraries"), QString());
+ auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray());
+ for (QJsonValueRef item : arr) {
+ auto obj = Json::requireObject(item);
+ Flame::Modloader loader;
+ loadModloaderV1(loader, obj);
+ m.modLoaders.append(loader);
+ }
+}
+
+static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest)
+{
+ auto mc = Json::requireObject(manifest, "minecraft");
+ loadMinecraftV1(m.minecraft, mc);
+ m.name = Json::ensureString(manifest, QString("name"), "Unnamed");
+ m.version = Json::ensureString(manifest, QString("version"), QString());
+ m.author =
+ Json::ensureString(manifest, QString("author"), "Anonymous Coward");
+ auto arr = Json::ensureArray(manifest, "files", QJsonArray());
+ for (QJsonValueRef item : arr) {
+ auto obj = Json::requireObject(item);
+ Flame::File file;
+ loadFileV1(file, obj);
+ m.files.append(file);
+ }
+ m.overrides = Json::ensureString(manifest, "overrides", "overrides");
+}
+
+void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
+{
+ auto doc = Json::requireDocument(filepath);
+ auto obj = Json::requireObject(doc);
+ m.manifestType = Json::requireString(obj, "manifestType");
+ if (m.manifestType != "minecraftModpack") {
+ throw JSONValidationError("Not a modpack manifest!");
+ }
+ m.manifestVersion = Json::requireInteger(obj, "manifestVersion");
+ if (m.manifestVersion != 1) {
+ throw JSONValidationError(
+ QString("Unknown manifest version (%1)").arg(m.manifestVersion));
+ }
+ loadManifestV1(m, obj);
+}
+
+bool Flame::File::parseFromBytes(const QByteArray& bytes)
+{
+ auto doc = Json::requireDocument(bytes);
+ auto obj = Json::requireObject(doc);
+ // result code signifies true failure.
+ if (obj.contains("code")) {
+ qCritical() << "Resolving of" << projectId << fileId
+ << "failed because of a negative result:";
+ qCritical() << bytes;
+ return false;
+ }
+ // CurseForge API v1 wraps the file object in "data"
+ if (obj.contains("data")) {
+ obj = Json::requireObject(obj, "data");
+ }
+ // Support both old cursemeta (FileNameOnDisk) and CurseForge API v1
+ // (fileName) field names
+ fileName = Json::ensureString(obj, "fileName", QString());
+ if (fileName.isEmpty()) {
+ fileName = Json::requireString(obj, "FileNameOnDisk");
+ }
+ QString rawUrl = Json::ensureString(obj, "downloadUrl", QString());
+ if (rawUrl.isEmpty()) {
+ rawUrl = Json::ensureString(obj, "DownloadURL", QString());
+ }
+ if (rawUrl.isEmpty()) {
+ // Mod has disabled third-party downloads — will be handled via browser
+ // download
+ qWarning() << "Mod" << projectId << fileId << "(" << fileName
+ << ") has no download URL (restricted)."
+ << "Will require browser download.";
+ resolved = false;
+ return true;
+ }
+ url = QUrl(rawUrl, QUrl::TolerantMode);
+ if (!url.isValid()) {
+ throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
+ }
+ // This is a piece of a Flame project JSON pulled out into the file metadata
+ // (here) for convenience It is also optional
+ QJsonObject projObj = Json::ensureObject(obj, "_Project", {});
+ if (!projObj.isEmpty()) {
+ QString strType =
+ Json::ensureString(projObj, "PackageType", "mod").toLower();
+ if (strType == "singlefile") {
+ type = File::Type::SingleFile;
+ } else if (strType == "ctoc") {
+ type = File::Type::Ctoc;
+ } else if (strType == "cmod2") {
+ type = File::Type::Cmod2;
+ } else if (strType == "mod") {
+ type = File::Type::Mod;
+ } else if (strType == "folder") {
+ type = File::Type::Folder;
+ } else if (strType == "modpack") {
+ type = File::Type::Modpack;
+ } else {
+ qCritical() << "Resolving of" << projectId << fileId
+ << "failed because of unknown file type:" << strType;
+ type = File::Type::Unknown;
+ return false;
+ }
+ targetFolder = Json::ensureString(projObj, "Path", "mods");
+ }
+ resolved = true;
+ return true;
+}
diff --git a/meshmc/launcher/modplatform/flame/PackManifest.h b/meshmc/launcher/modplatform/flame/PackManifest.h
new file mode 100644
index 0000000000..79ffb9585a
--- /dev/null
+++ b/meshmc/launcher/modplatform/flame/PackManifest.h
@@ -0,0 +1,79 @@
+/* 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 <QVector>
+#include <QUrl>
+
+namespace Flame
+{
+ struct File {
+ // NOTE: throws JSONValidationError
+ bool parseFromBytes(const QByteArray& bytes);
+
+ int projectId = 0;
+ int fileId = 0;
+ // NOTE: the opposite to 'optional'. This is at the time of writing
+ // unused.
+ bool required = true;
+
+ // our
+ bool resolved = false;
+ QString fileName;
+ QUrl url;
+ QString targetFolder = QLatin1String("mods");
+ enum class Type {
+ Unknown,
+ Folder,
+ Ctoc,
+ SingleFile,
+ Cmod2,
+ Modpack,
+ Mod
+ } type = Type::Mod;
+ };
+
+ struct Modloader {
+ QString id;
+ bool primary = false;
+ };
+
+ struct Minecraft {
+ QString version;
+ QString libraries;
+ QVector<Flame::Modloader> modLoaders;
+ };
+
+ struct Manifest {
+ QString manifestType;
+ int manifestVersion = 0;
+ Flame::Minecraft minecraft;
+ QString name;
+ QString version;
+ QString author;
+ QVector<Flame::File> files;
+ QString overrides;
+ };
+
+ void loadManifest(Flame::Manifest& m, const QString& filepath);
+} // namespace Flame
diff --git a/meshmc/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/meshmc/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
new file mode 100644
index 0000000000..eb4397337c
--- /dev/null
+++ b/meshmc/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
@@ -0,0 +1,202 @@
+/* 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 "PackFetchTask.h"
+#include "PrivatePackManager.h"
+
+#include <QDomDocument>
+#include "BuildConfig.h"
+#include "Application.h"
+
+namespace LegacyFTB
+{
+
+ void PackFetchTask::fetch()
+ {
+ publicPacks.clear();
+ thirdPartyPacks.clear();
+
+ jobPtr = new NetJob("LegacyFTB::ModpackFetch", m_network);
+
+ QUrl publicPacksUrl =
+ QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml");
+ qDebug() << "Downloading public version info from"
+ << publicPacksUrl.toString();
+ jobPtr->addNetAction(Net::Download::makeByteArray(
+ publicPacksUrl, &publicModpacksXmlFileData));
+
+ QUrl thirdPartyUrl =
+ QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml");
+ qDebug() << "Downloading thirdparty version info from"
+ << thirdPartyUrl.toString();
+ jobPtr->addNetAction(Net::Download::makeByteArray(
+ thirdPartyUrl, &thirdPartyModpacksXmlFileData));
+
+ QObject::connect(jobPtr.get(), &NetJob::succeeded, this,
+ &PackFetchTask::fileDownloadFinished);
+ QObject::connect(jobPtr.get(), &NetJob::failed, this,
+ &PackFetchTask::fileDownloadFailed);
+
+ jobPtr->start();
+ }
+
+ void PackFetchTask::fetchPrivate(const QStringList& toFetch)
+ {
+ QString privatePackBaseUrl =
+ BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml";
+
+ for (auto& packCode : toFetch) {
+ QByteArray* data = new QByteArray();
+ NetJob* job = new NetJob("Fetching private pack", m_network);
+ job->addNetAction(Net::Download::makeByteArray(
+ privatePackBaseUrl.arg(packCode), data));
+
+ QObject::connect(
+ job, &NetJob::succeeded, this, [this, job, data, packCode] {
+ ModpackList packs;
+ parseAndAddPacks(*data, PackType::Private, packs);
+ foreach (Modpack currentPack, packs) {
+ currentPack.packCode = packCode;
+ emit privateFileDownloadFinished(currentPack);
+ }
+
+ job->deleteLater();
+
+ data->clear();
+ delete data;
+ });
+
+ QObject::connect(job, &NetJob::failed, this,
+ [this, job, packCode, data](QString reason) {
+ emit privateFileDownloadFailed(reason,
+ packCode);
+ job->deleteLater();
+
+ data->clear();
+ delete data;
+ });
+
+ job->start();
+ }
+ }
+
+ void PackFetchTask::fileDownloadFinished()
+ {
+ jobPtr.reset();
+
+ QStringList failedLists;
+
+ if (!parseAndAddPacks(publicModpacksXmlFileData, PackType::Public,
+ publicPacks)) {
+ failedLists.append(tr("Public Packs"));
+ }
+
+ if (!parseAndAddPacks(thirdPartyModpacksXmlFileData,
+ PackType::ThirdParty, thirdPartyPacks)) {
+ failedLists.append(tr("Third Party Packs"));
+ }
+
+ if (failedLists.size() > 0) {
+ emit failed(tr("Failed to download some pack lists: %1")
+ .arg(failedLists.join("\n- ")));
+ } else {
+ emit finished(publicPacks, thirdPartyPacks);
+ }
+ }
+
+ bool PackFetchTask::parseAndAddPacks(QByteArray& data, PackType packType,
+ ModpackList& list)
+ {
+ QDomDocument doc;
+
+ QString errorMsg = "Unknown error.";
+ int errorLine = -1;
+ int errorCol = -1;
+
+ if (!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) {
+ auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:%3!")
+ .arg(errorMsg)
+ .arg(errorLine)
+ .arg(errorCol);
+ qWarning() << fullErrMsg;
+ data.clear();
+ return false;
+ }
+
+ QDomNodeList nodes = doc.elementsByTagName("modpack");
+ for (int i = 0; i < nodes.length(); i++) {
+ QDomElement element = nodes.at(i).toElement();
+
+ Modpack modpack;
+ modpack.name = element.attribute("name");
+ modpack.currentVersion = element.attribute("version");
+ modpack.mcVersion = element.attribute("mcVersion");
+ modpack.description = element.attribute("description");
+ modpack.mods = element.attribute("mods");
+ modpack.logo = element.attribute("logo");
+ modpack.oldVersions = element.attribute("oldVersions").split(";");
+ modpack.broken = false;
+ modpack.bugged = false;
+
+ // remove empty if the xml is bugged
+ for (QString curr : modpack.oldVersions) {
+ if (curr.isNull() || curr.isEmpty()) {
+ modpack.oldVersions.removeAll(curr);
+ modpack.bugged = true;
+ qWarning()
+ << "Removed some empty versions from" << modpack.name;
+ }
+ }
+
+ if (modpack.oldVersions.size() < 1) {
+ if (!modpack.currentVersion.isNull() &&
+ !modpack.currentVersion.isEmpty()) {
+ modpack.oldVersions.append(modpack.currentVersion);
+ qWarning() << "Added current version to oldVersions "
+ "because oldVersions was empty! (" +
+ modpack.name + ")";
+ } else {
+ modpack.broken = true;
+ qWarning() << "Broken pack:" << modpack.name
+ << " => No valid version!";
+ }
+ }
+
+ modpack.author = element.attribute("author");
+
+ modpack.dir = element.attribute("dir");
+ modpack.file = element.attribute("url");
+
+ modpack.type = packType;
+
+ list.append(modpack);
+ }
+
+ return true;
+ }
+
+ void PackFetchTask::fileDownloadFailed(QString reason)
+ {
+ qWarning() << "Fetching FTBPacks failed:" << reason;
+ emit failed(reason);
+ }
+
+} // namespace LegacyFTB
diff --git a/meshmc/launcher/modplatform/legacy_ftb/PackFetchTask.h b/meshmc/launcher/modplatform/legacy_ftb/PackFetchTask.h
new file mode 100644
index 0000000000..f791c78592
--- /dev/null
+++ b/meshmc/launcher/modplatform/legacy_ftb/PackFetchTask.h
@@ -0,0 +1,70 @@
+/* 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 "net/NetJob.h"
+#include <QTemporaryDir>
+#include <QByteArray>
+#include <QObject>
+#include "PackHelpers.h"
+
+namespace LegacyFTB
+{
+
+ class PackFetchTask : public QObject
+ {
+
+ Q_OBJECT
+
+ public:
+ PackFetchTask(shared_qobject_ptr<QNetworkAccessManager> network)
+ : QObject(nullptr), m_network(network) {};
+ virtual ~PackFetchTask() = default;
+
+ void fetch();
+ void fetchPrivate(const QStringList& toFetch);
+
+ private:
+ shared_qobject_ptr<QNetworkAccessManager> m_network;
+ NetJob::Ptr jobPtr;
+
+ QByteArray publicModpacksXmlFileData;
+ QByteArray thirdPartyModpacksXmlFileData;
+
+ bool parseAndAddPacks(QByteArray& data, PackType packType,
+ ModpackList& list);
+ ModpackList publicPacks;
+ ModpackList thirdPartyPacks;
+
+ protected slots:
+ void fileDownloadFinished();
+ void fileDownloadFailed(QString reason);
+
+ signals:
+ void finished(ModpackList publicPacks, ModpackList thirdPartyPacks);
+ void failed(QString reason);
+
+ void privateFileDownloadFinished(Modpack modpack);
+ void privateFileDownloadFailed(QString reason, QString packCode);
+ };
+
+} // namespace LegacyFTB
diff --git a/meshmc/launcher/modplatform/legacy_ftb/PackHelpers.h b/meshmc/launcher/modplatform/legacy_ftb/PackHelpers.h
new file mode 100644
index 0000000000..fa94194f6c
--- /dev/null
+++ b/meshmc/launcher/modplatform/legacy_ftb/PackHelpers.h
@@ -0,0 +1,61 @@
+/* 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 <QList>
+#include <QString>
+#include <QStringList>
+#include <QMetaType>
+
+namespace LegacyFTB
+{
+
+ // Header for structs etc...
+ enum class PackType { Public, ThirdParty, Private };
+
+ struct Modpack {
+ QString name;
+ QString description;
+ QString author;
+ QStringList oldVersions;
+ QString currentVersion;
+ QString mcVersion;
+ QString mods;
+ QString logo;
+
+ // Technical data
+ QString dir;
+ QString file; //<- Url in the xml, but doesn't make much sense
+
+ bool bugged = false;
+ bool broken = false;
+
+ PackType type;
+ QString packCode;
+ };
+
+ typedef QList<Modpack> ModpackList;
+
+} // namespace LegacyFTB
+
+// We need it for the proxy model
+Q_DECLARE_METATYPE(LegacyFTB::Modpack)
diff --git a/meshmc/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/meshmc/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
new file mode 100644
index 0000000000..f2ec74dec6
--- /dev/null
+++ b/meshmc/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
@@ -0,0 +1,247 @@
+/* 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 "PackInstallTask.h"
+
+#include <QtConcurrent>
+
+#include "MMCZip.h"
+#include "BaseInstance.h"
+#include "FileSystem.h"
+#include "settings/INISettingsObject.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "minecraft/GradleSpecifier.h"
+
+#include "BuildConfig.h"
+#include "Application.h"
+
+namespace LegacyFTB
+{
+
+ PackInstallTask::PackInstallTask(
+ shared_qobject_ptr<QNetworkAccessManager> network, Modpack pack,
+ QString version)
+ {
+ m_pack = pack;
+ m_version = version;
+ m_network = network;
+ }
+
+ void PackInstallTask::executeTask()
+ {
+ downloadPack();
+ }
+
+ void PackInstallTask::downloadPack()
+ {
+ setStatus(tr("Downloading zip for %1").arg(m_pack.name));
+
+ auto packoffset =
+ QString("%1/%2/%3")
+ .arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file);
+ auto entry =
+ APPLICATION->metacache()->resolveEntry("FTBPacks", packoffset);
+ netJobContainer = new NetJob("Download FTB Pack", m_network);
+
+ entry->setStale(true);
+ QString url;
+ if (m_pack.type == PackType::Private) {
+ url =
+ QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1")
+ .arg(packoffset);
+ } else {
+ url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1")
+ .arg(packoffset);
+ }
+ netJobContainer->addNetAction(Net::Download::makeCached(url, entry));
+ archivePath = entry->getFullPath();
+
+ connect(netJobContainer.get(), &NetJob::succeeded, this,
+ &PackInstallTask::onDownloadSucceeded);
+ connect(netJobContainer.get(), &NetJob::failed, this,
+ &PackInstallTask::onDownloadFailed);
+ connect(netJobContainer.get(), &NetJob::progress, this,
+ &PackInstallTask::onDownloadProgress);
+ netJobContainer->start();
+
+ progress(1, 4);
+ }
+
+ void PackInstallTask::onDownloadSucceeded()
+ {
+ abortable = false;
+ unzip();
+ }
+
+ void PackInstallTask::onDownloadFailed(QString reason)
+ {
+ abortable = false;
+ emitFailed(reason);
+ }
+
+ void PackInstallTask::onDownloadProgress(qint64 current, qint64 total)
+ {
+ abortable = true;
+ progress(current, total * 4);
+ setStatus(tr("Downloading zip for %1 (%2%)")
+ .arg(m_pack.name)
+ .arg(current / 10));
+ }
+
+ void PackInstallTask::unzip()
+ {
+ progress(2, 4);
+ setStatus(tr("Extracting modpack"));
+ QDir extractDir(m_stagingPath);
+
+ QString extractPath = extractDir.absolutePath() + "/unzip";
+ QString archivePathCopy = archivePath;
+ m_extractFuture = QtConcurrent::run(
+ QThreadPool::globalInstance(), [archivePathCopy, extractPath]() {
+ return MMCZip::extractDir(archivePathCopy, extractPath);
+ });
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished,
+ this, &PackInstallTask::onUnzipFinished);
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled,
+ this, &PackInstallTask::onUnzipCanceled);
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+ }
+
+ void PackInstallTask::onUnzipFinished()
+ {
+ install();
+ }
+
+ void PackInstallTask::onUnzipCanceled()
+ {
+ emitAborted();
+ }
+
+ void PackInstallTask::install()
+ {
+ progress(3, 4);
+ setStatus(tr("Installing modpack"));
+ QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
+ if (unzipMcDir.exists()) {
+ // ok, found minecraft dir, move contents to instance dir
+ if (!QDir().rename(m_stagingPath + "/unzip/minecraft",
+ m_stagingPath + "/.minecraft")) {
+ emitFailed(tr("Failed to move unzipped minecraft!"));
+ return;
+ }
+ }
+
+ QString instanceConfigPath =
+ FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings =
+ std::make_shared<INISettingsObject>(instanceConfigPath);
+ instanceSettings->suspendSave();
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+
+ MinecraftInstance instance(m_globalSettings, instanceSettings,
+ m_stagingPath);
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+ components->setComponentVersion("net.minecraft", m_pack.mcVersion,
+ true);
+
+ bool fallback = true;
+
+ // handle different versions
+ QFile packJson(m_stagingPath + "/.minecraft/pack.json");
+ QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods");
+ if (packJson.exists()) {
+ if (!packJson.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ emitFailed(tr("Failed to open pack.json!"));
+ return;
+ }
+ QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll());
+ packJson.close();
+
+ // we only care about the libs
+ QJsonArray libs = doc.object().value("libraries").toArray();
+
+ foreach (const QJsonValue& value, libs) {
+ QString nameValue = value.toObject().value("name").toString();
+ if (!nameValue.startsWith("net.minecraftforge")) {
+ continue;
+ }
+
+ GradleSpecifier forgeVersion(nameValue);
+
+ components->setComponentVersion(
+ "net.minecraftforge", forgeVersion.version()
+ .replace(m_pack.mcVersion, "")
+ .replace("-", ""));
+ packJson.remove();
+ fallback = false;
+ break;
+ }
+ }
+
+ if (jarmodDir.exists()) {
+ qDebug() << "Found jarmods, installing...";
+
+ QStringList jarmods;
+ for (auto info :
+ jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) {
+ qDebug() << "Jarmod:" << info.fileName();
+ jarmods.push_back(info.absoluteFilePath());
+ }
+
+ components->installJarMods(jarmods);
+ fallback = false;
+ }
+
+ // just nuke unzip directory, it s not needed anymore
+ FS::deletePath(m_stagingPath + "/unzip");
+
+ if (fallback) {
+ // TODO: Some fallback mechanism... or just keep failing!
+ emitFailed(tr("No installation method found!"));
+ return;
+ }
+
+ components->saveNow();
+
+ progress(4, 4);
+
+ instance.setName(m_instName);
+ if (m_instIcon == "default") {
+ m_instIcon = "ftb_logo";
+ }
+ instance.setIconKey(m_instIcon);
+ instanceSettings->resumeSave();
+
+ emitSucceeded();
+ }
+
+ bool PackInstallTask::abort()
+ {
+ if (abortable) {
+ return netJobContainer->abort();
+ }
+ return false;
+ }
+
+} // namespace LegacyFTB
diff --git a/meshmc/launcher/modplatform/legacy_ftb/PackInstallTask.h b/meshmc/launcher/modplatform/legacy_ftb/PackInstallTask.h
new file mode 100644
index 0000000000..fa20023421
--- /dev/null
+++ b/meshmc/launcher/modplatform/legacy_ftb/PackInstallTask.h
@@ -0,0 +1,82 @@
+/* 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 "InstanceTask.h"
+#include "net/NetJob.h"
+#include "meta/Index.h"
+#include "meta/Version.h"
+#include "meta/VersionList.h"
+#include "PackHelpers.h"
+
+#include "net/NetJob.h"
+
+#include <nonstd/optional>
+
+namespace LegacyFTB
+{
+
+ class PackInstallTask : public InstanceTask
+ {
+ Q_OBJECT
+
+ public:
+ explicit PackInstallTask(
+ shared_qobject_ptr<QNetworkAccessManager> network, Modpack pack,
+ QString version);
+ virtual ~PackInstallTask() {}
+
+ bool canAbort() const override
+ {
+ return true;
+ }
+ bool abort() override;
+
+ protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+
+ private:
+ void downloadPack();
+ void unzip();
+ void install();
+
+ private slots:
+ void onDownloadSucceeded();
+ void onDownloadFailed(QString reason);
+ void onDownloadProgress(qint64 current, qint64 total);
+
+ void onUnzipFinished();
+ void onUnzipCanceled();
+
+ private: /* data */
+ shared_qobject_ptr<QNetworkAccessManager> m_network;
+ bool abortable = false;
+ QFuture<nonstd::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ NetJob::Ptr netJobContainer;
+ QString archivePath;
+
+ Modpack m_pack;
+ QString m_version;
+ };
+
+} // namespace LegacyFTB
diff --git a/meshmc/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp b/meshmc/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp
new file mode 100644
index 0000000000..82c34b7ac2
--- /dev/null
+++ b/meshmc/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp
@@ -0,0 +1,60 @@
+/* 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 "PrivatePackManager.h"
+
+#include <QDebug>
+
+#include "FileSystem.h"
+
+namespace LegacyFTB
+{
+
+ void PrivatePackManager::load()
+ {
+ try {
+ auto parts = QString::fromUtf8(FS::read(m_filename))
+ .split('\n', Qt::SkipEmptyParts);
+ currentPacks = QSet<QString>(parts.begin(), parts.end());
+ dirty = false;
+ } catch (...) {
+ currentPacks = {};
+ qWarning() << "Failed to read third party FTB pack codes from"
+ << m_filename;
+ }
+ }
+
+ void PrivatePackManager::save() const
+ {
+ if (!dirty) {
+ return;
+ }
+ try {
+ QStringList list = currentPacks.values();
+ FS::write(m_filename, list.join('\n').toUtf8());
+ dirty = false;
+ } catch (...) {
+ qWarning() << "Failed to write third party FTB pack codes to"
+ << m_filename;
+ }
+ }
+
+} // namespace LegacyFTB
diff --git a/meshmc/launcher/modplatform/legacy_ftb/PrivatePackManager.h b/meshmc/launcher/modplatform/legacy_ftb/PrivatePackManager.h
new file mode 100644
index 0000000000..d7f94133b4
--- /dev/null
+++ b/meshmc/launcher/modplatform/legacy_ftb/PrivatePackManager.h
@@ -0,0 +1,65 @@
+/* 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 <QSet>
+#include <QString>
+#include <QFile>
+
+namespace LegacyFTB
+{
+
+ class PrivatePackManager
+ {
+ public:
+ ~PrivatePackManager()
+ {
+ save();
+ }
+ void load();
+ void save() const;
+ bool empty() const
+ {
+ return currentPacks.empty();
+ }
+ const QSet<QString>& getCurrentPackCodes() const
+ {
+ return currentPacks;
+ }
+ void add(const QString& code)
+ {
+ currentPacks.insert(code);
+ dirty = true;
+ }
+ void remove(const QString& code)
+ {
+ currentPacks.remove(code);
+ dirty = true;
+ }
+
+ private:
+ QSet<QString> currentPacks;
+ QString m_filename = "private_packs.txt";
+ mutable bool dirty = false;
+ };
+
+} // namespace LegacyFTB
diff --git a/meshmc/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/meshmc/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
new file mode 100644
index 0000000000..6c54cd6bcb
--- /dev/null
+++ b/meshmc/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
@@ -0,0 +1,280 @@
+/* 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
+ *
+ * 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 "FTBPackInstallTask.h"
+
+#include "FileSystem.h"
+#include "Json.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "net/ChecksumValidator.h"
+#include "settings/INISettingsObject.h"
+
+#include "BuildConfig.h"
+#include "Application.h"
+
+namespace ModpacksCH
+{
+
+ PackInstallTask::PackInstallTask(Modpack pack, QString version)
+ {
+ m_pack = pack;
+ m_version_name = version;
+ }
+
+ bool PackInstallTask::abort()
+ {
+ if (abortable) {
+ return jobPtr->abort();
+ }
+ return false;
+ }
+
+ void PackInstallTask::executeTask()
+ {
+ // Find pack version
+ bool found = false;
+ VersionInfo version;
+
+ for (auto vInfo : m_pack.versions) {
+ if (vInfo.name == m_version_name) {
+ found = true;
+ version = vInfo;
+ break;
+ }
+ }
+
+ if (!found) {
+ emitFailed(
+ tr("Failed to find pack version %1").arg(m_version_name));
+ return;
+ }
+
+ auto* netJob =
+ new NetJob("ModpacksCH::VersionFetch", APPLICATION->network());
+ auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL +
+ "public/modpack/%1/%2")
+ .arg(m_pack.id)
+ .arg(version.id);
+ netJob->addNetAction(
+ Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+
+ QObject::connect(netJob, &NetJob::succeeded, this,
+ &PackInstallTask::onDownloadSucceeded);
+ QObject::connect(netJob, &NetJob::failed, this,
+ &PackInstallTask::onDownloadFailed);
+ }
+
+ void PackInstallTask::onDownloadSucceeded()
+ {
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at "
+ << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto obj = doc.object();
+
+ ModpacksCH::Version version;
+ try {
+ ModpacksCH::loadVersion(version, obj);
+ } catch (const JSONValidationError& e) {
+ emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
+ jobPtr.reset();
+ return;
+ }
+ m_version = version;
+
+ downloadPack();
+ }
+
+ void PackInstallTask::onDownloadFailed(QString reason)
+ {
+ emitFailed(reason);
+ jobPtr.reset();
+ }
+
+ void PackInstallTask::downloadPack()
+ {
+ setStatus(tr("Downloading mods..."));
+
+ jobPtr = new NetJob(tr("Mod download"), APPLICATION->network());
+ for (auto file : m_version.files) {
+ if (file.serverOnly)
+ continue;
+ if (file.url.isEmpty()) {
+ qWarning() << "Skipping" << file.name
+ << "- no download URL available";
+ continue;
+ }
+
+ QFileInfo fileName(file.name);
+ auto cacheName = fileName.completeBaseName() + "-" + file.sha1 +
+ "." + fileName.suffix();
+
+ auto entry = APPLICATION->metacache()->resolveEntry(
+ "ModpacksCHPacks", cacheName);
+ entry->setStale(true);
+
+ auto relpath = FS::PathCombine("minecraft", file.path, file.name);
+ auto path = FS::PathCombine(m_stagingPath, relpath);
+
+ if (filesToCopy.contains(path)) {
+ qWarning() << "Ignoring" << file.url
+ << "as a file of that path is already downloading.";
+ continue;
+ }
+ qDebug() << "Will download" << file.url << "to" << path;
+ filesToCopy[path] = entry->getFullPath();
+
+ auto dl = Net::Download::makeCached(file.url, entry);
+ if (!file.sha1.isEmpty()) {
+ auto rawSha1 = QByteArray::fromHex(file.sha1.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(
+ QCryptographicHash::Sha1, rawSha1));
+ }
+ jobPtr->addNetAction(dl);
+ }
+
+ connect(jobPtr.get(), &NetJob::succeeded, this, [&]() {
+ abortable = false;
+ install();
+ jobPtr.reset();
+ });
+ connect(jobPtr.get(), &NetJob::failed, [&](QString reason) {
+ abortable = false;
+ emitFailed(reason);
+ jobPtr.reset();
+ });
+ connect(jobPtr.get(), &NetJob::progress,
+ [&](qint64 current, qint64 total) {
+ abortable = true;
+ setProgress(current, total);
+ });
+
+ jobPtr->start();
+ }
+
+ void PackInstallTask::install()
+ {
+ setStatus(tr("Copying modpack files"));
+
+ for (auto iter = filesToCopy.begin(); iter != filesToCopy.end();
+ iter++) {
+ auto& to = iter.key();
+ auto& from = iter.value();
+ FS::copy fileCopyOperation(from, to);
+ if (!fileCopyOperation()) {
+ qWarning() << "Failed to copy" << from << "to" << to;
+ emitFailed(tr("Failed to copy files"));
+ return;
+ }
+ }
+
+ setStatus(tr("Installing modpack"));
+
+ auto instanceConfigPath =
+ FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings =
+ std::make_shared<INISettingsObject>(instanceConfigPath);
+ instanceSettings->suspendSave();
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+
+ MinecraftInstance instance(m_globalSettings, instanceSettings,
+ m_stagingPath);
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+
+ for (auto target : m_version.targets) {
+ if (target.type == "game" && target.name == "minecraft") {
+ components->setComponentVersion("net.minecraft", target.version,
+ true);
+ break;
+ }
+ }
+
+ for (auto target : m_version.targets) {
+ if (target.type != "modloader")
+ continue;
+
+ if (target.name == "forge") {
+ components->setComponentVersion("net.minecraftforge",
+ target.version, true);
+ } else if (target.name == "neoforge") {
+ components->setComponentVersion("net.neoforged", target.version,
+ true);
+ } else if (target.name == "fabric") {
+ components->setComponentVersion("net.fabricmc.fabric-loader",
+ target.version, true);
+ } else if (target.name == "quilt-loader") {
+ components->setComponentVersion("org.quiltmc.quilt-loader",
+ target.version, true);
+ }
+ }
+
+ // install any jar mods
+ QDir jarModsDir(FS::PathCombine(m_stagingPath, "minecraft", "jarmods"));
+ if (jarModsDir.exists()) {
+ QStringList jarMods;
+
+ for (const auto& info :
+ jarModsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) {
+ jarMods.push_back(info.absoluteFilePath());
+ }
+
+ components->installJarMods(jarMods);
+ }
+
+ components->saveNow();
+
+ instance.setName(m_instName);
+ instance.setIconKey(m_instIcon);
+ instanceSettings->resumeSave();
+
+ emitSucceeded();
+ }
+
+} // namespace ModpacksCH
diff --git a/meshmc/launcher/modplatform/modpacksch/FTBPackInstallTask.h b/meshmc/launcher/modplatform/modpacksch/FTBPackInstallTask.h
new file mode 100644
index 0000000000..bc18e1c2c0
--- /dev/null
+++ b/meshmc/launcher/modplatform/modpacksch/FTBPackInstallTask.h
@@ -0,0 +1,88 @@
+/* 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
+ *
+ * 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 "FTBPackManifest.h"
+
+#include "InstanceTask.h"
+#include "net/NetJob.h"
+
+namespace ModpacksCH
+{
+
+ class PackInstallTask : public InstanceTask
+ {
+ Q_OBJECT
+
+ public:
+ explicit PackInstallTask(Modpack pack, QString version);
+ virtual ~PackInstallTask() {}
+
+ bool canAbort() const override
+ {
+ return true;
+ }
+ bool abort() override;
+
+ protected:
+ virtual void executeTask() override;
+
+ private slots:
+ void onDownloadSucceeded();
+ void onDownloadFailed(QString reason);
+
+ private:
+ void downloadPack();
+ void install();
+
+ private:
+ bool abortable = false;
+
+ NetJob::Ptr jobPtr;
+ QByteArray response;
+
+ Modpack m_pack;
+ QString m_version_name;
+ Version m_version;
+
+ QMap<QString, QString> filesToCopy;
+ };
+
+} // namespace ModpacksCH
diff --git a/meshmc/launcher/modplatform/modpacksch/FTBPackManifest.cpp b/meshmc/launcher/modplatform/modpacksch/FTBPackManifest.cpp
new file mode 100644
index 0000000000..afbf59736c
--- /dev/null
+++ b/meshmc/launcher/modplatform/modpacksch/FTBPackManifest.cpp
@@ -0,0 +1,205 @@
+/* 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 2020 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
+ *
+ * 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 "FTBPackManifest.h"
+
+#include "Json.h"
+
+static void loadSpecs(ModpacksCH::Specs& s, QJsonObject& obj)
+{
+ s.id = Json::requireInteger(obj, "id");
+ s.minimum = Json::requireInteger(obj, "minimum");
+ s.recommended = Json::requireInteger(obj, "recommended");
+}
+
+static void loadTag(ModpacksCH::Tag& t, QJsonObject& obj)
+{
+ t.id = Json::requireInteger(obj, "id");
+ t.name = Json::requireString(obj, "name");
+}
+
+static void loadArt(ModpacksCH::Art& a, QJsonObject& obj)
+{
+ a.id = Json::requireInteger(obj, "id");
+ a.url = Json::requireString(obj, "url");
+ a.type = Json::requireString(obj, "type");
+ a.width = Json::requireInteger(obj, "width");
+ a.height = Json::requireInteger(obj, "height");
+ a.compressed = Json::requireBoolean(obj, "compressed");
+ a.sha1 = Json::requireString(obj, "sha1");
+ a.size = Json::requireInteger(obj, "size");
+ a.updated = Json::requireInteger(obj, "updated");
+}
+
+static void loadAuthor(ModpacksCH::Author& a, QJsonObject& obj)
+{
+ a.id = Json::requireInteger(obj, "id");
+ a.name = Json::requireString(obj, "name");
+ a.type = Json::requireString(obj, "type");
+ a.website = Json::requireString(obj, "website");
+ a.updated = Json::requireInteger(obj, "updated");
+}
+
+static void loadVersionInfo(ModpacksCH::VersionInfo& v, QJsonObject& obj)
+{
+ v.id = Json::requireInteger(obj, "id");
+ v.name = Json::requireString(obj, "name");
+ v.type = Json::requireString(obj, "type");
+ v.updated = Json::requireInteger(obj, "updated");
+ auto specs = Json::requireObject(obj, "specs");
+ loadSpecs(v.specs, specs);
+}
+
+void ModpacksCH::loadModpack(ModpacksCH::Modpack& m, QJsonObject& obj)
+{
+ m.id = Json::requireInteger(obj, "id");
+ m.name = Json::requireString(obj, "name");
+ m.synopsis = Json::requireString(obj, "synopsis");
+ m.description = Json::requireString(obj, "description");
+ m.type = Json::requireString(obj, "type");
+ m.featured = Json::requireBoolean(obj, "featured");
+ m.installs = Json::requireInteger(obj, "installs");
+ m.plays = Json::requireInteger(obj, "plays");
+ m.updated = Json::requireInteger(obj, "updated");
+ m.refreshed = Json::requireInteger(obj, "refreshed");
+ auto artArr = Json::requireArray(obj, "art");
+ for (QJsonValueRef artRaw : artArr) {
+ auto artObj = Json::requireObject(artRaw);
+ ModpacksCH::Art art;
+ loadArt(art, artObj);
+ m.art.append(art);
+ }
+ auto authorArr = Json::requireArray(obj, "authors");
+ for (QJsonValueRef authorRaw : authorArr) {
+ auto authorObj = Json::requireObject(authorRaw);
+ ModpacksCH::Author author;
+ loadAuthor(author, authorObj);
+ m.authors.append(author);
+ }
+ auto versionArr = Json::requireArray(obj, "versions");
+ for (QJsonValueRef versionRaw : versionArr) {
+ auto versionObj = Json::requireObject(versionRaw);
+ ModpacksCH::VersionInfo version;
+ loadVersionInfo(version, versionObj);
+ m.versions.append(version);
+ }
+ auto tagArr = Json::requireArray(obj, "tags");
+ for (QJsonValueRef tagRaw : tagArr) {
+ auto tagObj = Json::requireObject(tagRaw);
+ ModpacksCH::Tag tag;
+ loadTag(tag, tagObj);
+ m.tags.append(tag);
+ }
+ m.updated = Json::requireInteger(obj, "updated");
+}
+
+static void loadVersionTarget(ModpacksCH::VersionTarget& a, QJsonObject& obj)
+{
+ a.id = Json::requireInteger(obj, "id");
+ a.name = Json::requireString(obj, "name");
+ a.type = Json::requireString(obj, "type");
+ a.version = Json::requireString(obj, "version");
+ a.updated = Json::requireInteger(obj, "updated");
+}
+
+static void loadVersionFile(ModpacksCH::VersionFile& a, QJsonObject& obj)
+{
+ a.id = Json::requireInteger(obj, "id");
+ a.type = Json::requireString(obj, "type");
+ a.path = Json::requireString(obj, "path");
+ a.name = Json::requireString(obj, "name");
+ a.version = Json::requireString(obj, "version");
+ a.url = Json::ensureString(obj, "url", QString());
+ a.sha1 = Json::ensureString(obj, "sha1", QString());
+ a.size = Json::requireInteger(obj, "size");
+ a.clientOnly = Json::requireBoolean(obj, "clientonly");
+ a.serverOnly = Json::requireBoolean(obj, "serveronly");
+ a.optional = Json::requireBoolean(obj, "optional");
+ a.updated = Json::requireInteger(obj, "updated");
+ // Some files reference CurseForge mods with no direct download URL.
+ // Construct edge CDN URL from CurseForge file ID if available.
+ if (a.url.isEmpty() && obj.contains("curseforge")) {
+ auto cf = Json::requireObject(obj, "curseforge");
+ int cfFileId = Json::requireInteger(cf, "file");
+ // CurseForge edge CDN URL format: files/{first 4 digits}/{remaining
+ // digits}/{filename}
+ QString fileIdStr = QString::number(cfFileId);
+ QString prefix = fileIdStr.mid(0, 4);
+ QString suffix = fileIdStr.mid(4);
+ a.url = QString("https://edge.forgecdn.net/files/%1/%2/%3")
+ .arg(prefix, suffix, a.name);
+ qDebug() << "Constructed CurseForge CDN URL for" << a.name << ":"
+ << a.url;
+ }
+}
+
+void ModpacksCH::loadVersion(ModpacksCH::Version& m, QJsonObject& obj)
+{
+ m.id = Json::requireInteger(obj, "id");
+ m.parent = Json::requireInteger(obj, "parent");
+ m.name = Json::requireString(obj, "name");
+ m.type = Json::requireString(obj, "type");
+ m.installs = Json::requireInteger(obj, "installs");
+ m.plays = Json::requireInteger(obj, "plays");
+ m.updated = Json::requireInteger(obj, "updated");
+ m.refreshed = Json::requireInteger(obj, "refreshed");
+ auto specs = Json::requireObject(obj, "specs");
+ loadSpecs(m.specs, specs);
+ auto targetArr = Json::requireArray(obj, "targets");
+ for (QJsonValueRef targetRaw : targetArr) {
+ auto versionObj = Json::requireObject(targetRaw);
+ ModpacksCH::VersionTarget target;
+ loadVersionTarget(target, versionObj);
+ m.targets.append(target);
+ }
+ auto fileArr = Json::requireArray(obj, "files");
+ for (QJsonValueRef fileRaw : fileArr) {
+ auto fileObj = Json::requireObject(fileRaw);
+ ModpacksCH::VersionFile file;
+ loadVersionFile(file, fileObj);
+ m.files.append(file);
+ }
+}
+
+// static void loadVersionChangelog(ModpacksCH::VersionChangelog & m,
+// QJsonObject & obj)
+//{
+// m.content = Json::requireString(obj, "content");
+// m.updated = Json::requireInteger(obj, "updated");
+// }
diff --git a/meshmc/launcher/modplatform/modpacksch/FTBPackManifest.h b/meshmc/launcher/modplatform/modpacksch/FTBPackManifest.h
new file mode 100644
index 0000000000..3a0d7655a3
--- /dev/null
+++ b/meshmc/launcher/modplatform/modpacksch/FTBPackManifest.h
@@ -0,0 +1,154 @@
+/* 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2020 Petr Mrazek <peterix@gmail.com>
+ *
+ * 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 <QString>
+#include <QVector>
+#include <QUrl>
+#include <QJsonObject>
+#include <QMetaType>
+
+namespace ModpacksCH
+{
+
+ struct Specs {
+ int id;
+ int minimum;
+ int recommended;
+ };
+
+ struct Tag {
+ int id;
+ QString name;
+ };
+
+ struct Art {
+ int id;
+ QString url;
+ QString type;
+ int width;
+ int height;
+ bool compressed;
+ QString sha1;
+ int size;
+ int64_t updated;
+ };
+
+ struct Author {
+ int id;
+ QString name;
+ QString type;
+ QString website;
+ int64_t updated;
+ };
+
+ struct VersionInfo {
+ int id;
+ QString name;
+ QString type;
+ int64_t updated;
+ Specs specs;
+ };
+
+ struct Modpack {
+ int id;
+ QString name;
+ QString synopsis;
+ QString description;
+ QString type;
+ bool featured;
+ int installs;
+ int plays;
+ int64_t updated;
+ int64_t refreshed;
+ QVector<Art> art;
+ QVector<Author> authors;
+ QVector<VersionInfo> versions;
+ QVector<Tag> tags;
+ };
+
+ struct VersionTarget {
+ int id;
+ QString type;
+ QString name;
+ QString version;
+ int64_t updated;
+ };
+
+ struct VersionFile {
+ int id;
+ QString type;
+ QString path;
+ QString name;
+ QString version;
+ QString url;
+ QString sha1;
+ int size;
+ bool clientOnly;
+ bool serverOnly;
+ bool optional;
+ int64_t updated;
+ };
+
+ struct Version {
+ int id;
+ int parent;
+ QString name;
+ QString type;
+ int installs;
+ int plays;
+ int64_t updated;
+ int64_t refreshed;
+ Specs specs;
+ QVector<VersionTarget> targets;
+ QVector<VersionFile> files;
+ };
+
+ struct VersionChangelog {
+ QString content;
+ int64_t updated;
+ };
+
+ void loadModpack(Modpack& m, QJsonObject& obj);
+
+ void loadVersion(Version& m, QJsonObject& obj);
+} // namespace ModpacksCH
+
+Q_DECLARE_METATYPE(ModpacksCH::Modpack)
diff --git a/meshmc/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/meshmc/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
new file mode 100644
index 0000000000..9690df8a13
--- /dev/null
+++ b/meshmc/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -0,0 +1,84 @@
+/* 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 "ModrinthPackIndex.h"
+
+#include "Json.h"
+
+void Modrinth::loadIndexedPack(Modrinth::IndexedPack& pack, QJsonObject& obj)
+{
+ pack.projectId = Json::ensureString(obj, "project_id", "");
+ if (pack.projectId.isEmpty()) {
+ pack.projectId = Json::requireString(obj, "id");
+ }
+ pack.slug = Json::ensureString(obj, "slug", "");
+ pack.name = Json::requireString(obj, "title");
+ pack.description = Json::ensureString(obj, "description", "");
+ pack.author = Json::ensureString(obj, "author", "");
+ pack.downloads = Json::ensureInteger(obj, "downloads", 0);
+
+ pack.iconUrl = Json::ensureString(obj, "icon_url", "");
+}
+
+void Modrinth::loadIndexedPackVersions(Modrinth::IndexedPack& pack,
+ QJsonArray& arr)
+{
+ pack.versions.clear();
+ for (auto versionRaw : arr) {
+ auto obj = versionRaw.toObject();
+ Modrinth::IndexedVersion version;
+ version.id = Json::requireString(obj, "id");
+ version.projectId =
+ Json::ensureString(obj, "project_id", pack.projectId);
+ version.name = Json::ensureString(obj, "name", "");
+ version.versionNumber = Json::requireString(obj, "version_number");
+
+ auto gameVersions = Json::ensureArray(obj, "game_versions");
+ if (!gameVersions.isEmpty()) {
+ version.mcVersion = gameVersions.first().toString();
+ }
+
+ auto loaders = Json::ensureArray(obj, "loaders");
+ QStringList loaderList;
+ for (auto loader : loaders) {
+ loaderList.append(loader.toString());
+ }
+ version.loaders = loaderList.join(", ");
+
+ auto files = Json::ensureArray(obj, "files");
+ for (auto fileRaw : files) {
+ auto fileObj = fileRaw.toObject();
+ bool primary = Json::ensureBoolean(fileObj, "primary", false);
+ if (primary || files.size() == 1) {
+ version.downloadUrl = Json::ensureString(fileObj, "url", "");
+ version.downloadSize = Json::ensureInteger(fileObj, "size", 0);
+ auto hashes = Json::ensureObject(fileObj, "hashes");
+ version.sha1 = Json::ensureString(hashes, "sha1", "");
+ break;
+ }
+ }
+
+ if (!version.downloadUrl.isEmpty()) {
+ pack.versions.append(version);
+ }
+ }
+ pack.versionsLoaded = true;
+}
diff --git a/meshmc/launcher/modplatform/modrinth/ModrinthPackIndex.h b/meshmc/launcher/modplatform/modrinth/ModrinthPackIndex.h
new file mode 100644
index 0000000000..3abadcb6b8
--- /dev/null
+++ b/meshmc/launcher/modplatform/modrinth/ModrinthPackIndex.h
@@ -0,0 +1,62 @@
+/* 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 <QList>
+#include <QMetaType>
+#include <QString>
+#include <QVector>
+
+namespace Modrinth
+{
+
+ struct IndexedVersion {
+ QString id;
+ QString projectId;
+ QString name;
+ QString versionNumber;
+ QString mcVersion;
+ QString downloadUrl;
+ int downloadSize = 0;
+ QString sha1;
+ QString loaders;
+ };
+
+ struct IndexedPack {
+ QString projectId;
+ QString slug;
+ QString name;
+ QString description;
+ QString author;
+ QString iconUrl;
+ int downloads = 0;
+
+ bool versionsLoaded = false;
+ QVector<IndexedVersion> versions;
+ };
+
+ void loadIndexedPack(IndexedPack& pack, QJsonObject& obj);
+ void loadIndexedPackVersions(IndexedPack& pack, QJsonArray& arr);
+
+} // namespace Modrinth
+
+Q_DECLARE_METATYPE(Modrinth::IndexedPack)
diff --git a/meshmc/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/meshmc/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
new file mode 100644
index 0000000000..f2962925b2
--- /dev/null
+++ b/meshmc/launcher/modplatform/modrinth/ModrinthPackManifest.cpp
@@ -0,0 +1,85 @@
+/* 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 "ModrinthPackManifest.h"
+
+#include "Json.h"
+
+static void loadFile(Modrinth::File& f, QJsonObject& fileObj)
+{
+ f.path = Json::requireString(fileObj, "path");
+
+ auto downloads = Json::requireArray(fileObj, "downloads");
+ if (!downloads.isEmpty()) {
+ f.downloadUrl = QUrl(downloads.first().toString());
+ }
+
+ auto hashes = Json::ensureObject(fileObj, "hashes");
+ f.sha1 = Json::ensureString(hashes, "sha1", "");
+ f.sha512 = Json::ensureString(hashes, "sha512", "");
+
+ f.fileSize = Json::ensureInteger(fileObj, "fileSize", 0);
+}
+
+static void loadDependencies(Modrinth::Manifest& m, QJsonObject& deps)
+{
+ m.minecraftVersion = Json::ensureString(deps, "minecraft", "");
+ m.forgeVersion = Json::ensureString(deps, "forge", "");
+ m.fabricVersion = Json::ensureString(deps, "fabric-loader", "");
+ m.quiltVersion = Json::ensureString(deps, "quilt-loader", "");
+ m.neoForgeVersion = Json::ensureString(deps, "neoforge", "");
+}
+
+void Modrinth::loadManifest(Modrinth::Manifest& m, const QString& filepath)
+{
+ auto doc = Json::requireDocument(filepath);
+ auto obj = Json::requireObject(doc);
+
+ m.formatVersion = Json::requireInteger(obj, "formatVersion");
+ if (m.formatVersion != 1) {
+ throw JSONValidationError(
+ QString("Unsupported Modrinth modpack format version: %1")
+ .arg(m.formatVersion));
+ }
+
+ m.game = Json::requireString(obj, "game");
+ if (m.game != "minecraft") {
+ throw JSONValidationError(
+ QString("Unsupported game in Modrinth modpack: %1").arg(m.game));
+ }
+
+ m.versionId = Json::ensureString(obj, "versionId", "");
+ m.name = Json::ensureString(obj, "name", "Unnamed");
+ m.summary = Json::ensureString(obj, "summary", "");
+
+ auto files = Json::requireArray(obj, "files");
+ for (auto fileRaw : files) {
+ auto fileObj = Json::requireObject(fileRaw);
+ Modrinth::File file;
+ loadFile(file, fileObj);
+ m.files.append(file);
+ }
+
+ auto deps = Json::ensureObject(obj, "dependencies");
+ if (!deps.isEmpty()) {
+ loadDependencies(m, deps);
+ }
+}
diff --git a/meshmc/launcher/modplatform/modrinth/ModrinthPackManifest.h b/meshmc/launcher/modplatform/modrinth/ModrinthPackManifest.h
new file mode 100644
index 0000000000..a9caa16f14
--- /dev/null
+++ b/meshmc/launcher/modplatform/modrinth/ModrinthPackManifest.h
@@ -0,0 +1,62 @@
+/* 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 <QUrl>
+#include <QVector>
+
+namespace Modrinth
+{
+
+ struct File {
+ QString path;
+ QUrl downloadUrl;
+ QString sha1;
+ QString sha512;
+ int fileSize = 0;
+ };
+
+ struct Dependency {
+ QString versionId;
+ QString projectId;
+ QString fileName;
+ };
+
+ struct Manifest {
+ int formatVersion = 0;
+ QString game;
+ QString versionId;
+ QString name;
+ QString summary;
+ QVector<Modrinth::File> files;
+
+ QString minecraftVersion;
+ QString forgeVersion;
+ QString fabricVersion;
+ QString quiltVersion;
+ QString neoForgeVersion;
+ };
+
+ void loadManifest(Modrinth::Manifest& m, const QString& filepath);
+
+} // namespace Modrinth
diff --git a/meshmc/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/meshmc/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
new file mode 100644
index 0000000000..e197ccbcd9
--- /dev/null
+++ b/meshmc/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
@@ -0,0 +1,168 @@
+/* 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 "SingleZipPackInstallTask.h"
+
+#include <QtConcurrent>
+
+#include "MMCZip.h"
+#include "TechnicPackProcessor.h"
+#include "FileSystem.h"
+
+#include "Application.h"
+
+Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(
+ const QUrl& sourceUrl, const QString& minecraftVersion)
+{
+ m_sourceUrl = sourceUrl;
+ m_minecraftVersion = minecraftVersion;
+}
+
+bool Technic::SingleZipPackInstallTask::abort()
+{
+ if (m_abortable) {
+ return m_filesNetJob->abort();
+ }
+ return false;
+}
+
+void Technic::SingleZipPackInstallTask::executeTask()
+{
+ setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
+
+ const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path();
+ auto entry = APPLICATION->metacache()->resolveEntry("general", path);
+ entry->setStale(true);
+ m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network());
+ m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
+ m_archivePath = entry->getFullPath();
+ auto job = m_filesNetJob.get();
+ connect(job, &NetJob::succeeded, this,
+ &Technic::SingleZipPackInstallTask::downloadSucceeded);
+ connect(job, &NetJob::progress, this,
+ &Technic::SingleZipPackInstallTask::downloadProgressChanged);
+ connect(job, &NetJob::failed, this,
+ &Technic::SingleZipPackInstallTask::downloadFailed);
+ m_filesNetJob->start();
+}
+
+void Technic::SingleZipPackInstallTask::downloadSucceeded()
+{
+ m_abortable = false;
+
+ setStatus(tr("Extracting modpack"));
+ QDir extractDir(FS::PathCombine(m_stagingPath, ".minecraft"));
+ qDebug() << "Attempting to create instance from" << m_archivePath;
+
+ QString archivePath = m_archivePath;
+ QString extractPath = extractDir.absolutePath();
+ m_extractFuture = QtConcurrent::run(
+ QThreadPool::globalInstance(), [archivePath, extractPath]() {
+ return MMCZip::extractSubDir(archivePath, QString(""), extractPath);
+ });
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished,
+ this, &Technic::SingleZipPackInstallTask::extractFinished);
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled,
+ this, &Technic::SingleZipPackInstallTask::extractAborted);
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+ m_filesNetJob.reset();
+}
+
+void Technic::SingleZipPackInstallTask::downloadFailed(QString reason)
+{
+ m_abortable = false;
+ emitFailed(reason);
+ m_filesNetJob.reset();
+}
+
+void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current,
+ qint64 total)
+{
+ m_abortable = true;
+ setProgress(current / 2, total);
+}
+
+void Technic::SingleZipPackInstallTask::extractFinished()
+{
+ if (!m_extractFuture.result()) {
+ emitFailed(tr("Failed to extract modpack"));
+ return;
+ }
+ QDir extractDir(m_stagingPath);
+
+ qDebug() << "Fixing permissions for extracted pack files...";
+ QDirIterator it(extractDir, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ auto filepath = it.next();
+ QFileInfo file(filepath);
+ auto permissions = QFile::permissions(filepath);
+ auto origPermissions = permissions;
+ if (file.isDir()) {
+ // Folder +rwx for current user
+ permissions |= QFileDevice::Permission::ReadUser |
+ QFileDevice::Permission::WriteUser |
+ QFileDevice::Permission::ExeUser;
+ } else {
+ // File +rw for current user
+ permissions |= QFileDevice::Permission::ReadUser |
+ QFileDevice::Permission::WriteUser;
+ }
+ if (origPermissions != permissions) {
+ if (!QFile::setPermissions(filepath, permissions)) {
+ logWarning(
+ tr("Could not fix permissions for %1").arg(filepath));
+ } else {
+ qDebug() << "Fixed" << filepath;
+ }
+ }
+ }
+
+ shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor =
+ new Technic::TechnicPackProcessor();
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded,
+ this, &Technic::SingleZipPackInstallTask::emitSucceeded);
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this,
+ &Technic::SingleZipPackInstallTask::emitFailed);
+ packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath,
+ m_minecraftVersion);
+}
+
+void Technic::SingleZipPackInstallTask::extractAborted()
+{
+ emitFailed(tr("Instance import has been aborted."));
+}
diff --git a/meshmc/launcher/modplatform/technic/SingleZipPackInstallTask.h b/meshmc/launcher/modplatform/technic/SingleZipPackInstallTask.h
new file mode 100644
index 0000000000..afad66f70b
--- /dev/null
+++ b/meshmc/launcher/modplatform/technic/SingleZipPackInstallTask.h
@@ -0,0 +1,88 @@
+/* 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 "InstanceTask.h"
+#include "net/NetJob.h"
+
+#include <QFutureWatcher>
+#include <QStringList>
+#include <QUrl>
+
+#include <nonstd/optional>
+
+namespace Technic
+{
+
+ class SingleZipPackInstallTask : public InstanceTask
+ {
+ Q_OBJECT
+
+ public:
+ SingleZipPackInstallTask(const QUrl& sourceUrl,
+ const QString& minecraftVersion);
+
+ bool canAbort() const override
+ {
+ return true;
+ }
+ bool abort() override;
+
+ protected:
+ void executeTask() override;
+
+ private slots:
+ void downloadSucceeded();
+ void downloadFailed(QString reason);
+ void downloadProgressChanged(qint64 current, qint64 total);
+ void extractFinished();
+ void extractAborted();
+
+ private:
+ bool m_abortable = false;
+
+ QUrl m_sourceUrl;
+ QString m_minecraftVersion;
+ QString m_archivePath;
+ NetJob::Ptr m_filesNetJob;
+ QFuture<nonstd::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ };
+
+} // namespace Technic
diff --git a/meshmc/launcher/modplatform/technic/SolderPackInstallTask.cpp b/meshmc/launcher/modplatform/technic/SolderPackInstallTask.cpp
new file mode 100644
index 0000000000..e5943ffdd4
--- /dev/null
+++ b/meshmc/launcher/modplatform/technic/SolderPackInstallTask.cpp
@@ -0,0 +1,236 @@
+/* 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 "SolderPackInstallTask.h"
+
+#include <FileSystem.h>
+#include <Json.h>
+#include <QtConcurrentRun>
+#include <MMCZip.h>
+#include "TechnicPackProcessor.h"
+
+Technic::SolderPackInstallTask::SolderPackInstallTask(
+ shared_qobject_ptr<QNetworkAccessManager> network, const QUrl& sourceUrl,
+ const QString& minecraftVersion)
+{
+ m_sourceUrl = sourceUrl;
+ m_minecraftVersion = minecraftVersion;
+ m_network = network;
+}
+
+bool Technic::SolderPackInstallTask::abort()
+{
+ if (m_abortable) {
+ return m_filesNetJob->abort();
+ }
+ return false;
+}
+
+void Technic::SolderPackInstallTask::executeTask()
+{
+ setStatus(
+ tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString()));
+ m_filesNetJob = new NetJob(tr("Finding recommended version"), m_network);
+ m_filesNetJob->addNetAction(
+ Net::Download::makeByteArray(m_sourceUrl, &m_response));
+ auto job = m_filesNetJob.get();
+ connect(job, &NetJob::succeeded, this,
+ &Technic::SolderPackInstallTask::versionSucceeded);
+ connect(job, &NetJob::failed, this,
+ &Technic::SolderPackInstallTask::downloadFailed);
+ m_filesNetJob->start();
+}
+
+void Technic::SolderPackInstallTask::versionSucceeded()
+{
+ try {
+ QJsonDocument doc = Json::requireDocument(m_response);
+ QJsonObject obj = Json::requireObject(doc);
+ QString version =
+ Json::requireString(obj, "recommended", "__placeholder__");
+ m_sourceUrl = m_sourceUrl.toString() + '/' + version;
+ } catch (const JSONValidationError& e) {
+ emitFailed(e.cause());
+ m_filesNetJob.reset();
+ return;
+ }
+
+ setStatus(tr("Resolving modpack files:\n%1").arg(m_sourceUrl.toString()));
+ m_filesNetJob = new NetJob(tr("Resolving modpack files"), m_network);
+ m_filesNetJob->addNetAction(
+ Net::Download::makeByteArray(m_sourceUrl, &m_response));
+ auto job = m_filesNetJob.get();
+ connect(job, &NetJob::succeeded, this,
+ &Technic::SolderPackInstallTask::fileListSucceeded);
+ connect(job, &NetJob::failed, this,
+ &Technic::SolderPackInstallTask::downloadFailed);
+ m_filesNetJob->start();
+}
+
+void Technic::SolderPackInstallTask::fileListSucceeded()
+{
+ setStatus(tr("Downloading modpack:"));
+ QStringList modUrls;
+ try {
+ QJsonDocument doc = Json::requireDocument(m_response);
+ QJsonObject obj = Json::requireObject(doc);
+ QString minecraftVersion =
+ Json::ensureString(obj, "minecraft", QString(), "__placeholder__");
+ if (!minecraftVersion.isEmpty())
+ m_minecraftVersion = minecraftVersion;
+ QJsonArray mods = Json::requireArray(obj, "mods", "'mods'");
+ for (auto mod : mods) {
+ QJsonObject modObject = Json::requireObject(mod);
+ modUrls.append(Json::requireString(modObject, "url", "'url'"));
+ }
+ } catch (const JSONValidationError& e) {
+ emitFailed(e.cause());
+ m_filesNetJob.reset();
+ return;
+ }
+ m_filesNetJob = new NetJob(tr("Downloading modpack"), m_network);
+ int i = 0;
+ for (auto& modUrl : modUrls) {
+ auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
+ m_filesNetJob->addNetAction(Net::Download::makeFile(modUrl, path));
+ i++;
+ }
+
+ m_modCount = modUrls.size();
+
+ connect(m_filesNetJob.get(), &NetJob::succeeded, this,
+ &Technic::SolderPackInstallTask::downloadSucceeded);
+ connect(m_filesNetJob.get(), &NetJob::progress, this,
+ &Technic::SolderPackInstallTask::downloadProgressChanged);
+ connect(m_filesNetJob.get(), &NetJob::failed, this,
+ &Technic::SolderPackInstallTask::downloadFailed);
+ m_filesNetJob->start();
+}
+
+void Technic::SolderPackInstallTask::downloadSucceeded()
+{
+ m_abortable = false;
+
+ setStatus(tr("Extracting modpack"));
+ m_filesNetJob.reset();
+ m_extractFuture = QtConcurrent::run([this]() {
+ int i = 0;
+ QString extractDir = FS::PathCombine(m_stagingPath, ".minecraft");
+ FS::ensureFolderPathExists(extractDir);
+
+ while (m_modCount > i) {
+ auto path =
+ FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
+ if (!MMCZip::extractDir(path, extractDir)) {
+ return false;
+ }
+ i++;
+ }
+ return true;
+ });
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished,
+ this, &Technic::SolderPackInstallTask::extractFinished);
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled,
+ this, &Technic::SolderPackInstallTask::extractAborted);
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+}
+
+void Technic::SolderPackInstallTask::downloadFailed(QString reason)
+{
+ m_abortable = false;
+ emitFailed(reason);
+ m_filesNetJob.reset();
+}
+
+void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current,
+ qint64 total)
+{
+ m_abortable = true;
+ setProgress(current / 2, total);
+}
+
+void Technic::SolderPackInstallTask::extractFinished()
+{
+ if (!m_extractFuture.result()) {
+ emitFailed(tr("Failed to extract modpack"));
+ return;
+ }
+ QDir extractDir(m_stagingPath);
+
+ qDebug() << "Fixing permissions for extracted pack files...";
+ QDirIterator it(extractDir, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ auto filepath = it.next();
+ QFileInfo file(filepath);
+ auto permissions = QFile::permissions(filepath);
+ auto origPermissions = permissions;
+ if (file.isDir()) {
+ // Folder +rwx for current user
+ permissions |= QFileDevice::Permission::ReadUser |
+ QFileDevice::Permission::WriteUser |
+ QFileDevice::Permission::ExeUser;
+ } else {
+ // File +rw for current user
+ permissions |= QFileDevice::Permission::ReadUser |
+ QFileDevice::Permission::WriteUser;
+ }
+ if (origPermissions != permissions) {
+ if (!QFile::setPermissions(filepath, permissions)) {
+ logWarning(
+ tr("Could not fix permissions for %1").arg(filepath));
+ } else {
+ qDebug() << "Fixed" << filepath;
+ }
+ }
+ }
+
+ shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor =
+ new Technic::TechnicPackProcessor();
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded,
+ this, &Technic::SolderPackInstallTask::emitSucceeded);
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this,
+ &Technic::SolderPackInstallTask::emitFailed);
+ packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath,
+ m_minecraftVersion, true);
+}
+
+void Technic::SolderPackInstallTask::extractAborted()
+{
+ emitFailed(tr("Instance import has been aborted."));
+ return;
+}
diff --git a/meshmc/launcher/modplatform/technic/SolderPackInstallTask.h b/meshmc/launcher/modplatform/technic/SolderPackInstallTask.h
new file mode 100644
index 0000000000..8a80c37580
--- /dev/null
+++ b/meshmc/launcher/modplatform/technic/SolderPackInstallTask.h
@@ -0,0 +1,90 @@
+/* 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 <InstanceTask.h>
+#include <net/NetJob.h>
+#include <tasks/Task.h>
+
+#include <QUrl>
+
+namespace Technic
+{
+ class SolderPackInstallTask : public InstanceTask
+ {
+ Q_OBJECT
+ public:
+ explicit SolderPackInstallTask(
+ shared_qobject_ptr<QNetworkAccessManager> network,
+ const QUrl& sourceUrl, const QString& minecraftVersion);
+
+ bool canAbort() const override
+ {
+ return true;
+ }
+ bool abort() override;
+
+ protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+
+ private slots:
+ void versionSucceeded();
+ void fileListSucceeded();
+ void downloadSucceeded();
+ void downloadFailed(QString reason);
+ void downloadProgressChanged(qint64 current, qint64 total);
+ void extractFinished();
+ void extractAborted();
+
+ private:
+ bool m_abortable = false;
+
+ shared_qobject_ptr<QNetworkAccessManager> m_network;
+
+ NetJob::Ptr m_filesNetJob;
+ QUrl m_sourceUrl;
+ QString m_minecraftVersion;
+ QByteArray m_response;
+ QTemporaryDir m_outputDir;
+ int m_modCount;
+ QFuture<bool> m_extractFuture;
+ QFutureWatcher<bool> m_extractFutureWatcher;
+ };
+} // namespace Technic
diff --git a/meshmc/launcher/modplatform/technic/TechnicPackProcessor.cpp b/meshmc/launcher/modplatform/technic/TechnicPackProcessor.cpp
new file mode 100644
index 0000000000..fe4e24efe9
--- /dev/null
+++ b/meshmc/launcher/modplatform/technic/TechnicPackProcessor.cpp
@@ -0,0 +1,218 @@
+/* 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 2020-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 "TechnicPackProcessor.h"
+
+#include <FileSystem.h>
+#include <Json.h>
+#include <minecraft/MinecraftInstance.h>
+#include <minecraft/PackProfile.h>
+#include <MMCZip.h>
+#include <settings/INISettingsObject.h>
+
+#include <memory>
+
+void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
+ const QString& instName,
+ const QString& instIcon,
+ const QString& stagingPath,
+ const QString& minecraftVersion,
+ const bool isSolder)
+{
+ QString minecraftPath = FS::PathCombine(stagingPath, ".minecraft");
+ QString configPath = FS::PathCombine(stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+ MinecraftInstance instance(globalSettings, instanceSettings, stagingPath);
+
+ instance.setName(instName);
+
+ if (instIcon != "default") {
+ instance.setIconKey(instIcon);
+ }
+
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+
+ QByteArray data;
+
+ QString modpackJar = FS::PathCombine(minecraftPath, "bin", "modpack.jar");
+ QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json");
+ QString fmlMinecraftVersion;
+ if (QFile::exists(modpackJar)) {
+ if (MMCZip::entryExists(modpackJar, "version.json")) {
+ if (MMCZip::entryExists(modpackJar, "fmlversion.properties")) {
+ QByteArray fmlVersionData = MMCZip::readFileFromZip(
+ modpackJar, "fmlversion.properties");
+ if (fmlVersionData.isEmpty()) {
+ emit failed(
+ tr("Unable to open \"fmlversion.properties\"!"));
+ return;
+ }
+ INIFile iniFile;
+ iniFile.loadFile(fmlVersionData);
+ // If not present, this evaluates to a null string
+ fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString();
+ }
+ data = MMCZip::readFileFromZip(modpackJar, "version.json");
+ if (data.isEmpty()) {
+ emit failed(tr("Unable to open \"version.json\"!"));
+ return;
+ }
+ } else {
+ if (minecraftVersion.isEmpty())
+ emit failed(tr(
+ "Could not find \"version.json\" inside "
+ "\"bin/modpack.jar\", but minecraft version is unknown"));
+ components->setComponentVersion("net.minecraft", minecraftVersion,
+ true);
+ components->installJarMods({modpackJar});
+
+ // Forge for 1.4.7 and for 1.5.2 require extra libraries.
+ // Figure out the forge version and add it as a component
+ // (the code still comes from the jar mod installed above)
+ if (MMCZip::entryExists(modpackJar, "forgeversion.properties")) {
+ QByteArray forgeVersionData = MMCZip::readFileFromZip(
+ modpackJar, "forgeversion.properties");
+ if (forgeVersionData.isEmpty()) {
+ // Really shouldn't happen, but error handling shall not be
+ // forgotten
+ emit failed(
+ tr("Unable to open \"forgeversion.properties\""));
+ return;
+ }
+ INIFile iniFile;
+ iniFile.loadFile(forgeVersionData);
+ QString major, minor, revision, build;
+ major = iniFile["forge.major.number"].toString();
+ minor = iniFile["forge.minor.number"].toString();
+ revision = iniFile["forge.revision.number"].toString();
+ build = iniFile["forge.build.number"].toString();
+
+ if (major.isEmpty() || minor.isEmpty() || revision.isEmpty() ||
+ build.isEmpty()) {
+ emit failed(tr("Invalid \"forgeversion.properties\"!"));
+ return;
+ }
+
+ components->setComponentVersion("net.minecraftforge",
+ major + '.' + minor + '.' +
+ revision + '.' + build);
+ }
+
+ components->saveNow();
+ emit succeeded();
+ return;
+ }
+ } else if (QFile::exists(versionJson)) {
+ QFile file(versionJson);
+ if (!file.open(QIODevice::ReadOnly)) {
+ emit failed(tr("Unable to open \"version.json\"!"));
+ return;
+ }
+ data = file.readAll();
+ file.close();
+ } else {
+ // This is the "Vanilla" modpack, excluded by the search code
+ emit failed(tr("Unable to find a \"version.json\"!"));
+ return;
+ }
+
+ try {
+ QJsonDocument doc = Json::requireDocument(data);
+ QJsonObject root = Json::requireObject(doc, "version.json");
+ QString minecraftVersion =
+ Json::ensureString(root, "inheritsFrom", QString(), "");
+ if (minecraftVersion.isEmpty()) {
+ if (fmlMinecraftVersion.isEmpty()) {
+ emit failed(tr("Could not understand "
+ "\"version.json\":\ninheritsFrom is missing"));
+ return;
+ }
+ minecraftVersion = fmlMinecraftVersion;
+ }
+ components->setComponentVersion("net.minecraft", minecraftVersion,
+ true);
+ for (auto library : Json::ensureArray(root, "libraries", {})) {
+ if (!library.isObject()) {
+ continue;
+ }
+
+ auto libraryObject = Json::ensureObject(library, {}, "");
+ auto libraryName =
+ Json::ensureString(libraryObject, "name", "", "");
+
+ if (libraryName.startsWith("net.minecraftforge:forge:") &&
+ libraryName.contains('-')) {
+ QString libraryVersion = libraryName.section(':', 2);
+ if (!libraryVersion.startsWith("1.7.10-")) {
+ components->setComponentVersion(
+ "net.minecraftforge", libraryName.section('-', 1));
+ } else {
+ // 1.7.10 versions sometimes look
+ // like 1.7.10-10.13.4.1614-1.7.10, this filters out
+ // the 10.13.4.1614 part
+ components->setComponentVersion(
+ "net.minecraftforge", libraryName.section('-', 1, 1));
+ }
+ } else if (libraryName.startsWith(
+ "net.minecraftforge:minecraftforge:")) {
+ components->setComponentVersion("net.minecraftforge",
+ libraryName.section(':', 2));
+ } else if (libraryName.startsWith("net.fabricmc:fabric-loader:")) {
+ components->setComponentVersion("net.fabricmc.fabric-loader",
+ libraryName.section(':', 2));
+ } else if (libraryName.startsWith(
+ "net.neoforged.fancymodloader:loader:") ||
+ libraryName.startsWith("net.neoforged:neoforge:")) {
+ components->setComponentVersion("net.neoforged",
+ libraryName.section(':', 2));
+ } else if (libraryName.startsWith("org.quiltmc:quilt-loader:")) {
+ components->setComponentVersion("org.quiltmc.quilt-loader",
+ libraryName.section(':', 2));
+ }
+ }
+ } catch (const JSONValidationError& e) {
+ emit failed(tr("Could not understand \"version.json\":\n") + e.cause());
+ return;
+ }
+
+ components->saveNow();
+ emit succeeded();
+}
diff --git a/meshmc/launcher/modplatform/technic/TechnicPackProcessor.h b/meshmc/launcher/modplatform/technic/TechnicPackProcessor.h
new file mode 100644
index 0000000000..8b41a5e3f8
--- /dev/null
+++ b/meshmc/launcher/modplatform/technic/TechnicPackProcessor.h
@@ -0,0 +1,62 @@
+/* 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 2020-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 <QString>
+#include "settings/SettingsObject.h"
+
+namespace Technic
+{
+ // not exporting it, only used in SingleZipPackInstallTask,
+ // InstanceImportTask and SolderPackInstallTask
+ class TechnicPackProcessor : public QObject
+ {
+ Q_OBJECT
+
+ signals:
+ void succeeded();
+ void failed(QString reason);
+
+ public:
+ void run(SettingsObjectPtr globalSettings, const QString& instName,
+ const QString& instIcon, const QString& stagingPath,
+ const QString& minecraftVersion = QString(),
+ const bool isSolder = false);
+ };
+} // namespace Technic