/*
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 "envfs.h"
#include
#include
#include "shared/appconfig.h"
#include
#include
#include
#include
#include
#include
using namespace MOBase;
using namespace MOShared;
env::ThreadPool ModInfo::s_Threads;
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;
}
QString ModInfo::getContentTypeName(int contentType)
{
switch (contentType) {
case CONTENT_PLUGIN: return tr("Plugins");
case CONTENT_TEXTURE: return tr("Textures");
case CONTENT_MESH: return tr("Meshes");
case CONTENT_BSA: return tr("Bethesda Archive");
case CONTENT_INTERFACE: return tr("UI Changes");
case CONTENT_SOUND: return tr("Sound Effects");
case CONTENT_SCRIPT: return tr("Scripts");
case CONTENT_SKSE: return tr("Script Extender");
case CONTENT_SKSEFILES: return tr("Script Extender Files");
case CONTENT_SKYPROC: return tr("SkyProc Tools");
case CONTENT_MCM: return tr("MCM Data");
case CONTENT_INI: return tr("INI files");
case CONTENT_MODGROUP: return tr("ModGroup files");
default: throw MyException(tr("invalid content type: %1").arg(contentType));
}
}
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->getGameName(), modInfo->getNexusID()));
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->getGameName(), modInfo->getNexusID())] = 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);
// This force loading a part of the FileTree:
s_Threads.setMax(refreshThreadCount);
for (auto& p : s_Collection) {
auto& t = s_Threads.request();
t.ptr = p;
t.wakeup();
}
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]->getGameName();
int modID = s_Collection[i]->getNexusID();
s_ModsByName[modName] = i;
s_ModsByModID[std::pair(game, modID)].push_back(i);
}
}
ModInfo::ModInfo(PluginContainer *pluginContainer)
: m_PrimaryCategory(-1)
{
}
bool ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiver)
{
bool updatesAvailable = true;
QDateTime earliest = QDateTime::currentDateTimeUtc();
QDateTime latest;
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->getGameName().toLower());
}
}
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->getGameName().toLower(), mod->getNexusID()));
}
}
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(pluginContainer)->requestUpdates(game.second, receiver, QVariant(), game.first, QString());
} else if (earliest < QDateTime::currentDateTimeUtc().addMonths(-1)) {
for (auto gameName : games)
NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(true), QString());
} else if (earliest < QDateTime::currentDateTimeUtc().addDays(-7)) {
for (auto gameName : games)
NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::MONTH, receiver, QVariant(false), QString());
} else if (earliest < QDateTime::currentDateTimeUtc().addDays(-1)) {
for (auto gameName : games)
NexusInterface::instance(pluginContainer)->requestUpdateInfo(gameName, NexusInterface::UpdatePeriod::WEEK, receiver, QVariant(false), QString());
} else {
for (auto gameName : games)
NexusInterface::instance(pluginContainer)->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->getNexusID() == update["mod_id"].toInt() && info->getGameName().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->getGameName().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->getGameName().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(PluginContainer *pluginContainer, 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->getNexusID() <= 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->getGameName().toLower(), mod->getNexusID()));
}
for (auto game : organizedGames) {
NexusInterface::instance(pluginContainer)->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::hasContent(ModInfo::EContent content) const
{
std::vector contents = getContents();
return std::find(contents.begin(), contents.end(), content) != contents.end();
}
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() || getCustomURL().isEmpty()) {
return {};
}
const auto url = QUrl::fromUserInput(getCustomURL());
if (!url.isValid()) {
log::error("mod '{}' has an invalid custom url '{}'", name(), getCustomURL());
return {};
}
return url;
}