Compare commits

...

3 Commits

Author SHA1 Message Date
NeySlim 1195af2510 decoder: add V_CMPX_U_F32 (#1120)
feat(decoder): add V_CMPX_U_F32
2026-10-07 01:16:23 +04:00
nmzik 9a5658c038 launcher: add per-game config import and export 2026-10-07 01:02:33 +04:00
Oberd 199d0a9d92 kernel: name guest threads on the host (#1106)
PthreadCreate carries a guest thread name, but RunThread never handed it to
the host, so every guest thread showed up as the same generic host thread
name in core dumps, perf samples and htop.

Call pthread_setname_np() on Linux so host-side diagnostics can attribute
work to the guest thread that caused it.
2026-10-06 23:43:36 +04:00
12 changed files with 326 additions and 97 deletions
@@ -382,6 +382,7 @@ enum class Opcode {
V_CMPX_LG_F32,
V_CMPX_GE_F32,
V_CMPX_O_F32,
V_CMPX_U_F32,
V_CMPX_NGE_F32,
V_CMPX_NLG_F32,
V_CMPX_NGT_F32,
@@ -232,7 +232,7 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0x11u, Opcode::V_CMPX_LT_F32}, {0x12u, Opcode::V_CMPX_EQ_F32},
{0x13u, Opcode::V_CMPX_LE_F32}, {0x14u, Opcode::V_CMPX_GT_F32},
{0x15u, Opcode::V_CMPX_LG_F32}, {0x16u, Opcode::V_CMPX_GE_F32},
{0x17u, Opcode::V_CMPX_O_F32},
{0x17u, Opcode::V_CMPX_O_F32}, {0x18u, Opcode::V_CMPX_U_F32},
{0x19u, Opcode::V_CMPX_NGE_F32}, {0x1au, Opcode::V_CMPX_NLG_F32},
{0x1bu, Opcode::V_CMPX_NGT_F32}, {0x1cu, Opcode::V_CMPX_NLE_F32},
{0x1du, Opcode::V_CMPX_NEQ_F32}, {0x1eu, Opcode::V_CMPX_NLT_F32},
@@ -892,6 +892,7 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
case Opcode::V_CMPX_LG_F32:
case Opcode::V_CMPX_GE_F32:
case Opcode::V_CMPX_O_F32:
case Opcode::V_CMPX_U_F32:
case Opcode::V_CMPX_NGE_F32:
case Opcode::V_CMPX_NLG_F32:
case Opcode::V_CMPX_NGT_F32:
@@ -1529,6 +1530,7 @@ bool IsVopcCompareExec(Opcode opcode) {
case Opcode::V_CMPX_LG_F32:
case Opcode::V_CMPX_GE_F32:
case Opcode::V_CMPX_O_F32:
case Opcode::V_CMPX_U_F32:
case Opcode::V_CMPX_NGE_F32:
case Opcode::V_CMPX_NLG_F32:
case Opcode::V_CMPX_NGT_F32:
@@ -344,6 +344,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMP_O_F32: EmitFloatOrderedCompare(inst, true, false); return;
case O::V_CMP_U_F32: EmitFloatOrderedCompare(inst, false, false); return;
case O::V_CMPX_O_F32: EmitFloatOrderedCompare(inst, true, true); return;
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false, true); return;
case O::V_CMP_CLASS_F32: EmitFloatClassCompare(inst, false); return;
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst, true); return;
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true, true); return;
+5
View File
@@ -3200,6 +3200,11 @@ static void* RunThread(void* arg) {
os_thread_id = GetHostThreadId();
#endif
thread->host_thread_id = os_thread_id;
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX
if (!thread->name.empty()) {
pthread_setname_np(pthread_self(), thread->name.substr(0, 15).c_str());
}
#endif
LOGF("\tPthread run begin: %s, id = %d, os_thread_id = %" PRIu64 ", entry = 0x%016" PRIx64
", arg = 0x%016" PRIx64 ", stack_addr = 0x%016" PRIx64 ", stack_size = %" PRIu64 "\n",
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>840</width>
<width>900</width>
<height>620</height>
</rect>
</property>
@@ -400,6 +400,13 @@ To use your own sound, replace the emulator's assets/sounds/trophy-unlock.wav wi
<property name="toolTip"><string>Restore default settings</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="settings_button">
<property name="minimumSize"><size><width>80</width><height>25</height></size></property>
<property name="text"><string>Game Configs</string></property>
<property name="toolTip"><string>Import or export game configs</string></property>
</widget>
</item>
<item>
<spacer name="buttonSpacer">
<property name="orientation"><enum>Qt::Horizontal</enum></property>
@@ -125,7 +125,7 @@
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Edit configuration&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>Edit game config</string>
</property>
<property name="text">
<string/>
@@ -150,7 +150,7 @@
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Delete configuration&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>Clear game config</string>
</property>
<property name="styleSheet">
<string notr="true"/>
+11 -87
View File
@@ -6,6 +6,7 @@
#include <QByteArray>
#include <QChar>
#include <QColor>
#include <QJsonObject>
#include <QMetaEnum>
#include <QMetaType>
#include <QObject>
@@ -14,9 +15,6 @@
#include <QStringList>
#include <QVariant>
#define KYTY_CFG_SET(n) s->setValue(#n, QVariant::fromValue(n).toString());
#define KYTY_CFG_GET(n) n = s->value(#n).value<decltype(n)>();
template <class T>
inline QStringList EnumToList() {
QStringList ret;
@@ -126,7 +124,7 @@ public:
int vblank_frequency = 60;
int console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
bool shader_validation_enabled = true;
bool shader_validation_enabled = false;
ShaderOptimizationType shader_optimization_type = ShaderOptimizationType::Performance;
LogDirection shader_log_direction = LogDirection::Silent;
QString shader_log_folder = "_Shaders";
@@ -188,90 +186,16 @@ public:
game_comment = other.game_comment;
}
void WriteSettings(QSettings* s) const {
KYTY_CFG_SET(name);
KYTY_CFG_SET(basedir);
KYTY_CFG_SET(game_path);
KYTY_CFG_SET(custom_settings);
KYTY_CFG_SET(screen_resolution);
KYTY_CFG_SET(user_name);
KYTY_CFG_SET(user_id);
KYTY_CFG_SET(audio_input_device);
KYTY_CFG_SET(present_mode);
KYTY_CFG_SET(gpu_index);
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(hide_cursor_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(trophy_enabled);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
KYTY_CFG_SET(shader_validation_enabled);
KYTY_CFG_SET(shader_optimization_type);
KYTY_CFG_SET(shader_log_direction);
KYTY_CFG_SET(shader_log_folder);
KYTY_CFG_SET(command_buffer_dump_enabled);
KYTY_CFG_SET(command_buffer_dump_folder);
KYTY_CFG_SET(printf_direction);
KYTY_CFG_SET(printf_output_file);
KYTY_CFG_SET(profiler_enabled);
KYTY_CFG_SET(renderdoc_enabled);
KYTY_CFG_SET(amd_cpu_enabled);
#if defined(_WIN32)
KYTY_CFG_SET(red_zone_protection_enabled);
#endif
s->setValue("host_input_mapping", host_input_mapping);
KYTY_CFG_SET(elf);
}
void WriteSettings(QSettings* s) const;
void ReadSettings(QSettings* s);
void ReadSettings(QSettings* s) {
KYTY_CFG_GET(name);
KYTY_CFG_GET(basedir);
KYTY_CFG_GET(game_path);
KYTY_CFG_GET(custom_settings);
KYTY_CFG_GET(screen_resolution);
user_name = s->value("user_name", user_name).toString();
bool user_id_ok = false;
auto saved_user_id = s->value("user_id", user_id).toInt(&user_id_ok);
user_id = user_id_ok && Config::IsConfiguredUserIdValid(saved_user_id)
? saved_user_id
: Config::DEFAULT_USER_ID;
audio_input_device = s->value("audio_input_device", audio_input_device).toString();
KYTY_CFG_GET(present_mode);
gpu_index = s->value("gpu_index", -1).toInt();
if (EnumToText(present_mode).isEmpty()) {
present_mode = PresentMode::Mailbox;
}
KYTY_CFG_GET(fullscreen_enabled);
KYTY_CFG_GET(hide_cursor_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(tessellation_enabled);
trophy_enabled = s->value("trophy_enabled", trophy_enabled).toBool();
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
console_language = s->value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
console_language = DEFAULT_CONSOLE_LANGUAGE;
}
KYTY_CFG_GET(vulkan_validation_enabled);
KYTY_CFG_GET(shader_validation_enabled);
KYTY_CFG_GET(shader_optimization_type);
KYTY_CFG_GET(shader_log_direction);
KYTY_CFG_GET(shader_log_folder);
KYTY_CFG_GET(command_buffer_dump_enabled);
KYTY_CFG_GET(command_buffer_dump_folder);
KYTY_CFG_GET(printf_direction);
KYTY_CFG_GET(printf_output_file);
KYTY_CFG_GET(profiler_enabled);
KYTY_CFG_GET(renderdoc_enabled);
amd_cpu_enabled = s->value("amd_cpu_enabled", false).toBool();
#if defined(_WIN32)
red_zone_protection_enabled =
s->value("red_zone_protection_enabled", red_zone_protection_enabled).toBool();
#endif
host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList();
elf = s->value("elf", elf).toString();
}
[[nodiscard]] QVariantMap GameSettings() const;
// Call on a temporary configuration: validation can fail after loading values.
bool SetGameSettings(const QJsonObject& settings, QString& error);
private:
template <class Settings>
void ReadGameSettingsValues(const Settings& s);
};
Q_DECLARE_METATYPE(Configuration*)
@@ -33,6 +33,8 @@ public:
signals:
void PreviewControllerColor(const QString& color);
void ImportGameSettings();
void ExportGameSettings();
private:
Ui::ConfigurationEditDialog* m_ui = nullptr;
@@ -74,6 +74,8 @@ private:
void SelectItem(QTreeWidgetItem* witem);
void ApplyCompatibility();
void UpdateToolbarIcons();
void ImportGameSettings(QWidget* parent);
void ExportGameSettings(QWidget* parent) const;
[[nodiscard]] bool HasValidGameDirectory() const;
ConfigurationItem* m_selected_item = nullptr;
+145
View File
@@ -0,0 +1,145 @@
#include "configuration.h"
QVariantMap Configuration::GameSettings() const {
QVariantMap values;
#define KYTY_CFG_SET(n) values.insert(QStringLiteral(#n), QVariant::fromValue(n).toString())
KYTY_CFG_SET(screen_resolution);
KYTY_CFG_SET(user_name);
KYTY_CFG_SET(user_id);
KYTY_CFG_SET(audio_input_device);
KYTY_CFG_SET(present_mode);
KYTY_CFG_SET(gpu_index);
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(hide_cursor_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(trophy_enabled);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
KYTY_CFG_SET(shader_validation_enabled);
KYTY_CFG_SET(shader_optimization_type);
KYTY_CFG_SET(shader_log_direction);
KYTY_CFG_SET(shader_log_folder);
KYTY_CFG_SET(command_buffer_dump_enabled);
KYTY_CFG_SET(command_buffer_dump_folder);
KYTY_CFG_SET(printf_direction);
KYTY_CFG_SET(printf_output_file);
KYTY_CFG_SET(profiler_enabled);
KYTY_CFG_SET(renderdoc_enabled);
KYTY_CFG_SET(amd_cpu_enabled);
#if defined(_WIN32)
KYTY_CFG_SET(red_zone_protection_enabled);
#endif
KYTY_CFG_SET(elf);
#undef KYTY_CFG_SET
return values;
}
template <class Settings>
void Configuration::ReadGameSettingsValues(const Settings& s) {
#define KYTY_CFG_GET(n) n = s.value(QStringLiteral(#n)).template value<decltype(n)>()
KYTY_CFG_GET(screen_resolution);
user_name = s.value("user_name", user_name).toString();
bool user_id_ok = false;
auto saved_user_id = s.value("user_id", user_id).toInt(&user_id_ok);
user_id = user_id_ok && Config::IsConfiguredUserIdValid(saved_user_id)
? saved_user_id
: Config::DEFAULT_USER_ID;
audio_input_device = s.value("audio_input_device", audio_input_device).toString();
KYTY_CFG_GET(present_mode);
gpu_index = s.value("gpu_index", -1).toInt();
if (EnumToText(present_mode).isEmpty()) {
present_mode = PresentMode::Mailbox;
}
KYTY_CFG_GET(fullscreen_enabled);
KYTY_CFG_GET(hide_cursor_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(tessellation_enabled);
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt();
console_language = s.value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
console_language = DEFAULT_CONSOLE_LANGUAGE;
}
KYTY_CFG_GET(vulkan_validation_enabled);
KYTY_CFG_GET(shader_validation_enabled);
KYTY_CFG_GET(shader_optimization_type);
KYTY_CFG_GET(shader_log_direction);
KYTY_CFG_GET(shader_log_folder);
KYTY_CFG_GET(command_buffer_dump_enabled);
KYTY_CFG_GET(command_buffer_dump_folder);
KYTY_CFG_GET(printf_direction);
KYTY_CFG_GET(printf_output_file);
KYTY_CFG_GET(profiler_enabled);
KYTY_CFG_GET(renderdoc_enabled);
amd_cpu_enabled = s.value("amd_cpu_enabled", false).toBool();
#if defined(_WIN32)
red_zone_protection_enabled =
s.value("red_zone_protection_enabled", red_zone_protection_enabled).toBool();
#endif
elf = s.value("elf", elf).toString();
#undef KYTY_CFG_GET
}
void Configuration::WriteSettings(QSettings* s) const {
const auto values = GameSettings();
for (auto it = values.constBegin(); it != values.constEnd(); ++it) {
s->setValue(it.key(), it.value());
}
s->setValue("name", name);
s->setValue("basedir", basedir);
s->setValue("game_path", game_path);
s->setValue("custom_settings", QVariant::fromValue(custom_settings).toString());
s->setValue("host_input_mapping", host_input_mapping);
}
void Configuration::ReadSettings(QSettings* s) {
ReadGameSettingsValues(*s);
name = s->value("name").toString();
basedir = s->value("basedir").toString();
game_path = s->value("game_path").toString();
custom_settings = s->value("custom_settings").toBool();
host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList();
}
bool Configuration::SetGameSettings(const QJsonObject& settings, QString& error) {
const auto fail = [&error](const QString& message) {
error = message;
return false;
};
auto values = GameSettings();
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
if (!values.contains(it.key())) {
return fail(tr("Unknown game setting: %1").arg(it.key()));
}
if (!it.value().isString()) {
return fail(tr("Game setting %1 must be a string.").arg(it.key()));
}
values.insert(it.key(), it.value().toString());
}
ReadGameSettingsValues(values);
const auto canonical = GameSettings();
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
if (canonical.value(it.key()) != values.value(it.key())) {
return fail(tr("Invalid value for game setting: %1").arg(it.key()));
}
}
if (user_name.trimmed().isEmpty() || user_name.toUtf8().size() > Config::MAX_USER_NAME_LENGTH) {
return fail(tr("User name must contain 1-16 UTF-8 bytes."));
}
if (gpu_index < -1) {
return fail(tr("GPU index must be -1 or greater."));
}
if (vblank_frequency < 30 || vblank_frequency > 360) {
return fail(tr("Vblank frequency must be between 30 and 360."));
}
if ((shader_log_direction == LogDirection::File && shader_log_folder.isEmpty()) ||
(printf_direction == LogDirection::File && printf_output_file.isEmpty()) ||
(command_buffer_dump_enabled && command_buffer_dump_folder.isEmpty())) {
return fail(tr("Enabled logging and command buffer dumps require an output path."));
}
return true;
}
@@ -19,6 +19,7 @@
#include <QLineEdit>
#include <QListView>
#include <QListWidget>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QResizeEvent>
@@ -126,6 +127,7 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
setMinimumWidth(width());
InitGameDirectories();
m_ui->controller_group->setVisible(false);
m_ui->settings_button->setVisible(false);
connect(m_ui->ok_button, &QPushButton::clicked, this, &ConfigurationEditDialog::save);
connect(m_ui->cancel_button, &QPushButton::clicked, this, &QDialog::reject);
@@ -354,6 +356,11 @@ void ConfigurationEditDialog::InitGameDirectories() {
void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs) {
m_global_settings = true;
auto* menu = new QMenu(m_ui->settings_button);
menu->addAction(tr("Import..."), this, &ConfigurationEditDialog::ImportGameSettings);
menu->addAction(tr("Export..."), this, &ConfigurationEditDialog::ExportGameSettings);
m_ui->settings_button->setMenu(menu);
m_ui->settings_button->setVisible(true);
m_game_dirs_list->clear();
m_game_dirs_group->setMinimumWidth(GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH);
+139 -6
View File
@@ -24,9 +24,11 @@
#include <QDialog>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QHeaderView>
#include <QIcon>
#include <QInputDialog>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
@@ -40,6 +42,7 @@
#include <QProcess>
#include <QPushButton>
#include <QRegularExpression>
#include <QSaveFile>
#include <QSet>
#include <QSettings>
#include <QSignalBlocker>
@@ -765,7 +768,7 @@ void ConfigurationListWidget::edit_configuration() {
auto info = CreateConfiguration(*item);
ConfigurationEditDialog dlg(*info, this);
dlg.setWindowTitle(tr("Edit game settings"));
dlg.setWindowTitle(tr("Edit game config"));
connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this,
&ConfigurationListWidget::PreviewControllerColor);
@@ -789,8 +792,8 @@ void ConfigurationListWidget::delete_configuartion() {
return;
}
if (QMessageBox::Yes == QMessageBox::question(this, tr("Clear custom settings"),
tr("Do you want to clear custom settings?"))) {
if (QMessageBox::Yes == QMessageBox::question(this, tr("Clear game config"),
tr("Clear this game's config and use global settings?"))) {
delete m_custom_infos.take(item->GetInfo().game_path);
item->GetInfo().custom_settings = false;
WriteSettings();
@@ -810,6 +813,10 @@ void ConfigurationListWidget::edit_global_settings() {
dlg.SetGlobalSettings(m_game_dirs);
connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this,
&ConfigurationListWidget::PreviewControllerColor);
connect(&dlg, &ConfigurationEditDialog::ImportGameSettings, this,
[this, &dlg]() { ImportGameSettings(&dlg); });
connect(&dlg, &ConfigurationEditDialog::ExportGameSettings, this,
[this, &dlg]() { ExportGameSettings(&dlg); });
if (dlg.exec() == QDialog::Accepted) {
m_global_info.CopyEmulatorSettingsFrom(info);
@@ -822,7 +829,133 @@ void ConfigurationListWidget::edit_global_settings() {
ScanGameDirectory();
}
}
emit Select();
SelectItem(m_ui->cfgs_list->currentItem());
}
static bool IsGameSettingsTitleId(const QString& title_id) {
static const QRegularExpression pattern(QStringLiteral("\\APPSA[0-9]{5}\\z"));
return pattern.match(title_id).hasMatch();
}
void ConfigurationListWidget::ImportGameSettings(QWidget* parent) {
const auto path = QFileDialog::getOpenFileName(parent, tr("Import game configs"), {},
tr("JSON files (*.json)"));
if (path.isEmpty()) {
return;
}
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::warning(parent, tr("Import failed"), file.errorString());
return;
}
const auto document = QJsonDocument::fromJson(file.readAll());
if (!document.isObject() || document.object().isEmpty()) {
QMessageBox::warning(
parent, tr("Import failed"),
tr("Expected a JSON object containing game configs keyed by PPSA code."));
return;
}
const auto settings = document.object();
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
Configuration info;
QString error;
if (!IsGameSettingsTitleId(it.key()) || !it.value().isObject() ||
it.value().toObject().isEmpty() ||
!info.SetGameSettings(it.value().toObject(), error)) {
QMessageBox::warning(parent, tr("Import failed"),
tr("Invalid game config for %1. %2").arg(it.key(), error));
return;
}
}
struct ImportedGame {
ConfigurationItem* item;
std::unique_ptr<Configuration> info;
};
std::vector<ImportedGame> imported;
auto unmatched = settings.keys();
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); ++index) {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
const auto title_id = item->GetInfo().title_id.trimmed().toUpper();
if (!settings.contains(title_id)) {
continue;
}
auto info = CreateConfiguration(*item);
QString error;
if (!info->SetGameSettings(settings.value(title_id).toObject(), error)) {
QMessageBox::warning(parent, tr("Import failed"),
tr("Invalid game config for %1. %2").arg(title_id, error));
return;
}
info->custom_settings = true;
imported.push_back({item, std::move(info)});
unmatched.removeAll(title_id);
}
auto message = imported.empty() ? tr("No matching games found in the library.")
: tr("Configs will be updated for the following games:\n");
for (const auto& game: imported) {
message += tr("\n%1 — %2").arg(game.info->title_id.trimmed().toUpper(), game.info->name);
}
if (!unmatched.isEmpty()) {
message += tr("\nGames not found: %1").arg(unmatched.join(QStringLiteral(", ")));
}
if (imported.empty()) {
QMessageBox::information(parent, tr("Import game configs"), message);
return;
}
QMessageBox confirmation(QMessageBox::Question, tr("Import game configs"), message,
QMessageBox::Apply | QMessageBox::Cancel, parent);
confirmation.setTextFormat(Qt::PlainText);
confirmation.setDefaultButton(QMessageBox::Cancel);
if (confirmation.exec() != QMessageBox::Apply) {
return;
}
for (auto& [item, info]: imported) {
const auto game_path = info->game_path;
delete m_custom_infos.take(game_path);
m_custom_infos.insert(game_path, info.release());
item->GetInfo().custom_settings = true;
item->Update();
}
WriteSettings();
}
void ConfigurationListWidget::ExportGameSettings(QWidget* parent) const {
QMap<QString, const ConfigurationItem*> games;
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); ++index) {
const auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
const auto& game = item->GetInfo();
const auto title_id = game.title_id.trimmed().toUpper();
if (m_custom_infos.contains(game.game_path) && IsGameSettingsTitleId(title_id)) {
games.insert(tr("%1 — %2 (%3)").arg(title_id, game.name, game.game_path), item);
}
}
if (games.isEmpty()) {
QMessageBox::information(parent, tr("Export game config"),
tr("No game configs with a PPSA code were found."));
return;
}
bool accepted = false;
const auto selected = QInputDialog::getItem(parent, tr("Export game config"), tr("Game:"),
games.keys(), 0, false, &accepted);
if (!accepted) {
return;
}
const auto& game = games.value(selected)->GetInfo();
const auto title_id = game.title_id.trimmed().toUpper();
const auto path =
QFileDialog::getSaveFileName(parent, tr("Export game config"),
title_id + QStringLiteral(".json"), tr("JSON files (*.json)"));
if (path.isEmpty()) {
return;
}
const auto settings =
QJsonObject::fromVariantMap(m_custom_infos.value(game.game_path)->GameSettings());
const auto data =
QJsonDocument(QJsonObject {{title_id, settings}}).toJson(QJsonDocument::Indented);
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) {
QMessageBox::warning(parent, tr("Export failed"), file.errorString());
}
}
void ConfigurationListWidget::edit_input_mapping() {
@@ -1040,11 +1173,11 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
tr("Remove save data..."), this, SLOT(remove_save_data()));
menu.addSeparator();
QAction* action_edit =
menu.addAction(style()->standardIcon(QStyle::SP_FileIcon), tr("Edit game settings..."),
menu.addAction(style()->standardIcon(QStyle::SP_FileIcon), tr("Edit game config..."),
this, SLOT(edit_configuration()));
QAction* action_delete =
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
tr("Clear custom settings"), this, SLOT(delete_configuartion()));
tr("Clear game config"), this, SLOT(delete_configuartion()));
if (item == nullptr) {
menu.addSeparator();