From 5d1154c24e2475ed2b7ac248de27ab7b501a1e5e Mon Sep 17 00:00:00 2001 From: Tannin Date: Sat, 16 Feb 2013 19:09:57 +0100 Subject: - hooks for CreateHardLink - createprocess hook will now reroute the cwd (fixes SUM, may break other tools?) - profile code moved to separate file - executables can now be linked to toolbar - category filters are now represented as a tree - csv export of mod list - ModInfoDialog is now "window modal" instead of "application modal" - nexus dialog now updates the mod-id field while browsing - right-click in nexus browser allows to open in external browser - ini viewer is no longer modal - bugfix: another attempt to fix processing of invalid header lines in fomod xmls (bom now handled) - bugfix: integrated fomod installer will no longer overwrite detected mod name by the one from the xml - bugfix: "hide file" from conflicted files list now updates that list - bugfix: conflicted files list no longer offers to hide files in BSAs - bugfix: pressing delete on the mod list with multiplie files selected offered to delete the wrong files - regression: mod name guessing with the old regular expression was less likely to lead to an empty mod name. Now both are tried --- src/modinfo.cpp | 1651 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 826 insertions(+), 825 deletions(-) (limited to 'src/modinfo.cpp') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index a46774c2..0a0cde68 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -17,828 +17,829 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "modinfo.h" -#include "utility.h" -#include "installationtester.h" -#include "categories.h" -#include "report.h" -#include "modinfodialog.h" -#include "overwriteinfodialog.h" -#include "json.h" -#include "messagedialog.h" - -#include -#include - -#include - -#include -#include -#include -#include - - -std::vector ModInfo::s_Collection; -std::map ModInfo::s_ModsByName; -std::map ModInfo::s_ModsByModID; -int ModInfo::s_NextID; -QMutex ModInfo::s_Mutex(QMutex::Recursive); - -QString ModInfo::s_HiddenExt(".mohidden"); - - -static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS) -{ - return QString::compare(LHS->name(), RHS->name(), Qt::CaseInsensitive) < 0; -} - -ModInfo::NexusFileInfo::NexusFileInfo(const QString &data) -{ - QVariantList result = QtJson::Json::parse(data).toList(); - - while (result.length() < 7) { - result.append(QVariant()); - } - id = result.at(0).toInt(); - name = result.at(1).toString(); - url = result.at(2).toString(); - version = result.at(3).toString(); - description = result.at(4).toString(); - category = result.at(5).toInt(); - size = result.at(6).toInt(); -} - - -QString ModInfo::NexusFileInfo::toString() const -{ - return QString("[ %1,\"%2\",\"%3\",\"%4\",\"%5\",%6,%7 ]") - .arg(id) - .arg(name) - .arg(url) - .arg(version) - .arg(description.mid(0).replace("\"", "'")) - .arg(category) - .arg(size); -} - - -ModInfo::Ptr ModInfo::createFrom(const QDir &dir, DirectoryEntry **directoryStructure) -{ - QMutexLocker locker(&s_Mutex); -// int id = s_NextID++; - static QRegExp backupExp(".*backup[0-9]*"); - ModInfo::Ptr result; - if (backupExp.exactMatch(dir.dirName())) { - result = ModInfo::Ptr(new ModInfoBackup(dir, directoryStructure)); - } else { - result = ModInfo::Ptr(new ModInfoRegular(dir, directoryStructure)); - } - s_Collection.push_back(result); - return result; -} - - -void ModInfo::createFromOverwrite() -{ - QMutexLocker locker(&s_Mutex); - - s_Collection.push_back(ModInfo::Ptr(new ModInfoOverwrite)); -} - - -unsigned int ModInfo::getNumMods() -{ - QMutexLocker locker(&s_Mutex); - return s_Collection.size(); -} - - -ModInfo::Ptr ModInfo::getByIndex(unsigned int index) -{ - QMutexLocker locker(&s_Mutex); - - if (index >= s_Collection.size()) { - throw MyException(tr("invalid index %1").arg(index)); - } - return s_Collection[index]; -} - - -ModInfo::Ptr ModInfo::getByModID(int modID, bool missingAcceptable) -{ - QMutexLocker locker(&s_Mutex); - - std::map::iterator iter = s_ModsByModID.find(modID); - if (iter == s_ModsByModID.end()) { - if (missingAcceptable) { - return ModInfo::Ptr(); - } else { - throw MyException(tr("invalid mod id %1").arg(modID)); - } - } - - return getByIndex(iter->second); -} - - -bool ModInfo::removeMod(unsigned int index) -{ - QMutexLocker locker(&s_Mutex); - - if (index >= s_Collection.size()) { - throw MyException(tr("invalid index %1").arg(index)); - } - - // update the indices first - ModInfo::Ptr modInfo = s_Collection[index]; - s_ModsByName.erase(s_ModsByName.find(modInfo->name())); - - //TODO this is a bit more complicated since multiple mods may have the - // same mod id but only one appears in the index - std::map::iterator iter = s_ModsByModID.find(modInfo->getNexusID()) ; - if ((iter != s_ModsByModID.end()) && - (iter->second == index)) { - s_ModsByModID.erase(iter); - } - - // 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) -{ - 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(QDir(modIter.next()), directoryStructure); - } - - createFromOverwrite(); - - std::sort(s_Collection.begin(), s_Collection.end(), ByName); - - updateIndices(); -} - - -void ModInfo::updateIndices() -{ - s_ModsByName.clear(); - s_ModsByModID.clear(); - QRegExp backupRegEx(".*backup[0-9]*$"); - - for (unsigned int i = 0; i < s_Collection.size(); ++i) { - QString modName = s_Collection[i]->name(); - int modID = s_Collection[i]->getNexusID(); - s_ModsByName[modName] = i; - - // don't overwrite a modid-entry with a backup entry. This is a bit of a workaround - if ((s_ModsByModID.find(modID) == s_ModsByModID.end()) || - !backupRegEx.exactMatch(modName)) { - s_ModsByModID[modID] = i; - } - } -} - - -ModInfo::ModInfo() - : m_Valid(false), m_PrimaryCategory(-1) -{ -} - - -void ModInfo::checkChunkForUpdate(const std::vector &modIDs, QObject *receiver) -{ - if (modIDs.size() != 0) { - NexusInterface::instance()->requestUpdates(modIDs, receiver, QVariant()); - } -} - - -int ModInfo::checkAllForUpdate(QObject *receiver) -{ - int result = 0; - std::vector modIDs; - - modIDs.push_back(GameInfo::instance().getNexusModID()); - - for (std::vector::iterator iter = s_Collection.begin(); - iter != s_Collection.end(); ++iter) { - if ((*iter)->canBeUpdated()) { - modIDs.push_back((*iter)->getNexusID()); - if (modIDs.size() >= 255) { - checkChunkForUpdate(modIDs, receiver); - modIDs.clear(); - } - } - } - - checkChunkForUpdate(modIDs, receiver); - - return result; -} - -void ModInfo::setVersion(const VersionInfo &version) -{ - m_Version = version; -} - - -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; -} - - -void ModInfo::testValid() -{ - m_Valid = false; - QDirIterator dirIter(absolutePath()); - while (dirIter.hasNext()) { - dirIter.next(); - if (dirIter.fileInfo().isDir()) { - if (InstallationTester::isTopLevelDirectory(dirIter.fileName())) { - m_Valid = true; - break; - } - } else { - if (InstallationTester::isTopLevelSuffix(dirIter.fileName())) { - m_Valid = true; - break; - } - } - } - - // NOTE: in Qt 4.7 it seems that QDirIterator leaves a file handle open if it is not iterated to the - // end - while (dirIter.hasNext()) { - dirIter.next(); - } -} - - -ModInfoRegular::ModInfoRegular(const QDir &path, DirectoryEntry **directoryStructure) - : ModInfo(), m_Name(path.dirName()), m_Path(path.absolutePath()), m_MetaInfoChanged(false), - m_EndorsedState(ENDORSED_UNKNOWN), m_DirectoryStructure(directoryStructure) -{ - testValid(); - - // read out the meta-file for information - QString metaFileName = path.absoluteFilePath("meta.ini"); - QSettings metaFile(metaFileName, QSettings::IniFormat); - - m_Notes = metaFile.value("notes", "").toString(); - m_NexusID = metaFile.value("modid", -1).toInt(); - m_Version.parse(metaFile.value("version", "").toString()); - m_NewestVersion = metaFile.value("newestVersion", "").toString(); - m_InstallationFile = metaFile.value("installationFile", "").toString(); - m_NexusDescription = metaFile.value("nexusDescription", "").toString(); - m_LastNexusQuery = QDateTime::fromString(metaFile.value("lastNexusQuery", "").toString(), Qt::ISODate); - if (metaFile.contains("endorsed")) { - m_EndorsedState = metaFile.value("endorsed", false).toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; - } - - int numNexusFiles = metaFile.beginReadArray("nexusFiles"); - for (int i = 0; i < numNexusFiles; ++i) { - metaFile.setArrayIndex(i); - QString infoString = metaFile.value("info", "").toString(); - NexusFileInfo info(infoString); - m_NexusFileInfos.push_back(info); - } - metaFile.endArray(); - - QString categoriesString = metaFile.value("category", "").toString(); - - QStringList categories = categoriesString.split(',', QString::SkipEmptyParts); - for (QStringList::iterator iter = categories.begin(); iter != categories.end(); ++iter) { - bool ok = false; - int categoryID = iter->toInt(&ok); - if (categoryID < 0) { - // ignore invalid id - continue; - } - if (ok && (categoryID != 0) && (CategoryFactory::instance().categoryExists(categoryID))) { - m_Categories.insert(categoryID); - if (iter == categories.begin()) { - m_PrimaryCategory = categoryID; - } - } - } - - connect(&m_NexusBridge, SIGNAL(nxmDescriptionAvailable(int,QVariant,QVariant)), this, SLOT(nxmDescriptionAvailable(int,QVariant,QVariant))); - connect(&m_NexusBridge, SIGNAL(nxmFilesAvailable(int,QVariant,QVariant)), this, SLOT(nxmFilesAvailable(int,QVariant,QVariant))); - connect(&m_NexusBridge, SIGNAL(nxmEndorsementToggled(int,QVariant,QVariant)), this, SLOT(nxmEndorsementToggled(int,QVariant,QVariant))); - connect(&m_NexusBridge, SIGNAL(nxmRequestFailed(int,QVariant,QString)), this, SLOT(nxmRequestFailed(int,QVariant,QString))); -} - - -ModInfoRegular::~ModInfoRegular() -{ - try { - //TODO this may cause the meta-file and the directory to be - // re-created after a remove - saveMeta(); - } catch (const std::exception &e) { - qCritical("failed to save meta information for \"%s\": %s", - m_Name.toUtf8().constData(), e.what()); - } -} - - -void ModInfoRegular::saveMeta() -{ - if (m_MetaInfoChanged) { - if (QFile::exists(absolutePath().append("/meta.ini"))) { - QSettings metaFile(absolutePath().append("/meta.ini"), QSettings::IniFormat); - if (metaFile.status() == QSettings::NoError) { - std::set temp = m_Categories; - temp.erase(m_PrimaryCategory); - metaFile.setValue("category", QString("%1").arg(m_PrimaryCategory) + "," + SetJoin(temp, ",")); - metaFile.setValue("newestVersion", m_NewestVersion.canonicalString()); - metaFile.setValue("version", m_Version.canonicalString()); - metaFile.setValue("modid", m_NexusID); - metaFile.setValue("notes", m_Notes); - metaFile.setValue("nexusDescription", m_NexusDescription); - metaFile.setValue("lastNexusQuery", m_LastNexusQuery.toString(Qt::ISODate)); - if (m_EndorsedState != ENDORSED_UNKNOWN) { - metaFile.setValue("endorsed", m_EndorsedState == ENDORSED_TRUE ? true : false); - } - - metaFile.beginWriteArray("nexusFiles"); - for (int i = 0; i < m_NexusFileInfos.size(); ++i) { - metaFile.setArrayIndex(i); - metaFile.setValue("info", m_NexusFileInfos.at(i).toString()); - } - metaFile.endArray(); - } else { - reportError(tr("failed to write %1/meta.ini: %2").arg(absolutePath()).arg(metaFile.status())); - } - } else { - qWarning("mod %s has no meta.ini at %s/meta.ini", m_Name.toUtf8().constData(), absolutePath().toUtf8().constData()); - } - m_MetaInfoChanged = false; - } -} - - -bool ModInfoRegular::updateAvailable() const -{ - return m_NewestVersion.isValid() && (m_Version < m_NewestVersion); -} - - -void ModInfoRegular::nxmDescriptionAvailable(int, QVariant, QVariant resultData) -{ - QVariantMap result = resultData.toMap(); - m_NewestVersion.parse(result["version"].toString()); - m_NexusDescription = result["description"].toString(); - m_EndorsedState = result["voted_by_user"].toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; - m_NexusBridge.requestFiles(m_NexusID, QVariant()); -} - - -void ModInfoRegular::nxmFilesAvailable(int, QVariant, QVariant resultData) -{ - m_NexusFileInfos.clear(); - - QVariantList result = resultData.toList(); - - foreach(QVariant file, result) { - QVariantMap fileInfo = file.toMap(); - - m_NexusFileInfos.push_back(NexusFileInfo(fileInfo["id"].toInt(), - fileInfo["name"].toString(), - fileInfo["uri"].toString(), - fileInfo["version"].toString(), - fileInfo["description"].toString(), - fileInfo["category_id"].toInt(), - fileInfo["size"].toInt())); - } - - m_LastNexusQuery = QDateTime::currentDateTime(); - m_MetaInfoChanged = true; - saveMeta(); - emit modDetailsUpdated(true); -} - - -void ModInfoRegular::nxmEndorsementToggled(int, QVariant, QVariant resultData) -{ - m_EndorsedState = resultData.toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; - m_MetaInfoChanged = true; - saveMeta(); - emit modDetailsUpdated(true); -} - - -void ModInfoRegular::nxmRequestFailed(int, QVariant userData, const QString &errorMessage) -{ - QString fullMessage = errorMessage; - if (userData.canConvert() && (userData.toInt() == 1)) { - fullMessage += "\nNexus will reject endorsements within 15 Minutes of a failed attempt, the error message may be misleading."; - } - if (QApplication::activeWindow() != NULL) { - MessageDialog::showMessage(fullMessage, QApplication::activeWindow()); - } - emit modDetailsUpdated(false); -} - - -bool ModInfoRegular::updateNXMInfo() -{ - if (m_NexusID > 0) { - m_NexusBridge.requestDescription(m_NexusID, QVariant()); - return true; - } - return false; -} - - -void ModInfoRegular::setCategory(int categoryID, bool active) -{ - m_MetaInfoChanged = true; - - if (active) { - m_Categories.insert(categoryID); - if (m_PrimaryCategory == -1) { - m_PrimaryCategory = categoryID; - } - } else { - std::set::iterator iter = m_Categories.find(categoryID); - if (iter != m_Categories.end()) { - m_Categories.erase(iter); - } - if (categoryID == m_PrimaryCategory) { - if (m_Categories.size() == 0) { - m_PrimaryCategory = -1; - } else { - m_PrimaryCategory = *(m_Categories.begin()); - } - } - } -} - - -bool ModInfoRegular::setName(const QString &name) -{ - if (name.contains('/') || name.contains('\\')) { - return false; - } - - QString newPath = m_Path.mid(0).replace(m_Path.length() - m_Name.length(), m_Name.length(), name); - QDir modDir(m_Path.mid(0, m_Path.length() - m_Name.length())); - if (m_Name.compare(name, Qt::CaseInsensitive) == 0) { - QString tempName = name; - tempName.append("_temp"); - while (modDir.exists(tempName)) { - tempName.append("_"); - } - if (!modDir.rename(m_Name, tempName)) { - return false; - } - if (!modDir.rename(tempName, name)) { - qCritical("rename to final name failed after successful rename to intermediate name"); - modDir.rename(tempName, m_Name); - return false; - } - } else { - if (!modDir.rename(m_Name, name)) { - return false; - } - } - - std::map::iterator nameIter = s_ModsByName.find(m_Name); - unsigned int index = nameIter->second; - s_ModsByName.erase(nameIter); - - m_Name = name; - m_Path = newPath; - - s_ModsByName[m_Name] = index; - - std::sort(s_Collection.begin(), s_Collection.end(), ByName); - - updateIndices(); - - return true; -} - -void ModInfoRegular::setNotes(const QString ¬es) -{ - m_Notes = notes; -} - -void ModInfoRegular::setVersion(const VersionInfo &version) -{ - m_Version = version; - m_MetaInfoChanged = true; -} - -void ModInfoRegular::setNexusDescription(const QString &description) -{ - m_NexusDescription = description; - m_MetaInfoChanged = true; -} - - -void ModInfoRegular::setIsEndorsed(bool endorsed) -{ - m_EndorsedState = endorsed ? ENDORSED_TRUE : ENDORSED_FALSE; - m_MetaInfoChanged = true; -} - - -bool ModInfoRegular::remove() -{ - m_MetaInfoChanged = false; - return removeDir(absolutePath()); -} - -void ModInfoRegular::endorse(bool doEndorse) -{ - if (doEndorse != (m_EndorsedState == ENDORSED_TRUE)) { - m_NexusBridge.requestToggleEndorsement(getNexusID(), doEndorse, QVariant(1)); - } -} - - -QString ModInfoRegular::absolutePath() const -{ - return m_Path; -} - - -std::vector ModInfoRegular::getFlags() const -{ - std::vector result; - if (!isValid()) { - result.push_back(ModInfo::FLAG_INVALID); - } - if ((m_NexusID != -1) && (endorsedState() == ENDORSED_FALSE)) { - result.push_back(ModInfo::FLAG_NOTENDORSED); - } - switch (isConflicted()) { - case CONFLICT_MIXED: { - result.push_back(ModInfo::FLAG_CONFLICT_MIXED); - } break; - case CONFLICT_OVERWRITE: { - result.push_back(ModInfo::FLAG_CONFLICT_OVERWRITE); - } break; - case CONFLICT_OVERWRITTEN: { - result.push_back(ModInfo::FLAG_CONFLICT_OVERWRITTEN); - } break; - case CONFLICT_REDUNDANT: { - result.push_back(ModInfo::FLAG_CONFLICT_REDUNDANT); - } break; - default: { /* NOP */ } - } - if (m_Notes.length() != 0) { - result.push_back(ModInfo::FLAG_NOTES); - } - return result; -} - - -int ModInfoRegular::getHighlight() const -{ - return isValid() ? HIGHLIGHT_NONE: HIGHLIGHT_INVALID; -} - - -QString ModInfoRegular::getDescription() const -{ - if (!isValid()) { - return tr("%1 contains no esp/esm and no asset (textures, meshes, interface, ...) directory").arg(name()); - } else { - const std::set &categories = getCategories(); - std::wostringstream categoryString; - categoryString << ToWString(tr("Categories:
")); - CategoryFactory &categoryFactory = CategoryFactory::instance(); - for (std::set::const_iterator catIter = categories.begin(); - catIter != categories.end(); ++catIter) { - if (catIter != categories.begin()) { - categoryString << " , "; - } - categoryString << "" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << ""; - } - - return ToQString(categoryString.str()); - } -} - -QString ModInfoRegular::notes() const -{ - return m_Notes; -} - -void ModInfoRegular::getNexusFiles(QList::const_iterator &begin, QList::const_iterator &end) -{ - begin = m_NexusFileInfos.begin(); - end = m_NexusFileInfos.end(); -} - -QString ModInfoRegular::getNexusDescription() const -{ - return m_NexusDescription; -} - - -ModInfoRegular::EEndorsedState ModInfoRegular::endorsedState() const -{ - return m_EndorsedState; -} - - -ModInfoRegular::EConflictType ModInfoRegular::isConflicted() const -{ - bool overwrite = false; - bool overwritten = false; - bool regular = false; - - int dataID = 0; - if ((*m_DirectoryStructure)->originExists(L"data")) { - dataID = (*m_DirectoryStructure)->getOriginByName(L"data").getID(); - } - - std::wstring name = ToWString(m_Name); - if ((*m_DirectoryStructure)->originExists(name)) { - FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); - std::vector files = origin.getFiles(); - for (auto iter = files.begin(); iter != files.end() && (!overwrite || !overwritten || !regular); ++iter) { - const std::vector &alternatives = (*iter)->getAlternatives(); - if (alternatives.size() == 0) { - regular = true; - } else { - for (auto altIter = alternatives.begin(); altIter != alternatives.end(); ++altIter) { - // don't treat files overwritten in data as "conflict"+ - if (*altIter != dataID) { - bool ignore = false; - if ((*iter)->getOrigin(ignore) == origin.getID()) { - overwrite = true; - break; - } else { - overwritten = true; - break; - } - } - } - } - } - } - - if (overwrite && overwritten) return CONFLICT_MIXED; - else if (overwrite) return CONFLICT_OVERWRITE; - else if (overwritten) { - if (!regular) { - return CONFLICT_REDUNDANT; - } else { - return CONFLICT_OVERWRITTEN; - } - } - else return CONFLICT_NONE; -} - - -bool ModInfoRegular::isRedundant() const -{ - std::wstring name = ToWString(m_Name); - if ((*m_DirectoryStructure)->originExists(name)) { - FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); - std::vector files = origin.getFiles(); - bool ignore = false; - for (auto iter = files.begin(); iter != files.end(); ++iter) { - if ((*iter)->getOrigin(ignore) == origin.getID()) { - return false; - } - } - return true; - } else { - return false; - } -} - - -QDateTime ModInfoRegular::getLastNexusQuery() const -{ - return m_LastNexusQuery; -} - -std::vector ModInfoRegular::getIniTweaks() const -{ - QString metaFileName = absolutePath().append("/meta.ini"); - QSettings metaFile(metaFileName, QSettings::IniFormat); - - std::vector result; - - int numTweaks = metaFile.beginReadArray("INI Tweaks"); - - if (numTweaks != 0) { - qDebug("%d active ini tweaks in %s", - numTweaks, metaFileName.toUtf8().constData()); - } - - for (int i = 0; i < numTweaks; ++i) { - metaFile.setArrayIndex(i); - QString filename = absolutePath().append("/INI Tweaks/").append(metaFile.value("name").toString()); - result.push_back(filename); - } - metaFile.endArray(); - return result; -} - -std::vector ModInfoBackup::getFlags() const -{ - std::vector result = ModInfoRegular::getFlags(); - result.insert(result.begin(), ModInfo::FLAG_BACKUP); - return result; -} - - -QString ModInfoBackup::getDescription() const -{ - return tr("This is the backup of a mod"); -} - - -ModInfoBackup::ModInfoBackup(const QDir &path, DirectoryEntry **directoryStructure) - : ModInfoRegular(path, directoryStructure) -{ -} - - -ModInfoOverwrite::ModInfoOverwrite() -{ - testValid(); - -} - - -QString ModInfoOverwrite::absolutePath() const -{ - return QDir::fromNativeSeparators(ToQString(GameInfo::instance().getOverwriteDir())); -} - -std::vector ModInfoOverwrite::getFlags() const -{ - std::vector result; - result.push_back(FLAG_OVERWRITE); - return result; -} - -int ModInfoOverwrite::getHighlight() const -{ - return (isValid() ? HIGHLIGHT_IMPORTANT : HIGHLIGHT_INVALID) | HIGHLIGHT_CENTER; -} - - -QString ModInfoOverwrite::getDescription() const -{ - return tr("This pseudo mod contains files from the virtual data tree that got " - "modified (i.e. by the construction kit)"); -} +#include "modinfo.h" +#include "utility.h" +#include "installationtester.h" +#include "categories.h" +#include "report.h" +#include "modinfodialog.h" +#include "overwriteinfodialog.h" +#include "json.h" +#include "messagedialog.h" + +#include +#include + +#include + +#include +#include +#include +#include + + +std::vector ModInfo::s_Collection; +std::map ModInfo::s_ModsByName; +std::map ModInfo::s_ModsByModID; +int ModInfo::s_NextID; +QMutex ModInfo::s_Mutex(QMutex::Recursive); + +QString ModInfo::s_HiddenExt(".mohidden"); + + +static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS) +{ + return QString::compare(LHS->name(), RHS->name(), Qt::CaseInsensitive) < 0; +} + +ModInfo::NexusFileInfo::NexusFileInfo(const QString &data) +{ + QVariantList result = QtJson::Json::parse(data).toList(); + + while (result.length() < 7) { + result.append(QVariant()); + } + id = result.at(0).toInt(); + name = result.at(1).toString(); + url = result.at(2).toString(); + version = result.at(3).toString(); + description = result.at(4).toString(); + category = result.at(5).toInt(); + size = result.at(6).toInt(); +} + + +QString ModInfo::NexusFileInfo::toString() const +{ + return QString("[ %1,\"%2\",\"%3\",\"%4\",\"%5\",%6,%7 ]") + .arg(id) + .arg(name) + .arg(url) + .arg(version) + .arg(description.mid(0).replace("\"", "'")) + .arg(category) + .arg(size); +} + + +ModInfo::Ptr ModInfo::createFrom(const QDir &dir, DirectoryEntry **directoryStructure) +{ + QMutexLocker locker(&s_Mutex); +// int id = s_NextID++; + static QRegExp backupExp(".*backup[0-9]*"); + ModInfo::Ptr result; + if (backupExp.exactMatch(dir.dirName())) { + result = ModInfo::Ptr(new ModInfoBackup(dir, directoryStructure)); + } else { + result = ModInfo::Ptr(new ModInfoRegular(dir, directoryStructure)); + } + s_Collection.push_back(result); + return result; +} + + +void ModInfo::createFromOverwrite() +{ + QMutexLocker locker(&s_Mutex); + + s_Collection.push_back(ModInfo::Ptr(new ModInfoOverwrite)); +} + + +unsigned int ModInfo::getNumMods() +{ + QMutexLocker locker(&s_Mutex); + return s_Collection.size(); +} + + +ModInfo::Ptr ModInfo::getByIndex(unsigned int index) +{ + QMutexLocker locker(&s_Mutex); + + if (index >= s_Collection.size()) { + throw MyException(tr("invalid index %1").arg(index)); + } + return s_Collection[index]; +} + + +ModInfo::Ptr ModInfo::getByModID(int modID, bool missingAcceptable) +{ + QMutexLocker locker(&s_Mutex); + + std::map::iterator iter = s_ModsByModID.find(modID); + if (iter == s_ModsByModID.end()) { + if (missingAcceptable) { + return ModInfo::Ptr(); + } else { + throw MyException(tr("invalid mod id %1").arg(modID)); + } + } + + return getByIndex(iter->second); +} + + +bool ModInfo::removeMod(unsigned int index) +{ + QMutexLocker locker(&s_Mutex); + + if (index >= s_Collection.size()) { + throw MyException(tr("invalid index %1").arg(index)); + } + + // update the indices first + ModInfo::Ptr modInfo = s_Collection[index]; + s_ModsByName.erase(s_ModsByName.find(modInfo->name())); + + //TODO this is a bit more complicated since multiple mods may have the + // same mod id but only one appears in the index + std::map::iterator iter = s_ModsByModID.find(modInfo->getNexusID()) ; + if ((iter != s_ModsByModID.end()) && + (iter->second == index)) { + s_ModsByModID.erase(iter); + } + + // 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) +{ + 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(QDir(modIter.next()), directoryStructure); + } + + createFromOverwrite(); + + std::sort(s_Collection.begin(), s_Collection.end(), ByName); + + updateIndices(); +} + + +void ModInfo::updateIndices() +{ + s_ModsByName.clear(); + s_ModsByModID.clear(); + QRegExp backupRegEx(".*backup[0-9]*$"); + + for (unsigned int i = 0; i < s_Collection.size(); ++i) { + QString modName = s_Collection[i]->name(); + int modID = s_Collection[i]->getNexusID(); + s_ModsByName[modName] = i; + + // don't overwrite a modid-entry with a backup entry. This is a bit of a workaround + if ((s_ModsByModID.find(modID) == s_ModsByModID.end()) || + !backupRegEx.exactMatch(modName)) { + s_ModsByModID[modID] = i; + } + } +} + + +ModInfo::ModInfo() + : m_Valid(false), m_PrimaryCategory(-1) +{ +} + + +void ModInfo::checkChunkForUpdate(const std::vector &modIDs, QObject *receiver) +{ + if (modIDs.size() != 0) { + NexusInterface::instance()->requestUpdates(modIDs, receiver, QVariant()); + } +} + + +int ModInfo::checkAllForUpdate(QObject *receiver) +{ + int result = 0; + std::vector modIDs; + + modIDs.push_back(GameInfo::instance().getNexusModID()); + + for (std::vector::iterator iter = s_Collection.begin(); + iter != s_Collection.end(); ++iter) { + if ((*iter)->canBeUpdated()) { + modIDs.push_back((*iter)->getNexusID()); + if (modIDs.size() >= 255) { + checkChunkForUpdate(modIDs, receiver); + modIDs.clear(); + } + } + } + + checkChunkForUpdate(modIDs, receiver); + + return result; +} + +void ModInfo::setVersion(const VersionInfo &version) +{ + m_Version = version; +} + + +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; +} + + +void ModInfo::testValid() +{ + m_Valid = false; + QDirIterator dirIter(absolutePath()); + while (dirIter.hasNext()) { + dirIter.next(); + if (dirIter.fileInfo().isDir()) { + if (InstallationTester::isTopLevelDirectory(dirIter.fileName())) { + m_Valid = true; + break; + } + } else { + if (InstallationTester::isTopLevelSuffix(dirIter.fileName())) { + m_Valid = true; + break; + } + } + } + + // NOTE: in Qt 4.7 it seems that QDirIterator leaves a file handle open if it is not iterated to the + // end + while (dirIter.hasNext()) { + dirIter.next(); + } +} + + +ModInfoRegular::ModInfoRegular(const QDir &path, DirectoryEntry **directoryStructure) + : ModInfo(), m_Name(path.dirName()), m_Path(path.absolutePath()), m_MetaInfoChanged(false), + m_EndorsedState(ENDORSED_UNKNOWN), m_DirectoryStructure(directoryStructure) +{ + testValid(); + + // read out the meta-file for information + QString metaFileName = path.absoluteFilePath("meta.ini"); + QSettings metaFile(metaFileName, QSettings::IniFormat); + + m_Notes = metaFile.value("notes", "").toString(); + m_NexusID = metaFile.value("modid", -1).toInt(); + m_Version.parse(metaFile.value("version", "").toString()); + m_NewestVersion = metaFile.value("newestVersion", "").toString(); + m_InstallationFile = metaFile.value("installationFile", "").toString(); + m_NexusDescription = metaFile.value("nexusDescription", "").toString(); + m_LastNexusQuery = QDateTime::fromString(metaFile.value("lastNexusQuery", "").toString(), Qt::ISODate); + if (metaFile.contains("endorsed")) { + m_EndorsedState = metaFile.value("endorsed", false).toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; + } + + int numNexusFiles = metaFile.beginReadArray("nexusFiles"); + for (int i = 0; i < numNexusFiles; ++i) { + metaFile.setArrayIndex(i); + QString infoString = metaFile.value("info", "").toString(); + NexusFileInfo info(infoString); + m_NexusFileInfos.push_back(info); + } + metaFile.endArray(); + + QString categoriesString = metaFile.value("category", "").toString(); + + QStringList categories = categoriesString.split(',', QString::SkipEmptyParts); + for (QStringList::iterator iter = categories.begin(); iter != categories.end(); ++iter) { + bool ok = false; + int categoryID = iter->toInt(&ok); + if (categoryID < 0) { + // ignore invalid id + continue; + } + if (ok && (categoryID != 0) && (CategoryFactory::instance().categoryExists(categoryID))) { + m_Categories.insert(categoryID); + if (iter == categories.begin()) { + m_PrimaryCategory = categoryID; + } + } + } + + connect(&m_NexusBridge, SIGNAL(nxmDescriptionAvailable(int,QVariant,QVariant)), this, SLOT(nxmDescriptionAvailable(int,QVariant,QVariant))); + connect(&m_NexusBridge, SIGNAL(nxmFilesAvailable(int,QVariant,QVariant)), this, SLOT(nxmFilesAvailable(int,QVariant,QVariant))); + connect(&m_NexusBridge, SIGNAL(nxmEndorsementToggled(int,QVariant,QVariant)), this, SLOT(nxmEndorsementToggled(int,QVariant,QVariant))); + connect(&m_NexusBridge, SIGNAL(nxmRequestFailed(int,QVariant,QString)), this, SLOT(nxmRequestFailed(int,QVariant,QString))); +} + + +ModInfoRegular::~ModInfoRegular() +{ + try { + //TODO this may cause the meta-file and the directory to be + // re-created after a remove + saveMeta(); + } catch (const std::exception &e) { + qCritical("failed to save meta information for \"%s\": %s", + m_Name.toUtf8().constData(), e.what()); + } +} + + +void ModInfoRegular::saveMeta() +{ + if (m_MetaInfoChanged) { + if (QFile::exists(absolutePath().append("/meta.ini"))) { + QSettings metaFile(absolutePath().append("/meta.ini"), QSettings::IniFormat); + if (metaFile.status() == QSettings::NoError) { + std::set temp = m_Categories; + temp.erase(m_PrimaryCategory); + metaFile.setValue("category", QString("%1").arg(m_PrimaryCategory) + "," + SetJoin(temp, ",")); + metaFile.setValue("newestVersion", m_NewestVersion.canonicalString()); + metaFile.setValue("version", m_Version.canonicalString()); + metaFile.setValue("modid", m_NexusID); + metaFile.setValue("notes", m_Notes); + metaFile.setValue("nexusDescription", m_NexusDescription); + metaFile.setValue("lastNexusQuery", m_LastNexusQuery.toString(Qt::ISODate)); + if (m_EndorsedState != ENDORSED_UNKNOWN) { + metaFile.setValue("endorsed", m_EndorsedState == ENDORSED_TRUE ? true : false); + } + + metaFile.beginWriteArray("nexusFiles"); + for (int i = 0; i < m_NexusFileInfos.size(); ++i) { + metaFile.setArrayIndex(i); + metaFile.setValue("info", m_NexusFileInfos.at(i).toString()); + } + metaFile.endArray(); + } else { + reportError(tr("failed to write %1/meta.ini: %2").arg(absolutePath()).arg(metaFile.status())); + } + } else { + qWarning("mod %s has no meta.ini at %s/meta.ini", m_Name.toUtf8().constData(), absolutePath().toUtf8().constData()); + } + m_MetaInfoChanged = false; + } +} + + +bool ModInfoRegular::updateAvailable() const +{ + return m_NewestVersion.isValid() && (m_Version < m_NewestVersion); +} + + +void ModInfoRegular::nxmDescriptionAvailable(int, QVariant, QVariant resultData) +{ + QVariantMap result = resultData.toMap(); + m_NewestVersion.parse(result["version"].toString()); + m_NexusDescription = result["description"].toString(); + m_EndorsedState = result["voted_by_user"].toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; + m_NexusBridge.requestFiles(m_NexusID, QVariant()); +} + + +void ModInfoRegular::nxmFilesAvailable(int, QVariant, QVariant resultData) +{ + m_NexusFileInfos.clear(); + + QVariantList result = resultData.toList(); + + foreach(QVariant file, result) { + QVariantMap fileInfo = file.toMap(); + + m_NexusFileInfos.push_back(NexusFileInfo(fileInfo["id"].toInt(), + fileInfo["name"].toString(), + fileInfo["uri"].toString(), + fileInfo["version"].toString(), + fileInfo["description"].toString(), + fileInfo["category_id"].toInt(), + fileInfo["size"].toInt())); + } + + m_LastNexusQuery = QDateTime::currentDateTime(); + m_MetaInfoChanged = true; + saveMeta(); + emit modDetailsUpdated(true); +} + + +void ModInfoRegular::nxmEndorsementToggled(int, QVariant, QVariant resultData) +{ + m_EndorsedState = resultData.toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; + m_MetaInfoChanged = true; + saveMeta(); + emit modDetailsUpdated(true); +} + + +void ModInfoRegular::nxmRequestFailed(int, QVariant userData, const QString &errorMessage) +{ + QString fullMessage = errorMessage; + if (userData.canConvert() && (userData.toInt() == 1)) { + fullMessage += "\nNexus will reject endorsements within 15 Minutes of a failed attempt, the error message may be misleading."; + } + if (QApplication::activeWindow() != NULL) { + MessageDialog::showMessage(fullMessage, QApplication::activeWindow()); + } + emit modDetailsUpdated(false); +} + + +bool ModInfoRegular::updateNXMInfo() +{ + if (m_NexusID > 0) { + m_NexusBridge.requestDescription(m_NexusID, QVariant()); + return true; + } + return false; +} + + +void ModInfoRegular::setCategory(int categoryID, bool active) +{ + m_MetaInfoChanged = true; + + if (active) { + m_Categories.insert(categoryID); + if (m_PrimaryCategory == -1) { + m_PrimaryCategory = categoryID; + } + } else { + std::set::iterator iter = m_Categories.find(categoryID); + if (iter != m_Categories.end()) { + m_Categories.erase(iter); + } + if (categoryID == m_PrimaryCategory) { + if (m_Categories.size() == 0) { + m_PrimaryCategory = -1; + } else { + m_PrimaryCategory = *(m_Categories.begin()); + } + } + } +} + + +bool ModInfoRegular::setName(const QString &name) +{ + if (name.contains('/') || name.contains('\\')) { + return false; + } + + QString newPath = m_Path.mid(0).replace(m_Path.length() - m_Name.length(), m_Name.length(), name); + QDir modDir(m_Path.mid(0, m_Path.length() - m_Name.length())); + if (m_Name.compare(name, Qt::CaseInsensitive) == 0) { + QString tempName = name; + tempName.append("_temp"); + while (modDir.exists(tempName)) { + tempName.append("_"); + } + if (!modDir.rename(m_Name, tempName)) { + return false; + } + if (!modDir.rename(tempName, name)) { + qCritical("rename to final name failed after successful rename to intermediate name"); + modDir.rename(tempName, m_Name); + return false; + } + } else { + if (!modDir.rename(m_Name, name)) { + return false; + } + } + + std::map::iterator nameIter = s_ModsByName.find(m_Name); + unsigned int index = nameIter->second; + s_ModsByName.erase(nameIter); + + m_Name = name; + m_Path = newPath; + + s_ModsByName[m_Name] = index; + + std::sort(s_Collection.begin(), s_Collection.end(), ByName); + + updateIndices(); + + return true; +} + +void ModInfoRegular::setNotes(const QString ¬es) +{ + m_Notes = notes; +} + +void ModInfoRegular::setVersion(const VersionInfo &version) +{ + m_Version = version; + m_MetaInfoChanged = true; +} + +void ModInfoRegular::setNexusDescription(const QString &description) +{ + m_NexusDescription = description; + m_MetaInfoChanged = true; +} + + +void ModInfoRegular::setIsEndorsed(bool endorsed) +{ + m_EndorsedState = endorsed ? ENDORSED_TRUE : ENDORSED_FALSE; + m_MetaInfoChanged = true; +} + + +bool ModInfoRegular::remove() +{ + m_MetaInfoChanged = false; + return removeDir(absolutePath()); +} + +void ModInfoRegular::endorse(bool doEndorse) +{ + if (doEndorse != (m_EndorsedState == ENDORSED_TRUE)) { + m_NexusBridge.requestToggleEndorsement(getNexusID(), doEndorse, QVariant(1)); + } +} + + +QString ModInfoRegular::absolutePath() const +{ + return m_Path; +} + + +std::vector ModInfoRegular::getFlags() const +{ + std::vector result; + if (!isValid()) { + result.push_back(ModInfo::FLAG_INVALID); + } + if ((m_NexusID != -1) && (endorsedState() == ENDORSED_FALSE)) { + result.push_back(ModInfo::FLAG_NOTENDORSED); + } + switch (isConflicted()) { + case CONFLICT_MIXED: { + result.push_back(ModInfo::FLAG_CONFLICT_MIXED); + } break; + case CONFLICT_OVERWRITE: { + result.push_back(ModInfo::FLAG_CONFLICT_OVERWRITE); + } break; + case CONFLICT_OVERWRITTEN: { + result.push_back(ModInfo::FLAG_CONFLICT_OVERWRITTEN); + } break; + case CONFLICT_REDUNDANT: { + result.push_back(ModInfo::FLAG_CONFLICT_REDUNDANT); + } break; + default: { /* NOP */ } + } + if (m_Notes.length() != 0) { + result.push_back(ModInfo::FLAG_NOTES); + } + return result; +} + + +int ModInfoRegular::getHighlight() const +{ + return isValid() ? HIGHLIGHT_NONE: HIGHLIGHT_INVALID; +} + + +QString ModInfoRegular::getDescription() const +{ + if (!isValid()) { + return tr("%1 contains no esp/esm and no asset (textures, meshes, interface, ...) directory").arg(name()); + } else { + const std::set &categories = getCategories(); + std::wostringstream categoryString; + categoryString << ToWString(tr("Categories:
")); + CategoryFactory &categoryFactory = CategoryFactory::instance(); + for (std::set::const_iterator catIter = categories.begin(); + catIter != categories.end(); ++catIter) { + if (catIter != categories.begin()) { + categoryString << " , "; + } + categoryString << "" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << ""; + } + + return ToQString(categoryString.str()); + } +} + +QString ModInfoRegular::notes() const +{ + return m_Notes; +} + +void ModInfoRegular::getNexusFiles(QList::const_iterator &begin, QList::const_iterator &end) +{ + begin = m_NexusFileInfos.begin(); + end = m_NexusFileInfos.end(); +} + +QString ModInfoRegular::getNexusDescription() const +{ + return m_NexusDescription; +} + + +ModInfoRegular::EEndorsedState ModInfoRegular::endorsedState() const +{ + return m_EndorsedState; +} + + +ModInfoRegular::EConflictType ModInfoRegular::isConflicted() const +{ + bool overwrite = false; + bool overwritten = false; + bool regular = false; + + int dataID = 0; + if ((*m_DirectoryStructure)->originExists(L"data")) { + dataID = (*m_DirectoryStructure)->getOriginByName(L"data").getID(); + } + + std::wstring name = ToWString(m_Name); + if ((*m_DirectoryStructure)->originExists(name)) { + FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); + std::vector files = origin.getFiles(); + for (auto iter = files.begin(); iter != files.end() && (!overwrite || !overwritten || !regular); ++iter) { + const std::vector &alternatives = (*iter)->getAlternatives(); + if (alternatives.size() == 0) { + // no alternatives -> no conflict + regular = true; + } else { + for (auto altIter = alternatives.begin(); altIter != alternatives.end(); ++altIter) { + // don't treat files overwritten in data as "conflict" + if (*altIter != dataID) { + bool ignore = false; + if ((*iter)->getOrigin(ignore) == origin.getID()) { + overwrite = true; + break; + } else { + overwritten = true; + break; + } + } + } + } + } + } + + if (overwrite && overwritten) return CONFLICT_MIXED; + else if (overwrite) return CONFLICT_OVERWRITE; + else if (overwritten) { + if (!regular) { + return CONFLICT_REDUNDANT; + } else { + return CONFLICT_OVERWRITTEN; + } + } + else return CONFLICT_NONE; +} + + +bool ModInfoRegular::isRedundant() const +{ + std::wstring name = ToWString(m_Name); + if ((*m_DirectoryStructure)->originExists(name)) { + FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); + std::vector files = origin.getFiles(); + bool ignore = false; + for (auto iter = files.begin(); iter != files.end(); ++iter) { + if ((*iter)->getOrigin(ignore) == origin.getID()) { + return false; + } + } + return true; + } else { + return false; + } +} + + +QDateTime ModInfoRegular::getLastNexusQuery() const +{ + return m_LastNexusQuery; +} + +std::vector ModInfoRegular::getIniTweaks() const +{ + QString metaFileName = absolutePath().append("/meta.ini"); + QSettings metaFile(metaFileName, QSettings::IniFormat); + + std::vector result; + + int numTweaks = metaFile.beginReadArray("INI Tweaks"); + + if (numTweaks != 0) { + qDebug("%d active ini tweaks in %s", + numTweaks, metaFileName.toUtf8().constData()); + } + + for (int i = 0; i < numTweaks; ++i) { + metaFile.setArrayIndex(i); + QString filename = absolutePath().append("/INI Tweaks/").append(metaFile.value("name").toString()); + result.push_back(filename); + } + metaFile.endArray(); + return result; +} + +std::vector ModInfoBackup::getFlags() const +{ + std::vector result = ModInfoRegular::getFlags(); + result.insert(result.begin(), ModInfo::FLAG_BACKUP); + return result; +} + + +QString ModInfoBackup::getDescription() const +{ + return tr("This is the backup of a mod"); +} + + +ModInfoBackup::ModInfoBackup(const QDir &path, DirectoryEntry **directoryStructure) + : ModInfoRegular(path, directoryStructure) +{ +} + + +ModInfoOverwrite::ModInfoOverwrite() +{ + testValid(); + +} + + +QString ModInfoOverwrite::absolutePath() const +{ + return QDir::fromNativeSeparators(ToQString(GameInfo::instance().getOverwriteDir())); +} + +std::vector ModInfoOverwrite::getFlags() const +{ + std::vector result; + result.push_back(FLAG_OVERWRITE); + return result; +} + +int ModInfoOverwrite::getHighlight() const +{ + return (isValid() ? HIGHLIGHT_IMPORTANT : HIGHLIGHT_INVALID) | HIGHLIGHT_CENTER; +} + + +QString ModInfoOverwrite::getDescription() const +{ + return tr("This pseudo mod contains files from the virtual data tree that got " + "modified (i.e. by the construction kit)"); +} -- cgit v1.3.1