Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions src/directoryrefresher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,9 @@ struct ModThread
std::wstring path;
int prio = -1;
std::vector<std::wstring> archives;
std::set<std::wstring> enabledArchives;
std::vector<std::wstring>* loadOrder = nullptr;
DirectoryStats* stats = nullptr;
const std::set<std::wstring>* enabledArchives = nullptr;
std::vector<std::wstring>* loadOrder = nullptr;
DirectoryStats* stats = nullptr;
env::DirectoryWalker walker;

std::condition_variable cv;
Expand Down Expand Up @@ -360,7 +360,7 @@ struct ModThread
ds->addFromOrigin(walker, modName, path, prio, *stats);

if (Settings::instance().archiveParsing()) {
ds->addFromAllBSAs(modName, path, prio, archives, enabledArchives, *loadOrder,
ds->addFromAllBSAs(modName, path, prio, archives, *enabledArchives, *loadOrder,
*stats);
}

Expand Down Expand Up @@ -406,6 +406,13 @@ void DirectoryRefresher::addMultipleModsFilesToStructure(
}
}

// the enabled-archive set is the same for every mod, so convert it to wstrings
// once here and share it by pointer instead of rebuilding it per mod thread
std::set<std::wstring> enabledArchives;
for (auto&& a : m_EnabledArchives) {
enabledArchives.insert(a.toStdWString());
}

for (std::size_t i = 0; i < entries.size(); ++i) {
const auto& e = entries[i];
const int prio = e.priority + 1;
Expand Down Expand Up @@ -437,10 +444,7 @@ void DirectoryRefresher::addMultipleModsFilesToStructure(
mt.archives.push_back(a.toStdWString());
}

mt.enabledArchives.clear();
for (auto&& a : m_EnabledArchives) {
mt.enabledArchives.insert(a.toStdWString());
}
mt.enabledArchives = &enabledArchives;

mt.loadOrder = &loadOrder;
mt.stats = &stats[i];
Expand Down
4 changes: 3 additions & 1 deletion src/envfs.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ class ThreadPool
}
}

std::this_thread::sleep_for(std::chrono::milliseconds(1));
// Constantly yield here - windows timer is too coarse otherwise (~16ms), leads to
// wasting a LOT of time idling
std::this_thread::yield();
}
}

Expand Down
17 changes: 16 additions & 1 deletion src/moapplication.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,22 @@ using namespace MOShared;
class ProxyStyle : public QProxyStyle
{
public:
ProxyStyle(QStyle* baseStyle = 0) : QProxyStyle(baseStyle) {}
ProxyStyle(QStyle* baseStyle = 0) : QProxyStyle(baseStyle)
{
// The Windows Vista / modern Windows style calls
// SystemParametersInfo(SPI_GETCLIENTAREAANIMATION) from drawControl() on
// *every* paint to decide whether to run transition animations. Item-view
// painting hits drawControl once per drawn cell. The whole
// animation pathis skipped when the style
// object carries a "_q_no_animation" property, so set it on the base style.

// Item views don't use these transitions, so nothing visible is lost there;
// the only effect elsewhere is dropping the subtle widget transition
// animations (e.g. button hover fades), which is a worthwhile trade.
if (baseStyle) {
baseStyle->setProperty("_q_no_animation", true);
}
}

void drawPrimitive(PrimitiveElement element, const QStyleOption* option,
QPainter* painter, const QWidget* widget) const override
Expand Down
57 changes: 45 additions & 12 deletions src/modinfowithconflictinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "shared/filesorigin.h"
#include "utility.h"
#include <filesystem>
#include <map>

#include "iplugingame.h"
#include "moddatachecker.h"
Expand Down Expand Up @@ -118,10 +119,46 @@ ModInfoWithConflictInfo::Conflicts ModInfoWithConflictInfo::doConflictCheck() co
std::vector<FileEntryPtr> files = origin.getFiles();
std::set<const DirectoryEntry*> checkedDirs;

// Hidden files/dirs end with ".mohidden". Test the suffix directly on the
// wide string (ASCII, case-insensitive) instead of converting every file and
// directory name to a QString just to call endsWith.
const std::wstring hiddenExt = ToWString(ModInfo::s_HiddenExt);
const auto endsWithHiddenExt = [&hiddenExt](const std::wstring& s) {
if (s.size() < hiddenExt.size()) {
return false;
}
const auto offset = s.size() - hiddenExt.size();
for (size_t i = 0; i < hiddenExt.size(); ++i) {
wchar_t a = s[offset + i];
wchar_t b = hiddenExt[i];
if (a >= L'A' && a <= L'Z')
a = static_cast<wchar_t>(a + (L'a' - L'A'));
if (b >= L'A' && b <= L'Z')
b = static_cast<wchar_t>(b + (L'a' - L'A'));
if (a != b) {
return false;
}
}
return true;
};

// The same alternative origins recur across many files; cache the
// origin-id -> mod-index mapping to avoid repeating the name lookup.
std::map<int, unsigned int> originModIndex;
const auto modIndexForOrigin = [&](int originID) -> unsigned int {
auto it = originModIndex.find(originID);
if (it != originModIndex.end()) {
return it->second;
}
const auto idx = ModInfo::getIndex(
ToQString(m_Core.directoryStructure()->getOriginByID(originID).getName()));
originModIndex.emplace(originID, idx);
return idx;
};

// for all files in this origin
for (FileEntryPtr file : files) {
if (QString::fromStdWString(file->getName())
.endsWith(ModInfo::s_HiddenExt, Qt::CaseInsensitive)) {
for (const FileEntryPtr& file : files) {
if (endsWithHiddenExt(file->getName())) {
hasHiddenFiles = true;
// skip hidden file conflicts
continue;
Expand All @@ -138,8 +175,7 @@ ModInfoWithConflictInfo::Conflicts ModInfoWithConflictInfo::doConflictCheck() co
// well
break;
} else {
if (QString::fromStdWString(parent->getName())
.endsWith(ModInfo::s_HiddenExt, Qt::CaseInsensitive)) {
if (endsWithHiddenExt(parent->getName())) {
hasHiddenFiles = hidden = true;
break;
}
Expand All @@ -152,8 +188,8 @@ ModInfoWithConflictInfo::Conflicts ModInfoWithConflictInfo::doConflictCheck() co
}
}

hasVisibleFiles = true;
auto alternatives = file->getAlternatives();
hasVisibleFiles = true;
const auto& alternatives = file->getAlternatives();
if ((alternatives.size() == 0) ||
std::find(dataIDs.begin(), dataIDs.end(), alternatives.back().originID()) !=
dataIDs.end()) {
Expand All @@ -175,9 +211,7 @@ ModInfoWithConflictInfo::Conflicts ModInfoWithConflictInfo::doConflictCheck() co

// If this is not the origin then determine the correct overwrite
if (file->getOrigin() != origin.getID()) {
FilesOrigin& altOrigin =
m_Core.directoryStructure()->getOriginByID(file->getOrigin());
unsigned int altIndex = ModInfo::getIndex(ToQString(altOrigin.getName()));
unsigned int altIndex = modIndexForOrigin(file->getOrigin());
if (!file->isFromArchive()) {
if (!archiveData.isValid())
conflicts.m_OverwrittenList.insert(altIndex);
Expand All @@ -197,8 +231,7 @@ ModInfoWithConflictInfo::Conflicts ModInfoWithConflictInfo::doConflictCheck() co
(altInfo.originID() != origin.getID())) {
FilesOrigin& altOrigin =
m_Core.directoryStructure()->getOriginByID(altInfo.originID());
QString altOriginName = ToQString(altOrigin.getName());
unsigned int altIndex = ModInfo::getIndex(altOriginName);
unsigned int altIndex = modIndexForOrigin(altInfo.originID());
if (!altInfo.isFromArchive()) {
if (!archiveData.isValid()) {
if (origin.getPriority() > altOrigin.getPriority()) {
Expand Down
11 changes: 6 additions & 5 deletions src/modlistversiondelegate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
#include "settings.h"

ModListVersionDelegate::ModListVersionDelegate(ModListView* view, Settings& settings)
: QItemDelegate(view), m_view(view), m_settings(settings)
: QItemDelegate(view), m_view(view), m_settings(settings),
m_upgradeIcon(":/MO/gui/update_available"), m_downgradeIcon(":/MO/gui/warning")
{}

void ModListVersionDelegate::paint(QPainter* painter,
Expand Down Expand Up @@ -41,8 +42,8 @@ void ModListVersionDelegate::paint(QPainter* painter,
opt.decorationAlignment = Qt::AlignCenter;

if (upgrade) {
QIcon icon(":/MO/gui/update_available");
QPixmap pixmap = decoration(opt, icon);
const QIcon& icon = m_upgradeIcon;
QPixmap pixmap = decoration(opt, icon);

QSize pm = icon.actualSize(opt.decorationSize);
pm.rwidth() += 2 * margin;
Expand All @@ -54,8 +55,8 @@ void ModListVersionDelegate::paint(QPainter* painter,
}

if (downgrade) {
QIcon icon(":/MO/gui/warning");
QPixmap pixmap = decoration(opt, icon);
const QIcon& icon = m_downgradeIcon;
QPixmap pixmap = decoration(opt, icon);

QSize pm = icon.actualSize(opt.decorationSize);
pm.rwidth() += 2 * margin;
Expand Down
4 changes: 4 additions & 0 deletions src/modlistversiondelegate.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef MODLISTVERSIONDELEGATE_H
#define MODLISTVERSIONDELEGATE_H

#include <QIcon>
#include <QStyledItemDelegate>

class ModListView;
Expand All @@ -17,6 +18,9 @@ class ModListVersionDelegate : public QItemDelegate
private:
ModListView* m_view;
Settings& m_settings;

QIcon m_upgradeIcon;
QIcon m_downgradeIcon;
};

#endif
94 changes: 80 additions & 14 deletions src/organizercore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1292,18 +1292,19 @@ void OrganizerCore::refresh(bool saveChanges)
emit refreshTriggered();
}

void OrganizerCore::refreshESPList(bool force)
void OrganizerCore::refreshESPList(bool force, bool lightRefresh)
{
onNextRefresh(
[this, force] {
[this, force, lightRefresh] {
TimeThis tt("OrganizerCore::refreshESPList()");

m_CurrentProfile->writeModlist();

// clear list
try {
m_PluginList.refresh(m_CurrentProfile->name(), *m_DirectoryStructure,
m_CurrentProfile->getLockedOrderFileName(), force);
m_CurrentProfile->getLockedOrderFileName(), force,
lightRefresh);
} catch (const std::exception& e) {
reportError(tr("Failed to refresh list of esps: %1").arg(e.what()));
}
Expand Down Expand Up @@ -1450,23 +1451,24 @@ void OrganizerCore::updateModsInDirectoryStructure(
m_DirectoryRefresher->addMultipleModsFilesToStructure(m_DirectoryStructure, entries);

DirectoryRefresher::cleanStructure(m_DirectoryStructure);
// need to refresh plugin list now so we can activate esps
refreshESPList(true);
// need to refresh plugin list now so we can activate esps. A light refresh is
// enough here: it only has to populate the plugin list and the enabled states
// for updateModsActiveState below. The caller (modStatusChanged) does a full
// refreshLists() afterwards, which rebuilds everything the light refresh skipped.
refreshESPList(true, /*lightRefresh=*/true);
// activate all esps of the specified mod so the bsas get activated along with
// it
m_PluginList.blockSignals(true);
updateModsActiveState(modInfo.keys(), true);
m_PluginList.blockSignals(false);
// now we need to refresh the bsa list and save it so there is no confusion
// about what archives are available and active
refreshBSAList();
if (m_UserInterface != nullptr) {
m_UserInterface->archivesWriter().writeImmediately(false);
}

std::vector<QString> archives = enabledArchives();
m_DirectoryRefresher->setMods(m_CurrentProfile->getActiveMods(),
std::set<QString>(archives.begin(), archives.end()));
// determine which archives are active so the directory refresher knows which
// bsas to load. This used to go through a full refreshBSAList() (which rebuilds
// the BSA tree widget and round-trips the active set through the archives file);
// since the trailing refreshLists() rebuilds the tree and rewrites that file
// anyway, compute the active set directly and skip the redundant UI work here.
const std::set<QString> archives = activeArchives();
m_DirectoryRefresher->setMods(m_CurrentProfile->getActiveMods(), archives);

// finally also add files from bsas to the directory structure
for (auto idx : modInfo.keys()) {
Expand Down Expand Up @@ -1587,6 +1589,70 @@ std::vector<QString> OrganizerCore::enabledArchives()
return result;
}

std::set<QString> OrganizerCore::activeArchives()
{
std::set<QString> result;

auto archives = gameFeatures().gameFeature<DataArchives>();
if (archives == nullptr) {
return result;
}

// default archives are the ones enabled outside MO (see refreshBSAList)
QStringList defaultArchives = archives->archives(m_CurrentProfile.get());
if (defaultArchives.isEmpty()) {
defaultArchives = archives->vanillaArchives();
}
const bool forceCore = settings().game().forceEnableCoreFiles();

// plugins present in the virtual data directory, keyed by their base name so we
// can match archives like "MyMod - Textures.bsa" to the plugin "MyMod.esp"
const QStringList plugins = findFiles("", [](const QString& fileName) -> bool {
return fileName.endsWith(".esp", Qt::CaseInsensitive) ||
fileName.endsWith(".esm", Qt::CaseInsensitive) ||
fileName.endsWith(".esl", Qt::CaseInsensitive);
});

QList<std::pair<QString, QString>> pluginNamePairs;
pluginNamePairs.reserve(plugins.size());
for (const QString& pluginName : plugins) {
QFileInfo pluginInfo(pluginName);
pluginNamePairs.append(
std::make_pair(pluginInfo.completeBaseName(), pluginInfo.fileName()));
}

auto hasAssociatedPlugin = [&](const QString& bsaName) -> bool {
for (const auto& [completeBaseName, fileName] : pluginNamePairs) {
if (bsaName.startsWith(completeBaseName, Qt::CaseInsensitive) &&
(m_PluginList.state(fileName) == IPluginList::STATE_ACTIVE)) {
return true;
}
}
return false;
};

// this must stay in sync with the check-state logic in MainWindow::updateBSAList
for (FileEntryPtr current : m_DirectoryStructure->getFiles()) {
if (current.get() == nullptr) {
continue;
}
QFileInfo fileInfo(ToQString(current->getName().c_str()));
const QString suffix = fileInfo.suffix().toLower();
if (suffix != "bsa" && suffix != "ba2") {
continue;
}

const QString name = fileInfo.fileName();
if ((forceCore && defaultArchives.contains(name)) ||
(name.compare("update.bsa", Qt::CaseInsensitive) == 0) ||
hasAssociatedPlugin(name)) {
result.insert(name);
}
}

return result;
}

void OrganizerCore::refreshDirectoryStructure()
{
if (m_DirectoryUpdate) {
Expand Down
14 changes: 13 additions & 1 deletion src/organizercore.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class PluginContainer;
class DirectoryRefresher;

#include <memory>
#include <set>
#include <vector>

namespace MOBase
Expand Down Expand Up @@ -280,6 +281,12 @@ class OrganizerCore : public QObject, public MOBase::IPluginDiagnose

std::vector<QString> enabledArchives();

// computes the set of archives (bsa/ba2) that should be active, using the same
// rules as MainWindow::updateBSAList (core/default archives, update.bsa and
// archives associated with an active plugin) but without touching the UI. Used
// to feed the directory refresher without rebuilding the BSA tree widget.
std::set<QString> activeArchives();

MOBase::Version getVersion() const { return m_Updater.getVersion(); }

// return the plugin container
Expand Down Expand Up @@ -318,7 +325,12 @@ class OrganizerCore : public QObject, public MOBase::IPluginDiagnose
ProcessRunner::Results
waitForAllUSVFSProcesses(UILocker::Reasons reason = UILocker::PreventExit);

void refreshESPList(bool force = false);
// when lightRefresh is true, only the work required to query and toggle plugin
// activation state is performed (plugin list + enabled states); the expensive
// relationship/master/index/load-order finalization is skipped. This is used
// for the intermediate refresh during a mod state change, where a second, full
// refresh follows anyway.
void refreshESPList(bool force = false, bool lightRefresh = false);
void refreshBSAList();

void refreshDirectoryStructure();
Expand Down
Loading
Loading