diff options
| author | isanae <14251494+isanae@users.noreply.github.com> | 2020-05-25 13:48:45 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-05-25 13:48:45 -0400 |
| commit | a721347fd130fcab19f2709892093f087bfac19e (patch) | |
| tree | ab4d6670668f519dd89066197cda349811f8978b /src | |
| parent | 08ae9db2fd2a07acb2433275fc84c2ca47267a92 (diff) | |
| parent | 9212a7d2e9611994dfc5fa2066b8ecbe68b198a8 (diff) | |
Merge pull request #1079 from Holt59/moinfo-improvements
ModInfo improvements
Diffstat (limited to 'src')
| -rw-r--r-- | src/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 4 | ||||
| -rw-r--r-- | src/modinfo.cpp | 12 | ||||
| -rw-r--r-- | src/modinfo.h | 36 | ||||
| -rw-r--r-- | src/modinfooverwrite.cpp | 1 | ||||
| -rw-r--r-- | src/modinforegular.cpp | 92 | ||||
| -rw-r--r-- | src/modinforegular.h | 7 | ||||
| -rw-r--r-- | src/modinfoseparator.h | 2 | ||||
| -rw-r--r-- | src/modinfowithconflictinfo.cpp | 33 | ||||
| -rw-r--r-- | src/modinfowithconflictinfo.h | 63 | ||||
| -rw-r--r-- | src/modlist.cpp | 4 | ||||
| -rw-r--r-- | src/modlist.h | 2 | ||||
| -rw-r--r-- | src/organizercore.cpp | 8 | ||||
| -rw-r--r-- | src/thread_utils.h | 103 |
14 files changed, 274 insertions, 94 deletions
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 ) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index b5beb229..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->testValid(); 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->testValid(); 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..15fa71da 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -29,6 +29,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "modinfodialog.h" #include "overwriteinfodialog.h" #include "versioninfo.h" +#include "thread_utils.h" #include <iplugingame.h> #include <versioninfo.h> @@ -249,6 +250,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()"); @@ -285,8 +287,11 @@ void ModInfo::updateFromDisc(const QString &modDirectory, createFromOverwrite(pluginContainer, game, directoryStructure); std::sort(s_Collection.begin(), s_Collection.end(), ModInfo::ByName); + + parallelMap(std::begin(s_Collection), std::end(s_Collection), &ModInfo::prefetch, refreshThreadCount); updateIndices(); + } @@ -306,7 +311,7 @@ void ModInfo::updateIndices() ModInfo::ModInfo(PluginContainer *pluginContainer) - : m_Valid(false), m_PrimaryCategory(-1) + : m_PrimaryCategory(-1) { } @@ -528,11 +533,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..29e6124d 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -142,6 +142,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(); } @@ -702,7 +703,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 @@ -715,11 +716,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 */ virtual void markConverted(bool) {} @@ -801,6 +797,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: /** @@ -812,17 +815,22 @@ signals: protected: + /** + * + */ ModInfo(PluginContainer *pluginContainer); - static void updateIndices(); - static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS); - /** - * @brief check if the content of this mod is valid. + * @brief Prefetch content for this mod. * - * @return true if the content is valid, false otherwize. - **/ - virtual bool doTestValid() const = 0; + * 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); private: @@ -848,8 +856,6 @@ private: static std::map<std::pair<QString, int>, std::vector<unsigned int> > 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..a1f1a04d 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(); @@ -654,61 +653,60 @@ std::vector<ModInfo::EFlag> ModInfoRegular::getFlags() const } -std::vector<ModInfo::EContent> ModInfoRegular::getContents() const +std::vector<ModInfo::EContent> ModInfoRegular::doGetContents() 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<ModInfo::EContent> 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().toLower(); + if (suffix == "esp" || suffix == "esm" || suffix == "esl") { + contents.push_back(CONTENT_PLUGIN); + } + else if (suffix == "bsa" || suffix == "ba2") { + contents.push_back(CONTENT_BSA); + } + 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<IPluginGame *>() - ->feature<ScriptExtender>(); - - 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<ScriptExtender>(); + if (extender != nullptr) { + auto e = tree->findDirectory(extender->PluginPath()); + if (e) { + contents.push_back(CONTENT_SKSEFILES); + for (auto f : *e) { + if (f->hasSuffix("dll")) { + 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..e63e7570 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -289,8 +289,6 @@ public: */ virtual std::vector<EFlag> getFlags() const override; - virtual std::vector<EContent> 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<EContent> doGetContents() const override; + ModInfoRegular(PluginContainer *pluginContainer, const MOBase::IPluginGame *game, const QDir &path, MOShared::DirectoryEntry **directoryStructure); private: @@ -462,9 +462,6 @@ private: NexusBridge m_NexusBridge; - mutable std::vector<ModInfo::EContent> m_CachedContent; - mutable QTime m_LastContentCheck; - bool needsDescriptionUpdate() const; }; 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 0c67b4f7..b0edf581 100644 --- a/src/modinfowithconflictinfo.cpp +++ b/src/modinfowithconflictinfo.cpp @@ -14,7 +14,11 @@ 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([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() { @@ -296,14 +300,37 @@ bool ModInfoWithConflictInfo::hasHiddenFiles() const return m_HasHiddenFiles; } +void ModInfoWithConflictInfo::diskContentModified() { + m_FileTree.invalidate(); + m_Valid.invalidate(); + m_Contents.invalidate(); +} + +void ModInfoWithConflictInfo::prefetch() { + // Populating the tree to 1-depth (IFileTree is lazy, so size() forces the + // tree to populate the first level): + contentFileTree()->size(); +} -bool ModInfoWithConflictInfo::doTestValid() const { +bool ModInfoWithConflictInfo::doIsValid() const { auto mdc = m_GamePlugin->feature<ModDataChecker>(); if (mdc) { - auto qdirfiletree = QDirFileTree::makeTree(absolutePath()); + auto qdirfiletree = contentFileTree(); return mdc->dataLooksValid(qdirfiletree); } return true; } + +std::shared_ptr<const IFileTree> ModInfoWithConflictInfo::contentFileTree() const { + return m_FileTree.value(); +} + +bool ModInfoWithConflictInfo::isValid() const { + return m_Valid.value(); +} + +std::vector<ModInfo::EContent> ModInfoWithConflictInfo::getContents() const { + return m_Contents.value(); +} diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h index 94a61c62..abc9f223 100644 --- a/src/modinfowithconflictinfo.h +++ b/src/modinfowithconflictinfo.h @@ -1,6 +1,9 @@ #ifndef MODINFOWITHCONFLICTINFO_H #define MODINFOWITHCONFLICTINFO_H +#include <ifiletree.h> + +#include "thread_utils.h" #include "modinfo.h" #include <QTime> @@ -14,6 +17,16 @@ public: virtual std::vector<ModInfo::EFlag> 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; + + /** + * @return a list of content types contained in a mod + */ + virtual std::vector<EContent> getContents() const override; + + /** * @brief clear all caches held for this mod */ virtual void clearCaches() override; @@ -32,14 +45,39 @@ 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: /** - * @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 otherwise. + **/ + virtual bool doIsValid() const; + + /** + * @brief Compute the contents for this mod. * - * @return true if the content is valid, false otherwize. + * @return the contents for this mod. **/ - virtual bool doTestValid() const; + virtual std::vector<EContent> doGetContents() const { return {}; } + + /** + * @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<const MOBase::IFileTree> contentFileTree() const; ModInfoWithConflictInfo( PluginContainer* pluginContainer, @@ -81,10 +119,25 @@ private: bool hasHiddenFiles() const; -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* m_GamePlugin; + MOBase::IPluginGame const * const m_GamePlugin; + +private: + + MOShared::MemoizedLocked<std::shared_ptr<const MOBase::IFileTree>> m_FileTree; + MOShared::MemoizedLocked<bool> m_Valid; + MOShared::MemoizedLocked<std::vector<EContent>> m_Contents; 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..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(); @@ -1629,7 +1631,7 @@ void OrganizerCore::syncOverwrite() qApp->activeWindow()); if (syncDialog.exec() == QDialog::Accepted) { syncDialog.apply(QDir::fromNativeSeparators(m_Settings.paths().mods())); - modInfo->testValid(); + modInfo->diskContentModified(); refreshDirectoryStructure(); } } diff --git a/src/thread_utils.h b/src/thread_utils.h new file mode 100644 index 00000000..f0067b6a --- /dev/null +++ b/src/thread_utils.h @@ -0,0 +1,103 @@ +#ifndef MO2_THREAD_UTILS_H +#define MO2_THREAD_UTILS_H + +#include <functional> +#include <mutex> +#include <thread> + +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 <class T, class Fn = std::function<T()>> +struct MemoizedLocked { + + template <class Callable> + MemoizedLocked(Callable &&callable, T value = {}) : + m_Fn{ std::forward<Callable>(callable) }, m_Value{ std::move(value) } { } + + template <class... Args> + 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>(args)... ); + m_NeedUpdating = false; + } + } + return m_Value; + } + + void invalidate() { + m_NeedUpdating = true; + } + +private: + mutable std::mutex m_Mutex; + mutable std::atomic<bool> 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. + * + * 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 <class It, class Callable> +void parallelMap(It begin, It end, Callable callable, std::size_t nThreads) +{ + std::mutex m; + std::vector<std::thread> threads(nThreads); + + // 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) { + 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 |
