diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/yuzu/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/yuzu/game_list.cpp | 221 | ||||
| -rw-r--r-- | src/yuzu/game_list_p.h | 76 | ||||
| -rw-r--r-- | src/yuzu/game_list_worker.cpp | 239 | ||||
| -rw-r--r-- | src/yuzu/game_list_worker.h | 72 |
5 files changed, 324 insertions, 286 deletions
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 | |||
| 43 | game_list.cpp | 43 | game_list.cpp |
| 44 | game_list.h | 44 | game_list.h |
| 45 | game_list_p.h | 45 | game_list_p.h |
| 46 | game_list_worker.cpp | ||
| 47 | game_list_worker.h | ||
| 46 | hotkeys.cpp | 48 | hotkeys.cpp |
| 47 | hotkeys.h | 49 | hotkeys.h |
| 48 | main.cpp | 50 | 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 @@ | |||
| 18 | #include "common/common_types.h" | 18 | #include "common/common_types.h" |
| 19 | #include "common/file_util.h" | 19 | #include "common/file_util.h" |
| 20 | #include "common/logging/log.h" | 20 | #include "common/logging/log.h" |
| 21 | #include "core/file_sys/content_archive.h" | ||
| 22 | #include "core/file_sys/control_metadata.h" | ||
| 23 | #include "core/file_sys/nca_metadata.h" | ||
| 24 | #include "core/file_sys/patch_manager.h" | 21 | #include "core/file_sys/patch_manager.h" |
| 25 | #include "core/file_sys/registered_cache.h" | ||
| 26 | #include "core/file_sys/romfs.h" | ||
| 27 | #include "core/file_sys/vfs_real.h" | ||
| 28 | #include "core/hle/service/filesystem/filesystem.h" | ||
| 29 | #include "core/loader/loader.h" | ||
| 30 | #include "yuzu/game_list.h" | 22 | #include "yuzu/game_list.h" |
| 31 | #include "yuzu/game_list_p.h" | 23 | #include "yuzu/game_list_p.h" |
| 24 | #include "yuzu/game_list_worker.h" | ||
| 32 | #include "yuzu/main.h" | 25 | #include "yuzu/main.h" |
| 33 | #include "yuzu/ui_settings.h" | 26 | #include "yuzu/ui_settings.h" |
| 34 | 27 | ||
| @@ -436,45 +429,6 @@ void GameList::LoadInterfaceLayout() { | |||
| 436 | 429 | ||
| 437 | const QStringList GameList::supported_file_extensions = {"nso", "nro", "nca", "xci", "nsp"}; | 430 | const QStringList GameList::supported_file_extensions = {"nso", "nro", "nca", "xci", "nsp"}; |
| 438 | 431 | ||
| 439 | static bool HasSupportedFileExtension(const std::string& file_name) { | ||
| 440 | const QFileInfo file = QFileInfo(QString::fromStdString(file_name)); | ||
| 441 | return GameList::supported_file_extensions.contains(file.suffix(), Qt::CaseInsensitive); | ||
| 442 | } | ||
| 443 | |||
| 444 | static bool IsExtractedNCAMain(const std::string& file_name) { | ||
| 445 | return QFileInfo(QString::fromStdString(file_name)).fileName() == "main"; | ||
| 446 | } | ||
| 447 | |||
| 448 | static QString FormatGameName(const std::string& physical_name) { | ||
| 449 | const QString physical_name_as_qstring = QString::fromStdString(physical_name); | ||
| 450 | const QFileInfo file_info(physical_name_as_qstring); | ||
| 451 | |||
| 452 | if (IsExtractedNCAMain(physical_name)) { | ||
| 453 | return file_info.dir().path(); | ||
| 454 | } | ||
| 455 | |||
| 456 | return physical_name_as_qstring; | ||
| 457 | } | ||
| 458 | |||
| 459 | static QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, | ||
| 460 | bool updatable = true) { | ||
| 461 | QString out; | ||
| 462 | for (const auto& kv : patch_manager.GetPatchVersionNames()) { | ||
| 463 | if (!updatable && kv.first == FileSys::PatchType::Update) | ||
| 464 | continue; | ||
| 465 | |||
| 466 | if (kv.second.empty()) { | ||
| 467 | out.append(fmt::format("{}\n", FileSys::FormatPatchTypeName(kv.first)).c_str()); | ||
| 468 | } else { | ||
| 469 | out.append(fmt::format("{} ({})\n", FileSys::FormatPatchTypeName(kv.first), kv.second) | ||
| 470 | .c_str()); | ||
| 471 | } | ||
| 472 | } | ||
| 473 | |||
| 474 | out.chop(1); | ||
| 475 | return out; | ||
| 476 | } | ||
| 477 | |||
| 478 | void GameList::RefreshGameDirectory() { | 432 | void GameList::RefreshGameDirectory() { |
| 479 | if (!UISettings::values.gamedir.isEmpty() && current_worker != nullptr) { | 433 | if (!UISettings::values.gamedir.isEmpty() && current_worker != nullptr) { |
| 480 | LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); | 434 | LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); |
| @@ -482,176 +436,3 @@ void GameList::RefreshGameDirectory() { | |||
| 482 | PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); | 436 | PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); |
| 483 | } | 437 | } |
| 484 | } | 438 | } |
| 485 | |||
| 486 | static void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, | ||
| 487 | const std::shared_ptr<FileSys::NCA>& nca, | ||
| 488 | std::vector<u8>& icon, std::string& name) { | ||
| 489 | auto [nacp, icon_file] = patch_manager.ParseControlNCA(nca); | ||
| 490 | if (icon_file != nullptr) | ||
| 491 | icon = icon_file->ReadAllBytes(); | ||
| 492 | if (nacp != nullptr) | ||
| 493 | name = nacp->GetApplicationName(); | ||
| 494 | } | ||
| 495 | |||
| 496 | GameListWorker::GameListWorker( | ||
| 497 | FileSys::VirtualFilesystem vfs, QString dir_path, bool deep_scan, | ||
| 498 | const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list) | ||
| 499 | : vfs(std::move(vfs)), dir_path(std::move(dir_path)), deep_scan(deep_scan), | ||
| 500 | compatibility_list(compatibility_list) {} | ||
| 501 | |||
| 502 | GameListWorker::~GameListWorker() = default; | ||
| 503 | |||
| 504 | void GameListWorker::AddInstalledTitlesToGameList() { | ||
| 505 | const auto cache = Service::FileSystem::GetUnionContents(); | ||
| 506 | const auto installed_games = cache->ListEntriesFilter(FileSys::TitleType::Application, | ||
| 507 | FileSys::ContentRecordType::Program); | ||
| 508 | |||
| 509 | for (const auto& game : installed_games) { | ||
| 510 | const auto& file = cache->GetEntryUnparsed(game); | ||
| 511 | std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(file); | ||
| 512 | if (!loader) | ||
| 513 | continue; | ||
| 514 | |||
| 515 | std::vector<u8> icon; | ||
| 516 | std::string name; | ||
| 517 | u64 program_id = 0; | ||
| 518 | loader->ReadProgramId(program_id); | ||
| 519 | |||
| 520 | const FileSys::PatchManager patch{program_id}; | ||
| 521 | const auto& control = cache->GetEntry(game.title_id, FileSys::ContentRecordType::Control); | ||
| 522 | if (control != nullptr) | ||
| 523 | GetMetadataFromControlNCA(patch, control, icon, name); | ||
| 524 | |||
| 525 | auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); | ||
| 526 | |||
| 527 | // The game list uses this as compatibility number for untested games | ||
| 528 | QString compatibility("99"); | ||
| 529 | if (it != compatibility_list.end()) | ||
| 530 | compatibility = it->second.first; | ||
| 531 | |||
| 532 | emit EntryReady({ | ||
| 533 | new GameListItemPath( | ||
| 534 | FormatGameName(file->GetFullPath()), icon, QString::fromStdString(name), | ||
| 535 | QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), | ||
| 536 | program_id), | ||
| 537 | new GameListItemCompat(compatibility), | ||
| 538 | new GameListItem(FormatPatchNameVersions(patch)), | ||
| 539 | new GameListItem( | ||
| 540 | QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), | ||
| 541 | new GameListItemSize(file->GetSize()), | ||
| 542 | }); | ||
| 543 | } | ||
| 544 | |||
| 545 | const auto control_data = cache->ListEntriesFilter(FileSys::TitleType::Application, | ||
| 546 | FileSys::ContentRecordType::Control); | ||
| 547 | |||
| 548 | for (const auto& entry : control_data) { | ||
| 549 | const auto nca = cache->GetEntry(entry); | ||
| 550 | if (nca != nullptr) | ||
| 551 | nca_control_map.insert_or_assign(entry.title_id, nca); | ||
| 552 | } | ||
| 553 | } | ||
| 554 | |||
| 555 | void GameListWorker::FillControlMap(const std::string& dir_path) { | ||
| 556 | const auto nca_control_callback = [this](u64* num_entries_out, const std::string& directory, | ||
| 557 | const std::string& virtual_name) -> bool { | ||
| 558 | std::string physical_name = directory + DIR_SEP + virtual_name; | ||
| 559 | |||
| 560 | if (stop_processing) | ||
| 561 | return false; // Breaks the callback loop. | ||
| 562 | |||
| 563 | bool is_dir = FileUtil::IsDirectory(physical_name); | ||
| 564 | QFileInfo file_info(physical_name.c_str()); | ||
| 565 | if (!is_dir && file_info.suffix().toStdString() == "nca") { | ||
| 566 | auto nca = | ||
| 567 | std::make_shared<FileSys::NCA>(vfs->OpenFile(physical_name, FileSys::Mode::Read)); | ||
| 568 | if (nca->GetType() == FileSys::NCAContentType::Control) | ||
| 569 | nca_control_map.insert_or_assign(nca->GetTitleId(), nca); | ||
| 570 | } | ||
| 571 | return true; | ||
| 572 | }; | ||
| 573 | |||
| 574 | FileUtil::ForeachDirectoryEntry(nullptr, dir_path, nca_control_callback); | ||
| 575 | } | ||
| 576 | |||
| 577 | void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) { | ||
| 578 | const auto callback = [this, recursion](u64* num_entries_out, const std::string& directory, | ||
| 579 | const std::string& virtual_name) -> bool { | ||
| 580 | std::string physical_name = directory + DIR_SEP + virtual_name; | ||
| 581 | |||
| 582 | if (stop_processing) | ||
| 583 | return false; // Breaks the callback loop. | ||
| 584 | |||
| 585 | bool is_dir = FileUtil::IsDirectory(physical_name); | ||
| 586 | if (!is_dir && | ||
| 587 | (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name))) { | ||
| 588 | std::unique_ptr<Loader::AppLoader> loader = | ||
| 589 | Loader::GetLoader(vfs->OpenFile(physical_name, FileSys::Mode::Read)); | ||
| 590 | if (!loader || ((loader->GetFileType() == Loader::FileType::Unknown || | ||
| 591 | loader->GetFileType() == Loader::FileType::Error) && | ||
| 592 | !UISettings::values.show_unknown)) | ||
| 593 | return true; | ||
| 594 | |||
| 595 | std::vector<u8> icon; | ||
| 596 | const auto res1 = loader->ReadIcon(icon); | ||
| 597 | |||
| 598 | u64 program_id = 0; | ||
| 599 | const auto res2 = loader->ReadProgramId(program_id); | ||
| 600 | |||
| 601 | std::string name = " "; | ||
| 602 | const auto res3 = loader->ReadTitle(name); | ||
| 603 | |||
| 604 | const FileSys::PatchManager patch{program_id}; | ||
| 605 | |||
| 606 | if (res1 != Loader::ResultStatus::Success && res3 != Loader::ResultStatus::Success && | ||
| 607 | res2 == Loader::ResultStatus::Success) { | ||
| 608 | // Use from metadata pool. | ||
| 609 | if (nca_control_map.find(program_id) != nca_control_map.end()) { | ||
| 610 | const auto nca = nca_control_map[program_id]; | ||
| 611 | GetMetadataFromControlNCA(patch, nca, icon, name); | ||
| 612 | } | ||
| 613 | } | ||
| 614 | |||
| 615 | auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); | ||
| 616 | |||
| 617 | // The game list uses this as compatibility number for untested games | ||
| 618 | QString compatibility("99"); | ||
| 619 | if (it != compatibility_list.end()) | ||
| 620 | compatibility = it->second.first; | ||
| 621 | |||
| 622 | emit EntryReady({ | ||
| 623 | new GameListItemPath( | ||
| 624 | FormatGameName(physical_name), icon, QString::fromStdString(name), | ||
| 625 | QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), | ||
| 626 | program_id), | ||
| 627 | new GameListItemCompat(compatibility), | ||
| 628 | new GameListItem(FormatPatchNameVersions(patch, loader->IsRomFSUpdatable())), | ||
| 629 | new GameListItem( | ||
| 630 | QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), | ||
| 631 | new GameListItemSize(FileUtil::GetSize(physical_name)), | ||
| 632 | }); | ||
| 633 | } else if (is_dir && recursion > 0) { | ||
| 634 | watch_list.append(QString::fromStdString(physical_name)); | ||
| 635 | AddFstEntriesToGameList(physical_name, recursion - 1); | ||
| 636 | } | ||
| 637 | |||
| 638 | return true; | ||
| 639 | }; | ||
| 640 | |||
| 641 | FileUtil::ForeachDirectoryEntry(nullptr, dir_path, callback); | ||
| 642 | } | ||
| 643 | |||
| 644 | void GameListWorker::run() { | ||
| 645 | stop_processing = false; | ||
| 646 | watch_list.append(dir_path); | ||
| 647 | FillControlMap(dir_path.toStdString()); | ||
| 648 | AddInstalledTitlesToGameList(); | ||
| 649 | AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0); | ||
| 650 | nca_control_map.clear(); | ||
| 651 | emit Finished(watch_list); | ||
| 652 | } | ||
| 653 | |||
| 654 | void GameListWorker::Cancel() { | ||
| 655 | this->disconnect(); | ||
| 656 | stop_processing = true; | ||
| 657 | } | ||
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 @@ | |||
| 6 | 6 | ||
| 7 | #include <algorithm> | 7 | #include <algorithm> |
| 8 | #include <array> | 8 | #include <array> |
| 9 | #include <atomic> | ||
| 10 | #include <map> | 9 | #include <map> |
| 11 | #include <memory> | ||
| 12 | #include <string> | 10 | #include <string> |
| 13 | #include <unordered_map> | 11 | #include <unordered_map> |
| 14 | #include <utility> | 12 | #include <utility> |
| @@ -16,7 +14,6 @@ | |||
| 16 | #include <QCoreApplication> | 14 | #include <QCoreApplication> |
| 17 | #include <QImage> | 15 | #include <QImage> |
| 18 | #include <QObject> | 16 | #include <QObject> |
| 19 | #include <QRunnable> | ||
| 20 | #include <QStandardItem> | 17 | #include <QStandardItem> |
| 21 | #include <QString> | 18 | #include <QString> |
| 22 | 19 | ||
| @@ -26,12 +23,6 @@ | |||
| 26 | #include "yuzu/ui_settings.h" | 23 | #include "yuzu/ui_settings.h" |
| 27 | #include "yuzu/util/util.h" | 24 | #include "yuzu/util/util.h" |
| 28 | 25 | ||
| 29 | namespace FileSys { | ||
| 30 | class NCA; | ||
| 31 | class RegisteredCache; | ||
| 32 | class VfsFilesystem; | ||
| 33 | } // namespace FileSys | ||
| 34 | |||
| 35 | /** | 26 | /** |
| 36 | * Gets the default icon (for games without valid SMDH) | 27 | * Gets the default icon (for games without valid SMDH) |
| 37 | * @param large If true, returns large icon (48x48), otherwise returns small icon (24x24) | 28 | * @param large If true, returns large icon (48x48), otherwise returns small icon (24x24) |
| @@ -43,17 +34,6 @@ static QPixmap GetDefaultIcon(u32 size) { | |||
| 43 | return icon; | 34 | return icon; |
| 44 | } | 35 | } |
| 45 | 36 | ||
| 46 | static auto FindMatchingCompatibilityEntry( | ||
| 47 | const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list, | ||
| 48 | u64 program_id) { | ||
| 49 | return std::find_if( | ||
| 50 | compatibility_list.begin(), compatibility_list.end(), | ||
| 51 | [program_id](const std::pair<std::string, std::pair<QString, QString>>& element) { | ||
| 52 | std::string pid = fmt::format("{:016X}", program_id); | ||
| 53 | return element.first == pid; | ||
| 54 | }); | ||
| 55 | } | ||
| 56 | |||
| 57 | class GameListItem : public QStandardItem { | 37 | class GameListItem : public QStandardItem { |
| 58 | 38 | ||
| 59 | public: | 39 | public: |
| @@ -197,49 +177,13 @@ public: | |||
| 197 | } | 177 | } |
| 198 | }; | 178 | }; |
| 199 | 179 | ||
| 200 | /** | 180 | inline auto FindMatchingCompatibilityEntry( |
| 201 | * Asynchronous worker object for populating the game list. | 181 | const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list, |
| 202 | * Communicates with other threads through Qt's signal/slot system. | 182 | u64 program_id) { |
| 203 | */ | 183 | return std::find_if( |
| 204 | class GameListWorker : public QObject, public QRunnable { | 184 | compatibility_list.begin(), compatibility_list.end(), |
| 205 | Q_OBJECT | 185 | [program_id](const std::pair<std::string, std::pair<QString, QString>>& element) { |
| 206 | 186 | std::string pid = fmt::format("{:016X}", program_id); | |
| 207 | public: | 187 | return element.first == pid; |
| 208 | GameListWorker( | 188 | }); |
| 209 | std::shared_ptr<FileSys::VfsFilesystem> vfs, QString dir_path, bool deep_scan, | 189 | } |
| 210 | const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list); | ||
| 211 | ~GameListWorker() override; | ||
| 212 | |||
| 213 | public slots: | ||
| 214 | /// Starts the processing of directory tree information. | ||
| 215 | void run() override; | ||
| 216 | /// Tells the worker that it should no longer continue processing. Thread-safe. | ||
| 217 | void Cancel(); | ||
| 218 | |||
| 219 | signals: | ||
| 220 | /** | ||
| 221 | * The `EntryReady` signal is emitted once an entry has been prepared and is ready | ||
| 222 | * to be added to the game list. | ||
| 223 | * @param entry_items a list with `QStandardItem`s that make up the columns of the new entry. | ||
| 224 | */ | ||
| 225 | void EntryReady(QList<QStandardItem*> entry_items); | ||
| 226 | |||
| 227 | /** | ||
| 228 | * After the worker has traversed the game directory looking for entries, this signal is emmited | ||
| 229 | * with a list of folders that should be watched for changes as well. | ||
| 230 | */ | ||
| 231 | void Finished(QStringList watch_list); | ||
| 232 | |||
| 233 | private: | ||
| 234 | std::shared_ptr<FileSys::VfsFilesystem> vfs; | ||
| 235 | std::map<u64, std::shared_ptr<FileSys::NCA>> nca_control_map; | ||
| 236 | QStringList watch_list; | ||
| 237 | QString dir_path; | ||
| 238 | bool deep_scan; | ||
| 239 | const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list; | ||
| 240 | std::atomic_bool stop_processing; | ||
| 241 | |||
| 242 | void AddInstalledTitlesToGameList(); | ||
| 243 | void FillControlMap(const std::string& dir_path); | ||
| 244 | void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0); | ||
| 245 | }; | ||
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 @@ | |||
| 1 | // Copyright 2018 yuzu emulator team | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include <memory> | ||
| 6 | #include <string> | ||
| 7 | #include <utility> | ||
| 8 | #include <vector> | ||
| 9 | |||
| 10 | #include <QDir> | ||
| 11 | #include <QFileInfo> | ||
| 12 | |||
| 13 | #include "common/common_paths.h" | ||
| 14 | #include "common/file_util.h" | ||
| 15 | #include "core/file_sys/content_archive.h" | ||
| 16 | #include "core/file_sys/control_metadata.h" | ||
| 17 | #include "core/file_sys/mode.h" | ||
| 18 | #include "core/file_sys/nca_metadata.h" | ||
| 19 | #include "core/file_sys/patch_manager.h" | ||
| 20 | #include "core/file_sys/registered_cache.h" | ||
| 21 | #include "core/hle/service/filesystem/filesystem.h" | ||
| 22 | #include "core/loader/loader.h" | ||
| 23 | #include "yuzu/game_list.h" | ||
| 24 | #include "yuzu/game_list_p.h" | ||
| 25 | #include "yuzu/game_list_worker.h" | ||
| 26 | #include "yuzu/ui_settings.h" | ||
| 27 | |||
| 28 | namespace { | ||
| 29 | void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, | ||
| 30 | const std::shared_ptr<FileSys::NCA>& nca, std::vector<u8>& icon, | ||
| 31 | std::string& name) { | ||
| 32 | auto [nacp, icon_file] = patch_manager.ParseControlNCA(nca); | ||
| 33 | if (icon_file != nullptr) | ||
| 34 | icon = icon_file->ReadAllBytes(); | ||
| 35 | if (nacp != nullptr) | ||
| 36 | name = nacp->GetApplicationName(); | ||
| 37 | } | ||
| 38 | |||
| 39 | bool HasSupportedFileExtension(const std::string& file_name) { | ||
| 40 | const QFileInfo file = QFileInfo(QString::fromStdString(file_name)); | ||
| 41 | return GameList::supported_file_extensions.contains(file.suffix(), Qt::CaseInsensitive); | ||
| 42 | } | ||
| 43 | |||
| 44 | bool IsExtractedNCAMain(const std::string& file_name) { | ||
| 45 | return QFileInfo(QString::fromStdString(file_name)).fileName() == "main"; | ||
| 46 | } | ||
| 47 | |||
| 48 | QString FormatGameName(const std::string& physical_name) { | ||
| 49 | const QString physical_name_as_qstring = QString::fromStdString(physical_name); | ||
| 50 | const QFileInfo file_info(physical_name_as_qstring); | ||
| 51 | |||
| 52 | if (IsExtractedNCAMain(physical_name)) { | ||
| 53 | return file_info.dir().path(); | ||
| 54 | } | ||
| 55 | |||
| 56 | return physical_name_as_qstring; | ||
| 57 | } | ||
| 58 | |||
| 59 | QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, bool updatable = true) { | ||
| 60 | QString out; | ||
| 61 | for (const auto& kv : patch_manager.GetPatchVersionNames()) { | ||
| 62 | if (!updatable && kv.first == FileSys::PatchType::Update) | ||
| 63 | continue; | ||
| 64 | |||
| 65 | if (kv.second.empty()) { | ||
| 66 | out.append(fmt::format("{}\n", FileSys::FormatPatchTypeName(kv.first)).c_str()); | ||
| 67 | } else { | ||
| 68 | out.append(fmt::format("{} ({})\n", FileSys::FormatPatchTypeName(kv.first), kv.second) | ||
| 69 | .c_str()); | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | out.chop(1); | ||
| 74 | return out; | ||
| 75 | } | ||
| 76 | } // Anonymous namespace | ||
| 77 | |||
| 78 | GameListWorker::GameListWorker( | ||
| 79 | FileSys::VirtualFilesystem vfs, QString dir_path, bool deep_scan, | ||
| 80 | const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list) | ||
| 81 | : vfs(std::move(vfs)), dir_path(std::move(dir_path)), deep_scan(deep_scan), | ||
| 82 | compatibility_list(compatibility_list) {} | ||
| 83 | |||
| 84 | GameListWorker::~GameListWorker() = default; | ||
| 85 | |||
| 86 | void GameListWorker::AddInstalledTitlesToGameList() { | ||
| 87 | const auto cache = Service::FileSystem::GetUnionContents(); | ||
| 88 | const auto installed_games = cache->ListEntriesFilter(FileSys::TitleType::Application, | ||
| 89 | FileSys::ContentRecordType::Program); | ||
| 90 | |||
| 91 | for (const auto& game : installed_games) { | ||
| 92 | const auto& file = cache->GetEntryUnparsed(game); | ||
| 93 | std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(file); | ||
| 94 | if (!loader) | ||
| 95 | continue; | ||
| 96 | |||
| 97 | std::vector<u8> icon; | ||
| 98 | std::string name; | ||
| 99 | u64 program_id = 0; | ||
| 100 | loader->ReadProgramId(program_id); | ||
| 101 | |||
| 102 | const FileSys::PatchManager patch{program_id}; | ||
| 103 | const auto& control = cache->GetEntry(game.title_id, FileSys::ContentRecordType::Control); | ||
| 104 | if (control != nullptr) | ||
| 105 | GetMetadataFromControlNCA(patch, control, icon, name); | ||
| 106 | |||
| 107 | auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); | ||
| 108 | |||
| 109 | // The game list uses this as compatibility number for untested games | ||
| 110 | QString compatibility("99"); | ||
| 111 | if (it != compatibility_list.end()) | ||
| 112 | compatibility = it->second.first; | ||
| 113 | |||
| 114 | emit EntryReady({ | ||
| 115 | new GameListItemPath( | ||
| 116 | FormatGameName(file->GetFullPath()), icon, QString::fromStdString(name), | ||
| 117 | QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), | ||
| 118 | program_id), | ||
| 119 | new GameListItemCompat(compatibility), | ||
| 120 | new GameListItem(FormatPatchNameVersions(patch)), | ||
| 121 | new GameListItem( | ||
| 122 | QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), | ||
| 123 | new GameListItemSize(file->GetSize()), | ||
| 124 | }); | ||
| 125 | } | ||
| 126 | |||
| 127 | const auto control_data = cache->ListEntriesFilter(FileSys::TitleType::Application, | ||
| 128 | FileSys::ContentRecordType::Control); | ||
| 129 | |||
| 130 | for (const auto& entry : control_data) { | ||
| 131 | const auto nca = cache->GetEntry(entry); | ||
| 132 | if (nca != nullptr) | ||
| 133 | nca_control_map.insert_or_assign(entry.title_id, nca); | ||
| 134 | } | ||
| 135 | } | ||
| 136 | |||
| 137 | void GameListWorker::FillControlMap(const std::string& dir_path) { | ||
| 138 | const auto nca_control_callback = [this](u64* num_entries_out, const std::string& directory, | ||
| 139 | const std::string& virtual_name) -> bool { | ||
| 140 | std::string physical_name = directory + DIR_SEP + virtual_name; | ||
| 141 | |||
| 142 | if (stop_processing) | ||
| 143 | return false; // Breaks the callback loop. | ||
| 144 | |||
| 145 | bool is_dir = FileUtil::IsDirectory(physical_name); | ||
| 146 | QFileInfo file_info(physical_name.c_str()); | ||
| 147 | if (!is_dir && file_info.suffix().toStdString() == "nca") { | ||
| 148 | auto nca = | ||
| 149 | std::make_shared<FileSys::NCA>(vfs->OpenFile(physical_name, FileSys::Mode::Read)); | ||
| 150 | if (nca->GetType() == FileSys::NCAContentType::Control) | ||
| 151 | nca_control_map.insert_or_assign(nca->GetTitleId(), nca); | ||
| 152 | } | ||
| 153 | return true; | ||
| 154 | }; | ||
| 155 | |||
| 156 | FileUtil::ForeachDirectoryEntry(nullptr, dir_path, nca_control_callback); | ||
| 157 | } | ||
| 158 | |||
| 159 | void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) { | ||
| 160 | const auto callback = [this, recursion](u64* num_entries_out, const std::string& directory, | ||
| 161 | const std::string& virtual_name) -> bool { | ||
| 162 | std::string physical_name = directory + DIR_SEP + virtual_name; | ||
| 163 | |||
| 164 | if (stop_processing) | ||
| 165 | return false; // Breaks the callback loop. | ||
| 166 | |||
| 167 | bool is_dir = FileUtil::IsDirectory(physical_name); | ||
| 168 | if (!is_dir && | ||
| 169 | (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name))) { | ||
| 170 | std::unique_ptr<Loader::AppLoader> loader = | ||
| 171 | Loader::GetLoader(vfs->OpenFile(physical_name, FileSys::Mode::Read)); | ||
| 172 | if (!loader || ((loader->GetFileType() == Loader::FileType::Unknown || | ||
| 173 | loader->GetFileType() == Loader::FileType::Error) && | ||
| 174 | !UISettings::values.show_unknown)) | ||
| 175 | return true; | ||
| 176 | |||
| 177 | std::vector<u8> icon; | ||
| 178 | const auto res1 = loader->ReadIcon(icon); | ||
| 179 | |||
| 180 | u64 program_id = 0; | ||
| 181 | const auto res2 = loader->ReadProgramId(program_id); | ||
| 182 | |||
| 183 | std::string name = " "; | ||
| 184 | const auto res3 = loader->ReadTitle(name); | ||
| 185 | |||
| 186 | const FileSys::PatchManager patch{program_id}; | ||
| 187 | |||
| 188 | if (res1 != Loader::ResultStatus::Success && res3 != Loader::ResultStatus::Success && | ||
| 189 | res2 == Loader::ResultStatus::Success) { | ||
| 190 | // Use from metadata pool. | ||
| 191 | if (nca_control_map.find(program_id) != nca_control_map.end()) { | ||
| 192 | const auto nca = nca_control_map[program_id]; | ||
| 193 | GetMetadataFromControlNCA(patch, nca, icon, name); | ||
| 194 | } | ||
| 195 | } | ||
| 196 | |||
| 197 | auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); | ||
| 198 | |||
| 199 | // The game list uses this as compatibility number for untested games | ||
| 200 | QString compatibility("99"); | ||
| 201 | if (it != compatibility_list.end()) | ||
| 202 | compatibility = it->second.first; | ||
| 203 | |||
| 204 | emit EntryReady({ | ||
| 205 | new GameListItemPath( | ||
| 206 | FormatGameName(physical_name), icon, QString::fromStdString(name), | ||
| 207 | QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), | ||
| 208 | program_id), | ||
| 209 | new GameListItemCompat(compatibility), | ||
| 210 | new GameListItem(FormatPatchNameVersions(patch, loader->IsRomFSUpdatable())), | ||
| 211 | new GameListItem( | ||
| 212 | QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), | ||
| 213 | new GameListItemSize(FileUtil::GetSize(physical_name)), | ||
| 214 | }); | ||
| 215 | } else if (is_dir && recursion > 0) { | ||
| 216 | watch_list.append(QString::fromStdString(physical_name)); | ||
| 217 | AddFstEntriesToGameList(physical_name, recursion - 1); | ||
| 218 | } | ||
| 219 | |||
| 220 | return true; | ||
| 221 | }; | ||
| 222 | |||
| 223 | FileUtil::ForeachDirectoryEntry(nullptr, dir_path, callback); | ||
| 224 | } | ||
| 225 | |||
| 226 | void GameListWorker::run() { | ||
| 227 | stop_processing = false; | ||
| 228 | watch_list.append(dir_path); | ||
| 229 | FillControlMap(dir_path.toStdString()); | ||
| 230 | AddInstalledTitlesToGameList(); | ||
| 231 | AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0); | ||
| 232 | nca_control_map.clear(); | ||
| 233 | emit Finished(watch_list); | ||
| 234 | } | ||
| 235 | |||
| 236 | void GameListWorker::Cancel() { | ||
| 237 | this->disconnect(); | ||
| 238 | stop_processing = true; | ||
| 239 | } | ||
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 @@ | |||
| 1 | // Copyright 2018 yuzu emulator team | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include <atomic> | ||
| 8 | #include <map> | ||
| 9 | #include <memory> | ||
| 10 | #include <string> | ||
| 11 | #include <unordered_map> | ||
| 12 | |||
| 13 | #include <QList> | ||
| 14 | #include <QObject> | ||
| 15 | #include <QRunnable> | ||
| 16 | #include <QString> | ||
| 17 | |||
| 18 | #include "common/common_types.h" | ||
| 19 | |||
| 20 | class QStandardItem; | ||
| 21 | |||
| 22 | namespace FileSys { | ||
| 23 | class NCA; | ||
| 24 | class VfsFilesystem; | ||
| 25 | } // namespace FileSys | ||
| 26 | |||
| 27 | /** | ||
| 28 | * Asynchronous worker object for populating the game list. | ||
| 29 | * Communicates with other threads through Qt's signal/slot system. | ||
| 30 | */ | ||
| 31 | class GameListWorker : public QObject, public QRunnable { | ||
| 32 | Q_OBJECT | ||
| 33 | |||
| 34 | public: | ||
| 35 | GameListWorker( | ||
| 36 | std::shared_ptr<FileSys::VfsFilesystem> vfs, QString dir_path, bool deep_scan, | ||
| 37 | const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list); | ||
| 38 | ~GameListWorker() override; | ||
| 39 | |||
| 40 | /// Starts the processing of directory tree information. | ||
| 41 | void run() override; | ||
| 42 | |||
| 43 | /// Tells the worker that it should no longer continue processing. Thread-safe. | ||
| 44 | void Cancel(); | ||
| 45 | |||
| 46 | signals: | ||
| 47 | /** | ||
| 48 | * The `EntryReady` signal is emitted once an entry has been prepared and is ready | ||
| 49 | * to be added to the game list. | ||
| 50 | * @param entry_items a list with `QStandardItem`s that make up the columns of the new entry. | ||
| 51 | */ | ||
| 52 | void EntryReady(QList<QStandardItem*> entry_items); | ||
| 53 | |||
| 54 | /** | ||
| 55 | * After the worker has traversed the game directory looking for entries, this signal is emitted | ||
| 56 | * with a list of folders that should be watched for changes as well. | ||
| 57 | */ | ||
| 58 | void Finished(QStringList watch_list); | ||
| 59 | |||
| 60 | private: | ||
| 61 | void AddInstalledTitlesToGameList(); | ||
| 62 | void FillControlMap(const std::string& dir_path); | ||
| 63 | void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0); | ||
| 64 | |||
| 65 | std::shared_ptr<FileSys::VfsFilesystem> vfs; | ||
| 66 | std::map<u64, std::shared_ptr<FileSys::NCA>> nca_control_map; | ||
| 67 | QStringList watch_list; | ||
| 68 | QString dir_path; | ||
| 69 | bool deep_scan; | ||
| 70 | const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list; | ||
| 71 | std::atomic_bool stop_processing; | ||
| 72 | }; | ||