// 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 #include #include #include #include #include "Common/FileSearch.h" #include "Common/FileUtil.h" #include "DiscIO/GameModDescriptor.h" #include "DiscIO/RiivolutionParser.h" #include "DiscIO/RiivolutionPatcher.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, std::string base_game_path, QWidget* parent) : QDialog(parent), m_game_id(std::move(game_id)), m_revision(revision), m_disc_number(disc), m_base_game_path(std::move(base_game_path)) { 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* save_preset_button = new QPushButton(tr("Save as Preset...")); 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(save_preset_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); connect(save_preset_button, &QPushButton::clicked, this, &RiivolutionBootWidget::SaveAsPreset); } void RiivolutionBootWidget::LoadMatchingXMLs() { const std::string& riivolution_dir = File::GetUserPath(D_RIIVOLUTION_IDX); const auto config = LoadConfigXML(riivolution_dir); 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; if (config) DiscIO::Riivolution::ApplyConfigDefaults(&*parsed, *config); MakeGUIForParsedFile(path, riivolution_dir, *parsed); } } static std::string FindRoot(const std::string& path) { // Try to set the virtual SD root to directory one up from current. // This mimics where the XML would be on a real SD card. QDir dir = QFileInfo(QString::fromStdString(path)).dir(); if (dir.cdUp()) return dir.absolutePath().toStdString(); return File::GetUserPath(D_RIIVOLUTION_IDX); } 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; } auto root = FindRoot(p); const auto config = LoadConfigXML(root); if (config) DiscIO::Riivolution::ApplyConfigDefaults(&*parsed, *config); MakeGUIForParsedFile(p, std::move(root), *parsed); } } void RiivolutionBootWidget::MakeGUIForParsedFile(std::string path, std::string root, DiscIO::Riivolution::Disc input_disc) { const size_t disc_index = m_discs.size(); const auto& disc = m_discs.emplace_back(DiscWithRoot{std::move(input_disc), std::move(root), std::move(path)}); auto* disc_box = new QGroupBox(QFileInfo(QString::fromStdString(disc.path)).fileName()); auto* disc_layout = new QVBoxLayout(); disc_box->setLayout(disc_layout); auto* xml_root_line_edit = new QLineEdit(QString::fromStdString(disc.root)); xml_root_line_edit->setReadOnly(true); auto* xml_root_layout = new QHBoxLayout(); auto* xml_root_open = new QPushButton(tr("...")); xml_root_layout->addWidget(new QLabel(tr("SD Root:")), 0); xml_root_layout->addWidget(xml_root_line_edit, 0); xml_root_layout->addWidget(xml_root_open, 0); disc_layout->addLayout(xml_root_layout); connect(xml_root_open, &QPushButton::clicked, this, [this, xml_root_line_edit, disc_index]() { QString dir = QDir::toNativeSeparators(QFileDialog::getExistingDirectory( this, tr("Select the Virtual SD Card Root"), xml_root_line_edit->text())); if (!dir.isEmpty()) { xml_root_line_edit->setText(dir); m_discs[disc_index].root = dir.toStdString(); } }); for (size_t section_index = 0; section_index < disc.disc.m_sections.size(); ++section_index) { const auto& section = disc.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& selected_disc = m_discs[gui_index.m_disc_index].disc; auto& selected_section = selected_disc.m_sections[gui_index.m_section_index]; auto& selected_option = selected_section.m_options[gui_index.m_option_index]; selected_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; } disc_layout->addWidget(group_box); } m_patch_section_layout->addWidget(disc_box); } std::optional RiivolutionBootWidget::LoadConfigXML(const std::string& root_directory) { // The way Riivolution stores settings only makes sense for standard game IDs. if (!(m_game_id.size() == 4 || m_game_id.size() == 6)) return std::nullopt; return DiscIO::Riivolution::ParseConfigFile( fmt::format("{}/riivolution/config/{}.xml", root_directory, m_game_id.substr(0, 4))); } void RiivolutionBootWidget::SaveConfigXMLs() { if (!(m_game_id.size() == 4 || m_game_id.size() == 6)) return; std::unordered_map map; for (const auto& disc : m_discs) { auto config = map.try_emplace(disc.root); auto& config_options = config.first->second.m_options; for (const auto& section : disc.disc.m_sections) { for (const auto& option : section.m_options) { std::string id = option.m_id.empty() ? (section.m_name + option.m_name) : option.m_id; config_options.emplace_back( DiscIO::Riivolution::ConfigOption{std::move(id), option.m_selected_choice}); } } } for (const auto& config : map) { DiscIO::Riivolution::WriteConfigFile( fmt::format("{}/riivolution/config/{}.xml", config.first, m_game_id.substr(0, 4)), config.second); } } void RiivolutionBootWidget::BootGame() { SaveConfigXMLs(); m_patches.clear(); for (const auto& disc : m_discs) { auto patches = disc.disc.GeneratePatches(m_game_id); // set the file loader for each patch for (auto& patch : patches) { patch.m_file_data_loader = std::make_shared( disc.root, disc.disc.m_xml_path, patch.m_root); } m_patches.insert(m_patches.end(), patches.begin(), patches.end()); } m_should_boot = true; close(); } void RiivolutionBootWidget::SaveAsPreset() { DiscIO::GameModDescriptor descriptor; descriptor.base_file = m_base_game_path; DiscIO::GameModDescriptorRiivolution riivolution_descriptor; for (const auto& disc : m_discs) { // filter out XMLs that don't actually contribute to the preset auto patches = disc.disc.GeneratePatches(m_game_id); if (patches.empty()) continue; auto& descriptor_patch = riivolution_descriptor.patches.emplace_back(); descriptor_patch.xml = disc.path; descriptor_patch.root = disc.root; for (const auto& section : disc.disc.m_sections) { for (const auto& option : section.m_options) { auto& descriptor_option = descriptor_patch.options.emplace_back(); descriptor_option.section_name = section.m_name; if (!option.m_id.empty()) descriptor_option.option_id = option.m_id; else descriptor_option.option_name = option.m_name; descriptor_option.choice = option.m_selected_choice; } } } if (!riivolution_descriptor.patches.empty()) descriptor.riivolution = std::move(riivolution_descriptor); QDir dir = QFileInfo(QString::fromStdString(m_base_game_path)).dir(); QString target_path = QFileDialog::getSaveFileName(this, tr("Save Preset"), dir.absolutePath(), QStringLiteral("%1 (*.json);;%2 (*)") .arg(tr("Dolphin Game Mod Preset")) .arg(tr("All Files"))); if (target_path.isEmpty()) return; descriptor.display_name = QFileInfo(target_path).fileName().toStdString(); auto dot = descriptor.display_name.rfind('.'); if (dot != std::string::npos) descriptor.display_name = descriptor.display_name.substr(0, dot); DiscIO::WriteGameModDescriptorFile(target_path.toStdString(), descriptor, true); }