From e26b59bab39ee5117b8a01f63d739f9608009e1c Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Thu, 23 Sep 2021 03:21:53 +0200 Subject: Core: Add RiivolutionParser to parse a Riivolution XML. --- Source/Core/DiscIO/RiivolutionParser.cpp | 336 +++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 Source/Core/DiscIO/RiivolutionParser.cpp (limited to 'Source/Core/DiscIO/RiivolutionParser.cpp') diff --git a/Source/Core/DiscIO/RiivolutionParser.cpp b/Source/Core/DiscIO/RiivolutionParser.cpp new file mode 100644 index 0000000000..3162c40d43 --- /dev/null +++ b/Source/Core/DiscIO/RiivolutionParser.cpp @@ -0,0 +1,336 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DiscIO/RiivolutionParser.h" + +#include +#include +#include +#include + +#include + +#include "Common/FileUtil.h" +#include "Common/IOFile.h" +#include "Common/StringUtil.h" + +namespace DiscIO::Riivolution +{ +std::optional ParseFile(const std::string& filename) +{ + ::File::IOFile f(filename, "rb"); + if (!f) + return std::nullopt; + + std::vector data; + data.resize(f.GetSize()); + if (!f.ReadBytes(data.data(), data.size())) + return std::nullopt; + + return ParseString(std::string_view(data.data(), data.size()), filename); +} + +static std::map ReadParams(const pugi::xml_node& node, + std::map params = {}) +{ + for (const auto& param_node : node.children("param")) + { + const std::string param_name = param_node.attribute("name").as_string(); + const std::string param_value = param_node.attribute("value").as_string(); + params[param_name] = param_value; + } + return params; +} + +static std::vector ReadHexString(std::string_view sv) +{ + if ((sv.size() % 2) == 1) + return {}; + if (StringBeginsWith(sv, "0x") || StringBeginsWith(sv, "0X")) + sv = sv.substr(2); + + std::vector result; + result.reserve(sv.size() / 2); + while (!sv.empty()) + { + u8 tmp; + if (!TryParse(std::string(sv.substr(0, 2)), &tmp, 16)) + return {}; + result.push_back(tmp); + sv = sv.substr(2); + } + return result; +}; + +std::optional ParseString(std::string_view xml, std::string xml_path) +{ + pugi::xml_document doc; + const auto parse_result = doc.load_buffer(xml.data(), xml.size()); + if (!parse_result) + return std::nullopt; + + const auto wiidisc = doc.child("wiidisc"); + if (!wiidisc) + return std::nullopt; + + Disc disc; + disc.m_xml_path = std::move(xml_path); + disc.m_version = wiidisc.attribute("version").as_int(-1); + if (disc.m_version != 1) + return std::nullopt; + const std::string default_root = wiidisc.attribute("root").as_string(); + + const auto id = wiidisc.child("id"); + if (id) + { + for (const auto& attribute : id.attributes()) + { + const std::string_view attribute_name(attribute.name()); + if (attribute_name == "game") + disc.m_game_filter.m_game = attribute.as_string(); + else if (attribute_name == "developer") + disc.m_game_filter.m_developer = attribute.as_string(); + else if (attribute_name == "disc") + disc.m_game_filter.m_disc = attribute.as_int(-1); + else if (attribute_name == "version") + disc.m_game_filter.m_version = attribute.as_int(-1); + } + + auto xml_regions = id.children("region"); + if (xml_regions.begin() != xml_regions.end()) + { + std::vector regions; + for (const auto& region : xml_regions) + regions.push_back(region.attribute("type").as_string()); + disc.m_game_filter.m_regions = std::move(regions); + } + } + + const auto options = wiidisc.child("options"); + if (options) + { + for (const auto& section_node : options.children("section")) + { + Section& section = disc.m_sections.emplace_back(); + section.m_name = section_node.attribute("name").as_string(); + for (const auto& option_node : section_node.children("option")) + { + Option& option = section.m_options.emplace_back(); + option.m_id = option_node.attribute("id").as_string(); + option.m_name = option_node.attribute("name").as_string(); + option.m_selected_choice = option_node.attribute("default").as_uint(0); + auto option_params = ReadParams(option_node); + for (const auto& choice_node : option_node.children("choice")) + { + Choice& choice = option.m_choices.emplace_back(); + choice.m_name = choice_node.attribute("name").as_string(); + auto choice_params = ReadParams(choice_node, option_params); + for (const auto& patchref_node : choice_node.children("patch")) + { + PatchReference& patchref = choice.m_patch_references.emplace_back(); + patchref.m_id = patchref_node.attribute("id").as_string(); + patchref.m_params = ReadParams(patchref_node, choice_params); + } + } + } + } + for (const auto& macro_node : options.children("macros")) + { + const std::string macro_id = macro_node.attribute("id").as_string(); + for (auto& section : disc.m_sections) + { + auto option_to_clone = std::find_if(section.m_options.begin(), section.m_options.end(), + [&](const Option& o) { return o.m_id == macro_id; }); + if (option_to_clone == section.m_options.end()) + continue; + + Option cloned_option = *option_to_clone; + cloned_option.m_name = macro_node.attribute("name").as_string(); + for (auto& choice : cloned_option.m_choices) + for (auto& patch_ref : choice.m_patch_references) + patch_ref.m_params = ReadParams(macro_node, patch_ref.m_params); + } + } + } + + const auto patches = wiidisc.children("patch"); + for (const auto& patch_node : patches) + { + Patch& patch = disc.m_patches.emplace_back(); + patch.m_id = patch_node.attribute("id").as_string(); + patch.m_root = patch_node.attribute("root").as_string(); + if (patch.m_root.empty()) + patch.m_root = default_root; + + for (const auto& patch_subnode : patch_node.children()) + { + const std::string_view patch_name(patch_subnode.name()); + if (patch_name == "file") + { + auto& file = patch.m_file_patches.emplace_back(); + file.m_disc = patch_subnode.attribute("disc").as_string(); + file.m_external = patch_subnode.attribute("external").as_string(); + file.m_resize = patch_subnode.attribute("resize").as_bool(true); + file.m_create = patch_subnode.attribute("create").as_bool(false); + file.m_offset = patch_subnode.attribute("offset").as_uint(0); + file.m_fileoffset = patch_subnode.attribute("fileoffset").as_uint(0); + file.m_length = patch_subnode.attribute("length").as_uint(0); + } + else if (patch_name == "folder") + { + auto& folder = patch.m_folder_patches.emplace_back(); + folder.m_disc = patch_subnode.attribute("disc").as_string(); + folder.m_external = patch_subnode.attribute("external").as_string(); + folder.m_resize = patch_subnode.attribute("resize").as_bool(true); + folder.m_create = patch_subnode.attribute("create").as_bool(false); + folder.m_recursive = patch_subnode.attribute("recursive").as_bool(true); + folder.m_length = patch_subnode.attribute("length").as_uint(0); + } + else if (patch_name == "savegame") + { + auto& savegame = patch.m_savegame_patches.emplace_back(); + savegame.m_external = patch_subnode.attribute("external").as_string(); + savegame.m_clone = patch_subnode.attribute("clone").as_bool(true); + } + else if (patch_name == "memory") + { + auto& memory = patch.m_memory_patches.emplace_back(); + memory.m_offset = patch_subnode.attribute("offset").as_uint(0); + memory.m_value = ReadHexString(patch_subnode.attribute("value").as_string()); + memory.m_valuefile = patch_subnode.attribute("valuefile").as_string(); + memory.m_original = ReadHexString(patch_subnode.attribute("original").as_string()); + memory.m_ocarina = patch_subnode.attribute("ocarina").as_bool(false); + memory.m_search = patch_subnode.attribute("search").as_bool(false); + memory.m_align = patch_subnode.attribute("align").as_uint(1); + } + } + } + + return disc; +} + +static bool CheckRegion(const std::vector& xml_regions, std::string_view game_region) +{ + if (xml_regions.begin() == xml_regions.end()) + return true; + + for (const auto& region : xml_regions) + { + if (region == game_region) + return true; + } + + return false; +} + +bool Disc::IsValidForGame(const std::string& game_id, std::optional revision, + std::optional disc_number) const +{ + if (game_id.size() != 6) + return false; + + const std::string_view game_id_full = std::string_view(game_id); + const std::string_view game_region = game_id_full.substr(3, 1); + const std::string_view game_developer = game_id_full.substr(4, 2); + const int disc_number_int = std::optional(disc_number).value_or(-1); + const int revision_int = std::optional(revision).value_or(-1); + + if (m_game_filter.m_game && !StringBeginsWith(game_id_full, *m_game_filter.m_game)) + return false; + if (m_game_filter.m_developer && game_developer != *m_game_filter.m_developer) + return false; + if (m_game_filter.m_disc && disc_number_int != *m_game_filter.m_disc) + return false; + if (m_game_filter.m_version && revision_int != *m_game_filter.m_version) + return false; + if (m_game_filter.m_regions && !CheckRegion(*m_game_filter.m_regions, game_region)) + return false; + + return true; +} + +std::vector Disc::GeneratePatches(const std::string& game_id) const +{ + const std::string_view game_id_full = std::string_view(game_id); + const std::string_view game_id_no_region = game_id_full.substr(0, 3); + const std::string_view game_region = game_id_full.substr(3, 1); + const std::string_view game_developer = game_id_full.substr(4, 2); + + const auto replace_variables = + [&](std::string_view sv, + const std::vector>& replacements) { + std::string result; + result.reserve(sv.size()); + while (!sv.empty()) + { + bool replaced = false; + for (const auto& r : replacements) + { + if (StringBeginsWith(sv, r.first)) + { + for (char c : r.second) + result.push_back(c); + sv = sv.substr(r.first.size()); + replaced = true; + break; + } + } + if (replaced) + continue; + result.push_back(sv[0]); + sv = sv.substr(1); + } + return result; + }; + + // Take only selected patches, replace placeholders in all strings, and return them. + std::vector active_patches; + for (const auto& section : m_sections) + { + for (const auto& option : section.m_options) + { + const u32 selected = option.m_selected_choice; + if (selected == 0 || selected > option.m_choices.size()) + continue; + const Choice& choice = option.m_choices[selected - 1]; + for (const auto& patch_ref : choice.m_patch_references) + { + const auto patch = std::find_if(m_patches.begin(), m_patches.end(), + [&](const Patch& p) { return patch_ref.m_id == p.m_id; }); + if (patch == m_patches.end()) + continue; + + std::vector> replacements; + replacements.emplace_back(std::pair{"{$__gameid}", game_id_no_region}); + replacements.emplace_back(std::pair{"{$__region}", game_region}); + replacements.emplace_back(std::pair{"{$__maker}", game_developer}); + for (const auto& param : patch_ref.m_params) + replacements.emplace_back(std::pair{"{$" + param.first + "}", param.second}); + + Patch& new_patch = active_patches.emplace_back(*patch); + new_patch.m_root = replace_variables(new_patch.m_root, replacements); + for (auto& file : new_patch.m_file_patches) + { + file.m_disc = replace_variables(file.m_disc, replacements); + file.m_external = replace_variables(file.m_external, replacements); + } + for (auto& folder : new_patch.m_folder_patches) + { + folder.m_disc = replace_variables(folder.m_disc, replacements); + folder.m_external = replace_variables(folder.m_external, replacements); + } + for (auto& savegame : new_patch.m_savegame_patches) + { + savegame.m_external = replace_variables(savegame.m_external, replacements); + } + for (auto& memory : new_patch.m_memory_patches) + { + memory.m_valuefile = replace_variables(memory.m_valuefile, replacements); + } + } + } + } + + return active_patches; +} +} // namespace DiscIO::Riivolution -- cgit v1.2.3 From 6ec4af7ea413ace595290d336bd88b30e035fa2f Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Tue, 28 Sep 2021 06:12:45 +0200 Subject: RiivolutionPatcher: Load external files with a layer of indirection during the patching process to properly resolve the paths given in the XML. This also may eventually allow loading patches from sources other than the 1:1 expected file structure host file system, such as memory or an archive file. --- Source/Core/DiscIO/RiivolutionParser.cpp | 3 +++ 1 file changed, 3 insertions(+) (limited to 'Source/Core/DiscIO/RiivolutionParser.cpp') diff --git a/Source/Core/DiscIO/RiivolutionParser.cpp b/Source/Core/DiscIO/RiivolutionParser.cpp index 3162c40d43..63a830d1c5 100644 --- a/Source/Core/DiscIO/RiivolutionParser.cpp +++ b/Source/Core/DiscIO/RiivolutionParser.cpp @@ -13,9 +13,12 @@ #include "Common/FileUtil.h" #include "Common/IOFile.h" #include "Common/StringUtil.h" +#include "DiscIO/RiivolutionPatcher.h" namespace DiscIO::Riivolution { +Patch::~Patch() = default; + std::optional ParseFile(const std::string& filename) { ::File::IOFile f(filename, "rb"); -- cgit v1.2.3 From 6394960f5412802f24bf664d1574f9126b1bb799 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Sat, 9 Oct 2021 03:17:38 +0200 Subject: Core: Add ability to specify and launch a riivolution-modded game via a .json file. --- Source/Core/DiscIO/RiivolutionParser.cpp | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) (limited to 'Source/Core/DiscIO/RiivolutionParser.cpp') diff --git a/Source/Core/DiscIO/RiivolutionParser.cpp b/Source/Core/DiscIO/RiivolutionParser.cpp index 63a830d1c5..e8a284e07b 100644 --- a/Source/Core/DiscIO/RiivolutionParser.cpp +++ b/Source/Core/DiscIO/RiivolutionParser.cpp @@ -13,6 +13,7 @@ #include "Common/FileUtil.h" #include "Common/IOFile.h" #include "Common/StringUtil.h" +#include "DiscIO/GameModDescriptor.h" #include "DiscIO/RiivolutionPatcher.h" namespace DiscIO::Riivolution @@ -336,4 +337,47 @@ std::vector Disc::GeneratePatches(const std::string& game_id) const return active_patches; } + +std::vector GenerateRiivolutionPatchesFromGameModDescriptor( + const GameModDescriptorRiivolution& descriptor, const std::string& game_id, + std::optional revision, std::optional disc_number) +{ + std::vector result; + for (const auto& patch_info : descriptor.patches) + { + auto parsed = ParseFile(patch_info.xml); + if (!parsed || !parsed->IsValidForGame(game_id, revision, disc_number)) + continue; + + for (auto& section : parsed->m_sections) + { + for (auto& option : section.m_options) + { + const auto* info = [&]() -> const DiscIO::GameModDescriptorRiivolutionPatchOption* { + for (const auto& o : patch_info.options) + { + if (o.section_name == section.m_name) + { + if (!o.option_id.empty() && o.option_id == option.m_id) + return &o; + if (!o.option_name.empty() && o.option_name == option.m_name) + return &o; + } + } + return nullptr; + }(); + if (info && info->choice <= option.m_choices.size()) + option.m_selected_choice = info->choice; + } + } + + for (auto& p : parsed->GeneratePatches(game_id)) + { + p.m_file_data_loader = std::make_shared( + patch_info.root, parsed->m_xml_path, p.m_root); + result.emplace_back(std::move(p)); + } + } + return result; +} } // namespace DiscIO::Riivolution -- cgit v1.2.3 From 2be62d94fc659b64c7de1d53cd0ef9a09a375570 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Tue, 19 Oct 2021 23:30:02 +0200 Subject: RiivolutionParser: Add code to read and write the Riivolution config XMLs that remember the last selection. --- Source/Core/DiscIO/RiivolutionParser.cpp | 103 +++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) (limited to 'Source/Core/DiscIO/RiivolutionParser.cpp') diff --git a/Source/Core/DiscIO/RiivolutionParser.cpp b/Source/Core/DiscIO/RiivolutionParser.cpp index e8a284e07b..92f37444a0 100644 --- a/Source/Core/DiscIO/RiivolutionParser.cpp +++ b/Source/Core/DiscIO/RiivolutionParser.cpp @@ -4,6 +4,7 @@ #include "DiscIO/RiivolutionParser.h" #include +#include #include #include #include @@ -380,4 +381,106 @@ std::vector GenerateRiivolutionPatchesFromGameModDescriptor( } return result; } + +std::optional ParseConfigFile(const std::string& filename) +{ + ::File::IOFile f(filename, "rb"); + if (!f) + return std::nullopt; + + std::vector data; + data.resize(f.GetSize()); + if (!f.ReadBytes(data.data(), data.size())) + return std::nullopt; + + return ParseConfigString(std::string_view(data.data(), data.size())); +} + +std::optional ParseConfigString(std::string_view xml) +{ + pugi::xml_document doc; + const auto parse_result = doc.load_buffer(xml.data(), xml.size()); + if (!parse_result) + return std::nullopt; + + const auto riivolution = doc.child("riivolution"); + if (!riivolution) + return std::nullopt; + + Config config; + config.m_version = riivolution.attribute("version").as_int(-1); + if (config.m_version != 2) + return std::nullopt; + + const auto options = riivolution.children("option"); + for (const auto& option_node : options) + { + auto& option = config.m_options.emplace_back(); + option.m_id = option_node.attribute("id").as_string(); + option.m_default = option_node.attribute("default").as_uint(0); + } + + return config; +} + +std::string WriteConfigString(const Config& config) +{ + pugi::xml_document doc; + auto riivolution = doc.append_child("riivolution"); + riivolution.append_attribute("version").set_value(config.m_version); + for (const auto& option : config.m_options) + { + auto option_node = riivolution.append_child("option"); + option_node.append_attribute("id").set_value(option.m_id.c_str()); + option_node.append_attribute("default").set_value(option.m_default); + } + + std::stringstream ss; + doc.print(ss, " "); + return ss.str(); +} + +bool WriteConfigFile(const std::string& filename, const Config& config) +{ + auto xml = WriteConfigString(config); + if (xml.empty()) + return false; + + ::File::IOFile f(filename, "wb"); + if (!f) + return false; + + if (!f.WriteString(xml)) + return false; + + return true; +} + +void ApplyConfigDefaults(Disc* disc, const Config& config) +{ + for (const auto& config_option : config.m_options) + { + auto* matching_option = [&]() -> DiscIO::Riivolution::Option* { + for (auto& section : disc->m_sections) + { + for (auto& option : section.m_options) + { + if (option.m_id.empty()) + { + if ((section.m_name + option.m_name) == config_option.m_id) + return &option; + } + else + { + if (option.m_id == config_option.m_id) + return &option; + } + } + } + return nullptr; + }(); + if (matching_option) + matching_option->m_selected_choice = config_option.m_default; + } +} } // namespace DiscIO::Riivolution -- cgit v1.2.3