diff --git a/Source/Core/DolphinQt2/GameList/GameFile.cpp b/Source/Core/DolphinQt2/GameList/GameFile.cpp index 3f1d60d65f..a23c7ab240 100644 --- a/Source/Core/DolphinQt2/GameList/GameFile.cpp +++ b/Source/Core/DolphinQt2/GameList/GameFile.cpp @@ -8,10 +8,14 @@ #include #include +#include "Common/Assert.h" #include "Common/FileUtil.h" +#include "Common/NandPaths.h" #include "Core/ConfigManager.h" +#include "Core/HW/WiiSaveCrypted.h" #include "DiscIO/Blob.h" #include "DiscIO/Enums.h" +#include "DiscIO/NANDContentLoader.h" #include "DiscIO/Volume.h" #include "DiscIO/VolumeCreator.h" #include "DolphinQt2/GameList/GameFile.h" @@ -144,6 +148,7 @@ bool GameFile::TryLoadVolume() m_game_id = QString::fromStdString(volume->GetGameID()); std::string maker_id = volume->GetMakerID(); + volume->GetTitleID(&m_title_id); m_maker = QString::fromStdString(DiscIO::GetCompanyFromID(maker_id)); m_maker_id = QString::fromStdString(maker_id); m_revision = volume->GetRevision(); @@ -291,6 +296,53 @@ QString GameFile::GetLanguage(DiscIO::Language lang) const } } +bool GameFile::IsInstalled() const +{ + _assert_(m_platform == DiscIO::Platform::WII_WAD); + + const std::string content_dir = + Common::GetTitleContentPath(m_title_id, Common::FromWhichRoot::FROM_CONFIGURED_ROOT); + + if (!File::IsDirectory(content_dir)) + return false; + + // Since this isn't IOS and we only need a simple way to figure out if a title is installed, + // we make the (reasonable) assumption that having more than just the TMD in the content + // directory means that the title is installed. + const auto entries = File::ScanDirectoryTree(content_dir, false); + return std::any_of(entries.children.begin(), entries.children.end(), + [](const auto& file) { return file.virtualName != "title.tmd"; }); +} + +bool GameFile::Install() +{ + _assert_(m_platform == DiscIO::Platform::WII_WAD); + + return DiscIO::CNANDContentManager::Access().Install_WiiWAD(m_path.toStdString()); +} + +bool GameFile::Uninstall() +{ + _assert_(m_platform == DiscIO::Platform::WII_WAD); + + return DiscIO::CNANDContentManager::Access().RemoveTitle(m_title_id, + Common::FROM_CONFIGURED_ROOT); +} + +bool GameFile::ExportWiiSave() +{ + return CWiiSaveCrypted::ExportWiiSave(m_title_id); +} + +QString GameFile::GetWiiFSPath() const +{ + _assert_(m_platform != DiscIO::Platform::GAMECUBE_DISC); + + const std::string path = Common::GetTitleDataPath(m_title_id, Common::FROM_CONFIGURED_ROOT); + + return QString::fromStdString(path); +} + // Convert an integer size to a friendly string representation. QString FormatSize(qint64 size) { diff --git a/Source/Core/DolphinQt2/GameList/GameFile.h b/Source/Core/DolphinQt2/GameList/GameFile.h index 9813a39054..47eef18381 100644 --- a/Source/Core/DolphinQt2/GameList/GameFile.h +++ b/Source/Core/DolphinQt2/GameList/GameFile.h @@ -38,6 +38,7 @@ public: QString GetGameID() const { return m_game_id; } QString GetMakerID() const { return m_maker_id; } QString GetMaker() const { return m_maker; } + u64 GetTitleID() const { return m_title_id; } u16 GetRevision() const { return m_revision; } QString GetInternalName() const { return m_internal_name; } u8 GetDiscNumber() const { return m_disc_number; } @@ -52,6 +53,8 @@ public: DiscIO::Country GetCountryID() const { return m_country; } QString GetCountry() const; DiscIO::BlobType GetBlobType() const { return m_blob_type; } + QString GetWiiFSPath() const; + bool IsInstalled() const; // Banner details QString GetLanguage(DiscIO::Language lang) const; QList GetAvailableLanguages() const; @@ -65,6 +68,10 @@ public: QString GetLongName(DiscIO::Language lang) const { return m_long_names[lang]; } QString GetLongMaker(DiscIO::Language lang) const { return m_long_makers[lang]; } QString GetDescription(DiscIO::Language lang) const { return m_descriptions[lang]; } + bool Install(); + bool Uninstall(); + bool ExportWiiSave(); + private: QString GetBannerString(const QMap& m) const; @@ -90,6 +97,7 @@ private: QString m_maker; QString m_maker_id; u16 m_revision = 0; + u64 m_title_id = 0; QString m_internal_name; QMap m_short_names; QMap m_long_names; diff --git a/Source/Core/DolphinQt2/GameList/GameList.cpp b/Source/Core/DolphinQt2/GameList/GameList.cpp index 637ddd93ea..9248624fd9 100644 --- a/Source/Core/DolphinQt2/GameList/GameList.cpp +++ b/Source/Core/DolphinQt2/GameList/GameList.cpp @@ -3,11 +3,19 @@ // Refer to the license.txt file included. #include +#include +#include +#include +#include #include #include #include +#include +#include #include +#include "Common/FileUtil.h" +#include "DiscIO/Blob.h" #include "DiscIO/Enums.h" #include "DolphinQt2/Config/PropertiesDialog.h" @@ -16,6 +24,8 @@ #include "DolphinQt2/GameList/TableDelegate.h" #include "DolphinQt2/Settings.h" +static bool CompressCB(const std::string&, float, void*); + GameList::GameList(QWidget* parent) : QStackedWidget(parent) { m_model = new GameListModel(this); @@ -105,18 +115,47 @@ void GameList::MakeListView() void GameList::ShowContextMenu(const QPoint&) { + const auto game = GetSelectedGame(); + if (game.isEmpty()) + return; + QMenu* menu = new QMenu(this); - DiscIO::Platform platform = GameFile(GetSelectedGame()).GetPlatformID(); + DiscIO::Platform platform = GameFile(game).GetPlatformID(); + menu->addAction(tr("Properties"), this, SLOT(OpenProperties())); + menu->addAction(tr("Wiki"), this, SLOT(OpenWiki())); + menu->addSeparator(); + if (platform == DiscIO::Platform::GAMECUBE_DISC || platform == DiscIO::Platform::WII_DISC) { - menu->addAction(tr("Properties"), this, SLOT(OpenProperties())); - menu->addAction(tr("Open Wiki Page"), this, SLOT(OpenWiki())); - menu->addAction(tr("Set as Default ISO"), this, SLOT(SetDefaultISO())); + menu->addAction(tr("Default ISO"), this, SLOT(SetDefaultISO())); + const auto blob_type = GameFile(game).GetBlobType(); + + if (blob_type == DiscIO::BlobType::GCZ) + menu->addAction(tr("Decompress ISO"), this, SLOT(DecompressISO())); + else if (blob_type == DiscIO::BlobType::PLAIN) + menu->addAction(tr("Compress ISO"), this, SLOT(CompressISO())); + + menu->addSeparator(); } - else + if (platform == DiscIO::Platform::WII_WAD) { - return; + menu->addAction(tr("Install to the NAND"), this, SLOT(InstallWAD())); + + if (GameFile(game).IsInstalled()) + menu->addAction(tr("Uninstall from the NAND"), this, SLOT(UninstallWAD())); + + menu->addSeparator(); } + + if (platform == DiscIO::Platform::WII_WAD || platform == DiscIO::Platform::WII_DISC) + { + menu->addAction(tr("Open Wii save folder"), this, SLOT(OpenSaveFolder())); + menu->addAction(tr("Export Wii save (Experimental)"), this, SLOT(ExportWiiSave())); + menu->addSeparator(); + } + + menu->addAction(tr("Open Containing Folder"), this, SLOT(OpenContainingFolder())); + menu->addAction(tr("Remove File"), this, SLOT(DeleteFile())); menu->exec(QCursor::pos()); } @@ -126,6 +165,18 @@ void GameList::OpenProperties() properties->show(); } +void GameList::ExportWiiSave() +{ + QMessageBox result_dialog(this); + + const bool success = GameFile(GetSelectedGame()).ExportWiiSave(); + + result_dialog.setIcon(success ? QMessageBox::Information : QMessageBox::Critical); + result_dialog.setText(success ? tr("Successfully exported save files") : + tr("Failed to export save files!")); + result_dialog.exec(); +} + void GameList::OpenWiki() { QString game_id = GameFile(GetSelectedGame()).GetGameID(); @@ -133,11 +184,160 @@ void GameList::OpenWiki() QDesktopServices::openUrl(QUrl(url)); } +void GameList::CompressISO() +{ + const auto original_path = GetSelectedGame(); + auto file = GameFile(original_path); + + const bool compressed = (file.GetBlobType() == DiscIO::BlobType::GCZ); + + if (!compressed && file.GetPlatformID() == DiscIO::Platform::WII_DISC) + { + QMessageBox wii_warning(this); + wii_warning.setIcon(QMessageBox::Warning); + wii_warning.setText(tr("Are you sure?")); + wii_warning.setInformativeText( + tr("Compressing a Wii disc image will irreversibly change the compressed copy by removing " + "padding data. Your disc image will still work.")); + wii_warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + + if (wii_warning.exec() == QMessageBox::No) + return; + } + + QString dst_path = QFileDialog::getSaveFileName( + this, compressed ? tr("Select where you want to save the decompressed image") : + tr("Select where you want to save the compressed image"), + QFileInfo(GetSelectedGame()) + .dir() + .absoluteFilePath(file.GetGameID()) + .append(compressed ? QStringLiteral(".gcm") : QStringLiteral(".gcz")), + compressed ? tr("Uncompressed GC/Wii images (*.iso *.gcm") : + tr("Compressed GC/Wii images (*.gcz)")); + + if (dst_path.isEmpty()) + return; + + QProgressDialog progress_dialog(compressed ? tr("Decompressing...") : tr("Compressing..."), + tr("Abort"), 0, 100, this); + progress_dialog.setWindowModality(Qt::WindowModal); + + bool good; + + if (compressed) + { + good = DiscIO::DecompressBlobToFile(original_path.toStdString(), dst_path.toStdString(), + &CompressCB, &progress_dialog); + } + else + { + good = DiscIO::CompressFileToBlob(original_path.toStdString(), dst_path.toStdString(), + file.GetPlatformID() == DiscIO::Platform::WII_DISC ? 1 : 0, + 16384, &CompressCB, &progress_dialog); + } + + if (good) + { + QMessageBox(QMessageBox::Information, tr("Success!"), tr("Successfully compressed image."), + QMessageBox::Ok, this) + .exec(); + } + else + { + QErrorMessage(this).showMessage(tr("Dolphin failed to complete the requested action.")); + } +} + +void GameList::InstallWAD() +{ + QMessageBox result_dialog(this); + + const bool success = GameFile(GetSelectedGame()).Install(); + + result_dialog.setIcon(success ? QMessageBox::Information : QMessageBox::Critical); + result_dialog.setText(success ? tr("Succesfully installed title to the NAND") : + tr("Failed to install title to the NAND")); + result_dialog.exec(); +} + +void GameList::UninstallWAD() +{ + QMessageBox warning_dialog(this); + + warning_dialog.setIcon(QMessageBox::Information); + warning_dialog.setText(tr("Uninstalling the WAD will remove the currently installed version of " + "this title from the NAND without deleting its save data. Continue?")); + warning_dialog.setStandardButtons(QMessageBox::No | QMessageBox::Yes); + + if (warning_dialog.exec() == QMessageBox::No) + return; + + QMessageBox result_dialog(this); + + const bool success = GameFile(GetSelectedGame()).Uninstall(); + + result_dialog.setIcon(success ? QMessageBox::Information : QMessageBox::Critical); + result_dialog.setText(success ? tr("Succesfully removed title from the NAND") : + tr("Failed to remove title from the NAND")); + result_dialog.exec(); +} + void GameList::SetDefaultISO() { Settings().SetDefaultGame(GetSelectedGame()); } +void GameList::OpenContainingFolder() +{ + QUrl url = QUrl::fromLocalFile(QFileInfo(GetSelectedGame()).dir().absolutePath()); + QDesktopServices::openUrl(url); +} + +void GameList::OpenSaveFolder() +{ + QUrl url = QUrl::fromLocalFile(GameFile(GetSelectedGame()).GetWiiFSPath()); + QDesktopServices::openUrl(url); +} + +void GameList::DeleteFile() +{ + const auto game = GetSelectedGame(); + QMessageBox confirm_dialog(this); + + confirm_dialog.setIcon(QMessageBox::Warning); + confirm_dialog.setText(tr("Are you sure you want to delete this file?")); + confirm_dialog.setInformativeText(tr("You won't be able to undo this!")); + confirm_dialog.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); + + if (confirm_dialog.exec() == QMessageBox::Yes) + { + bool deletion_successful = false; + + while (!deletion_successful) + { + deletion_successful = File::Delete(game.toStdString()); + + if (deletion_successful) + { + m_model->RemoveGame(game); + } + else + { + QMessageBox error_dialog(this); + + error_dialog.setIcon(QMessageBox::Critical); + error_dialog.setText(tr("Failed to delete the selected file.")); + error_dialog.setInformativeText(tr("Check whether you have the permissions required to " + "delete the file or whether it's still in use.")); + error_dialog.setStandardButtons(QMessageBox::Retry | QMessageBox::Abort); + + if (error_dialog.exec() == QMessageBox::Abort) + break; + } + } + } +} + QString GameList::GetSelectedGame() const { QAbstractItemView* view; @@ -189,3 +389,13 @@ void GameList::keyReleaseEvent(QKeyEvent* event) else QStackedWidget::keyReleaseEvent(event); } + +static bool CompressCB(const std::string& text, float percent, void* ptr) +{ + if (ptr == nullptr) + return false; + auto* progress_dialog = static_cast(ptr); + + progress_dialog->setValue(percent * 100); + return !progress_dialog->wasCanceled(); +} diff --git a/Source/Core/DolphinQt2/GameList/GameList.h b/Source/Core/DolphinQt2/GameList/GameList.h index 45f8b2d979..b6c51c6c0e 100644 --- a/Source/Core/DolphinQt2/GameList/GameList.h +++ b/Source/Core/DolphinQt2/GameList/GameList.h @@ -29,9 +29,16 @@ public slots: void SetViewColumn(int col, bool view) { m_table->setColumnHidden(col, !view); } private slots: void ShowContextMenu(const QPoint&); + void OpenContainingFolder(); void OpenProperties(); + void OpenSaveFolder(); void OpenWiki(); void SetDefaultISO(); + void DeleteFile(); + void InstallWAD(); + void UninstallWAD(); + void ExportWiiSave(); + void CompressISO(); signals: void GameSelected(); diff --git a/Source/Core/DolphinQt2/MenuBar.cpp b/Source/Core/DolphinQt2/MenuBar.cpp index 790afdf38d..417156552c 100644 --- a/Source/Core/DolphinQt2/MenuBar.cpp +++ b/Source/Core/DolphinQt2/MenuBar.cpp @@ -4,10 +4,13 @@ #include #include +#include +#include #include #include "Core/State.h" #include "DolphinQt2/AboutDialog.h" +#include "DolphinQt2/GameList/GameFile.h" #include "DolphinQt2/MenuBar.h" #include "DolphinQt2/Settings.h" @@ -17,7 +20,7 @@ MenuBar::MenuBar(QWidget* parent) : QMenuBar(parent) AddEmulationMenu(); addMenu(tr("Movie")); addMenu(tr("Options")); - addMenu(tr("Tools")); + AddToolsMenu(); AddViewMenu(); AddHelpMenu(); @@ -71,6 +74,12 @@ void MenuBar::AddFileMenu() m_exit_action = file_menu->addAction(tr("Exit"), this, SIGNAL(Exit())); } +void MenuBar::AddToolsMenu() +{ + QMenu* tools_menu = addMenu(tr("Tools")); + m_wad_install_action = tools_menu->addAction(tr("Install WAD..."), this, SLOT(InstallWAD())); +} + void MenuBar::AddEmulationMenu() { QMenu* emu_menu = addMenu(tr("Emulation")); @@ -206,3 +215,27 @@ void MenuBar::AddTableColumnsMenu(QMenu* view_menu) action->setCheckable(true); } } + +void MenuBar::InstallWAD() +{ + QString wad_file = QFileDialog::getOpenFileName(this, tr("Select a title to install to NAND"), + QString(), tr("WAD files (*.wad)")); + + if (wad_file.isEmpty()) + return; + + QMessageBox result_dialog(this); + + if (GameFile(wad_file).Install()) + { + result_dialog.setIcon(QMessageBox::Information); + result_dialog.setText(tr("Successfully installed title to the NAND")); + } + else + { + result_dialog.setIcon(QMessageBox::Critical); + result_dialog.setText(tr("Failed to install title to the NAND!")); + } + + result_dialog.exec(); +} diff --git a/Source/Core/DolphinQt2/MenuBar.h b/Source/Core/DolphinQt2/MenuBar.h index c0084ea257..697f9c6408 100644 --- a/Source/Core/DolphinQt2/MenuBar.h +++ b/Source/Core/DolphinQt2/MenuBar.h @@ -50,6 +50,9 @@ public slots: void EmulationStopped(); void UpdateStateSlotMenu(); + // Tools + void InstallWAD(); + private: void AddFileMenu(); @@ -62,12 +65,16 @@ private: void AddGameListTypeSection(QMenu* view_menu); void AddTableColumnsMenu(QMenu* view_menu); + void AddToolsMenu(); void AddHelpMenu(); // File QAction* m_open_action; QAction* m_exit_action; + // Tools + QAction* m_wad_install_action; + // Emulation QAction* m_play_action; QAction* m_pause_action;