Core: Add RiivolutionParser to parse a Riivolution XML.
This commit is contained in:
parent
a4da56e5e6
commit
e26b59bab3
|
@ -28,6 +28,8 @@ add_library(discio
|
|||
MultithreadedCompressor.h
|
||||
NANDImporter.cpp
|
||||
NANDImporter.h
|
||||
RiivolutionParser.cpp
|
||||
RiivolutionParser.h
|
||||
ScrubbedBlob.cpp
|
||||
ScrubbedBlob.h
|
||||
TGCBlob.cpp
|
||||
|
|
|
@ -0,0 +1,336 @@
|
|||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DiscIO/RiivolutionParser.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/IOFile.h"
|
||||
#include "Common/StringUtil.h"
|
||||
|
||||
namespace DiscIO::Riivolution
|
||||
{
|
||||
std::optional<Disc> ParseFile(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;
|
||||
|
||||
return ParseString(std::string_view(data.data(), data.size()), filename);
|
||||
}
|
||||
|
||||
static std::map<std::string, std::string> ReadParams(const pugi::xml_node& node,
|
||||
std::map<std::string, std::string> 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<u8> ReadHexString(std::string_view sv)
|
||||
{
|
||||
if ((sv.size() % 2) == 1)
|
||||
return {};
|
||||
if (StringBeginsWith(sv, "0x") || StringBeginsWith(sv, "0X"))
|
||||
sv = sv.substr(2);
|
||||
|
||||
std::vector<u8> 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<Disc> 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<std::string> 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<std::string>& 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<u16> revision,
|
||||
std::optional<u8> 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<int>(disc_number).value_or(-1);
|
||||
const int revision_int = std::optional<int>(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<Patch> 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<std::pair<std::string, std::string_view>>& 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<Patch> 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<std::pair<std::string, std::string_view>> 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
|
|
@ -0,0 +1,193 @@
|
|||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
namespace DiscIO::Riivolution
|
||||
{
|
||||
// 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.
|
||||
// We need to manually set this somehow because we have no SD root, and should ignore the path
|
||||
// from the XML.
|
||||
std::string m_root;
|
||||
|
||||
std::vector<File> m_file_patches;
|
||||
std::vector<Folder> m_folder_patches;
|
||||
std::vector<Savegame> m_savegame_patches;
|
||||
std::vector<Memory> m_memory_patches;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
std::optional<Disc> ParseFile(const std::string& filename);
|
||||
std::optional<Disc> ParseString(std::string_view xml, std::string xml_path);
|
||||
} // namespace DiscIO::Riivolution
|
|
@ -433,6 +433,7 @@
|
|||
<ClInclude Include="DiscIO\LaggedFibonacciGenerator.h" />
|
||||
<ClInclude Include="DiscIO\MultithreadedCompressor.h" />
|
||||
<ClInclude Include="DiscIO\NANDImporter.h" />
|
||||
<ClInclude Include="DiscIO\RiivolutionParser.h" />
|
||||
<ClInclude Include="DiscIO\ScrubbedBlob.h" />
|
||||
<ClInclude Include="DiscIO\TGCBlob.h" />
|
||||
<ClInclude Include="DiscIO\Volume.h" />
|
||||
|
@ -1019,6 +1020,7 @@
|
|||
<ClCompile Include="DiscIO\FileSystemGCWii.cpp" />
|
||||
<ClCompile Include="DiscIO\LaggedFibonacciGenerator.cpp" />
|
||||
<ClCompile Include="DiscIO\NANDImporter.cpp" />
|
||||
<ClCompile Include="DiscIO\RiivolutionParser.cpp" />
|
||||
<ClCompile Include="DiscIO\ScrubbedBlob.cpp" />
|
||||
<ClCompile Include="DiscIO\TGCBlob.cpp" />
|
||||
<ClCompile Include="DiscIO\Volume.cpp" />
|
||||
|
|
Loading…
Reference in New Issue