#include "bin_patch.h" #include "File.h" #include "Config.h" LOG_CHANNEL(patch_log); static const std::string yml_key_enable_legacy_patches = "Enable Legacy Patches"; template <> void fmt_class_string::format(std::string& out, u64 arg) { format_enum(out, arg, [](YAML::NodeType::value value) { switch (value) { case YAML::NodeType::Undefined: return "Undefined"; case YAML::NodeType::Null: return "Null"; case YAML::NodeType::Scalar: return "Scalar"; case YAML::NodeType::Sequence: return "Sequence"; case YAML::NodeType::Map: return "Map"; } return unknown; }); } template <> void fmt_class_string::format(std::string& out, u64 arg) { format_enum(out, arg, [](patch_type value) { switch (value) { case patch_type::load: return "load"; case patch_type::byte: return "byte"; case patch_type::le16: return "le16"; case patch_type::le32: return "le32"; case patch_type::le64: return "le64"; case patch_type::bef32: return "bef32"; case patch_type::bef64: return "bef64"; case patch_type::be16: return "be16"; case patch_type::be32: return "be32"; case patch_type::be64: return "be64"; case patch_type::lef32: return "lef32"; case patch_type::lef64: return "lef64"; } return unknown; }); } patch_engine::patch_engine() { const std::string patches_path = fs::get_config_dir() + "patches/"; if (!fs::create_path(patches_path)) { patch_log.fatal("Failed to create path: %s (%s)", patches_path, fs::g_tls_error); } } std::string patch_engine::get_patch_config_path() { #ifdef _WIN32 return fs::get_config_dir() + "config/patch_config.yml"; #else return fs::get_config_dir() + "patch_config.yml"; #endif } void patch_engine::load(patch_map& patches_map, const std::string& path) { // Load patch file fs::file file{ path }; if (!file) { // Do nothing return; } // Interpret yaml nodes auto [root, error] = yaml_load(file.to_string()); if (!error.empty()) { patch_log.fatal("Failed to load patch file %s:\n%s", path, error); return; } // Load patch config to determine which patches are enabled bool enable_legacy_patches; patch_config_map patch_config = load_config(enable_legacy_patches); static const std::string target_version = "1.0"; std::string version; bool is_legacy_patch = false; if (const auto version_node = root["Version"]) { version = version_node.Scalar(); if (version != target_version) { patch_log.error("Patch engine target version %s does not match file version %s in %s", target_version, version, path); return; } // We don't need the Version node in local memory anymore root.remove("Version"); } else { patch_log.warning("Patch engine version %s: Reading legacy patch file %s", target_version, path); is_legacy_patch = true; } // Go through each main key in the file for (auto pair : root) { const auto& main_key = pair.first.Scalar(); // Use old logic and yaml layout if this is a legacy patch if (is_legacy_patch) { struct patch_info info{}; info.hash = main_key; info.enabled = enable_legacy_patches; info.is_legacy = true; read_patch_node(info, pair.second, root); // Find or create an entry matching the key/hash in our map auto& title_info = patches_map[main_key]; title_info.hash = main_key; title_info.is_legacy = true; title_info.patch_info_map["legacy"] = info; continue; } // Use new logic and yaml layout if (const auto yml_type = pair.second.Type(); yml_type != YAML::NodeType::Map) { patch_log.error("Skipping key %s: expected Map, found %s (file: %s)", main_key, yml_type, path); continue; } // Skip Anchors if (main_key == "Anchors") { continue; } std::string title; std::string serials; if (const auto title_node = pair.second["Title"]) { title = title_node.Scalar(); } if (const auto serials_node = pair.second["Serials"]) { serials = serials_node.Scalar(); } if (const auto patches_node = pair.second["Patches"]) { if (const auto yml_type = patches_node.Type(); yml_type != YAML::NodeType::Map) { patch_log.error("Skipping Patches: expected Map, found %s (key: %s, file: %s)", yml_type, main_key, path); continue; } // Find or create an entry matching the key/hash in our map auto& title_info = patches_map[main_key]; title_info.is_legacy = false; title_info.hash = main_key; title_info.title = title; title_info.serials = serials; title_info.version = version; // Go through each patch for (auto patches_entry : patches_node) { // Each key in "Patches" is also the patch description const std::string description = patches_entry.first.Scalar(); // Find out if this patch was enabled in the patch config const bool enabled = patch_config[main_key][description]; // Compile patch information if (const auto yml_type = patches_entry.second.Type(); yml_type != YAML::NodeType::Map) { patch_log.error("Skipping Patch key %s: expected Map, found %s (key: %s, file: %s)", description, yml_type, main_key, path); continue; } struct patch_info info {}; info.enabled = enabled; info.description = description; info.hash = main_key; info.version = version; info.serials = serials; info.title = title; if (const auto author_node = patches_entry.second["Author"]) { info.author = author_node.Scalar(); } if (const auto patch_version_node = patches_entry.second["Version"]) { info.patch_version = patch_version_node.Scalar(); } if (const auto notes_node = patches_entry.second["Notes"]) { info.notes = notes_node.Scalar(); } if (const auto patch_node = patches_entry.second["Patch"]) { read_patch_node(info, patch_node, root); } // Insert patch information title_info.patch_info_map[description] = info; } } } } patch_type patch_engine::get_patch_type(YAML::Node node) { u64 type_val = 0; cfg::try_to_enum_value(&type_val, &fmt_class_string::format, node.Scalar()); return static_cast(type_val); } void patch_engine::add_patch_data(YAML::Node node, patch_info& info, u32 modifier, const YAML::Node& root) { const auto type_node = node[0]; auto addr_node = node[1]; const auto value_node = node[2]; struct patch_data p_data{}; p_data.type = get_patch_type(type_node); p_data.offset = addr_node.as(0) + modifier; switch (p_data.type) { case patch_type::load: { // Special syntax: anchors (named sequence) // Most legacy patches don't use the anchor syntax correctly, so try to sanitize it. if (info.is_legacy) { if (const auto yml_type = addr_node.Type(); yml_type == YAML::NodeType::Scalar) { const auto anchor = addr_node.Scalar(); const auto anchor_node = root[anchor]; if (anchor_node) { addr_node = anchor_node; patch_log.warning("Incorrect anchor syntax found in legacy patch: %s (key: %s)", anchor, info.hash); } else { patch_log.error("Anchor not found in legacy patch: %s (key: %s)", anchor, info.hash); return; } } } // Check if the anchor was resolved. if (const auto yml_type = addr_node.Type(); yml_type != YAML::NodeType::Sequence) { patch_log.error("Skipping sequence: expected Sequence, found %s (key: %s)", yml_type, info.hash); return; } // Address modifier (optional) const u32 mod = value_node.as(0); for (const auto& item : addr_node) { add_patch_data(item, info, mod, root); } return; } case patch_type::bef32: case patch_type::lef32: { p_data.value = std::bit_cast(value_node.as()); break; } case patch_type::bef64: case patch_type::lef64: { p_data.value = std::bit_cast(value_node.as()); break; } default: { p_data.value = value_node.as(); } } info.data_list.emplace_back(p_data); } void patch_engine::read_patch_node(patch_info& info, YAML::Node node, const YAML::Node& root) { if (const auto yml_type = node.Type(); yml_type != YAML::NodeType::Sequence) { patch_log.error("Skipping patch node %s: expected Sequence, found %s (key: %s)", info.description, yml_type, info.hash); return; } for (auto patch : node) { add_patch_data(patch, info, 0, root); } } void patch_engine::append(const std::string& patch) { load(m_map, patch); } void patch_engine::append_global_patches() { // Legacy patch.yml load(m_map, fs::get_config_dir() + "patch.yml"); // New patch.yml load(m_map, fs::get_config_dir() + "patches/patch.yml"); } void patch_engine::append_title_patches(const std::string& title_id) { if (title_id.empty()) { return; } // Legacy patch.yml load(m_map, fs::get_config_dir() + "data/" + title_id + "/patch.yml"); // New patch.yml load(m_map, fs::get_config_dir() + "patches/" + title_id + "_patch.yml"); } std::size_t patch_engine::apply(const std::string& name, u8* dst) const { return apply_patch(name, dst, 0, 0); } std::size_t patch_engine::apply_with_ls_check(const std::string& name, u8* dst, u32 filesz, u32 ls_addr) const { return apply_patch(name, dst, filesz, ls_addr); } template std::size_t patch_engine::apply_patch(const std::string& name, u8* dst, u32 filesz, u32 ls_addr) const { if (m_map.find(name) == m_map.cend()) { return 0; } size_t applied_total = 0; const auto& title_info = m_map.at(name); // Apply modifications sequentially for (const auto& [description, patch] : title_info.patch_info_map) { if (!patch.enabled) { continue; } size_t applied = 0; for (const auto& p : patch.data_list) { u32 offset = p.offset; if constexpr (check_local_storage) { offset -= ls_addr; if (offset < ls_addr || offset >= (ls_addr + filesz)) { // This patch is out of range for this segment continue; } } auto ptr = dst + offset; switch (p.type) { case patch_type::load: { // Invalid in this context continue; } case patch_type::byte: { *ptr = static_cast(p.value); break; } case patch_type::le16: { *reinterpret_cast*>(ptr) = static_cast(p.value); break; } case patch_type::le32: case patch_type::lef32: { *reinterpret_cast*>(ptr) = static_cast(p.value); break; } case patch_type::le64: case patch_type::lef64: { *reinterpret_cast*>(ptr) = static_cast(p.value); break; } case patch_type::be16: { *reinterpret_cast*>(ptr) = static_cast(p.value); break; } case patch_type::be32: case patch_type::bef32: { *reinterpret_cast*>(ptr) = static_cast(p.value); break; } case patch_type::be64: case patch_type::bef64: { *reinterpret_cast*>(ptr) = static_cast(p.value); break; } } ++applied; } if (title_info.is_legacy) { patch_log.notice("Applied legacy patch (<- %d)", applied); } else { patch_log.notice("Applied patch (description='%s', author='%s', patch_version='%s', file_version='%s') (<- %d)", description, patch.author, patch.patch_version, patch.version, applied); } applied_total += applied; } return applied_total; } void patch_engine::save_config(const patch_map& patches_map, bool enable_legacy_patches) { const std::string path = get_patch_config_path(); patch_log.notice("Saving patch config file %s", path); fs::file file(path, fs::rewrite); if (!file) { patch_log.fatal("Failed to open patch config file %s", path); return; } YAML::Emitter out; out << YAML::BeginMap; // Save "Enable Legacy Patches" out << yml_key_enable_legacy_patches << enable_legacy_patches; // Save 'enabled' state per hash and description patch_config_map config_map; for (const auto& [hash, title_info] : patches_map) { if (title_info.is_legacy) { continue; } for (const auto& [description, patch] : title_info.patch_info_map) { config_map[hash][description] = patch.enabled; } if (config_map[hash].size() > 0) { out << hash; out << YAML::BeginMap; for (const auto& [description, enabled] : config_map[hash]) { out << description; out << enabled; } out << YAML::EndMap; } } out << YAML::EndMap; file.write(out.c_str(), out.size()); } patch_engine::patch_config_map patch_engine::load_config(bool& enable_legacy_patches) { enable_legacy_patches = true; // Default to true patch_config_map config_map; const std::string path = get_patch_config_path(); patch_log.notice("Loading patch config file %s", path); if (fs::file f{ path }) { auto [root, error] = yaml_load(f.to_string()); if (!error.empty()) { patch_log.fatal("Failed to load patch config file %s:\n%s", path, error); return config_map; } // Try to load "Enable Legacy Patches" (default to true) if (auto enable_legacy_node = root[yml_key_enable_legacy_patches]) { enable_legacy_patches = enable_legacy_node.as(true); root.remove(yml_key_enable_legacy_patches); // Remove the node in order to skip it in the next part } for (auto pair : root) { auto& hash = pair.first.Scalar(); auto& data = config_map[hash]; if (const auto yml_type = pair.second.Type(); yml_type != YAML::NodeType::Map) { patch_log.error("Error loading patch config key %s: expected Map, found %s (file: %s)", hash, yml_type, path); continue; } for (auto patch : pair.second) { const auto description = patch.first.Scalar(); const auto enabled = patch.second.as(false); data[description] = enabled; } } } return config_map; }