// SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022-2023 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * * 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, see . * * 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 #include #include #include #include #include #include #include #include #include #include "Exception.h" #include "minecraft/OneSixVersionFormat.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/ProfileUtils.h" #include "Json.h" #include "PackProfile.h" #include "PackProfile_p.h" #include "ComponentUpdateTask.h" #include "Application.h" #include "modplatform/ResourceAPI.h" static const QMap modloaderMapping{ {"net.minecraftforge", ResourceAPI::Forge}, {"net.fabricmc.fabric-loader", ResourceAPI::Fabric}, {"org.quiltmc.quilt-loader", ResourceAPI::Quilt}, {"com.mumfrey.liteloader", ResourceAPI::LiteLoader} }; 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 = makeShared(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 comp_obj = Json::requireObject(item, "Component must be an object."); container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj)); } } catch ([[maybe_unused]] 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(); } RuntimeContext PackProfile::runtimeContext() { return d->m_instance->runtimeContext(); } 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(), static_cast(index), static_cast(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(sender()); if(!objPtr) { qWarning() << "PackProfile got dataChanged 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 dataChanged 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; } ComponentPtr PackProfile::getComponent(const QString &id) { auto iter = d->componentIndex.find(id); if (iter == d->componentIndex.end()) { return nullptr; } return (*iter); } ComponentPtr PackProfile::getComponent(size_t index) { if(index >= static_cast(d->components.size())) { return nullptr; } return d->components[index]; } 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, [[maybe_unused]] const QVariant& value, int role) { if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index.parent())) { 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 parent.isValid() ? 0 : d->components.size(); } int PackProfile::columnCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : 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); #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) d->components.swapItemsAt(index, theirIndex); #else d->components.swap(index, theirIndex); #endif endMoveRows(); invalidateLaunchProfile(); scheduleSave(); } void PackProfile::invalidateLaunchProfile() { d->m_profile.reset(); } void PackProfile::installJarMods(QStringList selectedFiles) { // FIXME: get rid of _internal installJarMods_internal(selectedFiles); } void PackProfile::installCustomJar(QString selectedFile) { // FIXME: get rid of _internal installCustomJar_internal(selectedFile); } bool PackProfile::installComponents(QStringList selectedFiles) { const QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if (!FS::ensureFolderPathExists(patchDir)) return false; bool result = true; for (const QString& source : selectedFiles) { const QFileInfo sourceInfo(source); auto versionFile = ProfileUtils::parseJsonFile(sourceInfo, false); const QString target = FS::PathCombine(patchDir, versionFile->uid + ".json"); if (!QFile::copy(source, target)) { qWarning() << "Component" << source << "could not be copied to target" << target; result = false; continue; } appendComponent(makeShared(this, versionFile->uid, versionFile)); } scheduleSave(); invalidateLaunchProfile(); return result; } void PackProfile::installAgents(QStringList selectedFiles) { // FIXME: get rid of _internal installAgents_internal(selectedFiles); } 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(); 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(makeShared(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(d->m_instance->runtimeContext(), 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); QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); QString target_filename = id + ".jar"; QString target_id = "custom.jarmod." + id; QString target_name = sourceInfo.completeBaseName() + " (jar mod)"; QString finalPath = FS::PathCombine(d->m_instance->jarModsDir(), target_filename); QFileInfo targetInfo(finalPath); Q_ASSERT(!targetInfo.exists()); if (!QFile::copy(sourceInfo.absoluteFilePath(),QFileInfo(finalPath).absoluteFilePath())) { return false; } auto f = std::make_shared(); auto jarMod = std::make_shared(); jarMod->setRawName(GradleSpecifier("custom.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(makeShared(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("custom: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(); auto jarMod = std::make_shared(); 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(makeShared(this, f->uid, f)); scheduleSave(); invalidateLaunchProfile(); return true; } bool PackProfile::installAgents_internal(QStringList filepaths) { // FIXME code duplication const QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if (!FS::ensureFolderPathExists(patchDir)) return false; const QString libDir = d->m_instance->getLocalLibraryPath(); if (!FS::ensureFolderPathExists(libDir)) return false; for (const QString& source : filepaths) { const QFileInfo sourceInfo(source); const QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); const QString targetBaseName = id + ".jar"; const QString targetId = "custom.agent." + id; const QString targetName = sourceInfo.completeBaseName() + " (agent)"; const QString target = FS::PathCombine(d->m_instance->getLocalLibraryPath(), targetBaseName); const QFileInfo targetInfo(target); Q_ASSERT(!targetInfo.exists()); if (!QFile::copy(source, target)) return false; auto versionFile = std::make_shared(); auto agent = std::make_shared(); agent->setRawName("custom.agents:" + id + ":1"); agent->setFilename(targetBaseName); agent->setDisplayName(sourceInfo.completeBaseName()); agent->setHint("local"); versionFile->agents.append(std::make_shared(agent, QString())); versionFile->name = targetName; versionFile->uid = targetId; QFile patchFile(FS::PathCombine(patchDir, targetId + ".json")); if (!patchFile.open(QFile::WriteOnly)) { qCritical() << "Error opening" << patchFile.fileName() << "for reading:" << patchFile.errorString(); return false; } patchFile.write(OneSixVersionFormat::versionFileToJson(versionFile).toJson()); patchFile.close(); appendComponent(makeShared(this, versionFile->uid, versionFile)); } scheduleSave(); invalidateLaunchProfile(); return true; } std::shared_ptr PackProfile::getProfile() const { if(!d->m_profile) { try { auto profile = std::make_shared(); 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 = makeShared(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)); } } } std::optional PackProfile::getModLoaders() { ResourceAPI::ModLoaderTypes result; bool has_any_loader = false; QMapIterator i(modloaderMapping); while (i.hasNext()) { i.next(); if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled()) { result |= i.value(); has_any_loader = true; } } if (!has_any_loader) return {}; return result; }