diff options
Diffstat (limited to 'archived/projt-launcher/launcher/InstanceImportTask.cpp')
| -rw-r--r-- | archived/projt-launcher/launcher/InstanceImportTask.cpp | 648 |
1 files changed, 648 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/InstanceImportTask.cpp b/archived/projt-launcher/launcher/InstanceImportTask.cpp new file mode 100644 index 0000000000..3a49d924d9 --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceImportTask.cpp @@ -0,0 +1,648 @@ +// 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) 2022 flowln <flowlnlnln@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. + * + ======================================================================== */ + +#include "InstanceImportTask.h" + +#include <QFuture> +#include <QString> +#include <QUrl> + +#include "Application.h" +#include "FileSystem.h" +#include "Json.h" +#include "MMCZip.h" +#include "NullInstance.h" + +#include "QObjectPtr.h" +#include "icons/IconList.hpp" +#include "icons/IconUtils.hpp" + +#include "modplatform/flame/FlameInstanceCreationTask.h" +#include "modplatform/modrinth/ModrinthInstanceCreationTask.h" +#include "modplatform/technic/TechnicPackProcessor.h" + +#include "settings/INISettingsObject.h" +#include "tasks/Task.h" + +#include "net/ApiDownload.h" + +#include <QtConcurrentRun> +#include <algorithm> +#include <memory> + +#include <quazip/quazipdir.h> + +InstanceImportTask::InstanceImportTask(const QUrl& sourceUrl, QWidget* parent, QMap<QString, QString>&& extra_info) + : m_sourceUrl(sourceUrl), + m_extra_info(extra_info) +{} + +bool InstanceImportTask::abort() +{ + if (!canAbort()) + return false; + + bool wasAborted = false; + if (m_task) + wasAborted = m_task->abort(); + return wasAborted; +} + +void InstanceImportTask::executeTask() +{ + setAbortable(true); + + if (m_sourceUrl.isLocalFile()) + { + m_archivePath = m_sourceUrl.toLocalFile(); + processZipPack(); + } + else + { + setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); + + downloadFromUrl(); + } +} + +void InstanceImportTask::downloadFromUrl() +{ + const QString path(m_sourceUrl.host() + '/' + m_sourceUrl.path()); + + auto entry = APPLICATION->metacache()->resolveEntry("general", path); + entry->setStale(true); + m_archivePath = entry->getFullPath(); + + auto filesNetJob = makeShared<NetJob>(tr("Modpack download"), APPLICATION->network()); + filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry)); + + connect(filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::processZipPack); + connect(filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::setProgress); + connect(filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress); + connect(filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::emitFailed); + connect(filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::emitAborted); + m_task.reset(filesNetJob); + filesNetJob->start(); +} + +QString InstanceImportTask::getRootFromZip(QuaZip* zip, const QString& root) +{ + if (!isRunning()) + { + return {}; + } + QuaZipDir rootDir(zip, root); + for (auto&& fileName : rootDir.entryList(QDir::Files)) + { + setDetails(fileName); + if (fileName == "instance.cfg") + { + qDebug() << "MultiMC:" << true; + m_modpackType = ModpackType::MultiMC; + return root; + } + if (fileName == "manifest.json") + { + qDebug() << "Flame:" << true; + m_modpackType = ModpackType::Flame; + return root; + } + + QCoreApplication::processEvents(); + } + + // Recurse the search to non-ignored subfolders + for (auto&& fileName : rootDir.entryList(QDir::Dirs)) + { + if ("overrides/" == fileName) + continue; + + QString result = getRootFromZip(zip, root + fileName); + if (!result.isEmpty()) + return result; + } + + return {}; +} + +void InstanceImportTask::processZipPack() +{ + setStatus(tr("Attempting to determine instance type")); + QDir extractDir(m_stagingPath); + qDebug() << "Attempting to create instance from" << m_archivePath; + + // open the zip and find relevant files in it + auto packZip = std::make_shared<QuaZip>(m_archivePath); + if (!packZip->open(QuaZip::mdUnzip)) + { + emitFailed(tr("Unable to open supplied modpack zip file.")); + return; + } + + QuaZipDir packZipDir(packZip.get()); + qDebug() << "Attempting to determine instance type"; + + QString root; + + // NOTE: Prioritize modpack platforms that aren't searched for recursively. + // Especially Flame has a very common filename for its manifest, which may appear inside overrides for example + // https://docs.modrinth.com/docs/modpacks/format_definition/#storage + if (packZipDir.exists("/modrinth.index.json")) + { + // process as Modrinth pack + qDebug() << "Modrinth:" << true; + m_modpackType = ModpackType::Modrinth; + } + else if (packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json")) + { + // process as Technic pack + qDebug() << "Technic:" << true; + extractDir.mkpath("minecraft"); + extractDir.cd("minecraft"); + m_modpackType = ModpackType::Technic; + } + else + { + root = getRootFromZip(packZip.get()); + setDetails(""); + } + if (m_modpackType == ModpackType::Unknown) + { + emitFailed(tr("Archive does not contain a recognized modpack type.")); + return; + } + setStatus(tr("Extracting modpack")); + + // make sure we extract just the pack + auto zipTask = makeShared<MMCZip::ExtractZipTask>(packZip, extractDir, root); + + 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, &InstanceImportTask::extractFinished, Qt::QueuedConnection); + connect(zipTask.get(), &Task::aborted, this, &InstanceImportTask::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, &InstanceImportTask::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); + }); + connect(zipTask.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); + m_task.reset(zipTask); + zipTask->start(); +} + +void InstanceImportTask::extractFinished() +{ + setAbortable(false); + 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; + } + } + } + + switch (m_modpackType) + { + case ModpackType::MultiMC: processMultiMC(); return; + case ModpackType::Technic: processTechnic(); return; + case ModpackType::Flame: processFlame(); return; + case ModpackType::Modrinth: processModrinth(); return; + case ModpackType::Unknown: emitFailed(tr("Archive does not contain a recognized modpack type.")); return; + } +} + +bool installIcon(QString root, QString instIconKey) +{ + auto importIconPath = projt::icons::findBestIconIn(root, instIconKey); + if (importIconPath.isNull() || !QFile::exists(importIconPath)) + importIconPath = projt::icons::findBestIconIn(root, "icon.png"); + if (importIconPath.isNull() || !QFile::exists(importIconPath)) + importIconPath = projt::icons::findBestIconIn(FS::PathCombine(root, "overrides"), "icon.png"); + if (!importIconPath.isNull() && QFile::exists(importIconPath)) + { + // import icon + auto iconList = APPLICATION->icons(); + if (iconList->iconFileExists(instIconKey)) + { + iconList->deleteIcon(instIconKey); + } + iconList->installIcon(importIconPath, instIconKey + ".png"); + return true; + } + return false; +} + +void InstanceImportTask::processFlame() +{ + shared_qobject_ptr<FlameCreationTask> inst_creation_task = nullptr; + if (!m_extra_info.isEmpty()) + { + auto pack_id_it = m_extra_info.constFind("pack_id"); + Q_ASSERT(pack_id_it != m_extra_info.constEnd()); + auto pack_id = pack_id_it.value(); + + auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); + Q_ASSERT(pack_version_id_it != m_extra_info.constEnd()); + auto pack_version_id = pack_version_id_it.value(); + + QString original_instance_id; + auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); + if (original_instance_id_it != m_extra_info.constEnd()) + original_instance_id = original_instance_id_it.value(); + + inst_creation_task = makeShared<FlameCreationTask>(m_stagingPath, + m_globalSettings, + nullptr, + pack_id, + pack_version_id, + original_instance_id); + } + else + { + // Extract pack IDs from manifest.json for direct ZIP imports + QString pack_id; + QString pack_version_id; + + const auto manifestPath = FS::PathCombine(m_stagingPath, "manifest.json"); + if (QFile::exists(manifestPath)) + { + try + { + auto doc = Json::requireDocument(manifestPath); + auto obj = doc.object(); + + if (obj.contains("projectID")) + { + pack_id = QString::number(obj.value("projectID").toInt()); + } + if (obj.contains("fileID")) + { + pack_version_id = QString::number(obj.value("fileID").toInt()); + } + } + catch (const Exception& e) + { + qWarning() << "Failed to parse manifest.json for ID extraction:" << e.cause(); + } + catch (...) + { + qWarning() << "Failed to parse manifest.json for ID extraction (unknown error)"; + } + } + + inst_creation_task = + makeShared<FlameCreationTask>(m_stagingPath, m_globalSettings, nullptr, pack_id, pack_version_id); + } + + inst_creation_task->setName(*this); + // if the icon was specified by user, use that. otherwise pull icon from the pack + if (m_instIcon == "default") + { + auto iconKey = QString("Flame_%1_Icon").arg(name()); + + if (installIcon(m_stagingPath, iconKey)) + { + m_instIcon = iconKey; + } + } + inst_creation_task->setIcon(m_instIcon); + inst_creation_task->setGroup(m_instGroup); + inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); + + auto weak = inst_creation_task.toWeakRef(); + connect(inst_creation_task.get(), + &Task::succeeded, + this, + [this, weak] + { + if (auto sp = weak.lock()) + { + setOverride(sp->shouldOverride(), sp->originalInstanceID()); + } + emitSucceeded(); + }); + connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); + connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress); + connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); + connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); + connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); + + connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); + connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); + connect(inst_creation_task.get(), &Task::abortButtonTextChanged, this, &Task::setAbortButtonText); + + connect(inst_creation_task.get(), + &Task::warningLogged, + this, + [this](const QString& line) { m_Warnings.append(line); }); + + m_task.reset(inst_creation_task); + setAbortable(true); + m_task->start(); +} + +void InstanceImportTask::processTechnic() +{ + shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor{ new Technic::TechnicPackProcessor }; + connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed); + packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath); +} + +void InstanceImportTask::processMultiMC() +{ + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared<INISettingsObject>(configPath); + + NullInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + + // reset time played on import... because packs. + instance.resetTimePlayed(); + + // set a new nice name + instance.setName(name()); + + // if the icon was specified by user, use that. otherwise pull icon from the pack + if (m_instIcon != "default") + { + instance.setIconKey(m_instIcon); + } + else + { + m_instIcon = instance.iconKey(); + + installIcon(instance.instanceRoot(), m_instIcon); + } + emitSucceeded(); +} + +void InstanceImportTask::processModrinth() +{ + shared_qobject_ptr<ModrinthCreationTask> inst_creation_task = nullptr; + if (!m_extra_info.isEmpty()) + { + auto pack_id_it = m_extra_info.constFind("pack_id"); + Q_ASSERT(pack_id_it != m_extra_info.constEnd()); + auto pack_id = pack_id_it.value(); + + QString pack_version_id; + auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); + if (pack_version_id_it != m_extra_info.constEnd()) + pack_version_id = pack_version_id_it.value(); + + QString original_instance_id; + auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); + if (original_instance_id_it != m_extra_info.constEnd()) + original_instance_id = original_instance_id_it.value(); + + inst_creation_task = makeShared<ModrinthCreationTask>(m_stagingPath, + m_globalSettings, + nullptr, + pack_id, + pack_version_id, + original_instance_id); + } + else + { + QString pack_id; + QString pack_version_id; + + // 1) Best source: modrinth.index.json inside staging + const auto indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + if (QFile::exists(indexPath)) + { + try + { + auto doc = Json::requireDocument(indexPath); + auto obj = doc.object(); + + if (obj.contains("projectId")) + { + pack_id = obj.value("projectId").toString(); + } + if (obj.contains("versionId")) + { + pack_version_id = obj.value("versionId").toString(); + } + } + catch (const Exception& e) + { + qWarning() << "Failed to read modrinth.index.json for pack IDs:" << e.cause(); + } + catch (...) + { + qWarning() << "Failed to read modrinth.index.json for pack IDs (unknown error)"; + } + } + + // 2) Fallback: derive pack_id from source URL or local filename + if (pack_id.isEmpty() && !m_sourceUrl.isEmpty()) + { + if (m_sourceUrl.isLocalFile()) + { + pack_id = QFileInfo(m_sourceUrl.toLocalFile()).baseName(); + } + else + { + static const QRegularExpression s_regex(R"(data\/([^\/]*)\/versions)"); + auto match = s_regex.match(m_sourceUrl.toString()); + if (match.hasMatch()) + { + pack_id = match.captured(1); + } + } + } + + // Extract version ID from modrinth.index.json for direct ZIP imports + // pack_version_id already declared above + // Attempt to read modrinth.index.json for metadata if available + try + { + QString indexFile = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + if (QFileInfo::exists(indexFile)) + { + auto doc = Json::requireDocument(indexFile); + auto obj = doc.object(); + // Valid Modrinth packs might not have projectID directly, but checking for it or 'name' matches + if (obj.contains("projectID")) + { + pack_id = QString::number(obj.value("projectID").toInt()); + } + else if (obj.contains("name") && pack_id.isEmpty()) + { + // Check if name contains ID-like pattern or use it as fallback? + // For now just keep existing heuristic if not found. + } + // Version extraction + if (obj.contains("versionId")) + { + pack_version_id = obj.value("versionId").toString(); + } + } + } + catch (...) + { + // Ignore format errors + } + + inst_creation_task = + makeShared<ModrinthCreationTask>(m_stagingPath, m_globalSettings, nullptr, pack_id, pack_version_id); + } + + inst_creation_task->setName(*this); + // if the icon was specified by user, use that. otherwise pull icon from the pack + if (m_instIcon == "default") + { + auto iconKey = QString("Modrinth_%1_Icon").arg(name()); + + if (installIcon(m_stagingPath, iconKey)) + { + m_instIcon = iconKey; + } + } + inst_creation_task->setIcon(m_instIcon); + inst_creation_task->setGroup(m_instGroup); + inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); + + auto weak = inst_creation_task.toWeakRef(); + connect(inst_creation_task.get(), + &Task::succeeded, + this, + [this, weak] + { + if (auto sp = weak.lock()) + { + setOverride(sp->shouldOverride(), sp->originalInstanceID()); + } + emitSucceeded(); + }); + connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); + connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress); + connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); + connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); + connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); + + connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); + connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); + connect(inst_creation_task.get(), &Task::abortButtonTextChanged, this, &Task::setAbortButtonText); + + connect(inst_creation_task.get(), + &Task::warningLogged, + this, + [this](const QString& line) { m_Warnings.append(line); }); + + m_task.reset(inst_creation_task); + setAbortable(true); + m_task->start(); +} |
