Qt: Allow to compare configurations in gamelist context menu

This commit is contained in:
Megamouse 2026-04-19 11:21:46 +02:00
parent bd5c10fd48
commit 06a6880c6c
6 changed files with 290 additions and 49 deletions

View File

@ -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 };

View File

@ -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 <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QMessageBox>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QLabel>
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<int>(cfg_mode::custom));
}
else
combo->addItem(tr("Database + Global Configuration"), static_cast<int>(cfg_mode::database_config));
combo->setCurrentIndex(combo->findData(static_cast<int>(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<cfg_mode>(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<cfg_mode>(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<cfg_root> config = std::make_unique<cfg_root>();
std::unique_ptr<cfg_root> 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<cfg_root>();
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<void(const cfg::_base*, std::string&, int)> print_diff_recursive;
print_diff_recursive = [&print_diff_recursive](const cfg::_base* base, std::string& diff, int indentation) -> void
std::function<void(const cfg::_base*, const cfg::_base*, std::string&, int)> 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<const cfg::node*>(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: <span style=\"color:red;\">%s</span><br>", base->get_name(), val);
fmt::append(diff, "%s:<br>", base->get_name());
midi_creator mc {};
mc.parse_devices(def);
const std::array<midi_device, max_midi_devices> def_devices = mc.get_selection_list();
mc.parse_devices(val);
const std::array<midi_device, max_midi_devices> 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: <span style=\"color:red;\">%s: %s</span><br>", i + 1, device.type, device.name);
}
break;
}
else
else if (base->get_name() == config->audio.microphone_devices.get_name())
{
fmt::append(diff, "%s: <span style=\"color:red;\">%s</span> <span style=\"color:gray;\">default:</span> <span style=\"color:green;\">%s</span><br>", base->get_name(), val, def);
fmt::append(diff, "%s:<br>", base->get_name());
microphone_creator mc {};
mc.parse_devices(def);
const std::array<std::string, 4> def_devices = mc.get_selection_list();
mc.parse_devices(val);
const std::array<std::string, 4> 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: <span style=\"color:red;\">%s</span><br>", 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: <span style=\"color:red;\">%s</span><br>", base_name_db(is_db_config), val);
}
else
{
fmt::append(diff, "%s: <span style=\"color:red;\">%s</span> <span style=\"color:gray;\">default:</span> <span style=\"color:green;\">%s</span><br>", 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;

View File

@ -1,13 +1,34 @@
#pragma once
#include "Emu/config_mode.h"
#include <QDialog>
#include <QLabel>
#include <QTextEdit>
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;
};

View File

@ -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<std::string> 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();

View File

@ -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();
});

View File

@ -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();
});