summaryrefslogtreecommitdiff
path: root/meshmc/launcher/minecraft/PackProfile.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'meshmc/launcher/minecraft/PackProfile.cpp')
-rw-r--r--meshmc/launcher/minecraft/PackProfile.cpp1191
1 files changed, 1191 insertions, 0 deletions
diff --git a/meshmc/launcher/minecraft/PackProfile.cpp b/meshmc/launcher/minecraft/PackProfile.cpp
new file mode 100644
index 0000000000..0bff119c66
--- /dev/null
+++ b/meshmc/launcher/minecraft/PackProfile.cpp
@@ -0,0 +1,1191 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 "meta/Index.h"
+#include "minecraft/MinecraftInstance.h"
+#include "Json.h"
+
+#include "PackProfile.h"
+#include "PackProfile_p.h"
+#include "ComponentUpdateTask.h"
+
+#include "Application.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);
+ }
+ 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();
+ QFile componentsFile(filename);
+
+ // migrate old config to new one, if needed
+ if (!componentsFile.exists()) {
+ if (!migratePreComponentConfig()) {
+ // FIXME: the user should be notified...
+ qCritical()
+ << "Failed to convert old pre-component config for instance"
+ << d->m_instance->name();
+ return false;
+ }
+ }
+
+ // 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();
+}
+
+// NOTE this is really old stuff, and only needs to be used when loading the old
+// hardcoded component-unaware format (loadPreComponentConfig).
+static void upgradeDeprecatedFiles(QString root, QString instanceName)
+{
+ auto versionJsonPath = FS::PathCombine(root, "version.json");
+ auto customJsonPath = FS::PathCombine(root, "custom.json");
+ auto mcJson = FS::PathCombine(root, "patches", "net.minecraft.json");
+
+ QString sourceFile;
+ QString renameFile;
+
+ // convert old crap.
+ if (QFile::exists(customJsonPath)) {
+ sourceFile = customJsonPath;
+ renameFile = versionJsonPath;
+ } else if (QFile::exists(versionJsonPath)) {
+ sourceFile = versionJsonPath;
+ }
+ if (!sourceFile.isEmpty() && !QFile::exists(mcJson)) {
+ if (!FS::ensureFilePathExists(mcJson)) {
+ qWarning() << "Couldn't create patches folder for" << instanceName;
+ return;
+ }
+ if (!renameFile.isEmpty() && QFile::exists(renameFile)) {
+ if (!QFile::rename(renameFile, renameFile + ".old")) {
+ qWarning() << "Couldn't rename" << renameFile << "to"
+ << renameFile + ".old" << "in" << instanceName;
+ return;
+ }
+ }
+ auto file = ProfileUtils::parseJsonFile(QFileInfo(sourceFile), false);
+ ProfileUtils::removeLwjglFromPatch(file);
+ file->uid = "net.minecraft";
+ file->version = file->minecraftVersion;
+ file->name = "Minecraft";
+
+ Meta::Require needsLwjgl;
+ needsLwjgl.uid = "org.lwjgl";
+ file->requirements.insert(needsLwjgl);
+
+ if (!ProfileUtils::saveJsonFile(
+ OneSixVersionFormat::versionFileToJson(file), mcJson)) {
+ return;
+ }
+ if (!QFile::rename(sourceFile, sourceFile + ".old")) {
+ qWarning() << "Couldn't rename" << sourceFile << "to"
+ << sourceFile + ".old" << "in" << instanceName;
+ return;
+ }
+ }
+}
+
+/*
+ * Migrate old layout to the component based one...
+ * - Part of the version information is taken from `instance.cfg` (fed to this
+ * class from outside).
+ * - Part is taken from the old order.json file.
+ * - Part is loaded from loose json files in the instance's `patches` directory.
+ */
+bool PackProfile::migratePreComponentConfig()
+{
+ // upgrade the very old files from the beginnings of MeshMC
+ upgradeDeprecatedFiles(d->m_instance->instanceRoot(),
+ d->m_instance->name());
+
+ QList<ComponentPtr> components;
+ QSet<QString> loaded;
+
+ auto addBuiltinPatch =
+ [&](const QString& uid, bool asDependency, const QString& emptyVersion,
+ const Meta::Require& req, const Meta::Require& conflict) {
+ auto jsonFilePath = FS::PathCombine(d->m_instance->instanceRoot(),
+ "patches", uid + ".json");
+ auto intendedVersion = d->getOldConfigVersion(uid);
+ // load up the base minecraft patch
+ ComponentPtr component;
+ if (QFile::exists(jsonFilePath)) {
+ if (intendedVersion.isEmpty()) {
+ intendedVersion = emptyVersion;
+ }
+ auto file =
+ ProfileUtils::parseJsonFile(QFileInfo(jsonFilePath), false);
+ // fix uid
+ file->uid = uid;
+ // if version is missing, add it from the outside.
+ if (file->version.isEmpty()) {
+ file->version = intendedVersion;
+ }
+ // if this is a dependency (LWJGL), mark it also as volatile
+ if (asDependency) {
+ file->m_volatile = true;
+ }
+ // insert requirements if needed
+ if (!req.uid.isEmpty()) {
+ file->requirements.insert(req);
+ }
+ // insert conflicts if needed
+ if (!conflict.uid.isEmpty()) {
+ file->conflicts.insert(conflict);
+ }
+ // FIXME: @QUALITY do not ignore return value
+ ProfileUtils::saveJsonFile(
+ OneSixVersionFormat::versionFileToJson(file), jsonFilePath);
+ component = new Component(this, uid, file);
+ component->m_version = intendedVersion;
+ } else if (!intendedVersion.isEmpty()) {
+ auto metaVersion =
+ APPLICATION->metadataIndex()->get(uid, intendedVersion);
+ component = new Component(this, metaVersion);
+ } else {
+ return;
+ }
+ component->m_dependencyOnly = asDependency;
+ component->m_important = !asDependency;
+ components.append(component);
+ };
+ // TODO: insert depends and conflicts here if these are customized files...
+ Meta::Require reqLwjgl;
+ reqLwjgl.uid = "org.lwjgl";
+ reqLwjgl.suggests = "2.9.1";
+ Meta::Require conflictLwjgl3;
+ conflictLwjgl3.uid = "org.lwjgl3";
+ Meta::Require nullReq;
+ addBuiltinPatch("org.lwjgl", true, "2.9.1", nullReq, conflictLwjgl3);
+ addBuiltinPatch("net.minecraft", false, QString(), reqLwjgl, nullReq);
+
+ // first, collect all other file-based patches and load them
+ QMap<QString, ComponentPtr> loadedComponents;
+ QDir patchesDir(FS::PathCombine(d->m_instance->instanceRoot(), "patches"));
+ for (auto info :
+ patchesDir.entryInfoList(QStringList() << "*.json", QDir::Files)) {
+ // parse the file
+ qDebug() << "Reading" << info.fileName();
+ auto file = ProfileUtils::parseJsonFile(info, true);
+
+ // correct missing or wrong uid based on the file name
+ QString uid = info.completeBaseName();
+
+ // ignore builtins, they've been handled already
+ if (uid == "net.minecraft")
+ continue;
+ if (uid == "org.lwjgl")
+ continue;
+
+ // handle horrible corner cases
+ if (uid.isEmpty()) {
+ // if you have a file named '.json', make it just go away.
+ // FIXME: @QUALITY do not ignore return value
+ QFile::remove(info.absoluteFilePath());
+ continue;
+ }
+ file->uid = uid;
+ // FIXME: @QUALITY do not ignore return value
+ ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file),
+ info.absoluteFilePath());
+
+ auto component = new Component(this, file->uid, file);
+ auto version = d->getOldConfigVersion(file->uid);
+ if (!version.isEmpty()) {
+ component->m_version = version;
+ }
+ loadedComponents[file->uid] = component;
+ }
+ // try to load the other 'hardcoded' patches (forge, liteloader), if they
+ // weren't loaded from files
+ auto loadSpecial = [&](const QString& uid, int order) {
+ auto patchVersion = d->getOldConfigVersion(uid);
+ if (!patchVersion.isEmpty() && !loadedComponents.contains(uid)) {
+ auto patch = new Component(
+ this, APPLICATION->metadataIndex()->get(uid, patchVersion));
+ patch->setOrder(order);
+ loadedComponents[uid] = patch;
+ }
+ };
+ loadSpecial("net.minecraftforge", 5);
+ loadSpecial("com.mumfrey.liteloader", 10);
+
+ // load the old order.json file, if present
+ ProfileUtils::PatchOrder userOrder;
+ ProfileUtils::readOverrideOrders(
+ FS::PathCombine(d->m_instance->instanceRoot(), "order.json"),
+ userOrder);
+
+ // now add all the patches by user sort order
+ for (auto uid : userOrder) {
+ // ignore builtins
+ if (uid == "net.minecraft")
+ continue;
+ if (uid == "org.lwjgl")
+ continue;
+ // ordering has a patch that is gone?
+ if (!loadedComponents.contains(uid)) {
+ continue;
+ }
+ components.append(loadedComponents.take(uid));
+ }
+
+ // is there anything left to sort? - this is used when there are leftover
+ // components that aren't part of the order.json
+ if (!loadedComponents.isEmpty()) {
+ // inserting into multimap by order number as key sorts the patches and
+ // detects duplicates
+ QMultiMap<int, ComponentPtr> files;
+ auto iter = loadedComponents.begin();
+ while (iter != loadedComponents.end()) {
+ files.insert((*iter)->getOrder(), *iter);
+ iter++;
+ }
+
+ // then just extract the patches and put them in the list
+ for (auto order : files.keys()) {
+ const auto& values = files.values(order);
+ for (auto& value : values) {
+ // TODO: put back the insertion of problem messages here, so the
+ // user knows about the id duplication
+ components.append(value);
+ }
+ }
+ }
+ // new we have a complete list of components...
+ return savePackProfile(componentsFilePath(), components);
+}
+
+// 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.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::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.projecttick.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.projecttick.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.projecttick: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;
+}
+
+void PackProfile::setOldConfigVersion(const QString& uid,
+ const QString& version)
+{
+ if (version.isEmpty()) {
+ return;
+ }
+ d->m_oldConfigVersions[uid] = version;
+}
+
+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));
+ }
+ }
+}