/* 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 .
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Exception.h"
#include "minecraft/OneSixVersionFormat.h"
#include "FileSystem.h"
#include "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 components;
QSet 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 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 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(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();
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();
auto jarMod = std::make_shared();
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();
auto jarMod = std::make_shared();
jarMod->setRawName(specifier);
jarMod->setDisplayName(sourceInfo.completeBaseName());
jarMod->setHint("local");
f->mainJar = jarMod;
f->name = target_name;
f->uid = target_id;
QString patchFileName = FS::PathCombine(patchDir, target_id + ".json");
QFile file(patchFileName);
if (!file.open(QFile::WriteOnly)) {
qCritical() << "Error opening" << file.fileName()
<< "for reading:" << file.errorString();
return false;
}
file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
file.close();
appendComponent(new Component(this, f->uid, f));
scheduleSave();
invalidateLaunchProfile();
return true;
}
std::shared_ptr PackProfile::getProfile() const
{
if (!d->m_profile) {
try {
auto profile = std::make_shared();
for (auto file : d->components) {
qDebug() << "Applying" << file->getID()
<< (file->getProblemSeverity() ==
ProblemSeverity::Error
? "ERROR"
: "GOOD");
file->applyTo(profile.get());
}
d->m_profile = profile;
} catch (const Exception& error) {
qWarning() << "Couldn't apply profile patches because: "
<< error.cause();
}
}
return d->m_profile;
}
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));
}
}
}