summaryrefslogtreecommitdiff
path: root/archived/projt-launcher/launcher/minecraft/PackProfile.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'archived/projt-launcher/launcher/minecraft/PackProfile.cpp')
-rw-r--r--archived/projt-launcher/launcher/minecraft/PackProfile.cpp1311
1 files changed, 1311 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/minecraft/PackProfile.cpp b/archived/projt-launcher/launcher/minecraft/PackProfile.cpp
new file mode 100644
index 0000000000..0fd908e8a6
--- /dev/null
+++ b/archived/projt-launcher/launcher/minecraft/PackProfile.cpp
@@ -0,0 +1,1311 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (C) 2022-2023 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * 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 <Version.h>
+#include <qlogging.h>
+#include <QCryptographicHash>
+#include <QDebug>
+#include <QDir>
+#include <QFile>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QSaveFile>
+#include <QTimer>
+#include <QUuid>
+#include <algorithm>
+#include <utility>
+
+#include "Application.h"
+#include "Exception.h"
+#include "FileSystem.h"
+#include "Json.h"
+#include "meta/Index.hpp"
+#include "meta/JsonFormat.hpp"
+#include "minecraft/Component.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/OneSixVersionFormat.h"
+#include "minecraft/ProfileUtils.h"
+
+#include "ComponentUpdateTask.h"
+#include "PackProfile.h"
+#include "PackProfile_p.h"
+#include "modplatform/ModIndex.h"
+
+#include "minecraft/Logging.h"
+
+#include "ui/dialogs/CustomMessageBox.h"
+
+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);
+ }
+ projt::meta::writeDependencies(obj, component->m_cachedRequires, "cachedRequires");
+ projt::meta::writeDependencies(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<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 values - use safe parsing with fallbacks for resilience
+ // Invalid or missing values are silently ignored to allow loading of
+ // partially corrupted profiles
+ try
+ {
+ component->m_cachedVersion = Json::ensureString(obj.value("cachedVersion"));
+ }
+ catch (...)
+ {
+ component->m_cachedVersion = QString();
+ }
+
+ try
+ {
+ component->m_cachedName = Json::ensureString(obj.value("cachedName"));
+ }
+ catch (...)
+ {
+ component->m_cachedName = QString();
+ }
+
+ try
+ {
+ component->m_cachedRequires = projt::meta::parseDependencies(obj, "cachedRequires");
+ }
+ catch (...)
+ {
+ component->m_cachedRequires = {};
+ }
+
+ try
+ {
+ component->m_cachedConflicts = projt::meta::parseDependencies(obj, "cachedConflicts");
+ }
+ catch (...)
+ {
+ component->m_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))
+ {
+ qCCritical(instanceProfileC) << "Couldn't open" << outFile.fileName()
+ << "for writing:" << outFile.errorString();
+ return false;
+ }
+ auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented);
+ if (outFile.write(data) != data.size())
+ {
+ qCCritical(instanceProfileC) << "Couldn't write all the data into" << outFile.fileName()
+ << "because:" << outFile.errorString();
+ return false;
+ }
+ if (!outFile.commit())
+ {
+ qCCritical(instanceProfileC) << "Couldn't save" << outFile.fileName() << "because:" << outFile.errorString();
+ }
+ return true;
+}
+
+// Read the given file into component containers
+static PackProfile::Result loadPackProfile(PackProfile* parent,
+ const QString& filename,
+ const QString& componentJsonPattern,
+ ComponentContainer& container)
+{
+ QFile componentsFile(filename);
+ if (!componentsFile.exists())
+ {
+ auto message = QObject::tr("Components file %1 doesn't exist. This should never happen.").arg(filename);
+ qCWarning(instanceProfileC) << message;
+ return PackProfile::Result::Error(message);
+ }
+ if (!componentsFile.open(QFile::ReadOnly))
+ {
+ auto message = QObject::tr("Couldn't open %1 for reading: %2")
+ .arg(componentsFile.fileName(), componentsFile.errorString());
+ qCCritical(instanceProfileC) << message;
+ qCWarning(instanceProfileC) << "Ignoring overridden order";
+ return PackProfile::Result::Error(message);
+ }
+
+ // and it's valid JSON
+ QJsonParseError error;
+ QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error);
+ if (error.error != QJsonParseError::NoError)
+ {
+ auto message = QObject::tr("Couldn't parse %1 as json: %2").arg(componentsFile.fileName(), error.errorString());
+ qCCritical(instanceProfileC) << message;
+ qCWarning(instanceProfileC) << "Ignoring overridden order";
+ return PackProfile::Result::Error(message);
+ }
+
+ // 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)
+ {
+ auto message = QObject::tr("Couldn't parse %1 : bad file format").arg(componentsFile.fileName());
+ qCCritical(instanceProfileC) << message;
+ qCWarning(instanceProfileC) << "error:" << err.what();
+ container.clear();
+ return PackProfile::Result::Error(message);
+ }
+ return PackProfile::Result::Success();
+}
+
+// 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() << d->m_instance->name() << "|"
+ << "Component list should never save if it didn't successfully load";
+ return;
+ }
+ if (!d->dirty)
+ {
+ d->dirty = true;
+ qDebug() << d->m_instance->name() << "|"
+ << "Component list save is scheduled";
+ }
+ 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() << d->m_instance->name() << "|"
+ << "Component list save performed now";
+ auto filename = componentsFilePath();
+ savePackProfile(filename, d->components);
+ d->dirty = false;
+}
+
+PackProfile::Result PackProfile::load()
+{
+ auto filename = componentsFilePath();
+
+ // load the new component list and swap it with the current one...
+ ComponentContainer newComponents;
+ if (auto result = loadPackProfile(this, filename, patchesPattern(), newComponents); !result)
+ {
+ qCritical() << d->m_instance->name() << "|"
+ << "Failed to load the component config";
+ return result;
+ }
+
+ // Optimization: check if there are any changes before resetting the model
+ bool changed = false;
+ if (d->components.size() != newComponents.size())
+ {
+ changed = true;
+ }
+ else
+ {
+ for (int i = 0; i < d->components.size(); ++i)
+ {
+ const auto& oldC = d->components[i];
+ const auto& newC = newComponents[i];
+ if (oldC->getID() != newC->getID() || oldC->getVersion() != newC->getVersion()
+ || oldC->m_important != newC->m_important || oldC->m_disabled != newC->m_disabled
+ || oldC->m_dependencyOnly != newC->m_dependencyOnly)
+ {
+ changed = true;
+ break;
+ }
+ }
+ }
+
+ if (!changed)
+ {
+ d->loaded = true;
+ return Result::Success(false);
+ }
+
+ // NOTE: 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() << d->m_instance->name() << "|"
+ << "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 Result::Success();
+}
+
+PackProfile::Result PackProfile::reload(Net::Mode netmode)
+{
+ // Do not reload when the update/resolve task is running. It is in control.
+ if (d->m_updateTask)
+ {
+ return Result::Success();
+ }
+
+ // flush any scheduled saves to not lose state
+ saveNow();
+
+ auto result = load();
+ if (!result)
+ {
+ return result;
+ }
+
+ if (result.changed)
+ {
+ invalidateLaunchProfile();
+ }
+
+ resolve(netmode);
+ return Result::Success();
+}
+
+Task::Ptr PackProfile::getCurrentTask()
+{
+ return d->m_updateTask;
+}
+
+void PackProfile::resolve(Net::Mode netmode)
+{
+ // Use Mode::Launch to ensure version details are downloaded, not just version lists
+ auto updateTask = new ComponentUpdateTask(ComponentUpdateTask::Mode::Launch, 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()
+{
+ qCDebug(instanceProfileC) << d->m_instance->name() << "|"
+ << "Component list update/resolve task succeeded";
+ d->m_updateTask.reset();
+ invalidateLaunchProfile();
+}
+
+void PackProfile::updateFailed(const QString& error)
+{
+ qCDebug(instanceProfileC) << d->m_instance->name() << "|"
+ << "Component list update/resolve task failed "
+ << "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())
+ {
+ qCWarning(instanceProfileC) << d->m_instance->name() << "|"
+ << "Attempt to add a component with empty ID!";
+ return;
+ }
+ if (d->componentIndex.contains(id))
+ {
+ qCWarning(instanceProfileC) << d->m_instance->name() << "|"
+ << "Attempt to add a component that is already present!";
+ return;
+ }
+ beginInsertRows(QModelIndex(), static_cast<int>(index), static_cast<int>(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)
+ {
+ qCWarning(instanceProfileC) << d->m_instance->name() << "|"
+ << "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++;
+ }
+ qCWarning(instanceProfileC) << d->m_instance->name() << "|"
+ << "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())
+ {
+ qCWarning(instanceProfileC) << d->m_instance->name() << "|"
+ << "Patch" << patch->getID() << "is non-removable";
+ return false;
+ }
+
+ if (!removeComponent_internal(patch))
+ {
+ qCCritical(instanceProfileC) << d->m_instance->name() << "|"
+ << "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())
+ {
+ qCDebug(instanceProfileC) << d->m_instance->name() << "|"
+ << "Patch" << patch->getID() << "is not customizable";
+ return false;
+ }
+ if (!patch->customize())
+ {
+ qCCritical(instanceProfileC) << d->m_instance->name() << "|"
+ << "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())
+ {
+ qCDebug(instanceProfileC) << d->m_instance->name() << "|"
+ << "Patch" << patch->getID() << "is not revertible";
+ return false;
+ }
+ if (!patch->revert())
+ {
+ qCCritical(instanceProfileC) << d->m_instance->name() << "|"
+ << "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<size_t>(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:
+ {
+ if (column == NameColumn)
+ return patch->isEnabled() ? Qt::Checked : Qt::Unchecked;
+ 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:
+ {
+ if (column == NameColumn)
+ {
+ auto severity = patch->getProblemSeverity();
+ switch (severity)
+ {
+ case ProblemSeverity::Warning: return "warning";
+ case ProblemSeverity::Error: return "error";
+ default: return QVariant();
+ }
+ }
+ 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();
+}
+
+// Note: This method intentionally uses no precision for row indices -
+// items are indexed by position, not by any floating-point value
+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);
+ // Components can only be toggled if they support disabling and the profile isn't locked
+ 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);
+ d->components.swapItemsAt(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::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))
+ {
+ qCWarning(instanceProfileC) << d->m_instance->name() << "|"
+ << "Component" << source << "could not be copied to target" << target;
+ result = false;
+ continue;
+ }
+
+ appendComponent(makeShared<Component>(this, versionFile->uid, versionFile));
+ }
+
+ scheduleSave();
+ invalidateLaunchProfile();
+
+ return result;
+}
+
+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))
+ {
+ qCCritical(instanceProfileC) << d->m_instance->name() << "|"
+ << "Error opening" << file.fileName() << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
+ file.close();
+
+ appendComponent(makeShared<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())
+ {
+ qCCritical(instanceProfileC) << d->m_instance->name() << "|"
+ << "File" << fileName
+ << "could not be removed because:" << patchFile.errorString();
+ return false;
+ }
+ }
+
+ // Generic local resource removal
+ // Handles jar mods, mods, and local libraries
+ auto removeLocalLibrary = [this](LibraryPtr lib, const QString& overridePath = QString()) -> bool
+ {
+ if (!lib->isLocal())
+ {
+ return true;
+ }
+ QStringList output, temp1, temp2, temp3;
+ lib->getApplicableFiles(d->m_instance->runtimeContext(), output, temp1, temp2, temp3, overridePath);
+ for (const auto& file : output)
+ {
+ QFile f(file);
+ if (f.exists() && !f.remove())
+ {
+ qCCritical(instanceProfileC) << d->m_instance->name() << "|"
+ << "File" << file << "could not be removed because:" << f.errorString();
+ return false;
+ }
+ }
+ return true;
+ };
+
+ auto vFile = patch->getVersionFile();
+ if (vFile)
+ {
+ // Jar Mods
+ for (auto& lib : vFile->jarMods)
+ {
+ ok &= removeLocalLibrary(lib, d->m_instance->jarmodsPath().absolutePath());
+ }
+ // Mods (Loader mods, generic mods) - Assuming they reside in 'mods' folder
+ QString modsPath = FS::PathCombine(d->m_instance->instanceRoot(), "mods");
+ for (auto& lib : vFile->mods)
+ {
+ ok &= removeLocalLibrary(lib, modsPath);
+ }
+ // Local Libraries (in libraries folder)
+ for (auto& lib : vFile->libraries)
+ {
+ if (lib->isLocal())
+ {
+ ok &= removeLocalLibrary(lib);
+ }
+ }
+ }
+ return ok;
+}
+
+bool PackProfile::ensureInstallDirs(QString& patchDir, QString& libDir)
+{
+ patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
+ if (!FS::ensureFolderPathExists(patchDir))
+ return false;
+
+ libDir = d->m_instance->getLocalLibraryPath();
+ if (!FS::ensureFolderPathExists(libDir))
+ return false;
+
+ return true;
+}
+
+bool PackProfile::writeVersionFile(const QString& patchDir, const std::shared_ptr<VersionFile>& versionFile)
+{
+ QString patchFileName = FS::PathCombine(patchDir, versionFile->uid + ".json");
+ QFile file(patchFileName);
+ if (!file.open(QFile::WriteOnly))
+ {
+ qCCritical(instanceProfileC) << d->m_instance->name() << "|"
+ << "Error opening" << file.fileName() << "for writing:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(versionFile).toJson());
+ file.close();
+
+ appendComponent(makeShared<Component>(this, versionFile->uid, versionFile));
+ return true;
+}
+
+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))
+ {
+ qCCritical(instanceProfileC) << d->m_instance->name() << "|"
+ << "Error opening" << file.fileName() << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
+ file.close();
+
+ appendComponent(makeShared<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 (!FS::deletePath(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))
+ {
+ qCCritical(instanceProfileC) << d->m_instance->name() << "|"
+ << "Error opening" << file.fileName() << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
+ file.close();
+
+ appendComponent(makeShared<Component>(this, f->uid, f));
+
+ scheduleSave();
+ invalidateLaunchProfile();
+ return true;
+}
+
+bool PackProfile::installAgents_internal(QStringList filepaths)
+{
+ QString patchDir, libDir;
+ if (!ensureInstallDirs(patchDir, 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(libDir, 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;
+
+ if (!writeVersionFile(patchDir, versionFile))
+ return false;
+ }
+
+ 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)
+ {
+ qCDebug(instanceProfileC) << d->m_instance->name() << "|"
+ << "Applying" << file->getID()
+ << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD");
+ file->applyTo(profile.get());
+ }
+ d->m_profile = profile;
+ }
+ catch (const Exception& error)
+ {
+ qCWarning(instanceProfileC) << d->m_instance->name() << "|"
+ << "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())
+ {
+ // set new version
+ auto oldVersion = component->getVersion();
+ component->setVersion(version);
+ component->setImportant(important);
+
+ if (important)
+ {
+ component->setUpdateAction(UpdateAction{ UpdateActionImportantChanged{ oldVersion } });
+ resolve(Net::Mode::Online);
+ }
+
+ return true;
+ }
+ return false;
+ }
+ else
+ {
+ // add new
+ auto component = makeShared<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<ModPlatform::ModLoaderTypes> PackProfile::getModLoaders()
+{
+ ModPlatform::ModLoaderTypes result;
+ bool has_any_loader = false;
+
+ QMapIterator<QString, ModloaderMapEntry> i(Component::KNOWN_MODLOADERS);
+
+ while (i.hasNext())
+ {
+ i.next();
+ if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled())
+ {
+ result |= i.value().type;
+ has_any_loader = true;
+ }
+ }
+
+ if (!has_any_loader)
+ return {};
+ return result;
+}
+
+std::optional<ModPlatform::ModLoaderTypes> PackProfile::getSupportedModLoaders()
+{
+ auto loadersOpt = getModLoaders();
+ if (!loadersOpt.has_value())
+ return loadersOpt;
+ auto loaders = loadersOpt.value();
+ if (loaders & ModPlatform::Quilt)
+ loaders |= ModPlatform::Fabric;
+ if (getComponentVersion("net.minecraft") == "1.20.1" && (loaders & ModPlatform::NeoForge))
+ loaders |= ModPlatform::Forge;
+ return loaders;
+}
+
+QList<ModPlatform::ModLoaderType> PackProfile::getModLoadersList()
+{
+ QList<ModPlatform::ModLoaderType> result;
+ for (auto c : d->components)
+ {
+ if (c->isEnabled() && Component::KNOWN_MODLOADERS.contains(c->getID()))
+ {
+ result.append(Component::KNOWN_MODLOADERS[c->getID()].type);
+ }
+ }
+
+ // Quilt provides Fabric compatibility for Minecraft versions < 1.22
+ // This may change when Quilt drops official Fabric support in future versions
+ if (result.contains(ModPlatform::Quilt) && !result.contains(ModPlatform::Fabric))
+ {
+ auto mcVersion = getComponentVersion("net.minecraft");
+ Version minecraftVer(mcVersion);
+ // Assume Quilt maintains Fabric compat for versions before 1.22
+ if (minecraftVer < Version("1.22"))
+ {
+ result.append(ModPlatform::Fabric);
+ }
+ }
+ if (getComponentVersion("net.minecraft") == "1.20.1" && result.contains(ModPlatform::NeoForge)
+ && !result.contains(ModPlatform::Forge))
+ {
+ result.append(ModPlatform::Forge);
+ }
+ return result;
+}