1227 lines
35 KiB
C++
1227 lines
35 KiB
C++
/* Copyright 2013-2021 MultiMC Contributors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include <QFile>
|
|
#include <QCryptographicHash>
|
|
#include <Version.h>
|
|
#include <QDir>
|
|
#include <QJsonDocument>
|
|
#include <QJsonArray>
|
|
#include <QDebug>
|
|
#include <QSaveFile>
|
|
#include <QUuid>
|
|
#include <QTimer>
|
|
|
|
#include "Exception.h"
|
|
#include "minecraft/OneSixVersionFormat.h"
|
|
#include "FileSystem.h"
|
|
#include "meta/Index.h"
|
|
#include "minecraft/MinecraftInstance.h"
|
|
#include "Json.h"
|
|
|
|
#include "PackProfile.h"
|
|
#include "PackProfile_p.h"
|
|
#include "ComponentUpdateTask.h"
|
|
|
|
#include "Application.h"
|
|
|
|
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->requires.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 MultiMC 5
|
|
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->requires.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.swap(index, theirIndex);
|
|
endMoveRows();
|
|
invalidateLaunchProfile();
|
|
scheduleSave();
|
|
}
|
|
|
|
void PackProfile::invalidateLaunchProfile()
|
|
{
|
|
d->m_profile.reset();
|
|
}
|
|
|
|
void PackProfile::installJarMods(QStringList selectedFiles)
|
|
{
|
|
installJarMods_internal(selectedFiles);
|
|
}
|
|
|
|
void PackProfile::installCustomJar(QString selectedFile)
|
|
{
|
|
installCustomJar_internal(selectedFile);
|
|
}
|
|
|
|
bool PackProfile::installEmpty(const QString& uid, const QString& name)
|
|
{
|
|
QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
|
|
if(!FS::ensureFolderPathExists(patchDir))
|
|
{
|
|
return false;
|
|
}
|
|
auto f = std::make_shared<VersionFile>();
|
|
f->name = name;
|
|
f->uid = uid;
|
|
f->version = "1";
|
|
QString patchFileName = FS::PathCombine(patchDir, uid + ".json");
|
|
QFile file(patchFileName);
|
|
if (!file.open(QFile::WriteOnly))
|
|
{
|
|
qCritical() << "Error opening" << file.fileName()
|
|
<< "for reading:" << file.errorString();
|
|
return false;
|
|
}
|
|
file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
|
|
file.close();
|
|
|
|
appendComponent(new Component(this, f->uid, f));
|
|
scheduleSave();
|
|
invalidateLaunchProfile();
|
|
return true;
|
|
}
|
|
|
|
bool PackProfile::removeComponent_internal(ComponentPtr patch)
|
|
{
|
|
bool ok = true;
|
|
// first, remove the patch file. this ensures it's not used anymore
|
|
auto fileName = patch->getFilename();
|
|
if(fileName.size())
|
|
{
|
|
QFile patchFile(fileName);
|
|
if(patchFile.exists() && !patchFile.remove())
|
|
{
|
|
qCritical() << "File" << fileName << "could not be removed because:" << patchFile.errorString();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// FIXME: we need a generic way of removing local resources, not just jar mods...
|
|
auto preRemoveJarMod = [&](LibraryPtr jarMod) -> bool
|
|
{
|
|
if (!jarMod->isLocal())
|
|
{
|
|
return true;
|
|
}
|
|
QStringList jar, temp1, temp2, temp3;
|
|
jarMod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, d->m_instance->jarmodsPath().absolutePath());
|
|
QFileInfo finfo (jar[0]);
|
|
if(finfo.exists())
|
|
{
|
|
QFile jarModFile(jar[0]);
|
|
if(!jarModFile.remove())
|
|
{
|
|
qCritical() << "File" << jar[0] << "could not be removed because:" << jarModFile.errorString();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
auto vFile = patch->getVersionFile();
|
|
if(vFile)
|
|
{
|
|
auto &jarMods = vFile->jarMods;
|
|
for(auto &jarmod: jarMods)
|
|
{
|
|
ok &= preRemoveJarMod(jarmod);
|
|
}
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
bool PackProfile::installJarMods_internal(QStringList filepaths)
|
|
{
|
|
QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
|
|
if(!FS::ensureFolderPathExists(patchDir))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!FS::ensureFolderPathExists(d->m_instance->jarModsDir()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for(auto filepath:filepaths)
|
|
{
|
|
QFileInfo sourceInfo(filepath);
|
|
auto uuid = QUuid::createUuid();
|
|
QString id = uuid.toString().remove('{').remove('}');
|
|
QString target_filename = id + ".jar";
|
|
QString target_id = "org.multimc.jarmod." + id;
|
|
QString target_name = sourceInfo.completeBaseName() + " (jar mod)";
|
|
QString finalPath = FS::PathCombine(d->m_instance->jarModsDir(), target_filename);
|
|
|
|
QFileInfo targetInfo(finalPath);
|
|
if(targetInfo.exists())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!QFile::copy(sourceInfo.absoluteFilePath(),QFileInfo(finalPath).absoluteFilePath()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto f = std::make_shared<VersionFile>();
|
|
auto jarMod = std::make_shared<Library>();
|
|
jarMod->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1"));
|
|
jarMod->setFilename(target_filename);
|
|
jarMod->setDisplayName(sourceInfo.completeBaseName());
|
|
jarMod->setHint("local");
|
|
f->jarMods.append(jarMod);
|
|
f->name = target_name;
|
|
f->uid = target_id;
|
|
QString patchFileName = FS::PathCombine(patchDir, target_id + ".json");
|
|
|
|
QFile file(patchFileName);
|
|
if (!file.open(QFile::WriteOnly))
|
|
{
|
|
qCritical() << "Error opening" << file.fileName()
|
|
<< "for reading:" << file.errorString();
|
|
return false;
|
|
}
|
|
file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
|
|
file.close();
|
|
|
|
appendComponent(new Component(this, f->uid, f));
|
|
}
|
|
scheduleSave();
|
|
invalidateLaunchProfile();
|
|
return true;
|
|
}
|
|
|
|
bool PackProfile::installCustomJar_internal(QString filepath)
|
|
{
|
|
QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
|
|
if(!FS::ensureFolderPathExists(patchDir))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
QString libDir = d->m_instance->getLocalLibraryPath();
|
|
if (!FS::ensureFolderPathExists(libDir))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto specifier = GradleSpecifier("org.multimc:customjar:1");
|
|
QFileInfo sourceInfo(filepath);
|
|
QString target_filename = specifier.getFileName();
|
|
QString target_id = specifier.artifactId();
|
|
QString target_name = sourceInfo.completeBaseName() + " (custom jar)";
|
|
QString finalPath = FS::PathCombine(libDir, target_filename);
|
|
|
|
QFileInfo jarInfo(finalPath);
|
|
if (jarInfo.exists())
|
|
{
|
|
if(!QFile::remove(finalPath))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
if (!QFile::copy(filepath, finalPath))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto f = std::make_shared<VersionFile>();
|
|
auto jarMod = std::make_shared<Library>();
|
|
jarMod->setRawName(specifier);
|
|
jarMod->setDisplayName(sourceInfo.completeBaseName());
|
|
jarMod->setHint("local");
|
|
f->mainJar = jarMod;
|
|
f->name = target_name;
|
|
f->uid = target_id;
|
|
QString patchFileName = FS::PathCombine(patchDir, target_id + ".json");
|
|
|
|
QFile file(patchFileName);
|
|
if (!file.open(QFile::WriteOnly))
|
|
{
|
|
qCritical() << "Error opening" << file.fileName()
|
|
<< "for reading:" << file.errorString();
|
|
return false;
|
|
}
|
|
file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
|
|
file.close();
|
|
|
|
appendComponent(new Component(this, f->uid, f));
|
|
|
|
scheduleSave();
|
|
invalidateLaunchProfile();
|
|
return true;
|
|
}
|
|
|
|
std::shared_ptr<LaunchProfile> PackProfile::getProfile() const
|
|
{
|
|
if(!d->m_profile)
|
|
{
|
|
try
|
|
{
|
|
auto profile = std::make_shared<LaunchProfile>();
|
|
for(auto file: d->components)
|
|
{
|
|
qDebug() << "Applying" << file->getID() << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD");
|
|
file->applyTo(profile.get());
|
|
}
|
|
d->m_profile = profile;
|
|
}
|
|
catch (const Exception &error)
|
|
{
|
|
qWarning() << "Couldn't apply profile patches because: " << error.cause();
|
|
}
|
|
}
|
|
return d->m_profile;
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|