diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:51:45 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:51:45 +0300 |
| commit | d3261e64152397db2dca4d691a990c6bc2a6f4dd (patch) | |
| tree | fac2f7be638651181a72453d714f0f96675c2b8b /archived/projt-launcher/launcher/modplatform | |
| parent | 31b9a8949ed0a288143e23bf739f2eb64fdc63be (diff) | |
| download | Project-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')
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 |
