summaryrefslogtreecommitdiff
path: root/src/pluginlist.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/pluginlist.cpp')
-rw-r--r--src/pluginlist.cpp3442
1 files changed, 1721 insertions, 1721 deletions
diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp
index 28e0af51..a4742529 100644
--- a/src/pluginlist.cpp
+++ b/src/pluginlist.cpp
@@ -1,1721 +1,1721 @@
-/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#include "pluginlist.h"
-#include "settings.h"
-#include "scopeguard.h"
-#include "modinfo.h"
-#include "modlist.h"
-#include "viewmarkingscrollbar.h"
-#include "shared/directoryentry.h"
-#include "shared/filesorigin.h"
-#include "shared/fileentry.h"
-
-#include <utility.h>
-#include <iplugingame.h>
-#include <espfile.h>
-#include <report.h>
-#include "shared/windows_error.h"
-#include <safewritefile.h>
-#include <gameplugins.h>
-
-#include <QtDebug>
-#include <QMessageBox>
-#include <QMimeData>
-#include <QCoreApplication>
-#include <QDateTime>
-#include <QDir>
-#include <QFile>
-#include <QFileInfo>
-#include <QFileInfo>
-#include <QListWidgetItem>
-#include <QRegularExpression>
-#include <QString>
-#include <QApplication>
-#include <QKeyEvent>
-#include <QSortFilterProxyModel>
-
-#include <ctime>
-#include <algorithm>
-#include <stdexcept>
-
-#include "organizercore.h"
-
-using namespace MOBase;
-using namespace MOShared;
-
-
-static QString TruncateString(const QString& text)
-{
- QString new_text = text;
-
- if (new_text.length() > 1024) {
- new_text.truncate(1024);
- new_text += "...";
- }
-
- return new_text;
-}
-
-
-PluginList::PluginList(OrganizerCore& organizer)
- : QAbstractItemModel(&organizer)
- , m_Organizer(organizer)
- , m_FontMetrics(QFont())
-{
- connect(this, SIGNAL(writePluginsList()), this, SLOT(generatePluginIndexes()));
- m_LastCheck.start();
-}
-
-PluginList::~PluginList()
-{
- m_Refreshed.disconnect_all_slots();
- m_PluginMoved.disconnect_all_slots();
- m_PluginStateChanged.disconnect_all_slots();
-}
-
-QString PluginList::getColumnName(int column)
-{
- switch (column) {
- case COL_NAME: return tr("Name");
- case COL_PRIORITY: return tr("Priority");
- case COL_MODINDEX: return tr("Mod Index");
- case COL_FLAGS: return tr("Flags");
- default: return tr("unknown");
- }
-}
-
-
-QString PluginList::getColumnToolTip(int column)
-{
- switch (column) {
- case COL_NAME: return tr("Name of the plugin");
- case COL_FLAGS: return tr("Emblems to highlight things that might require attention.");
- case COL_PRIORITY: return tr("Load priority of plugins. The higher, the more \"important\" it is and thus "
- "overwrites data from plugins with lower priority.");
- case COL_MODINDEX: return tr("Determines the formids of objects originating from this mods.");
- default: return tr("unknown");
- }
-}
-
-void PluginList::highlightPlugins(
- const std::vector<unsigned int>& modIndices,
- const MOShared::DirectoryEntry &directoryEntry)
-{
- auto* profile = m_Organizer.currentProfile();
-
- for (auto &esp : m_ESPs) {
- esp.modSelected = false;
- }
-
- for (auto& modIndex : modIndices) {
- ModInfo::Ptr selectedMod = ModInfo::getByIndex(modIndex);
- if (!selectedMod.isNull() && profile->modEnabled(modIndex)) {
- QDir dir(selectedMod->absolutePath());
- QStringList plugins = dir.entryList(QStringList() << "*.esp" << "*.esm" << "*.esl");
- const MOShared::FilesOrigin& origin = directoryEntry.getOriginByName(selectedMod->internalName().toStdWString());
- if (plugins.size() > 0) {
- for (auto plugin : plugins) {
- MOShared::FileEntryPtr file = directoryEntry.findFile(plugin.toStdWString());
- if (file && file->getOrigin() != origin.getID()) {
- const auto alternatives = file->getAlternatives();
- if (std::find_if(alternatives.begin(), alternatives.end(), [&](const FileAlternative& element) { return element.originID() == origin.getID(); }) == alternatives.end())
- continue;
- }
- std::map<QString, int>::iterator iter = m_ESPsByName.find(plugin);
- if (iter != m_ESPsByName.end()) {
- m_ESPs[iter->second].modSelected = true;
- }
- }
- }
- }
- }
-
- emit dataChanged(this->index(0, 0), this->index(static_cast<int>(m_ESPs.size()) - 1, this->columnCount() - 1));
-}
-
-void PluginList::refresh(const QString &profileName
- , const DirectoryEntry &baseDirectory
- , const QString &lockedOrderFile
- , bool force)
-{
- TimeThis tt("PluginList::refresh()");
-
- if (force) {
- m_ESPs.clear();
- m_ESPsByName.clear();
- m_ESPsByPriority.clear();
- }
-
- ChangeBracket<PluginList> layoutChange(this);
-
- QStringList primaryPlugins = m_GamePlugin->primaryPlugins();
- GamePlugins *gamePlugins = m_GamePlugin->feature<GamePlugins>();
- const bool lightPluginsAreSupported = gamePlugins ? gamePlugins->lightPluginsAreSupported() : false;
-
- m_CurrentProfile = profileName;
-
- QStringList availablePlugins;
-
- std::vector<FileEntryPtr> files = baseDirectory.getFiles();
- for (FileEntryPtr current : files) {
- if (current.get() == nullptr) {
- continue;
- }
- QString filename = ToQString(current->getName());
-
- if (filename.endsWith(".esp", Qt::CaseInsensitive) ||
- filename.endsWith(".esm", Qt::CaseInsensitive) ||
- filename.endsWith(".esl", Qt::CaseInsensitive)) {
-
- availablePlugins.append(filename);
-
- if (m_ESPsByName.find(filename) != m_ESPsByName.end()) {
- continue;
- }
-
- bool forceEnabled = Settings::instance().game().forceEnableCoreFiles() &&
- primaryPlugins.contains(filename, Qt::CaseInsensitive);
-
- bool archive = false;
- try {
- FilesOrigin &origin = baseDirectory.getOriginByID(current->getOrigin(archive));
-
- //name without extension
- QString baseName = QFileInfo(filename).completeBaseName();
-
- QString iniPath = baseName + ".ini";
- bool hasIni = baseDirectory.findFile(ToWString(iniPath)).get() != nullptr;
- std::set<QString> loadedArchives;
- QString candidateName;
- for (FileEntryPtr archiveCandidate : files) {
- candidateName = ToQString(archiveCandidate->getName());
- if (candidateName.startsWith(baseName, Qt::CaseInsensitive) &&
- (candidateName.endsWith(".bsa", Qt::CaseInsensitive) ||
- candidateName.endsWith(".ba2", Qt::CaseInsensitive))) {
- loadedArchives.insert(candidateName);
- }
- }
-
- QString originName = ToQString(origin.getName());
- unsigned int modIndex = ModInfo::getIndex(originName);
- if (modIndex != UINT_MAX) {
- ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
- originName = modInfo->name();
- }
-
- m_ESPs.push_back(ESPInfo(filename, forceEnabled, originName, ToQString(current->getFullPath()), hasIni, loadedArchives, lightPluginsAreSupported));
- m_ESPs.rbegin()->priority = -1;
- } catch (const std::exception &e) {
- reportError(tr("failed to update esp info for file %1 (source id: %2), error: %3").arg(filename).arg(current->getOrigin(archive)).arg(e.what()));
- }
- }
- }
-
- for (const auto &espName : m_ESPsByName) {
- if (!availablePlugins.contains(espName.first, Qt::CaseInsensitive)) {
- m_ESPs[espName.second].name = "";
- }
- }
-
- m_ESPs.erase(std::remove_if(m_ESPs.begin(), m_ESPs.end(),
- [](const ESPInfo &info) -> bool {
- return info.name.isEmpty();
- }),
- m_ESPs.end());
-
- fixPriorities();
-
- // functions in GamePlugins will use the IPluginList interface of this, so
- // indices need to work. priority will be off however
- updateIndices();
-
- if (gamePlugins) {
- gamePlugins->readPluginLists(m_Organizer.managedGameOrganizer()->pluginList());
- }
-
- fixPrimaryPlugins();
- fixPluginRelationships();
-
- testMasters();
-
- updateIndices();
-
- readLockedOrderFrom(lockedOrderFile);
-
- layoutChange.finish();
-
- refreshLoadOrder();
- emit dataChanged(this->index(0, 0),
- this->index(static_cast<int>(m_ESPs.size()), columnCount()));
-
- m_Refreshed();
-}
-
-void PluginList::fixPrimaryPlugins()
-{
- if (!m_Organizer.settings().game().forceEnableCoreFiles()) {
- return;
- }
-
- // This function ensures that the primary plugins are first and in the correct order
- QStringList primaryPlugins = m_Organizer.managedGame()->primaryPlugins();
- int prio = 0;
- bool somethingChanged = false;
- for (QString plugin : primaryPlugins) {
- std::map<QString, int>::iterator iter = m_ESPsByName.find(plugin);
- // Plugin is present?
- if (iter != m_ESPsByName.end()) {
- if (prio != m_ESPs[iter->second].priority) {
- // Priority is wrong! Fix it!
- int newPrio = prio;
- setPluginPriority(iter->second, newPrio, true /* isForced */);
- somethingChanged = true;
- }
- prio++;
- }
- }
-
- if (somethingChanged) {
- writePluginsList();
- }
-}
-
-void PluginList::fixPluginRelationships()
-{
- TimeThis timer("PluginList::fixPluginRelationships");
-
- // Count the types of plugins
- int masterCount = 0;
- for (auto plugin : m_ESPs) {
- if (plugin.hasLightExtension ||
- plugin.hasMasterExtension ||
- plugin.isMasterFlagged) {
- masterCount++;
- }
- }
-
- // Ensure masters are up top and normal plugins are down below
- for (int i = 0; i < m_ESPs.size(); i++) {
- ESPInfo& plugin = m_ESPs[i];
- if (plugin.hasLightExtension ||
- plugin.hasMasterExtension ||
- plugin.isMasterFlagged) {
- if (plugin.priority > masterCount) {
- int newPriority = masterCount + 1;
- setPluginPriority(i, newPriority);
- }
- }
- else {
- if (plugin.priority < masterCount) {
- int newPriority = masterCount + 1;
- setPluginPriority(i, newPriority);
- }
- }
- }
-
- // Ensure master/child relationships are observed
- for (int i = 0; i < m_ESPs.size(); i++) {
- ESPInfo& plugin = m_ESPs[i];
- int newPriority = plugin.priority;
- for (auto master : plugin.masters) {
- auto iter = m_ESPsByName.find(master);
- if (iter != m_ESPsByName.end()) {
- newPriority = std::max(newPriority, m_ESPs[iter->second].priority);
- }
- }
- if (newPriority != plugin.priority) {
- setPluginPriority(i, newPriority);
- }
- }
-}
-
-void PluginList::fixPriorities()
-{
- std::vector<std::pair<int, int>> espPrios;
-
- for (int i = 0; i < m_ESPs.size(); ++i) {
- int prio = m_ESPs[i].priority;
- if (prio == -1) {
- prio = INT_MAX;
- }
- espPrios.push_back(std::make_pair(prio, i));
- }
-
- std::sort(espPrios.begin(), espPrios.end(),
- [](const std::pair<int, int> &lhs, const std::pair<int, int> &rhs) {
- return lhs.first < rhs.first;
- });
-
- for (int i = 0; i < espPrios.size(); ++i) {
- m_ESPs[espPrios[i].second].priority = i;
- }
-}
-
-void PluginList::enableESP(const QString &name, bool enable)
-{
- std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
-
- if (iter != m_ESPsByName.end()) {
- auto enabled = m_ESPs[iter->second].enabled;
- m_ESPs[iter->second].enabled =
- enable || m_ESPs[iter->second].forceEnabled;
-
- emit writePluginsList();
- if (enabled != m_ESPs[iter->second].enabled) {
- pluginStatesChanged({ name }, state(name));
- }
- } else {
- reportError(tr("Plugin not found: %1").arg(qUtf8Printable(name)));
- }
-}
-
-int PluginList::findPluginByPriority(int priority)
-{
- for (int i = 0; i < m_ESPs.size(); i++ ) {
- if (m_ESPs[i].priority == priority) {
- return i;
- }
- }
- log::error("No plugin with priority {}", priority);
- return -1;
-}
-
-void PluginList::setEnabled(const QModelIndexList& indices, bool enabled)
-{
- QStringList dirty;
- for (auto& idx : indices) {
- if (m_ESPs[idx.row()].enabled != enabled) {
- m_ESPs[idx.row()].enabled = enabled;
- dirty.append(m_ESPs[idx.row()].name);
- }
- }
- if (!dirty.isEmpty()) {
- emit writePluginsList();
- pluginStatesChanged(dirty,
- enabled ? IPluginList::PluginState::STATE_ACTIVE : IPluginList::PluginState::STATE_INACTIVE);
- }
-}
-
-void PluginList::setEnabledAll(bool enabled)
-{
- QStringList dirty;
- for (ESPInfo &info : m_ESPs) {
- if (info.enabled != enabled) {
- info.enabled = enabled;
- dirty.append(info.name);
- }
- }
- if (!dirty.isEmpty()) {
- emit writePluginsList();
- pluginStatesChanged(dirty,
- enabled ? IPluginList::PluginState::STATE_ACTIVE : IPluginList::PluginState::STATE_INACTIVE);
- }
-}
-
-void PluginList::sendToPriority(const QModelIndexList& indices, int newPriority)
-{
- std::vector<int> pluginsToMove;
- for (auto& idx : indices) {
- if (!m_ESPs[idx.row()].forceEnabled) {
- pluginsToMove.push_back(idx.row());
- }
- }
- if (pluginsToMove.size()) {
- changePluginPriority(pluginsToMove, newPriority);
- }
-}
-
-void PluginList::shiftPluginsPriority(const QModelIndexList& indices, int offset)
-{
- // retrieve the plugin index and sort them by priority to avoid issue
- // when moving them
- std::vector<int> allIndex;
- for (auto& idx : indices) {
- allIndex.push_back(idx.row());
- }
- std::sort(allIndex.begin(), allIndex.end(), [=](int lhs, int rhs) {
- bool cmp = m_ESPs[lhs].priority < m_ESPs[rhs].priority;
- return offset > 0 ? !cmp : cmp;
- });
-
- for (auto index : allIndex) {
- int newPriority = m_ESPs[index].priority + offset;
- if (newPriority >= 0 && newPriority < rowCount()) {
- setPluginPriority(index, newPriority);
- }
- }
-
- refreshLoadOrder();
-}
-
-void PluginList::toggleState(const QModelIndexList& indices)
-{
- QModelIndex minRow, maxRow;
- for (auto& idx : indices) {
- if (!minRow.isValid() || (idx.row() < minRow.row())) {
- minRow = idx;
- }
- if (!maxRow.isValid() || (idx.row() > maxRow.row())) {
- maxRow = idx;
- }
- int oldState = idx.data(Qt::CheckStateRole).toInt();
- setData(idx, oldState == Qt::Unchecked ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
- }
-
- emit dataChanged(minRow, maxRow);
-}
-
-bool PluginList::isEnabled(const QString &name)
-{
- std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
-
- if (iter != m_ESPsByName.end()) {
- return m_ESPs[iter->second].enabled;
- } else {
- return false;
- }
-}
-
-void PluginList::clearInformation(const QString &name)
-{
- std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
-
- if (iter != m_ESPsByName.end()) {
- m_AdditionalInfo[name].messages.clear();
- }
-}
-
-void PluginList::clearAdditionalInformation()
-{
- m_AdditionalInfo.clear();
-}
-
-void PluginList::addInformation(const QString &name, const QString &message)
-{
- std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
-
- if (iter != m_ESPsByName.end()) {
- m_AdditionalInfo[name].messages.append(message);
- } else {
- log::warn("failed to associate message for \"{}\"", name);
- }
-}
-
-void PluginList::addLootReport(const QString& name, Loot::Plugin plugin)
-{
- auto iter = m_ESPsByName.find(name);
-
- if (iter != m_ESPsByName.end()) {
- m_AdditionalInfo[name].loot = std::move(plugin);
- } else {
- log::warn("failed to associate loot report for \"{}\"", name);
- }
-}
-
-bool PluginList::isEnabled(int index)
-{
- return m_ESPs.at(index).enabled;
-}
-
-void PluginList::readLockedOrderFrom(const QString &fileName)
-{
- m_LockedOrder.clear();
-
- QFile file(fileName);
- if (!file.exists()) {
- // no locked load order, that's ok
- return;
- }
-
- file.open(QIODevice::ReadOnly);
- int lineNumber = 0;
- while (!file.atEnd())
- {
- QByteArray line = file.readLine();
- ++lineNumber;
-
- // Skip empty lines or commented out lines (#)
- if ((line.size() <= 0) || (line.at(0) == '#'))
- {
- continue;
- }
-
- QList<QByteArray> fields = line.split('|');
- if (fields.count() != 2)
- {
- // Don't know how to parse this so run away
- log::error("locked order file: invalid line #{}: {}", lineNumber, QString::fromUtf8(line).trimmed());
- continue;
- }
-
- // Read the plugin name and priority
- QString pluginName = QString::fromUtf8(fields.at(0));
- int priority = fields.at(1).trimmed().toInt();
- if (priority < 0)
- {
- // WTF do you mean a negative priority?
- log::error("locked order file: invalid line #{}: {}", lineNumber, QString::fromUtf8(line).trimmed());
- continue;
- }
-
- // Determine the index of the plugin
- auto it = m_ESPsByName.find(pluginName);
- if (it == m_ESPsByName.end())
- {
- // Plugin does not exist in the current set of plugins
- m_LockedOrder[pluginName] = priority;
- continue;
- }
- int pluginIndex = it->second;
-
- // Do not allow locking forced plugins
- if (m_ESPs[pluginIndex].forceEnabled)
- {
- continue;
- }
-
- // If the priority is larger than the number of plugins, just keep it locked
- if (priority >= m_ESPsByPriority.size())
- {
- m_LockedOrder[pluginName] = priority;
- continue;
- }
-
- // These are some helper functions for figuring out what is already locked
- auto findLocked = [&](const std::pair<QString, int>& a) { return a.second == priority; };
- auto alreadyLocked = [&](){ return std::find_if(m_LockedOrder.begin(), m_LockedOrder.end(), findLocked) != m_LockedOrder.end(); };
-
- // See if we can just set the given priority
- if (!m_ESPs[m_ESPsByPriority.at(priority)].forceEnabled && !alreadyLocked())
- {
- m_LockedOrder[pluginName] = priority;
- continue;
- }
-
- // Find the next higher priority we can set the plugin to
- while (++priority < m_ESPs.size())
- {
- if (!m_ESPs[m_ESPsByPriority.at(priority)].forceEnabled && !alreadyLocked())
- {
- m_LockedOrder[pluginName] = priority;
- break;
- }
- }
-
- // See if we walked off the end of the plugin list
- if (priority >= m_ESPs.size())
- {
- // I guess go ahead and lock it here at the end of the list?
- m_LockedOrder[pluginName] = priority;
- continue;
- }
- } /* while (!file.atEnd()) */
- file.close();
-}
-
-void PluginList::writeLockedOrder(const QString &fileName) const
-{
- SafeWriteFile file(fileName);
-
- file->resize(0);
- file->write(QString("# This file was automatically generated by Mod Organizer.\r\n").toUtf8());
- for (auto iter = m_LockedOrder.begin(); iter != m_LockedOrder.end(); ++iter) {
- file->write(QString("%1|%2\r\n").arg(iter->first).arg(iter->second).toUtf8());
- }
- file.commit();
-}
-
-void PluginList::saveTo(const QString &lockedOrderFileName) const
-{
- GamePlugins *gamePlugins = m_GamePlugin->feature<GamePlugins>();
- if (gamePlugins) {
- gamePlugins->writePluginLists(m_Organizer.managedGameOrganizer()->pluginList());
- }
-
- writeLockedOrder(lockedOrderFileName);
-}
-
-
-bool PluginList::saveLoadOrder(DirectoryEntry &directoryStructure)
-{
- if (m_GamePlugin->loadOrderMechanism() != IPluginGame::LoadOrderMechanism::FileTime) {
- // nothing to do
- return true;
- }
-
- log::debug("setting file times on esps");
-
- for (ESPInfo &esp : m_ESPs) {
- std::wstring espName = ToWString(esp.name);
- const FileEntryPtr fileEntry = directoryStructure.findFile(espName);
- if (fileEntry.get() != nullptr) {
- QString fileName;
- bool archive = false;
- int originid = fileEntry->getOrigin(archive);
-
- fileName = QString("%1\\%2")
- .arg(QDir::toNativeSeparators(ToQString(directoryStructure.getOriginByID(originid).getPath())))
- .arg(esp.name);
-
- HANDLE file = ::CreateFile(ToWString(fileName).c_str(), GENERIC_READ | GENERIC_WRITE,
- 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
- if (file == INVALID_HANDLE_VALUE) {
- if (::GetLastError() == ERROR_SHARING_VIOLATION) {
- // file is locked, probably the game is running
- return false;
- } else {
- throw windows_error(QObject::tr("failed to access %1").arg(fileName).toUtf8().constData());
- }
- }
-
- ULONGLONG temp = 0;
- temp = (145731ULL + esp.priority) * 24 * 60 * 60 * 10000000ULL;
-
- FILETIME newWriteTime;
-
- newWriteTime.dwLowDateTime = (DWORD)(temp & 0xFFFFFFFF);
- newWriteTime.dwHighDateTime = (DWORD)(temp >> 32);
- esp.time = newWriteTime;
- fileEntry->setFileTime(newWriteTime);
- if (!::SetFileTime(file, nullptr, nullptr, &newWriteTime)) {
- throw windows_error(QObject::tr("failed to set file time %1").arg(fileName).toUtf8().constData());
- }
-
- CloseHandle(file);
- }
- }
- return true;
-}
-
-int PluginList::enabledCount() const
-{
- int enabled = 0;
- for (const auto &info : m_ESPs) {
- if (info.enabled) {
- ++enabled;
- }
- }
- return enabled;
-}
-
-QString PluginList::getIndexPriority(int index) const
-{
- return m_ESPs[index].index;
-}
-
-bool PluginList::isESPLocked(int index) const
-{
- return m_LockedOrder.find(m_ESPs.at(index).name) != m_LockedOrder.end();
-}
-
-void PluginList::lockESPIndex(int index, bool lock)
-{
- if (lock) {
- if (!m_ESPs.at(index).forceEnabled)
- m_LockedOrder[getName(index)] = m_ESPs.at(index).loadOrder;
- else
- return;
- } else {
- auto iter = m_LockedOrder.find(getName(index));
- if (iter != m_LockedOrder.end()) {
- m_LockedOrder.erase(iter);
- }
- }
- emit writePluginsList();
-}
-
-void PluginList::syncLoadOrder()
-{
- int loadOrder = 0;
- for (unsigned int i = 0; i < m_ESPs.size(); ++i) {
- int index = m_ESPsByPriority[i];
-
- if (m_ESPs[index].enabled) {
- m_ESPs[index].loadOrder = loadOrder++;
- } else {
- m_ESPs[index].loadOrder = -1;
- }
- }
-}
-
-void PluginList::refreshLoadOrder()
-{
- ChangeBracket<PluginList> layoutChange(this);
- syncLoadOrder();
- // set priorities according to locked load order
- std::map<int, QString> lockedLoadOrder;
- std::for_each(m_LockedOrder.begin(), m_LockedOrder.end(),
- [&lockedLoadOrder] (const std::pair<QString, int> &ele) {
- lockedLoadOrder[ele.second] = ele.first; });
-
- int targetPrio = 0;
- bool savePluginsList = false;
- // this is guaranteed to iterate from lowest key (load order) to highest
- for (auto iter = lockedLoadOrder.begin(); iter != lockedLoadOrder.end(); ++iter) {
- auto nameIter = m_ESPsByName.find(iter->second);
- if (nameIter != m_ESPsByName.end()) {
- // locked esp exists
-
- // find the location to insert at
- while ((targetPrio < static_cast<int>(m_ESPs.size() - 1)) &&
- (m_ESPs[m_ESPsByPriority[targetPrio]].loadOrder < iter->first)) {
- ++targetPrio;
- }
-
- if (static_cast<size_t>(targetPrio) >= m_ESPs.size()) {
- continue;
- }
-
- int temp = targetPrio;
- int index = nameIter->second;
- if (m_ESPs[index].priority != temp) {
- setPluginPriority(index, temp);
- m_ESPs[index].loadOrder = iter->first;
- syncLoadOrder();
- savePluginsList = true;
- }
- }
- }
- if (savePluginsList) {
- emit writePluginsList();
- }
-}
-
-void PluginList::disconnectSlots() {
- m_PluginMoved.disconnect_all_slots();
- m_Refreshed.disconnect_all_slots();
- m_PluginStateChanged.disconnect_all_slots();
-}
-
-int PluginList::timeElapsedSinceLastChecked() const
-{
- return m_LastCheck.elapsed();
-}
-
-QStringList PluginList::pluginNames() const
-{
- QStringList result;
-
- for (const ESPInfo &info : m_ESPs) {
- result.append(info.name);
- }
-
- return result;
-}
-
-IPluginList::PluginStates PluginList::state(const QString &name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return IPluginList::STATE_MISSING;
- } else {
- return m_ESPs[iter->second].enabled ? IPluginList::STATE_ACTIVE : IPluginList::STATE_INACTIVE;
- }
-}
-
-void PluginList::setState(const QString &name, PluginStates state) {
- auto iter = m_ESPsByName.find(name);
- if (iter != m_ESPsByName.end()) {
- m_ESPs[iter->second].enabled = (state == IPluginList::STATE_ACTIVE) ||
- m_ESPs[iter->second].forceEnabled;
- } else {
- log::warn("Plugin not found: {}", name);
- }
-}
-
-void PluginList::setLoadOrder(const QStringList &pluginList)
-{
- for (ESPInfo &info : m_ESPs) {
- info.priority = -1;
- }
- int maxPriority = 0;
- for (const QString &plugin : pluginList) {
- auto iter = m_ESPsByName.find(plugin);
- if (iter !=m_ESPsByName.end()) {
- m_ESPs[iter->second].priority = maxPriority++;
- }
- }
-
- // use old priorities
- for (ESPInfo &info : m_ESPs) {
- if (info.priority == -1) {
- info.priority = maxPriority++;
- }
- }
- updateIndices();
-}
-
-int PluginList::priority(const QString &name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return -1;
- } else {
- return m_ESPs[iter->second].priority;
- }
-}
-
-bool PluginList::setPriority(const QString& name, int newPriority) {
-
- if (newPriority < 0 || newPriority >= static_cast<int>(m_ESPsByPriority.size())) {
- return false;
- }
-
- auto oldPriority = priority(name);
- if (oldPriority == -1) {
- return false;
- }
-
- int rowIndex = findPluginByPriority(oldPriority);
-
- // We need to increment newPriority if its above the old one, otherwise the
- // plugin is place right below the new priority.
- if (oldPriority < newPriority) {
- newPriority += 1;
- }
- changePluginPriority({ rowIndex }, newPriority);
-
- return true;
-}
-
-int PluginList::loadOrder(const QString &name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return -1;
- } else {
- return m_ESPs[iter->second].loadOrder;
- }
-}
-
-QStringList PluginList::masters(const QString &name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return QStringList();
- } else {
- QStringList result;
- for (const QString &master : m_ESPs[iter->second].masters) {
- result.append(master);
- }
- return result;
- }
-}
-
-QString PluginList::origin(const QString &name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return QString();
- } else {
- return m_ESPs[iter->second].originName;
- }
-}
-
-bool PluginList::hasMasterExtension(const QString& name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return false;
- }
- else {
- return m_ESPs[iter->second].hasMasterExtension;
- }
-}
-
-bool PluginList::hasLightExtension(const QString& name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return false;
- }
- else {
- return m_ESPs[iter->second].hasLightExtension;
- }
-}
-
-bool PluginList::isMasterFlagged(const QString& name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return false;
- }
- else {
- return m_ESPs[iter->second].isMasterFlagged;
- }
-}
-
-bool PluginList::isLightFlagged(const QString& name) const
-{
- auto iter = m_ESPsByName.find(name);
- if (iter == m_ESPsByName.end()) {
- return false;
- }
- else {
- return m_ESPs[iter->second].isLightFlagged;
- }
-}
-
-boost::signals2::connection PluginList::onPluginStateChanged(const std::function<void(const std::map<QString, PluginStates>&)>& func)
-{
- return m_PluginStateChanged.connect(func);
-}
-
-void PluginList::pluginStatesChanged(QStringList const& pluginNames, PluginStates state) const {
- if (pluginNames.isEmpty()) {
- return;
- }
- std::map<QString, IPluginList::PluginStates> infos;
- for (auto& name : pluginNames) {
- infos[name] = state;
- }
- m_PluginStateChanged(infos);
-}
-
-boost::signals2::connection PluginList::onRefreshed(const std::function<void ()> &callback)
-{
- return m_Refreshed.connect(callback);
-}
-
-boost::signals2::connection PluginList::onPluginMoved(const std::function<void (const QString &, int, int)> &func)
-{
- return m_PluginMoved.connect(func);
-}
-
-void PluginList::updateIndices()
-{
- m_ESPsByName.clear();
- m_ESPsByPriority.clear();
- m_ESPsByPriority.resize(m_ESPs.size());
- for (unsigned int i = 0; i < m_ESPs.size(); ++i) {
- if (m_ESPs[i].priority < 0) {
- continue;
- }
- if (m_ESPs[i].priority >= static_cast<int>(m_ESPs.size())) {
- log::error("invalid plugin priority: {}", m_ESPs[i].priority);
- continue;
- }
- m_ESPsByName[m_ESPs[i].name] = i;
- m_ESPsByPriority.at(static_cast<size_t>(m_ESPs[i].priority)) = i;
- }
-
- generatePluginIndexes();
-}
-
-void PluginList::generatePluginIndexes()
-{
- int numESLs = 0;
- int numSkipped = 0;
-
- GamePlugins* gamePlugins = m_GamePlugin->feature<GamePlugins>();
- const bool lightPluginsSupported = gamePlugins ? gamePlugins->lightPluginsAreSupported() : false;
-
- for (int l = 0; l < m_ESPs.size(); ++l) {
- int i = m_ESPsByPriority.at(l);
- if (!m_ESPs[i].enabled) {
- m_ESPs[i].index = QString();
- ++numSkipped;
- continue;
- }
- if (lightPluginsSupported && (m_ESPs[i].hasLightExtension || m_ESPs[i].isLightFlagged)) {
- int ESLpos = 254 + ((numESLs + 1) / 4096);
- m_ESPs[i].index = QString("%1:%2").arg(ESLpos, 2, 16, QChar('0')).arg((numESLs) % 4096, 3, 16, QChar('0')).toUpper();
- ++numESLs;
- } else {
- m_ESPs[i].index = QString("%1").arg(l - numESLs - numSkipped, 2, 16, QChar('0')).toUpper();
- }
- }
- emit esplist_changed();
-}
-
-int PluginList::rowCount(const QModelIndex &parent) const
-{
- if (!parent.isValid()) {
- return static_cast<int>(m_ESPs.size());
- } else {
- return 0;
- }
-}
-
-int PluginList::columnCount(const QModelIndex &) const
-{
- return COL_LASTCOLUMN + 1;
-}
-
-void PluginList::testMasters()
-{
- std::set<QString, FileNameComparator> enabledMasters;
- for (const auto& iter: m_ESPs) {
- if (iter.enabled) {
- enabledMasters.insert(iter.name);
- }
- }
-
- for (auto& iter: m_ESPs) {
- iter.masterUnset.clear();
- if (iter.enabled) {
- for (const auto& master: iter.masters) {
- if (enabledMasters.find(master) == enabledMasters.end()) {
- iter.masterUnset.insert(master);
- }
- }
- }
- }
-}
-
-QVariant PluginList::data(const QModelIndex &modelIndex, int role) const
-{
- int index = modelIndex.row();
-
- if ((role == Qt::DisplayRole) || (role == Qt::EditRole)) {
- return displayData(modelIndex);
- } else if ((role == Qt::CheckStateRole) && (modelIndex.column() == 0)) {
- return checkstateData(modelIndex);
- } else if (role == Qt::ForegroundRole) {
- return foregroundData(modelIndex);
- } else if (role == Qt::BackgroundRole) {
- return backgroundData(modelIndex);
- } else if (role == Qt::FontRole) {
- return fontData(modelIndex);
- } else if (role == Qt::TextAlignmentRole) {
- return alignmentData(modelIndex);
- } else if (role == Qt::ToolTipRole) {
- return tooltipData(modelIndex);
- } else if (role == Qt::UserRole + 1) {
- return iconData(modelIndex);
- }
- return QVariant();
-}
-
-QVariant PluginList::displayData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
-
- switch (modelIndex.column())
- {
- case COL_NAME:
- return m_ESPs[index].name;
-
- case COL_PRIORITY:
- return m_ESPs[index].priority;
-
- case COL_MODINDEX:
- return m_ESPs[index].index;
-
- default:
- return {};
- }
-}
-
-QVariant PluginList::checkstateData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
-
- if (m_ESPs[index].forceEnabled) {
- return {};
- }
-
- return m_ESPs[index].enabled ? Qt::Checked : Qt::Unchecked;
-}
-
-QVariant PluginList::foregroundData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
-
- if ((modelIndex.column() == COL_NAME) && m_ESPs[index].forceEnabled) {
- return QBrush(Qt::gray);
- }
-
- return {};
-}
-
-QVariant PluginList::backgroundData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
-
- if (m_ESPs[index].modSelected) {
- return Settings::instance().colors().pluginListContained();
- }
-
- return {};
-}
-
-QVariant PluginList::fontData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
-
- QFont result;
-
- if (m_ESPs[index].hasMasterExtension ||
- m_ESPs[index].isMasterFlagged ||
- m_ESPs[index].hasLightExtension) {
- result.setItalic(true);
- result.setWeight(QFont::Bold);
- } else if (m_ESPs[index].isLightFlagged) {
- result.setItalic(true);
- }
-
- return result;
-}
-
-QVariant PluginList::alignmentData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
-
- if (modelIndex.column() == 0) {
- return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
- } else {
- return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
- }
-}
-
-QVariant PluginList::tooltipData(const QModelIndex &modelIndex) const
-{
- const int index = modelIndex.row();
- const auto& esp = m_ESPs[index];
-
- QString toolTip;
-
- toolTip += "<b>" + tr("Origin") + "</b>: " + esp.originName;
-
- if (esp.forceEnabled) {
- toolTip +=
- "<br><b><i>" +
- tr("This plugin can't be disabled (enforced by the game).") +
- "</i></b>";
- } else {
- if (!esp.author.isEmpty()) {
- toolTip +=
- "<br><b>" + tr("Author") + "</b>: " +
- TruncateString(esp.author);
- }
-
- if (esp.description.size() > 0) {
- toolTip +=
- "<br><b>" + tr("Description") + "</b>: " +
- TruncateString(esp.description);
- }
-
- if (esp.masterUnset.size() > 0) {
- toolTip +=
- "<br><b>" + tr("Missing Masters") + "</b>: " +
- "<b>" + TruncateString(QStringList(esp.masterUnset.begin(), esp.masterUnset.end()).join(", ")) + "</b>";
- }
-
- std::set<QString> enabledMasters;
- std::set_difference(esp.masters.begin(), esp.masters.end(),
- esp.masterUnset.begin(), esp.masterUnset.end(),
- std::inserter(enabledMasters, enabledMasters.end()));
-
- if (!enabledMasters.empty()) {
- toolTip +=
- "<br><b>" + tr("Enabled Masters") + "</b>: " +
- TruncateString(SetJoin(enabledMasters, ", "));
- }
-
- if (!esp.archives.empty()) {
- toolTip +=
- "<br><b>" + tr("Loads Archives") + "</b>: " +
- TruncateString(QStringList(esp.archives.begin(), esp.archives.end()).join(", ")) +
- "<br>" + tr(
- "There are Archives connected to this plugin. Their assets will be "
- "added to your game, overwriting in case of conflicts following the "
- "plugin order. Loose files will always overwrite assets from "
- "Archives. (This flag only checks for Archives from the same mod as "
- "the plugin)");
- }
-
- if (esp.hasIni) {
- toolTip +=
- "<br><b>" + tr("Loads INI settings") + "</b>: "
- "<br>" + tr(
- "There is an ini file connected to this plugin. Its settings will "
- "be added to your game settings, overwriting in case of conflicts.");
- }
-
- if (esp.isLightFlagged && !esp.hasLightExtension) {
- toolTip +=
- "<br><br>" + tr(
- "This ESP is flagged as an ESL. It will adhere to the ESP load "
- "order but the records will be loaded in ESL space.");
- }
- }
-
-
- // additional info
- auto itor = m_AdditionalInfo.find(esp.name);
-
- if (itor != m_AdditionalInfo.end()) {
- if (!itor->second.messages.isEmpty()) {
- toolTip += "<hr><ul style=\"margin-left:15px; -qt-list-indent: 0;\">";
-
- for (auto&& message : itor->second.messages) {
- toolTip += "<li>" + message + "</li>";
- }
-
- toolTip += "</ul>";
- }
-
- // loot
- toolTip += makeLootTooltip(itor->second.loot);
- }
-
- return toolTip;
-}
-
-QString PluginList::makeLootTooltip(const Loot::Plugin& loot) const
-{
- QString s;
-
- for (auto&& f : loot.incompatibilities) {
- s +=
- "<li>" + tr("Incompatible with %1")
- .arg(f.displayName.isEmpty() ? f.name : f.displayName) +
- "</li>";
- }
-
- for (auto&& m : loot.missingMasters) {
- s += "<li>" + tr("Depends on missing %1").arg(m) + "</li>";
- }
-
- for (auto&& m : loot.messages) {
- s += "<li>";
-
- switch (m.type)
- {
- case log::Warning:
- s += tr("Warning") + ": ";
- break;
-
- case log::Error:
- s += tr("Error") + ": ";
- break;
-
- case log::Info: // fall-through
- case log::Debug:
- default:
- // nothing
- break;
- }
-
- s += m.text + "</li>";
- }
-
- for (auto&& d : loot.dirty) {
- s += "<li>" + d.toString(false) + "</li>";
- }
-
- for (auto&& c : loot.clean) {
- s += "<li>" + c.toString(true) + "</li>";
- }
-
- if (!s.isEmpty()) {
- s =
- "<hr>"
- "<ul style=\"margin-top:0px; padding-top:0px; margin-left:15px; -qt-list-indent: 0;\">" +
- s +
- "</ul>";
- }
-
- return s;
-}
-
-QVariant PluginList::iconData(const QModelIndex &modelIndex) const
-{
- int index = modelIndex.row();
-
- QVariantList result;
-
- const auto& esp = m_ESPs[index];
-
- auto infoItor = m_AdditionalInfo.find(esp.name);
-
- const AdditionalInfo* info = nullptr;
- if (infoItor != m_AdditionalInfo.end()) {
- info = &infoItor->second;
- }
-
- if (isProblematic(esp, info)) {
- result.append(":/MO/gui/warning");
- }
-
- if (m_LockedOrder.find(esp.name) != m_LockedOrder.end()) {
- result.append(":/MO/gui/locked");
- }
-
- if (hasInfo(esp, info)) {
- result.append(":/MO/gui/information");
- }
-
- if (esp.hasIni) {
- result.append(":/MO/gui/attachment");
- }
-
- if (!esp.archives.empty()) {
- result.append(":/MO/gui/archive_conflict_neutral");
- }
-
- if (esp.isLightFlagged && !esp.hasLightExtension) {
- result.append(":/MO/gui/awaiting");
- }
-
- if (info && !info->loot.dirty.empty()) {
- result.append(":/MO/gui/edit_clear");
- }
-
- return result;
-}
-
-bool PluginList::isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const
-{
- if (esp.masterUnset.size() > 0) {
- return true;
- }
-
- if (info) {
- if (!info->loot.incompatibilities.empty()) {
- return true;
- }
-
- if (!info->loot.missingMasters.empty()) {
- return true;
- }
- }
-
- return false;
-}
-
-bool PluginList::hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const
-{
- if (info) {
- if (!info->messages.empty()) {
- return true;
- }
-
- if (!info->loot.messages.empty()) {
- return true;
- }
- }
-
- return false;
-}
-
-bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int role)
-{
- QString modName = modIndex.data().toString();
- IPluginList::PluginStates oldState = state(modName);
-
- bool result = false;
-
- if (role == Qt::CheckStateRole) {
- m_ESPs[modIndex.row()].enabled =
- value.toInt() == Qt::Checked || m_ESPs[modIndex.row()].forceEnabled;
- m_LastCheck.restart();
- emit dataChanged(modIndex, modIndex);
-
- refreshLoadOrder();
- emit writePluginsList();
-
- result = true;
- } else if (role == Qt::EditRole) {
- if (modIndex.column() == COL_PRIORITY) {
- bool ok = false;
- int newPriority = value.toInt(&ok);
- if (ok) {
- setPluginPriority(modIndex.row(), newPriority);
- result = true;
- }
- refreshLoadOrder();
- emit writePluginsList();
- }
- }
-
- IPluginList::PluginStates newState = state(modName);
- if (oldState != newState) {
- try {
- pluginStatesChanged({ modName }, newState);
- testMasters();
- emit dataChanged(
- this->index(0, 0),
- this->index(static_cast<int>(m_ESPs.size()), columnCount()));
- } catch (const std::exception &e) {
- log::error("failed to invoke state changed notification: {}", e.what());
- } catch (...) {
- log::error("failed to invoke state changed notification: unknown exception");
- }
- }
-
- return result;
-}
-
-QVariant PluginList::headerData(int section, Qt::Orientation orientation,
- int role) const
-{
- if (orientation == Qt::Horizontal) {
- if (role == Qt::DisplayRole) {
- return getColumnName(section);
- } else if (role == Qt::ToolTipRole) {
- return getColumnToolTip(section);
- }
- }
- return QAbstractItemModel::headerData(section, orientation, role);
-}
-
-Qt::ItemFlags PluginList::flags(const QModelIndex &modelIndex) const
-{
- int index = modelIndex.row();
- Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex);
-
- if (modelIndex.isValid()) {
- if (!m_ESPs[index].forceEnabled) {
- result |= Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled;
- }
- if (modelIndex.column() == COL_PRIORITY) {
- result |= Qt::ItemIsEditable;
- }
- result &= ~Qt::ItemIsDropEnabled;
- } else {
- result |= Qt::ItemIsDropEnabled;
- }
-
- return result;
-}
-
-void PluginList::setPluginPriority(int row, int &newPriority, bool isForced)
-{
- int newPriorityTemp = newPriority;
-
- // enforce valid range
- if (newPriorityTemp < 0)
- newPriorityTemp = 0;
- else if (newPriorityTemp >= static_cast<int>(m_ESPsByPriority.size()))
- newPriorityTemp = static_cast<int>(m_ESPsByPriority.size()) - 1;
-
- if (!m_ESPs[row].isMasterFlagged &&
- !m_ESPs[row].hasLightExtension &&
- !m_ESPs[row].hasMasterExtension) {
- // don't allow esps to be moved above esms
- while ((newPriorityTemp < static_cast<int>(m_ESPsByPriority.size() - 1)) &&
- (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMasterFlagged ||
- m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasLightExtension ||
- m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasMasterExtension)) {
- ++newPriorityTemp;
- }
- } else {
- // don't allow esms to be moved below esps
- while ((newPriorityTemp > 0) &&
- !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMasterFlagged &&
- !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasLightExtension &&
- !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasMasterExtension) {
- --newPriorityTemp;
- }
- // also don't allow "regular" esms to be moved above primary plugins
- while ((newPriorityTemp < static_cast<int>(m_ESPsByPriority.size() - 1)) &&
- (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).forceEnabled)) {
- ++newPriorityTemp;
- }
- }
-
- int oldPriority = m_ESPs.at(row).priority;
- if (newPriorityTemp < oldPriority) { // moving up
- // don't allow plugins to be moved above their masters
- for (auto master : m_ESPs[row].masters) {
- auto iter = m_ESPsByName.find(master);
- if (iter != m_ESPsByName.end()) {
- int masterPriority = m_ESPs[iter->second].priority;
- if (masterPriority >= newPriorityTemp)
- {
- newPriorityTemp = masterPriority + 1;
- }
- }
- }
- }
- else if (newPriorityTemp > oldPriority) { // moving down
- // don't allow masters to be moved below their children
- for (int i = oldPriority + 1; i <= newPriorityTemp; i++) {
- PluginList::ESPInfo* otherInfo = &m_ESPs.at(m_ESPsByPriority[i]);
- for (auto master : otherInfo->masters) {
- if (master.compare(m_ESPs[row].name, Qt::CaseInsensitive) == 0) {
- newPriorityTemp = otherInfo->priority - 1;
- break;
- }
- }
- }
- }
-
- try {
- if (newPriorityTemp != oldPriority) {
- if (newPriorityTemp > oldPriority) {
- // priority is higher than the old, so the gap we left is in lower priorities
- for (int i = oldPriority + 1; i <= newPriorityTemp; ++i) {
- --m_ESPs.at(m_ESPsByPriority.at(i)).priority;
- }
- emit dataChanged(index(oldPriority + 1, 0), index(newPriorityTemp, columnCount()));
- }
- else {
- for (int i = newPriorityTemp; i < oldPriority; ++i) {
- ++m_ESPs.at(m_ESPsByPriority.at(i)).priority;
- }
- emit dataChanged(index(newPriorityTemp, 0), index(oldPriority - 1, columnCount()));
- ++newPriority;
- }
-
- m_ESPs.at(row).priority = newPriorityTemp;
- emit dataChanged(index(row, 0), index(row, columnCount()));
- m_PluginMoved(m_ESPs[row].name, oldPriority, newPriorityTemp);
- }
- } catch (const std::out_of_range&) {
- reportError(tr("failed to restore load order for %1").arg(m_ESPs[row].name));
- }
-
- updateIndices();
-}
-
-void PluginList::changePluginPriority(std::vector<int> rows, int newPriority)
-{
- ChangeBracket<PluginList> layoutChange(this);
- const std::vector<ESPInfo> &esp = m_ESPs;
-
- int minPriority = INT_MAX;
- int maxPriority = INT_MIN;
-
- // don't try to move plugins before force-enabled plugins
- for (std::vector<ESPInfo>::const_iterator iter = m_ESPs.begin();
- iter != m_ESPs.end(); ++iter) {
- if (iter->forceEnabled) {
- newPriority = std::max(newPriority, iter->priority + 1);
- }
- maxPriority = std::max(maxPriority, iter->priority + 1);
- minPriority = std::min(minPriority, iter->priority);
- }
-
- // limit the new priority to existing priorities
- newPriority = std::min(newPriority, maxPriority);
- newPriority = std::max(newPriority, minPriority);
-
- // sort the moving plugins by ascending priorities
- std::sort(rows.begin(), rows.end(),
- [&esp](const int &LHS, const int &RHS) {
- return esp[LHS].priority < esp[RHS].priority;
- });
-
- // if at least on plugin is increasing in priority, the target index is
- // that of the row BELOW the dropped location, otherwise it's the one above
- for (std::vector<int>::const_iterator iter = rows.begin();
- iter != rows.end(); ++iter) {
- if (m_ESPs[*iter].priority < newPriority) {
- --newPriority;
- break;
- }
- }
-
- for (std::vector<int>::const_iterator iter = rows.begin(); iter != rows.end(); ++iter) {
- setPluginPriority(*iter, newPriority);
- }
-
- layoutChange.finish();
- refreshLoadOrder();
- emit writePluginsList();
-}
-
-bool PluginList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent)
-{
- if (action == Qt::IgnoreAction) {
- return true;
- }
-
- QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist");
- QDataStream stream(&encoded, QIODevice::ReadOnly);
-
- std::vector<int> sourceRows;
-
- while (!stream.atEnd()) {
- int sourceRow, col;
- QMap<int, QVariant> roleDataMap;
- stream >> sourceRow >> col >> roleDataMap;
- if (col == 0) { // only add each row once
- sourceRows.push_back(sourceRow);
- }
- }
-
- if (row == -1) {
- row = parent.row();
- }
-
- int newPriority;
-
- if ((row < 0) ||
- (row >= static_cast<int>(m_ESPs.size()))) {
- newPriority = static_cast<int>(m_ESPs.size());
- } else {
- newPriority = m_ESPs[row].priority;
- }
- changePluginPriority(sourceRows, newPriority);
-
- return false;
-}
-
-QModelIndex PluginList::index(int row, int column, const QModelIndex&) const
-{
- if ((row < 0) || (row >= rowCount()) || (column < 0) || (column >= columnCount())) {
- return QModelIndex();
- }
- return createIndex(row, column, row);
-}
-
-QModelIndex PluginList::parent(const QModelIndex&) const
-{
- return QModelIndex();
-}
-
-PluginList::ESPInfo::ESPInfo(const QString &name, bool enabled,
- const QString &originName, const QString &fullPath,
- bool hasIni, std::set<QString> archives, bool lightPluginsAreSupported)
- : name(name), fullPath(fullPath), enabled(enabled), forceEnabled(enabled),
- priority(0), loadOrder(-1), originName(originName), hasIni(hasIni),
- archives(archives.begin(), archives.end()), modSelected(false)
-{
- try {
- ESP::File file(ToWString(fullPath));
- auto extension = name.right(3).toLower();
- hasMasterExtension = (extension == "esm");
- hasLightExtension = lightPluginsAreSupported && (extension == "esl");
- isMasterFlagged = file.isMaster();
- isLightFlagged = lightPluginsAreSupported && file.isLight();
-
- author = QString::fromLatin1(file.author().c_str());
- description = QString::fromLatin1(file.description().c_str());
-
- for (auto&& m : file.masters()) {
- masters.insert(QString::fromStdString(m));
- }
- } catch (const std::exception &e) {
- log::error("failed to parse plugin file {}: {}", fullPath, e.what());
- hasMasterExtension = false;
- hasLightExtension = false;
- isMasterFlagged = false;
- isLightFlagged = false;
- }
-}
-
-void PluginList::managedGameChanged(const IPluginGame *gamePlugin)
-{
- m_GamePlugin = gamePlugin;
-}
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "pluginlist.h"
+#include "settings.h"
+#include "scopeguard.h"
+#include "modinfo.h"
+#include "modlist.h"
+#include "viewmarkingscrollbar.h"
+#include "shared/directoryentry.h"
+#include "shared/filesorigin.h"
+#include "shared/fileentry.h"
+
+#include <utility.h>
+#include <iplugingame.h>
+#include <espfile.h>
+#include <report.h>
+#include "shared/windows_error.h"
+#include <safewritefile.h>
+#include <gameplugins.h>
+
+#include <QtDebug>
+#include <QMessageBox>
+#include <QMimeData>
+#include <QCoreApplication>
+#include <QDateTime>
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <QFileInfo>
+#include <QListWidgetItem>
+#include <QRegularExpression>
+#include <QString>
+#include <QApplication>
+#include <QKeyEvent>
+#include <QSortFilterProxyModel>
+
+#include <ctime>
+#include <algorithm>
+#include <stdexcept>
+
+#include "organizercore.h"
+
+using namespace MOBase;
+using namespace MOShared;
+
+
+static QString TruncateString(const QString& text)
+{
+ QString new_text = text;
+
+ if (new_text.length() > 1024) {
+ new_text.truncate(1024);
+ new_text += "...";
+ }
+
+ return new_text;
+}
+
+
+PluginList::PluginList(OrganizerCore& organizer)
+ : QAbstractItemModel(&organizer)
+ , m_Organizer(organizer)
+ , m_FontMetrics(QFont())
+{
+ connect(this, SIGNAL(writePluginsList()), this, SLOT(generatePluginIndexes()));
+ m_LastCheck.start();
+}
+
+PluginList::~PluginList()
+{
+ m_Refreshed.disconnect_all_slots();
+ m_PluginMoved.disconnect_all_slots();
+ m_PluginStateChanged.disconnect_all_slots();
+}
+
+QString PluginList::getColumnName(int column)
+{
+ switch (column) {
+ case COL_NAME: return tr("Name");
+ case COL_PRIORITY: return tr("Priority");
+ case COL_MODINDEX: return tr("Mod Index");
+ case COL_FLAGS: return tr("Flags");
+ default: return tr("unknown");
+ }
+}
+
+
+QString PluginList::getColumnToolTip(int column)
+{
+ switch (column) {
+ case COL_NAME: return tr("Name of the plugin");
+ case COL_FLAGS: return tr("Emblems to highlight things that might require attention.");
+ case COL_PRIORITY: return tr("Load priority of plugins. The higher, the more \"important\" it is and thus "
+ "overwrites data from plugins with lower priority.");
+ case COL_MODINDEX: return tr("Determines the formids of objects originating from this mods.");
+ default: return tr("unknown");
+ }
+}
+
+void PluginList::highlightPlugins(
+ const std::vector<unsigned int>& modIndices,
+ const MOShared::DirectoryEntry &directoryEntry)
+{
+ auto* profile = m_Organizer.currentProfile();
+
+ for (auto &esp : m_ESPs) {
+ esp.modSelected = false;
+ }
+
+ for (auto& modIndex : modIndices) {
+ ModInfo::Ptr selectedMod = ModInfo::getByIndex(modIndex);
+ if (!selectedMod.isNull() && profile->modEnabled(modIndex)) {
+ QDir dir(selectedMod->absolutePath());
+ QStringList plugins = dir.entryList(QStringList() << "*.esp" << "*.esm" << "*.esl");
+ const MOShared::FilesOrigin& origin = directoryEntry.getOriginByName(selectedMod->internalName().toStdWString());
+ if (plugins.size() > 0) {
+ for (auto plugin : plugins) {
+ MOShared::FileEntryPtr file = directoryEntry.findFile(plugin.toStdWString());
+ if (file && file->getOrigin() != origin.getID()) {
+ const auto alternatives = file->getAlternatives();
+ if (std::find_if(alternatives.begin(), alternatives.end(), [&](const FileAlternative& element) { return element.originID() == origin.getID(); }) == alternatives.end())
+ continue;
+ }
+ std::map<QString, int>::iterator iter = m_ESPsByName.find(plugin);
+ if (iter != m_ESPsByName.end()) {
+ m_ESPs[iter->second].modSelected = true;
+ }
+ }
+ }
+ }
+ }
+
+ emit dataChanged(this->index(0, 0), this->index(static_cast<int>(m_ESPs.size()) - 1, this->columnCount() - 1));
+}
+
+void PluginList::refresh(const QString &profileName
+ , const DirectoryEntry &baseDirectory
+ , const QString &lockedOrderFile
+ , bool force)
+{
+ TimeThis tt("PluginList::refresh()");
+
+ if (force) {
+ m_ESPs.clear();
+ m_ESPsByName.clear();
+ m_ESPsByPriority.clear();
+ }
+
+ ChangeBracket<PluginList> layoutChange(this);
+
+ QStringList primaryPlugins = m_GamePlugin->primaryPlugins();
+ GamePlugins *gamePlugins = m_GamePlugin->feature<GamePlugins>();
+ const bool lightPluginsAreSupported = gamePlugins ? gamePlugins->lightPluginsAreSupported() : false;
+
+ m_CurrentProfile = profileName;
+
+ QStringList availablePlugins;
+
+ std::vector<FileEntryPtr> files = baseDirectory.getFiles();
+ for (FileEntryPtr current : files) {
+ if (current.get() == nullptr) {
+ continue;
+ }
+ QString filename = ToQString(current->getName());
+
+ if (filename.endsWith(".esp", Qt::CaseInsensitive) ||
+ filename.endsWith(".esm", Qt::CaseInsensitive) ||
+ filename.endsWith(".esl", Qt::CaseInsensitive)) {
+
+ availablePlugins.append(filename);
+
+ if (m_ESPsByName.find(filename) != m_ESPsByName.end()) {
+ continue;
+ }
+
+ bool forceEnabled = Settings::instance().game().forceEnableCoreFiles() &&
+ primaryPlugins.contains(filename, Qt::CaseInsensitive);
+
+ bool archive = false;
+ try {
+ FilesOrigin &origin = baseDirectory.getOriginByID(current->getOrigin(archive));
+
+ //name without extension
+ QString baseName = QFileInfo(filename).completeBaseName();
+
+ QString iniPath = baseName + ".ini";
+ bool hasIni = baseDirectory.findFile(ToWString(iniPath)).get() != nullptr;
+ std::set<QString> loadedArchives;
+ QString candidateName;
+ for (FileEntryPtr archiveCandidate : files) {
+ candidateName = ToQString(archiveCandidate->getName());
+ if (candidateName.startsWith(baseName, Qt::CaseInsensitive) &&
+ (candidateName.endsWith(".bsa", Qt::CaseInsensitive) ||
+ candidateName.endsWith(".ba2", Qt::CaseInsensitive))) {
+ loadedArchives.insert(candidateName);
+ }
+ }
+
+ QString originName = ToQString(origin.getName());
+ unsigned int modIndex = ModInfo::getIndex(originName);
+ if (modIndex != UINT_MAX) {
+ ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex);
+ originName = modInfo->name();
+ }
+
+ m_ESPs.push_back(ESPInfo(filename, forceEnabled, originName, ToQString(current->getFullPath()), hasIni, loadedArchives, lightPluginsAreSupported));
+ m_ESPs.rbegin()->priority = -1;
+ } catch (const std::exception &e) {
+ reportError(tr("failed to update esp info for file %1 (source id: %2), error: %3").arg(filename).arg(current->getOrigin(archive)).arg(e.what()));
+ }
+ }
+ }
+
+ for (const auto &espName : m_ESPsByName) {
+ if (!availablePlugins.contains(espName.first, Qt::CaseInsensitive)) {
+ m_ESPs[espName.second].name = "";
+ }
+ }
+
+ m_ESPs.erase(std::remove_if(m_ESPs.begin(), m_ESPs.end(),
+ [](const ESPInfo &info) -> bool {
+ return info.name.isEmpty();
+ }),
+ m_ESPs.end());
+
+ fixPriorities();
+
+ // functions in GamePlugins will use the IPluginList interface of this, so
+ // indices need to work. priority will be off however
+ updateIndices();
+
+ if (gamePlugins) {
+ gamePlugins->readPluginLists(m_Organizer.managedGameOrganizer()->pluginList());
+ }
+
+ fixPrimaryPlugins();
+ fixPluginRelationships();
+
+ testMasters();
+
+ updateIndices();
+
+ readLockedOrderFrom(lockedOrderFile);
+
+ layoutChange.finish();
+
+ refreshLoadOrder();
+ emit dataChanged(this->index(0, 0),
+ this->index(static_cast<int>(m_ESPs.size()), columnCount()));
+
+ m_Refreshed();
+}
+
+void PluginList::fixPrimaryPlugins()
+{
+ if (!m_Organizer.settings().game().forceEnableCoreFiles()) {
+ return;
+ }
+
+ // This function ensures that the primary plugins are first and in the correct order
+ QStringList primaryPlugins = m_Organizer.managedGame()->primaryPlugins();
+ int prio = 0;
+ bool somethingChanged = false;
+ for (QString plugin : primaryPlugins) {
+ std::map<QString, int>::iterator iter = m_ESPsByName.find(plugin);
+ // Plugin is present?
+ if (iter != m_ESPsByName.end()) {
+ if (prio != m_ESPs[iter->second].priority) {
+ // Priority is wrong! Fix it!
+ int newPrio = prio;
+ setPluginPriority(iter->second, newPrio, true /* isForced */);
+ somethingChanged = true;
+ }
+ prio++;
+ }
+ }
+
+ if (somethingChanged) {
+ writePluginsList();
+ }
+}
+
+void PluginList::fixPluginRelationships()
+{
+ TimeThis timer("PluginList::fixPluginRelationships");
+
+ // Count the types of plugins
+ int masterCount = 0;
+ for (auto plugin : m_ESPs) {
+ if (plugin.hasLightExtension ||
+ plugin.hasMasterExtension ||
+ plugin.isMasterFlagged) {
+ masterCount++;
+ }
+ }
+
+ // Ensure masters are up top and normal plugins are down below
+ for (int i = 0; i < m_ESPs.size(); i++) {
+ ESPInfo& plugin = m_ESPs[i];
+ if (plugin.hasLightExtension ||
+ plugin.hasMasterExtension ||
+ plugin.isMasterFlagged) {
+ if (plugin.priority > masterCount) {
+ int newPriority = masterCount + 1;
+ setPluginPriority(i, newPriority);
+ }
+ }
+ else {
+ if (plugin.priority < masterCount) {
+ int newPriority = masterCount + 1;
+ setPluginPriority(i, newPriority);
+ }
+ }
+ }
+
+ // Ensure master/child relationships are observed
+ for (int i = 0; i < m_ESPs.size(); i++) {
+ ESPInfo& plugin = m_ESPs[i];
+ int newPriority = plugin.priority;
+ for (auto master : plugin.masters) {
+ auto iter = m_ESPsByName.find(master);
+ if (iter != m_ESPsByName.end()) {
+ newPriority = std::max(newPriority, m_ESPs[iter->second].priority);
+ }
+ }
+ if (newPriority != plugin.priority) {
+ setPluginPriority(i, newPriority);
+ }
+ }
+}
+
+void PluginList::fixPriorities()
+{
+ std::vector<std::pair<int, int>> espPrios;
+
+ for (int i = 0; i < m_ESPs.size(); ++i) {
+ int prio = m_ESPs[i].priority;
+ if (prio == -1) {
+ prio = INT_MAX;
+ }
+ espPrios.push_back(std::make_pair(prio, i));
+ }
+
+ std::sort(espPrios.begin(), espPrios.end(),
+ [](const std::pair<int, int> &lhs, const std::pair<int, int> &rhs) {
+ return lhs.first < rhs.first;
+ });
+
+ for (int i = 0; i < espPrios.size(); ++i) {
+ m_ESPs[espPrios[i].second].priority = i;
+ }
+}
+
+void PluginList::enableESP(const QString &name, bool enable)
+{
+ std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
+
+ if (iter != m_ESPsByName.end()) {
+ auto enabled = m_ESPs[iter->second].enabled;
+ m_ESPs[iter->second].enabled =
+ enable || m_ESPs[iter->second].forceEnabled;
+
+ emit writePluginsList();
+ if (enabled != m_ESPs[iter->second].enabled) {
+ pluginStatesChanged({ name }, state(name));
+ }
+ } else {
+ reportError(tr("Plugin not found: %1").arg(qUtf8Printable(name)));
+ }
+}
+
+int PluginList::findPluginByPriority(int priority)
+{
+ for (int i = 0; i < m_ESPs.size(); i++ ) {
+ if (m_ESPs[i].priority == priority) {
+ return i;
+ }
+ }
+ log::error("No plugin with priority {}", priority);
+ return -1;
+}
+
+void PluginList::setEnabled(const QModelIndexList& indices, bool enabled)
+{
+ QStringList dirty;
+ for (auto& idx : indices) {
+ if (m_ESPs[idx.row()].enabled != enabled) {
+ m_ESPs[idx.row()].enabled = enabled;
+ dirty.append(m_ESPs[idx.row()].name);
+ }
+ }
+ if (!dirty.isEmpty()) {
+ emit writePluginsList();
+ pluginStatesChanged(dirty,
+ enabled ? IPluginList::PluginState::STATE_ACTIVE : IPluginList::PluginState::STATE_INACTIVE);
+ }
+}
+
+void PluginList::setEnabledAll(bool enabled)
+{
+ QStringList dirty;
+ for (ESPInfo &info : m_ESPs) {
+ if (info.enabled != enabled) {
+ info.enabled = enabled;
+ dirty.append(info.name);
+ }
+ }
+ if (!dirty.isEmpty()) {
+ emit writePluginsList();
+ pluginStatesChanged(dirty,
+ enabled ? IPluginList::PluginState::STATE_ACTIVE : IPluginList::PluginState::STATE_INACTIVE);
+ }
+}
+
+void PluginList::sendToPriority(const QModelIndexList& indices, int newPriority)
+{
+ std::vector<int> pluginsToMove;
+ for (auto& idx : indices) {
+ if (!m_ESPs[idx.row()].forceEnabled) {
+ pluginsToMove.push_back(idx.row());
+ }
+ }
+ if (pluginsToMove.size()) {
+ changePluginPriority(pluginsToMove, newPriority);
+ }
+}
+
+void PluginList::shiftPluginsPriority(const QModelIndexList& indices, int offset)
+{
+ // retrieve the plugin index and sort them by priority to avoid issue
+ // when moving them
+ std::vector<int> allIndex;
+ for (auto& idx : indices) {
+ allIndex.push_back(idx.row());
+ }
+ std::sort(allIndex.begin(), allIndex.end(), [=](int lhs, int rhs) {
+ bool cmp = m_ESPs[lhs].priority < m_ESPs[rhs].priority;
+ return offset > 0 ? !cmp : cmp;
+ });
+
+ for (auto index : allIndex) {
+ int newPriority = m_ESPs[index].priority + offset;
+ if (newPriority >= 0 && newPriority < rowCount()) {
+ setPluginPriority(index, newPriority);
+ }
+ }
+
+ refreshLoadOrder();
+}
+
+void PluginList::toggleState(const QModelIndexList& indices)
+{
+ QModelIndex minRow, maxRow;
+ for (auto& idx : indices) {
+ if (!minRow.isValid() || (idx.row() < minRow.row())) {
+ minRow = idx;
+ }
+ if (!maxRow.isValid() || (idx.row() > maxRow.row())) {
+ maxRow = idx;
+ }
+ int oldState = idx.data(Qt::CheckStateRole).toInt();
+ setData(idx, oldState == Qt::Unchecked ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
+ }
+
+ emit dataChanged(minRow, maxRow);
+}
+
+bool PluginList::isEnabled(const QString &name)
+{
+ std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
+
+ if (iter != m_ESPsByName.end()) {
+ return m_ESPs[iter->second].enabled;
+ } else {
+ return false;
+ }
+}
+
+void PluginList::clearInformation(const QString &name)
+{
+ std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
+
+ if (iter != m_ESPsByName.end()) {
+ m_AdditionalInfo[name].messages.clear();
+ }
+}
+
+void PluginList::clearAdditionalInformation()
+{
+ m_AdditionalInfo.clear();
+}
+
+void PluginList::addInformation(const QString &name, const QString &message)
+{
+ std::map<QString, int>::iterator iter = m_ESPsByName.find(name);
+
+ if (iter != m_ESPsByName.end()) {
+ m_AdditionalInfo[name].messages.append(message);
+ } else {
+ log::warn("failed to associate message for \"{}\"", name);
+ }
+}
+
+void PluginList::addLootReport(const QString& name, Loot::Plugin plugin)
+{
+ auto iter = m_ESPsByName.find(name);
+
+ if (iter != m_ESPsByName.end()) {
+ m_AdditionalInfo[name].loot = std::move(plugin);
+ } else {
+ log::warn("failed to associate loot report for \"{}\"", name);
+ }
+}
+
+bool PluginList::isEnabled(int index)
+{
+ return m_ESPs.at(index).enabled;
+}
+
+void PluginList::readLockedOrderFrom(const QString &fileName)
+{
+ m_LockedOrder.clear();
+
+ QFile file(fileName);
+ if (!file.exists()) {
+ // no locked load order, that's ok
+ return;
+ }
+
+ file.open(QIODevice::ReadOnly);
+ int lineNumber = 0;
+ while (!file.atEnd())
+ {
+ QByteArray line = file.readLine();
+ ++lineNumber;
+
+ // Skip empty lines or commented out lines (#)
+ if ((line.size() <= 0) || (line.at(0) == '#'))
+ {
+ continue;
+ }
+
+ QList<QByteArray> fields = line.split('|');
+ if (fields.count() != 2)
+ {
+ // Don't know how to parse this so run away
+ log::error("locked order file: invalid line #{}: {}", lineNumber, QString::fromUtf8(line).trimmed());
+ continue;
+ }
+
+ // Read the plugin name and priority
+ QString pluginName = QString::fromUtf8(fields.at(0));
+ int priority = fields.at(1).trimmed().toInt();
+ if (priority < 0)
+ {
+ // WTF do you mean a negative priority?
+ log::error("locked order file: invalid line #{}: {}", lineNumber, QString::fromUtf8(line).trimmed());
+ continue;
+ }
+
+ // Determine the index of the plugin
+ auto it = m_ESPsByName.find(pluginName);
+ if (it == m_ESPsByName.end())
+ {
+ // Plugin does not exist in the current set of plugins
+ m_LockedOrder[pluginName] = priority;
+ continue;
+ }
+ int pluginIndex = it->second;
+
+ // Do not allow locking forced plugins
+ if (m_ESPs[pluginIndex].forceEnabled)
+ {
+ continue;
+ }
+
+ // If the priority is larger than the number of plugins, just keep it locked
+ if (priority >= m_ESPsByPriority.size())
+ {
+ m_LockedOrder[pluginName] = priority;
+ continue;
+ }
+
+ // These are some helper functions for figuring out what is already locked
+ auto findLocked = [&](const std::pair<QString, int>& a) { return a.second == priority; };
+ auto alreadyLocked = [&](){ return std::find_if(m_LockedOrder.begin(), m_LockedOrder.end(), findLocked) != m_LockedOrder.end(); };
+
+ // See if we can just set the given priority
+ if (!m_ESPs[m_ESPsByPriority.at(priority)].forceEnabled && !alreadyLocked())
+ {
+ m_LockedOrder[pluginName] = priority;
+ continue;
+ }
+
+ // Find the next higher priority we can set the plugin to
+ while (++priority < m_ESPs.size())
+ {
+ if (!m_ESPs[m_ESPsByPriority.at(priority)].forceEnabled && !alreadyLocked())
+ {
+ m_LockedOrder[pluginName] = priority;
+ break;
+ }
+ }
+
+ // See if we walked off the end of the plugin list
+ if (priority >= m_ESPs.size())
+ {
+ // I guess go ahead and lock it here at the end of the list?
+ m_LockedOrder[pluginName] = priority;
+ continue;
+ }
+ } /* while (!file.atEnd()) */
+ file.close();
+}
+
+void PluginList::writeLockedOrder(const QString &fileName) const
+{
+ SafeWriteFile file(fileName);
+
+ file->resize(0);
+ file->write(QString("# This file was automatically generated by Mod Organizer.\r\n").toUtf8());
+ for (auto iter = m_LockedOrder.begin(); iter != m_LockedOrder.end(); ++iter) {
+ file->write(QString("%1|%2\r\n").arg(iter->first).arg(iter->second).toUtf8());
+ }
+ file.commit();
+}
+
+void PluginList::saveTo(const QString &lockedOrderFileName) const
+{
+ GamePlugins *gamePlugins = m_GamePlugin->feature<GamePlugins>();
+ if (gamePlugins) {
+ gamePlugins->writePluginLists(m_Organizer.managedGameOrganizer()->pluginList());
+ }
+
+ writeLockedOrder(lockedOrderFileName);
+}
+
+
+bool PluginList::saveLoadOrder(DirectoryEntry &directoryStructure)
+{
+ if (m_GamePlugin->loadOrderMechanism() != IPluginGame::LoadOrderMechanism::FileTime) {
+ // nothing to do
+ return true;
+ }
+
+ log::debug("setting file times on esps");
+
+ for (ESPInfo &esp : m_ESPs) {
+ std::wstring espName = ToWString(esp.name);
+ const FileEntryPtr fileEntry = directoryStructure.findFile(espName);
+ if (fileEntry.get() != nullptr) {
+ QString fileName;
+ bool archive = false;
+ int originid = fileEntry->getOrigin(archive);
+
+ fileName = QString("%1\\%2")
+ .arg(QDir::toNativeSeparators(ToQString(directoryStructure.getOriginByID(originid).getPath())))
+ .arg(esp.name);
+
+ HANDLE file = ::CreateFile(ToWString(fileName).c_str(), GENERIC_READ | GENERIC_WRITE,
+ 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
+ if (file == INVALID_HANDLE_VALUE) {
+ if (::GetLastError() == ERROR_SHARING_VIOLATION) {
+ // file is locked, probably the game is running
+ return false;
+ } else {
+ throw windows_error(QObject::tr("failed to access %1").arg(fileName).toUtf8().constData());
+ }
+ }
+
+ ULONGLONG temp = 0;
+ temp = (145731ULL + esp.priority) * 24 * 60 * 60 * 10000000ULL;
+
+ FILETIME newWriteTime;
+
+ newWriteTime.dwLowDateTime = (DWORD)(temp & 0xFFFFFFFF);
+ newWriteTime.dwHighDateTime = (DWORD)(temp >> 32);
+ esp.time = newWriteTime;
+ fileEntry->setFileTime(newWriteTime);
+ if (!::SetFileTime(file, nullptr, nullptr, &newWriteTime)) {
+ throw windows_error(QObject::tr("failed to set file time %1").arg(fileName).toUtf8().constData());
+ }
+
+ CloseHandle(file);
+ }
+ }
+ return true;
+}
+
+int PluginList::enabledCount() const
+{
+ int enabled = 0;
+ for (const auto &info : m_ESPs) {
+ if (info.enabled) {
+ ++enabled;
+ }
+ }
+ return enabled;
+}
+
+QString PluginList::getIndexPriority(int index) const
+{
+ return m_ESPs[index].index;
+}
+
+bool PluginList::isESPLocked(int index) const
+{
+ return m_LockedOrder.find(m_ESPs.at(index).name) != m_LockedOrder.end();
+}
+
+void PluginList::lockESPIndex(int index, bool lock)
+{
+ if (lock) {
+ if (!m_ESPs.at(index).forceEnabled)
+ m_LockedOrder[getName(index)] = m_ESPs.at(index).loadOrder;
+ else
+ return;
+ } else {
+ auto iter = m_LockedOrder.find(getName(index));
+ if (iter != m_LockedOrder.end()) {
+ m_LockedOrder.erase(iter);
+ }
+ }
+ emit writePluginsList();
+}
+
+void PluginList::syncLoadOrder()
+{
+ int loadOrder = 0;
+ for (unsigned int i = 0; i < m_ESPs.size(); ++i) {
+ int index = m_ESPsByPriority[i];
+
+ if (m_ESPs[index].enabled) {
+ m_ESPs[index].loadOrder = loadOrder++;
+ } else {
+ m_ESPs[index].loadOrder = -1;
+ }
+ }
+}
+
+void PluginList::refreshLoadOrder()
+{
+ ChangeBracket<PluginList> layoutChange(this);
+ syncLoadOrder();
+ // set priorities according to locked load order
+ std::map<int, QString> lockedLoadOrder;
+ std::for_each(m_LockedOrder.begin(), m_LockedOrder.end(),
+ [&lockedLoadOrder] (const std::pair<QString, int> &ele) {
+ lockedLoadOrder[ele.second] = ele.first; });
+
+ int targetPrio = 0;
+ bool savePluginsList = false;
+ // this is guaranteed to iterate from lowest key (load order) to highest
+ for (auto iter = lockedLoadOrder.begin(); iter != lockedLoadOrder.end(); ++iter) {
+ auto nameIter = m_ESPsByName.find(iter->second);
+ if (nameIter != m_ESPsByName.end()) {
+ // locked esp exists
+
+ // find the location to insert at
+ while ((targetPrio < static_cast<int>(m_ESPs.size() - 1)) &&
+ (m_ESPs[m_ESPsByPriority[targetPrio]].loadOrder < iter->first)) {
+ ++targetPrio;
+ }
+
+ if (static_cast<size_t>(targetPrio) >= m_ESPs.size()) {
+ continue;
+ }
+
+ int temp = targetPrio;
+ int index = nameIter->second;
+ if (m_ESPs[index].priority != temp) {
+ setPluginPriority(index, temp);
+ m_ESPs[index].loadOrder = iter->first;
+ syncLoadOrder();
+ savePluginsList = true;
+ }
+ }
+ }
+ if (savePluginsList) {
+ emit writePluginsList();
+ }
+}
+
+void PluginList::disconnectSlots() {
+ m_PluginMoved.disconnect_all_slots();
+ m_Refreshed.disconnect_all_slots();
+ m_PluginStateChanged.disconnect_all_slots();
+}
+
+int PluginList::timeElapsedSinceLastChecked() const
+{
+ return m_LastCheck.elapsed();
+}
+
+QStringList PluginList::pluginNames() const
+{
+ QStringList result;
+
+ for (const ESPInfo &info : m_ESPs) {
+ result.append(info.name);
+ }
+
+ return result;
+}
+
+IPluginList::PluginStates PluginList::state(const QString &name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return IPluginList::STATE_MISSING;
+ } else {
+ return m_ESPs[iter->second].enabled ? IPluginList::STATE_ACTIVE : IPluginList::STATE_INACTIVE;
+ }
+}
+
+void PluginList::setState(const QString &name, PluginStates state) {
+ auto iter = m_ESPsByName.find(name);
+ if (iter != m_ESPsByName.end()) {
+ m_ESPs[iter->second].enabled = (state == IPluginList::STATE_ACTIVE) ||
+ m_ESPs[iter->second].forceEnabled;
+ } else {
+ log::warn("Plugin not found: {}", name);
+ }
+}
+
+void PluginList::setLoadOrder(const QStringList &pluginList)
+{
+ for (ESPInfo &info : m_ESPs) {
+ info.priority = -1;
+ }
+ int maxPriority = 0;
+ for (const QString &plugin : pluginList) {
+ auto iter = m_ESPsByName.find(plugin);
+ if (iter !=m_ESPsByName.end()) {
+ m_ESPs[iter->second].priority = maxPriority++;
+ }
+ }
+
+ // use old priorities
+ for (ESPInfo &info : m_ESPs) {
+ if (info.priority == -1) {
+ info.priority = maxPriority++;
+ }
+ }
+ updateIndices();
+}
+
+int PluginList::priority(const QString &name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return -1;
+ } else {
+ return m_ESPs[iter->second].priority;
+ }
+}
+
+bool PluginList::setPriority(const QString& name, int newPriority) {
+
+ if (newPriority < 0 || newPriority >= static_cast<int>(m_ESPsByPriority.size())) {
+ return false;
+ }
+
+ auto oldPriority = priority(name);
+ if (oldPriority == -1) {
+ return false;
+ }
+
+ int rowIndex = findPluginByPriority(oldPriority);
+
+ // We need to increment newPriority if its above the old one, otherwise the
+ // plugin is place right below the new priority.
+ if (oldPriority < newPriority) {
+ newPriority += 1;
+ }
+ changePluginPriority({ rowIndex }, newPriority);
+
+ return true;
+}
+
+int PluginList::loadOrder(const QString &name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return -1;
+ } else {
+ return m_ESPs[iter->second].loadOrder;
+ }
+}
+
+QStringList PluginList::masters(const QString &name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return QStringList();
+ } else {
+ QStringList result;
+ for (const QString &master : m_ESPs[iter->second].masters) {
+ result.append(master);
+ }
+ return result;
+ }
+}
+
+QString PluginList::origin(const QString &name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return QString();
+ } else {
+ return m_ESPs[iter->second].originName;
+ }
+}
+
+bool PluginList::hasMasterExtension(const QString& name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return false;
+ }
+ else {
+ return m_ESPs[iter->second].hasMasterExtension;
+ }
+}
+
+bool PluginList::hasLightExtension(const QString& name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return false;
+ }
+ else {
+ return m_ESPs[iter->second].hasLightExtension;
+ }
+}
+
+bool PluginList::isMasterFlagged(const QString& name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return false;
+ }
+ else {
+ return m_ESPs[iter->second].isMasterFlagged;
+ }
+}
+
+bool PluginList::isLightFlagged(const QString& name) const
+{
+ auto iter = m_ESPsByName.find(name);
+ if (iter == m_ESPsByName.end()) {
+ return false;
+ }
+ else {
+ return m_ESPs[iter->second].isLightFlagged;
+ }
+}
+
+boost::signals2::connection PluginList::onPluginStateChanged(const std::function<void(const std::map<QString, PluginStates>&)>& func)
+{
+ return m_PluginStateChanged.connect(func);
+}
+
+void PluginList::pluginStatesChanged(QStringList const& pluginNames, PluginStates state) const {
+ if (pluginNames.isEmpty()) {
+ return;
+ }
+ std::map<QString, IPluginList::PluginStates> infos;
+ for (auto& name : pluginNames) {
+ infos[name] = state;
+ }
+ m_PluginStateChanged(infos);
+}
+
+boost::signals2::connection PluginList::onRefreshed(const std::function<void ()> &callback)
+{
+ return m_Refreshed.connect(callback);
+}
+
+boost::signals2::connection PluginList::onPluginMoved(const std::function<void (const QString &, int, int)> &func)
+{
+ return m_PluginMoved.connect(func);
+}
+
+void PluginList::updateIndices()
+{
+ m_ESPsByName.clear();
+ m_ESPsByPriority.clear();
+ m_ESPsByPriority.resize(m_ESPs.size());
+ for (unsigned int i = 0; i < m_ESPs.size(); ++i) {
+ if (m_ESPs[i].priority < 0) {
+ continue;
+ }
+ if (m_ESPs[i].priority >= static_cast<int>(m_ESPs.size())) {
+ log::error("invalid plugin priority: {}", m_ESPs[i].priority);
+ continue;
+ }
+ m_ESPsByName[m_ESPs[i].name] = i;
+ m_ESPsByPriority.at(static_cast<size_t>(m_ESPs[i].priority)) = i;
+ }
+
+ generatePluginIndexes();
+}
+
+void PluginList::generatePluginIndexes()
+{
+ int numESLs = 0;
+ int numSkipped = 0;
+
+ GamePlugins* gamePlugins = m_GamePlugin->feature<GamePlugins>();
+ const bool lightPluginsSupported = gamePlugins ? gamePlugins->lightPluginsAreSupported() : false;
+
+ for (int l = 0; l < m_ESPs.size(); ++l) {
+ int i = m_ESPsByPriority.at(l);
+ if (!m_ESPs[i].enabled) {
+ m_ESPs[i].index = QString();
+ ++numSkipped;
+ continue;
+ }
+ if (lightPluginsSupported && (m_ESPs[i].hasLightExtension || m_ESPs[i].isLightFlagged)) {
+ int ESLpos = 254 + ((numESLs + 1) / 4096);
+ m_ESPs[i].index = QString("%1:%2").arg(ESLpos, 2, 16, QChar('0')).arg((numESLs) % 4096, 3, 16, QChar('0')).toUpper();
+ ++numESLs;
+ } else {
+ m_ESPs[i].index = QString("%1").arg(l - numESLs - numSkipped, 2, 16, QChar('0')).toUpper();
+ }
+ }
+ emit esplist_changed();
+}
+
+int PluginList::rowCount(const QModelIndex &parent) const
+{
+ if (!parent.isValid()) {
+ return static_cast<int>(m_ESPs.size());
+ } else {
+ return 0;
+ }
+}
+
+int PluginList::columnCount(const QModelIndex &) const
+{
+ return COL_LASTCOLUMN + 1;
+}
+
+void PluginList::testMasters()
+{
+ std::set<QString, FileNameComparator> enabledMasters;
+ for (const auto& iter: m_ESPs) {
+ if (iter.enabled) {
+ enabledMasters.insert(iter.name);
+ }
+ }
+
+ for (auto& iter: m_ESPs) {
+ iter.masterUnset.clear();
+ if (iter.enabled) {
+ for (const auto& master: iter.masters) {
+ if (enabledMasters.find(master) == enabledMasters.end()) {
+ iter.masterUnset.insert(master);
+ }
+ }
+ }
+ }
+}
+
+QVariant PluginList::data(const QModelIndex &modelIndex, int role) const
+{
+ int index = modelIndex.row();
+
+ if ((role == Qt::DisplayRole) || (role == Qt::EditRole)) {
+ return displayData(modelIndex);
+ } else if ((role == Qt::CheckStateRole) && (modelIndex.column() == 0)) {
+ return checkstateData(modelIndex);
+ } else if (role == Qt::ForegroundRole) {
+ return foregroundData(modelIndex);
+ } else if (role == Qt::BackgroundRole) {
+ return backgroundData(modelIndex);
+ } else if (role == Qt::FontRole) {
+ return fontData(modelIndex);
+ } else if (role == Qt::TextAlignmentRole) {
+ return alignmentData(modelIndex);
+ } else if (role == Qt::ToolTipRole) {
+ return tooltipData(modelIndex);
+ } else if (role == Qt::UserRole + 1) {
+ return iconData(modelIndex);
+ }
+ return QVariant();
+}
+
+QVariant PluginList::displayData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ switch (modelIndex.column())
+ {
+ case COL_NAME:
+ return m_ESPs[index].name;
+
+ case COL_PRIORITY:
+ return m_ESPs[index].priority;
+
+ case COL_MODINDEX:
+ return m_ESPs[index].index;
+
+ default:
+ return {};
+ }
+}
+
+QVariant PluginList::checkstateData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if (m_ESPs[index].forceEnabled) {
+ return {};
+ }
+
+ return m_ESPs[index].enabled ? Qt::Checked : Qt::Unchecked;
+}
+
+QVariant PluginList::foregroundData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if ((modelIndex.column() == COL_NAME) && m_ESPs[index].forceEnabled) {
+ return QBrush(Qt::gray);
+ }
+
+ return {};
+}
+
+QVariant PluginList::backgroundData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if (m_ESPs[index].modSelected) {
+ return Settings::instance().colors().pluginListContained();
+ }
+
+ return {};
+}
+
+QVariant PluginList::fontData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ QFont result;
+
+ if (m_ESPs[index].hasMasterExtension ||
+ m_ESPs[index].isMasterFlagged ||
+ m_ESPs[index].hasLightExtension) {
+ result.setItalic(true);
+ result.setWeight(QFont::Bold);
+ } else if (m_ESPs[index].isLightFlagged) {
+ result.setItalic(true);
+ }
+
+ return result;
+}
+
+QVariant PluginList::alignmentData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+
+ if (modelIndex.column() == 0) {
+ return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
+ } else {
+ return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
+ }
+}
+
+QVariant PluginList::tooltipData(const QModelIndex &modelIndex) const
+{
+ const int index = modelIndex.row();
+ const auto& esp = m_ESPs[index];
+
+ QString toolTip;
+
+ toolTip += "<b>" + tr("Origin") + "</b>: " + esp.originName;
+
+ if (esp.forceEnabled) {
+ toolTip +=
+ "<br><b><i>" +
+ tr("This plugin can't be disabled (enforced by the game).") +
+ "</i></b>";
+ } else {
+ if (!esp.author.isEmpty()) {
+ toolTip +=
+ "<br><b>" + tr("Author") + "</b>: " +
+ TruncateString(esp.author);
+ }
+
+ if (esp.description.size() > 0) {
+ toolTip +=
+ "<br><b>" + tr("Description") + "</b>: " +
+ TruncateString(esp.description);
+ }
+
+ if (esp.masterUnset.size() > 0) {
+ toolTip +=
+ "<br><b>" + tr("Missing Masters") + "</b>: " +
+ "<b>" + TruncateString(QStringList(esp.masterUnset.begin(), esp.masterUnset.end()).join(", ")) + "</b>";
+ }
+
+ std::set<QString> enabledMasters;
+ std::set_difference(esp.masters.begin(), esp.masters.end(),
+ esp.masterUnset.begin(), esp.masterUnset.end(),
+ std::inserter(enabledMasters, enabledMasters.end()));
+
+ if (!enabledMasters.empty()) {
+ toolTip +=
+ "<br><b>" + tr("Enabled Masters") + "</b>: " +
+ TruncateString(SetJoin(enabledMasters, ", "));
+ }
+
+ if (!esp.archives.empty()) {
+ toolTip +=
+ "<br><b>" + tr("Loads Archives") + "</b>: " +
+ TruncateString(QStringList(esp.archives.begin(), esp.archives.end()).join(", ")) +
+ "<br>" + tr(
+ "There are Archives connected to this plugin. Their assets will be "
+ "added to your game, overwriting in case of conflicts following the "
+ "plugin order. Loose files will always overwrite assets from "
+ "Archives. (This flag only checks for Archives from the same mod as "
+ "the plugin)");
+ }
+
+ if (esp.hasIni) {
+ toolTip +=
+ "<br><b>" + tr("Loads INI settings") + "</b>: "
+ "<br>" + tr(
+ "There is an ini file connected to this plugin. Its settings will "
+ "be added to your game settings, overwriting in case of conflicts.");
+ }
+
+ if (esp.isLightFlagged && !esp.hasLightExtension) {
+ toolTip +=
+ "<br><br>" + tr(
+ "This ESP is flagged as an ESL. It will adhere to the ESP load "
+ "order but the records will be loaded in ESL space.");
+ }
+ }
+
+
+ // additional info
+ auto itor = m_AdditionalInfo.find(esp.name);
+
+ if (itor != m_AdditionalInfo.end()) {
+ if (!itor->second.messages.isEmpty()) {
+ toolTip += "<hr><ul style=\"margin-left:15px; -qt-list-indent: 0;\">";
+
+ for (auto&& message : itor->second.messages) {
+ toolTip += "<li>" + message + "</li>";
+ }
+
+ toolTip += "</ul>";
+ }
+
+ // loot
+ toolTip += makeLootTooltip(itor->second.loot);
+ }
+
+ return toolTip;
+}
+
+QString PluginList::makeLootTooltip(const Loot::Plugin& loot) const
+{
+ QString s;
+
+ for (auto&& f : loot.incompatibilities) {
+ s +=
+ "<li>" + tr("Incompatible with %1")
+ .arg(f.displayName.isEmpty() ? f.name : f.displayName) +
+ "</li>";
+ }
+
+ for (auto&& m : loot.missingMasters) {
+ s += "<li>" + tr("Depends on missing %1").arg(m) + "</li>";
+ }
+
+ for (auto&& m : loot.messages) {
+ s += "<li>";
+
+ switch (m.type)
+ {
+ case log::Warning:
+ s += tr("Warning") + ": ";
+ break;
+
+ case log::Error:
+ s += tr("Error") + ": ";
+ break;
+
+ case log::Info: // fall-through
+ case log::Debug:
+ default:
+ // nothing
+ break;
+ }
+
+ s += m.text + "</li>";
+ }
+
+ for (auto&& d : loot.dirty) {
+ s += "<li>" + d.toString(false) + "</li>";
+ }
+
+ for (auto&& c : loot.clean) {
+ s += "<li>" + c.toString(true) + "</li>";
+ }
+
+ if (!s.isEmpty()) {
+ s =
+ "<hr>"
+ "<ul style=\"margin-top:0px; padding-top:0px; margin-left:15px; -qt-list-indent: 0;\">" +
+ s +
+ "</ul>";
+ }
+
+ return s;
+}
+
+QVariant PluginList::iconData(const QModelIndex &modelIndex) const
+{
+ int index = modelIndex.row();
+
+ QVariantList result;
+
+ const auto& esp = m_ESPs[index];
+
+ auto infoItor = m_AdditionalInfo.find(esp.name);
+
+ const AdditionalInfo* info = nullptr;
+ if (infoItor != m_AdditionalInfo.end()) {
+ info = &infoItor->second;
+ }
+
+ if (isProblematic(esp, info)) {
+ result.append(":/MO/gui/warning");
+ }
+
+ if (m_LockedOrder.find(esp.name) != m_LockedOrder.end()) {
+ result.append(":/MO/gui/locked");
+ }
+
+ if (hasInfo(esp, info)) {
+ result.append(":/MO/gui/information");
+ }
+
+ if (esp.hasIni) {
+ result.append(":/MO/gui/attachment");
+ }
+
+ if (!esp.archives.empty()) {
+ result.append(":/MO/gui/archive_conflict_neutral");
+ }
+
+ if (esp.isLightFlagged && !esp.hasLightExtension) {
+ result.append(":/MO/gui/awaiting");
+ }
+
+ if (info && !info->loot.dirty.empty()) {
+ result.append(":/MO/gui/edit_clear");
+ }
+
+ return result;
+}
+
+bool PluginList::isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const
+{
+ if (esp.masterUnset.size() > 0) {
+ return true;
+ }
+
+ if (info) {
+ if (!info->loot.incompatibilities.empty()) {
+ return true;
+ }
+
+ if (!info->loot.missingMasters.empty()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool PluginList::hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const
+{
+ if (info) {
+ if (!info->messages.empty()) {
+ return true;
+ }
+
+ if (!info->loot.messages.empty()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int role)
+{
+ QString modName = modIndex.data().toString();
+ IPluginList::PluginStates oldState = state(modName);
+
+ bool result = false;
+
+ if (role == Qt::CheckStateRole) {
+ m_ESPs[modIndex.row()].enabled =
+ value.toInt() == Qt::Checked || m_ESPs[modIndex.row()].forceEnabled;
+ m_LastCheck.restart();
+ emit dataChanged(modIndex, modIndex);
+
+ refreshLoadOrder();
+ emit writePluginsList();
+
+ result = true;
+ } else if (role == Qt::EditRole) {
+ if (modIndex.column() == COL_PRIORITY) {
+ bool ok = false;
+ int newPriority = value.toInt(&ok);
+ if (ok) {
+ setPluginPriority(modIndex.row(), newPriority);
+ result = true;
+ }
+ refreshLoadOrder();
+ emit writePluginsList();
+ }
+ }
+
+ IPluginList::PluginStates newState = state(modName);
+ if (oldState != newState) {
+ try {
+ pluginStatesChanged({ modName }, newState);
+ testMasters();
+ emit dataChanged(
+ this->index(0, 0),
+ this->index(static_cast<int>(m_ESPs.size()), columnCount()));
+ } catch (const std::exception &e) {
+ log::error("failed to invoke state changed notification: {}", e.what());
+ } catch (...) {
+ log::error("failed to invoke state changed notification: unknown exception");
+ }
+ }
+
+ return result;
+}
+
+QVariant PluginList::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ if (orientation == Qt::Horizontal) {
+ if (role == Qt::DisplayRole) {
+ return getColumnName(section);
+ } else if (role == Qt::ToolTipRole) {
+ return getColumnToolTip(section);
+ }
+ }
+ return QAbstractItemModel::headerData(section, orientation, role);
+}
+
+Qt::ItemFlags PluginList::flags(const QModelIndex &modelIndex) const
+{
+ int index = modelIndex.row();
+ Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex);
+
+ if (modelIndex.isValid()) {
+ if (!m_ESPs[index].forceEnabled) {
+ result |= Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled;
+ }
+ if (modelIndex.column() == COL_PRIORITY) {
+ result |= Qt::ItemIsEditable;
+ }
+ result &= ~Qt::ItemIsDropEnabled;
+ } else {
+ result |= Qt::ItemIsDropEnabled;
+ }
+
+ return result;
+}
+
+void PluginList::setPluginPriority(int row, int &newPriority, bool isForced)
+{
+ int newPriorityTemp = newPriority;
+
+ // enforce valid range
+ if (newPriorityTemp < 0)
+ newPriorityTemp = 0;
+ else if (newPriorityTemp >= static_cast<int>(m_ESPsByPriority.size()))
+ newPriorityTemp = static_cast<int>(m_ESPsByPriority.size()) - 1;
+
+ if (!m_ESPs[row].isMasterFlagged &&
+ !m_ESPs[row].hasLightExtension &&
+ !m_ESPs[row].hasMasterExtension) {
+ // don't allow esps to be moved above esms
+ while ((newPriorityTemp < static_cast<int>(m_ESPsByPriority.size() - 1)) &&
+ (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMasterFlagged ||
+ m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasLightExtension ||
+ m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasMasterExtension)) {
+ ++newPriorityTemp;
+ }
+ } else {
+ // don't allow esms to be moved below esps
+ while ((newPriorityTemp > 0) &&
+ !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMasterFlagged &&
+ !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasLightExtension &&
+ !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).hasMasterExtension) {
+ --newPriorityTemp;
+ }
+ // also don't allow "regular" esms to be moved above primary plugins
+ while ((newPriorityTemp < static_cast<int>(m_ESPsByPriority.size() - 1)) &&
+ (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).forceEnabled)) {
+ ++newPriorityTemp;
+ }
+ }
+
+ int oldPriority = m_ESPs.at(row).priority;
+ if (newPriorityTemp < oldPriority) { // moving up
+ // don't allow plugins to be moved above their masters
+ for (auto master : m_ESPs[row].masters) {
+ auto iter = m_ESPsByName.find(master);
+ if (iter != m_ESPsByName.end()) {
+ int masterPriority = m_ESPs[iter->second].priority;
+ if (masterPriority >= newPriorityTemp)
+ {
+ newPriorityTemp = masterPriority + 1;
+ }
+ }
+ }
+ }
+ else if (newPriorityTemp > oldPriority) { // moving down
+ // don't allow masters to be moved below their children
+ for (int i = oldPriority + 1; i <= newPriorityTemp; i++) {
+ PluginList::ESPInfo* otherInfo = &m_ESPs.at(m_ESPsByPriority[i]);
+ for (auto master : otherInfo->masters) {
+ if (master.compare(m_ESPs[row].name, Qt::CaseInsensitive) == 0) {
+ newPriorityTemp = otherInfo->priority - 1;
+ break;
+ }
+ }
+ }
+ }
+
+ try {
+ if (newPriorityTemp != oldPriority) {
+ if (newPriorityTemp > oldPriority) {
+ // priority is higher than the old, so the gap we left is in lower priorities
+ for (int i = oldPriority + 1; i <= newPriorityTemp; ++i) {
+ --m_ESPs.at(m_ESPsByPriority.at(i)).priority;
+ }
+ emit dataChanged(index(oldPriority + 1, 0), index(newPriorityTemp, columnCount()));
+ }
+ else {
+ for (int i = newPriorityTemp; i < oldPriority; ++i) {
+ ++m_ESPs.at(m_ESPsByPriority.at(i)).priority;
+ }
+ emit dataChanged(index(newPriorityTemp, 0), index(oldPriority - 1, columnCount()));
+ ++newPriority;
+ }
+
+ m_ESPs.at(row).priority = newPriorityTemp;
+ emit dataChanged(index(row, 0), index(row, columnCount()));
+ m_PluginMoved(m_ESPs[row].name, oldPriority, newPriorityTemp);
+ }
+ } catch (const std::out_of_range&) {
+ reportError(tr("failed to restore load order for %1").arg(m_ESPs[row].name));
+ }
+
+ updateIndices();
+}
+
+void PluginList::changePluginPriority(std::vector<int> rows, int newPriority)
+{
+ ChangeBracket<PluginList> layoutChange(this);
+ const std::vector<ESPInfo> &esp = m_ESPs;
+
+ int minPriority = INT_MAX;
+ int maxPriority = INT_MIN;
+
+ // don't try to move plugins before force-enabled plugins
+ for (std::vector<ESPInfo>::const_iterator iter = m_ESPs.begin();
+ iter != m_ESPs.end(); ++iter) {
+ if (iter->forceEnabled) {
+ newPriority = std::max(newPriority, iter->priority + 1);
+ }
+ maxPriority = std::max(maxPriority, iter->priority + 1);
+ minPriority = std::min(minPriority, iter->priority);
+ }
+
+ // limit the new priority to existing priorities
+ newPriority = std::min(newPriority, maxPriority);
+ newPriority = std::max(newPriority, minPriority);
+
+ // sort the moving plugins by ascending priorities
+ std::sort(rows.begin(), rows.end(),
+ [&esp](const int &LHS, const int &RHS) {
+ return esp[LHS].priority < esp[RHS].priority;
+ });
+
+ // if at least on plugin is increasing in priority, the target index is
+ // that of the row BELOW the dropped location, otherwise it's the one above
+ for (std::vector<int>::const_iterator iter = rows.begin();
+ iter != rows.end(); ++iter) {
+ if (m_ESPs[*iter].priority < newPriority) {
+ --newPriority;
+ break;
+ }
+ }
+
+ for (std::vector<int>::const_iterator iter = rows.begin(); iter != rows.end(); ++iter) {
+ setPluginPriority(*iter, newPriority);
+ }
+
+ layoutChange.finish();
+ refreshLoadOrder();
+ emit writePluginsList();
+}
+
+bool PluginList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent)
+{
+ if (action == Qt::IgnoreAction) {
+ return true;
+ }
+
+ QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist");
+ QDataStream stream(&encoded, QIODevice::ReadOnly);
+
+ std::vector<int> sourceRows;
+
+ while (!stream.atEnd()) {
+ int sourceRow, col;
+ QMap<int, QVariant> roleDataMap;
+ stream >> sourceRow >> col >> roleDataMap;
+ if (col == 0) { // only add each row once
+ sourceRows.push_back(sourceRow);
+ }
+ }
+
+ if (row == -1) {
+ row = parent.row();
+ }
+
+ int newPriority;
+
+ if ((row < 0) ||
+ (row >= static_cast<int>(m_ESPs.size()))) {
+ newPriority = static_cast<int>(m_ESPs.size());
+ } else {
+ newPriority = m_ESPs[row].priority;
+ }
+ changePluginPriority(sourceRows, newPriority);
+
+ return false;
+}
+
+QModelIndex PluginList::index(int row, int column, const QModelIndex&) const
+{
+ if ((row < 0) || (row >= rowCount()) || (column < 0) || (column >= columnCount())) {
+ return QModelIndex();
+ }
+ return createIndex(row, column, row);
+}
+
+QModelIndex PluginList::parent(const QModelIndex&) const
+{
+ return QModelIndex();
+}
+
+PluginList::ESPInfo::ESPInfo(const QString &name, bool enabled,
+ const QString &originName, const QString &fullPath,
+ bool hasIni, std::set<QString> archives, bool lightPluginsAreSupported)
+ : name(name), fullPath(fullPath), enabled(enabled), forceEnabled(enabled),
+ priority(0), loadOrder(-1), originName(originName), hasIni(hasIni),
+ archives(archives.begin(), archives.end()), modSelected(false)
+{
+ try {
+ ESP::File file(ToWString(fullPath));
+ auto extension = name.right(3).toLower();
+ hasMasterExtension = (extension == "esm");
+ hasLightExtension = lightPluginsAreSupported && (extension == "esl");
+ isMasterFlagged = file.isMaster();
+ isLightFlagged = lightPluginsAreSupported && file.isLight();
+
+ author = QString::fromLatin1(file.author().c_str());
+ description = QString::fromLatin1(file.description().c_str());
+
+ for (auto&& m : file.masters()) {
+ masters.insert(QString::fromStdString(m));
+ }
+ } catch (const std::exception &e) {
+ log::error("failed to parse plugin file {}: {}", fullPath, e.what());
+ hasMasterExtension = false;
+ hasLightExtension = false;
+ isMasterFlagged = false;
+ isLightFlagged = false;
+ }
+}
+
+void PluginList::managedGameChanged(const IPluginGame *gamePlugin)
+{
+ m_GamePlugin = gamePlugin;
+}