diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 1735ff7b96..fd9394ff69 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -3717,8 +3717,9 @@ game_boot_result Emulator::AddGameToYml(const std::string& path) bool Emulator::IsPathInsideDir(std::string_view path, std::string_view dir) const { const std::string dir_path = GetCallbacks().resolve_path(dir); + const std::string resolved_path = GetCallbacks().resolve_path(path); - return !dir_path.empty() && (GetCallbacks().resolve_path(path) + '/').starts_with((dir_path.back() == '/') ? dir_path : (dir_path + '/')); + return !dir_path.empty() && !resolved_path.empty() && (resolved_path + '/').starts_with((dir_path.back() == '/') ? dir_path : (dir_path + '/')); } game_boot_result Emulator::VerifyPathCasing( diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 3d1691e3e1..27d87fd66b 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -813,6 +813,11 @@ void game_list_frame::OnRefreshFinished() m_progress_dialog->deleteLater(); m_progress_dialog = nullptr; } + + // Emit signal and remove slots + Q_EMIT Refreshed(); + m_refresh_funcs_manage_type.reset(); + m_refresh_funcs_manage_type.emplace(); } void game_list_frame::OnCompatFinished() @@ -1923,11 +1928,11 @@ void game_list_frame::RemoveHDD1Cache(const std::string& base_dir, const std::st game_list_log.fatal("Only %d/%d HDD1 cache directories could be removed in %s (%s)", dirs_removed, dirs_total, base_dir, title_id); } -void game_list_frame::BatchCreateCPUCaches() +void game_list_frame::BatchCreateCPUCaches(const QList& game_data) { const std::string vsh_path = g_cfg_vfs.get_dev_flash() + "vsh/module/"; - const bool vsh_exists = fs::is_file(vsh_path + "vsh.self"); - const u32 total = m_game_data.size() + (vsh_exists ? 1 : 0); + const bool vsh_exists = game_data.isEmpty() && fs::is_file(vsh_path + "vsh.self"); + const u32 total = !game_data.isEmpty() ? game_data.size() : (m_game_data.size() + (vsh_exists ? 1 : 0)); if (total == 0) { @@ -1975,7 +1980,7 @@ void game_list_frame::BatchCreateCPUCaches() } } - for (const auto& game : m_game_data) + for (const auto& game : (game_data.isEmpty() ? m_game_data : game_data)) { if (pdlg->wasCanceled() || g_system_progress_canceled) { diff --git a/rpcs3/rpcs3qt/game_list_frame.h b/rpcs3/rpcs3qt/game_list_frame.h index ecc7e106c7..c786768cae 100644 --- a/rpcs3/rpcs3qt/game_list_frame.h +++ b/rpcs3/rpcs3qt/game_list_frame.h @@ -6,6 +6,7 @@ #include "shortcut_utils.h" #include "Utilities/lockless.h" #include "Utilities/mutex.h" +#include "util/auto_typemap.hpp" #include "Emu/config_mode.h" #include @@ -17,6 +18,7 @@ #include #include +#include #include class game_list_table; @@ -63,7 +65,7 @@ public: bool IsEntryVisible(const game_info& game, bool search_fallback = false) const; public Q_SLOTS: - void BatchCreateCPUCaches(); + void BatchCreateCPUCaches(const QList& game_data = QList{}); void BatchRemovePPUCaches(); void BatchRemoveSPUCaches(); void BatchRemoveCustomConfigurations(); @@ -92,6 +94,31 @@ Q_SIGNALS: void RequestIconSizeChange(const int& val); void NotifyEmuSettingsChange(); void FocusToSearchBar(); + void Refreshed(); + +public: + + template + struct GameIdsTable + { + // List of Game IDS an operation has been done on for the use of the slot function + std::set m_done_ids; + }; + + template + void AddRefreshedSlot(Func&& func) + { + if (!m_refresh_funcs_manage_type.has_value()) + { + m_refresh_funcs_manage_type.emplace(); + } + + connect(this, &game_list_frame::Refreshed, this, [this, func = std::move(func)]() mutable + { + func(m_refresh_funcs_manage_type->get>().m_done_ids); + }, Qt::SingleShotConnection); + } + protected: /** Override inherited method from Qt to allow signalling when close happened.*/ void closeEvent(QCloseEvent* event) override; @@ -168,6 +195,7 @@ private: const std::array m_parsing_threads{0}; QFutureWatcher m_parsing_watcher; QFutureWatcher m_refresh_watcher; + usz m_refresh_counter = 0; QSet m_hidden_list; bool m_show_hidden{false}; @@ -185,4 +213,5 @@ private: bool m_draw_compat_status_to_grid = false; bool m_show_custom_icons = true; bool m_play_hover_movies = true; + std::optional> m_refresh_funcs_manage_type; }; diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index f5ca6984fc..f77bf21030 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -39,6 +39,7 @@ #include #include +#include #include #include @@ -1046,7 +1047,8 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo if (success) { pdlg.SetValue(pdlg.maximum()); - std::this_thread::sleep_for(100ms); + + const u64 start_time = get_system_time(); for (usz i = 0; i < packages.size(); i++) { @@ -1081,8 +1083,6 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo } } - m_game_list_frame->Refresh(true); - std::map bootable_paths_installed; // -> title id for (usz index = 0; index < bootable_paths.size(); index++) @@ -1095,80 +1095,44 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo bootable_paths_installed[bootable_paths[index]] = packages[index].title_id; } - pdlg.hide(); + const bool installed_a_whole_package_without_new_software = bootable_paths_installed.empty() && !cancelled; - if (!cancelled || !bootable_paths_installed.empty()) + if (!bootable_paths_installed.empty()) { - if (bootable_paths_installed.empty()) + m_game_list_frame->AddRefreshedSlot([this, paths = std::move(bootable_paths_installed)](std::set& IDs) mutable { - m_gui_settings->ShowInfoBox(tr("Success!"), tr("Successfully installed software from package(s)!"), gui::ib_pkg_success, this); - return true; - } - - auto dlg = new QDialog(this); - dlg->setWindowTitle(tr("Success!")); - - QVBoxLayout* vlayout = new QVBoxLayout(dlg); - - QCheckBox* desk_check = new QCheckBox(tr("Add desktop shortcut(s)")); -#ifdef _WIN32 - QCheckBox* quick_check = new QCheckBox(tr("Add Start menu shortcut(s)")); -#elif defined(__APPLE__) - QCheckBox* quick_check = new QCheckBox(tr("Add dock shortcut(s)")); -#else - QCheckBox* quick_check = new QCheckBox(tr("Add launcher shortcut(s)")); -#endif - QLabel* label = new QLabel(tr("Successfully installed software from package(s)!\nWould you like to install shortcuts to the installed software? (%1 new software detected)\n\n").arg(bootable_paths_installed.size()), dlg); - - vlayout->addWidget(label); - vlayout->addStretch(10); - vlayout->addWidget(desk_check); - vlayout->addStretch(3); - vlayout->addWidget(quick_check); - vlayout->addStretch(3); - - QDialogButtonBox* btn_box = new QDialogButtonBox(QDialogButtonBox::Ok); - - vlayout->addWidget(btn_box); - dlg->setLayout(vlayout); - - bool create_desktop_shortcuts = false; - bool create_app_shortcut = false; - - connect(btn_box, &QDialogButtonBox::accepted, this, [&]() - { - create_desktop_shortcuts = desk_check->isChecked(); - create_app_shortcut = quick_check->isChecked(); - dlg->accept(); - }); - - dlg->setAttribute(Qt::WA_DeleteOnClose); - dlg->exec(); - - std::set locations; -#ifdef _WIN32 - locations.insert(gui::utils::shortcut_location::rpcs3_shortcuts); -#endif - if (create_desktop_shortcuts) - { - locations.insert(gui::utils::shortcut_location::desktop); - } - if (create_app_shortcut) - { - locations.insert(gui::utils::shortcut_location::applications); - } - - for (const auto& [boot_path, title_id] : bootable_paths_installed) - { - for (const game_info& gameinfo : m_game_list_frame->GetGameInfo()) + // Try to claim operaions on ID + for (auto it = paths.begin(); it != paths.end();) { - if (gameinfo && gameinfo->info.bootable && gameinfo->info.serial == sstr(title_id) && boot_path.starts_with(gameinfo->info.path)) + if (IDs.count(it->second)) { - m_game_list_frame->CreateShortcuts(gameinfo, locations); - break; + it = paths.erase(it); + } + else + { + IDs.emplace(it->second); + it++; } } - } + + ShowOptionalGamePreparations(tr("Success!"), tr("Successfully installed software from package(s)!"), std::move(paths)); + }); + } + + m_game_list_frame->Refresh(true); + + std::this_thread::sleep_for(std::chrono::microseconds(100'000 - std::min(100'000, get_system_time() - start_time))); + pdlg.hide(); + + if (installed_a_whole_package_without_new_software) + { + m_gui_settings->ShowInfoBox(tr("Success!"), tr("Successfully installed software from package(s)!"), gui::ib_pkg_success, this); + return true; + } + + if (!cancelled) + { + return true; } } else @@ -2282,6 +2246,104 @@ void main_window::ShowTitleBars(bool show) const m_log_frame->SetTitleBarVisible(show); } +void main_window::ShowOptionalGamePreparations(const QString& title, const QString& message, std::map bootable_paths) +{ + if (bootable_paths.empty()) + { + m_gui_settings->ShowInfoBox(title, message, gui::ib_pkg_success, this); + return; + } + + QDialog* dlg = new QDialog(this); + dlg->setObjectName("game_prepare_window"); + dlg->setWindowTitle(title); + + QVBoxLayout* vlayout = new QVBoxLayout(dlg); + + QCheckBox* desk_check = new QCheckBox(tr("Add desktop shortcut(s)")); +#ifdef _WIN32 + QCheckBox* quick_check = new QCheckBox(tr("Add Start menu shortcut(s)")); +#elif defined(__APPLE__) + QCheckBox* quick_check = new QCheckBox(tr("Add dock shortcut(s)")); +#else + QCheckBox* quick_check = new QCheckBox(tr("Add launcher shortcut(s)")); +#endif + QCheckBox* precompile_check = new QCheckBox(tr("Precompile caches")); + QLabel* label = new QLabel(tr("%1\nWould you like to install shortcuts to the installed software and precompile caches? (%2 new software detected)\n\n").arg(message).arg(bootable_paths.size()), dlg); + + vlayout->addWidget(label); + vlayout->addStretch(10); + vlayout->addWidget(desk_check); + vlayout->addStretch(3); + vlayout->addWidget(quick_check); + vlayout->addStretch(3); + vlayout->addWidget(precompile_check); + vlayout->addStretch(3); + + precompile_check->setToolTip(tr("Spend time building data needed for game boot now instead of at launch.")); + + QDialogButtonBox* btn_box = new QDialogButtonBox(QDialogButtonBox::Ok); + + vlayout->addWidget(btn_box); + dlg->setLayout(vlayout); + + connect(btn_box, &QDialogButtonBox::accepted, this, [=, paths = std::move(bootable_paths)]() + { + const bool create_desktop_shortcuts = desk_check->isChecked(); + const bool create_app_shortcut = quick_check->isChecked(); + const bool create_caches = precompile_check->isChecked(); + + dlg->hide(); + dlg->accept(); + + std::set locations; + +#ifdef _WIN32 + locations.insert(gui::utils::shortcut_location::rpcs3_shortcuts); +#endif + if (create_desktop_shortcuts) + { + locations.insert(gui::utils::shortcut_location::desktop); + } + + if (create_app_shortcut) + { + locations.insert(gui::utils::shortcut_location::applications); + } + + QList game_data; + + for (const auto& [boot_path, title_id] : paths) + { + for (const game_info& gameinfo : m_game_list_frame->GetGameInfo()) + { + if (gameinfo && gameinfo->info.serial == sstr(title_id)) + { + if (Emu.IsPathInsideDir(boot_path, gameinfo->info.path)) + { + m_game_list_frame->CreateShortcuts(gameinfo, locations); + + if (create_caches) + { + game_data.push_back(gameinfo); + } + } + + break; + } + } + } + + if (!game_data.isEmpty()) + { + m_game_list_frame->BatchCreateCPUCaches(game_data); + } + }); + + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->open(); +} + void main_window::CreateActions() { ui->exitAct->setShortcuts(QKeySequence::Quit); @@ -2344,17 +2406,7 @@ void main_window::CreateConnects() // Only select one folder for now paths << QFileDialog::getExistingDirectory(this, tr("Select a folder containing one or more games"), qstr(fs::get_config_dir()), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - - if (!paths.isEmpty()) - { - Emu.GracefulShutdown(false); - - for (const QString& path : paths) - { - AddGamesFromDir(path); - } - m_game_list_frame->Refresh(true); - } + AddGamesFromDirs(paths); }); connect(ui->bootRecentMenu, &QMenu::aboutToShow, this, [this]() @@ -2525,7 +2577,7 @@ void main_window::CreateConnects() }); connect(ui->exitAct, &QAction::triggered, this, &QWidget::close); - connect(ui->batchCreateCPUCachesAct, &QAction::triggered, m_game_list_frame, &game_list_frame::BatchCreateCPUCaches); + connect(ui->batchCreateCPUCachesAct, &QAction::triggered, m_game_list_frame, [list = m_game_list_frame]() { list->BatchCreateCPUCaches(); }); connect(ui->batchRemovePPUCachesAct, &QAction::triggered, m_game_list_frame, &game_list_frame::BatchRemovePPUCaches); connect(ui->batchRemoveSPUCachesAct, &QAction::triggered, m_game_list_frame, &game_list_frame::BatchRemoveSPUCaches); connect(ui->batchRemoveShaderCachesAct, &QAction::triggered, m_game_list_frame, &game_list_frame::BatchRemoveShaderCaches); @@ -3446,16 +3498,73 @@ void main_window::closeEvent(QCloseEvent* closeEvent) /** Add valid disc games to gamelist (games.yml) -@param path = dir path to scan for game +@param paths = dir paths to scan for game */ -void main_window::AddGamesFromDir(const QString& path) +void main_window::AddGamesFromDirs(const QStringList& paths) { - if (!QFileInfo(path).isDir()) + if (paths.isEmpty()) { return; } - Emu.AddGamesFromDir(sstr(path)); + // Obtain list of previously existing entries under the specificied parent paths for comparison + std::unordered_set existing; + + for (const game_info& game : m_game_list_frame->GetGameInfo()) + { + if (game) + { + for (const auto& dir_path : paths) + { + if (dir_path.startsWith(game->info.path.c_str()) && fs::exists(game->info.path)) + { + existing.insert(game->info.path); + break; + } + } + } + } + + for (const QString& path : paths) + { + Emu.AddGamesFromDir(sstr(path)); + } + + m_game_list_frame->AddRefreshedSlot([this, paths = std::move(paths), existing = std::move(existing)](std::set& IDs) + { + // Execute followup operations only for newly added entries under the specified paths + std::map paths_added; // -> title id + + for (const game_info& game : m_game_list_frame->GetGameInfo()) + { + if (game && !existing.contains(game->info.path)) + { + for (const auto& dir_path : paths) + { + if (Emu.IsPathInsideDir(game->info.path, sstr(dir_path))) + { + // Try to claim operaion on ID + const QString title_id = qstr(game->info.serial); + + if (!IDs.count(title_id)) + { + IDs.emplace(title_id); + paths_added.emplace(game->info.path, title_id); + } + + break; + } + } + } + } + + if (!paths_added.empty()) + { + ShowOptionalGamePreparations(tr("Success!"), tr("Successfully added software to game list from path(s)!"), std::move(paths_added)); + } + }); + + m_game_list_frame->Refresh(true); } /** @@ -3632,12 +3741,7 @@ void main_window::dropEvent(QDropEvent* event) } case drop_type::drop_dir: // import valid games to gamelist (games.yaml) { - for (const auto& path : drop_paths) - { - AddGamesFromDir(path); - } - - m_game_list_frame->Refresh(true); + AddGamesFromDirs(drop_paths); break; } case drop_type::drop_game: // import valid games to gamelist (games.yaml) diff --git a/rpcs3/rpcs3qt/main_window.h b/rpcs3/rpcs3qt/main_window.h index dfd8234ee3..45aaa7681c 100644 --- a/rpcs3/rpcs3qt/main_window.h +++ b/rpcs3/rpcs3qt/main_window.h @@ -150,6 +150,7 @@ private: void CreateDockWindows(); void EnableMenus(bool enabled) const; void ShowTitleBars(bool show) const; + void ShowOptionalGamePreparations(const QString& title, const QString& message, std::map game_path); static bool InstallFileInExData(const std::string& extension, const QString& path, const std::string& filename); @@ -165,7 +166,7 @@ private: u64 m_drop_file_timestamp = umax; drop_type m_drop_file_cached_drop_type = drop_type::drop_error; drop_type IsValidFile(const QMimeData& md, QStringList* drop_paths = nullptr); - static void AddGamesFromDir(const QString& path); + void AddGamesFromDirs(const QStringList& paths); QAction* CreateRecentAction(const q_string_pair& entry, const uint& sc_idx); void BootRecentAction(const QAction* act);