RiivolutionParser.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. // Copyright 2021 Dolphin Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include "DiscIO/RiivolutionParser.h"
  4. #include <algorithm>
  5. #include <sstream>
  6. #include <string>
  7. #include <string_view>
  8. #include <vector>
  9. #include <fmt/format.h>
  10. #include <pugixml.hpp>
  11. #include "Common/FileSearch.h"
  12. #include "Common/FileUtil.h"
  13. #include "Common/IOFile.h"
  14. #include "Common/StringUtil.h"
  15. #include "DiscIO/GameModDescriptor.h"
  16. #include "DiscIO/RiivolutionPatcher.h"
  17. namespace DiscIO::Riivolution
  18. {
  19. Patch::~Patch() = default;
  20. std::optional<Disc> ParseFile(const std::string& filename)
  21. {
  22. ::File::IOFile f(filename, "rb");
  23. if (!f)
  24. return std::nullopt;
  25. std::vector<char> data;
  26. data.resize(f.GetSize());
  27. if (!f.ReadBytes(data.data(), data.size()))
  28. return std::nullopt;
  29. return ParseString(std::string_view(data.data(), data.size()), filename);
  30. }
  31. static std::map<std::string, std::string> ReadParams(const pugi::xml_node& node,
  32. std::map<std::string, std::string> params = {})
  33. {
  34. for (const auto& param_node : node.children("param"))
  35. {
  36. const std::string param_name = param_node.attribute("name").as_string();
  37. const std::string param_value = param_node.attribute("value").as_string();
  38. params[param_name] = param_value;
  39. }
  40. return params;
  41. }
  42. static std::vector<u8> ReadHexString(std::string_view sv)
  43. {
  44. if ((sv.size() % 2) == 1)
  45. return {};
  46. if (sv.starts_with("0x") || sv.starts_with("0X"))
  47. sv = sv.substr(2);
  48. std::vector<u8> result;
  49. result.reserve(sv.size() / 2);
  50. while (!sv.empty())
  51. {
  52. u8 tmp;
  53. if (!TryParse(std::string(sv.substr(0, 2)), &tmp, 16))
  54. return {};
  55. result.push_back(tmp);
  56. sv = sv.substr(2);
  57. }
  58. return result;
  59. }
  60. std::optional<Disc> ParseString(std::string_view xml, std::string xml_path)
  61. {
  62. pugi::xml_document doc;
  63. const auto parse_result = doc.load_buffer(xml.data(), xml.size());
  64. if (!parse_result)
  65. return std::nullopt;
  66. const auto wiidisc = doc.child("wiidisc");
  67. if (!wiidisc)
  68. return std::nullopt;
  69. Disc disc;
  70. disc.m_xml_path = std::move(xml_path);
  71. disc.m_version = wiidisc.attribute("version").as_int(-1);
  72. if (disc.m_version != 1)
  73. return std::nullopt;
  74. const std::string default_root = wiidisc.attribute("root").as_string();
  75. const auto id = wiidisc.child("id");
  76. if (id)
  77. {
  78. for (const auto& attribute : id.attributes())
  79. {
  80. const std::string_view attribute_name(attribute.name());
  81. if (attribute_name == "game")
  82. disc.m_game_filter.m_game = attribute.as_string();
  83. else if (attribute_name == "developer")
  84. disc.m_game_filter.m_developer = attribute.as_string();
  85. else if (attribute_name == "disc")
  86. disc.m_game_filter.m_disc = attribute.as_int(-1);
  87. else if (attribute_name == "version")
  88. disc.m_game_filter.m_version = attribute.as_int(-1);
  89. }
  90. auto xml_regions = id.children("region");
  91. if (xml_regions.begin() != xml_regions.end())
  92. {
  93. std::vector<std::string> regions;
  94. for (const auto& region : xml_regions)
  95. regions.push_back(region.attribute("type").as_string());
  96. disc.m_game_filter.m_regions = std::move(regions);
  97. }
  98. }
  99. const auto options = wiidisc.child("options");
  100. if (options)
  101. {
  102. for (const auto& section_node : options.children("section"))
  103. {
  104. Section& section = disc.m_sections.emplace_back();
  105. section.m_name = section_node.attribute("name").as_string();
  106. for (const auto& option_node : section_node.children("option"))
  107. {
  108. Option& option = section.m_options.emplace_back();
  109. option.m_id = option_node.attribute("id").as_string();
  110. option.m_name = option_node.attribute("name").as_string();
  111. option.m_selected_choice = option_node.attribute("default").as_uint(0);
  112. auto option_params = ReadParams(option_node);
  113. for (const auto& choice_node : option_node.children("choice"))
  114. {
  115. Choice& choice = option.m_choices.emplace_back();
  116. choice.m_name = choice_node.attribute("name").as_string();
  117. auto choice_params = ReadParams(choice_node, option_params);
  118. for (const auto& patchref_node : choice_node.children("patch"))
  119. {
  120. PatchReference& patchref = choice.m_patch_references.emplace_back();
  121. patchref.m_id = patchref_node.attribute("id").as_string();
  122. patchref.m_params = ReadParams(patchref_node, choice_params);
  123. }
  124. }
  125. }
  126. }
  127. for (const auto& macro_node : options.children("macros"))
  128. {
  129. const std::string macro_id = macro_node.attribute("id").as_string();
  130. for (auto& section : disc.m_sections)
  131. {
  132. auto option_to_clone = std::find_if(section.m_options.begin(), section.m_options.end(),
  133. [&](const Option& o) { return o.m_id == macro_id; });
  134. if (option_to_clone == section.m_options.end())
  135. continue;
  136. Option cloned_option = *option_to_clone;
  137. cloned_option.m_name = macro_node.attribute("name").as_string();
  138. for (auto& choice : cloned_option.m_choices)
  139. for (auto& patch_ref : choice.m_patch_references)
  140. patch_ref.m_params = ReadParams(macro_node, patch_ref.m_params);
  141. }
  142. }
  143. }
  144. const auto patches = wiidisc.children("patch");
  145. for (const auto& patch_node : patches)
  146. {
  147. Patch& patch = disc.m_patches.emplace_back();
  148. patch.m_id = patch_node.attribute("id").as_string();
  149. patch.m_root = patch_node.attribute("root").as_string();
  150. if (patch.m_root.empty())
  151. patch.m_root = default_root;
  152. for (const auto& patch_subnode : patch_node.children())
  153. {
  154. const std::string_view patch_name(patch_subnode.name());
  155. if (patch_name == "file" || patch_name == "dolphin_sys_file")
  156. {
  157. auto& file = patch_name == "dolphin_sys_file" ? patch.m_sys_file_patches.emplace_back() :
  158. patch.m_file_patches.emplace_back();
  159. file.m_disc = patch_subnode.attribute("disc").as_string();
  160. file.m_external = patch_subnode.attribute("external").as_string();
  161. file.m_resize = patch_subnode.attribute("resize").as_bool(true);
  162. file.m_create = patch_subnode.attribute("create").as_bool(false);
  163. file.m_offset = patch_subnode.attribute("offset").as_uint(0);
  164. file.m_fileoffset = patch_subnode.attribute("fileoffset").as_uint(0);
  165. file.m_length = patch_subnode.attribute("length").as_uint(0);
  166. }
  167. else if (patch_name == "folder" || patch_name == "dolphin_sys_folder")
  168. {
  169. auto& folder = patch_name == "dolphin_sys_folder" ?
  170. patch.m_sys_folder_patches.emplace_back() :
  171. patch.m_folder_patches.emplace_back();
  172. folder.m_disc = patch_subnode.attribute("disc").as_string();
  173. folder.m_external = patch_subnode.attribute("external").as_string();
  174. folder.m_resize = patch_subnode.attribute("resize").as_bool(true);
  175. folder.m_create = patch_subnode.attribute("create").as_bool(false);
  176. folder.m_recursive = patch_subnode.attribute("recursive").as_bool(true);
  177. folder.m_length = patch_subnode.attribute("length").as_uint(0);
  178. }
  179. else if (patch_name == "savegame")
  180. {
  181. auto& savegame = patch.m_savegame_patches.emplace_back();
  182. savegame.m_external = patch_subnode.attribute("external").as_string();
  183. savegame.m_clone = patch_subnode.attribute("clone").as_bool(true);
  184. }
  185. else if (patch_name == "memory")
  186. {
  187. auto& memory = patch.m_memory_patches.emplace_back();
  188. memory.m_offset = patch_subnode.attribute("offset").as_uint(0);
  189. memory.m_value = ReadHexString(patch_subnode.attribute("value").as_string());
  190. memory.m_valuefile = patch_subnode.attribute("valuefile").as_string();
  191. memory.m_original = ReadHexString(patch_subnode.attribute("original").as_string());
  192. memory.m_ocarina = patch_subnode.attribute("ocarina").as_bool(false);
  193. memory.m_search = patch_subnode.attribute("search").as_bool(false);
  194. memory.m_align = patch_subnode.attribute("align").as_uint(1);
  195. }
  196. }
  197. }
  198. return disc;
  199. }
  200. static bool CheckRegion(const std::vector<std::string>& xml_regions, std::string_view game_region)
  201. {
  202. if (xml_regions.begin() == xml_regions.end())
  203. return true;
  204. for (const auto& region : xml_regions)
  205. {
  206. if (region == game_region)
  207. return true;
  208. }
  209. return false;
  210. }
  211. bool Disc::IsValidForGame(const std::string& game_id, std::optional<u16> revision,
  212. std::optional<u8> disc_number) const
  213. {
  214. if (game_id.size() != 6)
  215. return false;
  216. const std::string_view game_id_full = std::string_view(game_id);
  217. const std::string_view game_region = game_id_full.substr(3, 1);
  218. const std::string_view game_developer = game_id_full.substr(4, 2);
  219. const int disc_number_int = std::optional<int>(disc_number).value_or(-1);
  220. const int revision_int = std::optional<int>(revision).value_or(-1);
  221. if (m_game_filter.m_game && !game_id_full.starts_with(*m_game_filter.m_game))
  222. return false;
  223. if (m_game_filter.m_developer && game_developer != *m_game_filter.m_developer)
  224. return false;
  225. if (m_game_filter.m_disc && disc_number_int != *m_game_filter.m_disc)
  226. return false;
  227. if (m_game_filter.m_version && revision_int != *m_game_filter.m_version)
  228. return false;
  229. if (m_game_filter.m_regions && !CheckRegion(*m_game_filter.m_regions, game_region))
  230. return false;
  231. return true;
  232. }
  233. std::vector<Patch> Disc::GeneratePatches(const std::string& game_id) const
  234. {
  235. const std::string_view game_id_full = std::string_view(game_id);
  236. const std::string_view game_id_no_region = game_id_full.substr(0, 3);
  237. const std::string_view game_region = game_id_full.substr(3, 1);
  238. const std::string_view game_developer = game_id_full.substr(4, 2);
  239. const auto replace_variables =
  240. [&](std::string_view sv,
  241. const std::vector<std::pair<std::string, std::string_view>>& replacements) {
  242. std::string result;
  243. result.reserve(sv.size());
  244. while (!sv.empty())
  245. {
  246. bool replaced = false;
  247. for (const auto& r : replacements)
  248. {
  249. if (sv.starts_with(r.first))
  250. {
  251. for (char c : r.second)
  252. result.push_back(c);
  253. sv = sv.substr(r.first.size());
  254. replaced = true;
  255. break;
  256. }
  257. }
  258. if (replaced)
  259. continue;
  260. result.push_back(sv[0]);
  261. sv = sv.substr(1);
  262. }
  263. return result;
  264. };
  265. // Take only selected patches, replace placeholders in all strings, and return them.
  266. std::vector<Patch> active_patches;
  267. for (const auto& section : m_sections)
  268. {
  269. for (const auto& option : section.m_options)
  270. {
  271. const u32 selected = option.m_selected_choice;
  272. if (selected == 0 || selected > option.m_choices.size())
  273. continue;
  274. const Choice& choice = option.m_choices[selected - 1];
  275. for (const auto& patch_ref : choice.m_patch_references)
  276. {
  277. const auto patch = std::find_if(m_patches.begin(), m_patches.end(),
  278. [&](const Patch& p) { return patch_ref.m_id == p.m_id; });
  279. if (patch == m_patches.end())
  280. continue;
  281. std::vector<std::pair<std::string, std::string_view>> replacements;
  282. replacements.emplace_back(std::pair{"{$__gameid}", game_id_no_region});
  283. replacements.emplace_back(std::pair{"{$__region}", game_region});
  284. replacements.emplace_back(std::pair{"{$__maker}", game_developer});
  285. for (const auto& param : patch_ref.m_params)
  286. replacements.emplace_back(std::pair{"{$" + param.first + "}", param.second});
  287. Patch& new_patch = active_patches.emplace_back(*patch);
  288. new_patch.m_root = replace_variables(new_patch.m_root, replacements);
  289. for (auto& file : new_patch.m_file_patches)
  290. {
  291. file.m_disc = replace_variables(file.m_disc, replacements);
  292. file.m_external = replace_variables(file.m_external, replacements);
  293. }
  294. for (auto& folder : new_patch.m_folder_patches)
  295. {
  296. folder.m_disc = replace_variables(folder.m_disc, replacements);
  297. folder.m_external = replace_variables(folder.m_external, replacements);
  298. }
  299. for (auto& savegame : new_patch.m_savegame_patches)
  300. {
  301. savegame.m_external = replace_variables(savegame.m_external, replacements);
  302. }
  303. for (auto& memory : new_patch.m_memory_patches)
  304. {
  305. memory.m_valuefile = replace_variables(memory.m_valuefile, replacements);
  306. }
  307. }
  308. }
  309. }
  310. return active_patches;
  311. }
  312. std::vector<Patch> GenerateRiivolutionPatchesFromGameModDescriptor(
  313. const GameModDescriptorRiivolution& descriptor, const std::string& game_id,
  314. std::optional<u16> revision, std::optional<u8> disc_number)
  315. {
  316. std::vector<Patch> result;
  317. for (const auto& patch_info : descriptor.patches)
  318. {
  319. auto parsed = ParseFile(patch_info.xml);
  320. if (!parsed || !parsed->IsValidForGame(game_id, revision, disc_number))
  321. continue;
  322. for (auto& section : parsed->m_sections)
  323. {
  324. for (auto& option : section.m_options)
  325. {
  326. const auto* info = [&]() -> const GameModDescriptorRiivolutionPatchOption* {
  327. for (const auto& o : patch_info.options)
  328. {
  329. if (o.section_name == section.m_name)
  330. {
  331. if (!o.option_id.empty() && o.option_id == option.m_id)
  332. return &o;
  333. if (!o.option_name.empty() && o.option_name == option.m_name)
  334. return &o;
  335. }
  336. }
  337. return nullptr;
  338. }();
  339. if (info && info->choice <= option.m_choices.size())
  340. option.m_selected_choice = info->choice;
  341. }
  342. }
  343. for (auto& p : parsed->GeneratePatches(game_id))
  344. {
  345. p.m_file_data_loader =
  346. std::make_shared<FileDataLoaderHostFS>(patch_info.root, parsed->m_xml_path, p.m_root);
  347. result.emplace_back(std::move(p));
  348. }
  349. }
  350. return result;
  351. }
  352. std::vector<Patch> GenerateRiivolutionPatchesFromConfig(const std::string root_directory,
  353. const std::string& game_id,
  354. std::optional<u16> revision,
  355. std::optional<u8> disc_number)
  356. {
  357. std::vector<Patch> result;
  358. // The way Riivolution stores settings only makes sense for standard game IDs.
  359. if (!(game_id.size() == 4 || game_id.size() == 6))
  360. return result;
  361. const std::optional<Config> config = ParseConfigFile(
  362. fmt::format("{}/riivolution/config/{}.xml", root_directory, game_id.substr(0, 4)));
  363. for (const std::string& path : Common::DoFileSearch({root_directory + "riivolution"}, {".xml"}))
  364. {
  365. std::optional<Disc> parsed = ParseFile(path);
  366. if (!parsed || !parsed->IsValidForGame(game_id, revision, disc_number))
  367. continue;
  368. if (config)
  369. ApplyConfigDefaults(&*parsed, *config);
  370. for (auto& patch : parsed->GeneratePatches(game_id))
  371. {
  372. patch.m_file_data_loader =
  373. std::make_shared<FileDataLoaderHostFS>(root_directory, parsed->m_xml_path, patch.m_root);
  374. result.emplace_back(std::move(patch));
  375. }
  376. }
  377. return result;
  378. }
  379. std::optional<Config> ParseConfigFile(const std::string& filename)
  380. {
  381. ::File::IOFile f(filename, "rb");
  382. if (!f)
  383. return std::nullopt;
  384. std::vector<char> data;
  385. data.resize(f.GetSize());
  386. if (!f.ReadBytes(data.data(), data.size()))
  387. return std::nullopt;
  388. return ParseConfigString(std::string_view(data.data(), data.size()));
  389. }
  390. std::optional<Config> ParseConfigString(std::string_view xml)
  391. {
  392. pugi::xml_document doc;
  393. const auto parse_result = doc.load_buffer(xml.data(), xml.size());
  394. if (!parse_result)
  395. return std::nullopt;
  396. const auto riivolution = doc.child("riivolution");
  397. if (!riivolution)
  398. return std::nullopt;
  399. Config config;
  400. config.m_version = riivolution.attribute("version").as_int(-1);
  401. if (config.m_version != 2)
  402. return std::nullopt;
  403. const auto options = riivolution.children("option");
  404. for (const auto& option_node : options)
  405. {
  406. auto& option = config.m_options.emplace_back();
  407. option.m_id = option_node.attribute("id").as_string();
  408. option.m_default = option_node.attribute("default").as_uint(0);
  409. }
  410. return config;
  411. }
  412. std::string WriteConfigString(const Config& config)
  413. {
  414. pugi::xml_document doc;
  415. auto riivolution = doc.append_child("riivolution");
  416. riivolution.append_attribute("version").set_value(config.m_version);
  417. for (const auto& option : config.m_options)
  418. {
  419. auto option_node = riivolution.append_child("option");
  420. option_node.append_attribute("id").set_value(option.m_id.c_str());
  421. option_node.append_attribute("default").set_value(option.m_default);
  422. }
  423. std::stringstream ss;
  424. doc.print(ss, " ");
  425. return ss.str();
  426. }
  427. bool WriteConfigFile(const std::string& filename, const Config& config)
  428. {
  429. auto xml = WriteConfigString(config);
  430. if (xml.empty())
  431. return false;
  432. ::File::CreateFullPath(filename);
  433. ::File::IOFile f(filename, "wb");
  434. if (!f)
  435. return false;
  436. if (!f.WriteString(xml))
  437. return false;
  438. return true;
  439. }
  440. void ApplyConfigDefaults(Disc* disc, const Config& config)
  441. {
  442. for (const auto& config_option : config.m_options)
  443. {
  444. auto* matching_option = [&]() -> Option* {
  445. for (auto& section : disc->m_sections)
  446. {
  447. for (auto& option : section.m_options)
  448. {
  449. if (option.m_id.empty())
  450. {
  451. if ((section.m_name + option.m_name) == config_option.m_id)
  452. return &option;
  453. }
  454. else
  455. {
  456. if (option.m_id == config_option.m_id)
  457. return &option;
  458. }
  459. }
  460. }
  461. return nullptr;
  462. }();
  463. if (matching_option)
  464. matching_option->m_selected_choice = config_option.m_default;
  465. }
  466. }
  467. } // namespace DiscIO::Riivolution