pollymc/launcher/minecraft/PackProfile.cpp
flow 5da87d1904
fix: add missing connections to the abort signal
Beginning with efa3fbff39, we separated
the failing and the aborting signals, as they can mean different
things in certain contexts. Still, some places are not yet changed to
reflect this modification. This can cause aborting of progress dialogs
to not work, instead making the application hang in an unusable satte.

This goes through some places where it's not hooked up yet, fixing their
behaviour in those kinds of situation.
2022-06-22 20:20:39 -03:00

998 lines
27 KiB
C++

/* 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 <QFile>
#include <QCryptographicHash>
#include <Version.h>
#include <QDir>
#include <QJsonDocument>
#include <QJsonArray>
#include <QDebug>
#include <QSaveFile>
#include <QUuid>
#include <QTimer>
#include "Exception.h"
#include "minecraft/OneSixVersionFormat.h"
#include "FileSystem.h"
#include "meta/Index.h"
#include "minecraft/MinecraftInstance.h"
#include "Json.h"
#include "PackProfile.h"
#include "PackProfile_p.h"
#include "ComponentUpdateTask.h"
#include "Application.h"
#include "modplatform/ModAPI.h"
static const QMap<QString, ModAPI::ModLoaderType> modloaderMapping{
{"net.minecraftforge", ModAPI::Forge},
{"net.fabricmc.fabric-loader", ModAPI::Fabric},
{"org.quiltmc.quilt-loader", ModAPI::Quilt}
};
PackProfile::PackProfile(MinecraftInstance * instance)
: QAbstractListModel()
{
d.reset(new PackProfileData);
d->m_instance = instance;
d->m_saveTimer.setSingleShot(true);
d->m_saveTimer.setInterval(5000);
d->interactionDisabled = instance->isRunning();
connect(d->m_instance, &BaseInstance::runningStatusChanged, this, &PackProfile::disableInteraction);
connect(&d->m_saveTimer, &QTimer::timeout, this, &PackProfile::save_internal);
}
PackProfile::~PackProfile()
{
saveNow();
}
// BEGIN: component file format
static const int currentComponentsFileVersion = 1;
static QJsonObject componentToJsonV1(ComponentPtr component)
{
QJsonObject obj;
// critical
obj.insert("uid", component->m_uid);
if(!component->m_version.isEmpty())
{
obj.insert("version", component->m_version);
}
if(component->m_dependencyOnly)
{
obj.insert("dependencyOnly", true);
}
if(component->m_important)
{
obj.insert("important", true);
}
if(component->m_disabled)
{
obj.insert("disabled", true);
}
// cached
if(!component->m_cachedVersion.isEmpty())
{
obj.insert("cachedVersion", component->m_cachedVersion);
}
if(!component->m_cachedName.isEmpty())
{
obj.insert("cachedName", component->m_cachedName);
}
Meta::serializeRequires(obj, &component->m_cachedRequires, "cachedRequires");
Meta::serializeRequires(obj, &component->m_cachedConflicts, "cachedConflicts");
if(component->m_cachedVolatile)
{
obj.insert("cachedVolatile", true);
}
return obj;
}
static ComponentPtr componentFromJsonV1(PackProfile * parent, const QString & componentJsonPattern, const QJsonObject &obj)
{
// critical
auto uid = Json::requireString(obj.value("uid"));
auto filePath = componentJsonPattern.arg(uid);
auto component = new Component(parent, uid);
component->m_version = Json::ensureString(obj.value("version"));
component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false);
component->m_important = Json::ensureBoolean(obj.value("important"), false);
// cached
// TODO @RESILIENCE: ignore invalid values/structure here?
component->m_cachedVersion = Json::ensureString(obj.value("cachedVersion"));
component->m_cachedName = Json::ensureString(obj.value("cachedName"));
Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires");
Meta::parseRequires(obj, &component->m_cachedConflicts, "cachedConflicts");
component->m_cachedVolatile = Json::ensureBoolean(obj.value("volatile"), false);
bool disabled = Json::ensureBoolean(obj.value("disabled"), false);
component->setEnabled(!disabled);
return component;
}
// Save the given component container data to a file
static bool savePackProfile(const QString & filename, const ComponentContainer & container)
{
QJsonObject obj;
obj.insert("formatVersion", currentComponentsFileVersion);
QJsonArray orderArray;
for(auto component: container)
{
orderArray.append(componentToJsonV1(component));
}
obj.insert("components", orderArray);
QSaveFile outFile(filename);
if (!outFile.open(QFile::WriteOnly))
{
qCritical() << "Couldn't open" << outFile.fileName()
<< "for writing:" << outFile.errorString();
return false;
}
auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented);
if(outFile.write(data) != data.size())
{
qCritical() << "Couldn't write all the data into" << outFile.fileName()
<< "because:" << outFile.errorString();
return false;
}
if(!outFile.commit())
{
qCritical() << "Couldn't save" << outFile.fileName()
<< "because:" << outFile.errorString();
}
return true;
}
// Read the given file into component containers
static bool loadPackProfile(PackProfile * parent, const QString & filename, const QString & componentJsonPattern, ComponentContainer & container)
{
QFile componentsFile(filename);
if (!componentsFile.exists())
{
qWarning() << "Components file doesn't exist. This should never happen.";
return false;
}
if (!componentsFile.open(QFile::ReadOnly))
{
qCritical() << "Couldn't open" << componentsFile.fileName()
<< " for reading:" << componentsFile.errorString();
qWarning() << "Ignoring overriden order";
return false;
}
// and it's valid JSON
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error);
if (error.error != QJsonParseError::NoError)
{
qCritical() << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString();
qWarning() << "Ignoring overriden order";
return false;
}
// and then read it and process it if all above is true.
try
{
auto obj = Json::requireObject(doc);
// check order file version.
auto version = Json::requireInteger(obj.value("formatVersion"));
if (version != currentComponentsFileVersion)
{
throw JSONValidationError(QObject::tr("Invalid component file version, expected %1")
.arg(currentComponentsFileVersion));
}
auto orderArray = Json::requireArray(obj.value("components"));
for(auto item: orderArray)
{
auto obj = Json::requireObject(item, "Component must be an object.");
container.append(componentFromJsonV1(parent, componentJsonPattern, obj));
}
}
catch (const JSONValidationError &err)
{
qCritical() << "Couldn't parse" << componentsFile.fileName() << ": bad file format";
container.clear();
return false;
}
return true;
}
// END: component file format
// BEGIN: save/load logic
void PackProfile::saveNow()
{
if(saveIsScheduled())
{
d->m_saveTimer.stop();
save_internal();
}
}
bool PackProfile::saveIsScheduled() const
{
return d->dirty;
}
void PackProfile::buildingFromScratch()
{
d->loaded = true;
d->dirty = true;
}
void PackProfile::scheduleSave()
{
if(!d->loaded)
{
qDebug() << "Component list should never save if it didn't successfully load, instance:" << d->m_instance->name();
return;
}
if(!d->dirty)
{
d->dirty = true;
qDebug() << "Component list save is scheduled for" << d->m_instance->name();
}
d->m_saveTimer.start();
}
QString PackProfile::componentsFilePath() const
{
return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json");
}
QString PackProfile::patchesPattern() const
{
return FS::PathCombine(d->m_instance->instanceRoot(), "patches", "%1.json");
}
QString PackProfile::patchFilePathForUid(const QString& uid) const
{
return patchesPattern().arg(uid);
}
void PackProfile::save_internal()
{
qDebug() << "Component list save performed now for" << d->m_instance->name();
auto filename = componentsFilePath();
savePackProfile(filename, d->components);
d->dirty = false;
}
bool PackProfile::load()
{
auto filename = componentsFilePath();
// load the new component list and swap it with the current one...
ComponentContainer newComponents;
if(!loadPackProfile(this, filename, patchesPattern(), newComponents))
{
qCritical() << "Failed to load the component config for instance" << d->m_instance->name();
return false;
}
else
{
// FIXME: actually use fine-grained updates, not this...
beginResetModel();
// disconnect all the old components
for(auto component: d->components)
{
disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
}
d->components.clear();
d->componentIndex.clear();
for(auto component: newComponents)
{
if(d->componentIndex.contains(component->m_uid))
{
qWarning() << "Ignoring duplicate component entry" << component->m_uid;
continue;
}
connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
d->components.append(component);
d->componentIndex[component->m_uid] = component;
}
endResetModel();
d->loaded = true;
return true;
}
}
void PackProfile::reload(Net::Mode netmode)
{
// Do not reload when the update/resolve task is running. It is in control.
if(d->m_updateTask)
{
return;
}
// flush any scheduled saves to not lose state
saveNow();
// FIXME: differentiate when a reapply is required by propagating state from components
invalidateLaunchProfile();
if(load())
{
resolve(netmode);
}
}
Task::Ptr PackProfile::getCurrentTask()
{
return d->m_updateTask;
}
void PackProfile::resolve(Net::Mode netmode)
{
auto updateTask = new ComponentUpdateTask(ComponentUpdateTask::Mode::Resolution, netmode, this);
d->m_updateTask.reset(updateTask);
connect(updateTask, &ComponentUpdateTask::succeeded, this, &PackProfile::updateSucceeded);
connect(updateTask, &ComponentUpdateTask::failed, this, &PackProfile::updateFailed);
connect(updateTask, &ComponentUpdateTask::aborted, this, [this]{ updateFailed(tr("Aborted")); });
d->m_updateTask->start();
}
void PackProfile::updateSucceeded()
{
qDebug() << "Component list update/resolve task succeeded for" << d->m_instance->name();
d->m_updateTask.reset();
invalidateLaunchProfile();
}
void PackProfile::updateFailed(const QString& error)
{
qDebug() << "Component list update/resolve task failed for" << d->m_instance->name() << "Reason:" << error;
d->m_updateTask.reset();
invalidateLaunchProfile();
}
// END: save/load
void PackProfile::appendComponent(ComponentPtr component)
{
insertComponent(d->components.size(), component);
}
void PackProfile::insertComponent(size_t index, ComponentPtr component)
{
auto id = component->getID();
if(id.isEmpty())
{
qWarning() << "Attempt to add a component with empty ID!";
return;
}
if(d->componentIndex.contains(id))
{
qWarning() << "Attempt to add a component that is already present!";
return;
}
beginInsertRows(QModelIndex(), index, index);
d->components.insert(index, component);
d->componentIndex[id] = component;
endInsertRows();
connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
scheduleSave();
}
void PackProfile::componentDataChanged()
{
auto objPtr = qobject_cast<Component *>(sender());
if(!objPtr)
{
qWarning() << "PackProfile got dataChenged signal from a non-Component!";
return;
}
if(objPtr->getID() == "net.minecraft") {
emit minecraftChanged();
}
// figure out which one is it... in a seriously dumb way.
int index = 0;
for (auto component: d->components)
{
if(component.get() == objPtr)
{
emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1));
scheduleSave();
return;
}
index++;
}
qWarning() << "PackProfile got dataChenged signal from a Component which does not belong to it!";
}
bool PackProfile::remove(const int index)
{
auto patch = getComponent(index);
if (!patch->isRemovable())
{
qWarning() << "Patch" << patch->getID() << "is non-removable";
return false;
}
if(!removeComponent_internal(patch))
{
qCritical() << "Patch" << patch->getID() << "could not be removed";
return false;
}
beginRemoveRows(QModelIndex(), index, index);
d->components.removeAt(index);
d->componentIndex.remove(patch->getID());
endRemoveRows();
invalidateLaunchProfile();
scheduleSave();
return true;
}
bool PackProfile::remove(const QString id)
{
int i = 0;
for (auto patch : d->components)
{
if (patch->getID() == id)
{
return remove(i);
}
i++;
}
return false;
}
bool PackProfile::customize(int index)
{
auto patch = getComponent(index);
if (!patch->isCustomizable())
{
qDebug() << "Patch" << patch->getID() << "is not customizable";
return false;
}
if(!patch->customize())
{
qCritical() << "Patch" << patch->getID() << "could not be customized";
return false;
}
invalidateLaunchProfile();
scheduleSave();
return true;
}
bool PackProfile::revertToBase(int index)
{
auto patch = getComponent(index);
if (!patch->isRevertible())
{
qDebug() << "Patch" << patch->getID() << "is not revertible";
return false;
}
if(!patch->revert())
{
qCritical() << "Patch" << patch->getID() << "could not be reverted";
return false;
}
invalidateLaunchProfile();
scheduleSave();
return true;
}
Component * PackProfile::getComponent(const QString &id)
{
auto iter = d->componentIndex.find(id);
if (iter == d->componentIndex.end())
{
return nullptr;
}
return (*iter).get();
}
Component * PackProfile::getComponent(int index)
{
if(index < 0 || index >= d->components.size())
{
return nullptr;
}
return d->components[index].get();
}
QVariant PackProfile::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
int row = index.row();
int column = index.column();
if (row < 0 || row >= d->components.size())
return QVariant();
auto patch = d->components.at(row);
switch (role)
{
case Qt::CheckStateRole:
{
switch (column)
{
case NameColumn: {
return patch->isEnabled() ? Qt::Checked : Qt::Unchecked;
}
default:
return QVariant();
}
}
case Qt::DisplayRole:
{
switch (column)
{
case NameColumn:
return patch->getName();
case VersionColumn:
{
if(patch->isCustom())
{
return QString("%1 (Custom)").arg(patch->getVersion());
}
else
{
return patch->getVersion();
}
}
default:
return QVariant();
}
}
case Qt::DecorationRole:
{
switch(column)
{
case NameColumn:
{
auto severity = patch->getProblemSeverity();
switch (severity)
{
case ProblemSeverity::Warning:
return "warning";
case ProblemSeverity::Error:
return "error";
default:
return QVariant();
}
}
default:
{
return QVariant();
}
}
}
}
return QVariant();
}
bool PackProfile::setData(const QModelIndex& index, const QVariant& value, int role)
{
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index))
{
return false;
}
if (role == Qt::CheckStateRole)
{
auto component = d->components[index.row()];
if (component->setEnabled(!component->isEnabled()))
{
return true;
}
}
return false;
}
QVariant PackProfile::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Horizontal)
{
if (role == Qt::DisplayRole)
{
switch (section)
{
case NameColumn:
return tr("Name");
case VersionColumn:
return tr("Version");
default:
return QVariant();
}
}
}
return QVariant();
}
// FIXME: zero precision mess
Qt::ItemFlags PackProfile::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
Qt::ItemFlags outFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
int row = index.row();
if (row < 0 || row >= d->components.size()) {
return Qt::NoItemFlags;
}
auto patch = d->components.at(row);
// TODO: this will need fine-tuning later...
if(patch->canBeDisabled() && !d->interactionDisabled)
{
outFlags |= Qt::ItemIsUserCheckable;
}
return outFlags;
}
int PackProfile::rowCount(const QModelIndex &parent) const
{
return d->components.size();
}
int PackProfile::columnCount(const QModelIndex &parent) const
{
return NUM_COLUMNS;
}
void PackProfile::move(const int index, const MoveDirection direction)
{
int theirIndex;
if (direction == MoveUp)
{
theirIndex = index - 1;
}
else
{
theirIndex = index + 1;
}
if (index < 0 || index >= d->components.size())
return;
if (theirIndex >= rowCount())
theirIndex = rowCount() - 1;
if (theirIndex == -1)
theirIndex = rowCount() - 1;
if (index == theirIndex)
return;
int togap = theirIndex > index ? theirIndex + 1 : theirIndex;
auto from = getComponent(index);
auto to = getComponent(theirIndex);
if (!from || !to || !to->isMoveable() || !from->isMoveable())
{
return;
}
beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap);
d->components.swap(index, theirIndex);
endMoveRows();
invalidateLaunchProfile();
scheduleSave();
}
void PackProfile::invalidateLaunchProfile()
{
d->m_profile.reset();
}
void PackProfile::installJarMods(QStringList selectedFiles)
{
installJarMods_internal(selectedFiles);
}
void PackProfile::installCustomJar(QString selectedFile)
{
installCustomJar_internal(selectedFile);
}
bool PackProfile::installEmpty(const QString& uid, const QString& name)
{
QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
if(!FS::ensureFolderPathExists(patchDir))
{
return false;
}
auto f = std::make_shared<VersionFile>();
f->name = name;
f->uid = uid;
f->version = "1";
QString patchFileName = FS::PathCombine(patchDir, uid + ".json");
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();
appendComponent(new Component(this, f->uid, f));
scheduleSave();
invalidateLaunchProfile();
return true;
}
bool PackProfile::removeComponent_internal(ComponentPtr patch)
{
bool ok = true;
// first, remove the patch file. this ensures it's not used anymore
auto fileName = patch->getFilename();
if(fileName.size())
{
QFile patchFile(fileName);
if(patchFile.exists() && !patchFile.remove())
{
qCritical() << "File" << fileName << "could not be removed because:" << patchFile.errorString();
return false;
}
}
// FIXME: we need a generic way of removing local resources, not just jar mods...
auto preRemoveJarMod = [&](LibraryPtr jarMod) -> bool
{
if (!jarMod->isLocal())
{
return true;
}
QStringList jar, temp1, temp2, temp3;
jarMod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, d->m_instance->jarmodsPath().absolutePath());
QFileInfo finfo (jar[0]);
if(finfo.exists())
{
QFile jarModFile(jar[0]);
if(!jarModFile.remove())
{
qCritical() << "File" << jar[0] << "could not be removed because:" << jarModFile.errorString();
return false;
}
return true;
}
return true;
};
auto vFile = patch->getVersionFile();
if(vFile)
{
auto &jarMods = vFile->jarMods;
for(auto &jarmod: jarMods)
{
ok &= preRemoveJarMod(jarmod);
}
}
return ok;
}
bool PackProfile::installJarMods_internal(QStringList filepaths)
{
QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
if(!FS::ensureFolderPathExists(patchDir))
{
return false;
}
if (!FS::ensureFolderPathExists(d->m_instance->jarModsDir()))
{
return false;
}
for(auto filepath:filepaths)
{
QFileInfo sourceInfo(filepath);
auto uuid = QUuid::createUuid();
QString id = uuid.toString().remove('{').remove('}');
QString target_filename = id + ".jar";
QString target_id = "org.multimc.jarmod." + id;
QString target_name = sourceInfo.completeBaseName() + " (jar mod)";
QString finalPath = FS::PathCombine(d->m_instance->jarModsDir(), target_filename);
QFileInfo targetInfo(finalPath);
if(targetInfo.exists())
{
return false;
}
if (!QFile::copy(sourceInfo.absoluteFilePath(),QFileInfo(finalPath).absoluteFilePath()))
{
return false;
}
auto f = std::make_shared<VersionFile>();
auto jarMod = std::make_shared<Library>();
jarMod->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1"));
jarMod->setFilename(target_filename);
jarMod->setDisplayName(sourceInfo.completeBaseName());
jarMod->setHint("local");
f->jarMods.append(jarMod);
f->name = target_name;
f->uid = target_id;
QString patchFileName = FS::PathCombine(patchDir, target_id + ".json");
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();
appendComponent(new Component(this, f->uid, f));
}
scheduleSave();
invalidateLaunchProfile();
return true;
}
bool PackProfile::installCustomJar_internal(QString filepath)
{
QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
if(!FS::ensureFolderPathExists(patchDir))
{
return false;
}
QString libDir = d->m_instance->getLocalLibraryPath();
if (!FS::ensureFolderPathExists(libDir))
{
return false;
}
auto specifier = GradleSpecifier("org.multimc:customjar:1");
QFileInfo sourceInfo(filepath);
QString target_filename = specifier.getFileName();
QString target_id = specifier.artifactId();
QString target_name = sourceInfo.completeBaseName() + " (custom jar)";
QString finalPath = FS::PathCombine(libDir, target_filename);
QFileInfo jarInfo(finalPath);
if (jarInfo.exists())
{
if(!QFile::remove(finalPath))
{
return false;
}
}
if (!QFile::copy(filepath, finalPath))
{
return false;
}
auto f = std::make_shared<VersionFile>();
auto jarMod = std::make_shared<Library>();
jarMod->setRawName(specifier);
jarMod->setDisplayName(sourceInfo.completeBaseName());
jarMod->setHint("local");
f->mainJar = jarMod;
f->name = target_name;
f->uid = target_id;
QString patchFileName = FS::PathCombine(patchDir, target_id + ".json");
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();
appendComponent(new Component(this, f->uid, f));
scheduleSave();
invalidateLaunchProfile();
return true;
}
std::shared_ptr<LaunchProfile> PackProfile::getProfile() const
{
if(!d->m_profile)
{
try
{
auto profile = std::make_shared<LaunchProfile>();
for(auto file: d->components)
{
qDebug() << "Applying" << file->getID() << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD");
file->applyTo(profile.get());
}
d->m_profile = profile;
}
catch (const Exception &error)
{
qWarning() << "Couldn't apply profile patches because: " << error.cause();
}
}
return d->m_profile;
}
bool PackProfile::setComponentVersion(const QString& uid, const QString& version, bool important)
{
auto iter = d->componentIndex.find(uid);
if(iter != d->componentIndex.end())
{
ComponentPtr component = *iter;
// set existing
if(component->revert())
{
component->setVersion(version);
component->setImportant(important);
return true;
}
return false;
}
else
{
// add new
auto component = new Component(this, uid);
component->m_version = version;
component->m_important = important;
appendComponent(component);
return true;
}
}
QString PackProfile::getComponentVersion(const QString& uid) const
{
const auto iter = d->componentIndex.find(uid);
if (iter != d->componentIndex.end())
{
return (*iter)->getVersion();
}
return QString();
}
void PackProfile::disableInteraction(bool disable)
{
if(d->interactionDisabled != disable) {
d->interactionDisabled = disable;
auto size = d->components.size();
if(size) {
emit dataChanged(index(0), index(size - 1));
}
}
}
ModAPI::ModLoaderTypes PackProfile::getModLoaders()
{
ModAPI::ModLoaderTypes result = ModAPI::Unspecified;
QMapIterator<QString, ModAPI::ModLoaderType> i(modloaderMapping);
while (i.hasNext())
{
i.next();
Component* c = getComponent(i.key());
if (c != nullptr && c->isEnabled()) {
result |= i.value();
}
}
return result;
}