/* Copyright (C) 2012 Sebastian Herbord. All rights reserved. This file is part of Mod Organizer. Mod Organizer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Mod Organizer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ #include "modinfo.h" #include "modinfobackup.h" #include "modinfoforeign.h" #include "modinfooverwrite.h" #include "modinforegular.h" #include "modinfoseparator.h" #include "categories.h" #include "modinfodialog.h" #include "modlist.h" #include "organizercore.h" #include "overwriteinfodialog.h" #include "thread_utils.h" #include "versioninfo.h" #include "shared/appconfig.h" #include #include #include #include #include #include #include #include #include using namespace MOBase; using namespace MOShared; const std::set ModInfo::s_EmptySet; std::vector ModInfo::s_Collection; ModInfo::Ptr ModInfo::s_Overwrite; std::map ModInfo::s_ModsByName; std::map, std::vector> ModInfo::s_ModsByModID; int ModInfo::s_NextID; QRecursiveMutex ModInfo::s_Mutex; QString ModInfo::s_HiddenExt(".mohidden"); bool ModInfo::ByName(const ModInfo::Ptr& LHS, const ModInfo::Ptr& RHS) { return QString::compare(LHS->name(), RHS->name(), Qt::CaseInsensitive) < 0; } bool ModInfo::isSeparatorName(const QString& name) { static QRegularExpression separatorExp( QRegularExpression::anchoredPattern(".*_separator")); return separatorExp.match(name).hasMatch(); } bool ModInfo::isBackupName(const QString& name) { static QRegularExpression backupExp( QRegularExpression::anchoredPattern(".*backup[0-9]*")); return backupExp.match(name).hasMatch(); } bool ModInfo::isRegularName(const QString& name) { return !isSeparatorName(name) && !isBackupName(name); } ModInfo::Ptr ModInfo::createFrom(const QDir& dir, OrganizerCore& core) { QMutexLocker locker(&s_Mutex); ModInfo::Ptr result; if (isBackupName(dir.dirName())) { result = ModInfo::Ptr(new ModInfoBackup(dir, core)); } else if (isSeparatorName(dir.dirName())) { result = Ptr(new ModInfoSeparator(dir, core)); } else { result = ModInfo::Ptr(new ModInfoRegular(dir, core)); } result->m_Index = s_Collection.size(); s_Collection.push_back(result); return result; } ModInfo::Ptr ModInfo::createFromPlugin(const QString& modName, const QString& espName, const QStringList& bsaNames, ModInfo::EModType modType, OrganizerCore& core) { QMutexLocker locker(&s_Mutex); ModInfo::Ptr result = ModInfo::Ptr(new ModInfoForeign(modName, espName, bsaNames, modType, core)); result->m_Index = s_Collection.size(); s_Collection.push_back(result); return result; } ModInfo::Ptr ModInfo::createFromOverwrite(OrganizerCore& core) { QMutexLocker locker(&s_Mutex); ModInfo::Ptr overwrite = ModInfo::Ptr(new ModInfoOverwrite(core)); overwrite->m_Index = s_Collection.size(); s_Collection.push_back(overwrite); return overwrite; } unsigned int ModInfo::getNumMods() { QMutexLocker locker(&s_Mutex); return static_cast(s_Collection.size()); } ModInfo::Ptr ModInfo::getByIndex(unsigned int index) { QMutexLocker locker(&s_Mutex); if (index >= s_Collection.size() && index != ULONG_MAX) { throw MyException(tr("invalid mod index: %1").arg(index)); } if (index == ULONG_MAX) return s_Collection[ModInfo::getIndex("Overwrite")]; return s_Collection[index]; } std::vector ModInfo::getByModID(QString game, int modID) { QMutexLocker locker(&s_Mutex); std::vector match; for (auto iter : s_ModsByModID) { if (iter.first.second == modID) { if (iter.first.first.compare(game, Qt::CaseInsensitive) == 0) { match.insert(match.end(), iter.second.begin(), iter.second.end()); } } } if (match.empty()) { return std::vector(); } std::vector result; for (auto iter : match) { result.push_back(getByIndex(iter)); } return result; } ModInfo::Ptr ModInfo::getByName(const QString& name) { QMutexLocker locker(&s_Mutex); return s_Collection[ModInfo::getIndex(name)]; } bool ModInfo::removeMod(unsigned int index) { QMutexLocker locker(&s_Mutex); if (index >= s_Collection.size()) { throw Exception(tr("remove: invalid mod index %1").arg(index)); } ModInfo::Ptr modInfo = s_Collection[index]; // remove the actual mod (this is the most likely to fail so we do this first) if (modInfo->isRegular()) { if (!shellDelete(QStringList(modInfo->absolutePath()), true)) { reportError( tr("remove: failed to delete mod '%1' directory").arg(modInfo->name())); return false; } } // update the indices s_ModsByName.erase(s_ModsByName.find(modInfo->name())); auto iter = s_ModsByModID.find( std::pair(modInfo->gameName(), modInfo->nexusId())); if (iter != s_ModsByModID.end()) { std::vector indices = iter->second; indices.erase(std::remove(indices.begin(), indices.end(), index), indices.end()); s_ModsByModID[std::pair(modInfo->gameName(), modInfo->nexusId())] = indices; } // finally, remove the mod from the collection s_Collection.erase(s_Collection.begin() + index); // and update the indices updateIndices(); return true; } unsigned int ModInfo::getIndex(const QString& name) { QMutexLocker locker(&s_Mutex); std::map::iterator iter = s_ModsByName.find(name); if (iter == s_ModsByName.end()) { return UINT_MAX; } return iter->second; } unsigned int ModInfo::findMod(const boost::function& filter) { for (unsigned int i = 0U; i < s_Collection.size(); ++i) { if (filter(s_Collection[i])) { return i; } } return UINT_MAX; } void ModInfo::updateFromDisc(const QString& modsDirectory, OrganizerCore& core, bool displayForeign, std::size_t refreshThreadCount) { TimeThis tt("ModInfo::updateFromDisc()"); QMutexLocker lock(&s_Mutex); s_Collection.clear(); s_NextID = 0; s_Overwrite = nullptr; { // list all directories in the mod directory and make a mod out of each QDir mods(QDir::fromNativeSeparators(modsDirectory)); mods.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); QDirIterator modIter(mods); while (modIter.hasNext()) { createFrom(QDir(modIter.next()), core); } } auto* game = core.managedGame(); auto& features = core.pluginContainer().gameFeatures(); auto unmanaged = features.gameFeature(); if (unmanaged != nullptr) { for (const QString& modName : unmanaged->mods(!displayForeign)) { ModInfo::EModType modType = game->DLCPlugins().contains(unmanaged->referenceFile(modName).fileName(), Qt::CaseInsensitive) ? ModInfo::EModType::MOD_DLC : (game->CCPlugins().contains( unmanaged->referenceFile(modName).fileName(), Qt::CaseInsensitive) ? ModInfo::EModType::MOD_CC : ModInfo::EModType::MOD_DEFAULT); createFromPlugin(unmanaged->displayName(modName), unmanaged->referenceFile(modName).absoluteFilePath(), unmanaged->secondaryFiles(modName), modType, core); } } s_Overwrite = createFromOverwrite(core); std::sort(s_Collection.begin(), s_Collection.end(), ModInfo::ByName); parallelMap(std::begin(s_Collection), std::end(s_Collection), &ModInfo::prefetch, refreshThreadCount); updateIndices(); } void ModInfo::updateIndices() { s_ModsByName.clear(); s_ModsByModID.clear(); for (unsigned int i = 0; i < s_Collection.size(); ++i) { QString modName = s_Collection[i]->internalName(); QString game = s_Collection[i]->gameName(); int modID = s_Collection[i]->nexusId(); s_Collection[i]->m_Index = i; s_ModsByName[modName] = i; s_ModsByModID[std::pair(game, modID)].push_back(i); } } ModInfo::ModInfo(OrganizerCore& core) : m_PrimaryCategory(-1), m_Core(core) {} bool ModInfo::checkAllForUpdate(PluginContainer* pluginContainer, QObject* receiver) { bool updatesAvailable = true; QDateTime earliest = QDateTime::currentDateTimeUtc(); QDateTime latest = QDateTime::fromMSecsSinceEpoch(0); std::set games; for (auto mod : s_Collection) { if (mod->canBeUpdated()) { if (mod->getLastNexusUpdate() < earliest) earliest = mod->getLastNexusUpdate(); if (mod->getLastNexusUpdate() > latest) latest = mod->getLastNexusUpdate(); games.insert(mod->gameName().toLower()); } } // Detect invalid source games const auto& gamePlugins = pluginContainer->plugins(); for (auto itr = games.begin(); itr != games.end();) { IPluginGame* gamePlugin = qApp->property("managed_game").value(); for (auto plugin : gamePlugins) { if (plugin != nullptr && plugin->gameShortName().compare(*itr, Qt::CaseInsensitive) == 0) { gamePlugin = plugin; break; } } if (gamePlugin != nullptr && gamePlugin->gameNexusName().isEmpty()) { log::warn("{}", tr("The update check has found a mod with a Nexus ID and source " "game of %1, but this game is not a valid Nexus source.") .arg(gamePlugin->gameName())); itr = games.erase(itr); } else { ++itr; } } if (latest < QDateTime::currentDateTimeUtc().addMonths(-1)) { std::set> organizedGames; for (auto mod : s_Collection) { if (mod->canBeUpdated() && mod->getLastNexusUpdate() < QDateTime::currentDateTimeUtc().addMonths(-1)) { organizedGames.insert( std::make_pair(mod->gameName().toLower(), mod->nexusId())); } } if (organizedGames.empty()) { log::warn("{}", tr("All of your mods have been checked recently. We restrict update " "checks to help preserve your available API requests.")); updatesAvailable = false; } else { log::info("{}", tr("You have mods that haven't been checked within the last " "month using the new API. These mods must be checked before " "we can use the bulk update API. " "This will consume significantly more API requests than " "usual. You will need to rerun the update check once complete " "in order to parse the remaining mods.")); } for (auto game : organizedGames) NexusInterface::instance().requestUpdates(game.second, receiver, QVariant(), game.first, QString()); } else if (earliest < QDateTime::currentDateTimeUtc().addMonths(-1)) { for (auto gameName : games) NexusInterface::instance().requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(true), QString()); } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-7)) { for (auto gameName : games) NexusInterface::instance().requestUpdateInfo( gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(false), QString()); } else if (earliest < QDateTime::currentDateTimeUtc().addDays(-1)) { for (auto gameName : games) NexusInterface::instance().requestUpdateInfo( gameName, NexusInterface::UpdatePeriod::WEEK, receiver, QVariant(false), QString()); } else { for (auto gameName : games) NexusInterface::instance().requestUpdateInfo( gameName, NexusInterface::UpdatePeriod::DAY, receiver, QVariant(false), QString()); } return updatesAvailable; } std::set> ModInfo::filteredMods(QString gameName, QVariantList updateData, bool addOldMods, bool markUpdated) { std::set> finalMods; for (QVariant result : updateData) { QVariantMap update = result.toMap(); std::copy_if(s_Collection.begin(), s_Collection.end(), std::inserter(finalMods, finalMods.end()), [=](QSharedPointer info) -> bool { if (info->nexusId() == update["mod_id"].toInt() && info->gameName().compare(gameName, Qt::CaseInsensitive) == 0) if (info->getLastNexusUpdate().addSecs(-3600) < QDateTime::fromSecsSinceEpoch( update["latest_file_update"].toInt(), Qt::UTC)) return true; return false; }); } if (addOldMods) for (auto mod : s_Collection) if (mod->getLastNexusUpdate() < QDateTime::currentDateTimeUtc().addMonths(-1) && mod->gameName().compare(gameName, Qt::CaseInsensitive) == 0) finalMods.insert(mod); if (markUpdated) { std::set> updates; std::copy_if(s_Collection.begin(), s_Collection.end(), std::inserter(updates, updates.end()), [=](QSharedPointer info) -> bool { if (info->gameName().compare(gameName, Qt::CaseInsensitive) == 0 && info->canBeUpdated()) return true; return false; }); std::set> diff; std::set_difference(updates.begin(), updates.end(), finalMods.begin(), finalMods.end(), std::inserter(diff, diff.end())); for (auto skipped : diff) { skipped->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); } } return finalMods; } void ModInfo::manualUpdateCheck(QObject* receiver, std::multimap IDs) { std::vector> mods; std::set> organizedGames; for (auto ID : IDs) { for (auto matchedMod : getByModID(ID.first, ID.second)) { bool alreadyMatched = false; for (auto mod : mods) { if (mod == matchedMod) { alreadyMatched = true; break; } } if (!alreadyMatched) mods.push_back(matchedMod); } } mods.erase(std::remove_if(mods.begin(), mods.end(), [](ModInfo::Ptr mod) -> bool { return mod->nexusId() <= 0; }), mods.end()); for (auto mod : mods) { mod->setLastNexusUpdate(QDateTime()); } std::sort(mods.begin(), mods.end(), [](QSharedPointer a, QSharedPointer b) -> bool { return a->getLastNexusUpdate() < b->getLastNexusUpdate(); }); if (mods.size()) { log::info("Checking updates for {} mods...", mods.size()); for (auto mod : mods) { organizedGames.insert( std::make_pair(mod->gameName().toLower(), mod->nexusId())); } for (auto game : organizedGames) { NexusInterface::instance().requestUpdates(game.second, receiver, QVariant(), game.first, QString()); } } else { log::info("None of the selected mods can be updated."); } } void ModInfo::setVersion(const VersionInfo& version) { m_Version = version; } void ModInfo::setPluginSelected(const bool& isSelected) { m_PluginSelected = isSelected; } void ModInfo::addCategory(const QString& categoryName) { int id = CategoryFactory::instance().getCategoryID(categoryName); if (id == -1) { id = CategoryFactory::instance().addCategory( categoryName, std::vector(), 0); } setCategory(id, true); } bool ModInfo::removeCategory(const QString& categoryName) { int id = CategoryFactory::instance().getCategoryID(categoryName); if (id == -1) { return false; } if (!categorySet(id)) { return false; } setCategory(id, false); return true; } QStringList ModInfo::categories() const { QStringList result; CategoryFactory& catFac = CategoryFactory::instance(); for (int id : m_Categories) { result.append(catFac.getCategoryName(catFac.getCategoryIndex(id))); } return result; } bool ModInfo::hasFlag(ModInfo::EFlag flag) const { std::vector flags = getFlags(); return std::find(flags.begin(), flags.end(), flag) != flags.end(); } bool ModInfo::hasAnyOfTheseFlags(std::vector flags) const { std::vector modFlags = getFlags(); for (auto modFlag : modFlags) { for (auto flag : flags) { if (modFlag == flag) { return true; } } } return false; } bool ModInfo::categorySet(int categoryID) const { for (std::set::const_iterator iter = m_Categories.begin(); iter != m_Categories.end(); ++iter) { if ((*iter == categoryID) || (CategoryFactory::instance().isDescendantOf(*iter, categoryID))) { return true; } } return false; } QUrl ModInfo::parseCustomURL() const { if (!hasCustomURL() || url().isEmpty()) { return {}; } const auto url = QUrl::fromUserInput(this->url()); if (!url.isValid()) { log::error("mod '{}' has an invalid custom url '{}'", name(), this->url()); return {}; } return url; }