/*
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 .
*/
#include "pluginlist.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "gameplugins.h"
#include "modinfo.h"
#include "modlist.h"
#include "organizercore.h"
#include "settings.h"
#include "shared/directoryentry.h"
#include "shared/fileentry.h"
#include "shared/filesorigin.h"
#include "shared/windows_error.h"
#include "viewmarkingscrollbar.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");
case COL_FORMVERSION:
return tr("Form Version");
case COL_HEADERVERSION:
return tr("Header Version");
case COL_AUTHOR:
return tr("Author");
case COL_DESCRIPTION:
return tr("Description");
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.");
case COL_FORMVERSION:
return tr("Form version of the plugin.");
case COL_HEADERVERSION:
return tr("Header version of the plugin.");
case COL_AUTHOR:
return tr("Author of the plugin.");
case COL_DESCRIPTION:
return tr("Description of the plugin.");
default:
return tr("unknown");
}
}
void PluginList::highlightPlugins(const std::vector& 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)) {
QString modDataPath = selectedMod->absolutePath();
modDataPath =
m_Organizer.managedGame()->modDataDirectory().isEmpty()
? modDataPath
: modDataPath + "/" + m_Organizer.managedGame()->modDataDirectory();
QDir dir(modDataPath);
QStringList plugins;
if (dir.exists()) {
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::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(m_ESPs.size()) - 1,
this->columnCount() - 1));
}
void PluginList::highlightMasters(const QModelIndexList& selectedPluginIndices)
{
for (auto& esp : m_ESPs) {
esp.isMasterOfSelectedPlugin = false;
}
for (const auto& pluginIndex : selectedPluginIndices) {
const ESPInfo& plugin = m_ESPs[pluginIndex.row()];
for (const auto& master : plugin.masters) {
const auto iter = m_ESPsByName.find(master);
if (iter != m_ESPsByName.end()) {
m_ESPs[iter->second].isMasterOfSelectedPlugin = true;
}
}
}
}
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 layoutChange(this);
QStringList primaryPlugins = m_GamePlugin->primaryPlugins();
QStringList enabledPlugins = m_GamePlugin->enabledPlugins();
auto gamePlugins = m_Organizer.gameFeatures().gameFeature();
const bool lightPluginsAreSupported =
gamePlugins ? gamePlugins->lightPluginsAreSupported() : false;
const bool mediumPluginsAreSupported =
gamePlugins ? gamePlugins->mediumPluginsAreSupported() : false;
const bool blueprintPluginsAreSupported =
gamePlugins ? gamePlugins->blueprintPluginsAreSupported() : false;
const bool loadOrderMechanismNone =
m_GamePlugin->loadOrderMechanism() == IPluginGame::LoadOrderMechanism::None;
const QString& blueprintPrefix = m_GamePlugin->blueprintPrefix();
m_CurrentProfile = profileName;
m_BlueprintPlugins = blueprintPluginsAreSupported;
std::unordered_map availablePlugins;
QStringList archiveCandidates;
for (FileEntryPtr current : baseDirectory.getFiles()) {
if (current.get() == nullptr) {
continue;
}
const QString& filename = ToQString(current->getName());
if (filename.endsWith(".esp", Qt::CaseInsensitive) ||
filename.endsWith(".esm", Qt::CaseInsensitive) ||
filename.endsWith(".esl", Qt::CaseInsensitive)) {
availablePlugins.insert(std::make_pair(filename, current));
} else if (filename.endsWith(".bsa", Qt::CaseInsensitive) ||
filename.endsWith("ba2", Qt::CaseInsensitive)) {
archiveCandidates.append(filename);
}
}
for (const auto& [filename, current] : availablePlugins) {
if (m_ESPsByName.contains(filename)) {
continue;
}
bool forceLoaded = Settings::instance().game().forceEnableCoreFiles() &&
primaryPlugins.contains(filename, Qt::CaseInsensitive);
bool forceEnabled = enabledPlugins.contains(filename, Qt::CaseInsensitive);
bool forceDisabled = loadOrderMechanismNone && !forceLoaded && !forceEnabled;
if (!lightPluginsAreSupported && filename.endsWith(".esl")) {
forceDisabled = true;
}
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 loadedArchives;
for (const auto& archiveName : archiveCandidates) {
if (archiveName.startsWith(baseName, Qt::CaseInsensitive)) {
loadedArchives.insert(archiveName);
}
}
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.emplace_back(filename, forceLoaded, forceEnabled, forceDisabled,
originName, ToQString(current->getFullPath()), hasIni,
loadedArchives, lightPluginsAreSupported,
mediumPluginsAreSupported, blueprintPluginsAreSupported,
blueprintPrefix);
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)) {
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(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;
int prioBlueprint = 0;
bool somethingChanged = false;
for (auto esp : m_ESPs) {
if (!esp.isBlueprintFlagged)
prioBlueprint++;
}
for (QString plugin : primaryPlugins) {
std::map::iterator iter = m_ESPsByName.find(plugin);
// Plugin is present?
if (iter != m_ESPsByName.end()) {
if (!m_ESPs[iter->second].isBlueprintFlagged) {
if (prio != m_ESPs[iter->second].priority) {
// Priority is wrong! Fix it!
int newPrio = prio;
setPluginPriority(iter->second, newPrio, true /* isForced */);
somethingChanged = true;
}
prio++;
} else {
if (prioBlueprint != m_ESPs[iter->second].priority) {
// Priority is wrong! Fix it!
int newPrio = prioBlueprint;
setPluginPriority(iter->second, newPrio, true /* isForced */);
somethingChanged = true;
}
prioBlueprint++;
}
}
}
if (somethingChanged) {
writePluginsList();
}
}
void PluginList::fixPluginRelationships()
{
TimeThis timer("PluginList::fixPluginRelationships");
// Count the types of plugins
int standardCount = 0;
int masterCount = 0;
int blueprintMasterCount = 0;
for (auto plugin : m_ESPs) {
if (plugin.hasLightExtension || plugin.hasMasterExtension ||
plugin.isMasterFlagged) {
if (plugin.isBlueprintFlagged) {
blueprintMasterCount++;
} else {
masterCount++;
}
}
if (!plugin.isBlueprintFlagged) {
standardCount++;
}
}
// 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.isBlueprintFlagged) {
if (plugin.priority > standardCount + blueprintMasterCount) {
int newPriority = standardCount + blueprintMasterCount;
setPluginPriority(i, newPriority);
}
} else {
if (plugin.priority > masterCount) {
int newPriority = masterCount;
setPluginPriority(i, newPriority);
}
}
} else {
if (plugin.isBlueprintFlagged) {
if (plugin.priority < standardCount + blueprintMasterCount) {
int newPriority = standardCount + blueprintMasterCount + 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()) {
if (m_ESPs.at(iter->second).isBlueprintFlagged == plugin.isBlueprintFlagged) {
newPriority = std::max(newPriority, m_ESPs[iter->second].priority);
}
}
}
if (newPriority != plugin.priority) {
setPluginPriority(i, newPriority);
}
}
}
void PluginList::fixPriorities()
{
std::vector> 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& lhs, const std::pair& 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::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].forceDisabled) ||
m_ESPs[iter->second].forceLoaded ||
m_ESPs[iter->second].forceEnabled;
if (m_BlueprintPlugins &&
!name.startsWith(m_GamePlugin->blueprintPrefix(), Qt::CaseInsensitive)) {
QString blueprint = m_GamePlugin->blueprintPrefix();
blueprint += name.left(name.size() - 4) + ".esm";
auto blueprintPlugin = m_ESPsByName.find(blueprint);
if (blueprintPlugin != m_ESPsByName.end()) {
if (m_ESPs[iter->second].enabled) {
m_ESPs[blueprintPlugin->second].forceDisabled = false;
m_ESPs[blueprintPlugin->second].forceEnabled = true;
m_ESPs[blueprintPlugin->second].enabled = true;
} else {
m_ESPs[blueprintPlugin->second].forceDisabled = true;
m_ESPs[blueprintPlugin->second].forceEnabled = false;
m_ESPs[blueprintPlugin->second].enabled = false;
}
}
}
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()].forceLoaded || m_ESPs[idx.row()].forceEnabled ||
m_ESPs[idx.row()].forceDisabled)
continue;
if (m_ESPs[idx.row()].enabled != enabled) {
m_ESPs[idx.row()].enabled = enabled;
dirty.append(m_ESPs[idx.row()].name);
if (m_BlueprintPlugins &&
!m_ESPs[idx.row()].name.startsWith(m_GamePlugin->blueprintPrefix(),
Qt::CaseInsensitive)) {
QString blueprint = m_GamePlugin->blueprintPrefix();
blueprint +=
m_ESPs[idx.row()].name.left(m_ESPs[idx.row()].name.size() - 4) + ".esm";
auto blueprintPlugin = m_ESPsByName.find(blueprint);
if (blueprintPlugin != m_ESPsByName.end()) {
m_ESPs[blueprintPlugin->second].forceDisabled = false;
m_ESPs[blueprintPlugin->second].forceEnabled = true;
m_ESPs[blueprintPlugin->second].enabled = true;
dirty.append(m_ESPs[blueprintPlugin->second].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.forceLoaded || info.forceEnabled || info.forceDisabled)
continue;
if (info.enabled != enabled) {
info.enabled = enabled;
dirty.append(info.name);
if (m_BlueprintPlugins &&
!info.name.startsWith(m_GamePlugin->blueprintPrefix(), Qt::CaseInsensitive)) {
QString blueprint = m_GamePlugin->blueprintPrefix();
blueprint += info.name.left(info.name.size() - 4) + ".esm";
auto blueprintPlugin = m_ESPsByName.find(blueprint);
if (blueprintPlugin != m_ESPsByName.end()) {
m_ESPs[blueprintPlugin->second].forceDisabled = false;
m_ESPs[blueprintPlugin->second].forceEnabled = true;
m_ESPs[blueprintPlugin->second].enabled = true;
dirty.append(m_ESPs[blueprintPlugin->second].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 pluginsToMove;
for (auto& idx : indices) {
if (!m_ESPs[idx.row()].forceLoaded) {
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 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::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::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::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 | QIODevice::Text);
const QByteArray contents = file.readAll();
file.close();
int lineNumber = 0;
for (const QByteArray& line : contents.split('\n')) {
++lineNumber;
// Skip empty lines or commented out lines (#)
if ((line.size() <= 0) || (line.at(0) == '#')) {
continue;
}
QList 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].forceLoaded) {
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& 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)].forceLoaded && !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)].forceLoaded && !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;
}
}
}
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
{
auto gamePlugins = m_Organizer.gameFeatures().gameFeature();
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).forceLoaded)
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 layoutChange(this);
syncLoadOrder();
// set priorities according to locked load order
std::map lockedLoadOrder;
std::for_each(m_LockedOrder.begin(), m_LockedOrder.end(),
[&lockedLoadOrder](const std::pair& 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(m_ESPs.size() - 1)) &&
(m_ESPs[m_ESPsByPriority[targetPrio]].loadOrder < iter->first)) {
++targetPrio;
}
if (static_cast(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].forceDisabled) ||
m_ESPs[iter->second].forceLoaded || m_ESPs[iter->second].forceEnabled;
if (m_BlueprintPlugins &&
!m_ESPs[iter->second].name.startsWith(m_GamePlugin->blueprintPrefix(),
Qt::CaseInsensitive)) {
QString blueprint = m_GamePlugin->blueprintPrefix();
blueprint +=
m_ESPs[iter->second].name.left(m_ESPs[iter->second].name.size() - 4) + ".esm";
auto blueprintPlugin = m_ESPsByName.find(blueprint);
if (blueprintPlugin != m_ESPsByName.end()) {
if (m_ESPs[iter->second].enabled) {
m_ESPs[blueprintPlugin->second].forceDisabled = false;
m_ESPs[blueprintPlugin->second].forceEnabled = true;
m_ESPs[blueprintPlugin->second].enabled = true;
} else {
m_ESPs[blueprintPlugin->second].forceDisabled = true;
m_ESPs[blueprintPlugin->second].forceEnabled = false;
m_ESPs[blueprintPlugin->second].enabled = false;
}
}
}
} 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(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::isMediumFlagged(const QString& name) const
{
auto iter = m_ESPsByName.find(name);
if (iter == m_ESPsByName.end()) {
return false;
} else {
return m_ESPs[iter->second].isMediumFlagged;
}
}
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;
}
}
bool PluginList::isBlueprintFlagged(const QString& name) const
{
auto iter = m_ESPsByName.find(name);
if (iter == m_ESPsByName.end()) {
return false;
} else {
return m_ESPs[iter->second].isBlueprintFlagged;
}
}
bool PluginList::hasNoRecords(const QString& name) const
{
auto iter = m_ESPsByName.find(name);
if (iter == m_ESPsByName.end()) {
return false;
} else {
return m_ESPs[iter->second].hasNoRecords;
}
}
int PluginList::formVersion(const QString& name) const
{
auto iter = m_ESPsByName.find(name);
if (iter == m_ESPsByName.end()) {
return -1;
} else {
return m_ESPs[iter->second].formVersion;
}
}
float PluginList::headerVersion(const QString& name) const
{
auto iter = m_ESPsByName.find(name);
if (iter == m_ESPsByName.end()) {
return -1;
} else {
return m_ESPs[iter->second].headerVersion;
}
}
QString PluginList::author(const QString& name) const
{
auto iter = m_ESPsByName.find(name);
if (iter == m_ESPsByName.end()) {
return QString();
} else {
return m_ESPs[iter->second].author;
}
}
QString PluginList::description(const QString& name) const
{
auto iter = m_ESPsByName.find(name);
if (iter == m_ESPsByName.end()) {
return QString();
} else {
return m_ESPs[iter->second].description;
}
}
boost::signals2::connection PluginList::onPluginStateChanged(
const std::function&)>& func)
{
return m_PluginStateChanged.connect(func);
}
void PluginList::pluginStatesChanged(QStringList const& pluginNames,
PluginStates state) const
{
if (pluginNames.isEmpty()) {
return;
}
std::map infos;
for (auto& name : pluginNames) {
infos[name] = state;
}
m_PluginStateChanged(infos);
}
boost::signals2::connection
PluginList::onRefreshed(const std::function& callback)
{
return m_Refreshed.connect(callback);
}
boost::signals2::connection
PluginList::onPluginMoved(const std::function& 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(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(m_ESPs[i].priority)) = i;
}
generatePluginIndexes();
}
void PluginList::generatePluginIndexes()
{
int numESLs = 0;
int numESHs = 0;
int numSkipped = 0;
auto gamePlugins = m_Organizer.gameFeatures().gameFeature();
const bool lightPluginsSupported =
gamePlugins ? gamePlugins->lightPluginsAreSupported() : false;
const bool mediumPluginsSupported =
gamePlugins ? gamePlugins->mediumPluginsAreSupported() : false;
std::vector coreLightPlugins;
std::vector coreMediumPlugins;
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 (mediumPluginsSupported && m_ESPs[i].isMediumFlagged) {
int ESHpos = 253 + (numESHs / 256);
m_ESPs[i].index = QString("%1:%2")
.arg(ESHpos, 2, 16, QChar('0'))
.arg(numESHs % 256, 2, 16, QChar('0'))
.toUpper();
++numESHs;
} else if (lightPluginsSupported &&
(m_ESPs[i].hasLightExtension || m_ESPs[i].isLightFlagged)) {
int ESLpos = 254 + (numESLs / 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 - numESHs - numESLs - numSkipped, 2, 16, QChar('0'))
.toUpper();
}
}
emit esplist_changed();
}
int PluginList::rowCount(const QModelIndex& parent) const
{
if (!parent.isValid()) {
return static_cast(m_ESPs.size());
} else {
return 0;
}
}
int PluginList::columnCount(const QModelIndex&) const
{
return COL_LASTCOLUMN + 1;
}
void PluginList::testMasters()
{
std::set 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();
const auto& plugin = m_ESPs[index];
switch (modelIndex.column()) {
case COL_NAME:
return plugin.name;
case COL_PRIORITY:
return QString::number(plugin.priority);
case COL_MODINDEX:
return plugin.index;
case COL_FORMVERSION:
return plugin.formVersion != 0 ? QString::number(plugin.formVersion) : QString();
case COL_HEADERVERSION:
return QString::number(plugin.headerVersion);
case COL_AUTHOR:
return plugin.author;
case COL_DESCRIPTION:
return plugin.description;
default:
return {};
}
}
QVariant PluginList::checkstateData(const QModelIndex& modelIndex) const
{
const int index = modelIndex.row();
if (m_ESPs[index].forceLoaded || m_ESPs[index].forceEnabled) {
return Qt::Checked;
} else if (m_ESPs[index].forceDisabled) {
return Qt::Unchecked;
}
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].forceLoaded) {
return QBrush(Qt::gray);
}
if ((modelIndex.column() == COL_NAME) && m_ESPs[index].forceDisabled) {
return QBrush(Qt::darkRed);
}
return {};
}
QVariant PluginList::backgroundData(const QModelIndex& modelIndex) const
{
const int index = modelIndex.row();
const ESPInfo& plugin = m_ESPs[index];
if (plugin.modSelected) {
return Settings::instance().colors().pluginListContained();
} else if (plugin.isMasterOfSelectedPlugin) {
return Settings::instance().colors().pluginListMaster();
}
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.setWeight(QFont::Bold);
if (m_ESPs[index].isLightFlagged || m_ESPs[index].hasLightExtension)
result.setItalic(true);
else if (m_ESPs[index].isMediumFlagged)
result.setUnderline(true);
if (m_BlueprintPlugins &&
(m_ESPs[index].isBlueprintFlagged || m_ESPs[index].isBlueprintPrefixed))
result.setLetterSpacing(QFont::SpacingType::AbsoluteSpacing, 2);
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 += "" + tr("Origin") + ": " + esp.originName;
if (esp.forceLoaded) {
toolTip += "
" +
tr("This plugin can't be disabled or moved (enforced by the game).") +
"";
}
if (esp.forceEnabled) {
toolTip += "
" +
tr("This plugin can't be disabled (enforced by the game).") + "";
}
if (esp.formVersion != 0) {
// Oblivion-style plugin headers don't have a form version
toolTip +=
"
" + tr("Form Version") + ": " + QString::number(esp.formVersion);
}
toolTip +=
"
" + tr("Header Version") + ": " + QString::number(esp.headerVersion);
if (!esp.author.isEmpty()) {
toolTip += "
" + tr("Author") + ": " + TruncateString(esp.author);
}
if (esp.description.size() > 0) {
toolTip +=
"
" + tr("Description") + ": " + TruncateString(esp.description);
}
if (esp.masterUnset.size() > 0) {
toolTip +=
"
" + tr("Missing Masters") + ": " + "" +
TruncateString(
QStringList(esp.masterUnset.begin(), esp.masterUnset.end()).join(", ")) +
"";
}
std::set 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 += "
" + tr("Enabled Masters") +
": " + TruncateString(SetJoin(enabledMasters, ", "));
}
if (!esp.archives.empty()) {
QString archiveString =
esp.archives.size() < 6
? TruncateString(
QStringList(esp.archives.begin(), esp.archives.end()).join(", ")) +
"
"
: "";
toolTip += "
" + tr("Loads Archives") + ": " + archiveString +
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 += "
" + tr("Loads INI settings") +
": "
"
" +
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) {
QString type = esp.hasMasterExtension ? "ESM" : "ESP";
toolTip +=
"
" +
tr("This %1 is flagged as a light plugin (ESL). It will adhere to the %1 load "
"order but the records will be loaded in ESL space (FE/FF). You can have up "
"to 4096 light plugins in addition to other plugin types.")
.arg(type);
} else if (esp.isMediumFlagged && esp.hasMasterExtension) {
toolTip += "
" +
tr("This ESM is flagged as a medium plugin (ESH). It adheres to the ESM "
"load order but loads records in ESH space (FD). You can have 256 "
"medium plugins in addition to other plugin types.");
}
if (m_BlueprintPlugins) {
if (esp.isBlueprintFlagged) {
toolTip += "
" +
tr("This plugin has the blueprint flag. This forces it to load after "
"every other non-blueprint plugin.");
if (!esp.enabled) {
toolTip += " " +
tr("Blueprint plugins are removed "
"from plugins.txt by the game and are effectively disabled. They "
"are intended to be loaded by using the blueprint prefix:") +
" " + m_GamePlugin->blueprintPrefix() + "[main plugin].esm";
} else if (esp.isBlueprintPrefixed) {
toolTip +=
" " +
tr("This plugin is prefixed and being loaded by a paired main plugin: ") +
" " + m_GamePlugin->blueprintPrefix() + "[main plugin].esm";
}
} else if (esp.isBlueprintPrefixed && esp.enabled) {
toolTip += "
" +
tr("WARNING: This is a blueprint-prefixed but unflagged plugin being "
"autoloaded by another main plugin file. This is unintended usage "
"and may result in an ambiguous load order.");
}
}
if (esp.isLightFlagged && esp.isMediumFlagged) {
toolTip +=
"
" +
tr("WARNING: This plugin is both light and medium flagged. "
"This could indicate that the file was saved improperly "
"and may have mismatched record references. Use it at your own risk.");
}
if (esp.hasNoRecords) {
toolTip += "
" + tr("This is a dummy plugin. It contains no records and is "
"typically used to load a paired archive file.");
}
if (esp.forceDisabled) {
auto feature = m_Organizer.gameFeatures().gameFeature();
if (feature && esp.hasLightExtension && feature->lightPluginsAreSupported()) {
toolTip += "
" + tr("Light plugins (ESL) are not supported by this game.");
} else if (m_BlueprintPlugins && esp.isBlueprintFlagged &&
esp.isBlueprintPrefixed) {
toolTip += "
" +
tr("This blueprint plugin must be enabled by a paired main plugin.");
} else if (m_BlueprintPlugins && esp.isBlueprintFlagged) {
toolTip += "
" + tr("This blueprint plugin is not using the blueprint "
"prefix and can't be loaded.");
} else if (m_BlueprintPlugins && esp.isBlueprintPrefixed) {
toolTip +=
"
" + tr("This is an invalid blueprint file with no blueprint flag.");
} else {
toolTip += "
" + tr("This game does not currently permit custom plugin "
"loading. There may be manual workarounds.");
}
}
// additional info
auto itor = m_AdditionalInfo.find(esp.name);
if (itor != m_AdditionalInfo.end()) {
if (!itor->second.messages.isEmpty()) {
toolTip += "
";
for (auto&& message : itor->second.messages) {
toolTip += "- " + message + "
";
}
toolTip += "
";
}
// loot
toolTip += makeLootTooltip(itor->second.loot);
}
return toolTip;
}
QString PluginList::makeLootTooltip(const Loot::Plugin& loot) const
{
QString s;
for (auto&& f : loot.incompatibilities) {
s += "" +
tr("Incompatible with %1")
.arg(f.displayName.isEmpty() ? f.name : f.displayName) +
"";
}
for (auto&& m : loot.missingMasters) {
s += "" + tr("Depends on missing %1").arg(m) + "";
}
for (auto&& m : loot.messages) {
s += "";
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 + "";
}
for (auto&& d : loot.dirty) {
s += "" + d.toString(false) + "";
}
for (auto&& c : loot.clean) {
s += "" + c.toString(true) + "";
}
if (!s.isEmpty()) {
s = "
"
"";
}
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 (esp.isMediumFlagged) {
result.append(":/MO/gui/run");
if (esp.isLightFlagged) {
result.append(":/MO/gui/warning");
}
}
if (m_BlueprintPlugins && esp.isBlueprintFlagged) {
result.append(":/MO/gui/resources/go-down.png");
}
if (esp.hasNoRecords) {
result.append(":/MO/gui/unchecked-checkbox");
}
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 (m_BlueprintPlugins) {
if (esp.isBlueprintPrefixed && esp.enabled && !esp.isBlueprintFlagged) {
return true;
}
if (esp.isBlueprintFlagged && !esp.isBlueprintPrefixed) {
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()].forceLoaded ||
m_ESPs[modIndex.row()].forceEnabled;
m_LastCheck.restart();
emit dataChanged(modIndex, modIndex);
if (m_BlueprintPlugins &&
!m_ESPs[modIndex.row()].name.startsWith(m_GamePlugin->blueprintPrefix(),
Qt::CaseInsensitive)) {
QString blueprint = m_GamePlugin->blueprintPrefix();
blueprint +=
m_ESPs[modIndex.row()].name.left(m_ESPs[modIndex.row()].name.size() - 4) +
".esm";
auto blueprintPlugin = m_ESPsByName.find(blueprint);
if (blueprintPlugin != m_ESPsByName.end()) {
if (m_ESPs[modIndex.row()].enabled) {
m_ESPs[blueprintPlugin->second].forceDisabled = false;
m_ESPs[blueprintPlugin->second].forceEnabled = true;
m_ESPs[blueprintPlugin->second].enabled = true;
} else {
m_ESPs[blueprintPlugin->second].forceDisabled = true;
m_ESPs[blueprintPlugin->second].forceEnabled = false;
m_ESPs[blueprintPlugin->second].enabled = false;
}
}
}
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(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].forceLoaded && !m_ESPs[index].forceDisabled)
result |= Qt::ItemIsDragEnabled;
if (!m_ESPs[index].forceLoaded && !m_ESPs[index].forceEnabled &&
!m_ESPs[index].forceDisabled)
result |= Qt::ItemIsUserCheckable;
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(m_ESPsByPriority.size()))
newPriorityTemp = static_cast(m_ESPsByPriority.size()) - 1;
int blueprintStartPos = 0;
for (auto esp : m_ESPs) {
if (!esp.isBlueprintFlagged) {
blueprintStartPos++;
}
}
bool isBlueprint = m_ESPs[row].isBlueprintFlagged;
int lowerLimit = 0;
int upperLimit = 0;
if (isBlueprint) {
if (newPriorityTemp < blueprintStartPos) {
newPriorityTemp = blueprintStartPos;
}
lowerLimit = blueprintStartPos;
upperLimit = static_cast(m_ESPsByPriority.size() - 1);
}
if (!isBlueprint) {
if (newPriorityTemp >= blueprintStartPos) {
newPriorityTemp = blueprintStartPos - 1;
}
upperLimit = blueprintStartPos - 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 < upperLimit) &&
(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 > lowerLimit) &&
!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 < upperLimit) &&
(m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).forceLoaded)) {
++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()) {
if (m_ESPs[iter->second].isBlueprintFlagged ==
m_ESPs.at(row).isBlueprintFlagged) {
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) {
auto iter = m_ESPsByName.find(master);
if (iter != m_ESPsByName.end()) {
if (m_ESPs[iter->second].isBlueprintFlagged ==
otherInfo->isBlueprintFlagged) {
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 rows, int newPriority)
{
ChangeBracket layoutChange(this);
const std::vector& esp = m_ESPs;
// 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::const_iterator iter = rows.begin(); iter != rows.end();
++iter) {
if (m_ESPs[*iter].priority < newPriority) {
--newPriority;
break;
}
}
for (std::vector::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 sourceRows;
while (!stream.atEnd()) {
int sourceRow, col;
QMap 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(m_ESPs.size()))) {
newPriority = static_cast(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 forceLoaded, bool forceEnabled,
bool forceDisabled, const QString& originName,
const QString& fullPath, bool hasIni,
std::set archives, bool lightSupported,
bool mediumSupported, bool blueprintSupported,
const QString& blueprintPrefix)
: name(name), fullPath(fullPath), enabled(forceLoaded), forceLoaded(forceLoaded),
forceEnabled(forceEnabled), forceDisabled(forceDisabled), priority(0),
loadOrder(-1), originName(originName), hasIni(hasIni),
archives(archives.begin(), archives.end()), modSelected(false),
isMasterOfSelectedPlugin(false)
{
try {
ESP::File file(ToWString(fullPath));
auto extension = name.right(3).toLower();
hasMasterExtension = (extension == "esm");
hasLightExtension = (extension == "esl");
isMasterFlagged = file.isMaster();
isLightFlagged = lightSupported && file.isLight(mediumSupported);
isMediumFlagged = mediumSupported && file.isMedium();
isBlueprintFlagged = blueprintSupported &&
(isMasterFlagged || hasMasterExtension || hasLightExtension) &&
file.isBlueprint();
isBlueprintPrefixed =
blueprintSupported && name.startsWith(blueprintPrefix, Qt::CaseInsensitive);
hasNoRecords = file.isDummy();
if (blueprintSupported) {
if ((isBlueprintFlagged || isBlueprintPrefixed) && !this->forceEnabled) {
this->forceDisabled = true;
}
}
hasNoRecords = file.isDummy();
formVersion = file.formVersion();
headerVersion = file.headerVersion();
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;
isMediumFlagged = false;
isBlueprintFlagged = false;
hasNoRecords = false;
}
}
void PluginList::managedGameChanged(const IPluginGame* gamePlugin)
{
m_GamePlugin = gamePlugin;
}