PrismLauncher/launcher/minecraft/PackProfile.cpp
flow 6a18079953
refactor: generalize mod models and APIs to resources
Firstly, this abstract away behavior in the mod download models that can
also be applied to other types of resources into a superclass, allowing
other resource types to be implemented without so much code duplication.

For that, this also generalizes the APIs used (currently, ModrinthAPI
and FlameAPI) to be able to make requests to other types of resources.

It also does a general cleanup of both of those. In particular, this
makes use of std::optional instead of invalid values for errors and,
well, optional values :p

This is a squash of some commits that were becoming too interlaced
together to be cleanly separated.

Signed-off-by: flow <flowlnlnln@gmail.com>
2023-01-13 16:23:00 -03:00

1089 lines
30 KiB
C++

// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* 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 <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 <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 "minecraft/MinecraftInstance.h"
#include "Json.h"
#include "PackProfile.h"
#include "PackProfile_p.h"
#include "ComponentUpdateTask.h"
#include "Application.h"
#include "modplatform/ResourceAPI.h"
static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{
{"net.minecraftforge", ResourceAPI::Forge},
{"net.fabricmc.fabric-loader", ResourceAPI::Fabric},
{"org.quiltmc.quilt-loader", ResourceAPI::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();
}
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(), 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.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)
{
installJarMods_internal(selectedFiles);
}
void PackProfile::installCustomJar(QString selectedFile)
{
installCustomJar_internal(selectedFile);
}
void PackProfile::installAgents(QStringList selectedFiles)
{
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<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(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<VersionFile>();
auto jarMod = std::make_shared<Library>();
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(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("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<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;
}
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<VersionFile>();
auto agent = std::make_shared<Library>();
agent->setRawName("custom.agents:" + id + ":1");
agent->setFilename(targetBaseName);
agent->setDisplayName(sourceInfo.completeBaseName());
agent->setHint("local");
versionFile->agents.append(std::make_shared<Agent>(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(new Component(this, versionFile->uid, versionFile));
}
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));
}
}
}
std::optional<ResourceAPI::ModLoaderTypes> PackProfile::getModLoaders()
{
ResourceAPI::ModLoaderTypes result;
bool has_any_loader = false;
QMapIterator<QString, ResourceAPI::ModLoaderType> 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;
}