#include "gamebryogameplugins.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using MOBase::IOrganizer; using MOBase::IPluginList; using MOBase::reportError; using MOBase::SafeWriteFile; GamebryoGamePlugins::GamebryoGamePlugins(IOrganizer* organizer) : m_Organizer(organizer) {} void GamebryoGamePlugins::writePluginLists(const IPluginList* pluginList) { if (!m_LastRead.isValid()) { // attempt to write uninitialized plugin lists return; } writePluginList(pluginList, m_Organizer->profile()->absolutePath() + "/plugins.txt"); writeLoadOrderList(pluginList, m_Organizer->profile()->absolutePath() + "/loadorder.txt"); m_LastRead = QDateTime::currentDateTime(); } void GamebryoGamePlugins::readPluginLists(MOBase::IPluginList* pluginList) { QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || QFileInfo(loadOrderPath).lastModified() > m_LastRead; bool pluginsIsNew = !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; if (loadOrderIsNew || !pluginsIsNew) { // read both files if they are both new or both older than the last read QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath); pluginList->setLoadOrder(loadOrder); readPluginList(pluginList); } else { // If the plugins is new but not loadorder, we must reparse the load order from the // plugin files QStringList loadOrder = readPluginList(pluginList); pluginList->setLoadOrder(loadOrder); } m_LastRead = QDateTime::currentDateTime(); } QStringList GamebryoGamePlugins::getLoadOrder() { QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || QFileInfo(loadOrderPath).lastModified() > m_LastRead; bool pluginsIsNew = !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; if (loadOrderIsNew || !pluginsIsNew) { return readLoadOrderList(m_Organizer->pluginList(), loadOrderPath); } else { return readPluginList(m_Organizer->pluginList()); } } void GamebryoGamePlugins::writePluginList(const MOBase::IPluginList* pluginList, const QString& filePath) { return writeList(pluginList, filePath, false); } void GamebryoGamePlugins::writeLoadOrderList(const MOBase::IPluginList* pluginList, const QString& filePath) { return writeList(pluginList, filePath, true); } void GamebryoGamePlugins::writeList(const IPluginList* pluginList, const QString& filePath, bool loadOrder) { SafeWriteFile file(filePath); QStringEncoder encoder = loadOrder ? QStringEncoder(QStringConverter::Encoding::Utf8) : QStringEncoder(QStringConverter::Encoding::System); file->resize(0); file->write( encoder.encode("# This file was automatically generated by Mod Organizer.\r\n")); bool invalidFileNames = false; int writtenCount = 0; QStringList plugins = pluginList->pluginNames(); std::sort(plugins.begin(), plugins.end(), [pluginList](const QString& lhs, const QString& rhs) { return pluginList->priority(lhs) < pluginList->priority(rhs); }); for (const QString& pluginName : plugins) { if (loadOrder || (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE)) { auto result = encoder.encode(pluginName); if (encoder.hasError()) { invalidFileNames = true; qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); } else { file->write(result); } file->write("\r\n"); ++writtenCount; } } if (invalidFileNames) { reportError(QObject::tr("Some of your plugins have invalid names! These " "plugins can not be loaded by the game. Please see " "mo_interface.log for a list of affected plugins " "and rename them.")); } if (writtenCount == 0) { qWarning("plugin list would be empty, this is almost certainly wrong. Not " "saving."); } else { file->commit(); } } QStringList GamebryoGamePlugins::readLoadOrderList(MOBase::IPluginList* pluginList, const QString& filePath) { QStringList pluginNames = organizer()->managedGame()->primaryPlugins(); std::set pluginLookup; for (auto&& name : pluginNames) { pluginLookup.insert(name.toLower()); } const auto b = MOBase::forEachLineInFile(filePath, [&](QString s) { if (!pluginLookup.contains(s.toLower())) { pluginLookup.insert(s); pluginNames.push_back(std::move(s)); } }); if (!b) { return readPluginList(pluginList); } return pluginNames; } QStringList GamebryoGamePlugins::readPluginList(MOBase::IPluginList* pluginList) { QStringList primary = organizer()->managedGame()->primaryPlugins(); for (const QString& pluginName : primary) { if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); } } QStringList plugins = pluginList->pluginNames(); QStringList pluginsClone(plugins); // Do not sort the primary plugins. Their load order should be locked as defined in // "primaryPlugins". for (const auto& plugin : pluginsClone) { if (primary.contains(plugin, Qt::CaseInsensitive)) plugins.removeAll(plugin); } // Always use filetime loadorder to get the actual load order QHash modificationTimes; modificationTimes.reserve(plugins.size()); for (const auto& pluginName : plugins) { MOBase::IModInterface* mod = organizer()->modList()->getMod(pluginList->origin(pluginName)); QDir dir = mod ? mod->absolutePath() : organizer()->managedGame()->dataDirectory(); QString absPath = dir.absoluteFilePath(pluginName); modificationTimes.insert(pluginName, QFileInfo(absPath).lastModified()); } std::ranges::sort( plugins, [&modificationTimes](const QString& lhs, const QString& rhs) { return modificationTimes.value(lhs) < modificationTimes.value(rhs); }); // Determine plugin active state by the plugins.txt file. bool pluginsTxtExists = true; QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { pluginsTxtExists = false; } ON_BLOCK_EXIT([&]() { file.close(); }); if (file.size() == 0) { // MO stores at least a header in the file. if it's completely empty the // file is broken pluginsTxtExists = false; } QStringList activePlugins; if (pluginsTxtExists) { // Two-pass read: first detect whether the file uses the Bethesda/LOOT // asterisk format (*Plugin.esp = enabled, Plugin.esp = disabled) vs // MO2's own format (only active plugins listed, no asterisks). struct PluginEntry { QString name; bool hadAsterisk; }; QList entries; bool hasAsterisks = false; while (!file.atEnd()) { QByteArray line = file.readLine(); if (line.size() > 0 && line.at(0) != '#') { QString pluginName = QString::fromLocal8Bit(line.trimmed()); bool asterisk = false; if (pluginName.startsWith('*')) { pluginName = pluginName.mid(1); asterisk = true; hasAsterisks = true; } if (!pluginName.isEmpty()) { entries.append({pluginName, asterisk}); } } } // If file uses asterisks (LOOT/Bethesda format): only *-prefixed are active. // If no asterisks (MO2 format): all listed plugins are active. for (const auto& entry : entries) { bool isActive = hasAsterisks ? entry.hadAsterisk : true; if (isActive) { pluginList->setState(entry.name, IPluginList::STATE_ACTIVE); activePlugins.push_back(entry.name); } } for (const auto& pluginName : plugins) { if (!activePlugins.contains(pluginName, Qt::CaseInsensitive)) { pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); } } } else { for (const QString& pluginName : plugins) { pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); } } return primary + plugins; }