/*
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 "modinforegular.h"
#include "modinfoforeign.h"
#include "modinfooverwrite.h"
#include "modinfoseparator.h"
#include "categories.h"
#include "modinfodialog.h"
#include "overwriteinfodialog.h"
#include "versioninfo.h"
#include "thread_utils.h"
#include
#include
#include "shared/appconfig.h"
#include
#include
#include
#include
#include
#include
using namespace MOBase;
using namespace MOShared;
std::vector ModInfo::s_Collection;
std::map ModInfo::s_ModsByName;
std::map, std::vector> ModInfo::s_ModsByModID;
int ModInfo::s_NextID;
QMutex ModInfo::s_Mutex(QMutex::Recursive);
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 QRegExp separatorExp(".*_separator");
return separatorExp.exactMatch(name);
}
bool ModInfo::isBackupName(const QString& name)
{
static QRegExp backupExp(".*backup[0-9]*");
return backupExp.exactMatch(name);
}
bool ModInfo::isRegularName(const QString& name)
{
return !isSeparatorName(name) && !isBackupName(name);
}
ModInfo::Ptr ModInfo::createFrom(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &dir, DirectoryEntry **directoryStructure)
{
QMutexLocker locker(&s_Mutex);
ModInfo::Ptr result;
if (isBackupName(dir.dirName())) {
result = ModInfo::Ptr(new ModInfoBackup(pluginContainer, game, dir, directoryStructure));
} else if (isSeparatorName(dir.dirName())) {
result = Ptr(new ModInfoSeparator(pluginContainer, game, dir, directoryStructure));
} else {
result = ModInfo::Ptr(new ModInfoRegular(pluginContainer, game, dir, directoryStructure));
}
s_Collection.push_back(result);
return result;
}
ModInfo::Ptr ModInfo::createFromPlugin(const QString &modName,
const QString &espName,
const QStringList &bsaNames,
ModInfo::EModType modType,
const MOBase::IPluginGame* game,
DirectoryEntry **directoryStructure,
PluginContainer *pluginContainer) {
QMutexLocker locker(&s_Mutex);
ModInfo::Ptr result = ModInfo::Ptr(
new ModInfoForeign(modName, espName, bsaNames, modType, game, directoryStructure, pluginContainer));
s_Collection.push_back(result);
return result;
}
void ModInfo::createFromOverwrite(PluginContainer *pluginContainer,
const MOBase::IPluginGame* game,
MOShared::DirectoryEntry **directoryStructure)
{
QMutexLocker locker(&s_Mutex);
s_Collection.push_back(ModInfo::Ptr(new ModInfoOverwrite(pluginContainer, game, directoryStructure)));
}
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 MyException(tr("remove: invalid mod index %1").arg(index));
}
// update the indices first
ModInfo::Ptr modInfo = s_Collection[index];
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;
}
// physically remove the mod directory
//TODO the return value is ignored because the indices were already removed here, so stopping
// would cause data inconsistencies. Instead we go through with the removal but the mod will show up
// again if the user refreshes
modInfo->remove();
// 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 &modDirectory,
DirectoryEntry **directoryStructure,
PluginContainer *pluginContainer,
bool displayForeign,
std::size_t refreshThreadCount,
MOBase::IPluginGame const *game)
{
TimeThis tt("ModInfo::updateFromDisc()");
QMutexLocker lock(&s_Mutex);
s_Collection.clear();
s_NextID = 0;
{ // list all directories in the mod directory and make a mod out of each
QDir mods(QDir::fromNativeSeparators(modDirectory));
mods.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
QDirIterator modIter(mods);
while (modIter.hasNext()) {
createFrom(pluginContainer, game, QDir(modIter.next()), directoryStructure);
}
}
UnmanagedMods *unmanaged = game->feature();
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,
game,
directoryStructure,
pluginContainer);
}
}
createFromOverwrite(pluginContainer, game, directoryStructure);
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_ModsByName[modName] = i;
s_ModsByModID[std::pair(game, modID)].push_back(i);
}
}
ModInfo::ModInfo(PluginContainer *pluginContainer)
: m_PrimaryCategory(-1)
{
}
bool ModInfo::checkAllForUpdate(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
for (auto itr = games.begin(); itr != games.end();) {
auto gamePlugins = pluginContainer->plugins();
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().isDecendantOf(*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;
}