/* * Copyright (C) 2013 Sebastian Herbord. All rights reserved. * * This file is part of the basic diagnosis plugin for Mod Organizer * * This plugin 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. * * This plugin 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 this plugin. If not, see . */ #include "diagnosebasic.h" #include "filenamestring.h" #include "iplugingame.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #pragma warning( push, 2 ) #include #include #include #include #include #pragma warning( pop ) using namespace MOBase; DiagnoseBasic::DiagnoseBasic() : m_MOInfo(nullptr) { } bool DiagnoseBasic::init(IOrganizer *moInfo) { m_MOInfo = moInfo; m_MOInfo->modList()->onModStateChanged([&] (const QString &modName, IModList::ModStates) { if (modName == "Overwrite") invalidate(); }); m_MOInfo->modList()->onModMoved([&] (const QString&, int, int) { // invalidates only the assetOrder check but there is currently no way to recheck individual // checks invalidate(); }); m_MOInfo->pluginList()->onPluginMoved([&] (const QString&, int, int) { invalidate(); }); m_MOInfo->pluginList()->onRefreshed([&] () { invalidate(); }); m_MOInfo->pluginList()->onPluginStateChanged([&] (const QString &, IPluginList::PluginStates) { invalidate(); }); return true; } QString DiagnoseBasic::name() const { return "Basic diagnosis plugin"; } QString DiagnoseBasic::author() const { return "Tannin"; } QString DiagnoseBasic::description() const { return tr("Checks for problems unrelated to other plugins"); } VersionInfo DiagnoseBasic::version() const { return VersionInfo(1, 1, 2, VersionInfo::RELEASE_FINAL); } bool DiagnoseBasic::isActive() const { return true; } QList DiagnoseBasic::settings() const { return QList() << PluginSetting("check_errorlog", tr("Warn when an error occured last time an application was run"), true) << PluginSetting("check_overwrite", tr("Warn when there are files in the overwrite directory"), true) << PluginSetting("check_font", tr("Warn when the font configuration refers to files that aren't installed"), true) << PluginSetting("check_conflict", tr("Warn when mods are installed that conflict with MO functionality"), true) << PluginSetting("check_missingmasters", tr("Warn when there are esps with missing masters"), true) << PluginSetting("check_alternategames", tr("Warn when an installed mod came from an alternative game source"), false) << PluginSetting("ow_ignore_empty", tr("Ignore empty directories when checking overwrite directory"), false) << PluginSetting("ow_ignore_log", tr("Ignore .log files and empty directories when checking overwrite directory"), false) ; } bool DiagnoseBasic::errorReported() const { QDir dir(qApp->property("dataPath").toString() + "/logs"); QFileInfoList files = dir.entryInfoList(QStringList("ModOrganizer_??_??_??_??_??.log"), QDir::Files, QDir::Name | QDir::Reversed); if (files.count() > 0) { QString logFile = files.at(0).absoluteFilePath(); QFile file(logFile); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { char buffer[1024]; int line = 0; qint64 lineLengths[NUM_CONTEXT_ROWS]; for (int i = 0; i < NUM_CONTEXT_ROWS; ++i) { lineLengths[i] = 0; } while (!file.atEnd()) { lineLengths[line % NUM_CONTEXT_ROWS] = file.readLine(buffer, 1024) + 1; if (strncmp(buffer, "ERROR", 5) == 0) { qint64 sumChars = 0; for (int i = 0; i < NUM_CONTEXT_ROWS; ++i) { sumChars += lineLengths[i]; } file.seek(file.pos() - sumChars); m_ErrorMessage = ""; for (int i = 0; i < 2 * NUM_CONTEXT_ROWS; ++i) { file.readLine(buffer, 1024); QString lineString = QString::fromUtf8(buffer); if (lineString.startsWith("ERROR")) { m_ErrorMessage += "" + lineString + ""; } else { m_ErrorMessage += lineString; } } return true; } // prevent this function from taking forever if (line++ >= 50000) { break; } } } } return false; } bool DiagnoseBasic::checkEmpty(QString const &path) const { QDir dir(path); dir.setFilter(QDir::Files | QDir::Hidden | QDir::System); //Search files first for (QString const &f : dir.entryList()) { FileNameString file(f); if (! m_MOInfo->pluginSetting(name(), "ow_ignore_log").toBool() || ! file.endsWith(".log")) { return false; } } //Then directories dir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); for (QFileInfo const &subdir : dir.entryInfoList()) { if (!checkEmpty(subdir.absoluteFilePath())) { return false; } } return true; } bool DiagnoseBasic::overwriteFiles() const { //QString dirname(qApp->property("dataPath").toString() + "/overwrite"); QString dirname(m_MOInfo->overwritePath()); if (m_MOInfo->pluginSetting(name(), "ow_ignore_empty").toBool() || m_MOInfo->pluginSetting(name(), "ow_ignore_log").toBool()) { return !checkEmpty(dirname); } QDir dir(dirname); return dir.count() != 2; // account for . and .. } bool DiagnoseBasic::nitpickInstalled() const { QString path = m_MOInfo->resolvePath("skse/plugins/nitpick.dll"); return !path.isEmpty(); } /// unused code to remove duplicates from a vector template void makeUnique(std::vector &vector) { std::set done; auto read = vector.begin(); auto write = vector.begin(); for (; read != vector.end(); ++read) { if (done.insert(*read).second) { *write = *read; ++write; } } vector.erase(write, vector.end()); } bool DiagnoseBasic::missingMasters() const { std::set enabledPlugins; QStringList esps = m_MOInfo->findFiles("", [] (const QString &fileName) -> bool { return fileName.endsWith(".esp", Qt::CaseInsensitive) || fileName.endsWith(".esm", Qt::CaseInsensitive) || fileName.endsWith(".esl", Qt::CaseInsensitive); }); // gather enabled masters first for (const QString &esp : esps) { QString baseName = QFileInfo(esp).fileName(); if (m_MOInfo->pluginList()->state(baseName) == IPluginList::STATE_ACTIVE) { enabledPlugins.insert(baseName.toLower()); } } m_MissingMasters.clear(); // for each required master in each esp, test if it's in the list of enabled masters. for (const QString &esp : esps) { QString baseName = QFileInfo(esp).fileName(); if (m_MOInfo->pluginList()->state(baseName) == IPluginList::STATE_ACTIVE) { for (const QString master : m_MOInfo->pluginList()->masters(baseName)) { if (enabledPlugins.find(master.toLower()) == enabledPlugins.end()) { m_MissingMasters.insert(master); } } } } return !m_MissingMasters.empty(); } bool DiagnoseBasic::alternateGame() const { QStringList mods = m_MOInfo->modList()->allMods(); for (QString mod : mods) { if (m_MOInfo->modList()->state(mod) & MOBase::IModList::STATE_ALTERNATE && m_MOInfo->modList()->state(mod) & MOBase::IModList::STATE_ACTIVE) return true; } return false; } bool DiagnoseBasic::invalidFontConfig() const { if ((m_MOInfo->managedGame()->gameName() != "Skyrim") && (m_MOInfo->managedGame()->gameName() != "SkyrimSE")) { // this check is only for skyrim return false; } // files from skyrim_interface.bsa static std::vector defaultFonts = boost::assign::list_of("interface\\fonts_console.swf") ("interface\\fonts_en.swf"); QString configPath = m_MOInfo->resolvePath("interface/fontconfig.txt"); if (configPath.isEmpty()) { return false; } QFile config(configPath); if (!config.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug("failed to open %s", qPrintable(configPath)); return false; } std::regex exp("^fontlib \"([^\"]*)\"$"); while (!config.atEnd()) { QByteArray row = config.readLine(); std::cmatch match; if (std::regex_search(row.constData(), match, exp)) { std::string temp = match[1]; QString path(temp.c_str()); bool isDefault = false; for (const QString &def : defaultFonts) { if (QString::compare(def, path, Qt::CaseInsensitive) == 0) { isDefault = true; break; } } if (!isDefault && m_MOInfo->resolvePath(path).isEmpty()) { return true; } } } return false; } std::vector DiagnoseBasic::activeProblems() const { std::vector result; if (m_MOInfo->pluginSetting(name(), "check_errorlog").toBool() && errorReported()) { result.push_back(PROBLEM_ERRORLOG); } if (m_MOInfo->pluginSetting(name(), "check_overwrite").toBool() && overwriteFiles()) { result.push_back(PROBLEM_OVERWRITE); } if (m_MOInfo->pluginSetting(name(), "check_font").toBool() && invalidFontConfig()) { result.push_back(PROBLEM_INVALIDFONT); } if (m_MOInfo->pluginSetting(name(), "check_conflict").toBool() && nitpickInstalled()) { result.push_back(PROBLEM_NITPICKINSTALLED); } if (m_MOInfo->pluginSetting(name(), "check_missingmasters").toBool() && missingMasters()) { result.push_back(PROBLEM_MISSINGMASTERS); } if (m_MOInfo->pluginSetting(name(), "check_alternategames").toBool() && alternateGame()) { result.push_back(PROBLEM_ALTERNATE); } if (QFile::exists(m_MOInfo->profilePath() + "/profile_tweaks.ini")) { result.push_back(PROBLEM_PROFILETWEAKS); } return result; } QString DiagnoseBasic::shortDescription(unsigned int key) const { switch (key) { case PROBLEM_ERRORLOG: return tr("There was an error reported recently"); case PROBLEM_OVERWRITE: return tr("There are files in your overwrite mod"); case PROBLEM_INVALIDFONT: return tr("Your font configuration may be broken"); case PROBLEM_NITPICKINSTALLED: return tr("Nitpick installed"); case PROBLEM_PROFILETWEAKS: return tr("Ini Tweaks overwritten"); case PROBLEM_MISSINGMASTERS: return tr("Missing Masters"); case PROBLEM_ALTERNATE: return tr("At least one unverified mod is using an alternative game source"); default: throw MyException(tr("invalid problem key %1").arg(key)); } } QString DiagnoseBasic::fullDescription(unsigned int key) const { switch (key) { case PROBLEM_ERRORLOG: return "" + m_ErrorMessage.replace("\n", "
") + "
"; case PROBLEM_OVERWRITE: return tr("Files in the Overwrite mod are are usually files created by an external tool (i.e. Wrye Bash, Automatic Variants, ...).
" "It is advisable you empty the Overwrite directory by moving those files to an existing mod. You can do this by double-clicking the Overwrite mod and use drag&drop to move the files to a mod.
" "Alternatively, right-click on Overwrite and create a new regular mod from the files there.
" "
" "Why is this necessary? Generated files may depend on the other mods active in a profile and may thus be incompatible with a different profile (i.e. bashed patches from Wrye Bash). " "On the other hand the file may be necessary in all profiles (i.e. dlc esms after cleaning with TESVEdit)
" "This can NOT be automated you HAVE to read up on the tools you use and make an educated decision."); case PROBLEM_INVALIDFONT: return tr("Your current configuration seems to reference a font that is not installed. You may see only boxes instead of letters.
" "The font configuration is in Data\\interface\\fontconfig.txt. Most likely you have a broken installation of a font replacer mod."); case PROBLEM_NITPICKINSTALLED: return tr("You have the nitpick skse plugin installed. This plugin is not needed with Mod Organizer because MO already offers the same functionality. " "Worse: The two solutions may conflict so it's strongly suggested you remove this plugin."); case PROBLEM_PROFILETWEAKS: { QString fileContent = readFileText(m_MOInfo->profilePath() + "/profile_tweaks.ini"); return tr("Settings provided in ini tweaks have been overwritten in-game or in an applications.
" "These overwrites are stored in a separate file (profile_tweaks.ini within the profile directory)
" "to keep ini-tweaks in their original state but you should really get rid of this file as there is
" "no tool support in MO to work on it.
" "Advice: Copy settings you want to keep to an appropriate ini tweak, then delete profile_tweaks.ini.
" "Hitting the Fix button will delete that file") + "
profile_tweaks.ini:
" + fileContent + "
"; } break; case PROBLEM_MISSINGMASTERS: { return tr("The masters for some plugins (esp/esl/esm) are not enabled.
" "The game will crash unless you install and enable the following plugins: ") + "
  • " + SetJoin(m_MissingMasters, "
  • ") + "
"; } break; case PROBLEM_ALTERNATE: { return tr("You have at least one active mod installed from an alternative game source.
" "This means that the mod was downloaded from a game source which does not match
" "the expected primary game.

" "Depending on the type of mod, this may require converting various files to run correctly.

" "Advice: Once you have verified the mod is working correctly, you can use the context menu
" "and select \"Mark as converted/working\" to remove the flag and warning."); } break; default: throw MyException(tr("invalid problem key %1").arg(key)); } } bool DiagnoseBasic::hasGuidedFix(unsigned int key) const { return (key == PROBLEM_PROFILETWEAKS); } void DiagnoseBasic::startGuidedFix(unsigned int key) const { switch (key) { case PROBLEM_PROFILETWEAKS: { shellDeleteQuiet(m_MOInfo->profilePath() + "/profile_tweaks.ini"); } break; default: throw MyException(tr("invalid problem key %1").arg(key)); } }