From 07839fb3cee0ed55fa90d56e44b6eb49de65c9e9 Mon Sep 17 00:00:00 2001 From: Steveice10 <1269164+Steveice10@users.noreply.github.com> Date: Sat, 14 Oct 2023 18:11:59 -0700 Subject: [PATCH] qt: Add option to uninstall a game. (#7064) * qt: Add option to uninstall a game. * Address review comments. --- src/citra_qt/game_list.cpp | 118 +++++++++++++++++++++++++----- src/citra_qt/game_list.h | 7 +- src/citra_qt/game_list_p.h | 8 +- src/citra_qt/game_list_worker.cpp | 24 +++--- src/citra_qt/game_list_worker.h | 6 +- src/citra_qt/main.cpp | 52 +++++++++++++ src/citra_qt/main.h | 7 ++ src/core/hle/service/am/am.cpp | 39 ++++++---- src/core/hle/service/am/am.h | 8 ++ 9 files changed, 223 insertions(+), 46 deletions(-) diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index bfb0c2b20b..38dc63cf5c 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -459,8 +460,11 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { switch (selected.data(GameListItem::TypeRole).value()) { case GameListItemType::Game: AddGamePopup(context_menu, selected.data(GameListItemPath::FullPathRole).toString(), + selected.data(GameListItemPath::TitleRole).toString(), selected.data(GameListItemPath::ProgramIdRole).toULongLong(), - selected.data(GameListItemPath::ExtdataIdRole).toULongLong()); + selected.data(GameListItemPath::ExtdataIdRole).toULongLong(), + static_cast( + selected.data(GameListItemPath::MediaTypeRole).toUInt())); break; case GameListItemType::CustomDir: AddPermDirPopup(context_menu, selected); @@ -522,28 +526,36 @@ void ForEachOpenGLCacheFile(u64 program_id, auto func) { } } -void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 program_id, - u64 extdata_id) { +void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QString& name, + u64 program_id, u64 extdata_id, Service::FS::MediaType media_type) { QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); QAction* open_extdata_location = context_menu.addAction(tr("Open Extra Data Location")); QAction* open_application_location = context_menu.addAction(tr("Open Application Location")); QAction* open_update_location = context_menu.addAction(tr("Open Update Data Location")); + QAction* open_dlc_location = context_menu.addAction(tr("Open DLC Data Location")); QAction* open_texture_dump_location = context_menu.addAction(tr("Open Texture Dump Location")); QAction* open_texture_load_location = context_menu.addAction(tr("Open Custom Texture Location")); QAction* open_mods_location = context_menu.addAction(tr("Open Mods Location")); - QAction* open_dlc_location = context_menu.addAction(tr("Open DLC Data Location")); - QMenu* shader_menu = context_menu.addMenu(tr("Disk Shader Cache")); QAction* dump_romfs = context_menu.addAction(tr("Dump RomFS")); - QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); - context_menu.addSeparator(); - QAction* properties = context_menu.addAction(tr("Properties")); + QMenu* shader_menu = context_menu.addMenu(tr("Disk Shader Cache")); QAction* open_shader_cache_location = shader_menu->addAction(tr("Open Shader Cache Location")); shader_menu->addSeparator(); QAction* delete_opengl_disk_shader_cache = shader_menu->addAction(tr("Delete OpenGL Shader Cache")); + QMenu* uninstall_menu = context_menu.addMenu(tr("Uninstall")); + QAction* uninstall_all = uninstall_menu->addAction(tr("Everything")); + uninstall_menu->addSeparator(); + QAction* uninstall_game = uninstall_menu->addAction(tr("Game")); + QAction* uninstall_update = uninstall_menu->addAction(tr("Update")); + QAction* uninstall_dlc = uninstall_menu->addAction(tr("DLC")); + + QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); + context_menu.addSeparator(); + QAction* properties = context_menu.addAction(tr("Properties")); + const u32 program_id_high = (program_id >> 32) & 0xFFFFFFFF; const bool is_application = program_id_high == 0x00040000 || program_id_high == 0x00040010; @@ -564,22 +576,36 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 progra open_extdata_location->setVisible(false); } - auto media_type = Service::AM::GetTitleMediaType(program_id); - open_application_location->setEnabled(path.toStdString() == - Service::AM::GetTitleContentPath(media_type, program_id)); - open_update_location->setEnabled( - is_application && FileUtil::Exists(Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, - program_id + 0xe00000000) + - "content/")); - auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); + const auto update_program_id = program_id | 0xE00000000; + const auto dlc_program_id = program_id | 0x8C00000000; + const auto is_installed = + media_type == Service::FS::MediaType::NAND || media_type == Service::FS::MediaType::SDMC; + const auto has_update = + is_application && FileUtil::Exists(Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, + update_program_id) + + "content/"); + const auto has_dlc = + is_application && + FileUtil::Exists(Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, dlc_program_id) + + "content/"); + + open_application_location->setEnabled(is_installed); + open_update_location->setEnabled(has_update); + open_dlc_location->setEnabled(has_dlc); open_texture_dump_location->setEnabled(is_application); open_texture_load_location->setEnabled(is_application); open_mods_location->setEnabled(is_application); - open_dlc_location->setEnabled(is_application); dump_romfs->setEnabled(is_application); + delete_opengl_disk_shader_cache->setEnabled(opengl_cache_exists); + uninstall_all->setEnabled(is_installed || has_update || has_dlc); + uninstall_game->setEnabled(is_installed); + uninstall_update->setEnabled(has_update); + uninstall_dlc->setEnabled(has_dlc); + + auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); navigate_to_gamedb_entry->setVisible(it != compatibility_list.end()); connect(open_save_location, &QAction::triggered, this, [this, program_id] { @@ -641,7 +667,63 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 progra connect(delete_opengl_disk_shader_cache, &QAction::triggered, this, [program_id] { ForEachOpenGLCacheFile(program_id, [](QFile& file) { file.remove(); }); }); -}; + connect(uninstall_all, &QAction::triggered, this, [=, this] { + QMessageBox::StandardButton answer = QMessageBox::question( + this, tr("Citra"), + tr("Are you sure you want to completely uninstall '%1'?\n\nThis will " + "delete the game if installed, as well as any installed updates or DLC.") + .arg(name), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (answer == QMessageBox::Yes) { + std::vector> titles; + if (is_installed) { + titles.emplace_back(media_type, program_id, name); + } + if (has_update) { + titles.emplace_back(Service::FS::MediaType::SDMC, update_program_id, + tr("%1 (Update)").arg(name)); + } + if (has_dlc) { + titles.emplace_back(Service::FS::MediaType::SDMC, dlc_program_id, + tr("%1 (DLC)").arg(name)); + } + main_window->UninstallTitles(titles); + } + }); + connect(uninstall_game, &QAction::triggered, this, [this, name, media_type, program_id] { + QMessageBox::StandardButton answer = QMessageBox::question( + this, tr("Citra"), tr("Are you sure you want to uninstall '%1'?").arg(name), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (answer == QMessageBox::Yes) { + std::vector> titles; + titles.emplace_back(media_type, program_id, name); + main_window->UninstallTitles(titles); + } + }); + connect(uninstall_update, &QAction::triggered, this, [this, name, update_program_id] { + QMessageBox::StandardButton answer = QMessageBox::question( + this, tr("Citra"), + tr("Are you sure you want to uninstall the update for '%1'?").arg(name), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (answer == QMessageBox::Yes) { + std::vector> titles; + titles.emplace_back(Service::FS::MediaType::SDMC, update_program_id, + tr("%1 (Update)").arg(name)); + main_window->UninstallTitles(titles); + } + }); + connect(uninstall_dlc, &QAction::triggered, this, [this, name, dlc_program_id] { + QMessageBox::StandardButton answer = QMessageBox::question( + this, tr("Citra"), tr("Are you sure you want to uninstall all DLC for '%1'?").arg(name), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (answer == QMessageBox::Yes) { + std::vector> titles; + titles.emplace_back(Service::FS::MediaType::SDMC, dlc_program_id, + tr("%1 (DLC)").arg(name)); + main_window->UninstallTitles(titles); + } + }); +} void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) { UISettings::GameDir& game_dir = diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h index 92a5016762..9b9ccb05aa 100644 --- a/src/citra_qt/game_list.h +++ b/src/citra_qt/game_list.h @@ -12,6 +12,10 @@ #include "common/common_types.h" #include "uisettings.h" +namespace Service::FS { +enum class MediaType : u32; +} + class GameListWorker; class GameListDir; class GameListSearchField; @@ -105,7 +109,8 @@ private: void PopupContextMenu(const QPoint& menu_location); void PopupHeaderContextMenu(const QPoint& menu_location); - void AddGamePopup(QMenu& context_menu, const QString& path, u64 program_id, u64 extdata_id); + void AddGamePopup(QMenu& context_menu, const QString& path, const QString& name, u64 program_id, + u64 extdata_id, Service::FS::MediaType media_type); void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); void UpdateColumnVisibility(); diff --git a/src/citra_qt/game_list_p.h b/src/citra_qt/game_list_p.h index e7394609aa..338d8cee79 100644 --- a/src/citra_qt/game_list_p.h +++ b/src/citra_qt/game_list_p.h @@ -25,6 +25,10 @@ #include "common/string_util.h" #include "core/loader/smdh.h" +namespace Service::FS { +enum class MediaType : u32; +} + enum class GameListItemType { Game = QStandardItem::UserType + 1, CustomDir = QStandardItem::UserType + 2, @@ -153,14 +157,16 @@ public: static constexpr int ProgramIdRole = SortRole + 3; static constexpr int ExtdataIdRole = SortRole + 4; static constexpr int LongTitleRole = SortRole + 5; + static constexpr int MediaTypeRole = SortRole + 6; GameListItemPath() = default; GameListItemPath(const QString& game_path, std::span smdh_data, u64 program_id, - u64 extdata_id) { + u64 extdata_id, Service::FS::MediaType media_type) { setData(type(), TypeRole); setData(game_path, FullPathRole); setData(qulonglong(program_id), ProgramIdRole); setData(qulonglong(extdata_id), ExtdataIdRole); + setData(quint32(media_type), MediaTypeRole); if (UISettings::values.game_list_icon_size.GetValue() == UISettings::GameListIconSize::NoIcon) { diff --git a/src/citra_qt/game_list_worker.cpp b/src/citra_qt/game_list_worker.cpp index 15ed1e7ac3..e4471b6ae7 100644 --- a/src/citra_qt/game_list_worker.cpp +++ b/src/citra_qt/game_list_worker.cpp @@ -33,10 +33,11 @@ GameListWorker::GameListWorker(QVector& game_dirs, GameListWorker::~GameListWorker() = default; void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion, - GameListDir* parent_dir) { - const auto callback = [this, recursion, parent_dir](u64* num_entries_out, - const std::string& directory, - const std::string& virtual_name) -> bool { + GameListDir* parent_dir, + Service::FS::MediaType media_type) { + const auto callback = [this, recursion, parent_dir, + media_type](u64* num_entries_out, const std::string& directory, + const std::string& virtual_name) -> bool { if (stop_processing) { // Breaks the callback loop. return false; @@ -105,7 +106,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign emit EntryReady( { new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id, - extdata_id), + extdata_id, media_type), new GameListItemCompat(compatibility), new GameListItemRegion(smdh), new GameListItem( @@ -116,7 +117,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign } else if (is_dir && recursion > 0) { watch_list.append(QString::fromStdString(physical_name)); - AddFstEntriesToGameList(physical_name, recursion - 1, parent_dir); + AddFstEntriesToGameList(physical_name, recursion - 1, parent_dir, media_type); } return true; @@ -144,8 +145,10 @@ void GameListWorker::run() { watch_list.append(demos_path); auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir); emit DirEntryReady(game_list_dir); - AddFstEntriesToGameList(games_path.toStdString(), 2, game_list_dir); - AddFstEntriesToGameList(demos_path.toStdString(), 2, game_list_dir); + AddFstEntriesToGameList(games_path.toStdString(), 2, game_list_dir, + Service::FS::MediaType::SDMC); + AddFstEntriesToGameList(demos_path.toStdString(), 2, game_list_dir, + Service::FS::MediaType::SDMC); } else if (game_dir.path == QStringLiteral("SYSTEM")) { QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)) + @@ -153,13 +156,14 @@ void GameListWorker::run() { watch_list.append(path); auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir); emit DirEntryReady(game_list_dir); - AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir); + AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir, + Service::FS::MediaType::NAND); } else { watch_list.append(game_dir.path); auto* const game_list_dir = new GameListDir(game_dir); emit DirEntryReady(game_list_dir); AddFstEntriesToGameList(game_dir.path.toStdString(), game_dir.deep_scan ? 256 : 0, - game_list_dir); + game_list_dir, Service::FS::MediaType::GameCard); } } diff --git a/src/citra_qt/game_list_worker.h b/src/citra_qt/game_list_worker.h index b5ca89463d..60012e62a6 100644 --- a/src/citra_qt/game_list_worker.h +++ b/src/citra_qt/game_list_worker.h @@ -15,6 +15,10 @@ #include "citra_qt/compatibility_list.h" #include "common/common_types.h" +namespace Service::FS { +enum class MediaType : u32; +} + class QStandardItem; /** @@ -52,7 +56,7 @@ signals: private: void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion, - GameListDir* parent_dir); + GameListDir* parent_dir, Service::FS::MediaType media_type); QVector& game_dirs; const CompatibilityList& compatibility_list; diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 6ef30d21df..671c8b6479 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -1750,6 +1751,57 @@ void GMainWindow::OnCIAInstallFinished() { game_list->PopulateAsync(UISettings::values.game_dirs); } +void GMainWindow::UninstallTitles( + const std::vector>& titles) { + if (titles.empty()) { + return; + } + + // Select the first title in the list as representative. + const auto first_name = std::get(titles[0]); + + QProgressDialog progress(tr("Uninstalling '%1'...").arg(first_name), tr("Cancel"), 0, + static_cast(titles.size()), this); + progress.setWindowModality(Qt::WindowModal); + + QFutureWatcher future_watcher; + QObject::connect(&future_watcher, &QFutureWatcher::finished, &progress, + &QProgressDialog::reset); + QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher, + &QFutureWatcher::cancel); + QObject::connect(&future_watcher, &QFutureWatcher::progressValueChanged, &progress, + &QProgressDialog::setValue); + + auto failed = false; + QString failed_name; + + const auto uninstall_title = [&future_watcher, &failed, &failed_name](const auto& title) { + const auto name = std::get(title); + const auto media_type = std::get(title); + const auto program_id = std::get(title); + + const auto result = Service::AM::UninstallProgram(media_type, program_id); + if (result.IsError()) { + LOG_ERROR(Frontend, "Failed to uninstall '{}': 0x{:08X}", name.toStdString(), + result.raw); + failed = true; + failed_name = name; + future_watcher.cancel(); + } + }; + + future_watcher.setFuture(QtConcurrent::map(titles, uninstall_title)); + progress.exec(); + future_watcher.waitForFinished(); + + if (failed) { + QMessageBox::critical(this, tr("Citra"), tr("Failed to uninstall '%1'.").arg(failed_name)); + } else if (!future_watcher.isCanceled()) { + QMessageBox::information(this, tr("Citra"), + tr("Successfully uninstalled '%1'.").arg(first_name)); + } +} + void GMainWindow::OnMenuRecentFile() { QAction* action = qobject_cast(sender()); ASSERT(action); diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index c5de701e19..c95ca9f860 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -73,6 +73,10 @@ namespace Service::AM { enum class InstallStatus : u32; } +namespace Service::FS { +enum class MediaType : u32; +} + class GMainWindow : public QMainWindow { Q_OBJECT @@ -100,6 +104,9 @@ public: bool DropAction(QDropEvent* event); void AcceptDropEvent(QDropEvent* event); + void UninstallTitles( + const std::vector>& titles); + public slots: void OnAppFocusStateChanged(Qt::ApplicationState state); void OnLoadComplete(); diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index ee5d0c88ed..ad463631fa 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -1557,24 +1557,33 @@ void Module::Interface::GetRequiredSizeFromCia(Kernel::HLERequestContext& ctx) { rb.Push(container.GetTitleMetadata().GetContentSizeByIndex(FileSys::TMDContentIndex::Main)); } +ResultCode UninstallProgram(const FS::MediaType media_type, const u64 title_id) { + // Use the content folder so we don't delete the user's save data. + const auto path = GetTitlePath(media_type, title_id) + "content/"; + if (!FileUtil::Exists(path)) { + return {ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent}; + } + if (!FileUtil::DeleteDirRecursively(path)) { + // TODO: Determine the right error code for this. + return {ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent}; + } + return RESULT_SUCCESS; +} + void Module::Interface::DeleteProgram(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - auto media_type = rp.PopEnum(); - u64 title_id = rp.Pop(); - LOG_INFO(Service_AM, "Deleting title 0x{:016x}", title_id); - std::string path = GetTitlePath(media_type, title_id); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - if (!FileUtil::Exists(path)) { - rb.Push(ResultCode(ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState, - ErrorLevel::Permanent)); - LOG_ERROR(Service_AM, "Title not found"); - return; - } - bool success = FileUtil::DeleteDirRecursively(path); + const auto media_type = rp.PopEnum(); + const auto title_id = rp.Pop(); + + LOG_INFO(Service_AM, "called, title={:016x}", title_id); + + const auto result = UninstallProgram(media_type, title_id); am->ScanForAllTitles(); - rb.Push(RESULT_SUCCESS); - if (!success) - LOG_ERROR(Service_AM, "FileUtil::DeleteDirRecursively unexpectedly failed"); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(result); } void Module::Interface::GetSystemUpdaterMutex(Kernel::HLERequestContext& ctx) { diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 1bd6a399cb..54749b2125 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -172,6 +172,14 @@ std::string GetTitlePath(Service::FS::MediaType media_type, u64 tid); */ std::string GetMediaTitlePath(Service::FS::MediaType media_type); +/** + * Uninstalls the specified title. + * @param media_type the storage medium the title is installed to + * @param title_id the title ID to uninstall + * @return result of the uninstall operation + */ +ResultCode UninstallProgram(const FS::MediaType media_type, const u64 title_id); + class Module final { public: explicit Module(Core::System& system);