diff --git a/Source/Core/DolphinQt2/GameList/GameListModel.cpp b/Source/Core/DolphinQt2/GameList/GameListModel.cpp index e57ac80f77..7f26c87435 100644 --- a/Source/Core/DolphinQt2/GameList/GameListModel.cpp +++ b/Source/Core/DolphinQt2/GameList/GameListModel.cpp @@ -18,8 +18,7 @@ const QSize GAMECUBE_BANNER_SIZE(96, 32); GameListModel::GameListModel(QObject* parent) : QAbstractTableModel(parent) { connect(&m_tracker, &GameTracker::GameLoaded, this, &GameListModel::UpdateGame); - connect(&m_tracker, &GameTracker::GameRemoved, this, - [this](const QString& path) { RemoveGame(path.toStdString()); }); + connect(&m_tracker, &GameTracker::GameRemoved, this, &GameListModel::RemoveGame); connect(&Settings::Instance(), &Settings::PathAdded, &m_tracker, &GameTracker::AddDirectory); connect(&Settings::Instance(), &Settings::PathRemoved, &m_tracker, &GameTracker::RemoveDirectory); connect(&Settings::Instance(), &Settings::PathReloadRequested, &m_tracker, @@ -28,6 +27,8 @@ GameListModel::GameListModel(QObject* parent) : QAbstractTableModel(parent) for (const QString& dir : Settings::Instance().GetPaths()) m_tracker.AddDirectory(dir); + m_tracker.Start(); + connect(&Settings::Instance(), &Settings::ThemeChanged, [this] { // Tell the view to repaint. The signal 'dataChanged' also seems like it would work here, but // unfortunately it won't cause a repaint until the view is focused. diff --git a/Source/Core/DolphinQt2/GameList/GameTracker.cpp b/Source/Core/DolphinQt2/GameList/GameTracker.cpp index 605275bf82..958236d227 100644 --- a/Source/Core/DolphinQt2/GameList/GameTracker.cpp +++ b/Source/Core/DolphinQt2/GameList/GameTracker.cpp @@ -30,8 +30,10 @@ GameTracker::GameTracker(QObject* parent) : QFileSystemWatcher(parent) switch (command.type) { case CommandType::LoadCache: - m_cache.Load(); + LoadCache(); break; + case CommandType::Start: + StartInternal(); case CommandType::AddDirectory: AddDirectoryInternal(command.path); break; @@ -52,6 +54,52 @@ GameTracker::GameTracker(QObject* parent) : QFileSystemWatcher(parent) // TODO: When language changes, reload m_title_database and call m_cache.UpdateAdditionalMetadata } +void GameTracker::LoadCache() +{ + std::lock_guard lk(m_mutex); + m_cache.Load(); +} + +void GameTracker::Start() +{ + if (m_initial_games_emitted) + return; + + m_initial_games_emitted = true; + + std::lock_guard lk(m_mutex); + + m_load_thread.EmplaceItem(Command{CommandType::Start, {}}); + + m_cache.ForEach( + [this](const std::shared_ptr& game) { emit GameLoaded(game); }); +} + +void GameTracker::StartInternal() +{ + if (m_started) + return; + + m_started = true; + + std::vector paths; + paths.reserve(m_tracked_files.size()); + for (const QString& path : m_tracked_files.keys()) + paths.push_back(path.toStdString()); + + auto emit_game_loaded = [this](const std::shared_ptr& game) { + emit GameLoaded(std::move(game)); + }; + auto emit_game_removed = [this](const std::string& path) { emit GameRemoved(path); }; + + std::lock_guard lk(m_mutex); + + bool cache_updated = m_cache.Update(paths, emit_game_loaded, emit_game_removed); + cache_updated |= m_cache.UpdateAdditionalMetadata(m_title_database, emit_game_loaded); + if (cache_updated) + m_cache.Save(); +} + void GameTracker::AddDirectory(const QString& dir) { m_load_thread.EmplaceItem(Command{CommandType::AddDirectory, dir}); @@ -108,7 +156,8 @@ void GameTracker::RemoveDirectoryInternal(const QString& dir) { removePath(path); m_tracked_files.remove(path); - emit GameRemoved(path); + if (m_started) + emit GameRemoved(path.toStdString()); } } } @@ -143,7 +192,8 @@ void GameTracker::UpdateDirectoryInternal(const QString& dir) if (tracked_file.empty()) { m_tracked_files.remove(missing); - GameRemoved(missing); + if (m_started) + GameRemoved(missing.toStdString()); } } } @@ -152,14 +202,16 @@ void GameTracker::UpdateFileInternal(const QString& file) { if (QFileInfo(file).exists()) { - GameRemoved(file); + if (m_started) + GameRemoved(file.toStdString()); addPath(file); LoadGame(file); } else if (removePath(file)) { m_tracked_files.remove(file); - emit GameRemoved(file); + if (m_started) + emit GameRemoved(file.toStdString()); } } @@ -187,6 +239,9 @@ QSet GameTracker::FindMissingFiles(const QString& dir) void GameTracker::LoadGame(const QString& path) { + if (!m_started) + return; + const std::string converted_path = path.toStdString(); if (!DiscIO::ShouldHideFromGameList(converted_path)) { diff --git a/Source/Core/DolphinQt2/GameList/GameTracker.h b/Source/Core/DolphinQt2/GameList/GameTracker.h index b6bc8835a1..cbe6c63cba 100644 --- a/Source/Core/DolphinQt2/GameList/GameTracker.h +++ b/Source/Core/DolphinQt2/GameList/GameTracker.h @@ -5,6 +5,8 @@ #pragma once #include +#include +#include #include #include @@ -26,15 +28,23 @@ class GameTracker final : public QFileSystemWatcher public: explicit GameTracker(QObject* parent = nullptr); + // A GameTracker won't emit any signals until this function has been called. + // Before you call this function, make sure to call AddDirectory for all + // directories you want to track, otherwise games will briefly disappear + // until you call AddDirectory and the GameTracker finishes scanning the file system. + void Start(); + void AddDirectory(const QString& dir); void RemoveDirectory(const QString& dir); void ReloadDirectory(const QString& dir); signals: void GameLoaded(std::shared_ptr game); - void GameRemoved(const QString& path); + void GameRemoved(const std::string& path); private: + void LoadCache(); + void StartInternal(); void UpdateDirectory(const QString& dir); void UpdateFile(const QString& path); void AddDirectoryInternal(const QString& dir); @@ -47,6 +57,7 @@ private: enum class CommandType { LoadCache, + Start, AddDirectory, RemoveDirectory, UpdateDirectory, @@ -64,6 +75,10 @@ private: Common::WorkQueueThread m_load_thread; UICommon::GameFileCache m_cache; Core::TitleDatabase m_title_database; + std::mutex m_mutex; + bool m_started = false; + bool m_initial_games_emitted = false; }; Q_DECLARE_METATYPE(std::shared_ptr) +Q_DECLARE_METATYPE(std::string) diff --git a/Source/Core/UICommon/GameFileCache.cpp b/Source/Core/UICommon/GameFileCache.cpp index 9dc25740f9..7f9b7015b2 100644 --- a/Source/Core/UICommon/GameFileCache.cpp +++ b/Source/Core/UICommon/GameFileCache.cpp @@ -73,7 +73,10 @@ std::shared_ptr GameFileCache::AddOrGet(const std::string& path, return result; } -bool GameFileCache::Update(const std::vector& all_game_paths) +bool GameFileCache::Update( + const std::vector& all_game_paths, + std::function&)> game_added_to_cache, + std::function game_removed_from_cache) { // Copy game paths into a set, except ones that match DiscIO::ShouldHideFromGameList. // TODO: Prevent DoFileSearch from looking inside /files/ directories of DirectoryBlobs at all? @@ -89,7 +92,7 @@ bool GameFileCache::Update(const std::vector& all_game_paths) bool cache_changed = false; // Delete paths that aren't in game_paths from m_cached_files, - // while simultaneously deleting paths that aren't in m_cached_files from game_paths. + // while simultaneously deleting paths that are in m_cached_files from game_paths. // For the sake of speed, we don't care about maintaining the order of m_cached_files. { auto it = m_cached_files.begin(); @@ -102,21 +105,27 @@ bool GameFileCache::Update(const std::vector& all_game_paths) } else { + if (game_removed_from_cache) + game_removed_from_cache((*it)->GetFilePath()); + cache_changed = true; --end; *it = std::move(*end); + m_cached_files.pop_back(); } } - m_cached_files.erase(it, m_cached_files.end()); } // Now that the previous loop has run, game_paths only contains paths that // aren't in m_cached_files, so we simply add all of them to m_cached_files. - for (const auto& path : game_paths) + for (const std::string& path : game_paths) { auto file = std::make_shared(path); if (file->IsValid()) { + if (game_added_to_cache) + game_added_to_cache(file); + cache_changed = true; m_cached_files.push_back(std::move(file)); } @@ -125,12 +134,19 @@ bool GameFileCache::Update(const std::vector& all_game_paths) return cache_changed; } -bool GameFileCache::UpdateAdditionalMetadata(const Core::TitleDatabase& title_database) +bool GameFileCache::UpdateAdditionalMetadata( + const Core::TitleDatabase& title_database, + std::function&)> game_updated) { bool cache_changed = false; - for (auto& file : m_cached_files) - cache_changed |= UpdateAdditionalMetadata(&file, title_database); + for (std::shared_ptr& file : m_cached_files) + { + const bool updated = UpdateAdditionalMetadata(&file, title_database); + cache_changed |= updated; + if (game_updated && updated) + game_updated(file); + } return cache_changed; } diff --git a/Source/Core/UICommon/GameFileCache.h b/Source/Core/UICommon/GameFileCache.h index 8a13dffc41..3a23f5211b 100644 --- a/Source/Core/UICommon/GameFileCache.h +++ b/Source/Core/UICommon/GameFileCache.h @@ -40,8 +40,12 @@ public: const Core::TitleDatabase& title_database); // These functions return true if the call modified the cache. - bool Update(const std::vector& all_game_paths); - bool UpdateAdditionalMetadata(const Core::TitleDatabase& title_database); + bool Update(const std::vector& all_game_paths, + std::function&)> game_added_to_cache = {}, + std::function game_removed_from_cache = {}); + bool UpdateAdditionalMetadata( + const Core::TitleDatabase& title_database, + std::function&)> game_updated = {}); bool Load(); bool Save();