From 7ee008e150bc5bcf76082d726f719ee0fdfda982 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 11 Feb 2026 02:37:39 -0600 Subject: Fluorine Manager: full Linux port of Mod Organizer 2 Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 --- libs/diagnose_basic/src/CMakeLists.txt | 13 + libs/diagnose_basic/src/diagnose_basic_en.ts | 178 ++++++ libs/diagnose_basic/src/diagnosebasic.cpp | 803 +++++++++++++++++++++++++++ libs/diagnose_basic/src/diagnosebasic.h | 142 +++++ 4 files changed, 1136 insertions(+) create mode 100644 libs/diagnose_basic/src/CMakeLists.txt create mode 100644 libs/diagnose_basic/src/diagnose_basic_en.ts create mode 100644 libs/diagnose_basic/src/diagnosebasic.cpp create mode 100644 libs/diagnose_basic/src/diagnosebasic.h (limited to 'libs/diagnose_basic/src') diff --git a/libs/diagnose_basic/src/CMakeLists.txt b/libs/diagnose_basic/src/CMakeLists.txt new file mode 100644 index 0000000..86951b8 --- /dev/null +++ b/libs/diagnose_basic/src/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) + +file(GLOB diagnose_basic_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h + ${CMAKE_CURRENT_SOURCE_DIR}/*.ui + ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc +) + +add_library(diagnose_basic SHARED ${diagnose_basic_SOURCES}) +mo2_configure_plugin(diagnose_basic NO_SOURCES WARNINGS OFF) +target_link_libraries(diagnose_basic PRIVATE mo2::uibase) +mo2_install_plugin(diagnose_basic) diff --git a/libs/diagnose_basic/src/diagnose_basic_en.ts b/libs/diagnose_basic/src/diagnose_basic_en.ts new file mode 100644 index 0000000..cb5fb2f --- /dev/null +++ b/libs/diagnose_basic/src/diagnose_basic_en.ts @@ -0,0 +1,178 @@ + + + + + DiagnoseBasic + + + Checks for problems unrelated to other plugins + + + + + Warn when an error occurred last time an application was run + + + + + Warn when there are files in the overwrite directory + + + + + Warn when the font configuration refers to files that aren't installed + + + + + Warn when mods are installed that conflict with MO functionality + + + + + Warn when there are esps with missing masters + + + + + Warn when an installed mod came from an alternative game source + + + + + Warn when files have unwanted attributes + + + + + Ignore empty directories when checking overwrite directory + + + + + Ignore .log files and empty directories when checking overwrite directory + + + + + File attribute checker +Searching for problems... + + + + + Invalid file attributes found + + + + + One or more of your files has attributes that may prevent the game from reading the files. This can result in missing plugins, missing textures, and other such problems. + +Fix the file attributes? + + + + + File attribute checker +Fixing file attributes... + + + + + Unable to set file attributes + + + + + Mod Organizer was unable to fix the file attributes of at least one file. + +Continue launching %1? + + + + + There was an error reported recently + + + + + There are files in your Overwrite mod directory + There are files in your overwrite mod + + + + + Your font configuration may be broken + + + + + Nitpick installed + + + + + INI Tweaks overwritten + Ini Tweaks overwritten + + + + + Missing Masters + + + + + At least one unverified mod is using an alternative game source + + + + + + + invalid problem key %1 + + + + + There are currently files in your <span style="color: red;"><i>Overwrite</i></span> directory. These files are typically newly created files, usually generated by an external mod tool (i.e. Wrye Bash, xEdit, FNIS, ...). Creation Club ESL files will also end up here when downloaded. Any files in <span style="font-weight: bold;">Overwrite</span> will take top priority when loading your mod files and will always overwrite any other mod in your profile.<br><br>It is recommended that you review the files in <span style="font-weight: bold;">Overwrite</span> and move any relevant files to a new or existing mod. You can do this by double-clicking the <span style="font-weight: bold;">Overwrite</span> mod and dragging files from the Overwrite window to a mod entry in the main mod list. It is also possible to move all current <span style="font-weight: bold;">Overwrite</span> files to a new mod by right-clicking on the Overwrite mod.<br><br>Not all files in <span style="font-weight: bold;">Overwrite</span> need to be removed, but there are several reasons to do so. Some generated files will be directly related to the active mods in your profile and will be incompatible with different mod setups. Since <span style="font-weight: bold;">Overwrite</span> is always active, this could cause conflicts between profiles. Additionally, moving relevant game files into a normal mod will give you greater control over those files. Some files can live safely in <span style="font-weight: bold;">Overwrite</span>, such as basic logs and cache files. It is up to you to understand how best to manage these files.<br><br>If you do not wish to see this warning and understand how to handle your <span style="font-weight: bold;">Overwrite</span> directory, you can open the Mod Organizer settings and disable this warning under the "Diagnose Basic" plugin configuration. + There are currently files in your <font color="red"><i>Overwrite</i></font> directory. These files are typically newly created files, usually generated by an external mod tool (i.e. Wrye Bash, xEdit, FNIS, ...). Creation Club ESL files will also end up here when downloaded. Any files in <font color="red"><i>Overwrite</i></font> will take top priority when loading your mod files and will always overwrite any other mod in your profile.<br><br>It is recommended that you review the files in <font color="red"><i>Overwrite</i></font> and move any relevant files to a new or existing mod. You can do this by double-clicking the <font color="red"><i>Overwrite</i></font> mod and dragging files from the <font color="red"><i>Overwrite</i></font> window to a mod entry in the main mod list. It is also possible to move all current <font color="red"><i>Overwrite</i></font> files to a new mod by right-clicking on the <font color="red"><i>Overwrite</i></font> mod.<br><br>Not all files in <font color="red"><i>Overwrite</i></font> need to be removed, but there are several reasons to do so. Some generated files will be directly related to the active mods in your profile and will be incompatible with different mod setups. Since <font color="red"><i>Overwrite</i></font> is always active, this could cause conflicts between profiles. Additionally, moving relevant game files into a normal mod will give you greater control over those files. Some files can live safely in <font color="red"><i>Overwrite</i></font>, such as basic logs and cache files. It is up to you to understand how best to manage these files.<br><br>If you do not wish to see this warning and understand how to handle your <font color="red"><i>Overwrite</i></font> directory, you can open the Mod Organizer settings and disable this warning under the "Diagnose Basic" plugin configuration. + + + + + Your current configuration seems to reference a font that is not installed. You may see only boxes instead of letters.<br>The font configuration is in Data\interface\fontconfig.txt. Most likely you have a broken installation of a font replacer mod. + + + + + 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. + + + + + Settings provided in ini tweaks have been overwritten in-game or in an applications.<br>These overwrites are stored in a separate file (<i>profile_tweaks.ini</i> within the profile directory)<br>to keep ini-tweaks in their original state but you should really get rid of this file as there is<br>no tool support in MO to work on it. <br>Advice: Copy settings you want to keep to an appropriate ini tweak, then delete <i>profile_tweaks.ini</i>.<br>Hitting the <i>Fix</i> button will delete that file + + + + + The masters for some plugins (esp/esl/esm) are not enabled.<br>The game will crash unless you install and enable the following plugins: + + + + + Master + + + + + Required By + + + + + You have at least one active mod installed from an alternative game source.<br>This means that the mod was downloaded from a game source which does not match<br>the expected primary game.<br><br>Depending on the type of mod, this may require converting various files to run correctly.<br><br>Advice: Once you have verified the mod is working correctly, you can use the context menu<br>and select "Mark as converted/working" to remove the flag and warning. + + + + diff --git a/libs/diagnose_basic/src/diagnosebasic.cpp b/libs/diagnose_basic/src/diagnosebasic.cpp new file mode 100644 index 0000000..395a049 --- /dev/null +++ b/libs/diagnose_basic/src/diagnosebasic.cpp @@ -0,0 +1,803 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace MOBase; + +const QRegularExpression DiagnoseBasic::RE_LOG_FILE(".*[.]log[0-9]*$"); +const unsigned int DiagnoseBasic::PROBLEM_ERRORLOG; +const unsigned int DiagnoseBasic::PROBLEM_OVERWRITE; +const unsigned int DiagnoseBasic::PROBLEM_INVALIDFONT; +const unsigned int DiagnoseBasic::PROBLEM_NITPICKINSTALLED; +const unsigned int DiagnoseBasic::PROBLEM_PROFILETWEAKS; +const unsigned int DiagnoseBasic::PROBLEM_MISSINGMASTERS; +const unsigned int DiagnoseBasic::PROBLEM_ALTERNATE; +const unsigned int DiagnoseBasic::NUM_CONTEXT_ROWS; + +DiagnoseBasic::DiagnoseBasic() : m_MOInfo(nullptr) {} + +bool DiagnoseBasic::init(IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + + m_MOInfo->modList()->onModStateChanged( + [&](const std::map& mods) { + if (mods.contains("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([&](auto const&) { + invalidate(); + }); + m_MOInfo->onAboutToRun([&](const QString& executable) { + return fileAttributes(executable); + }); + + 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, 3, VersionInfo::RELEASE_FINAL); +} + +bool DiagnoseBasic::isActive() const +{ + return true; +} + +QList DiagnoseBasic::settings() const +{ + return QList() + << PluginSetting( + "check_errorlog", + tr("Warn when an error occurred 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("check_fileattributes", + tr("Warn when files have unwanted attributes"), 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 (auto const& file : dir.entryList()) { + if (!m_MOInfo->pluginSetting(name(), "ow_ignore_log").toBool() || + !RE_LOG_FILE.match(file).hasMatch()) { + 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); + bool checkDirs = m_MOInfo->managedGame()->getModMappings().keys().size() > 1 || + m_MOInfo->managedGame()->getModMappings().keys().first() != ""; + if (checkDirs) { + bool empty = true; + for (auto dir : m_MOInfo->managedGame()->getModMappings().keys()) { + auto mapDir = QDir(dirname).filePath(dir); + if (QDir(mapDir).exists()) { + empty = QDir(mapDir).count() == 2; // account for . and .. + } + if (!empty) + break; + } + return !empty; + } + 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", FileNameComparator::CaseSensitivity) || + fileName.endsWith(".esm", FileNameComparator::CaseSensitivity) || + fileName.endsWith(".esl", FileNameComparator::CaseSensitivity); + }); + // 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(); + m_PluginChildren.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); + m_PluginChildren[master].insert(baseName); + } + } + } + } + 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()->gameShortName() != "SkyrimSE")) { + // this check is only for skyrim + return false; + } + + // files from skyrim_interface.bsa + static std::vector defaultFonts{"interface\\fonts_console.swf", + "interface\\fonts_en.swf", + "interface\\fonts_cclub.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", qUtf8Printable(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, FileNameComparator::CaseSensitivity) == 0) { + isDefault = true; + break; + } + } + + if (!isDefault && m_MOInfo->resolvePath(path).isEmpty()) { + return true; + } + } + } + return false; +} + +static bool checkFileAttributes(const QString& path) +{ +#ifdef _WIN32 + WCHAR w_path[32767]; + memset(w_path, 0, sizeof(w_path)); + path.toWCharArray(w_path); + + DWORD attrs = GetFileAttributes(w_path); + if (attrs != INVALID_FILE_ATTRIBUTES) { + if (!(attrs & FILE_ATTRIBUTE_ARCHIVE) && !(attrs & FILE_ATTRIBUTE_NORMAL) && + (attrs & ~(FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_ARCHIVE))) { + QString debug; + debug += QString("%1 ").arg(attrs, 8, 16, QLatin1Char('0')); + + // A C D H I O P R S U V X Z + debug += (attrs & FILE_ATTRIBUTE_DIRECTORY) ? "D" : " "; + debug += (attrs & FILE_ATTRIBUTE_ARCHIVE) ? "A" : " "; + debug += (attrs & FILE_ATTRIBUTE_READONLY) ? "R" : " "; + debug += (attrs & FILE_ATTRIBUTE_SYSTEM) ? "S" : " "; + debug += (attrs & FILE_ATTRIBUTE_HIDDEN) ? "H" : " "; + debug += (attrs & FILE_ATTRIBUTE_OFFLINE) ? "O" : " "; + debug += (attrs & FILE_ATTRIBUTE_NOT_CONTENT_INDEXED) ? "I" : " "; + debug += (attrs & FILE_ATTRIBUTE_NO_SCRUB_DATA) ? "X" : " "; + debug += (attrs & FILE_ATTRIBUTE_INTEGRITY_STREAM) ? "V" : " "; + debug += (attrs & FILE_ATTRIBUTE_PINNED) ? "P" : " "; + debug += (attrs & FILE_ATTRIBUTE_UNPINNED) ? "U" : " "; + debug += (attrs & FILE_ATTRIBUTE_COMPRESSED) ? "C" : " "; + debug += (attrs & FILE_ATTRIBUTE_SPARSE_FILE) ? "Z" : " "; + + debug += QString(" %1").arg(path); + + qDebug() << debug; + + return true; + } + } else { + DWORD error = ::GetLastError(); + qWarning(qUtf8Printable(QString("Unable to get file attributes for %1 (error %2)") + .arg(w_path) + .arg(error))); + } + return false; +#else + Q_UNUSED(path); + return false; +#endif +} + +static bool fixFileAttributes(const QString& path) +{ +#ifdef _WIN32 + bool success = true; + + WCHAR w_path[32767]; + memset(w_path, 0, sizeof(w_path)); + path.toWCharArray(w_path); + + DWORD attrs = GetFileAttributes(w_path); + if (attrs != INVALID_FILE_ATTRIBUTES) { + // Clear all the attributes possible, except ARCHIVE, with SetFileAttributes + if (!SetFileAttributes( + w_path, attrs & FILE_ATTRIBUTE_ARCHIVE ? FILE_ATTRIBUTE_ARCHIVE : 0)) { + DWORD error = GetLastError(); + qWarning(qUtf8Printable(QString("Unable to set file attributes for %1 (error %2)") + .arg(path) + .arg(error))); + success = false; + } + + // Compression requires DeviceIoControl + if (attrs & FILE_ATTRIBUTE_COMPRESSED) { + HANDLE hndl = CreateFile(w_path, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + if (hndl != INVALID_HANDLE_VALUE) { + USHORT compressionSetting = COMPRESSION_FORMAT_NONE; + DWORD bytesReturned = 0; + if (!DeviceIoControl(hndl, FSCTL_SET_COMPRESSION, &compressionSetting, + sizeof(compressionSetting), NULL, 0, &bytesReturned, + NULL)) { + DWORD error = GetLastError(); + qWarning(qUtf8Printable( + QString("Unable to disable compression for file %1 (error %2)") + .arg(path) + .arg(error))); + success = false; + } + CloseHandle(hndl); + } else { + DWORD error = GetLastError(); + qWarning(qUtf8Printable( + QString("Unable to open file %1 (error %2)").arg(path).arg(error))); + success = false; + } + } + + // Sparseness requires DeviceIoControl + if (attrs & FILE_ATTRIBUTE_SPARSE_FILE) { + HANDLE hndl = CreateFile(w_path, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + if (hndl != INVALID_HANDLE_VALUE) { + FILE_SET_SPARSE_BUFFER setting = {FALSE}; + DWORD bytesReturned = 0; + if (!DeviceIoControl(hndl, FSCTL_SET_SPARSE, &setting, sizeof(setting), NULL, 0, + &bytesReturned, NULL)) { + DWORD error = GetLastError(); + qWarning(qUtf8Printable( + QString("Unable to disable sparseness for file %1 (error %2)") + .arg(path) + .arg(error))); + success = false; + } + CloseHandle(hndl); + } else { + DWORD error = GetLastError(); + qWarning(qUtf8Printable( + QString("Unable to open file %1 (error %2)").arg(path).arg(error))); + success = false; + } + } + + // As a last ditch effort, set the archive flag + if (!success) { + attrs = GetFileAttributes(w_path); + if (attrs != INVALID_FILE_ATTRIBUTES) { + if (!SetFileAttributes(w_path, attrs | FILE_ATTRIBUTE_ARCHIVE)) { + DWORD error = GetLastError(); + qWarning( + qUtf8Printable(QString("Unable to set file attributes for %1 (error %2)") + .arg(path) + .arg(error))); + } else { + success = true; + } + } else { + DWORD error = GetLastError(); + qWarning( + qUtf8Printable(QString("Unable to get file attributes for %1 (error %2)") + .arg(path) + .arg(error))); + } + } + } else { + DWORD error = GetLastError(); + qWarning(qUtf8Printable(QString("Unable to get file attributes for %1 (error %2)") + .arg(path) + .arg(error))); + success = false; + } + + return success; +#else + Q_UNUSED(path); + return true; +#endif +} + +bool DiagnoseBasic::fileAttributes(const QString& executable) const +{ + if (!m_MOInfo->pluginSetting(name(), "check_fileattributes").toBool()) + return true; + + QStringList filesToFix; + QStringList directoriesToSearch; + + // Search the game directory for problems + directoriesToSearch << m_MOInfo->managedGame()->dataDirectory().absolutePath(); + + // Find the active mods to search them too + for (QString mod : m_MOInfo->modList()->allMods()) { + if (m_MOInfo->modList()->state(mod) & MOBase::IModList::STATE_ACTIVE) { + directoriesToSearch << m_MOInfo->modList()->getMod(mod)->absolutePath(); + } + } + + // Find the subdirectories of all the directories + for (int i = 0; i < directoriesToSearch.length(); i++) { + for (QString dir : + QDir(directoriesToSearch[i]) + .entryList(QDir::Hidden | QDir::AllDirs | QDir::NoDotAndDotDot)) { + directoriesToSearch << QDir(directoriesToSearch[i]).filePath(dir); + } + } + + // Set up a progress bar since this can take a while + QPushButton* progressButton = new QPushButton("Cancel"); + progressButton->setEnabled(false); + + QLabel* progressLabel = new QLabel; + progressLabel->setText(tr("File attribute checker\nSearching for problems...")); + progressLabel->setAlignment(Qt::AlignCenter); + + QProgressDialog dialog; + dialog.setWindowModality(Qt::ApplicationModal); + dialog.setCancelButton(progressButton); + dialog.setLabel(progressLabel); + dialog.setMinimumDuration(1); + dialog.show(); + dialog.setMaximum(directoriesToSearch.length()); + + // Find problems with the directories and files + for (int i = 0; i < directoriesToSearch.length(); i++) { + QString* dirPath = &directoriesToSearch[i]; +#ifdef _WIN32 + QString fixedPath = QString("\\\\?\\%1").arg(QDir::toNativeSeparators(*dirPath)); +#else + QString fixedPath = *dirPath; +#endif + if (checkFileAttributes(fixedPath)) + filesToFix << fixedPath; + + for (QString file : QDir(*dirPath).entryList(QDir::Hidden | QDir::AllEntries | + QDir::NoDotAndDotDot)) { +#ifdef _WIN32 + fixedPath = + QString("\\\\?\\%1").arg(QDir::toNativeSeparators(*dirPath + "\\" + file)); +#else + fixedPath = QDir(*dirPath).filePath(file); +#endif + if (checkFileAttributes(fixedPath)) + filesToFix << fixedPath; + } + dialog.setValue(i); + } + + if (filesToFix.length() == 0) { + return true; + } + + QMessageBox::StandardButton userResponse = + QMessageBox::question(nullptr, tr("Invalid file attributes found"), + tr("One or more of your files has attributes that may " + "prevent the game from reading the files. " + "This can result in missing plugins, missing textures, " + "and other such problems.\n\n" + "Fix the file attributes?"), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (userResponse == QMessageBox::Cancel) { + qDebug() << "User canceled executable launch due to file attributes"; + return false; + } else if (userResponse == QMessageBox::No) { + return true; + } + + // Reset progress bar + progressLabel->setText(tr("File attribute checker\nFixing file attributes...")); + dialog.setValue(0); + dialog.setMaximum(filesToFix.length()); + + // Start iterating through files fixing problems + bool success = true; + for (int i = 0; i < filesToFix.length(); i++) { + if (!fixFileAttributes(filesToFix[i])) + success = false; + dialog.setValue(i); + } + + if (!success) { + if (QMessageBox::question(nullptr, tr("Unable to set file attributes"), + tr("Mod Organizer was unable to fix the file attributes " + "of at least one file.\n\n" + "Continue launching %1?") + .arg(executable), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + return false; + } + } + + return true; +} + +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 directory"); + 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( + "There are currently files in your Overwrite directory. These files are typically newly " + "created files, usually generated by an external mod tool (i.e. Wrye Bash, " + "xEdit, FNIS, ...). Creation Club ESL files will also end up here when " + "downloaded. Any files in Overwrite " + "will take top priority when loading your mod files and will always overwrite " + "any other mod in your profile.
" + "
" + "It is recommended that you review the files in Overwrite and move any relevant files to a new or existing " + "mod. You can do this by double-clicking the Overwrite mod and dragging files from the Overwrite window to " + "a mod entry in the main mod list. It is also possible to move all current " + "Overwrite files to a new mod by " + "right-clicking on the Overwrite mod.
" + "
" + "Not all files in Overwrite need to " + "be removed, but there are several reasons to do so. Some generated files will " + "be directly related to the active mods in your profile and will be " + "incompatible with different mod setups. Since Overwrite is always active, this could cause conflicts between " + "profiles. Additionally, moving relevant game files into a normal mod will " + "give you greater control over those files. Some files can live safely in " + "Overwrite, such as basic logs and " + "cache files. It is up to you to understand how best to manage these files.
" + "
" + "If you do not wish to see this warning and understand how to handle your " + "Overwrite directory, you can open " + "the Mod Organizer settings and disable this warning under the \"Diagnose " + "Basic\" plugin configuration."); + 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: { + QString masterInfo; + for (auto master : m_MissingMasters) { + masterInfo += ""; + masterInfo += "" + master + ""; + masterInfo += "" + + SetJoin(m_PluginChildren[master], ", ") + ""; + masterInfo += ""; + } + 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: ") + + "
" + "" + + "" + masterInfo + "
" + + tr("Master") + "" + tr("Required By") + + "
"; + } 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)); + } +} diff --git a/libs/diagnose_basic/src/diagnosebasic.h b/libs/diagnose_basic/src/diagnosebasic.h new file mode 100644 index 0000000..826158a --- /dev/null +++ b/libs/diagnose_basic/src/diagnosebasic.h @@ -0,0 +1,142 @@ +/* +Copyright (C) 2013 Sebastian Herbord. All rights reserved. + +This file is part of basic diagnosis plugin for MO + +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 . +*/ + +#ifndef DIAGNOSEBASIC_H +#define DIAGNOSEBASIC_H + +#include +#include +#include + +#include +#include +#include +#include + +class DiagnoseBasic : public QObject, + public MOBase::IPlugin, + public MOBase::IPluginDiagnose +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginDiagnose) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.tannin.DiagnoseBasic") +#endif + +public: + DiagnoseBasic(); + +public: // IPlugin + virtual bool init(MOBase::IOrganizer* moInfo); + virtual QString name() const; + virtual QString author() const; + virtual QString description() const; + virtual MOBase::VersionInfo version() const; + virtual bool isActive() const; + virtual QList settings() const; + +public: // IPluginDiagnose + virtual std::vector activeProblems() const; + virtual QString shortDescription(unsigned int key) const; + virtual QString fullDescription(unsigned int key) const; + virtual bool hasGuidedFix(unsigned int key) const; + virtual void startGuidedFix(unsigned int key) const; + +private: + bool errorReported() const; + bool overwriteFiles() const; + bool invalidFontConfig() const; + bool nitpickInstalled() const; + bool assetOrder() const; + bool missingMasters() const; + bool alternateGame() const; + bool fileAttributes(const QString& executable) const; + +private: + static const unsigned int PROBLEM_ERRORLOG = 1; + static const unsigned int PROBLEM_OVERWRITE = 2; + static const unsigned int PROBLEM_INVALIDFONT = 3; + static const unsigned int PROBLEM_NITPICKINSTALLED = 4; + static const unsigned int PROBLEM_PROFILETWEAKS = 7; + static const unsigned int PROBLEM_MISSINGMASTERS = 8; + static const unsigned int PROBLEM_ALTERNATE = 9; + + static const unsigned int NUM_CONTEXT_ROWS = 5; + + static const QRegularExpression RE_LOG_FILE; + +private: + struct ListElement + { + QString espName; + QString modName; + int pluginPriority; + int modPriority; + int sortGroup; + bool avoidMove; + QSet relevantScripts; + }; + + struct Move + { + ListElement item; + ListElement reference; + enum EType + { + BEFORE, + AFTER + } type; + Move(const ListElement& initItem, const ListElement& initReference, EType initType) + : item(initItem), reference(initReference), type(initType) + {} + }; + + struct Sorter + { + struct + { + int operator()(const ListElement& lhs, const ListElement& rhs) + { + return lhs.modPriority < rhs.modPriority; + } + } minMod; + + std::vector moves; + + void operator()(std::vector modList); + + private: + void sortGroup(std::vector modList); + }; + + friend bool operator<(const Move& lhs, const Move& rhs); + +private: + void topoSort(std::vector& list) const; + bool checkEmpty(const QString& path) const; + +private: + MOBase::IOrganizer* m_MOInfo; + mutable QString m_ErrorMessage; + mutable QString m_NewestModlistBackup; + mutable std::set m_MissingMasters; + mutable std::map> m_PluginChildren; +}; + +#endif // DIAGNOSEBASIC_H -- cgit v1.3.1