diff --git a/Utilities/bin_patch.cpp b/Utilities/bin_patch.cpp index 18a5cd6c6b..7c405c33bb 100644 --- a/Utilities/bin_patch.cpp +++ b/Utilities/bin_patch.cpp @@ -1,10 +1,27 @@ -#include "bin_patch.h" -#include "util/yaml.hpp" +#include "bin_patch.h" #include "File.h" #include "Config.h" LOG_CHANNEL(patch_log); +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) { @@ -30,219 +47,507 @@ void fmt_class_string::format(std::string& out, u64 arg) }); } -void patch_engine::append(const std::string& patch) +patch_engine::patch_engine() { - if (fs::file f{patch}) - { - auto [root, error] = yaml_load(f.to_string()); + const std::string patches_path = fs::get_config_dir() + "patches/"; - if (!error.empty()) + 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 + patch_config_map patch_config = load_config(); + + 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.fatal("Failed to load patch file %s:\n%s", patch, error); + patch_log.error("Patch engine target version %s does not match file version %s in %s", target_version, version, path); return; } - for (auto pair : root) + // 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) { - auto& name = pair.first.Scalar(); - auto& data = m_map[name]; + struct patch_info info{}; + info.hash = main_key; + info.enabled = true; + info.is_legacy = true; - for (auto patch : pair.second) + 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) { - u64 type64 = 0; - cfg::try_to_enum_value(&type64, &fmt_class_string::format, patch[0].Scalar()); + patch_log.error("Skipping Patches: expected Map, found %s (key: %s, file: %s)", yml_type, main_key, path); + continue; + } - struct patch info{}; - info.type = static_cast(type64); - info.offset = patch[1].as(0); + // 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; - switch (info.type) + // 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) { - case patch_type::load: - { - // Special syntax: copy named sequence (must be loaded before) - const auto found = m_map.find(patch[1].Scalar()); - - if (found != m_map.end()) - { - // Address modifier (optional) - const u32 mod = patch[2].as(0); - - for (const auto& rd : found->second) - { - info = rd; - info.offset += mod; - data.emplace_back(info); - } - - continue; - } - - // TODO: error - break; - } - case patch_type::bef32: - case patch_type::lef32: - { - info.value = std::bit_cast(patch[2].as()); - break; - } - case patch_type::bef64: - case patch_type::lef64: - { - info.value = std::bit_cast(patch[2].as()); - break; - } - default: - { - info.value = patch[2].as(); - break; - } + patch_log.error("Skipping Patch key %s: expected Map, found %s (key: %s, file: %s)", description, yml_type, main_key, path); + continue; } - data.emplace_back(info); + 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(); + patch_log.warning("Incorrect anchor syntax found in legacy patch: %s (key: %s)", anchor, info.hash); + + if (!(addr_node = root[anchor])) + { + 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 { - const auto found = m_map.find(name); - - if (found == m_map.cend()) - { - return 0; - } - - // Apply modifications sequentially - for (const auto& p : found->second) - { - auto ptr = dst + p.offset; - - switch (p.type) - { - case patch_type::load: - { - // Invalid in this context - break; - } - 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; - } - } - } - - return found->second.size(); + 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 { - u32 rejected = 0; + return apply_patch(name, dst, filesz, ls_addr); +} - const auto found = m_map.find(name); - - if (found == m_map.cend()) +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; } - // Apply modifications sequentially - for (const auto& p : found->second) - { - auto ptr = dst + (p.offset - ls_addr); + size_t applied_total = 0; + const auto& title_info = m_map.at(name); - if(p.offset < ls_addr || p.offset >= (ls_addr + filesz)) + // Apply modifications sequentially + for (const auto& [description, patch] : title_info.patch_info_map) + { + if (!patch.enabled) { - // This patch is out of range for this segment - rejected++; continue; } - switch (p.type) + size_t applied = 0; + + for (const auto& p : patch.data_list) { - case patch_type::load: - { - // Invalid in this context - break; + 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; } - case patch_type::byte: + + if (title_info.is_legacy) { - *ptr = static_cast(p.value); - break; + patch_log.notice("Applied legacy patch (<- %d)", applied); } - case patch_type::le16: + else { - *reinterpret_cast*>(ptr) = static_cast(p.value); - break; + 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); } - case patch_type::le32: - case patch_type::lef32: + + applied_total += applied; + } + + return applied_total; +} + +void patch_engine::save_config(const patch_map& patches_map) +{ + 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; + + patch_config_map config_map; + + for (const auto& [hash, title_info] : patches_map) + { + if (title_info.is_legacy) { - *reinterpret_cast*>(ptr) = static_cast(p.value); - break; + continue; } - case patch_type::le64: - case patch_type::lef64: + + for (const auto& [description, patch] : title_info.patch_info_map) { - *reinterpret_cast*>(ptr) = static_cast(p.value); - break; + config_map[hash][description] = patch.enabled; } - case patch_type::be16: + + if (config_map[hash].size() > 0) { - *reinterpret_cast*>(ptr) = static_cast(p.value); - break; + out << hash; + out << YAML::BeginMap; + + for (const auto& [description, enabled] : config_map[hash]) + { + out << description; + out << enabled; + } + + out << YAML::EndMap; } - case patch_type::be32: - case patch_type::bef32: + } + out << YAML::EndMap; + + file.write(out.c_str(), out.size()); +} + +patch_engine::patch_config_map patch_engine::load_config() +{ + 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()) { - *reinterpret_cast*>(ptr) = static_cast(p.value); - break; + patch_log.fatal("Failed to load patch config file %s:\n%s", path, error); + return config_map; } - case patch_type::be64: - case patch_type::bef64: + + for (auto pair : root) { - *reinterpret_cast*>(ptr) = static_cast(p.value); - break; - } + 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 (found->second.size() - rejected); + return config_map; } diff --git a/Utilities/bin_patch.h b/Utilities/bin_patch.h index 8bff69ab5f..92a7dab34d 100644 --- a/Utilities/bin_patch.h +++ b/Utilities/bin_patch.h @@ -1,10 +1,12 @@ -#pragma once +#pragma once #include "BEType.h" #include #include #include +#include "util/yaml.hpp" + enum class patch_type { load, @@ -23,22 +25,100 @@ enum class patch_type class patch_engine { - struct patch +public: + struct patch_data { - patch_type type; - u32 offset; - u64 value; + patch_type type = patch_type::load; + u32 offset = 0; + u64 value = 0; }; - // Database - std::unordered_map> m_map; + struct patch_info + { + // Patch information + std::vector data_list; + std::string description; + std::string patch_version; + std::string author; + std::string notes; + bool enabled = false; -public: - // Load from file - void append(const std::string& path); + // Redundant information for accessibility (see patch_title_info) + std::string hash; + std::string version; + std::string title; + std::string serials; + bool is_legacy = false; + }; + + struct patch_title_info + { + std::unordered_map patch_info_map; + std::string hash; + std::string version; + std::string title; + std::string serials; + bool is_legacy = false; + }; + + using patch_map = std::unordered_map; + using patch_config_map = std::unordered_map>; + + patch_engine(); + + // Returns the directory in which patch_config.yml is located + static std::string get_patch_config_path(); + + // Load from file and append to specified patches map + // Example entry: + // + // PPU-8007056e52279bea26c15669d1ee08c2df321d00: + // Title: Fancy Game + // Serials: ABCD12345, SUPA13337 v.1.3 + // Patches: + // 60fps: + // Author: Batman bin Suparman + // Notes: This is super + // Patch: + // - [ be32, 0x000e522c, 0x995d0072 ] + // - [ be32, 0x000e5234, 0x995d0074 ] + static void load(patch_map& patches, const std::string& path); + + // Read and add a patch node to the patch info + static void read_patch_node(patch_info& info, YAML::Node node, const YAML::Node& root); + + // Get the patch type of a patch node + static patch_type get_patch_type(YAML::Node node); + + // Add the data of a patch node + static void add_patch_data(YAML::Node node, patch_info& info, u32 modifier, const YAML::Node& root); + + // Save to patch_config.yml + static void save_config(const patch_map& patches_map); + + // Load patch_config.yml + static patch_config_map load_config(); + + // Load from file and append to member patches map + void append_global_patches(); + + // Load from title relevant files and append to member patches map + void append_title_patches(const std::string& title_id); // Apply patch (returns the number of entries applied) std::size_t apply(const std::string& name, u8* dst) const; + // Apply patch with a check that the address exists in SPU local storage - std::size_t apply_with_ls_check(const std::string&name, u8*dst, u32 filesz, u32 ls_addr) const; + std::size_t apply_with_ls_check(const std::string& name, u8* dst, u32 filesz, u32 ls_addr) const; + +private: + // Load from file and append to member patches map + void append(const std::string& path); + + // Internal: Apply patch (returns the number of entries applied) + template + std::size_t apply_patch(const std::string& name, u8* dst, u32 filesz, u32 ls_addr) const; + + // Database + patch_map m_map; }; diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index c32eb6d229..28b5e78321 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -220,7 +220,7 @@ void Emulator::Init() make_path_verbose(fs::get_config_dir() + "captures/"); // Initialize patch engine - g_fxo->init()->append(fs::get_config_dir() + "/patch.yml"); + g_fxo->init()->append_global_patches(); } namespace @@ -914,7 +914,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool add_only, bool } // Load patches from different locations - g_fxo->get()->append(fs::get_config_dir() + "data/" + m_title_id + "/patch.yml"); + g_fxo->get()->append_title_patches(m_title_id); // Mount all devices const std::string emu_dir = GetEmuDir(); diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index e7b3e80a0b..5ea2777541 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -508,6 +508,11 @@ true true + + true + true + true + true true @@ -788,6 +793,11 @@ true true + + true + true + true + true true @@ -1088,6 +1098,11 @@ true true + + true + true + true + true true @@ -1368,6 +1383,11 @@ true true + + true + true + true + true true @@ -1494,6 +1514,7 @@ + @@ -1909,6 +1930,7 @@ + @@ -2267,6 +2289,24 @@ $(QTDIR)\bin\moc.exe;%(FullPath) + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl" "-I.\..\3rdparty\curl\include" "-I.\..\3rdparty\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\XAudio2Redist\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl" "-I.\..\3rdparty\curl\include" "-I.\..\3rdparty\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\XAudio2Redist\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl" "-I.\..\3rdparty\curl\include" "-I.\..\3rdparty\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\XAudio2Redist\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl" "-I.\..\3rdparty\curl\include" "-I.\..\3rdparty\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\XAudio2Redist\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) @@ -2862,6 +2902,26 @@ "$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)" + + + $(QTDIR)\bin\uic.exe;%(AdditionalInputs) + Uic%27ing %(Identity)... + .\QTGeneratedFiles\ui_%(Filename).h;%(Outputs) + "$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)" + $(QTDIR)\bin\uic.exe;%(AdditionalInputs) + Uic%27ing %(Identity)... + .\QTGeneratedFiles\ui_%(Filename).h;%(Outputs) + "$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)" + $(QTDIR)\bin\uic.exe;%(AdditionalInputs) + Uic%27ing %(Identity)... + .\QTGeneratedFiles\ui_%(Filename).h;%(Outputs) + "$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)" + $(QTDIR)\bin\uic.exe;%(AdditionalInputs) + Uic%27ing %(Identity)... + .\QTGeneratedFiles\ui_%(Filename).h;%(Outputs) + "$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)" + + diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index ff1972d704..fc89752a84 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -128,6 +128,9 @@ {b227bdd4-16f5-4f6e-a8b2-8b1f4bdc606a} + + {e72a0cbe-fbcd-4a0b-8c17-a2a3b7a42258} + @@ -1039,6 +1042,21 @@ Gui\settings + + Gui\patch manager + + + Generated Files\Release - LLVM + + + Generated Files\Debug + + + Generated Files\Release + + + Generated Files\Debug - LLVM + @@ -1137,6 +1155,9 @@ Io + + Generated Files + @@ -1349,6 +1370,12 @@ Gui\settings + + Gui\patch manager + + + Form Files + diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index 4aeb303922..4be54297cd 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -36,6 +36,7 @@ osk_dialog_frame.cpp pad_led_settings_dialog.cpp pad_settings_dialog.cpp + patch_manager_dialog.cpp persistent_settings.cpp pkg_install_dialog.cpp progress_dialog.cpp diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 7161144137..ca02f8e5b5 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -22,6 +22,7 @@ #include "progress_dialog.h" #include "skylander_dialog.h" #include "cheat_manager.h" +#include "patch_manager_dialog.h" #include "pkg_install_dialog.h" #include "category.h" #include "gui_settings.h" @@ -1548,6 +1549,12 @@ void main_window::CreateConnects() cheat_manager->show(); }); + connect(ui->actionManage_Game_Patches, &QAction::triggered, [this] + { + patch_manager_dialog patch_manager(this); + patch_manager.exec(); + }); + connect(ui->actionManage_Users, &QAction::triggered, [this] { user_manager_dialog user_manager(m_gui_settings, this); diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index e6e33c9e02..a7498b081e 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -237,6 +237,7 @@ + @@ -1068,6 +1069,11 @@ Create RSX Capture + + + Game Patches + + diff --git a/rpcs3/rpcs3qt/patch_manager_dialog.cpp b/rpcs3/rpcs3qt/patch_manager_dialog.cpp new file mode 100644 index 0000000000..d0f52b536c --- /dev/null +++ b/rpcs3/rpcs3qt/patch_manager_dialog.cpp @@ -0,0 +1,344 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "ui_patch_manager_dialog.h" +#include "patch_manager_dialog.h" +#include "table_item_delegate.h" +#include "qt_utils.h" +#include "Utilities/File.h" +#include "util/logs.hpp" + +LOG_CHANNEL(patch_log); + +enum patch_column : int +{ + enabled, + title, + serials, + description, + patch_version, + author, + notes +}; + +enum patch_role : int +{ + hash_role = Qt::UserRole, + description_role +}; + +patch_manager_dialog::patch_manager_dialog(QWidget* parent) + : QDialog(parent) + , ui(new Ui::patch_manager_dialog) +{ + ui->setupUi(this); + setModal(true); + + load_patches(); + populate_tree(); + + resize(QGuiApplication::primaryScreen()->availableSize() * 0.7); + + connect(ui->patch_filter, &QLineEdit::textChanged, this, &patch_manager_dialog::filter_patches); + connect(ui->patch_tree, &QTreeWidget::currentItemChanged, this, &patch_manager_dialog::on_item_selected); + connect(ui->patch_tree, &QTreeWidget::itemChanged, this, &patch_manager_dialog::on_item_changed); + connect(ui->patch_tree, &QTreeWidget::customContextMenuRequested, this, &patch_manager_dialog::on_custom_context_menu_requested); + connect(ui->pb_expand_all, &QAbstractButton::clicked, ui->patch_tree, &QTreeWidget::expandAll); + connect(ui->pb_collapse_all, &QAbstractButton::clicked, ui->patch_tree, &QTreeWidget::collapseAll); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QWidget::close); + connect(ui->buttonBox, &QDialogButtonBox::clicked, [this](QAbstractButton* button) + { + if (button == ui->buttonBox->button(QDialogButtonBox::Save)) + { + save(); + accept(); + } + else if (button == ui->buttonBox->button(QDialogButtonBox::Apply)) + { + save(); + } + }); +} + +patch_manager_dialog::~patch_manager_dialog() +{ + delete ui; +} + +void patch_manager_dialog::load_patches() +{ + // Legacy path (in case someone puts it there) + patch_engine::load(m_map, fs::get_config_dir() + "patch.yml"); + + // New paths + const std::string patches_path = fs::get_config_dir() + "patches/"; + const QStringList filters = QStringList() << "*patch.yml"; + const QStringList path_list = QDir(QString::fromStdString(patches_path)).entryList(filters); + + for (const auto& path : path_list) + { + patch_engine::load(m_map, patches_path + path.toStdString()); + } +} + +static QList find_children_by_data(QTreeWidgetItem* parent, int role, const QVariant& data) +{ + QList list; + + if (parent) + { + for (int i = 0; i < parent->childCount(); i++) + { + if (parent->child(i)->data(0, role) == data) + { + list << parent->child(i); + } + } + } + + return list; +} + +void patch_manager_dialog::populate_tree() +{ + ui->patch_tree->clear(); + + for (const auto& [hash, title_info] : m_map) + { + // Don't show legacy patches, because you can't configure them anyway + if (title_info.is_legacy) + { + continue; + } + + QTreeWidgetItem* title_level_item = nullptr; + QTreeWidgetItem* serial_level_item = nullptr; + + const QString q_hash = QString::fromStdString(hash); + const QString q_serials = title_info.serials.empty() ? tr("Unknown Version") : QString::fromStdString(title_info.serials); + const QString q_title = QString::fromStdString(title_info.title); + + // Find top level item for this title + if (const auto list = ui->patch_tree->findItems(q_title, Qt::MatchFlag::MatchExactly, 0); list.size() > 0) + { + title_level_item = list[0]; + } + + // Find out if there is a node item for this serial + serial_level_item = gui::utils::find_child(title_level_item, q_serials); + + // Add patch items + for (const auto& [description, patch] : title_info.patch_info_map) + { + // Add a top level item for this title if it doesn't exist yet + if (!title_level_item) + { + title_level_item = new QTreeWidgetItem(); + title_level_item->setText(0, q_title); + title_level_item->setData(0, hash_role, q_hash); + + ui->patch_tree->addTopLevelItem(title_level_item); + } + assert(title_level_item); + + // Add a node item for this serial if it doesn't exist yet + if (!serial_level_item) + { + serial_level_item = new QTreeWidgetItem(); + serial_level_item->setText(0, q_serials); + serial_level_item->setData(0, hash_role, q_hash); + + title_level_item->addChild(serial_level_item); + } + assert(serial_level_item); + + // Add a checkable leaf item for this patch + const QString q_description = QString::fromStdString(description); + QString visible_description = q_description; + + // Add counter to leafs if the name already exists due to different hashes of the same game (PPU, SPU, PRX, OVL) + if (const auto matches = find_children_by_data(serial_level_item, description_role, q_description); matches.count() > 0) + { + if (auto only_match = matches.count() == 1 ? matches[0] : nullptr) + { + only_match->setText(0, q_description + QStringLiteral(" (1)")); + } + visible_description += QStringLiteral(" (") + QString::number(matches.count() + 1) + QStringLiteral(")"); + } + + QTreeWidgetItem* patch_level_item = new QTreeWidgetItem(); + patch_level_item->setText(0, visible_description); + patch_level_item->setCheckState(0, patch.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + patch_level_item->setData(0, hash_role, q_hash); + patch_level_item->setData(0, description_role, q_description); + + serial_level_item->addChild(patch_level_item); + } + + if (title_level_item) + { + title_level_item->sortChildren(0, Qt::SortOrder::AscendingOrder); + } + if (serial_level_item) + { + serial_level_item->sortChildren(0, Qt::SortOrder::AscendingOrder); + } + } + + ui->patch_tree->sortByColumn(0, Qt::SortOrder::AscendingOrder); +} + +void patch_manager_dialog::save() +{ + patch_engine::save_config(m_map); +} + +void patch_manager_dialog::filter_patches(const QString& term) +{ + // Recursive function to show all matching items and their children. + // @return number of visible children of item, including item + std::function show_matches; + show_matches = [&show_matches, search_text = term.toLower()](QTreeWidgetItem* item, bool parent_visible) -> int + { + if (!item) return 0; + + // Only try to match if the parent is not visible + parent_visible = parent_visible || item->text(0).toLower().contains(search_text); + int visible_items = parent_visible ? 1 : 0; + + // Get the number of visible children recursively + for (int i = 0; i < item->childCount(); i++) + { + visible_items += show_matches(item->child(i), parent_visible); + } + + // Only show item if itself or any of its children is visible + item->setHidden(visible_items <= 0); + return visible_items; + }; + + // Go through each top level item and try to find matches + for (auto top_level_item : ui->patch_tree->findItems(".*", Qt::MatchRegExp)) + { + show_matches(top_level_item, false); + } +} + +void patch_manager_dialog::update_patch_info(const patch_engine::patch_info& info) +{ + ui->label_hash->setText(QString::fromStdString(info.hash)); + ui->label_author->setText(QString::fromStdString(info.author)); + ui->label_notes->setText(QString::fromStdString(info.notes)); + ui->label_description->setText(QString::fromStdString(info.description)); + ui->label_patch_version->setText(QString::fromStdString(info.patch_version)); + ui->label_serials->setText(QString::fromStdString(info.serials)); + ui->label_title->setText(QString::fromStdString(info.title)); +} + +void patch_manager_dialog::on_item_selected(QTreeWidgetItem *current, QTreeWidgetItem * /*previous*/) +{ + if (!current) + { + return; + } + + // Get patch identifiers stored in item data + const std::string hash = current->data(0, hash_role).toString().toStdString(); + const std::string description = current->data(0, description_role).toString().toStdString(); + + if (m_map.find(hash) != m_map.end()) + { + const auto& title_info = m_map.at(hash); + + // Find the patch for this item and show its metadata + if (!title_info.is_legacy && title_info.patch_info_map.find(description) != title_info.patch_info_map.end()) + { + update_patch_info(title_info.patch_info_map.at(description)); + return; + } + + // Show shared info if no patch was found + patch_engine::patch_info info{}; + info.hash = hash; + info.title = title_info.title; + info.serials = title_info.serials; + info.version = title_info.version; + update_patch_info(info); + return; + } + + // Clear patch info if no info was found + patch_engine::patch_info info{}; + update_patch_info(info); +} + +void patch_manager_dialog::on_item_changed(QTreeWidgetItem *item, int /*column*/) +{ + if (!item) + { + return; + } + + // Get checkstate of the item + const bool enabled = item->checkState(0) == Qt::CheckState::Checked; + + // Get patch identifiers stored in item data + const std::string hash = item->data(0, hash_role).toString().toStdString(); + const std::string description = item->data(0, description_role).toString().toStdString(); + + // Enable/disable the patch for this item and show its metadata + if (m_map.find(hash) != m_map.end()) + { + auto& title_info = m_map[hash]; + + if (!title_info.is_legacy && title_info.patch_info_map.find(description) != title_info.patch_info_map.end()) + { + auto& patch = m_map[hash].patch_info_map[description]; + patch.enabled = enabled; + update_patch_info(patch); + return; + } + } +} + +void patch_manager_dialog::on_custom_context_menu_requested(const QPoint &pos) +{ + QTreeWidgetItem* item = ui->patch_tree->itemAt(pos); + + if (!item) + { + return; + } + + QMenu* menu = new QMenu(this); + + if (item->childCount() > 0) + { + QAction* collapse_children = new QAction("Collapse Children"); + menu->addAction(collapse_children); + connect(collapse_children, &QAction::triggered, this, [&item](bool) + { + for (int i = 0; i < item->childCount(); i++) + { + item->child(i)->setExpanded(false); + } + }); + + QAction* expand_children = new QAction("Expand Children"); + menu->addAction(expand_children); + connect(expand_children, &QAction::triggered, this, [&item](bool) + { + for (int i = 0; i < item->childCount(); i++) + { + item->child(i)->setExpanded(true); + } + }); + } + + menu->exec(ui->patch_tree->viewport()->mapToGlobal(pos)); +} diff --git a/rpcs3/rpcs3qt/patch_manager_dialog.h b/rpcs3/rpcs3qt/patch_manager_dialog.h new file mode 100644 index 0000000000..7edeae4b42 --- /dev/null +++ b/rpcs3/rpcs3qt/patch_manager_dialog.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include "Utilities/bin_patch.h" + +namespace Ui +{ + class patch_manager_dialog; +} + +class patch_manager_dialog : public QDialog +{ + Q_OBJECT + +public: + explicit patch_manager_dialog(QWidget* parent = nullptr); + ~patch_manager_dialog(); + +private Q_SLOTS: + void filter_patches(const QString& term); + void on_item_selected(QTreeWidgetItem *current, QTreeWidgetItem *previous); + void on_item_changed(QTreeWidgetItem *item, int column); + void on_custom_context_menu_requested(const QPoint& pos); + +private: + void load_patches(); + void populate_tree(); + void save(); + + void update_patch_info(const patch_engine::patch_info& info); + + patch_engine::patch_map m_map; + + Ui::patch_manager_dialog *ui; +}; diff --git a/rpcs3/rpcs3qt/patch_manager_dialog.ui b/rpcs3/rpcs3qt/patch_manager_dialog.ui new file mode 100644 index 0000000000..0a9e3044b7 --- /dev/null +++ b/rpcs3/rpcs3qt/patch_manager_dialog.ui @@ -0,0 +1,247 @@ + + + patch_manager_dialog + + + + 0 + 0 + 837 + 659 + + + + Patch Manager + + + + + + Qt::Horizontal + + + + + + + + + + 0 + 0 + + + + Filter patches + + + + + + + Collapse All + + + + + + + Expand All + + + + + + + + + Qt::CustomContextMenu + + + true + + + + 1 + + + + + + + + + + + + Patch Information + + + + + + Hash + + + + + + + + + true + + + + + + + + + + Title + + + + + + + + + true + + + + + + + + + + Serials + + + + + + + + + true + + + + + + + + + + Description + + + + + + + + + true + + + + + + + + + + Patch Version + + + + + + + + + true + + + + + + + + + + Author + + + + + + + + + true + + + + + + + + + + Notes + + + + + + + + + true + + + + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 0 + 0 + + + + + + + + + + + + + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + + + + + + diff --git a/rpcs3/rpcs3qt/qt_utils.cpp b/rpcs3/rpcs3qt/qt_utils.cpp index 9b6203b023..dc933ec46c 100644 --- a/rpcs3/rpcs3qt/qt_utils.cpp +++ b/rpcs3/rpcs3qt/qt_utils.cpp @@ -314,6 +314,21 @@ namespace gui open_dir(sstr(path)); } + QTreeWidgetItem* find_child(QTreeWidgetItem* parent, const QString& text) + { + if (parent) + { + for (int i = 0; i < parent->childCount(); i++) + { + if (parent->child(i)->text(0) == text) + { + return parent->child(i); + } + } + } + return nullptr; + } + QTreeWidgetItem* add_child(QTreeWidgetItem *parent, const QString& text, int column) { if (parent) diff --git a/rpcs3/rpcs3qt/qt_utils.h b/rpcs3/rpcs3qt/qt_utils.h index bd64a5aff9..2068710440 100644 --- a/rpcs3/rpcs3qt/qt_utils.h +++ b/rpcs3/rpcs3qt/qt_utils.h @@ -66,6 +66,9 @@ namespace gui // Open a path in the explorer and mark the file void open_dir(const QString& path); + + // Finds a child of a QTreeWidgetItem with given text + QTreeWidgetItem* find_child(QTreeWidgetItem* parent, const QString& text); // Constructs and adds a child to a QTreeWidgetItem QTreeWidgetItem* add_child(QTreeWidgetItem* parent, const QString& text, int column = 0); diff --git a/rpcs3/rpcs3qt/user_manager_dialog.cpp b/rpcs3/rpcs3qt/user_manager_dialog.cpp index 8876b403bc..e8224b9bd4 100644 --- a/rpcs3/rpcs3qt/user_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/user_manager_dialog.cpp @@ -2,6 +2,15 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "user_manager_dialog.h" #include "table_item_delegate.h" @@ -14,19 +23,6 @@ #include "Utilities/File.h" #include "util/logs.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - constexpr auto qstr = QString::fromStdString; LOG_CHANNEL(gui_log, "GUI");