/* SPDX-FileCopyrightText: 2026 Project Tick * SPDX-FileContributor: Project Tick * SPDX-License-Identifier: GPL-3.0-or-later * * MeshMC - A Custom Launcher for Minecraft * Copyright (C) 2026 Project Tick * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "PackInstallTask.h" #include #include "MMCZip.h" #include "BaseInstance.h" #include "FileSystem.h" #include "settings/INISettingsObject.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "minecraft/GradleSpecifier.h" #include "BuildConfig.h" #include "Application.h" namespace LegacyFTB { PackInstallTask::PackInstallTask( shared_qobject_ptr network, Modpack pack, QString version) { m_pack = pack; m_version = version; m_network = network; } void PackInstallTask::executeTask() { downloadPack(); } void PackInstallTask::downloadPack() { setStatus(tr("Downloading zip for %1").arg(m_pack.name)); auto packoffset = QString("%1/%2/%3") .arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); auto entry = APPLICATION->metacache()->resolveEntry("FTBPacks", packoffset); netJobContainer = new NetJob("Download FTB Pack", m_network); entry->setStale(true); QString url; if (m_pack.type == PackType::Private) { url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1") .arg(packoffset); } else { url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1") .arg(packoffset); } netJobContainer->addNetAction(Net::Download::makeCached(url, entry)); archivePath = entry->getFullPath(); connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); connect(netJobContainer.get(), &NetJob::progress, this, &PackInstallTask::onDownloadProgress); netJobContainer->start(); progress(1, 4); } void PackInstallTask::onDownloadSucceeded() { abortable = false; unzip(); } void PackInstallTask::onDownloadFailed(QString reason) { abortable = false; emitFailed(reason); } void PackInstallTask::onDownloadProgress(qint64 current, qint64 total) { abortable = true; progress(current, total * 4); setStatus(tr("Downloading zip for %1 (%2%)") .arg(m_pack.name) .arg(current / 10)); } void PackInstallTask::unzip() { progress(2, 4); setStatus(tr("Extracting modpack")); QDir extractDir(m_stagingPath); QString extractPath = extractDir.absolutePath() + "/unzip"; QString archivePathCopy = archivePath; m_extractFuture = QtConcurrent::run( QThreadPool::globalInstance(), [archivePathCopy, extractPath]() { return MMCZip::extractDir(archivePathCopy, extractPath); }); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onUnzipFinished); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::onUnzipCanceled); m_extractFutureWatcher.setFuture(m_extractFuture); } void PackInstallTask::onUnzipFinished() { install(); } void PackInstallTask::onUnzipCanceled() { emitAborted(); } void PackInstallTask::install() { progress(3, 4); setStatus(tr("Installing modpack")); QDir unzipMcDir(m_stagingPath + "/unzip/minecraft"); if (unzipMcDir.exists()) { // ok, found minecraft dir, move contents to instance dir if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft")) { emitFailed(tr("Failed to move unzipped minecraft!")); return; } } QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(instanceConfigPath); instanceSettings->suspendSave(); instanceSettings->registerSetting("InstanceType", "Legacy"); instanceSettings->set("InstanceType", "OneSix"); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); auto components = instance.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); bool fallback = true; // handle different versions QFile packJson(m_stagingPath + "/.minecraft/pack.json"); QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods"); if (packJson.exists()) { if (!packJson.open(QIODevice::ReadOnly | QIODevice::Text)) { emitFailed(tr("Failed to open pack.json!")); return; } QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll()); packJson.close(); // we only care about the libs QJsonArray libs = doc.object().value("libraries").toArray(); foreach (const QJsonValue& value, libs) { QString nameValue = value.toObject().value("name").toString(); if (!nameValue.startsWith("net.minecraftforge")) { continue; } GradleSpecifier forgeVersion(nameValue); components->setComponentVersion( "net.minecraftforge", forgeVersion.version() .replace(m_pack.mcVersion, "") .replace("-", "")); packJson.remove(); fallback = false; break; } } if (jarmodDir.exists()) { qDebug() << "Found jarmods, installing..."; QStringList jarmods; for (auto info : jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) { qDebug() << "Jarmod:" << info.fileName(); jarmods.push_back(info.absoluteFilePath()); } components->installJarMods(jarmods); fallback = false; } // just nuke unzip directory, it s not needed anymore FS::deletePath(m_stagingPath + "/unzip"); if (fallback) { // TODO: Some fallback mechanism... or just keep failing! emitFailed(tr("No installation method found!")); return; } components->saveNow(); progress(4, 4); instance.setName(m_instName); if (m_instIcon == "default") { m_instIcon = "ftb_logo"; } instance.setIconKey(m_instIcon); instanceSettings->resumeSave(); emitSucceeded(); } bool PackInstallTask::abort() { if (abortable) { return netJobContainer->abort(); } return false; } } // namespace LegacyFTB