/* Copyright 2013-2017 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 "minecraft/MinecraftProfile.h" #include "Exception.h" #include #include #include #include #include #include #include MinecraftProfile::MinecraftProfile(MinecraftInstance * instance) : QAbstractListModel() { m_instance = instance; clear(); } MinecraftProfile::~MinecraftProfile() { } void MinecraftProfile::reload() { beginResetModel(); load_internal(); reapplyPatches(); endResetModel(); } void MinecraftProfile::clear() { m_minecraftVersion.clear(); m_minecraftVersionType.clear(); m_minecraftAssets.reset(); m_minecraftArguments.clear(); m_tweakers.clear(); m_mainClass.clear(); m_appletClass.clear(); m_libraries.clear(); m_traits.clear(); m_jarMods.clear(); m_mainJar.reset(); m_problemSeverity = ProblemSeverity::None; } void MinecraftProfile::clearPatches() { beginResetModel(); m_patches.clear(); endResetModel(); } void MinecraftProfile::appendPatch(ProfilePatchPtr patch) { int index = m_patches.size(); beginInsertRows(QModelIndex(), index, index); m_patches.append(patch); endInsertRows(); } bool MinecraftProfile::remove(const int index) { auto patch = versionPatch(index); if (!patch->isRemovable()) { qDebug() << "Patch" << patch->getID() << "is non-removable"; return false; } if(!removePatch_internal(patch)) { qCritical() << "Patch" << patch->getID() << "could not be removed"; return false; } beginRemoveRows(QModelIndex(), index, index); m_patches.removeAt(index); endRemoveRows(); reapplyPatches(); saveCurrentOrder(); return true; } bool MinecraftProfile::remove(const QString id) { int i = 0; for (auto patch : m_patches) { if (patch->getID() == id) { return remove(i); } i++; } return false; } bool MinecraftProfile::customize(int index) { auto patch = versionPatch(index); if (!patch->isCustomizable()) { qDebug() << "Patch" << patch->getID() << "is not customizable"; return false; } if(!customizePatch_internal(patch)) { qCritical() << "Patch" << patch->getID() << "could not be customized"; return false; } reapplyPatches(); saveCurrentOrder(); // FIXME: maybe later in unstable // emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); return true; } bool MinecraftProfile::revertToBase(int index) { auto patch = versionPatch(index); if (!patch->isRevertible()) { qDebug() << "Patch" << patch->getID() << "is not revertible"; return false; } if(!revertPatch_internal(patch)) { qCritical() << "Patch" << patch->getID() << "could not be reverted"; return false; } reapplyPatches(); saveCurrentOrder(); // FIXME: maybe later in unstable // emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); return true; } ProfilePatchPtr MinecraftProfile::versionPatch(const QString &id) { for (auto patch : m_patches) { if (patch->getID() == id) { return patch; } } return nullptr; } ProfilePatchPtr MinecraftProfile::versionPatch(int index) { if(index < 0 || index >= m_patches.size()) return nullptr; return m_patches[index]; } bool MinecraftProfile::isVanilla() { for(auto patchptr: m_patches) { if(patchptr->isCustom()) return false; } return true; } bool MinecraftProfile::revertToVanilla() { // remove patches, if present auto VersionPatchesCopy = m_patches; for(auto & it: VersionPatchesCopy) { if (!it->isCustom()) { continue; } if(it->isRevertible() || it->isRemovable()) { if(!remove(it->getID())) { qWarning() << "Couldn't remove" << it->getID() << "from profile!"; reapplyPatches(); saveCurrentOrder(); return false; } } } reapplyPatches(); saveCurrentOrder(); return true; } QVariant MinecraftProfile::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); int row = index.row(); int column = index.column(); if (row < 0 || row >= m_patches.size()) return QVariant(); auto patch = m_patches.at(row); if (role == Qt::DisplayRole) { switch (column) { case 0: return m_patches.at(row)->getName(); case 1: { if(patch->isCustom()) { return QString("%1 (Custom)").arg(patch->getVersion()); } else { return patch->getVersion(); } } default: return QVariant(); } } if(role == Qt::DecorationRole) { switch(column) { case 0: { auto severity = patch->getProblemSeverity(); switch (severity) { case ProblemSeverity::Warning: return "warning"; case ProblemSeverity::Error: return "error"; default: return QVariant(); } } default: { return QVariant(); } } } return QVariant(); } QVariant MinecraftProfile::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (role == Qt::DisplayRole) { switch (section) { case 0: return tr("Name"); case 1: return tr("Version"); default: return QVariant(); } } } return QVariant(); } Qt::ItemFlags MinecraftProfile::flags(const QModelIndex &index) const { if (!index.isValid()) return Qt::NoItemFlags; return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } int MinecraftProfile::rowCount(const QModelIndex &parent) const { return m_patches.size(); } int MinecraftProfile::columnCount(const QModelIndex &parent) const { return 2; } void MinecraftProfile::saveCurrentOrder() const { ProfileUtils::PatchOrder order; for(auto item: m_patches) { if(!item->isMoveable()) continue; order.append(item->getID()); } saveOrder_internal(order); } void MinecraftProfile::move(const int index, const MoveDirection direction) { int theirIndex; if (direction == MoveUp) { theirIndex = index - 1; } else { theirIndex = index + 1; } if (index < 0 || index >= m_patches.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 = versionPatch(index); auto to = versionPatch(theirIndex); if (!from || !to || !to->isMoveable() || !from->isMoveable()) { return; } beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); m_patches.swap(index, theirIndex); endMoveRows(); reapplyPatches(); saveCurrentOrder(); } void MinecraftProfile::resetOrder() { resetOrder_internal(); reload(); } bool MinecraftProfile::reapplyPatches() { try { clear(); for(auto file: m_patches) { qDebug() << "Applying" << file->getID() << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD"); file->applyTo(this); } } catch (Exception & error) { clear(); qWarning() << "Couldn't apply profile patches because: " << error.cause(); return false; } return true; } static void applyString(const QString & from, QString & to) { if(from.isEmpty()) return; to = from; } void MinecraftProfile::applyMinecraftVersion(const QString& id) { applyString(id, this->m_minecraftVersion); } void MinecraftProfile::applyAppletClass(const QString& appletClass) { applyString(appletClass, this->m_appletClass); } void MinecraftProfile::applyMainClass(const QString& mainClass) { applyString(mainClass, this->m_mainClass); } void MinecraftProfile::applyMinecraftArguments(const QString& minecraftArguments) { applyString(minecraftArguments, this->m_minecraftArguments); } void MinecraftProfile::applyMinecraftVersionType(const QString& type) { applyString(type, this->m_minecraftVersionType); } void MinecraftProfile::applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets) { if(assets) { m_minecraftAssets = assets; } } void MinecraftProfile::applyTraits(const QSet& traits) { this->m_traits.unite(traits); } void MinecraftProfile::applyTweakers(const QStringList& tweakers) { // FIXME: check for dupes? // FIXME: does order matter? for (auto tweaker : tweakers) { this->m_tweakers += tweaker; } } void MinecraftProfile::applyJarMods(const QList& jarMods) { this->m_jarMods.append(jarMods); } static int findLibraryByName(QList *haystack, const GradleSpecifier &needle) { int retval = -1; for (int i = 0; i < haystack->size(); ++i) { if (haystack->at(i)->rawName().matchName(needle)) { // only one is allowed. if (retval != -1) return -1; retval = i; } } return retval; } void MinecraftProfile::applyMods(const QList& mods) { QList * list = &m_mods; for(auto & mod: mods) { auto modCopy = Library::limitedCopy(mod); // find the mod by name. const int index = findLibraryByName(list, mod->rawName()); // mod not found? just add it. if (index < 0) { list->append(modCopy); return; } auto existingLibrary = list->at(index); // if we are higher it means we should update if (Version(mod->version()) > Version(existingLibrary->version())) { list->replace(index, modCopy); } } } void MinecraftProfile::applyLibrary(LibraryPtr library) { if(!library->isActive()) { return; } QList * list = &m_libraries; if(library->isNative()) { list = &m_nativeLibraries; } auto libraryCopy = Library::limitedCopy(library); // find the library by name. const int index = findLibraryByName(list, library->rawName()); // library not found? just add it. if (index < 0) { list->append(libraryCopy); return; } auto existingLibrary = list->at(index); // if we are higher it means we should update if (Version(library->version()) > Version(existingLibrary->version())) { list->replace(index, libraryCopy); } } const LibraryPtr MinecraftProfile::getMainJar() const { return m_mainJar; } void MinecraftProfile::applyMainJar(LibraryPtr jar) { if(jar) { m_mainJar = jar; } } void MinecraftProfile::applyProblemSeverity(ProblemSeverity severity) { if (m_problemSeverity < severity) { m_problemSeverity = severity; } } QString MinecraftProfile::getMinecraftVersion() const { return m_minecraftVersion; } QString MinecraftProfile::getAppletClass() const { return m_appletClass; } QString MinecraftProfile::getMainClass() const { return m_mainClass; } const QSet &MinecraftProfile::getTraits() const { return m_traits; } const QStringList & MinecraftProfile::getTweakers() const { return m_tweakers; } bool MinecraftProfile::hasTrait(const QString& trait) const { return m_traits.contains(trait); } ProblemSeverity MinecraftProfile::getProblemSeverity() const { return m_problemSeverity; } QString MinecraftProfile::getMinecraftVersionType() const { return m_minecraftVersionType; } std::shared_ptr MinecraftProfile::getMinecraftAssets() const { if(!m_minecraftAssets) { return std::make_shared("legacy"); } return m_minecraftAssets; } QString MinecraftProfile::getMinecraftArguments() const { return m_minecraftArguments; } const QList & MinecraftProfile::getJarMods() const { return m_jarMods; } const QList & MinecraftProfile::getLibraries() const { return m_libraries; } const QList & MinecraftProfile::getNativeLibraries() const { return m_nativeLibraries; } void MinecraftProfile::getLibraryFiles(const QString& architecture, QStringList& jars, QStringList& nativeJars, const QString& overridePath, const QString& tempPath) const { QStringList native32, native64; jars.clear(); nativeJars.clear(); for (auto lib : getLibraries()) { lib->getApplicableFiles(currentSystem, jars, nativeJars, native32, native64, overridePath); } // NOTE: order is important here, add main jar last to the lists if(m_mainJar) { // FIXME: HACK!! jar modding is weird and unsystematic! if(m_jarMods.size()) { QDir tempDir(tempPath); jars.append(tempDir.absoluteFilePath("minecraft.jar")); } else { m_mainJar->getApplicableFiles(currentSystem, jars, nativeJars, native32, native64, overridePath); } } for (auto lib : getNativeLibraries()) { lib->getApplicableFiles(currentSystem, jars, nativeJars, native32, native64, overridePath); } if(architecture == "32") { nativeJars.append(native32); } else if(architecture == "64") { nativeJars.append(native64); } } void MinecraftProfile::installJarMods(QStringList selectedFiles) { installJarMods_internal(selectedFiles); } void MinecraftProfile::installCustomJar(QString selectedFile) { installCustomJar_internal(selectedFile); } /* * TODO: get rid of this. Get rid of all order numbers. */ int MinecraftProfile::getFreeOrderNumber() { int largest = 100; // yes, I do realize this is dumb. The order thing itself is dumb. and to be removed next. for(auto thing: m_patches) { int order = thing->getOrder(); if(order > largest) largest = order; } return largest + 1; } void MinecraftProfile::loadDefaultBuiltinPatches_internal() { auto addBuiltinPatch = [&](const QString &uid, const QString intendedVersion, int order) { auto jsonFilePath = FS::PathCombine(m_instance->instanceRoot(), "patches" , uid + ".json"); // load up the base minecraft patch ProfilePatchPtr profilePatch; if(QFile::exists(jsonFilePath)) { auto file = ProfileUtils::parseJsonFile(QFileInfo(jsonFilePath), false); if(file->version.isEmpty()) { file->version = intendedVersion; } profilePatch = std::make_shared(file, jsonFilePath); profilePatch->setVanilla(false); profilePatch->setRevertible(true); } else { auto metaVersion = ENV.metadataIndex()->get(uid, intendedVersion); profilePatch = std::make_shared(metaVersion); profilePatch->setVanilla(true); } profilePatch->setOrder(order); appendPatch(profilePatch); }; addBuiltinPatch("net.minecraft", m_instance->getComponentVersion("net.minecraft"), -2); addBuiltinPatch("org.lwjgl", m_instance->getComponentVersion("org.lwjgl"), -1); } void MinecraftProfile::loadUserPatches_internal() { // first, collect all patches (that are not builtins of OneSix) and load them QMap loadedPatches; QDir patchesDir(FS::PathCombine(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); // ignore builtins if (file->uid == "net.minecraft") continue; if (file->uid == "org.lwjgl") continue; auto patch = std::make_shared(file, info.filePath()); patch->setRemovable(true); patch->setMovable(true); if(ENV.metadataIndex()->hasUid(file->uid)) { // FIXME: requesting a uid/list creates it in the index... this allows reverting to possibly invalid versions... patch->setRevertible(true); } loadedPatches[file->uid] = patch; } // these are 'special'... if not already loaded from instance files, grab them from the metadata repo. auto loadSpecial = [&](const QString & uid, int order) { auto patchVersion = m_instance->getComponentVersion(uid); if(!patchVersion.isEmpty() && !loadedPatches.contains(uid)) { auto patch = std::make_shared(ENV.metadataIndex()->get(uid, patchVersion)); patch->setOrder(order); patch->setVanilla(true); patch->setRemovable(true); patch->setMovable(true); loadedPatches[uid] = patch; } }; loadSpecial("net.minecraftforge", 5); loadSpecial("com.mumfrey.liteloader", 10); // now add all the patches by user sort order ProfileUtils::PatchOrder userOrder; ProfileUtils::readOverrideOrders(FS::PathCombine(m_instance->instanceRoot(), "order.json"), userOrder); for (auto uid : userOrder) { // ignore builtins if (uid == "net.minecraft") continue; if (uid == "org.lwjgl") continue; // ordering has a patch that is gone? if(!loadedPatches.contains(uid)) { continue; } appendPatch(loadedPatches.take(uid)); } // is there anything left to sort? if(loadedPatches.isEmpty()) { // TODO: save the order here? return; } // inserting into multimap by order number as key sorts the patches and detects duplicates QMultiMap files; auto iter = loadedPatches.begin(); while(iter != loadedPatches.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 appendPatch(value); } } // TODO: save the order here? } void MinecraftProfile::load_internal() { clearPatches(); loadDefaultBuiltinPatches_internal(); loadUserPatches_internal(); } bool MinecraftProfile::saveOrder_internal(ProfileUtils::PatchOrder order) const { return ProfileUtils::writeOverrideOrders(FS::PathCombine(m_instance->instanceRoot(), "order.json"), order); } bool MinecraftProfile::resetOrder_internal() { return QDir(m_instance->instanceRoot()).remove("order.json"); } bool MinecraftProfile::removePatch_internal(ProfilePatchPtr 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; } } if(!m_instance->getComponentVersion(patch->getID()).isEmpty()) { m_instance->setComponentVersion(patch->getID(), QString()); } // 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, 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 &jarMods = patch->getVersionFile()->jarMods; for(auto &jarmod: jarMods) { ok &= preRemoveJarMod(jarmod); } return ok; } bool MinecraftProfile::customizePatch_internal(ProfilePatchPtr patch) { if(patch->isCustom()) { return false; } auto filename = FS::PathCombine(m_instance->instanceRoot(), "patches" , patch->getID() + ".json"); if(!FS::ensureFilePathExists(filename)) { return false; } // FIXME: get rid of this try-catch. try { QSaveFile jsonFile(filename); if(!jsonFile.open(QIODevice::WriteOnly)) { return false; } auto vfile = patch->getVersionFile(); if(!vfile) { return false; } auto document = OneSixVersionFormat::versionFileToJson(vfile, true); jsonFile.write(document.toJson()); if(!jsonFile.commit()) { return false; } load_internal(); } catch (Exception &error) { qWarning() << "Version could not be loaded:" << error.cause(); } return true; } bool MinecraftProfile::revertPatch_internal(ProfilePatchPtr patch) { if(!patch->isCustom()) { // already not custom return true; } auto filename = patch->getFilename(); if(!QFile::exists(filename)) { // already gone / not custom return true; } // just kill the file and reload bool result = QFile::remove(filename); // FIXME: get rid of this try-catch. try { load_internal(); } catch (Exception &error) { qWarning() << "Version could not be loaded:" << error.cause(); } return result; } bool MinecraftProfile::installJarMods_internal(QStringList filepaths) { QString patchDir = FS::PathCombine(m_instance->instanceRoot(), "patches"); if(!FS::ensureFolderPathExists(patchDir)) { return false; } if (!FS::ensureFolderPathExists(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(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; f->order = getFreeOrderNumber(); 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, true).toJson()); file.close(); auto patch = std::make_shared(f, patchFileName); patch->setMovable(true); patch->setRemovable(true); appendPatch(patch); } saveCurrentOrder(); reapplyPatches(); return true; } bool MinecraftProfile::installCustomJar_internal(QString filepath) { QString patchDir = FS::PathCombine(m_instance->instanceRoot(), "patches"); if(!FS::ensureFolderPathExists(patchDir)) { return false; } QString libDir = 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; f->order = getFreeOrderNumber(); 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, true).toJson()); file.close(); auto patch = std::make_shared(f, patchFileName); patch->setMovable(true); patch->setRemovable(true); appendPatch(patch); saveCurrentOrder(); reapplyPatches(); return true; }