// Riivolution patch-XML parsing and patch selection. // // Ported from Dolphin Emulator's DiscIO/RiivolutionParser.h/cpp and the // external-path resolution rules of DiscIO/RiivolutionPatcher.cpp // (https://github.com/dolphin-emu/dolphin). // Copyright 2021 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later // // Deviations from Dolphin, all deliberate: // - patches are parsed but never applied here: guest code patching // belongs to the translator's Code.pul/lowmem pipeline, not the runtime. // - Riivolution "macros" are not supported #pragma once #include #include #include #include #include #include #include #include #include namespace RiivolutionContract { // ============================================================================ // Minimal XML document model // ============================================================================ struct XmlNode { std::string name; std::vector> attributes; std::vector children; const std::string* FindAttribute(std::string_view attributeName) const { for (const auto& attribute : attributes) { if (attribute.first == attributeName) { return &attribute.second; } } return nullptr; } std::string Attribute(std::string_view attributeName, std::string_view fallback = {}) const { const std::string* value = FindAttribute(attributeName); return value ? *value : std::string(fallback); } bool AttributeBool(std::string_view attributeName, bool fallback) const { const std::string* value = FindAttribute(attributeName); if (!value) { return fallback; } if (*value == "true" || *value == "1" || *value == "yes") { return true; } if (*value == "false" || *value == "0" || *value == "no") { return false; } return fallback; } // Accepts decimal and 0x-prefixed hex, matching pugixml's number parsing // (Riivolution XMLs write memory offsets as 0x........). uint32_t AttributeUint(std::string_view attributeName, uint32_t fallback) const { const std::string* value = FindAttribute(attributeName); if (!value || value->empty()) { return fallback; } const std::string& text = *value; size_t index = 0; uint32_t base = 10; if (text.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) { base = 16; index = 2; } uint64_t result = 0; for (; index < text.size(); ++index) { const char c = text[index]; uint32_t digit; if (c >= '0' && c <= '9') { digit = static_cast(c - '0'); } else if (base == 16 && c >= 'a' && c <= 'f') { digit = static_cast(c - 'a' + 10); } else if (base == 16 && c >= 'A' && c <= 'F') { digit = static_cast(c - 'A' + 10); } else { return fallback; } result = result * base + digit; if (result > 0xffffffffull) { return fallback; } } return index > (base == 16 ? 2u : 0u) ? static_cast(result) : fallback; } int AttributeInt(std::string_view attributeName, int fallback) const { const std::string* value = FindAttribute(attributeName); if (!value || value->empty()) { return fallback; } const bool negative = (*value)[0] == '-'; const uint32_t magnitude = AttributeUintFromText(negative ? value->substr(1) : *value, 0x80000000u); if (magnitude == 0x80000000u && !negative) { return fallback; } return negative ? -static_cast(magnitude) : static_cast(magnitude); } private: static uint32_t AttributeUintFromText(const std::string& text, uint32_t fallback) { XmlNode probe; probe.attributes.push_back({"v", text}); return probe.AttributeUint("v", fallback); } }; namespace XmlDetail { inline bool StartsWith(std::string_view text, std::string_view prefix) { return text.size() >= prefix.size() && text.compare(0, prefix.size(), prefix) == 0; } } // namespace XmlDetail // Converts only element and attribute data from pugixml. Riivolution carries // its data in attributes, so text, declarations, comments, and CDATA do not // need to become part of the contract's data model. inline XmlNode CopyXmlNode(const pugi::xml_node& source) { XmlNode destination; destination.name = source.name(); for (const pugi::xml_attribute& attribute : source.attributes()) { destination.attributes.emplace_back(attribute.name(), attribute.value()); } for (const pugi::xml_node& child : source.children()) { if (child.type() == pugi::node_element) { destination.children.push_back(CopyXmlNode(child)); } } return destination; } // Parses a document with pugixml and returns its root element, or nullopt when // malformed. load_buffer accepts the UTF-8 BOM used by some Riivolution packs. inline std::optional ParseXml(std::string_view text) { pugi::xml_document document; const pugi::xml_parse_result result = document.load_buffer( text.data(), text.size(), pugi::parse_default, pugi::encoding_utf8); if (!result) { return std::nullopt; } const pugi::xml_node root = document.document_element(); if (!root) { return std::nullopt; } return CopyXmlNode(root); } // ============================================================================ // Riivolution data model (mirrors Dolphin's DiscIO::Riivolution) // ============================================================================ struct GameFilter { std::optional game; std::optional developer; std::optional disc; std::optional version; std::optional> regions; }; struct PatchReference { std::string id; std::map params; }; struct Choice { std::string name; std::vector patchReferences; }; struct Option { std::string name; std::string id; std::vector choices; // 1-based index into choices; 0 means disabled. uint32_t selectedChoice = 0; }; struct Section { std::string name; std::vector