summaryrefslogtreecommitdiff
path: root/archived/projt-launcher/launcher/modplatform
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:51:45 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:51:45 +0300
commitd3261e64152397db2dca4d691a990c6bc2a6f4dd (patch)
treefac2f7be638651181a72453d714f0f96675c2b8b /archived/projt-launcher/launcher/modplatform
parent31b9a8949ed0a288143e23bf739f2eb64fdc63be (diff)
downloadProject-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.tar.gz
Project-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.zip
NOISSUE add archived projects
Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
Diffstat (limited to 'archived/projt-launcher/launcher/modplatform')
-rw-r--r--archived/projt-launcher/launcher/modplatform/CheckUpdateTask.h106
-rw-r--r--archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.cpp668
-rw-r--r--archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.h97
-rw-r--r--archived/projt-launcher/launcher/modplatform/ModIndex.cpp226
-rw-r--r--archived/projt-launcher/launcher/modplatform/ModIndex.h352
-rw-r--r--archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp393
-rw-r--r--archived/projt-launcher/launcher/modplatform/ResourceAPI.h189
-rw-r--r--archived/projt-launcher/launcher/modplatform/ResourceType.cpp74
-rw-r--r--archived/projt-launcher/launcher/modplatform/ResourceType.h79
-rw-r--r--archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.cpp71
-rw-r--r--archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.h73
-rw-r--r--archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp1273
-rw-r--r--archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.h188
-rw-r--r--archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.cpp427
-rw-r--r--archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.h238
-rw-r--r--archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.cpp87
-rw-r--r--archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.h75
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.cpp389
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.h81
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/FlameAPI.cpp341
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/FlameAPI.h232
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.cpp263
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.h50
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.cpp934
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.h125
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.cpp239
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.h36
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.cpp566
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.h116
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/PackManifest.cpp95
-rw-r--r--archived/projt-launcher/launcher/modplatform/flame/PackManifest.h117
-rw-r--r--archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.cpp261
-rw-r--r--archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.h68
-rw-r--r--archived/projt-launcher/launcher/modplatform/helpers/HashUtils.cpp199
-rw-r--r--archived/projt-launcher/launcher/modplatform/helpers/HashUtils.h90
-rw-r--r--archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.cpp92
-rw-r--r--archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.h41
-rw-r--r--archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.cpp180
-rw-r--r--archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.h82
-rw-r--r--archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.cpp146
-rw-r--r--archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.h76
-rw-r--r--archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.cpp258
-rw-r--r--archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.h69
-rw-r--r--archived/projt-launcher/launcher/modplatform/legacy_ftb/PackHelpers.h66
-rw-r--r--archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.cpp283
-rw-r--r--archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.h82
-rw-r--r--archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp103
-rw-r--r--archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.h64
-rw-r--r--archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.cpp207
-rw-r--r--archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.h291
-rw-r--r--archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp291
-rw-r--r--archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.h51
-rw-r--r--archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.cpp355
-rw-r--r--archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.h98
-rw-r--r--archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp668
-rw-r--r--archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h104
-rw-r--r--archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp386
-rw-r--r--archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.h103
-rw-r--r--archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.cpp284
-rw-r--r--archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.h56
-rw-r--r--archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.cpp494
-rw-r--r--archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.h145
-rw-r--r--archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.cpp185
-rw-r--r--archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.h89
-rw-r--r--archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.cpp276
-rw-r--r--archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.h116
-rw-r--r--archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.cpp85
-rw-r--r--archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.h77
-rw-r--r--archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.cpp277
-rw-r--r--archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.h62
70 files changed, 15060 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/modplatform/CheckUpdateTask.h b/archived/projt-launcher/launcher/modplatform/CheckUpdateTask.h
new file mode 100644
index 0000000000..81ef90b9a6
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/CheckUpdateTask.h
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include "minecraft/mod/tasks/GetModDependenciesTask.hpp"
+#include "modplatform/ModIndex.h"
+#include "tasks/Task.h"
+
+class ResourceDownloadTask;
+class ModFolderModel;
+
+class CheckUpdateTask : public Task
+{
+ Q_OBJECT
+
+ public:
+ CheckUpdateTask(QList<Resource*>& resources,
+ std::list<Version>& mcVersions,
+ QList<ModPlatform::ModLoaderType> loadersList,
+ std::shared_ptr<ResourceFolderModel> resourceModel)
+ : Task(),
+ m_resources(resources),
+ m_gameVersions(mcVersions),
+ m_loadersList(std::move(loadersList)),
+ m_resourceModel(std::move(resourceModel))
+ {}
+
+ struct Update
+ {
+ QString name;
+ QString old_hash;
+ QString old_version;
+ QString new_version;
+ std::optional<ModPlatform::IndexedVersionType> new_version_type;
+ QString changelog;
+ ModPlatform::ResourceProvider provider;
+ shared_qobject_ptr<ResourceDownloadTask> download;
+ bool enabled = true;
+
+ public:
+ Update(QString name,
+ QString old_h,
+ QString old_v,
+ QString new_v,
+ std::optional<ModPlatform::IndexedVersionType> new_v_type,
+ QString changelog,
+ ModPlatform::ResourceProvider p,
+ shared_qobject_ptr<ResourceDownloadTask> t,
+ bool enabled = true)
+ : name(std::move(name)),
+ old_hash(std::move(old_h)),
+ old_version(std::move(old_v)),
+ new_version(std::move(new_v)),
+ new_version_type(std::move(new_v_type)),
+ changelog(std::move(changelog)),
+ provider(p),
+ download(std::move(t)),
+ enabled(enabled)
+ {}
+ };
+
+ auto getUpdates() -> std::vector<Update>&&
+ {
+ return std::move(m_updates);
+ }
+ auto getDependencies() -> QList<std::shared_ptr<GetModDependenciesTask::PackDependency>>&&
+ {
+ return std::move(m_deps);
+ }
+
+ public slots:
+ bool abort() override = 0;
+
+ protected slots:
+ void executeTask() override = 0;
+
+ signals:
+ void checkFailed(Resource* failed, QString reason, QUrl recover_url = {});
+
+ protected:
+ QList<Resource*>& m_resources;
+ std::list<Version>& m_gameVersions;
+ QList<ModPlatform::ModLoaderType> m_loadersList;
+ std::shared_ptr<ResourceFolderModel> m_resourceModel;
+
+ std::vector<Update> m_updates;
+ QList<std::shared_ptr<GetModDependenciesTask::PackDependency>> m_deps;
+};
diff --git a/archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.cpp b/archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.cpp
new file mode 100644
index 0000000000..460e9473ed
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.cpp
@@ -0,0 +1,668 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "EnsureMetadataTask.h"
+
+#include <MurmurHash2.h>
+#include <QDebug>
+
+#include "Application.h"
+#include "Json.h"
+
+#include "QObjectPtr.h"
+#include "minecraft/mod/Mod.hpp"
+#include "minecraft/mod/tasks/LocalResourceUpdateTask.hpp"
+
+#include "modplatform/flame/FlameAPI.h"
+#include "modplatform/flame/FlameModIndex.h"
+#include "modplatform/helpers/HashUtils.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
+#include "modplatform/modrinth/ModrinthPackIndex.h"
+
+static ModrinthAPI modrinth_api;
+static FlameAPI flame_api;
+
+EnsureMetadataTask::EnsureMetadataTask(Resource* resource, QDir dir, ModPlatform::ResourceProvider prov)
+ : Task(),
+ m_indexDir(dir),
+ m_provider(prov),
+ m_hashingTask(nullptr),
+ m_currentTask(nullptr)
+{
+ auto hashTask = createNewHash(resource);
+ if (!hashTask)
+ return;
+ connect(hashTask.get(),
+ &Hashing::Hasher::resultsReady,
+ [this, resource](QString hash) { m_resources.insert(hash, resource); });
+ connect(hashTask.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
+ m_hashingTask = hashTask;
+}
+
+EnsureMetadataTask::EnsureMetadataTask(QList<Resource*>& resources, QDir dir, ModPlatform::ResourceProvider prov)
+ : Task(),
+ m_indexDir(dir),
+ m_provider(prov),
+ m_currentTask(nullptr)
+{
+ auto hashTask =
+ makeShared<ConcurrentTask>("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
+ m_hashingTask = hashTask;
+ for (auto* resource : resources)
+ {
+ auto hash_task = createNewHash(resource);
+ if (!hash_task)
+ continue;
+ connect(hash_task.get(),
+ &Hashing::Hasher::resultsReady,
+ [this, resource](QString hash) { m_resources.insert(hash, resource); });
+ connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
+ hashTask->addTask(hash_task);
+ }
+}
+
+EnsureMetadataTask::EnsureMetadataTask(QHash<QString, Resource*>& resources,
+ QDir dir,
+ ModPlatform::ResourceProvider prov)
+ : Task(),
+ m_resources(resources),
+ m_indexDir(dir),
+ m_provider(prov),
+ m_currentTask(nullptr)
+{}
+
+Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Resource* resource)
+{
+ if (!resource || !resource->valid() || resource->type() == ResourceType::FOLDER)
+ return nullptr;
+
+ return Hashing::createHasher(resource->fileinfo().absoluteFilePath(), m_provider);
+}
+
+QString EnsureMetadataTask::getExistingHash(Resource* resource)
+{
+ // Check for already computed hashes
+ // (linear on the number of mods vs. linear on the size of the mod's JAR)
+ auto it = m_resources.keyValueBegin();
+ while (it != m_resources.keyValueEnd())
+ {
+ if ((*it).second == resource)
+ break;
+ it++;
+ }
+
+ // We already have the hash computed
+ if (it != m_resources.keyValueEnd())
+ {
+ return (*it).first;
+ }
+
+ // No existing hash
+ return {};
+}
+
+bool EnsureMetadataTask::abort()
+{
+ // Prevent sending signals to a dead object
+ disconnect(this, 0, 0, 0);
+
+ if (m_currentTask)
+ return m_currentTask->abort();
+ return true;
+}
+
+void EnsureMetadataTask::executeTask()
+{
+ setStatus(tr("Checking if resources have metadata..."));
+
+ for (auto* resource : m_resources)
+ {
+ if (!resource->valid())
+ {
+ qDebug() << "Resource" << resource->name() << "is invalid!";
+ emitFail(resource);
+ continue;
+ }
+
+ // They already have the right metadata :o
+ if (resource->status() != ResourceStatus::NO_METADATA && resource->metadata()
+ && resource->metadata()->provider == m_provider)
+ {
+ qDebug() << "Resource" << resource->name() << "already has metadata!";
+ emitReady(resource);
+ continue;
+ }
+
+ // Folders don't have metadata
+ if (resource->type() == ResourceType::FOLDER)
+ {
+ emitReady(resource);
+ }
+ }
+
+ Task::Ptr version_task;
+
+ switch (m_provider)
+ {
+ case (ModPlatform::ResourceProvider::MODRINTH): version_task = modrinthVersionsTask(); break;
+ case (ModPlatform::ResourceProvider::FLAME): version_task = flameVersionsTask(); break;
+ }
+
+ auto invalidade_leftover = [this]
+ {
+ for (auto resource = m_resources.constBegin(); resource != m_resources.constEnd(); resource++)
+ emitFail(resource.value(), resource.key(), RemoveFromList::No);
+ m_resources.clear();
+
+ emitSucceeded();
+ };
+
+ connect(version_task.get(),
+ &Task::finished,
+ this,
+ [this, invalidade_leftover]
+ {
+ Task::Ptr project_task;
+
+ switch (m_provider)
+ {
+ case (ModPlatform::ResourceProvider::MODRINTH): project_task = modrinthProjectsTask(); break;
+ case (ModPlatform::ResourceProvider::FLAME): project_task = flameProjectsTask(); break;
+ }
+
+ if (!project_task)
+ {
+ invalidade_leftover();
+ return;
+ }
+
+ connect(project_task.get(),
+ &Task::finished,
+ this,
+ [this, invalidade_leftover, project_task]
+ {
+ invalidade_leftover();
+ project_task->deleteLater();
+ if (m_currentTask)
+ m_currentTask.reset();
+ });
+ connect(project_task.get(), &Task::failed, this, &EnsureMetadataTask::emitFailed);
+
+ m_currentTask = project_task;
+ project_task->start();
+ });
+
+ if (m_resources.size() > 1)
+ setStatus(tr("Requesting metadata information from %1...")
+ .arg(ModPlatform::ProviderCapabilities::readableName(m_provider)));
+ else if (!m_resources.empty())
+ setStatus(
+ tr("Requesting metadata information from %1 for '%2'...")
+ .arg(ModPlatform::ProviderCapabilities::readableName(m_provider), m_resources.begin().value()->name()));
+
+ m_currentTask = version_task;
+ version_task->start();
+}
+
+void EnsureMetadataTask::emitReady(Resource* resource, QString key, RemoveFromList remove)
+{
+ if (!resource)
+ {
+ qCritical() << "Tried to mark a null resource as ready.";
+ if (!key.isEmpty())
+ m_resources.remove(key);
+
+ return;
+ }
+
+ qDebug() << QString("Generated metadata for %1").arg(resource->name());
+ emit metadataReady(resource);
+
+ if (remove == RemoveFromList::Yes)
+ {
+ if (key.isEmpty())
+ key = getExistingHash(resource);
+ m_resources.remove(key);
+ }
+}
+
+void EnsureMetadataTask::emitFail(Resource* resource, QString key, RemoveFromList remove)
+{
+ if (!resource)
+ {
+ qCritical() << "Tried to mark a null resource as failed.";
+ if (!key.isEmpty())
+ m_resources.remove(key);
+
+ return;
+ }
+
+ qDebug() << QString("Failed to generate metadata for %1").arg(resource->name());
+ emit metadataFailed(resource);
+
+ if (remove == RemoveFromList::Yes)
+ {
+ if (key.isEmpty())
+ key = getExistingHash(resource);
+ m_resources.remove(key);
+ }
+}
+
+// Modrinth
+
+Task::Ptr EnsureMetadataTask::modrinthVersionsTask()
+{
+ auto hash_type = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first();
+
+ auto response = std::make_shared<QByteArray>();
+ auto ver_task = modrinth_api.currentVersions(m_resources.keys(), hash_type, response);
+
+ // Prevents unfortunate timings when aborting the task
+ if (!ver_task)
+ return Task::Ptr{ nullptr };
+
+ connect(ver_task.get(),
+ &Task::succeeded,
+ this,
+ [this, response]
+ {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ failed(parse_error.errorString());
+ return;
+ }
+
+ try
+ {
+ auto entries = Json::requireObject(doc);
+ for (auto& hash : m_resources.keys())
+ {
+ auto resource = m_resources.find(hash).value();
+ try
+ {
+ auto entry = Json::requireObject(entries, hash);
+
+ setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name()));
+ qDebug() << "Getting version for" << resource->name() << "from Modrinth";
+
+ m_tempVersions.insert(hash, Modrinth::loadIndexedPackVersion(entry));
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << entries;
+
+ emitFail(resource);
+ }
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ return ver_task;
+}
+
+Task::Ptr EnsureMetadataTask::modrinthProjectsTask()
+{
+ QHash<QString, QString> addonIds;
+ for (auto const& data : m_tempVersions)
+ addonIds.insert(data.addonId.toString(), data.hash);
+
+ auto response = std::make_shared<QByteArray>();
+ Task::Ptr proj_task;
+
+ if (addonIds.isEmpty())
+ {
+ qWarning() << "No addonId found!";
+ }
+ else if (addonIds.size() == 1)
+ {
+ proj_task = modrinth_api.getProject(*addonIds.keyBegin(), response);
+ }
+ else
+ {
+ proj_task = modrinth_api.getProjects(addonIds.keys(), response);
+ }
+
+ // Prevents unfortunate timings when aborting the task
+ if (!proj_task)
+ return Task::Ptr{ nullptr };
+
+ connect(proj_task.get(),
+ &Task::succeeded,
+ this,
+ [this, response, addonIds]
+ {
+ QJsonParseError parse_error{};
+ auto doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Modrinth projects task at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ QJsonArray entries;
+
+ try
+ {
+ if (addonIds.size() == 1)
+ entries = { doc.object() };
+ else
+ entries = Json::requireArray(doc);
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+
+ for (auto entry : entries)
+ {
+ ModPlatform::IndexedPack pack;
+
+ try
+ {
+ auto entry_obj = Json::requireObject(entry);
+
+ Modrinth::loadIndexedPack(pack, entry_obj);
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << doc;
+
+ // Skip this entry, since it has problems
+ continue;
+ }
+
+ auto hashIt = addonIds.find(pack.addonId.toString());
+ if (hashIt == addonIds.end())
+ {
+ qWarning() << "Invalid project id from the API response.";
+ continue;
+ }
+ const auto& hash = hashIt.value();
+
+ auto resource_iter = m_resources.find(hash);
+ if (resource_iter == m_resources.end())
+ {
+ qWarning() << "Invalid project id from the API response.";
+ continue;
+ }
+
+ auto* resource = resource_iter.value();
+ auto versionIter = m_tempVersions.find(hash);
+ if (versionIter == m_tempVersions.end())
+ {
+ qWarning() << "Missing temporary version data for Modrinth project.";
+ continue;
+ }
+
+ setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name()));
+
+ updateMetadata(pack, versionIter.value(), resource);
+ }
+ });
+
+ return proj_task;
+}
+
+// Flame
+Task::Ptr EnsureMetadataTask::flameVersionsTask()
+{
+ auto response = std::make_shared<QByteArray>();
+
+ QList<uint> fingerprints;
+ for (auto& murmur : m_resources.keys())
+ {
+ fingerprints.push_back(murmur.toUInt());
+ }
+
+ auto ver_task = flame_api.matchFingerprints(fingerprints, response);
+
+ connect(ver_task.get(),
+ &Task::succeeded,
+ this,
+ [this, response]
+ {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ failed(parse_error.errorString());
+ return;
+ }
+
+ try
+ {
+ auto doc_obj = Json::requireObject(doc);
+ auto data_obj = Json::requireObject(doc_obj, "data");
+ auto data_arr = Json::requireArray(data_obj, "exactMatches");
+
+ if (data_arr.isEmpty())
+ {
+ qWarning() << "No matches found for fingerprint search!";
+
+ return;
+ }
+
+ for (auto match : data_arr)
+ {
+ auto match_obj = Json::ensureObject(match, {});
+ auto file_obj = Json::ensureObject(match_obj, "file", {});
+
+ if (match_obj.isEmpty() || file_obj.isEmpty())
+ {
+ qWarning() << "Fingerprint match is empty!";
+
+ return;
+ }
+
+ auto fingerprint = QString::number(Json::ensureVariant(file_obj, "fileFingerprint").toUInt());
+ auto resource = m_resources.find(fingerprint);
+ if (resource == m_resources.end())
+ {
+ qWarning() << "Invalid fingerprint from the API response.";
+ continue;
+ }
+
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*resource)->name()));
+
+ m_tempVersions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj));
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ return ver_task;
+}
+
+Task::Ptr EnsureMetadataTask::flameProjectsTask()
+{
+ QHash<QString, QString> addonIds;
+ for (auto const& hash : m_resources.keys())
+ {
+ if (m_tempVersions.contains(hash))
+ {
+ auto data = m_tempVersions.find(hash).value();
+
+ auto id_str = data.addonId.toString();
+ if (!id_str.isEmpty())
+ addonIds.insert(data.addonId.toString(), hash);
+ }
+ }
+
+ auto response = std::make_shared<QByteArray>();
+ Task::Ptr proj_task;
+
+ if (addonIds.isEmpty())
+ {
+ qWarning() << "No addonId found!";
+ }
+ else if (addonIds.size() == 1)
+ {
+ proj_task = flame_api.getProject(*addonIds.keyBegin(), response);
+ }
+ else
+ {
+ proj_task = flame_api.getProjects(addonIds.keys(), response);
+ }
+
+ // Prevents unfortunate timings when aborting the task
+ if (!proj_task)
+ return Task::Ptr{ nullptr };
+
+ connect(proj_task.get(),
+ &Task::succeeded,
+ this,
+ [this, response, addonIds]
+ {
+ QJsonParseError parse_error{};
+ auto doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Modrinth projects task at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ try
+ {
+ QJsonArray entries;
+ if (addonIds.size() == 1)
+ entries = { Json::requireObject(Json::requireObject(doc), "data") };
+ else
+ entries = Json::requireArray(Json::requireObject(doc), "data");
+
+ for (auto entry : entries)
+ {
+ auto entry_obj = Json::requireObject(entry);
+
+ auto id = QString::number(Json::requireInteger(entry_obj, "id"));
+ auto hashIt = addonIds.find(id);
+ if (hashIt == addonIds.end())
+ {
+ qWarning() << "Invalid project id from the API response.";
+ continue;
+ }
+ const auto& hash = hashIt.value();
+ auto resourceIt = m_resources.find(hash);
+ if (resourceIt == m_resources.end())
+ {
+ qWarning() << "Invalid fingerprint from the API response.";
+ continue;
+ }
+ auto resource = resourceIt.value();
+ auto versionIter = m_tempVersions.find(hash);
+ if (versionIter == m_tempVersions.end())
+ {
+ qWarning() << "Missing temporary version data for CurseForge project.";
+ continue;
+ }
+
+ ModPlatform::IndexedPack pack;
+ try
+ {
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name()));
+
+ FlameMod::loadIndexedPack(pack, entry_obj);
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << entries;
+
+ emitFail(resource);
+ }
+ updateMetadata(pack, versionIter.value(), resource);
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ return proj_task;
+}
+
+void EnsureMetadataTask::updateMetadata(ModPlatform::IndexedPack& pack,
+ ModPlatform::IndexedVersion& ver,
+ Resource* resource)
+{
+ try
+ {
+ // Prevent file name mismatch
+ ver.fileName = resource->fileinfo().fileName();
+ if (ver.fileName.endsWith(".disabled"))
+ ver.fileName.chop(9);
+
+ auto task = makeShared<LocalResourceUpdateTask>(m_indexDir, pack, ver);
+
+ connect(task.get(), &Task::finished, this, [this, &pack, resource] { updateMetadataCallback(pack, resource); });
+
+ m_updateMetadataTasks[ModPlatform::ProviderCapabilities::name(pack.provider) + pack.addonId.toString()] = task;
+ task->start();
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+
+ emitFail(resource);
+ }
+}
+
+void EnsureMetadataTask::updateMetadataCallback(ModPlatform::IndexedPack& pack, Resource* resource)
+{
+ QDir tmpIndexDir(m_indexDir);
+ auto metadata = Metadata::get(tmpIndexDir, pack.slug);
+ if (!metadata.isValid())
+ {
+ qCritical() << "Failed to generate metadata at last step!";
+ emitFail(resource);
+ return;
+ }
+
+ resource->setMetadata(metadata);
+
+ emitReady(resource);
+}
diff --git a/archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.h b/archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.h
new file mode 100644
index 0000000000..7578155783
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.h
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include "ModIndex.h"
+#include "net/NetJob.h"
+
+#include "modplatform/helpers/HashUtils.h"
+
+#include "minecraft/mod/Resource.hpp"
+#include "tasks/ConcurrentTask.h"
+
+class Mod;
+class QDir;
+
+class EnsureMetadataTask : public Task
+{
+ Q_OBJECT
+
+ public:
+ EnsureMetadataTask(Resource*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
+ EnsureMetadataTask(QList<Resource*>&,
+ QDir,
+ ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
+ EnsureMetadataTask(QHash<QString, Resource*>&,
+ QDir,
+ ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
+
+ ~EnsureMetadataTask() = default;
+
+ Task::Ptr getHashingTask()
+ {
+ return m_hashingTask;
+ }
+
+ public slots:
+ bool abort() override;
+ protected slots:
+ void executeTask() override;
+
+ private:
+ // Platform-specific version/project fetching (kept together for consistency)
+ Task::Ptr modrinthVersionsTask();
+ Task::Ptr modrinthProjectsTask();
+
+ Task::Ptr flameVersionsTask();
+ Task::Ptr flameProjectsTask();
+
+ // Helpers
+ enum class RemoveFromList
+ {
+ Yes,
+ No
+ };
+ void emitReady(Resource*, QString key = {}, RemoveFromList = RemoveFromList::Yes);
+ void emitFail(Resource*, QString key = {}, RemoveFromList = RemoveFromList::Yes);
+
+ // Hashes and stuff
+ Hashing::Hasher::Ptr createNewHash(Resource*);
+ QString getExistingHash(Resource*);
+
+ private slots:
+ void updateMetadata(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource*);
+ void updateMetadataCallback(ModPlatform::IndexedPack& pack, Resource* resource);
+
+ signals:
+ void metadataReady(Resource*);
+ void metadataFailed(Resource*);
+
+ private:
+ QHash<QString, Resource*> m_resources;
+ QDir m_indexDir;
+ ModPlatform::ResourceProvider m_provider;
+
+ QHash<QString, ModPlatform::IndexedVersion> m_tempVersions;
+ Task::Ptr m_hashingTask;
+ Task::Ptr m_currentTask;
+ QHash<QString, Task::Ptr> m_updateMetadataTasks;
+};
diff --git a/archived/projt-launcher/launcher/modplatform/ModIndex.cpp b/archived/projt-launcher/launcher/modplatform/ModIndex.cpp
new file mode 100644
index 0000000000..7499b81720
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/ModIndex.cpp
@@ -0,0 +1,226 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "modplatform/ModIndex.h"
+
+#include <QCryptographicHash>
+#include <QDebug>
+#include <QIODevice>
+
+namespace ModPlatform
+{
+
+ static const QMap<QString, IndexedVersionType::VersionType> s_indexed_version_type_names = {
+ { "release", IndexedVersionType::VersionType::Release },
+ { "beta", IndexedVersionType::VersionType::Beta },
+ { "alpha", IndexedVersionType::VersionType::Alpha }
+ };
+
+ static const QList<ModLoaderType> loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt,
+ Fabric, Babric, BTA, LegacyFabric, Ornithe,
+ Rift, Risugami, StationLoader, ModLoaderMP, Optifine };
+
+ QList<ModLoaderType> modLoaderTypesToList(ModLoaderTypes flags)
+ {
+ QList<ModLoaderType> flagList;
+ for (auto flag : loaderList)
+ {
+ if (flags.testFlag(flag))
+ {
+ flagList.append(flag);
+ }
+ }
+ return flagList;
+ }
+
+ IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type))
+ {}
+
+ IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type)
+ {
+ m_type = type;
+ }
+
+ IndexedVersionType::IndexedVersionType(const IndexedVersionType& other)
+ {
+ m_type = other.m_type;
+ }
+
+ IndexedVersionType& IndexedVersionType::operator=(const IndexedVersionType& other)
+ {
+ m_type = other.m_type;
+ return *this;
+ }
+
+ const QString IndexedVersionType::toString(const IndexedVersionType::VersionType& type)
+ {
+ return s_indexed_version_type_names.key(type, "unknown");
+ }
+
+ IndexedVersionType::VersionType IndexedVersionType::enumFromString(const QString& type)
+ {
+ return s_indexed_version_type_names.value(type, IndexedVersionType::VersionType::Unknown);
+ }
+
+ const char* ProviderCapabilities::name(ResourceProvider p)
+ {
+ switch (p)
+ {
+ case ResourceProvider::MODRINTH: return "modrinth";
+ case ResourceProvider::FLAME: return "curseforge";
+ }
+ return {};
+ }
+
+ QString ProviderCapabilities::readableName(ResourceProvider p)
+ {
+ switch (p)
+ {
+ case ResourceProvider::MODRINTH: return "Modrinth";
+ case ResourceProvider::FLAME: return "CurseForge";
+ }
+ return {};
+ }
+
+ QStringList ProviderCapabilities::hashType(ResourceProvider p)
+ {
+ switch (p)
+ {
+ case ResourceProvider::MODRINTH: return { "sha512", "sha1" };
+ case ResourceProvider::FLAME:
+ // Try newer formats first, fall back to old format
+ return { "sha1", "md5", "murmur2" };
+ }
+ return {};
+ }
+
+ QString getMetaURL(ResourceProvider provider, QVariant projectID)
+ {
+ return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/"
+ : "https://modrinth.com/mod/")
+ + projectID.toString();
+ }
+
+ auto getModLoaderAsString(ModLoaderType type) -> const QString
+ {
+ switch (type)
+ {
+ case NeoForge: return "neoforge";
+ case Forge: return "forge";
+ case Cauldron: return "cauldron";
+ case LiteLoader: return "liteloader";
+ case Fabric: return "fabric";
+ case Quilt: return "quilt";
+ case DataPack: return "datapack";
+ case Babric: return "babric";
+ case BTA: return "bta-babric";
+ case LegacyFabric: return "legacy-fabric";
+ case Ornithe: return "ornithe";
+ case Rift: return "rift";
+ case Risugami: return "risugami";
+ case StationLoader: return "station-loader";
+ case ModLoaderMP: return "modloadermp";
+ case Optifine: return "optifine";
+ default: break;
+ }
+ return "";
+ }
+
+ auto getModLoaderFromString(QString type) -> ModLoaderType
+ {
+ if (type == "neoforge")
+ return NeoForge;
+ if (type == "forge")
+ return Forge;
+ if (type == "cauldron")
+ return Cauldron;
+ if (type == "liteloader")
+ return LiteLoader;
+ if (type == "fabric")
+ return Fabric;
+ if (type == "quilt")
+ return Quilt;
+ if (type == "babric")
+ return Babric;
+ if (type == "bta-babric")
+ return BTA;
+ if (type == "legacy-fabric")
+ return LegacyFabric;
+ if (type == "ornithe")
+ return Ornithe;
+ if (type == "rift")
+ return Rift;
+ if (type == "risugami")
+ return Risugami;
+ if (type == "station-loader")
+ return StationLoader;
+ if (type == "modloadermp")
+ return ModLoaderMP;
+ if (type == "optifine")
+ return Optifine;
+ return {};
+ }
+
+ QString SideUtils::toString(Side side)
+ {
+ switch (side)
+ {
+ case Side::ClientSide: return "client";
+ case Side::ServerSide: return "server";
+ case Side::UniversalSide: return "both";
+ case Side::NoSide: break;
+ }
+ return {};
+ }
+
+ Side SideUtils::fromString(QString side)
+ {
+ if (side == "client")
+ return Side::ClientSide;
+ if (side == "server")
+ return Side::ServerSide;
+ if (side == "both")
+ return Side::UniversalSide;
+ return Side::UniversalSide;
+ }
+} // namespace ModPlatform
diff --git a/archived/projt-launcher/launcher/modplatform/ModIndex.h b/archived/projt-launcher/launcher/modplatform/ModIndex.h
new file mode 100644
index 0000000000..c168fef72d
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/ModIndex.h
@@ -0,0 +1,352 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QList>
+#include <QMetaType>
+#include <QString>
+#include <QVariant>
+#include <memory>
+
+class QIODevice;
+
+namespace ModPlatform
+{
+
+ enum ModLoaderType
+ {
+ NeoForge = 1 << 0,
+ Forge = 1 << 1,
+ Cauldron = 1 << 2,
+ LiteLoader = 1 << 3,
+ Fabric = 1 << 4,
+ Quilt = 1 << 5,
+ DataPack = 1 << 6,
+ Babric = 1 << 7,
+ BTA = 1 << 8,
+ LegacyFabric = 1 << 9,
+ Ornithe = 1 << 10,
+ Rift = 1 << 11,
+ Risugami = 1 << 12,
+ StationLoader = 1 << 13,
+ ModLoaderMP = 1 << 14,
+ Optifine = 1 << 15
+ };
+ Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType)
+ QList<ModLoaderType> modLoaderTypesToList(ModLoaderTypes flags);
+
+ enum class ResourceProvider
+ {
+ MODRINTH,
+ FLAME
+ };
+
+ enum class DependencyType
+ {
+ REQUIRED,
+ OPTIONAL,
+ INCOMPATIBLE,
+ EMBEDDED,
+ TOOL,
+ INCLUDE,
+ UNKNOWN
+ };
+
+ enum class Side
+ {
+ NoSide = 0,
+ ClientSide = 1 << 0,
+ ServerSide = 1 << 1,
+ UniversalSide = ClientSide | ServerSide
+ };
+
+ namespace SideUtils
+ {
+ QString toString(Side side);
+ Side fromString(QString side);
+ } // namespace SideUtils
+
+ namespace ProviderCapabilities
+ {
+ const char* name(ResourceProvider);
+ QString readableName(ResourceProvider);
+ QStringList hashType(ResourceProvider);
+ } // namespace ProviderCapabilities
+
+ struct ModpackAuthor
+ {
+ QString name;
+ QString url;
+ };
+
+ struct DonationData
+ {
+ QString id;
+ QString platform;
+ QString url;
+ };
+
+ struct IndexedVersionType
+ {
+ enum class VersionType
+ {
+ Release = 1,
+ Beta,
+ Alpha,
+ Unknown
+ };
+ IndexedVersionType(const QString& type);
+ IndexedVersionType(const IndexedVersionType::VersionType& type);
+ IndexedVersionType(const IndexedVersionType& type);
+ IndexedVersionType() : IndexedVersionType(IndexedVersionType::VersionType::Unknown)
+ {}
+ static const QString toString(const IndexedVersionType::VersionType& type);
+ static IndexedVersionType::VersionType enumFromString(const QString& type);
+ bool isValid() const
+ {
+ return m_type != IndexedVersionType::VersionType::Unknown;
+ }
+ IndexedVersionType& operator=(const IndexedVersionType& other);
+ bool operator==(const IndexedVersionType& other) const
+ {
+ return m_type == other.m_type;
+ }
+ bool operator==(const IndexedVersionType::VersionType& type) const
+ {
+ return m_type == type;
+ }
+ bool operator!=(const IndexedVersionType& other) const
+ {
+ return m_type != other.m_type;
+ }
+ bool operator!=(const IndexedVersionType::VersionType& type) const
+ {
+ return m_type != type;
+ }
+ bool operator<(const IndexedVersionType& other) const
+ {
+ return m_type < other.m_type;
+ }
+ bool operator<(const IndexedVersionType::VersionType& type) const
+ {
+ return m_type < type;
+ }
+ bool operator<=(const IndexedVersionType& other) const
+ {
+ return m_type <= other.m_type;
+ }
+ bool operator<=(const IndexedVersionType::VersionType& type) const
+ {
+ return m_type <= type;
+ }
+ bool operator>(const IndexedVersionType& other) const
+ {
+ return m_type > other.m_type;
+ }
+ bool operator>(const IndexedVersionType::VersionType& type) const
+ {
+ return m_type > type;
+ }
+ bool operator>=(const IndexedVersionType& other) const
+ {
+ return m_type >= other.m_type;
+ }
+ bool operator>=(const IndexedVersionType::VersionType& type) const
+ {
+ return m_type >= type;
+ }
+
+ QString toString() const
+ {
+ return toString(m_type);
+ }
+
+ IndexedVersionType::VersionType m_type;
+ };
+
+ struct Dependency
+ {
+ QVariant addonId;
+ DependencyType type;
+ QString version;
+ };
+
+ struct IndexedVersion
+ {
+ QVariant addonId;
+ QVariant fileId;
+ QString version;
+ QString version_number = {};
+ IndexedVersionType version_type;
+ QStringList mcVersion;
+ QString downloadUrl;
+ QString date;
+ QString fileName;
+ ModLoaderTypes loaders = {};
+ QString hash_type;
+ QString hash;
+ bool is_preferred = true;
+ QString changelog;
+ QList<Dependency> dependencies;
+ Side side; // this is for flame API
+ QString relativePath; // Generic target path override
+
+ // For internal use, not provided by APIs
+ bool is_currently_selected = false;
+
+ QString getVersionDisplayString() const
+ {
+ auto release_type = version_type.isValid() ? QString(" [%1]").arg(version_type.toString()) : "";
+ auto versionStr = !version.contains(version_number) ? version_number : "";
+ QString gameVersion = "";
+ for (auto v : mcVersion)
+ {
+ if (version.contains(v))
+ {
+ gameVersion = "";
+ break;
+ }
+ if (gameVersion.isEmpty())
+ {
+ gameVersion = QObject::tr(" for %1").arg(v);
+ }
+ }
+ return QString("%1%2 — %3%4").arg(version, gameVersion, versionStr, release_type);
+ }
+ };
+
+ struct ExtraPackData
+ {
+ QList<DonationData> donate;
+
+ QString issuesUrl;
+ QString sourceUrl;
+ QString wikiUrl;
+ QString discordUrl;
+
+ QString status;
+
+ QString body;
+ };
+
+ struct IndexedPack
+ {
+ using Ptr = std::shared_ptr<IndexedPack>;
+
+ QVariant addonId;
+ ResourceProvider provider;
+ QString name;
+ QString slug;
+ QString description;
+ QList<ModpackAuthor> authors;
+ QString logoName;
+ QString logoUrl;
+ QString websiteUrl;
+ Side side;
+
+ bool versionsLoaded = false;
+ QList<IndexedVersion> versions;
+
+ // Don't load by default, since some modplatform don't have that info
+ bool extraDataLoaded = true;
+ ExtraPackData extraData;
+
+ // For internal use, not provided by APIs
+ bool isVersionSelected(int index) const
+ {
+ if (!versionsLoaded)
+ return false;
+
+ return versions.at(index).is_currently_selected;
+ }
+ bool isAnyVersionSelected() const
+ {
+ if (!versionsLoaded)
+ return false;
+
+ return std::any_of(versions.constBegin(),
+ versions.constEnd(),
+ [](auto const& v) { return v.is_currently_selected; });
+ }
+ };
+
+ struct OverrideDep
+ {
+ QString quilt;
+ QString fabric;
+ QString slug;
+ ModPlatform::ResourceProvider provider;
+ };
+
+ inline auto getOverrideDeps() -> QList<OverrideDep>
+ {
+ return { { "634179", "306612", "API", ModPlatform::ResourceProvider::FLAME },
+ { "720410", "308769", "KotlinLibraries", ModPlatform::ResourceProvider::FLAME },
+
+ { "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH },
+ { "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } };
+ }
+
+ QString getMetaURL(ResourceProvider provider, QVariant projectID);
+
+ auto getModLoaderAsString(ModLoaderType type) -> const QString;
+ auto getModLoaderFromString(QString type) -> ModLoaderType;
+
+ constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept
+ {
+ auto x = static_cast<int>(l);
+ return x && !(x & (x - 1));
+ }
+
+ struct Category
+ {
+ QString name;
+ QString id;
+ };
+
+} // namespace ModPlatform
+
+Q_DECLARE_METATYPE(ModPlatform::IndexedPack)
+Q_DECLARE_METATYPE(ModPlatform::IndexedPack::Ptr)
+Q_DECLARE_METATYPE(ModPlatform::ResourceProvider)
diff --git a/archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp b/archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp
new file mode 100644
index 0000000000..324f2d41d9
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp
@@ -0,0 +1,393 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "modplatform/ResourceAPI.h"
+
+#include "Application.h"
+#include "Json.h"
+#include "net/NetJob.h"
+
+#include "modplatform/ModIndex.h"
+
+#include "net/ApiDownload.h"
+
+Task::Ptr ResourceAPI::searchProjects(SearchArgs&& args,
+ Callback<QList<ModPlatform::IndexedPack::Ptr>>&& callbacks) const
+{
+ auto search_url_optional = getSearchURL(args);
+ if (!search_url_optional.has_value())
+ {
+ callbacks.on_fail("Failed to create search URL", -1);
+ return nullptr;
+ }
+
+ auto search_url = search_url_optional.value();
+
+ auto response = std::make_shared<QByteArray>();
+ auto netJob = makeShared<NetJob>(QString("%1::Search").arg(debugName()), APPLICATION->network());
+
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(search_url), response));
+
+ QObject::connect(netJob.get(),
+ &NetJob::succeeded,
+ [this, response, callbacks]
+ {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from " << debugName() << " at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ callbacks.on_fail(parse_error.errorString(), -1);
+
+ return;
+ }
+
+ QList<ModPlatform::IndexedPack::Ptr> newList;
+ auto packs = documentToArray(doc);
+
+ for (auto packRaw : packs)
+ {
+ auto packObj = packRaw.toObject();
+
+ ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>();
+ try
+ {
+ loadIndexedPack(*pack, packObj);
+ newList << pack;
+ }
+ catch (const JSONValidationError& e)
+ {
+ qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause();
+ continue;
+ }
+ }
+
+ callbacks.on_succeed(newList);
+ });
+
+ // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
+ // This prevents the lambda from extending the lifetime of the shared resource,
+ // as it only temporarily locks the resource when needed.
+ auto weak = netJob.toWeakRef();
+ QObject::connect(netJob.get(),
+ &NetJob::failed,
+ [weak, callbacks](const QString& reason)
+ {
+ int network_error_code = -1;
+ if (auto netJob = weak.lock())
+ {
+ if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
+ network_error_code = failed_action->replyStatusCode();
+ }
+ callbacks.on_fail(reason, network_error_code);
+ });
+ QObject::connect(netJob.get(),
+ &NetJob::aborted,
+ [callbacks]
+ {
+ if (callbacks.on_abort != nullptr)
+ callbacks.on_abort();
+ });
+
+ return netJob;
+}
+
+Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args,
+ Callback<QVector<ModPlatform::IndexedVersion>>&& callbacks) const
+{
+ auto versions_url_optional = getVersionsURL(args);
+ if (!versions_url_optional.has_value())
+ return nullptr;
+
+ auto versions_url = versions_url_optional.value();
+
+ auto netJob = makeShared<NetJob>(QString("%1::Versions").arg(args.pack->name), APPLICATION->network());
+ auto response = std::make_shared<QByteArray>();
+
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
+
+ QObject::connect(netJob.get(),
+ &NetJob::succeeded,
+ [this, response, callbacks, args]
+ {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response for getting versions at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ QVector<ModPlatform::IndexedVersion> unsortedVersions;
+ try
+ {
+ auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
+
+ for (auto versionIter : arr)
+ {
+ auto obj = versionIter.toObject();
+
+ auto file = loadIndexedPackVersion(obj, args.resourceType);
+ if (!file.addonId.isValid())
+ file.addonId = args.pack->addonId;
+
+ if (file.fileId.isValid() && !file.downloadUrl.isEmpty()) // Heuristic to check if the
+ // returned value is valid
+ unsortedVersions.append(file);
+ }
+
+ auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a,
+ const ModPlatform::IndexedVersion& b) -> bool
+ {
+ // dates are in RFC 3339 format
+ return a.date > b.date;
+ };
+ std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
+ }
+ catch (const JSONValidationError& e)
+ {
+ qDebug() << doc;
+ qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause();
+ }
+
+ callbacks.on_succeed(unsortedVersions);
+ });
+
+ // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
+ // This prevents the lambda from extending the lifetime of the shared resource,
+ // as it only temporarily locks the resource when needed.
+ auto weak = netJob.toWeakRef();
+ QObject::connect(netJob.get(),
+ &NetJob::failed,
+ [weak, callbacks](const QString& reason)
+ {
+ int network_error_code = -1;
+ if (auto netJob = weak.lock())
+ {
+ if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
+ network_error_code = failed_action->replyStatusCode();
+ }
+ callbacks.on_fail(reason, network_error_code);
+ });
+ QObject::connect(netJob.get(),
+ &NetJob::aborted,
+ [callbacks]
+ {
+ if (callbacks.on_abort != nullptr)
+ callbacks.on_abort();
+ });
+
+ return netJob;
+}
+
+Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack::Ptr>&& callbacks) const
+{
+ auto response = std::make_shared<QByteArray>();
+ auto job = getProject(args.pack->addonId.toString(), response);
+
+ QObject::connect(job.get(),
+ &NetJob::succeeded,
+ [this, response, callbacks, args]
+ {
+ auto pack = args.pack;
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+ try
+ {
+ auto obj = Json::requireObject(doc);
+ if (obj.contains("data"))
+ obj = Json::requireObject(obj, "data");
+ loadIndexedPack(*pack, obj);
+ loadExtraPackInfo(*pack, obj);
+ }
+ catch (const JSONValidationError& e)
+ {
+ qDebug() << doc;
+ qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
+ }
+ callbacks.on_succeed(pack);
+ });
+ // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
+ // This prevents the lambda from extending the lifetime of the shared resource,
+ // as it only temporarily locks the resource when needed.
+ auto weak = job.toWeakRef();
+ QObject::connect(job.get(),
+ &NetJob::failed,
+ [weak, callbacks](const QString& reason)
+ {
+ int network_error_code = -1;
+ if (auto job = weak.lock())
+ {
+ if (auto netJob = qSharedPointerDynamicCast<NetJob>(job))
+ {
+ if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
+ {
+ network_error_code = failed_action->replyStatusCode();
+ }
+ }
+ }
+ callbacks.on_fail(reason, network_error_code);
+ });
+ QObject::connect(job.get(),
+ &NetJob::aborted,
+ [callbacks]
+ {
+ if (callbacks.on_abort != nullptr)
+ callbacks.on_abort();
+ });
+ return job;
+}
+
+Task::Ptr ResourceAPI::getDependencyVersion(DependencySearchArgs&& args,
+ Callback<ModPlatform::IndexedVersion>&& callbacks) const
+{
+ auto versions_url_optional = getDependencyURL(args);
+ if (!versions_url_optional.has_value())
+ return nullptr;
+
+ auto versions_url = versions_url_optional.value();
+
+ auto netJob =
+ makeShared<NetJob>(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network());
+ auto response = std::make_shared<QByteArray>();
+
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
+
+ QObject::connect(netJob.get(),
+ &NetJob::succeeded,
+ [this, response, callbacks, args]
+ {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response for getting versions at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ QJsonArray arr;
+ if (args.dependency.version.length() != 0 && doc.isObject())
+ {
+ arr.append(doc.object());
+ }
+ else
+ {
+ arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
+ }
+
+ QVector<ModPlatform::IndexedVersion> versions;
+ for (auto versionIter : arr)
+ {
+ auto obj = versionIter.toObject();
+
+ auto file = loadIndexedPackVersion(obj, ModPlatform::ResourceType::Mod);
+ if (!file.addonId.isValid())
+ file.addonId = args.dependency.addonId;
+
+ if (file.fileId.isValid() && (!file.loaders || args.loader & file.loaders)) // Heuristic to
+ // check if the
+ // returned
+ // value is
+ // valid
+ versions.append(file);
+ }
+
+ auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a,
+ const ModPlatform::IndexedVersion& b) -> bool
+ {
+ // dates are in RFC 3339 format
+ return a.date > b.date;
+ };
+ std::sort(versions.begin(), versions.end(), orderSortPredicate);
+ auto bestMatch = versions.size() != 0 ? versions.front() : ModPlatform::IndexedVersion();
+ callbacks.on_succeed(bestMatch);
+ });
+
+ // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
+ // This prevents the lambda from extending the lifetime of the shared resource,
+ // as it only temporarily locks the resource when needed.
+ auto weak = netJob.toWeakRef();
+ QObject::connect(netJob.get(),
+ &NetJob::failed,
+ [weak, callbacks](const QString& reason)
+ {
+ int network_error_code = -1;
+ if (auto netJob = weak.lock())
+ {
+ if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
+ network_error_code = failed_action->replyStatusCode();
+ }
+ callbacks.on_fail(reason, network_error_code);
+ });
+ return netJob;
+}
+
+QString ResourceAPI::getGameVersionsString(std::list<Version> mcVersions) const
+{
+ QString s;
+ for (auto& ver : mcVersions)
+ {
+ s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver));
+ }
+ s.remove(s.length() - 1, 1); // remove last comma
+ return s;
+}
+
+QString ResourceAPI::mapMCVersionToModrinth(Version v) const
+{
+ static const QString preString = " Pre-Release ";
+ auto verStr = v.toString();
+
+ if (verStr.contains(preString))
+ {
+ verStr.replace(preString, "-pre");
+ }
+ verStr.replace(" ", "-");
+ return verStr;
+}
+
+Task::Ptr ResourceAPI::getProject(QString addonId, std::shared_ptr<QByteArray> response) const
+{
+ auto project_url_optional = getInfoURL(addonId);
+ if (!project_url_optional.has_value())
+ return nullptr;
+
+ auto project_url = project_url_optional.value();
+
+ auto netJob = makeShared<NetJob>(QString("%1::GetProject").arg(addonId), APPLICATION->network());
+
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(project_url), response));
+
+ return netJob;
+}
diff --git a/archived/projt-launcher/launcher/modplatform/ResourceAPI.h b/archived/projt-launcher/launcher/modplatform/ResourceAPI.h
new file mode 100644
index 0000000000..3762ba22de
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/ResourceAPI.h
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (c) 2023-2025 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <QDebug>
+#include <QList>
+#include <QString>
+
+#include <list>
+#include <optional>
+
+#include "../Version.h"
+
+#include "modplatform/ModIndex.h"
+#include "modplatform/ResourceType.h"
+#include "tasks/Task.h"
+
+/* Simple class with a common interface for interacting with APIs */
+class ResourceAPI
+{
+ public:
+ virtual ~ResourceAPI() = default;
+
+ struct SortingMethod
+ {
+ // The index of the sorting method. Used to allow for arbitrary ordering in the list of methods.
+ // Used by Flame in the API request.
+ unsigned int index;
+ // The real name of the sorting, as used in the respective API specification.
+ // Used by Modrinth in the API request.
+ QString name;
+ // The human-readable name of the sorting, used for display in the UI.
+ QString readable_name;
+ };
+
+ template <typename T>
+ struct Callback
+ {
+ std::function<void(T&)> on_succeed;
+ std::function<void(QString const& reason, int network_error_code)> on_fail;
+ std::function<void()> on_abort;
+ };
+
+ struct SearchArgs
+ {
+ ModPlatform::ResourceType type{};
+ int offset = 0;
+
+ std::optional<QString> search;
+ std::optional<SortingMethod> sorting;
+ std::optional<ModPlatform::ModLoaderTypes> loaders;
+ std::optional<std::list<Version>> versions;
+ std::optional<ModPlatform::Side> side;
+ std::optional<QStringList> categoryIds;
+ bool openSource;
+ };
+
+ struct VersionSearchArgs
+ {
+ ModPlatform::IndexedPack::Ptr pack;
+
+ std::optional<std::list<Version>> mcVersions;
+ std::optional<ModPlatform::ModLoaderTypes> loaders;
+ ModPlatform::ResourceType resourceType;
+ };
+
+ struct ProjectInfoArgs
+ {
+ ModPlatform::IndexedPack::Ptr pack;
+ };
+
+ struct DependencySearchArgs
+ {
+ ModPlatform::Dependency dependency;
+ Version mcVersion;
+ ModPlatform::ModLoaderTypes loader;
+ };
+
+ public:
+ /** Gets a list of available sorting methods for this API. */
+ virtual auto getSortingMethods() const -> QList<SortingMethod> = 0;
+
+ public slots:
+ virtual Task::Ptr searchProjects(SearchArgs&&, Callback<QList<ModPlatform::IndexedPack::Ptr>>&&) const;
+
+ virtual Task::Ptr getProject(QString addonId, std::shared_ptr<QByteArray> response) const;
+ virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const = 0;
+
+ virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback<ModPlatform::IndexedPack::Ptr>&&) const;
+ Task::Ptr getProjectVersions(VersionSearchArgs&& args,
+ Callback<QVector<ModPlatform::IndexedVersion>>&& callbacks) const;
+ virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback<ModPlatform::IndexedVersion>&&) const;
+
+ protected:
+ inline QString debugName() const
+ {
+ return "External resource API";
+ }
+
+ QString mapMCVersionToModrinth(Version v) const;
+
+ QString getGameVersionsString(std::list<Version> mcVersions) const;
+
+ public:
+ virtual auto getSearchURL(SearchArgs const& args) const -> std::optional<QString> = 0;
+ virtual auto getInfoURL(QString const& id) const -> std::optional<QString> = 0;
+ virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional<QString> = 0;
+ virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional<QString> = 0;
+
+ /** Functions to load data into a pack.
+ *
+ * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way.
+ */
+
+ virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) const = 0;
+ virtual ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const = 0;
+
+ /** Converts a JSON document to a common array format.
+ *
+ * This is needed so that different providers, with different JSON structures, can be parsed
+ * uniformally. You NEED to re-implement this if you intend on using the default callbacks.
+ */
+ virtual QJsonArray documentToArray(QJsonDocument& obj) const = 0;
+
+ /** Functions to load data into a pack.
+ *
+ * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way.
+ */
+
+ virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) const = 0;
+};
diff --git a/archived/projt-launcher/launcher/modplatform/ResourceType.cpp b/archived/projt-launcher/launcher/modplatform/ResourceType.cpp
new file mode 100644
index 0000000000..badbab541c
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/ResourceType.cpp
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ *
+ *
+ * ======================================================================== */
+
+#include "ResourceType.h"
+
+namespace ModPlatform
+{
+ static const QMap<ResourceType, QString> s_packedTypeNames = {
+ { ResourceType::ResourcePack, QObject::tr("resource pack") },
+ { ResourceType::TexturePack, QObject::tr("texture pack") },
+ { ResourceType::DataPack, QObject::tr("data pack") },
+ { ResourceType::ShaderPack, QObject::tr("shader pack") },
+ { ResourceType::World, QObject::tr("world save") },
+ { ResourceType::Mod, QObject::tr("mod") },
+ { ResourceType::Unknown, QObject::tr("unknown") }
+ };
+
+ namespace ResourceTypeUtils
+ {
+
+ QString getName(ResourceType type)
+ {
+ const auto typeIt = s_packedTypeNames.constFind(type);
+ if (typeIt != s_packedTypeNames.cend())
+ return *typeIt;
+ return s_packedTypeNames.value(ResourceType::Unknown);
+ }
+
+ } // namespace ResourceTypeUtils
+} // namespace ModPlatform
diff --git a/archived/projt-launcher/launcher/modplatform/ResourceType.h b/archived/projt-launcher/launcher/modplatform/ResourceType.h
new file mode 100644
index 0000000000..c2ec5ddaf3
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/ResourceType.h
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ *
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <set>
+
+#include <QDebug>
+#include <QFileInfo>
+#include <QObject>
+
+namespace ModPlatform
+{
+
+ enum class ResourceType
+ {
+ Mod,
+ ResourcePack,
+ ShaderPack,
+ Modpack,
+ DataPack,
+ World,
+ Screenshots,
+ TexturePack,
+ Unknown
+ };
+
+ namespace ResourceTypeUtils
+ {
+ static const std::set<ResourceType> VALID_RESOURCES = { ResourceType::DataPack, ResourceType::ResourcePack,
+ ResourceType::TexturePack, ResourceType::ShaderPack,
+ ResourceType::World, ResourceType::Mod };
+ QString getName(ResourceType type);
+ } // namespace ResourceTypeUtils
+} // namespace ModPlatform \ No newline at end of file
diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.cpp b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.cpp
new file mode 100644
index 0000000000..8e2be2cd75
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.cpp
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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", "");
+
+ static const QRegularExpression s_regex("[^A-Za-z0-9]");
+ m.safeName = Json::requireString(obj, "name").replace(s_regex, "").toLower() + ".png";
+}
diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.h b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.h
new file mode 100644
index 0000000000..947b680763
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.h
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 "ATLPackManifest.h"
+
+#include <QList>
+#include <QMetaType>
+#include <QString>
+
+namespace ATLauncher
+{
+
+ struct IndexedVersion
+ {
+ QString version;
+ QString minecraft;
+ };
+
+ struct IndexedPack
+ {
+ int id;
+ int position;
+ QString name;
+ PackType type;
+ QList<IndexedVersion> versions;
+ bool system;
+ QString description;
+
+ QString safeName;
+ };
+
+ void loadIndexedPack(IndexedPack& m, QJsonObject& obj);
+} // namespace ATLauncher
+
+Q_DECLARE_METATYPE(ATLauncher::IndexedPack)
+Q_DECLARE_METATYPE(QList<ATLauncher::IndexedVersion>) \ No newline at end of file
diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
new file mode 100644
index 0000000000..a870d387dc
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -0,0 +1,1273 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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>
+#include <algorithm>
+
+#include <quazip/quazip.h>
+
+#include "FileSystem.h"
+#include "Json.h"
+#include "MMCZip.h"
+#include "Version.h"
+#include "meta/Index.hpp"
+#include "meta/Version.hpp"
+#include "meta/VersionList.hpp"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/OneSixVersionFormat.h"
+#include "minecraft/PackProfile.h"
+#include "modplatform/atlauncher/ATLPackManifest.h"
+#include "net/ChecksumValidator.h"
+#include "settings/INISettingsObject.h"
+
+#include "net/ApiDownload.h"
+
+#include "Application.h"
+#include "BuildConfig.h"
+#include "ui/dialogs/BlockedModsDialog.h"
+
+namespace ATLauncher
+{
+
+ static projt::meta::MetaVersion::Ptr getComponentVersion(const QString& uid, const QString& version);
+
+ PackInstallTask::PackInstallTask(UserInteractionSupport* support,
+ QString packName,
+ QString version,
+ InstallMode installMode)
+ {
+ m_support = support;
+ m_pack_name = packName;
+ static const QRegularExpression s_regex("[^A-Za-z0-9]");
+ m_pack_safe_name = packName.replace(s_regex, "");
+ m_version_name = version;
+ m_install_mode = installMode;
+ }
+
+ bool PackInstallTask::abort()
+ {
+ if (abortable)
+ {
+ return jobPtr->abort();
+ }
+ return false;
+ }
+
+ void PackInstallTask::executeTask()
+ {
+ qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId();
+ NetJob::Ptr netJob{ new NetJob("ATLauncher::VersionFetch", APPLICATION->network()) };
+ auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json")
+ .arg(m_pack_safe_name)
+ .arg(m_version_name);
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response));
+
+ connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
+ connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
+ connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted);
+
+ jobPtr = netJob;
+ jobPtr->start();
+ }
+
+ 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 ATLauncher at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response.get();
+ 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;
+ }
+ version.pack = m_pack_safe_name;
+ m_version = version;
+
+ // Derived from the installation mode
+ QString message;
+ bool resetDirectory;
+
+ switch (m_install_mode)
+ {
+ case InstallMode::Reinstall:
+ case InstallMode::Update:
+ message = m_version.messages.update;
+ resetDirectory = true;
+ break;
+
+ case InstallMode::Install:
+ message = m_version.messages.install;
+ resetDirectory = false;
+ break;
+
+ default: emitFailed(tr("Unsupported installation mode")); return;
+ }
+
+ // Display message if one exists
+ if (!message.isEmpty())
+ m_support->displayMessage(message);
+
+ auto ver = getComponentVersion("net.minecraft", m_version.minecraft);
+ if (!ver)
+ {
+ emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft", m_version.minecraft));
+ return;
+ }
+ minecraftVersion = ver;
+
+ if (resetDirectory)
+ {
+ deleteExistingFiles();
+ }
+
+ if (m_version.noConfigs)
+ {
+ downloadMods();
+ }
+ else
+ {
+ installConfigs();
+ }
+ }
+
+ void PackInstallTask::onDownloadFailed(QString reason)
+ {
+ qDebug() << "PackInstallTask::onDownloadFailed: " << QThread::currentThreadId();
+ jobPtr.reset();
+ emitFailed(reason);
+ }
+
+ void PackInstallTask::onDownloadAborted()
+ {
+ jobPtr.reset();
+ emitAborted();
+ }
+
+ void PackInstallTask::deleteExistingFiles()
+ {
+ setStatus(tr("Deleting existing files..."));
+
+ // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/delete
+ VersionDeletes deletes;
+ deletes.folders.append(VersionDelete{ "root", "mods%s%" });
+ deletes.folders.append(VersionDelete{ "root", "configs%s%" });
+ deletes.folders.append(VersionDelete{ "root", "bin%s%" });
+
+ // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/keep
+ VersionKeeps keeps;
+ keeps.files.append(VersionKeep{ "root", "mods%s%PortalGunSounds.pak" });
+ keeps.folders.append(VersionKeep{ "root", "mods%s%rei_minimap%s%" });
+ keeps.folders.append(VersionKeep{ "root", "mods%s%VoxelMods%s%" });
+ keeps.files.append(VersionKeep{ "root", "config%s%NEI.cfg" });
+ keeps.files.append(VersionKeep{ "root", "options.txt" });
+ keeps.files.append(VersionKeep{ "root", "servers.dat" });
+
+ // Merge with version deletes and keeps
+ for (const auto& item : m_version.deletes.files)
+ deletes.files.append(item);
+ for (const auto& item : m_version.deletes.folders)
+ deletes.folders.append(item);
+ for (const auto& item : m_version.keeps.files)
+ keeps.files.append(item);
+ for (const auto& item : m_version.keeps.folders)
+ keeps.folders.append(item);
+
+ auto getPathForBase = [this](const QString& base)
+ {
+ auto minecraftPath = FS::PathCombine(m_stagingPath, "minecraft");
+
+ if (base == "root")
+ {
+ return minecraftPath;
+ }
+ else if (base == "config")
+ {
+ return FS::PathCombine(minecraftPath, "config");
+ }
+ else
+ {
+ qWarning() << "Unrecognised base path" << base;
+ return minecraftPath;
+ }
+ };
+
+ auto convertToSystemPath = [](const QString& path)
+ {
+ auto t = path;
+ t.replace("%s%", QDir::separator());
+ return t;
+ };
+
+ auto shouldKeep = [keeps, getPathForBase, convertToSystemPath](const QString& fullPath)
+ {
+ for (const auto& item : keeps.files)
+ {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto path = FS::PathCombine(basePath, targetPath);
+
+ if (fullPath == path)
+ {
+ return true;
+ }
+ }
+
+ for (const auto& item : keeps.folders)
+ {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto path = FS::PathCombine(basePath, targetPath);
+
+ if (fullPath.startsWith(path))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ // Keep track of files to delete
+ QSet<QString> filesToDelete;
+
+ for (const auto& item : deletes.files)
+ {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto fullPath = FS::PathCombine(basePath, targetPath);
+
+ if (shouldKeep(fullPath))
+ continue;
+
+ filesToDelete.insert(fullPath);
+ }
+
+ for (const auto& item : deletes.folders)
+ {
+ auto basePath = getPathForBase(item.base);
+ auto targetPath = convertToSystemPath(item.target);
+ auto fullPath = FS::PathCombine(basePath, targetPath);
+
+ QDirIterator it(fullPath, QDirIterator::Subdirectories);
+ while (it.hasNext())
+ {
+ auto path = it.next();
+
+ if (shouldKeep(path))
+ continue;
+
+ filesToDelete.insert(path);
+ }
+ }
+
+ // Delete the files
+ for (const auto& item : filesToDelete)
+ {
+ FS::deletePath(item);
+ }
+ }
+
+ 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()->component(uid);
+ if (!vlist)
+ {
+ emitFailed(tr("Failed to get local metadata index for %1").arg(uid));
+ return Q_NULLPTR;
+ }
+
+ vlist->waitUntilReady();
+
+ if (m_version.loader.recommended || m_version.loader.latest)
+ {
+ for (int i = 0; i < vlist->allVersions().size(); i++)
+ {
+ auto version = vlist->allVersions().at(i);
+ auto reqs = version->dependencies();
+
+ // 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 projt::meta::ComponentDependency& 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->isStable())
+ 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(const 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.multimc.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 componentIt = componentsToInstall.constFind(componentUid);
+ if (componentIt == componentsToInstall.cend() || !componentIt.value())
+ continue;
+ const auto& componentVersion = componentIt.value();
+ const auto& detailedData = componentVersion->detailedData();
+ if (detailedData)
+ {
+ for (const auto& library : detailedData->libraries)
+ {
+ GradleSpecifier lib(library->rawName());
+ exempt.append(lib);
+ }
+ }
+ }
+
+ const auto& minecraftDetailedData = minecraftVersion->detailedData();
+ if (minecraftDetailedData)
+ {
+ for (const auto& library : minecraftDetailedData->libraries)
+ {
+ GradleSpecifier lib(library->rawName());
+ exempt.append(lib);
+ }
+ }
+
+ auto id = QUuid::createUuid().toString(QUuid::WithoutBraces);
+ auto target_id = "org.multimc.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_name + " " + m_version_name + " (libraries)";
+
+ const static QMap<QString, QString> liteLoaderMap = {
+ { "61179803bcd5fb7790789b790908663d", "1.12-SNAPSHOT" },
+ { "1420785ecbfed5aff4a586c5c9dd97eb", "1.12.2-SNAPSHOT" },
+ { "073f68e2fcb518b91fd0d99462441714", "1.6.2_03" },
+ { "10a15b52fc59b1bfb9c05b56de1097d6", "1.6.2_02" },
+ { "b52f90f08303edd3d4c374e268a5acf1", "1.6.2_04" },
+ { "ea747e24e03e24b7cad5bc8a246e0319", "1.6.2_01" },
+ { "55785ccc82c07ff0ba038fe24be63ea2", "1.7.10_01" },
+ { "63ada46e033d0cb6782bada09ad5ca4e", "1.7.10_04" },
+ { "7983e4b28217c9ae8569074388409c86", "1.7.10_03" },
+ { "c09882458d74fe0697c7681b8993097e", "1.7.10_02" },
+ { "db7235aefd407ac1fde09a7baba50839", "1.7.10_00" },
+ { "6e9028816027f53957bd8fcdfabae064", "1.8" },
+ { "5e732dc446f9fe2abe5f9decaec40cde", "1.10-SNAPSHOT" },
+ { "3a98b5ed95810bf164e71c1a53be568d", "1.11.2-SNAPSHOT" },
+ { "ba8e6285966d7d988a96496f48cbddaa", "1.8.9-SNAPSHOT" },
+ { "8524af3ac3325a82444cc75ae6e9112f", "1.11-SNAPSHOT" },
+ { "53639d52340479ccf206a04f5e16606f", "1.5.2_01" },
+ { "1fcdcf66ce0a0806b7ad8686afdce3f7", "1.6.4_00" },
+ { "531c116f71ae2b11033f9a11a0f8e668", "1.6.4_01" },
+ { "4009eeb99c9068f608d3483a6439af88", "1.7.2_03" },
+ { "66f343354b8417abce1a10d557d2c6e9", "1.7.2_04" },
+ { "ab554c21f28fbc4ae9b098bcb5f4cceb", "1.7.2_05" },
+ { "e1d76a05a3723920e2f80a5e66c45f16", "1.7.2_02" },
+ { "00318cb0c787934d523f63cdfe8ddde4", "1.9-SNAPSHOT" },
+ { "986fd1ee9525cb0dcab7609401cef754", "1.9.4-SNAPSHOT" },
+ { "571ad5e6edd5ff40259570c9be588bb5", "1.9.4" },
+ { "1cdd72f7232e45551f16cc8ffd27ccf3", "1.10.2-SNAPSHOT" },
+ { "8a7c21f32d77ee08b393dd3921ced8eb", "1.10.2" },
+ { "b9bef8abc8dc309069aeba6fbbe58980", "1.12.1-SNAPSHOT" }
+ };
+
+ for (const auto& lib : m_version.libraries)
+ {
+ // If the library is LiteLoader, we need to ignore it and handle it separately.
+ if (liteLoaderMap.contains(lib.md5))
+ {
+ auto ver = getComponentVersion("com.mumfrey.liteloader", liteLoaderMap.value(lib.md5));
+ if (ver)
+ {
+ componentsToInstall.insert("com.mumfrey.liteloader", ver);
+ continue;
+ }
+ }
+
+ 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(ComponentPtr{ new Component(profile.get(), target_id, f) });
+ return true;
+ }
+
+ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile)
+ {
+ if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.arguments.isEmpty())
+ {
+ return true;
+ }
+
+ auto mainClass = m_version.mainClass.mainClass;
+ auto extraArguments = m_version.extraArguments.arguments;
+
+ auto hasMainClassDepends = !m_version.mainClass.depends.isEmpty();
+ auto hasExtraArgumentsDepends = !m_version.extraArguments.depends.isEmpty();
+ if (hasMainClassDepends || hasExtraArgumentsDepends)
+ {
+ QSet<QString> mods;
+ for (const auto& item : m_version.mods)
+ {
+ mods.insert(item.name);
+ }
+
+ if (hasMainClassDepends && !mods.contains(m_version.mainClass.depends))
+ {
+ mainClass = "";
+ }
+
+ if (hasExtraArgumentsDepends && !mods.contains(m_version.extraArguments.depends))
+ {
+ extraArguments = "";
+ }
+ }
+
+ if (mainClass.isEmpty() && extraArguments.isEmpty())
+ {
+ return true;
+ }
+
+ auto id = QUuid::createUuid().toString(QUuid::WithoutBraces);
+ auto target_id = "org.multimc.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 componentIt = componentsToInstall.constFind(componentUid);
+ if (componentIt == componentsToInstall.cend() || !componentIt.value())
+ continue;
+ const auto& componentVersion = componentIt.value();
+ const auto& detailedData = componentVersion->detailedData();
+ if (detailedData)
+ {
+ if (detailedData->mainClass != QString(""))
+ {
+ mainClasses.append(detailedData->mainClass);
+ }
+ tweakers.append(detailedData->addTweakers);
+ }
+ }
+
+ auto f = std::make_shared<VersionFile>();
+ f->name = m_pack_name + " " + m_version_name;
+ if (!mainClass.isEmpty() && !mainClasses.contains(mainClass))
+ {
+ f->mainClass = mainClass;
+ }
+
+ // Parse out tweakers
+ auto args = 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(ComponentPtr{ new Component(profile.get(), target_id, f) });
+ return true;
+ }
+
+ void PackInstallTask::installConfigs()
+ {
+ qDebug() << "PackInstallTask::installConfigs: " << QThread::currentThreadId();
+ setStatus(tr("Downloading configs..."));
+ jobPtr.reset(new NetJob(tr("Config download"), APPLICATION->network()));
+
+ auto path = QString("Configs/%1/%2.zip").arg(m_pack_safe_name).arg(m_version_name);
+ auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip")
+ .arg(m_pack_safe_name)
+ .arg(m_version_name);
+ auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", path);
+ entry->setStale(true);
+
+ auto dl = Net::ApiDownload::makeCached(url, entry);
+ if (!m_version.configs.sha1.isEmpty())
+ {
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, m_version.configs.sha1));
+ }
+ jobPtr->addNetAction(dl);
+ archivePath = entry->getFullPath();
+
+ connect(jobPtr.get(),
+ &NetJob::succeeded,
+ this,
+ [this]()
+ {
+ abortable = false;
+ jobPtr.reset();
+ extractConfigs();
+ });
+ connect(jobPtr.get(),
+ &NetJob::failed,
+ [this](QString reason)
+ {
+ abortable = false;
+ jobPtr.reset();
+ emitFailed(reason);
+ });
+ connect(jobPtr.get(),
+ &NetJob::progress,
+ [this](qint64 current, qint64 total)
+ {
+ abortable = true;
+ setProgress(current, total);
+ });
+ connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress);
+ connect(jobPtr.get(),
+ &NetJob::aborted,
+ [this]
+ {
+ abortable = false;
+ jobPtr.reset();
+ emitAborted();
+ });
+
+ jobPtr->start();
+ }
+
+ void PackInstallTask::extractConfigs()
+ {
+ qDebug() << "PackInstallTask::extractConfigs: " << QThread::currentThreadId();
+ setStatus(tr("Extracting configs..."));
+
+ QDir extractDir(m_stagingPath);
+
+ QuaZip packZip(archivePath);
+ if (!packZip.open(QuaZip::mdUnzip))
+ {
+ emitFailed(tr("Failed to open pack configs %1!").arg(archivePath));
+ return;
+ }
+
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(),
+ QOverload<QString, QString>::of(MMCZip::extractDir),
+ archivePath,
+ extractDir.absolutePath() + "/minecraft");
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, [this]() { downloadMods(); });
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, [this]() { emitAborted(); });
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+ }
+
+ void PackInstallTask::downloadMods()
+ {
+ qDebug() << "PackInstallTask::installMods: " << QThread::currentThreadId();
+
+ QList<ATLauncher::VersionMod> optionalMods;
+ for (const auto& mod : m_version.mods)
+ {
+ if (mod.optional)
+ {
+ optionalMods.push_back(mod);
+ }
+ }
+
+ // Select optional mods, if pack contains any
+ QList<QString> selectedMods;
+ if (!optionalMods.isEmpty())
+ {
+ setStatus(tr("Selecting optional mods..."));
+ auto mods = m_support->chooseOptionalMods(m_version, optionalMods);
+ if (!mods.has_value())
+ {
+ emitAborted();
+ return;
+ }
+ selectedMods = mods.value();
+ }
+
+ setStatus(tr("Downloading mods..."));
+
+ jarmods.clear();
+ jobPtr.reset(new NetJob(tr("Mod download"), APPLICATION->network()));
+
+ QList<VersionMod> blocked_mods;
+ 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:
+ {
+ blocked_mods.append(mod);
+ continue;
+ }
+ 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::ApiDownload::makeCached(url, entry);
+ if (!mod.md5.isEmpty())
+ {
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5));
+ }
+ 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::ApiDownload::makeCached(url, entry);
+ if (!mod.md5.isEmpty())
+ {
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5));
+ }
+ 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::ApiDownload::makeCached(url, entry);
+ if (!mod.md5.isEmpty())
+ {
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5));
+ }
+ jobPtr->addNetAction(dl);
+
+ auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file);
+
+ if (mod.type == ModType::Forge)
+ {
+ auto ver = getComponentVersion("net.minecraftforge", mod.version);
+ if (ver)
+ {
+ 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);
+ }
+
+ // Download after Forge handling, to avoid downloading Forge twice.
+ qDebug() << "Will download" << url << "to" << path;
+ modsToCopy[entry->getFullPath()] = path;
+ }
+ }
+ if (!blocked_mods.isEmpty())
+ {
+ QList<BlockedMod> mods;
+
+ for (auto mod : blocked_mods)
+ {
+ BlockedMod blocked_mod;
+ blocked_mod.name = mod.file;
+ blocked_mod.websiteUrl = mod.url;
+ blocked_mod.hash = mod.md5;
+ blocked_mod.matched = false;
+ blocked_mod.localPath = "";
+
+ mods.append(blocked_mod);
+ }
+
+ qWarning() << "Blocked mods found, displaying mod list";
+
+ BlockedModsDialog message_dialog(
+ nullptr,
+ tr("Blocked mods found"),
+ tr("The following files are not available for download in third party launchers.<br/>"
+ "You will need to manually download them and add them to the instance."),
+ mods,
+ "md5");
+
+ message_dialog.setModal(true);
+
+ if (message_dialog.exec())
+ {
+ qDebug() << "Post dialog blocked mods list: " << mods;
+ for (auto blocked : mods)
+ {
+ if (!blocked.matched)
+ {
+ qDebug() << blocked.name << "was not matched to a local file, skipping copy";
+ continue;
+ }
+ auto modIter =
+ std::find_if(blocked_mods.begin(),
+ blocked_mods.end(),
+ [blocked](const VersionMod& mod) { return mod.url == blocked.websiteUrl; });
+ if (modIter == blocked_mods.end())
+ continue;
+ auto mod = *modIter;
+ if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract
+ || mod.type == ModType::ResourcePackExtract)
+ {
+ modsToExtract.insert(blocked.localPath, mod);
+ }
+ else if (mod.type == ModType::Decomp)
+ {
+ modsToDecomp.insert(blocked.localPath, mod);
+ }
+ else
+ {
+ auto relpath = getDirForModType(mod.type, mod.type_raw);
+ if (relpath == Q_NULLPTR)
+ continue;
+
+ auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file);
+
+ if (mod.type == ModType::Forge)
+ {
+ auto ver = getComponentVersion("net.minecraftforge", mod.version);
+ if (ver)
+ {
+ 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);
+ }
+
+ modsToCopy[blocked.localPath] = path;
+ }
+ }
+ }
+ else
+ {
+ emitFailed(tr("Unknown download type: %1").arg("browser"));
+ return;
+ }
+ }
+
+ connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded);
+ connect(jobPtr.get(),
+ &NetJob::progress,
+ [this](qint64 current, qint64 total)
+ {
+ setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
+ abortable = true;
+ setProgress(current, total);
+ });
+ connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress);
+ connect(jobPtr.get(), &NetJob::aborted, &PackInstallTask::emitAborted);
+ connect(jobPtr.get(), &NetJob::failed, &PackInstallTask::emitFailed);
+
+ jobPtr->start();
+ }
+
+ void PackInstallTask::onModsDownloaded()
+ {
+ abortable = false;
+
+ qDebug() << "PackInstallTask::onModsDownloaded: " << QThread::currentThreadId();
+ jobPtr.reset();
+
+ if (!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty())
+ {
+ m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(),
+ &PackInstallTask::extractMods,
+ this,
+ modsToExtract,
+ modsToDecomp,
+ modsToCopy);
+ connect(&m_modExtractFutureWatcher,
+ &QFutureWatcher<QStringList>::finished,
+ this,
+ &PackInstallTask::onModsExtracted);
+ connect(&m_modExtractFutureWatcher,
+ &QFutureWatcher<QStringList>::canceled,
+ this,
+ &PackInstallTask::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;
+ static const QRegularExpression s_regex("^/");
+ folderToExtract.remove(s_regex);
+ }
+
+ 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();
+
+ // If the file already exists, assume the mod is the correct copy - and remove
+ // the copy from the Configs.zip
+ QFileInfo fileInfo(to);
+ if (fileInfo.exists())
+ {
+ if (!FS::deletePath(to))
+ {
+ qWarning() << "Failed to delete" << to;
+ return false;
+ }
+ }
+
+ 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();
+
+ 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);
+ }
+ else if (m_version.loader.type == QString("neoforge"))
+ {
+ auto version = getVersionForLoader("net.neoforged");
+ if (version == Q_NULLPTR)
+ return;
+
+ components->setComponentVersion("net.neoforged", version);
+ }
+ 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);
+ }
+ 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->versionId());
+ }
+
+ 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(name());
+ instance.setIconKey(m_instIcon);
+ instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name);
+ instanceSettings->resumeSave();
+
+ jarmods.clear();
+ emitSucceeded();
+ }
+
+ static projt::meta::MetaVersion::Ptr getComponentVersion(const QString& uid, const QString& version)
+ {
+ return APPLICATION->metadataIndex()->loadVersionBlocking(uid, version);
+ }
+
+} // namespace ATLauncher
diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.h
new file mode 100644
index 0000000000..f3ab5946e0
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.h
@@ -0,0 +1,188 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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.hpp>
+#include "ATLPackManifest.h"
+
+#include "InstanceTask.h"
+#include "meta/Version.hpp"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "net/NetJob.h"
+#include "settings/INISettingsObject.h"
+
+#include <memory>
+#include <optional>
+
+namespace ATLauncher
+{
+
+ enum class InstallMode
+ {
+ Install,
+ Reinstall,
+ Update,
+ };
+
+ class UserInteractionSupport
+ {
+ public:
+ /**
+ * Requests a user interaction to select which optional mods should be installed.
+ */
+ virtual std::optional<QList<QString>> chooseOptionalMods(const PackVersion& version,
+ QList<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(projt::meta::MetaVersionList::Ptr vlist, QString minecraftVersion) = 0;
+
+ /**
+ * Requests a user interaction to display a message.
+ */
+ virtual void displayMessage(QString message) = 0;
+
+ virtual ~UserInteractionSupport() = default;
+ };
+
+ class PackInstallTask : public InstanceTask
+ {
+ Q_OBJECT
+
+ public:
+ explicit PackInstallTask(UserInteractionSupport* support,
+ QString packName,
+ QString version,
+ InstallMode installMode = InstallMode::Install);
+ virtual ~PackInstallTask()
+ {
+ delete m_support;
+ }
+
+ bool canAbort() const override
+ {
+ return true;
+ }
+ bool abort() override;
+
+ protected:
+ virtual void executeTask() override;
+
+ private slots:
+ void onDownloadSucceeded();
+ void onDownloadFailed(QString reason);
+ void onDownloadAborted();
+
+ void onModsDownloaded();
+ void onModsExtracted();
+
+ private:
+ QString getDirForModType(ModType type, QString raw);
+ QString getVersionForLoader(QString uid);
+ QString detectLibrary(const VersionLibrary& library);
+
+ bool createLibrariesComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile);
+ bool createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile);
+
+ void deleteExistingFiles();
+ 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;
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
+
+ InstallMode m_install_mode;
+ QString m_pack_name;
+ QString m_pack_safe_name;
+ QString m_version_name;
+ PackVersion m_version;
+
+ QMap<QString, VersionMod> modsToExtract;
+ QMap<QString, VersionMod> modsToDecomp;
+ QMap<QString, QString> modsToCopy;
+
+ QString archivePath;
+ QStringList jarmods;
+ projt::meta::MetaVersion::Ptr minecraftVersion;
+ QMap<QString, projt::meta::MetaVersion::Ptr> componentsToInstall;
+
+ QFuture<std::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
+
+ QFuture<bool> m_modExtractFuture;
+ QFutureWatcher<bool> m_modExtractFutureWatcher;
+ };
+
+} // namespace ATLauncher
diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.cpp
new file mode 100644
index 0000000000..35cc7dec17
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.cpp
@@ -0,0 +1,427 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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.colour = Json::ensureString(obj, QString("colour"), "");
+ p.warning = Json::ensureString(obj, QString("warning"), "");
+
+ p.client = Json::ensureBoolean(obj, QString("client"), false);
+
+ // computed
+ p.effectively_hidden = p.hidden || p.library;
+}
+
+static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj)
+{
+ m.install = Json::ensureString(obj, "install", "");
+ m.update = Json::ensureString(obj, "update", "");
+}
+
+static void loadVersionMainClass(ATLauncher::PackVersionMainClass& m, QJsonObject& obj)
+{
+ m.mainClass = Json::ensureString(obj, "mainClass", "");
+ m.depends = Json::ensureString(obj, "depends", "");
+}
+
+static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a, QJsonObject& obj)
+{
+ a.arguments = Json::ensureString(obj, "arguments", "");
+ a.depends = Json::ensureString(obj, "depends", "");
+}
+
+static void loadVersionKeep(ATLauncher::VersionKeep& k, QJsonObject& obj)
+{
+ k.base = Json::requireString(obj, "base");
+ k.target = Json::requireString(obj, "target");
+}
+
+static void loadVersionKeeps(ATLauncher::VersionKeeps& k, QJsonObject& obj)
+{
+ if (obj.contains("files"))
+ {
+ auto files = Json::requireArray(obj, "files");
+ for (const auto keepRaw : files)
+ {
+ auto keepObj = Json::requireObject(keepRaw);
+ ATLauncher::VersionKeep keep;
+ loadVersionKeep(keep, keepObj);
+ k.files.append(keep);
+ }
+ }
+
+ if (obj.contains("folders"))
+ {
+ auto folders = Json::requireArray(obj, "folders");
+ for (const auto keepRaw : folders)
+ {
+ auto keepObj = Json::requireObject(keepRaw);
+ ATLauncher::VersionKeep keep;
+ loadVersionKeep(keep, keepObj);
+ k.folders.append(keep);
+ }
+ }
+}
+
+static void loadVersionDelete(ATLauncher::VersionDelete& d, QJsonObject& obj)
+{
+ d.base = Json::requireString(obj, "base");
+ d.target = Json::requireString(obj, "target");
+}
+
+static void loadVersionDeletes(ATLauncher::VersionDeletes& d, QJsonObject& obj)
+{
+ if (obj.contains("files"))
+ {
+ auto files = Json::requireArray(obj, "files");
+ for (const auto deleteRaw : files)
+ {
+ auto deleteObj = Json::requireObject(deleteRaw);
+ ATLauncher::VersionDelete versionDelete;
+ loadVersionDelete(versionDelete, deleteObj);
+ d.files.append(versionDelete);
+ }
+ }
+
+ if (obj.contains("folders"))
+ {
+ auto folders = Json::requireArray(obj, "folders");
+ for (const auto deleteRaw : folders)
+ {
+ auto deleteObj = Json::requireObject(deleteRaw);
+ ATLauncher::VersionDelete versionDelete;
+ loadVersionDelete(versionDelete, deleteObj);
+ d.folders.append(versionDelete);
+ }
+ }
+}
+
+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");
+ loadVersionMainClass(v.mainClass, main);
+ }
+
+ if (obj.contains("extraArguments"))
+ {
+ auto arguments = Json::requireObject(obj, "extraArguments");
+ loadVersionExtraArguments(v.extraArguments, 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);
+ }
+
+ auto colourObj = Json::ensureObject(obj, "colours");
+ for (const auto& key : colourObj.keys())
+ {
+ v.colours[key] = Json::requireString(colourObj.value(key), "colour");
+ }
+
+ auto warningsObj = Json::ensureObject(obj, "warnings");
+ for (const auto& key : warningsObj.keys())
+ {
+ v.warnings[key] = Json::requireString(warningsObj.value(key), "warning");
+ }
+
+ auto messages = Json::ensureObject(obj, "messages");
+ loadVersionMessages(v.messages, messages);
+
+ auto keeps = Json::ensureObject(obj, "keeps");
+ loadVersionKeeps(v.keeps, keeps);
+
+ auto deletes = Json::ensureObject(obj, "deletes");
+ loadVersionDeletes(v.deletes, deletes);
+}
diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.h b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.h
new file mode 100644
index 0000000000..5b09621de1
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.h
@@ -0,0 +1,238 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <QJsonObject>
+#include <QList>
+#include <QMap>
+#include <QString>
+
+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;
+ QStringList depends;
+ QString colour;
+ QString warning;
+
+ bool client;
+
+ // computed
+ bool effectively_hidden;
+ };
+
+ struct VersionConfigs
+ {
+ int filesize;
+ QString sha1;
+ };
+
+ struct VersionMessages
+ {
+ QString install;
+ QString update;
+ };
+
+ struct VersionKeep
+ {
+ QString base;
+ QString target;
+ };
+
+ struct VersionKeeps
+ {
+ QList<VersionKeep> files;
+ QList<VersionKeep> folders;
+ };
+
+ struct VersionDelete
+ {
+ QString base;
+ QString target;
+ };
+
+ struct VersionDeletes
+ {
+ QList<VersionDelete> files;
+ QList<VersionDelete> folders;
+ };
+
+ struct PackVersionMainClass
+ {
+ QString mainClass;
+ QString depends;
+ };
+
+ struct PackVersionExtraArguments
+ {
+ QString arguments;
+ QString depends;
+ };
+
+ struct PackVersion
+ {
+ QString pack;
+ QString version;
+ QString minecraft;
+ bool noConfigs;
+ PackVersionMainClass mainClass;
+ PackVersionExtraArguments extraArguments;
+
+ VersionLoader loader;
+ QList<VersionLibrary> libraries;
+ QList<VersionMod> mods;
+ VersionConfigs configs;
+
+ QMap<QString, QString> colours;
+ QMap<QString, QString> warnings;
+ VersionMessages messages;
+
+ VersionKeeps keeps;
+ VersionDeletes deletes;
+ };
+
+ void loadVersion(PackVersion& v, QJsonObject& obj);
+
+} // namespace ATLauncher
diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.cpp b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.cpp
new file mode 100644
index 0000000000..ac1c2ceb05
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.cpp
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "ATLShareCode.h"
+
+#include "Json.h"
+
+namespace ATLauncher
+{
+
+ static void loadShareCodeMod(ShareCodeMod& m, QJsonObject& obj)
+ {
+ m.selected = Json::requireBoolean(obj, "selected");
+ m.name = Json::requireString(obj, "name");
+ }
+
+ static void loadShareCode(ShareCode& c, QJsonObject& obj)
+ {
+ c.pack = Json::requireString(obj, "pack");
+ c.version = Json::requireString(obj, "version");
+
+ auto mods = Json::requireObject(obj, "mods");
+ auto optional = Json::requireArray(mods, "optional");
+ for (const auto modRaw : optional)
+ {
+ auto modObj = Json::requireObject(modRaw);
+ ShareCodeMod mod;
+ loadShareCodeMod(mod, modObj);
+ c.mods.append(mod);
+ }
+ }
+
+ void loadShareCodeResponse(ShareCodeResponse& r, QJsonObject& obj)
+ {
+ r.error = Json::requireBoolean(obj, "error");
+ r.code = Json::requireInteger(obj, "code");
+
+ if (obj.contains("message") && !obj.value("message").isNull())
+ r.message = Json::requireString(obj, "message");
+
+ if (!r.error)
+ {
+ auto dataRaw = Json::requireObject(obj, "data");
+ loadShareCode(r.data, dataRaw);
+ }
+ }
+
+} // namespace ATLauncher
diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.h b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.h
new file mode 100644
index 0000000000..c49153af67
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QJsonObject>
+#include <QList>
+#include <QString>
+
+namespace ATLauncher
+{
+
+ struct ShareCodeMod
+ {
+ bool selected;
+ QString name;
+ };
+
+ struct ShareCode
+ {
+ QString pack;
+ QString version;
+ QList<ShareCodeMod> mods;
+ };
+
+ struct ShareCodeResponse
+ {
+ bool error;
+ int code;
+ QString message;
+ ShareCode data;
+ };
+
+ void loadShareCodeResponse(ShareCodeResponse& r, QJsonObject& obj);
+
+} // namespace ATLauncher
diff --git a/archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.cpp b/archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.cpp
new file mode 100644
index 0000000000..e526dde959
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -0,0 +1,389 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "FileResolvingTask.h"
+#include <algorithm>
+
+#include "Application.h"
+#include "Json.h"
+#include "modplatform/ModIndex.h"
+#include "modplatform/flame/FlameAPI.h"
+#include "modplatform/flame/FlameModIndex.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
+
+#include "modplatform/modrinth/ModrinthPackIndex.h"
+#include "net/NetJob.h"
+#include "tasks/Task.h"
+
+static const FlameAPI flameAPI;
+static ModrinthAPI modrinthAPI;
+
+Flame::FileResolvingTask::FileResolvingTask(Flame::Manifest& toProcess) : m_manifest(toProcess)
+{}
+
+bool Flame::FileResolvingTask::abort()
+{
+ bool aborted = true;
+ if (m_task)
+ {
+ aborted = m_task->abort();
+ }
+ return aborted ? Task::abort() : false;
+}
+
+void Flame::FileResolvingTask::executeTask()
+{
+ if (m_manifest.files.isEmpty())
+ { // no file to resolve so leave it empty and emit success immediately
+ emitSucceeded();
+ return;
+ }
+ setStatus(tr("Resolving mod IDs..."));
+ setProgress(0, 3);
+ m_result.reset(new QByteArray());
+
+ QStringList fileIds;
+ for (auto file : m_manifest.files)
+ {
+ fileIds.push_back(QString::number(file.fileId));
+ }
+ m_task = flameAPI.getFiles(fileIds, m_result);
+
+ auto step_progress = std::make_shared<TaskStepProgress>();
+ connect(m_task.get(),
+ &Task::succeeded,
+ this,
+ [this, step_progress]()
+ {
+ step_progress->state = TaskStepState::Succeeded;
+ stepProgress(*step_progress);
+ netJobFinished();
+ });
+ connect(m_task.get(),
+ &Task::failed,
+ this,
+ [this, step_progress](QString reason)
+ {
+ step_progress->state = TaskStepState::Failed;
+ stepProgress(*step_progress);
+ emitFailed(reason);
+ });
+ connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress);
+ connect(m_task.get(),
+ &Task::progress,
+ this,
+ [this, step_progress](qint64 current, qint64 total)
+ {
+ qDebug() << "Resolve slug progress" << current << total;
+ step_progress->update(current, total);
+ stepProgress(*step_progress);
+ });
+ connect(m_task.get(),
+ &Task::status,
+ this,
+ [this, step_progress](QString status)
+ {
+ step_progress->status = status;
+ stepProgress(*step_progress);
+ });
+
+ m_task->start();
+}
+
+ModPlatform::ResourceType getResourceType(int classId)
+{
+ switch (classId)
+ {
+ case 17: // Worlds
+ return ModPlatform::ResourceType::World;
+ case 6: // Mods
+ return ModPlatform::ResourceType::Mod;
+ case 12: // Resource Packs
+ // return ModPlatform::ResourceType::ResourcePack; // not really a resourcepack
+ /* fallthrough */
+ case 4546: // Customization
+ // return ModPlatform::ResourceType::ShaderPack; // not really a shaderPack
+ /* fallthrough */
+ case 4471: // Modpacks
+ /* fallthrough */
+ case 5: // Bukkit Plugins
+ /* fallthrough */
+ case 4559: // Addons
+ /* fallthrough */
+ default: return ModPlatform::ResourceType::Unknown;
+ }
+}
+
+void Flame::FileResolvingTask::netJobFinished()
+{
+ setProgress(1, 3);
+ // job to check modrinth for blocked projects
+ QJsonDocument doc;
+ QJsonArray array;
+
+ try
+ {
+ doc = Json::requireDocument(*m_result);
+ array = Json::requireArray(doc.object()["data"]);
+ }
+ catch (Json::JsonException& e)
+ {
+ qCritical() << "Non-JSON data returned from the CF API";
+ qCritical() << e.cause();
+
+ emitFailed(tr("Invalid data returned from the API."));
+
+ return;
+ }
+
+ QStringList hashes;
+ for (QJsonValueRef file : array)
+ {
+ try
+ {
+ auto obj = Json::requireObject(file);
+ auto version = FlameMod::loadIndexedPackVersion(obj);
+ auto fileid = version.fileId.toInt();
+ Q_ASSERT(fileid != 0);
+ Q_ASSERT(m_manifest.files.contains(fileid));
+ m_manifest.files[fileid].version = version;
+ auto url = QUrl(version.downloadUrl, QUrl::TolerantMode);
+ if (!url.isValid() && "sha1" == version.hash_type && !version.hash.isEmpty())
+ {
+ hashes.push_back(version.hash);
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ qCritical() << "Non-JSON data returned from the CF API";
+ qCritical() << e.cause();
+
+ emitFailed(tr("Invalid data returned from the API."));
+
+ return;
+ }
+ }
+ if (hashes.isEmpty() || !APPLICATION->settings()->get("FallbackMRBlockedMods").toBool())
+ {
+ getFlameProjects();
+ return;
+ }
+ m_result.reset(new QByteArray());
+ m_task = modrinthAPI.currentVersions(hashes, "sha1", m_result);
+ (dynamic_cast<NetJob*>(m_task.get()))->setAskRetry(false);
+ auto step_progress = std::make_shared<TaskStepProgress>();
+ connect(m_task.get(),
+ &Task::succeeded,
+ this,
+ [this, step_progress]()
+ {
+ step_progress->state = TaskStepState::Succeeded;
+ stepProgress(*step_progress);
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*m_result, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *m_result;
+
+ getFlameProjects();
+ return;
+ }
+
+ try
+ {
+ auto entries = Json::requireObject(doc);
+ for (auto& out : m_manifest.files)
+ {
+ auto url = QUrl(out.version.downloadUrl, QUrl::TolerantMode);
+ if (!url.isValid() && "sha1" == out.version.hash_type && !out.version.hash.isEmpty())
+ {
+ try
+ {
+ auto entry = Json::requireObject(entries, out.version.hash);
+
+ auto file = Modrinth::loadIndexedPackVersion(entry);
+
+ out.version.downloadUrl = file.downloadUrl;
+ qDebug() << "Found alternative on modrinth " << out.version.fileName;
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << entries;
+ }
+ }
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ getFlameProjects();
+ });
+ connect(m_task.get(),
+ &Task::failed,
+ this,
+ [this, step_progress](QString reason)
+ {
+ step_progress->state = TaskStepState::Failed;
+ stepProgress(*step_progress);
+ getFlameProjects();
+ });
+ connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress);
+ connect(m_task.get(),
+ &Task::progress,
+ this,
+ [this, step_progress](qint64 current, qint64 total)
+ {
+ qDebug() << "Resolve slug progress" << current << total;
+ step_progress->update(current, total);
+ stepProgress(*step_progress);
+ });
+ connect(m_task.get(),
+ &Task::status,
+ this,
+ [this, step_progress](QString status)
+ {
+ step_progress->status = status;
+ stepProgress(*step_progress);
+ });
+
+ m_task->start();
+}
+
+void Flame::FileResolvingTask::getFlameProjects()
+{
+ setProgress(2, 3);
+ m_result.reset(new QByteArray());
+ QStringList addonIds;
+ for (auto file : m_manifest.files)
+ {
+ addonIds.push_back(QString::number(file.projectId));
+ }
+
+ m_task = flameAPI.getProjects(addonIds, m_result);
+
+ auto step_progress = std::make_shared<TaskStepProgress>();
+ connect(m_task.get(),
+ &Task::succeeded,
+ this,
+ [this, step_progress]
+ {
+ QJsonParseError parse_error{};
+ auto doc = QJsonDocument::fromJson(*m_result, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Modrinth projects task at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *m_result;
+ return;
+ }
+
+ try
+ {
+ QJsonArray entries;
+ entries = Json::requireArray(Json::requireObject(doc), "data");
+
+ for (auto entry : entries)
+ {
+ auto entry_obj = Json::requireObject(entry);
+ auto id = Json::requireInteger(entry_obj, "id");
+ auto file = std::find_if(m_manifest.files.begin(),
+ m_manifest.files.end(),
+ [id](const Flame::File& file) { return file.projectId == id; });
+ if (file == m_manifest.files.end())
+ {
+ continue;
+ }
+
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName));
+ FlameMod::loadIndexedPack(file->pack, entry_obj);
+ file->resourceType = getResourceType(Json::requireInteger(entry_obj, "classId", "modClassId"));
+ if (file->resourceType == ModPlatform::ResourceType::World)
+ {
+ file->targetFolder = "saves";
+ }
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ step_progress->state = TaskStepState::Succeeded;
+ stepProgress(*step_progress);
+ emitSucceeded();
+ });
+
+ connect(m_task.get(),
+ &Task::failed,
+ this,
+ [this, step_progress](QString reason)
+ {
+ step_progress->state = TaskStepState::Failed;
+ stepProgress(*step_progress);
+ emitFailed(reason);
+ });
+ connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress);
+ connect(m_task.get(),
+ &Task::progress,
+ this,
+ [this, step_progress](qint64 current, qint64 total)
+ {
+ qDebug() << "Resolve slug progress" << current << total;
+ step_progress->update(current, total);
+ stepProgress(*step_progress);
+ });
+ connect(m_task.get(),
+ &Task::status,
+ this,
+ [this, step_progress](QString status)
+ {
+ step_progress->status = status;
+ stepProgress(*step_progress);
+ });
+
+ m_task->start();
+}
diff --git a/archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.h b/archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.h
new file mode 100644
index 0000000000..e752c85d73
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.h
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+#pragma once
+
+#include "PackManifest.h"
+#include "tasks/Task.h"
+
+namespace Flame
+{
+ class FileResolvingTask : public Task
+ {
+ Q_OBJECT
+ public:
+ explicit FileResolvingTask(Flame::Manifest& toProcess);
+ virtual ~FileResolvingTask() = default;
+
+ bool canAbort() const override
+ {
+ return true;
+ }
+ bool abort() override;
+
+ const Flame::Manifest& getResults() const
+ {
+ return m_manifest;
+ }
+
+ protected:
+ virtual void executeTask() override;
+
+ protected slots:
+ void netJobFinished();
+
+ private:
+ void getFlameProjects();
+
+ private: /* data */
+ Flame::Manifest m_manifest;
+ std::shared_ptr<QByteArray> m_result;
+ Task::Ptr m_task;
+ };
+} // namespace Flame
diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameAPI.cpp b/archived/projt-launcher/launcher/modplatform/flame/FlameAPI.cpp
new file mode 100644
index 0000000000..69d103d5bd
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/FlameAPI.cpp
@@ -0,0 +1,341 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ *
+ *
+ * ======================================================================== */
+
+#include "FlameAPI.h"
+#include <memory>
+#include <optional>
+#include "BuildConfig.h"
+#include "FlameModIndex.h"
+
+#include "Application.h"
+#include "Json.h"
+#include "modplatform/ModIndex.h"
+#include "net/ApiDownload.h"
+#include "net/ApiUpload.h"
+#include "net/NetJob.h"
+
+Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response)
+{
+ auto netJob = makeShared<NetJob>(QString("Flame::MatchFingerprints"), APPLICATION->network());
+
+ QJsonObject body_obj;
+ QJsonArray fingerprints_arr;
+ for (auto& fp : fingerprints)
+ {
+ fingerprints_arr.append(QString("%1").arg(fp));
+ }
+
+ body_obj["fingerprints"] = fingerprints_arr;
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(
+ Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/fingerprints"), response, body_raw));
+
+ return netJob;
+}
+
+QString FlameAPI::getModFileChangelog(int modId, int fileId)
+{
+ QEventLoop lock;
+ QString changelog;
+
+ auto netJob = makeShared<NetJob>(QString("Flame::FileChangelog"), APPLICATION->network());
+ auto response = std::make_shared<QByteArray>();
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(
+ QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2/changelog")
+ .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))),
+ response));
+
+ QObject::connect(netJob.get(),
+ &NetJob::succeeded,
+ [&netJob, response, &changelog]
+ {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Flame::FileChangelog at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ netJob->failed(parse_error.errorString());
+ return;
+ }
+
+ changelog = Json::ensureString(doc.object(), "data");
+ });
+
+ QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); });
+
+ netJob->start();
+ lock.exec();
+
+ return changelog;
+}
+
+QString FlameAPI::getModDescription(int modId)
+{
+ QEventLoop lock;
+ QString description;
+
+ auto netJob = makeShared<NetJob>(QString("Flame::ModDescription"), APPLICATION->network());
+ auto response = std::make_shared<QByteArray>();
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(
+ QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/description").arg(QString::number(modId)),
+ response));
+
+ QObject::connect(netJob.get(),
+ &NetJob::succeeded,
+ [&netJob, response, &description]
+ {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Flame::ModDescription at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ netJob->failed(parse_error.errorString());
+ return;
+ }
+
+ description = Json::ensureString(doc.object(), "data");
+ });
+
+ QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); });
+
+ netJob->start();
+ lock.exec();
+
+ return description;
+}
+
+Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const
+{
+ auto netJob = makeShared<NetJob>(QString("Flame::GetProjects"), APPLICATION->network());
+
+ QJsonObject body_obj;
+ QJsonArray addons_arr;
+ for (auto& addonId : addonIds)
+ {
+ addons_arr.append(addonId);
+ }
+
+ body_obj["modIds"] = addons_arr;
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(
+ Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods"), response, body_raw));
+
+ QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; });
+
+ return netJob;
+}
+
+Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptr<QByteArray> response) const
+{
+ auto netJob = makeShared<NetJob>(QString("Flame::GetFiles"), APPLICATION->network());
+
+ QJsonObject body_obj;
+ QJsonArray files_arr;
+ for (auto& fileId : fileIds)
+ {
+ files_arr.append(fileId);
+ }
+
+ body_obj["fileIds"] = files_arr;
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(
+ Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods/files"), response, body_raw));
+
+ QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; });
+
+ return netJob;
+}
+
+Task::Ptr FlameAPI::getFile(const QString& addonId, const QString& fileId, std::shared_ptr<QByteArray> response) const
+{
+ auto netJob = makeShared<NetJob>(QString("Flame::GetFile"), APPLICATION->network());
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(
+ QUrl(QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2").arg(addonId, fileId)),
+ response));
+
+ QObject::connect(netJob.get(),
+ &NetJob::failed,
+ [addonId, fileId] { qDebug() << "Flame API file failure" << addonId << fileId; });
+
+ return netJob;
+}
+
+QList<ResourceAPI::SortingMethod> FlameAPI::getSortingMethods() const
+{
+ // https://docs.curseforge.com/?python#tocS_ModsSearchSortField
+ return { { 1, "Featured", QObject::tr("Sort by Featured") },
+ { 2, "Popularity", QObject::tr("Sort by Popularity") },
+ { 3, "LastUpdated", QObject::tr("Sort by Last Updated") },
+ { 4, "Name", QObject::tr("Sort by Name") },
+ { 5, "Author", QObject::tr("Sort by Author") },
+ { 6, "TotalDownloads", QObject::tr("Sort by Downloads") },
+ { 7, "Category", QObject::tr("Sort by Category") },
+ { 8, "GameVersion", QObject::tr("Sort by Game Version") } };
+}
+
+Task::Ptr FlameAPI::getCategories(std::shared_ptr<QByteArray> response, ModPlatform::ResourceType type)
+{
+ auto netJob = makeShared<NetJob>(QString("Flame::GetCategories"), APPLICATION->network());
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(
+ QUrl(QString(BuildConfig.FLAME_BASE_URL + "/categories?gameId=432&classId=%1").arg(getClassId(type))),
+ response));
+ QObject::connect(netJob.get(),
+ &Task::failed,
+ [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; });
+ return netJob;
+}
+
+Task::Ptr FlameAPI::getModCategories(std::shared_ptr<QByteArray> response)
+{
+ return getCategories(response, ModPlatform::ResourceType::Mod);
+}
+
+QList<ModPlatform::Category> FlameAPI::loadModCategories(std::shared_ptr<QByteArray> response)
+{
+ QList<ModPlatform::Category> categories;
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from categories at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return categories;
+ }
+
+ try
+ {
+ auto obj = Json::requireObject(doc);
+ auto arr = Json::requireArray(obj, "data");
+
+ for (auto val : arr)
+ {
+ auto cat = Json::requireObject(val);
+ auto id = Json::requireInteger(cat, "id");
+ auto name = Json::requireString(cat, "name");
+ categories.push_back({ name, QString::number(id) });
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ qCritical() << "Failed to parse response from a version request.";
+ qCritical() << e.what();
+ qDebug() << doc;
+ }
+ return categories;
+};
+
+std::optional<ModPlatform::IndexedVersion> FlameAPI::getLatestVersion(QList<ModPlatform::IndexedVersion> versions,
+ QList<ModPlatform::ModLoaderType> instanceLoaders,
+ ModPlatform::ModLoaderTypes modLoaders,
+ bool checkLoaders)
+{
+ static const auto noLoader = ModPlatform::ModLoaderType(0);
+ if (!checkLoaders)
+ {
+ std::optional<ModPlatform::IndexedVersion> ver;
+ for (auto file_tmp : versions)
+ {
+ if (!ver.has_value() || file_tmp.date > ver->date)
+ {
+ ver = file_tmp;
+ }
+ }
+ return ver;
+ }
+ QHash<ModPlatform::ModLoaderType, ModPlatform::IndexedVersion> bestMatch;
+ auto checkVersion =
+ [&bestMatch](const ModPlatform::IndexedVersion& version, const ModPlatform::ModLoaderType& loader)
+ {
+ if (bestMatch.contains(loader))
+ {
+ auto best = bestMatch.value(loader);
+ if (version.date > best.date)
+ {
+ bestMatch[loader] = version;
+ }
+ }
+ else
+ {
+ bestMatch[loader] = version;
+ }
+ };
+ for (auto file_tmp : versions)
+ {
+ auto loaders = ModPlatform::modLoaderTypesToList(file_tmp.loaders);
+ if (loaders.isEmpty())
+ {
+ checkVersion(file_tmp, noLoader);
+ }
+ else
+ {
+ for (auto loader : loaders)
+ {
+ checkVersion(file_tmp, loader);
+ }
+ }
+ }
+ // edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on
+ // update
+ auto currentLoaders = instanceLoaders + ModPlatform::modLoaderTypesToList(modLoaders);
+ currentLoaders.append(noLoader); // add a fallback in case the versions do not define a loader
+
+ for (auto loader : currentLoaders)
+ {
+ if (bestMatch.contains(loader))
+ {
+ auto bestForLoader = bestMatch.value(loader);
+ // awkward case where the mod has only two loaders and one of them is not specified
+ if (loader != noLoader && bestMatch.contains(noLoader) && bestMatch.size() == 2)
+ {
+ auto bestForNoLoader = bestMatch.value(noLoader);
+ if (bestForNoLoader.date > bestForLoader.date)
+ {
+ return bestForNoLoader;
+ }
+ }
+ return bestForLoader;
+ }
+ }
+ return {};
+}
diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameAPI.h b/archived/projt-launcher/launcher/modplatform/flame/FlameAPI.h
new file mode 100644
index 0000000000..40197b9f51
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/FlameAPI.h
@@ -0,0 +1,232 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ *
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QList>
+#include <memory>
+#include "BuildConfig.h"
+#include "Json.h"
+#include "Version.h"
+#include "modplatform/ModIndex.h"
+#include "modplatform/ResourceAPI.h"
+#include "modplatform/flame/FlameModIndex.h"
+
+class FlameAPI : public ResourceAPI
+{
+ public:
+ QString getModFileChangelog(int modId, int fileId);
+ QString getModDescription(int modId);
+
+ std::optional<ModPlatform::IndexedVersion> getLatestVersion(QList<ModPlatform::IndexedVersion> versions,
+ QList<ModPlatform::ModLoaderType> instanceLoaders,
+ ModPlatform::ModLoaderTypes fallback,
+ bool checkLoaders);
+
+ Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
+ Task::Ptr matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response);
+ Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr<QByteArray> response) const;
+ Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr<QByteArray> response) const;
+
+ static Task::Ptr getCategories(std::shared_ptr<QByteArray> response, ModPlatform::ResourceType type);
+ static Task::Ptr getModCategories(std::shared_ptr<QByteArray> response);
+ static QList<ModPlatform::Category> loadModCategories(std::shared_ptr<QByteArray> response);
+
+ QList<ResourceAPI::SortingMethod> getSortingMethods() const override;
+
+ static inline bool validateModLoaders(ModPlatform::ModLoaderTypes loaders)
+ {
+ return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt);
+ }
+
+ private:
+ static int getClassId(ModPlatform::ResourceType type)
+ {
+ switch (type)
+ {
+ default:
+ case ModPlatform::ResourceType::Mod: return 6;
+ case ModPlatform::ResourceType::ResourcePack: return 12;
+ case ModPlatform::ResourceType::ShaderPack: return 6552;
+ case ModPlatform::ResourceType::Modpack: return 4471;
+ case ModPlatform::ResourceType::DataPack: return 6945;
+ }
+ }
+
+ static int getMappedModLoader(ModPlatform::ModLoaderType loaders)
+ {
+ // https://docs.curseforge.com/?http#tocS_ModLoaderType
+ switch (loaders)
+ {
+ case ModPlatform::Forge: return 1;
+ case ModPlatform::Cauldron: return 2;
+ case ModPlatform::LiteLoader: return 3;
+ case ModPlatform::Fabric: return 4;
+ case ModPlatform::Quilt: return 5;
+ case ModPlatform::NeoForge: return 6;
+ case ModPlatform::DataPack:
+ case ModPlatform::Babric:
+ case ModPlatform::BTA:
+ case ModPlatform::LegacyFabric:
+ case ModPlatform::Ornithe:
+ case ModPlatform::Rift: break; // not supported
+ default: break;
+ }
+ return 0;
+ }
+
+ static const QStringList getModLoaderStrings(const ModPlatform::ModLoaderTypes types)
+ {
+ QStringList l;
+ for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt })
+ {
+ if (types & loader)
+ {
+ l << QString::number(getMappedModLoader(loader));
+ }
+ }
+ return l;
+ }
+
+ static const QString getModLoaderFilters(ModPlatform::ModLoaderTypes types)
+ {
+ return "[" + getModLoaderStrings(types).join(',') + "]";
+ }
+
+ public:
+ std::optional<QString> getSearchURL(SearchArgs const& args) const override
+ {
+ QStringList get_arguments;
+ get_arguments.append(QString("classId=%1").arg(getClassId(args.type)));
+ get_arguments.append(QString("index=%1").arg(args.offset));
+ get_arguments.append("pageSize=25");
+ if (args.search.has_value())
+ get_arguments.append(QString("searchFilter=%1").arg(args.search.value()));
+ if (args.sorting.has_value())
+ get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index));
+ get_arguments.append("sortOrder=desc");
+ if (args.loaders.has_value())
+ {
+ ModPlatform::ModLoaderTypes loaders = args.loaders.value();
+ loaders &= ~ModPlatform::ModLoaderType::DataPack;
+ if (loaders != 0)
+ get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(loaders)));
+ }
+ if (args.categoryIds.has_value() && !args.categoryIds->empty())
+ get_arguments.append(QString("categoryIds=[%1]").arg(args.categoryIds->join(",")));
+
+ if (args.versions.has_value() && !args.versions.value().empty())
+ get_arguments.append(QString("gameVersion=%1").arg(args.versions.value().front().toString()));
+
+ return BuildConfig.FLAME_BASE_URL + "/mods/search?gameId=432&" + get_arguments.join('&');
+ }
+
+ std::optional<QString> getVersionsURL(VersionSearchArgs const& args) const override
+ {
+ auto addonId = args.pack->addonId.toString();
+ QString url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000").arg(addonId);
+
+ if (args.mcVersions.has_value())
+ url += QString("&gameVersion=%1").arg(args.mcVersions.value().front().toString());
+
+ if (args.loaders.has_value() && args.loaders.value() != ModPlatform::ModLoaderType::DataPack
+ && ModPlatform::hasSingleModLoaderSelected(args.loaders.value()))
+ {
+ int mappedModLoader =
+ getMappedModLoader(static_cast<ModPlatform::ModLoaderType>(static_cast<int>(args.loaders.value())));
+ url += QString("&modLoaderType=%1").arg(mappedModLoader);
+ }
+ return url;
+ }
+
+ QJsonArray documentToArray(QJsonDocument& obj) const override
+ {
+ return Json::ensureArray(obj.object(), "data");
+ }
+ void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override
+ {
+ FlameMod::loadIndexedPack(m, obj);
+ }
+ ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj,
+ ModPlatform::ResourceType resourceType) const override
+ {
+ auto arr = FlameMod::loadIndexedPackVersion(obj);
+ if (resourceType != ModPlatform::ResourceType::TexturePack)
+ {
+ return arr;
+ }
+
+ // Filter texture packs based on Minecraft version compatibility
+ // Texture packs with the old format (pre-1.6) use a different structure
+ // than resource packs (1.6+). This filtering ensures we only show
+ // compatible texture packs for older Minecraft versions.
+ auto const& mc_versions = arr.mcVersion;
+
+ if (mc_versions.isEmpty())
+ {
+ // No version info available - allow it through
+ return arr;
+ }
+
+ // Check if any of the supported versions is 1.6 or older (texture pack era)
+ bool hasOldVersion = std::any_of(mc_versions.constBegin(),
+ mc_versions.constEnd(),
+ [](auto const& mc_version) { return Version(mc_version) <= Version("1.6"); });
+
+ if (hasOldVersion)
+ {
+ return arr;
+ }
+
+ // Version 1.6+ uses resource packs, not texture packs
+ return {};
+ };
+ void loadExtraPackInfo(ModPlatform::IndexedPack& m, [[maybe_unused]] QJsonObject&) const override
+ {
+ FlameMod::loadBody(m);
+ }
+
+ private:
+ std::optional<QString> getInfoURL(QString const& id) const override
+ {
+ return QString(BuildConfig.FLAME_BASE_URL + "/mods/%1").arg(id);
+ }
+ std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
+ {
+ auto addonId = args.dependency.addonId.toString();
+ auto url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000&gameVersion=%2")
+ .arg(addonId, args.mcVersion.toString());
+ if (args.loader && ModPlatform::hasSingleModLoaderSelected(args.loader))
+ {
+ int mappedModLoader =
+ getMappedModLoader(static_cast<ModPlatform::ModLoaderType>(static_cast<int>(args.loader)));
+ url += QString("&modLoaderType=%1").arg(mappedModLoader);
+ }
+ return url;
+ }
+};
diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.cpp b/archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.cpp
new file mode 100644
index 0000000000..eaa3e6e830
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.cpp
@@ -0,0 +1,263 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "FlameCheckUpdate.h"
+#include "Application.h"
+#include "FlameAPI.h"
+#include "FlameModIndex.h"
+
+#include <QHash>
+#include <memory>
+
+#include "Json.h"
+
+#include "QObjectPtr.h"
+#include "ResourceDownloadTask.h"
+
+#include "minecraft/mod/tasks/GetModDependenciesTask.hpp"
+
+#include "modplatform/ModIndex.h"
+#include "net/ApiDownload.h"
+#include "net/NetJob.h"
+#include "tasks/Task.h"
+
+static FlameAPI api;
+
+bool FlameCheckUpdate::abort()
+{
+ bool result = false;
+ if (m_task && m_task->canAbort())
+ {
+ result = m_task->abort();
+ }
+ Task::abort();
+ return result;
+}
+
+/* Check for update:
+ * - Get latest version available
+ * - Compare hash of the latest version with the current hash
+ * - If equal, no updates, else, there's updates, so add to the list
+ * */
+void FlameCheckUpdate::executeTask()
+{
+ setStatus(tr("Preparing resources for CurseForge..."));
+
+ auto netJob = new NetJob("Get latest versions", APPLICATION->network());
+ connect(netJob, &Task::finished, this, &FlameCheckUpdate::collectBlockedMods);
+
+ connect(netJob, &Task::progress, this, &FlameCheckUpdate::setProgress);
+ connect(netJob, &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress);
+ connect(netJob, &Task::details, this, &FlameCheckUpdate::setDetails);
+ for (auto* resource : m_resources)
+ {
+ auto project = std::make_shared<ModPlatform::IndexedPack>();
+ project->addonId = resource->metadata()->project_id.toString();
+ auto versionsUrlOptional = api.getVersionsURL({ project, m_gameVersions });
+ if (!versionsUrlOptional.has_value())
+ continue;
+
+ auto response = std::make_shared<QByteArray>();
+ auto task = Net::ApiDownload::makeByteArray(versionsUrlOptional.value(), response);
+
+ connect(task.get(),
+ &Task::succeeded,
+ this,
+ [this, resource, response] { getLatestVersionCallback(resource, response); });
+ netJob->addNetAction(task);
+ }
+ m_task.reset(netJob);
+ m_task->start();
+}
+
+void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, std::shared_ptr<QByteArray> response)
+{
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ // Fake pack with the necessary info to pass to the download task :)
+ auto pack = std::make_shared<ModPlatform::IndexedPack>();
+ pack->name = resource->name();
+ pack->slug = resource->metadata()->slug;
+ pack->addonId = resource->metadata()->project_id;
+ pack->provider = ModPlatform::ResourceProvider::FLAME;
+ try
+ {
+ auto obj = Json::requireObject(doc);
+ auto arr = Json::requireArray(obj, "data");
+
+ FlameMod::loadIndexedPackVersions(*pack.get(), arr);
+ }
+ catch (Json::JsonException& e)
+ {
+ qCritical() << "Failed to parse response from a version request.";
+ qCritical() << e.what();
+ qDebug() << doc;
+ }
+ auto latest_ver =
+ api.getLatestVersion(pack->versions, m_loadersList, resource->metadata()->loaders, !m_loadersList.isEmpty());
+
+ setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name()));
+
+ if (!latest_ver.has_value() || !latest_ver->addonId.isValid())
+ {
+ QString reason;
+ if (dynamic_cast<Mod*>(resource) != nullptr)
+ reason = tr("No valid version found for this resource. It's probably unavailable for the current game "
+ "version / mod loader.");
+ else
+ reason =
+ tr("No valid version found for this resource. It's probably unavailable for the current game version.");
+
+ emit checkFailed(resource, reason);
+ return;
+ }
+
+ if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != resource->metadata()->file_id)
+ {
+ m_blocked[resource] = latest_ver->fileId.toString();
+ return;
+ }
+
+ if (!latest_ver->hash.isEmpty()
+ && (resource->metadata()->hash != latest_ver->hash || resource->status() == ResourceStatus::NOT_INSTALLED))
+ {
+ auto old_version = resource->metadata()->version_number;
+ if (old_version.isEmpty())
+ {
+ if (resource->status() == ResourceStatus::NOT_INSTALLED)
+ old_version = tr("Not installed");
+ else
+ old_version = tr("Unknown");
+ }
+
+ auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver.value(), m_resourceModel);
+ m_updates.emplace_back(pack->name,
+ resource->metadata()->hash,
+ old_version,
+ latest_ver->version,
+ latest_ver->version_type,
+ api.getModFileChangelog(latest_ver->addonId.toInt(), latest_ver->fileId.toInt()),
+ ModPlatform::ResourceProvider::FLAME,
+ download_task,
+ resource->enabled());
+ }
+ m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, latest_ver.value()));
+}
+
+void FlameCheckUpdate::collectBlockedMods()
+{
+ QStringList addonIds;
+ QHash<QString, Resource*> quickSearch;
+ for (auto const& resource : m_blocked.keys())
+ {
+ auto addonId = resource->metadata()->project_id.toString();
+ addonIds.append(addonId);
+ quickSearch[addonId] = resource;
+ }
+
+ auto response = std::make_shared<QByteArray>();
+ Task::Ptr projTask;
+
+ if (addonIds.isEmpty())
+ {
+ emitSucceeded();
+ return;
+ }
+ else if (addonIds.size() == 1)
+ {
+ projTask = api.getProject(*addonIds.begin(), response);
+ }
+ else
+ {
+ projTask = api.getProjects(addonIds, response);
+ }
+
+ connect(projTask.get(),
+ &Task::succeeded,
+ this,
+ [this, response, addonIds, quickSearch]
+ {
+ QJsonParseError parse_error{};
+ auto doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Flame projects task at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ try
+ {
+ QJsonArray entries;
+ if (addonIds.size() == 1)
+ entries = { Json::requireObject(Json::requireObject(doc), "data") };
+ else
+ entries = Json::requireArray(Json::requireObject(doc), "data");
+
+ for (auto entry : entries)
+ {
+ auto entry_obj = Json::requireObject(entry);
+
+ auto id = QString::number(Json::requireInteger(entry_obj, "id"));
+
+ auto resource = quickSearch.find(id).value();
+
+ ModPlatform::IndexedPack pack;
+ try
+ {
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name()));
+
+ FlameMod::loadIndexedPack(pack, entry_obj);
+ auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, m_blocked[resource]);
+ emit checkFailed(
+ resource,
+ tr("Resource has a new update available, but is not downloadable using CurseForge."),
+ recover_url);
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << entries;
+ }
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ connect(projTask.get(), &Task::finished, this, &FlameCheckUpdate::emitSucceeded); // do not care much about error
+ connect(projTask.get(), &Task::progress, this, &FlameCheckUpdate::setProgress);
+ connect(projTask.get(), &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress);
+ connect(projTask.get(), &Task::details, this, &FlameCheckUpdate::setDetails);
+ m_task.reset(projTask);
+ m_task->start();
+} \ No newline at end of file
diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.h b/archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.h
new file mode 100644
index 0000000000..69f963328b
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.h
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include "modplatform/CheckUpdateTask.h"
+
+class FlameCheckUpdate : public CheckUpdateTask
+{
+ Q_OBJECT
+
+ public:
+ FlameCheckUpdate(QList<Resource*>& resources,
+ std::list<Version>& mcVersions,
+ QList<ModPlatform::ModLoaderType> loadersList,
+ std::shared_ptr<ResourceFolderModel> resourceModel)
+ : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(resourceModel))
+ {}
+
+ public slots:
+ bool abort() override;
+
+ protected slots:
+ void executeTask() override;
+ private slots:
+ void getLatestVersionCallback(Resource* resource, std::shared_ptr<QByteArray> response);
+ void collectBlockedMods();
+
+ private:
+ Task::Ptr m_task = nullptr;
+
+ QHash<Resource*, QString> m_blocked;
+};
diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
new file mode 100644
index 0000000000..8786509646
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
@@ -0,0 +1,934 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "FlameInstanceCreationTask.h"
+
+#include "QObjectPtr.h"
+#include "minecraft/mod/tasks/LocalResourceUpdateTask.hpp"
+#include "modplatform/flame/FileResolvingTask.h"
+#include "modplatform/flame/FlameAPI.h"
+#include "modplatform/flame/FlameModIndex.h"
+#include "modplatform/flame/PackManifest.h"
+
+#include "Application.h"
+#include "FileSystem.h"
+#include "InstanceList.h"
+#include "Json.h"
+
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+
+#include "modplatform/helpers/OverrideUtils.h"
+
+#include "settings/INISettingsObject.h"
+
+#include "tasks/ConcurrentTask.h"
+#include "ui/dialogs/BlockedModsDialog.h"
+#include "ui/dialogs/CustomMessageBox.h"
+
+#include <QDebug>
+#include <QFileInfo>
+
+#include "meta/Index.hpp"
+#include "HardwareInfo.h"
+#include "minecraft/World.h"
+#include "minecraft/mod/tasks/LocalResourceParse.hpp"
+#include "net/ApiDownload.h"
+#include "ui/pages/modplatform/OptionalModDialog.h"
+
+static const FlameAPI api;
+
+bool FlameCreationTask::abort()
+{
+ if (!canAbort())
+ return false;
+
+ if (m_processUpdateFileInfoJob)
+ m_processUpdateFileInfoJob->abort();
+ if (m_filesJob)
+ m_filesJob->abort();
+ if (m_modIdResolver)
+ m_modIdResolver->abort();
+
+ return InstanceCreationTask::abort();
+}
+
+bool FlameCreationTask::updateInstance()
+{
+ auto instance_list = APPLICATION->instances();
+
+ // Note: Duplicate modpack detection uses managed name or instance ID lookup.
+ // If multiple installations exist, the first match is updated.
+ InstancePtr inst;
+ if (auto original_id = originalInstanceID(); !original_id.isEmpty())
+ {
+ inst = instance_list->getInstanceById(original_id);
+ Q_ASSERT(inst);
+ }
+ else
+ {
+ // Duplicate Detection: Check for duplicates before assuming
+ auto all_instances = instance_list->getAllInstancesByManagedName(originalName());
+
+ if (all_instances.size() > 1)
+ {
+ emitFailed(tr("Multiple instances found for this modpack. Please update the specific instance you want to "
+ "modify to avoid ambiguity."));
+ return false;
+ }
+
+ if (all_instances.size() == 1)
+ {
+ inst = all_instances.first();
+ }
+ else
+ {
+ // Fallback to name-based lookup if not found by managed ID
+ inst = instance_list->getInstanceById(originalName());
+ }
+
+ if (!inst)
+ {
+ // New instance creation flow - return false to call createInstance()
+ return false;
+ }
+ }
+
+ QString index_path(FS::PathCombine(m_stagingPath, "manifest.json"));
+
+ try
+ {
+ Flame::loadManifest(m_pack, index_path);
+ }
+ catch (const JSONValidationError& e)
+ {
+ setError(tr("Could not understand pack manifest:\n") + e.cause());
+ return false;
+ }
+
+ auto version_id = inst->getManagedPackVersionName();
+ auto version_str = !version_id.isEmpty() ? tr(" (version %1)").arg(version_id) : "";
+
+ if (shouldConfirmUpdate())
+ {
+ auto should_update = askIfShouldUpdate(m_parent, version_str);
+ if (should_update == ShouldUpdate::SkipUpdating)
+ return false;
+ if (should_update == ShouldUpdate::Cancel)
+ {
+ m_abort = true;
+ return false;
+ }
+ }
+
+ QDir old_inst_dir(inst->instanceRoot());
+
+ QString old_index_folder(FS::PathCombine(old_inst_dir.absolutePath(), "flame"));
+ QString old_index_path(FS::PathCombine(old_index_folder, "manifest.json"));
+
+ QFileInfo old_index_file(old_index_path);
+ if (old_index_file.exists())
+ {
+ Flame::Manifest old_pack;
+ Flame::loadManifest(old_pack, old_index_path);
+
+ auto& old_files = old_pack.files;
+
+ auto& files = m_pack.files;
+
+ // Remove repeated files, we don't need to download them!
+ auto files_iterator = files.begin();
+ while (files_iterator != files.end())
+ {
+ auto const& file = files_iterator;
+
+ auto old_file = old_files.find(file.key());
+ if (old_file != old_files.end())
+ {
+ // We found a match, but is it a different version?
+ if (old_file->fileId == file->fileId)
+ {
+ qDebug() << "Removed file at" << file->targetFolder << "with id" << file->fileId
+ << "from list of downloads";
+
+ old_files.remove(file.key());
+ files_iterator = files.erase(files_iterator);
+
+ if (files_iterator != files.begin())
+ files_iterator--;
+ }
+ }
+
+ files_iterator++;
+ }
+
+ QDir old_minecraft_dir(inst->gameRoot());
+
+ // We will remove all the previous overrides, to prevent duplicate files!
+ // Note: Overrides intentionally replace all files on update - this matches modpack author expectations.
+ auto old_overrides = Override::readOverrides("overrides", old_index_folder);
+ for (const auto& entry : old_overrides)
+ {
+ if (entry.isEmpty())
+ continue;
+
+ // Skip removal of .disabled files (user-disabled mods should be preserved)
+ if (entry.endsWith(".disabled", Qt::CaseInsensitive))
+ {
+ qDebug() << "Preserving disabled mod:" << entry;
+ continue;
+ }
+
+ qDebug() << "Scheduling" << entry << "for removal";
+ m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry));
+ m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry + ".disabled"));
+ }
+
+ // Remove remaining old files (we need to do an API request to know which ids are which files...)
+ QStringList fileIds;
+
+ for (auto& file : old_files)
+ {
+ fileIds.append(QString::number(file.fileId));
+ }
+
+ auto raw_response = std::make_shared<QByteArray>();
+ auto job = api.getFiles(fileIds, raw_response);
+
+ QEventLoop loop;
+
+ connect(job.get(),
+ &Task::succeeded,
+ this,
+ [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir]
+ {
+ // Parse the API response
+ QJsonParseError parse_error{};
+ auto doc = QJsonDocument::fromJson(*raw_response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Flame files task at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *raw_response;
+ return;
+ }
+
+ try
+ {
+ QJsonArray entries;
+ if (fileIds.size() == 1)
+ entries = { Json::requireObject(Json::requireObject(doc), "data") };
+ else
+ entries = Json::requireArray(Json::requireObject(doc), "data");
+
+ for (auto entry : entries)
+ {
+ auto entry_obj = Json::requireObject(entry);
+
+ Flame::File file;
+ // We don't care about blocked mods, we just need local data to delete the file
+ file.version = FlameMod::loadIndexedPackVersion(entry_obj);
+ auto id = Json::requireInteger(entry_obj, "id");
+ old_files.insert(id, file);
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ qCritical() << e.cause() << e.what();
+ }
+
+ // Delete the files
+ for (auto& file : old_files)
+ {
+ if (file.version.fileName.isEmpty() || file.targetFolder.isEmpty())
+ continue;
+
+ QString relative_path(FS::PathCombine(file.targetFolder, file.version.fileName));
+ qDebug() << "Scheduling" << relative_path << "for removal";
+ m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path));
+ if (relative_path.endsWith(".disabled"))
+ { // remove it if it was enabled/disabled by user
+ m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path.chopped(9)));
+ }
+ else
+ {
+ m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path + ".disabled"));
+ }
+ }
+ });
+ connect(job.get(),
+ &Task::failed,
+ this,
+ [](QString reason) { qCritical() << "Failed to get files: " << reason; });
+ connect(job.get(), &Task::finished, &loop, &QEventLoop::quit);
+
+ m_processUpdateFileInfoJob = job;
+ job->start();
+
+ loop.exec();
+
+ m_processUpdateFileInfoJob = nullptr;
+ }
+ else
+ {
+ // We don't have an old index file, so we may duplicate stuff!
+ auto dialog = CustomMessageBox::selectable(
+ m_parent,
+ tr("No index file."),
+ tr("We couldn't find a suitable index file for the older version. This may cause some "
+ "of the files to be duplicated. Do you want to continue?"),
+ QMessageBox::Warning,
+ QMessageBox::Ok | QMessageBox::Cancel);
+
+ if (dialog->exec() == QDialog::DialogCode::Rejected)
+ {
+ m_abort = true;
+ return false;
+ }
+ }
+
+ setOverride(true, inst->id());
+ qDebug() << "Will override instance!";
+
+ m_instance = inst;
+
+ // We let it go through the createInstance() stage, just with a couple modifications for updating
+ return false;
+}
+
+QString FlameCreationTask::getVersionForLoader(QString uid,
+ QString loaderType,
+ QString loaderVersion,
+ QString mcVersion)
+{
+ if (loaderVersion == "recommended")
+ {
+ auto vlist = APPLICATION->metadataIndex()->component(uid);
+ if (!vlist)
+ {
+ setError(tr("Failed to get local metadata index for %1").arg(uid));
+ return {};
+ }
+
+ if (!vlist->isLoaded())
+ {
+ QEventLoop loadVersionLoop;
+ auto task = vlist->getLoadTask();
+ connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit);
+ if (!task->isRunning())
+ task->start();
+
+ loadVersionLoop.exec();
+ }
+
+ for (auto version : vlist->allVersions())
+ {
+ // first recommended build we find, we use.
+ if (!version->isStable())
+ continue;
+ auto reqs = version->dependencies();
+
+ // 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 (loaderType == "forge" || loaderType == "neoforge")
+ {
+ auto iter = std::find_if(reqs.begin(),
+ reqs.end(),
+ [mcVersion](const projt::meta::ComponentDependency& req)
+ { return req.uid == "net.minecraft" && req.equalsVersion == mcVersion; });
+ if (iter == reqs.end())
+ continue;
+ }
+ return version->descriptor();
+ }
+
+ setError(tr("Failed to find version for %1 loader").arg(loaderType));
+ return {};
+ }
+
+ if (loaderVersion.isEmpty())
+ {
+ emitFailed(tr("No loader version set for modpack!"));
+ return {};
+ }
+
+ return loaderVersion;
+}
+
+std::unique_ptr<MinecraftInstance> FlameCreationTask::createInstance()
+{
+ QEventLoop loop;
+
+ QString parent_folder(FS::PathCombine(m_stagingPath, "flame"));
+
+ try
+ {
+ QString index_path(FS::PathCombine(m_stagingPath, "manifest.json"));
+ if (!m_pack.is_loaded)
+ Flame::loadManifest(m_pack, index_path);
+
+ // Keep index file in case we need it some other time (like when changing versions)
+ QString new_index_place(FS::PathCombine(parent_folder, "manifest.json"));
+ FS::ensureFilePathExists(new_index_place);
+ FS::move(index_path, new_index_place);
+ }
+ catch (const JSONValidationError& e)
+ {
+ setError(tr("Could not understand pack manifest:\n") + e.cause());
+ return nullptr;
+ }
+
+ if (!m_pack.overrides.isEmpty())
+ {
+ QString overridePath = FS::PathCombine(m_stagingPath, m_pack.overrides);
+ if (QFile::exists(overridePath))
+ {
+ // Create a list of overrides in "overrides.txt" inside flame/
+ Override::createOverrides("overrides", parent_folder, overridePath);
+
+ QString mcPath = FS::PathCombine(m_stagingPath, "minecraft");
+ if (!FS::move(overridePath, mcPath))
+ {
+ setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides);
+ return nullptr;
+ }
+ }
+ else
+ {
+ logWarning(tr("The specified overrides folder (%1) is missing. Maybe the modpack was already used before?")
+ .arg(m_pack.overrides));
+ }
+ }
+
+ QString loaderType;
+ QString loaderUid;
+ QString loaderVersion;
+
+ for (auto& loader : m_pack.minecraft.modLoaders)
+ {
+ auto id = loader.id;
+ if (id.startsWith("neoforge-"))
+ {
+ id.remove("neoforge-");
+ if (id.startsWith("1.20.1-"))
+ id.remove("1.20.1-"); // this is a mess for curseforge
+ loaderType = "neoforge";
+ loaderUid = "net.neoforged";
+ }
+ else if (id.startsWith("forge-"))
+ {
+ id.remove("forge-");
+ loaderType = "forge";
+ loaderUid = "net.minecraftforge";
+ }
+ else if (id.startsWith("fabric-"))
+ {
+ id.remove("fabric-");
+ loaderType = "fabric";
+ loaderUid = "net.fabricmc.fabric-loader";
+ }
+ else if (id.startsWith("quilt-"))
+ {
+ id.remove("quilt-");
+ loaderType = "quilt";
+ loaderUid = "org.quiltmc.quilt-loader";
+ }
+ else
+ {
+ logWarning(tr("Unknown mod loader in manifest: %1").arg(id));
+ continue;
+ }
+ loaderVersion = id;
+ }
+
+ QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
+ auto createdInstance = std::make_unique<MinecraftInstance>(m_globalSettings, instanceSettings, m_stagingPath);
+ auto& instance = *createdInstance;
+ auto mcVersion = m_pack.minecraft.version;
+
+ // Hack to correct some 'special sauce'...
+ if (mcVersion.endsWith('.'))
+ {
+ static const QRegularExpression s_regex("[.]+$");
+ mcVersion.remove(s_regex);
+ logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack."));
+ }
+
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+ components->setComponentVersion("net.minecraft", mcVersion, true);
+ if (!loaderType.isEmpty())
+ {
+ auto version = getVersionForLoader(loaderUid, loaderType, loaderVersion, mcVersion);
+ if (version.isEmpty())
+ return nullptr;
+ components->setComponentVersion(loaderUid, version);
+ }
+
+ if (m_instIcon != "default")
+ {
+ instance.setIconKey(m_instIcon);
+ }
+ else
+ {
+ if (m_pack.name.contains("Direwolf20"))
+ {
+ instance.setIconKey("steve");
+ }
+ else if (m_pack.name.contains("FTB") || m_pack.name.contains("Feed The Beast"))
+ {
+ instance.setIconKey("ftb_logo");
+ }
+ else
+ {
+ instance.setIconKey("flame");
+ }
+ }
+
+ int recommendedRAM = m_pack.minecraft.recommendedRAM;
+
+ // only set memory if this is a fresh instance
+ if (m_instance == nullptr && recommendedRAM > 0)
+ {
+ const uint64_t sysMiB = HardwareInfo::totalRamMiB();
+ const uint64_t max = sysMiB * 0.9;
+ if (static_cast<uint64_t>(recommendedRAM) > max)
+ {
+ logWarning(tr("The recommended memory of the modpack exceeds 90% of your system RAM—reducing it from %1 "
+ "MiB to %2 MiB!")
+ .arg(recommendedRAM)
+ .arg(max));
+ recommendedRAM = max;
+ }
+
+ instance.settings()->set("OverrideMemory", true);
+ instance.settings()->set("MaxMemAlloc", recommendedRAM);
+ }
+
+ QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods");
+ QFileInfo jarmodsInfo(jarmodsPath);
+ if (jarmodsInfo.isDir())
+ {
+ // install all the jar mods
+ qDebug() << "Found jarmods:";
+ QDir jarmodsDir(jarmodsPath);
+ QStringList jarMods;
+ for (const auto& info : jarmodsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files))
+ {
+ qDebug() << info.fileName();
+ jarMods.push_back(info.absoluteFilePath());
+ }
+ auto profile = instance.getPackProfile();
+ profile->installJarMods(jarMods);
+ // nuke the original files
+ FS::deletePath(jarmodsPath);
+ }
+
+ // Don't add managed info to packs without an ID (most likely imported from ZIP)
+ if (!m_managedId.isEmpty())
+ instance.setManagedPack("flame", m_managedId, m_pack.name, m_managedVersionId, m_pack.version);
+ else
+ instance.setManagedPack("flame", "", name(), "", "");
+
+ instance.setName(name());
+
+ m_modIdResolver.reset(new Flame::FileResolvingTask(m_pack));
+ connect(m_modIdResolver.get(),
+ &Flame::FileResolvingTask::succeeded,
+ this,
+ [this, &loop] { idResolverSucceeded(loop); });
+ connect(m_modIdResolver.get(),
+ &Flame::FileResolvingTask::failed,
+ [this, &loop](QString reason)
+ {
+ m_modIdResolver.reset();
+ setError(tr("Unable to resolve mod IDs:\n") + reason);
+ loop.quit();
+ });
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit);
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
+ connect(m_modIdResolver.get(),
+ &Flame::FileResolvingTask::stepProgress,
+ this,
+ &FlameCreationTask::propagateStepProgress);
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails);
+ m_modIdResolver->start();
+
+ loop.exec();
+
+ bool did_succeed = getError().isEmpty();
+
+ // Update information of the already installed instance, if any.
+ if (m_instance && did_succeed)
+ {
+ setAbortable(false);
+ auto inst = m_instance.value();
+
+ inst->copyManagedPack(instance);
+ }
+
+ if (did_succeed)
+ {
+ return createdInstance;
+ }
+ return nullptr;
+}
+
+void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
+{
+ auto results = m_modIdResolver->getResults().files;
+
+ QStringList optionalFiles;
+ for (auto& result : results)
+ {
+ if (!result.required)
+ {
+ optionalFiles << FS::PathCombine(result.targetFolder, result.version.fileName);
+ }
+ }
+
+ if (!optionalFiles.empty())
+ {
+ OptionalModDialog optionalModDialog(m_parent, optionalFiles);
+ if (optionalModDialog.exec() == QDialog::Rejected)
+ {
+ emitAborted();
+ loop.quit();
+ return;
+ }
+
+ m_selectedOptionalMods = optionalModDialog.getResult();
+ }
+
+ // first check for blocked mods
+ QList<BlockedMod> blocked_mods;
+ auto anyBlocked = false;
+ for (const auto& result : results.values())
+ {
+ if (result.resourceType != ModPlatform::ResourceType::Mod)
+ {
+ m_otherResources.append(std::make_pair(result.version.fileName, result.targetFolder));
+ }
+
+ // skip optional mods that were not selected
+ if (result.version.downloadUrl.isEmpty())
+ {
+ BlockedMod blocked_mod;
+ blocked_mod.name = result.version.fileName;
+ blocked_mod.websiteUrl =
+ QString("%1/download/%2").arg(result.pack.websiteUrl, QString::number(result.fileId));
+ blocked_mod.hash = result.version.hash;
+ blocked_mod.matched = false;
+ blocked_mod.localPath = "";
+ blocked_mod.targetFolder = result.targetFolder;
+ auto fileName = result.version.fileName;
+ fileName = FS::RemoveInvalidPathChars(fileName);
+ auto relpath = FS::PathCombine(result.targetFolder, fileName);
+ blocked_mod.disabled = !result.required && !m_selectedOptionalMods.contains(relpath);
+
+ blocked_mods.append(blocked_mod);
+
+ anyBlocked = true;
+ }
+ }
+ if (anyBlocked)
+ {
+ qWarning() << "Blocked mods found, displaying mod list";
+
+ BlockedModsDialog message_dialog(
+ m_parent,
+ tr("Blocked mods found"),
+ tr("The following files are not available for download in third party launchers.<br/>"
+ "You will need to manually download them and add them to the instance."),
+ blocked_mods);
+
+ message_dialog.setModal(true);
+
+ if (message_dialog.exec())
+ {
+ qDebug() << "Post dialog blocked mods list: " << blocked_mods;
+ copyBlockedMods(blocked_mods);
+ setupDownloadJob(loop);
+ }
+ else
+ {
+ m_modIdResolver.reset();
+ setError("Canceled");
+ loop.quit();
+ }
+ }
+ else
+ {
+ setupDownloadJob(loop);
+ }
+}
+
+void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
+{
+ m_filesJob.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network()));
+ auto results = m_modIdResolver->getResults().files;
+
+ for (const auto& result : results)
+ {
+ auto fileName = result.version.fileName;
+ fileName = FS::RemoveInvalidPathChars(fileName);
+ auto relpath = FS::PathCombine(result.targetFolder, fileName);
+
+ if (!result.required && !m_selectedOptionalMods.contains(relpath))
+ {
+ relpath += ".disabled";
+ }
+
+ relpath = FS::PathCombine("minecraft", relpath);
+ auto path = FS::PathCombine(m_stagingPath, relpath);
+
+ if (!result.version.downloadUrl.isEmpty())
+ {
+ qDebug() << "Will download" << result.version.downloadUrl << "to" << path;
+ auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path);
+ m_filesJob->addNetAction(dl);
+ }
+ }
+
+ connect(m_filesJob.get(),
+ &NetJob::finished,
+ this,
+ [this, &loop]()
+ {
+ m_filesJob.reset();
+ validateOtherResources(loop);
+ });
+ connect(m_filesJob.get(),
+ &NetJob::failed,
+ [this](QString reason)
+ {
+ m_filesJob.reset();
+ setError(reason);
+ });
+ connect(m_filesJob.get(),
+ &NetJob::progress,
+ this,
+ [this](qint64 current, qint64 total)
+ {
+ setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
+ setProgress(current, total);
+ });
+ connect(m_filesJob.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
+
+ setStatus(tr("Downloading mods..."));
+ m_filesJob->start();
+}
+
+/// @brief copy the matched blocked mods to the instance staging area
+/// @param blocked_mods list of the blocked mods and their matched paths
+void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
+{
+ setStatus(tr("Copying Blocked Mods..."));
+ setAbortable(false);
+ int i = 0;
+ int total = blocked_mods.length();
+ setProgress(i, total);
+ for (auto const& mod : blocked_mods)
+ {
+ if (!mod.matched)
+ {
+ qDebug() << mod.name << "was not matched to a local file, skipping copy";
+ continue;
+ }
+
+ auto destPath = FS::PathCombine(m_stagingPath, "minecraft", mod.targetFolder, mod.name);
+ if (mod.disabled)
+ destPath += ".disabled";
+
+ setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total)));
+
+ qDebug() << "Will try to copy" << mod.localPath << "to" << destPath;
+
+ if (mod.move)
+ {
+ if (!FS::move(mod.localPath, destPath))
+ {
+ qDebug() << "Move of" << mod.localPath << "to" << destPath << "Failed";
+ }
+ }
+ else
+ {
+ if (!FS::copy(mod.localPath, destPath)())
+ {
+ qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed";
+ }
+ }
+
+ i++;
+ setProgress(i, total);
+ }
+
+ setAbortable(true);
+}
+
+void FlameCreationTask::validateOtherResources(QEventLoop& loop)
+{
+ qDebug() << "Validating whether other resources are in the right place";
+ QStringList zipMods;
+ for (auto [fileName, targetFolder] : m_otherResources)
+ {
+ qDebug() << "Checking" << fileName << "...";
+ auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName);
+
+ /// @brief check the target and move the the file
+ /// @return path where file can now be found
+ auto validatePath = [&localPath, this](QString fileName, QString targetFolder, QString realTarget)
+ {
+ if (targetFolder != realTarget)
+ {
+ qDebug() << "Target folder of" << fileName << "is incorrect, it belongs in" << realTarget;
+ auto destPath = FS::PathCombine(m_stagingPath, "minecraft", realTarget, fileName);
+ qDebug() << "Moving" << localPath << "to" << destPath;
+ if (FS::move(localPath, destPath))
+ {
+ return destPath;
+ }
+ }
+ else
+ {
+ qDebug() << "Target folder of" << fileName << "is correct at" << targetFolder;
+ }
+ return localPath;
+ };
+
+ auto installWorld = [this](QString worldPath)
+ {
+ qDebug() << "Installing World from" << worldPath;
+ QFileInfo worldFileInfo(worldPath);
+ World w(worldFileInfo);
+ if (!w.isValid())
+ {
+ qDebug() << "World at" << worldPath << "is not valid, skipping install.";
+ }
+ else
+ {
+ w.install(FS::PathCombine(m_stagingPath, "minecraft", "saves"));
+ }
+ };
+
+ QFileInfo localFileInfo(localPath);
+ auto type = ResourceUtils::identify(localFileInfo);
+
+ QString worldPath;
+
+ switch (type)
+ {
+ case ModPlatform::ResourceType::Mod:
+ validatePath(fileName, targetFolder, "mods");
+ zipMods.push_back(fileName);
+ break;
+ case ModPlatform::ResourceType::ResourcePack: validatePath(fileName, targetFolder, "resourcepacks"); break;
+ case ModPlatform::ResourceType::TexturePack: validatePath(fileName, targetFolder, "texturepacks"); break;
+ case ModPlatform::ResourceType::DataPack: validatePath(fileName, targetFolder, "datapacks"); break;
+ case ModPlatform::ResourceType::ShaderPack:
+ // in theory flame API can't do this but who knows, that *may* change ?
+ // better to handle it if it *does* occur in the future
+ validatePath(fileName, targetFolder, "shaderpacks");
+ break;
+ case ModPlatform::ResourceType::World:
+ worldPath = validatePath(fileName, targetFolder, "saves");
+ installWorld(worldPath);
+ break;
+ case ModPlatform::ResourceType::Unknown:
+ /* fallthrough */
+ default:
+ qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is.";
+ break;
+ }
+ }
+ // Generic metadata creation for supported resource types
+ auto task = makeShared<ConcurrentTask>("CreateModMetadata",
+ APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
+ auto results = m_modIdResolver->getResults().files;
+
+ for (auto file : results)
+ {
+ QString indexParent;
+ switch (file.resourceType)
+ {
+ case ModPlatform::ResourceType::Mod:
+ // Skip if it looks like a zip but wasn't identified as a mod (e.g. valid resource pack that got
+ // confused, or invalid file)
+ if (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))
+ {
+ continue;
+ }
+ indexParent = "mods";
+ break;
+ case ModPlatform::ResourceType::ResourcePack: indexParent = "resourcepacks"; break;
+ case ModPlatform::ResourceType::ShaderPack: indexParent = "shaderpacks"; break;
+ default: continue;
+ }
+
+ auto folder = FS::PathCombine(m_stagingPath, "minecraft", indexParent, ".index");
+ task->addTask(makeShared<LocalResourceUpdateTask>(folder, file.pack, file.version));
+ }
+
+ connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
+ m_processUpdateFileInfoJob = task;
+ task->start();
+}
diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.h b/archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.h
new file mode 100644
index 0000000000..488b13e905
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.h
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "InstanceCreationTask.h"
+
+#include <optional>
+
+#include "minecraft/MinecraftInstance.h"
+
+#include "modplatform/flame/FileResolvingTask.h"
+
+#include "net/NetJob.h"
+
+#include "ui/dialogs/BlockedModsDialog.h"
+
+class FlameCreationTask final : public InstanceCreationTask
+{
+ Q_OBJECT
+
+ public:
+ FlameCreationTask(const QString& staging_path,
+ SettingsObjectPtr global_settings,
+ QWidget* parent,
+ QString id,
+ QString version_id,
+ QString original_instance_id = {})
+ : InstanceCreationTask(),
+ m_parent(parent),
+ m_managedId(std::move(id)),
+ m_managedVersionId(std::move(version_id))
+ {
+ setStagingPath(staging_path);
+ setParentSettings(global_settings);
+
+ m_original_instance_id = std::move(original_instance_id);
+ }
+
+ bool abort() override;
+
+ bool updateInstance() override;
+ std::unique_ptr<MinecraftInstance> createInstance() override;
+
+ private slots:
+ void idResolverSucceeded(QEventLoop&);
+ void setupDownloadJob(QEventLoop&);
+ void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
+ void validateOtherResources(QEventLoop& loop);
+ QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion);
+
+ private:
+ QWidget* m_parent = nullptr;
+
+ shared_qobject_ptr<Flame::FileResolvingTask> m_modIdResolver;
+ Flame::Manifest m_pack;
+
+ // Handle to allow aborting
+ Task::Ptr m_processUpdateFileInfoJob = nullptr;
+ NetJob::Ptr m_filesJob = nullptr;
+
+ QString m_managedId, m_managedVersionId;
+
+ QList<std::pair<QString, QString>> m_otherResources;
+
+ std::optional<InstancePtr> m_instance;
+
+ QStringList m_selectedOptionalMods;
+};
diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.cpp b/archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.cpp
new file mode 100644
index 0000000000..7bf4917df8
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.cpp
@@ -0,0 +1,239 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "FlameModIndex.h"
+
+#include "FileSystem.h"
+#include "Json.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "modplatform/ModIndex.h"
+#include "modplatform/flame/FlameAPI.h"
+
+static FlameAPI api;
+
+void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
+{
+ pack.addonId = Json::requireInteger(obj, "id");
+ pack.provider = ModPlatform::ResourceProvider::FLAME;
+ pack.name = Json::requireString(obj, "name");
+ pack.slug = Json::requireString(obj, "slug");
+ pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", "");
+ pack.description = Json::ensureString(obj, "summary", "");
+
+ QJsonObject logo = Json::ensureObject(obj, "logo");
+ pack.logoName = Json::ensureString(logo, "title");
+ pack.logoUrl = Json::ensureString(logo, "thumbnailUrl");
+ if (pack.logoUrl.isEmpty())
+ {
+ pack.logoUrl = Json::ensureString(logo, "url");
+ }
+
+ auto authors = Json::ensureArray(obj, "authors");
+ if (!authors.isEmpty())
+ {
+ pack.authors.clear();
+ for (auto authorIter : authors)
+ {
+ auto author = Json::requireObject(authorIter);
+ ModPlatform::ModpackAuthor packAuthor;
+ packAuthor.name = Json::requireString(author, "name");
+ packAuthor.url = Json::requireString(author, "url");
+ pack.authors.append(packAuthor);
+ }
+ }
+
+ pack.extraDataLoaded = false;
+ loadURLs(pack, obj);
+}
+
+void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj)
+{
+ auto links_obj = Json::ensureObject(obj, "links");
+
+ pack.extraData.issuesUrl = Json::ensureString(links_obj, "issuesUrl");
+ if (pack.extraData.issuesUrl.endsWith('/'))
+ pack.extraData.issuesUrl.chop(1);
+
+ pack.extraData.sourceUrl = Json::ensureString(links_obj, "sourceUrl");
+ if (pack.extraData.sourceUrl.endsWith('/'))
+ pack.extraData.sourceUrl.chop(1);
+
+ pack.extraData.wikiUrl = Json::ensureString(links_obj, "wikiUrl");
+ if (pack.extraData.wikiUrl.endsWith('/'))
+ pack.extraData.wikiUrl.chop(1);
+
+ if (!pack.extraData.body.isEmpty())
+ pack.extraDataLoaded = true;
+}
+
+void FlameMod::loadBody(ModPlatform::IndexedPack& pack)
+{
+ pack.extraData.body = api.getModDescription(pack.addonId.toInt());
+
+ if (!pack.extraData.issuesUrl.isEmpty() || !pack.extraData.sourceUrl.isEmpty() || !pack.extraData.wikiUrl.isEmpty())
+ pack.extraDataLoaded = true;
+}
+
+static QString enumToString(int hash_algorithm)
+{
+ switch (hash_algorithm)
+ {
+ default:
+ case 1: return "sha1";
+ case 2: return "md5";
+ }
+}
+
+void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr)
+{
+ QList<ModPlatform::IndexedVersion> unsortedVersions;
+ for (auto versionIter : arr)
+ {
+ auto obj = versionIter.toObject();
+
+ auto file = loadIndexedPackVersion(obj);
+ if (!file.addonId.isValid())
+ file.addonId = pack.addonId;
+
+ if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
+ unsortedVersions.append(file);
+ }
+
+ auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool
+ {
+ // dates are in RFC 3339 format
+ return a.date > b.date;
+ };
+ std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
+ pack.versions = unsortedVersions;
+ pack.versionsLoaded = true;
+}
+
+auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> ModPlatform::IndexedVersion
+{
+ auto versionArray = Json::requireArray(obj, "gameVersions");
+
+ ModPlatform::IndexedVersion file;
+ for (auto mcVer : versionArray)
+ {
+ auto str = mcVer.toString();
+
+ if (str.contains('.'))
+ file.mcVersion.append(str);
+
+ file.side = ModPlatform::Side::NoSide;
+ if (auto loader = str.toLower(); loader == "neoforge")
+ file.loaders |= ModPlatform::NeoForge;
+ else if (loader == "forge")
+ file.loaders |= ModPlatform::Forge;
+ else if (loader == "cauldron")
+ file.loaders |= ModPlatform::Cauldron;
+ else if (loader == "liteloader")
+ file.loaders |= ModPlatform::LiteLoader;
+ else if (loader == "fabric")
+ file.loaders |= ModPlatform::Fabric;
+ else if (loader == "risugami")
+ file.loaders |= ModPlatform::Risugami;
+ else if (loader == "station-loader")
+ file.loaders |= ModPlatform::StationLoader;
+ else if (loader == "modloadermp")
+ file.loaders |= ModPlatform::ModLoaderMP;
+ else if (loader == "optifine")
+ file.loaders |= ModPlatform::Optifine;
+ else if (loader == "quilt")
+ file.loaders |= ModPlatform::Quilt;
+ else if (loader == "server" || loader == "client")
+ {
+ if (file.side == ModPlatform::Side::NoSide)
+ file.side = ModPlatform::SideUtils::fromString(loader);
+ else if (file.side != ModPlatform::SideUtils::fromString(loader))
+ file.side = ModPlatform::Side::UniversalSide;
+ }
+ }
+
+ file.addonId = Json::requireInteger(obj, "modId");
+ file.fileId = Json::requireInteger(obj, "id");
+ file.date = Json::requireString(obj, "fileDate");
+ file.version = Json::requireString(obj, "displayName");
+ file.downloadUrl = Json::ensureString(obj, "downloadUrl");
+ file.fileName = Json::requireString(obj, "fileName");
+ file.fileName = FS::RemoveInvalidPathChars(file.fileName);
+
+ ModPlatform::IndexedVersionType::VersionType ver_type;
+ switch (Json::requireInteger(obj, "releaseType"))
+ {
+ case 1: ver_type = ModPlatform::IndexedVersionType::VersionType::Release; break;
+ case 2: ver_type = ModPlatform::IndexedVersionType::VersionType::Beta; break;
+ case 3: ver_type = ModPlatform::IndexedVersionType::VersionType::Alpha; break;
+ default: ver_type = ModPlatform::IndexedVersionType::VersionType::Unknown;
+ }
+ file.version_type = ModPlatform::IndexedVersionType(ver_type);
+
+ auto hash_list = Json::ensureArray(obj, "hashes");
+ for (auto h : hash_list)
+ {
+ auto hash_entry = Json::ensureObject(h);
+ auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::FLAME);
+ auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm"));
+ if (hash_types.contains(hash_algo))
+ {
+ file.hash = Json::requireString(hash_entry, "value");
+ file.hash_type = hash_algo;
+ break;
+ }
+ }
+
+ auto dependencies = Json::ensureArray(obj, "dependencies");
+ for (auto d : dependencies)
+ {
+ auto dep = Json::ensureObject(d);
+ ModPlatform::Dependency dependency;
+ dependency.addonId = Json::requireInteger(dep, "modId");
+ switch (Json::requireInteger(dep, "relationType"))
+ {
+ case 1: // EmbeddedLibrary
+ dependency.type = ModPlatform::DependencyType::EMBEDDED;
+ break;
+ case 2: // OptionalDependency
+ dependency.type = ModPlatform::DependencyType::OPTIONAL;
+ break;
+ case 3: // RequiredDependency
+ dependency.type = ModPlatform::DependencyType::REQUIRED;
+ break;
+ case 4: // Tool
+ dependency.type = ModPlatform::DependencyType::TOOL;
+ break;
+ case 5: // Incompatible
+ dependency.type = ModPlatform::DependencyType::INCOMPATIBLE;
+ break;
+ case 6: // Include
+ dependency.type = ModPlatform::DependencyType::INCLUDE;
+ break;
+ default: dependency.type = ModPlatform::DependencyType::UNKNOWN; break;
+ }
+ file.dependencies.append(dependency);
+ }
+
+ if (load_changelog)
+ file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt());
+
+ return file;
+}
diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.h b/archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.h
new file mode 100644
index 0000000000..aebd7f2976
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.h
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#include "modplatform/ModIndex.h"
+
+#include "BaseInstance.h"
+
+namespace FlameMod
+{
+
+ void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
+ void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj);
+ void loadBody(ModPlatform::IndexedPack& m);
+ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr);
+ ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false);
+} // namespace FlameMod \ No newline at end of file
diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.cpp b/archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.cpp
new file mode 100644
index 0000000000..4113ffd7ba
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.cpp
@@ -0,0 +1,566 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "FlamePackExportTask.h"
+#include <QJsonArray>
+#include <QJsonObject>
+
+#include <QCryptographicHash>
+#include <QFileInfo>
+#include <QMessageBox>
+#include <QtConcurrentRun>
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include "Application.h"
+#include "Json.h"
+#include "MMCZip.h"
+#include "minecraft/PackProfile.h"
+#include "minecraft/mod/ModFolderModel.hpp"
+#include "modplatform/ModIndex.h"
+#include "modplatform/flame/FlameModIndex.h"
+#include "modplatform/helpers/HashUtils.h"
+#include "tasks/Task.h"
+
+const QString FlamePackExportTask::TEMPLATE = "<li><a href=\"{url}\">{name}{authors}</a></li>\n";
+const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" });
+
+FlamePackExportTask::FlamePackExportTask(FlamePackExportOptions&& options)
+ : m_options(std::move(options)),
+ m_gameRoot(m_options.instance->gameRoot())
+{}
+
+void FlamePackExportTask::executeTask()
+{
+ setStatus(tr("Searching for files..."));
+ setProgress(0, 5);
+ collectFiles();
+}
+
+bool FlamePackExportTask::abort()
+{
+ if (task)
+ {
+ task->abort();
+ return true;
+ }
+ return false;
+}
+
+void FlamePackExportTask::collectFiles()
+{
+ setAbortable(false);
+ QCoreApplication::processEvents();
+
+ m_files.clear();
+ if (!MMCZip::collectFileListRecursively(m_options.instance->gameRoot(), nullptr, &m_files, m_options.filter))
+ {
+ emitFailed(tr("Could not search for files"));
+ return;
+ }
+
+ pendingHashes.clear();
+ resolvedFiles.clear();
+
+ m_options.instance->loaderModList()->update();
+ connect(m_options.instance->loaderModList().get(),
+ &ModFolderModel::updateFinished,
+ this,
+ &FlamePackExportTask::collectHashes);
+}
+
+void FlamePackExportTask::collectHashes()
+{
+ setAbortable(true);
+ setStatus(tr("Finding file hashes..."));
+ setProgress(1, 5);
+ auto allMods = m_options.instance->loaderModList()->allMods();
+ ConcurrentTask::Ptr hashingTask(
+ new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()));
+ task.reset(hashingTask);
+ for (const QFileInfo& file : m_files)
+ {
+ const QString relative = m_gameRoot.relativeFilePath(file.absoluteFilePath());
+ // require sensible file types
+ if (!std::any_of(
+ FILE_EXTENSIONS.begin(),
+ FILE_EXTENSIONS.end(),
+ [&relative](const QString& extension)
+ { return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); }))
+ continue;
+
+ if (relative.startsWith("resourcepacks/") && (relative.endsWith(".zip") || relative.endsWith(".zip.disabled")))
+ { // is resourcepack
+ auto hashTask = Hashing::createHasher(file.absoluteFilePath(), ModPlatform::ResourceProvider::FLAME);
+ connect(hashTask.get(),
+ &Hashing::Hasher::resultsReady,
+ [this, relative, file](QString hash)
+ {
+ if (m_state == Task::State::Running)
+ {
+ pendingHashes.insert(hash,
+ { relative, file.absoluteFilePath(), relative.endsWith(".zip") });
+ }
+ });
+ connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
+ hashingTask->addTask(hashTask);
+ continue;
+ }
+
+ if (auto modIter =
+ std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; });
+ modIter != allMods.end())
+ {
+ const Mod* mod = *modIter;
+ if (!mod || mod->type() == ResourceType::FOLDER)
+ {
+ continue;
+ }
+ if (mod->metadata() && mod->metadata()->provider == ModPlatform::ResourceProvider::FLAME)
+ {
+ resolvedFiles.insert(mod->fileinfo().absoluteFilePath(),
+ { mod->metadata()->project_id.toInt(),
+ mod->metadata()->file_id.toInt(),
+ mod->enabled(),
+ true,
+ mod->metadata()->name,
+ mod->metadata()->slug,
+ mod->authors().join(", ") });
+ continue;
+ }
+
+ auto hashTask =
+ Hashing::createHasher(mod->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::FLAME);
+ connect(hashTask.get(),
+ &Hashing::Hasher::resultsReady,
+ [this, mod](QString hash)
+ {
+ if (m_state == Task::State::Running)
+ {
+ pendingHashes.insert(
+ hash,
+ { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true });
+ }
+ });
+ connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
+ hashingTask->addTask(hashTask);
+ }
+ }
+ auto progressStep = std::make_shared<TaskStepProgress>();
+ connect(hashingTask.get(),
+ &Task::finished,
+ this,
+ [this, progressStep]
+ {
+ progressStep->state = TaskStepState::Succeeded;
+ stepProgress(*progressStep);
+ });
+
+ connect(hashingTask.get(), &Task::succeeded, this, &FlamePackExportTask::makeApiRequest);
+ connect(hashingTask.get(),
+ &Task::failed,
+ this,
+ [this, progressStep](QString reason)
+ {
+ progressStep->state = TaskStepState::Failed;
+ stepProgress(*progressStep);
+ emitFailed(reason);
+ });
+ connect(hashingTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propagateStepProgress);
+
+ connect(hashingTask.get(),
+ &Task::progress,
+ this,
+ [this, progressStep](qint64 current, qint64 total)
+ {
+ progressStep->update(current, total);
+ stepProgress(*progressStep);
+ });
+ connect(hashingTask.get(),
+ &Task::status,
+ this,
+ [this, progressStep](QString status)
+ {
+ progressStep->status = status;
+ stepProgress(*progressStep);
+ });
+ connect(hashingTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted);
+ hashingTask->start();
+}
+
+void FlamePackExportTask::makeApiRequest()
+{
+ if (pendingHashes.isEmpty())
+ {
+ buildZip();
+ return;
+ }
+
+ setStatus(tr("Finding versions for hashes..."));
+ setProgress(2, 5);
+ auto response = std::make_shared<QByteArray>();
+
+ QList<uint> fingerprints;
+ for (auto& murmur : pendingHashes.keys())
+ {
+ fingerprints.push_back(murmur.toUInt());
+ }
+
+ task.reset(api.matchFingerprints(fingerprints, response));
+
+ connect(task.get(),
+ &Task::succeeded,
+ this,
+ [this, response]
+ {
+ QJsonParseError parseError{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError);
+ if (parseError.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from CurseForge::CurrentVersions at "
+ << parseError.offset << " reason: " << parseError.errorString();
+ qWarning() << *response;
+
+ emitFailed(parseError.errorString());
+ return;
+ }
+
+ try
+ {
+ auto docObj = Json::requireObject(doc);
+ auto dataObj = Json::requireObject(docObj, "data");
+ auto dataArr = Json::requireArray(dataObj, "exactMatches");
+
+ if (dataArr.isEmpty())
+ {
+ qWarning() << "No matches found for fingerprint search!";
+
+ getProjectsInfo();
+ return;
+ }
+ for (auto match : dataArr)
+ {
+ auto matchObj = Json::ensureObject(match, {});
+ auto fileObj = Json::ensureObject(matchObj, "file", {});
+
+ if (matchObj.isEmpty() || fileObj.isEmpty())
+ {
+ qWarning() << "Fingerprint match is empty!";
+
+ return;
+ }
+
+ auto fingerprint = QString::number(Json::ensureVariant(fileObj, "fileFingerprint").toUInt());
+ auto mod = pendingHashes.find(fingerprint);
+ if (mod == pendingHashes.end())
+ {
+ qWarning() << "Invalid fingerprint from the API response.";
+ continue;
+ }
+
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name));
+ if (Json::ensureBoolean(fileObj, "isAvailable", false, "isAvailable"))
+ resolvedFiles.insert(mod->path,
+ { Json::requireInteger(fileObj, "modId"),
+ Json::requireInteger(fileObj, "id"),
+ mod->enabled,
+ mod->isMod });
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ pendingHashes.clear();
+ getProjectsInfo();
+ });
+ connect(task.get(), &Task::failed, this, &FlamePackExportTask::getProjectsInfo);
+ connect(task.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted);
+ task->start();
+}
+
+void FlamePackExportTask::getProjectsInfo()
+{
+ setStatus(tr("Finding project info from CurseForge..."));
+ setProgress(3, 5);
+ QStringList addonIds;
+ for (const auto& resolved : resolvedFiles)
+ {
+ if (resolved.slug.isEmpty())
+ {
+ addonIds << QString::number(resolved.addonId);
+ }
+ }
+
+ auto response = std::make_shared<QByteArray>();
+ Task::Ptr projTask;
+
+ if (addonIds.isEmpty())
+ {
+ buildZip();
+ return;
+ }
+ else if (addonIds.size() == 1)
+ {
+ projTask = api.getProject(*addonIds.begin(), response);
+ }
+ else
+ {
+ projTask = api.getProjects(addonIds, response);
+ }
+
+ connect(projTask.get(),
+ &Task::succeeded,
+ this,
+ [this, response, addonIds]
+ {
+ QJsonParseError parseError{};
+ auto doc = QJsonDocument::fromJson(*response, &parseError);
+ if (parseError.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from CurseForge projects task at "
+ << parseError.offset << " reason: " << parseError.errorString();
+ qWarning() << *response;
+ emitFailed(parseError.errorString());
+ return;
+ }
+
+ try
+ {
+ QJsonArray entries;
+ if (addonIds.size() == 1)
+ entries = { Json::requireObject(Json::requireObject(doc), "data") };
+ else
+ entries = Json::requireArray(Json::requireObject(doc), "data");
+
+ for (auto entry : entries)
+ {
+ auto entryObj = Json::requireObject(entry);
+
+ try
+ {
+ setStatus(tr("Parsing API response from CurseForge for '%1'...")
+ .arg(Json::requireString(entryObj, "name")));
+
+ ModPlatform::IndexedPack pack;
+ FlameMod::loadIndexedPack(pack, entryObj);
+ for (auto key : resolvedFiles.keys())
+ {
+ auto val = resolvedFiles.value(key);
+ if (val.addonId == pack.addonId)
+ {
+ val.name = pack.name;
+ val.slug = pack.slug;
+ QStringList authors;
+ for (auto author : pack.authors)
+ authors << author.name;
+
+ val.authors = authors.join(", ");
+ resolvedFiles[key] = val;
+ }
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << entries;
+ }
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ buildZip();
+ });
+ connect(projTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
+ connect(projTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted);
+ task.reset(projTask);
+ task->start();
+}
+
+void FlamePackExportTask::buildZip()
+{
+ setStatus(tr("Adding files..."));
+ setProgress(4, 5);
+
+ auto zipTask = makeShared<MMCZip::ExportToZipTask>(m_options.output, m_gameRoot, m_files, "overrides/", true, false);
+ zipTask->addExtraFile("manifest.json", generateIndex());
+ zipTask->addExtraFile("modlist.html", generateHTML());
+
+ QStringList exclude;
+ std::transform(resolvedFiles.keyBegin(),
+ resolvedFiles.keyEnd(),
+ std::back_insert_iterator(exclude),
+ [this](QString file) { return m_gameRoot.relativeFilePath(file); });
+ zipTask->setExcludeFiles(exclude);
+
+ auto progressStep = std::make_shared<TaskStepProgress>();
+ connect(zipTask.get(),
+ &Task::finished,
+ this,
+ [this, progressStep]
+ {
+ progressStep->state = TaskStepState::Succeeded;
+ stepProgress(*progressStep);
+ });
+
+ connect(zipTask.get(), &Task::succeeded, this, &FlamePackExportTask::emitSucceeded);
+ connect(zipTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted);
+ connect(zipTask.get(),
+ &Task::failed,
+ this,
+ [this, progressStep](QString reason)
+ {
+ progressStep->state = TaskStepState::Failed;
+ stepProgress(*progressStep);
+ emitFailed(reason);
+ });
+ connect(zipTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propagateStepProgress);
+
+ connect(zipTask.get(),
+ &Task::progress,
+ this,
+ [this, progressStep](qint64 current, qint64 total)
+ {
+ progressStep->update(current, total);
+ stepProgress(*progressStep);
+ });
+ connect(zipTask.get(),
+ &Task::status,
+ this,
+ [this, progressStep](QString status)
+ {
+ progressStep->status = status;
+ stepProgress(*progressStep);
+ });
+ task.reset(zipTask);
+ zipTask->start();
+}
+
+QByteArray FlamePackExportTask::generateIndex()
+{
+ QJsonObject obj;
+ obj["manifestType"] = "minecraftModpack";
+ obj["manifestVersion"] = 1;
+ obj["name"] = m_options.name;
+ obj["version"] = m_options.version;
+ obj["author"] = m_options.author;
+ obj["overrides"] = "overrides";
+
+ QJsonObject version;
+
+ auto profile = m_options.instance->getPackProfile();
+ // collect all supported components
+ const ComponentPtr minecraft = profile->getComponent("net.minecraft");
+ const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
+ const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
+ const ComponentPtr forge = profile->getComponent("net.minecraftforge");
+ const ComponentPtr neoforge = profile->getComponent("net.neoforged");
+
+ // convert all available components to mrpack dependencies
+ if (minecraft != nullptr)
+ version["version"] = minecraft->m_version;
+ QString id;
+ if (quilt != nullptr)
+ id = "quilt-" + quilt->m_version;
+ else if (fabric != nullptr)
+ id = "fabric-" + fabric->m_version;
+ else if (forge != nullptr)
+ id = "forge-" + forge->m_version;
+ else if (neoforge != nullptr)
+ {
+ id = "neoforge-";
+ if (minecraft->m_version == "1.20.1")
+ id += "1.20.1-";
+ id += neoforge->m_version;
+ }
+ version["modLoaders"] = QJsonArray();
+ if (!id.isEmpty())
+ {
+ QJsonObject loader;
+ loader["id"] = id;
+ loader["primary"] = true;
+ version["modLoaders"] = QJsonArray({ loader });
+ }
+
+ if (m_options.recommendedRAM > 0)
+ version["recommendedRam"] = m_options.recommendedRAM;
+
+ obj["minecraft"] = version;
+
+ QJsonArray files;
+ for (auto mod : resolvedFiles)
+ {
+ QJsonObject file;
+ file["projectID"] = mod.addonId;
+ file["fileID"] = mod.version;
+ file["required"] = mod.enabled || !m_options.optionalFiles;
+ files << file;
+ }
+ obj["files"] = files;
+
+ return QJsonDocument(obj).toJson(QJsonDocument::Compact);
+}
+
+QByteArray FlamePackExportTask::generateHTML()
+{
+ QString content = "";
+ for (auto mod : resolvedFiles)
+ {
+ if (mod.isMod)
+ {
+ content +=
+ QString(TEMPLATE)
+ .replace("{name}", mod.name.toHtmlEscaped())
+ .replace("{url}",
+ ModPlatform::getMetaURL(ModPlatform::ResourceProvider::FLAME, mod.addonId).toHtmlEscaped())
+ .replace("{authors}",
+ !mod.authors.isEmpty() ? QString(" (by %1)").arg(mod.authors).toHtmlEscaped() : "");
+ }
+ }
+ content = "<ul>" + content + "</ul>";
+ return content.toUtf8();
+}
diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.h b/archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.h
new file mode 100644
index 0000000000..2535960a38
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.h
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include "MMCZip.h"
+#include "minecraft/MinecraftInstance.h"
+#include "modplatform/flame/FlameAPI.h"
+#include "tasks/Task.h"
+
+struct FlamePackExportOptions
+{
+ QString name;
+ QString version;
+ QString author;
+ bool optionalFiles;
+ MinecraftInstancePtr instance;
+ QString output;
+ MMCZip::FilterFileFunction filter;
+ int recommendedRAM;
+};
+
+class FlamePackExportTask : public Task
+{
+ Q_OBJECT
+ public:
+ FlamePackExportTask(FlamePackExportOptions&& options);
+
+ protected:
+ void executeTask() override;
+ bool abort() override;
+
+ private:
+ static const QString TEMPLATE;
+ static const QStringList FILE_EXTENSIONS;
+
+ // inputs
+
+ struct ResolvedFile
+ {
+ int addonId;
+ int version;
+ bool enabled;
+ bool isMod;
+
+ QString name;
+ QString slug;
+ QString authors;
+ };
+ struct HashInfo
+ {
+ QString name;
+ QString path;
+ bool enabled;
+ bool isMod;
+ };
+
+ FlamePackExportOptions m_options;
+ QDir m_gameRoot;
+
+ FlameAPI api;
+
+ QFileInfoList m_files;
+ QMap<QString, HashInfo> pendingHashes{};
+ QMap<QString, ResolvedFile> resolvedFiles{};
+ Task::Ptr task;
+
+ void collectFiles();
+ void collectHashes();
+ void makeApiRequest();
+ void getProjectsInfo();
+ void buildZip();
+
+ QByteArray generateIndex();
+ QByteArray generateHTML();
+};
diff --git a/archived/projt-launcher/launcher/modplatform/flame/PackManifest.cpp b/archived/projt-launcher/launcher/modplatform/flame/PackManifest.cpp
new file mode 100644
index 0000000000..9bd57387d6
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/PackManifest.cpp
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#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);
+ }
+ m.recommendedRAM = Json::ensureInteger(minecraft, "recommendedRam", 0);
+}
+
+static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest)
+{
+ auto mc = Json::requireObject(manifest, "minecraft");
+
+ loadMinecraftV1(pack.minecraft, mc);
+
+ pack.name = Json::ensureString(manifest, QString("name"), "Unnamed");
+ pack.version = Json::ensureString(manifest, QString("version"), QString());
+ pack.author = Json::ensureString(manifest, QString("author"), "Anonymous");
+
+ auto arr = Json::ensureArray(manifest, "files", QJsonArray());
+ for (auto item : arr)
+ {
+ auto obj = Json::requireObject(item);
+
+ Flame::File file;
+ loadFileV1(file, obj);
+ Q_ASSERT(file.projectId != 0);
+ pack.files.insert(file.fileId, file);
+ }
+
+ pack.overrides = Json::ensureString(manifest, "overrides", "overrides");
+
+ pack.is_loaded = true;
+}
+
+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);
+}
diff --git a/archived/projt-launcher/launcher/modplatform/flame/PackManifest.h b/archived/projt-launcher/launcher/modplatform/flame/PackManifest.h
new file mode 100644
index 0000000000..26856f5f60
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/flame/PackManifest.h
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <QJsonObject>
+#include <QList>
+#include <QMap>
+#include <QString>
+#include <QUrl>
+#include "modplatform/ModIndex.h"
+#include "modplatform/ResourceType.h"
+
+namespace Flame
+{
+ struct File
+ {
+ int projectId = 0;
+ int fileId = 0;
+ // NOTE: the opposite to 'optional'
+ bool required = true;
+
+ ModPlatform::IndexedPack pack;
+ ModPlatform::IndexedVersion version;
+
+ // our
+ QString targetFolder = QStringLiteral("mods");
+ ModPlatform::ResourceType resourceType;
+ };
+
+ struct Modloader
+ {
+ QString id;
+ bool primary = false;
+ };
+
+ struct Minecraft
+ {
+ QString version;
+ QString libraries;
+ QList<Flame::Modloader> modLoaders;
+ int recommendedRAM;
+ };
+
+ struct Manifest
+ {
+ QString manifestType;
+ int manifestVersion = 0;
+ Flame::Minecraft minecraft;
+ QString name;
+ QString version;
+ QString author;
+ // File id -> File
+ QMap<int, Flame::File> files;
+ QString overrides;
+
+ bool is_loaded = false;
+ };
+
+ void loadManifest(Flame::Manifest& m, const QString& filepath);
+} // namespace Flame
diff --git a/archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.cpp b/archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.cpp
new file mode 100644
index 0000000000..28e3b60c33
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.cpp
@@ -0,0 +1,261 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+#include "ExportToModList.h"
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+
+namespace ExportToModList
+{
+ QString toHTML(QList<Mod*> mods, OptionalData extraData)
+ {
+ QStringList lines;
+ for (auto mod : mods)
+ {
+ auto meta = mod->metadata();
+ auto modName = mod->name().toHtmlEscaped();
+ if (extraData & Url)
+ {
+ auto url = mod->homepage().toHtmlEscaped();
+ if (!url.isEmpty())
+ modName = QString("<a href=\"%1\">%2</a>").arg(url, modName);
+ }
+ auto line = modName;
+ if (extraData & Version)
+ {
+ auto ver = mod->version();
+ if (ver.isEmpty() && meta != nullptr)
+ ver = meta->version().toString();
+ if (!ver.isEmpty())
+ line += QString(" [%1]").arg(ver.toHtmlEscaped());
+ }
+ if (extraData & Authors && !mod->authors().isEmpty())
+ line += " by " + mod->authors().join(", ").toHtmlEscaped();
+ if (extraData & FileName)
+ line += QString(" (%1)").arg(mod->fileinfo().fileName().toHtmlEscaped());
+
+ lines.append(QString("<li>%1</li>").arg(line));
+ }
+ return QString("<html><body><ul>\n\t%1\n</ul></body></html>").arg(lines.join("\n\t"));
+ }
+
+ QString toMarkdownEscaped(QString src)
+ {
+ for (auto ch : "\\`*_{}[]<>()#+-.!|")
+ src.replace(ch, QString("\\%1").arg(ch));
+ return src;
+ }
+
+ QString toMarkdown(QList<Mod*> mods, OptionalData extraData)
+ {
+ QStringList lines;
+
+ for (auto mod : mods)
+ {
+ auto meta = mod->metadata();
+ auto modName = toMarkdownEscaped(mod->name());
+ if (extraData & Url)
+ {
+ auto url = mod->homepage();
+ if (!url.isEmpty())
+ modName = QString("[%1](%2)").arg(modName, url);
+ }
+ auto line = modName;
+ if (extraData & Version)
+ {
+ auto ver = toMarkdownEscaped(mod->version());
+ if (ver.isEmpty() && meta != nullptr)
+ ver = toMarkdownEscaped(meta->version().toString());
+ if (!ver.isEmpty())
+ line += QString(" [%1]").arg(ver);
+ }
+ if (extraData & Authors && !mod->authors().isEmpty())
+ line += " by " + toMarkdownEscaped(mod->authors().join(", "));
+ if (extraData & FileName)
+ line += QString(" (%1)").arg(toMarkdownEscaped(mod->fileinfo().fileName()));
+ lines << "- " + line;
+ }
+ return lines.join("\n");
+ }
+
+ QString toPlainTXT(QList<Mod*> mods, OptionalData extraData)
+ {
+ QStringList lines;
+ for (auto mod : mods)
+ {
+ auto meta = mod->metadata();
+ auto modName = mod->name();
+
+ auto line = modName;
+ if (extraData & Url)
+ {
+ auto url = mod->homepage();
+ if (!url.isEmpty())
+ line += QString(" (%1)").arg(url);
+ }
+ if (extraData & Version)
+ {
+ auto ver = mod->version();
+ if (ver.isEmpty() && meta != nullptr)
+ ver = meta->version().toString();
+ if (!ver.isEmpty())
+ line += QString(" [%1]").arg(ver);
+ }
+ if (extraData & Authors && !mod->authors().isEmpty())
+ line += " by " + mod->authors().join(", ");
+ if (extraData & FileName)
+ line += QString(" (%1)").arg(mod->fileinfo().fileName());
+ lines << line;
+ }
+ return lines.join("\n");
+ }
+
+ QString toJSON(QList<Mod*> mods, OptionalData extraData)
+ {
+ QJsonArray lines;
+ for (auto mod : mods)
+ {
+ auto meta = mod->metadata();
+ auto modName = mod->name();
+ QJsonObject line;
+ line["name"] = modName;
+ if (extraData & Url)
+ {
+ auto url = mod->homepage();
+ if (!url.isEmpty())
+ line["url"] = url;
+ }
+ if (extraData & Version)
+ {
+ auto ver = mod->version();
+ if (ver.isEmpty() && meta != nullptr)
+ ver = meta->version().toString();
+ if (!ver.isEmpty())
+ line["version"] = ver;
+ }
+ if (extraData & Authors && !mod->authors().isEmpty())
+ line["authors"] = QJsonArray::fromStringList(mod->authors());
+ if (extraData & FileName)
+ line["filename"] = mod->fileinfo().fileName();
+ lines << line;
+ }
+ QJsonDocument doc;
+ doc.setArray(lines);
+ return doc.toJson();
+ }
+
+ QString toCSV(QList<Mod*> mods, OptionalData extraData)
+ {
+ QStringList lines;
+ for (auto mod : mods)
+ {
+ QStringList data;
+ auto meta = mod->metadata();
+ auto modName = mod->name();
+
+ data << modName;
+ if (extraData & Url)
+ data << mod->homepage();
+ if (extraData & Version)
+ {
+ auto ver = mod->version();
+ if (ver.isEmpty() && meta != nullptr)
+ ver = meta->version().toString();
+ data << ver;
+ }
+ if (extraData & Authors)
+ {
+ QString authors;
+ if (mod->authors().length() == 1)
+ authors = mod->authors().back();
+ else if (mod->authors().length() > 1)
+ authors = QString("\"%1\"").arg(mod->authors().join(","));
+ data << authors;
+ }
+ if (extraData & FileName)
+ data << mod->fileinfo().fileName();
+ lines << data.join(",");
+ }
+ return lines.join("\n");
+ }
+
+ QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData)
+ {
+ switch (format)
+ {
+ case HTML: return toHTML(mods, extraData);
+ case MARKDOWN: return toMarkdown(mods, extraData);
+ case PLAINTXT: return toPlainTXT(mods, extraData);
+ case JSON: return toJSON(mods, extraData);
+ case CSV: return toCSV(mods, extraData);
+ default:
+ {
+ return QString("unknown format:%1").arg(format);
+ }
+ }
+ }
+
+ QString exportToModList(QList<Mod*> mods, QString lineTemplate)
+ {
+ QStringList lines;
+ for (auto mod : mods)
+ {
+ auto meta = mod->metadata();
+ auto modName = mod->name();
+ auto modID = mod->mod_id();
+ auto url = mod->homepage();
+ auto ver = mod->version();
+ if (ver.isEmpty() && meta != nullptr)
+ ver = meta->version().toString();
+ auto authors = mod->authors().join(", ");
+ auto filename = mod->fileinfo().fileName();
+ lines << QString(lineTemplate)
+ .replace("{name}", modName)
+ .replace("{mod_id}", modID)
+ .replace("{url}", url)
+ .replace("{version}", ver)
+ .replace("{authors}", authors)
+ .replace("{filename}", filename);
+ }
+ return lines.join("\n");
+ }
+} // namespace ExportToModList \ No newline at end of file
diff --git a/archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.h b/archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.h
new file mode 100644
index 0000000000..5a1dd32826
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.h
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+#pragma once
+#include <QList>
+#include <QString>
+#include "minecraft/mod/Mod.hpp"
+
+namespace ExportToModList
+{
+
+ enum Formats
+ {
+ HTML,
+ MARKDOWN,
+ PLAINTXT,
+ JSON,
+ CSV,
+ CUSTOM
+ };
+ enum OptionalData
+ {
+ Authors = 1 << 0,
+ Url = 1 << 1,
+ Version = 1 << 2,
+ FileName = 1 << 3
+ };
+ QString exportToModList(QList<Mod*> mods, Formats format, OptionalData extraData);
+ QString exportToModList(QList<Mod*> mods, QString lineTemplate);
+} // namespace ExportToModList
diff --git a/archived/projt-launcher/launcher/modplatform/helpers/HashUtils.cpp b/archived/projt-launcher/launcher/modplatform/helpers/HashUtils.cpp
new file mode 100644
index 0000000000..2a36cc7d0d
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/helpers/HashUtils.cpp
@@ -0,0 +1,199 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "HashUtils.h"
+
+#include <QBuffer>
+#include <QDebug>
+#include <QFile>
+#include <QtConcurrentRun>
+
+#include <MurmurHash2.h>
+
+namespace Hashing
+{
+
+ Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider)
+ {
+ switch (provider)
+ {
+ case ModPlatform::ResourceProvider::MODRINTH:
+ return makeShared<Hasher>(
+ file_path,
+ ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first());
+ case ModPlatform::ResourceProvider::FLAME: return makeShared<Hasher>(file_path, Algorithm::Murmur2);
+ default:
+ qCritical() << "[Hashing]"
+ << "Unrecognized mod platform!";
+ return nullptr;
+ }
+ }
+
+ Hasher::Ptr createHasher(QString file_path, QString type)
+ {
+ return makeShared<Hasher>(file_path, type);
+ }
+
+ class QIODeviceReader : public Murmur2::Reader
+ {
+ public:
+ QIODeviceReader(QIODevice* device) : m_device(device)
+ {}
+ virtual ~QIODeviceReader() = default;
+ virtual int read(char* s, int n)
+ {
+ return m_device->read(s, n);
+ }
+ virtual bool eof()
+ {
+ return m_device->atEnd();
+ }
+ virtual void goToBeginning()
+ {
+ m_device->seek(0);
+ }
+ virtual void close()
+ {
+ m_device->close();
+ }
+
+ private:
+ QIODevice* m_device;
+ };
+
+ QString algorithmToString(Algorithm type)
+ {
+ switch (type)
+ {
+ case Algorithm::Md4: return "md4";
+ case Algorithm::Md5: return "md5";
+ case Algorithm::Sha1: return "sha1";
+ case Algorithm::Sha256: return "sha256";
+ case Algorithm::Sha512: return "sha512";
+ case Algorithm::Murmur2: return "murmur2";
+ // case Algorithm::Unknown:
+ default: break;
+ }
+ return "unknown";
+ }
+
+ Algorithm algorithmFromString(QString type)
+ {
+ if (type == "md4")
+ return Algorithm::Md4;
+ if (type == "md5")
+ return Algorithm::Md5;
+ if (type == "sha1")
+ return Algorithm::Sha1;
+ if (type == "sha256")
+ return Algorithm::Sha256;
+ if (type == "sha512")
+ return Algorithm::Sha512;
+ if (type == "murmur2")
+ return Algorithm::Murmur2;
+ return Algorithm::Unknown;
+ }
+
+ QString hash(QIODevice* device, Algorithm type)
+ {
+ if (!device->isOpen() && !device->open(QFile::ReadOnly))
+ return "";
+ QCryptographicHash::Algorithm alg = QCryptographicHash::Sha1;
+ switch (type)
+ {
+ case Algorithm::Md4: alg = QCryptographicHash::Algorithm::Md4; break;
+ case Algorithm::Md5: alg = QCryptographicHash::Algorithm::Md5; break;
+ case Algorithm::Sha1: alg = QCryptographicHash::Algorithm::Sha1; break;
+ case Algorithm::Sha256: alg = QCryptographicHash::Algorithm::Sha256; break;
+ case Algorithm::Sha512: alg = QCryptographicHash::Algorithm::Sha512; break;
+ case Algorithm::Murmur2:
+ { // CF-specific
+ auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); };
+ auto reader = std::make_unique<QIODeviceReader>(device);
+ auto result = QString::number(Murmur2::hash(reader.get(), 4 * MiB, should_filter_out));
+ device->close();
+ return result;
+ }
+ case Algorithm::Unknown: device->close(); return "";
+ }
+
+ QCryptographicHash hash(alg);
+ if (!hash.addData(device))
+ qCritical() << "Failed to read JAR to create hash!";
+
+ Q_ASSERT(hash.result().length() == hash.hashLength(alg));
+ auto result = hash.result().toHex();
+ device->close();
+ return result;
+ }
+
+ QString hash(QString fileName, Algorithm type)
+ {
+ QFile file(fileName);
+ return hash(&file, type);
+ }
+
+ QString hash(QByteArray data, Algorithm type)
+ {
+ QBuffer buff(&data);
+ return hash(&buff, type);
+ }
+
+ void Hasher::executeTask()
+ {
+ m_future = QtConcurrent::run(
+ QThreadPool::globalInstance(),
+ [](QString fileName, Algorithm type) { return hash(fileName, type); },
+ m_path,
+ m_alg);
+ connect(&m_watcher,
+ &QFutureWatcher<QString>::finished,
+ this,
+ [this]
+ {
+ if (m_future.isCanceled())
+ {
+ emitAborted();
+ }
+ else if (m_result = m_future.result(); m_result.isEmpty())
+ {
+ emitFailed("Empty hash!");
+ }
+ else
+ {
+ emitSucceeded();
+ emit resultsReady(m_result);
+ }
+ });
+ m_watcher.setFuture(m_future);
+ }
+
+ bool Hasher::abort()
+ {
+ if (m_future.isRunning())
+ {
+ m_future.cancel();
+ // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually
+ // cancels, which may not occur immediately.
+ return true;
+ }
+ return false;
+ }
+} // namespace Hashing
diff --git a/archived/projt-launcher/launcher/modplatform/helpers/HashUtils.h b/archived/projt-launcher/launcher/modplatform/helpers/HashUtils.h
new file mode 100644
index 0000000000..571fbc4d1a
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/helpers/HashUtils.h
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QCryptographicHash>
+#include <QFuture>
+#include <QFutureWatcher>
+#include <QString>
+
+#include "modplatform/ModIndex.h"
+#include "tasks/Task.h"
+
+namespace Hashing
+{
+
+ enum class Algorithm
+ {
+ Md4,
+ Md5,
+ Sha1,
+ Sha256,
+ Sha512,
+ Murmur2,
+ Unknown
+ };
+
+ QString algorithmToString(Algorithm type);
+ Algorithm algorithmFromString(QString type);
+ QString hash(QIODevice* device, Algorithm type);
+ QString hash(QString fileName, Algorithm type);
+ QString hash(QByteArray data, Algorithm type);
+
+ class Hasher : public Task
+ {
+ Q_OBJECT
+ public:
+ using Ptr = shared_qobject_ptr<Hasher>;
+
+ Hasher(QString file_path, Algorithm alg) : m_path(file_path), m_alg(alg)
+ {}
+ Hasher(QString file_path, QString alg) : Hasher(file_path, algorithmFromString(alg))
+ {}
+
+ bool abort() override;
+
+ void executeTask() override;
+
+ QString getResult() const
+ {
+ return m_result;
+ };
+ QString getPath() const
+ {
+ return m_path;
+ };
+
+ signals:
+ void resultsReady(QString hash);
+
+ private:
+ QString m_result;
+ QString m_path;
+ Algorithm m_alg;
+
+ QFuture<QString> m_future;
+ QFutureWatcher<QString> m_watcher;
+ };
+
+ Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider);
+ Hasher::Ptr createHasher(QString file_path, QString type);
+
+} // namespace Hashing
diff --git a/archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.cpp b/archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.cpp
new file mode 100644
index 0000000000..3a833cecd3
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.cpp
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "OverrideUtils.h"
+
+#include <QDirIterator>
+
+#include "FileSystem.h"
+
+namespace Override
+{
+
+ void createOverrides(const QString& name, const QString& parent_folder, const QString& override_path)
+ {
+ QString file_path(FS::PathCombine(parent_folder, name + ".txt"));
+ if (QFile::exists(file_path))
+ FS::deletePath(file_path);
+
+ FS::ensureFilePathExists(file_path);
+
+ QFile file(file_path);
+ if (!file.open(QFile::WriteOnly))
+ {
+ qWarning() << "Failed to open file '" << file.fileName() << "' for writing!";
+ return;
+ }
+
+ QDirIterator override_iterator(override_path, QDirIterator::Subdirectories);
+ while (override_iterator.hasNext())
+ {
+ auto override_file_path = override_iterator.next();
+ QFileInfo info(override_file_path);
+ if (info.isFile())
+ {
+ // Absolute path with temp directory -> relative path
+ override_file_path = override_file_path.split(name).last().remove(0, 1);
+
+ file.write(override_file_path.toUtf8());
+ file.write("\n");
+ }
+ }
+
+ file.close();
+ }
+
+ QStringList readOverrides(const QString& name, const QString& parent_folder)
+ {
+ QString file_path(FS::PathCombine(parent_folder, name + ".txt"));
+
+ QFile file(file_path);
+ if (!file.exists())
+ return {};
+
+ QStringList previous_overrides;
+
+ if (!file.open(QFile::ReadOnly))
+ {
+ qWarning() << "Failed to open file '" << file.fileName() << "' for reading!";
+ return previous_overrides;
+ }
+
+ QString entry;
+ do
+ {
+ entry = file.readLine();
+ previous_overrides.append(entry.trimmed());
+ }
+ while (!entry.isEmpty());
+
+ file.close();
+
+ return previous_overrides;
+ }
+
+} // namespace Override
diff --git a/archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.h b/archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.h
new file mode 100644
index 0000000000..555a42d181
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.h
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QString>
+
+namespace Override
+{
+
+ /** This creates a file in `parent_folder` that holds information about which
+ * overrides are in `override_path`.
+ *
+ * If there's already an existing such file, it will be ovewritten.
+ */
+ void createOverrides(const QString& name, const QString& parent_folder, const QString& override_path);
+
+ /** This reads an existing overrides archive, returning a list of overrides.
+ *
+ * If there's no such file in `parent_folder`, it will return an empty list.
+ */
+ QStringList readOverrides(const QString& name, const QString& parent_folder);
+
+} // namespace Override
diff --git a/archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.cpp b/archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.cpp
new file mode 100644
index 0000000000..42726e4a81
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.cpp
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "modplatform/import_ftb/PackHelpers.h"
+
+#include <QIcon>
+#include <QImageReader>
+#include <QString>
+#include <QVariant>
+
+#include "FileSystem.h"
+#include "Json.h"
+
+namespace FTBImportAPP
+{
+
+ QIcon loadFTBIcon(const QString& imagePath)
+ {
+ // Map of type byte to image type string
+ static const QHash<char, QByteArray> imageTypeMap = { { 0x00, "png" },
+ { 0x01, "jpg" },
+ { 0x02, "gif" },
+ { 0x03, "webp" } };
+ QFile file(imagePath);
+ if (!file.exists() || !file.open(QIODevice::ReadOnly))
+ {
+ return QIcon();
+ }
+ char type;
+ if (!file.getChar(&type))
+ {
+ qDebug() << "Missing FTB image type header at" << imagePath;
+ return QIcon();
+ }
+ if (!imageTypeMap.contains(type))
+ {
+ qDebug().nospace().noquote() << "Don't recognize FTB image type 0x" << QString::number(type, 16);
+ return QIcon();
+ }
+
+ auto imageType = imageTypeMap[type];
+ // Extract actual image data beyond the first byte
+ QImageReader reader(&file, imageType);
+ auto pixmap = QPixmap::fromImageReader(&reader);
+ if (pixmap.isNull())
+ {
+ qDebug() << "The FTB image at" << imagePath << "is not valid";
+ return QIcon();
+ }
+ return QIcon(pixmap);
+ }
+
+ Modpack parseDirectory(QString path)
+ {
+ Modpack modpack{ path };
+ auto instanceFile = QFileInfo(FS::PathCombine(path, "instance.json"));
+ if (!instanceFile.exists() || !instanceFile.isFile())
+ return {};
+ try
+ {
+ auto doc = Json::requireDocument(instanceFile.absoluteFilePath(), "FTB_APP instance JSON file");
+ const auto root = doc.object();
+ modpack.uuid = Json::requireString(root, "uuid", "uuid");
+ modpack.id = Json::requireInteger(root, "id", "id");
+ modpack.versionId = Json::requireInteger(root, "versionId", "versionId");
+ modpack.name = Json::requireString(root, "name", "name");
+ modpack.version = Json::requireString(root, "version", "version");
+ modpack.mcVersion = Json::requireString(root, "mcVersion", "mcVersion");
+ modpack.jvmArgs = Json::ensureVariant(root, "jvmArgs", {}, "jvmArgs");
+ modpack.totalPlayTime = Json::requireInteger(root, "totalPlayTime", "totalPlayTime");
+ }
+ catch (const Exception& e)
+ {
+ qDebug() << "Couldn't load ftb instance json: " << e.cause();
+ return {};
+ }
+
+ auto versionsFile = QFileInfo(FS::PathCombine(path, ".ftbapp", "version.json"));
+ if (!versionsFile.exists() || !versionsFile.isFile())
+ {
+ versionsFile = QFileInfo(FS::PathCombine(path, "version.json"));
+ }
+ if (!versionsFile.exists() || !versionsFile.isFile())
+ {
+ return {};
+ }
+ try
+ {
+ auto doc = Json::requireDocument(versionsFile.absoluteFilePath(), "FTB_APP version JSON file");
+ const auto root = doc.object();
+ auto targets = Json::requireArray(root, "targets", "targets");
+
+ for (auto target : targets)
+ {
+ auto obj = Json::requireObject(target, "target");
+ auto name = Json::requireString(obj, "name", "name");
+ auto version = Json::requireString(obj, "version", "version");
+ if (name == "neoforge")
+ {
+ modpack.loaderType = ModPlatform::NeoForge;
+ modpack.version = version;
+ break;
+ }
+ else if (name == "forge")
+ {
+ modpack.loaderType = ModPlatform::Forge;
+ modpack.version = version;
+ break;
+ }
+ else if (name == "fabric")
+ {
+ modpack.loaderType = ModPlatform::Fabric;
+ modpack.version = version;
+ break;
+ }
+ else if (name == "quilt")
+ {
+ modpack.loaderType = ModPlatform::Quilt;
+ modpack.version = version;
+ break;
+ }
+ }
+ }
+ catch (const Exception& e)
+ {
+ qDebug() << "Couldn't load ftb version json: " << e.cause();
+ return {};
+ }
+ auto iconFile = QFileInfo(FS::PathCombine(path, "folder.jpg"));
+ if (iconFile.exists() && iconFile.isFile())
+ {
+ modpack.icon = QIcon(iconFile.absoluteFilePath());
+ }
+ else
+ { // the logo is a file that the first bit denotes the image tipe followed by the actual image data
+ modpack.icon = loadFTBIcon(FS::PathCombine(path, ".ftbapp", "logo"));
+ }
+ return modpack;
+ }
+
+} // namespace FTBImportAPP
diff --git a/archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.h b/archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.h
new file mode 100644
index 0000000000..47d7f83b22
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.h
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+#pragma once
+
+#include <QIcon>
+#include <QList>
+#include <QMetaType>
+#include <QString>
+#include <QVariant>
+#include "modplatform/ResourceAPI.h"
+
+namespace FTBImportAPP
+{
+
+ struct Modpack
+ {
+ QString path;
+
+ // json data
+ QString uuid;
+ int id;
+ int versionId;
+ QString name;
+ QString version;
+ QString mcVersion;
+ int totalPlayTime;
+ // not needed for instance creation
+ QVariant jvmArgs;
+
+ std::optional<ModPlatform::ModLoaderType> loaderType;
+ QString loaderVersion;
+
+ QIcon icon;
+ };
+
+ using ModpackList = QList<Modpack>;
+
+ Modpack parseDirectory(QString path);
+
+} // namespace FTBImportAPP
+
+// We need it for the proxy model
+Q_DECLARE_METATYPE(FTBImportAPP::Modpack)
diff --git a/archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.cpp b/archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.cpp
new file mode 100644
index 0000000000..42e60a4ebb
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.cpp
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "PackInstallTask.h"
+
+#include <QtConcurrent>
+
+#include "BaseInstance.h"
+#include "FileSystem.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "modplatform/ResourceAPI.h"
+#include "modplatform/import_ftb/PackHelpers.h"
+#include "settings/INISettingsObject.h"
+
+namespace FTBImportAPP
+{
+
+ void PackInstallTask::executeTask()
+ {
+ setStatus(tr("Copying files..."));
+ setAbortable(false);
+ progress(1, 2);
+
+ m_copyFuture =
+ QtConcurrent::run(QThreadPool::globalInstance(),
+ [this]
+ {
+ FS::copy folderCopy(m_pack.path, FS::PathCombine(m_stagingPath, "minecraft"));
+ folderCopy.followSymlinks(true);
+ return folderCopy();
+ });
+ connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &PackInstallTask::copySettings);
+ connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &PackInstallTask::emitAborted);
+ m_copyFutureWatcher.setFuture(m_copyFuture);
+ }
+
+ void PackInstallTask::copySettings()
+ {
+ setStatus(tr("Copying settings..."));
+ progress(2, 2);
+ QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath);
+ instanceSettings->suspendSave();
+ MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
+ instance.settings()->set("InstanceType", "OneSix");
+ instance.settings()->set("totalTimePlayed", m_pack.totalPlayTime / 1000);
+
+ if (m_pack.jvmArgs.isValid() && !m_pack.jvmArgs.toString().isEmpty())
+ {
+ instance.settings()->set("OverrideJavaArgs", true);
+ instance.settings()->set("JvmArgs", m_pack.jvmArgs.toString());
+ }
+
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+ components->setComponentVersion("net.minecraft", m_pack.mcVersion, true);
+
+ auto modloader = m_pack.loaderType;
+ if (modloader.has_value())
+ switch (modloader.value())
+ {
+ case ModPlatform::NeoForge:
+ {
+ components->setComponentVersion("net.neoforged", m_pack.version, true);
+ break;
+ }
+ case ModPlatform::Forge:
+ {
+ components->setComponentVersion("net.minecraftforge", m_pack.version, true);
+ break;
+ }
+ case ModPlatform::Fabric:
+ {
+ components->setComponentVersion("net.fabricmc.fabric-loader", m_pack.version, true);
+ break;
+ }
+ case ModPlatform::Quilt:
+ {
+ components->setComponentVersion("org.quiltmc.quilt-loader", m_pack.version, true);
+ break;
+ }
+ case ModPlatform::Cauldron: break;
+ case ModPlatform::LiteLoader: break;
+ case ModPlatform::DataPack: break;
+ case ModPlatform::Babric: break;
+ case ModPlatform::BTA: break;
+ case ModPlatform::LegacyFabric: break;
+ case ModPlatform::Ornithe: break;
+ case ModPlatform::Rift: break;
+ case ModPlatform::Risugami: break;
+ case ModPlatform::StationLoader: break;
+ case ModPlatform::ModLoaderMP: break;
+ case ModPlatform::Optifine: break;
+ }
+ components->saveNow();
+
+ instance.setName(name());
+ if (m_instIcon == "default")
+ m_instIcon = "ftb_logo";
+ instance.setIconKey(m_instIcon);
+ instanceSettings->resumeSave();
+
+ emitSucceeded();
+ }
+
+} // namespace FTBImportAPP
diff --git a/archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.h b/archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.h
new file mode 100644
index 0000000000..f58b0e7a3c
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.h
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QFuture>
+#include <QFutureWatcher>
+
+#include "InstanceTask.h"
+#include "PackHelpers.h"
+
+namespace FTBImportAPP
+{
+
+ class PackInstallTask : public InstanceTask
+ {
+ Q_OBJECT
+
+ public:
+ explicit PackInstallTask(const Modpack& pack) : m_pack(pack)
+ {}
+ virtual ~PackInstallTask() = default;
+
+ protected:
+ virtual void executeTask() override;
+
+ private slots:
+ void copySettings();
+
+ private:
+ QFuture<bool> m_copyFuture;
+ QFutureWatcher<bool> m_copyFutureWatcher;
+
+ const Modpack m_pack;
+ };
+
+} // namespace FTBImportAPP
diff --git a/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
new file mode 100644
index 0000000000..5077a10d3b
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
@@ -0,0 +1,258 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "PackFetchTask.h"
+#include "PrivatePackManager.h"
+
+#include <QDomDocument>
+#include "Application.h"
+#include "BuildConfig.h"
+
+#include "net/ApiDownload.h"
+
+namespace LegacyFTB
+{
+
+ void PackFetchTask::fetch()
+ {
+ publicPacks.clear();
+ thirdPartyPacks.clear();
+
+ jobPtr.reset(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::ApiDownload::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));
+
+ connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished);
+ connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed);
+ connect(jobPtr.get(), &NetJob::aborted, this, &PackFetchTask::fileDownloadAborted);
+
+ jobPtr->start();
+ }
+
+ void PackFetchTask::fetchPrivate(const QStringList& toFetch)
+ {
+ QString privatePackBaseUrl = BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml";
+
+ for (auto& packCode : toFetch)
+ {
+ auto data = std::make_shared<QByteArray>();
+ NetJob* job = new NetJob("Fetching private pack", m_network);
+ job->addNetAction(Net::ApiDownload::makeByteArray(privatePackBaseUrl.arg(packCode), data));
+ job->setAskRetry(false);
+
+ connect(job,
+ &NetJob::succeeded,
+ this,
+ [this, job, data, packCode]
+ {
+ ModpackList packs;
+ parseAndAddPacks(*data, PackType::Private, packs);
+ for (auto& currentPack : packs)
+ {
+ currentPack.packCode = packCode;
+ emit privateFileDownloadFinished(currentPack);
+ }
+
+ job->deleteLater();
+
+ data->clear();
+ });
+
+ connect(job,
+ &NetJob::failed,
+ this,
+ [this, job, packCode, data](QString reason)
+ {
+ emit privateFileDownloadFailed(reason, packCode);
+ job->deleteLater();
+
+ data->clear();
+ });
+
+ connect(job,
+ &NetJob::aborted,
+ this,
+ [this, job, data]
+ {
+ emit aborted();
+ job->deleteLater();
+
+ data->clear();
+ });
+
+ 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);
+ }
+
+ void PackFetchTask::fileDownloadAborted()
+ {
+ emit aborted();
+ }
+
+} // namespace LegacyFTB
diff --git a/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.h b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.h
new file mode 100644
index 0000000000..1c40c6cc88
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.h
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QByteArray>
+#include <QObject>
+#include <QTemporaryDir>
+#include <memory>
+#include "PackHelpers.h"
+#include "net/NetJob.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;
+
+ std::shared_ptr<QByteArray> publicModpacksXmlFileData = std::make_shared<QByteArray>();
+ std::shared_ptr<QByteArray> thirdPartyModpacksXmlFileData = std::make_shared<QByteArray>();
+
+ bool parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list);
+ ModpackList publicPacks;
+ ModpackList thirdPartyPacks;
+
+ protected slots:
+ void fileDownloadFinished();
+ void fileDownloadFailed(QString reason);
+ void fileDownloadAborted();
+
+ signals:
+ void finished(ModpackList publicPacks, ModpackList thirdPartyPacks);
+ void failed(QString reason);
+ void aborted();
+
+ void privateFileDownloadFinished(const Modpack& modpack);
+ void privateFileDownloadFailed(QString reason, QString packCode);
+ };
+
+} // namespace LegacyFTB
diff --git a/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackHelpers.h b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackHelpers.h
new file mode 100644
index 0000000000..ac7cf75da3
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackHelpers.h
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QList>
+#include <QMetaType>
+#include <QString>
+#include <QStringList>
+
+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;
+ };
+
+ using ModpackList = QList<Modpack>;
+
+} // namespace LegacyFTB
+
+// We need it for the proxy model
+Q_DECLARE_METATYPE(LegacyFTB::Modpack)
diff --git a/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
new file mode 100644
index 0000000000..ec8472506b
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
@@ -0,0 +1,283 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "PackInstallTask.h"
+
+#include <QtConcurrent>
+
+#include "BaseInstance.h"
+#include "FileSystem.h"
+#include "MMCZip.h"
+#include "minecraft/GradleSpecifier.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "settings/INISettingsObject.h"
+
+#include "Application.h"
+#include "BuildConfig.h"
+
+#include "net/ApiDownload.h"
+
+namespace LegacyFTB
+{
+
+ PackInstallTask::PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network,
+ const 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));
+ setProgress(1, 4);
+ setAbortable(false);
+
+ auto path = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file);
+ auto entry = APPLICATION->metacache()->resolveEntry("FTBPacks", path);
+ entry->setStale(true);
+ archivePath = entry->getFullPath();
+ netJobContainer.reset(new NetJob("Download FTB Pack", m_network));
+ QString url;
+ if (m_pack.type == PackType::Private)
+ {
+ url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(path);
+ }
+ else
+ {
+ url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(path);
+ }
+ netJobContainer->addNetAction(Net::ApiDownload::makeCached(url, entry));
+
+ connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip);
+ connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed);
+ connect(netJobContainer.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress);
+ connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::emitAborted);
+
+ netJobContainer->start();
+
+ setAbortable(true);
+ progress(1, 4);
+ }
+
+ void PackInstallTask::unzip()
+ {
+ setStatus(tr("Extracting modpack"));
+ setAbortable(false);
+ progress(2, 4);
+
+ QDir extractDir(m_stagingPath);
+
+ m_packZip.reset(new QuaZip(archivePath));
+ if (!m_packZip->open(QuaZip::mdUnzip))
+ {
+ emitFailed(tr("Failed to open modpack file %1!").arg(archivePath));
+ return;
+ }
+
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(),
+ QOverload<QString, QString>::of(MMCZip::extractDir),
+ archivePath,
+ extractDir.absolutePath() + "/unzip");
+ 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()
+ {
+ setStatus(tr("Installing modpack"));
+ progress(3, 4);
+ QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
+ if (unzipMcDir.exists())
+ {
+ // ok, found minecraft dir, move contents to instance dir
+ if (!FS::move(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft"))
+ {
+ emitFailed(tr("Failed to move unpacked Minecraft!"));
+ return;
+ }
+ }
+
+ QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath);
+ instanceSettings->suspendSave();
+
+ 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))
+ {
+ QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll());
+ packJson.close();
+
+ // we only care about the libs
+ QJsonArray libs = doc.object().value("libraries").toArray();
+
+ for (const auto& 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;
+ }
+ }
+ else
+ {
+ qWarning() << "Failed to open file '" << packJson.fileName() << "' for reading!";
+ }
+ }
+
+ 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)
+ {
+ // Fallback to vanilla Minecraft if no modloader was detected
+ qWarning() << "No Forge version or jarmods found, creating vanilla instance";
+ // Components already has Minecraft version set, no need to fail
+ // Just continue with vanilla Minecraft
+ fallback = false;
+ }
+
+ components->saveNow();
+
+ progress(4, 4);
+
+ instance.setName(name());
+ if (m_instIcon == "default")
+ {
+ m_instIcon = "ftb_logo";
+ }
+ instance.setIconKey(m_instIcon);
+ instanceSettings->resumeSave();
+
+ emitSucceeded();
+ }
+
+ bool PackInstallTask::abort()
+ {
+ if (!canAbort())
+ {
+ return false;
+ }
+
+ netJobContainer->abort();
+ return InstanceTask::abort();
+ }
+
+} // namespace LegacyFTB
diff --git a/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.h b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.h
new file mode 100644
index 0000000000..07575fa483
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.h
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+#include <quazip/quazip.h>
+#include <quazip/quazipdir.h>
+#include "InstanceTask.h"
+#include "PackHelpers.h"
+#include "meta/Index.hpp"
+#include "meta/Version.hpp"
+#include "meta/VersionList.hpp"
+#include "net/NetJob.h"
+
+#include "net/NetJob.h"
+
+#include <optional>
+
+namespace LegacyFTB
+{
+
+ class PackInstallTask : public InstanceTask
+ {
+ Q_OBJECT
+
+ public:
+ explicit PackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network,
+ const 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 onUnzipFinished();
+ void onUnzipCanceled();
+
+ private: /* data */
+ shared_qobject_ptr<QNetworkAccessManager> m_network;
+ bool abortable = false;
+ std::unique_ptr<QuaZip> m_packZip;
+ QFuture<std::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
+ NetJob::Ptr netJobContainer;
+ QString archivePath;
+
+ Modpack m_pack;
+ QString m_version;
+ };
+
+} // namespace LegacyFTB
diff --git a/archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp
new file mode 100644
index 0000000000..80d5906e84
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 "PrivatePackManager.h"
+
+#include <QDebug>
+
+#include "FileSystem.h"
+
+namespace LegacyFTB
+{
+
+ void PrivatePackManager::load()
+ {
+ try
+ {
+ auto foo = QString::fromUtf8(FS::read(m_filename)).split('\n', Qt::SkipEmptyParts);
+ currentPacks = QSet<QString>(foo.begin(), foo.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/archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.h b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.h
new file mode 100644
index 0000000000..918d1e98bd
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.h
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <QFile>
+#include <QSet>
+#include <QString>
+
+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/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.cpp b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.cpp
new file mode 100644
index 0000000000..f19889a66c
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.cpp
@@ -0,0 +1,207 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ *
+ *
+ * ======================================================================== */
+
+#include "ModrinthAPI.h"
+
+#include "Application.h"
+#include "Json.h"
+#include "net/ApiDownload.h"
+#include "net/ApiUpload.h"
+#include "net/NetJob.h"
+#include "net/Upload.h"
+
+Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, std::shared_ptr<QByteArray> response)
+{
+ auto netJob = makeShared<NetJob>(QString("Modrinth::GetCurrentVersion"), APPLICATION->network());
+
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(
+ QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format),
+ response));
+
+ return netJob;
+}
+
+Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes,
+ QString hash_format,
+ std::shared_ptr<QByteArray> response)
+{
+ auto netJob = makeShared<NetJob>(QString("Modrinth::GetCurrentVersions"), APPLICATION->network());
+
+ QJsonObject body_obj;
+
+ Json::writeStringList(body_obj, "hashes", hashes);
+ Json::writeString(body_obj, "algorithm", hash_format);
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(
+ Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw));
+ netJob->setAskRetry(false);
+ return netJob;
+}
+
+Task::Ptr ModrinthAPI::latestVersion(QString hash,
+ QString hash_format,
+ std::optional<std::list<Version>> mcVersions,
+ std::optional<ModPlatform::ModLoaderTypes> loaders,
+ std::shared_ptr<QByteArray> response)
+{
+ auto netJob = makeShared<NetJob>(QString("Modrinth::GetLatestVersion"), APPLICATION->network());
+
+ QJsonObject body_obj;
+
+ if (loaders.has_value())
+ Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value()));
+
+ if (mcVersions.has_value())
+ {
+ QStringList game_versions;
+ for (auto& ver : mcVersions.value())
+ {
+ game_versions.append(mapMCVersionToModrinth(ver));
+ }
+ Json::writeStringList(body_obj, "game_versions", game_versions);
+ }
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::ApiUpload::makeByteArray(
+ QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format),
+ response,
+ body_raw));
+
+ return netJob;
+}
+
+Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes,
+ QString hash_format,
+ std::optional<std::list<Version>> mcVersions,
+ std::optional<ModPlatform::ModLoaderTypes> loaders,
+ std::shared_ptr<QByteArray> response)
+{
+ auto netJob = makeShared<NetJob>(QString("Modrinth::GetLatestVersions"), APPLICATION->network());
+
+ QJsonObject body_obj;
+
+ Json::writeStringList(body_obj, "hashes", hashes);
+ Json::writeString(body_obj, "algorithm", hash_format);
+
+ if (loaders.has_value())
+ Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value()));
+
+ if (mcVersions.has_value())
+ {
+ QStringList game_versions;
+ for (auto& ver : mcVersions.value())
+ {
+ game_versions.append(mapMCVersionToModrinth(ver));
+ }
+ Json::writeStringList(body_obj, "game_versions", game_versions);
+ }
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"),
+ response,
+ body_raw));
+
+ return netJob;
+}
+
+Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const
+{
+ auto netJob = makeShared<NetJob>(QString("Modrinth::GetProjects"), APPLICATION->network());
+ auto searchUrl = getMultipleModInfoURL(addonIds);
+
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response));
+
+ return netJob;
+}
+
+QList<ResourceAPI::SortingMethod> ModrinthAPI::getSortingMethods() const
+{
+ // https://docs.modrinth.com/api-spec/#tag/projects/operation/searchProjects
+ return { { 1, "relevance", QObject::tr("Sort by Relevance") },
+ { 2, "downloads", QObject::tr("Sort by Downloads") },
+ { 3, "follows", QObject::tr("Sort by Follows") },
+ { 4, "newest", QObject::tr("Sort by Newest") },
+ { 5, "updated", QObject::tr("Sort by Last Updated") } };
+}
+
+Task::Ptr ModrinthAPI::getModCategories(std::shared_ptr<QByteArray> response)
+{
+ auto netJob = makeShared<NetJob>(QString("Modrinth::GetCategories"), APPLICATION->network());
+ netJob->addNetAction(
+ Net::ApiDownload::makeByteArray(QUrl(BuildConfig.MODRINTH_PROD_URL + "/tag/category"), response));
+ QObject::connect(netJob.get(),
+ &Task::failed,
+ [](QString msg) { qDebug() << "Modrinth failed to get categories:" << msg; });
+ return netJob;
+}
+
+QList<ModPlatform::Category> ModrinthAPI::loadCategories(std::shared_ptr<QByteArray> response, QString projectType)
+{
+ QList<ModPlatform::Category> categories;
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from categories at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return categories;
+ }
+
+ try
+ {
+ auto arr = Json::requireArray(doc);
+
+ for (auto val : arr)
+ {
+ auto cat = Json::requireObject(val);
+ auto name = Json::requireString(cat, "name");
+ if (Json::ensureString(cat, "project_type", "") == projectType)
+ categories.push_back({ name, name });
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ qCritical() << "Failed to parse response from a version request.";
+ qCritical() << e.what();
+ qDebug() << doc;
+ }
+ return categories;
+}
+
+QList<ModPlatform::Category> ModrinthAPI::loadModCategories(std::shared_ptr<QByteArray> response)
+{
+ return loadCategories(response, "mod");
+};
diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.h b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.h
new file mode 100644
index 0000000000..e1637a7e8a
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -0,0 +1,291 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ *
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include "BuildConfig.h"
+#include "Json.h"
+#include "modplatform/ModIndex.h"
+#include "modplatform/ResourceAPI.h"
+#include "modplatform/modrinth/ModrinthPackIndex.h"
+
+#include <QDebug>
+
+class ModrinthAPI : public ResourceAPI
+{
+ public:
+ Task::Ptr currentVersion(QString hash, QString hash_format, std::shared_ptr<QByteArray> response);
+
+ Task::Ptr currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr<QByteArray> response);
+
+ Task::Ptr latestVersion(QString hash,
+ QString hash_format,
+ std::optional<std::list<Version>> mcVersions,
+ std::optional<ModPlatform::ModLoaderTypes> loaders,
+ std::shared_ptr<QByteArray> response);
+
+ Task::Ptr latestVersions(const QStringList& hashes,
+ QString hash_format,
+ std::optional<std::list<Version>> mcVersions,
+ std::optional<ModPlatform::ModLoaderTypes> loaders,
+ std::shared_ptr<QByteArray> response);
+
+ Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
+
+ static Task::Ptr getModCategories(std::shared_ptr<QByteArray> response);
+ static QList<ModPlatform::Category> loadCategories(std::shared_ptr<QByteArray> response, QString projectType);
+ static QList<ModPlatform::Category> loadModCategories(std::shared_ptr<QByteArray> response);
+
+ public:
+ auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
+
+ inline auto getAuthorURL(const QString& name) const -> QString
+ {
+ return "https://modrinth.com/user/" + name;
+ };
+
+ static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList
+ {
+ QStringList l;
+ for (auto loader : { ModPlatform::NeoForge,
+ ModPlatform::Forge,
+ ModPlatform::Fabric,
+ ModPlatform::Quilt,
+ ModPlatform::LiteLoader,
+ ModPlatform::DataPack,
+ ModPlatform::Babric,
+ ModPlatform::BTA,
+ ModPlatform::LegacyFabric,
+ ModPlatform::Ornithe,
+ ModPlatform::Rift,
+ ModPlatform::Risugami,
+ ModPlatform::StationLoader,
+ ModPlatform::ModLoaderMP,
+ ModPlatform::Optifine })
+ {
+ if (types & loader)
+ {
+ l << getModLoaderAsString(loader);
+ }
+ }
+ return l;
+ }
+
+ static auto getModLoaderFilters(ModPlatform::ModLoaderTypes types) -> const QString
+ {
+ QStringList l;
+ for (auto loader : getModLoaderStrings(types))
+ {
+ l << QString("\"categories:%1\"").arg(loader);
+ }
+ return l.join(',');
+ }
+
+ static auto getCategoriesFilters(QStringList categories) -> const QString
+ {
+ QStringList l;
+ for (auto cat : categories)
+ {
+ l << QString("\"categories:%1\"").arg(cat);
+ }
+ return l.join(',');
+ }
+
+ static QString getSideFilters(ModPlatform::Side side)
+ {
+ switch (side)
+ {
+ case ModPlatform::Side::ClientSide:
+ return QString("\"client_side:required\",\"client_side:optional\"],[\"server_side:optional\",\"server_"
+ "side:unsupported\"");
+ case ModPlatform::Side::ServerSide:
+ return QString("\"server_side:required\",\"server_side:optional\"],[\"client_side:optional\",\"client_"
+ "side:unsupported\"");
+ case ModPlatform::Side::UniversalSide:
+ return QString("\"client_side:required\"],[\"server_side:required\"");
+ case ModPlatform::Side::NoSide:
+ // fallthrough
+ default: return {};
+ }
+ }
+
+ static inline QString mapMCVersionFromModrinth(QString v)
+ {
+ static const QString preString = " Pre-Release ";
+ bool pre = false;
+ if (v.contains("-pre"))
+ {
+ pre = true;
+ v.replace("-pre", preString);
+ }
+ v.replace("-", " ");
+ if (pre)
+ {
+ v.replace(" Pre Release ", preString);
+ }
+ return v;
+ }
+
+ private:
+ static QString resourceTypeParameter(ModPlatform::ResourceType type)
+ {
+ switch (type)
+ {
+ case ModPlatform::ResourceType::Mod: return "mod";
+ case ModPlatform::ResourceType::ResourcePack: return "resourcepack";
+ case ModPlatform::ResourceType::ShaderPack: return "shader";
+ case ModPlatform::ResourceType::DataPack: return "datapack";
+ case ModPlatform::ResourceType::Modpack: return "modpack";
+ default: qWarning() << "Invalid resource type for Modrinth API!"; break;
+ }
+
+ return "";
+ }
+
+ QString createFacets(SearchArgs const& args) const
+ {
+ QStringList facets_list;
+
+ if (args.loaders.has_value() && args.loaders.value() != 0)
+ facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value())));
+ if (args.versions.has_value() && !args.versions.value().empty())
+ facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value())));
+ if (args.side.has_value())
+ {
+ auto side = getSideFilters(args.side.value());
+ if (!side.isEmpty())
+ facets_list.append(QString("[%1]").arg(side));
+ }
+ if (args.categoryIds.has_value() && !args.categoryIds->empty())
+ facets_list.append(QString("[%1]").arg(getCategoriesFilters(args.categoryIds.value())));
+ if (args.openSource)
+ facets_list.append("[\"open_source:true\"]");
+
+ facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type)));
+
+ return QString("[%1]").arg(facets_list.join(','));
+ }
+
+ public:
+ inline auto getSearchURL(SearchArgs const& args) const -> std::optional<QString> override
+ {
+ if (args.loaders.has_value() && args.loaders.value() != 0)
+ {
+ if (!validateModLoaders(args.loaders.value()))
+ {
+ qWarning() << "Modrinth - or our interface - does not support any the provided mod loaders!";
+ return {};
+ }
+ }
+
+ QStringList get_arguments;
+ get_arguments.append(QString("offset=%1").arg(args.offset));
+ get_arguments.append(QString("limit=25"));
+ if (args.search.has_value())
+ get_arguments.append(QString("query=%1").arg(args.search.value()));
+ if (args.sorting.has_value())
+ get_arguments.append(QString("index=%1").arg(args.sorting.value().name));
+ get_arguments.append(QString("facets=%1").arg(createFacets(args)));
+
+ return BuildConfig.MODRINTH_PROD_URL + "/search?" + get_arguments.join('&');
+ };
+
+ inline auto getInfoURL(QString const& id) const -> std::optional<QString> override
+ {
+ return BuildConfig.MODRINTH_PROD_URL + "/project/" + id;
+ };
+
+ inline auto getMultipleModInfoURL(QStringList ids) const -> QString
+ {
+ return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\""));
+ };
+
+ inline auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional<QString> override
+ {
+ QStringList get_arguments;
+ if (args.mcVersions.has_value())
+ get_arguments.append(QString("game_versions=[%1]").arg(getGameVersionsString(args.mcVersions.value())));
+ if (args.loaders.has_value())
+ get_arguments.append(
+ QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\"")));
+
+ return QString("%1/project/%2/version%3%4")
+ .arg(BuildConfig.MODRINTH_PROD_URL,
+ args.pack->addonId.toString(),
+ get_arguments.isEmpty() ? "" : "?",
+ get_arguments.join('&'));
+ };
+
+ QString getGameVersionsArray(std::list<Version> mcVersions) const
+ {
+ QString s;
+ for (auto& ver : mcVersions)
+ {
+ s += QString("\"versions:%1\",").arg(mapMCVersionToModrinth(ver));
+ }
+ s.remove(s.length() - 1, 1); // remove last comma
+ return s.isEmpty() ? QString() : s;
+ }
+
+ static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool
+ {
+ return loaders
+ & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt
+ | ModPlatform::LiteLoader | ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA
+ | ModPlatform::LegacyFabric | ModPlatform::Ornithe | ModPlatform::Rift | ModPlatform::Risugami
+ | ModPlatform::StationLoader | ModPlatform::ModLoaderMP | ModPlatform::Optifine);
+ }
+
+ std::optional<QString> getDependencyURL(DependencySearchArgs const& args) const override
+ {
+ return args.dependency.version.length() != 0
+ ? QString("%1/version/%2").arg(BuildConfig.MODRINTH_PROD_URL, args.dependency.version)
+ : QString("%1/project/%2/version?game_versions=[\"%3\"]&loaders=[\"%4\"]")
+ .arg(BuildConfig.MODRINTH_PROD_URL)
+ .arg(args.dependency.addonId.toString())
+ .arg(mapMCVersionToModrinth(args.mcVersion))
+ .arg(getModLoaderStrings(args.loader).join("\",\""));
+ };
+
+ QJsonArray documentToArray(QJsonDocument& obj) const override
+ {
+ return obj.object().value("hits").toArray();
+ }
+ void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override
+ {
+ Modrinth::loadIndexedPack(m, obj);
+ }
+ ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const override
+ {
+ return Modrinth::loadIndexedPackVersion(obj);
+ };
+ void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) const override
+ {
+ Modrinth::loadExtraPackData(m, obj);
+ }
+};
diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
new file mode 100644
index 0000000000..fdac618a44
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
@@ -0,0 +1,291 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "ModrinthCheckUpdate.h"
+#include "Application.h"
+#include "ModrinthAPI.h"
+#include "ModrinthPackIndex.h"
+
+#include "Json.h"
+
+#include "QObjectPtr.h"
+#include "ResourceDownloadTask.h"
+
+#include "modplatform/ModIndex.h"
+#include "modplatform/helpers/HashUtils.h"
+
+#include "tasks/ConcurrentTask.h"
+
+static ModrinthAPI api;
+
+ModrinthCheckUpdate::ModrinthCheckUpdate(QList<Resource*>& resources,
+ std::list<Version>& mcVersions,
+ QList<ModPlatform::ModLoaderType> loadersList,
+ std::shared_ptr<ResourceFolderModel> resourceModel)
+ : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(resourceModel)),
+ m_hashType(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first())
+{
+ if (!m_loadersList.isEmpty())
+ { // this is for mods so append all the other posible loaders to the initial list
+ m_initialSize = m_loadersList.length();
+ ModPlatform::ModLoaderTypes modLoaders;
+ for (auto m : resources)
+ {
+ modLoaders |= m->metadata()->loaders;
+ }
+ for (auto l : m_loadersList)
+ {
+ modLoaders &= ~l;
+ }
+ m_loadersList.append(ModPlatform::modLoaderTypesToList(modLoaders));
+ }
+}
+
+bool ModrinthCheckUpdate::abort()
+{
+ if (m_job)
+ return m_job->abort();
+ return true;
+}
+
+/* Check for update:
+ * - Get latest version available
+ * - Compare hash of the latest version with the current hash
+ * - If equal, no updates, else, there's updates, so add to the list
+ * */
+void ModrinthCheckUpdate::executeTask()
+{
+ setStatus(tr("Preparing resources for Modrinth..."));
+ setProgress(0, (m_loadersList.isEmpty() ? 1 : m_loadersList.length()) * 2 + 1);
+
+ auto hashing_task = makeShared<ConcurrentTask>("MakeModrinthHashesTask",
+ APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
+ bool startHasing = false;
+ for (auto* resource : m_resources)
+ {
+ auto hash = resource->metadata()->hash;
+
+ // Sadly the API can only handle one hash type per call, se we
+ // need to generate a new hash if the current one is innadequate
+ // (though it will rarely happen, if at all)
+ if (resource->metadata()->hash_format != m_hashType)
+ {
+ auto hash_task =
+ Hashing::createHasher(resource->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH);
+ connect(hash_task.get(),
+ &Hashing::Hasher::resultsReady,
+ [this, resource](QString hash) { m_mappings.insert(hash, resource); });
+ connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); });
+ hashing_task->addTask(hash_task);
+ startHasing = true;
+ }
+ else
+ {
+ m_mappings.insert(hash, resource);
+ }
+ }
+
+ if (startHasing)
+ {
+ connect(hashing_task.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader);
+ m_job = hashing_task;
+ hashing_task->start();
+ }
+ else
+ {
+ checkNextLoader();
+ }
+}
+
+void ModrinthCheckUpdate::getUpdateModsForLoader(std::optional<ModPlatform::ModLoaderTypes> loader,
+ bool forceModLoaderCheck)
+{
+ setStatus(tr("Waiting for the API response from Modrinth..."));
+ setProgress(m_progress + 1, m_progressTotal);
+
+ auto response = std::make_shared<QByteArray>();
+ QStringList hashes;
+ if (forceModLoaderCheck && loader.has_value())
+ {
+ for (auto hash : m_mappings.keys())
+ {
+ if (m_mappings[hash]->metadata()->loaders & loader.value())
+ {
+ hashes.append(hash);
+ }
+ }
+ }
+ else
+ {
+ hashes = m_mappings.keys();
+ }
+ auto job = api.latestVersions(hashes, m_hashType, m_gameVersions, loader, response);
+
+ connect(job.get(), &Task::succeeded, this, [this, response, loader] { checkVersionsResponse(response, loader); });
+
+ connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::checkNextLoader);
+
+ m_job = job;
+ m_loaderIdx++;
+ job->start();
+}
+
+void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr<QByteArray> response,
+ std::optional<ModPlatform::ModLoaderTypes> loader)
+{
+ setStatus(tr("Parsing the API response from Modrinth..."));
+ setProgress(m_progress + 1, m_progressTotal);
+
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ emitFailed(parse_error.errorString());
+ return;
+ }
+
+ try
+ {
+ auto iter = m_mappings.begin();
+
+ while (iter != m_mappings.end())
+ {
+ const QString hash = iter.key();
+ Resource* resource = iter.value();
+
+ auto project_obj = doc[hash].toObject();
+
+ // If the returned project is empty, but we have Modrinth metadata,
+ // it means this specific version is not available
+ if (project_obj.isEmpty())
+ {
+ qDebug() << "Mod " << m_mappings.find(hash).value()->name() << " got an empty response."
+ << "Hash: " << hash;
+ ++iter;
+ continue;
+ }
+
+ // Sometimes a version may have multiple files, one with "forge" and one with "fabric",
+ // so we may want to filter it
+ QString loader_filter;
+ if (loader.has_value() && loader != 0)
+ {
+ auto modLoaders = ModPlatform::modLoaderTypesToList(*loader);
+ if (!modLoaders.isEmpty())
+ {
+ loader_filter = ModPlatform::getModLoaderAsString(modLoaders.first());
+ }
+ }
+
+ // Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
+ // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of
+ // the loader_filter
+ // - The version reported by the JAR is different from the version reported by the indexed version (it's
+ // usually the case) Such is the pain of having arbitrary files for a given version .-.
+
+ auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, m_hashType, loader_filter);
+ if (project_ver.downloadUrl.isEmpty())
+ {
+ qCritical() << "Modrinth mod without download url!" << project_ver.fileName;
+ ++iter;
+ continue;
+ }
+
+ // Fake pack with the necessary info to pass to the download task :)
+ auto pack = std::make_shared<ModPlatform::IndexedPack>();
+ pack->name = resource->name();
+ pack->slug = resource->metadata()->slug;
+ pack->addonId = resource->metadata()->project_id;
+ pack->provider = ModPlatform::ResourceProvider::MODRINTH;
+ if ((project_ver.hash != hash && project_ver.is_preferred)
+ || (resource->status() == ResourceStatus::NOT_INSTALLED))
+ {
+ auto download_task = makeShared<ResourceDownloadTask>(pack, project_ver, m_resourceModel);
+
+ QString old_version = resource->metadata()->version_number;
+ if (old_version.isEmpty())
+ {
+ if (resource->status() == ResourceStatus::NOT_INSTALLED)
+ old_version = tr("Not installed");
+ else
+ old_version = tr("Unknown");
+ }
+
+ m_updates.emplace_back(pack->name,
+ hash,
+ old_version,
+ project_ver.version_number,
+ project_ver.version_type,
+ project_ver.changelog,
+ ModPlatform::ResourceProvider::MODRINTH,
+ download_task,
+ resource->enabled());
+ }
+ m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, project_ver));
+
+ iter = m_mappings.erase(iter);
+ }
+ }
+ catch (Json::JsonException& e)
+ {
+ emitFailed(e.cause() + ": " + e.what());
+ return;
+ }
+ checkNextLoader();
+}
+
+void ModrinthCheckUpdate::checkNextLoader()
+{
+ if (m_mappings.isEmpty())
+ {
+ emitSucceeded();
+ return;
+ }
+ if (m_loaderIdx < m_loadersList.size())
+ { // this are mods so check with loades
+ getUpdateModsForLoader(m_loadersList.at(m_loaderIdx), m_loaderIdx > m_initialSize);
+ return;
+ }
+ else if (m_loadersList.isEmpty() && m_loaderIdx == 0)
+ { // this are other resources no need to check more than once with empty loader
+ getUpdateModsForLoader();
+ return;
+ }
+
+ for (auto resource : m_mappings)
+ {
+ QString reason;
+
+ if (dynamic_cast<Mod*>(resource) != nullptr)
+ reason = tr("No valid version found for this resource. It's probably unavailable for the current game "
+ "version / mod loader.");
+ else
+ reason =
+ tr("No valid version found for this resource. It's probably unavailable for the current game version.");
+
+ emit checkFailed(resource, reason);
+ }
+
+ emitSucceeded();
+}
diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
new file mode 100644
index 0000000000..d6ab02d653
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include "modplatform/CheckUpdateTask.h"
+
+class ModrinthCheckUpdate : public CheckUpdateTask
+{
+ Q_OBJECT
+
+ public:
+ ModrinthCheckUpdate(QList<Resource*>& resources,
+ std::list<Version>& mcVersions,
+ QList<ModPlatform::ModLoaderType> loadersList,
+ std::shared_ptr<ResourceFolderModel> resourceModel);
+
+ public slots:
+ bool abort() override;
+
+ protected slots:
+ void executeTask() override;
+ void getUpdateModsForLoader(std::optional<ModPlatform::ModLoaderTypes> loader = {},
+ bool forceModLoaderCheck = false);
+ void checkVersionsResponse(std::shared_ptr<QByteArray> response, std::optional<ModPlatform::ModLoaderTypes> loader);
+ void checkNextLoader();
+
+ private:
+ Task::Ptr m_job = nullptr;
+ QHash<QString, Resource*> m_mappings;
+ QString m_hashType;
+ int m_loaderIdx = 0;
+ int m_initialSize = 0;
+};
diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.cpp b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.cpp
new file mode 100644
index 0000000000..23e5500fa1
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.cpp
@@ -0,0 +1,355 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ========================================================================
+ */
+
+#include "ModrinthCollectionImportTask.h"
+
+#include "Application.h"
+#include "Json.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
+#include "net/ApiDownload.h"
+#include "net/NetJob.h"
+
+#include <QJsonDocument>
+#include <QRegularExpression>
+#include <QSet>
+#include <QUrl>
+
+#include <utility>
+
+namespace
+{
+ constexpr int kProjectBatchSize = 50;
+}
+
+ModrinthCollectionImportTask::ModrinthCollectionImportTask(QString collection_reference, MinecraftInstance* instance)
+ : Task(false),
+ m_collection_reference(std::move(collection_reference).trimmed()),
+ m_instance(instance)
+{
+ if (m_instance)
+ {
+ auto pack_profile = m_instance->getPackProfile();
+ if (auto minecraft = pack_profile->getComponent("net.minecraft"); minecraft)
+ m_mc_versions.push_back(minecraft->getVersion());
+ m_loaders = pack_profile->getSupportedModLoaders().value_or(ModPlatform::ModLoaderTypes{});
+ }
+}
+
+bool ModrinthCollectionImportTask::abort()
+{
+ if (!canAbort())
+ return false;
+
+ if (m_current_task)
+ return m_current_task->abort();
+
+ emitAborted();
+ return true;
+}
+
+void ModrinthCollectionImportTask::executeTask()
+{
+ setAbortable(true);
+
+ if (!m_instance)
+ {
+ emitFailed(tr("Missing instance context for Modrinth collection import."));
+ return;
+ }
+
+ m_collection_reference = normalizeCollectionReference(m_collection_reference);
+ if (m_collection_reference.isEmpty())
+ {
+ emitFailed(tr("Enter a valid Modrinth collection URL or collection ID."));
+ return;
+ }
+
+ fetchCollectionPage();
+}
+
+QString ModrinthCollectionImportTask::normalizeCollectionReference(QString reference) const
+{
+ reference = reference.trimmed();
+ if (reference.isEmpty())
+ return {};
+
+ static const QRegularExpression collectionPathPattern(R"(^(?:/?collection/)?([^/?#]+))");
+
+ if (reference.startsWith("http://") || reference.startsWith("https://"))
+ {
+ QUrl url(reference);
+ if (!url.isValid())
+ return {};
+
+ const auto host = url.host();
+ if (host != "modrinth.com" && host != "www.modrinth.com")
+ return {};
+
+ auto match = collectionPathPattern.match(url.path().mid(1));
+ return match.hasMatch() ? match.captured(1) : QString{};
+ }
+
+ auto match = collectionPathPattern.match(reference);
+ return match.hasMatch() ? match.captured(1) : reference;
+}
+
+QString ModrinthCollectionImportTask::collectionUrl() const
+{
+ return QString("https://modrinth.com/collection/%1")
+ .arg(QString::fromUtf8(QUrl::toPercentEncoding(m_collection_reference)));
+}
+
+bool ModrinthCollectionImportTask::extractCollectionMetadata(const QByteArray& html)
+{
+ QString text = QString::fromUtf8(html);
+
+ static const QRegularExpression titlePattern(R"(<title>([^<]+)</title>)");
+ if (auto match = titlePattern.match(text); match.hasMatch())
+ {
+ m_collection_name = match.captured(1).trimmed();
+ const QString suffix = QStringLiteral(" - Modrinth");
+ if (m_collection_name.endsWith(suffix))
+ m_collection_name.chop(suffix.size());
+ }
+
+ QStringList best_ids;
+ static const QRegularExpression projectsPattern(R"(projects\?ids=%5B([^"]+)%5D)");
+ auto match_it = projectsPattern.globalMatch(text);
+ while (match_it.hasNext())
+ {
+ auto match = match_it.next();
+ QStringList ids;
+ auto decoded = QUrl::fromPercentEncoding(match.captured(1).toUtf8());
+
+ static const QRegularExpression idPattern(QStringLiteral("\"([A-Za-z0-9]+)\""));
+ auto id_it = idPattern.globalMatch(decoded);
+ QSet<QString> seen;
+ while (id_it.hasNext())
+ {
+ auto id_match = id_it.next();
+ auto id = id_match.captured(1);
+ if (!seen.contains(id))
+ {
+ seen.insert(id);
+ ids.append(id);
+ }
+ }
+
+ if (ids.size() > best_ids.size())
+ best_ids = ids;
+ }
+
+ m_project_ids = best_ids;
+ return !m_project_ids.isEmpty();
+}
+
+void ModrinthCollectionImportTask::fetchCollectionPage()
+{
+ setStatus(tr("Loading Modrinth collection page..."));
+ setProgress(0, 1);
+
+ auto response = std::make_shared<QByteArray>();
+ auto job = makeShared<NetJob>(tr("Modrinth collection page"), APPLICATION->network());
+ job->addNetAction(Net::ApiDownload::makeByteArray(QUrl(collectionUrl()), response));
+
+ connect(job.get(),
+ &NetJob::succeeded,
+ this,
+ [this, response]
+ {
+ if (!extractCollectionMetadata(*response))
+ {
+ emitFailed(tr("Could not extract any projects from this Modrinth collection page."));
+ return;
+ }
+
+ const int project_batches = (m_project_ids.size() + kProjectBatchSize - 1) / kProjectBatchSize;
+ setProgress(1, 1 + project_batches + m_project_ids.size());
+ QMetaObject::invokeMethod(this, &ModrinthCollectionImportTask::fetchProjectBatch, Qt::QueuedConnection);
+ });
+ connect(job.get(), &NetJob::failed, this, &ModrinthCollectionImportTask::emitFailed);
+ connect(job.get(), &NetJob::aborted, this, &ModrinthCollectionImportTask::emitAborted);
+ connect(job.get(), &NetJob::stepProgress, this, &ModrinthCollectionImportTask::propagateStepProgress);
+
+ m_current_task = job;
+ job->start();
+}
+
+void ModrinthCollectionImportTask::fetchProjectBatch()
+{
+ if (m_project_batch_offset >= m_project_ids.size())
+ {
+ QHash<QString, ModPlatform::IndexedPack::Ptr> pack_lookup;
+ for (auto& pack : std::as_const(m_packs))
+ pack_lookup.insert(pack->addonId.toString(), pack);
+
+ QList<ModPlatform::IndexedPack::Ptr> ordered_packs;
+ for (auto& id : std::as_const(m_project_ids))
+ {
+ if (pack_lookup.contains(id))
+ ordered_packs.append(pack_lookup.value(id));
+ }
+ m_packs = ordered_packs;
+
+ if (m_packs.isEmpty())
+ {
+ emitFailed(
+ tr("This collection does not contain any projects that could be resolved through the Modrinth API."));
+ return;
+ }
+
+ QMetaObject::invokeMethod(this, &ModrinthCollectionImportTask::fetchNextVersion, Qt::QueuedConnection);
+ return;
+ }
+
+ setStatus(tr("Loading collection projects..."));
+
+ auto response = std::make_shared<QByteArray>();
+ const auto batch = m_project_ids.mid(m_project_batch_offset, kProjectBatchSize);
+ auto job = m_api.getProjects(batch, response);
+ if (!job)
+ {
+ emitFailed(tr("Failed to request Modrinth project metadata for this collection."));
+ return;
+ }
+
+ connect(job.get(),
+ &Task::succeeded,
+ this,
+ [this, response, batch]
+ {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ emitFailed(tr("Failed to parse Modrinth project metadata for this collection."));
+ return;
+ }
+
+ if (!doc.isArray())
+ {
+ emitFailed(tr("Modrinth returned an unexpected project list for this collection."));
+ return;
+ }
+
+ QSet<QString> seen_in_batch;
+ for (auto entry : doc.array())
+ {
+ try
+ {
+ auto obj = Json::requireObject(entry);
+ auto pack = std::make_shared<ModPlatform::IndexedPack>();
+ m_api.loadIndexedPack(*pack, obj);
+ m_api.loadExtraPackInfo(*pack, obj);
+ if (!seen_in_batch.contains(pack->addonId.toString()))
+ {
+ m_packs.append(pack);
+ seen_in_batch.insert(pack->addonId.toString());
+ }
+ }
+ catch (const JSONValidationError&)
+ {
+ continue;
+ }
+ }
+
+ m_project_batch_offset += batch.size();
+ setProgress(getProgress() + 1, getTotalProgress());
+ QMetaObject::invokeMethod(this, &ModrinthCollectionImportTask::fetchProjectBatch, Qt::QueuedConnection);
+ });
+ connect(job.get(), &Task::failed, this, &ModrinthCollectionImportTask::emitFailed);
+ connect(job.get(), &Task::aborted, this, &ModrinthCollectionImportTask::emitAborted);
+ connect(job.get(), &Task::stepProgress, this, &ModrinthCollectionImportTask::propagateStepProgress);
+
+ m_current_task = job;
+ job->start();
+}
+
+void ModrinthCollectionImportTask::fetchNextVersion()
+{
+ if (m_version_index >= m_packs.size())
+ {
+ finishImport();
+ return;
+ }
+
+ auto pack = m_packs.at(m_version_index);
+ setStatus(tr("Resolving a compatible version for %1...").arg(pack->name));
+
+ ResourceAPI::Callback<QVector<ModPlatform::IndexedVersion>> callbacks;
+ callbacks.on_succeed = [this, pack](auto& versions)
+ {
+ if (!versions.isEmpty())
+ m_imported_resources.append({ pack, versions.first() });
+ else
+ m_skipped_resources.append(pack->name);
+
+ ++m_version_index;
+ setProgress(getProgress() + 1, getTotalProgress());
+ QMetaObject::invokeMethod(this, &ModrinthCollectionImportTask::fetchNextVersion, Qt::QueuedConnection);
+ };
+ callbacks.on_fail = [this, pack](QString const&, int)
+ {
+ m_skipped_resources.append(pack->name);
+ ++m_version_index;
+ setProgress(getProgress() + 1, getTotalProgress());
+ QMetaObject::invokeMethod(this, &ModrinthCollectionImportTask::fetchNextVersion, Qt::QueuedConnection);
+ };
+ callbacks.on_abort = [this] { emitAborted(); };
+
+ ResourceAPI::VersionSearchArgs args{ pack, m_mc_versions, m_loaders, ModPlatform::ResourceType::Mod };
+ auto job = m_api.getProjectVersions(std::move(args), std::move(callbacks));
+ if (!job)
+ {
+ m_skipped_resources.append(pack->name);
+ ++m_version_index;
+ setProgress(getProgress() + 1, getTotalProgress());
+ QMetaObject::invokeMethod(this, &ModrinthCollectionImportTask::fetchNextVersion, Qt::QueuedConnection);
+ return;
+ }
+
+ connect(job.get(), &Task::stepProgress, this, &ModrinthCollectionImportTask::propagateStepProgress);
+
+ m_current_task = job;
+ job->start();
+}
+
+void ModrinthCollectionImportTask::finishImport()
+{
+ if (m_imported_resources.isEmpty())
+ {
+ emitFailed(tr("No compatible mods were found in this collection for the current instance."));
+ return;
+ }
+
+ if (!m_skipped_resources.isEmpty())
+ {
+ logWarning(tr("Skipped %1 project(s) without a compatible version: %2")
+ .arg(m_skipped_resources.size())
+ .arg(m_skipped_resources.join(", ")));
+ }
+
+ emitSucceeded();
+}
diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.h b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.h
new file mode 100644
index 0000000000..06618f475c
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.h
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ========================================================================
+ */
+
+#pragma once
+
+#include "Version.h"
+#include "modplatform/ModIndex.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
+#include "tasks/Task.h"
+
+#include <QList>
+#include <QString>
+
+#include <list>
+
+class MinecraftInstance;
+
+class ModrinthCollectionImportTask final : public Task
+{
+ Q_OBJECT
+
+ public:
+ struct ImportedResource
+ {
+ ModPlatform::IndexedPack::Ptr pack;
+ ModPlatform::IndexedVersion version;
+ };
+
+ explicit ModrinthCollectionImportTask(QString collection_reference, MinecraftInstance* instance);
+
+ bool abort() override;
+
+ QString collectionName() const
+ {
+ return m_collection_name;
+ }
+
+ QList<ImportedResource> importedResources() const
+ {
+ return m_imported_resources;
+ }
+
+ QStringList skippedResources() const
+ {
+ return m_skipped_resources;
+ }
+
+ protected:
+ void executeTask() override;
+
+ private:
+ QString normalizeCollectionReference(QString reference) const;
+ QString collectionUrl() const;
+ bool extractCollectionMetadata(const QByteArray& html);
+ void fetchCollectionPage();
+ void fetchProjectBatch();
+ void fetchNextVersion();
+ void finishImport();
+
+ private:
+ QString m_collection_reference;
+ QString m_collection_name;
+ MinecraftInstance* m_instance = nullptr;
+
+ std::list<Version> m_mc_versions;
+ ModPlatform::ModLoaderTypes m_loaders = {};
+
+ QStringList m_project_ids;
+ int m_project_batch_offset = 0;
+ int m_version_index = 0;
+
+ QList<ModPlatform::IndexedPack::Ptr> m_packs;
+ QList<ImportedResource> m_imported_resources;
+ QStringList m_skipped_resources;
+
+ ModrinthAPI m_api;
+ shared_qobject_ptr<Task> m_current_task;
+};
diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
new file mode 100644
index 0000000000..7e9c732eb6
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
@@ -0,0 +1,668 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "ModrinthInstanceCreationTask.h"
+
+#include "Application.h"
+#include "FileSystem.h"
+#include "InstanceList.h"
+#include "Json.h"
+
+#include "QObjectPtr.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+
+#include "minecraft/mod/Mod.hpp"
+#include "modplatform/EnsureMetadataTask.h"
+#include "modplatform/helpers/OverrideUtils.h"
+
+#include "net/ChecksumValidator.h"
+
+#include "net/ApiDownload.h"
+#include "net/NetJob.h"
+#include "settings/INISettingsObject.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+#include "ui/pages/modplatform/OptionalModDialog.h"
+
+#include <QAbstractButton>
+#include <QFileInfo>
+#include <QHash>
+#include <vector>
+
+#include "tasks/MultipleOptionsTask.h"
+
+bool ModrinthCreationTask::abort()
+{
+ if (!canAbort())
+ return false;
+
+ if (m_task)
+ m_task->abort();
+ return InstanceCreationTask::abort();
+}
+
+bool ModrinthCreationTask::updateInstance()
+{
+ auto instance_list = APPLICATION->instances();
+
+ // Note: Duplicate modpack detection uses managed name or instance ID lookup.
+ // If multiple installations exist, the first match is updated.
+ InstancePtr inst;
+ if (auto original_id = originalInstanceID(); !original_id.isEmpty())
+ {
+ inst = instance_list->getInstanceById(original_id);
+ Q_ASSERT(inst);
+ }
+ else
+ {
+ // Duplicate Detection: Check for duplicates before assuming
+ auto all_instances = instance_list->getAllInstancesByManagedName(originalName());
+
+ if (all_instances.size() > 1)
+ {
+ emitFailed(tr("Multiple instances found for this modpack. Please update the specific instance you want to "
+ "modify to avoid ambiguity."));
+ return false;
+ }
+
+ if (all_instances.size() == 1)
+ {
+ inst = all_instances.first();
+ }
+ else
+ {
+ // Fallback to name-based lookup if not found by managed ID (e.g. legacy/broken instances)
+ inst = instance_list->getInstanceById(originalName());
+ }
+
+ if (!inst)
+ {
+ // New instance creation flow continues...
+ return true;
+ }
+ }
+
+ QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json");
+ if (!parseManifest(index_path, m_files, true, false))
+ return false;
+
+ auto version_name = inst->getManagedPackVersionName();
+ m_root_path = QFileInfo(inst->gameRoot()).fileName();
+ auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : "";
+
+ if (shouldConfirmUpdate())
+ {
+ auto should_update = askIfShouldUpdate(m_parent, version_str);
+ if (should_update == ShouldUpdate::SkipUpdating)
+ return false;
+ if (should_update == ShouldUpdate::Cancel)
+ {
+ m_abort = true;
+ return false;
+ }
+ }
+
+ // Remove repeated files, we don't need to download them!
+ QDir old_inst_dir(inst->instanceRoot());
+
+ QString old_index_folder(FS::PathCombine(old_inst_dir.absolutePath(), "mrpack"));
+
+ QString old_index_path(FS::PathCombine(old_index_folder, "modrinth.index.json"));
+ QFileInfo old_index_file(old_index_path);
+ if (old_index_file.exists())
+ {
+ std::vector<File> old_files;
+ parseManifest(old_index_path, old_files, false, false);
+
+ // Let's remove all duplicated, identical resources!
+ auto files_iterator = m_files.begin();
+ begin:
+ while (files_iterator != m_files.end())
+ {
+ auto const& file = *files_iterator;
+
+ auto old_files_iterator = old_files.begin();
+ while (old_files_iterator != old_files.end())
+ {
+ auto const& old_file = *old_files_iterator;
+
+ if (old_file.hash == file.hash)
+ {
+ qDebug() << "Removed file at" << file.path << "from list of downloads";
+ files_iterator = m_files.erase(files_iterator);
+ old_files_iterator = old_files.erase(old_files_iterator);
+ goto begin; // Sorry :c
+ }
+
+ old_files_iterator++;
+ }
+
+ files_iterator++;
+ }
+
+ QDir old_minecraft_dir(inst->gameRoot());
+
+ // Some files were removed from the old version, and some will be downloaded in an updated version,
+ // so we're fine removing them!
+ if (!old_files.empty())
+ {
+ for (auto const& file : old_files)
+ {
+ if (file.path.isEmpty())
+ continue;
+ qDebug() << "Scheduling" << file.path << "for removal";
+ m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path));
+ if (file.path.endsWith(".disabled"))
+ { // remove it if it was enabled/disabled by user
+ m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path.chopped(9)));
+ }
+ else
+ {
+ m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path + ".disabled"));
+ }
+ }
+ }
+
+ // We will remove all the previous overrides, to prevent duplicate files!
+ // Note: Overrides intentionally replace all files on update - this matches modpack author expectations.
+ // Disabled mods (.disabled extension) are handled by the file exclusion logic above.
+ auto old_overrides = Override::readOverrides("overrides", old_index_folder);
+ for (const auto& entry : old_overrides)
+ {
+ if (entry.isEmpty())
+ continue;
+
+ // Skip removal of .disabled files (user-disabled mods should be preserved)
+ if (entry.endsWith(".disabled", Qt::CaseInsensitive))
+ {
+ qDebug() << "Preserving disabled mod:" << entry;
+ continue;
+ }
+
+ qDebug() << "Scheduling" << entry << "for removal";
+ m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry));
+ }
+
+ auto old_client_overrides = Override::readOverrides("client-overrides", old_index_folder);
+ for (const auto& entry : old_client_overrides)
+ {
+ if (entry.isEmpty())
+ continue;
+
+ // Skip removal of .disabled files (user-disabled mods should be preserved)
+ if (entry.endsWith(".disabled", Qt::CaseInsensitive))
+ {
+ qDebug() << "Preserving disabled mod:" << entry;
+ continue;
+ }
+
+ qDebug() << "Scheduling" << entry << "for removal";
+ m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry));
+ }
+ }
+ else
+ {
+ // We don't have an old index file, so we may duplicate stuff!
+ auto dialog = CustomMessageBox::selectable(
+ m_parent,
+ tr("No index file."),
+ tr("We couldn't find a suitable index file for the older version. This may cause some "
+ "of the files to be duplicated. Do you want to continue?"),
+ QMessageBox::Warning,
+ QMessageBox::Ok | QMessageBox::Cancel);
+
+ if (dialog->exec() == QDialog::DialogCode::Rejected)
+ {
+ m_abort = true;
+ return false;
+ }
+ }
+
+ setOverride(true, inst->id());
+ qDebug() << "Will override instance!";
+
+ m_instance = inst;
+
+ // We let it go through the createInstance() stage, just with a couple modifications for updating
+ return false;
+}
+
+// https://docs.modrinth.com/docs/modpacks/format_definition/
+std::unique_ptr<MinecraftInstance> ModrinthCreationTask::createInstance()
+{
+ QEventLoop loop;
+
+ QString parent_folder(FS::PathCombine(m_stagingPath, "mrpack"));
+
+ QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json");
+ if (m_files.empty() && !parseManifest(index_path, m_files, true, true))
+ return nullptr;
+
+ // Keep index file in case we need it some other time (like when changing versions)
+ QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json"));
+ FS::ensureFilePathExists(new_index_place);
+ FS::move(index_path, new_index_place);
+
+ auto mcPath = FS::PathCombine(m_stagingPath, m_root_path);
+
+ auto override_path = FS::PathCombine(m_stagingPath, "overrides");
+ if (QFile::exists(override_path))
+ {
+ // Create a list of overrides in "overrides.txt" inside mrpack/
+ Override::createOverrides("overrides", parent_folder, override_path);
+
+ // Apply the overrides
+ if (!FS::move(override_path, mcPath))
+ {
+ setError(tr("Could not rename the overrides folder:\n") + "overrides");
+ return nullptr;
+ }
+ }
+
+ // Do client overrides
+ auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides");
+ if (QFile::exists(client_override_path))
+ {
+ // Create a list of overrides in "client-overrides.txt" inside mrpack/
+ Override::createOverrides("client-overrides", parent_folder, client_override_path);
+
+ // Apply the overrides
+ if (!FS::overrideFolder(mcPath, client_override_path))
+ {
+ setError(tr("Could not rename the client overrides folder:\n") + "client overrides");
+ return nullptr;
+ }
+ }
+
+ QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
+ auto createdInstance = std::make_unique<MinecraftInstance>(m_globalSettings, instanceSettings, m_stagingPath);
+ auto& instance = *createdInstance;
+
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+ components->setComponentVersion("net.minecraft", m_minecraft_version, true);
+
+ if (!m_fabric_version.isEmpty())
+ components->setComponentVersion("net.fabricmc.fabric-loader", m_fabric_version);
+ if (!m_quilt_version.isEmpty())
+ components->setComponentVersion("org.quiltmc.quilt-loader", m_quilt_version);
+ if (!m_forge_version.isEmpty())
+ components->setComponentVersion("net.minecraftforge", m_forge_version);
+ if (!m_neoForge_version.isEmpty())
+ components->setComponentVersion("net.neoforged", m_neoForge_version);
+
+ if (m_instIcon != "default")
+ {
+ instance.setIconKey(m_instIcon);
+ }
+ else if (!m_managed_id.isEmpty())
+ {
+ instance.setIconKey("modrinth");
+ }
+
+ // Don't add managed info to packs without an ID (most likely imported from ZIP)
+ if (!m_managed_id.isEmpty())
+ instance.setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version());
+ else
+ instance.setManagedPack("modrinth", "", name(), "", "");
+
+ instance.setName(name());
+ instance.saveNow();
+
+ auto downloadMods = makeShared<NetJob>(tr("Mod Download Modrinth"), APPLICATION->network());
+
+ // Clear any previous alternative URLs tracking
+ m_alternativeUrls.clear();
+
+ auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path);
+ auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path);
+ // Note: Currently handles mods, resource packs, and shader packs.
+ // Additional resource types can be added by extending the path prefix checks below.
+ QHash<QString, Resource*> resources;
+ for (auto& file : m_files)
+ {
+ auto fileName = file.path;
+ fileName = FS::RemoveInvalidPathChars(fileName);
+ auto file_path = FS::PathCombine(root_modpack_path, fileName);
+ if (!root_modpack_url.isParentOf(QUrl::fromLocalFile(file_path)))
+ {
+ // This means we somehow got out of the root folder, so abort here to prevent exploits
+ setError(tr("One of the files has a path that leads to an arbitrary location (%1). This is a security risk "
+ "and isn't allowed.")
+ .arg(fileName));
+ return nullptr;
+ }
+ if (fileName.startsWith("mods/"))
+ {
+ auto mod = new Mod(file_path);
+ ModDetails d;
+ d.mod_id = file_path;
+ mod->setDetails(d);
+ resources[file.hash.toHex()] = mod;
+ }
+ if (file.downloads.empty())
+ {
+ setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName));
+ return nullptr;
+ }
+
+ auto fileTask = makeShared<MultipleOptionsTask>(tr("Download %1").arg(fileName));
+
+ for (const auto& url : file.downloads)
+ {
+ auto dl = Net::ApiDownload::makeFile(QUrl(url.toString()), file_path);
+ dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
+ fileTask->addTask(dl);
+ }
+
+ downloadMods->addTask(fileTask);
+ }
+
+ bool ended_well = false;
+
+ connect(downloadMods.get(), &NetJob::succeeded, this, [&ended_well]() { ended_well = true; });
+ connect(downloadMods.get(),
+ &NetJob::failed,
+ [this, &ended_well](const QString& reason)
+ {
+ ended_well = false;
+ setError(reason);
+ });
+ connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit);
+ connect(downloadMods.get(),
+ &NetJob::progress,
+ [this](qint64 current, qint64 total)
+ {
+ setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
+ setProgress(current, total);
+ });
+ connect(downloadMods.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress);
+
+ setStatus(tr("Downloading mods..."));
+ downloadMods->start();
+ m_task = downloadMods;
+
+ loop.exec();
+
+ if (!ended_well)
+ {
+ for (auto resource : resources)
+ {
+ delete resource;
+ }
+ return nullptr;
+ }
+
+ QEventLoop ensureMetaLoop;
+ QDir folder = FS::PathCombine(instance.modsRoot(), ".index");
+ auto ensureMetadataTask =
+ makeShared<EnsureMetadataTask>(resources, folder, ModPlatform::ResourceProvider::MODRINTH);
+ connect(ensureMetadataTask.get(), &Task::succeeded, this, [&ended_well]() { ended_well = true; });
+ connect(ensureMetadataTask.get(), &Task::finished, &ensureMetaLoop, &QEventLoop::quit);
+ connect(ensureMetadataTask.get(),
+ &Task::progress,
+ [this](qint64 current, qint64 total)
+ {
+ setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
+ setProgress(current, total);
+ });
+ connect(ensureMetadataTask.get(), &Task::stepProgress, this, &ModrinthCreationTask::propagateStepProgress);
+
+ ensureMetadataTask->start();
+ m_task = ensureMetadataTask;
+
+ ensureMetaLoop.exec();
+ for (auto resource : resources)
+ {
+ delete resource;
+ }
+ resources.clear();
+
+ // Update information of the already installed instance, if any.
+ if (m_instance && ended_well)
+ {
+ setAbortable(false);
+ auto inst = m_instance.value();
+
+ // Only change the name if it didn't use a custom name, so that the previous custom name
+ // is preserved, but if we're using the original one, we update the version string.
+ // NOTE: This needs to come before the copyManagedPack call!
+ if (inst->name().contains(inst->getManagedPackVersionName()) && inst->name() != instance.name())
+ {
+ if (askForChangingInstanceName(m_parent, inst->name(), instance.name()) == InstanceNameChange::ShouldChange)
+ inst->setName(instance.name());
+ }
+
+ inst->copyManagedPack(instance);
+ }
+
+ if (ended_well)
+ {
+ return createdInstance;
+ }
+ return nullptr;
+}
+
+bool ModrinthCreationTask::parseManifest(const QString& index_path,
+ std::vector<File>& files,
+ bool set_internal_data,
+ bool show_optional_dialog)
+{
+ try
+ {
+ auto doc = Json::requireDocument(index_path);
+ auto obj = Json::requireObject(doc, "modrinth.index.json");
+ int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json");
+ if (formatVersion == 1)
+ {
+ auto game = Json::requireString(obj, "game", "modrinth.index.json");
+ if (game != "minecraft")
+ {
+ throw JSONValidationError("Unknown game: " + game);
+ }
+
+ if (set_internal_data)
+ {
+ if (m_managed_version_id.isEmpty())
+ m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID");
+ m_managed_name = Json::ensureString(obj, "name", {}, "Managed Name");
+ }
+
+ auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
+ std::vector<File> optionalFiles;
+ for (const auto& modInfo : jsonFiles)
+ {
+ File file;
+ file.path = Json::requireString(modInfo, "path").replace("\\", "/");
+
+ auto env = Json::ensureObject(modInfo, "env");
+ // 'env' field is optional
+ if (!env.isEmpty())
+ {
+ QString support = Json::ensureString(env, "client", "unsupported");
+ if (support == "unsupported")
+ {
+ continue;
+ }
+ else if (support == "optional")
+ {
+ file.required = false;
+ }
+ }
+
+ QJsonObject hashes = Json::requireObject(modInfo, "hashes");
+ file.hash = QByteArray::fromHex(Json::requireString(hashes, "sha512").toLatin1());
+ file.hashAlgorithm = QCryptographicHash::Sha512;
+
+ // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode
+ // (as Modrinth seems to incorrectly handle spaces)
+
+ auto download_arr = Json::ensureArray(modInfo, "downloads");
+ for (auto download : download_arr)
+ {
+ qWarning() << download.toString();
+ bool is_last = download.toString() == download_arr.last().toString();
+
+ auto download_url = QUrl(download.toString());
+
+ if (!download_url.isValid())
+ {
+ qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL")
+ .arg(download_url.toString(), file.path);
+ if (is_last && file.downloads.isEmpty())
+ throw JSONValidationError(
+ tr("Download URL for %1 is not a correctly formatted URL").arg(file.path));
+ }
+ else
+ {
+ file.downloads.push_back(download_url);
+ }
+ }
+
+ (file.required ? files : optionalFiles).push_back(file);
+ }
+
+ if (!optionalFiles.empty())
+ {
+ if (show_optional_dialog)
+ {
+ QStringList oFiles;
+ for (auto file : optionalFiles)
+ oFiles.push_back(file.path);
+ OptionalModDialog optionalModDialog(m_parent, oFiles);
+ if (optionalModDialog.exec() == QDialog::Rejected)
+ {
+ emitAborted();
+ return false;
+ }
+
+ auto selectedMods = optionalModDialog.getResult();
+ for (auto file : optionalFiles)
+ {
+ if (selectedMods.contains(file.path))
+ {
+ file.required = true;
+ }
+ else
+ {
+ file.path += ".disabled";
+ }
+ files.push_back(file);
+ }
+ }
+ else
+ {
+ for (auto file : optionalFiles)
+ {
+ file.path += ".disabled";
+ files.push_back(file);
+ }
+ }
+ }
+ if (set_internal_data)
+ {
+ auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json");
+ for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it)
+ {
+ QString name = it.key();
+ if (name == "minecraft")
+ {
+ m_minecraft_version = Json::requireString(*it, "Minecraft version");
+ }
+ else if (name == "fabric-loader")
+ {
+ m_fabric_version = Json::requireString(*it, "Fabric Loader version");
+ }
+ else if (name == "quilt-loader")
+ {
+ m_quilt_version = Json::requireString(*it, "Quilt Loader version");
+ }
+ else if (name == "forge")
+ {
+ m_forge_version = Json::requireString(*it, "Forge version");
+ }
+ else if (name == "neoforge")
+ {
+ m_neoForge_version = Json::requireString(*it, "NeoForge version");
+ }
+ else
+ {
+ throw JSONValidationError("Unknown dependency type: " + name);
+ }
+ }
+ }
+ }
+ else
+ {
+ throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion));
+ }
+ }
+ catch (const JSONValidationError& e)
+ {
+ setError(tr("Could not understand pack index:\n") + e.cause());
+ return false;
+ }
+
+ return true;
+}
+
+void ModrinthCreationTask::attachRetryHandler(Net::Download::Ptr dl, shared_qobject_ptr<NetJob> downloadMods)
+{
+ // Connect to failed signal for retry logic
+ connect(dl.get(),
+ &Task::failed,
+ this,
+ [this, downloadMods, dl](QString reason)
+ {
+ Q_UNUSED(reason);
+ auto it = m_alternativeUrls.find(dl.get());
+ if (it == m_alternativeUrls.end())
+ {
+ return;
+ }
+
+ FileDownloadInfo info = it.value();
+ if (info.remainingUrls.isEmpty())
+ {
+ m_alternativeUrls.remove(dl.get());
+ return;
+ }
+
+ QString nextUrl = info.remainingUrls.dequeue();
+ qDebug() << "Retrying download with alternative URL:" << nextUrl;
+
+ auto newDl = Net::ApiDownload::makeFile(QUrl(nextUrl), info.filePath);
+ newDl->addValidator(new Net::ChecksumValidator(info.hashAlgorithm, info.hash));
+
+ if (!info.remainingUrls.isEmpty())
+ {
+ m_alternativeUrls[newDl.get()] = info;
+ }
+
+ m_alternativeUrls.remove(dl.get());
+ attachRetryHandler(newDl, downloadMods);
+ downloadMods->addNetAction(newDl);
+ });
+
+ // Clean up map entry on success
+ connect(dl.get(), &Task::succeeded, this, [this, dl]() { m_alternativeUrls.remove(dl.get()); });
+}
diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h
new file mode 100644
index 0000000000..94ca441803
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h
@@ -0,0 +1,104 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#pragma once
+
+#include <optional>
+
+#include <QByteArray>
+#include <QCryptographicHash>
+#include <QHash>
+#include <QQueue>
+#include <QString>
+#include <QUrl>
+#include <QVector>
+
+#include "BaseInstance.h"
+#include "InstanceCreationTask.h"
+#include "net/Download.h"
+
+class NetJob;
+
+class ModrinthCreationTask final : public InstanceCreationTask
+{
+ Q_OBJECT
+ struct File
+ {
+ QString path;
+
+ QCryptographicHash::Algorithm hashAlgorithm;
+ QByteArray hash;
+ QQueue<QUrl> downloads;
+ bool required = true;
+ };
+
+ public:
+ ModrinthCreationTask(QString staging_path,
+ SettingsObjectPtr global_settings,
+ QWidget* parent,
+ QString id,
+ QString version_id = {},
+ QString original_instance_id = {})
+ : InstanceCreationTask(),
+ m_parent(parent),
+ m_managed_id(std::move(id)),
+ m_managed_version_id(std::move(version_id))
+ {
+ setStagingPath(staging_path);
+ setParentSettings(global_settings);
+
+ m_original_instance_id = std::move(original_instance_id);
+ }
+
+ bool abort() override;
+
+ bool updateInstance() override;
+ std::unique_ptr<MinecraftInstance> createInstance() override;
+
+ private:
+ bool parseManifest(const QString&,
+ std::vector<File>&,
+ bool set_internal_data = true,
+ bool show_optional_dialog = true);
+ void attachRetryHandler(Net::Download::Ptr dl, shared_qobject_ptr<NetJob> downloadMods);
+
+ private:
+ QWidget* m_parent = nullptr;
+
+ QString m_minecraft_version, m_fabric_version, m_quilt_version, m_forge_version, m_neoForge_version;
+ QString m_managed_id, m_managed_version_id, m_managed_name;
+
+ std::vector<File> m_files;
+ Task::Ptr m_task;
+
+ std::optional<InstancePtr> m_instance;
+
+ QString m_root_path = "minecraft";
+
+ // Alternative URLs tracking for download retry mechanism
+ struct FileDownloadInfo
+ {
+ QString filePath;
+ QQueue<QString> remainingUrls;
+ QByteArray hash;
+ QCryptographicHash::Algorithm hashAlgorithm;
+ };
+ QHash<Net::Download*, FileDownloadInfo> m_alternativeUrls;
+};
diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
new file mode 100644
index 0000000000..e03682f45a
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp
@@ -0,0 +1,386 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "ModrinthPackExportTask.h"
+
+#include <QCoreApplication>
+#include <QCryptographicHash>
+#include <QFileInfo>
+#include <QMessageBox>
+#include <QtConcurrentRun>
+#include "Json.h"
+#include "MMCZip.h"
+#include "minecraft/PackProfile.h"
+#include "minecraft/mod/MetadataHandler.hpp"
+#include "minecraft/mod/ModFolderModel.hpp"
+#include "modplatform/ModIndex.h"
+#include "modplatform/helpers/HashUtils.h"
+#include "tasks/Task.h"
+
+const QStringList ModrinthPackExportTask::PREFIXES(
+ { "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" });
+const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "zip" });
+
+ModrinthPackExportTask::ModrinthPackExportTask(const QString& name,
+ const QString& version,
+ const QString& summary,
+ bool optionalFiles,
+ InstancePtr instance,
+ const QString& output,
+ MMCZip::FilterFileFunction filter)
+ : name(name),
+ version(version),
+ summary(summary),
+ optionalFiles(optionalFiles),
+ instance(instance),
+ mcInstance(dynamic_cast<MinecraftInstance*>(instance.get())),
+ gameRoot(instance->gameRoot()),
+ output(output),
+ filter(filter)
+{}
+
+void ModrinthPackExportTask::executeTask()
+{
+ setStatus(tr("Searching for files..."));
+ setProgress(0, 0);
+ collectFiles();
+}
+
+bool ModrinthPackExportTask::abort()
+{
+ if (task)
+ {
+ task->abort();
+ return true;
+ }
+ return false;
+}
+
+void ModrinthPackExportTask::collectFiles()
+{
+ setAbortable(false);
+ QCoreApplication::processEvents();
+
+ files.clear();
+ if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter))
+ {
+ emitFailed(tr("Could not search for files"));
+ return;
+ }
+
+ pendingHashes.clear();
+ resolvedFiles.clear();
+
+ if (mcInstance)
+ {
+ mcInstance->loaderModList()->update();
+ connect(mcInstance->loaderModList().get(),
+ &ModFolderModel::updateFinished,
+ this,
+ &ModrinthPackExportTask::collectHashes);
+ }
+ else
+ collectHashes();
+}
+
+void ModrinthPackExportTask::collectHashes()
+{
+ setStatus(tr("Finding file hashes..."));
+ for (const QFileInfo& file : files)
+ {
+ QCoreApplication::processEvents();
+
+ const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath());
+ // require sensible file types
+ if (!std::any_of(PREFIXES.begin(),
+ PREFIXES.end(),
+ [&relative](const QString& prefix) { return relative.startsWith(prefix); }))
+ continue;
+ if (!std::any_of(
+ FILE_EXTENSIONS.begin(),
+ FILE_EXTENSIONS.end(),
+ [&relative](const QString& extension)
+ { return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); }))
+ continue;
+
+ QFile openFile(file.absoluteFilePath());
+ if (!openFile.open(QFile::ReadOnly))
+ {
+ qWarning() << "Could not open" << file << "for hashing";
+ continue;
+ }
+
+ const QByteArray data = openFile.readAll();
+ if (openFile.error() != QFileDevice::NoError)
+ {
+ qWarning() << "Could not read" << file;
+ continue;
+ }
+ auto sha512 = Hashing::hash(data, Hashing::Algorithm::Sha512);
+
+ auto allMods = mcInstance->loaderModList()->allMods();
+ if (auto modIter =
+ std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; });
+ modIter != allMods.end())
+ {
+ const Mod* mod = *modIter;
+ if (mod->metadata() != nullptr)
+ {
+ const QUrl& url = mod->metadata()->url;
+ // ensure the url is permitted on modrinth.com
+ if (!url.isEmpty() && BuildConfig.MODRINTH_MRPACK_HOSTS.contains(url.host()))
+ {
+ qDebug() << "Resolving" << relative << "from index";
+
+ auto sha1 = Hashing::hash(data, Hashing::Algorithm::Sha1);
+
+ ResolvedFile resolvedFile{ sha1, sha512, url.toEncoded(), openFile.size(), mod->metadata()->side };
+ resolvedFiles[relative] = resolvedFile;
+
+ // nice! we've managed to resolve based on local metadata!
+ // no need to enqueue it
+ continue;
+ }
+ }
+ }
+
+ qDebug() << "Enqueueing" << relative << "for Modrinth query";
+ pendingHashes[relative] = sha512;
+ }
+
+ setAbortable(true);
+ makeApiRequest();
+}
+
+void ModrinthPackExportTask::makeApiRequest()
+{
+ if (pendingHashes.isEmpty())
+ buildZip();
+ else
+ {
+ setStatus(tr("Finding versions for hashes..."));
+ auto response = std::make_shared<QByteArray>();
+ task = api.currentVersions(pendingHashes.values(), "sha512", response);
+ connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); });
+ connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed);
+ connect(task.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted);
+ task->start();
+ }
+}
+
+void ModrinthPackExportTask::parseApiResponse(const std::shared_ptr<QByteArray> response)
+{
+ task = nullptr;
+
+ try
+ {
+ const QJsonDocument doc = Json::requireDocument(*response);
+
+ QMapIterator<QString, QString> iterator(pendingHashes);
+ while (iterator.hasNext())
+ {
+ iterator.next();
+
+ const QJsonObject obj = doc[iterator.value()].toObject();
+ if (obj.isEmpty())
+ continue;
+
+ const QJsonArray files_array = obj["files"].toArray();
+ if (auto fileIter = std::find_if(files_array.begin(),
+ files_array.end(),
+ [&iterator](const QJsonValue& file)
+ { return file["hashes"]["sha512"] == iterator.value(); });
+ fileIter != files_array.end())
+ {
+ // map the file to the url
+ resolvedFiles[iterator.key()] =
+ ResolvedFile{ fileIter->toObject()["hashes"].toObject()["sha1"].toString(),
+ iterator.value(),
+ fileIter->toObject()["url"].toString(),
+ fileIter->toObject()["size"].toInt() };
+ }
+ }
+ }
+ catch (const Json::JsonException& e)
+ {
+ emitFailed(tr("Failed to parse versions response: %1").arg(e.what()));
+ return;
+ }
+ pendingHashes.clear();
+ buildZip();
+}
+
+void ModrinthPackExportTask::buildZip()
+{
+ setStatus(tr("Adding files..."));
+
+ auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true, true);
+ zipTask->addExtraFile("modrinth.index.json", generateIndex());
+
+ zipTask->setExcludeFiles(resolvedFiles.keys());
+
+ auto progressStep = std::make_shared<TaskStepProgress>();
+ connect(zipTask.get(),
+ &Task::finished,
+ this,
+ [this, progressStep]
+ {
+ progressStep->state = TaskStepState::Succeeded;
+ stepProgress(*progressStep);
+ });
+
+ connect(zipTask.get(), &Task::succeeded, this, &ModrinthPackExportTask::emitSucceeded);
+ connect(zipTask.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted);
+ connect(zipTask.get(),
+ &Task::failed,
+ this,
+ [this, progressStep](QString reason)
+ {
+ progressStep->state = TaskStepState::Failed;
+ stepProgress(*progressStep);
+ emitFailed(reason);
+ });
+ connect(zipTask.get(), &Task::stepProgress, this, &ModrinthPackExportTask::propagateStepProgress);
+
+ connect(zipTask.get(),
+ &Task::progress,
+ this,
+ [this, progressStep](qint64 current, qint64 total)
+ {
+ progressStep->update(current, total);
+ stepProgress(*progressStep);
+ });
+ connect(zipTask.get(),
+ &Task::status,
+ this,
+ [this, progressStep](QString status)
+ {
+ progressStep->status = status;
+ stepProgress(*progressStep);
+ });
+ task.reset(zipTask);
+ zipTask->start();
+}
+
+QByteArray ModrinthPackExportTask::generateIndex()
+{
+ QJsonObject out;
+ out["formatVersion"] = 1;
+ out["game"] = "minecraft";
+ out["name"] = name;
+ out["versionId"] = version;
+ if (!summary.isEmpty())
+ out["summary"] = summary;
+
+ if (mcInstance)
+ {
+ auto profile = mcInstance->getPackProfile();
+ // collect all supported components
+ const ComponentPtr minecraft = profile->getComponent("net.minecraft");
+ const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader");
+ const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader");
+ const ComponentPtr forge = profile->getComponent("net.minecraftforge");
+ const ComponentPtr neoForge = profile->getComponent("net.neoforged");
+
+ // convert all available components to mrpack dependencies
+ QJsonObject dependencies;
+ if (minecraft != nullptr)
+ dependencies["minecraft"] = minecraft->m_version;
+ if (quilt != nullptr)
+ dependencies["quilt-loader"] = quilt->m_version;
+ if (fabric != nullptr)
+ dependencies["fabric-loader"] = fabric->m_version;
+ if (forge != nullptr)
+ dependencies["forge"] = forge->m_version;
+ if (neoForge != nullptr)
+ dependencies["neoforge"] = neoForge->m_version;
+
+ out["dependencies"] = dependencies;
+ }
+
+ QJsonArray filesOut;
+ for (auto iterator = resolvedFiles.constBegin(); iterator != resolvedFiles.constEnd(); iterator++)
+ {
+ QJsonObject fileOut;
+
+ QString path = iterator.key();
+ const ResolvedFile& value = iterator.value();
+
+ QJsonObject env;
+
+ // detect disabled mod
+ const QFileInfo pathInfo(path);
+ if (optionalFiles && pathInfo.suffix() == "disabled")
+ {
+ // rename it
+ path = pathInfo.dir().filePath(pathInfo.completeBaseName());
+ env["client"] = "optional";
+ env["server"] = "optional";
+ }
+ else
+ {
+ env["client"] = "required";
+ env["server"] = "required";
+ }
+
+ // a server side mod does not imply that the mod does not work on the client
+ // however, if a mrpack mod is marked as server-only it will not install on the client
+ if (iterator->side == ModPlatform::Side::ClientSide)
+ env["server"] = "unsupported";
+
+ fileOut["env"] = env;
+
+ fileOut["path"] = path;
+ fileOut["downloads"] = QJsonArray{ iterator->url };
+
+ QJsonObject hashes;
+ hashes["sha1"] = value.sha1;
+ hashes["sha512"] = value.sha512;
+ fileOut["hashes"] = hashes;
+
+ fileOut["fileSize"] = value.size;
+ filesOut << fileOut;
+ }
+ out["files"] = filesOut;
+
+ return QJsonDocument(out).toJson(QJsonDocument::Compact);
+}
diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.h
new file mode 100644
index 0000000000..95c9d77b22
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.h
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QFuture>
+#include <QFutureWatcher>
+#include "BaseInstance.h"
+#include "MMCZip.h"
+#include "minecraft/MinecraftInstance.h"
+#include "modplatform/ModIndex.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
+#include "tasks/Task.h"
+
+class ModrinthPackExportTask : public Task
+{
+ Q_OBJECT
+ public:
+ ModrinthPackExportTask(const QString& name,
+ const QString& version,
+ const QString& summary,
+ bool optionalFiles,
+ InstancePtr instance,
+ const QString& output,
+ MMCZip::FilterFileFunction filter);
+
+ protected:
+ void executeTask() override;
+ bool abort() override;
+
+ private:
+ struct ResolvedFile
+ {
+ QString sha1, sha512, url;
+ qint64 size;
+ ModPlatform::Side side;
+ };
+
+ static const QStringList PREFIXES;
+ static const QStringList FILE_EXTENSIONS;
+
+ // inputs
+ const QString name, version, summary;
+ const bool optionalFiles;
+ const InstancePtr instance;
+ MinecraftInstance* mcInstance;
+ const QDir gameRoot;
+ const QString output;
+ const MMCZip::FilterFileFunction filter;
+
+ ModrinthAPI api;
+ QFileInfoList files;
+ QMap<QString, QString> pendingHashes;
+ QMap<QString, ResolvedFile> resolvedFiles;
+ Task::Ptr task;
+
+ void collectFiles();
+ void collectHashes();
+ void makeApiRequest();
+ void parseApiResponse(std::shared_ptr<QByteArray> response);
+ void buildZip();
+
+ QByteArray generateIndex();
+};
diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
new file mode 100644
index 0000000000..aecc1879be
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -0,0 +1,284 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "ModrinthPackIndex.h"
+#include "FileSystem.h"
+#include "ModrinthAPI.h"
+
+#include "Json.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "modplatform/ModIndex.h"
+
+static ModrinthAPI api;
+
+bool shouldDownloadOnSide(QString side)
+{
+ return side == "required" || side == "optional";
+}
+
+// https://docs.modrinth.com/api/operations/getproject/
+void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
+{
+ pack.addonId = Json::ensureString(obj, "project_id");
+ if (pack.addonId.toString().isEmpty())
+ pack.addonId = Json::requireString(obj, "id");
+
+ pack.provider = ModPlatform::ResourceProvider::MODRINTH;
+ pack.name = Json::requireString(obj, "title");
+
+ pack.slug = Json::ensureString(obj, "slug", "");
+ if (!pack.slug.isEmpty())
+ pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug;
+ else
+ pack.websiteUrl = "";
+
+ pack.description = Json::ensureString(obj, "description", "");
+
+ pack.logoUrl = Json::ensureString(obj, "icon_url", "");
+ pack.logoName =
+ QString("%1.%2").arg(Json::ensureString(obj, "slug"), QFileInfo(QUrl(pack.logoUrl).fileName()).suffix());
+
+ if (obj.contains("author"))
+ {
+ ModPlatform::ModpackAuthor modAuthor;
+ modAuthor.name = Json::ensureString(obj, "author");
+ modAuthor.url = api.getAuthorURL(modAuthor.name);
+ pack.authors = { modAuthor };
+ }
+
+ auto client = shouldDownloadOnSide(Json::ensureString(obj, "client_side"));
+ auto server = shouldDownloadOnSide(Json::ensureString(obj, "server_side"));
+
+ if (server && client)
+ {
+ pack.side = ModPlatform::Side::UniversalSide;
+ }
+ else if (server)
+ {
+ pack.side = ModPlatform::Side::ServerSide;
+ }
+ else if (client)
+ {
+ pack.side = ModPlatform::Side::ClientSide;
+ }
+
+ // Modrinth can have more data than what's provided by the basic search :)
+ pack.extraDataLoaded = false;
+}
+
+void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj)
+{
+ pack.extraData.issuesUrl = Json::ensureString(obj, "issues_url");
+ if (pack.extraData.issuesUrl.endsWith('/'))
+ pack.extraData.issuesUrl.chop(1);
+
+ pack.extraData.sourceUrl = Json::ensureString(obj, "source_url");
+ if (pack.extraData.sourceUrl.endsWith('/'))
+ pack.extraData.sourceUrl.chop(1);
+
+ pack.extraData.wikiUrl = Json::ensureString(obj, "wiki_url");
+ if (pack.extraData.wikiUrl.endsWith('/'))
+ pack.extraData.wikiUrl.chop(1);
+
+ pack.extraData.discordUrl = Json::ensureString(obj, "discord_url");
+ if (pack.extraData.discordUrl.endsWith('/'))
+ pack.extraData.discordUrl.chop(1);
+
+ auto donate_arr = Json::ensureArray(obj, "donation_urls");
+ for (auto d : donate_arr)
+ {
+ auto d_obj = Json::requireObject(d);
+
+ ModPlatform::DonationData donate;
+
+ donate.id = Json::ensureString(d_obj, "id");
+ donate.platform = Json::ensureString(d_obj, "platform");
+ donate.url = Json::ensureString(d_obj, "url");
+
+ pack.extraData.donate.append(donate);
+ }
+
+ pack.extraData.status = Json::ensureString(obj, "status");
+
+ pack.extraData.body = Json::ensureString(obj, "body").remove("<br>");
+
+ pack.extraDataLoaded = true;
+}
+
+ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj,
+ QString preferred_hash_type,
+ QString preferred_file_name)
+{
+ ModPlatform::IndexedVersion file;
+
+ file.addonId = Json::requireString(obj, "project_id");
+ file.fileId = Json::requireString(obj, "id");
+ file.date = Json::requireString(obj, "date_published");
+ auto versionArray = Json::requireArray(obj, "game_versions");
+ if (versionArray.empty())
+ {
+ return {};
+ }
+ for (auto mcVer : versionArray)
+ {
+ file.mcVersion.append(ModrinthAPI::mapMCVersionFromModrinth(mcVer.toString()));
+ }
+ auto loaders = Json::requireArray(obj, "loaders");
+ for (auto loader : loaders)
+ {
+ if (loader == "neoforge")
+ file.loaders |= ModPlatform::NeoForge;
+ else if (loader == "forge")
+ file.loaders |= ModPlatform::Forge;
+ else if (loader == "cauldron")
+ file.loaders |= ModPlatform::Cauldron;
+ else if (loader == "liteloader")
+ file.loaders |= ModPlatform::LiteLoader;
+ else if (loader == "fabric")
+ file.loaders |= ModPlatform::Fabric;
+ else if (loader == "quilt")
+ file.loaders |= ModPlatform::Quilt;
+ else if (loader == "risugami")
+ file.loaders |= ModPlatform::Risugami;
+ else if (loader == "station-loader")
+ file.loaders |= ModPlatform::StationLoader;
+ else if (loader == "modloadermp")
+ file.loaders |= ModPlatform::ModLoaderMP;
+ else if (loader == "optifine")
+ file.loaders |= ModPlatform::Optifine;
+ }
+ file.version = Json::requireString(obj, "name");
+ file.version_number = Json::requireString(obj, "version_number");
+ file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type"));
+
+ file.changelog = Json::requireString(obj, "changelog");
+
+ auto dependencies = Json::ensureArray(obj, "dependencies");
+ for (auto d : dependencies)
+ {
+ auto dep = Json::ensureObject(d);
+ ModPlatform::Dependency dependency;
+ dependency.addonId = Json::ensureString(dep, "project_id");
+ dependency.version = Json::ensureString(dep, "version_id");
+ auto depType = Json::requireString(dep, "dependency_type");
+
+ if (depType == "required")
+ dependency.type = ModPlatform::DependencyType::REQUIRED;
+ else if (depType == "optional")
+ dependency.type = ModPlatform::DependencyType::OPTIONAL;
+ else if (depType == "incompatible")
+ dependency.type = ModPlatform::DependencyType::INCOMPATIBLE;
+ else if (depType == "embedded")
+ dependency.type = ModPlatform::DependencyType::EMBEDDED;
+ else
+ dependency.type = ModPlatform::DependencyType::UNKNOWN;
+
+ file.dependencies.append(dependency);
+ }
+
+ auto files = Json::requireArray(obj, "files");
+ int i = 0;
+
+ if (files.empty())
+ {
+ // This should not happen normally, but check just in case
+ qWarning() << "Modrinth returned an unexpected empty list of files:" << obj;
+ return {};
+ }
+
+ // Find correct file (needed in cases where one version may have multiple files)
+ // Will default to the last one if there's no primary (though I think Modrinth requires that
+ // at least one file is primary, idk)
+ // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed
+ while (i < files.count() - 1)
+ {
+ auto parent = files[i].toObject();
+ auto fileName = Json::requireString(parent, "filename");
+
+ if (!preferred_file_name.isEmpty() && fileName.contains(preferred_file_name))
+ {
+ file.is_preferred = true;
+ break;
+ }
+
+ // Grab the primary file, if available
+ if (Json::requireBoolean(parent, "primary"))
+ break;
+
+ i++;
+ }
+
+ auto parent = files[i].toObject();
+ if (parent.contains("url"))
+ {
+ file.downloadUrl = Json::requireString(parent, "url");
+ file.fileName = Json::requireString(parent, "filename");
+ file.fileName = FS::RemoveInvalidPathChars(file.fileName);
+ file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1);
+ auto hash_list = Json::requireObject(parent, "hashes");
+
+ if (hash_list.contains(preferred_hash_type))
+ {
+ file.hash = Json::requireString(hash_list, preferred_hash_type);
+ file.hash_type = preferred_hash_type;
+ }
+ else
+ {
+ auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH);
+ for (auto& hash_type : hash_types)
+ {
+ if (hash_list.contains(hash_type))
+ {
+ file.hash = Json::requireString(hash_list, hash_type);
+ file.hash_type = hash_type;
+ break;
+ }
+ }
+ }
+
+ return file;
+ }
+
+ return {};
+}
diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.h b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.h
new file mode 100644
index 0000000000..45f33733ce
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.h
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include "modplatform/ModIndex.h"
+
+#include "BaseInstance.h"
+
+namespace Modrinth
+{
+
+ void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj);
+ void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj);
+ auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "")
+ -> ModPlatform::IndexedVersion;
+
+} // namespace Modrinth
diff --git a/archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.cpp b/archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.cpp
new file mode 100644
index 0000000000..0d2aa5857a
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.cpp
@@ -0,0 +1,494 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "Packwiz.h"
+
+#include <algorithm>
+#include <QDebug>
+#include <QDir>
+#include <QObject>
+#include <sstream>
+#include <string>
+
+#include "FileSystem.h"
+#include "StringUtils.h"
+#include "Version.h"
+
+#include "modplatform/ModIndex.h"
+
+#include <toml++/toml.h>
+
+#ifdef Q_OS_WIN32
+#include <windows.h>
+#endif
+
+namespace Packwiz
+{
+ namespace
+ {
+ void sortMinecraftVersionsDescending(QStringList& versions)
+ {
+ std::sort(versions.begin(), versions.end(), [](const QString& left, const QString& right)
+ { return Version(left) > Version(right); });
+ }
+ } // namespace
+
+ auto getRealIndexName(const QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString
+ {
+ QFile index_file(index_dir.absoluteFilePath(normalized_fname));
+
+ QString real_fname = normalized_fname;
+ if (!index_file.exists())
+ {
+ // Tries to get similar entries
+ for (auto& file_name : index_dir.entryList(QDir::Filter::Files))
+ {
+ if (!QString::compare(normalized_fname, file_name, Qt::CaseInsensitive))
+ {
+ real_fname = file_name;
+ break;
+ }
+ }
+
+ if (should_find_match && !QString::compare(normalized_fname, real_fname, Qt::CaseSensitive))
+ {
+ qCritical() << "Could not find a match for a valid metadata file!";
+ qCritical() << "File: " << normalized_fname;
+ return {};
+ }
+ }
+
+ return real_fname;
+ }
+
+ // Helpers
+ static inline auto indexFileName(const QString& mod_slug) -> QString
+ {
+ if (mod_slug.endsWith(".pw.toml"))
+ return mod_slug;
+ return QString("%1.pw.toml").arg(mod_slug);
+ }
+
+ // Helper functions for extracting data from the TOML file
+ auto stringEntry(toml::table table, QString entry_name) -> QString
+ {
+ auto node = table[StringUtils::toStdString(entry_name)];
+ if (!node)
+ {
+ qWarning() << "Failed to read str property '" + entry_name + "' in mod metadata.";
+ return {};
+ }
+
+ return node.value_or("");
+ }
+
+ auto intEntry(toml::table table, QString entry_name) -> int
+ {
+ auto node = table[StringUtils::toStdString(entry_name)];
+ if (!node)
+ {
+ qWarning() << "Failed to read int property '" + entry_name + "' in mod metadata.";
+ return {};
+ }
+
+ return node.value_or(0);
+ }
+
+ auto V1::createModFormat([[maybe_unused]] const QDir& index_dir,
+ ModPlatform::IndexedPack& mod_pack,
+ ModPlatform::IndexedVersion& mod_version) -> Mod
+ {
+ Mod mod;
+
+ mod.slug = mod_pack.slug;
+ mod.name = mod_pack.name;
+ mod.filename = mod_version.fileName;
+
+ if (mod_pack.provider == ModPlatform::ResourceProvider::FLAME)
+ {
+ mod.mode = "metadata:curseforge";
+ }
+ else
+ {
+ mod.mode = "url";
+ mod.url = mod_version.downloadUrl;
+ }
+
+ mod.hash_format = mod_version.hash_type;
+ mod.hash = mod_version.hash;
+
+ mod.provider = mod_pack.provider;
+ mod.file_id = mod_version.fileId;
+ mod.project_id = mod_pack.addonId;
+ mod.side = mod_version.side == ModPlatform::Side::NoSide ? mod_pack.side : mod_version.side;
+ mod.loaders = mod_version.loaders;
+ mod.mcVersions = mod_version.mcVersion;
+ sortMinecraftVersionsDescending(mod.mcVersions);
+ mod.releaseType = mod_version.version_type;
+
+ mod.version_number = mod_version.version_number;
+ if (mod.version_number.isNull()) // on CurseForge, there is only a version name - not a version number
+ mod.version_number = mod_version.version;
+
+ return mod;
+ }
+
+ void V1::updateModIndex(const QDir& index_dir, Mod& mod)
+ {
+ // Call with null callback - no user confirmation
+ updateModIndex(index_dir, mod, nullptr);
+ }
+
+ void V1::updateModIndex(const QDir& index_dir,
+ Mod& mod,
+ std::function<bool(const QString&, const QString&, const QString&)> confirmOverride)
+ {
+ if (!mod.isValid())
+ {
+ qCritical() << QString("Tried to update metadata of an invalid mod!");
+ return;
+ }
+
+ // Ensure the corresponding mod's info exists, and create it if not
+
+ auto normalized_fname = indexFileName(mod.slug);
+ auto real_fname = getRealIndexName(index_dir, normalized_fname);
+
+ QFile index_file(index_dir.absoluteFilePath(real_fname));
+
+ if (real_fname != normalized_fname)
+ index_file.rename(normalized_fname);
+
+ // Logic: Backup existing metadata instead of deleting it, allowing recovery.
+ if (index_file.exists())
+ {
+ try
+ {
+ auto old_metadata = toml::parse_file(index_file.fileName().toStdString());
+ auto version_node = old_metadata.at_path("update.flame.file-id");
+
+ if (version_node)
+ {
+ if (auto* str_node = version_node.as_string())
+ {
+ const auto old_version_id = QString::fromStdString(str_node->get());
+
+ if (old_version_id != mod.file_id.toString())
+ {
+ // Ask user for confirmation if callback is provided
+ if (confirmOverride)
+ {
+ bool shouldOverride = confirmOverride(old_version_id, mod.file_id.toString(), mod.name);
+ if (!shouldOverride)
+ {
+ qDebug() << "User cancelled override for mod" << mod.name;
+ return;
+ }
+ }
+
+ qDebug() << "Updating existing mod" << mod.name << "from version" << old_version_id << "to"
+ << mod.file_id.toString();
+ index_file.remove();
+ }
+ else
+ {
+ qDebug() << "Mod" << mod.name << "is already up to date, skipping";
+ return;
+ }
+ }
+ else
+ {
+ // Node exists but isn't a string -> metadata is weird; recreate safely
+ qWarning() << "Existing Packwiz metadata has non-string update.flame.file-id for" << mod.name
+ << "- recreating";
+ index_file.remove();
+ }
+ }
+ }
+ catch (const toml::parse_error&)
+ {
+ // If parsing fails, just remove and recreate
+ qWarning() << "Failed to parse existing mod index, recreating";
+ index_file.remove();
+ }
+ }
+ else
+ {
+ FS::ensureFilePathExists(index_file.fileName());
+ }
+
+#ifdef Q_OS_WIN32
+ // `.index` is an internal metadata directory and should be hidden on Windows.
+ // This covers paths that create it implicitly (e.g. via `ensureFilePathExists`) instead of via
+ // LocalResourceUpdateTask.
+ SetFileAttributesW(index_dir.path().toStdWString().c_str(),
+ FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
+#endif
+
+ toml::table update;
+ switch (mod.provider)
+ {
+ case (ModPlatform::ResourceProvider::FLAME):
+ if (mod.file_id.toInt() == 0 || mod.project_id.toInt() == 0)
+ {
+ qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname);
+ return;
+ }
+ update = toml::table{
+ { "file-id", mod.file_id.toInt() },
+ { "project-id", mod.project_id.toInt() },
+ };
+ break;
+ case (ModPlatform::ResourceProvider::MODRINTH):
+ if (mod.mod_id().toString().isEmpty() || mod.version().toString().isEmpty())
+ {
+ qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname);
+ return;
+ }
+ update = toml::table{
+ { "mod-id", mod.mod_id().toString().toStdString() },
+ { "version", mod.version().toString().toStdString() },
+ };
+ break;
+ }
+
+ toml::array loaders;
+ for (auto loader : ModPlatform::modLoaderTypesToList(mod.loaders))
+ {
+ loaders.push_back(getModLoaderAsString(loader).toStdString());
+ }
+ toml::array mcVersions;
+ for (auto version : mod.mcVersions)
+ {
+ mcVersions.push_back(version.toStdString());
+ }
+
+ if (!index_file.open(QIODevice::ReadWrite))
+ {
+ qCritical() << QString("Could not open file %1!").arg(normalized_fname);
+ return;
+ }
+
+ // Put TOML data into the file
+ QTextStream in_stream(&index_file);
+ {
+ auto tbl =
+ toml::table{ { "name", mod.name.toStdString() },
+ { "filename", mod.filename.toStdString() },
+ { "side", ModPlatform::SideUtils::toString(mod.side).toStdString() },
+ { "x-projtlauncher-loaders", loaders },
+ { "x-projtlauncher-mc-versions", mcVersions },
+ { "x-projtlauncher-release-type", mod.releaseType.toString().toStdString() },
+ { "x-projtlauncher-version-number", mod.version_number.toStdString() },
+ { "download",
+ toml::table{
+ { "mode", mod.mode.toStdString() },
+ { "url", mod.url.toString().toStdString() },
+ { "hash-format", mod.hash_format.toStdString() },
+ { "hash", mod.hash.toStdString() },
+ } },
+ { "update",
+ toml::table{ { ModPlatform::ProviderCapabilities::name(mod.provider), update } } } };
+ std::stringstream ss;
+ ss << tbl;
+ in_stream << QString::fromStdString(ss.str());
+ }
+
+ index_file.flush();
+ index_file.close();
+ }
+
+ void V1::deleteModIndex(const QDir& index_dir, QString& mod_slug)
+ {
+ auto normalized_fname = indexFileName(mod_slug);
+ auto real_fname = getRealIndexName(index_dir, normalized_fname);
+ if (real_fname.isEmpty())
+ return;
+
+ QFile index_file(index_dir.absoluteFilePath(real_fname));
+
+ if (!index_file.exists())
+ {
+ qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_slug);
+ return;
+ }
+
+ if (!index_file.remove())
+ {
+ qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_slug);
+ }
+ }
+
+ auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod
+ {
+ Mod mod;
+
+ auto normalized_fname = indexFileName(slug);
+ auto real_fname = getRealIndexName(index_dir, normalized_fname, true);
+ if (real_fname.isEmpty())
+ return {};
+
+ toml::table table;
+#if TOML_EXCEPTIONS
+ try
+ {
+ table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname)));
+ }
+ catch (const toml::parse_error& err)
+ {
+ qWarning() << QString("Could not open file %1!").arg(normalized_fname);
+ qWarning() << "Reason: " << QString(err.what());
+ return {};
+ }
+#else
+ toml::parse_result result = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname)));
+ if (!result)
+ {
+ qWarning() << QString("Could not open file %1!").arg(normalized_fname);
+ qWarning() << "Reason: " << result.error().description();
+ return {};
+ }
+ table = result.table();
+#endif
+
+ // index_file.close();
+
+ mod.slug = slug;
+
+ { // Basic info
+ mod.name = stringEntry(table, "name");
+ mod.filename = stringEntry(table, "filename");
+ mod.side = ModPlatform::SideUtils::fromString(stringEntry(table, "side"));
+ mod.releaseType = ModPlatform::IndexedVersionType(table["x-projtlauncher-release-type"].value_or(""));
+ if (auto loaders = table["x-projtlauncher-loaders"]; loaders && loaders.is_array())
+ {
+ for (auto&& loader : *loaders.as_array())
+ {
+ if (loader.is_string())
+ {
+ mod.loaders |= ModPlatform::getModLoaderFromString(
+ QString::fromStdString(loader.as_string()->value_or("")));
+ }
+ }
+ }
+ if (auto versions = table["x-projtlauncher-mc-versions"]; versions && versions.is_array())
+ {
+ for (auto&& version : *versions.as_array())
+ {
+ if (version.is_string())
+ {
+ auto ver = QString::fromStdString(version.as_string()->value_or(""));
+ if (!ver.isEmpty())
+ {
+ mod.mcVersions << ver;
+ }
+ }
+ }
+ sortMinecraftVersionsDescending(mod.mcVersions);
+ }
+ }
+ mod.version_number = table["x-projtlauncher-version-number"].value_or("");
+
+ { // [download] info
+ auto download_table = table["download"].as_table();
+ if (!download_table)
+ {
+ qCritical() << QString("No [download] section found on mod metadata!");
+ return {};
+ }
+
+ mod.mode = stringEntry(*download_table, "mode");
+ mod.url = stringEntry(*download_table, "url");
+ mod.hash_format = stringEntry(*download_table, "hash-format");
+ mod.hash = stringEntry(*download_table, "hash");
+ }
+
+ { // [update] info
+ using Provider = ModPlatform::ResourceProvider;
+
+ auto update_table = table["update"];
+ if (!update_table || !update_table.is_table())
+ {
+ qCritical() << QString("No [update] section found on mod metadata!");
+ return {};
+ }
+
+ toml::table* mod_provider_table = nullptr;
+ if ((mod_provider_table =
+ update_table[ModPlatform::ProviderCapabilities::name(Provider::FLAME)].as_table()))
+ {
+ mod.provider = Provider::FLAME;
+ mod.file_id = intEntry(*mod_provider_table, "file-id");
+ mod.project_id = intEntry(*mod_provider_table, "project-id");
+ }
+ else if ((mod_provider_table =
+ update_table[ModPlatform::ProviderCapabilities::name(Provider::MODRINTH)].as_table()))
+ {
+ mod.provider = Provider::MODRINTH;
+ mod.mod_id() = stringEntry(*mod_provider_table, "mod-id");
+ mod.version() = stringEntry(*mod_provider_table, "version");
+ }
+ else
+ {
+ qCritical() << QString("No mod provider on mod metadata!");
+ return {};
+ }
+ }
+
+ return mod;
+ }
+
+ auto V1::getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod
+ {
+ for (auto& file_name : index_dir.entryList(QDir::Filter::Files))
+ {
+ auto mod = getIndexForMod(index_dir, file_name);
+
+ if (mod.mod_id() == mod_id)
+ return mod;
+ }
+
+ return {};
+ }
+
+} // namespace Packwiz
diff --git a/archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.h b/archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.h
new file mode 100644
index 0000000000..a0c136f785
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.h
@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include "modplatform/ModIndex.h"
+
+#include <QString>
+#include <QUrl>
+#include <QVariant>
+
+class QDir;
+
+namespace Packwiz
+{
+
+ auto getRealIndexName(const QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString;
+
+ class V1
+ {
+ public:
+ // can also represent other resources beside loader mods - but this is what packwiz calls it
+ struct Mod
+ {
+ QString slug{};
+ QString name{};
+ QString filename{};
+ ModPlatform::Side side{ ModPlatform::Side::UniversalSide };
+ ModPlatform::ModLoaderTypes loaders;
+ QStringList mcVersions;
+ ModPlatform::IndexedVersionType releaseType;
+
+ // [download]
+ QString mode{};
+ QUrl url{};
+ QString hash_format{};
+ QString hash{};
+
+ // [update]
+ ModPlatform::ResourceProvider provider{};
+ QVariant file_id{};
+ QVariant project_id{};
+ QString version_number{};
+
+ public:
+ // This is a totally heuristic, but should work for now.
+ auto isValid() const -> bool
+ {
+ return !slug.isEmpty() && !project_id.isNull();
+ }
+
+ // Different providers can use different names for the same thing
+ // Modrinth-specific
+ auto mod_id() -> QVariant&
+ {
+ return project_id;
+ }
+ auto version() -> QVariant&
+ {
+ return file_id;
+ }
+ };
+
+ /* Generates the object representing the information in a mod.pw.toml file via
+ * its common representation in the launcher, when downloading mods.
+ * */
+ static auto createModFormat(const QDir& index_dir,
+ ModPlatform::IndexedPack& mod_pack,
+ ModPlatform::IndexedVersion& mod_version) -> Mod;
+
+ /* Updates the mod index for the provided mod.
+ * This creates a new index if one does not exist already.
+ *
+ * If a mod with the same slug already exists:
+ * - If version differs: old metadata is removed and new one is created
+ * - If version is the same: update is skipped (mod is already up to date)
+ *
+ * The optional callback can be used by UI code to prompt user for override confirmation.
+ * If callback returns false, the update is cancelled.
+ * */
+ static void updateModIndex(const QDir& index_dir, Mod& mod);
+
+ /** Same as updateModIndex but with override confirmation callback.
+ * @param confirmOverride Called when an existing different version is found.
+ * Receives (old_version, new_version, mod_name). Return true to proceed.
+ */
+ static void updateModIndex(const QDir& index_dir,
+ Mod& mod,
+ std::function<bool(const QString&, const QString&, const QString&)> confirmOverride);
+
+ /* Deletes the metadata for the mod with the given slug. If the metadata doesn't exist, it does nothing. */
+ static void deleteModIndex(const QDir& index_dir, QString& mod_slug);
+
+ /* Gets the metadata for a mod with a particular file name.
+ * If the mod doesn't have a metadata, it simply returns an empty Mod object.
+ * */
+ static auto getIndexForMod(const QDir& index_dir, QString slug) -> Mod;
+
+ /* Gets the metadata for a mod with a particular id.
+ * If the mod doesn't have a metadata, it simply returns an empty Mod object.
+ * */
+ static auto getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod;
+ };
+
+} // namespace Packwiz
diff --git a/archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
new file mode 100644
index 0000000000..ae50295a4d
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
@@ -0,0 +1,185 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 "FileSystem.h"
+#include "MMCZip.h"
+#include "TechnicPackProcessor.h"
+
+#include "Application.h"
+
+#include "net/ApiDownload.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.reset(new NetJob(tr("Modpack download"), APPLICATION->network()));
+ m_filesNetJob->addNetAction(Net::ApiDownload::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::stepProgress, this, &Technic::SingleZipPackInstallTask::propagateStepProgress);
+ 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;
+
+ // open the zip and find relevant files in it
+ m_packZip.reset(new QuaZip(m_archivePath));
+ if (!m_packZip->open(QuaZip::mdUnzip))
+ {
+ emitFailed(tr("Unable to open supplied modpack zip file."));
+ return;
+ }
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(),
+ MMCZip::extractSubDir,
+ m_packZip.get(),
+ QString(""),
+ extractDir.absolutePath());
+ 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()
+{
+ m_packZip.reset();
+ 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;
+ }
+ }
+ }
+
+ auto packProcessor = makeShared<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, name(), m_instIcon, m_stagingPath, m_minecraftVersion);
+}
+
+void Technic::SingleZipPackInstallTask::extractAborted()
+{
+ emitFailed(tr("Instance import has been aborted."));
+}
diff --git a/archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.h b/archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.h
new file mode 100644
index 0000000000..74ecd8801c
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.h
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 <quazip/quazip.h>
+
+#include <QFutureWatcher>
+#include <QStringList>
+#include <QUrl>
+
+#include <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;
+ std::unique_ptr<QuaZip> m_packZip;
+ QFuture<std::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
+ };
+
+} // namespace Technic
diff --git a/archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.cpp b/archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.cpp
new file mode 100644
index 0000000000..9caca4be9f
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.cpp
@@ -0,0 +1,276 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2021-2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <MMCZip.h>
+#include <QtConcurrentRun>
+
+#include "SolderPackManifest.h"
+#include "TechnicPackProcessor.h"
+#include "net/ApiDownload.h"
+#include "net/ChecksumValidator.h"
+
+Technic::SolderPackInstallTask::SolderPackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network,
+ const QUrl& solderUrl,
+ const QString& pack,
+ const QString& version,
+ const QString& minecraftVersion)
+{
+ m_solderUrl = solderUrl;
+ m_pack = pack;
+ m_version = version;
+ m_network = network;
+ m_minecraftVersion = minecraftVersion;
+}
+
+bool Technic::SolderPackInstallTask::abort()
+{
+ if (m_abortable)
+ {
+ return m_filesNetJob->abort();
+ }
+ return false;
+}
+
+void Technic::SolderPackInstallTask::executeTask()
+{
+ setStatus(tr("Resolving modpack files"));
+
+ m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"), m_network));
+ auto sourceUrl = QString("%1/modpack/%2/%3").arg(m_solderUrl.toString(), m_pack, m_version);
+ m_filesNetJob->addNetAction(Net::ApiDownload::makeByteArray(sourceUrl, m_response));
+
+ auto job = m_filesNetJob.get();
+ connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded);
+ connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
+ connect(job, &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted);
+ m_filesNetJob->start();
+}
+
+void Technic::SolderPackInstallTask::fileListSucceeded()
+{
+ setStatus(tr("Downloading modpack"));
+
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*m_response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *m_response;
+ return;
+ }
+ auto obj = doc.object();
+
+ TechnicSolder::PackBuild build;
+ try
+ {
+ TechnicSolder::loadPackBuild(build, obj);
+ }
+ catch (const JSONValidationError& e)
+ {
+ emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
+ m_filesNetJob.reset();
+ return;
+ }
+
+ if (!build.minecraft.isEmpty())
+ m_minecraftVersion = build.minecraft;
+
+ m_filesNetJob.reset(new NetJob(tr("Downloading modpack"), m_network));
+
+ int i = 0;
+ for (const auto& mod : build.mods)
+ {
+ auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
+
+ auto dl = Net::ApiDownload::makeFile(mod.url, path);
+ if (!mod.md5.isEmpty())
+ {
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5));
+ }
+ m_filesNetJob->addNetAction(dl);
+
+ i++;
+ }
+
+ m_modCount = build.mods.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::stepProgress, this, &Technic::SolderPackInstallTask::propagateStepProgress);
+ connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
+ connect(m_filesNetJob.get(), &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted);
+ 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::downloadAborted()
+{
+ emitAborted();
+ m_filesNetJob.reset();
+}
+
+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;
+ }
+ }
+ }
+
+ auto packProcessor = makeShared<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, name(), m_instIcon, m_stagingPath, m_minecraftVersion, true);
+}
+
+void Technic::SolderPackInstallTask::extractAborted()
+{
+ emitFailed(tr("Instance import has been aborted."));
+}
diff --git a/archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.h b/archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.h
new file mode 100644
index 0000000000..b2d37fe3ad
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.h
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2021-2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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>
+#include <memory>
+
+namespace Technic
+{
+ class SolderPackInstallTask : public InstanceTask
+ {
+ Q_OBJECT
+ public:
+ explicit SolderPackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network,
+ const QUrl& solderUrl,
+ const QString& pack,
+ const QString& version,
+ const QString& minecraftVersion);
+
+ bool canAbort() const override
+ {
+ return true;
+ }
+ bool abort() override;
+
+ protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+
+ private slots:
+ void fileListSucceeded();
+ void downloadSucceeded();
+ void downloadFailed(QString reason);
+ void downloadProgressChanged(qint64 current, qint64 total);
+ void downloadAborted();
+ void extractFinished();
+ void extractAborted();
+
+ private:
+ bool m_abortable = false;
+
+ shared_qobject_ptr<QNetworkAccessManager> m_network;
+
+ NetJob::Ptr m_filesNetJob;
+ QUrl m_solderUrl;
+ QString m_pack;
+ QString m_version;
+ QString m_minecraftVersion;
+ std::shared_ptr<QByteArray> m_response = std::make_shared<QByteArray>();
+ QTemporaryDir m_outputDir;
+ int m_modCount;
+ QFuture<bool> m_extractFuture;
+ QFutureWatcher<bool> m_extractFutureWatcher;
+ };
+} // namespace Technic
diff --git a/archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.cpp b/archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.cpp
new file mode 100644
index 0000000000..9f5b3c56fc
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.cpp
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#include "SolderPackManifest.h"
+
+#include "Json.h"
+
+namespace TechnicSolder
+{
+
+ void loadPack(Pack& v, QJsonObject& obj)
+ {
+ v.recommended = Json::requireString(obj, "recommended");
+ v.latest = Json::requireString(obj, "latest");
+
+ auto builds = Json::requireArray(obj, "builds");
+ for (const auto buildRaw : builds)
+ {
+ auto build = Json::requireString(buildRaw);
+ v.builds.append(build);
+ }
+ }
+
+ static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj)
+ {
+ b.name = Json::requireString(obj, "name");
+ b.version = Json::ensureString(obj, "version", "");
+ b.md5 = Json::requireString(obj, "md5");
+ b.url = Json::requireString(obj, "url");
+ }
+
+ void loadPackBuild(PackBuild& v, QJsonObject& obj)
+ {
+ v.minecraft = Json::requireString(obj, "minecraft");
+
+ auto mods = Json::requireArray(obj, "mods");
+ for (const auto modRaw : mods)
+ {
+ auto modObj = Json::requireObject(modRaw);
+ PackBuildMod mod;
+ loadPackBuildMod(mod, modObj);
+ v.mods.append(mod);
+ }
+ }
+
+} // namespace TechnicSolder
diff --git a/archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.h b/archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.h
new file mode 100644
index 0000000000..ee10179950
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.h
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ *
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * ======================================================================== */
+
+#pragma once
+
+#include <QJsonObject>
+#include <QList>
+#include <QString>
+
+namespace TechnicSolder
+{
+
+ struct Pack
+ {
+ QString recommended;
+ QString latest;
+ QList<QString> builds;
+ };
+
+ void loadPack(Pack& v, QJsonObject& obj);
+
+ struct PackBuildMod
+ {
+ QString name;
+ QString version;
+ QString md5;
+ QString url;
+ };
+
+ struct PackBuild
+ {
+ QString minecraft;
+ QList<PackBuildMod> mods;
+ };
+
+ void loadPackBuild(PackBuild& v, QJsonObject& obj);
+
+} // namespace TechnicSolder
diff --git a/archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.cpp b/archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.cpp
new file mode 100644
index 0000000000..753a1910f4
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.cpp
@@ -0,0 +1,277 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 <quazip/quazip.h>
+#include <quazip/quazipdir.h>
+#include <quazip/quazipfile.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,
+ [[maybe_unused]] const bool isSolder)
+{
+ QString minecraftPath = FS::PathCombine(stagingPath, "minecraft");
+ QString configPath = FS::PathCombine(stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
+ 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))
+ {
+ QuaZip zipFile(modpackJar);
+ if (!zipFile.open(QuaZip::mdUnzip))
+ {
+ emit failed(tr("Unable to open \"bin/modpack.jar\" file!"));
+ return;
+ }
+ QuaZipDir zipFileRoot(&zipFile, "/");
+ if (zipFileRoot.exists("/version.json"))
+ {
+ if (zipFileRoot.exists("/fmlversion.properties"))
+ {
+ zipFile.setCurrentFile("fmlversion.properties");
+ QuaZipFile file(&zipFile);
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ emit failed(tr("Unable to open \"fmlversion.properties\"!"));
+ return;
+ }
+ QByteArray fmlVersionData = file.readAll();
+ file.close();
+ INIFile iniFile;
+ iniFile.loadFile(fmlVersionData);
+ // If not present, this evaluates to a null string
+ fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString();
+ }
+ zipFile.setCurrentFile("version.json", QuaZip::csSensitive);
+ QuaZipFile file(&zipFile);
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ emit failed(tr("Unable to open \"version.json\"!"));
+ return;
+ }
+ data = file.readAll();
+ file.close();
+ }
+ else
+ {
+ if (minecraftVersion.isEmpty())
+ {
+ emit failed(
+ tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown"));
+ return;
+ }
+ 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 (zipFileRoot.exists("/forgeversion.properties"))
+ {
+ zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive);
+ QuaZipFile file(&zipFile);
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ // Really shouldn't happen, but error handling shall not be forgotten
+ emit failed(tr("Unable to open \"forgeversion.properties\""));
+ return;
+ }
+ QByteArray forgeVersionData = file.readAll();
+ file.close();
+ 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
+ components->setComponentVersion("net.minecraft", minecraftVersion, true);
+ components->saveNow();
+ emit succeeded();
+ return;
+ }
+
+ try
+ {
+ QJsonDocument doc = Json::requireDocument(data);
+ QJsonObject root = Json::requireObject(doc, "version.json");
+ QString packMinecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), "");
+ if (packMinecraftVersion.isEmpty())
+ {
+ if (fmlMinecraftVersion.isEmpty())
+ {
+ emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing"));
+ return;
+ }
+ packMinecraftVersion = fmlMinecraftVersion;
+ }
+ components->setComponentVersion("net.minecraft", packMinecraftVersion, 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.neoforged.fancymodloader:"))
+ { // it is neoforge
+ // no easy way to get the version from the libs so use the arguments
+ auto arguments = Json::ensureObject(root, "arguments", {});
+ bool isVersionArg = false;
+ QString neoforgeVersion;
+ for (auto arg : Json::ensureArray(arguments, "game", {}))
+ {
+ auto argument = Json::ensureString(arg, "");
+ if (isVersionArg)
+ {
+ neoforgeVersion = argument;
+ break;
+ }
+ else
+ {
+ isVersionArg = "--fml.neoForgeVersion" == argument || "--fml.forgeVersion" == argument;
+ }
+ }
+ if (!neoforgeVersion.isEmpty())
+ {
+ components->setComponentVersion("net.neoforged", neoforgeVersion);
+ }
+ break;
+ }
+ else if ((libraryName.startsWith("net.minecraftforge:forge:")
+ || libraryName.startsWith("net.minecraftforge:fmlloader:"))
+ && 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));
+ }
+ break;
+ }
+ else
+ {
+ // <Technic library name prefix> -> <our component name>
+ static QMap<QString, QString> loaderMap{ { "net.minecraftforge:minecraftforge:", "net.minecraftforge" },
+ { "net.fabricmc:fabric-loader:",
+ "net.fabricmc.fabric-loader" },
+ { "org.quiltmc:quilt-loader:", "org.quiltmc.quilt-loader" } };
+ for (const auto& loader : loaderMap.keys())
+ {
+ if (libraryName.startsWith(loader))
+ {
+ components->setComponentVersion(loaderMap.value(loader), libraryName.section(':', 2));
+ break;
+ }
+ }
+ }
+ }
+ }
+ catch (const JSONValidationError& e)
+ {
+ emit failed(tr("Could not understand \"version.json\":\n") + e.cause());
+ return;
+ }
+
+ components->saveNow();
+ emit succeeded();
+}
diff --git a/archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.h b/archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.h
new file mode 100644
index 0000000000..bd9eeab814
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.h
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, version 3.
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ * 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 <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(),
+ bool isSolder = false);
+ };
+} // namespace Technic