From a0ef20a264656c127dd62eb9140e89a2fda6a8e0 Mon Sep 17 00:00:00 2001 From: Petr Mrázek Date: Sat, 27 Jun 2020 12:02:31 +0200 Subject: NOISSUE rename ComponentList to PackProfile It's not just components, so the naming needed cleaning up. --- api/logic/minecraft/Component.cpp | 8 +- api/logic/minecraft/Component.h | 10 +- api/logic/minecraft/ComponentList.cpp | 1225 ---------------------- api/logic/minecraft/ComponentList.h | 150 --- api/logic/minecraft/ComponentList_p.h | 43 - api/logic/minecraft/ComponentUpdateTask.cpp | 20 +- api/logic/minecraft/ComponentUpdateTask.h | 4 +- api/logic/minecraft/ComponentUpdateTask_p.h | 6 +- api/logic/minecraft/MinecraftInstance.cpp | 10 +- api/logic/minecraft/MinecraftInstance.h | 6 +- api/logic/minecraft/MinecraftLoadAndCheck.cpp | 4 +- api/logic/minecraft/MinecraftUpdate.cpp | 4 +- api/logic/minecraft/OneSixVersionFormat.h | 2 +- api/logic/minecraft/PackProfile.cpp | 1225 ++++++++++++++++++++++ api/logic/minecraft/PackProfile.h | 150 +++ api/logic/minecraft/PackProfile_p.h | 43 + api/logic/minecraft/VersionFile.cpp | 2 +- api/logic/minecraft/VersionFile.h | 2 +- api/logic/minecraft/launch/ModMinecraftJar.cpp | 4 +- api/logic/minecraft/launch/ReconstructAssets.cpp | 4 +- api/logic/minecraft/legacy/LegacyUpgradeTask.cpp | 4 +- api/logic/minecraft/update/AssetUpdateTask.cpp | 6 +- api/logic/minecraft/update/FMLLibrariesTask.cpp | 4 +- api/logic/minecraft/update/LibrariesTask.cpp | 4 +- 24 files changed, 1470 insertions(+), 1470 deletions(-) delete mode 100644 api/logic/minecraft/ComponentList.cpp delete mode 100644 api/logic/minecraft/ComponentList.h delete mode 100644 api/logic/minecraft/ComponentList_p.h create mode 100644 api/logic/minecraft/PackProfile.cpp create mode 100644 api/logic/minecraft/PackProfile.h create mode 100644 api/logic/minecraft/PackProfile_p.h (limited to 'api/logic/minecraft') diff --git a/api/logic/minecraft/Component.cpp b/api/logic/minecraft/Component.cpp index 51957d17..92821065 100644 --- a/api/logic/minecraft/Component.cpp +++ b/api/logic/minecraft/Component.cpp @@ -5,13 +5,13 @@ #include "meta/Version.h" #include "VersionFile.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include #include #include "OneSixVersionFormat.h" #include -Component::Component(ComponentList * parent, const QString& uid) +Component::Component(PackProfile * parent, const QString& uid) { assert(parent); m_parent = parent; @@ -19,7 +19,7 @@ Component::Component(ComponentList * parent, const QString& uid) m_uid = uid; } -Component::Component(ComponentList * parent, std::shared_ptr version) +Component::Component(PackProfile * parent, std::shared_ptr version) { assert(parent); m_parent = parent; @@ -31,7 +31,7 @@ Component::Component(ComponentList * parent, std::shared_ptr vers m_loaded = version->isLoaded(); } -Component::Component(ComponentList * parent, const QString& uid, std::shared_ptr file) +Component::Component(PackProfile * parent, const QString& uid, std::shared_ptr file) { assert(parent); m_parent = parent; diff --git a/api/logic/minecraft/Component.h b/api/logic/minecraft/Component.h index 6a0f86c8..cb202f7f 100644 --- a/api/logic/minecraft/Component.h +++ b/api/logic/minecraft/Component.h @@ -9,7 +9,7 @@ #include "QObjectPtr.h" #include "multimc_logic_export.h" -class ComponentList; +class PackProfile; class LaunchProfile; namespace Meta { @@ -22,11 +22,11 @@ class MULTIMC_LOGIC_EXPORT Component : public QObject, public ProblemProvider { Q_OBJECT public: - Component(ComponentList * parent, const QString &uid); + Component(PackProfile * parent, const QString &uid); // DEPRECATED: remove these constructors? - Component(ComponentList * parent, std::shared_ptr version); - Component(ComponentList * parent, const QString & uid, std::shared_ptr file); + Component(PackProfile * parent, std::shared_ptr version); + Component(PackProfile * parent, const QString & uid, std::shared_ptr file); virtual ~Component(){}; void applyTo(LaunchProfile *profile); @@ -73,7 +73,7 @@ signals: void dataChanged(); public: /* data */ - ComponentList * m_parent; + PackProfile * m_parent; // BEGIN: persistent component list properties /// ID of the component diff --git a/api/logic/minecraft/ComponentList.cpp b/api/logic/minecraft/ComponentList.cpp deleted file mode 100644 index 51fe214d..00000000 --- a/api/logic/minecraft/ComponentList.cpp +++ /dev/null @@ -1,1225 +0,0 @@ -/* Copyright 2013-2019 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 "Exception.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ComponentList.h" -#include "ComponentList_p.h" -#include "ComponentUpdateTask.h" - -ComponentList::ComponentList(MinecraftInstance * instance) - : QAbstractListModel() -{ - d.reset(new ComponentListData); - 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, &ComponentList::disableInteraction); - connect(&d->m_saveTimer, &QTimer::timeout, this, &ComponentList::save_internal); -} - -ComponentList::~ComponentList() -{ - 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(ComponentList * 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 saveComponentList(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 loadComponentList(ComponentList * 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 ComponentList::saveNow() -{ - if(saveIsScheduled()) - { - d->m_saveTimer.stop(); - save_internal(); - } -} - -bool ComponentList::saveIsScheduled() const -{ - return d->dirty; -} - -void ComponentList::buildingFromScratch() -{ - d->loaded = true; - d->dirty = true; -} - -void ComponentList::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 ComponentList::componentsFilePath() const -{ - return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json"); -} - -QString ComponentList::patchesPattern() const -{ - return FS::PathCombine(d->m_instance->instanceRoot(), "patches", "%1.json"); -} - -QString ComponentList::patchFilePathForUid(const QString& uid) const -{ - return patchesPattern().arg(uid); -} - -void ComponentList::save_internal() -{ - qDebug() << "Component list save performed now for" << d->m_instance->name(); - auto filename = componentsFilePath(); - saveComponentList(filename, d->components); - d->dirty = false; -} - -bool ComponentList::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(!loadComponentList(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, &ComponentList::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, &ComponentList::componentDataChanged); - d->components.append(component); - d->componentIndex[component->m_uid] = component; - } - endResetModel(); - d->loaded = true; - return true; - } -} - -void ComponentList::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); - } -} - -shared_qobject_ptr ComponentList::getCurrentTask() -{ - return d->m_updateTask; -} - -void ComponentList::resolve(Net::Mode netmode) -{ - auto updateTask = new ComponentUpdateTask(ComponentUpdateTask::Mode::Resolution, netmode, this); - d->m_updateTask.reset(updateTask); - connect(updateTask, &ComponentUpdateTask::succeeded, this, &ComponentList::updateSucceeded); - connect(updateTask, &ComponentUpdateTask::failed, this, &ComponentList::updateFailed); - d->m_updateTask->start(); -} - - -void ComponentList::updateSucceeded() -{ - qDebug() << "Component list update/resolve task succeeded for" << d->m_instance->name(); - d->m_updateTask.reset(); - invalidateLaunchProfile(); -} - -void ComponentList::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 ComponentList::migratePreComponentConfig() -{ - // upgrade the very old files from the beginnings of MultiMC 5 - 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->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 = ENV.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, ENV.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 saveComponentList(componentsFilePath(), components); -} - -// END: save/load - -void ComponentList::appendComponent(ComponentPtr component) -{ - insertComponent(d->components.size(), component); -} - -void ComponentList::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, &ComponentList::componentDataChanged); - scheduleSave(); -} - -void ComponentList::componentDataChanged() -{ - auto objPtr = qobject_cast(sender()); - if(!objPtr) - { - qWarning() << "ComponentList 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() << "ComponentList got dataChenged signal from a Component which does not belong to it!"; -} - -bool ComponentList::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 ComponentList::remove(const QString id) -{ - int i = 0; - for (auto patch : d->components) - { - if (patch->getID() == id) - { - return remove(i); - } - i++; - } - return false; -} - -bool ComponentList::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 ComponentList::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 * ComponentList::getComponent(const QString &id) -{ - auto iter = d->componentIndex.find(id); - if (iter == d->componentIndex.end()) - { - return nullptr; - } - return (*iter).get(); -} - -Component * ComponentList::getComponent(int index) -{ - if(index < 0 || index >= d->components.size()) - { - return nullptr; - } - return d->components[index].get(); -} - -QVariant ComponentList::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 ComponentList::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 ComponentList::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 ComponentList::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 ComponentList::rowCount(const QModelIndex &parent) const -{ - return d->components.size(); -} - -int ComponentList::columnCount(const QModelIndex &parent) const -{ - return NUM_COLUMNS; -} - -void ComponentList::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 ComponentList::invalidateLaunchProfile() -{ - d->m_profile.reset(); -} - -void ComponentList::installJarMods(QStringList selectedFiles) -{ - installJarMods_internal(selectedFiles); -} - -void ComponentList::installCustomJar(QString selectedFile) -{ - installCustomJar_internal(selectedFile); -} - -bool ComponentList::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 ComponentList::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 ComponentList::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(); - auto jarMod = std::make_shared(); - 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 ComponentList::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(); - 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 ComponentList::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 ComponentList::setOldConfigVersion(const QString& uid, const QString& version) -{ - if(version.isEmpty()) - { - return; - } - d->m_oldConfigVersions[uid] = version; -} - -bool ComponentList::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 ComponentList::getComponentVersion(const QString& uid) const -{ - const auto iter = d->componentIndex.find(uid); - if (iter != d->componentIndex.end()) - { - return (*iter)->getVersion(); - } - return QString(); -} - -void ComponentList::disableInteraction(bool disable) -{ - if(d->interactionDisabled != disable) { - d->interactionDisabled = disable; - auto size = d->components.size(); - if(size) { - emit dataChanged(index(0), index(size - 1)); - } - } -} diff --git a/api/logic/minecraft/ComponentList.h b/api/logic/minecraft/ComponentList.h deleted file mode 100644 index 7b5e1385..00000000 --- a/api/logic/minecraft/ComponentList.h +++ /dev/null @@ -1,150 +0,0 @@ -/* Copyright 2013-2019 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. - */ - -#pragma once - -#include - -#include -#include -#include - -#include "Library.h" -#include "LaunchProfile.h" -#include "Component.h" -#include "ProfileUtils.h" -#include "BaseVersion.h" -#include "MojangDownloadInfo.h" -#include "multimc_logic_export.h" -#include "net/Mode.h" - -class MinecraftInstance; -struct ComponentListData; -class ComponentUpdateTask; - -class MULTIMC_LOGIC_EXPORT ComponentList : public QAbstractListModel -{ - Q_OBJECT - friend ComponentUpdateTask; -public: - enum Columns - { - NameColumn = 0, - VersionColumn, - NUM_COLUMNS - }; - - explicit ComponentList(MinecraftInstance * instance); - virtual ~ComponentList(); - - virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; - virtual int columnCount(const QModelIndex &parent) const override; - virtual Qt::ItemFlags flags(const QModelIndex &index) const override; - - /// call this to explicitly mark the component list as loaded - this is used to build a new component list from scratch. - void buildingFromScratch(); - - /// install more jar mods - void installJarMods(QStringList selectedFiles); - - /// install a jar/zip as a replacement for the main jar - void installCustomJar(QString selectedFile); - - enum MoveDirection { MoveUp, MoveDown }; - /// move component file # up or down the list - void move(const int index, const MoveDirection direction); - - /// remove component file # - including files/records - bool remove(const int index); - - /// remove component file by id - including files/records - bool remove(const QString id); - - bool customize(int index); - - bool revertToBase(int index); - - /// reload the list, reload all components, resolve dependencies - void reload(Net::Mode netmode); - - // reload all components, resolve dependencies - void resolve(Net::Mode netmode); - - /// get current running task... - shared_qobject_ptr getCurrentTask(); - - std::shared_ptr getProfile() const; - - // NOTE: used ONLY by MinecraftInstance to provide legacy version mappings from instance config - void setOldConfigVersion(const QString &uid, const QString &version); - - QString getComponentVersion(const QString &uid) const; - - bool setComponentVersion(const QString &uid, const QString &version, bool important = false); - - bool installEmpty(const QString &uid, const QString &name); - - QString patchFilePathForUid(const QString &uid) const; - - /// if there is a save scheduled, do it now. - void saveNow(); - -signals: - void minecraftChanged(); - -public: - /// get the profile component by id - Component * getComponent(const QString &id); - - /// get the profile component by index - Component * getComponent(int index); - -private: - void scheduleSave(); - bool saveIsScheduled() const; - - /// apply the component patches. Catches all the errors and returns true/false for success/failure - void invalidateLaunchProfile(); - - /// Add the component to the internal list of patches - void appendComponent(ComponentPtr component); - /// insert component so that its index is ideally the specified one (returns real index) - void insertComponent(size_t index, ComponentPtr component); - - QString componentsFilePath() const; - QString patchesPattern() const; - -private slots: - void save_internal(); - void updateSucceeded(); - void updateFailed(const QString & error); - void componentDataChanged(); - void disableInteraction(bool disable); - -private: - bool load(); - bool installJarMods_internal(QStringList filepaths); - bool installCustomJar_internal(QString filepath); - bool removeComponent_internal(ComponentPtr patch); - - bool migratePreComponentConfig(); - -private: /* data */ - - std::unique_ptr d; -}; diff --git a/api/logic/minecraft/ComponentList_p.h b/api/logic/minecraft/ComponentList_p.h deleted file mode 100644 index 7a3d498b..00000000 --- a/api/logic/minecraft/ComponentList_p.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include "Component.h" -#include -#include -#include -#include - -class MinecraftInstance; -using ComponentContainer = QList; -using ComponentIndex = QMap; -using ConnectionList = QList; - -struct ComponentListData -{ - // the instance this belongs to - MinecraftInstance *m_instance; - - // the launch profile (volatile, temporary thing created on demand) - std::shared_ptr m_profile; - - // version information migrated from instance.cfg file. Single use on migration! - std::map m_oldConfigVersions; - QString getOldConfigVersion(const QString& uid) const - { - const auto iter = m_oldConfigVersions.find(uid); - if(iter != m_oldConfigVersions.cend()) - { - return (*iter).second; - } - return QString(); - } - - // persistent list of components and related machinery - ComponentContainer components; - ComponentIndex componentIndex; - bool dirty = false; - QTimer m_saveTimer; - shared_qobject_ptr m_updateTask; - bool loaded = false; - bool interactionDisabled = true; -}; - diff --git a/api/logic/minecraft/ComponentUpdateTask.cpp b/api/logic/minecraft/ComponentUpdateTask.cpp index 84c0474c..241d9a49 100644 --- a/api/logic/minecraft/ComponentUpdateTask.cpp +++ b/api/logic/minecraft/ComponentUpdateTask.cpp @@ -1,7 +1,7 @@ #include "ComponentUpdateTask.h" -#include "ComponentList_p.h" -#include "ComponentList.h" +#include "PackProfile_p.h" +#include "PackProfile.h" #include "Component.h" #include #include @@ -22,16 +22,16 @@ * Really, it should be a reactor/state machine that receives input from the application * and dynamically adapts to changing requirements... * - * The reactor should be the only entry into manipulating the ComponentList. + * The reactor should be the only entry into manipulating the PackProfile. * See: https://en.wikipedia.org/wiki/Reactor_pattern */ /* - * Or make this operate on a snapshot of the ComponentList state, then merge results in as long as the snapshot and ComponentList didn't change? + * Or make this operate on a snapshot of the PackProfile state, then merge results in as long as the snapshot and PackProfile didn't change? * If the component list changes, start over. */ -ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, ComponentList* list, QObject* parent) +ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list, QObject* parent) : Task(parent) { d.reset(new ComponentUpdateTaskData); @@ -126,7 +126,7 @@ static LoadResult loadComponent(ComponentPtr component, shared_qobject_ptr // FIXME: dead code. determine if this can still be useful? /* -static LoadResult loadComponentList(ComponentPtr component, shared_qobject_ptr& loadTask, Net::Mode netmode) +static LoadResult loadPackProfile(ComponentPtr component, shared_qobject_ptr& loadTask, Net::Mode netmode) { if(component->m_loaded) { @@ -217,7 +217,7 @@ void ComponentUpdateTask::loadComponents() } case Mode::Resolution: { - singleResult = loadComponentList(component, loadTask, d->netmode); + singleResult = loadPackProfile(component, loadTask, d->netmode); loadType = RemoteLoadStatus::Type::List; break; } @@ -244,7 +244,7 @@ void ComponentUpdateTask::loadComponents() }); RemoteLoadStatus status; status.type = loadType; - status.componentListIndex = componentIndex; + status.PackProfileIndex = componentIndex; d->remoteLoadStatusList.append(status); taskIndex++; } @@ -495,7 +495,7 @@ static bool getTrivialComponentChanges(const ComponentIndex & index, const Requi } // FIXME, TODO: decouple dependency resolution from loading -// FIXME: This works directly with the ComponentList internals. It shouldn't! It needs richer data types than ComponentList uses. +// FIXME: This works directly with the PackProfile internals. It shouldn't! It needs richer data types than PackProfile uses. // FIXME: throw all this away and use a graph void ComponentUpdateTask::resolveDependencies(bool checkOnly) { @@ -648,7 +648,7 @@ void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) // update the cached data of the component from the downloaded version file. if (taskSlot.type == RemoteLoadStatus::Type::Version) { - auto component = d->m_list->getComponent(taskSlot.componentListIndex); + auto component = d->m_list->getComponent(taskSlot.PackProfileIndex); component->m_loaded = true; component->updateCachedData(); } diff --git a/api/logic/minecraft/ComponentUpdateTask.h b/api/logic/minecraft/ComponentUpdateTask.h index 4cb29a89..4274cabb 100644 --- a/api/logic/minecraft/ComponentUpdateTask.h +++ b/api/logic/minecraft/ComponentUpdateTask.h @@ -4,7 +4,7 @@ #include "net/Mode.h" #include -class ComponentList; +class PackProfile; struct ComponentUpdateTaskData; class ComponentUpdateTask : public Task @@ -18,7 +18,7 @@ public: }; public: - explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, ComponentList * list, QObject *parent = 0); + explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile * list, QObject *parent = 0); virtual ~ComponentUpdateTask(); protected: diff --git a/api/logic/minecraft/ComponentUpdateTask_p.h b/api/logic/minecraft/ComponentUpdateTask_p.h index 5989cf07..5b02431b 100644 --- a/api/logic/minecraft/ComponentUpdateTask_p.h +++ b/api/logic/minecraft/ComponentUpdateTask_p.h @@ -5,7 +5,7 @@ #include #include "net/Mode.h" -class ComponentList; +class PackProfile; struct RemoteLoadStatus { @@ -15,7 +15,7 @@ struct RemoteLoadStatus List, Version } type = Type::Version; - size_t componentListIndex = 0; + size_t PackProfileIndex = 0; bool finished = false; bool succeeded = false; QString error; @@ -23,7 +23,7 @@ struct RemoteLoadStatus struct ComponentUpdateTaskData { - ComponentList * m_list = nullptr; + PackProfile * m_list = nullptr; QList remoteLoadStatusList; bool remoteLoadSuccessful = true; size_t remoteTasksInProgress = 0; diff --git a/api/logic/minecraft/MinecraftInstance.cpp b/api/logic/minecraft/MinecraftInstance.cpp index 28073edf..db259395 100644 --- a/api/logic/minecraft/MinecraftInstance.cpp +++ b/api/logic/minecraft/MinecraftInstance.cpp @@ -33,7 +33,7 @@ #include "icons/IIconList.h" #include -#include "ComponentList.h" +#include "PackProfile.h" #include "AssetsUtils.h" #include "MinecraftUpdate.h" #include "MinecraftLoadAndCheck.h" @@ -106,7 +106,7 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO m_settings->registerSetting("ForgeVersion", ""); m_settings->registerSetting("LiteloaderVersion", ""); - m_components.reset(new ComponentList(this)); + m_components.reset(new PackProfile(this)); m_components->setOldConfigVersion("net.minecraft", m_settings->get("IntendedVersion").toString()); auto setting = m_settings->getSetting("LWJGLVersion"); m_components->setOldConfigVersion("org.lwjgl", m_settings->get("LWJGLVersion").toString()); @@ -124,14 +124,14 @@ QString MinecraftInstance::typeName() const return "Minecraft"; } -std::shared_ptr MinecraftInstance::getComponentList() const +std::shared_ptr MinecraftInstance::getPackProfile() const { return m_components; } QSet MinecraftInstance::traits() const { - auto components = getComponentList(); + auto components = getPackProfile(); if (!components) { return {"version-incomplete"}; @@ -265,7 +265,7 @@ QStringList MinecraftInstance::getNativeJars() const QStringList MinecraftInstance::extraArguments() const { auto list = BaseInstance::extraArguments(); - auto version = getComponentList(); + auto version = getPackProfile(); if (!version) return list; auto jarMods = getJarMods(); diff --git a/api/logic/minecraft/MinecraftInstance.h b/api/logic/minecraft/MinecraftInstance.h index a8f64109..985a8a76 100644 --- a/api/logic/minecraft/MinecraftInstance.h +++ b/api/logic/minecraft/MinecraftInstance.h @@ -10,7 +10,7 @@ class ModFolderModel; class WorldList; class GameOptions; class LaunchStep; -class ComponentList; +class PackProfile; class MULTIMC_LOGIC_EXPORT MinecraftInstance: public BaseInstance { @@ -64,7 +64,7 @@ public: ////// Profile management ////// - std::shared_ptr getComponentList() const; + std::shared_ptr getPackProfile() const; ////// Mod Lists ////// std::shared_ptr loaderModList() const; @@ -120,7 +120,7 @@ private: QString prettifyTimeDuration(int64_t duration); protected: // data - std::shared_ptr m_components; + std::shared_ptr m_components; mutable std::shared_ptr m_loader_mod_list; mutable std::shared_ptr m_core_mod_list; mutable std::shared_ptr m_resource_pack_list; diff --git a/api/logic/minecraft/MinecraftLoadAndCheck.cpp b/api/logic/minecraft/MinecraftLoadAndCheck.cpp index 593e2fc4..79b0c484 100644 --- a/api/logic/minecraft/MinecraftLoadAndCheck.cpp +++ b/api/logic/minecraft/MinecraftLoadAndCheck.cpp @@ -1,6 +1,6 @@ #include "MinecraftLoadAndCheck.h" #include "MinecraftInstance.h" -#include "ComponentList.h" +#include "PackProfile.h" MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance *inst, QObject *parent) : Task(parent), m_inst(inst) { @@ -9,7 +9,7 @@ MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance *inst, QObject *p void MinecraftLoadAndCheck::executeTask() { // add offline metadata load task - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); components->reload(Net::Mode::Offline); m_task = components->getCurrentTask(); diff --git a/api/logic/minecraft/MinecraftUpdate.cpp b/api/logic/minecraft/MinecraftUpdate.cpp index f9ff114d..ca3e7617 100644 --- a/api/logic/minecraft/MinecraftUpdate.cpp +++ b/api/logic/minecraft/MinecraftUpdate.cpp @@ -23,7 +23,7 @@ #include #include "BaseInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include "minecraft/Library.h" #include "net/URLConstants.h" #include @@ -50,7 +50,7 @@ void MinecraftUpdate::executeTask() // add metadata update task if necessary { - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); components->reload(Net::Mode::Online); auto task = components->getCurrentTask(); if(task) diff --git a/api/logic/minecraft/OneSixVersionFormat.h b/api/logic/minecraft/OneSixVersionFormat.h index 6bbebca0..14ae385c 100644 --- a/api/logic/minecraft/OneSixVersionFormat.h +++ b/api/logic/minecraft/OneSixVersionFormat.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include #include diff --git a/api/logic/minecraft/PackProfile.cpp b/api/logic/minecraft/PackProfile.cpp new file mode 100644 index 00000000..15f2f55d --- /dev/null +++ b/api/logic/minecraft/PackProfile.cpp @@ -0,0 +1,1225 @@ +/* Copyright 2013-2019 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 "Exception.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "PackProfile.h" +#include "PackProfile_p.h" +#include "ComponentUpdateTask.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); + } +} + +shared_qobject_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 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->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 = ENV.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, ENV.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.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(); + 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(); + auto jarMod = std::make_shared(); + 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(); + 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)); + } + } +} diff --git a/api/logic/minecraft/PackProfile.h b/api/logic/minecraft/PackProfile.h new file mode 100644 index 00000000..66cd5e3e --- /dev/null +++ b/api/logic/minecraft/PackProfile.h @@ -0,0 +1,150 @@ +/* Copyright 2013-2019 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. + */ + +#pragma once + +#include + +#include +#include +#include + +#include "Library.h" +#include "LaunchProfile.h" +#include "Component.h" +#include "ProfileUtils.h" +#include "BaseVersion.h" +#include "MojangDownloadInfo.h" +#include "multimc_logic_export.h" +#include "net/Mode.h" + +class MinecraftInstance; +struct PackProfileData; +class ComponentUpdateTask; + +class MULTIMC_LOGIC_EXPORT PackProfile : public QAbstractListModel +{ + Q_OBJECT + friend ComponentUpdateTask; +public: + enum Columns + { + NameColumn = 0, + VersionColumn, + NUM_COLUMNS + }; + + explicit PackProfile(MinecraftInstance * instance); + virtual ~PackProfile(); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + virtual int columnCount(const QModelIndex &parent) const override; + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + + /// call this to explicitly mark the component list as loaded - this is used to build a new component list from scratch. + void buildingFromScratch(); + + /// install more jar mods + void installJarMods(QStringList selectedFiles); + + /// install a jar/zip as a replacement for the main jar + void installCustomJar(QString selectedFile); + + enum MoveDirection { MoveUp, MoveDown }; + /// move component file # up or down the list + void move(const int index, const MoveDirection direction); + + /// remove component file # - including files/records + bool remove(const int index); + + /// remove component file by id - including files/records + bool remove(const QString id); + + bool customize(int index); + + bool revertToBase(int index); + + /// reload the list, reload all components, resolve dependencies + void reload(Net::Mode netmode); + + // reload all components, resolve dependencies + void resolve(Net::Mode netmode); + + /// get current running task... + shared_qobject_ptr getCurrentTask(); + + std::shared_ptr getProfile() const; + + // NOTE: used ONLY by MinecraftInstance to provide legacy version mappings from instance config + void setOldConfigVersion(const QString &uid, const QString &version); + + QString getComponentVersion(const QString &uid) const; + + bool setComponentVersion(const QString &uid, const QString &version, bool important = false); + + bool installEmpty(const QString &uid, const QString &name); + + QString patchFilePathForUid(const QString &uid) const; + + /// if there is a save scheduled, do it now. + void saveNow(); + +signals: + void minecraftChanged(); + +public: + /// get the profile component by id + Component * getComponent(const QString &id); + + /// get the profile component by index + Component * getComponent(int index); + +private: + void scheduleSave(); + bool saveIsScheduled() const; + + /// apply the component patches. Catches all the errors and returns true/false for success/failure + void invalidateLaunchProfile(); + + /// Add the component to the internal list of patches + void appendComponent(ComponentPtr component); + /// insert component so that its index is ideally the specified one (returns real index) + void insertComponent(size_t index, ComponentPtr component); + + QString componentsFilePath() const; + QString patchesPattern() const; + +private slots: + void save_internal(); + void updateSucceeded(); + void updateFailed(const QString & error); + void componentDataChanged(); + void disableInteraction(bool disable); + +private: + bool load(); + bool installJarMods_internal(QStringList filepaths); + bool installCustomJar_internal(QString filepath); + bool removeComponent_internal(ComponentPtr patch); + + bool migratePreComponentConfig(); + +private: /* data */ + + std::unique_ptr d; +}; diff --git a/api/logic/minecraft/PackProfile_p.h b/api/logic/minecraft/PackProfile_p.h new file mode 100644 index 00000000..2f7cc3d7 --- /dev/null +++ b/api/logic/minecraft/PackProfile_p.h @@ -0,0 +1,43 @@ +#pragma once + +#include "Component.h" +#include +#include +#include +#include + +class MinecraftInstance; +using ComponentContainer = QList; +using ComponentIndex = QMap; +using ConnectionList = QList; + +struct PackProfileData +{ + // the instance this belongs to + MinecraftInstance *m_instance; + + // the launch profile (volatile, temporary thing created on demand) + std::shared_ptr m_profile; + + // version information migrated from instance.cfg file. Single use on migration! + std::map m_oldConfigVersions; + QString getOldConfigVersion(const QString& uid) const + { + const auto iter = m_oldConfigVersions.find(uid); + if(iter != m_oldConfigVersions.cend()) + { + return (*iter).second; + } + return QString(); + } + + // persistent list of components and related machinery + ComponentContainer components; + ComponentIndex componentIndex; + bool dirty = false; + QTimer m_saveTimer; + shared_qobject_ptr m_updateTask; + bool loaded = false; + bool interactionDisabled = true; +}; + diff --git a/api/logic/minecraft/VersionFile.cpp b/api/logic/minecraft/VersionFile.cpp index 5bad57e9..d0a1a507 100644 --- a/api/logic/minecraft/VersionFile.cpp +++ b/api/logic/minecraft/VersionFile.cpp @@ -5,7 +5,7 @@ #include "minecraft/VersionFile.h" #include "minecraft/Library.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include "ParseUtils.h" #include diff --git a/api/logic/minecraft/VersionFile.h b/api/logic/minecraft/VersionFile.h index 29ddd0bc..b79fcd4f 100644 --- a/api/logic/minecraft/VersionFile.h +++ b/api/logic/minecraft/VersionFile.h @@ -12,7 +12,7 @@ #include "Library.h" #include -class ComponentList; +class PackProfile; class VersionFile; class LaunchProfile; struct MojangDownloadInfo; diff --git a/api/logic/minecraft/launch/ModMinecraftJar.cpp b/api/logic/minecraft/launch/ModMinecraftJar.cpp index ceaad175..ba43a68d 100644 --- a/api/logic/minecraft/launch/ModMinecraftJar.cpp +++ b/api/logic/minecraft/launch/ModMinecraftJar.cpp @@ -19,7 +19,7 @@ #include "minecraft/OpSys.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" void ModMinecraftJar::executeTask() { @@ -43,7 +43,7 @@ void ModMinecraftJar::executeTask() } // create temporary modded jar, if needed - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto jarMods = m_inst->getJarMods(); if(jarMods.size()) diff --git a/api/logic/minecraft/launch/ReconstructAssets.cpp b/api/logic/minecraft/launch/ReconstructAssets.cpp index af9af127..3ac320bd 100644 --- a/api/logic/minecraft/launch/ReconstructAssets.cpp +++ b/api/logic/minecraft/launch/ReconstructAssets.cpp @@ -15,7 +15,7 @@ #include "ReconstructAssets.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include "minecraft/AssetsUtils.h" #include "launch/LaunchTask.h" @@ -23,7 +23,7 @@ void ReconstructAssets::executeTask() { auto instance = m_parent->instance(); std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); - auto components = minecraftInstance->getComponentList(); + auto components = minecraftInstance->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); diff --git a/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp b/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp index 9d86a7b5..a4ea60cd 100644 --- a/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp +++ b/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp @@ -6,7 +6,7 @@ #include #include "LegacyInstance.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include "LegacyModList.h" #include "classparser.h" @@ -84,7 +84,7 @@ void LegacyUpgradeTask::copyFinished() } } } - auto components = inst.getComponentList(); + auto components = inst.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", preferredVersionNumber, true); diff --git a/api/logic/minecraft/update/AssetUpdateTask.cpp b/api/logic/minecraft/update/AssetUpdateTask.cpp index 051b1279..e26ab4ef 100644 --- a/api/logic/minecraft/update/AssetUpdateTask.cpp +++ b/api/logic/minecraft/update/AssetUpdateTask.cpp @@ -1,7 +1,7 @@ #include "Env.h" #include "AssetUpdateTask.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include "net/ChecksumValidator.h" #include "minecraft/AssetsUtils.h" @@ -17,7 +17,7 @@ AssetUpdateTask::~AssetUpdateTask() void AssetUpdateTask::executeTask() { setStatus(tr("Updating assets index...")); - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); QUrl indexUrl = assets->url; @@ -54,7 +54,7 @@ void AssetUpdateTask::assetIndexFinished() AssetsIndex index; qDebug() << m_inst->name() << ": Finished asset index download"; - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); diff --git a/api/logic/minecraft/update/FMLLibrariesTask.cpp b/api/logic/minecraft/update/FMLLibrariesTask.cpp index 52a8375b..850130a1 100644 --- a/api/logic/minecraft/update/FMLLibrariesTask.cpp +++ b/api/logic/minecraft/update/FMLLibrariesTask.cpp @@ -3,7 +3,7 @@ #include #include "FMLLibrariesTask.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" FMLLibrariesTask::FMLLibrariesTask(MinecraftInstance * inst) { @@ -13,7 +13,7 @@ void FMLLibrariesTask::executeTask() { // Get the mod list MinecraftInstance *inst = (MinecraftInstance *)m_inst; - auto components = inst->getComponentList(); + auto components = inst->getPackProfile(); auto profile = components->getProfile(); if (!profile->hasTrait("legacyFML")) diff --git a/api/logic/minecraft/update/LibrariesTask.cpp b/api/logic/minecraft/update/LibrariesTask.cpp index a000f77f..7f66a651 100644 --- a/api/logic/minecraft/update/LibrariesTask.cpp +++ b/api/logic/minecraft/update/LibrariesTask.cpp @@ -1,7 +1,7 @@ #include "Env.h" #include "LibrariesTask.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" LibrariesTask::LibrariesTask(MinecraftInstance * inst) { @@ -15,7 +15,7 @@ void LibrariesTask::executeTask() MinecraftInstance *inst = (MinecraftInstance *)m_inst; // Build a list of URLs that will need to be downloaded. - auto components = inst->getComponentList(); + auto components = inst->getPackProfile(); auto profile = components->getProfile(); auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name())); -- cgit