From 06a6880c6c21f28ed7eddf03883a74d3ff993399 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sun, 19 Apr 2026 11:21:46 +0200 Subject: [PATCH] Qt: Allow to compare configurations in gamelist context menu --- rpcs3/Emu/system_config.h | 2 +- rpcs3/rpcs3qt/config_checker.cpp | 287 +++++++++++++++++++---- rpcs3/rpcs3qt/config_checker.h | 25 +- rpcs3/rpcs3qt/game_list_context_menu.cpp | 21 ++ rpcs3/rpcs3qt/log_viewer.cpp | 2 +- rpcs3/rpcs3qt/main_window.cpp | 2 +- 6 files changed, 290 insertions(+), 49 deletions(-) diff --git a/rpcs3/Emu/system_config.h b/rpcs3/Emu/system_config.h index befb64f282..ba52f14d5d 100644 --- a/rpcs3/Emu/system_config.h +++ b/rpcs3/Emu/system_config.h @@ -282,7 +282,7 @@ struct cfg_root : cfg::node cfg::_bool paint_move_spheres{this, "Paint move spheres", false, true}; cfg::_bool allow_move_hue_set_by_game{this, "Allow move hue set by game", false, true}; cfg::_bool lock_overlay_input_to_player_one{this, "Lock overlay input to player one", false, true}; - cfg::string midi_devices{this, "Emulated Midi devices", "ßßß@@@ßßß@@@ßßß@@@"}; + cfg::string midi_devices{this, "Emulated Midi devices", "Keyboardßßß@@@Keyboardßßß@@@Keyboardßßß@@@"}; cfg::_bool load_sdl_mappings{ this, "Load SDL GameController Mappings", true }; cfg::_bool pad_debug_overlay{ this, "IO Debug overlay", false, true }; cfg::_bool mouse_debug_overlay{ this, "Mouse Debug overlay", false, true }; diff --git a/rpcs3/rpcs3qt/config_checker.cpp b/rpcs3/rpcs3qt/config_checker.cpp index 6a0851362a..f8ff600205 100644 --- a/rpcs3/rpcs3qt/config_checker.cpp +++ b/rpcs3/rpcs3qt/config_checker.cpp @@ -1,76 +1,127 @@ #include "stdafx.h" #include "config_checker.h" +#include "midi_creator.h" +#include "microphone_creator.h" #include "Emu/system_config.h" +#include "Emu/system_utils.hpp" +#include #include #include #include -#include #include -#include LOG_CHANNEL(gui_log, "GUI"); -config_checker::config_checker(QWidget* parent, const QString& content, bool is_log) : QDialog(parent) +config_checker::config_checker(QWidget* parent, const QString& content_or_serial, checker_mode mode, const std::string& db_config) + : QDialog(parent) + , m_checker_mode(mode) + , m_content_or_serial(content_or_serial) + , m_db_config(db_config) { setObjectName("config_checker"); + setWindowTitle(tr("Config Checker")); setAttribute(Qt::WA_DeleteOnClose); QVBoxLayout* layout = new QVBoxLayout(); - QLabel* label = new QLabel(this); - layout->addWidget(label); + QComboBox* combo = nullptr; - QString result; - - if (check_config(content, result, is_log)) + if (mode == checker_mode::gamelist) { - setWindowTitle(tr("Interesting!")); + m_serial = content_or_serial.toStdString(); - if (result.isEmpty()) + combo = new QComboBox(this); + + std::string custom_config_path; + if (std::string config_path = rpcs3::utils::get_custom_config_path(m_serial); fs::is_file(config_path)) { - label->setText(tr("Found config.\nIt seems to match the default config.")); + custom_config_path = std::move(config_path); + combo->addItem(tr("Custom Configuration"), static_cast(cfg_mode::custom)); } - else + + combo->addItem(tr("Database + Global Configuration"), static_cast(cfg_mode::database_config)); + combo->setCurrentIndex(combo->findData(static_cast(custom_config_path.empty() ? cfg_mode::database_config : cfg_mode::custom))); + + connect(combo, &QComboBox::currentIndexChanged, this, [this, combo]() { - label->setText(tr("Found config.\nSome settings seem to deviate from the default config:")); + check_config(static_cast(combo->currentData().toInt())); + }); - QTextEdit* text_box = new QTextEdit(); - text_box->setReadOnly(true); - text_box->setHtml(result); - layout->addWidget(text_box); + layout->addWidget(combo); + } - resize(400, 600); - } - } - else - { - setWindowTitle(tr("Ooops!")); - label->setText(result); - } + m_label = new QLabel(this); + layout->addWidget(m_label); + + m_text_box = new QTextEdit(); + m_text_box->setReadOnly(true); + layout->addWidget(m_text_box); QDialogButtonBox* box = new QDialogButtonBox(QDialogButtonBox::Close); connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject); layout->addWidget(box); setLayout(layout); + resize(400, 600); + + check_config(combo ? static_cast(combo->currentData().toInt()) : cfg_mode::database_config); } -bool config_checker::check_config(QString content, QString& result, bool is_log) +void config_checker::check_config(cfg_mode mode) { - cfg_root config{}; + QString result; - if (is_log) + if (check_config(mode, m_content_or_serial, result)) + { + if (m_checker_mode == checker_mode::gamelist) + { + if (result.isEmpty()) + { + m_label->setText(tr("The configuration seems to match the default config.")); + } + else + { + m_label->setText(tr("Config database settings are marked with an * in front of the name.\nSome settings seem to deviate from the default config:")); + } + } + else + { + if (result.isEmpty()) + { + m_label->setText(tr("Found config.\nIt seems to match the default config.")); + } + else + { + m_label->setText(tr("Found config.\nSome settings seem to deviate from the default config:")); + } + } + + m_text_box->setVisible(!result.isEmpty()); + m_text_box->setHtml(result); + } + else + { + m_label->setText(result); + } +} + +bool config_checker::check_config(cfg_mode mode, QString content_or_serial, QString& result) +{ + std::unique_ptr config = std::make_unique(); + std::unique_ptr config_db_only; + + if (m_checker_mode == checker_mode::log) { const QString start_token = "SYS: Used configuration:\n"; const QString end_token = "\n·"; - qsizetype start = content.indexOf(start_token); + qsizetype start = content_or_serial.indexOf(start_token); qsizetype end = -1; if (start >= 0) { start += start_token.size(); - end = content.indexOf(end_token, start); + end = content_or_serial.indexOf(end_token, start); } if (end < 0) @@ -79,24 +130,93 @@ bool config_checker::check_config(QString content, QString& result, bool is_log) return false; } - content = content.mid(start, end - start); + content_or_serial = content_or_serial.mid(start, end - start); } - if (!config.from_string(content.toStdString())) + if (m_checker_mode == checker_mode::gamelist) { - gui_log.error("log_viewer: Failed to parse config:\n%s", content); + config->from_default(); + + // Load global config + const std::string cfg_path = fs::get_config_dir(true) + "config.yml"; + if (const fs::file cfg_file{cfg_path}) + { + gui_log.notice("config_checker: Applying global config: %s", cfg_path); + + if (!config->from_string(cfg_file.to_string())) + { + gui_log.error("config_checker: Failed to apply global config: %s", cfg_path); + result = tr("Failed to apply global config!"); + return false; + } + } + + // Load custom config + const std::string custom_config_path = rpcs3::utils::get_custom_config_path(m_serial); + if (mode == cfg_mode::custom && !custom_config_path.empty()) + { + if (const fs::file cfg_file{custom_config_path}) + { + gui_log.notice("config_checker: Applying custom config: %s", custom_config_path); + + if (!config->from_string(cfg_file.to_string())) + { + gui_log.error("config_checker: Failed to apply custom config: %s", custom_config_path); + result = tr("Failed to apply custom config!"); + return false; + } + } + } + + if (mode == cfg_mode::database_config && !m_db_config.empty()) + { + gui_log.notice("config_checker: Applying database config: %s", custom_config_path); + + if (!config->from_string(m_db_config)) + { + gui_log.error("config_checker: Failed to apply database config:\n%s", m_db_config); + result = tr("Failed to apply database config!"); + return false; + } + + config_db_only = std::make_unique(); + config_db_only->from_default(); + if (!config_db_only->from_string(m_db_config)) + { + gui_log.error("config_checker: Failed to apply database config:\n%s", m_db_config); + result = tr("Failed to apply database config!"); + return false; + } + } + } + else if (!config->from_string(content_or_serial.toStdString())) + { + gui_log.error("config_checker: Failed to parse config:\n%s", content_or_serial); result = tr("Cannot find any config!"); return false; } - std::function print_diff_recursive; - print_diff_recursive = [&print_diff_recursive](const cfg::_base* base, std::string& diff, int indentation) -> void + std::function print_diff_recursive; + print_diff_recursive = [this, &print_diff_recursive, &config](const cfg::_base* base, const cfg::_base* base_db_only, std::string& diff, int indentation) -> void { if (!base) { return; } + // Ignore some irrelevant settings in gamelist mode + if (m_checker_mode == checker_mode::gamelist && base->get_type() != cfg::type::node) + { + const std::string key = base->get_name(); + + if (key == config->sys.console_psid.get_name() || + key == config->sys.system_name.get_name() || + key == config->video.vk.adapter.get_name()) + { + return; + } + } + const auto indent = [](std::string& str, int indentation) { for (int i = 0; i < indentation * 2; i++) @@ -105,6 +225,16 @@ bool config_checker::check_config(QString content, QString& result, bool is_log) } }; + const auto base_name_db = [base](bool is_db_config) + { + if (is_db_config) + { + return "*" + base->get_name(); + } + + return base->get_name(); + }; + switch (base->get_type()) { case cfg::type::node: @@ -115,7 +245,20 @@ bool config_checker::check_config(QString content, QString& result, bool is_log) for (const auto& n : node->get_nodes()) { - print_diff_recursive(n, diff_tmp, indentation + 1); + const cfg::_base* n_db_only = nullptr; + if (const auto& node_db_only = static_cast(base_db_only)) + { + for (const auto& n_db : node_db_only->get_nodes()) + { + if (n_db->get_name() == n->get_name()) + { + n_db_only = n_db; + break; + } + } + } + + print_diff_recursive(n, n_db_only, diff_tmp, indentation + 1); } if (!diff_tmp.empty()) @@ -142,19 +285,75 @@ bool config_checker::check_config(QString content, QString& result, bool is_log) const std::string val = base->to_string(); const std::string def = base->def_to_string(); - if (val != def) - { - indent(diff, indentation); + if (val == def) + break; - if (def.empty()) + indent(diff, indentation); + + if (m_checker_mode == checker_mode::gamelist) + { + if (base->get_name() == config->io.midi_devices.get_name()) { - fmt::append(diff, "%s: %s
", base->get_name(), val); + fmt::append(diff, "%s:
", base->get_name()); + + midi_creator mc {}; + + mc.parse_devices(def); + const std::array def_devices = mc.get_selection_list(); + + mc.parse_devices(val); + const std::array devices = mc.get_selection_list(); + + for (usz i = 0; i < devices.size(); i++) + { + const midi_device& def_device = def_devices[i]; + const midi_device& device = devices[i]; + + if (device.name == def_device.name) + continue; + + indent(diff, indentation + 1); + fmt::append(diff, "Device %d: %s: %s
", i + 1, device.type, device.name); + } + break; } - else + else if (base->get_name() == config->audio.microphone_devices.get_name()) { - fmt::append(diff, "%s: %s default: %s
", base->get_name(), val, def); + fmt::append(diff, "%s:
", base->get_name()); + + microphone_creator mc {}; + + mc.parse_devices(def); + const std::array def_devices = mc.get_selection_list(); + + mc.parse_devices(val); + const std::array devices = mc.get_selection_list(); + + for (usz i = 0; i < devices.size(); i++) + { + const std::string& def_device = def_devices[i]; + const std::string& device = devices[i]; + + if (device == def_device) + continue; + + indent(diff, indentation + 1); + fmt::append(diff, "Device %d: %s
", i + 1, device); + } + break; } } + + const bool is_db_config = base_db_only && base_db_only->to_string() != def; + + if (def.empty()) + { + fmt::append(diff, "%s: %s
", base_name_db(is_db_config), val); + } + else + { + fmt::append(diff, "%s: %s default: %s
", base_name_db(is_db_config), val, def); + } break; } case cfg::type::set: @@ -208,7 +407,7 @@ bool config_checker::check_config(QString content, QString& result, bool is_log) }; std::string diff; - print_diff_recursive(&config, diff, 0); + print_diff_recursive(config.get(), config_db_only.get(), diff, 0); result = QString::fromStdString(diff); return true; diff --git a/rpcs3/rpcs3qt/config_checker.h b/rpcs3/rpcs3qt/config_checker.h index 900fb30351..71b4a27de3 100644 --- a/rpcs3/rpcs3qt/config_checker.h +++ b/rpcs3/rpcs3qt/config_checker.h @@ -1,13 +1,34 @@ #pragma once +#include "Emu/config_mode.h" + #include +#include +#include class config_checker : public QDialog { Q_OBJECT public: - config_checker(QWidget* parent, const QString& path, bool is_log); + enum class checker_mode + { + config, + log, + gamelist + }; - bool check_config(QString content, QString& result, bool is_log); + config_checker(QWidget* parent, const QString& content_or_serial, checker_mode mode, const std::string& db_config = {}); + +private: + void check_config(cfg_mode mode); + bool check_config(cfg_mode mode, QString content_or_serial, QString& result); + + QLabel* m_label = nullptr; + QTextEdit* m_text_box = nullptr; + + checker_mode m_checker_mode = checker_mode::config; + QString m_content_or_serial; + std::string m_db_config; + std::string m_serial; }; diff --git a/rpcs3/rpcs3qt/game_list_context_menu.cpp b/rpcs3/rpcs3qt/game_list_context_menu.cpp index 05fc9fdb8d..f8a26d3f80 100644 --- a/rpcs3/rpcs3qt/game_list_context_menu.cpp +++ b/rpcs3/rpcs3qt/game_list_context_menu.cpp @@ -12,6 +12,7 @@ #include "patch_manager_dialog.h" #include "persistent_settings.h" #include "config_database.h" +#include "config_checker.h" #include "Utilities/File.h" #include "Emu/system_utils.hpp" @@ -167,6 +168,26 @@ void game_list_context_menu::show_single_selection_context_menu(const game_info& QAction* pad_configure = addAction(gameinfo->has_custom_pad_config ? tr("&Change Custom Gamepad Configuration") : tr("&Create Custom Gamepad Configuration")); + + QAction* compare_config = addAction(tr("&Compare Configurations")); + connect(compare_config, &QAction::triggered, this, [this, serial]() + { + std::string db_config; + if (config_database* db = m_game_list_frame->GetConfigDatabase(); db->has_config(serial)) + { + if (const std::optional config = db->get_config(serial)) + { + db_config = *config; + } + else + { + game_list_log.error("No database config found for '%s'", serial); + } + } + config_checker* dlg = new config_checker(m_game_list_frame, QString::fromStdString(serial), config_checker::checker_mode::gamelist, db_config); + dlg->open(); + }); + QAction* configure_patches = addAction(tr("&Manage Game Patches")); addSeparator(); diff --git a/rpcs3/rpcs3qt/log_viewer.cpp b/rpcs3/rpcs3qt/log_viewer.cpp index b69c07901e..623a74b48a 100644 --- a/rpcs3/rpcs3qt/log_viewer.cpp +++ b/rpcs3/rpcs3qt/log_viewer.cpp @@ -201,7 +201,7 @@ void log_viewer::show_context_menu(const QPoint& pos) connect(config, &QAction::triggered, this, [this]() { - config_checker* dlg = new config_checker(this, m_full_log, true); + config_checker* dlg = new config_checker(this, m_full_log, config_checker::checker_mode::log); dlg->open(); }); diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index cfd2394d7c..9dd437b535 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -3250,7 +3250,7 @@ void main_window::CreateConnects() m_gui_settings->SetValue(gui::fd_cfg_check, file_info.path()); - config_checker* dlg = new config_checker(this, content, file_path.endsWith(".log") || file_path.endsWith(".log.gz")); + config_checker* dlg = new config_checker(this, content, (file_path.endsWith(".log") || file_path.endsWith(".log.gz")) ? config_checker::checker_mode::log : config_checker::checker_mode::config); dlg->open(); });