summaryrefslogtreecommitdiff
path: root/meshmc/launcher/InstanceImportTask.cpp
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
commit31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch)
tree8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/launcher/InstanceImportTask.cpp
parent934382c8a1ce738589dee9ee0f14e1cec812770e (diff)
parentfad6a1066616b69d7f5fef01178efdf014c59537 (diff)
downloadProject-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz
Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/launcher/InstanceImportTask.cpp')
-rw-r--r--meshmc/launcher/InstanceImportTask.cpp674
1 files changed, 674 insertions, 0 deletions
diff --git a/meshmc/launcher/InstanceImportTask.cpp b/meshmc/launcher/InstanceImportTask.cpp
new file mode 100644
index 0000000000..df569edc6c
--- /dev/null
+++ b/meshmc/launcher/InstanceImportTask.cpp
@@ -0,0 +1,674 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "InstanceImportTask.h"
+#include "BaseInstance.h"
+#include "FileSystem.h"
+#include "Application.h"
+#include "MMCZip.h"
+#include "NullInstance.h"
+#include "settings/INISettingsObject.h"
+#include "icons/IconUtils.h"
+#include <QRegularExpression>
+#include <QtConcurrentRun>
+
+// FIXME: this does not belong here, it's Minecraft/Flame specific
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "modplatform/flame/FileResolvingTask.h"
+#include "modplatform/flame/PackManifest.h"
+#include "modplatform/modrinth/ModrinthPackManifest.h"
+#include "Json.h"
+#include "modplatform/technic/TechnicPackProcessor.h"
+
+#include "icons/IconList.h"
+#include "Application.h"
+#include "ui/dialogs/BlockedModsDialog.h"
+
+#include <QDir>
+#include <QStandardPaths>
+
+InstanceImportTask::InstanceImportTask(const QUrl sourceUrl)
+{
+ m_sourceUrl = sourceUrl;
+}
+
+void InstanceImportTask::executeTask()
+{
+ if (m_sourceUrl.isLocalFile()) {
+ m_archivePath = m_sourceUrl.toLocalFile();
+ processZipPack();
+ } else {
+ setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
+ m_downloadRequired = true;
+
+ const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path();
+ auto entry = APPLICATION->metacache()->resolveEntry("general", path);
+ entry->setStale(true);
+ m_filesNetJob =
+ new NetJob(tr("Modpack download"), APPLICATION->network());
+ m_filesNetJob->addNetAction(
+ Net::Download::makeCached(m_sourceUrl, entry));
+ m_archivePath = entry->getFullPath();
+ auto job = m_filesNetJob.get();
+ connect(job, &NetJob::succeeded, this,
+ &InstanceImportTask::downloadSucceeded);
+ connect(job, &NetJob::progress, this,
+ &InstanceImportTask::downloadProgressChanged);
+ connect(job, &NetJob::failed, this,
+ &InstanceImportTask::downloadFailed);
+ m_filesNetJob->start();
+ }
+}
+
+void InstanceImportTask::downloadSucceeded()
+{
+ processZipPack();
+ m_filesNetJob.reset();
+}
+
+void InstanceImportTask::downloadFailed(QString reason)
+{
+ emitFailed(reason);
+ m_filesNetJob.reset();
+}
+
+void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total)
+{
+ setProgress(current / 2, total);
+}
+
+void InstanceImportTask::processZipPack()
+{
+ setStatus(tr("Extracting modpack"));
+ QDir extractDir(m_stagingPath);
+ qDebug() << "Attempting to create instance from" << m_archivePath;
+
+ // find relevant files in the zip
+ QStringList blacklist = {"instance.cfg", "manifest.json",
+ "modrinth.index.json"};
+ QString mmcFound =
+ MMCZip::findFolderOfFileInZip(m_archivePath, "instance.cfg");
+ bool technicFound = MMCZip::entryExists(m_archivePath, "bin/modpack.jar") ||
+ MMCZip::entryExists(m_archivePath, "bin/version.json");
+ QString flameFound =
+ MMCZip::findFolderOfFileInZip(m_archivePath, "manifest.json");
+ QString modrinthFound =
+ MMCZip::findFolderOfFileInZip(m_archivePath, "modrinth.index.json");
+ QString root;
+ if (!mmcFound.isNull()) {
+ // process as MeshMC instance/pack
+ qDebug() << "MeshMC:" << mmcFound;
+ root = mmcFound;
+ m_modpackType = ModpackType::MeshMC;
+ } else if (!modrinthFound.isNull()) {
+ // process as Modrinth pack
+ qDebug() << "Modrinth:" << modrinthFound;
+ root = modrinthFound;
+ m_modpackType = ModpackType::Modrinth;
+ } else if (technicFound) {
+ // process as Technic pack
+ qDebug() << "Technic:" << technicFound;
+ extractDir.mkpath(".minecraft");
+ extractDir.cd(".minecraft");
+ m_modpackType = ModpackType::Technic;
+ } else if (!flameFound.isNull()) {
+ // process as Flame pack
+ qDebug() << "Flame:" << flameFound;
+ root = flameFound;
+ m_modpackType = ModpackType::Flame;
+ }
+ if (m_modpackType == ModpackType::Unknown) {
+ emitFailed(tr("Archive does not contain a recognized modpack type."));
+ return;
+ }
+
+ // make sure we extract just the pack
+ QString archivePath = m_archivePath;
+ m_extractFuture =
+ QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir,
+ archivePath, root, extractDir.absolutePath());
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished,
+ this, &InstanceImportTask::extractFinished);
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled,
+ this, &InstanceImportTask::extractAborted);
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+}
+
+void InstanceImportTask::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;
+ }
+ }
+ }
+
+ switch (m_modpackType) {
+ case ModpackType::Flame:
+ processFlame();
+ return;
+ case ModpackType::Modrinth:
+ processModrinth();
+ return;
+ case ModpackType::MeshMC:
+ processMeshMC();
+ return;
+ case ModpackType::Technic:
+ processTechnic();
+ return;
+ case ModpackType::Unknown:
+ emitFailed(
+ tr("Archive does not contain a recognized modpack type."));
+ return;
+ }
+}
+
+void InstanceImportTask::extractAborted()
+{
+ emitFailed(tr("Instance import has been aborted."));
+ return;
+}
+
+void InstanceImportTask::processFlame()
+{
+ Flame::Manifest pack;
+ try {
+ QString configPath = FS::PathCombine(m_stagingPath, "manifest.json");
+ Flame::loadManifest(pack, configPath);
+ if (!QFile::remove(configPath)) {
+ qWarning() << "Could not remove manifest.json from staging";
+ }
+ } catch (const JSONValidationError& e) {
+ emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
+ return;
+ }
+ if (!pack.overrides.isEmpty()) {
+ QString overridePath = FS::PathCombine(m_stagingPath, pack.overrides);
+ if (QFile::exists(overridePath)) {
+ QString mcPath = FS::PathCombine(m_stagingPath, "minecraft");
+ if (!QFile::rename(overridePath, mcPath)) {
+ emitFailed(tr("Could not rename the overrides folder:\n") +
+ pack.overrides);
+ return;
+ }
+ } else {
+ logWarning(tr("The specified overrides folder (%1) is missing. "
+ "Maybe the modpack was already used before?")
+ .arg(pack.overrides));
+ }
+ }
+
+ configureFlameInstance(pack);
+
+ m_modIdResolver =
+ new Flame::FileResolvingTask(APPLICATION->network(), pack);
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, this,
+ &InstanceImportTask::onFlameFileResolutionSucceeded);
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed,
+ [&](QString reason) {
+ m_modIdResolver.reset();
+ emitFailed(tr("Unable to resolve mod IDs:\n") + reason);
+ });
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress,
+ [&](qint64 current, qint64 total) { setProgress(current, total); });
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status,
+ [&](QString status) { setStatus(status); });
+ m_modIdResolver->start();
+}
+
+static QString selectFlameIcon(const QString& instIcon,
+ const Flame::Manifest& pack)
+{
+ if (instIcon != "default")
+ return instIcon;
+ if (pack.name.contains("Direwolf20"))
+ return "steve";
+ if (pack.name.contains("FTB") || pack.name.contains("Feed The Beast"))
+ return "ftb_logo";
+ // default to something other than the MeshMC default to distinguish these
+ return "flame";
+}
+
+void InstanceImportTask::configureFlameInstance(Flame::Manifest& pack)
+{
+ const static QMap<QString, QString> forgemap = {{"1.2.5", "3.4.9.171"},
+ {"1.4.2", "6.0.1.355"},
+ {"1.4.7", "6.6.2.534"},
+ {"1.5.2", "7.8.1.737"}};
+
+ struct FlameLoaderMapping {
+ const char* prefix;
+ QString version;
+ const char* componentId;
+ };
+ FlameLoaderMapping loaderMappings[] = {
+ {"forge-", {}, "net.minecraftforge"},
+ {"fabric-", {}, "net.fabricmc.fabric-loader"},
+ {"neoforge-", {}, "net.neoforged"},
+ {"quilt-", {}, "org.quiltmc.quilt-loader"},
+ };
+ for (auto& loader : pack.minecraft.modLoaders) {
+ auto id = loader.id;
+ bool matched = false;
+ for (auto& mapping : loaderMappings) {
+ if (id.startsWith(mapping.prefix)) {
+ id.remove(mapping.prefix);
+ mapping.version = id;
+ matched = true;
+ break;
+ }
+ }
+ if (!matched) {
+ logWarning(tr("Unknown mod loader in manifest: %1").arg(id));
+ }
+ }
+
+ QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+ MinecraftInstance instance(m_globalSettings, instanceSettings,
+ m_stagingPath);
+ auto mcVersion = pack.minecraft.version;
+ // Hack to correct some 'special sauce'...
+ if (mcVersion.endsWith('.')) {
+ mcVersion.remove(QRegularExpression("[.]+$"));
+ logWarning(tr("Mysterious trailing dots removed from Minecraft version "
+ "while importing pack."));
+ }
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+ components->setComponentVersion("net.minecraft", mcVersion, true);
+
+ // Handle Forge "recommended" version mapping
+ auto& forgeMapping = loaderMappings[0];
+ if (forgeMapping.version == "recommended") {
+ if (forgemap.contains(mcVersion)) {
+ forgeMapping.version = forgemap[mcVersion];
+ } else {
+ logWarning(
+ tr("Could not map recommended forge version for Minecraft %1")
+ .arg(mcVersion));
+ }
+ }
+
+ for (const auto& mapping : loaderMappings) {
+ if (!mapping.version.isEmpty()) {
+ components->setComponentVersion(mapping.componentId,
+ mapping.version);
+ }
+ }
+
+ instance.setIconKey(selectFlameIcon(m_instIcon, pack));
+
+ 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 (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);
+ }
+ instance.setName(m_instName);
+}
+
+void InstanceImportTask::onFlameFileResolutionSucceeded()
+{
+ auto results = m_modIdResolver->getResults();
+ m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
+
+ // Collect restricted mods that need browser download
+ QList<BlockedMod> blockedMods;
+
+ for (auto result : results.files) {
+ QString filename = result.fileName;
+ if (!result.required) {
+ filename += ".disabled";
+ }
+
+ auto relpath =
+ FS::PathCombine("minecraft", result.targetFolder, filename);
+ auto path = FS::PathCombine(m_stagingPath, relpath);
+
+ switch (result.type) {
+ case Flame::File::Type::Folder: {
+ logWarning(
+ tr("This 'Folder' may need extracting: %1").arg(relpath));
+ [[fallthrough]];
+ }
+ case Flame::File::Type::SingleFile:
+ [[fallthrough]];
+ case Flame::File::Type::Mod: {
+ bool isBlocked = !result.resolved || !result.url.isValid() ||
+ result.url.isEmpty();
+ if (isBlocked && !result.fileName.isEmpty()) {
+ blockedMods.append({result.projectId, result.fileId,
+ result.fileName, path, false});
+ break;
+ }
+ if (isBlocked) {
+ logWarning(tr("Skipping mod %1 (project %2) - no download "
+ "URL and no filename available")
+ .arg(result.fileId)
+ .arg(result.projectId));
+ break;
+ }
+ qDebug() << "Will download" << result.url << "to" << path;
+ auto dl = Net::Download::makeFile(result.url, path);
+ m_filesNetJob->addNetAction(dl);
+ break;
+ }
+ case Flame::File::Type::Modpack:
+ logWarning(tr("Nesting modpacks in modpacks is not "
+ "implemented, nothing was downloaded: %1")
+ .arg(relpath));
+ break;
+ case Flame::File::Type::Cmod2:
+ [[fallthrough]];
+ case Flame::File::Type::Ctoc:
+ [[fallthrough]];
+ case Flame::File::Type::Unknown:
+ logWarning(tr("Unrecognized/unhandled PackageType for: %1")
+ .arg(relpath));
+ break;
+ }
+ }
+
+ // Handle restricted mods via dialog
+ if (!blockedMods.isEmpty()) {
+ BlockedModsDialog dlg(nullptr, tr("Restricted Mods"),
+ tr("The following mods have restricted downloads "
+ "and are not available through the API.\n"
+ "Click the Download button next to each mod "
+ "to open its download page in your browser.\n"
+ "Once all files appear in your Downloads "
+ "folder, click Continue."),
+ blockedMods);
+
+ if (dlg.exec() == QDialog::Accepted) {
+ QString downloadDir = QStandardPaths::writableLocation(
+ QStandardPaths::DownloadLocation);
+ for (const auto& mod : blockedMods) {
+ if (mod.found) {
+ QString srcPath =
+ FS::PathCombine(downloadDir, mod.fileName);
+ QFileInfo targetInfo(mod.targetPath);
+ QDir().mkpath(targetInfo.absolutePath());
+
+ if (QFile::copy(srcPath, mod.targetPath)) {
+ qDebug() << "Copied restricted mod:" << mod.fileName;
+ } else {
+ logWarning(tr("Failed to copy %1 from downloads folder")
+ .arg(mod.fileName));
+ }
+ }
+ }
+ } else {
+ logWarning(tr("User cancelled restricted mod downloads - %1 mod(s) "
+ "will be missing")
+ .arg(blockedMods.size()));
+ }
+ }
+
+ m_modIdResolver.reset();
+ connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() {
+ emitSucceeded();
+ m_filesNetJob.reset();
+ });
+ connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
+ emitFailed(reason);
+ m_filesNetJob.reset();
+ });
+ connect(m_filesNetJob.get(), &NetJob::progress,
+ [&](qint64 current, qint64 total) { setProgress(current, total); });
+ setStatus(tr("Downloading mods..."));
+ m_filesNetJob->start();
+}
+
+static void applyModrinthOverrides(const QString& stagingPath,
+ const QString& mcPath)
+{
+ QString overridePath = FS::PathCombine(stagingPath, "overrides");
+ QString clientOverridePath =
+ FS::PathCombine(stagingPath, "client-overrides");
+
+ if (QFile::exists(overridePath)) {
+ if (!FS::copy(overridePath, mcPath)()) {
+ qWarning() << "Could not apply overrides from the modpack.";
+ }
+ FS::deletePath(overridePath);
+ }
+
+ if (QFile::exists(clientOverridePath)) {
+ if (!FS::copy(clientOverridePath, mcPath)()) {
+ qWarning() << "Could not apply client-overrides from the modpack.";
+ }
+ FS::deletePath(clientOverridePath);
+ }
+}
+
+void InstanceImportTask::processModrinth()
+{
+ Modrinth::Manifest pack;
+ try {
+ QString configPath =
+ FS::PathCombine(m_stagingPath, "modrinth.index.json");
+ Modrinth::loadManifest(pack, configPath);
+ if (!QFile::remove(configPath)) {
+ qWarning() << "Could not remove modrinth.index.json from staging";
+ }
+ } catch (const JSONValidationError& e) {
+ emitFailed(tr("Could not understand Modrinth modpack manifest:\n") +
+ e.cause());
+ return;
+ }
+
+ // Move overrides folder contents to minecraft directory
+ QString mcPath = FS::PathCombine(m_stagingPath, "minecraft");
+
+ QDir mcDir(mcPath);
+ if (!mcDir.exists()) {
+ mcDir.mkpath(".");
+ }
+
+ applyModrinthOverrides(m_stagingPath, mcPath);
+
+ // Create instance config
+ QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+ MinecraftInstance instance(m_globalSettings, instanceSettings,
+ m_stagingPath);
+
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+
+ struct ModLoaderMapping {
+ const QString& version;
+ const char* componentId;
+ };
+ const ModLoaderMapping loaders[] = {
+ {pack.forgeVersion, "net.minecraftforge"},
+ {pack.fabricVersion, "net.fabricmc.fabric-loader"},
+ {pack.quiltVersion, "org.quiltmc.quilt-loader"},
+ {pack.neoForgeVersion, "net.neoforged"},
+ };
+
+ if (!pack.minecraftVersion.isEmpty()) {
+ components->setComponentVersion("net.minecraft", pack.minecraftVersion,
+ true);
+ }
+ for (const auto& loader : loaders) {
+ if (!loader.version.isEmpty()) {
+ components->setComponentVersion(loader.componentId, loader.version);
+ }
+ }
+
+ if (m_instIcon != "default") {
+ instance.setIconKey(m_instIcon);
+ } else {
+ instance.setIconKey("modrinth");
+ }
+
+ instance.setName(m_instName);
+
+ // Download all mod files
+ m_filesNetJob =
+ new NetJob(tr("Modrinth mod download"), APPLICATION->network());
+ auto minecraftDir = FS::PathCombine(m_stagingPath, "minecraft");
+ auto canonicalBase = QDir(minecraftDir).canonicalPath();
+ for (auto& file : pack.files) {
+ if (file.path.contains("..") || QDir::isAbsolutePath(file.path)) {
+ qWarning() << "Skipping potentially malicious file path:"
+ << file.path;
+ continue;
+ }
+ auto path = FS::PathCombine(minecraftDir, file.path);
+ auto canonicalDir = QFileInfo(path).absolutePath();
+ if (!canonicalDir.startsWith(canonicalBase)) {
+ qWarning() << "Skipping file path that escapes staging directory:"
+ << file.path;
+ continue;
+ }
+ if (!file.downloadUrl.isValid() || file.downloadUrl.isEmpty()) {
+ logWarning(
+ tr("Skipping file with no download URL: %1").arg(file.path));
+ continue;
+ }
+ qDebug() << "Will download" << file.downloadUrl << "to" << path;
+ auto dl = Net::Download::makeFile(file.downloadUrl, path);
+ m_filesNetJob->addNetAction(dl);
+ }
+
+ connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() {
+ emitSucceeded();
+ m_filesNetJob.reset();
+ });
+ connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason) {
+ emitFailed(reason);
+ m_filesNetJob.reset();
+ });
+ connect(m_filesNetJob.get(), &NetJob::progress,
+ [&](qint64 current, qint64 total) { setProgress(current, total); });
+
+ setStatus(tr("Downloading mods..."));
+ m_filesNetJob->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, m_instName, m_instIcon, m_stagingPath);
+}
+
+void InstanceImportTask::processMeshMC()
+{
+ QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+
+ NullInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
+
+ // reset time played on import... because packs.
+ instance.resetTimePlayed();
+
+ // Set a new name for the imported instance
+ instance.setName(m_instName);
+
+ // Use user-specified icon if available, otherwise import from the pack
+ if (m_instIcon != "default") {
+ instance.setIconKey(m_instIcon);
+ } else {
+ m_instIcon = instance.iconKey();
+
+ auto importIconPath =
+ IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon);
+ if (!importIconPath.isNull() && QFile::exists(importIconPath)) {
+ // import icon
+ auto iconList = APPLICATION->icons();
+ if (iconList->iconFileExists(m_instIcon)) {
+ iconList->deleteIcon(m_instIcon);
+ }
+ iconList->installIcons({importIconPath});
+ }
+ }
+ emitSucceeded();
+}