From 12dded403fdfa967aabf4dc488f1ecbfc30eeb43 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Fri, 26 Jun 2020 02:58:06 +0200 Subject: [PATCH] patch_manager: implement serials and app_versions --- Utilities/bin_patch.cpp | 372 ++++++++++++++++++------- Utilities/bin_patch.h | 17 +- rpcs3/Emu/System.cpp | 4 +- rpcs3/Emu/System.h | 6 + rpcs3/rpcs3qt/patch_manager_dialog.cpp | 325 ++++++++++++--------- rpcs3/rpcs3qt/patch_manager_dialog.h | 14 +- rpcs3/rpcs3qt/patch_manager_dialog.ui | 62 +++-- rpcs3/rpcs3qt/qt_utils.cpp | 66 ++++- rpcs3/rpcs3qt/qt_utils.h | 8 +- 9 files changed, 596 insertions(+), 278 deletions(-) diff --git a/Utilities/bin_patch.cpp b/Utilities/bin_patch.cpp index bd567b7f8b..7ff4ab7766 100644 --- a/Utilities/bin_patch.cpp +++ b/Utilities/bin_patch.cpp @@ -2,10 +2,11 @@ #include "File.h" #include "Config.h" #include "version.h" +#include "Emu/System.h" LOG_CHANNEL(patch_log); -static const std::string patch_engine_version = "1.1"; +static const std::string patch_engine_version = "1.2"; static const std::string yml_key_enable_legacy_patches = "Enable Legacy Patches"; template <> @@ -113,7 +114,7 @@ bool patch_engine::load(patch_map& patches_map, const std::string& path, bool im // Load patch config to determine which patches are enabled bool enable_legacy_patches; - patch_config_map patch_config; + patch_map patch_config; if (!importing) { @@ -161,7 +162,7 @@ bool patch_engine::load(patch_map& patches_map, const std::string& path, bool im { struct patch_info info{}; info.hash = main_key; - info.enabled = enable_legacy_patches; + info.is_enabled = enable_legacy_patches; info.is_legacy = true; info.source_path = path; @@ -194,103 +195,143 @@ bool patch_engine::load(patch_map& patches_map, const std::string& path, bool im continue; } - if (const auto patches_node = pair.second["Patches"]) + // Find or create an entry matching the key/hash in our map + auto& container = patches_map[main_key]; + container.is_legacy = false; + container.hash = main_key; + container.version = version; + + // Go through each patch + for (auto patches_entry : pair.second) { - if (const auto yml_type = patches_node.Type(); yml_type != YAML::NodeType::Map) + // Each key in "Patches" is also the patch description + const std::string& description = patches_entry.first.Scalar(); + + // Compile patch information + + if (const auto yml_type = patches_entry.second.Type(); yml_type != YAML::NodeType::Map) { - append_log_message(log_messages, fmt::format("Error: Skipping Patches: expected Map, found %s (key: %s)", yml_type, main_key)); - patch_log.error("Skipping Patches: expected Map, found %s (key: %s, file: %s)", yml_type, main_key, path); + append_log_message(log_messages, fmt::format("Error: Skipping Patch key %s: expected Map, found %s (key: %s)", description, yml_type, main_key)); + patch_log.error("Skipping Patch key %s: expected Map, found %s (key: %s, file: %s)", description, yml_type, main_key, path); is_valid = false; continue; } - // Find or create an entry matching the key/hash in our map - auto& container = patches_map[main_key]; - container.is_legacy = false; - container.hash = main_key; - container.version = version; + struct patch_info info {}; + info.description = description; + info.hash = main_key; + info.version = version; + info.source_path = path; - // Go through each patch - for (auto patches_entry : patches_node) + if (const auto games_node = patches_entry.second["Games"]) { - // 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) + if (const auto yml_type = games_node.Type(); yml_type != YAML::NodeType::Map) { - append_log_message(log_messages, fmt::format("Error: Skipping Patch key %s: expected Map, found %s (key: %s)", description, yml_type, main_key)); - patch_log.error("Skipping Patch key %s: expected Map, found %s (key: %s, file: %s)", description, yml_type, main_key, path); + append_log_message(log_messages, fmt::format("Error: Skipping Games key: expected Map, found %s (patch: %s, key: %s)", yml_type, description, main_key)); + patch_log.error("Skipping Games key: expected Map, found %s (patch: %s, key: %s, file: %s)", yml_type, description, main_key, path); is_valid = false; continue; } - struct patch_info info {}; - info.enabled = enabled; - info.description = description; - info.hash = main_key; - info.version = version; - info.source_path = path; - - if (const auto title_node = patches_entry.second["Game Title"]) + for (const auto game_node : games_node) { - info.title = title_node.Scalar(); - } + const std::string& title = game_node.first.Scalar(); - if (const auto serials_node = patches_entry.second["Serials"]) - { - info.serials = serials_node.Scalar(); - } - - if (const auto author_node = patches_entry.second["Author"]) - { - info.author = author_node.Scalar(); - } - - if (const auto patch_version_node = patches_entry.second["Patch 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"]) - { - if (!read_patch_node(info, patch_node, root, log_messages)) + if (const auto yml_type = game_node.second.Type(); yml_type != YAML::NodeType::Map) { + append_log_message(log_messages, fmt::format("Error: Skipping %s: expected Map, found %s (patch: %s, key: %s)", title, yml_type, description, main_key)); + patch_log.error("Skipping %s: expected Map, found %s (patch: %s, key: %s, file: %s)", title, yml_type, description, main_key, path); is_valid = false; - } - } - - // Skip this patch if a higher patch version already exists - if (container.patch_info_map.find(description) != container.patch_info_map.end()) - { - bool ok; - const auto existing_version = container.patch_info_map[description].patch_version; - const bool version_is_bigger = utils::compare_versions(info.patch_version, existing_version, ok) > 0; - - if (!ok || !version_is_bigger) - { - patch_log.warning("A higher or equal patch version already exists ('%s' vs '%s') for %s: %s (in file %s)", info.patch_version, existing_version, main_key, description, path); - append_log_message(log_messages, fmt::format("A higher or equal patch version already exists ('%s' vs '%s') for %s: %s (in file %s)", info.patch_version, existing_version, main_key, description, path)); continue; } - else if (!importing) + + for (const auto serial_node : game_node.second) { - patch_log.warning("A lower patch version was found ('%s' vs '%s') for %s: %s (in file %s)", existing_version, info.patch_version, main_key, description, container.patch_info_map[description].source_path); + const std::string& serial = serial_node.first.Scalar(); + + if (const auto yml_type = serial_node.second.Type(); yml_type != YAML::NodeType::Sequence) + { + append_log_message(log_messages, fmt::format("Error: Skipping %s: expected Sequence, found %s (title: %s, patch: %s, key: %s)", serial, title, yml_type, description, main_key)); + patch_log.error("Skipping %s: expected Sequence, found %s (title: %s, patch: %s, key: %s, file: %s)", serial, title, yml_type, description, main_key, path); + is_valid = false; + continue; + } + + patch_engine::patch_app_versions app_versions; + + for (const auto version : serial_node.second) + { + const auto& app_version = version.Scalar(); + + // Find out if this patch was enabled in the patch config + const bool enabled = patch_config[main_key].patch_info_map[description].titles[title][serial][app_version]; + + app_versions.emplace(version.Scalar(), enabled); + } + + if (app_versions.empty()) + { + append_log_message(log_messages, fmt::format("Error: Skipping %s: empty Sequence (title: %s, patch: %s, key: %s)", serial, title, description, main_key)); + patch_log.error("Skipping %s: empty Sequence (title: %s, patch: %s, key: %s, file: %s)", serial,title, description, main_key, path); + is_valid = false; + } + else + { + info.titles[title][serial] = app_versions; + } } } - - // Insert patch information - container.patch_info_map[description] = info; } + + if (const auto author_node = patches_entry.second["Author"]) + { + info.author = author_node.Scalar(); + } + + if (const auto patch_version_node = patches_entry.second["Patch 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_group_node = patches_entry.second["Group"]) + { + info.patch_group = patch_group_node.Scalar(); + } + + if (const auto patch_node = patches_entry.second["Patch"]) + { + if (!read_patch_node(info, patch_node, root, log_messages)) + { + is_valid = false; + } + } + + // Skip this patch if a higher patch version already exists + if (container.patch_info_map.find(description) != container.patch_info_map.end()) + { + bool ok; + const auto existing_version = container.patch_info_map[description].patch_version; + const bool version_is_bigger = utils::compare_versions(info.patch_version, existing_version, ok) > 0; + + if (!ok || !version_is_bigger) + { + patch_log.warning("A higher or equal patch version already exists ('%s' vs '%s') for %s: %s (in file %s)", info.patch_version, existing_version, main_key, description, path); + append_log_message(log_messages, fmt::format("A higher or equal patch version already exists ('%s' vs '%s') for %s: %s (in file %s)", info.patch_version, existing_version, main_key, description, path)); + continue; + } + else if (!importing) + { + patch_log.warning("A lower patch version was found ('%s' vs '%s') for %s: %s (in file %s)", existing_version, info.patch_version, main_key, description, container.patch_info_map[description].source_path); + } + } + + // Insert patch information + container.patch_info_map[description] = info; } } @@ -507,11 +548,35 @@ std::size_t patch_engine::apply_patch(const std::string& name, u8* dst, u32 file size_t applied_total = 0; const auto& container = m_map.at(name); + const auto serial = Emu.GetTitleID(); + const auto app_version = Emu.GetAppVersion(); // Apply modifications sequentially for (const auto& [description, patch] : container.patch_info_map) { - if (!patch.enabled) + if (patch.is_legacy && !patch.is_enabled) + { + continue; + } + + bool enabled = false; + + for (const auto& [title, serials] : patch.titles) + { + if (serials.find(serial) != serials.end()) + { + if (const auto& app_versions = serials.at(serial); app_versions.find(app_version) != app_versions.end()) + { + if (app_versions.at(app_version)) + { + enabled = true; + break; + } + } + } + } + + if (!enabled) { continue; } @@ -636,8 +701,8 @@ void patch_engine::save_config(const patch_map& patches_map, bool enable_legacy_ // 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; + // Save 'enabled' state per hash, description, serial and app_version + patch_map config_map; for (const auto& [hash, container] : patches_map) { @@ -648,23 +713,62 @@ void patch_engine::save_config(const patch_map& patches_map, bool enable_legacy_ for (const auto& [description, patch] : container.patch_info_map) { - config_map[hash][description] = patch.enabled; + if (patch.is_legacy) + { + continue; + } + + for (const auto& [title, serials] : patch.titles) + { + for (const auto& [serial, app_versions] : serials) + { + for (const auto& [app_version, enabled] : app_versions) + { + if (enabled) + { + config_map[hash].patch_info_map[description].titles[title][serial][app_version] = true; + } + } + } + } } - if (config_map[hash].size() > 0) + if (const auto& enabled_patches = config_map[hash].patch_info_map; enabled_patches.size() > 0) { - out << hash; - out << YAML::BeginMap; + out << hash << YAML::BeginMap; - for (const auto& [description, enabled] : config_map[hash]) + for (const auto& [description, patch] : enabled_patches) { - out << description; - out << enabled; + const auto& titles = patch.titles; + + out << description << YAML::BeginMap; + + for (const auto& [title, serials] : titles) + { + out << title << YAML::BeginMap; + + for (const auto& [serial, app_versions] : serials) + { + out << serial << YAML::BeginMap; + + for (const auto& [app_version, enabled] : app_versions) + { + out << app_version << enabled; + } + + out << YAML::EndMap; + } + + out << YAML::EndMap; + } + + out << YAML::EndMap; } out << YAML::EndMap; } } + out << YAML::EndMap; file.write(out.c_str(), out.size()); @@ -713,9 +817,18 @@ static void append_patches(patch_engine::patch_map& existing_patches, const patc continue; } + for (const auto& [title, new_serials] : new_info.titles) + { + for (const auto& [serial, new_app_versions] : new_serials) + { + if (!new_app_versions.empty()) + { + info.titles[title][serial].insert(new_app_versions.begin(), new_app_versions.end()); + } + } + } + if (!new_info.patch_version.empty()) info.patch_version = new_info.patch_version; - if (!new_info.title.empty()) info.title = new_info.title; - if (!new_info.serials.empty()) info.serials = new_info.serials; if (!new_info.author.empty()) info.author = new_info.author; if (!new_info.notes.empty()) info.notes = new_info.notes; if (!new_info.data_list.empty()) info.data_list = new_info.data_list; @@ -751,19 +864,37 @@ bool patch_engine::save_patches(const patch_map& patches, const std::string& pat out << hash << YAML::BeginMap; out << "Patches" << YAML::BeginMap; - for (auto [description, info] : container.patch_info_map) + for (const auto& [description, info] : container.patch_info_map) { - out << description; - out << YAML::BeginMap; + out << description << YAML::BeginMap; + out << "Games" << YAML::BeginMap; + + for (const auto& [title, serials] : info.titles) + { + out << title << YAML::BeginMap; + + for (const auto& [serial, app_versions] : serials) + { + out << serial << YAML::BeginSeq; + + for (const auto& app_version : serials) + { + out << app_version.first; + } + + out << YAML::EndSeq; + } + + out << YAML::EndMap; + } + + out << YAML::EndMap; - if (!info.title.empty()) out << "Game Title" << info.title; - if (!info.serials.empty()) out << "Serials" << info.serials; if (!info.author.empty()) out << "Author" << info.author; if (!info.patch_version.empty()) out << "Patch Version" << info.patch_version; if (!info.notes.empty()) out << "Notes" << info.notes; - out << "Patch"; - out << YAML::BeginSeq; + out << "Patch" << YAML::BeginSeq; for (const auto& data : info.data_list) { @@ -830,11 +961,11 @@ bool patch_engine::remove_patch(const patch_info& info) return false; } -patch_engine::patch_config_map patch_engine::load_config(bool& enable_legacy_patches) +patch_engine::patch_map patch_engine::load_config(bool& enable_legacy_patches) { enable_legacy_patches = true; // Default to true - patch_config_map config_map; + patch_map config_map; const std::string path = get_patch_config_path(); patch_log.notice("Loading patch config file %s", path); @@ -856,10 +987,9 @@ patch_engine::patch_config_map patch_engine::load_config(bool& enable_legacy_pat root.remove(yml_key_enable_legacy_patches); // Remove the node in order to skip it in the next part } - for (auto pair : root) + for (const auto pair : root) { - auto& hash = pair.first.Scalar(); - auto& data = config_map[hash]; + const auto& hash = pair.first.Scalar(); if (const auto yml_type = pair.second.Type(); yml_type != YAML::NodeType::Map) { @@ -867,12 +997,44 @@ patch_engine::patch_config_map patch_engine::load_config(bool& enable_legacy_pat continue; } - for (auto patch : pair.second) + for (const auto patch : pair.second) { - const auto description = patch.first.Scalar(); - const auto enabled = patch.second.as(false); + const auto& description = patch.first.Scalar(); - data[description] = enabled; + if (const auto yml_type = patch.second.Type(); yml_type != YAML::NodeType::Map) + { + patch_log.error("Error loading patch %s: expected Map, found %s (hash: %s, file: %s)", description, yml_type, hash, path); + continue; + } + + for (const auto title_node : patch.second) + { + const auto& title = title_node.first.Scalar(); + + if (const auto yml_type = title_node.second.Type(); yml_type != YAML::NodeType::Map) + { + patch_log.error("Error loading %s: expected Map, found %s (description: %s, hash: %s, file: %s)", title, yml_type, description, hash, path); + continue; + } + + for (const auto serial_node : title_node.second) + { + const auto& serial = serial_node.first.Scalar(); + + if (const auto yml_type = serial_node.second.Type(); yml_type != YAML::NodeType::Map) + { + patch_log.error("Error loading %s: expected Map, found %s (title: %s, description: %s, hash: %s, file: %s)", serial, yml_type, title, description, hash, path); + continue; + } + + for (const auto app_version_node : serial_node.second) + { + const auto& app_version = app_version_node.first.Scalar(); + const bool enabled = app_version_node.second.as(false); + config_map[hash].patch_info_map[description].titles[title][serial][app_version] = enabled; + } + } + } } } } diff --git a/Utilities/bin_patch.h b/Utilities/bin_patch.h index 44a3245ccd..0c13c0cd9f 100644 --- a/Utilities/bin_patch.h +++ b/Utilities/bin_patch.h @@ -38,36 +38,39 @@ public: f64 double_value; } value { 0 }; }; + + using patch_app_versions = std::unordered_map; + using patch_serials = std::unordered_map; + using patch_titles = std::unordered_map; struct patch_info { // Patch information - std::vector data_list; + std::vector data_list; + patch_titles titles; std::string description; - std::string title; - std::string serials; std::string patch_version; + std::string patch_group; std::string author; std::string notes; std::string source_path; - bool enabled = false; // Redundant information for accessibility (see patch_container) std::string hash; std::string version; bool is_legacy = false; + bool is_enabled = false; // only for legacy patches }; struct patch_container { - std::unordered_map patch_info_map; + std::unordered_map patch_info_map; std::string hash; std::string version; bool is_legacy = false; }; using patch_map = std::unordered_map; - using patch_config_map = std::unordered_map>; patch_engine(); @@ -115,7 +118,7 @@ public: static bool remove_patch(const patch_info& info); // Load patch_config.yml - static patch_config_map load_config(bool& enable_legacy_patches); + static patch_map load_config(bool& enable_legacy_patches); // Load from file and append to member patches map void append_global_patches(); diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 2f2bd5370f..4bfd1da740 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -850,7 +850,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool add_only, bool m_title_id = psf::get_string(_psf, "TITLE_ID"); m_cat = psf::get_string(_psf, "CATEGORY"); - const std::string version_app = psf::get_string(_psf, "APP_VER", "Unknown"); + m_app_version = psf::get_string(_psf, "APP_VER", "Unknown"); const std::string version_disc = psf::get_string(_psf, "VERSION", "Unknown"); if (!_psf.empty() && m_cat.empty()) @@ -862,7 +862,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool add_only, bool sys_log.notice("Title: %s", GetTitle()); sys_log.notice("Serial: %s", GetTitleID()); sys_log.notice("Category: %s", GetCat()); - sys_log.notice("Version: %s / %s", version_app, version_disc); + sys_log.notice("Version: %s / %s", GetAppVersion(), version_disc); if (!add_only && !force_global_config) { diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index 10d37f9a14..15bbc75c70 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -64,6 +64,7 @@ class Emulator final std::string m_path_old; std::string m_title_id; std::string m_title; + std::string m_app_version; std::string m_cat; std::string m_dir; std::string m_sfo_dir; @@ -131,6 +132,11 @@ public: return m_title + (m_title_id.empty() ? "" : " [" + m_title_id + "]"); } + const std::string& GetAppVersion() const + { + return m_app_version; + } + const std::string& GetCat() const { return m_cat; diff --git a/rpcs3/rpcs3qt/patch_manager_dialog.cpp b/rpcs3/rpcs3qt/patch_manager_dialog.cpp index ea2bc44a0a..77c980c72a 100644 --- a/rpcs3/rpcs3qt/patch_manager_dialog.cpp +++ b/rpcs3/rpcs3qt/patch_manager_dialog.cpp @@ -32,7 +32,11 @@ enum patch_column : int enum patch_role : int { hash_role = Qt::UserRole, + title_role, + serial_role, + app_version_role, description_role, + patch_group_role, persistance_role, node_level_role }; @@ -41,6 +45,7 @@ enum node_level : int { title_level, serial_level, + app_version_level, patch_level }; @@ -166,77 +171,122 @@ void patch_manager_dialog::populate_tree() continue; } - const QString q_title = patch.title.empty() ? tr("Unknown Title") : QString::fromStdString(patch.title); - const QString q_serials = patch.serials.empty() ? tr("Unknown Version") : QString::fromStdString(patch.serials); + const QString q_patch_group = QString::fromStdString(patch.patch_group); - QTreeWidgetItem* title_level_item = nullptr; - - // Find top level item for this title - if (const auto list = ui->patch_tree->findItems(q_title, Qt::MatchFlag::MatchExactly, 0); list.size() > 0) + for (const auto& [title, serials] : patch.titles) { - title_level_item = list[0]; - } - - // 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); - title_level_item->setData(0, node_level_role, node_level::title_level); - - ui->patch_tree->addTopLevelItem(title_level_item); - } - assert(title_level_item); - - // Find out if there is a node item for this serial - QTreeWidgetItem* serial_level_item = gui::utils::find_child(title_level_item, q_serials); - - // 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); - serial_level_item->setData(0, node_level_role, node_level::serial_level); - - 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; - - const auto match_criteria = QList>() << QPair(description_role, q_description) << QPair(persistance_role, true); - - // 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 = gui::utils::find_children_by_data(serial_level_item, match_criteria); matches.count() > 0) - { - if (auto only_match = matches.count() == 1 ? matches[0] : nullptr) + if (serials.empty()) { - only_match->setText(0, q_description + QStringLiteral(" (1)")); + continue; + } + + const QString q_title = QString::fromStdString(title); + + QTreeWidgetItem* title_level_item = nullptr; + + // 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]; + } + + // 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, title_role, q_title); + title_level_item->setData(0, node_level_role, node_level::title_level); + title_level_item->setData(0, persistance_role, true); + + ui->patch_tree->addTopLevelItem(title_level_item); + } + assert(title_level_item); + + for (const auto& [serial, app_versions] : serials) + { + if (app_versions.empty()) + { + continue; + } + + const QString q_serial = QString::fromStdString(serial); + + // Find out if there is a node item for this serial + QTreeWidgetItem* serial_level_item = gui::utils::find_child(title_level_item, q_serial); + + // 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_serial); + serial_level_item->setData(0, title_role, q_title); + serial_level_item->setData(0, serial_role, q_serial); + serial_level_item->setData(0, node_level_role, node_level::serial_level); + serial_level_item->setData(0, persistance_role, true); + + title_level_item->addChild(serial_level_item); + } + assert(serial_level_item); + + for (const auto& [app_version, enabled] : app_versions) + { + const QString q_app_version = QString::fromStdString(app_version); + + // Find out if there is a node item for this app version + QTreeWidgetItem* app_version_level_item = gui::utils::find_child(serial_level_item, q_app_version); + + // Add a node item for this app version if it doesn't exist yet + if (!app_version_level_item) + { + app_version_level_item = new QTreeWidgetItem(); + app_version_level_item->setText(0, q_app_version); + app_version_level_item->setData(0, title_role, q_title); + app_version_level_item->setData(0, serial_role, q_serial); + app_version_level_item->setData(0, app_version_role, q_app_version); + app_version_level_item->setData(0, node_level_role, node_level::app_version_level); + app_version_level_item->setData(0, persistance_role, true); + + serial_level_item->addChild(app_version_level_item); + } + assert(app_version_level_item); + + // Add a checkable leaf item for this patch + const QString q_description = QString::fromStdString(description); + QString visible_description = q_description; + + const auto match_criteria = QList>() << QPair(description_role, q_description) << QPair(persistance_role, true); + + // 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 = gui::utils::find_children_by_data(app_version_level_item, match_criteria, false); 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, enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + patch_level_item->setData(0, hash_role, q_hash); + patch_level_item->setData(0, title_role, q_title); + patch_level_item->setData(0, serial_role, q_serial); + patch_level_item->setData(0, app_version_role, q_app_version); + patch_level_item->setData(0, description_role, q_description); + patch_level_item->setData(0, patch_group_role, q_patch_group); + patch_level_item->setData(0, node_level_role, node_level::patch_level); + patch_level_item->setData(0, persistance_role, true); + + app_version_level_item->addChild(patch_level_item); + } } - 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); - patch_level_item->setData(0, node_level_role, node_level::patch_level); - - serial_level_item->addChild(patch_level_item); - - // Persist items - title_level_item->setData(0, persistance_role, true); - serial_level_item->setData(0, persistance_role, true); - patch_level_item->setData(0, persistance_role, true); } } - ui->patch_tree->sortByColumn(0, Qt::SortOrder::AscendingOrder); + const auto match_criteria = QList>() << QPair(persistance_role, true); for (int i = ui->patch_tree->topLevelItemCount() - 1; i >= 0; i--) { @@ -248,34 +298,11 @@ void patch_manager_dialog::populate_tree() continue; } - title_level_item->sortChildren(0, Qt::SortOrder::AscendingOrder); - - for (int j = title_level_item->childCount() - 1; j >= 0; j--) - { - if (auto serial_level_item = title_level_item->child(j)) - { - if (!serial_level_item->data(0, persistance_role).toBool()) - { - delete title_level_item->takeChild(j); - continue; - } - - for (int k = serial_level_item->childCount() - 1; k >= 0; k--) - { - if (auto leaf_item = serial_level_item->child(k)) - { - if (!leaf_item->data(0, persistance_role).toBool()) - { - delete serial_level_item->takeChild(k); - } - } - } - - serial_level_item->sortChildren(0, Qt::SortOrder::AscendingOrder); - } - } + gui::utils::remove_children(title_level_item, match_criteria, true); } } + + gui::utils::sort_tree(ui->patch_tree, Qt::SortOrder::AscendingOrder, true); } void patch_manager_dialog::save_config() @@ -314,73 +341,74 @@ void patch_manager_dialog::filter_patches(const QString& term) } } -void patch_manager_dialog::update_patch_info(const patch_engine::patch_info& info) +void patch_manager_dialog::update_patch_info(const patch_manager_dialog::gui_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)); + ui->label_hash->setText(info.hash); + ui->label_author->setText(info.author); + ui->label_notes->setText(info.notes); + ui->label_description->setText(info.description); + ui->label_patch_version->setText(info.patch_version); + ui->label_serial->setText(info.serial); + ui->label_title->setText(info.title); + ui->label_app_version->setText(info.app_version); } void patch_manager_dialog::on_item_selected(QTreeWidgetItem *current, QTreeWidgetItem * /*previous*/) { if (!current) { + // Clear patch info if no item is selected + update_patch_info({}); return; } - // Get patch identifiers stored in item data const node_level level = static_cast(current->data(0, node_level_role).toInt()); - const std::string hash = current->data(0, hash_role).toString().toStdString(); - std::string description = current->data(0, description_role).toString().toStdString(); - if (m_map.find(hash) != m_map.end()) + patch_manager_dialog::gui_patch_info info{}; + + switch (level) { - const auto& container = m_map.at(hash); + case node_level::patch_level: + { + // Get patch identifiers stored in item data + info.hash = current->data(0, hash_role).toString(); + const std::string hash = info.hash.toStdString(); + const std::string description = current->data(0, description_role).toString().toStdString(); - // Find the patch for this item and show its metadata - if (!container.is_legacy && container.patch_info_map.find(description) != container.patch_info_map.end()) + // Find the patch for this item and get its metadata + if (m_map.find(hash) != m_map.end()) { - update_patch_info(container.patch_info_map.at(description)); - return; - } + const auto& container = m_map.at(hash); - // Show shared info if no patch was found - patch_engine::patch_info info{}; - info.hash = hash; - info.version = container.version; - - // Use the first entry for more shared information - const auto match_criteria = QList>() << QPair(node_level_role, node_level::patch_level); - - if (const auto matches = gui::utils::find_children_by_data(current, match_criteria, true); matches.count() > 0 && matches[0]) - { - description = matches[0]->data(0, description_role).toString().toStdString(); - - if (container.patch_info_map.find(description) != container.patch_info_map.end()) + if (!container.is_legacy && container.patch_info_map.find(description) != container.patch_info_map.end()) { - const auto& fallback_info = container.patch_info_map.at(description); - - if (level >= node_level::title_level) - { - info.title = fallback_info.title; - } - if (level >= node_level::serial_level) - { - info.serials = fallback_info.serials; - } + const auto& found_info = container.patch_info_map.at(description); + info.author = QString::fromStdString(found_info.author); + info.notes = QString::fromStdString(found_info.notes); + info.description = QString::fromStdString(found_info.description); + info.patch_version = QString::fromStdString(found_info.patch_version); } } - - update_patch_info(info); - return; + [[fallthrough]]; + } + case node_level::app_version_level: + { + info.app_version = current->data(0, app_version_role).toString(); + [[fallthrough]]; + } + case node_level::serial_level: + { + info.serial = current->data(0, serial_role).toString(); + [[fallthrough]]; + } + case node_level::title_level: + default: + { + info.title = current->data(0, title_role).toString(); + break; + } } - // Clear patch info if no info was found - patch_engine::patch_info info{}; update_patch_info(info); } @@ -395,8 +423,30 @@ void patch_manager_dialog::on_item_changed(QTreeWidgetItem *item, int /*column*/ const bool enabled = item->checkState(0) == Qt::CheckState::Checked; // Get patch identifiers stored in item data + const node_level level = static_cast(item->data(0, node_level_role).toInt()); const std::string hash = item->data(0, hash_role).toString().toStdString(); + const std::string title = item->data(0, title_role).toString().toStdString(); + const std::string serial = item->data(0, serial_role).toString().toStdString(); + const std::string app_version = item->data(0, app_version_role).toString().toStdString(); const std::string description = item->data(0, description_role).toString().toStdString(); + const std::string patch_group = item->data(0, patch_group_role).toString().toStdString(); + + // Uncheck other patches with the same patch_group if this patch was enabled + if (const auto node = item->parent(); node && enabled && !patch_group.empty() && level == node_level::patch_level) + { + for (int i = 0; i < node->childCount(); i++) + { + if (const auto other = node->child(i); other && other != item) + { + const std::string other_patch_group = other->data(0, patch_group_role).toString().toStdString(); + + if (other_patch_group == patch_group) + { + other->setCheckState(0, Qt::CheckState::Unchecked); + } + } + } + } // Enable/disable the patch for this item and show its metadata if (m_map.find(hash) != m_map.end()) @@ -405,9 +455,8 @@ void patch_manager_dialog::on_item_changed(QTreeWidgetItem *item, int /*column*/ if (!container.is_legacy && container.patch_info_map.find(description) != container.patch_info_map.end()) { - auto& patch = m_map[hash].patch_info_map[description]; - patch.enabled = enabled; - update_patch_info(patch); + m_map[hash].patch_info_map[description].titles[title][serial][app_version] = enabled; + on_item_selected(item, nullptr); return; } } diff --git a/rpcs3/rpcs3qt/patch_manager_dialog.h b/rpcs3/rpcs3qt/patch_manager_dialog.h index 59c3b151db..8120dddd3e 100644 --- a/rpcs3/rpcs3qt/patch_manager_dialog.h +++ b/rpcs3/rpcs3qt/patch_manager_dialog.h @@ -18,6 +18,18 @@ class patch_manager_dialog : public QDialog { Q_OBJECT + struct gui_patch_info + { + QString hash; + QString title; + QString serial; + QString app_version; + QString author; + QString notes; + QString description; + QString patch_version; + }; + public: explicit patch_manager_dialog(std::shared_ptr gui_settings, QWidget* parent = nullptr); ~patch_manager_dialog(); @@ -36,7 +48,7 @@ private: void load_patches(); void populate_tree(); void save_config(); - void update_patch_info(const patch_engine::patch_info& info); + void update_patch_info(const gui_patch_info& info); bool is_valid_file(const QMimeData& md, QStringList* drop_paths = nullptr); std::shared_ptr m_gui_settings; diff --git a/rpcs3/rpcs3qt/patch_manager_dialog.ui b/rpcs3/rpcs3qt/patch_manager_dialog.ui index 6ff394f2eb..700eb2b406 100644 --- a/rpcs3/rpcs3qt/patch_manager_dialog.ui +++ b/rpcs3/rpcs3qt/patch_manager_dialog.ui @@ -87,25 +87,6 @@ Patch Information - - - - Hash - - - - - - - - - true - - - - - - @@ -126,13 +107,48 @@ - + - Serials + Serial - + - + + + + + + true + + + + + + + + + + App Version + + + + + + + + + + + + + + + + Hash + + + + diff --git a/rpcs3/rpcs3qt/qt_utils.cpp b/rpcs3/rpcs3qt/qt_utils.cpp index 0e74736e65..f7b2949a4d 100644 --- a/rpcs3/rpcs3qt/qt_utils.cpp +++ b/rpcs3/rpcs3qt/qt_utils.cpp @@ -382,11 +382,75 @@ namespace gui { if (parent) { - for (int i = 0; i < parent->childCount(); i++) + for (int i = parent->childCount() - 1; i >= 0; i--) { parent->removeChild(parent->child(i)); } } } + + void remove_children(QTreeWidgetItem* parent, const QList>& criteria, bool recursive) + { + if (parent) + { + for (int i = parent->childCount() - 1; i >= 0; i--) + { + if (auto item = parent->child(i)) + { + bool match = true; + + for (const auto [role, data] : criteria) + { + if (item->data(0, role) != data) + { + match = false; + break; + } + } + + if (!match) + { + parent->removeChild(item); + } + else if (recursive) + { + remove_children(item, criteria, recursive); + } + } + } + } + } + + void sort_tree_item(QTreeWidgetItem* item, Qt::SortOrder sort_order, bool recursive) + { + if (item) + { + item->sortChildren(0, sort_order); + + if (recursive) + { + for (int i = 0; i < item->childCount(); i++) + { + sort_tree_item(item->child(i), sort_order, recursive); + } + } + } + } + + void sort_tree(QTreeWidget* tree, Qt::SortOrder sort_order, bool recursive) + { + if (tree) + { + tree->sortByColumn(0, sort_order); + + if (recursive) + { + for (int i = 0; i < tree->topLevelItemCount(); i++) + { + sort_tree_item(tree->topLevelItem(i), sort_order, recursive); + } + } + } + } } // utils } // gui diff --git a/rpcs3/rpcs3qt/qt_utils.h b/rpcs3/rpcs3qt/qt_utils.h index 39d12357bf..a7de7cb441 100644 --- a/rpcs3/rpcs3qt/qt_utils.h +++ b/rpcs3/rpcs3qt/qt_utils.h @@ -71,12 +71,18 @@ namespace gui QTreeWidgetItem* find_child(QTreeWidgetItem* parent, const QString& text); // Finds all children of a QTreeWidgetItem that match the given criteria - QList find_children_by_data(QTreeWidgetItem* parent, const QList>& criteria, bool recursive = false); + QList find_children_by_data(QTreeWidgetItem* parent, const QList>& criteria, bool recursive); // Constructs and adds a child to a QTreeWidgetItem QTreeWidgetItem* add_child(QTreeWidgetItem* parent, const QString& text, int column = 0); // Removes all children of a QTreeWidgetItem void remove_children(QTreeWidgetItem* parent); + + // Removes all children of a QTreeWidgetItem that don't match the given criteria + void remove_children(QTreeWidgetItem* parent, const QList>& criteria, bool recursive); + + // Sort a QTreeWidget (currently only column 0) + void sort_tree(QTreeWidget* tree, Qt::SortOrder sort_order, bool recursive); } // utils } // gui