From 175f225ac1daa1f50badb8858137b1e91b067073 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Sun, 26 Sep 2021 06:17:51 +0200 Subject: [PATCH] DolphinQt: Add ability to start a game with Riivolution patches from the GUI. --- Source/Core/Core/Boot/Boot.cpp | 3 + Source/Core/Core/Boot/Boot.h | 2 + Source/Core/DolphinQt/CMakeLists.txt | 2 + Source/Core/DolphinQt/DolphinQt.vcxproj | 2 + Source/Core/DolphinQt/GameList/GameList.cpp | 14 ++ Source/Core/DolphinQt/GameList/GameList.h | 2 + Source/Core/DolphinQt/MainWindow.cpp | 38 ++++ Source/Core/DolphinQt/MainWindow.h | 1 + .../Core/DolphinQt/RiivolutionBootWidget.cpp | 195 ++++++++++++++++++ Source/Core/DolphinQt/RiivolutionBootWidget.h | 45 ++++ 10 files changed, 304 insertions(+) create mode 100644 Source/Core/DolphinQt/RiivolutionBootWidget.cpp create mode 100644 Source/Core/DolphinQt/RiivolutionBootWidget.h diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp index 3149a960f9..6a019402b4 100644 --- a/Source/Core/Core/Boot/Boot.cpp +++ b/Source/Core/Core/Boot/Boot.cpp @@ -57,6 +57,7 @@ namespace fs = std::filesystem; #include "Core/PowerPC/PowerPC.h" #include "DiscIO/Enums.h" +#include "DiscIO/RiivolutionPatcher.h" #include "DiscIO/VolumeDisc.h" #include "DiscIO/VolumeWad.h" @@ -547,6 +548,8 @@ bool CBoot::BootUp(std::unique_ptr boot) if (!std::visit(BootTitle(), boot->parameters)) return false; + DiscIO::Riivolution::ApplyPatchesToMemory(boot->riivolution_patches); + return true; } diff --git a/Source/Core/Core/Boot/Boot.h b/Source/Core/Core/Boot/Boot.h index 94fbe43ca3..4c27151c06 100644 --- a/Source/Core/Core/Boot/Boot.h +++ b/Source/Core/Core/Boot/Boot.h @@ -15,6 +15,7 @@ #include "Core/IOS/IOSC.h" #include "DiscIO/Blob.h" #include "DiscIO/Enums.h" +#include "DiscIO/RiivolutionParser.h" #include "DiscIO/VolumeDisc.h" #include "DiscIO/VolumeWad.h" @@ -78,6 +79,7 @@ struct BootParameters BootParameters(Parameters&& parameters_, const std::optional& savestate_path_ = {}); Parameters parameters; + std::vector riivolution_patches; std::optional savestate_path; bool delete_savestate = false; }; diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 4ea8b22aa5..8b3c7b77be 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -291,6 +291,8 @@ add_executable(dolphin-emu QtUtils/AspectRatioWidget.h ResourcePackManager.cpp ResourcePackManager.h + RiivolutionBootWidget.cpp + RiivolutionBootWidget.h Settings/AdvancedPane.cpp Settings/AdvancedPane.h Settings/AudioPane.cpp diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 27d4038832..6348a75019 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -179,6 +179,7 @@ + @@ -351,6 +352,7 @@ + diff --git a/Source/Core/DolphinQt/GameList/GameList.cpp b/Source/Core/DolphinQt/GameList/GameList.cpp index cd6ea5e2b2..5fc48eb2d6 100644 --- a/Source/Core/DolphinQt/GameList/GameList.cpp +++ b/Source/Core/DolphinQt/GameList/GameList.cpp @@ -353,6 +353,11 @@ void GameList::ShowContextMenu(const QPoint&) if (DiscIO::IsDisc(platform)) { + menu->addAction(tr("Start with Riivolution Patches..."), this, + &GameList::StartWithRiivolution); + + menu->addSeparator(); + menu->addAction(tr("Set as &Default ISO"), this, &GameList::SetDefaultISO); if (game->ShouldAllowConversion()) @@ -587,6 +592,15 @@ void GameList::UninstallWAD() result_dialog.exec(); } +void GameList::StartWithRiivolution() +{ + const auto game = GetSelectedGame(); + if (!game) + return; + + emit OnStartWithRiivolution(*game); +} + void GameList::SetDefaultISO() { const auto game = GetSelectedGame(); diff --git a/Source/Core/DolphinQt/GameList/GameList.h b/Source/Core/DolphinQt/GameList/GameList.h index 4c96464cf8..5f972e8762 100644 --- a/Source/Core/DolphinQt/GameList/GameList.h +++ b/Source/Core/DolphinQt/GameList/GameList.h @@ -50,6 +50,7 @@ public: signals: void GameSelected(); + void OnStartWithRiivolution(const UICommon::GameFile& game); void NetPlayHost(const UICommon::GameFile& game); void SelectionChanged(std::shared_ptr game_file); void OpenGeneralSettings(); @@ -62,6 +63,7 @@ private: void OpenWiiSaveFolder(); void OpenGCSaveFolder(); void OpenWiki(); + void StartWithRiivolution(); void SetDefaultISO(); void DeleteFile(); #ifdef _WIN32 diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 5d3366b914..a581f9516b 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -58,7 +58,9 @@ #include "Core/State.h" #include "Core/WiiUtils.h" +#include "DiscIO/DirectoryBlob.h" #include "DiscIO/NANDImporter.h" +#include "DiscIO/RiivolutionPatcher.h" #include "DolphinQt/AboutDialog.h" #include "DolphinQt/CheatsManager.h" @@ -100,6 +102,7 @@ #include "DolphinQt/RenderWidget.h" #include "DolphinQt/ResourcePackManager.h" #include "DolphinQt/Resources.h" +#include "DolphinQt/RiivolutionBootWidget.h" #include "DolphinQt/SearchBar.h" #include "DolphinQt/Settings.h" #include "DolphinQt/TAS/GCTASInputWindow.h" @@ -643,6 +646,8 @@ void MainWindow::ConnectGameList() { connect(m_game_list, &GameList::GameSelected, this, [this]() { Play(); }); connect(m_game_list, &GameList::NetPlayHost, this, &MainWindow::NetPlayHost); + connect(m_game_list, &GameList::OnStartWithRiivolution, this, + &MainWindow::ShowRiivolutionBootWidget); connect(m_game_list, &GameList::OpenGeneralSettings, this, &MainWindow::ShowGeneralWindow); } @@ -1807,6 +1812,39 @@ void MainWindow::ShowCheatsManager() m_cheats_manager->show(); } +void MainWindow::ShowRiivolutionBootWidget(const UICommon::GameFile& game) +{ + auto second_game = m_game_list->FindSecondDisc(game); + std::vector paths = {game.GetFilePath()}; + if (second_game != nullptr) + paths.push_back(second_game->GetFilePath()); + std::unique_ptr boot_params = + BootParameters::GenerateFromFile(paths, std::nullopt); + if (!boot_params) + return; + if (!std::holds_alternative(boot_params->parameters)) + return; + + auto& disc = std::get(boot_params->parameters); + RiivolutionBootWidget w(disc.volume->GetGameID(), disc.volume->GetRevision(), + disc.volume->GetDiscNumber(), this); + w.exec(); + if (!w.ShouldBoot()) + return; + + if (!w.GetPatches().empty()) + { + disc.volume = DiscIO::CreateDisc(DiscIO::DirectoryBlobReader::Create( + std::move(disc.volume), + [&](std::vector* fst, DiscIO::FSTBuilderNode* dol_node) { + DiscIO::Riivolution::ApplyPatchesToFiles(w.GetPatches(), fst, dol_node); + })); + boot_params->riivolution_patches = std::move(w.GetPatches()); + } + + StartGame(std::move(boot_params)); +} + void MainWindow::Show() { if (!Settings::Instance().IsBatchModeEnabled()) diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h index d8dce27e5e..c0e97af96d 100644 --- a/Source/Core/DolphinQt/MainWindow.h +++ b/Source/Core/DolphinQt/MainWindow.h @@ -157,6 +157,7 @@ private: void ShowMemcardManager(); void ShowResourcePackManager(); void ShowCheatsManager(); + void ShowRiivolutionBootWidget(const UICommon::GameFile& game); void NetPlayInit(); bool NetPlayJoin(); diff --git a/Source/Core/DolphinQt/RiivolutionBootWidget.cpp b/Source/Core/DolphinQt/RiivolutionBootWidget.cpp new file mode 100644 index 0000000000..9c74412da5 --- /dev/null +++ b/Source/Core/DolphinQt/RiivolutionBootWidget.cpp @@ -0,0 +1,195 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/RiivolutionBootWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common/FileSearch.h" +#include "Common/FileUtil.h" +#include "Common/StringUtil.h" +#include "DiscIO/RiivolutionParser.h" +#include "DolphinQt/QtUtils/ModalMessageBox.h" + +struct GuiRiivolutionPatchIndex +{ + size_t m_disc_index; + size_t m_section_index; + size_t m_option_index; + size_t m_choice_index; +}; + +Q_DECLARE_METATYPE(GuiRiivolutionPatchIndex); + +RiivolutionBootWidget::RiivolutionBootWidget(std::string game_id, std::optional revision, + std::optional disc, QWidget* parent) + : QDialog(parent), m_game_id(std::move(game_id)), m_revision(revision), m_disc_number(disc) +{ + setWindowTitle(tr("Start with Riivolution Patches")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + CreateWidgets(); + LoadMatchingXMLs(); + + resize(QSize(400, 600)); +} + +RiivolutionBootWidget::~RiivolutionBootWidget() = default; + +void RiivolutionBootWidget::CreateWidgets() +{ + auto* open_xml_button = new QPushButton(tr("Open Riivolution XML...")); + auto* boot_game_button = new QPushButton(tr("Start")); + boot_game_button->setDefault(true); + auto* group_box = new QGroupBox(); + auto* scroll_area = new QScrollArea(); + + auto* stretch_helper = new QVBoxLayout(); + m_patch_section_layout = new QVBoxLayout(); + stretch_helper->addLayout(m_patch_section_layout); + stretch_helper->addStretch(); + group_box->setLayout(stretch_helper); + scroll_area->setWidget(group_box); + scroll_area->setWidgetResizable(true); + + auto* button_layout = new QHBoxLayout(); + button_layout->addStretch(); + button_layout->addWidget(open_xml_button, 0, Qt::AlignRight); + button_layout->addWidget(boot_game_button, 0, Qt::AlignRight); + + auto* layout = new QVBoxLayout(); + layout->addWidget(scroll_area); + layout->addLayout(button_layout); + setLayout(layout); + + connect(open_xml_button, &QPushButton::clicked, this, &RiivolutionBootWidget::OpenXML); + connect(boot_game_button, &QPushButton::clicked, this, &RiivolutionBootWidget::BootGame); +} + +void RiivolutionBootWidget::LoadMatchingXMLs() +{ + const std::string& riivolution_dir = File::GetUserPath(D_RIIVOLUTION_IDX); + for (const std::string& path : Common::DoFileSearch({riivolution_dir + "riivolution"}, {".xml"})) + { + auto parsed = DiscIO::Riivolution::ParseFile(path); + if (!parsed || !parsed->IsValidForGame(m_game_id, m_revision, m_disc_number)) + continue; + MakeGUIForParsedFile(path, *parsed); + } +} + +void RiivolutionBootWidget::OpenXML() +{ + const std::string& riivolution_dir = File::GetUserPath(D_RIIVOLUTION_IDX); + QStringList paths = QFileDialog::getOpenFileNames( + this, tr("Select Riivolution XML file"), QString::fromStdString(riivolution_dir), + QStringLiteral("%1 (*.xml);;%2 (*)").arg(tr("Riivolution XML files")).arg(tr("All Files"))); + if (paths.isEmpty()) + return; + + for (const QString& path : paths) + { + std::string p = path.toStdString(); + auto parsed = DiscIO::Riivolution::ParseFile(p); + if (!parsed) + { + ModalMessageBox::warning( + this, tr("Failed loading XML."), + tr("Did not recognize %1 as a valid Riivolution XML file.").arg(path)); + continue; + } + + if (!parsed->IsValidForGame(m_game_id, m_revision, m_disc_number)) + { + ModalMessageBox::warning( + this, tr("Invalid game."), + tr("The patches in %1 are not for the selected game or game revision.").arg(path)); + continue; + } + + MakeGUIForParsedFile(p, *parsed); + } +} + +void RiivolutionBootWidget::MakeGUIForParsedFile(const std::string& path, + DiscIO::Riivolution::Disc input_disc) +{ + const size_t disc_index = m_discs.size(); + const auto& disc = m_discs.emplace_back(std::move(input_disc)); + + for (size_t section_index = 0; section_index < disc.m_sections.size(); ++section_index) + { + const auto& section = disc.m_sections[section_index]; + auto* group_box = new QGroupBox(QString::fromStdString(section.m_name)); + auto* grid_layout = new QGridLayout(); + group_box->setLayout(grid_layout); + + int row = 0; + for (size_t option_index = 0; option_index < section.m_options.size(); ++option_index) + { + const auto& option = section.m_options[option_index]; + auto* label = new QLabel(QString::fromStdString(option.m_name)); + auto* selection = new QComboBox(); + const GuiRiivolutionPatchIndex gui_disabled_index{disc_index, section_index, option_index, 0}; + selection->addItem(tr("Disabled"), QVariant::fromValue(gui_disabled_index)); + for (size_t choice_index = 0; choice_index < option.m_choices.size(); ++choice_index) + { + const auto& choice = option.m_choices[choice_index]; + const GuiRiivolutionPatchIndex gui_index{disc_index, section_index, option_index, + choice_index + 1}; + selection->addItem(QString::fromStdString(choice.m_name), QVariant::fromValue(gui_index)); + } + if (option.m_selected_choice <= option.m_choices.size()) + selection->setCurrentIndex(static_cast(option.m_selected_choice)); + + connect(selection, qOverload(&QComboBox::currentIndexChanged), this, + [this, selection](int idx) { + const auto gui_index = selection->currentData().value(); + auto& disc = m_discs[gui_index.m_disc_index]; + auto& section = disc.m_sections[gui_index.m_section_index]; + auto& option = section.m_options[gui_index.m_option_index]; + option.m_selected_choice = static_cast(gui_index.m_choice_index); + }); + + grid_layout->addWidget(label, row, 0, 1, 1); + grid_layout->addWidget(selection, row, 1, 1, 1); + ++row; + } + + m_patch_section_layout->addWidget(group_box); + } +} + +void RiivolutionBootWidget::BootGame() +{ + const std::string& riivolution_dir = File::GetUserPath(D_RIIVOLUTION_IDX); + + m_patches.clear(); + for (const auto& disc : m_discs) + { + auto patches = disc.GeneratePatches(m_game_id); + + // set the root path for each patch + for (auto& patch : patches) + { + if (patch.m_root.empty()) + SplitPath(disc.m_xml_path, &patch.m_root, nullptr, nullptr); + else + patch.m_root = riivolution_dir + "/" + patch.m_root; + } + + m_patches.insert(m_patches.end(), patches.begin(), patches.end()); + } + + m_should_boot = true; + close(); +} diff --git a/Source/Core/DolphinQt/RiivolutionBootWidget.h b/Source/Core/DolphinQt/RiivolutionBootWidget.h new file mode 100644 index 0000000000..5168099f33 --- /dev/null +++ b/Source/Core/DolphinQt/RiivolutionBootWidget.h @@ -0,0 +1,45 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include + +#include "Common/CommonTypes.h" +#include "DiscIO/RiivolutionParser.h" + +class QPushButton; +class QVBoxLayout; + +class RiivolutionBootWidget : public QDialog +{ + Q_OBJECT +public: + explicit RiivolutionBootWidget(std::string game_id, std::optional revision, + std::optional disc, QWidget* parent = nullptr); + ~RiivolutionBootWidget(); + + bool ShouldBoot() const { return m_should_boot; } + std::vector& GetPatches() { return m_patches; } + +private: + void CreateWidgets(); + + void LoadMatchingXMLs(); + void OpenXML(); + void MakeGUIForParsedFile(const std::string& path, DiscIO::Riivolution::Disc input_disc); + void BootGame(); + + std::string m_game_id; + std::optional m_revision; + std::optional m_disc_number; + + bool m_should_boot = false; + std::vector m_discs; + std::vector m_patches; + + QVBoxLayout* m_patch_section_layout; +};