/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include #include "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)); } } }