diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index ea9ea69e4..a2b6e984e 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -43,6 +43,8 @@ add_executable(yuzu 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/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index a3b841684..86532e4a9 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -18,17 +18,10 @@ #include "common/common_types.h" #include "common/file_util.h" #include "common/logging/log.h" -#include "core/file_sys/content_archive.h" -#include "core/file_sys/control_metadata.h" -#include "core/file_sys/nca_metadata.h" #include "core/file_sys/patch_manager.h" -#include "core/file_sys/registered_cache.h" -#include "core/file_sys/romfs.h" -#include "core/file_sys/vfs_real.h" -#include "core/hle/service/filesystem/filesystem.h" -#include "core/loader/loader.h" #include "yuzu/game_list.h" #include "yuzu/game_list_p.h" +#include "yuzu/game_list_worker.h" #include "yuzu/main.h" #include "yuzu/ui_settings.h" @@ -436,45 +429,6 @@ void GameList::LoadInterfaceLayout() { const QStringList GameList::supported_file_extensions = {"nso", "nro", "nca", "xci", "nsp"}; -static 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); -} - -static bool IsExtractedNCAMain(const std::string& file_name) { - return QFileInfo(QString::fromStdString(file_name)).fileName() == "main"; -} - -static QString FormatGameName(const std::string& physical_name) { - const QString physical_name_as_qstring = QString::fromStdString(physical_name); - const QFileInfo file_info(physical_name_as_qstring); - - if (IsExtractedNCAMain(physical_name)) { - return file_info.dir().path(); - } - - return physical_name_as_qstring; -} - -static QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, - bool updatable = true) { - QString out; - for (const auto& kv : patch_manager.GetPatchVersionNames()) { - if (!updatable && kv.first == FileSys::PatchType::Update) - continue; - - if (kv.second.empty()) { - out.append(fmt::format("{}\n", FileSys::FormatPatchTypeName(kv.first)).c_str()); - } else { - out.append(fmt::format("{} ({})\n", FileSys::FormatPatchTypeName(kv.first), kv.second) - .c_str()); - } - } - - out.chop(1); - return out; -} - void GameList::RefreshGameDirectory() { if (!UISettings::values.gamedir.isEmpty() && current_worker != nullptr) { LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); @@ -482,176 +436,3 @@ void GameList::RefreshGameDirectory() { PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); } } - -static void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, - const std::shared_ptr& nca, - std::vector& icon, std::string& name) { - auto [nacp, icon_file] = patch_manager.ParseControlNCA(nca); - if (icon_file != nullptr) - icon = icon_file->ReadAllBytes(); - if (nacp != nullptr) - name = nacp->GetApplicationName(); -} - -GameListWorker::GameListWorker( - FileSys::VirtualFilesystem vfs, QString dir_path, bool deep_scan, - const std::unordered_map>& compatibility_list) - : vfs(std::move(vfs)), dir_path(std::move(dir_path)), deep_scan(deep_scan), - compatibility_list(compatibility_list) {} - -GameListWorker::~GameListWorker() = default; - -void GameListWorker::AddInstalledTitlesToGameList() { - const auto cache = Service::FileSystem::GetUnionContents(); - const auto installed_games = cache->ListEntriesFilter(FileSys::TitleType::Application, - FileSys::ContentRecordType::Program); - - for (const auto& game : installed_games) { - const auto& file = cache->GetEntryUnparsed(game); - std::unique_ptr loader = Loader::GetLoader(file); - if (!loader) - continue; - - std::vector icon; - std::string name; - u64 program_id = 0; - loader->ReadProgramId(program_id); - - const FileSys::PatchManager patch{program_id}; - const auto& control = cache->GetEntry(game.title_id, FileSys::ContentRecordType::Control); - if (control != nullptr) - GetMetadataFromControlNCA(patch, control, icon, name); - - 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( - FormatGameName(file->GetFullPath()), icon, QString::fromStdString(name), - QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), - program_id), - new GameListItemCompat(compatibility), - new GameListItem(FormatPatchNameVersions(patch)), - new GameListItem( - QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), - new GameListItemSize(file->GetSize()), - }); - } - - const auto control_data = cache->ListEntriesFilter(FileSys::TitleType::Application, - FileSys::ContentRecordType::Control); - - for (const auto& entry : control_data) { - const auto nca = cache->GetEntry(entry); - if (nca != nullptr) - nca_control_map.insert_or_assign(entry.title_id, nca); - } -} - -void GameListWorker::FillControlMap(const std::string& dir_path) { - const auto nca_control_callback = [this](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); - QFileInfo file_info(physical_name.c_str()); - if (!is_dir && file_info.suffix().toStdString() == "nca") { - auto nca = - std::make_shared(vfs->OpenFile(physical_name, FileSys::Mode::Read)); - if (nca->GetType() == FileSys::NCAContentType::Control) - nca_control_map.insert_or_assign(nca->GetTitleId(), nca); - } - return true; - }; - - FileUtil::ForeachDirectoryEntry(nullptr, dir_path, nca_control_callback); -} - -void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) { - const auto callback = [this, recursion](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) || IsExtractedNCAMain(physical_name))) { - std::unique_ptr loader = - Loader::GetLoader(vfs->OpenFile(physical_name, FileSys::Mode::Read)); - if (!loader || ((loader->GetFileType() == Loader::FileType::Unknown || - loader->GetFileType() == Loader::FileType::Error) && - !UISettings::values.show_unknown)) - return true; - - std::vector icon; - const auto res1 = loader->ReadIcon(icon); - - u64 program_id = 0; - const auto res2 = loader->ReadProgramId(program_id); - - std::string name = " "; - const auto res3 = loader->ReadTitle(name); - - const FileSys::PatchManager patch{program_id}; - - if (res1 != Loader::ResultStatus::Success && res3 != Loader::ResultStatus::Success && - res2 == Loader::ResultStatus::Success) { - // Use from metadata pool. - if (nca_control_map.find(program_id) != nca_control_map.end()) { - const auto nca = nca_control_map[program_id]; - GetMetadataFromControlNCA(patch, nca, icon, name); - } - } - - 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( - FormatGameName(physical_name), icon, QString::fromStdString(name), - QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), - program_id), - new GameListItemCompat(compatibility), - new GameListItem(FormatPatchNameVersions(patch, loader->IsRomFSUpdatable())), - new GameListItem( - QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), - new GameListItemSize(FileUtil::GetSize(physical_name)), - }); - } else if (is_dir && recursion > 0) { - watch_list.append(QString::fromStdString(physical_name)); - AddFstEntriesToGameList(physical_name, recursion - 1); - } - - return true; - }; - - FileUtil::ForeachDirectoryEntry(nullptr, dir_path, callback); -} - -void GameListWorker::run() { - stop_processing = false; - watch_list.append(dir_path); - FillControlMap(dir_path.toStdString()); - AddInstalledTitlesToGameList(); - AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0); - nca_control_map.clear(); - emit Finished(watch_list); -} - -void GameListWorker::Cancel() { - this->disconnect(); - stop_processing = true; -} diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index a70a151c5..2720bf143 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h @@ -6,9 +6,7 @@ #include #include -#include #include -#include #include #include #include @@ -16,7 +14,6 @@ #include #include #include -#include #include #include @@ -26,12 +23,6 @@ #include "yuzu/ui_settings.h" #include "yuzu/util/util.h" -namespace FileSys { -class NCA; -class RegisteredCache; -class VfsFilesystem; -} // namespace FileSys - /** * Gets the default icon (for games without valid SMDH) * @param large If true, returns large icon (48x48), otherwise returns small icon (24x24) @@ -43,17 +34,6 @@ static QPixmap GetDefaultIcon(u32 size) { 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; - }); -} - class GameListItem : public QStandardItem { public: @@ -197,49 +177,13 @@ 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: - GameListWorker( - std::shared_ptr vfs, QString dir_path, bool deep_scan, - const std::unordered_map>& compatibility_list); - ~GameListWorker() override; - -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 EntryReady(QList entry_items); - - /** - * After the worker has traversed the game directory looking for entries, this signal is emmited - * with a list of folders that should be watched for changes as well. - */ - void Finished(QStringList watch_list); - -private: - std::shared_ptr vfs; - std::map> nca_control_map; - QStringList watch_list; - QString dir_path; - bool deep_scan; - const std::unordered_map>& compatibility_list; - std::atomic_bool stop_processing; - - void AddInstalledTitlesToGameList(); - void FillControlMap(const std::string& dir_path); - void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0); -}; +inline 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; + }); +} diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp new file mode 100644 index 000000000..9f26935d6 --- /dev/null +++ b/src/yuzu/game_list_worker.cpp @@ -0,0 +1,239 @@ +// 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 "common/common_paths.h" +#include "common/file_util.h" +#include "core/file_sys/content_archive.h" +#include "core/file_sys/control_metadata.h" +#include "core/file_sys/mode.h" +#include "core/file_sys/nca_metadata.h" +#include "core/file_sys/patch_manager.h" +#include "core/file_sys/registered_cache.h" +#include "core/hle/service/filesystem/filesystem.h" +#include "core/loader/loader.h" +#include "yuzu/game_list.h" +#include "yuzu/game_list_p.h" +#include "yuzu/game_list_worker.h" +#include "yuzu/ui_settings.h" + +namespace { +void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, + const std::shared_ptr& nca, std::vector& icon, + std::string& name) { + auto [nacp, icon_file] = patch_manager.ParseControlNCA(nca); + if (icon_file != nullptr) + icon = icon_file->ReadAllBytes(); + if (nacp != nullptr) + name = nacp->GetApplicationName(); +} + +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); +} + +bool IsExtractedNCAMain(const std::string& file_name) { + return QFileInfo(QString::fromStdString(file_name)).fileName() == "main"; +} + +QString FormatGameName(const std::string& physical_name) { + const QString physical_name_as_qstring = QString::fromStdString(physical_name); + const QFileInfo file_info(physical_name_as_qstring); + + if (IsExtractedNCAMain(physical_name)) { + return file_info.dir().path(); + } + + return physical_name_as_qstring; +} + +QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, bool updatable = true) { + QString out; + for (const auto& kv : patch_manager.GetPatchVersionNames()) { + if (!updatable && kv.first == FileSys::PatchType::Update) + continue; + + if (kv.second.empty()) { + out.append(fmt::format("{}\n", FileSys::FormatPatchTypeName(kv.first)).c_str()); + } else { + out.append(fmt::format("{} ({})\n", FileSys::FormatPatchTypeName(kv.first), kv.second) + .c_str()); + } + } + + out.chop(1); + return out; +} +} // Anonymous namespace + +GameListWorker::GameListWorker( + FileSys::VirtualFilesystem vfs, QString dir_path, bool deep_scan, + const std::unordered_map>& compatibility_list) + : vfs(std::move(vfs)), dir_path(std::move(dir_path)), deep_scan(deep_scan), + compatibility_list(compatibility_list) {} + +GameListWorker::~GameListWorker() = default; + +void GameListWorker::AddInstalledTitlesToGameList() { + const auto cache = Service::FileSystem::GetUnionContents(); + const auto installed_games = cache->ListEntriesFilter(FileSys::TitleType::Application, + FileSys::ContentRecordType::Program); + + for (const auto& game : installed_games) { + const auto& file = cache->GetEntryUnparsed(game); + std::unique_ptr loader = Loader::GetLoader(file); + if (!loader) + continue; + + std::vector icon; + std::string name; + u64 program_id = 0; + loader->ReadProgramId(program_id); + + const FileSys::PatchManager patch{program_id}; + const auto& control = cache->GetEntry(game.title_id, FileSys::ContentRecordType::Control); + if (control != nullptr) + GetMetadataFromControlNCA(patch, control, icon, name); + + 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( + FormatGameName(file->GetFullPath()), icon, QString::fromStdString(name), + QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), + program_id), + new GameListItemCompat(compatibility), + new GameListItem(FormatPatchNameVersions(patch)), + new GameListItem( + QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), + new GameListItemSize(file->GetSize()), + }); + } + + const auto control_data = cache->ListEntriesFilter(FileSys::TitleType::Application, + FileSys::ContentRecordType::Control); + + for (const auto& entry : control_data) { + const auto nca = cache->GetEntry(entry); + if (nca != nullptr) + nca_control_map.insert_or_assign(entry.title_id, nca); + } +} + +void GameListWorker::FillControlMap(const std::string& dir_path) { + const auto nca_control_callback = [this](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); + QFileInfo file_info(physical_name.c_str()); + if (!is_dir && file_info.suffix().toStdString() == "nca") { + auto nca = + std::make_shared(vfs->OpenFile(physical_name, FileSys::Mode::Read)); + if (nca->GetType() == FileSys::NCAContentType::Control) + nca_control_map.insert_or_assign(nca->GetTitleId(), nca); + } + return true; + }; + + FileUtil::ForeachDirectoryEntry(nullptr, dir_path, nca_control_callback); +} + +void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) { + const auto callback = [this, recursion](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) || IsExtractedNCAMain(physical_name))) { + std::unique_ptr loader = + Loader::GetLoader(vfs->OpenFile(physical_name, FileSys::Mode::Read)); + if (!loader || ((loader->GetFileType() == Loader::FileType::Unknown || + loader->GetFileType() == Loader::FileType::Error) && + !UISettings::values.show_unknown)) + return true; + + std::vector icon; + const auto res1 = loader->ReadIcon(icon); + + u64 program_id = 0; + const auto res2 = loader->ReadProgramId(program_id); + + std::string name = " "; + const auto res3 = loader->ReadTitle(name); + + const FileSys::PatchManager patch{program_id}; + + if (res1 != Loader::ResultStatus::Success && res3 != Loader::ResultStatus::Success && + res2 == Loader::ResultStatus::Success) { + // Use from metadata pool. + if (nca_control_map.find(program_id) != nca_control_map.end()) { + const auto nca = nca_control_map[program_id]; + GetMetadataFromControlNCA(patch, nca, icon, name); + } + } + + 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( + FormatGameName(physical_name), icon, QString::fromStdString(name), + QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), + program_id), + new GameListItemCompat(compatibility), + new GameListItem(FormatPatchNameVersions(patch, loader->IsRomFSUpdatable())), + new GameListItem( + QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), + new GameListItemSize(FileUtil::GetSize(physical_name)), + }); + } else if (is_dir && recursion > 0) { + watch_list.append(QString::fromStdString(physical_name)); + AddFstEntriesToGameList(physical_name, recursion - 1); + } + + return true; + }; + + FileUtil::ForeachDirectoryEntry(nullptr, dir_path, callback); +} + +void GameListWorker::run() { + stop_processing = false; + watch_list.append(dir_path); + FillControlMap(dir_path.toStdString()); + AddInstalledTitlesToGameList(); + AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0); + nca_control_map.clear(); + emit Finished(watch_list); +} + +void GameListWorker::Cancel() { + this->disconnect(); + stop_processing = true; +} diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h new file mode 100644 index 000000000..42c93fc31 --- /dev/null +++ b/src/yuzu/game_list_worker.h @@ -0,0 +1,72 @@ +// 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 "common/common_types.h" + +class QStandardItem; + +namespace FileSys { +class NCA; +class VfsFilesystem; +} // namespace FileSys + +/** + * 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( + std::shared_ptr vfs, QString dir_path, bool deep_scan, + const std::unordered_map>& 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 EntryReady(QList entry_items); + + /** + * 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 AddInstalledTitlesToGameList(); + void FillControlMap(const std::string& dir_path); + void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0); + + std::shared_ptr vfs; + std::map> nca_control_map; + QStringList watch_list; + QString dir_path; + bool deep_scan; + const std::unordered_map>& compatibility_list; + std::atomic_bool stop_processing; +};