From 9fb639a00e1a809a9ea9b5e68777ec46f534761c Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sun, 24 May 2020 13:27:40 +0200 Subject: Update ModInfo to use a single file tree for all content-related operations. --- src/mainwindow.cpp | 4 +- src/modinfo.cpp | 7 +-- src/modinfo.h | 23 ++++------ src/modinfooverwrite.cpp | 1 - src/modinforegular.cpp | 94 ++++++++++++++++++++--------------------- src/modinforegular.h | 3 -- src/modinfowithconflictinfo.cpp | 18 +++++++- src/modinfowithconflictinfo.h | 38 ++++++++++++++++- src/modlist.cpp | 4 +- src/modlist.h | 2 - src/organizercore.cpp | 2 +- 11 files changed, 114 insertions(+), 82 deletions(-) (limited to 'src') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index b5beb229..cc681dba 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -3062,7 +3062,7 @@ void MainWindow::ignoreMissingData_clicked() int row_idx = idx.data(Qt::UserRole + 1).toInt(); ModInfo::Ptr info = ModInfo::getByIndex(row_idx); //QDir(info->absolutePath()).mkdir("textures"); - info->testValid(); + info->diskContentModified(); info->markValidated(true); connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); @@ -3071,7 +3071,7 @@ void MainWindow::ignoreMissingData_clicked() } else { ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); //QDir(info->absolutePath()).mkdir("textures"); - info->testValid(); + info->diskContentModified(); info->markValidated(true); connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 379fa071..0dd113c5 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -306,7 +306,7 @@ void ModInfo::updateIndices() ModInfo::ModInfo(PluginContainer *pluginContainer) - : m_Valid(false), m_PrimaryCategory(-1) + : m_PrimaryCategory(-1) { } @@ -528,11 +528,6 @@ bool ModInfo::categorySet(int categoryID) const return false; } -void ModInfo::testValid() -{ - m_Valid = doTestValid(); -} - QUrl ModInfo::parseCustomURL() const { if (!hasCustomURL() || getCustomURL().isEmpty()) { diff --git a/src/modinfo.h b/src/modinfo.h index f582cc32..b3e75bc4 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -702,7 +702,7 @@ public: /** * @return true if this mod is considered "valid", that is: it contains data used by the game **/ - virtual bool isValid() const { return m_Valid; } + virtual bool isValid() const = 0; /** * @return true if the file has been endorsed on nexus @@ -714,11 +714,6 @@ public: */ virtual ETrackedState trackedState() const { return TRACKED_FALSE; } - /** - * @brief updates the valid-flag for this mod - */ - void testValid(); - /** * @brief updates the mod to flag it as converted in order to ignore the alternate game warning */ @@ -801,6 +796,13 @@ public: **/ QUrl parseCustomURL() const; +public slots: + + /** + * @brief Notify this mod that the content of the disk may have changed. + */ + virtual void diskContentModified() = 0; + signals: /** @@ -817,13 +819,6 @@ protected: static void updateIndices(); static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS); - /** - * @brief check if the content of this mod is valid. - * - * @return true if the content is valid, false otherwize. - **/ - virtual bool doTestValid() const = 0; - private: static void createFromOverwrite(PluginContainer *pluginContainer, @@ -848,8 +843,6 @@ private: static std::map, std::vector > s_ModsByModID; static int s_NextID; - bool m_Valid; - }; diff --git a/src/modinfooverwrite.cpp b/src/modinfooverwrite.cpp index 3b914134..8aad6209 100644 --- a/src/modinfooverwrite.cpp +++ b/src/modinfooverwrite.cpp @@ -9,7 +9,6 @@ ModInfoOverwrite::ModInfoOverwrite(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, MOShared::DirectoryEntry **directoryStructure) : ModInfoWithConflictInfo(pluginContainer, game, directoryStructure) { - testValid(); } bool ModInfoOverwrite::isEmpty() const diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index d89716da..3c1d49ea 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -38,7 +38,6 @@ ModInfoRegular::ModInfoRegular(PluginContainer *pluginContainer, const IPluginGa , m_TrackedState(TRACKED_UNKNOWN) , m_NexusBridge(pluginContainer) { - testValid(); m_CreationTime = QFileInfo(path.absolutePath()).birthTime(); // read out the meta-file for information readMeta(); @@ -656,58 +655,59 @@ std::vector ModInfoRegular::getFlags() const std::vector ModInfoRegular::getContents() const { - QTime now = QTime::currentTime(); - if (m_LastContentCheck.isNull() || (m_LastContentCheck.secsTo(now) > 60)) { - m_CachedContent.clear(); - QDir dir(absolutePath()); - if (dir.entryList(QStringList() << "*.esp" << "*.esm" << "*.esl").size() > 0) { - m_CachedContent.push_back(CONTENT_PLUGIN); - } - if (dir.entryList(QStringList() << "*.bsa" << "*.ba2").size() > 0) { - m_CachedContent.push_back(CONTENT_BSA); - } - //use >1 for ini files since there is meta.ini in all mods already. - if (dir.entryList(QStringList() << "*.ini").size() > 1) { - m_CachedContent.push_back(CONTENT_INI); - } + auto tree = contentFileTree(); + std::vector contents; - if (dir.entryList(QStringList() << "*.modgroups").size() > 0) { - m_CachedContent.push_back(CONTENT_MODGROUP); + for (auto e : *tree) { + if (e->isFile()) { + auto suffix = e->suffix(); + if (suffix == "esp" || suffix == "esm" || suffix == "esl") { + contents.push_back(CONTENT_PLUGIN); + } + else if (suffix == "bsa" || suffix == "ba2") { + contents.push_back(CONTENT_BSA); + } + //use >1 for ini files since there is meta.ini in all mods already. + else if (suffix == "ini" && e->compare("meta.ini") != 0) { + contents.push_back(CONTENT_INI); + } + else if (suffix == "modgroups") { + contents.push_back(CONTENT_MODGROUP); + } + } + else { + if (e->compare("textures") == 0 || e->compare("icons") == 0 || e->compare("bookart") == 0) + contents.push_back(CONTENT_TEXTURE); + if (e->compare("meshes") == 0) + contents.push_back(CONTENT_MESH); + if (e->compare("interface") == 0 || e->compare("menus") == 0) + contents.push_back(CONTENT_INTERFACE); + if (e->compare("music") == 0 || e->compare("sound") == 0) + contents.push_back(CONTENT_SOUND); + if (e->compare("scripts") == 0) + contents.push_back(CONTENT_SCRIPT); + if (e->compare("SkyProc Patchers") == 0) + contents.push_back(CONTENT_SKYPROC); + if (e->compare("MCM") == 0) + contents.push_back(CONTENT_MCM); } + } - ScriptExtender *extender = qApp->property("managed_game") - .value() - ->feature(); - - if (extender != nullptr) { - QString sePluginPath = extender->PluginPath(); - if (dir.exists(sePluginPath)) { - m_CachedContent.push_back(CONTENT_SKSEFILES); - QDir sePluginDir(absolutePath() + "/" + sePluginPath); - if (sePluginDir.entryList(QStringList() << "*.dll").size() > 0) { - m_CachedContent.push_back(CONTENT_SKSE); + ScriptExtender* extender = m_GamePlugin->feature(); + if (extender != nullptr) { + auto e = tree->findDirectory(extender->PluginPath()); + if (e) { + contents.push_back(CONTENT_SKSEFILES); + for (auto f : *e) { + if (e->suffix().compare("dll") == 0) { + contents.push_back(CONTENT_SKSE); + break; } } } - if (dir.exists("textures") || dir.exists("icons") || dir.exists("bookart")) - m_CachedContent.push_back(CONTENT_TEXTURE); - if (dir.exists("meshes")) - m_CachedContent.push_back(CONTENT_MESH); - if (dir.exists("interface") || dir.exists("menus")) - m_CachedContent.push_back(CONTENT_INTERFACE); - if (dir.exists("music") || dir.exists("sound")) - m_CachedContent.push_back(CONTENT_SOUND); - if (dir.exists("scripts")) - m_CachedContent.push_back(CONTENT_SCRIPT); - if (dir.exists("SkyProc Patchers")) - m_CachedContent.push_back(CONTENT_SKYPROC); - if (dir.exists("MCM")) - m_CachedContent.push_back(CONTENT_MCM); - - m_LastContentCheck = QTime::currentTime(); - } - - return m_CachedContent; + } + + return contents; } diff --git a/src/modinforegular.h b/src/modinforegular.h index 0f22b6cb..08546c69 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -462,9 +462,6 @@ private: NexusBridge m_NexusBridge; - mutable std::vector m_CachedContent; - mutable QTime m_LastContentCheck; - bool needsDescriptionUpdate() const; }; diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp index 0c67b4f7..45ecffb7 100644 --- a/src/modinfowithconflictinfo.cpp +++ b/src/modinfowithconflictinfo.cpp @@ -296,14 +296,30 @@ bool ModInfoWithConflictInfo::hasHiddenFiles() const return m_HasHiddenFiles; } +void ModInfoWithConflictInfo::diskContentModified() { + std::unique_lock lock(m_Mutex); + m_FileTree = nullptr; +} + +std::shared_ptr ModInfoWithConflictInfo::contentFileTree() const { + std::unique_lock lock(m_Mutex); + if (!m_FileTree) { + m_FileTree = QDirFileTree::makeTree(absolutePath()); + } + return m_FileTree; +} bool ModInfoWithConflictInfo::doTestValid() const { auto mdc = m_GamePlugin->feature(); if (mdc) { - auto qdirfiletree = QDirFileTree::makeTree(absolutePath()); + auto qdirfiletree = contentFileTree(); return mdc->dataLooksValid(qdirfiletree); } return true; } + +bool ModInfoWithConflictInfo::isValid() const { + return doTestValid(); +} diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h index 94a61c62..5bc1d585 100644 --- a/src/modinfowithconflictinfo.h +++ b/src/modinfowithconflictinfo.h @@ -1,6 +1,8 @@ #ifndef MODINFOWITHCONFLICTINFO_H #define MODINFOWITHCONFLICTINFO_H +#include + #include "modinfo.h" #include @@ -13,6 +15,11 @@ public: std::vector getConflictFlags() const override; virtual std::vector getFlags() const override; + /** + * @return true if this mod is considered "valid", that is: it contains data used by the game + **/ + virtual bool isValid() const override; + /** * @brief clear all caches held for this mod */ @@ -32,6 +39,13 @@ public: virtual void doConflictCheck() const override; +public slots: + + /** + * @brief Notify this mod that the content of the disk may have changed. + */ + virtual void diskContentModified(); + protected: /** @@ -41,6 +55,17 @@ protected: **/ virtual bool doTestValid() const; + /** + * @brief Retrieve a file tree corresponding to the underlying disk content + * of this mod. + * + * The file tree should not be cached since it is already cached and updated when + * required. + * + * @return a file tree representing the content of this mod. + */ + std::shared_ptr contentFileTree() const; + ModInfoWithConflictInfo( PluginContainer* pluginContainer, const MOBase::IPluginGame* gamePlugin, @@ -81,10 +106,19 @@ private: bool hasHiddenFiles() const; -private: +protected: // Current game plugin running in MO2: - MOBase::IPluginGame const* m_GamePlugin; + MOBase::IPluginGame const * const m_GamePlugin; + +private: + + // Mutex: + mutable std::mutex m_Mutex; + + // File tree representing the content of the disk. A null pointer indicates + // that the content needs to be loaded from the disk: + mutable std::shared_ptr m_FileTree = nullptr; MOShared::DirectoryEntry **m_DirectoryStructure; diff --git a/src/modlist.cpp b/src/modlist.cpp index ea4aa2e3..e8c14434 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -849,7 +849,7 @@ void ModList::modInfoChanged(ModInfo::Ptr info) } int row = ModInfo::getIndex(info->name()); - info->testValid(); + info->diskContentModified(); emit aboutToChangeData(); emit dataChanged(index(row, 0), index(row, columnCount())); emit postDataChanged(); @@ -1076,7 +1076,7 @@ bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &pa } if (!modInfo->isValid()) { - modInfo->testValid(); + modInfo->diskContentModified(); } return true; diff --git a/src/modlist.h b/src/modlist.h index d8980dec..4d456a9a 100644 --- a/src/modlist.h +++ b/src/modlist.h @@ -276,8 +276,6 @@ protected: private: - bool testValid(const QString &modDir); - QVariant getOverwriteData(int column, int role) const; QString getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const; diff --git a/src/organizercore.cpp b/src/organizercore.cpp index feab76a7..74163d8d 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -1629,7 +1629,7 @@ void OrganizerCore::syncOverwrite() qApp->activeWindow()); if (syncDialog.exec() == QDialog::Accepted) { syncDialog.apply(QDir::fromNativeSeparators(m_Settings.paths().mods())); - modInfo->testValid(); + modInfo->diskContentModified(); refreshDirectoryStructure(); } } -- cgit v1.3.1 From a284c64dc109797514b4664d37e6b6dc3d5c36c7 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sun, 24 May 2020 13:28:00 +0200 Subject: Update file tree in parallel when loading from disk. --- src/modinfo.cpp | 8 ++++++++ src/modinfo.h | 31 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) (limited to 'src') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 0dd113c5..5a90d74b 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -29,6 +29,7 @@ along with Mod Organizer. If not, see . #include "modinfodialog.h" #include "overwriteinfodialog.h" #include "versioninfo.h" +#include "envfs.h" #include #include @@ -45,6 +46,7 @@ using namespace MOBase; using namespace MOShared; +env::ThreadPool ModInfo::s_Threads(10); std::vector ModInfo::s_Collection; std::map ModInfo::s_ModsByName; std::map, std::vector> ModInfo::s_ModsByModID; @@ -287,6 +289,12 @@ void ModInfo::updateFromDisc(const QString &modDirectory, std::sort(s_Collection.begin(), s_Collection.end(), ModInfo::ByName); updateIndices(); + + for (auto& p : s_Collection) { + auto &t = s_Threads.request(); + t.ptr = p; + t.wakeup(); + } } diff --git a/src/modinfo.h b/src/modinfo.h index b3e75bc4..d3d13d9c 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -22,6 +22,7 @@ along with Mod Organizer. If not, see . #include "imodinterface.h" #include "versioninfo.h" +#include "envfs.h" class PluginContainer; class QDir; @@ -839,6 +840,36 @@ protected: private: + struct ModThread + { + ModInfo::Ptr ptr; + + std::condition_variable cv; + std::mutex mutex; + bool ready = false; + + void wakeup() + { + { + std::scoped_lock lock(mutex); + ready = true; + } + + cv.notify_one(); + } + + void run() + { + std::unique_lock lock(mutex); + cv.wait(lock, [&] { return ready; }); + + ptr->isValid(); + ready = false; + } + }; + + static env::ThreadPool s_Threads; + static QMutex s_Mutex; static std::map, std::vector > s_ModsByModID; static int s_NextID; -- cgit v1.3.1 From 73248d9e4036ad6b9494f678efbc97e919ebfbcf Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sun, 24 May 2020 13:36:16 +0200 Subject: Use the refresh_thread_count setting for ModInfo::updateFromDisc. --- src/modinfo.cpp | 14 +++++++++----- src/modinfo.h | 1 + src/organizercore.cpp | 6 ++++-- 3 files changed, 14 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 5a90d74b..81b45a49 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -46,7 +46,7 @@ using namespace MOBase; using namespace MOShared; -env::ThreadPool ModInfo::s_Threads(10); +env::ThreadPool ModInfo::s_Threads; std::vector ModInfo::s_Collection; std::map ModInfo::s_ModsByName; std::map, std::vector> ModInfo::s_ModsByModID; @@ -251,6 +251,7 @@ void ModInfo::updateFromDisc(const QString &modDirectory, DirectoryEntry **directoryStructure, PluginContainer *pluginContainer, bool displayForeign, + std::size_t refreshThreadCount, MOBase::IPluginGame const *game) { TimeThis tt("ModInfo::updateFromDisc()"); @@ -287,14 +288,17 @@ void ModInfo::updateFromDisc(const QString &modDirectory, createFromOverwrite(pluginContainer, game, directoryStructure); std::sort(s_Collection.begin(), s_Collection.end(), ModInfo::ByName); - - updateIndices(); - + + // This force loading a part of the FileTree: + s_Threads.setMax(refreshThreadCount); for (auto& p : s_Collection) { - auto &t = s_Threads.request(); + auto& t = s_Threads.request(); t.ptr = p; t.wakeup(); } + + updateIndices(); + } diff --git a/src/modinfo.h b/src/modinfo.h index d3d13d9c..081f3660 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -143,6 +143,7 @@ public: MOShared::DirectoryEntry **directoryStructure, PluginContainer *pluginContainer, bool displayForeign, + std::size_t refreshThreadCount, MOBase::IPluginGame const *game); static void clear() { s_Collection.clear(); s_ModsByName.clear(); s_ModsByModID.clear(); } diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 74163d8d..6e4b3fbf 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -208,7 +208,8 @@ void OrganizerCore::updateExecutablesList() void OrganizerCore::updateModInfoFromDisc() { ModInfo::updateFromDisc( m_Settings.paths().mods(), &m_DirectoryStructure, - m_PluginContainer, m_Settings.interface().displayForeign(), managedGame()); + m_PluginContainer, m_Settings.interface().displayForeign(), + m_Settings.refreshThreadCount(), managedGame()); } void OrganizerCore::setUserInterface(IUserInterface* ui) @@ -1097,7 +1098,8 @@ void OrganizerCore::refreshModList(bool saveChanges) ModInfo::updateFromDisc( m_Settings.paths().mods(), &m_DirectoryStructure, - m_PluginContainer, m_Settings.interface().displayForeign(), managedGame()); + m_PluginContainer, m_Settings.interface().displayForeign(), + m_Settings.refreshThreadCount(), managedGame()); m_CurrentProfile->refreshModStatus(); -- cgit v1.3.1 From ff4fbbdc5fa5ada01bd5bc3d8f8004279d7cc6b8 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sun, 24 May 2020 14:39:39 +0200 Subject: Switch from ThreadPool to a simpler thread map for containers. --- src/modinfo.cpp | 11 ++--------- src/modinfo.h | 31 ------------------------------ src/thread_utils.h | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 40 deletions(-) create mode 100644 src/thread_utils.h (limited to 'src') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index 81b45a49..eb0f0b4f 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -29,7 +29,7 @@ along with Mod Organizer. If not, see . #include "modinfodialog.h" #include "overwriteinfodialog.h" #include "versioninfo.h" -#include "envfs.h" +#include "thread_utils.h" #include #include @@ -46,7 +46,6 @@ using namespace MOBase; using namespace MOShared; -env::ThreadPool ModInfo::s_Threads; std::vector ModInfo::s_Collection; std::map ModInfo::s_ModsByName; std::map, std::vector> ModInfo::s_ModsByModID; @@ -289,13 +288,7 @@ void ModInfo::updateFromDisc(const QString &modDirectory, std::sort(s_Collection.begin(), s_Collection.end(), ModInfo::ByName); - // This force loading a part of the FileTree: - s_Threads.setMax(refreshThreadCount); - for (auto& p : s_Collection) { - auto& t = s_Threads.request(); - t.ptr = p; - t.wakeup(); - } + parallelMap(std::begin(s_Collection), std::end(s_Collection), &ModInfo::isValid, refreshThreadCount); updateIndices(); diff --git a/src/modinfo.h b/src/modinfo.h index 081f3660..37a2d24d 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -22,7 +22,6 @@ along with Mod Organizer. If not, see . #include "imodinterface.h" #include "versioninfo.h" -#include "envfs.h" class PluginContainer; class QDir; @@ -841,36 +840,6 @@ protected: private: - struct ModThread - { - ModInfo::Ptr ptr; - - std::condition_variable cv; - std::mutex mutex; - bool ready = false; - - void wakeup() - { - { - std::scoped_lock lock(mutex); - ready = true; - } - - cv.notify_one(); - } - - void run() - { - std::unique_lock lock(mutex); - cv.wait(lock, [&] { return ready; }); - - ptr->isValid(); - ready = false; - } - }; - - static env::ThreadPool s_Threads; - static QMutex s_Mutex; static std::map, std::vector > s_ModsByModID; static int s_NextID; diff --git a/src/thread_utils.h b/src/thread_utils.h new file mode 100644 index 00000000..fe096a36 --- /dev/null +++ b/src/thread_utils.h @@ -0,0 +1,55 @@ +#ifndef MO2_THREAD_UTILS_H +#define MO2_THREAD_UTILS_H + +#include +#include + +namespace MOShared { + +/** + * @brief Apply the given callable to each element between the two given iterators + * in a parallel way. + * + * The callable should be independent, or properly synchronized, and the source of + * the range should not change during this call. + * + * @param start Beginning of the range. + * @param end End of the range. + * @param callable Callable to apply to every element of the range. See std::invoke + * requirements. Must be copiable. + * @param nThreads Number of threads to use. + * + */ +template +void parallelMap(It begin, It end, Callable callable, std::size_t nThreads) { + std::vector threads(nThreads); + + std::mutex m; + for (auto &thread: threads) { + thread = std::thread([&m, &begin, end, callable]() { + while (true) { + decltype(begin) it; + { + std::scoped_lock lock(m); + if (begin == end) { + break; + } + it = begin++; + } + if (it != end) { + std::invoke(callable, *it); + } + } + }); + } + + + // Join everything: + for (auto& t : threads) { + t.join(); + } +} + +} + +#endif -- cgit v1.3.1 From 001e44fec45be3fe94d5075812547c0277f2e6d1 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sun, 24 May 2020 14:40:44 +0200 Subject: Minor cleaning. --- src/thread_utils.h | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/thread_utils.h b/src/thread_utils.h index fe096a36..2cb87a8d 100644 --- a/src/thread_utils.h +++ b/src/thread_utils.h @@ -21,10 +21,14 @@ namespace MOShared { * */ template -void parallelMap(It begin, It end, Callable callable, std::size_t nThreads) { +void parallelMap(It begin, It end, Callable callable, std::size_t nThreads) +{ + std::mutex m; std::vector threads(nThreads); - std::mutex m; + // Create the thread: + // - The mutex is only used to fetch/increment the iterator. + // - The callable is copied in each thread to avoid conflicts. for (auto &thread: threads) { thread = std::thread([&m, &begin, end, callable]() { while (true) { @@ -43,7 +47,6 @@ void parallelMap(It begin, It end, Callable callable, std::size_t nThreads) { }); } - // Join everything: for (auto& t : threads) { t.join(); -- cgit v1.3.1 From 4bcb92fc47181c62af62a55377b62dcfa02252ae Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sun, 24 May 2020 14:49:51 +0200 Subject: Use a more generic 'prefetch' method instead of isValid(). --- src/modinfo.cpp | 2 +- src/modinfo.h | 12 ++++++++++++ src/modinfowithconflictinfo.cpp | 5 +++++ src/modinfowithconflictinfo.h | 9 +++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/modinfo.cpp b/src/modinfo.cpp index eb0f0b4f..15fa71da 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -288,7 +288,7 @@ void ModInfo::updateFromDisc(const QString &modDirectory, std::sort(s_Collection.begin(), s_Collection.end(), ModInfo::ByName); - parallelMap(std::begin(s_Collection), std::end(s_Collection), &ModInfo::isValid, refreshThreadCount); + parallelMap(std::begin(s_Collection), std::end(s_Collection), &ModInfo::prefetch, refreshThreadCount); updateIndices(); diff --git a/src/modinfo.h b/src/modinfo.h index 37a2d24d..29e6124d 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -815,8 +815,20 @@ signals: protected: + /** + * + */ ModInfo(PluginContainer *pluginContainer); + /** + * @brief Prefetch content for this mod. + * + * This method can be used to prefetch content from the mod, e.g., for isValid() + * or getContents(). This method will only be called when first creating the mod + * using multiple threads for all the mods. + */ + virtual void prefetch() = 0; + static void updateIndices(); static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS); diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp index 45ecffb7..d2202c3d 100644 --- a/src/modinfowithconflictinfo.cpp +++ b/src/modinfowithconflictinfo.cpp @@ -309,6 +309,11 @@ std::shared_ptr ModInfoWithConflictInfo::contentFileTree() cons return m_FileTree; } +void ModInfoWithConflictInfo::prefetch() { + // Populating the tree to 1-depth: + contentFileTree()->size(); +} + bool ModInfoWithConflictInfo::doTestValid() const { auto mdc = m_GamePlugin->feature(); diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h index 5bc1d585..68675397 100644 --- a/src/modinfowithconflictinfo.h +++ b/src/modinfowithconflictinfo.h @@ -108,6 +108,15 @@ private: protected: + /** + * @brief Prefetch content for this mod. + * + * This method can be used to prefetch content from the mod, e.g., for isValid() + * or getContents(). This method will only be called when first creating the mod + * using multiple threads for all the mods. + */ + virtual void prefetch() override; + // Current game plugin running in MO2: MOBase::IPluginGame const * const m_GamePlugin; -- cgit v1.3.1 From a62bf7aa1289b21e0e9df75c860b2fd97fb915e4 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sun, 24 May 2020 15:21:08 +0200 Subject: Fix typo in getContents(). --- src/modinforegular.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 3c1d49ea..87f6ce77 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -699,7 +699,7 @@ std::vector ModInfoRegular::getContents() const if (e) { contents.push_back(CONTENT_SKSEFILES); for (auto f : *e) { - if (e->suffix().compare("dll") == 0) { + if (f->suffix().compare("dll") == 0) { contents.push_back(CONTENT_SKSE); break; } -- cgit v1.3.1 From a67a99acb7394861e253c28336eba28e7b68b77c Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sun, 24 May 2020 15:35:09 +0200 Subject: Force lower case comparison for extensions in getContents(). --- src/modinforegular.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 87f6ce77..85659bd2 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -660,7 +660,7 @@ std::vector ModInfoRegular::getContents() const for (auto e : *tree) { if (e->isFile()) { - auto suffix = e->suffix(); + auto suffix = e->suffix().toLower(); if (suffix == "esp" || suffix == "esm" || suffix == "esl") { contents.push_back(CONTENT_PLUGIN); } -- cgit v1.3.1 From 4d8b5aaae8adc08f5392bc631e7f88c501dea386 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sun, 24 May 2020 15:35:35 +0200 Subject: Remove old comment. --- src/modinforegular.cpp | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 85659bd2..87483a64 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -667,7 +667,6 @@ std::vector ModInfoRegular::getContents() const else if (suffix == "bsa" || suffix == "ba2") { contents.push_back(CONTENT_BSA); } - //use >1 for ini files since there is meta.ini in all mods already. else if (suffix == "ini" && e->compare("meta.ini") != 0) { contents.push_back(CONTENT_INI); } -- cgit v1.3.1 From b352aa53e0005cc262ab7441bd374570d6423279 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sun, 24 May 2020 23:31:34 +0200 Subject: Add thread_utils to CMakeList.txt. --- src/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6625e4b6..f1f54cbc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -186,6 +186,7 @@ add_filter(NAME src/utilities GROUPS shared/util usvfsconnector shared/windows_error + thread_utils json ) -- cgit v1.3.1 From 0aac4f3291d65ebc1b0bbee744fca347ecef7022 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sun, 24 May 2020 23:32:47 +0200 Subject: Add MemoizedLocked. --- src/thread_utils.h | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) (limited to 'src') diff --git a/src/thread_utils.h b/src/thread_utils.h index 2cb87a8d..6c044138 100644 --- a/src/thread_utils.h +++ b/src/thread_utils.h @@ -6,6 +6,49 @@ namespace MOShared { +/** + * Class that can be used to perform thread-safe memoization. + * + * Each instance hold a flag indicating if the current value is up-to-date + * or not. This flag can be reset using `invalidate()`. When the value is queried, + * the flag is checked, and if it is not up-to-date, the given callback is used + * to compute the value. + * + * The computation and update of the value is locked to avoid concurrent modifications. + * + * @tparam T Type of value ot memoized. + * @tparam Fn Type of the callback. + */ +template +struct MemoizedLocked { + + MemoizedLocked(Fn callback, T value = {}) : + m_Fn{ callback }, m_Value{ std::move(value) } { } + + template + T& value(Args&&... args) const { + if (m_NeedUpdating) { + std::scoped_lock lock(m_Mutex); + if (m_NeedUpdating) { + m_Value = std::invoke(m_Fn, std::forward(args)... ); + m_NeedUpdating.store(false); + } + } + return m_Value; + } + + void invalidate() const { + m_NeedUpdating.store(true); + } + +private: + mutable std::mutex m_Mutex; + mutable std::atomic m_NeedUpdating{ true }; + + Fn m_Fn; + mutable T m_Value; +}; + /** * @brief Apply the given callable to each element between the two given iterators * in a parallel way. -- cgit v1.3.1 From 238e233c18d22d01e966efaa86975f5f37bb7fa2 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sun, 24 May 2020 23:33:21 +0200 Subject: Use memoization for file tree, contents and validity of ModInfo. --- src/modinforegular.cpp | 3 +-- src/modinforegular.h | 4 ++-- src/modinfowithconflictinfo.cpp | 25 ++++++++++++++++--------- src/modinfowithconflictinfo.h | 31 ++++++++++++++++++++++++------- 4 files changed, 43 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 87483a64..d8e42383 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -653,7 +653,7 @@ std::vector ModInfoRegular::getFlags() const } -std::vector ModInfoRegular::getContents() const +std::vector ModInfoRegular::doGetContents() const { auto tree = contentFileTree(); std::vector contents; @@ -707,7 +707,6 @@ std::vector ModInfoRegular::getContents() const } return contents; - } diff --git a/src/modinforegular.h b/src/modinforegular.h index 08546c69..e63e7570 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -289,8 +289,6 @@ public: */ virtual std::vector getFlags() const override; - virtual std::vector getContents() const override; - /** * @return an indicator if and how this mod should be highlighted by the UI */ @@ -418,6 +416,8 @@ private slots: protected: + virtual std::vector doGetContents() const override; + ModInfoRegular(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &path, MOShared::DirectoryEntry **directoryStructure); private: diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp index d2202c3d..d2a0ca51 100644 --- a/src/modinfowithconflictinfo.cpp +++ b/src/modinfowithconflictinfo.cpp @@ -14,7 +14,9 @@ namespace fs = std::filesystem; ModInfoWithConflictInfo::ModInfoWithConflictInfo( PluginContainer *pluginContainer, const MOBase::IPluginGame* gamePlugin, DirectoryEntry **directoryStructure) - : ModInfo(pluginContainer), m_GamePlugin(gamePlugin), m_DirectoryStructure(directoryStructure), m_HasLooseOverwrite(false), m_HasHiddenFiles(false) {} + : ModInfo(pluginContainer), m_GamePlugin(gamePlugin), + m_FileTree(&ModInfoWithConflictInfo::updateFileTree), m_Valid(&ModInfoWithConflictInfo::doTestValid), m_Contents(&ModInfoWithConflictInfo::doGetContents), + m_DirectoryStructure(directoryStructure), m_HasLooseOverwrite(false), m_HasHiddenFiles(false) {} void ModInfoWithConflictInfo::clearCaches() { @@ -297,16 +299,17 @@ bool ModInfoWithConflictInfo::hasHiddenFiles() const } void ModInfoWithConflictInfo::diskContentModified() { - std::unique_lock lock(m_Mutex); - m_FileTree = nullptr; + m_FileTree.invalidate(); + m_Valid.invalidate(); + m_Contents.invalidate(); +} + +std::shared_ptr ModInfoWithConflictInfo::updateFileTree() const { + return QDirFileTree::makeTree(absolutePath()); } std::shared_ptr ModInfoWithConflictInfo::contentFileTree() const { - std::unique_lock lock(m_Mutex); - if (!m_FileTree) { - m_FileTree = QDirFileTree::makeTree(absolutePath()); - } - return m_FileTree; + return m_FileTree.value(this); } void ModInfoWithConflictInfo::prefetch() { @@ -326,5 +329,9 @@ bool ModInfoWithConflictInfo::doTestValid() const { } bool ModInfoWithConflictInfo::isValid() const { - return doTestValid(); + return m_Valid.value(this); +} + +std::vector ModInfoWithConflictInfo::getContents() const { + return m_Contents.value(this); } diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h index 68675397..3adbb998 100644 --- a/src/modinfowithconflictinfo.h +++ b/src/modinfowithconflictinfo.h @@ -3,6 +3,7 @@ #include +#include "thread_utils.h" #include "modinfo.h" #include @@ -20,6 +21,11 @@ public: **/ virtual bool isValid() const override; + /** + * @return a list of content types contained in a mod + */ + virtual std::vector getContents() const override; + /** * @brief clear all caches held for this mod */ @@ -49,12 +55,19 @@ public slots: protected: /** - * @brief check if the content of this mod is valid. + * @brief Check if the content of this mod is valid. * - * @return true if the content is valid, false otherwize. + * @return true if the content is valid, false otherwise. **/ virtual bool doTestValid() const; + /** + * @brief Compute the contents for this mod. + * + * @return the contents for this mod. + **/ + virtual std::vector doGetContents() const { return {}; } + /** * @brief Retrieve a file tree corresponding to the underlying disk content * of this mod. @@ -122,12 +135,16 @@ protected: private: - // Mutex: - mutable std::mutex m_Mutex; + /** + * @return a file tree for this mod. + */ + std::shared_ptr updateFileTree() const; - // File tree representing the content of the disk. A null pointer indicates - // that the content needs to be loaded from the disk: - mutable std::shared_ptr m_FileTree = nullptr; + MOShared::MemoizedLocked< + std::shared_ptr, + decltype(&ModInfoWithConflictInfo::updateFileTree)> m_FileTree; + MOShared::MemoizedLocked m_Valid; + MOShared::MemoizedLocked, decltype(&ModInfoWithConflictInfo::doGetContents)> m_Contents; MOShared::DirectoryEntry **m_DirectoryStructure; -- cgit v1.3.1 From e7afedaebee928ac64bc009f7019117b1cae0b69 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Mon, 25 May 2020 17:50:49 +0200 Subject: Remove useless calls to diskContentModified(). --- src/mainwindow.cpp | 4 ---- 1 file changed, 4 deletions(-) (limited to 'src') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index cc681dba..de4a6b39 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -3061,8 +3061,6 @@ void MainWindow::ignoreMissingData_clicked() for (QModelIndex idx : selection->selectedRows()) { int row_idx = idx.data(Qt::UserRole + 1).toInt(); ModInfo::Ptr info = ModInfo::getByIndex(row_idx); - //QDir(info->absolutePath()).mkdir("textures"); - info->diskContentModified(); info->markValidated(true); connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); @@ -3070,8 +3068,6 @@ void MainWindow::ignoreMissingData_clicked() } } else { ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - //QDir(info->absolutePath()).mkdir("textures"); - info->diskContentModified(); info->markValidated(true); connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); -- cgit v1.3.1 From 86f516953df9dc80c6726c30f47e15b660a89a4e Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Mon, 25 May 2020 17:56:45 +0200 Subject: Update MemoizedLock to allow more flexible callable. --- src/thread_utils.h | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/thread_utils.h b/src/thread_utils.h index 6c044138..f0067b6a 100644 --- a/src/thread_utils.h +++ b/src/thread_utils.h @@ -1,6 +1,7 @@ #ifndef MO2_THREAD_UTILS_H #define MO2_THREAD_UTILS_H +#include #include #include @@ -19,11 +20,12 @@ namespace MOShared { * @tparam T Type of value ot memoized. * @tparam Fn Type of the callback. */ -template +template > struct MemoizedLocked { - MemoizedLocked(Fn callback, T value = {}) : - m_Fn{ callback }, m_Value{ std::move(value) } { } + template + MemoizedLocked(Callable &&callable, T value = {}) : + m_Fn{ std::forward(callable) }, m_Value{ std::move(value) } { } template T& value(Args&&... args) const { @@ -31,14 +33,14 @@ struct MemoizedLocked { std::scoped_lock lock(m_Mutex); if (m_NeedUpdating) { m_Value = std::invoke(m_Fn, std::forward(args)... ); - m_NeedUpdating.store(false); + m_NeedUpdating = false; } } return m_Value; } - void invalidate() const { - m_NeedUpdating.store(true); + void invalidate() { + m_NeedUpdating = true; } private: -- cgit v1.3.1 From 03fc2b0dc25ae00322c6681d99f48857ecf77935 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Mon, 25 May 2020 17:57:07 +0200 Subject: Update ModInfo classes following MemoizedLocked changes. --- src/modinfoseparator.h | 2 +- src/modinfowithconflictinfo.cpp | 22 ++++++++++------------ src/modinfowithconflictinfo.h | 15 ++++----------- 3 files changed, 15 insertions(+), 24 deletions(-) (limited to 'src') diff --git a/src/modinfoseparator.h b/src/modinfoseparator.h index 4cfb9ea0..a7a6cb5f 100644 --- a/src/modinfoseparator.h +++ b/src/modinfoseparator.h @@ -49,7 +49,7 @@ public: virtual void addInstalledFile(int /*modId*/, int /*fileId*/) override { } protected: - virtual bool doTestValid() const override { return true; } + virtual bool doIsValid() const override { return true; } private: diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp index d2a0ca51..7181bef9 100644 --- a/src/modinfowithconflictinfo.cpp +++ b/src/modinfowithconflictinfo.cpp @@ -15,7 +15,9 @@ namespace fs = std::filesystem; ModInfoWithConflictInfo::ModInfoWithConflictInfo( PluginContainer *pluginContainer, const MOBase::IPluginGame* gamePlugin, DirectoryEntry **directoryStructure) : ModInfo(pluginContainer), m_GamePlugin(gamePlugin), - m_FileTree(&ModInfoWithConflictInfo::updateFileTree), m_Valid(&ModInfoWithConflictInfo::doTestValid), m_Contents(&ModInfoWithConflictInfo::doGetContents), + m_FileTree([this]() { return QDirFileTree::makeTree(absolutePath()); }), + m_Valid([this]() { return doIsValid(); }), + m_Contents([this]() { return doGetContents(); }), m_DirectoryStructure(directoryStructure), m_HasLooseOverwrite(false), m_HasHiddenFiles(false) {} void ModInfoWithConflictInfo::clearCaches() @@ -304,20 +306,12 @@ void ModInfoWithConflictInfo::diskContentModified() { m_Contents.invalidate(); } -std::shared_ptr ModInfoWithConflictInfo::updateFileTree() const { - return QDirFileTree::makeTree(absolutePath()); -} - -std::shared_ptr ModInfoWithConflictInfo::contentFileTree() const { - return m_FileTree.value(this); -} - void ModInfoWithConflictInfo::prefetch() { // Populating the tree to 1-depth: contentFileTree()->size(); } -bool ModInfoWithConflictInfo::doTestValid() const { +bool ModInfoWithConflictInfo::doIsValid() const { auto mdc = m_GamePlugin->feature(); if (mdc) { @@ -328,10 +322,14 @@ bool ModInfoWithConflictInfo::doTestValid() const { return true; } +std::shared_ptr ModInfoWithConflictInfo::contentFileTree() const { + return m_FileTree.value(); +} + bool ModInfoWithConflictInfo::isValid() const { - return m_Valid.value(this); + return m_Valid.value(); } std::vector ModInfoWithConflictInfo::getContents() const { - return m_Contents.value(this); + return m_Contents.value(); } diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h index 3adbb998..abc9f223 100644 --- a/src/modinfowithconflictinfo.h +++ b/src/modinfowithconflictinfo.h @@ -59,7 +59,7 @@ protected: * * @return true if the content is valid, false otherwise. **/ - virtual bool doTestValid() const; + virtual bool doIsValid() const; /** * @brief Compute the contents for this mod. @@ -135,16 +135,9 @@ protected: private: - /** - * @return a file tree for this mod. - */ - std::shared_ptr updateFileTree() const; - - MOShared::MemoizedLocked< - std::shared_ptr, - decltype(&ModInfoWithConflictInfo::updateFileTree)> m_FileTree; - MOShared::MemoizedLocked m_Valid; - MOShared::MemoizedLocked, decltype(&ModInfoWithConflictInfo::doGetContents)> m_Contents; + MOShared::MemoizedLocked> m_FileTree; + MOShared::MemoizedLocked m_Valid; + MOShared::MemoizedLocked> m_Contents; MOShared::DirectoryEntry **m_DirectoryStructure; -- cgit v1.3.1 From c5bbf7360c4fc60e691f5e42606e4510172698f2 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Mon, 25 May 2020 17:58:58 +0200 Subject: Fix suffix comparison for SKSE DLLs. --- src/modinforegular.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index d8e42383..8d5d702d 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -698,7 +698,7 @@ std::vector ModInfoRegular::doGetContents() const if (e) { contents.push_back(CONTENT_SKSEFILES); for (auto f : *e) { - if (f->suffix().compare("dll") == 0) { + if (f->suffix().compare("dll", FileNameComparator::CaseSensitivity) == 0) { contents.push_back(CONTENT_SKSE); break; } -- cgit v1.3.1 From 09e75d14e561ae58db0ca8fdb37921c40fa8d420 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Mon, 25 May 2020 17:59:07 +0200 Subject: Fix comment for prefetch(). --- src/modinfowithconflictinfo.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp index 7181bef9..b0edf581 100644 --- a/src/modinfowithconflictinfo.cpp +++ b/src/modinfowithconflictinfo.cpp @@ -307,7 +307,8 @@ void ModInfoWithConflictInfo::diskContentModified() { } void ModInfoWithConflictInfo::prefetch() { - // Populating the tree to 1-depth: + // Populating the tree to 1-depth (IFileTree is lazy, so size() forces the + // tree to populate the first level): contentFileTree()->size(); } -- cgit v1.3.1 From 9212a7d2e9611994dfc5fa2066b8ecbe68b198a8 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Mon, 25 May 2020 18:27:49 +0200 Subject: Use hasSuffix() where needed. --- src/modinforegular.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 8d5d702d..a1f1a04d 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -698,7 +698,7 @@ std::vector ModInfoRegular::doGetContents() const if (e) { contents.push_back(CONTENT_SKSEFILES); for (auto f : *e) { - if (f->suffix().compare("dll", FileNameComparator::CaseSensitivity) == 0) { + if (f->hasSuffix("dll")) { contents.push_back(CONTENT_SKSE); break; } -- cgit v1.3.1