| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| // Copyright 2021 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #include <optional> | ||
| #include <string> | ||
| #include <string_view> | ||
| #include <vector> | ||
|
|
||
| #include "DiscIO/GameModDescriptor.h" | ||
|
|
||
| #include <picojson.h> | ||
|
|
||
| #include "Common/IOFile.h" | ||
| #include "Common/MathUtil.h" | ||
| #include "Common/StringUtil.h" | ||
|
|
||
| namespace DiscIO | ||
| { | ||
| static std::string MakeAbsolute(const std::string& directory, const std::string& path) | ||
| { | ||
| #ifdef _WIN32 | ||
| return PathToString(StringToPath(directory) / StringToPath(path)); | ||
| #else | ||
| if (StringBeginsWith(path, "/")) | ||
| return path; | ||
| return directory + "/" + path; | ||
| #endif | ||
| } | ||
|
|
||
| std::optional<GameModDescriptor> ParseGameModDescriptorFile(const std::string& filename) | ||
| { | ||
| ::File::IOFile f(filename, "rb"); | ||
| if (!f) | ||
| return std::nullopt; | ||
|
|
||
| std::vector<char> data; | ||
| data.resize(f.GetSize()); | ||
| if (!f.ReadBytes(data.data(), data.size())) | ||
| return std::nullopt; | ||
|
|
||
| #ifdef _WIN32 | ||
| std::string path = ReplaceAll(filename, "\\", "/"); | ||
| #else | ||
| const std::string& path = filename; | ||
| #endif | ||
| return ParseGameModDescriptorString(std::string_view(data.data(), data.size()), path); | ||
| } | ||
|
|
||
| static std::vector<GameModDescriptorRiivolutionPatchOption> | ||
| ParseRiivolutionOptions(const picojson::array& array) | ||
| { | ||
| std::vector<GameModDescriptorRiivolutionPatchOption> options; | ||
| for (const auto& option_object : array) | ||
| { | ||
| if (!option_object.is<picojson::object>()) | ||
| continue; | ||
|
|
||
| auto& option = options.emplace_back(); | ||
| for (const auto& [key, value] : option_object.get<picojson::object>()) | ||
| { | ||
| if (key == "section-name" && value.is<std::string>()) | ||
| option.section_name = value.get<std::string>(); | ||
| else if (key == "option-id" && value.is<std::string>()) | ||
| option.option_id = value.get<std::string>(); | ||
| else if (key == "option-name" && value.is<std::string>()) | ||
| option.option_name = value.get<std::string>(); | ||
| else if (key == "choice" && value.is<double>()) | ||
| option.choice = MathUtil::SaturatingCast<u32>(value.get<double>()); | ||
| } | ||
| } | ||
| return options; | ||
| } | ||
|
|
||
| static GameModDescriptorRiivolution ParseRiivolutionObject(const std::string& json_directory, | ||
| const picojson::object& object) | ||
| { | ||
| GameModDescriptorRiivolution r; | ||
| for (const auto& [element_key, element_value] : object) | ||
| { | ||
| if (element_key == "patches" && element_value.is<picojson::array>()) | ||
| { | ||
| for (const auto& patch_object : element_value.get<picojson::array>()) | ||
| { | ||
| if (!patch_object.is<picojson::object>()) | ||
| continue; | ||
|
|
||
| auto& patch = r.patches.emplace_back(); | ||
| for (const auto& [key, value] : patch_object.get<picojson::object>()) | ||
| { | ||
| if (key == "xml" && value.is<std::string>()) | ||
| patch.xml = MakeAbsolute(json_directory, value.get<std::string>()); | ||
| else if (key == "root" && value.is<std::string>()) | ||
| patch.root = MakeAbsolute(json_directory, value.get<std::string>()); | ||
| else if (key == "options" && value.is<picojson::array>()) | ||
| patch.options = ParseRiivolutionOptions(value.get<picojson::array>()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return r; | ||
| } | ||
|
|
||
| std::optional<GameModDescriptor> ParseGameModDescriptorString(std::string_view json, | ||
| std::string_view json_path) | ||
| { | ||
| std::string json_directory; | ||
| SplitPath(json_path, &json_directory, nullptr, nullptr); | ||
|
|
||
| picojson::value json_root; | ||
| std::string err; | ||
| picojson::parse(json_root, json.begin(), json.end(), &err); | ||
| if (!err.empty()) | ||
| return std::nullopt; | ||
| if (!json_root.is<picojson::object>()) | ||
| return std::nullopt; | ||
|
|
||
| GameModDescriptor descriptor; | ||
| bool is_valid_version = false; | ||
| for (const auto& [key, value] : json_root.get<picojson::object>()) | ||
| { | ||
| if (key == "version" && value.is<double>()) | ||
| { | ||
| is_valid_version = value.get<double>() == 1.0; | ||
| } | ||
| else if (key == "base-file" && value.is<std::string>()) | ||
| { | ||
| descriptor.base_file = MakeAbsolute(json_directory, value.get<std::string>()); | ||
| } | ||
| else if (key == "display-name" && value.is<std::string>()) | ||
| { | ||
| descriptor.display_name = value.get<std::string>(); | ||
| } | ||
| else if (key == "banner" && value.is<std::string>()) | ||
| { | ||
| descriptor.banner = MakeAbsolute(json_directory, value.get<std::string>()); | ||
| } | ||
| else if (key == "riivolution" && value.is<picojson::object>()) | ||
| { | ||
| descriptor.riivolution = | ||
| ParseRiivolutionObject(json_directory, value.get<picojson::object>()); | ||
| } | ||
| } | ||
| if (!is_valid_version) | ||
| return std::nullopt; | ||
| return descriptor; | ||
| } | ||
| } // namespace DiscIO |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| // Copyright 2021 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <optional> | ||
| #include <string> | ||
| #include <string_view> | ||
| #include <vector> | ||
|
|
||
| #include "Common/CommonTypes.h" | ||
|
|
||
| namespace DiscIO | ||
| { | ||
| struct GameModDescriptorRiivolutionPatchOption | ||
| { | ||
| std::string section_name; | ||
| std::string option_id; | ||
| std::string option_name; | ||
| u32 choice = 0; | ||
| }; | ||
|
|
||
| struct GameModDescriptorRiivolutionPatch | ||
| { | ||
| std::string xml; | ||
| std::string root; | ||
| std::vector<GameModDescriptorRiivolutionPatchOption> options; | ||
| }; | ||
|
|
||
| struct GameModDescriptorRiivolution | ||
| { | ||
| std::vector<GameModDescriptorRiivolutionPatch> patches; | ||
| }; | ||
|
|
||
| struct GameModDescriptor | ||
| { | ||
| std::string base_file; | ||
| std::string display_name; | ||
| std::string banner; | ||
| std::optional<GameModDescriptorRiivolution> riivolution = std::nullopt; | ||
| }; | ||
|
|
||
| std::optional<GameModDescriptor> ParseGameModDescriptorFile(const std::string& filename); | ||
| std::optional<GameModDescriptor> ParseGameModDescriptorString(std::string_view json, | ||
| std::string_view json_path); | ||
| } // namespace DiscIO |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,231 @@ | ||
| // Copyright 2021 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <map> | ||
| #include <memory> | ||
| #include <optional> | ||
| #include <string> | ||
| #include <string_view> | ||
| #include <vector> | ||
|
|
||
| #include "Common/CommonTypes.h" | ||
|
|
||
| namespace DiscIO | ||
| { | ||
| struct GameModDescriptorRiivolution; | ||
| } | ||
|
|
||
| namespace DiscIO::Riivolution | ||
| { | ||
| class FileDataLoader; | ||
|
|
||
| // Data to determine the game patches are valid for. | ||
| struct GameFilter | ||
| { | ||
| std::optional<std::string> m_game; | ||
| std::optional<std::string> m_developer; | ||
| std::optional<int> m_disc; | ||
| std::optional<int> m_version; | ||
| std::optional<std::vector<std::string>> m_regions; | ||
| }; | ||
|
|
||
| // Which patches will get activated by selecting a Choice in the Riivolution GUI. | ||
| struct PatchReference | ||
| { | ||
| std::string m_id; | ||
| std::map<std::string, std::string> m_params; | ||
| }; | ||
|
|
||
| // A single choice within an Option in the Riivolution GUI. | ||
| struct Choice | ||
| { | ||
| std::string m_name; | ||
| std::vector<PatchReference> m_patch_references; | ||
| }; | ||
|
|
||
| // A single option presented to the user in the Riivolution GUI. | ||
| struct Option | ||
| { | ||
| std::string m_name; | ||
| std::string m_id; | ||
| std::vector<Choice> m_choices; | ||
|
|
||
| // The currently selected patch choice in the m_choices vector. | ||
| // Note that this index is 1-based; 0 means no choice is selected and this Option is disabled. | ||
| u32 m_selected_choice; | ||
| }; | ||
|
|
||
| // A single page of options presented to the user in the Riivolution GUI. | ||
| struct Section | ||
| { | ||
| std::string m_name; | ||
| std::vector<Option> m_options; | ||
| }; | ||
|
|
||
| // Replaces, adds, or modifies a file on disc. | ||
| struct File | ||
| { | ||
| // Path of the file on disc to modify. | ||
| std::string m_disc; | ||
|
|
||
| // Path of the file on SD card to use for modification. | ||
| std::string m_external; | ||
|
|
||
| // If true, the file on the disc is truncated if the external file end is before the disc file | ||
| // end. If false, the bytes after the external file end stay as they were. | ||
| bool m_resize = true; | ||
|
|
||
| // If true, a new file is created if it does not already exist at the disc path. Otherwise this | ||
| // modification is ignored if the file does not exist on disc. | ||
| bool m_create = false; | ||
|
|
||
| // Offset of where to start replacing bytes in the on-disc file. | ||
| u32 m_offset = 0; | ||
|
|
||
| // Offset of where to start reading bytes in the external file. | ||
| u32 m_fileoffset = 0; | ||
|
|
||
| // Amount of bytes to copy from the external file to the disc file. | ||
| // If left zero, the entire file (starting at fileoffset) is used. | ||
| u32 m_length = 0; | ||
| }; | ||
|
|
||
| // Adds or modifies a folder on disc. | ||
| struct Folder | ||
| { | ||
| // Path of the folder on disc to modify. | ||
| // Can be left empty to replace files matching the filename without specifying the folder. | ||
| std::string m_disc; | ||
|
|
||
| // Path of the folder on SD card to use for modification. | ||
| std::string m_external; | ||
|
|
||
| // Like File::m_resize but for each file in the folder. | ||
| bool m_resize = true; | ||
|
|
||
| // Like File::m_create but for each file in the folder. | ||
| bool m_create = false; | ||
|
|
||
| // Whether to also traverse subdirectories. (TODO: of the disc folder? external folder? both?) | ||
| bool m_recursive = true; | ||
|
|
||
| // Like File::m_length but for each file in the folder. | ||
| u32 m_length = 0; | ||
| }; | ||
|
|
||
| // Redirects the save file from the Wii NAND to a folder on SD card. | ||
| struct Savegame | ||
| { | ||
| // The folder on SD card to use for the save files. Is created if it does not exist. | ||
| std::string m_external; | ||
|
|
||
| // If this is set to true and the external folder is empty or does not exist, the existing save on | ||
| // NAND is copied to the new folder on game boot. | ||
| bool m_clone = true; | ||
| }; | ||
|
|
||
| // Modifies the game RAM right before jumping into the game executable. | ||
| struct Memory | ||
| { | ||
| // Memory address where this modification takes place. | ||
| u32 m_offset = 0; | ||
|
|
||
| // Bytes to write to that address. | ||
| std::vector<u8> m_value; | ||
|
|
||
| // Like m_value, but read the bytes from a file instead. | ||
| std::string m_valuefile; | ||
|
|
||
| // If set, the memory at that address will be checked before the value is written, and the | ||
| // replacement value only written if the bytes there match this. | ||
| std::vector<u8> m_original; | ||
|
|
||
| // If true, this memory patch is an ocarina-style patch. | ||
| // TODO: I'm unsure what this means exactly, need to check some examples... | ||
| bool m_ocarina = false; | ||
|
|
||
| // If true, the offset is not known, and instead we should search for the m_original bytes in | ||
| // memory and replace them where found. Only searches in MEM1, and only replaces the first match. | ||
| bool m_search = false; | ||
|
|
||
| // For m_search. The byte stride between search points. | ||
| u32 m_align = 1; | ||
| }; | ||
|
|
||
| struct Patch | ||
| { | ||
| // Internal name of this patch. | ||
| std::string m_id; | ||
|
|
||
| // Defines a SD card path that all other paths are relative to. | ||
| // For actually loading file data Dolphin uses the loader below instead. | ||
| std::string m_root; | ||
|
|
||
| std::shared_ptr<FileDataLoader> m_file_data_loader; | ||
|
|
||
| std::vector<File> m_file_patches; | ||
| std::vector<Folder> m_folder_patches; | ||
| std::vector<Savegame> m_savegame_patches; | ||
| std::vector<Memory> m_memory_patches; | ||
|
|
||
| ~Patch(); | ||
| }; | ||
|
|
||
| struct Disc | ||
| { | ||
| // Riivolution version. Only '1' exists at time of writing. | ||
| int m_version; | ||
|
|
||
| // Info about which game and revision these patches are for. | ||
| GameFilter m_game_filter; | ||
|
|
||
| // The options shown to the user in the UI. | ||
| std::vector<Section> m_sections; | ||
|
|
||
| // The actual patch instructions. | ||
| std::vector<Patch> m_patches; | ||
|
|
||
| // The path to the parsed XML file. | ||
| std::string m_xml_path; | ||
|
|
||
| // Checks whether these patches are valid for the given game. | ||
| bool IsValidForGame(const std::string& game_id, std::optional<u16> revision, | ||
| std::optional<u8> disc_number) const; | ||
|
|
||
| // Transforms an abstract XML-parsed patch set into a concrete one, with only the selected | ||
| // patches applied and all placeholders replaced. | ||
| std::vector<Patch> GeneratePatches(const std::string& game_id) const; | ||
| }; | ||
|
|
||
| // Config format that remembers which patches are enabled/disabled for the next run. | ||
| // Some patches ship with pre-made config XMLs instead of baking their defaults into the actual | ||
| // patch XMLs, so it makes sense to support this format directly. | ||
| struct ConfigOption | ||
| { | ||
| // The identifier for the referenced Option. | ||
| std::string m_id; | ||
|
|
||
| // The selected Choice index. | ||
| u32 m_default; | ||
| }; | ||
|
|
||
| struct Config | ||
| { | ||
| // Config version. Riivolution itself only accepts '2' here. | ||
| int m_version = 2; | ||
| std::vector<ConfigOption> m_options; | ||
| }; | ||
|
|
||
| std::optional<Disc> ParseFile(const std::string& filename); | ||
| std::optional<Disc> ParseString(std::string_view xml, std::string xml_path); | ||
| std::vector<Patch> GenerateRiivolutionPatchesFromGameModDescriptor( | ||
| const GameModDescriptorRiivolution& descriptor, const std::string& game_id, | ||
| std::optional<u16> revision, std::optional<u8> disc_number); | ||
| std::optional<Config> ParseConfigFile(const std::string& filename); | ||
| std::optional<Config> ParseConfigString(std::string_view xml); | ||
| std::string WriteConfigString(const Config& config); | ||
| bool WriteConfigFile(const std::string& filename, const Config& config); | ||
| void ApplyConfigDefaults(Disc* disc, const Config& config); | ||
| } // namespace DiscIO::Riivolution |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // Copyright 2021 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <optional> | ||
| #include <string> | ||
| #include <string_view> | ||
| #include <vector> | ||
|
|
||
| #include "DiscIO/DirectoryBlob.h" | ||
| #include "DiscIO/RiivolutionParser.h" | ||
|
|
||
| namespace DiscIO::Riivolution | ||
| { | ||
| struct SavegameRedirect | ||
| { | ||
| std::string m_target_path; | ||
| bool m_clone; | ||
| }; | ||
|
|
||
| class FileDataLoader | ||
| { | ||
| public: | ||
| struct Node | ||
| { | ||
| std::string m_filename; | ||
| bool m_is_directory; | ||
| }; | ||
|
|
||
| virtual ~FileDataLoader(); | ||
| virtual std::optional<u64> GetExternalFileSize(std::string_view external_relative_path) = 0; | ||
| virtual std::vector<u8> GetFileContents(std::string_view external_relative_path) = 0; | ||
| virtual std::vector<Node> GetFolderContents(std::string_view external_relative_path) = 0; | ||
| virtual BuilderContentSource MakeContentSource(std::string_view external_relative_path, | ||
| u64 external_offset, u64 external_size, | ||
| u64 disc_offset) = 0; | ||
| virtual std::optional<std::string> | ||
| ResolveSavegameRedirectPath(std::string_view external_relative_path) = 0; | ||
| }; | ||
|
|
||
| class FileDataLoaderHostFS : public FileDataLoader | ||
| { | ||
| public: | ||
| // sd_root should be an absolute path to the folder representing our virtual SD card | ||
| // xml_path should be an absolute path to the parsed XML file | ||
| // patch_root should be the 'root' attribute given in the 'patch' or 'wiiroot' XML element | ||
| FileDataLoaderHostFS(std::string sd_root, const std::string& xml_path, | ||
| std::string_view patch_root); | ||
|
|
||
| std::optional<u64> GetExternalFileSize(std::string_view external_relative_path) override; | ||
| std::vector<u8> GetFileContents(std::string_view external_relative_path) override; | ||
| std::vector<FileDataLoader::Node> | ||
| GetFolderContents(std::string_view external_relative_path) override; | ||
| BuilderContentSource MakeContentSource(std::string_view external_relative_path, | ||
| u64 external_offset, u64 external_size, | ||
| u64 disc_offset) override; | ||
| std::optional<std::string> | ||
| ResolveSavegameRedirectPath(std::string_view external_relative_path) override; | ||
|
|
||
| private: | ||
| std::optional<std::string> MakeAbsoluteFromRelative(std::string_view external_relative_path); | ||
|
|
||
| std::string m_sd_root; | ||
| std::string m_patch_root; | ||
| }; | ||
|
|
||
| void ApplyPatchesToFiles(const std::vector<Patch>& patches, | ||
| std::vector<DiscIO::FSTBuilderNode>* fst, | ||
| DiscIO::FSTBuilderNode* dol_node); | ||
| void ApplyGeneralMemoryPatches(const std::vector<Patch>& patches); | ||
| void ApplyApploaderMemoryPatches(const std::vector<Patch>& patches, u32 ram_address, | ||
| u32 ram_length); | ||
| std::optional<SavegameRedirect> | ||
| ExtractSavegameRedirect(const std::vector<Patch>& riivolution_patches); | ||
| } // namespace DiscIO::Riivolution |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,281 @@ | ||
| // Copyright 2021 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #include "DolphinQt/RiivolutionBootWidget.h" | ||
|
|
||
| #include <unordered_map> | ||
|
|
||
| #include <fmt/format.h> | ||
|
|
||
| #include <QComboBox> | ||
| #include <QDialogButtonBox> | ||
| #include <QDir> | ||
| #include <QFileDialog> | ||
| #include <QFileInfo> | ||
| #include <QGridLayout> | ||
| #include <QGroupBox> | ||
| #include <QLabel> | ||
| #include <QLineEdit> | ||
| #include <QMetaType> | ||
| #include <QPushButton> | ||
| #include <QScrollArea> | ||
| #include <QVBoxLayout> | ||
|
|
||
| #include "Common/FileSearch.h" | ||
| #include "Common/FileUtil.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<u16> revision, | ||
| std::optional<u8> 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); | ||
| 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(const 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)}); | ||
|
|
||
| auto* disc_box = new QGroupBox(QFileInfo(QString::fromStdString(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<int>(option.m_selected_choice)); | ||
|
|
||
| connect(selection, qOverload<int>(&QComboBox::currentIndexChanged), this, | ||
| [this, selection](int idx) { | ||
| const auto gui_index = selection->currentData().value<GuiRiivolutionPatchIndex>(); | ||
| auto& disc = m_discs[gui_index.m_disc_index].disc; | ||
| 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<u32>(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<DiscIO::Riivolution::Config> | ||
| 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<std::string, DiscIO::Riivolution::Config> 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<DiscIO::Riivolution::FileDataLoaderHostFS>( | ||
| 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(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| // Copyright 2021 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <optional> | ||
| #include <string> | ||
|
|
||
| #include <QDialog> | ||
|
|
||
| #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<u16> revision, | ||
| std::optional<u8> disc, QWidget* parent = nullptr); | ||
| ~RiivolutionBootWidget(); | ||
|
|
||
| bool ShouldBoot() const { return m_should_boot; } | ||
| std::vector<DiscIO::Riivolution::Patch>& GetPatches() { return m_patches; } | ||
|
|
||
| private: | ||
| void CreateWidgets(); | ||
|
|
||
| void LoadMatchingXMLs(); | ||
| void OpenXML(); | ||
| void MakeGUIForParsedFile(const std::string& path, std::string root, | ||
| DiscIO::Riivolution::Disc input_disc); | ||
| std::optional<DiscIO::Riivolution::Config> LoadConfigXML(const std::string& root_directory); | ||
| void SaveConfigXMLs(); | ||
| void BootGame(); | ||
|
|
||
| std::string m_game_id; | ||
| std::optional<u16> m_revision; | ||
| std::optional<u8> m_disc_number; | ||
|
|
||
| bool m_should_boot = false; | ||
| struct DiscWithRoot | ||
| { | ||
| DiscIO::Riivolution::Disc disc; | ||
| std::string root; | ||
| }; | ||
| std::vector<DiscWithRoot> m_discs; | ||
| std::vector<DiscIO::Riivolution::Patch> m_patches; | ||
|
|
||
| QVBoxLayout* m_patch_section_layout; | ||
| }; |