/* 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 .
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2020-2021 Jamie Mansfield
* Copyright 2021 Petr Mrazek
*
* 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
#include
#include "MMCZip.h"
#include "minecraft/OneSixVersionFormat.h"
#include "Version.h"
#include "net/ChecksumValidator.h"
#include "FileSystem.h"
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "settings/INISettingsObject.h"
#include "meta/Index.h"
#include "meta/Version.h"
#include "meta/VersionList.h"
#include "BuildConfig.h"
#include "Application.h"
namespace ATLauncher
{
PackInstallTask::PackInstallTask(UserInteractionSupport* support,
QString pack, QString version)
{
m_support = support;
m_pack = pack;
m_version_name = version;
}
bool PackInstallTask::abort()
{
if (abortable) {
return jobPtr->abort();
}
return false;
}
void PackInstallTask::executeTask()
{
qDebug() << "PackInstallTask::executeTask: "
<< QThread::currentThreadId();
auto* netJob =
new NetJob("ATLauncher::VersionFetch", APPLICATION->network());
auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL +
"packs/%1/versions/%2/Configs.json")
.arg(m_pack)
.arg(m_version_name);
netJob->addNetAction(
Net::Download::makeByteArray(QUrl(searchUrl), &response));
jobPtr = netJob;
jobPtr->start();
QObject::connect(netJob, &NetJob::succeeded, this,
&PackInstallTask::onDownloadSucceeded);
QObject::connect(netJob, &NetJob::failed, this,
&PackInstallTask::onDownloadFailed);
}
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 FTB at "
<< parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << response;
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;
}
m_version = version;
auto vlist = APPLICATION->metadataIndex()->get("net.minecraft");
if (!vlist) {
emitFailed(tr("Failed to get local metadata index for %1")
.arg("net.minecraft"));
return;
}
auto ver = vlist->getVersion(m_version.minecraft);
if (!ver) {
emitFailed(tr("Failed to get local metadata index for '%1' v%2")
.arg("net.minecraft")
.arg(m_version.minecraft));
return;
}
ver->load(Net::Mode::Online);
minecraftVersion = ver;
if (m_version.noConfigs) {
downloadMods();
} else {
installConfigs();
}
}
void PackInstallTask::onDownloadFailed(QString reason)
{
qDebug() << "PackInstallTask::onDownloadFailed: "
<< QThread::currentThreadId();
emitFailed(reason);
jobPtr.reset();
}
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()->get(uid);
if (!vlist) {
emitFailed(
tr("Failed to get local metadata index for %1").arg(uid));
return Q_NULLPTR;
}
if (!vlist->isLoaded()) {
vlist->load(Net::Mode::Online);
}
if (m_version.loader.recommended || m_version.loader.latest) {
for (int i = 0; i < vlist->versions().size(); i++) {
auto version = vlist->versions().at(i);
auto reqs = version->requirements();
// 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 Meta::Require& 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->isRecommended())
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(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.projecttick.atlauncher:" + library.md5 + ":1";
}
bool PackInstallTask::createLibrariesComponent(
QString instanceRoot, std::shared_ptr profile)
{
if (m_version.libraries.isEmpty()) {
return true;
}
QList exempt;
for (const auto& componentUid : componentsToInstall.keys()) {
auto componentVersion = componentsToInstall.value(componentUid);
for (const auto& library : componentVersion->data()->libraries) {
GradleSpecifier lib(library->rawName());
exempt.append(lib);
}
}
{
for (const auto& library : minecraftVersion->data()->libraries) {
GradleSpecifier lib(library->rawName());
exempt.append(lib);
}
}
auto uuid = QUuid::createUuid();
auto id = uuid.toString().remove('{').remove('}');
auto target_id = "org.projecttick.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();
f->name = m_pack + " " + m_version_name + " (libraries)";
for (const auto& lib : m_version.libraries) {
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->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(new Component(profile.get(), target_id, f));
return true;
}
bool
PackInstallTask::createPackComponent(QString instanceRoot,
std::shared_ptr profile)
{
if (m_version.mainClass == QString() &&
m_version.extraArguments == QString()) {
return true;
}
auto uuid = QUuid::createUuid();
auto id = uuid.toString().remove('{').remove('}');
auto target_id = "org.projecttick.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 componentVersion = componentsToInstall.value(componentUid);
if (componentVersion->data()->mainClass != QString("")) {
mainClasses.append(componentVersion->data()->mainClass);
}
tweakers.append(componentVersion->data()->addTweakers);
}
auto f = std::make_shared();
f->name = m_pack + " " + m_version_name;
if (m_version.mainClass != QString() &&
!mainClasses.contains(m_version.mainClass)) {
f->mainClass = m_version.mainClass;
}
// Parse out tweakers
auto args = m_version.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(new Component(profile.get(), target_id, f));
return true;
}
void PackInstallTask::installConfigs()
{
qDebug() << "PackInstallTask::installConfigs: "
<< QThread::currentThreadId();
setStatus(tr("Downloading configs..."));
jobPtr = new NetJob(tr("Config download"), APPLICATION->network());
auto path =
QString("Configs/%1/%2.zip").arg(m_pack).arg(m_version_name);
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL +
"packs/%1/versions/%2/Configs.zip")
.arg(m_pack)
.arg(m_version_name);
auto entry =
APPLICATION->metacache()->resolveEntry("ATLauncherPacks", path);
entry->setStale(true);
auto dl = Net::Download::makeCached(url, entry);
if (!m_version.configs.sha1.isEmpty()) {
auto rawSha1 =
QByteArray::fromHex(m_version.configs.sha1.toLatin1());
dl->addValidator(
new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
}
jobPtr->addNetAction(dl);
archivePath = entry->getFullPath();
connect(jobPtr.get(), &NetJob::succeeded, this, [&]() {
abortable = false;
extractConfigs();
jobPtr.reset();
});
connect(jobPtr.get(), &NetJob::failed, [&](QString reason) {
abortable = false;
emitFailed(reason);
jobPtr.reset();
});
connect(jobPtr.get(), &NetJob::progress,
[&](qint64 current, qint64 total) {
abortable = true;
setProgress(current, total);
});
jobPtr->start();
}
void PackInstallTask::extractConfigs()
{
qDebug() << "PackInstallTask::extractConfigs: "
<< QThread::currentThreadId();
setStatus(tr("Extracting configs..."));
QDir extractDir(m_stagingPath);
QString extractPath = extractDir.absolutePath() + "/minecraft";
QString archivePathCopy = archivePath;
m_extractFuture = QtConcurrent::run(
QThreadPool::globalInstance(), [archivePathCopy, extractPath]() {
return MMCZip::extractDir(archivePathCopy, extractPath);
});
connect(&m_extractFutureWatcher, &QFutureWatcher::finished,
this, [&]() { downloadMods(); });
connect(&m_extractFutureWatcher, &QFutureWatcher::canceled,
this, [&]() { emitAborted(); });
m_extractFutureWatcher.setFuture(m_extractFuture);
}
void PackInstallTask::downloadMods()
{
qDebug() << "PackInstallTask::installMods: "
<< QThread::currentThreadId();
QVector optionalMods;
for (const auto& mod : m_version.mods) {
if (mod.optional) {
optionalMods.push_back(mod);
}
}
// Select optional mods, if pack contains any
QVector selectedMods;
if (!optionalMods.isEmpty()) {
setStatus(tr("Selecting optional mods..."));
selectedMods = m_support->chooseOptionalMods(optionalMods);
}
setStatus(tr("Downloading mods..."));
jarmods.clear();
jobPtr = new NetJob(tr("Mod download"), APPLICATION->network());
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:
emitFailed(tr("Unsupported download type: %1")
.arg(mod.download_raw));
return;
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::Download::makeCached(url, entry);
if (!mod.md5.isEmpty()) {
auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
dl->addValidator(new Net::ChecksumValidator(
QCryptographicHash::Md5, rawMd5));
}
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::Download::makeCached(url, entry);
if (!mod.md5.isEmpty()) {
auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
dl->addValidator(new Net::ChecksumValidator(
QCryptographicHash::Md5, rawMd5));
}
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::Download::makeCached(url, entry);
if (!mod.md5.isEmpty()) {
auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
dl->addValidator(new Net::ChecksumValidator(
QCryptographicHash::Md5, rawMd5));
}
jobPtr->addNetAction(dl);
auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath,
mod.file);
qDebug() << "Will download" << url << "to" << path;
modsToCopy[entry->getFullPath()] = path;
if (mod.type == ModType::Forge) {
auto vlist =
APPLICATION->metadataIndex()->get("net.minecraftforge");
if (vlist) {
auto ver = vlist->getVersion(mod.version);
if (ver) {
ver->load(Net::Mode::Online);
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);
}
}
}
connect(jobPtr.get(), &NetJob::succeeded, this,
&PackInstallTask::onModsDownloaded);
connect(jobPtr.get(), &NetJob::failed, [&](QString reason) {
abortable = false;
emitFailed(reason);
jobPtr.reset();
});
connect(jobPtr.get(), &NetJob::progress,
[&](qint64 current, qint64 total) {
abortable = true;
setProgress(current, total);
});
jobPtr->start();
}
void PackInstallTask::onModsDownloaded()
{
abortable = false;
qDebug() << "PackInstallTask::onModsDownloaded: "
<< QThread::currentThreadId();
if (!modsToExtract.empty() || !modsToDecomp.empty() ||
!modsToCopy.empty()) {
auto modsToExtractCopy = modsToExtract;
auto modsToDecompCopy = modsToDecomp;
auto modsToCopyCopy = modsToCopy;
m_modExtractFuture = QtConcurrent::run(
QThreadPool::globalInstance(),
[this, modsToExtractCopy, modsToDecompCopy, modsToCopyCopy]() {
return this->extractMods(modsToExtractCopy,
modsToDecompCopy, modsToCopyCopy);
});
connect(&m_modExtractFutureWatcher,
&QFutureWatcher::finished, this,
&PackInstallTask::onModsExtracted);
connect(&m_modExtractFutureWatcher,
&QFutureWatcher::canceled, this,
[&]() { 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& toExtract,
const QMap& toDecomp,
const QMap& 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;
folderToExtract.remove(QRegularExpression("^/"));
}
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();
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(instanceConfigPath);
instanceSettings->suspendSave();
instanceSettings->registerSetting("InstanceType", "Legacy");
instanceSettings->set("InstanceType", "OneSix");
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,
true);
} 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, true);
} else if (m_version.loader.type == QString("neoforge")) {
auto version = getVersionForLoader("net.neoforged");
if (version == Q_NULLPTR)
return;
components->setComponentVersion("net.neoforged", version, true);
} else if (m_version.loader.type == QString("quilt")) {
auto version = getVersionForLoader("org.quiltmc.quilt-loader");
if (version == Q_NULLPTR)
return;
components->setComponentVersion("org.quiltmc.quilt-loader", version,
true);
} 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->version());
}
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(m_instName);
instance.setIconKey(m_instIcon);
instanceSettings->resumeSave();
jarmods.clear();
emitSucceeded();
}
} // namespace ATLauncher