997 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			997 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);
 | |
|     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;
 | |
| }
 |