diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 0be279ddb..f1c937780 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -14,6 +14,8 @@ add_executable(citra-qt applets/swkbd.h bootmanager.cpp bootmanager.h + compatibility_list.cpp + compatibility_list.h camera/camera_util.cpp camera/camera_util.h camera/still_image_camera.cpp @@ -76,6 +78,8 @@ add_executable(citra-qt game_list.cpp game_list.h game_list_p.h + game_list_worker.cpp + game_list_worker.h hotkeys.cpp hotkeys.h main.cpp diff --git a/src/citra_qt/compatibility_list.cpp b/src/citra_qt/compatibility_list.cpp new file mode 100644 index 000000000..92e729dee --- /dev/null +++ b/src/citra_qt/compatibility_list.cpp @@ -0,0 +1,16 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "citra_qt/compatibility_list.h" + +CompatibilityList::const_iterator FindMatchingCompatibilityEntry( + const CompatibilityList& compatibility_list, u64 program_id) { + return std::find_if(compatibility_list.begin(), compatibility_list.end(), + [program_id](const auto& element) { + std::string pid = fmt::format("{:016X}", program_id); + return element.first == pid; + }); +} diff --git a/src/citra_qt/compatibility_list.h b/src/citra_qt/compatibility_list.h new file mode 100644 index 000000000..df2dbd60a --- /dev/null +++ b/src/citra_qt/compatibility_list.h @@ -0,0 +1,15 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/common_types.h" + +using CompatibilityList = std::unordered_map>; + +CompatibilityList::const_iterator FindMatchingCompatibilityEntry( + const CompatibilityList& compatibility_list, u64 program_id); diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index f736c21df..c28878b88 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -21,8 +21,10 @@ #include #include #include +#include "citra_qt/compatibility_list.h" #include "citra_qt/game_list.h" #include "citra_qt/game_list_p.h" +#include "citra_qt/game_list_worker.h" #include "citra_qt/main.h" #include "citra_qt/ui_settings.h" #include "common/common_paths.h" @@ -30,7 +32,6 @@ #include "core/file_sys/archive_extsavedata.h" #include "core/file_sys/archive_source_sd_savedata.h" #include "core/hle/service/fs/archive.h" -#include "core/loader/loader.h" GameListSearchField::KeyReleaseEater::KeyReleaseEater(GameList* gamelist) : gamelist{gamelist} {} @@ -648,11 +649,6 @@ void GameList::LoadInterfaceLayout() { const QStringList GameList::supported_file_extensions = {"3ds", "3dsx", "elf", "axf", "cci", "cxi", "app"}; -static bool HasSupportedFileExtension(const std::string& file_name) { - QFileInfo file = QFileInfo(QString::fromStdString(file_name)); - return GameList::supported_file_extensions.contains(file.suffix(), Qt::CaseInsensitive); -} - void GameList::RefreshGameDirectory() { if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) { LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); @@ -678,123 +674,6 @@ QString GameList::FindGameByProgramID(QStandardItem* current_item, u64 program_i return ""; } -void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion, - GameListDir* parent_dir) { - const auto callback = [this, recursion, parent_dir](u64* num_entries_out, - const std::string& directory, - const std::string& virtual_name) -> bool { - std::string physical_name = directory + DIR_SEP + virtual_name; - - if (stop_processing) - return false; // Breaks the callback loop. - - bool is_dir = FileUtil::IsDirectory(physical_name); - if (!is_dir && HasSupportedFileExtension(physical_name)) { - std::unique_ptr loader = Loader::GetLoader(physical_name); - if (!loader) - return true; - - u64 program_id = 0; - loader->ReadProgramId(program_id); - - u64 extdata_id = 0; - loader->ReadExtdataId(extdata_id); - - std::vector smdh = [program_id, &loader]() -> std::vector { - std::vector original_smdh; - loader->ReadIcon(original_smdh); - - if (program_id < 0x0004000000000000 || program_id > 0x00040000FFFFFFFF) - return original_smdh; - - std::string update_path = Service::AM::GetTitleContentPath( - Service::FS::MediaType::SDMC, program_id + 0x0000000E00000000); - - if (!FileUtil::Exists(update_path)) - return original_smdh; - - std::unique_ptr update_loader = Loader::GetLoader(update_path); - - if (!update_loader) - return original_smdh; - - std::vector update_smdh; - update_loader->ReadIcon(update_smdh); - return update_smdh; - }(); - - if (!Loader::IsValidSMDH(smdh) && UISettings::values.game_list_hide_no_icon) { - // Skip this invalid entry - return true; - } - - auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); - - // The game list uses this as compatibility number for untested games - QString compatibility("99"); - if (it != compatibility_list.end()) - compatibility = it->second.first; - - emit EntryReady( - { - new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id, - extdata_id), - new GameListItemCompat(compatibility), - new GameListItemRegion(smdh), - new GameListItem( - QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), - new GameListItemSize(FileUtil::GetSize(physical_name)), - }, - parent_dir); - - } else if (is_dir && recursion > 0) { - watch_list.append(QString::fromStdString(physical_name)); - AddFstEntriesToGameList(physical_name, recursion - 1, parent_dir); - } - - return true; - }; - - FileUtil::ForeachDirectoryEntry(nullptr, dir_path, callback); -} - -void GameListWorker::run() { - stop_processing = false; - for (UISettings::GameDir& game_dir : game_dirs) { - if (game_dir.path == "INSTALLED") { - QString path = - QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir)) + - "Nintendo " - "3DS/00000000000000000000000000000000/" - "00000000000000000000000000000000/title/00040000"; - watch_list.append(path); - GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir); - emit DirEntryReady({game_list_dir}); - AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir); - } else if (game_dir.path == "SYSTEM") { - QString path = - QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)) + - "00000000000000000000000000000000/title/00040010"; - watch_list.append(path); - GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir); - emit DirEntryReady({game_list_dir}); - AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir); - } else { - watch_list.append(game_dir.path); - GameListDir* game_list_dir = new GameListDir(game_dir); - emit DirEntryReady({game_list_dir}); - AddFstEntriesToGameList(game_dir.path.toStdString(), game_dir.deep_scan ? 256 : 0, - game_list_dir); - } - }; - emit Finished(watch_list); -} - -void GameListWorker::Cancel() { - this->disconnect(); - stop_processing = true; -} - GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} { this->main_window = parent; diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h index c355ecc1e..a10d7fe15 100644 --- a/src/citra_qt/game_list.h +++ b/src/citra_qt/game_list.h @@ -4,9 +4,10 @@ #pragma once -#include +#include #include #include +#include "citra_qt/compatibility_list.h" #include "common/common_types.h" #include "ui_settings.h" @@ -70,9 +71,8 @@ signals: void GameChosen(QString game_path); void ShouldCancelWorker(); void OpenFolderRequested(u64 program_id, GameListOpenTarget target); - void NavigateToGamedbEntryRequested( - u64 program_id, - std::unordered_map>& compatibility_list); + void NavigateToGamedbEntryRequested(u64 program_id, + const CompatibilityList& compatibility_list); void OpenDirectory(QString directory); void AddDirectory(); void ShowList(bool show); @@ -103,7 +103,7 @@ private: QStandardItemModel* item_model = nullptr; GameListWorker* current_worker = nullptr; QFileSystemWatcher* watcher = nullptr; - std::unordered_map> compatibility_list; + CompatibilityList compatibility_list; friend class GameListSearchField; }; diff --git a/src/citra_qt/game_list_p.h b/src/citra_qt/game_list_p.h index cc8b92851..b3f473397 100644 --- a/src/citra_qt/game_list_p.h +++ b/src/citra_qt/game_list_p.h @@ -4,7 +4,6 @@ #pragma once -#include #include #include #include @@ -59,17 +58,6 @@ static QPixmap GetDefaultIcon(bool large) { return icon; } -static auto FindMatchingCompatibilityEntry( - const std::unordered_map>& compatibility_list, - u64 program_id) { - return std::find_if( - compatibility_list.begin(), compatibility_list.end(), - [program_id](const std::pair>& element) { - std::string pid = fmt::format("{:016X}", program_id); - return element.first == pid; - }); -} - /** * Gets the short game title from SMDH data. * @param smdh SMDH data @@ -216,7 +204,7 @@ class GameListItemCompat : public GameListItem { public: static const int CompatNumberRole = SortRole; GameListItemCompat() = default; - explicit GameListItemCompat(const QString& compatiblity) { + explicit GameListItemCompat(const QString& compatibility) { setData(type(), TypeRole); struct CompatStatus { @@ -235,13 +223,13 @@ public: {"99", {"#000000", QT_TR_NOOP("Not Tested"), QT_TR_NOOP("The game has not yet been tested.")}}}; // clang-format on - auto iterator = status_data.find(compatiblity); + auto iterator = status_data.find(compatibility); if (iterator == status_data.end()) { - LOG_WARNING(Frontend, "Invalid compatibility number {}", compatiblity.toStdString()); + LOG_WARNING(Frontend, "Invalid compatibility number {}", compatibility.toStdString()); return; } - CompatStatus status = iterator->second; - setData(compatiblity, CompatNumberRole); + const CompatStatus& status = iterator->second; + setData(compatibility, CompatNumberRole); setText(QObject::tr(status.text)); setToolTip(QObject::tr(status.tooltip)); setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole); @@ -373,51 +361,6 @@ public: } }; -/** - * Asynchronous worker object for populating the game list. - * Communicates with other threads through Qt's signal/slot system. - */ -class GameListWorker : public QObject, public QRunnable { - Q_OBJECT - -public: - explicit GameListWorker( - QList& game_dirs, - const std::unordered_map>& compatibility_list) - : game_dirs(game_dirs), compatibility_list(compatibility_list) {} - -public slots: - /// Starts the processing of directory tree information. - void run() override; - /// Tells the worker that it should no longer continue processing. Thread-safe. - void Cancel(); - -signals: - /** - * The `EntryReady` signal is emitted once an entry has been prepared and is ready - * to be added to the game list. - * @param entry_items a list with `QStandardItem`s that make up the columns of the new - * entry. - */ - void DirEntryReady(GameListDir* entry_items); - void EntryReady(QList entry_items, GameListDir* parent_dir); - - /** - * After the worker has traversed the game directory looking for entries, this signal is - * emitted with a list of folders that should be watched for changes as well. - */ - void Finished(QStringList watch_list); - -private: - QStringList watch_list; - const std::unordered_map>& compatibility_list; - QList& game_dirs; - std::atomic_bool stop_processing; - - void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion, - GameListDir* parent_dir); -}; - class GameList; class QHBoxLayout; class QTreeView; diff --git a/src/citra_qt/game_list_worker.cpp b/src/citra_qt/game_list_worker.cpp new file mode 100644 index 000000000..7ec7c7677 --- /dev/null +++ b/src/citra_qt/game_list_worker.cpp @@ -0,0 +1,150 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include "citra_qt/compatibility_list.h" +#include "citra_qt/game_list.h" +#include "citra_qt/game_list_p.h" +#include "citra_qt/game_list_worker.h" +#include "citra_qt/ui_settings.h" +#include "common/common_paths.h" +#include "common/file_util.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/fs/archive.h" +#include "core/loader/loader.h" + +namespace { +bool HasSupportedFileExtension(const std::string& file_name) { + const QFileInfo file = QFileInfo(QString::fromStdString(file_name)); + return GameList::supported_file_extensions.contains(file.suffix(), Qt::CaseInsensitive); +} +} // Anonymous namespace + +GameListWorker::GameListWorker(QList& game_dirs, + const CompatibilityList& compatibility_list) + : game_dirs(game_dirs), compatibility_list(compatibility_list) {} + +GameListWorker::~GameListWorker() = default; + +void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion, + GameListDir* parent_dir) { + const auto callback = [this, recursion, parent_dir](u64* num_entries_out, + const std::string& directory, + const std::string& virtual_name) -> bool { + std::string physical_name = directory + DIR_SEP + virtual_name; + + if (stop_processing) + return false; // Breaks the callback loop. + + bool is_dir = FileUtil::IsDirectory(physical_name); + if (!is_dir && HasSupportedFileExtension(physical_name)) { + std::unique_ptr loader = Loader::GetLoader(physical_name); + if (!loader) + return true; + + u64 program_id = 0; + loader->ReadProgramId(program_id); + + u64 extdata_id = 0; + loader->ReadExtdataId(extdata_id); + + std::vector smdh = [program_id, &loader]() -> std::vector { + std::vector original_smdh; + loader->ReadIcon(original_smdh); + + if (program_id < 0x0004000000000000 || program_id > 0x00040000FFFFFFFF) + return original_smdh; + + std::string update_path = Service::AM::GetTitleContentPath( + Service::FS::MediaType::SDMC, program_id + 0x0000000E00000000); + + if (!FileUtil::Exists(update_path)) + return original_smdh; + + std::unique_ptr update_loader = Loader::GetLoader(update_path); + + if (!update_loader) + return original_smdh; + + std::vector update_smdh; + update_loader->ReadIcon(update_smdh); + return update_smdh; + }(); + + if (!Loader::IsValidSMDH(smdh) && UISettings::values.game_list_hide_no_icon) { + // Skip this invalid entry + return true; + } + + auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); + + // The game list uses this as compatibility number for untested games + QString compatibility("99"); + if (it != compatibility_list.end()) + compatibility = it->second.first; + + emit EntryReady( + { + new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id, + extdata_id), + new GameListItemCompat(compatibility), + new GameListItemRegion(smdh), + new GameListItem( + QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), + new GameListItemSize(FileUtil::GetSize(physical_name)), + }, + parent_dir); + + } else if (is_dir && recursion > 0) { + watch_list.append(QString::fromStdString(physical_name)); + AddFstEntriesToGameList(physical_name, recursion - 1, parent_dir); + } + + return true; + }; + + FileUtil::ForeachDirectoryEntry(nullptr, dir_path, callback); +} + +void GameListWorker::run() { + stop_processing = false; + for (UISettings::GameDir& game_dir : game_dirs) { + if (game_dir.path == "INSTALLED") { + QString path = + QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir)) + + "Nintendo " + "3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/00040000"; + watch_list.append(path); + GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir); + emit DirEntryReady({game_list_dir}); + AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir); + } else if (game_dir.path == "SYSTEM") { + QString path = + QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)) + + "00000000000000000000000000000000/title/00040010"; + watch_list.append(path); + GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir); + emit DirEntryReady({game_list_dir}); + AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir); + } else { + watch_list.append(game_dir.path); + GameListDir* game_list_dir = new GameListDir(game_dir); + emit DirEntryReady({game_list_dir}); + AddFstEntriesToGameList(game_dir.path.toStdString(), game_dir.deep_scan ? 256 : 0, + game_list_dir); + } + }; + emit Finished(watch_list); +} + +void GameListWorker::Cancel() { + this->disconnect(); + stop_processing = true; +} diff --git a/src/citra_qt/game_list_worker.h b/src/citra_qt/game_list_worker.h new file mode 100644 index 000000000..a955e6376 --- /dev/null +++ b/src/citra_qt/game_list_worker.h @@ -0,0 +1,62 @@ +// Copyright 2018 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "citra_qt/compatibility_list.h" +#include "common/common_types.h" + +class QStandardItem; + +/** + * Asynchronous worker object for populating the game list. + * Communicates with other threads through Qt's signal/slot system. + */ +class GameListWorker : public QObject, public QRunnable { + Q_OBJECT + +public: + GameListWorker(QList& game_dirs, + const CompatibilityList& compatibility_list); + ~GameListWorker() override; + + /// Starts the processing of directory tree information. + void run() override; + + /// Tells the worker that it should no longer continue processing. Thread-safe. + void Cancel(); + +signals: + /** + * The `EntryReady` signal is emitted once an entry has been prepared and is ready + * to be added to the game list. + * @param entry_items a list with `QStandardItem`s that make up the columns of the new entry. + */ + void DirEntryReady(GameListDir* entry_items); + void EntryReady(QList entry_items, GameListDir* parent_dir); + + /** + * After the worker has traversed the game directory looking for entries, this signal is emitted + * with a list of folders that should be watched for changes as well. + */ + void Finished(QStringList watch_list); + +private: + void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion, + GameListDir* parent_dir); + + QStringList watch_list; + const CompatibilityList& compatibility_list; + QList& game_dirs; + std::atomic_bool stop_processing; +}; diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 669ced7f6..af33515c1 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -21,6 +21,7 @@ #include "citra_qt/camera/qt_multimedia_camera.h" #include "citra_qt/camera/still_image_camera.h" #include "citra_qt/compatdb.h" +#include "citra_qt/compatibility_list.h" #include "citra_qt/configuration/config.h" #include "citra_qt/configuration/configure_dialog.h" #include "citra_qt/debugger/console.h" @@ -960,14 +961,11 @@ void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) { QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); } -void GMainWindow::OnGameListNavigateToGamedbEntry( - u64 program_id, - std::unordered_map>& compatibility_list) { - +void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, + const CompatibilityList& compatibility_list) { auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); QString directory; - if (it != compatibility_list.end()) directory = it->second.second; diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index bbc0714c5..fc1382ddd 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -9,6 +9,7 @@ #include #include #include +#include "citra_qt/compatibility_list.h" #include "citra_qt/hotkeys.h" #include "common/announce_multiplayer_room.h" #include "core/core.h" @@ -153,9 +154,8 @@ private slots: /// Called whenever a user selects a game in the game list widget. void OnGameListLoadFile(QString game_path); void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target); - void OnGameListNavigateToGamedbEntry( - u64 program_id, - std::unordered_map>& compatibility_list); + void OnGameListNavigateToGamedbEntry(u64 program_id, + const CompatibilityList& compatibility_list); void OnGameListOpenDirectory(QString path); void OnGameListAddDirectory(); void OnGameListShowList(bool show);