diff --git a/src/citron/configuration/configure_filesystem.cpp b/src/citron/configuration/configure_filesystem.cpp index f2d61681b..bab26c910 100644 --- a/src/citron/configuration/configure_filesystem.cpp +++ b/src/citron/configuration/configure_filesystem.cpp @@ -72,6 +72,9 @@ void ConfigureFilesystem::SetConfiguration() { ui->cache_game_list->setChecked(UISettings::values.cache_game_list.GetValue()); ui->prompt_for_autoloader->setChecked(UISettings::values.prompt_for_autoloader.GetValue()); + // NCA Scanning Toggle + ui->scan_nca->setChecked(UISettings::values.scan_nca.GetValue()); + #ifdef __linux__ ui->enable_backups_checkbox->setChecked(UISettings::values.updater_enable_backups.GetValue()); const std::string& backup_path = UISettings::values.updater_backup_path.GetValue(); @@ -100,6 +103,9 @@ void ConfigureFilesystem::ApplyConfiguration() { UISettings::values.cache_game_list = ui->cache_game_list->isChecked(); UISettings::values.prompt_for_autoloader = ui->prompt_for_autoloader->isChecked(); + // NCA Scanning Toggle + UISettings::values.scan_nca = ui->scan_nca->isChecked(); + #ifdef __linux__ UISettings::values.updater_enable_backups = ui->enable_backups_checkbox->isChecked(); const bool new_custom_backup_enabled = ui->custom_backup_location_checkbox->isChecked(); diff --git a/src/citron/configuration/configure_filesystem.ui b/src/citron/configuration/configure_filesystem.ui index 3fe6b8fcf..18d085c9a 100644 --- a/src/citron/configuration/configure_filesystem.ui +++ b/src/citron/configuration/configure_filesystem.ui @@ -7,294 +7,90 @@ 0 0 453 - 561 + 650 Form - - Filesystem - - - - - - Storage Directories - - - - - - NAND - - - - - - - ... - - - - - - - - - - - - - SD Card - - - - - - - ... - - - - - - - Qt::Horizontal - - - QSizePolicy::Maximum - - - - 60 - 20 - - - - - - - - - - - Gamecard - - - - - - Path - - - - - - - - - - Inserted - - - - - - - Current Game - - - - - - - ... - - - - - - - - - - Patch Manager - - - - - - - - - - - - ... - - - - - - - ... - - - - - - - - - Dump Decompressed NSOs - - - - - - - Dump ExeFS - - - - - - - - - Mod Load Root - - - - - - - Dump Root - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - - - - Autoloader - - - - - - Prompt to run Autoloader when a new game directory is added - - - - - - - Run Autoloader Now - - - - - - - - - - Updater - - - - - - Enable AppImage Backups - - - - - - - Use Custom Backup Location for AppImage Updates - - - - - - - - - - ... - - - - - - - - - - Caching - - - - - - - - Cache Game List Metadata - - - - - - - Reset Metadata Cache - - - - - - - - - + + + Storage Directories + + + NAND + + ... + SD Card + + ... + + - - - Qt::Vertical - - - - 20 - 40 - - - + + Gamecard + + Inserted + Current Game + Path + + ... + + + + + Patch Manager + + Dump Root + + ... + Mod Load Root + + ... + + + Dump Decompressed NSOs + Dump ExeFS + + + + + + + + Autoloader + + Prompt to run Autoloader when a new game directory is added + Run Autoloader Now + + + + + + Updater + + Enable AppImage Backups + Use Custom Backup Location + + ... + + + + + + Caching & Scanning + + Cache Game List Metadata + Reset Metadata Cache + Scan for .nca files (Advanced: Significantly slows down scanning) + + + + Qt::Vertical - - diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp index e5ad53c22..7deb9c445 100644 --- a/src/citron/game_list.cpp +++ b/src/citron/game_list.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -660,6 +661,16 @@ play_time_manager{play_time_manager_}, system{system_} { )); connect(btn_sort_az, &QToolButton::clicked, this, &GameList::ToggleSortOrder); + // Create progress bar + progress_bar = new QProgressBar(this); + progress_bar->setVisible(false); + progress_bar->setFixedHeight(4); + progress_bar->setTextVisible(false); + progress_bar->setStyleSheet(QStringLiteral( + "QProgressBar { border: none; background: transparent; } " + "QProgressBar::chunk { background-color: #0078d4; }" + )); + // Add widgets to toolbar toolbar_layout->addWidget(btn_list_view); toolbar_layout->addWidget(btn_grid_view); @@ -671,6 +682,7 @@ play_time_manager{play_time_manager_}, system{system_} { layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); layout->addWidget(toolbar); + layout->addWidget(progress_bar); layout->addWidget(tree_view); layout->addWidget(list_view); setLayout(layout); @@ -749,16 +761,21 @@ void GameList::UpdateOnlineStatus() { // Run the blocking network call in a background thread using QtConcurrent QFuture>> future = QtConcurrent::run([session]() { - std::map> stats; - AnnounceMultiplayerRoom::RoomList room_list = session->GetRoomList(); - for (const auto& room : room_list) { - u64 game_id = room.information.preferred_game.id; - if (game_id != 0) { - stats[game_id].first += room.members.size(); - stats[game_id].second++; + try { + std::map> stats; + AnnounceMultiplayerRoom::RoomList room_list = session->GetRoomList(); + for (const auto& room : room_list) { + u64 game_id = room.information.preferred_game.id; + if (game_id != 0) { + stats[game_id].first += (int)room.members.size(); + stats[game_id].second++; + } } + return stats; + } catch (const std::exception& e) { + LOG_ERROR(Frontend, "Exception in Online Status thread: {}", e.what()); + return std::map>{}; } - return stats; }); online_status_watcher->setFuture(future); @@ -859,6 +876,9 @@ bool GameList::IsEmpty() const { } void GameList::DonePopulating(const QStringList& watch_list) { + if (progress_bar) { + progress_bar->setVisible(false); + } emit ShowList(!IsEmpty()); item_model->invisibleRootItem()->appendRow(new GameListAddDir()); item_model->invisibleRootItem()->insertRow(0, new GameListFavorites()); @@ -1283,7 +1303,9 @@ QStandardItemModel* GameList::GetModel() const { } void GameList::PopulateAsync(QVector& game_dirs) { + UpdateProgressBarColor(); tree_view->setEnabled(false); + emit ShowList(true); tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons); tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat); tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_types); @@ -1293,8 +1315,19 @@ void GameList::PopulateAsync(QVector& game_dirs) { current_worker.reset(); item_model->removeRows(0, item_model->rowCount()); search_field->clear(); + + if (progress_bar) { + progress_bar->setValue(0); + progress_bar->setVisible(true); + } + current_worker = std::make_unique(vfs, provider, game_dirs, compatibility_list, play_time_manager, system, main_window->GetMultiplayerState()->GetSession()); connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameList::WorkerEvent, Qt::QueuedConnection); + + if (progress_bar) { + connect(current_worker.get(), &GameListWorker::ProgressUpdated, progress_bar, &QProgressBar::setValue, Qt::QueuedConnection); + } + QThreadPool::globalInstance()->start(current_worker.get()); } @@ -1312,15 +1345,16 @@ void GameList::LoadInterfaceLayout() { } const QStringList GameList::supported_file_extensions = { - QStringLiteral("nso"), QStringLiteral("nro"), QStringLiteral("nca"), - QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")}; + QStringLiteral("xci"), QStringLiteral("nsp"), + QStringLiteral("nso"), QStringLiteral("nro"), QStringLiteral("kip") +}; - void GameList::RefreshGameDirectory() { - if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) { - LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); - PopulateAsync(UISettings::values.game_dirs); - } +void GameList::RefreshGameDirectory() { + if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) { + LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); + PopulateAsync(UISettings::values.game_dirs); } +} void GameList::ToggleFavorite(u64 program_id) { if (!UISettings::values.favorited_ids.contains(program_id)) { @@ -1588,3 +1622,26 @@ const QStringList GameList::supported_file_extensions = { } btn_sort_az->setIcon(sort_icon); } + +void GameList::UpdateProgressBarColor() { + if (!progress_bar) return; + + // Convert the Hex String from settings to a QColor + QColor accent(QString::fromStdString(UISettings::values.accent_color.GetValue())); + + if (UISettings::values.enable_rainbow_mode.GetValue()) { + progress_bar->setStyleSheet(QStringLiteral( + "QProgressBar { border: none; background: transparent; } " + "QProgressBar::chunk { " + "background: qlineargradient(x1:0, y1:0, x2:1, y2:0, " + "stop:0 #ff0000, stop:0.16 #ffff00, stop:0.33 #00ff00, " + "stop:0.5 #00ffff, stop:0.66 #0000ff, stop:0.83 #ff00ff, stop:1 #ff0000); " + "}" + )); + } else { + progress_bar->setStyleSheet(QStringLiteral( + "QProgressBar { border: none; background: transparent; } " + "QProgressBar::chunk { background-color: %1; }" + ).arg(accent.name())); + } +} diff --git a/src/citron/game_list.h b/src/citron/game_list.h index 06554d939..7438a6e78 100644 --- a/src/citron/game_list.h +++ b/src/citron/game_list.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -146,6 +147,7 @@ public slots: void OnConfigurationChanged(); private slots: + void UpdateProgressBarColor(); void OnItemExpanded(const QModelIndex& item); void OnTextChanged(const QString& new_text); void OnFilterCloseClicked(); @@ -201,6 +203,7 @@ private: QListView* list_view = nullptr; QStandardItemModel* item_model = nullptr; std::unique_ptr current_worker; + QProgressBar* progress_bar = nullptr; QFileSystemWatcher* watcher = nullptr; ControllerNavigation* controller_navigation = nullptr; CompatibilityList compatibility_list; diff --git a/src/citron/game_list_worker.cpp b/src/citron/game_list_worker.cpp index 51a7085f4..b97225258 100644 --- a/src/citron/game_list_worker.cpp +++ b/src/citron/game_list_worker.cpp @@ -349,7 +349,19 @@ void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, const 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); + const QString suffix = file.suffix().toLower(); + + // 1. Check if it's a standard game container (.nsp, .xci, etc.) + if (GameList::supported_file_extensions.contains(suffix)) { + return true; + } + + // 2. Only allow .nca if the user explicitly enabled it in the UI Settings + if (suffix == QStringLiteral("nca") && UISettings::values.scan_nca.GetValue()) { + return true; + } + + return false; } bool IsExtractedNCAMain(const std::string& file_name) { @@ -551,145 +563,116 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir, const std::map } void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan, - GameListDir* parent_dir, const std::map>& online_stats) { - const auto callback = [this, target, parent_dir, &online_stats](const std::filesystem::path& path) -> bool { - if (stop_requested) { - // Breaks the callback loop. - return false; - } + GameListDir* parent_dir, const std::map>& online_stats, + int& processed_files, int total_files) { + const auto callback = [this, target, parent_dir, &online_stats, &processed_files, total_files](const std::filesystem::path& path) -> bool { + if (stop_requested) return false; const auto physical_name = Common::FS::PathToUTF8String(path); - const auto is_dir = Common::FS::IsDir(path); - if (!is_dir && - (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name))) { - // Try to get cached metadata first - const auto* cached_metadata = GetCachedGameMetadata(physical_name); - - const auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read); - if (!file) { - return true; - } - - auto loader = Loader::GetLoader(system, file); - if (!loader) { - return true; - } - - const auto file_type = loader->GetFileType(); - if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) { - return true; - } - - u64 program_id = 0; - const auto res2 = loader->ReadProgramId(program_id); - - if (target == ScanTarget::FillManualContentProvider) { - if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) { - provider->AddEntry(FileSys::TitleType::Application, - FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), - program_id, file); - } else if (res2 == Loader::ResultStatus::Success && - (file_type == Loader::FileType::XCI || - file_type == Loader::FileType::NSP)) { - const auto nsp = file_type == Loader::FileType::NSP - ? std::make_shared(file) - : FileSys::XCI{file}.GetSecurePartitionNSP(); - for (const auto& title : nsp->GetNCAs()) { - for (const auto& entry : title.second) { - provider->AddEntry(entry.first.first, entry.first.second, title.first, - entry.second->GetBaseFile()); - } - } - } - } else { - std::vector program_ids; - loader->ReadProgramIds(program_ids); - - if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 && - (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) { - for (const auto id : program_ids) { - loader = Loader::GetLoader(system, file, id); - if (!loader) { - continue; - } - - std::vector icon; - [[maybe_unused]] const auto res1 = loader->ReadIcon(icon); - - std::string name = " "; - [[maybe_unused]] const auto res3 = loader->ReadTitle(name); - - const FileSys::PatchManager patch{id, system.GetFileSystemController(), - system.GetContentProvider()}; - - auto entry = MakeGameListEntry( - physical_name, name, Common::FS::GetSize(physical_name), icon, *loader, - id, compatibility_list, play_time_manager, patch, online_stats); - - RecordEvent( - [=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); }); - } - } else { - // Use cached metadata if available, otherwise read from file - std::vector icon; - std::string name = " "; - std::size_t file_size = 0; - - if (cached_metadata && cached_metadata->program_id == program_id) { - // Use cached data - icon = cached_metadata->icon; - name = cached_metadata->title; - file_size = cached_metadata->file_size; - } else { - // Read from file - [[maybe_unused]] const auto res1 = loader->ReadIcon(icon); - [[maybe_unused]] const auto res3 = loader->ReadTitle(name); - file_size = Common::FS::GetSize(physical_name); - - // Cache it for next time - if (res2 == Loader::ResultStatus::Success) { - CacheGameMetadata(physical_name, program_id, file_type, file_size, name, icon); - } - } - - const FileSys::PatchManager patch{program_id, system.GetFileSystemController(), - system.GetContentProvider()}; - - auto entry = MakeGameListEntry( - physical_name, name, file_size, icon, *loader, - program_id, compatibility_list, play_time_manager, patch, online_stats); - - RecordEvent( - [=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); }); - } - } - } else if (is_dir) { - watch_list.append(QString::fromStdString(physical_name)); + if (physical_name.find("/nand/") != std::string::npos || + physical_name.find("\\nand\\") != std::string::npos || + physical_name.find("/registered/") != std::string::npos || + physical_name.find("\\registered\\") != std::string::npos) { + return true; } + if (!HasSupportedFileExtension(physical_name) && !IsExtractedNCAMain(physical_name)) { + return true; + } + + // Cache Check + const auto* cached = GetCachedGameMetadata(physical_name); + if (cached && cached->IsValid() && (target == ScanTarget::PopulateGameList || target == ScanTarget::Both)) { + if ((cached->program_id & 0xFFF) == 0) { + const FileSys::PatchManager patch{cached->program_id, system.GetFileSystemController(), system.GetContentProvider()}; + auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read); + if (file) { + auto loader = Loader::GetLoader(system, file); + if (loader) { + auto entry = MakeGameListEntry(physical_name, cached->title, cached->file_size, cached->icon, *loader, + cached->program_id, compatibility_list, play_time_manager, patch, online_stats); + RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); }); + } + } + } + processed_files++; + emit ProgressUpdated(std::min(100, (processed_files * 100) / total_files)); + return true; + } + + // Full Scan + const auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read); + if (!file) { + processed_files++; + return true; + } + + auto loader = Loader::GetLoader(system, file); + if (!loader) { + processed_files++; + return true; + } + + u64 program_id = 0; + const auto res2 = loader->ReadProgramId(program_id); + const auto file_type = loader->GetFileType(); + + if (res2 == Loader::ResultStatus::Success && program_id != 0) { + + if (target == ScanTarget::FillManualContentProvider || target == ScanTarget::Both) { + if (file_type == Loader::FileType::NCA) { + provider->AddEntry(FileSys::TitleType::Application, FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), program_id, file); + } else if (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP) { + const auto nsp = file_type == Loader::FileType::NSP ? std::make_shared(file) : FileSys::XCI{file}.GetSecurePartitionNSP(); + for (const auto& title : nsp->GetNCAs()) { + for (const auto& entry : title.second) { + provider->AddEntry(entry.first.first, entry.first.second, title.first, entry.second->GetBaseFile()); + } + } + } + } + + if (target == ScanTarget::PopulateGameList || target == ScanTarget::Both) { + // 3. FILTER UPDATES: Only add to UI if it's a Base Game (ID ends in 000) + if ((program_id & 0xFFF) == 0) { + std::vector icon; + std::string name = " "; + loader->ReadIcon(icon); + loader->ReadTitle(name); + std::size_t file_size = Common::FS::GetSize(physical_name); + + CacheGameMetadata(physical_name, program_id, file_type, file_size, name, icon); + + const FileSys::PatchManager patch{program_id, system.GetFileSystemController(), system.GetContentProvider()}; + auto entry = MakeGameListEntry(physical_name, name, file_size, icon, *loader, program_id, compatibility_list, play_time_manager, patch, online_stats); + RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); }); + } + } + } + + processed_files++; + emit ProgressUpdated(std::min(100, (processed_files * 100) / total_files)); return true; }; if (deep_scan) { - Common::FS::IterateDirEntriesRecursively(dir_path, callback, - Common::FS::DirEntryFilter::All); + Common::FS::IterateDirEntriesRecursively(dir_path, callback, Common::FS::DirEntryFilter::File); } else { Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::File); } } void GameListWorker::run() { - // Load cached game metadata at the start LoadGameMetadataCache(); - std::map> online_stats; // Game ID -> {player_count, server_count} + std::map> online_stats; if (session) { AnnounceMultiplayerRoom::RoomList room_list = session->GetRoomList(); for (const auto& room : room_list) { u64 game_id = room.information.preferred_game.id; if (game_id != 0) { - online_stats[game_id].first += room.members.size(); + online_stats[game_id].first += (int)room.members.size(); online_stats[game_id].second++; } } @@ -698,42 +681,46 @@ void GameListWorker::run() { watch_list.clear(); provider->ClearAllEntries(); + int total_files = 0; + int processed_files = 0; + + for (const auto& game_dir : game_dirs) { + if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") continue; + + auto count_callback = [&](const std::filesystem::path& path) -> bool { + const std::string physical_name = Common::FS::PathToUTF8String(path); + if (HasSupportedFileExtension(physical_name)) { + total_files++; + } + return true; + }; + + if (game_dir.deep_scan) { + Common::FS::IterateDirEntriesRecursively(game_dir.path, count_callback, Common::FS::DirEntryFilter::File); + } else { + Common::FS::IterateDirEntries(game_dir.path, count_callback, Common::FS::DirEntryFilter::File); + } + } + + if (total_files <= 0) total_files = 1; + const auto DirEntryReady = [&](GameListDir* game_list_dir) { RecordEvent([=](GameList* game_list) { game_list->AddDirEntry(game_list_dir); }); }; for (UISettings::GameDir& game_dir : game_dirs) { - if (stop_requested) { - break; - } + if (stop_requested) break; - if (game_dir.path == std::string("SDMC")) { - auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir); - DirEntryReady(game_list_dir); - AddTitlesToGameList(game_list_dir, online_stats); - } else if (game_dir.path == std::string("UserNAND")) { - auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir); - DirEntryReady(game_list_dir); - AddTitlesToGameList(game_list_dir, online_stats); - } else if (game_dir.path == std::string("SysNAND")) { - auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir); - DirEntryReady(game_list_dir); - AddTitlesToGameList(game_list_dir, online_stats); - } else { - watch_list.append(QString::fromStdString(game_dir.path)); - auto* const game_list_dir = new GameListDir(game_dir); - DirEntryReady(game_list_dir); - ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path, game_dir.deep_scan, - game_list_dir, online_stats); - ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path, game_dir.deep_scan, - game_list_dir, online_stats); - } + if (game_dir.path == "SDMC" || game_dir.path == "UserNAND" || game_dir.path == "SysNAND") continue; + + watch_list.append(QString::fromStdString(game_dir.path)); + auto* const game_list_dir = new GameListDir(game_dir); + DirEntryReady(game_list_dir); + + ScanFileSystem(ScanTarget::Both, game_dir.path, game_dir.deep_scan, game_list_dir, online_stats, processed_files, total_files); } RecordEvent([this](GameList* game_list) { game_list->DonePopulating(watch_list); }); - - // Save cached game metadata at the end SaveGameMetadataCache(); - processing_completed.Set(); } diff --git a/src/citron/game_list_worker.h b/src/citron/game_list_worker.h index 723bbeb51..8ad673eeb 100644 --- a/src/citron/game_list_worker.h +++ b/src/citron/game_list_worker.h @@ -47,6 +47,7 @@ public: enum class ScanTarget { FillManualContentProvider, PopulateGameList, + Both, }; explicit GameListWorker(std::shared_ptr vfs_, @@ -73,6 +74,7 @@ public: signals: void DataAvailable(); + void ProgressUpdated(int percent); private: template @@ -84,7 +86,8 @@ private: void ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan, GameListDir* parent_dir, - const std::map>& online_stats); + const std::map>& online_stats, + int& processed_files, int total_files); std::shared_ptr vfs; FileSys::ManualContentProvider* provider; diff --git a/src/citron/main.cpp b/src/citron/main.cpp index b0692c9b7..e1458fe59 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -2272,11 +2272,9 @@ void GMainWindow::OnEmulationStopped() { render_window->hide(); loading_screen->hide(); loading_screen->Clear(); - if (game_list->IsEmpty()) { - game_list_placeholder->show(); - } else { - game_list->show(); - } + + game_list->show(); + game_list_placeholder->hide(); game_list->SetFilterFocus(); tas_label->clear(); input_subsystem->GetTas()->Stop(); diff --git a/src/citron/uisettings.h b/src/citron/uisettings.h index 36b615ebe..6aece0855 100644 --- a/src/citron/uisettings.h +++ b/src/citron/uisettings.h @@ -219,6 +219,7 @@ namespace UISettings { Setting game_list_grid_view{linkage, false, "game_list_grid_view", Category::UiGameList}; std::atomic_bool is_game_list_reload_pending{false}; Setting cache_game_list{linkage, true, "cache_game_list", Category::UiGameList}; + Setting scan_nca{linkage, false, "scan_nca", Category::UiGameList}; Setting prompt_for_autoloader{linkage, true, "prompt_for_autoloader", Category::UiGameList}; Setting favorites_expanded{linkage, true, "favorites_expanded", Category::UiGameList}; QVector favorited_ids; diff --git a/src/common/fs/fs.cpp b/src/common/fs/fs.cpp index 174aed49b..71ac8c27a 100644 --- a/src/common/fs/fs.cpp +++ b/src/common/fs/fs.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/fs/file.h" @@ -324,7 +325,6 @@ bool RemoveDirContentsRecursively(const fs::path& path) { std::error_code ec; - // TODO (Morph): Replace this with recursive_directory_iterator once it's fixed in MSVC. for (const auto& entry : fs::directory_iterator(path, ec)) { if (ec) { LOG_ERROR(Common_Filesystem, @@ -342,10 +342,11 @@ bool RemoveDirContentsRecursively(const fs::path& path) { break; } - // TODO (Morph): Remove this when MSVC fixes recursive_directory_iterator. - // recursive_directory_iterator throws an exception despite passing in a std::error_code. - if (entry.status().type() == fs::file_type::directory) { - return RemoveDirContentsRecursively(entry.path()); + std::error_code status_ec; + if (entry.status(status_ec).type() == fs::file_type::directory) { + if (!RemoveDirContentsRecursively(entry.path())) { + return false; + } } } @@ -434,8 +435,12 @@ void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable break; } + std::error_code status_ec; + const auto st = entry.status(status_ec); + if (status_ec) continue; + if (True(filter & DirEntryFilter::File) && - entry.status().type() == fs::file_type::regular) { + st.type() == fs::file_type::regular) { if (!callback(entry)) { callback_error = true; break; @@ -443,7 +448,7 @@ void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable } if (True(filter & DirEntryFilter::Directory) && - entry.status().type() == fs::file_type::directory) { + st.type() == fs::file_type::directory) { if (!callback(entry)) { callback_error = true; break; @@ -462,6 +467,42 @@ void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable PathToUTF8String(path)); } +void IterateDirEntriesRecursivelyInternal(const std::filesystem::path& path, + const DirEntryCallable& callback, DirEntryFilter filter, + int depth) { + if (depth > 12) return; + + std::error_code ec; + auto it = fs::directory_iterator(path, ec); + if (ec) return; + + while (it != fs::directory_iterator() && !ec) { + const auto& entry = *it; + +#ifndef _WIN32 + const std::string filename = entry.path().filename().string(); + if (filename[0] == '$' || filename == "Windows" || filename == "Program Files" || + filename == "Program Files (x86)" || filename == "System Volume Information" || + filename == "ProgramData" || filename == "Application Data" || + filename == "Users" || filename == "SteamLibrary") { + it.increment(ec); + continue; + } +#endif + + std::error_code status_ec; + if (entry.is_directory(status_ec)) { + if (True(filter & DirEntryFilter::Directory)) { if (!callback(entry)) break; } + IterateDirEntriesRecursivelyInternal(entry.path(), callback, filter, depth + 1); + } else { + if (True(filter & DirEntryFilter::File)) { if (!callback(entry)) break; } + } + + it.increment(ec); + if (ec) { ec.clear(); break; } + } +} + void IterateDirEntriesRecursively(const std::filesystem::path& path, const DirEntryCallable& callback, DirEntryFilter filter) { if (!ValidatePath(path)) { @@ -469,59 +510,10 @@ void IterateDirEntriesRecursively(const std::filesystem::path& path, return; } - if (!Exists(path)) { - LOG_ERROR(Common_Filesystem, "Filesystem object at path={} does not exist", - PathToUTF8String(path)); - return; - } + // Start the recursion at depth 0 + IterateDirEntriesRecursivelyInternal(path, callback, filter, 0); - if (!IsDir(path)) { - LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a directory", - PathToUTF8String(path)); - return; - } - - bool callback_error = false; - - std::error_code ec; - - // TODO (Morph): Replace this with recursive_directory_iterator once it's fixed in MSVC. - for (const auto& entry : fs::directory_iterator(path, ec)) { - if (ec) { - break; - } - - if (True(filter & DirEntryFilter::File) && - entry.status().type() == fs::file_type::regular) { - if (!callback(entry)) { - callback_error = true; - break; - } - } - - if (True(filter & DirEntryFilter::Directory) && - entry.status().type() == fs::file_type::directory) { - if (!callback(entry)) { - callback_error = true; - break; - } - } - - // TODO (Morph): Remove this when MSVC fixes recursive_directory_iterator. - // recursive_directory_iterator throws an exception despite passing in a std::error_code. - if (entry.status().type() == fs::file_type::directory) { - IterateDirEntriesRecursively(entry.path(), callback, filter); - } - } - - if (callback_error || ec) { - LOG_ERROR(Common_Filesystem, - "Failed to visit all the directory entries of path={}, ec_message={}", - PathToUTF8String(path), ec.message()); - return; - } - - LOG_DEBUG(Common_Filesystem, "Successfully visited all the directory entries of path={}", + LOG_DEBUG(Common_Filesystem, "Finished visiting directory entries of path={}", PathToUTF8String(path)); } diff --git a/src/common/fs/fs.h b/src/common/fs/fs.h index ce3eb309a..42b66ea8d 100644 --- a/src/common/fs/fs.h +++ b/src/common/fs/fs.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -399,6 +400,10 @@ void IterateDirEntriesRecursively(const std::filesystem::path& path, const DirEntryCallable& callback, DirEntryFilter filter = DirEntryFilter::All); +void IterateDirEntriesRecursivelyInternal(const std::filesystem::path& path, + const DirEntryCallable& callback, + DirEntryFilter filter, int depth); + #ifdef _WIN32 template void IterateDirEntriesRecursively(const Path& path, const DirEntryCallable& callback,