@@ -4,6 +4,7 @@

#include "DolphinQt/GameList/GameTracker.h"

#include <QApplication>
#include <QDir>
#include <QDirIterator>
#include <QFile>
@@ -15,7 +16,6 @@
#include "DiscIO/DirectoryBlob.h"

#include "DolphinQt/QtUtils/QueueOnObject.h"
#include "DolphinQt/QtUtils/RunOnObject.h"

#include "DolphinQt/Settings.h"

@@ -35,6 +35,10 @@ GameTracker::GameTracker(QObject* parent) : QFileSystemWatcher(parent)
qRegisterMetaType<std::shared_ptr<const UICommon::GameFile>>();
qRegisterMetaType<std::string>();

connect(qApp, &QApplication::aboutToQuit, this, [this] {
m_processing_halted = true;
m_load_thread.Cancel();
});
connect(this, &QFileSystemWatcher::directoryChanged, this, &GameTracker::UpdateDirectory);
connect(this, &QFileSystemWatcher::fileChanged, this, &GameTracker::UpdateFile);
connect(&Settings::Instance(), &Settings::AutoRefreshToggled, this, [] {
@@ -74,27 +78,24 @@ GameTracker::GameTracker(QObject* parent) : QFileSystemWatcher(parent)
break;
case CommandType::UpdateMetadata:
m_cache.UpdateAdditionalMetadata(
[this](const std::shared_ptr<const UICommon::GameFile>& game) {
emit GameUpdated(game);
});
[this](const std::shared_ptr<const UICommon::GameFile>& game) { emit GameUpdated(game); },
m_processing_halted);
QueueOnObject(this, [] { Settings::Instance().NotifyMetadataRefreshComplete(); });
break;
case CommandType::ResumeProcessing:
m_processing_halted = false;
break;
case CommandType::PurgeCache:
m_cache.Clear(UICommon::GameFileCache::DeleteOnDisk::Yes);
break;
case CommandType::BeginRefresh:
if (m_busy_count++ == 0)
{
for (auto& file : m_tracked_files.keys())
emit GameRemoved(file.toStdString());
m_tracked_files.clear();
}
QueueOnObject(this, [] { Settings::Instance().NotifyRefreshGameListStarted(); });
for (auto& file : m_tracked_files.keys())
emit GameRemoved(file.toStdString());
m_tracked_files.clear();
break;
case CommandType::EndRefresh:
if (--m_busy_count == 0)
{
QueueOnObject(this, [] { Settings::Instance().NotifyRefreshGameListComplete(); });
}
QueueOnObject(this, [] { Settings::Instance().NotifyRefreshGameListComplete(); });
break;
}
});
@@ -134,6 +135,8 @@ void GameTracker::StartInternal()

m_started = true;

QueueOnObject(this, [] { Settings::Instance().NotifyRefreshGameListStarted(); });

std::vector<std::string> paths;
paths.reserve(m_tracked_files.size());
for (const QString& path : m_tracked_files.keys())
@@ -149,18 +152,20 @@ void GameTracker::StartInternal()

m_initial_games_emitted_event.Wait();

bool cache_updated = m_cache.Update(paths, emit_game_loaded, emit_game_removed);
cache_updated |= m_cache.UpdateAdditionalMetadata(emit_game_updated);
bool cache_updated =
m_cache.Update(paths, emit_game_loaded, emit_game_removed, m_processing_halted);
cache_updated |= m_cache.UpdateAdditionalMetadata(emit_game_updated, m_processing_halted);
if (cache_updated)
m_cache.Save();

QueueOnObject(this, [] { Settings::Instance().NotifyMetadataRefreshComplete(); });
QueueOnObject(this, [] { Settings::Instance().NotifyRefreshGameListComplete(); });
}

bool GameTracker::AddPath(const QString& dir)
{
if (Settings::Instance().IsAutoRefreshEnabled())
RunOnObject(this, [this, dir] { return addPath(dir); });
QueueOnObject(this, [this, dir] { return addPath(dir); });

m_tracked_paths.push_back(dir);

@@ -170,7 +175,7 @@ bool GameTracker::AddPath(const QString& dir)
bool GameTracker::RemovePath(const QString& dir)
{
if (Settings::Instance().IsAutoRefreshEnabled())
RunOnObject(this, [this, dir] { return removePath(dir); });
QueueOnObject(this, [this, dir] { return removePath(dir); });

const auto index = m_tracked_paths.indexOf(dir);

@@ -194,6 +199,16 @@ void GameTracker::RemoveDirectory(const QString& dir)

void GameTracker::RefreshAll()
{
m_processing_halted = true;
m_load_thread.Clear();
m_load_thread.EmplaceItem(Command{CommandType::ResumeProcessing, {}});

if (m_needs_purge)
{
m_load_thread.EmplaceItem(Command{CommandType::PurgeCache, {}});
m_needs_purge = false;
}

m_load_thread.EmplaceItem(Command{CommandType::BeginRefresh});

for (const QString& dir : Settings::Instance().GetPaths())
@@ -255,7 +270,7 @@ void GameTracker::RemoveDirectoryInternal(const QString& dir)
void GameTracker::UpdateDirectoryInternal(const QString& dir)
{
auto it = GetIterator(dir);
while (it->hasNext())
while (it->hasNext() && !m_processing_halted)
{
QString path = QFileInfo(it->next()).canonicalFilePath();

@@ -275,6 +290,9 @@ void GameTracker::UpdateDirectoryInternal(const QString& dir)

for (const auto& missing : FindMissingFiles(dir))
{
if (m_processing_halted)
break;

auto& tracked_file = m_tracked_files[missing];

tracked_file.remove(dir);
@@ -345,6 +363,6 @@ void GameTracker::LoadGame(const QString& path)

void GameTracker::PurgeCache()
{
m_load_thread.EmplaceItem(Command{CommandType::PurgeCache, {}});
RefreshAll();
m_needs_purge = true;
Settings::Instance().RefreshGameList();
}
@@ -4,6 +4,7 @@

#pragma once

#include <atomic>
#include <memory>
#include <string>

@@ -72,6 +73,7 @@ class GameTracker final : public QFileSystemWatcher
UpdateDirectory,
UpdateFile,
UpdateMetadata,
ResumeProcessing,
PurgeCache,
BeginRefresh,
EndRefresh,
@@ -92,8 +94,8 @@ class GameTracker final : public QFileSystemWatcher
Common::Event m_initial_games_emitted_event;
bool m_initial_games_emitted = false;
bool m_started = false;
// Count of currently running refresh jobs
u32 m_busy_count = 0;
bool m_needs_purge = false;
std::atomic_bool m_processing_halted = false;
};

Q_DECLARE_METATYPE(std::shared_ptr<const UICommon::GameFile>)
@@ -399,7 +399,7 @@ void MainWindow::CreateComponents()
m_watch_widget = new WatchWidget(this);
m_breakpoint_widget = new BreakpointWidget(this);
m_code_widget = new CodeWidget(this);
m_cheats_manager = new CheatsManager(this);
m_cheats_manager = new CheatsManager(m_game_list->GetGameListModel(), this);

const auto request_watch = [this](QString name, u32 addr) {
m_watch_widget->AddWatch(name, addr);
@@ -1276,8 +1276,9 @@ void MainWindow::BootWiiSystemMenu()

void MainWindow::NetPlayInit()
{
m_netplay_setup_dialog = new NetPlaySetupDialog(this);
m_netplay_dialog = new NetPlayDialog;
const auto& game_list_model = m_game_list->GetGameListModel();
m_netplay_setup_dialog = new NetPlaySetupDialog(game_list_model, this);
m_netplay_dialog = new NetPlayDialog(game_list_model);
#ifdef USE_DISCORD_PRESENCE
m_netplay_discord = new DiscordHandler(this);
#endif
@@ -506,7 +506,13 @@ void MenuBar::AddViewMenu()
AddShowRegionsMenu(view_menu);

view_menu->addSeparator();
view_menu->addAction(tr("Purge Game List Cache"), this, &MenuBar::PurgeGameListCache);
QAction* const purge_action =
view_menu->addAction(tr("Purge Game List Cache"), this, &MenuBar::PurgeGameListCache);
purge_action->setEnabled(false);
connect(&Settings::Instance(), &Settings::GameListRefreshRequested, purge_action,
[purge_action] { purge_action->setEnabled(false); });
connect(&Settings::Instance(), &Settings::GameListRefreshStarted, purge_action,
[purge_action] { purge_action->setEnabled(true); });
view_menu->addSeparator();
view_menu->addAction(tr("Search"), this, &MenuBar::ShowSearch, QKeySequence::Find);
}
@@ -10,11 +10,10 @@
#include <QListWidget>
#include <QVBoxLayout>

#include "DolphinQt/GameList/GameListModel.h"
#include "DolphinQt/Settings.h"
#include "UICommon/GameFile.h"

GameListDialog::GameListDialog(QWidget* parent) : QDialog(parent)
GameListDialog::GameListDialog(const GameListModel& game_list_model, QWidget* parent)
: QDialog(parent), m_game_list_model(game_list_model)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowTitle(tr("Select a game"));
@@ -47,16 +46,14 @@ void GameListDialog::ConnectWidgets()

void GameListDialog::PopulateGameList()
{
auto* game_list_model = Settings::Instance().GetGameListModel();

m_game_list->clear();

for (int i = 0; i < game_list_model->rowCount(QModelIndex()); i++)
for (int i = 0; i < m_game_list_model.rowCount(QModelIndex()); i++)
{
std::shared_ptr<const UICommon::GameFile> game = game_list_model->GetGameFile(i);
std::shared_ptr<const UICommon::GameFile> game = m_game_list_model.GetGameFile(i);

auto* item =
new QListWidgetItem(QString::fromStdString(game_list_model->GetNetPlayName(*game)));
new QListWidgetItem(QString::fromStdString(m_game_list_model.GetNetPlayName(*game)));
item->setData(Qt::UserRole, QVariant::fromValue(std::move(game)));
m_game_list->addItem(item);
}
@@ -6,7 +6,8 @@

#include <QDialog>

class GameListModel;
#include "DolphinQt/GameList/GameListModel.h"

class QVBoxLayout;
class QListWidget;
class QDialogButtonBox;
@@ -20,7 +21,7 @@ class GameListDialog : public QDialog
{
Q_OBJECT
public:
explicit GameListDialog(QWidget* parent);
explicit GameListDialog(const GameListModel& game_list_model, QWidget* parent);

int exec() override;
const UICommon::GameFile& GetSelectedGame() const;
@@ -30,6 +31,7 @@ class GameListDialog : public QDialog
void ConnectWidgets();
void PopulateGameList();

const GameListModel& m_game_list_model;
QVBoxLayout* m_main_layout;
QListWidget* m_game_list;
QDialogButtonBox* m_button_box;
@@ -40,7 +40,6 @@
#include "Core/NetPlayServer.h"
#include "Core/SyncIdentifier.h"

#include "DolphinQt/GameList/GameListModel.h"
#include "DolphinQt/NetPlay/ChunkedProgressDialog.h"
#include "DolphinQt/NetPlay/GameListDialog.h"
#include "DolphinQt/NetPlay/MD5Dialog.h"
@@ -60,8 +59,8 @@
#include "VideoCommon/RenderBase.h"
#include "VideoCommon/VideoConfig.h"

NetPlayDialog::NetPlayDialog(QWidget* parent)
: QDialog(parent), m_game_list_model(Settings::Instance().GetGameListModel())
NetPlayDialog::NetPlayDialog(const GameListModel& game_list_model, QWidget* parent)
: QDialog(parent), m_game_list_model(game_list_model)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);

@@ -158,7 +157,7 @@ void NetPlayDialog::CreateMainLayout()
Settings::Instance().GetNetPlayServer()->ComputeMD5(m_current_game_identifier);
});
m_md5_menu->addAction(tr("Other game..."), this, [this] {
GameListDialog gld(this);
GameListDialog gld(m_game_list_model, this);

if (gld.exec() != QDialog::Accepted)
return;
@@ -322,13 +321,13 @@ void NetPlayDialog::ConnectWidgets()
connect(m_quit_button, &QPushButton::clicked, this, &NetPlayDialog::reject);

connect(m_game_button, &QPushButton::clicked, [this] {
GameListDialog gld(this);
GameListDialog gld(m_game_list_model, this);
if (gld.exec() == QDialog::Accepted)
{
Settings& settings = Settings::Instance();

const UICommon::GameFile& game = gld.GetSelectedGame();
const std::string netplay_name = settings.GetGameListModel()->GetNetPlayName(game);
const std::string netplay_name = m_game_list_model.GetNetPlayName(game);

settings.GetNetPlayServer()->ChangeGame(game.GetSyncIdentifier(), netplay_name);
Settings::GetQSettings().setValue(QStringLiteral("netplay/hostgame"),
@@ -1048,9 +1047,9 @@ NetPlayDialog::FindGameFile(const NetPlay::SyncIdentifier& sync_identifier,

std::optional<std::shared_ptr<const UICommon::GameFile>> game_file =
RunOnObject(this, [this, &sync_identifier, found] {
for (int i = 0; i < m_game_list_model->rowCount(QModelIndex()); i++)
for (int i = 0; i < m_game_list_model.rowCount(QModelIndex()); i++)
{
auto game_file = m_game_list_model->GetGameFile(i);
auto game_file = m_game_list_model.GetGameFile(i);
*found = std::min(*found, game_file->CompareSyncIdentifier(sync_identifier));
if (*found == NetPlay::SyncIdentifierComparison::SameGame)
return game_file;
@@ -9,11 +9,11 @@

#include "Common/Lazy.h"
#include "Core/NetPlayClient.h"
#include "DolphinQt/GameList/GameListModel.h"
#include "VideoCommon/OnScreenDisplay.h"

class ChunkedProgressDialog;
class MD5Dialog;
class GameListModel;
class PadMappingDialog;
class QCheckBox;
class QComboBox;
@@ -31,7 +31,7 @@ class NetPlayDialog : public QDialog, public NetPlay::NetPlayUI
{
Q_OBJECT
public:
explicit NetPlayDialog(QWidget* parent = nullptr);
explicit NetPlayDialog(const GameListModel& game_list_model, QWidget* parent = nullptr);
~NetPlayDialog();

void show(std::string nickname, bool use_traversal);
@@ -151,7 +151,7 @@ class NetPlayDialog : public QDialog, public NetPlay::NetPlayUI
std::string m_current_game_name;
Common::Lazy<std::string> m_external_ip_address;
std::string m_nickname;
GameListModel* m_game_list_model = nullptr;
const GameListModel& m_game_list_model;
bool m_use_traversal = false;
bool m_is_copy_button_retry = false;
bool m_got_stop_request = true;
@@ -21,16 +21,15 @@
#include "Core/Config/NetplaySettings.h"
#include "Core/NetPlayProto.h"

#include "DolphinQt/GameList/GameListModel.h"
#include "DolphinQt/QtUtils/ModalMessageBox.h"
#include "DolphinQt/QtUtils/UTF8CodePointCountValidator.h"
#include "DolphinQt/Settings.h"

#include "UICommon/GameFile.h"
#include "UICommon/NetPlayIndex.h"

NetPlaySetupDialog::NetPlaySetupDialog(QWidget* parent)
: QDialog(parent), m_game_list_model(Settings::Instance().GetGameListModel())
NetPlaySetupDialog::NetPlaySetupDialog(const GameListModel& game_list_model, QWidget* parent)
: QDialog(parent), m_game_list_model(game_list_model)
{
setWindowTitle(tr("NetPlay Setup"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
@@ -359,12 +358,12 @@ void NetPlaySetupDialog::PopulateGameList()
QSignalBlocker blocker(m_host_games);

m_host_games->clear();
for (int i = 0; i < m_game_list_model->rowCount(QModelIndex()); i++)
for (int i = 0; i < m_game_list_model.rowCount(QModelIndex()); i++)
{
std::shared_ptr<const UICommon::GameFile> game = m_game_list_model->GetGameFile(i);
std::shared_ptr<const UICommon::GameFile> game = m_game_list_model.GetGameFile(i);

auto* item =
new QListWidgetItem(QString::fromStdString(m_game_list_model->GetNetPlayName(*game)));
new QListWidgetItem(QString::fromStdString(m_game_list_model.GetNetPlayName(*game)));
item->setData(Qt::UserRole, QVariant::fromValue(std::move(game)));
m_host_games->addItem(item);
}
@@ -6,7 +6,8 @@

#include <QDialog>

class GameListModel;
#include "DolphinQt/GameList/GameListModel.h"

class QCheckBox;
class QComboBox;
class QDialogButtonBox;
@@ -27,7 +28,7 @@ class NetPlaySetupDialog : public QDialog
{
Q_OBJECT
public:
explicit NetPlaySetupDialog(QWidget* parent);
explicit NetPlaySetupDialog(const GameListModel& game_list_model, QWidget* parent);

void accept() override;
void show();
@@ -79,5 +80,5 @@ class NetPlaySetupDialog : public QDialog
QCheckBox* m_host_upnp;
#endif

GameListModel* m_game_list_model;
const GameListModel& m_game_list_model;
};
@@ -24,7 +24,6 @@
#include "Core/NetPlayClient.h"
#include "Core/NetPlayServer.h"

#include "DolphinQt/GameList/GameListModel.h"
#include "DolphinQt/QtUtils/QueueOnObject.h"

#include "InputCommon/ControllerInterface/ControllerInterface.h"
@@ -148,6 +147,11 @@ void Settings::RefreshGameList()
emit GameListRefreshRequested();
}

void Settings::NotifyRefreshGameListStarted()
{
emit GameListRefreshStarted();
}

void Settings::NotifyRefreshGameListComplete()
{
emit GameListRefreshCompleted();
@@ -296,12 +300,6 @@ void Settings::SetLogConfigVisible(bool visible)
}
}

GameListModel* Settings::GetGameListModel() const
{
static GameListModel* model = new GameListModel;
return model;
}

std::shared_ptr<NetPlay::NetPlayClient> Settings::GetNetPlayClient()
{
return m_client;
@@ -26,7 +26,6 @@ class NetPlayClient;
class NetPlayServer;
} // namespace NetPlay

class GameListModel;
class InputConfig;

// UI settings to be stored in the config directory.
@@ -73,6 +72,7 @@ class Settings final : public QObject
QString GetDefaultGame() const;
void SetDefaultGame(QString path);
void RefreshGameList();
void NotifyRefreshGameListStarted();
void NotifyRefreshGameListComplete();
void RefreshMetadata();
void NotifyMetadataRefreshComplete();
@@ -143,8 +143,6 @@ class Settings final : public QObject
bool IsAnalyticsEnabled() const;
void SetAnalyticsEnabled(bool enabled);

// Other
GameListModel* GetGameListModel() const;
signals:
void ConfigChanged();
void EmulationStateChanged(Core::State new_state);
@@ -153,6 +151,7 @@ class Settings final : public QObject
void PathRemoved(const QString&);
void DefaultGameChanged(const QString&);
void GameListRefreshRequested();
void GameListRefreshStarted();
void GameListRefreshCompleted();
void TitleDBReloadRequested();
void MetadataRefreshRequested();
@@ -23,7 +23,6 @@
#include "Core/Config/UISettings.h"
#include "Core/ConfigManager.h"

#include "DolphinQt/GameList/GameListModel.h"
#include "DolphinQt/QtUtils/ModalMessageBox.h"
#include "DolphinQt/Settings.h"

@@ -46,7 +46,9 @@ ToolBar::ToolBar(QWidget* parent) : QToolBar(parent)
connect(&Settings::Instance(), &Settings::WidgetLockChanged, this,
[this](bool locked) { setMovable(!locked); });

connect(&Settings::Instance(), &Settings::GameListRefreshCompleted, this,
connect(&Settings::Instance(), &Settings::GameListRefreshRequested, this,
[this] { m_refresh_action->setEnabled(false); });
connect(&Settings::Instance(), &Settings::GameListRefreshStarted, this,
[this] { m_refresh_action->setEnabled(true); });

OnEmulationStateChanged(Core::GetState());
@@ -112,10 +114,8 @@ void ToolBar::MakeActions()
m_set_pc_action = addAction(tr("Set PC"), this, &ToolBar::SetPCPressed);

m_open_action = addAction(tr("Open"), this, &ToolBar::OpenPressed);
m_refresh_action = addAction(tr("Refresh"), [this] {
m_refresh_action->setEnabled(false);
emit RefreshPressed();
});
m_refresh_action = addAction(tr("Refresh"), [this] { emit RefreshPressed(); });
m_refresh_action->setEnabled(false);

addSeparator();

@@ -90,7 +90,8 @@ std::shared_ptr<const GameFile> GameFileCache::AddOrGet(const std::string& path,
bool GameFileCache::Update(
const std::vector<std::string>& all_game_paths,
std::function<void(const std::shared_ptr<const GameFile>&)> game_added_to_cache,
std::function<void(const std::string&)> game_removed_from_cache)
std::function<void(const std::string&)> game_removed_from_cache,
const std::atomic_bool& processing_halted)
{
// Copy game paths into a set, except ones that match DiscIO::ShouldHideFromGameList.
// TODO: Prevent DoFileSearch from looking inside /files/ directories of DirectoryBlobs at all?
@@ -113,6 +114,9 @@ bool GameFileCache::Update(
auto end = m_cached_files.end();
while (it != end)
{
if (processing_halted)
break;

if (game_paths.erase((*it)->GetFilePath()))
{
++it;
@@ -134,6 +138,9 @@ bool GameFileCache::Update(
// aren't in m_cached_files, so we simply add all of them to m_cached_files.
for (const std::string& path : game_paths)
{
if (processing_halted)
break;

auto file = std::make_shared<GameFile>(path);
if (file->IsValid())
{
@@ -149,12 +156,16 @@ bool GameFileCache::Update(
}

bool GameFileCache::UpdateAdditionalMetadata(
std::function<void(const std::shared_ptr<const GameFile>&)> game_updated)
std::function<void(const std::shared_ptr<const GameFile>&)> game_updated,
const std::atomic_bool& processing_halted)
{
bool cache_changed = false;

for (std::shared_ptr<GameFile>& file : m_cached_files)
{
if (processing_halted)
break;

const bool updated = UpdateAdditionalMetadata(&file);
cache_changed |= updated;
if (game_updated && updated)
@@ -4,6 +4,7 @@

#pragma once

#include <atomic>
#include <cstddef>
#include <functional>
#include <memory>
@@ -44,9 +45,11 @@ class GameFileCache
// These functions return true if the call modified the cache.
bool Update(const std::vector<std::string>& all_game_paths,
std::function<void(const std::shared_ptr<const GameFile>&)> game_added_to_cache = {},
std::function<void(const std::string&)> game_removed_from_cache = {});
std::function<void(const std::string&)> game_removed_from_cache = {},
const std::atomic_bool& processing_halted = false);
bool UpdateAdditionalMetadata(
std::function<void(const std::shared_ptr<const GameFile>&)> game_updated = {});
std::function<void(const std::shared_ptr<const GameFile>&)> game_updated = {},
const std::atomic_bool& processing_halted = false);

bool Load();
bool Save();